How OpenFlux Tunnels TCP Traffic Using gVisor and Raw Sockets
OpenFlux tunnels TCP traffic by embedding a userspace gVisor network stack that intercepts connections through a virtual NIC, forwarding packets over pluggable transports using either proxy mode (socket-level forwarding) or raw mode (layer-3 packet injection with SNAT).
OpenFlux is an open-source tunneling tool that creates a transparent TCP proxy without kernel modifications. The project at p1neappleXpress/OpenFlux implements a complete IPv4/TCP stack in userspace using Google's gVisor, enabling it to tunnel arbitrary TCP streams over various underlying transports while supporting two distinct forwarding strategies for the exit node.
Architectural Foundation
The tunnel architecture centers on a virtual network stack that runs entirely in userspace, decoupling TCP logic from the host operating system.
gVisor Stack Initialization
The core tunnel logic in tunnel/tunnel.go instantiates a gVisor stack.Stack configured for IPv4 and TCP protocols. During initialization in NewTCPTunnelMode (lines 87‑105), the code creates the stack with protocol factories:
t.gvisorStack = stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol},
})
This userspace stack handles the entire TCP state machine, congestion control, and reassembly independently of the host kernel.
Virtual NIC and Transport Bridge
OpenFlux bridges the gVisor stack to the underlying transport (such as Yandex or OneMe) through a custom TunnelLinkEndpoint defined in tunnel/endpoint.go. This virtual NIC implements two critical paths:
- Outbound: The stack calls
tunnelEP.onOutgoingPacketto hand packets to the transport layer. - Inbound: The transport injects received packets via
tunnelEP.InjectInbound.
This design allows the tunnel to operate over any reliable transport while maintaining standard TCP semantics.
Tunnel Operating Modes
OpenFlux supports two exit-node modes selected via the --mode flag: Proxy (ExitModeProxy) and Raw (ExitModeRaw).
Proxy Mode (ExitModeProxy)
In proxy mode, the exit node terminates TCP connections inside the gVisor stack and opens new outbound sockets to the destination. Implemented in setupExitNodeProxy (lines 33‑44) and handleExitTCP (lines 46‑84) of tunnel/tunnel.go, this mode:
- Puts the stack into promiscuous and spoofing mode.
- Registers a
tcp.Forwarderto intercept outbound connection attempts. - Accepts the gVisor-side connection via
gonet.TCPConn. - Dials the real destination using
net.DialTimeout. - Shuffles data bidirectionally between the two streams.
This mode requires no special privileges and works on all operating systems, making it the default portable option.
Raw Mode (ExitModeRaw)
Raw mode operates at layer 3, forwarding packets without terminating TCP connections. Available only on Linux and requiring root privileges, this mode is implemented in setupExitNodeRaw (lines 89‑143):
- Creates a
RawSocketEndpoint(defined intunnel/rawsocket_linux.go) that binds to a raw socket. - Discovers the exit node's local IP via
getLocalIPand registers it as a protocol address on a second NIC (ID 2). - Configures routing so internet-bound traffic leaves via the raw NIC while tunnel-subnet traffic uses NIC 1.
- Performs source-IP rewriting (SNAT) before injecting packets into the host network.
To prevent the host kernel from sending RST packets in response to unsolicited packets, raw mode typically requires an iptables rule to drop outbound TCP RSTs targeting the tunnel subnet.
Packet Flow and Lifecycle
Client Mode Operation
When running as a client, OpenFlux assigns the virtual IP 10.10.10.2 to its NIC, adds a default route pointing to the exit node, and uses the stack's gonet.DialTCP to initiate connections. All TCP handshakes and window management occur within the gVisor stack, with payload data encapsulated in the chosen transport protocol.
Exit Node Packet Processing
Proxy flow: Incoming packets from the transport are injected into the gVisor stack via InjectInbound. The stack processes them through the tcp.Forwarder, which triggers handleExitTCP to proxy the connection to the real destination.
Raw flow: The RawSocketEndpoint receives packets from the transport and forwards them directly to the raw socket via SetTransportSender. The host kernel processes these as if they originated locally, preserving the original TCP ports and sequence numbers.
Public API and Usage
The TCPTunnel type exposes a simple API for callers, implemented in tunnel/tunnel.go.
Creating a Tunnel
As used in main/main.go (lines 46‑48), initialization follows this pattern:
// Parse the desired exit mode from the CLI flag
exitMode, _ := tunnel.ParseExitMode(*mode)
// Build the transport (e.g., Yandex Docs)
trans := transport.NewCompressedTransport(innerTransport)
// Initialise the tunnel; *exitNode indicates whether this instance is the exit
tun := tunnel.NewTCPTunnelMode(trans, *exitNode, exitMode)
Dialing Remote Hosts
The DialTCP method (lines 60‑82) provides a standard net.Conn interface:
conn, err := tun.DialTCP("example.com:443")
if err != nil {
log.Fatalf("dial error: %v", err)
}
defer conn.Close()
// Now conn behaves like a normal net.Conn (TLS, HTTP, etc.)
Listening for Connections
Exit nodes can accept inbound tunnel connections using ListenTCP (lines 86‑90):
listener, err := tun.ListenTCP(8443)
if err != nil {
log.Fatalf("listen error: %v", err)
}
defer listener.Close()
for {
client, err := listener.Accept()
if err != nil {
continue // handle error as needed
}
go func(c net.Conn) {
defer c.Close()
// Proxy the connection to a real destination
remote, _ := net.Dial("tcp", "target.internal:22")
io.Copy(c, remote)
io.Copy(remote, c)
}(client)
}
Running Raw Mode
To start the exit node in raw mode with SNAT (as referenced in main.go lines 23‑30):
sudo ./openflux -exit-node -mode raw -local-ip 10.0.0.42
Key Source Files
tunnel/tunnel.go: Core implementation includingNewTCPTunnelMode,setupExitNodeProxy,setupExitNodeRaw,handleExitTCP,DialTCP, andListenTCP.tunnel/rawsocket_linux.go: Linux-specificRawSocketEndpointfor layer-3 packet forwarding.tunnel/endpoint.go: Definition ofTunnelLinkEndpointbridging gVisor and the transport layer.main/main.go: CLI entry point that wires transports, tunnel modes, and the SOCKS5 server.
Summary
- OpenFlux implements TCP tunneling through a userspace gVisor stack that runs independently of the host kernel, providing portable TCP semantics.
- Two exit modes provide flexibility: Proxy mode terminates TCP for compatibility across all platforms, while raw mode preserves end-to-end TCP behavior on Linux using raw sockets and SNAT.
- The virtual NIC architecture decouples the network stack from underlying transports via
TunnelLinkEndpoint, enabling operation over protocols like Yandex Docs or OneMe. - The public API exposes standard Go networking primitives (
DialTCP,ListenTCP) while internally managing complex gVisor interactions and routing tables.
Frequently Asked Questions
What underlying transports does OpenFlux support?
OpenFlux abstracts the network layer through a transport interface defined in transport/transport.go. The codebase includes implementations that can operate over various carriers (referenced as "Yandex," "OneMe," etc.), with support for compression and encryption layers wrapped via NewCompressedTransport.
Why does raw mode require root privileges while proxy mode does not?
Raw mode needs to create raw sockets (syscall.SOCK_RAW) to inject packets at the IP layer and typically requires iptables rules to manage TCP RST suppression. Proxy mode operates entirely at the socket layer using standard net.Dial and gonet calls, which any unprivileged process can execute.
How does OpenFlux handle TCP connection state?
All TCP state—including handshakes, window scaling, and congestion control—is managed within the gVisor stack instance created in tunnel/tunnel.go. The host kernel only sees either proxied connections (proxy mode) or raw IP packets (raw mode), never the internal TCP state of the tunnel clients.
What IP addressing does the tunnel use internally?
The client node assigns itself the hardcoded virtual IP 10.10.10.2 (as seen in the client initialization code), while the exit node discovers its local IP via getLocalIP. Traffic destined outside the tunnel subnet routes through NIC 1 (tunnel) or NIC 2 (raw internet interface) depending on the configured mode and destination.
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 →