tunnel

package
v0.0.0-...-9e7beb5 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package tunnel carries the wire protocol over a single outbound connection.

One session multiplexes many channels — control, tool calls, and interactive consoles — so the agent opens exactly one connection outward and never needs an inbound rule. Multiplexing and per-channel flow control come from yamux: a console channel streaming `tail -f` has its own window and cannot starve a tool call sharing the connection.

The session layer works over any net.Conn, which keeps the websocket out of the core and makes the protocol testable without a network. Both sides import this package so the framing has one implementation rather than two that must agree.

Index

Constants

View Source
const WSPath = "/tunnel"

WSPath is the URL path that carries the tunnel websocket. It is wire contract: an agent and a SaaS that disagree on it never reach the Hello exchange, so it lives here where both sides import it.

Variables

This section is empty.

Functions

func AcceptWS

AcceptWS upgrades an inbound request and returns it as a net.Conn.

func DialWS

func DialWS(ctx context.Context, url string, hdr http.Header, tlsClient *http.Client) (net.Conn, error)

DialWS opens an outbound websocket and returns it as a net.Conn.

Outbound-only over 443 is the whole connectivity story: it traverses corporate egress proxies with no inbound rule and no VPN.

func NewTLSClient

func NewTLSClient(cfg *tls.Config) *http.Client

NewTLSClient returns the http.Client DialWS needs for a wss URL secured by cfg. The transport is HTTP/1.1 only — the websocket upgrade is an HTTP/1.1 mechanism, and negotiating h2 would break it.

func Sleep

func Sleep(ctx context.Context, d time.Duration) error

Sleep waits for d or until ctx ends, whichever is first. Exported so the reconnect loop can honour a FlapGuard delay without reimplementing the context dance.

Types

type Backoff

type Backoff struct {
	Min    time.Duration
	Max    time.Duration
	Factor float64
	// Jitter is the fraction of the computed delay that is randomised, 0..1.
	// 0 makes the schedule deterministic, which is what the tests want.
	Jitter float64
	// contains filtered or unexported fields
}

Backoff schedules reconnect attempts.

The agent's connection is the customer's only path to support, so it retries indefinitely rather than giving up — but it must not become a thundering herd when the SaaS restarts and every enrolled cluster reconnects at once. Hence the jitter, which is the part that matters at fleet scale.

func DefaultBackoff

func DefaultBackoff() Backoff

DefaultBackoff is a sane reconnect schedule: fast enough that a brief SaaS restart is invisible, slow enough that an hour-long outage does not generate a million connection attempts.

func (Backoff) Delay

func (b Backoff) Delay(n int) time.Duration

Delay returns the wait before attempt n, counting from 0.

func (Backoff) Reconnect

func (b Backoff) Reconnect(
	ctx context.Context,
	connect func(context.Context) (*Session, error),
	sleep func(context.Context, time.Duration) error,
) (*Session, error)

Reconnect calls connect until it returns without error or ctx ends, waiting per the backoff schedule between attempts. sleep is injectable so the retry policy can be tested without real time passing.

It returns the first successful session, or ctx.Err().

type Channel

type Channel struct {
	net.Conn
	Open tunnelproto.ChannelOpen
	// contains filtered or unexported fields
}

Channel is an accepted stream plus the request that opened it.

Read is overridden rather than inherited from the embedded Conn. The header is parsed with a json.Decoder, which reads in chunks and keeps whatever it over-read — for a console channel that surplus is the first bytes the user typed. Reading the raw stream afterwards would silently drop them, so the decoder's buffered remainder is spliced in front.

func (*Channel) Read

func (c *Channel) Read(p []byte) (int, error)

type FlapGuard

type FlapGuard struct {
	// Threshold is the lifetime below which an ended session counts as a flap.
	Threshold time.Duration
	// Backoff schedules the wait for consecutive flaps. Its floor should be
	// tens of seconds — the whole point is to be much slower than the connect
	// ramp.
	Backoff Backoff
	// contains filtered or unexported fields
}

FlapGuard escalates the wait between reconnects when sessions keep dying young.

The case it exists for is two agents sharing one identity: the SaaS supersedes the older session on every reconnect, each agent's connect succeeds instantly, its session dies moments later, and the pair fight at full speed — the lab run measured ~17k tunnel log lines in two minutes. The agent cannot tell it was superseded: the SaaS closes the old mux without a reason frame, and adding one would be a wire change for a signal this guard gets from timing alone. So it watches the symptom instead — connect succeeded, session ended within Threshold — which also covers a crash-looping SaaS and a misrouting load balancer.

A session that lives past Threshold resets the guard: normal operation pays nothing.

func DefaultFlapGuard

func DefaultFlapGuard() *FlapGuard

DefaultFlapGuard waits 10s, 20s, 40s… up to 2m between young deaths.

func (*FlapGuard) SessionEnded

func (g *FlapGuard) SessionEnded(lifetime time.Duration) time.Duration

SessionEnded records one session's lifetime and returns how long to wait before reconnecting: zero after a session that lived, an escalating delay after each one that died young.

type Session

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

Session is one multiplexed tunnel.

func Accept

func Accept(ctx context.Context, conn net.Conn, admit func(tunnelproto.Hello) tunnelproto.HelloAck) (*Session, error)

Accept performs the SaaS side over an established conn: it becomes the multiplexing server, accepts the control stream and applies admit to the agent's Hello.

A refused agent still receives its HelloAck before the session closes — the reason has to reach the operator, and a bare disconnect tells them nothing.

func Dial

func Dial(ctx context.Context, conn net.Conn, hello tunnelproto.Hello) (*Session, error)

Dial performs the agent side of session setup over an established conn: it becomes the multiplexing client, opens the control stream and sends Hello.

A refusal is returned as an error carrying the SaaS's operator-readable reason, because the agent's log is where somebody will look first.

func (*Session) AcceptChannel

func (s *Session) AcceptChannel(ctx context.Context) (*Channel, error)

AcceptChannel accepts the next channel and validates its open request.

Validation here is not redundant with the sender's: the agent must never trust the SaaS, so an open naming an address — or crossing the plane boundary — is rejected on receipt regardless of what the sender checked.

func (*Session) Ack

func (s *Session) Ack() tunnelproto.HelloAck

Ack is the handshake result the peer returned (agent side) or produced (SaaS side).

func (*Session) AwaitKillSwitch

func (s *Session) AwaitKillSwitch() (tunnelproto.KillSwitch, error)

AwaitKillSwitch blocks until the peer sends one.

func (*Session) Close

func (s *Session) Close() error

Close drops the session and every channel on it — including live console sessions, which is the point: a kill switch that waits politely is not one.

func (*Session) IsClosed

func (s *Session) IsClosed() bool

IsClosed reports whether the session has gone away.

func (*Session) OpenChannel

func (s *Session) OpenChannel(ctx context.Context, id uint32, kind tunnelproto.ChannelKind, target tunnelproto.Target) (net.Conn, error)

OpenChannel opens a new channel to a symbolic target.

The request is validated before it is sent even though the peer validates on receipt; a caller should never knowingly emit something the peer will reject.

func (*Session) SendKillSwitch

func (s *Session) SendKillSwitch(k tunnelproto.KillSwitch) error

SendKillSwitch tells the peer to drop everything. The customer holds this control, so it takes effect on the peer's next read rather than waiting for anything in flight to finish.

Jump to

Keyboard shortcuts

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