p2p

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 2 Imported by: 0

README

p2p

Go Reference

English · 简体中文

Peer-to-peer tunnel host for GOST's p2p plugin protocol, usable either as a standalone binary or as a Go library embedded in another process (see In-process embedding). It lets GOST establish the network path to a chain node through a tunnel opened by this host — the traversal strategy (rendezvous, relay, hole punching) is entirely up to the plugin, and GOST only ever sees a plain byte stream (the Tunnel gRPC stream) to carry its protocol over.

Status: stub + mux + DERP relay + STUN/UDP hole punching + datagram links. The host bridges tunnels with a local TCP forward, either directly (stub mode, loopback) or through a DERP relay (engine mode, cross-machine/NAT). In engine mode, after a relay session is up, both peers probe their NAT via STUN and punch a UDP hole; once the direct path (KCP + smux) is established, new tunnels flow over it while the relay stays up as fallback. Inner dialers tcp/tls/ws/mtcp/mtls/mws/udp are supported — the mux family reuses one tunnel as a multiplexed session, and udp asks for a datagram stream instead of a byte stream (this is what carries a tun link: GOST owns the device, this host is only the pipe).

How it works

GOST client ──OpenTunnel(peer, network)──▶ p2p host (gRPC, :8003)
GOST client ◀──{ok, id}────────────────────  p2p host
GOST client ══Tunnel stream ("id" key)════▶ p2p host ──bridge──▶ peer
  • peer is opaque: its semantics are defined by the plugin (a base64 public key in DERP mode, a plain host:port in stub mode).
  • OpenTunnel only authorizes the tunnel and returns a cryptographically random, single-use id. The data rides the Tunnel gRPC stream bound to that id (sent as the id metadata key) — there is no local endpoint to dial, so a firewall between GOST and this host can't block the data path. Closing the stream closes the tunnel; the stream's lifetime is the tunnel's lifetime.
  • The GOST-side wiring (tunnel dialer, config) lives in go-gost/x (x/p2p/); the wire contract lives in go-gost/plugin (p2p/proto).

Positioning: a generic P2P connectivity primitive

p2p is a P2P connectivity layer, not a turnkey secure tunnel. Its contract is deliberately narrow:

Give me a peer public key, get back a TCP tunnel; NAT traversal is best-effort (STUN + UDP hole punching with a relay fallback); end-to-end reachability is this layer's job, security is the caller's.

It provides reachability, not policy — the same layering as IP/TCP:

  • Encryption is out of scope by design. The relay and hole-punched transports carry plaintext (matching the relay's trust model: it can observe but never decrypt). Confidentiality belongs to the layer above — run tls/mtls/wss over the tunnel, exactly as the GOST inner dialers do. This is the standard connectivity/security layering, not a gap.
  • Peer discovery is an enhancement, not a requirement. Peers are addressed by base64 curve25519 public key — a complete addressing scheme. Name→key lookup is intentionally not built in (see Roadmap).
  • A relay is inherent to NAT traversal. Cross-NAT reachability without a rendezvous is impossible; derper is a deployment/infrastructure choice, not a design flaw. Symmetric-NAT peers stay on relay permanently.

What an integrator must supply: a relay (self-hosted derper or a third-party DERP), the public keys of the peers to reach, and — if confidentiality is required — its own encryption above the tunnel.

Quick start

go build -o p2p ./cmd/p2p
./p2p --addr 127.0.0.1:8003
Flag Default Meaning
-C (empty) config file (YAML); config values are defaults, explicitly-set flags override
--addr 127.0.0.1:8003 gRPC control-plane listen address
--token (empty) control-plane auth token; empty disables checking
--derp (empty) DERP relay URL (wss://host/derp); enables engine mode
--key $XDG_CONFIG_HOME/p2p/key-v1 curve25519 private key file (hex); created if missing
--target (empty) inbound bridge target (repeatable; bare "host:port" feeds the tcp pool, "udp://host:port" the udp pool; DERP mode)
--forward (empty) static port forward "listen-addr=peer-key" (repeatable; DERP mode)
--stun (empty) STUN server (host:port) for the IPv4 direct path; IPv6 direct works without STUN
--direct true attempt a direct (hole-punched) path; false forces relay-only
--tls.secure true verify the relay's TLS certificate (false to trust any cert)
--tls.caFile (empty) PEM CA file to trust the relay's self-signed certificate
--log.level info log level: trace, debug, info, warn, error, fatal
--log.format json log format: json or text
--log.output stderr log output: stderr, stdout, none, or a file path (size-rotates at 100 MB)

Configuration file

Every flag can live in a YAML config file instead (gost-style -C): a config value is the default, and an explicitly-set flag overrides it. --forward flags and the config forwards list are additive.

The file is read by the CLI (cmd/p2p); the library has no file reader, and addr, token and log are the CLI's own keys — a p2p.Config has no such fields (they are deployment settings, not endpoint settings).

./p2p -C p2p.yaml
addr: 127.0.0.1:8003
token: gost
derp: wss://derp.example.com/derp
key: peer.key
target: 127.0.0.1:18080
stun: stun.example.com:3478
tls:
  secure: false
  caFile: /etc/p2p/ca.pem
log:
  level: debug
  format: text
  output: /var/log/p2p.log
  rotation:
    maxSize: 50
    maxAge: 7
    maxBackups: 3
    localTime: true
    compress: true
forwards:
  - listen: 127.0.0.1:18080
    peer: <peerB-key>

Point a GOST chain node at it:

p2ps:
  - name: p2p-1
    plugin:
      type: grpc
      addr: 127.0.0.1:8003
      token: gost                       # matches the host's --token

chains:
  - name: chain-0
    hops:
      - name: hop-0
        nodes:
          - name: node-0
            addr: 192.168.1.10:8080   # the "peer" this stub bridges to
            connector:
              type: http              # connector follows the peer's protocol
            dialer:
              type: tcp
            metadata:
              p2p: p2p-1              # this node's base path goes through the plugin

In-process embedding

p2p is a plain Go library — the CLI is only a flag/config front end over it. An application can embed an endpoint in its own process instead of running this binary next to it: no subprocess, no loopback gRPC control plane, no auth token. The data plane is the same one the gRPC transport uses (both run the same serveTunnel; only the stream carrier differs — an in-memory pipe), and every tunnel is handed back to the caller as a net.Conn.

The library is four packages, one direction of dependency: github.com/go-gost/p2p (the contracts — Config, Status, the sentinel errors; standard library only), …/p2p/endpoint (the endpoint: identity, relay engine, forwards, Dial/Listen), …/p2p/grpc (the transport that serves an endpoint over the plugin protocol), and internal/host (the engine behind the endpoint, unimportable). Identity handling, the timing knobs and the trust boundary are covered in the embedding guide.

Upgrading from v0.4.x: p2p.New/p2p.Host are now endpoint.New/endpoint.Endpoint, and the Host.Tunnel() facade is gone — Dial/Listen live on the endpoint itself.

import (
	"github.com/go-gost/p2p"
	"github.com/go-gost/p2p/endpoint"
)

ep, err := endpoint.New(&p2p.Config{
	Derp:   "wss://derp.example.com/derp", // empty = stub mode (peer is a plain host:port)
	Key:    "peer.key",                    // curve25519 key file; created if missing
	Target: "127.0.0.1:18080",             // inbound tunnels bridge here (DERP mode)
})
if err != nil {
	return err
}
defer ep.Close() // the endpoint owns the engine, the forwards and the inbound listener

// Connect brings up the DERP engine and the configured forwards. Not needed in
// stub mode. A connect failure is not fatal here: the engine retries in the
// background. A failed forward registration is returned — that one is fatal.
_ = ep.Connect()
log.Printf("my public key: %s", ep.PublicKey()) // peers address the endpoint by this

// peer: a base64 public key in DERP mode, a host:port in stub mode.
conn, err := ep.Dial(ctx, "tcp", peer)
if err != nil {
	return err
}
defer conn.Close() // the conn IS the tunnel — closing it tears the tunnel down
  • What you get. Dial returns a net.Conn: read and write it like a socket, and whatever your application already carries over TCP (its own protocol, TLS, a request/response loop) rides inside it unchanged. network picks the stream's semantics — udp (also udp4/udp6) returns a conn that preserves datagram boundaries, anything else a byte stream.
  • Lifetime. ctx bounds only the call; the tunnel outlives it. The returned conn is the cancellation handle — close it and the tunnel, its peer dial, and its bookkeeping all go away. There is nothing else to track and no close RPC.
  • Inbound. In DERP mode the same endpoint also accepts tunnels from its peers. With a Target/Targets configured, each inbound stream is bridged to one of them for that stream's lifetime. With none configured, Listen() hands the inbound streams to the embedder instead: a net.Listener whose accepted conns carry the peer's base64 key as RemoteAddr(), so the embedder can route by peer and own the service stack (stats, auth, recording). The two are mutually exclusive.
  • Shutdown. ep.Close() shuts the endpoint down: the engine, the forwards, the inbound listener and the tunnel bookkeeping (idempotent). A transport attached to the endpoint ends with it; closing a transport only stops its own listener.
  • No control plane. Start/Serve bind the gRPC listener, which only out-of-process clients need; an embedder calls Connect (or nothing at all, in stub mode) and never starts a server. Every field under Configuration file is a Config field you can set in code.
Serving the plugin protocol from the same process

An endpoint is shared: attach the gRPC transport to serve GOST's plugin client while the same process dials in-process — one identity, one relay connection, and one datagram link per udp dial.

import (
	"github.com/go-gost/p2p"
	"github.com/go-gost/p2p/endpoint"
	"github.com/go-gost/p2p/grpc"
)

ep, _ := endpoint.New(&p2p.Config{Derp: "wss://derp.example.com/derp", Key: "peer.key"})
srv, _ := grpc.New(ep, grpc.WithAddr("127.0.0.1:8003"), grpc.WithToken(token))
addr, err := srv.Start() // binds the control plane and connects the endpoint

srv.Close() // stops the control plane; the endpoint keeps running
ep.Close()  // tears the endpoint (and every transport on it) down

The endpoint is deliberately structural — Dial(ctx, network, peer) + Close(), net-style — so an application that already defines its own transport interface can let *endpoint.Endpoint satisfy it directly instead of writing an adapter.

DERP mode (cross-machine)

The DERP engine relays tunnels through a DERP server so peers behind NAT/firewalls can reach each other. The relay server is the official derper binary; this host speaks its WebSocket path (Upgrade: websocket + subprotocol derp), which is also what keeps it deployable behind Cloudflare and other WebSocket-capable proxies. Note: this WebSocket path is Tailscale's own browser-client transport (cmd/tsconnect/wasm, via derpserver.AddWebSocketSupport) — it is not described in the custom DERP servers docs, which only cover the native Upgrade: DERP hijack, so treat it as a de-facto rather than a documented API.

# relay server (self-hosted; auto-creates its key config on first run)
derper -c /etc/derper/derper.json -hostname derp.example.com -certmode manual -certdir /etc/derper/certs -a :443

# peer side — registers at the relay, bridges inbound tunnels to the local GOST
./p2p --derp wss://derp.example.com/derp --key peer.key --target 127.0.0.1:18080

# client side — serves the GOST control plane as usual
./p2p --derp wss://derp.example.com/derp --key client.key --addr 127.0.0.1:8003

Each host generates a curve25519 keypair on first run and prints its public key (base64) at startup. In DERP mode the GOST chain node's addr is the peer host's public key, not a host:port. Everything else on the GOST side is unchanged.

DERP wire frames

The relay link is a WebSocket carrying DERP binary frames. Two layers are worth knowing: the DERP protocol itself (shared with every Tailscale client) and the small p2p framing this host puts inside a relayed packet.

DERP frame — [type 1B][length 4B big-endian][body], from tailscale.com/derp@v1.102.3:

Type Name Body
0x01 ServerKey 8-byte magic (DERP + key emoji) + 32-byte server public key
0x02 ClientInfo 32-byte client pubkey + 24-byte nonce + NaCl-box(JSON {Version, CanAckPings}) to the server key
0x03 ServerInfo 24-byte nonce + NaCl-box(JSON); token-bucket hints, advisory
0x04 SendPacket 32-byte destination pubkey + packet bytes (≤ 64 KiB)
0x05 RecvPacket v2: 32-byte source pubkey + packet bytes
0x06 KeepAlive none — no-op
0x08 PeerGone 32-byte pubkey + 1-byte reason (informational)
0x09 PeerPresent 32-byte pubkey (informational; derper only sends it to mesh watchers)
0x12 / 0x13 Ping / Pong 8-byte payload; Recv echoes a Ping back as Pong

Handshake: the server greets with ServerKey, the client replies ClientInfo (boxed to the server key, proving possession of its private key), the server answers ServerInfo. Everything else is SendPacket/RecvPacket. Unknown frame types are skipped (the reference client's recv switch has no default case) — the same forward-compat rule applies here. The client is addressed purely by its 32-byte public key; the base64 form is that key.

p2p packet framing — the bytes inside every SendPacket/RecvPacket start with a type byte:

Byte Meaning
0x00 control frame: [0x00][kind 1B][NaCl-boxed payload]
0x01 data frame: [0x01][smux byte stream] — one DERP packet per smux write

Control kinds: 0x02 punch candidates (sealed [count]([family][addr][port])*), 0x04 capability bitfield (sealed 1 byte; bit 0 = IPv6-aware, re-sent with each broadcast and OR'd by the receiver). (0x03, the udp-tunnel dial notice, is retired: a datagram link presents its own edge, so no notice is sent and none is acted on.) All control payloads are sealed to the peer key (PrivateKey.SealTo), so the relay can route but not forge them. Control frames are consumed by the engine; data frames feed the per-peer smux session.

Hole punching

When both peers are in engine mode, the relay is only used to establish the first session and carry the control channel. In the background each peer queries the derper's built-in STUN server (default port 3478, -stun is on by default) to learn its public UDP endpoint, exchanges it with the peer over the relay, and builds a KCP session over the same UDP socket.

The punch is symmetric: both peers dial the peer's candidate with the same deterministic KCP conv (mutual simultaneous open), and a session is only used once both sides complete the echo handshake — each peer must see its own token round-trip, so a half-open path can never produce a "false direct" session. This removes the old "one side dials, the other accepts" asymmetry, which failed when only one direction could be punched (e.g. a peer inside a k3s pod whose inbound UDP needs the peer to have sent first).

On success new tunnels open streams over the direct smux session (KCP + smux); the relay session stays up so a direct-path failure silently falls back to relay and re-punches. The direct path keeps working even if the relay drops — only a new punch needs the relay back. A static --forward warms its peer's direct path at startup when --stun is set, so the first connection skips the punch latency. Direct is on by default (--direct=false forces relay-only). IPv6 is a first-class candidate family: a host with a global IPv6 egress binds and advertises that address (no STUN, no NAT), and both peers prefer IPv6 when both offer it, retrying IPv4 in the same round if the v6 path does not complete.

Timings are tunable from the config file only (not flags); unset values keep the defaults, and invalid values fail at startup:

timeouts:
  punchWait: 5s       # how long a stream waits for the direct path before relay
  punch: 10s          # whole-punch window (candidate wait + dial/seed)
  seed: 5s            # symmetric echo handshake window
  backoff: 30s        # retry interval after a failed punch
  derpKeepAlive: 30s  # DERP keepalive (keep below proxy idle timeouts)
  smux:
    interval: 10s
    timeout: 30s      # must be >= 2x interval

Symmetric NAT defeats UDP punching; those peers stay on relay permanently (periodic retry). The KCP transport is unencrypted, matching the relay's trust model — confidentiality is the inner dialer's job (mtls/tls/wss).

The derper's STUN server answers only Tailscale's binding-request dialect (SOFTWARE + FINGERPRINT attributes) and binds to the same IP as -a. Run derper with an explicit IP (-a 1.2.3.4:443) so STUN is reachable on the address family the peers will query; with a wildcard -a :443 it binds IPv6-only, and the default IPv4 --stun (derp host :3478) won't reach it — set --stun explicitly in that case.

A chain node whose dialer is udp asks for a datagram stream (network=udp) instead of a byte stream. The Tunnel stream carries it like any other tunnel: each dial owns one datagram link that pairs the dial's stream (its local edge) with one peer edge — a P2PU-tagged stream to the other host over the direct-or-relay path — and pumps bytes between them. The GOST-side conn frames each datagram into a length-prefixed frame and the peer's GOST-side conn parses it, so packet boundaries survive the byte-stream data plane and this host never parses the data. That is what makes a tun-to-tun link possible — GOST owns the device (the tun listener creates and configures it, the tun handler bridges it), this host is only the pipe. A tun device is exclusive-open, so the device cannot be shared: GOST having its own tun stack is the whole point.

# both ends: no --target, no device flags. The peer's key goes in the GOST node addr.
./p2p --derp wss://derp.example.com/derp --key a.key --addr 127.0.0.1:8003 --stun derp.example.com:3478
# GOST side (each end; see play/p2p-tun.yaml in the gost repo)
p2ps:
  - name: p2p-1
    plugin: {type: grpc, addr: 127.0.0.1:8003}
services:
  - name: tun-0
    addr: :0                      # the tun listener binds no socket
    handler: {type: tun, chain: chain-0}      # chain without forwarder = client mode
    listener:
      type: tun
      metadata: {name: p2p0, net: 10.10.0.1/30, mtu: 1420}   # 10.10.0.2/30 on the peer
chains:
  - name: chain-0
    hops:
      - name: hop-0
        nodes:
          - name: node-0
            addr: <peerB-key>     # in p2p mode the addr IS the peer key
            dialer: {type: udp}   # datagram semantics
            connector: {type: forward}   # transparent (the default is http)
            metadata: {p2p: p2p-1}

The dialing side always presents: the link opens its own tagged edge as soon as the dial is allocated (re-presenting with backoff while it has none), so a one-sided link — the peer holds no dial of its own — works whatever the key order, and concurrent dials to one peer never share an edge. Two dials, one on each side of the same pair, are a rendezvous on one edge: the larger public key adopts the smaller key's presentation (its own presentation is provisional and is dropped at adoption), and a link that holds an adopted edge is never displaced. The link prefers the direct (hole-punched) path and falls back to the relay, exactly like a tunnel. network=udp requires engine mode (--derp): a link is addressed by peer key.

A link with no live peer edge buffers what the local side writes (32 KiB, then drops), so the datagram that triggered a dial is not lost while the presentation opens; after that bytes are dropped whenever the opposite edge is absent (IP tolerates loss), so a side that has nothing to send yet is still reachable. The data path is unencrypted like the rest of the data plane: there is no inner dialer here to secure it, so run it over a trusted path or add your own encryption above it.

UDP target (a global datagram outlet)

A udp:// --target is a global datagram outlet — the other end of a datagram link, and unlike a link it is not bound to any peer. Every inbound datagram stream (a stream prefixed with the P2PU tag) picks a target from the udp pool, dials it, and is bridged to it for the stream's lifetime: the stream's frames become raw IP datagrams at the outlet, and each datagram comes back framed on the stream. The outlet side keeps zero per-peer state — no link, no per-key socket, nothing that outlives a single stream. This is the shape a tun server behind NAT wants: many NAT'd tun clients reach one NAT'd peer that holds a single tun device.

# outlet side: p2p with a udp target pointing at the tun server — no --allow, no per-peer config
./p2p --derp wss://derp.example.com/derp --key outlet.key \
      --target udp://127.0.0.1:8421 --stun derp.example.com:3478

# outlet side: GOST tun SERVER — do NOT set tun.p2p (that flag is the single-peer mode)
#   gost -L "tun://127.0.0.1:8421?net=10.10.0.1/24&keepalive=true&ttl=10s&token=<passphrase>"
#   auther: user = each spoke's tun IP, password = the shared passphrase
#   sysctl -w net.ipv4.ip_forward=1     # to reach a network behind the outlet

The spoke is a stock tun client: net 10.10.0.<n>/24, keepalive: true, the same token, and a route for whatever lies behind the outlet; its chain node addr is the outlet host's key with dialer: udp / connector: forward, exactly like the point-to-point link.

  • One link per dial; the outlet is the other shape. Every dial owns its own link, so N concurrent clients to one peer never share an edge — and a one-sided link works whatever the key order, because the dialing side presents its own edge. Two dials, one on each side of the same pair, are a rendezvous on one edge: the larger key adopts the smaller's presentation. The outlet holds no link at all: each inbound tagged stream is bridged to a target for the stream's lifetime, zero per-peer state.
  • Admission is the caller's, not p2p's. Who may use the outlet is decided above this layer: the tun auther's per-spoke passphrase, the relay's -verify-clients=true, or simply port binding/firewall. An unauthenticated udp:// outlet is equivalent to exposing an unauthenticated tun server on the data plane — put the tun handler's token/passphrase (or an equivalent) in front of it.
  • keepalive is the tun app's job, above p2p. The outlet's source port changes whenever the peer edge is (re)established, so the tun server's route (keyed by spoke IP) is refreshed by the tun client's next keepalive — the tun application's responsibility, not a p2p contract. A tun client that never sends keepalive will, after its first reconnect, have the server's downstream pointed at a dead port. Set keepalive on the spokes (their peer is the tun server, which echoes and registers their route); the server-side keepalive/ttl also expires an absent spoke's route instead of black-holing.

--target is repeatable and feeds two pools: a bare host:port is a tcp target (inbound byte-stream tunnels), udp://host:port is a udp target (the outlet above). Multiple udp targets round-robin inbound datagram streams across outlets.

Transport stats

The host reports its direct/relay statistics over the existing gRPC Status RPC, so you can tell whether hole punching actually works for your peers:

grpcurl -plaintext 127.0.0.1:8003 proto.P2P/Status
{
  "tunnels": 4,
  "directPeers": 12, "derpPeers": 3,
  "punchAttempts": 45, "punchSuccess": 15,
  "streamsDirect": 210, "streamsDerp": 30
}

directPeers/derpPeers are gauges — where each peer's traffic goes right now; punch* and streams* are cumulative since start. A punchAttempts that climbs while punchSuccess stays flat means hole punching is being tried and failing (symmetric NAT / CGNAT) — the case IPv6 or port mapping would address. With --token set, add -H 'token: <token>'.

Security

The control channel is unauthenticated by default: any process that can reach --addr can make this host dial arbitrary addresses. Keep --addr on loopback (the default). For cross-machine deployment set --token (the GOST client sends it as gRPC metadata) and control TLS — the token alone travels over a plaintext gRPC channel today.

A DERP relay with -verify-clients=false is an open relay: it sees and can drop the bytes, but never decrypts them. Confidentiality is the inner protocol's job (use mtls/tls/wss inner dialers); the relay is transport, not trust.

A datagram link is plaintext too: IP packets cross the relay or hole-punched path unencrypted, and unlike a tunnel dialer there is nothing above them in this host to secure them. Run it only over a trusted path, or add your own encryption above the link (GOST's tls/mtls dialers do not apply to a udp tunnel).

A udp outlet shifts admission to the caller: p2p holds none. Who may reach the outlet's tun server is decided above — the tun auther's per-spoke passphrase, the relay's -verify-clients=true, or port binding/firewall. An outlet with nothing in front of it is an unauthenticated tun server exposed on the data plane.

Roadmap

  1. STUN + UDP hole-punched tunnels — shipped (KCP + smux direct path, DERP relay as fallback).
  2. Rendezvous address discovery — abandoned: derper v1.102.3 only sends PeerPresent to mesh watchers, never to open-relay clients, so presence-driven name discovery is infeasible. Peers are addressed by base64 public key; a human-friendly name belongs in the GOST config, not a p2p-side registry.
  3. Datagram links (tun/tap over the tunnel) — shipped (network=udp): one link per dial, pairing that dial's stream with a peer edge, carried inside the same Tunnel streams. The device itself is GOST's (the tun listener/handler), this host is only the pipe.
  4. UDP target outlet — shipped: a udp:// target is a global datagram outlet (stream-level, no per-peer state), so many NAT'd spokes reach one NAT'd peer holding a single tun device (the server's per-IP route table is GOST's). Admission is the caller's.

License

MIT

Documentation

Overview

Package p2p is the contract layer of the p2p library: the configuration an endpoint reads, the status it reports, and the sentinel errors callers match on. It holds no implementation and no dependency outside the standard library, so importing it never pulls an engine, a transport, or a parser.

An endpoint is built from a Config and used in one of two shapes:

  • in-process (github.com/go-gost/p2p/endpoint): the caller dials tunnels and takes inbound ones directly, with no wire protocol in between;
  • over a wire protocol (github.com/go-gost/p2p/grpc): a transport serves the same endpoint so a remote client — GOST's p2p plugin client — can open tunnels to it.

Both shapes share one endpoint, so a process that does both uses one identity and one relay connection.

Trust boundary: the data plane is plaintext and the control plane is unauthenticated by default — the loopback default listen address is the security boundary. Confidentiality is the inner protocol's job; see the package documentation of endpoint and grpc for the specifics.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidNetwork reports a network the tunnel protocol does not define:
	// anything other than tcp or udp.
	ErrInvalidNetwork = errors.New("p2p: invalid network")
	// ErrInvalidPeer reports a peer string that does not fit the mode: a
	// host:port in stub mode, a base64 public key in DERP mode.
	ErrInvalidPeer = errors.New("p2p: invalid peer")
	// ErrUnknownTunnel reports a tunnel id that was never issued or has been
	// reclaimed by the pending GC.
	ErrUnknownTunnel = errors.New("p2p: unknown tunnel")
	// ErrTunnelAttached reports a second carrier for a tunnel that already has
	// one: an id is single use.
	ErrTunnelAttached = errors.New("p2p: tunnel already attached")
	// ErrPeerUnreachable reports that the tunnel's peer end could not be
	// opened; the cause is wrapped, so errors.Is/errors.As still match it.
	ErrPeerUnreachable = errors.New("p2p: peer unreachable")
	// ErrForward reports a failed static forward registration: a configuration
	// error (bad listen address, no relay configured, port in use), not a
	// transient one. A caller that serves through an unreachable relay must
	// still fail on this; errors.Is is the test. The cause is wrapped.
	ErrForward = errors.New("p2p: forward")
)

Sentinel errors returned by the endpoint and the host seam. A transport maps them to its own error shape (gRPC codes today); an in-process caller matches them with errors.Is. Errors that carry a cause — a failed peer open, a rejected configuration — wrap one, so the message stays useful while the sentinel keeps the classification.

Functions

This section is empty.

Types

type Config

type Config struct {
	Derp     string          `yaml:"derp,omitempty"`
	Key      string          `yaml:"key,omitempty"`
	KeyHex   string          `yaml:"keyHex,omitempty"`
	Target   string          `yaml:"target,omitempty"`
	Targets  []string        `yaml:"targets,omitempty"`
	Stun     string          `yaml:"stun,omitempty"`
	Direct   *bool           `yaml:"direct,omitempty"`
	TLS      *TLSConfig      `yaml:"tls,omitempty"`
	Timeouts *TimeoutsConfig `yaml:"timeouts,omitempty"`
	Forwards []ForwardConfig `yaml:"forwards,omitempty"`
}

Config is the endpoint configuration: everything the host reads to build its identity, its relay engine and its data planes. The yaml tags are the config-file schema, but the file itself is read by the binary that deploys the library (cmd/p2p), not by this package.

func (*Config) TargetList

func (c *Config) TargetList() []string

TargetList merges the legacy scalar `target` with the `targets` list, scalar first, into the raw spec list the engine parses.

type ForwardConfig

type ForwardConfig struct {
	Listen string `yaml:"listen,omitempty"`
	Peer   string `yaml:"peer,omitempty"`
}

ForwardConfig is a pre-configured static port forward: bind Listen and bridge accepted connections to the peer's public key.

type SmuxTimeouts

type SmuxTimeouts struct {
	Interval time.Duration `yaml:"interval,omitempty"`
	Timeout  time.Duration `yaml:"timeout,omitempty"`
}

SmuxTimeouts tunes the smux keepalive shared by the relay and direct sessions. Timeout must be >= 2x Interval (validated when the endpoint is created).

type Status added in v0.5.0

type Status struct {
	Tunnels       int   // active tunnels held by this endpoint (pending records included)
	DirectPeers   int   // gauge: peers with a live direct session
	DerpPeers     int   // gauge: peers on relay only (no live direct session)
	PunchAttempts int64 // counter
	PunchSuccess  int64 // counter: attempts that reached direct
	StreamsDirect int64 // counter
	StreamsDerp   int64 // counter
	// PeerTransports names each connected peer's current path, keyed by base64
	// public key. A peer with no session at all is absent: every value
	// describes a peer that is reachable, and says whether it rides a
	// hole-punched session or, if not, the most specific reason available —
	// so DirectPeers/DerpPeers are its counts.
	//
	// One of:
	//
	//	"direct"           a live hole-punched session
	//	"punching"         a punch for this peer is in flight
	//	"failed"           this peer's punch failed (usually a symmetric NAT)
	//	"derp"             on the relay, with nothing in the way of a punch
	//	"disabled"         the direct path is off (Config.Direct)
	//	"no-candidates"    no STUN server and no IPv6 egress: nothing to punch with
	//	"stun-unreachable" STUN is configured but not answering, and there is no
	//	                   IPv6 egress to fall back on
	//
	// The gRPC transport does not carry it: its proto is frozen, so plugin
	// clients see the counts only.
	PeerTransports map[string]string
}

Status is a point-in-time snapshot of an endpoint: the live tunnel count and the transport counters behind it. A stub-mode endpoint (no relay configured) reports zeros for the transport fields.

type TLSConfig

type TLSConfig struct {
	Secure *bool  `yaml:"secure,omitempty"`
	CAFile string `yaml:"caFile,omitempty"`
}

TLSConfig mirrors the --tls.* flags. Secure is a pointer so an omitted "secure" (default true) is distinguishable from an explicit "secure: false".

type TimeoutsConfig

type TimeoutsConfig struct {
	PunchWait     time.Duration `yaml:"punchWait,omitempty"`
	Punch         time.Duration `yaml:"punch,omitempty"`
	Seed          time.Duration `yaml:"seed,omitempty"`
	Backoff       time.Duration `yaml:"backoff,omitempty"`
	DerpKeepAlive time.Duration `yaml:"derpKeepAlive,omitempty"`
	Smux          *SmuxTimeouts `yaml:"smux,omitempty"`
}

TimeoutsConfig tunes deployment-dependent timings. Zero values keep the built-in defaults; internal mechanism timeouts stay hardcoded.

The timings are process-wide: they are applied when an endpoint is created and a later endpoint inherits the values already applied. One endpoint per process is the model; a process that builds two must give them identical timeouts (or none).

Directories

Path Synopsis
cmd
p2p command
Command p2p runs a p2p endpoint standalone: it builds the endpoint from a config file and flags, and serves it over the gRPC control plane so a GOST plugin client can open tunnels to it.
Command p2p runs a p2p endpoint standalone: it builds the endpoint from a config file and flags, and serves it over the gRPC control plane so a GOST plugin client can open tunnels to it.
Package endpoint is the in-process door onto a p2p endpoint: one identity, one relay engine (when a relay is configured), its static forwards, and the API to dial tunnels and take inbound ones with no wire protocol in between.
Package endpoint is the in-process door onto a p2p endpoint: one identity, one relay engine (when a relay is configured), its static forwards, and the API to dial tunnels and take inbound ones with no wire protocol in between.
Package grpc serves a p2p endpoint over the GOST p2p plugin protocol: OpenTunnel authorizes a tunnel and issues its id, and the Tunnel bidi stream bound to that id carries the tunnel's data.
Package grpc serves a p2p endpoint over the GOST p2p plugin protocol: OpenTunnel authorizes a tunnel and issues its id, and the Tunnel bidi stream bound to that id carries the tunnel's data.
internal
derpclient
Package derpclient implements the minimal client side of the DERP protocol (Tailscale's Designated Encrypted Relay for Packets) over the WebSocket transport, enough to act as a p2p rendezvous/relay client against the official derper binary.
Package derpclient implements the minimal client side of the DERP protocol (Tailscale's Designated Encrypted Relay for Packets) over the WebSocket transport, enough to act as a p2p rendezvous/relay client against the official derper binary.
stun
Package stun implements the minimal client side of STUN (RFC 5389): a single binding request, enough to learn the public NAT mapping of a UDP socket for UDP hole punching.
Package stun implements the minimal client side of STUN (RFC 5389): a single binding request, enough to learn the public NAT mapping of a UDP socket for UDP hole punching.
tests
e2e/helper command
Command helper is the e2e suite's sidecar: the pieces a shell script would otherwise need extra tooling for.
Command helper is the e2e suite's sidecar: the pieces a shell script would otherwise need extra tooling for.

Jump to

Keyboard shortcuts

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