github.com/sagernet/sing-tun · Go

A userspace
TUN stack for Go.

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.

what it is

One library, two jobs.

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.

layer 1

The TUN device

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.

layer 2

The network stack

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.

your code

The 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.

choose a stack

system, gvisor, or mixed.

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 →

"system"

system

NATs packets back to the TUN and lets the host kernel terminate TCP. The userspace only rewrites headers — no TCP state machine.

  • + Kernel-grade TCP: BBR/CUBIC, offloads, low heap
  • + No build tag — always compiled
  • One extra socket per connection
  • Unavailable under iOS includeAllNetworks
no build tag
"gvisor"

gvisor

A full TCP/IP stack in userspace (Google gVisor netstack). Terminates everything in-process — maximum portability and control.

  • + Works everywhere, incl. iOS includeAllNetworks
  • + No host-routing tricks, fewer fds
  • Per-conn buffers live in your process
  • Needs -tags with_gvisor
-tags with_gvisor
"mixed"

mixed

gVisor for TCP, the system path for UDP. The default when gVisor is built and GSO is off — a pragmatic middle ground.

  • + gVisor TCP correctness + system UDP
  • + Sensible auto-default (stack: "")
  • Also blocked by includeAllNetworks
  • Needs -tags with_gvisor
-tags with_gvisor
integrate

Four steps to packets.

A minimal, real integration. Everything below maps directly to the public API in github.com/sagernet/sing-tun.

01

Install & pick your build tags

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).

shell
# add the dependency
go get github.com/sagernet/sing-tun

# build with the userspace stack enabled
go build -tags with_gvisor ./...
02

Create the TUN interface

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.

main.go
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) }
03

Implement the Handler

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.

handler.go
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 }
04

Build & start the stack

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.

main.go
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
i

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.

platform support

Where each stack runs.

system is the always-compiled baseline; gvisor is the universal fallback (with the build tag). The exceptions are the interesting part.

PlatformsystemgvisorNotes
LinuxAutoRedirect available for eBPF-assisted routing
macOSutun; system is the common default
Windowsrequires the Wintun driver
Androidsystem 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.

Read the deep dive