pete > courses > CS 431 Spring 25 > Lecture 06: Network Layer I – IP intuition
Lecture 06: Network Layer I – IP intuition
Goals
- use struct definitions and struct pointers to access… structured data
- use Wireshark to observe frames in action
- define the purpose of IP addresses
- define the purpose of a router and a routing table
first, a couple technical things that will come in handy for the implementation work you’ll be doing
starting with: structs!
we’re going to look at a program line by line
starting with this definition:
struct widget { uint32_t the_answer; uint16_t a; uint16_t b; };
what does it do?
my guess is that the primary purpose behind your use of structs in the past has been to gather together a bunch of different values that all pertain to a single thing
the implication of the above definition is that there are three pieces of information relevant to a particular widget: a 4-byte unsigned integer and two 2-byte unsigned integers
structs are very useful to gather this stuff together in one thing that can be passed around to functions and such
but there is another important aspect to it that you may not have interacted with directly: a struct definition also establishes a memory layout
the above definition comprises 8 bytes (ie, the sum of the sizes of the constituent fields), so a value of type struct widget needs 8 bytes of memory
indeed, if we declare a local variable of type struct widget, which the compiler will make room for in the current stack frame, this is how the compiler know how much memory to allocate
furthermore, the definition says that the field the_answer is 4 bytes long and comes first, followed by a, followed by b
this means that if I’ve got a value of type struct widget in memory, its the_answer field starts at the beginning of that chunk of memory; its a field starts 4 bytes later; and its b field starts 2 bytes after that
hold onto that thought for a moment
consider this line:
uint8_t *mem;
what does it do?
it declares a pointer variable—that is, a variable whose value is a memory address
and if we go to that memory address, we’re supposed to interpret the bits we find as unsigned 8-bit integers
now this line:
mem = malloc(8);
asks for 8 bytes of memory in the heap and stores the address of those bytes in the variable mem
and this line:
memcpy(mem, "\x2a\x00\x00\x00\x88\x0e\x01\x00", 8);
copies 8 bytes of data into the memory pointed to by mem
(the "\x2a" notation is a way to specify an arbitrary byte within an otherwise-ASCII string: the two digits following the "\x" are the pair of hex digits that represent the desired byte)
so now we’ve filled this chunk of memory with 8 bytes of data
now the tricky bit:
struct widget *foo = (struct widget *) mem;
we’re assigning the value of mem to foo
mem points to the 8 bytes of memory we just allocated and filled with raw bytes
mem is declared to have type uint8_t *, which means that it’s a pointer to unsigned 8-bit integers
but we’re assigning that value (ie, the memory address) to foo, which has type struct widget *, which means that it’s a pointer to a struct widget
this suggests that the value of foo tells us where in memory to find a value of type struct widget
but we never put a struct widget there, we just put arbitrary bytes
how can this possibly work?
recall that the struct definition also specifies the memory layout, and that the first four bytes of this struct comprise the the_answer field
so if we use the foo->the_answer notation, this will access the first four bytes of memory pointed to by foo
which happens to also be the first four bytes pointed to by mem
which we set to be 0x2a, 0x00,0x00, and0x00` in the memcpy above
like so:
printf("foo->the_answer is %d\n", foo->the_answer); printf("foo->a is %d\n", foo->a); printf("foo->b is %d\n", foo->b);
full program here: struct-overlay.c & Makefile
compile and run:
$ gmake cc -g -Wall -pedantic -c -o struct-overlay.o struct-overlay.c cc -o struct-overlay struct-overlay.o $ ./struct-overlay foo->the_answer is 42 foo->a is 3720 foo->b is 1
(it prints 42 because my machine is big-endian)
how is this useful in networking?
well, you’re going to be writing code that receives chunks of bytes (eg, using read(2)) and those bytes will not have a type associated with them: they’ll be like a uint8_t *
but stuff like the Ethernet header is structured data
and it’s far easier to manipulate (both read and write) fields within the Ethernet header using structs than direct bit operations
second technical topic: the network setup we’ll be working with
in the upcoming assignment, you’ll write code that does Ethernet-y stuff
and in the following assignment, you’ll write code that does IP-ish stuff
it’s worth talking about how all of this is going to fit together
you’re going to write a program that behaves like a host: it will send frames and receive frames
we’re going to use software called "Virtual Distributed Ethernet" to create a virtual switch
and we’re going to run your program and plug the resulting process into it
we can plug many different processes into the same switch, acting like a bunch of machines on the same network
an important development tool is Wireshark, which will allow us to observe frames that are sent and received
we will first run a program that creates an imaginary interface (the name of the interface will always begin with the word tap) and connects it to the virtual switch
so any packets the switch sees, it will send to the interface
then, we will run Wireshark and pass it any packets that the interface sees
the real power of Wireshark is that it will deconstruct each frame and tell us how the various bits are being interpreted
(Wireshark is THE tool for this job. everybody in networking knows it and uses it. the old-school, command-line-only equivalent, is tcpdump, whose existence is worth knowing about because it was previously just as pervasive as Wireshark is now.)
in the upcoming assignment, I will give explicit directions and code to do all of the above; I just want to cover the concepts here
when Wireshark is running, it has three main panes
(the default layout has all three panes stacked vertically; to change it, go to Edit -> Preferences -> Appearance -> Layout; I like the second choice)
the top pane is the list of frames, one per line
if you click a frame in that list, it will show details about the frame in the other two frames
one pane (bottom in the default layout, bottom-right in my layout) shows the raw bytes of the frame, in the same format as you produce with binary_to_hex from assignment 1
the other pane (middle in the default layout, bottom-left in mine) shows the deconstructed frame
you can click on the little triangles to expand the information about each header
and within you can click on a field and it will highlight the bytes in the frame that correspond to that information
Wireshark will be very useful when you’re writing code that produces frames and it’s not clear why the frames are not being interpreted the way you want
back to where we left off last time
we defined network, which is the set of hosts that can be reached by a broadcast frame
we defined bridge, which is a computer that has multiple interfaces, on different networks and combines them into a single one, by passing frames back and forth between them
we observed that this doesn’t scale: as the number of hosts grows, eventually the task of passing frames back and forth (not to mention sending broadcast frames to all destinations) gets intractible
we are now going to develop a solution
let us imagine an internetwork that consists of four networks: One, Two, Three, and Four
Host A resides on Network One
Host B resides on Network Two
Host C resides on Network Three
Host D resides on Network Four
Host R1 resides on both Network One and Network Two
Host R2 resides on both Network One and Network Three
Host R3 resides on both Network Two and Network Four
because these are separate networks, broadcast frames will not be passed through hosts R1, R2, and R3
we will refer to machines that connect together networks as routers
so R1, R2, and R3 are routers
so how can Host A get a frame to Host B?
it has to go through Host R1
likewise, for host A to get a frame to Host D, it has to first go through R1 and then through R3
but how can A know that the frame destined for D has to go through R1?
how can it know that R2 won’t get the job done?
one possible solution is that A sends the frame down all possible paths that might lead to D
this is potentially extraordinarily wasteful: that frame might get sent to a ton of different networks that serve no purpose in its path to D
another possible solution is that A knows the path to every other machine (MAC address) in the internetwork
as the number of machines grows, this quickly becomes intractible: it’s too much data and it’s too difficult to keep it up to date, especially when machines might move among networks (in the distant past, not a huge problem; but these days it happens all the time)
a slight modification of the second possible solution is that routers have knowledge of what MAC addresses can be reached through them
but this just kicks the problem one step over: it still doesn’t scale
it could scale, however, if there was an efficient way of describing which addresses are accessible through a particular router
for example, instead of enumerating 16,384 different MAC addresses, we could much more efficiently say "all MAC addresses beginning with 11:22:33:44"
this won’t work because MAC addresses are assigned when the NIC is manufactured and there is no telling where (geographically) a particular NIC will end up
so we can’t depend on MAC addresses being organized in a way that will permit such an efficient way to describing their location
nor can we coerce it into happening ourselves
the conclusion is that MAC addresses are not suitable for the purpose of identifying a machine on a different (but, ultimately, connected) network
we need another kind of address, one that does reflect the network structure, and thus will allow us to say things like "all machines with addresses in this range are over there"
enter: the IP address
the identifying information used in the Internet Protocol
ie, the protocol that enables sending and receiving packets across an internetwork
(in the world of IP, we will use the word "packet" to refer to the unit of data transmission, just as we used the word "frame" to refer to the same idea at the link layer)
the idea behind IP addresses is that a given network will all have IP addresses in the same range
an IP address is a 32-bit (4-byte) value and is usually written as a "dotted quad", meaning four 8-bit (ie, in the range 0-255, inclusive) numeric values separated by dots
for example: 140.233.20.7 and 192.168.1.19 are both IP addresses
in the example above, all the machines on Network 1 could have IP addresses that begin with 140.233.20
furthermore, the implication is that every machine with an IP address in that range will reside in that single network: it cannot live anywhere else in the internetwork
likewise, machines on Network 2 could have IP addresses that begin with 187.54
machines on Network 3 could have IP addresses that begin with 97.146.83
and machines on Network 4 could have IP addreses that begin with 1.2.3
with this arrangement, then, host A needs to know four things:
- machines with addresses that begin with 140.233.20 are accessible on the local network
- machines with addresses that begin with 187.54 are accessible through R1
- machines with addresses that begin with 97.146.83 are accessible through R2
- machines with addresses that begin with 1.2.3 are accessible through R1
important things to note:
- no machine in this setup needs to know about every individual address
- host A only needs to know about the first router in the path to the destination: when sending to D, it doesn’t need to know that R3 even exists, only that sending to R1 will get the packet on the correct path
the IP address 140.233.20.6 is an example of an IP address in Network 1
we can divide this address into two parts:
the "140.233.20" part identifies the network
and the ".6" part identifies the host within the network
likewise, the IP address 187.54.19.201 is an example of an IP address in Network 2
the 187.54 part identifies the network
and the ".19.201" part identifies the host within the network
so for all machines in the 140.233.20 network, it is those first 24 bits that identifies the network and the last 8 bits that identifies the host
likewise, for all machines in the 187.54 network, the first 16 bits identifies the network and the last 16 bits identifies the host
taking this one step further, we can describe these networks using two pieces of information: the leading bits that are common to all IP addresses in the network and the number of bits
so the two networks from the previous examples could also be described as 140.233.20/24 and 187.54/16, where the number after the slash refers to the number of bits used to idenfify the network in those IP addresses
to bring this back to the four things that host A needs to know, each of them can be described by three values:
- the collection of bits common to all addresses in the network
- the length of that bit sequence
- the IP address of the router that leads to that network
so "machines with addresses that begin with 187.54 are accessible through R1" can be remembered as "187.54/16 -> R1’s IP address on Network 1"
and "machines with addresses that begin with 97.146.83 are accessible through R2" can be remembered as "97.146.83/16 -> R2’s IP address on Network 2"
all of this information will be remembered in a data structure within Host A called a routing table
this is the big high-level concept underlying IP; we’ll look at concrete specifics next time