netlink

package
v0.9.3 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package netlink implements DokiLink-Lite: a pure-Go network proxy and mesh layer that replaces external tools (socat, slirp4netns, pasta) for container port forwarding in environments where they are not available (notably Termux on Android, which lacks network namespaces and iptables).

DokiLink-Lite is intentionally minimal:

  • TCPProxy / UDPProxy: bidirectional byte-stream proxies using only the Go standard library. No gVisor, no WireGuard, no netstack.
  • Optional TLS 1.3 wrapping (see crypto.go).
  • Optional NaCl secretbox payload encryption (see crypto.go).
  • Optional LAN-only mesh via mDNS or static peer JSON (see mesh.go).

The package is designed to be safe to import on every platform: when the caller does not invoke any of the proxy/mesh entry points, no network resources are opened.

Index

Constants

View Source
const MaxGossipMessageBytes = 4 * 1024

MaxGossipMessageBytes is the on-wire size cap.

Variables

View Source
var ErrIdentityNotLoaded = errors.New("netlink: identity not loaded")

ErrIdentityNotLoaded is returned by NewIdentity when generation failed.

Functions

func DeriveSecretKey

func DeriveSecretKey(localPub, remotePub []byte) [32]byte

DeriveSecretKey derives a 32-byte symmetric key from two Ed25519 public keys. The order matters: peers MUST call this with their own public key first, then the remote peer's, to get the same key.

func TLSConfigFor

func TLSConfigFor(leaf LoadedCert, rootCAs *x509.CertPool, serverName string) (*tls.Config, error)

TLSConfigFor builds a *tls.Config ready to drop into tls.NewListener (server) or tls.Dial (client). It converts a LoadedCert into the cert slice tls.Config expects.

Types

type GossipMessage

type GossipMessage struct {
	Type      string           `json:"type"` // "hello", "peer", "container"
	From      string           `json:"from"` // sender short id
	Time      int64            `json:"time"` // unix nanos
	Nonce     string           `json:"nonce,omitempty"`
	Peers     []PeerSnapshot   `json:"peers,omitempty"`
	Container *RemoteContainer `json:"container,omitempty"`
	Signature string           `json:"sig"` // base64 ed25519 sig over canonical JSON
}

GossipMessage is the wire format for inter-peer gossip. Every message is JSON, ed25519-signed, and capped at 4 KiB. Larger messages are truncated by the sender.

func (*GossipMessage) Sign

func (m *GossipMessage) Sign(priv []byte) error

Sign signs the message in-place using priv. The signature is base64.

func (*GossipMessage) Verify

func (m *GossipMessage) Verify(pub []byte) error

Verify checks that the message was signed by the holder of pub.

type Identity

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

Identity holds the per-Doki-install cryptographic identity used by the mesh layer. It consists of:

  • An Ed25519 keypair (identity), used to sign mesh messages (HELLO, ADVERTISE, REVOKE, BYE). The private key never leaves the host. The public key is broadcast as the peer "fingerprint".
  • A self-signed ECDSA P-256 certificate authority (CA), used to mint per-link TLS certificates on demand. The CA private key never leaves the host.
  • Zero or more per-link ECDSA certificates, lazily generated and cached on disk at keys/<id>.crt / keys/<id>.key.

The Identity is loaded (or generated) at daemon startup. Key files are stored with 0600 permissions. Loading is safe to call multiple times; generation is gated by sync.Once.

func NewIdentity

func NewIdentity(root string) (*Identity, error)

NewIdentity returns the Doki install identity, loading from disk or generating fresh material if absent. root is the keys directory (typically $DOKI_ROOT/keys).

func (*Identity) CACert

func (id *Identity) CACert() *x509.Certificate

CACert returns the self-signed CA certificate.

func (*Identity) CACertPEM

func (id *Identity) CACertPEM() []byte

CACertPEM returns the CA certificate in PEM form, suitable for use as tls.Config.RootCAs or for inclusion in a tls.Config.Certificates pair.

func (*Identity) CAKeyPEM

func (id *Identity) CAKeyPEM() []byte

CAKeyPEM returns the CA private key in PEM form. NEVER ship this over the wire; it is exposed here only for the local TLS wrapper to load into crypto/tls.

func (*Identity) InstallID

func (id *Identity) InstallID() string

InstallID returns a short, stable, URL-safe identifier derived from the Ed25519 public key (first 16 chars of base32). It is safe to log.

func (*Identity) IssueLinkCert

func (id *Identity) IssueLinkCert(commonName string) (LoadedCert, error)

IssueLinkCert generates a fresh ECDSA P-256 leaf certificate signed by the CA. commonName is the link/peer identifier (e.g. the remote peer's install ID or a container name).

The returned LoadedCert is the minimal subset of tls.Certificate we need: a parsed leaf plus DER-encoded key. The TLS wrapper (crypto.go) turns it into a real crypto/tls.Certificate.

func (*Identity) IssueLinkCertForContainer

func (id *Identity) IssueLinkCertForContainer(containerID string) (LoadedCert, error)

IssueLinkCertForContainer is a convenience wrapper for the typical "generate a cert for a container's exposed port" use case.

func (*Identity) PeerKeyFile

func (id *Identity) PeerKeyFile(peerID string) string

PeerKeyFile returns the on-disk path for a peer's pinned public key. Used by the mesh layer for trust-on-first-use storage.

func (*Identity) PrivateKey

func (id *Identity) PrivateKey() ed25519.PrivateKey

PrivateKey returns the Ed25519 private key bytes (64). Callers must not mutate the returned slice. Only used for signing in this package's mesh code.

func (*Identity) PublicKey

func (id *Identity) PublicKey() ed25519.PublicKey

PublicKey returns the raw Ed25519 public key bytes.

func (*Identity) PublicKeyPEM

func (id *Identity) PublicKeyPEM() string

PublicKeyPEM returns the Ed25519 public key in PEM format for export.

func (*Identity) ShortID

func (id *Identity) ShortID() string

ShortID returns the short install identifier derived from the Ed25519 public key. Stable for the lifetime of the keys on disk.

func (*Identity) Sign

func (id *Identity) Sign(msg []byte) []byte

Sign returns the Ed25519 signature of msg.

func (*Identity) Verify

func (id *Identity) Verify(msg, sig []byte) bool

Verify verifies an Ed25519 signature made by this identity. Used in tests; mesh code calls ed25519.Verify directly with the remote peer's public key.

type LoadedCert

type LoadedCert struct {
	Leaf   *x509.Certificate
	KeyDER []byte
}

LoadedCert is the minimal subset of tls.Certificate we need: a parsed leaf certificate plus the matching private key in DER. We avoid importing crypto/tls here to keep the keys file portable; the TLS wrapper (crypto.go) does the conversion.

type MDNSEntry

type MDNSEntry struct {
	ID     string    `json:"id"`
	Addr   string    `json:"addr"`
	Port   int       `json:"port"`
	PubKey string    `json:"pubkey"`
	Source string    `json:"source"`
	Seen   time.Time `json:"seen"`
}

MDNSEntry describes a peer found via mDNS. The struct lives in a build-tag-neutral file so that the stub and full implementations share the same return type.

type MDNSService

type MDNSService struct{}

MDNSService is the build-tag stub for installations built without the netlink_mdns tag. All methods are no-ops.

func NewMDNSService

func NewMDNSService(identity *Identity, port int, logger *slog.Logger) *MDNSService

NewMDNSService returns a disabled service. The full implementation is gated behind the `netlink_mdns` build tag and depends on github.com/hashicorp/mdns.

func (*MDNSService) Entries

func (m *MDNSService) Entries() []MDNSEntry

Entries always returns an empty slice.

func (*MDNSService) Start

func (m *MDNSService) Start(ctx context.Context) error

Start is a no-op when mDNS is disabled at build time.

func (*MDNSService) Stop

func (m *MDNSService) Stop() error

Stop is a no-op when mDNS is disabled at build time.

type Mesh

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

Mesh wires together identity, trust, discovery, and a TCP listener that exchanges gossip with peers. It is the single entry point for the daemon to start participating in a DokiLink-Lite mesh.

func NewMesh

func NewMesh(cfg MeshConfig) (*Mesh, error)

NewMesh constructs a Mesh. The identity is required; everything else is optional (defaulted). The mesh does not listen until Start().

func (*Mesh) AnnounceContainer

func (m *Mesh) AnnounceContainer(c RemoteContainer) error

AnnounceContainer publishes a container descriptor to all peers.

func (*Mesh) Peer

func (m *Mesh) Peer(id string) *Peer

Peer returns a single peer by ID.

func (*Mesh) Peers

func (m *Mesh) Peers() []*Peer

Peers returns a snapshot of currently known peers.

func (*Mesh) SendHello

func (m *Mesh) SendHello(addr string) error

SendHello sends a hello message to one peer, returning any error.

func (*Mesh) Start

func (m *Mesh) Start(ctx context.Context) error

Start opens the gossip listener and begins the gossip loop. The returned context is cancelled on Stop.

func (*Mesh) Stop

func (m *Mesh) Stop() error

Stop closes the listener, mDNS, and stops the gossip loop.

type MeshConfig

type MeshConfig struct {
	Identity   *Identity
	Trust      *TrustStore
	Static     *StaticPeers
	ListenAddr string // host:port to listen for gossip, e.g. ":7432"
	Logger     *slog.Logger
	// EnableMDNS requires the netlink_mdns build tag. With the default
	// build the mDNS service is a no-op stub.
	EnableMDNS bool
}

MeshConfig configures a Mesh.

type Peer

type Peer struct {
	ID         string            `json:"id"`      // short install id
	Name       string            `json:"name"`    // human-friendly (defaults to ID)
	Addr       string            `json:"addr"`    // host:port the peer listens on
	PubKey     string            `json:"pubkey"`  // base64 of Ed25519 public key
	CACert     string            `json:"ca_cert"` // PEM-encoded CA cert
	LastSeen   time.Time         `json:"last_seen"`
	Containers []RemoteContainer `json:"containers,omitempty"`
}

Peer represents a remote Doki instance participating in the mesh. Each peer is uniquely identified by its short install ID (the first 12 chars of base32 of its Ed25519 public key).

type PeerSnapshot

type PeerSnapshot struct {
	ID     string `json:"id"`
	Addr   string `json:"addr"`
	PubKey string `json:"pubkey"`
}

PeerSnapshot is a compact, wire-friendly peer record. It is what gets gossiped; full Peer objects (with .Containers) live only in local memory.

type RemoteContainer

type RemoteContainer struct {
	Name     string `json:"name"`
	ID       string `json:"id"`
	IP       string `json:"ip"`
	Port     uint16 `json:"port"`
	Proto    string `json:"proto"`
	HostPort uint16 `json:"host_port,omitempty"`
}

RemoteContainer describes a container running on a peer that we can reach through the mesh.

type SecretboxWrapper

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

SecretboxWrapper adds a NaCl secretbox layer on top of whatever transport (TCP or UDP) is being wrapped. It uses a single 32-byte key shared by both peers; that key is derived from the Ed25519 identities of the two endpoints (see DeriveSecretKey).

On TCP, the wrapper applies a per-connection nonce (24 bytes secretbox.NonceSize + 16 bytes secretbox.Overhead) and uses a 4-byte big-endian length prefix per frame. On UDP, it uses the standard nacl/secretbox.Seal/Open API with a per-datagram nonce derived from the message counter.

SecretboxWrapper implements both TransportWrapper (for TCP) and udpWrapper (for UDP).

func NewSecretboxWrapper

func NewSecretboxWrapper(key [32]byte) *SecretboxWrapper

NewSecretboxWrapper builds a wrapper from a 32-byte shared key. Use DeriveSecretKey to compute it from peer identities.

func (*SecretboxWrapper) UnwrapInbound

func (s *SecretboxWrapper) UnwrapInbound(payload []byte) ([]byte, error)

UnwrapInbound reverses WrapOutbound.

func (*SecretboxWrapper) WrapClient

func (s *SecretboxWrapper) WrapClient(ctx context.Context, c net.Conn) (net.Conn, error)

WrapClient mirrors WrapServer; the stream conn is symmetric.

func (*SecretboxWrapper) WrapOutbound

func (s *SecretboxWrapper) WrapOutbound(payload []byte) ([]byte, error)

WrapOutbound / UnwrapInbound for UDP: each datagram is one secretbox.Seal'd payload with its own nonce.

func (*SecretboxWrapper) WrapServer

func (s *SecretboxWrapper) WrapServer(ctx context.Context, c net.Conn) (net.Conn, error)

WrapServer / WrapClient for TCP: wrap the conn in a framed secretbox stream that prepends a 4-byte big-endian length to each encrypted frame. The frame payload itself is:

nonce (24 bytes) || secretbox(plaintext)

type StaticPeers

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

StaticPeers is an in-memory view of $DOKI_ROOT/mesh/peers.json with safe concurrent access and atomic file writes.

func NewStaticPeers

func NewStaticPeers(path string) (*StaticPeers, error)

NewStaticPeers loads (or creates) the static peers file at path.

func (*StaticPeers) Add

func (sp *StaticPeers) Add(p *Peer) error

Add inserts or updates a peer, normalising fields, and persists the file.

func (*StaticPeers) Get

func (sp *StaticPeers) Get(id string) *Peer

Get returns a single peer by ID, or nil.

func (*StaticPeers) List

func (sp *StaticPeers) List() []*Peer

List returns a copy of the static peer list.

func (*StaticPeers) Remove

func (sp *StaticPeers) Remove(id string) error

Remove deletes a peer by ID and persists the file.

func (*StaticPeers) Touch

func (sp *StaticPeers) Touch(id string)

Touch updates LastSeen to time.Now() for the given peer ID.

type StaticPeersConfig

type StaticPeersConfig struct {
	Peers []*Peer `json:"peers"`
}

StaticPeersConfig is the on-disk representation of manually configured peers. The file is $DOKI_ROOT/mesh/peers.json and can be edited by hand or by `doki link add <name> <addr> --pub <b64>`.

type TCPProxy

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

TCPProxy is a TCP byte-stream proxy. It listens on a host address and, for each accepted client connection, dials the upstream (container) address and pumps bytes in both directions until either side closes.

func NewTCPProxy

func NewTCPProxy(cfg TCPProxyConfig) *TCPProxy

NewTCPProxy creates a TCPProxy in the "configured but not listening" state. Call Start to bind the listener and begin accepting connections.

func (*TCPProxy) Addr

func (p *TCPProxy) Addr() string

Addr returns the bound listen address. Useful when ListenAddr contained port 0 (OS-assigned). Returns empty string if Start has not succeeded.

func (*TCPProxy) Close

func (p *TCPProxy) Close() error

Close stops accepting new connections and terminates all active connections. Safe to call multiple times; subsequent calls are no-ops.

func (*TCPProxy) Start

func (p *TCPProxy) Start(ctx context.Context) error

Start binds the listener and spawns the accept loop. Returns an error if the listen address is invalid or already in use.

type TCPProxyConfig

type TCPProxyConfig struct {
	ListenAddr  string           // e.g. "0.0.0.0:8888" or "127.0.0.1:0"
	Upstream    string           // e.g. "127.0.0.1:80"
	Wrap        TransportWrapper // optional
	IdleTimeout time.Duration    // 0 = no timeout
	Logger      *slog.Logger     // nil = slog.Default()
}

TCPProxyConfig configures a TCPProxy.

type TLSWrapper

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

TLSWrapper wraps a net.Conn with TLS 1.3. It is configured once at construction and then applied to every wrapped connection.

func NewTLSWrapper

func NewTLSWrapper(cfg *tls.Config) *TLSWrapper

NewTLSWrapper builds a wrapper from a *tls.Config. The same config is used for both WrapServer and WrapClient; whether the resulting conn behaves as server or client is decided by the handshake.

func (*TLSWrapper) WrapClient

func (t *TLSWrapper) WrapClient(ctx context.Context, c net.Conn) (net.Conn, error)

WrapClient wraps the client-side (container/upstream) of a connection. The returned net.Conn has completed a TLS handshake by the time the function returns.

func (*TLSWrapper) WrapServer

func (t *TLSWrapper) WrapServer(ctx context.Context, c net.Conn) (net.Conn, error)

WrapServer wraps the server-side (host) of a connection. The returned net.Conn has completed a TLS handshake by the time the function returns (or has returned an error).

type TransportWrapper

type TransportWrapper interface {
	// WrapServer wraps the server-side (host) of a connection. If the
	// implementation performs a handshake (e.g. TLS), it MUST respect
	// ctx.Done().
	WrapServer(ctx context.Context, c net.Conn) (net.Conn, error)
	// WrapClient wraps the client-side (container/upstream) of a connection.
	WrapClient(ctx context.Context, c net.Conn) (net.Conn, error)
}

TransportWrapper optionally wraps a net.Conn to add encryption or authentication. It must be safe to call from multiple goroutines.

type TrustStore

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

TrustStore persists known peer public keys and CA certs. Trust is established on first contact (TOFU) and then verified on every subsequent message.

func NewTrustStore

func NewTrustStore(dir string) (*TrustStore, error)

NewTrustStore creates a TrustStore rooted at dir. The directory is created if it does not exist.

func (*TrustStore) CACert

func (ts *TrustStore) CACert(peerID string) *x509.Certificate

CACert returns the trusted CA cert for peerID, or nil if none was stored.

func (*TrustStore) ListIDs

func (ts *TrustStore) ListIDs() []string

ListIDs returns all trusted peer IDs.

func (*TrustStore) Load

func (ts *TrustStore) Load() error

Load reads all .pub.pem and .ca.pem files from the trust store directory. Called at startup.

func (*TrustStore) PubKey

func (ts *TrustStore) PubKey(peerID string) (ed25519.PublicKey, error)

PubKey returns the trusted public key for peerID, or an error.

func (*TrustStore) Trust

func (ts *TrustStore) Trust(peerID string, pub ed25519.PublicKey, ca *x509.Certificate) error

Trust records a peer's public key and (optionally) CA cert. If the peer is already trusted, the existing record is returned unless the new pubkey differs (in which case an error is returned to surface the potential MITM or key-rotation mismatch).

func (*TrustStore) Trusted

func (ts *TrustStore) Trusted(peerID string) bool

Trusted returns true if peerID has been recorded in the store.

type UDPProxy

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

UDPProxy is a stateless UDP packet forwarder plus a small session tracker for return traffic. Each incoming datagram from the host side is forwarded to the upstream address, and each datagram from the upstream is forwarded back to the most-recent sender for the matching session.

Unlike TCP, UDP is connectionless: we do not maintain a 1:1 proxy connection. Instead, we track sessions by (hostAddr, upstreamAddr) pair and time them out after sessionTimeout of inactivity.

func NewUDPProxy

func NewUDPProxy(cfg UDPProxyConfig) *UDPProxy

NewUDPProxy creates a UDPProxy in the "configured but not listening" state. Call Start to bind the socket and begin forwarding.

func (*UDPProxy) Addr

func (p *UDPProxy) Addr() string

Addr returns the bound listen address.

func (*UDPProxy) Close

func (p *UDPProxy) Close() error

Close stops forwarding and closes all sockets. Safe to call multiple times; subsequent calls are no-ops.

func (*UDPProxy) Start

func (p *UDPProxy) Start(ctx context.Context) error

Start binds the UDP socket and spawns the forwarder goroutines.

type UDPProxyConfig

type UDPProxyConfig struct {
	ListenAddr     string
	Upstream       string
	Wrap           TransportWrapper
	SessionTimeout time.Duration // default 30s
	MaxSessions    int           // default 1024
	ReadBufSize    int           // default 65535
	Logger         *slog.Logger
}

UDPProxyConfig configures a UDPProxy.

Jump to

Keyboard shortcuts

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