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
- Variables
- func DeriveSecretKey(localPub, remotePub []byte) [32]byte
- func TLSConfigFor(leaf LoadedCert, rootCAs *x509.CertPool, serverName string) (*tls.Config, error)
- type GossipMessage
- type Identity
- func (id *Identity) CACert() *x509.Certificate
- func (id *Identity) CACertPEM() []byte
- func (id *Identity) CAKeyPEM() []byte
- func (id *Identity) InstallID() string
- func (id *Identity) IssueLinkCert(commonName string) (LoadedCert, error)
- func (id *Identity) IssueLinkCertForContainer(containerID string) (LoadedCert, error)
- func (id *Identity) PeerKeyFile(peerID string) string
- func (id *Identity) PrivateKey() ed25519.PrivateKey
- func (id *Identity) PublicKey() ed25519.PublicKey
- func (id *Identity) PublicKeyPEM() string
- func (id *Identity) ShortID() string
- func (id *Identity) Sign(msg []byte) []byte
- func (id *Identity) Verify(msg, sig []byte) bool
- type LoadedCert
- type MDNSEntry
- type MDNSService
- type Mesh
- type MeshConfig
- type Peer
- type PeerSnapshot
- type RemoteContainer
- type SecretboxWrapper
- func (s *SecretboxWrapper) UnwrapInbound(payload []byte) ([]byte, error)
- func (s *SecretboxWrapper) WrapClient(_ context.Context, c net.Conn) (net.Conn, error)
- func (s *SecretboxWrapper) WrapOutbound(payload []byte) ([]byte, error)
- func (s *SecretboxWrapper) WrapServer(_ context.Context, c net.Conn) (net.Conn, error)
- type StaticPeers
- type StaticPeersConfig
- type TCPProxy
- type TCPProxyConfig
- type TLSWrapper
- type TransportWrapper
- type TrustStore
- func (ts *TrustStore) CACert(peerID string) *x509.Certificate
- func (ts *TrustStore) ListIDs() []string
- func (ts *TrustStore) Load() error
- func (ts *TrustStore) PubKey(peerID string) (ed25519.PublicKey, error)
- func (ts *TrustStore) Trust(peerID string, pub ed25519.PublicKey, ca *x509.Certificate) error
- func (ts *TrustStore) Trusted(peerID string) bool
- type UDPProxy
- type UDPProxyConfig
Constants ¶
const MaxGossipMessageBytes = 4 * 1024
MaxGossipMessageBytes is the on-wire size cap.
Variables ¶
var ErrIdentityNotLoaded = errors.New("netlink: identity not loaded")
ErrIdentityNotLoaded is returned by NewIdentity when generation failed.
Functions ¶
func DeriveSecretKey ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) PublicKeyPEM ¶
PublicKeyPEM returns the Ed25519 public key in PEM format for export.
func (*Identity) ShortID ¶
ShortID returns the short install identifier derived from the Ed25519 public key. Stable for the lifetime of the keys on disk.
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, _ int, _ *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(_ 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.
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 ¶
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 ¶
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 ¶
Addr returns the bound listen address. Useful when ListenAddr contained port 0 (OS-assigned). Returns empty string if Start has not succeeded.
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, error)
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 ¶
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 ¶
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.