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
5 changes: 5 additions & 0 deletions .changeset/cloud-tun-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": patch
---

A box whose FTW Cloud link carries the phones' sessions says so in `hello_ok.caps` as `cloud.tun`, a new registry capability, so a phone uses that path only when the box takes it. The registry also names the carrier state `tunnel` for a session that FTW Cloud carries.
8 changes: 7 additions & 1 deletion contract/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ capabilities:
# only and offers no FTW Cloud. It says nothing about whether the owner
# turned FTW Cloud on; the box's grant decides that.
- cloud.link
# The box takes its app session through FTW Cloud, end to end, as `tun`
# (docs/protocol-cloud.md, Tunnel, in srcfl/ftw). Absent means a phone
# reaches the box through the relay, even when it knows the home id.
- cloud.tun
# The box can take a watch: it opens the relay's exchange socket and mints
# watch codes. Absent means the phone offers no Add watch. The wire is
# docs/watch-exchange.md in srcfl/ftw-webapp.
Expand Down Expand Up @@ -274,7 +278,9 @@ client_errors:
# the two into one enum cannot express "connected, but the inverter is quiet".
# ---------------------------------------------------------------------------
source_states: [live, lagging, stale, down, never]
carrier_states: [webrtc, relay, cloud, cache, none]
# tunnel is the box's own session carried by FTW Cloud, which cannot read it.
# cloud is the box-signed copy FTW Cloud holds and can read.
carrier_states: [webrtc, relay, tunnel, cloud, cache, none]

# ---------------------------------------------------------------------------
# History resolutions backed by what the box actually stores.
Expand Down
6 changes: 6 additions & 0 deletions docs/protocol-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,10 @@ Noise session, in the app protocol's `hello_ok.cloud`.
added later has no name on the phone until its next session with the box.
- The box lists `cloud.link` in `hello_ok.caps` when its build can run the
uplink. A build that names no service does not.
- It also lists `cloud.tun` when its link carries the phones' own sessions
as [`tun`](#tunnel). A phone sends its box session through the service
only to a box that said so at its last hello, or to one it has never
reached when a v3 QR named the home. Otherwise it uses the relay.

## Tunnel

Expand Down Expand Up @@ -1106,6 +1110,8 @@ to the app.
| `cloud_purposes` | List: `serve`, `stats` | `registry.yaml`, new block |
| `cloud.link` | Capability: the box can run the FTW Cloud uplink. Absent means the app uses the relay only and offers no FTW Cloud | `registry.yaml`, `capabilities` |
| `cloud` | Carrier state, between `relay` and `cache` | `registry.yaml`, `carrier_states` |
| `cloud.tun` | Capability: the box takes its app session through the service as `tun`. Absent means the phone uses the relay, even when it knows the home id | `registry.yaml`, `capabilities` |
| `tunnel` | Carrier state, between `relay` and `cloud`: the box's own session, carried by the service | `registry.yaml`, `carrier_states` |
| `cloud.granted`, `cloud.revoked` | Push kinds, sent to every owner when FTW Cloud is turned on or off | `push-catalogue.yaml` |

Class names, field names, close codes and the signing tags are not registry
Expand Down
12 changes: 9 additions & 3 deletions go/cmd/ftw/cloud_link.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,16 @@ func (c *cloudSession) load() *cloudlink.Link {
}

// caps adds cloud.link to an app session's capabilities when this build can
// run the uplink. A build that names no service says nothing.
// run the uplink, and cloud.tun when the link also carries the phones' own
// sessions. A build that names no service says nothing.
func (c *cloudSession) caps(caps []string) []string {
if link := c.load(); link != nil && link.Available() {
return append(slices.Clip(caps), appproto.CapCloudLink)
link := c.load()
if link == nil || !link.Available() {
return caps
}
caps = append(slices.Clip(caps), appproto.CapCloudLink)
if link.TakesTun() {
caps = append(caps, appproto.CapCloudTun)
}
return caps
}
Expand Down
45 changes: 45 additions & 0 deletions go/cmd/ftw/cloud_link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,53 @@ func TestAppSessionsHearOfFTWCloudOnlyWhenItCanRun(t *testing.T) {
t.Fatalf("source %q named %q", id, name)
}
}
if slices.Contains(caps, appproto.CapCloudTun) {
t.Fatalf("cloud.tun from a link that takes no tun: %v", caps)
}
}

// A phone hears cloud.tun only from a box whose link carries its session, so
// a phone that knows the home id never waits on a tunnel the box cannot take.
func TestAppSessionsHearOfTheTunnelOnlyWhenTheLinkTakesIt(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
keys := []*ecdsa.PublicKey{&key.PublicKey}
open := func(tunnel cloudlink.Tunnel, keys []*ecdsa.PublicKey) []string {
st, err := state.Open(filepath.Join(t.TempDir(), "state.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
link, err := cloudlink.New(cloudlink.Options{
Site: quietCloudSite{}, Roster: noPhones{}, KV: st, Tunnel: tunnel,
KeyPath: filepath.Join(t.TempDir(), "cloud.key"), ServiceKeys: keys,
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
})
if err != nil {
t.Fatal(err)
}
c := &cloudSession{}
c.bind(link)
return c.caps(appproto.DefaultCaps())
}
if caps := open(nil, keys); slices.Contains(caps, appproto.CapCloudTun) {
t.Fatalf("no tunnel: %v", caps)
}
if caps := open(nopTunnel{}, []*ecdsa.PublicKey{}); slices.Contains(caps, appproto.CapCloudTun) || slices.Contains(caps, appproto.CapCloudLink) {
t.Fatalf("a build that names no service: %v", caps)
}
if caps := open(nopTunnel{}, keys); !slices.Contains(caps, appproto.CapCloudTun) || !slices.Contains(caps, appproto.CapCloudLink) {
t.Fatalf("a link with a tunnel: %v", caps)
}
}

type nopTunnel struct{}

func (nopTunnel) Deliver(cloudwire.Tun) {}
func (nopTunnel) Drop() {}

// FTW Cloud's live body is the same field set the app session streams, and
// each phase follows the site meter's freshness there too. The fuse is
// config, so it names no driver.
Expand Down
1 change: 1 addition & 0 deletions go/internal/appproto/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ const (
CapPriceSpot = protocol.CapPriceSpot
CapApiPassthrough = protocol.CapApiPassthrough
CapCloudLink = protocol.CapCloudLink
CapCloudTun = protocol.CapCloudTun
CapCmdExchange = protocol.CapCmdExchange
ScopeLiveRead = protocol.ScopeLiveRead
ScopeHistoryRead = protocol.ScopeHistoryRead
Expand Down
4 changes: 4 additions & 0 deletions go/internal/cloudlink/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,10 @@ func (l *Link) Home() string {
// uplink at all. Release builds without service keys cannot.
func (l *Link) Available() bool { return len(l.opts.ServiceKeys) > 0 }

// TakesTun reports whether the link carries the phones' own sessions as tun,
// so a phone that knows the home id can reach the box without the relay.
func (l *Link) TakesTun() bool { return l.Available() && l.opts.Tunnel != nil }

// SessionInfo is what a phone needs, over its Noise session with the box, to
// reach this home through FTW Cloud: the home id, the key that signs the
// readable copy, the boot that anchors its times and names for the opaque
Expand Down
2 changes: 2 additions & 0 deletions go/protocol/contract_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.