How OpenFlux Configures the gVisor Stack for Exit Node Mode

OpenFlux configures the gVisor stack for exit node mode by instantiating a lightweight userspace TCP/IP stack from gvisor.dev/gvisor/pkg/tcpip/stack, then wiring it for either proxy mode (TCP termination with Go net.Dial forwarding) or raw mode (Linux raw sockets with SNAT) based on the --mode flag.

The p1neappleXpress/OpenFlux project implements a lightweight VPN exit node using the gVisor network stack to handle TCP traffic in userspace. When started with the --exit-node flag, OpenFlux initializes a custom stack.Stack that operates differently depending on whether you select proxy or raw exit mode, with both configurations managed in main/tunnel/tunnel.go.

Creating the gVisor Stack Foundation

All exit node configurations begin with the NewTCPTunnelMode function, which constructs a new gVisor stack configured specifically for IPv4 TCP networking. This foundation supports both exit node variations and standard client modes.

t.gvisorStack = stack.New(stack.Options{
    NetworkProtocols:   []stack.NetworkProtocolFactory{ipv4.NewProtocol},
    TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol},
})
SetTCPBuffers(t.gvisorStack)

The SetTCPBuffers call applies memory limits to prevent unbounded buffer growth during high-throughput scenarios, ensuring stable resource usage regardless of connection count.

Selecting the Exit Mode

OpenFlux determines the exit node behavior through the --mode command-line flag, parsed into either ExitModeProxy (default) or ExitModeRaw. During tunnel initialization, the code branches based on this selection to invoke the appropriate setup routine:

if mode == ExitModeRaw {
    t.setupExitNodeRaw(tunnelNIC)
} else {
    t.setupExitNodeProxy(tunnelNIC)
}

If raw socket creation fails due to insufficient privileges, the system automatically falls back to ExitModeProxy, ensuring the exit node remains functional even without root access.

Proxy Mode Configuration

Proxy mode terminates TCP connections locally within the gVisor stack, then forwards traffic to the internet using Go's standard net.Dial API. This portable approach works across Linux, macOS, and Windows.

Enabling Packet Spoofing and Promiscuous Mode

The stack must accept packets destined for any IP and transmit packets with arbitrary source addresses to proxy on behalf of VPN clients. The code enables these capabilities in main/tunnel/tunnel.go:

t.gvisorStack.SetPromiscuousMode(tunnelNIC, true)
t.gvisorStack.SetSpoofing(tunnelNIC, true)

Installing the TCP Forwarder

A tcp.NewForwarder handles incoming connections by creating gVisor endpoints and proxying them to remote destinations via net.DialTimeout:

forwarder := tcp.NewForwarder(t.gvisorStack, 0, 1024, t.handleExitTCP)
t.gvisorStack.SetTransportProtocolHandler(tcp.ProtocolNumber, forwarder.HandlePacket)

The handleExitTCP callback manages bidirectional data transfer between the gVisor endpoint and the actual remote server, effectively bridging the userspace stack to the real internet.

Routing Configuration

The setup adds a default route directing all outbound traffic through the tunnel NIC:

t.gvisorStack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: tunnelNIC})

This ensures that any packet not matching a specific subnet routes through the virtual tunnel interface for processing by the forwarder.

Raw Mode Configuration

Raw mode bypasses Go's net stack entirely, using Linux raw sockets to perform SNAT and ARP handling directly at the packet level. This requires root privileges and is implemented exclusively in main/tunnel/rawsocket_linux.go.

Raw Socket Endpoint Setup

The code creates a RawSocketEndpoint bound to a second NIC (internetNIC) that represents the physical network interface:

rawEP := NewRawSocketEndpoint(t.gvisorStack, internetNIC)
rawEP.SetTransportSender(t.gvisorStack)

IP Assignment and Forwarding

The exit node's public egress IP is assigned to the internet-facing NIC, and forwarding is enabled for both interfaces:

t.gvisorStack.AddProtocolAddress(internetNIC, protocolAddr, stack.AddressProperties{})
t.gvisorStack.SetForwardingDefaultAndAllNICs(ipv4.ProtocolNumber, true)
t.gvisorStack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: internetNIC})
t.gvisorStack.AddRoute(tcpip.Route{Destination: tunnelSubnet, NIC: tunnelNIC})

These steps configure the gVisor stack to route packets between the tunnel NIC and the raw internet NIC, performing network address translation without kernel networking overhead.

Client-Mode Setup

When operating as a client (non-exit node), the configuration simplifies significantly. The stack assigns a tunnel address (typically 10.10.10.2) and installs a default route pointing toward the exit node, delegating all internet-bound traffic to the remote peer without local TCP termination.

Summary

  • Stack Creation: NewTCPTunnelMode instantiates a gVisor stack.Stack with IPv4 and TCP protocols, applying TCP buffer limits via SetTCPBuffers in main/tunnel/tunnel.go.
  • Mode Selection: The --mode flag determines whether to call setupExitNodeProxy or setupExitNodeRaw, with automatic fallback to proxy mode if raw sockets are unavailable.
  • Proxy Mode: Enables promiscuous mode and spoofing on the tunnel NIC, installs a TCP forwarder that uses net.Dial, and routes all traffic through the tunnel interface for portable cross-platform operation.
  • Raw Mode: Attaches a Linux raw socket endpoint to a second NIC via NewRawSocketEndpoint, configures SNAT with the egress IP using AddProtocolAddress, and enables inter-NIC forwarding for kernel-bypass networking.
  • Transport Interface: Both modes use the transport interface defined in main/transport/transport.go to send and receive raw packets between the gVisor stack and the underlying network.

Frequently Asked Questions

What is the difference between proxy mode and raw mode in OpenFlux?

Proxy mode terminates TCP connections inside the gVisor stack and forwards them using Go's net.Dial API, making it portable across operating systems but introducing additional latency from context switching. Raw mode uses Linux raw sockets to perform packet-level SNAT and forwarding without traversing the Go network stack, offering lower overhead but requiring root privileges and Linux kernel support.

Can I run OpenFlux in raw mode on macOS or Windows?

No. Raw mode relies on main/tunnel/rawsocket_linux.go, which implements NewRawSocketEndpoint using Linux-specific raw socket syscalls. The repository includes stub implementations for other platforms (rawsocket_windows.go and rawsocket_darwin.go) that lack functionality, forcing an automatic fallback to proxy mode when running on non-Linux systems.

Why does OpenFlux enable promiscuous mode and spoofing in proxy mode?

The gVisor stack must accept packets destined for any IP address (promiscuous mode) and transmit packets with arbitrary source IPs (spoofing) to properly handle traffic from multiple downstream VPN clients. These settings allow the exit node to proxy connections on behalf of the entire VPN subnet without maintaining individual virtual interfaces for each client IP.

How does raw mode handle IP address assignment differently than proxy mode?

In raw mode, the exit node's public egress IP is parsed from configuration and explicitly assigned to the internetNIC using AddProtocolAddress, enabling the raw socket endpoint to perform SNAT by rewriting packet headers with the correct source address before transmission. Proxy mode does not require this assignment because it uses the host operating system's networking stack through net.Dial, which handles source IP selection automatically.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →