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);