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
- func AcceptWS(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOptions) (net.Conn, error)
- func DialWS(ctx context.Context, url string, hdr http.Header, tlsClient *http.Client) (net.Conn, error)
- func NewTLSClient(cfg *tls.Config) *http.Client
- type Backoff
- type Channel
- type Session
- func (s *Session) AcceptChannel(ctx context.Context) (*Channel, error)
- func (s *Session) Ack() tunnelproto.HelloAck
- func (s *Session) AwaitKillSwitch() (tunnelproto.KillSwitch, error)
- func (s *Session) Close() error
- func (s *Session) IsClosed() bool
- func (s *Session) OpenChannel(ctx context.Context, id uint32, kind tunnelproto.ChannelKind, ...) (net.Conn, error)
- func (s *Session) SendKillSwitch(k tunnelproto.KillSwitch) error
Constants ¶
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 ¶
func AcceptWS(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOptions) (net.Conn, error)
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.
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) 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.
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 ¶
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 ¶
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 ¶
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) 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.