Example Socket API

This section contains an example socket API similar to the API provided by Go.

The main idea is that your host binary should be able to use your socket API in a similar to how you would use a real socket API, like the one provided by your language. Similar to your IP stack API, you do not need to follow this exact specification, so long as you provide some kind of API that implements the features described here.

In general, this API is designed to represent the most basic socket operations. You are NOT required to implement other extra features that your language’s socket API may support, such as SetReadDeadline or similar.

What to expect: There’s a lot of information here, but you don’t need it all at once! Many API functions have several implementation notes about how to handle certain kinds of edge cases or errors. We’ve done our best to describe how these can be handled in a minimal way for this project, or to specify what sort of errors you can ignore.

On your first reading of this specification, just skim over the major components to get a sense of what the API functions are and what they do. Then, you can return here for the details as you need them.

How this example works

In Go, sockets are represented as structs, which have helper methods to perform most socket operations. In our example, we have two types of socket structs:

  • VTCPListener represents a listener socket (similar to Go’s net.TCPListener)
  • VTCPConn represents a “normal” socket for a TCP connection between two endpoints (similar to Go’s net.TCPConn)

Note: For clarity, we prefix each function’s name with V to ensure it’s clear when we’re talking about the virtual socket API in this document–you do not need to adhere to this requirement, we only use it to ensure it’s clear when we refer to our example API vs. any other language’s API.

Don’t want to use this version? We’ve described a similar, C-style API that returns socket numbers similar to file descriptors. You are welcome to use either version as a model for your API (or neither, so long as you have the same features).

Error handling

An important part of writing any API is communicating about errors. In Go, for example, your functions should return an error value that is nil on success, or contains an error with info about the failure.

Click for an example
func SqrtPositiveOnly(num int) (int, error) {
    if num < 0 { // Error 
        return nil, errors.New("Value cannot be negative")
    } else { // Success
        return math.Sqrt(num), nil
    }

Our example API follows this convention–we suggest doing something similar, as it will make it easier to implement our REPL commands that use this API.

Syntax notes

Our example API defines methods of the two socket structs VTCPConn and VTCPListener. Like most Go syntax, the syntax for defining methods on structs is a bit weird. Here’s an example of how it works:

Click to expand

Go defines method signatures like this:

func [receiver] FuncName([arg 0, arg 1, ...]) [return arg 0,
return arg 1, ...]

Where receiver is the name of the struct that provides this method.
For example:

type Thing struct {
	A int
}

func (t *Thing) AddSomething(x int) {
    t.A += x
}

func main() {
    // Make a thing and call a method on it
    tt := Thing{A: 41}
    tt.AddSomething(1)
}

Listen socket API

This section contains example API functions for listen sockets.

VListen

func VListen(port uint16) (*VTCPListener, error)

VListen creates a new listening socket bound to the specified port. After binding, this socket moves into the LISTEN state—this is known as “passive open” in the RFC.

VListen returns a TCPListener on success. On failure, it should return an error describing the failure.

Example usage

listenConn, err := tcpstack.VListen(9999) // Listen on port 9999
if err != nil {
 // . . .
}

VAccept

Example:

func (*VTCPListener) VAccept() (*VTCPConn, error)

VAccept waits for new TCP connections on the given listening socket. If no new clients have connected, this function MUST block until a new connection occurs.

When a new client connects, VAccept returns a new normal-type socket to represent the new connection. In our example, it returns a new VTCPConn struct for this socket.

On failure, this method should return nil for the socket and a non-nil error value describing the failure.

Example usage

listenConn, err := tcpstack.VListen(9999) // Listen on port 9999
if err != nil {
 // . . .
}

clientConn, err := listenConn.VAccept()
if err != nil {
 // . . .
}

// clientConn refers to new normal socket for client that connected

VClose (for listen sockets)

func (*VTCPListener) VClose() error

Closes this listening socket, removing it from the socket table. No new connection may be made on this socket. Any pending requests to create new connections should be deleted.

This method should return an error if the close process fails (eg, if the socket is already closed.)

Example usage

listenConn, err := tcpstack.VListen(9999) // Listen on port 9999
// . . .

listenConn.VClose() // Close this socket

Normal socket API

This section contains example API functions for normal sockets.

VConnect

func VConnect(addr netip.Addr, port int16) (VTCPConn, error)

This function creates a new socket that connects to the specified virtual IP address and port–this corresponds to an “active OPEN” in the RFC. VConnect MUST block until the connection is established, or an error occurs.

Notes

  • How to choose a source port when connecting? We recommend picking a random, unused, high-numbered TCP port (eg. 20000 or higher), like a real TCP stack. Starting with a random number will also make sure that Wireshark can analyze your TCP connections properly.
  • If the destination fails to respond to your initial connection attempt, you should retransmit your SYN until a maximum number of retries is reached. A good metric is after 3 re-transmitted SYNs fail to be ACKed. To implement this, you may use a constant multi-second timeout, e.g. 3 seconds. Alternatively, you can start off at 2 seconds, and double the time with each SYN you retransmit.

VRead

func (*VTCPConn) VRead(buf []byte) (int, error)

This method reads data from the TCP socket (equivalent to RECEIVE in RFC). In this version, data is read into a slice (buf) passed as argument.

VRead MUST block when there is no available data to read. All reads should return at least one byte, unless a failure or EOF occurs.

VRead MUST return number of bytes read into the buffer. The returned error is nil on success, io.EOF if other side of connection has finished, or another error describing other failure cases.

Example usage

conn, err := tcpstack.VConnect(...)

buf := make([]byte, 1024)
b, err := conn.VRead(buf)
fmt.Println("%d bytes read", b)

VWrite

func (*VTCPConn) VWrite(data []byte) (int, error)

This method writes data to the TCP socket (equivalent to SEND in the RFC). In this version, data to write is passed as a byte slice (data).

This method MUST block until all bytes are in the send buffer. If the send buffer becomes full, VWrite should block until space is available.

On success, returns the number of bytes written to the connection. On failure, should return a non-nil error if the connection is closed or on other failures.

VClose

func (*VTCPConn) VClose() error

Initiates the connection termination process for this socket (equivalent to CLOSE in the RFC). This method should be used to indicate that the user is done sending/receiving on this socket.

Calling VClose should send a FIN, and all subsequent calls to VRead and VWrite on this socket should return an error. Any data not yet ACKed should still be retransmitted as normal.

VClose only initiates the close process–it MUST NOT block. Consequently, VClose does not delete sockets, the socket MUST NOT be deleted from the socket table until it has reached the CLOSED state (ie, after the other side closes the connection, or an error occurs).

C-style socket API

This section describes a virtual socket API similar to C’s socket API–for complete details, see the relevant sections for each function as above.

In this version, you would create new connections as virtual sockets using your own socket table and virtual file descriptors to allow connecting and listening, reading and writing into buffers, etc.

Like C socket functions, these functions generally return integers that represent either 1) file descriptors for sockets, 2) numbers of bytes read/written, or error codes. All error codes should be negative values to distinguish them from cases 1–2–in C, you should use standard errno error codes (such as EBADF) for this.

      /* creates a new socket, binds the socket to a port
      After binding, moves socket into LISTEN state (passive OPEN in the RFC)
      returns socket number on success or negative number on failure
      Some possible failures: ENOMEM, EADDRINUSE, EADDRNOTAVAIL
      (Note that a listening socket is used for "accepting new
      connections") */
      int v_listen(uint16_t port);

      /* creates a new socket and connects to an address (active OPEN in the RFC)
      returns the socket number on success or a negative number on failure
      You may choose to implement a blocking connect or non-blocking connect
      Some possible failures: EAGAIN, ECONNREFUSED, ENETUNREACH, ETIMEDOUT */
      int v_connect(struct in_addr *addr, uint16_t port);

      /* accept a requested connection from the listening socket's connection queue
      returns new socket handle on success or error on failure.
      if node is not null, it should fill node with the new
	  connection's address accept is REQUIRED to block when there is no awaiting connection
	  Some possible failures: EBADF, EINVAL, ENOMEM */
      int v_accept(int socket, struct in_addr *node);

    /* read on an open socket (RECEIVE in the RFC)
       return num bytes read or negative number on failure or 0 on eof and shutdown_read
       nbyte = 0 should return 0 as well
       read is REQUIRED to block when there is no available data
       All reads should return at least one data byte unless failure or eof occurs 
       Some possible failures : EBADF, EINVAL */
    int v_read(int socket, void *buf, size_t nbyte);

    /* write on an open socket (SEND in the RFC)
       return num bytes written or negative number on failure
       nbyte = 0 should return 0 as well
       write is REQUIRED to block until all bytes are in the send buffer 
       Some possible failures : EBADF, EINVAL, EPIPE */
    int v_write(int socket, const void *buf, size_t nbyte);

    /* Invalidate this socket, making the underlying connection inaccessible to
       ANY of these API functions. If the writing part of the socket has not been
       shutdown yet, then do so. The connection shouldn't be terminated, though;
       any data not yet ACKed should still be retransmitted. 
       Some possible failures : EBADF */
    int v_close(int socket);