relay

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package relay is the blind pipe between a machine that runs codeaf and a machine somebody is sitting at.

IT EXISTS BECAUSE THE ENGINE MACHINE ONLY EVER DIALS OUT. A home server, an office workstation, a laptop on a café network — none of them can be reached from outside, and telling a person to forward a port is the onboarding cliff this whole lane was written to remove. So the engine machine opens ONE outbound connection to the relay and holds it. NAT and firewalls become irrelevant, because nothing was opened.

THE RELAY MUST NOT BE ABLE TO READ A FRAME, AND THAT IS A PROPERTY OF THE CODE AND NOT A PROMISE IN A DOCUMENT. This package imports neither internal/remote nor internal/pair; it has no key material of the conversation's, no decoder for the conversation's frames, and no type in it that could hold a plaintext. What it moves is a byte slice it never looks inside. Its total knowledge of a session is the machine's NAME, the TIMING, and the BYTE COUNTS — and a test in this package pins exactly that (relay_blind_test.go).

WHAT IT IS NOT is a trust root. The name a machine registers under is derived from that machine's own key, and the relay checks possession of that key before it hands the name over — but a name is a rendezvous label, never an identity. Everything that makes a connection safe happens ABOVE this package, end to end, in internal/pair: a PAKE over a code shown on the engine machine's own screen, and thereafter a Noise handshake between keys both ends have pinned. A relay that lied about which machine is which produces a failed handshake, not a compromise.

THE CARRIER IS AN HTTP UPGRADE OVER ONE TCP CONNECTION, in front of TLS in production. The design doc says "outbound websocket" and means the property rather than the framing: one long-lived connection the engine machine dials out on, over the port every network already lets out. A websocket's masking and its close protocol buy nothing here — both ends of this pipe are codeaf, and the payloads are already ciphertext — while its framing would be a second framing on top of the one below, which this package needs anyway to carry several surfaces down one carrier.

Index

Constants

View Source
const (
	// EnginePath is where a machine registers itself and holds the carrier.
	EnginePath = "/v1/engine"
	// DialPrefix is where a surface asks for a machine by name; the name is
	// the rest of the path.
	DialPrefix = "/v1/dial/"
)

The doors. Both are GET so that an ordinary HTTP front end, a load balancer or a corporate proxy sees a request shape it already knows how to pass.

View Source
const (
	// HeaderName carries the machine name being claimed.
	HeaderName = "Aforge-Name" // legacy-name: persisted on the wire.
	// HeaderKey carries the machine's long-term public key, base64 raw-url.
	HeaderKey = "Aforge-Key" // legacy-name: persisted on the wire.
)

The headers of the registration request. They are headers rather than a first frame because a relay operator's logs, metrics and rate limiters all read headers, and a name that only appeared after the upgrade would be invisible to every one of them.

View Source
const (
	// MaxPayload is the largest body one framed message may carry. It is a
	// little over 64 KiB because the tunnel above frames its ciphertext at
	// Noise's own 65535-byte ceiling and adds a small header; a limit under
	// that would cut a legal message in half.
	MaxPayload = 1 << 17

	// MaxStreams is how many surfaces may be attached to one machine at once.
	// It is a limit on the relay's memory and on nothing else — the engine
	// machine has its own opinion about how many surfaces a session will have,
	// and this is only the number the pipe will carry.
	MaxStreams = 16

	// PingEvery is how often the relay pokes an idle carrier, and IdleAfter is
	// how long it will wait for any byte before it decides the machine is gone.
	// A NAT that has forgotten a mapping is silent rather than closed, so a
	// carrier with no traffic on it is a carrier that has to be tested.
	PingEvery = 45 * time.Second
	IdleAfter = 2 * PingEvery

	// HandshakeWithin bounds the registration exchange. A connection that has
	// claimed a name but not yet proved it holds the key is a connection that
	// could hold a name for free, so it is given one round trip's worth of time
	// and no more.
	HandshakeWithin = 20 * time.Second

	// DialsPerMinute and RegistrationsPerMinute are the per-address ceilings.
	// They are deliberately generous for a person and stingy for a script: a
	// person dials once and stays, and reconnects a handful of times a day.
	DialsPerMinute         = 30
	RegistrationsPerMinute = 10
)

The tuning, all in one place so that a relay operator changes a number here and every sentence in this package that quotes it changes with it. ONE SOURCE OF TRUTH: nothing below re-states a limit as a literal.

View Source
const Names = `` /* 177-byte string literal not displayed */

Names is the name-squatting policy, written down where the code that enforces it can be read beside it. It is a constant so that the relay's own operator documentation and the manual page quote the same words rather than two paraphrases that drift.

THE POLICY IS FOUR RULES:

  1. A name must agree with the key that claims it. The relay recomputes NameFor over the offered key and refuses a registration where the two disagree, so a name cannot be claimed by a machine that does not hold a key for it.
  2. Possession is proved, not asserted. The registering machine answers a challenge that can only be answered with the private half of the key it offered, so a name cannot be claimed by replaying somebody's public key.
  3. A live registration is never taken over. While a machine is connected under a name, a second machine offering the same name is refused — even with a valid key. Whoever is there stays there until they leave.
  4. The name is not the trust root, and the relay says so out loud. Grinding a key whose hash lands on a given name costs seconds; it buys the name and nothing else, because the surface dialling it has pinned the key it paired with and the handshake fails against any other.
View Source
const Protocol = "aforge-relay/1" // legacy-name

Protocol is the upgrade token both ends spell, and the door that refuses a build that would misread the bytes. It is bumped when the rendezvous framing below changes, which is a different clock from internal/remote's Version — the relay carries session frames it cannot read, so the two versions move independently and neither may be inferred from the other. This is an ON-THE-WIRE identifier, not product prose. A product rename may not split old and new relay clients into different protocols.

Variables

View Source
var (
	// ErrUnreachable is the relay itself not answering.
	ErrUnreachable = errors.New("relay: cannot be reached")
	// ErrNoMachine is the relay answering that nothing is registered under
	// that name right now.
	ErrNoMachine = errors.New("relay: no machine connected under that name")
	// ErrNameTaken is a registration refused because that name is already
	// connected — rule three of [Names].
	ErrNameTaken = errors.New("relay: that name is already connected")
	// ErrTooMany is a rate limit, either door.
	ErrTooMany = errors.New("relay: too many connections from here")
)

The facts a caller above has to be able to tell apart.

Functions

func DecodeKey

func DecodeKey(text string) ([]byte, error)

func Dial

func Dial(ctx context.Context, service, name string) (io.ReadWriteCloser, error)

Dial reaches a machine by name, and hands back the raw byte pipe to it.

WHAT COMES BACK IS NOT AUTHENTICATED AND NOT ENCRYPTED. It is a pipe to whoever is registered under that name, brokered by a relay this package does not ask anybody to trust. internal/pair is what turns it into a connection — a Noise handshake against a pinned key — and nothing else in this tree may put a session frame on this pipe.

func EncodeKey

func EncodeKey(key []byte) string

EncodeKey and DecodeKey are how a public key travels in a header. Raw URL base64 because a header is a header: no padding to be stripped by a proxy, no slash to be read as a path.

func NameFor

func NameFor(publicKey []byte) string

NameFor is the name a machine holding this public key registers under.

The shape is two words and a two-digit number — `otter-lamp-42` — because that is a thing a person reads off one screen and types into another without writing it down. The number exists so that two machines whose keys collide on both words still differ, and so that the name never looks like an English phrase somebody might try to guess at.

func ValidName

func ValidName(name string) bool

ValidName says whether a string could be a name this package ever minted. It is a cheap door on the dial path so that a garbage path never reaches the registration table, and it is deliberately a SHAPE check rather than a membership one: the relay does not hold a list of the names that exist, and asking it to would be asking it to hold a directory of everybody's machines.

Types

type Ledger

type Ledger struct {
	// Name is the machine's key-derived name.
	Name string
	// Since is when it registered.
	Since time.Time
	// Streams is how many surfaces are attached right now.
	Streams int
	// Up and Down are bytes carried, surface→machine and machine→surface.
	// They are counts. The relay has never seen what any of them were.
	Up, Down int64
}

Ledger is everything the relay knows about one connected machine. IT IS THE COMPLETE LIST BY CONSTRUCTION: the relay's only other data is the socket.

type Registration

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

Registration is a machine's held connection to the relay and the surfaces arriving on it.

func Register

func Register(ctx context.Context, service string, private *ecdh.PrivateKey) (*Registration, error)

Register holds this machine's outbound connection open under the name its key derives.

It returns once the relay has accepted the registration, so a caller that gets no error can print the name and know it is real. Everything after that happens on Registration.Accept.

func (*Registration) Accept

func (r *Registration) Accept() (io.ReadWriteCloser, error)

Accept hands back the next surface that dialled this machine. It blocks; a closed registration returns io.EOF, which is the ordinary way the loop above it ends.

func (*Registration) Close

func (r *Registration) Close() error

Close gives the name up. Every surface on it goes with it, which is right: the machine has stopped being reachable.

func (*Registration) Name

func (r *Registration) Name() string

Name is what a person types to reach this machine.

type Server

type Server struct {
	// Now is the clock, swapped by tests. Nil is [time.Now].
	Now func() time.Time
	// Note is where a relay operator's log line goes, if they want one. It is
	// handed the name and a short reason and NEVER a payload, because there is
	// no payload in this package to hand it.
	Note func(name, what string)
	// contains filtered or unexported fields
}

Server is the relay.

The zero value works: an empty table, the wall clock, and no logging. Every limit it enforces comes from the constants in relay.go, so an operator who changes one changes it in the one place the sentences quote.

func (*Server) Ledgers

func (s *Server) Ledgers() []Ledger

Ledgers is what the relay currently knows, for an operator's status page and for this package's own tests.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

Jump to

Keyboard shortcuts

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