uis

package module
v0.0.0-...-a502c75 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 27, 2026 License: GPL-3.0 Imports: 25 Imported by: 0

README

Userspace Internet Simulation

GoDoc Build Status codecov

Userspace Internet Simulation (uis) is a Go package that provides you with the ability to create userspace TCP/IP client and server stacks that communicate exchanging raw IP packets.

As such, it is a fundamental building block for writing integration tests where the test cases interfere with the exchanged packets.

Basic usage is like:

import (
	"context"
	"net/netip"
	"os"
	"sync"

	"github.com/bassosimone/runtimex"
	"github.com/bassosimone/uis"
)

// Create the virtual internet and two TCP/IP stacks.
internet := uis.NewInternet()

serverAddr := netip.MustParseAddr("10.0.0.1")
serverEndpoint := netip.AddrPortFrom(serverAddr, 80)
serverStack := runtimex.PanicOnError1(internet.NewStack(uis.MTUEthernet, serverAddr))
defer serverStack.Close()

clientAddr := netip.MustParseAddr("10.0.0.2")
clientStack := runtimex.PanicOnError1(internet.NewStack(uis.MTUEthernet, clientAddr))
defer clientStack.Close()

// Start the server goroutine.
ctx := context.Background()
wg := &sync.WaitGroup{}
ready := make(chan struct{})
wg.Go(func() {
	listenCfg := uis.NewListenConfig(serverStack)
	listener := runtimex.PanicOnError1(listenCfg.Listen(ctx, "tcp", serverEndpoint.String()))
	close(ready)
	conn := runtimex.PanicOnError1(listener.Accept())
	// TODO: do something with the conn
})

// Start the client goroutine.
wg.Go(func() {
	<-ready
	connector := uis.NewConnector(clientStack)
	conn := runtimex.PanicOnError1(connector.DialContext(ctx, "tcp", serverEndpoint.String()))
	// TODO: do something with the conn
})

// Wait for both goroutines to finish.
stopped := make(chan struct{})
go func() {
	wg.Wait()
	close(stopped)
}()

// Route and capture packets between stacks until both sides finish.
traceFile := runtimex.PanicOnError1(os.Create("capture.pcap"))
trace := uis.NewPCAPTrace(traceFile, uis.MTUEthernet)
loop:
for {
	select {
	case frame := <-internet.InFlight():
		trace.Dump(frame.Packet)
		_ = internet.Deliver(frame)
	case <-stopped:
		break loop
	}
}
runtimex.PanicOnError0(trace.Close())

The example_test.go file shows a complete example.

Stdlib Compatibility

  • Connector: a stdlib-like dialer for IP literal endpoints only.
  • ListenConfig: a stdlib-like listener config for IP literal endpoints only.

Because we implement these two fundamental stdlib-like interfaces, uis is suitable to be used instead of stdlib-based code in tests. Common networking code could depend on DialContext, Listen, and ListenPacket like functions. For example:

// This is how you could define a Dialer that uses either the [*net.Dialer]
// or [*uis.Connector] to establish TCP/UDP connections.

type Connector interface {
	DialContext(ctx context.Context, network, address string) (net.Conn, error)
}

type Resolver interface {
	LookupHost(ctx context.Context, name string) ([]string, error)
}

type Dialer struct {
	Connector Connector
	Resolver  Resolver
}

func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	host, port, err := net.SplitHostPort(address)
	if err != nil {
		return nil, err
	}
	addrs, err := d.Resolver.LookupHost(ctx, host)
	if err != nil {
		return nil, err
	}
	for _, addr := range addrs {
		conn, err := d.Connector.DialContext(ctx, network, net.JoinHostPort(addr, port))
		if err != nil {
			continue
		}
		return conn, nil
	}
	return nil, errors.New("dial failed")
}

// This is how you would use the above code in production
dialerProd := &Dialer{Connector: &net.Dialer{}, Resolver: &net.Resolver{}}

// This is instead how you would use the above code for tests
// assuming you also implemented a Resolver based on `uis`.
dialerTests := &Dialer{
	Connector: uis.NewConnector(stack),
	Resolver: NewUISResolver(stack),
}

Installation

To add this package as a dependency to your module:

go get github.com/bassosimone/uis

Development

To run the tests:

go test -v .

To measure test coverage:

go test -v -cover .

Benchmark

The cmd/benchmark/main.go file implements a benchmark measuring the download speed:

go build -v ./cmd/benchmark
./benchmark

Use ./benchmark -help to get help.

License

SPDX-License-Identifier: GPL-3.0-or-later

History

Adapted from ooni/netem.

Documentation

Overview

Package uis (Userspace Internet Simulation) provides basic building blocks for userspace networking tests using gVisor.

The package models a virtual internet where multiple network stacks can communicate. It provides direct control over packet flow, leaving routing policy and network conditions to the caller.

The typical usage is to create a *Internet and use *Internet.NewStack to create two or more *Stack instances. The created instances are already configured for sending and receiving raw internet packets.

The Connector type is a stdlib-like dialer for IP literal endpoints only. The ListenConfig type is a stdlib-like listener config for IP literal endpoints only. Use these types to plug this package into higher-level code that expects the net package interfaces.

To route packets, you need to read packets using *Internet.InFlight. If you choose to forward the read packets, then you can deliver them to the right destination using *Internet.Deliver. We don't model L2 frames (we just move raw IP packets around) and we don't model multiple hops. These choices keep this package focused on fundamental primitives rather than full frameworks.

The *PCAPTrace type allows you to capture packets in flight in a PCAP format so that you can inspect what happened using tools such as wireshark.

Example (TcpDownloadIPv4)

This example creates a client and the server and the client downloads a small number of bytes from the server.

package main

import (
	"context"
	"fmt"
	"net/netip"
	"os"
	"sync"

	"github.com/bassosimone/runtimex"
	"github.com/bassosimone/uis"
)

func main() {
	// create the internet instance.
	ix := uis.NewInternet(uis.InternetOptionMaxInflight(256))

	// create the server and client stacks
	const mtu = uis.MTUJumbo
	srv := runtimex.PanicOnError1(ix.NewStack(mtu, netip.MustParseAddr("10.0.0.1")))
	defer srv.Close()

	clnt := runtimex.PanicOnError1(ix.NewStack(mtu, netip.MustParseAddr("10.0.0.2")))
	defer clnt.Close()

	// create a context used by connector and listener
	ctx := context.Background()

	// run the server in the background
	wg := &sync.WaitGroup{}
	ready := make(chan struct{})
	wg.Go(func() {
		listenCfg := uis.NewListenConfig(srv)
		listener := runtimex.PanicOnError1(listenCfg.Listen(ctx, "tcp", "10.0.0.1:80"))
		close(ready)
		conn := runtimex.PanicOnError1(listener.Accept())
		message := []byte("Hello, world!\n")
		_ = runtimex.PanicOnError1(conn.Write(message))
		runtimex.PanicOnError0(conn.Close())
		runtimex.PanicOnError0(listener.Close())
	})

	// run the client in the background
	messagech := make(chan []byte, 1)
	wg.Go(func() {
		<-ready
		connector := uis.NewConnector(clnt)
		conn := runtimex.PanicOnError1(connector.DialContext(ctx, "tcp", "10.0.0.1:80"))
		buffer := make([]byte, 1024)
		count := runtimex.PanicOnError1(conn.Read(buffer))
		messagech <- buffer[:count]
		runtimex.PanicOnError0(conn.Close())
	})

	// know when both goroutines have stopped
	stopped := make(chan struct{})
	go func() {
		wg.Wait()
		close(stopped)
	}()

	// route and capture packets in the foreground
	traceFile := runtimex.PanicOnError1(os.Create("tcpDownloadIPv4.pcap"))
	trace := uis.NewPCAPTrace(traceFile, uis.MTUJumbo)
loop:
	for {
		select {
		case frame := <-ix.InFlight():
			trace.Dump(frame.Packet)
			_ = ix.Deliver(frame)
		case <-stopped:
			break loop
		}
	}
	runtimex.PanicOnError0(trace.Close())

	// receive and print the server message
	message := <-messagech
	fmt.Printf("%s", string(message))

}
Output:

Hello, world!
Example (UdpEchoIPv4)

This example creates a client and server using UDP over IPv4. The server echoes back whatever it receives.

package main

import (
	"context"
	"fmt"
	"net/netip"
	"os"
	"sync"

	"github.com/bassosimone/runtimex"
	"github.com/bassosimone/uis"
)

func main() {
	// create the internet instance.
	ix := uis.NewInternet(uis.InternetOptionMaxInflight(256))

	// create the server and client stacks
	const mtu = uis.MTUJumbo
	srv := runtimex.PanicOnError1(ix.NewStack(mtu, netip.MustParseAddr("10.0.0.1")))
	defer srv.Close()

	clnt := runtimex.PanicOnError1(ix.NewStack(mtu, netip.MustParseAddr("10.0.0.2")))
	defer clnt.Close()

	// create a context used by connector and listener
	ctx := context.Background()

	// run the server in the background
	wg := &sync.WaitGroup{}
	ready := make(chan struct{})
	wg.Go(func() {
		listenCfg := uis.NewListenConfig(srv)
		pconn := runtimex.PanicOnError1(listenCfg.ListenPacket(ctx, "udp", "10.0.0.1:53"))
		defer pconn.Close()
		close(ready)
		buffer := make([]byte, 2048)
		count, addr := runtimex.PanicOnError2(pconn.ReadFrom(buffer))
		_ = runtimex.PanicOnError1(pconn.WriteTo(buffer[:count], addr))
	})

	// run the client in the background
	messagech := make(chan []byte, 1)
	wg.Go(func() {
		<-ready
		connector := uis.NewConnector(clnt)
		conn := runtimex.PanicOnError1(connector.DialContext(ctx, "udp", "10.0.0.1:53"))
		message := []byte("Hello, IPv4!\n")
		_ = runtimex.PanicOnError1(conn.Write(message))
		buffer := make([]byte, 1024)
		count := runtimex.PanicOnError1(conn.Read(buffer))
		messagech <- buffer[:count]
		runtimex.PanicOnError0(conn.Close())
	})

	// know when both goroutines have stopped
	stopped := make(chan struct{})
	go func() {
		wg.Wait()
		close(stopped)
	}()

	// route and capture packets in the foreground
	traceFile := runtimex.PanicOnError1(os.Create("udpEchoIPv4.pcap"))
	trace := uis.NewPCAPTrace(traceFile, uis.MTUJumbo)
loop:
	for {
		select {
		case frame := <-ix.InFlight():
			trace.Dump(frame.Packet)
			_ = ix.Deliver(frame)
		case <-stopped:
			break loop
		}
	}
	runtimex.PanicOnError0(trace.Close())

	// receive and print the echoed message
	message := <-messagech
	fmt.Printf("%s", string(message))

}
Output:

Hello, IPv4!
Example (UdpEchoIPv6)

This example creates a client and server using UDP over IPv6. The server echoes back whatever it receives.

package main

import (
	"context"
	"fmt"
	"net/netip"
	"os"
	"sync"

	"github.com/bassosimone/runtimex"
	"github.com/bassosimone/uis"
)

func main() {
	// create the internet instance.
	ix := uis.NewInternet(uis.InternetOptionMaxInflight(256))

	// create the server and client stacks
	const mtu = uis.MTUJumbo
	srv := runtimex.PanicOnError1(ix.NewStack(mtu, netip.MustParseAddr("2001:db8::1")))
	defer srv.Close()

	clnt := runtimex.PanicOnError1(ix.NewStack(mtu, netip.MustParseAddr("2001:db8::2")))
	defer clnt.Close()

	// create a context used by connector and listener
	ctx := context.Background()

	// run the server in the background
	wg := &sync.WaitGroup{}
	ready := make(chan struct{})
	wg.Go(func() {
		listenCfg := uis.NewListenConfig(srv)
		pconn := runtimex.PanicOnError1(listenCfg.ListenPacket(ctx, "udp", "[2001:db8::1]:53"))
		defer pconn.Close()
		close(ready)
		buffer := make([]byte, 2048)
		count, addr := runtimex.PanicOnError2(pconn.ReadFrom(buffer))
		_ = runtimex.PanicOnError1(pconn.WriteTo(buffer[:count], addr))
	})

	// run the client in the background
	messagech := make(chan []byte, 1)
	wg.Go(func() {
		<-ready
		connector := uis.NewConnector(clnt)
		conn := runtimex.PanicOnError1(connector.DialContext(ctx, "udp", "[2001:db8::1]:53"))
		message := []byte("Hello, IPv6!\n")
		_ = runtimex.PanicOnError1(conn.Write(message))
		buffer := make([]byte, 1024)
		count := runtimex.PanicOnError1(conn.Read(buffer))
		messagech <- buffer[:count]
		runtimex.PanicOnError0(conn.Close())
	})

	// know when both goroutines have stopped
	stopped := make(chan struct{})
	go func() {
		wg.Wait()
		close(stopped)
	}()

	// route and capture packets in the foreground
	traceFile := runtimex.PanicOnError1(os.Create("udpEchoIPv6.pcap"))
	trace := uis.NewPCAPTrace(traceFile, uis.MTUJumbo)
loop:
	for {
		select {
		case frame := <-ix.InFlight():
			trace.Dump(frame.Packet)
			_ = ix.Deliver(frame)
		case <-stopped:
			break loop
		}
	}
	runtimex.PanicOnError0(trace.Close())

	// receive and print the echoed message
	message := <-messagech
	fmt.Printf("%s", string(message))

}
Output:

Hello, IPv6!

Index

Examples

Constants

View Source
const (
	// MTUEthernet is the MTU used by Ethernet.
	MTUEthernet = 1500

	// MTUMinimumIPv6 is the minimum MTU required by IPv6.
	MTUMinimumIPv6 = 1280

	// MTUJumo is the MTU used by jumbo frames.
	MTUJumbo = 9000
)

Enumerate common MTU values.

View Source
const DefaultMaxInflight = 1024

DefaultMaxInflight is the default maximum number of inflight packets.

Variables

This section is empty.

Functions

This section is empty.

Types

type Connector

type Connector struct {
	// contains filtered or unexported fields
}

Connector allows to dial net.Conn connections pretty much like *net.Dialer except that here we use a *Stack implementation as the networking backend.

The zero value is invalid. Construct using NewConnector.

Only IP literal endpoints are supported. Dialing a hostname will fail.

func NewConnector

func NewConnector(stack *Stack) *Connector

NewConnector creates a new *Connector instance.

func (*Connector) DialContext

func (c *Connector) DialContext(ctx context.Context, network string, address string) (net.Conn, error)

DialContext creates a new net.Conn connection.

type Internet

type Internet struct {
	// contains filtered or unexported fields
}

Internet models the entire internet.

Construct using NewInternet.

func NewInternet

func NewInternet(options ...InternetOption) *Internet

NewInternet creates and returns a new *Internet instance.

func (*Internet) AddRoute

func (ix *Internet) AddRoute(vnic *VNIC, addrs ...netip.Addr) error

AddRoute registers the given *VNIC to have the given addresses such that it is possible to route packets to it.

This method fails if the claimed addresses are already in use.

func (*Internet) Deliver

func (ix *Internet) Deliver(frame VNICFrame) bool

Deliver routes a frame to the appropriate host based on destination IP.

It parses the destination IP from the raw packet, looks up the registered host for that address, and injects the frame into that host stack.

Returns false if the destination IP cannot be parsed, is not routable (no host registered for that address), or injection fails.

func (*Internet) InFlight

func (ix *Internet) InFlight() <-chan VNICFrame

InFlight returns the channel where the in flight VNICFrame are posted.

func (*Internet) NewStack

func (ix *Internet) NewStack(mtu uint32, addrs ...netip.Addr) (*Stack, error)

NewStack creates and attaches a *Stack to the *Internet.

The mtu parameter sets the MTU in bytes. Common values:

- MTUEthernet - MTUMinimumIPv6 - MTUJumbo

The addrs argument contains the IPv4/IPv6 addresses to configure.

This method implementation combines:

1. *Internet.NewVNIC to create a virtual NIC

2. NewStack to create a *Stack associated to a virtual NIC

3. [*Internet.AddrRoute] to create the return routes

func (*Internet) NewVNIC

func (ix *Internet) NewVNIC(mtu uint32) *VNIC

NewVNIC constructs a new *VNIC attached to the *Internet.

The mtu parameter sets the MTU in bytes. Common values:

- MTUEthernet - MTUMinimumIPv6 - MTUJumbo

This method internally invokes the NewVNIC factory func.

type InternetOption

type InternetOption func(cfg *internetConfig)

InternetOption is an option for NewInternet.

func InternetOptionMaxInflight

func InternetOptionMaxInflight(max int) InternetOption

InternetOptionMaxInflight sets the maximum number of inflight packets.

The default is DefaultMaxInflight packets. When the channel is full, additional packets are silently dropped.

type ListenConfig

type ListenConfig struct {
	// contains filtered or unexported fields
}

ListenConfig allows to listen pretty much like *net.ListenConfig except that here we use a *Stack implementation as the networking backend.

The zero value is invalid. Construct using NewListenConfig.

Only IP literal endpoints are supported. Listening on a hostname will fail.

func NewListenConfig

func NewListenConfig(stack *Stack) *ListenConfig

NewListenConfig creates a new *ListenConfig instance.

func (*ListenConfig) Listen

func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (net.Listener, error)

Listen creates a listening TCP socket.

func (*ListenConfig) ListenPacket

func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error)

ListenPacket creates a listening packet conn.

type PCAPTrace

type PCAPTrace struct {
	// contains filtered or unexported fields
}

PCAPTrace is an open PCAP trace.

func NewPCAPTrace

func NewPCAPTrace(wc io.WriteCloser, snapSize uint16, options ...PCAPTraceOption) *PCAPTrace

NewPCAPTrace creates a new *PCAPTrace instance.

Takes ownership of the io.WriteCloser and ensures the file is closed and flushed when you invoke the *PCAPTrace.Close method.

We recommend using a large snapshot size for inspecting the full packets that are exchanged by the *Stack you are using in your tests.

func (*PCAPTrace) Close

func (tr *PCAPTrace) Close() (err error)

Close interrupts the background goroutine and waits for it to join before closing the packet capture file.

func (*PCAPTrace) Dropped

func (tr *PCAPTrace) Dropped() uint64

Dropped returns the number of packets dropped due to buffer overflow.

Packets are dropped when Dump is called but the internal buffer is full. This happens when disk I/O cannot keep up with packet capture rate.

func (*PCAPTrace) Dump

func (tr *PCAPTrace) Dump(packet []byte)

Dump dumps the information about the given raw IPv4/IPv6 packet.

type PCAPTraceOption

type PCAPTraceOption func(cfg *pcapTraceConfig)

PCAPTraceOption is an option for NewPCAPTrace.

func PCAPTraceOptionBuffer

func PCAPTraceOptionBuffer(bufferSize int) PCAPTraceOption

PCAPTraceOptionBuffer sets the buffer size for the internal packet channel.

The default is 4096 snapshots. When the buffer is full, new snapshots are dropped and counted using *PCAPTrace.Dropped.

A zero or negative value is silently ignored.

type Stack

type Stack struct {
	Stack *stack.Stack
}

Stack is a wrapper for *stack.Stack allowing basic network operations with gVisor's TCP and UDP conns.

Construct using NewStack.

func NewStack

func NewStack(vnic stack.LinkEndpoint, addrs ...netip.Addr) *Stack

NewStack creates a new *Stack using a stack.LinkEndpoint.

func (*Stack) Close

func (sx *Stack) Close()

Close shuts down the stack and waits for the NIC teardown to finish.

func (*Stack) DialTCP

func (sx *Stack) DialTCP(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error)

DialTCP establishes a new *gonet.TCPConn.

func (*Stack) DialUDP

func (sx *Stack) DialUDP(addr netip.AddrPort) (*gonet.UDPConn, error)

DialUDP creates a new connected *gonet.UDPConn.

func (*Stack) ListenTCP

func (sx *Stack) ListenTCP(addr netip.AddrPort) (*gonet.TCPListener, error)

ListenTCP creates a new *gonet.TCPListener.

func (*Stack) ListenUDP

func (sx *Stack) ListenUDP(addr netip.AddrPort) (*gonet.UDPConn, error)

ListenUDP creates a new listening *gonet.UDPConn.

type VNIC

type VNIC struct {
	// contains filtered or unexported fields
}

VNIC models a virtual NIC. This type is compatible with stack.Stack because it implements the stack.LinkEndpoint interface.

To send packets, stack.Stack invokes *VNIC.WritePackets, which, in turn, invokes the attached VNICNetwork SendFrame.

To receive packets, the attached VNICNetwork invokes *VNIC.InjectFrame, which invokes the stack.NetworkDispatcher do dispatch it.

The stack.Stack configures the *VNIC stack.NetworkDispatcher used for dispatching via the *VNIC.Attach method.

Construct using NewVNIC.

func NewVNIC

func NewVNIC(mtu uint32, network VNICNetwork) *VNIC

NewVNIC creates a new *VNIC instance.

The mtu parameter sets the MTU in bytes. Common values:

- MTUEthernet - MTUMinimumIPv6 - MTUJumbo

The network parameter is the *VNICNetwork to use.

func (*VNIC) ARPHardwareType

func (n *VNIC) ARPHardwareType() header.ARPHardwareType

ARPHardwareType implements stack.LinkEndpoint.

func (*VNIC) AddHeader

func (n *VNIC) AddHeader(pbuf *stack.PacketBuffer)

AddHeader implements stack.LinkEndpoint.

func (*VNIC) Attach

func (n *VNIC) Attach(disp stack.NetworkDispatcher)

Attach implements stack.LinkEndpoint.

func (*VNIC) Capabilities

func (n *VNIC) Capabilities() stack.LinkEndpointCapabilities

Capabilities implements stack.LinkEndpoint.

func (*VNIC) Close

func (n *VNIC) Close()

Close implements stack.LinkEndpoint.

func (*VNIC) InjectFrame

func (n *VNIC) InjectFrame(frame VNICFrame) bool

InjectFrame injects an inbound raw IPv4/IPv6 packet into the stack.

func (*VNIC) IsAttached

func (n *VNIC) IsAttached() bool

IsAttached implements stack.LinkEndpoint.

func (*VNIC) LinkAddress

func (n *VNIC) LinkAddress() tcpip.LinkAddress

LinkAddress implements stack.LinkEndpoint.

func (*VNIC) MTU

func (n *VNIC) MTU() uint32

MTU implements stack.LinkEndpoint.

func (*VNIC) MaxHeaderLength

func (n *VNIC) MaxHeaderLength() uint16

MaxHeaderLength implements stack.LinkEndpoint.

func (*VNIC) ParseHeader

func (n *VNIC) ParseHeader(pbuf *stack.PacketBuffer) bool

ParseHeader implements stack.LinkEndpoint.

func (*VNIC) SetLinkAddress

func (n *VNIC) SetLinkAddress(addr tcpip.LinkAddress)

SetLinkAddress implements stack.LinkEndpoint.

func (*VNIC) SetMTU

func (n *VNIC) SetMTU(mtu uint32)

SetMTU implements stack.LinkEndpoint.

func (*VNIC) SetOnCloseAction

func (n *VNIC) SetOnCloseAction(action func())

SetOnCloseAction implements stack.LinkEndpoint.

func (*VNIC) Wait

func (n *VNIC) Wait()

Wait implements stack.LinkEndpoint.

func (*VNIC) WritePackets

func (n *VNIC) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error)

WritePackets implements stack.LinkEndpoint.

type VNICFrame

type VNICFrame struct {
	// Packet contains a raw IP packet (IPv4 or IPv6).
	Packet []byte
}

VNICFrame models a virtual link-layer frame without addressing.

type VNICNetwork

type VNICNetwork interface {
	SendFrame(frame VNICFrame) bool
}

VNICNetwork models the network that a VNIC sends packets to.

The *Internet implements this interface.

Directories

Path Synopsis
cmd
benchmark command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL