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
11 changes: 6 additions & 5 deletions cmd/codeaf/chatv3_at.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ func openChatV3At(launch atLaunch) error {
Label: pair.ThisMachineLabel(),
// EVERY LINE OF THE PAIRING GOES TO STDERR, so that `--once` piped into
// a file is the reply and nothing else.
Say: func(line string) { fmt.Fprintln(os.Stderr, line) },
Say: func(line string) { fmt.Fprintln(os.Stderr, line) },
Sync: syncRelayURL(),
}
if stdinIsTerminal(os.Stdin) {
reach.AskCode = askPairingCode
Expand Down Expand Up @@ -261,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)
relayAddress := flags.String("via", "", "the sync address to be reachable through; empty reads "+pair.RelayEnv+", then the address this machine syncs through")
renamedFlag(flags, "relay", "via")
if err := parseCommandFlags(flags, args); err != nil {
return err
Expand All @@ -272,11 +273,11 @@ func runServe(args []string) error {

service := strings.TrimSpace(*relayAddress)
if service == "" {
service = pair.Relay()
service = pair.Relay(syncRelayURL())
}
if service == "" {
// THE HONEST STATE, AND THE ONE MOST PEOPLE WILL MEET. The relay is a
// service and this build does not assume one exists.
// Only a machine whose person turned sync off and named no relay
// gets here; every other machine has sync's address.
return errors.New("no sync address is set up on this machine, so there is nowhere to be reachable from — set " + pair.RelayEnv + " to your sync address, or let people in over ssh with `codeaf chat --host` from their side")
}

Expand Down
44 changes: 41 additions & 3 deletions cmd/codeaf/chatv3_at_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package main
import (
"strings"
"testing"

"github.com/Agent-Field/codeaf/internal/home"
"github.com/Agent-Field/codeaf/internal/pair"
"github.com/Agent-Field/codeaf/internal/syncsetup"
)

// The target is read the way the ssh door reads its own, because it is the same
Expand Down Expand Up @@ -58,12 +62,13 @@ func TestTheFlagsThatBuildASessionCannotTravelOverAt(t *testing.T) {
}

// WITH NO RELAY SET UP, BOTH DOORS SAY SO IN ONE SENTENCE AND NAME THE ROAD
// THAT DOES WORK. The relay is a service, this build does not assume one is
// running, and a person who meets this must not be left guessing whether their
// network is broken.
// THAT DOES WORK. Sync falls back to the hosted relay, so the only machine
// with no relay is one whose person turned sync off, and they must not be
// left guessing whether their network is broken.
func TestWithNoRelayBothDoorsSayWhatIsWrongAndWhatToDoInstead(t *testing.T) {
t.Setenv("CODEAF_HOME", t.TempDir())
t.Setenv("CODEAF_RELAY", "")
t.Setenv(syncsetup.URLVar, "off")

err := openChatV3At(atLaunch{target: "otter-lamp-42"})
if err == nil {
Expand Down Expand Up @@ -91,6 +96,39 @@ func TestWithNoRelayBothDoorsSayWhatIsWrongAndWhatToDoInstead(t *testing.T) {
}
}

// SERVE, --at, PAIRING AND SYNC AGREE ABOUT THE RELAY. A machine whose chats
// sync is reachable through that same relay with nothing else set up, whether
// sync's address came from the variable, a saved pairing or the hosted default.
func TestServeAtPairingAndSyncResolveOneRelay(t *testing.T) {
cases := []struct{ name, variable, saved, want string }{
{"the hosted default", "", "", syncsetup.HostedRelayURL},
{"the relay a pairing saved", "", "https://saved.example/fabric", "https://saved.example/fabric"},
{"the variable", "https://variable.example", "https://saved.example/fabric", "https://variable.example"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Setenv("CODEAF_HOME", t.TempDir())
t.Setenv(pair.RelayEnv, "")
t.Setenv(syncsetup.URLVar, c.variable)
if c.saved != "" {
if err := syncsetup.SaveRelayURL(home.Dir(), c.saved); err != nil {
t.Fatal(err)
}
}
mailbox, err := pairMailbox("")
if err != nil {
t.Fatal(err)
}
sync := syncsetup.Resolve(home.Dir()).URL
serve := pair.Relay(syncRelayURL())
at := pair.RelayFor(pair.Known{}, false, syncRelayURL())
if sync != c.want || mailbox.URL != c.want || serve != c.want || at != c.want {
t.Fatalf("sync %q, pairing %q, serve %q, --at %q; want all %q", sync, mailbox.URL, serve, at, c.want)
}
})
}
}

// `codeaf devices` on a machine nothing has paired with is one sentence and an
// invitation, not a heading over an empty table.
func TestDevicesOnAFreshMachineSaysNothingIsPaired(t *testing.T) {
Expand Down
10 changes: 10 additions & 0 deletions cmd/codeaf/pairroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ import (
// wait and this client never cuts a healthy one short.
const pairPollTimeout = pairbox.MaxWait + 10*time.Second

// syncRelayURL is the relay this computer syncs through, empty when sync is
// off. `codeaf serve` and `--at` fall back to it, so a machine whose chats sync
// is reachable through the same relay with nothing else set up.
func syncRelayURL() string {
if relay := syncsetup.Resolve(home.Dir()); relay.On() {
return relay.URL
}
return ""
}

// pairMailbox is the relay a pairing runs through: the --relay flag, else the
// relay this computer syncs through. A relay named by flag is the one the other
// device is told to pass, so it is `Shown`; the default needs no telling.
Expand Down
15 changes: 15 additions & 0 deletions docs/changes/unreleased/2073-reach-machines-through-sync-relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
kind: fixed
title: codeaf serve and chat --at work through the relay your chats already sync through
pr: 2073
surface: [remote, docs]
invalidates:
- "codeaf serve and chat --at needed CODEAF_RELAY or a saved relay file; with neither they said no sync address was set up. They now fall back to the relay sync resolves (CODEAF_SYNC_URL, the relay a pairing saved, then the hosted relay)."
- "The relay client dropped the path of a relay address, so https://host/fabric was called at https://host/v1/engine. It now calls https://host/fabric/v1/engine and /fabric/v1/dial/<name>, as every other client does."
- "The machine carrier was only the raw aforge-relay/1 HTTP upgrade. It now rides a WebSocket offering that protocol and falls back to the raw upgrade when a relay answers 426; codeaf relay accepts both."
- "The hosted relay had no machine doors and answered /v1/engine with 404. It now serves /v1/engine and /v1/dial/<name> through one Durable Object per machine name, which needs the v4-machines migration and the MACHINE binding in a deployment's config."
- "codeaf serve said lost sync on every retry and quit when the relay still held its own name from a dropped connection. It now says it once per outage and waits the stale name out."
---

A relay that is down, or one that cannot connect machines, is now one sentence naming the relay and what
to try, never an upstream body such as `error code: 522`.
2 changes: 1 addition & 1 deletion docs/remote-access-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ clear refusal. Run it anyway; the refusals are the deliverable.

```sh
codeaf devices # who may reach this machine, and its name
codeaf serve # be reachable (needs CODEAF_RELAY set)
codeaf serve # be reachable (through the relay sync uses, or CODEAF_RELAY)
codeaf chat --at otter-lamp-42 # reach a machine by its name
```

Expand Down
57 changes: 42 additions & 15 deletions internal/manual/chat/reaching-this-machine-without-ssh.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@ laptop$ codeaf chat --at otter-lamp-42
router setting. That machine dials *out* to a relay and stays connected, which is why
firewalls and NAT stop mattering.

**This needs a relay service, and there is not one running yet.** Until one is switched
on, `--at` prints:

```
no service address is set up on this machine, so --at has nowhere to look for otter-lamp-42 — set CODEAF_RELAY to a service address, or reach that machine with --host over ssh
```
**There is nothing to set up.** Both machines go through the relay their chats already
sync through: the hosted relay, unless sync was pointed somewhere else with
`CODEAF_SYNC_URL` or by pairing. `CODEAF_RELAY`, or `--via` on `codeaf serve`, picks a
different relay for this door alone, such as one you run yourself.

`--host` works today and is not going anywhere. If you can already reach the machine that
way, use that; the two doors are compared at the bottom of this page.
Expand All @@ -65,19 +63,39 @@ 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. `--via` beats the `CODEAF_RELAY`
environment variable.
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.

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

```
this machine is already connected to the service as otter-lamp-42 — there is only one of it, so close the other `codeaf serve`
this machine is already connected to sync as otter-lamp-42 — there is only one of it, so close the other `codeaf serve`
```

## codeaf serve when the network drops or the relay says no

If the network drops while it runs, it says once:

```
lost sync — trying again
```

With no relay set up at all it says:
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.

A relay that answers but cannot connect machines, such as an older sync service, stops
it with:

```
no service address is set up on this machine, so there is nowhere to be reachable from — set CODEAF_RELAY to your service address, or let people in over ssh with `codeaf chat --host` from their side
the service at https://service.example.com syncs chats but does not connect machines — run your own with `codeaf relay` and set CODEAF_RELAY to it, or reach that machine with --host over ssh
```

A relay that cannot be reached when it starts stops it with the sentence in *When --at
will not open*. With sync turned off (`CODEAF_SYNC_URL=off`) and no `CODEAF_RELAY`, there
is no relay at all:

```
no sync address is set up on this machine, so there is nowhere to be reachable from — set CODEAF_RELAY to your sync address, or let people in over ssh with `codeaf chat --host` from their side
```

Ctrl+c ends it, and the machine gives its name back on the way out:
Expand Down Expand Up @@ -351,9 +369,9 @@ that still has it in the list, which is what revoking removes.

## When --at will not open

There are four different reasons, and each says which one it is, in one sentence.
There are five different reasons, and each says which one it is, in one sentence.

**No relay is set up on this machine:**
**No relay is set up on this machine** (sync is off and `CODEAF_RELAY` is empty):

```
no service address is set up on this machine, so --at has nowhere to look for otter-lamp-42 — set CODEAF_RELAY to a service address, or reach that machine with --host over ssh
Expand All @@ -365,6 +383,12 @@ no service address is set up on this machine, so --at has nowhere to look for ot
the service at https://service.example.com cannot be reached from here — check this machine's network, or reach that machine with --host over ssh
```

**The relay answers but cannot connect machines:**

```
the service at https://service.example.com syncs chats but does not connect machines — run your own with `codeaf relay` and set CODEAF_RELAY to it, or reach that machine with --host over ssh
```

**The relay is fine and that machine is not connected to it:**

```
Expand Down Expand Up @@ -464,15 +488,18 @@ sitting at. They differ only in how the two halves reach each other.

| | `--host` | `--at` |
| --- | --- | --- |
| Needs | `ssh <machine>` already works | a relay service, and one pairing |
| Needs | `ssh <machine>` already works | one pairing; the relay is the one sync uses |
| Set up on the far machine | nothing | `codeaf serve` running |
| Works through NAT and firewalls | only if ssh does | yes — that machine dials out |
| Anything in the middle | nothing of ours | a relay that cannot read the conversation |
| Available today | yes | not until a relay is running |
| Available today | yes | yes |

**Prefer `--host` when ssh works.** It has nothing between the two machines, nothing to
set up, and no service to depend on. `--at` is for the machine ssh cannot reach.

Both machines must use the same relay. If one syncs through a relay of your own and the
other through the hosted one, set `CODEAF_RELAY` to the same address on both.

Both open the same surface with the same limits — what does and does not work over a
connection is a property of the connection, not of which of these two doors opened it.

Expand Down
7 changes: 6 additions & 1 deletion internal/pair/dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ type Reach struct {
AskCode func(name string) (string, error)
// Say is where the pairing's own lines go. Nil says nothing.
Say func(string)
// Sync is the relay this device syncs through, used when nothing more
// particular names one; see [Relay].
Sync string
// Now is the clock, swapped by tests.
Now func() time.Time
}
Expand Down Expand Up @@ -102,7 +105,7 @@ func (r Reach) Open(ctx context.Context) (*Tunnel, error) {
if err != nil {
return nil, err
}
service := RelayFor(known, found)
service := RelayFor(known, found, r.Sync)
if strings.TrimSpace(service) == "" {
return nil, NoRelay(r.Name)
}
Expand Down Expand Up @@ -204,6 +207,8 @@ func (r Reach) dial(ctx context.Context, service string) (io.ReadWriteCloser, er
return nil, NotConnected(r.Name)
case errors.Is(err, relay.ErrUnreachable):
return nil, Unreachable(service)
case errors.Is(err, relay.ErrNoDoor):
return nil, NoDoor(service)
case errors.Is(err, relay.ErrTooMany):
return nil, Busy()
default:
Expand Down
Loading
Loading