tun

package
v0.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 22, 2026 License: CC0-1.0 Imports: 8 Imported by: 0

Documentation

Overview

Package tun provides a TUN (network tunnel) interface for handling virtual network devices. It defines the Tun interface, which is compatible with wireguard-go and similar projects, along with utility functions for I/O adaptation, testing, and bidirectional packet copying.

Detach wraps a Tun with wrapper-local Up, Down, and Close state. Because Tun has no deadline or context API, the wrapper copies packets at the boundary and uses internal read/write pumps so pending wrapper Read and Write calls can return without closing the wrapped device. If the wrapped Tun itself blocks without cancellation support, an internal pump may remain blocked until that underlying operation completes.

Only one active detached wrapper for a Tun instance generally makes sense, or one nested stack created by wrapping an existing DetachedTun. Multiple parallel wrappers around the same Tun, or direct use of the wrapped Tun while a wrapper is active, may split reads and writes across consumers and can cause operations to hang. Nested detached Tun wrappers share the first wrapper's pumps, avoiding an extra pump and packet-copy stage per nested layer.

Index

Constants

View Source
const (
	EventUp = 1 << iota
	EventDown
	EventMTUUpdate
)

Variables

View Source
var (
	// ErrDetachedTunDown is returned by Read and Write when the detachable
	// wrapper is down. The wrapped Tun remains open.
	ErrDetachedTunDown = errors.Join(
		os.ErrClosed,
		errors.New("tun: detached wrapper is down"),
	)
	// ErrDetachedTunClosed is returned after the detachable wrapper is closed.
	// The wrapped Tun remains open.
	ErrDetachedTunClosed = errors.Join(
		os.ErrClosed,
		errors.New("tun: detached wrapper is closed"),
	)
)
View Source
var (
	// ErrJoinerClosed is returned after Joiner is closed.
	ErrJoinerClosed = errors.Join(
		os.ErrClosed,
		errors.New("tun: joiner closed"),
	)

	// ErrJoinerSmallOffset is returned when Joiner.Read or Joiner.Write is
	// called with an offset smaller than Joiner's MRO/MWO.
	ErrJoinerSmallOffset = errors.New("tun: joiner offset is too small")

	// ErrJoinerDuplicateTun is returned when a Tun is attached more than once
	// or attached both as the default and as a secondary Tun.
	ErrJoinerDuplicateTun = errors.New("tun: joiner duplicate nested tun")

	// ErrJoinerNilTun is returned when attaching a nil Tun.
	ErrJoinerNilTun = errors.New("tun: joiner nil nested tun")
)
View Source
var (
	// ErrSplitterClosed is returned after Splitter is closed.
	ErrSplitterClosed = errors.Join(
		os.ErrClosed,
		errors.New("tun: splitter closed"),
	)

	// ErrSplitterFrontendClosed is returned after a SplitFrontend is closed.
	ErrSplitterFrontendClosed = errors.Join(
		os.ErrClosed,
		errors.New("tun: splitter frontend closed"),
	)

	// ErrSplitterFrontendDown is returned by SplitFrontend operations while
	// that frontend is down. The Splitter and backend Tun remain open.
	ErrSplitterFrontendDown = errors.Join(
		os.ErrClosed,
		errors.New("tun: splitter frontend is down"),
	)

	// ErrSplitterSmallOffset is returned when SplitFrontend.Read or
	// SplitFrontend.Write is called with an offset smaller than its MRO/MWO.
	ErrSplitterSmallOffset = errors.New(
		"tun: splitter frontend offset is too small",
	)

	// ErrSplitterNilTun is returned when attaching a nil backend Tun.
	ErrSplitterNilTun = errors.New("tun: splitter nil backend tun")
)
View Source
var ErrReadOnClosedChan = errors.Join(
	os.ErrClosed,
	errors.New("tun: read on closed Tun channel"),
)
View Source
var ErrReadOnClosedPipe = errors.Join(
	os.ErrClosed,
	errors.New("tun: read on closed Tun"),
)
View Source
var ErrWriteOnClosedChan = errors.Join(
	os.ErrClosed,
	errors.New("tun: write on closed Tun channel"),
)
View Source
var ErrWriteOnClosedPipe = errors.Join(
	os.ErrClosed,
	errors.New("tun: write on closed Tun"),
)

Functions

func Copy

func Copy(a, b Tun) error

Copy copies packets bidirectionally between two Tun implementations. It uses the batch nature of the Tun interface for optimal performance. Copy blocks until one of the Tuns is closed or encounters an error, then closes both Tuns and returns the first error encountered (if any).

func IsTunTermError added in v0.14.0

func IsTunTermError(err error) bool

IsTunTermError reports whether err should be treated as terminating the current Tun use.

It returns false for nil and for known non-terminal errors where callers can keep using the same Tun, such as temporary errors and capacity errors produced when the read buffer batch is too small. It returns true for all other errors, including closed-device errors.

func Pipe

func Pipe(batch int, mtu, mwo, mro int) (Tun, Tun)

Pipe creates two connected Tun implementations that are bound together via Channel instances. Packets written to one Tun can be read from the other, similar to net.Pipe.

Types

type CallbackTUN added in v0.5.0

type CallbackTUN struct {
	Tun
	OnRead  func(n int, err error)
	OnWrite func(n int, err error)
}

CallbackTUN wraps a Tun interface adding callbacks for read and write operations.

func (*CallbackTUN) IsNative added in v0.14.0

func (t *CallbackTUN) IsNative() bool

func (*CallbackTUN) Read added in v0.5.0

func (t *CallbackTUN) Read(
	bufs [][]byte,
	sizes []int,
	offset int,
) (n int, err error)

func (*CallbackTUN) Write added in v0.5.0

func (t *CallbackTUN) Write(bufs [][]byte, offset int) (n int, err error)

type Channel added in v0.5.0

type Channel struct {
	// contains filtered or unexported fields
}

Channel is a batched communication channel for byte slices that partially implements Tun interface. For bi-directional full Tun implementation see Pipe.

func NewChan added in v0.5.0

func NewChan() *Channel

NewChan builds a new Channel.

func (*Channel) Close added in v0.5.0

func (ch *Channel) Close() (err error)

func (*Channel) Read added in v0.5.0

func (p *Channel) Read(
	bufs [][]byte,
	sizes []int,
	offset int,
) (n int, err error)

func (*Channel) Write added in v0.5.0

func (p *Channel) Write(bufs [][]byte, offset int) (written int, err error)

type DetachedTun added in v0.13.0

type DetachedTun struct {
	// contains filtered or unexported fields
}

DetachedTun is an independently stoppable wrapper around a Tun.

Down and Close affect only the wrapper and do not close the wrapped Tun. Pending Read and Write calls on the wrapper return when the wrapper is taken down or closed. To make that possible for arbitrary Tun implementations, DetachedTun copies packet data at the wrapper boundary and uses internal read/write pumps. If the wrapped Tun blocks without any cancellation support, a pump goroutine may remain blocked inside the wrapped Tun until that wrapped operation completes; caller buffers are not used by those goroutines.

Tun implementations are generally single-consumer/single-producer devices. Creating more than one active DetachedTun for the same underlying Tun, or using the wrapped Tun directly while a DetachedTun is active, can reorder or steal packets just like concurrent direct reads from the same Tun would.

When wrapping another DetachedTun, Detach flattens the data path onto the first wrapper's pumps. Nested wrappers still have independent Down and Close state, but they do not add another pump or another packet-copy stage.

func Detach added in v0.13.0

func Detach(t Tun, pools ...bufpool.Pool) *DetachedTun

Detach creates an independently stoppable wrapper around t. If t is already a DetachedTun, the new wrapper shares t's underlying pumps instead of wrapping the public Read and Write methods again.

func (*DetachedTun) BatchSize added in v0.13.0

func (d *DetachedTun) BatchSize() int

func (*DetachedTun) Close added in v0.13.0

func (d *DetachedTun) Close() error

Close permanently closes this wrapper. It does not close the wrapped Tun.

func (*DetachedTun) Down added in v0.13.0

func (d *DetachedTun) Down() error

Down stops this wrapper and releases pending Read and Write calls. It does not close the wrapped Tun.

func (*DetachedTun) Events added in v0.13.0

func (d *DetachedTun) Events() <-chan Event

func (*DetachedTun) File added in v0.13.0

func (d *DetachedTun) File() *os.File

func (*DetachedTun) IsNative added in v0.14.0

func (d *DetachedTun) IsNative() bool

func (*DetachedTun) IsUp added in v0.13.0

func (d *DetachedTun) IsUp() (bool, error)

IsUp reports whether this wrapper is currently up.

func (*DetachedTun) MRO added in v0.13.0

func (d *DetachedTun) MRO() int

func (*DetachedTun) MTU added in v0.13.0

func (d *DetachedTun) MTU() (int, error)

func (*DetachedTun) MWO added in v0.13.0

func (d *DetachedTun) MWO() int

func (*DetachedTun) Name added in v0.13.0

func (d *DetachedTun) Name() (string, error)

func (*DetachedTun) Read added in v0.13.0

func (d *DetachedTun) Read(
	bufs [][]byte,
	sizes []int,
	offset int,
) (int, error)

func (*DetachedTun) Up added in v0.13.0

func (d *DetachedTun) Up() error

Up re-enables the wrapper. It does not call Up or otherwise mutate the wrapped Tun.

func (*DetachedTun) Write added in v0.13.0

func (d *DetachedTun) Write(bufs [][]byte, offset int) (int, error)

type Event

type Event int

type Forwarder added in v0.5.0

type Forwarder struct {
	// contains filtered or unexported fields
}

Forwarder manages bidirectional forwarding between two TUN devices. It runs two goroutines: one for reading from the read TUN and one for writing to the write TUN. The forwarder can be reconfigured dynamically (e.g., swap TUN devices) without stopping.

For bi-directional forwarding see Point2Point.

func NewForwarder added in v0.5.0

func NewForwarder(pool bufpool.Pool) *Forwarder

NewForwarder creates a new Forwarder with the given buffer pool (can be nil). It starts the reader and writer goroutines. The forwarder initially has no TUN devices and must be configured via SetReadTun and SetWriteTun.

func (*Forwarder) SetReadTun added in v0.5.0

func (f *Forwarder) SetReadTun(tun Tun)

SetReadTun dynamically replaces the TUN device used for reading. The old read TUN (if any) is closed. If the forwarder is stopped, this call does nothing.

func (*Forwarder) SetWriteTun added in v0.5.0

func (f *Forwarder) SetWriteTun(tun Tun)

SetWriteTun dynamically replaces the TUN device used for writing. The old write TUN (if any) is closed. If the forwarder is stopped, this call does nothing.

func (*Forwarder) Stop added in v0.5.0

func (f *Forwarder) Stop()

Stop gracefully shuts down the forwarder. It closes the TUN devices, signals the goroutines to exit, waits for them, and releases all pooled buffers.

type IO

type IO struct {
	Tun
	// contains filtered or unexported fields
}

IO is an io.ReadWriteCloser wrapper for a Tun. It adapts the batch-oriented Tun interface to the single-buffer io.ReadWriteCloser interface, handling one packet at a time.

func NewIO

func NewIO(tun Tun, pool bufpool.Pool) *IO

NewIO creates a new IO wrapper for the given Tun.

func (*IO) Close

func (r *IO) Close() error

Close implements io.Closer. It closes the underlying Device.

func (*IO) Read

func (r *IO) Read(p []byte) (int, error)

Read implements io.Reader. It reads a single packet from the Device.

func (*IO) Write

func (r *IO) Write(p []byte) (int, error)

Write implements io.Writer. It writes a single packet to the Device.

type Joiner added in v0.14.0

type Joiner struct {
	// contains filtered or unexported fields
}

Joiner combines several nested Tuns into one virtual Tun.

Packets read from nested Tuns are emitted by Joiner.Read as a single outgoing stream. Joiner learns IPv4 and IPv6 source addresses from those outgoing packets and later routes Joiner.Write packets to the nested Tun associated with their destination address. Packets with unknown or malformed destinations are routed to the current default Tun; if no default is attached, they are dropped.

Detaching a nested Tun closes it. This is intentional: Tun has no deadline or context parameter, so Close is the only portable way to unblock pending nested Read or Write calls.

func NewJoiner added in v0.14.0

func NewJoiner(pools ...bufpool.Pool) *Joiner

NewJoiner creates an empty Joiner.

func (*Joiner) AttachDefault added in v0.14.0

func (j *Joiner) AttachDefault(t Tun) error

AttachDefault attaches t as the default nested Tun. If another default Tun is already attached, it is detached and closed first.

func (*Joiner) AttachSecondary added in v0.14.0

func (j *Joiner) AttachSecondary(t Tun) error

AttachSecondary attaches t as a non-default nested Tun.

func (*Joiner) BatchSize added in v0.14.0

func (j *Joiner) BatchSize() int

func (*Joiner) Close added in v0.14.0

func (j *Joiner) Close() error

func (*Joiner) Detach added in v0.14.0

func (j *Joiner) Detach(t Tun) error

Detach detaches and closes t if it is currently attached.

func (*Joiner) DetachDefault added in v0.14.0

func (j *Joiner) DetachDefault() error

DetachDefault detaches and closes the current default Tun, if any.

func (*Joiner) Events added in v0.14.0

func (j *Joiner) Events() <-chan Event

func (*Joiner) File added in v0.14.0

func (j *Joiner) File() *os.File

func (*Joiner) IsNative added in v0.14.0

func (j *Joiner) IsNative() bool

func (*Joiner) MRO added in v0.14.0

func (j *Joiner) MRO() int

func (*Joiner) MTU added in v0.14.0

func (j *Joiner) MTU() (int, error)

func (*Joiner) MWO added in v0.14.0

func (j *Joiner) MWO() int

func (*Joiner) Name added in v0.14.0

func (j *Joiner) Name() (string, error)

func (*Joiner) Read added in v0.14.0

func (j *Joiner) Read(bufs [][]byte, sizes []int, offset int) (int, error)

func (*Joiner) Write added in v0.14.0

func (j *Joiner) Write(bufs [][]byte, offset int) (int, error)

type Point2Point added in v0.5.0

type Point2Point struct {
	// contains filtered or unexported fields
}

Point2Point manages bidirectional forwarding between two TUN devices, conventionally named A and B. It can be reconfigured dynamically (e.g., swap TUN devices) without stopping.

func NewP2P added in v0.5.0

func NewP2P(pool bufpool.Pool) *Point2Point

NewP2P creates a new Point2Point instance using the provided buffer pool. Point2Point start with no TUN devices and must be configured using SetA and SetB.

func (*Point2Point) SetA added in v0.5.0

func (p *Point2Point) SetA(tun Tun)

SetA configures the given TUN device as endpoint A. Endpoint A will be used for reading packets to send to B, and will also receive packets coming from B (i.e., writes from B to A are written to this TUN).

func (*Point2Point) SetB added in v0.5.0

func (p *Point2Point) SetB(tun Tun)

SetB configures the given TUN device as endpoint B. Endpoint B will be used for reading packets to send to A, and will also receive packets coming from A (i.e., writes from A to B are written to this TUN).

func (*Point2Point) Stop added in v0.5.0

func (p *Point2Point) Stop()

Stop gracefully shuts down both internal forwarders, releasing all resources and waiting for goroutines to finish.

type SplitFrontend added in v0.14.0

type SplitFrontend struct {
	// contains filtered or unexported fields
}

SplitFrontend is one virtual Tun produced by Splitter.Get.

Closing or taking a frontend down affects only that frontend. Packets routed to a closed, down, or never-created frontend are dropped.

func (*SplitFrontend) BatchSize added in v0.14.0

func (f *SplitFrontend) BatchSize() int

func (*SplitFrontend) Close added in v0.14.0

func (f *SplitFrontend) Close() error

func (*SplitFrontend) Down added in v0.14.0

func (f *SplitFrontend) Down() error

Down stops this frontend and releases pending Read and Write calls. It does not affect the Splitter or backend Tun.

func (*SplitFrontend) Events added in v0.14.0

func (f *SplitFrontend) Events() <-chan Event

func (*SplitFrontend) File added in v0.14.0

func (f *SplitFrontend) File() *os.File

func (*SplitFrontend) IsNative added in v0.14.0

func (f *SplitFrontend) IsNative() bool

func (*SplitFrontend) IsUp added in v0.14.0

func (f *SplitFrontend) IsUp() (bool, error)

IsUp reports whether this frontend is currently up.

func (*SplitFrontend) MRO added in v0.14.0

func (f *SplitFrontend) MRO() int

func (*SplitFrontend) MTU added in v0.14.0

func (f *SplitFrontend) MTU() (int, error)

func (*SplitFrontend) MWO added in v0.14.0

func (f *SplitFrontend) MWO() int

func (*SplitFrontend) Name added in v0.14.0

func (f *SplitFrontend) Name() (string, error)

func (*SplitFrontend) Read added in v0.14.0

func (f *SplitFrontend) Read(
	bufs [][]byte,
	sizes []int,
	offset int,
) (int, error)

func (*SplitFrontend) Up added in v0.14.0

func (f *SplitFrontend) Up() error

Up re-enables this frontend.

func (*SplitFrontend) Write added in v0.14.0

func (f *SplitFrontend) Write(bufs [][]byte, offset int) (int, error)

type SplitRouter added in v0.14.0

type SplitRouter interface {
	Lock()
	Unlock()
	Route(buf []byte, offset int, isNative bool) int
}

SplitRouter selects the SplitFrontend that should receive a packet read from the backend Tun.

Splitter calls Lock once per backend read batch, calls Route for each packet in that batch, then calls Unlock. Route receives the packet buffer and offset as returned by the backend Tun, plus the backend's IsNative value. It must return a frontend index in the inclusive range 1..16. Any other value drops the packet. A nil router routes every packet to frontend 1.

type Splitter added in v0.14.0

type Splitter struct {
	// contains filtered or unexported fields
}

Splitter fans one backend Tun out to up to 16 virtual Tun frontends.

Backend reads are routed to frontends through the current SplitRouter. Writes from any active frontend are forwarded to the current backend. The backend can be attached, detached, or replaced at runtime; detaching closes the backend to unblock in-flight backend I/O.

func NewSplitter added in v0.14.0

func NewSplitter(pools ...bufpool.Pool) *Splitter

NewSplitter creates a Splitter with no attached backend.

func (*Splitter) Attach added in v0.14.0

func (s *Splitter) Attach(t Tun) error

Attach replaces the current backend Tun with t. The previous backend, if any, is detached and closed.

func (*Splitter) Close added in v0.14.0

func (s *Splitter) Close() error

Close closes the Splitter, all frontends, and the current backend.

func (*Splitter) Detach added in v0.14.0

func (s *Splitter) Detach() error

Detach removes and closes the current backend Tun, if any.

func (*Splitter) Get added in v0.14.0

func (s *Splitter) Get(index int) *SplitFrontend

Get returns a new frontend for index. Valid indexes are 1 through 16. If a previous frontend exists for that index, it is closed first.

func (*Splitter) RemoveRouter added in v0.14.0

func (s *Splitter) RemoveRouter()

RemoveRouter removes the current SplitRouter.

func (*Splitter) ResetRouter added in v0.14.0

func (s *Splitter) ResetRouter(r SplitRouter)

ResetRouter replaces the current SplitRouter with r.

func (*Splitter) SetRouter added in v0.14.0

func (s *Splitter) SetRouter(r SplitRouter)

SetRouter installs r as the current packet router. Passing nil removes the router and restores the default route to frontend 1.

type Tun

type Tun interface {
	// File returns the file descriptor of the tun device.
	// It may be nil for virtual/mock/etc implementations.
	File() *os.File

	// IsNative reports whether this Tun provides direct access to an OS TUN
	// device. Consumers may use low-level optimized operations bypassing Tun's
	// methods only when IsNative returns true.
	IsNative() bool

	// Read a batch of packets from Tun.
	// If original source (e.g. linux tun interface) ruturn additional headers,
	// they are stripped under the hood.
	// On a successful read it returns the number of packets read, and sets
	// packet lengths within the sizes slice. len(sizes) must be >= len(bufs).
	// Callers must size bufs from the source Tun's BatchSize(); a single Read
	// may yield multiple logical packets, and some native TUN implementations
	// can require multiple buffers even for one inbound frame.
	// A nonzero offset can be used to instruct the Tun on where to begin
	// reading into each element of the bufs slice.
	// If Read returns a non-terminal error, callers may retry using the same
	// Tun. If it returns a terminal error as reported by IsTunTermError,
	// callers should stop using this Tun instance for the current data path.
	Read(bufs [][]byte, sizes []int, offset int) (n int, err error)

	// Write one or more packets to the tun (without any additional headers).
	// On a successful write it returns the number of packets written. A nonzero
	// offset can be used to instruct the Device on where to begin writing from
	// each packet contained within the bufs slice. Callers must chunk writes
	// using the destination Tun's BatchSize() and handle partial writes.
	// After Close, Write should return an error matching os.ErrClosed.
	Write(bufs [][]byte, offset int) (int, error)

	// MWO stands for Minimal Write Offset.
	// It is typically used by native tun implementations to reserver space for
	// OS specific headers.
	MWO() int

	// MRO stands for Minimal Read Offset.
	// It isn't used anywhere at the moment but added for future use.
	MRO() int

	// MTU returns the MTU of the Device.
	MTU() (int, error)

	// Name returns the current name of the Device.
	Name() (string, error)

	// Events returns a channel of type Event, which is fed Device events.
	// EventDown means the interface is down, not that the Tun is closed. The
	// channel is closed when the Tun is closed.
	Events() <-chan Event

	// Close permanently stops the Device and closes the Event channel. After
	// Close, Read and Write should return errors matching os.ErrClosed.
	Close() error

	// BatchSize returns the preferred/max number of packets that this Tun can
	// read or write in a single read/write call. BatchSize must not change over
	// the lifetime of a Device. Callers must not assume symmetric batch
	// compatibility across two different Tun implementations: reads should be
	// sized from the source Tun, and writes should be chunked for the
	// destination Tun.
	BatchSize() int
}

Tun interface is borrowed from wireguard-go. There is multiple projects that use same or similar interfaces so it is a good choice for a de-facto standard role.

A native Tun provides direct access to an OS TUN device. Native Tuns may expose an OS file descriptor through File, may reserve MRO/MWO space for platform headers, and may be used by consumers that intentionally bypass Tun methods for low-level optimized operations. A virtual Tun emulates or wraps packet transport in user space and must report IsNative as false. Simple wrappers around a native Tun may preserve native status only when bypassing their Tun methods would not bypass required wrapper behavior.

Implementations should distinguish a down interface from a closed Tun. A down interface is reported with EventDown and may later report EventUp; the Tun remains open, and Read or Write calls may keep blocking or may keep returning packets according to the underlying implementation. Close is permanent: implementations should unblock pending Read and Write calls when possible, close the Events channel, and make future I/O fail with an error that matches os.ErrClosed.

Read can also return non-terminal errors after which the same Tun can still be used. Known examples from native and third-party implementations include temporary errors and capacity errors such as "too many segments" or "need more buffers" when the caller supplied too few read buffers. IsTunTermError classifies errors for callers that need to decide whether to stop using a Tun after a Read error.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL