Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/codeaf/chatv3_at.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func askPairingCode(name string) (string, error) {
func runServe(args []string) error {
flags := commandFlags("serve")
workspace := flags.String("workspace", "", "directory a connection works in when it does not name one; empty is the directory this command was run in")
relayAddress := flags.String("via", "", "the sync address to be reachable through; empty reads "+pair.RelayEnv+", then the address this machine syncs through")
relayAddress := flags.String("via", "", "the sync address to be reachable through; empty reads "+pair.RelayEnv+", ~/.codeaf/v3/remote/relay, then the address this machine syncs through")
renamedFlag(flags, "relay", "via")
if err := parseCommandFlags(flags, args); err != nil {
return err
Expand Down
49 changes: 49 additions & 0 deletions cmd/codeaf/chatv3_at_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package main

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -146,3 +149,49 @@ func TestDevicesOnAFreshMachineSaysNothingIsPaired(t *testing.T) {
t.Fatal("an unknown word was accepted")
}
}

// Contract item 7 (Finding 10): `codeaf serve --help` names the full relay
// precedence — CODEAF_RELAY, ~/.codeaf/v3/remote/relay, then sync — matching
// what pair.Relay and pair.RelayFor actually resolve.
func TestServeHelpAndRelayPrecedenceIncludeSavedRemoteRelayFile(t *testing.T) {
var buf bytes.Buffer
previous := usageOut
usageOut = &buf
t.Cleanup(func() { usageOut = previous })

if err := runServe([]string{"--help"}); err != exitHelped {
t.Fatalf("runServe(--help) returned %v, want exitHelped", err)
}
help := strings.Join(strings.Fields(buf.String()), " ")
envPos := strings.Index(help, pair.RelayEnv)
filePos := strings.Index(help, "~/.codeaf/v3/remote/relay")
syncPos := strings.Index(help, "syncs through")
if envPos < 0 || filePos < 0 || syncPos < 0 || !(envPos < filePos && filePos < syncPos) {
t.Fatalf("serve --help did not list %s, ~/.codeaf/v3/remote/relay, then sync in order: %q", pair.RelayEnv, help)
}

t.Setenv("CODEAF_HOME", t.TempDir())
t.Setenv(pair.RelayEnv, "")
t.Setenv(syncsetup.URLVar, "https://sync.example")
if err := os.MkdirAll(filepath.Dir(pair.RelayFile()), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(pair.RelayFile(), []byte("https://file.example/fabric\n"), 0o600); err != nil {
t.Fatal(err)
}

if got := pair.Relay(syncRelayURL()); got != "https://file.example/fabric" {
t.Fatalf("serve precedence with v3/remote/relay gave %q, want file override", got)
}
if got := pair.RelayFor(pair.Known{}, false, syncRelayURL()); got != "https://file.example/fabric" {
t.Fatalf("unpaired --at precedence with v3/remote/relay gave %q, want file override", got)
}
paired := pair.Known{Service: "https://paired.example/fabric"}
if got := pair.RelayFor(paired, true, syncRelayURL()); got != "https://paired.example/fabric" {
t.Fatalf("paired --at precedence gave %q, want paired machine service over file", got)
}
t.Setenv(pair.RelayEnv, "https://env.example/fabric")
if got := pair.RelayFor(paired, true, syncRelayURL()); got != "https://env.example/fabric" {
t.Fatalf("CODEAF_RELAY precedence gave %q, want env override", got)
}
}
25 changes: 25 additions & 0 deletions docs/changes/unreleased/2076-relay-machine-door-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
kind: fixed
title: a stranger can no longer hold, squat or flood a machine reached through the relay
pr: 2076
surface: [remote]
invalidates:
- >-
A relay stream had no deadline, so the serving machine's 30-second
handshake limit never applied: silent dials could hold all 16 of a
machine's slots, and one byte could burn its pairing code and freeze
pairing. Streams now end at the handshake deadline.
- >-
The hosted relay kept its rate limits per machine name. It now limits each
caller network at the front door (30 dials and 10 registrations a minute)
and refuses names codeaf cannot produce before any object is created.
- >-
A registration with a different key could replace a live machine that
stalled for five seconds. Only the same key may replace a silent one.
- >-
codeaf serve printed "lost sync — trying again" and then nothing when it
recovered. It now answers with "back — reachable as <name> again".
---

Found by a review of #2073 after its machine doors went live on the hosted
relay, and checked by hand against the Worker run locally.
29 changes: 24 additions & 5 deletions internal/manual/chat/reaching-this-machine-without-ssh.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ digits, the same every time, on any relay. You cannot pick a nicer one, and nobo
can register under yours while your machine is connected.

`--workspace` is the directory a connection works in when it does not name one; empty
means the directory you ran the command in. The relay is, in order: `--via`, the
`CODEAF_RELAY` environment variable, then the relay this machine syncs through.
means the directory you ran the command in. For `codeaf serve`, the relay is, in order:
`--via`, the `CODEAF_RELAY` environment variable, `~/.codeaf/v3/remote/relay`, then the
relay this machine syncs through. For `codeaf chat --at`, it is `CODEAF_RELAY`, the relay
that machine was paired through, `~/.codeaf/v3/remote/relay`, then the relay this machine
syncs through.

Running it twice on one machine is refused, because there is only one of it:

Expand All @@ -80,8 +83,22 @@ If the network drops while it runs, it says once:
lost sync — trying again
```

and keeps trying, waiting a little longer each time up to a minute, until it prints its
name and code again. It does not need restarting, and devices reach it as soon as it has.
and keeps trying, waiting a little longer each time up to a minute, resetting that wait
only after a connection has held for at least a minute. When it reconnects, it says once:

```
back — reachable as otter-lamp-42 again
```

It prints its name and code again only if the name or code changed. It does not need
restarting, and devices reach it as soon as it is back.

If the relay turns registrations away — it allows 10 registrations a minute from one
address — `codeaf serve` backs off the same way and says once:

```
the service is turning registrations away right now — waiting before trying again
```

A relay that answers but cannot connect machines, such as an older sync service, stops
it with:
Expand Down Expand Up @@ -410,7 +427,9 @@ Two more you may meet. A machine that does not answer the handshake at all:
`otter-lamp-42 did not answer this device's handshake, so nothing was sent — if that machine was rebuilt it has a new key and this device has to pair with it again`.
And a relay that is turning connections away:
`the service is turning connections away right now — try again in a minute` — it allows
30 connections a minute from one address.
30 connections a minute from one address. If a proxy or wrong path at the relay address
refuses the upgrade, it names the full address it tried:
`the relay at https://service.example.com/wrong-prefix refused this connection (404) — check the address it was given`.

**A pairing that says the code was wrong when you are sure it was right** has one other
cause, and it is the machine's own disk. The machine writes a device into its list
Expand Down
Loading
Loading