aboutsummaryrefslogtreecommitdiff

Introduction to JailPT2's IP Project!

This project is an implementation of IP pipelines in Go. The project is split into two parts, the first being the IPStack, which is a library that implements the IP pipeline, and the second being the vhost and vrouter, which are the two nodes that are used to test the IPStack.

Because the vhost and vrouter are so similar, we maintain most of the logic within the IPStack in which the vrouter and vhost call when necessary.

Abstractions

IP Layer and Interface abstraction

Using these two structs and two data structures below, we abstract for the interfaces into the IP layer:

// structs
type Interface struct {
    Name     string
    IpPrefix netip.Prefix
    UdpAddr  netip.AddrPort

    Socket    net.UDPConn
    SocketChannel chan bool
    State         bool
}

type Neighbor struct {
    Name    string
    VipAddr netip.Addr
    UdpAddr netip.AddrPort
}

// data structures
var myInterfaces []*Interface
var myNeighbors = make(map[string][]*Neighbor)

The interface struct holds the UDP socket used for sending and receiving data over a particular interface, abstracting its low-level Link Layer into a Socket. The IP stack then populates the myInterfaces slice with every interface defined for the node to store and access its interfaces.

The Neighbor struct represents "endpoints" of the Link Layer from a particular interface, where, in our case, the "endpoint" is the UDPAddr of the neighbor. The myNeighbors maps each interface to a slice of neighbors, abstracting one end of an interface's Link Layer to all possible endpoints (Neighbors) on the other side of the switch.

In this way, if the IPStack needs to send a packet to a particular neighbor, it must iterate over every interface and check if the neighbor is in the slice of neighbors for that interface. But, more often than not, we know the interface to receive or send on, so finding the neighbor to communicate with doesn't require an iteration over all interfaces.

vrouter/vhost and ipstack abstraction

The ipstack offers an API to nodes to abstract the large amounts of similar code that different types of nodes share. The API is as follows:

// functions
func Initialize(lnxFilePath string) error
func RegisterProtocolHandler(protocolNumber uint8) bool
func SendIP(src *netip.Addr, dest *Neighbor, protocolNum int, 
        message []byte, destIP string, hdr *ipv4header.IPv4Header) (int, error)
func InterfaceUp(iface *Interface)
func InterfaceDown(iface *Interface)
func Route(src netip.Addr) Hop
func SprintRoutingTable() string
func SprintNeighbors() string
func SprintInterfaces() string
// ... other getters not shown

// constants
RIP_PROTOCOL
TEST_PROTOCOL

In general, to correctly use the API, the node must: 1) First, call initialize to setup the IPStack data structures. 2) Then, call RegisterProtocolHandler(X_PROTOCOL) to subscribe to a protocol. 3) After that, depending on the node's needs, use other API functions to interact with the IPStack.

The only protocols that are currently supported are the TEST_PROTOCOL and RIP_PROTOCOL, as given by the constants. To add more, like TCP_PROTOCOL, the current best way is to implement them in the IPStack.

In our case, vhost will subscribe to ONLY the TEST_PROTOCOL, while vrouter will subscribe to BOTH the TEST_PROTOCOL and RIP_PROTOCOL.

The other API functions allow for more specific actions within vrouter and vhost, such as Route, SprintNeighbors, SendIP, (and all getters). The REPL inside a node is expected to use these functions to implement its REPL commands.

For vhost and vrouter, we use the API's functions to implement the following REPL:

li: List interfaces
lr: List routes
ln: List available neighbors
up: Enable an interface
down: Disable an interface
send: Send test packet
q/exit: Quit program

Note: we originally planned to also incorporate the REPL into either the IPStack or separate API, but due to time constraints, we have the REPL in the VRouter and VHost. For TCP, we will consider abstracting the REPL in this way.

Thread Design

Main routines are clearly distinguished by the function name, ending in Routine (e.g. InterfaceListenerRoutine, ManageTimeoutRoutine, etc.). These main routines are defined as having "forever" loops, repeatedly executing the same task in succession.

There are cases where we can spawn wait-free threads doing parallelize actions for every element in a loop, but it's not worth to have this our implementation. We simply use threads to avoid blocking conditions or update something consecutively.

The main routines are discussed below.

Listener Routine

Each interface needs a listener routine to ensure that the node acts when it receives a packet on any interface. Hence, this routine should be setup for each interface. It hangs on an interface's UDP socket, then calls RecvIP upon receiving a packet. RecvIP beings the pipeline of corresponding function calls, based on the packet.

There is some added complexity with the listener thread interacting with the state of the interface (up, down) through a channel, managed by a subroutine inside of this listener routine. For more detail on this, see the implementation of InterfaceListenerRoutine() in ipstack.go.

RIP Routines

Following StartRipRoutines(), we use two main routines to maintain the RIP protocol: PeriodicUpdateRoutine() and ManageTimeoutRoutine().

Periodic Update Routine

This routine is responsible for sending periodic updates to all neighbors every 5 seconds.

Manage Timeout Routine

This routine is responsible for managing the timeout table. Every second it increments the timeout of every entry in the table by 1. Once the entry reaches MAX_TIMEOUT == 12 (if it wasn't reset to 0 by an update), this thread removes the entry from the routing table (and timeout table) then sends a triggered update to all neighbors. A mutex is used on the timeout table to ensure safe interaction from RIP updates resetting timeouts (in a separate thread).

Processing IP packets

Processing Incoming Packets

Upon receiving a packet at the Link Layer, a node should call RecvIP to process the IP packet.

RecvIP then does the following: 1) Check if the interface is up/down. If down, drop the packet (i.e. return). 2) Parse the header. Check if the packet is valid by validating checksum and seeing TTL > 0. If not, drop the packet. 3) Check if the packet is for me (any of my interface). If so, SENDUP to handler (if protocol is register) with the parsed message. End function. 4) Check the routing table for the next hop. If not found, drop the packet. If found, determine if the hop is a neighbor or not, then: 1) If the hop is a neighbor, send directly to that neighbor. 2) If the hop is not neighbor, forward the packet to the appropriate neighbor (i.e. the neighbor matches the packet's destination header). 8) If failed to find a route in the table, print an error & drop the packet (note: vhost will always succeed due it's static route).

This allows for the ip stack to completely handle the processing of packets, given that even the listener thread that calls RecvIP is managed by the IPStack.

Processing Outgoing Packets

To sending a packet at the Link Layer, a node should call SendIP to process the IP packet, which consumes the source address, neighbor to send to, protocol number, and message content. For forwarding, you can resuse the same header in SendIP's final argument.

SendIP then does the following: 1) Check if the interface is up/down. If down, don't send the packet (i.e. return). 2) Build the header, then update its checksum. 3) Build the message buffer. 4) Send the packet to the desired neighbor's socket.

Miscellaneous

Notable Design Decisions

Our design is mostly straighforward. However, we did improve our design from the basic design in two following ways.

More RIP requests and triggered updates

When an interface is set to down or up from the REPL, the router instantly knows new information. Hence, it makes sense to act on this new information to propagate information quicker. - If an interface goes down, then send a triggered update to all neighbors of the new INFINITY cost to the neighbors affected by that LL loss. - If an interface goes up, then send a RIP request to all neighbors to instantly get the new, expanded routes, versus waiting 5 seconds for the next periodic update.

See InterfaceUp() & InterfaceDown() for specific info.

Specific Garbage Collection for Expired Entries

If a route is expired, as told by receiving a cost of INFINITY for a route, we let that route exist for 12 seconds with value INFINITY before deleting it. The textbook states this is the preferred method.

This is different from the reference, which would delete these routes instantly.

Known Bugs

No known bugs :)

However, the reference often crashed on startup when spawning the loop network with tmux.