Read raw packets off a virtual interface, terminate TCP & UDP, and hand connections to your own handler. The networking core behind sing-box — cross-platform, and finally documented.
sing-tun creates and configures the virtual network interface on every major OS, then runs a TCP/IP stack on top of it so your app sees clean connections instead of raw IP packets.
Cross-platform creation, addressing, MTU, routing and the per-OS plumbing (Wintun on Windows, utun on Darwin, netlink on Linux, fd on mobile). One Options struct.
Turns IP packets into accepted connections. Choose how TCP is terminated — in the host kernel, in userspace via gVisor, or a blend — without changing your handler.
Implement three methods and every tunneled flow lands in your hands as a net.Conn with its real destination. Proxy it, block it, route it — your call.
The stack string you pass to NewStack decides where TCP lives. They share the same handler interface — switching is a one-word change. See the deep dive →
NATs packets back to the TUN and lets the host kernel terminate TCP. The userspace only rewrites headers — no TCP state machine.
A full TCP/IP stack in userspace (Google gVisor netstack). Terminates everything in-process — maximum portability and control.
gVisor for TCP, the system path for UDP. The default when gVisor is built and GSO is off — a pragmatic middle ground.
A minimal, real integration. Everything below maps directly to the public API in github.com/sagernet/sing-tun.
Pull the module. The gvisor and mixed stacks live behind a build tag — omit it and only system is available (requesting the others returns ErrGVisorNotIncluded).
# add the dependency go get github.com/sagernet/sing-tun # build with the userspace stack enabled go build -tags with_gvisor ./...
Describe the interface once with tun.Options. AutoRoute installs the routes that steer traffic into the device. Keep this value — the stack needs it too.
import ( "net/netip" tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" ) opts := tun.Options{ Name: "tun0", MTU: 9000, Inet4Address: []netip.Prefix{netip.MustParsePrefix("172.19.0.1/30")}, AutoRoute: true, // install routes into the TUN Logger: logger.NOP(), } tunIf, err := tun.New(opts) if err != nil { log.Fatal(err) }
This is your proxy logic. The stack hands you a ready net.Conn plus the original destination — TCP via NewConnectionEx, UDP via NewPacketConnectionEx. Return tun.ErrDrop from PrepareConnection to reject a flow.
type handler struct{} // TCP — relay the tunneled connection to its real destination. func (h *handler) NewConnectionEx(ctx context.Context, conn net.Conn, source, destination M.Socksaddr, onClose N.CloseHandlerFunc) { go func() { defer conn.Close() remote, err := net.Dial("tcp", destination.String()) if err != nil { return } defer remote.Close() relay(conn, remote) // bidirectional io.Copy if onClose != nil { onClose(nil) } }() } // UDP — same shape, with an N.PacketConn. func (h *handler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source, destination M.Socksaddr, onClose N.CloseHandlerFunc) { /* ... */ } func (h *handler) PrepareConnection(network string, source, destination M.Socksaddr) error { return nil }
Wire the TUN, your handler and the same opts into NewStack, then Start(). Swap the first argument between "system", "gvisor", "mixed", or "" to auto-select.
stack, err := tun.NewStack("gvisor", tun.StackOptions{ Context: context.Background(), Tun: tunIf, TunOptions: opts, // the Options from step 2 Handler: &handler{}, UDPTimeout: 5 * time.Minute, Logger: logger.NOP(), }) if err != nil { log.Fatal(err) } // ErrGVisorNotIncluded if tag missing if err := stack.Start(); err != nil { log.Fatal(err) } // packets now flow: TUN → stack → your Handler
Auto-select. Pass stack: "" and sing-tun chooses for you: gvisor when includeAllNetworks is on, mixed when gVisor is built and GSO is off, otherwise system. Explicit is better when you know your platform.
system is the always-compiled baseline; gvisor is the universal fallback (with the build tag). The exceptions are the interesting part.
| Platform | system | gvisor | Notes |
|---|---|---|---|
| Linux | ✓ | ✓ | AutoRedirect available for eBPF-assisted routing |
| macOS | ✓ | ✓ | utun; system is the common default |
| Windows | ✓ | ✓ | requires the Wintun driver |
| Android | ✓ | ✓ | system needs kernel ≥ 5.10 — older kernels fall back to gvisor |
| iOS | ✓ * | ✓ | *system works on iOS, but not while includeAllNetworks is enabled (sing-tun#25). Apps that enable it for full-tunnel / cellular DNS use gvisor. |
Want the mechanism? How the system stack NATs packets back to the kernel, why it's fast, and the resource tradeoff vs gVisor.