How OpenFlux Implements gVisor's Userspace TCP/IP Stack for Network Tunneling
OpenFlux implements a complete userspace TCP/IP stack using gVisor's networking library by instantiating a virtual stack.Stack in tunnel/tunnel.go, configuring dual NICs for tunnel and Internet traffic, and exposing standard Go net.Conn interfaces through the gonet adapter to route packets between encrypted transports and raw host sockets.
OpenFlux is an open-source tunneling solution that leverages gVisor's userspace networking to avoid kernel dependencies for TCP/IP processing. By embedding the gvisor.dev/gvisor/pkg/tcpip/stack package, the project creates an isolated networking environment where all protocol handling—from packet parsing to connection state management—runs entirely in userspace.
Core Architecture: The gVisor Stack in tunnel/tunnel.go
The foundation of OpenFlux's networking implementation resides in tunnel/tunnel.go, where the repository initializes a self-contained TCP/IP stack using gVisor's library.
Stack Instantiation
The constructor creates a new stack instance configured specifically for IPv4 and TCP protocols, deliberately omitting unnecessary protocol handlers to minimize memory footprint and attack surface:
t.gvisorStack = stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol},
})
TCP Buffer Configuration
Before accepting connections, OpenFlux tunes the stack's buffer limits via SetTCPBuffers, applying minimum, default, and maximum buffer sizes to the TCP transport protocol. This prevents memory exhaustion during high-throughput scenarios and ensures consistent latency characteristics across different deployment environments.
Virtual Network Interface Configuration
OpenFlux creates two distinct Network Interface Controllers (NICs) within the gVisor stack to segregate tunnel traffic from Internet-bound traffic.
Exit Node Setup with Raw Sockets
When operating as an exit node, OpenFlux activates setupExitNode to create a raw-socket NIC (NIC 2) that bridges the userspace stack to the host operating system's network interface. This function assigns the node's external IP address, enables IPv4 forwarding between NICs, and installs two critical routes: one for the tunnel subnet (10.10.10.0/24) directing traffic toward NIC 1, and a default route (0.0.0.0/0) for Internet-bound traffic via NIC 2:
t.gvisorStack.CreateNIC(internetNIC, rawEP)
t.gvisorStack.AddProtocolAddress(internetNIC, tcpip.ProtocolAddress{ … })
t.gvisorStack.SetForwardingDefaultAndAllNICs(ipv4.ProtocolNumber, true)
Client-Side Tunnel Configuration
For client nodes, setupClient registers only the tunnel NIC (NIC 1) with a static address of 10.10.10.2/24 and establishes a default route that directs all traffic through the encrypted tunnel endpoint. This configuration ensures complete traffic isolation; no packets leak to the host network stack until they traverse the tunnel and reach the exit node.
Data Flow and Packet Routing
The userspace TCP/IP stack relies on a custom link endpoint implementation to exchange raw IP packets between gVisor's networking layer and OpenFlux's transport layer.
Outbound Packet Handling
The TunnelLinkEndpoint struct implements an onOutgoingPacket callback that intercepts every serialized IP packet produced by the gVisor stack. When an application writes to a TCP connection, the stack encapsulates the data into an IP packet and invokes this callback, which forwards the raw bytes to the configured transport layer via transport.Transport.Send():
tunnelEP.onOutgoingPacket = func(data []byte) { trans.Send(data) }
Inbound Packet Injection
For incoming traffic, the transport layer's Receive handler takes encrypted packets from the network, decrypts them if necessary, and injects the raw IP payloads back into the gVisor stack through tunnelEP.InjectInbound(). The stack then processes these packets through its TCP implementation, delivering the payload to the appropriate socket buffer:
trans.Receive(func(data []byte) { tunnelEP.InjectInbound(data) })
Exposing Standard Go Networking Primitives
OpenFlux abstracts gVisor's internal socket mechanisms using the gvisor.dev/gvisor/pkg/tcpip/adapters/gonet package, allowing applications to use familiar net.Conn and net.Listener interfaces without awareness of the underlying userspace stack.
TCP Dialing via gonet
The DialTCP method utilizes gonet.DialTCP to establish connections within the virtual stack. Depending on the node's role, the connection routes through NIC 1 (tunnel) for client nodes or NIC 2 (Internet) for exit nodes:
conn, err := clientTunnel.DialTCP("example.com:443")
if err != nil { log.Fatal(err) }
defer conn.Close()
TCP Listening via gonet
For accepting inbound connections, ListenTCP leverages gonet.ListenTCP to bind to the userspace stack rather than the host kernel. This enables exit nodes to accept TCP connections from the Internet while processing them entirely within the gVisor TCP/IP implementation:
ln, err := clientTunnel.ListenTCP(8080)
if err != nil { log.Fatal(err) }
for {
c, _ := ln.Accept()
go handle(c) // c is a *net.TCPConn backed by the gVisor stack
}
Monitoring Stack Statistics
OpenFlux includes a background statistics collector (printStats) that periodically queries t.gvisorStack.Stats() to log metrics including the number of connected sockets, established connections, and TCP retransmissions. This visibility into the userspace TCP/IP stack's internal state aids in debugging congestion control issues and monitoring tunnel health in production deployments.
Summary
- gVisor Integration: OpenFlux instantiates a complete TCP/IP stack in
tunnel/tunnel.gousingstack.Newwith IPv4 and TCP protocol factories, eliminating kernel networking dependencies. - Dual NIC Architecture: The implementation creates NIC 1 for tunnel traffic (via
TunnelLinkEndpoint) and NIC 2 for Internet traffic (viaRawSocketEndpoint), enabling flexible routing for both client and exit-node modes. - Packet Bridging: Raw IP packets flow between the transport layer and gVisor through
onOutgoingPacketcallbacks andInjectInboundmethods, maintaining full userspace control over data paths. - Standard Interfaces: The
gonetadapter package exposesnet.Connandnet.Listenerprimitives, allowing existing Go applications to utilize the userspace stack without code modifications. - Observability: Built-in statistics collection via
stack.Stats()provides real-time visibility into TCP connection states and performance metrics.
Frequently Asked Questions
What is the advantage of using gVisor's userspace TCP/IP stack over the host kernel's networking?
Running the TCP/IP stack in userspace provides deterministic network behavior across different operating systems and kernel versions, eliminates the need for elevated privileges to modify routing tables, and enables encrypted tunneling of raw TCP connections without kernel modules. According to the OpenFlux source code, this approach allows the exit node to process Internet traffic entirely within the application process while maintaining isolation between the tunnel subnet (10.10.10.0/24) and host network interfaces.
How does OpenFlux route traffic between the tunnel and the Internet?
OpenFlux configures forwarding between two virtual NICs within the gVisor stack using SetForwardingDefaultAndAllNICs(ipv4.ProtocolNumber, true). NIC 1 handles packets to and from the tunnel subnet (10.10.10.0/24), while NIC 2 connects to the host's raw socket for Internet access. When operating as an exit node, the stack routes packets received from tunnel clients (NIC 1) out to the Internet (NIC 2) and vice versa, functioning as a userspace IP router.
Can applications use standard Go networking libraries with OpenFlux's implementation?
Yes. OpenFlux utilizes the gonet adapter package from gVisor to wrap internal stack sockets into standard net.Conn and net.Listener objects. This means applications can call DialTCP or ListenTCP and receive *net.TCPConn instances that behave identically to kernel-managed connections, despite actually traversing the userspace TCP/IP stack and custom transport encryption layer.
Where is the TCP buffer size configured in the OpenFlux stack?
Buffer tuning occurs in tunnel/tunnel.go within the SetTCPBuffers function, which applies minimum, default, and maximum values to the stack's TCP transport protocol immediately after stack initialization. This configuration controls memory allocation for send and receive buffers across all connections managed by the gVisor userspace stack, directly impacting throughput and latency characteristics.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →