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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,32 @@ Both peers must use matching encryption/geofence or audio fails silently. `enabl

## How It Works

### Astation connections

Atem tries a configured direct endpoint first and then the identity relay after
it has learned the target Astation identity. Direct and relay connections use
the same device session, so one Atem can move between networks without pairing
again. Successful authentication stores `astation_relay_code` automatically;
manual configuration is only needed for an override or headless provisioning.

| Atem location | Endpoint | First connection | Internet required |
|---------------|----------|------------------|-------------------|
| Same Mac | `ws://127.0.0.1:8080/ws` | Transparent same-user proof | No |
| Another LAN machine | `ws://<astation-ip>:8080/ws` | Approve pairing in Astation | No |
| Remote network | Public WSS identity relay | Approve pairing in Astation | Yes |

All paths use device authentication v2. Astation sends a fresh challenge; Atem
proves possession of the local bootstrap token or its saved device-session token
with HMAC-SHA256. A session ID by itself is not accepted. Same-Mac operation
continues when Wi-Fi is disabled, and LAN operation needs no relay once the IP is
configured.

Direct LAN currently uses plaintext `ws://`. Authentication prevents a stolen
session ID from being sufficient, but it does not stop traffic inspection or an
active LAN attacker during initial pairing. Treat direct LAN as pre-production
until WSS certificate pinning is implemented. The wire contract and test matrix
are documented in [`designs/session-auth.md`](designs/session-auth.md).

### TUI Modes

| Mode | Description |
Expand Down Expand Up @@ -223,6 +249,7 @@ Files in `~/.config/atem/`:
| `convo.toml` | ConvoAI agent config (API keys, provider params) | None (chmod 0600) |
| `credentials.enc` | SSO tokens | AES-256-GCM (machine-bound) |
| `project_cache.enc` | Project list + active project selection | AES-256-GCM (machine-bound) |
| `sessions.json` | Per-Astation device session IDs and tokens | Plaintext (chmod 0600) |

Encrypted files are bound to the machine they were created on — copying them to another machine won't decrypt.

Expand All @@ -233,6 +260,7 @@ Encrypted files are bound to the machine they were created on — copying them t
```toml
# astation_ws = "ws://127.0.0.1:8080/ws"
# astation_relay_url = "https://station.agora.build"
# astation_relay_code = "astation-..." # learned automatically after authentication
# bff_url = "https://agora-cli.agora.io"
# sso_url = "https://sso2.agora.io"

Expand All @@ -245,6 +273,9 @@ Encrypted files are bound to the machine they were created on — copying them t
```bash
ATEM_BFF_URL=... # Override BFF API base URL
ATEM_SSO_URL=... # Override SSO base URL
ASTATION_WS=... # Direct loopback, LAN, or VPN WebSocket endpoint
ASTATION_RELAY_URL=... # Relay HTTP(S) base URL
ASTATION_RELAY_CODE=... # Target Astation identity room
AGORA_APP_ID=... # Override active project App ID
AGORA_APP_CERTIFICATE=... # Override active project certificate
```
Expand Down
4 changes: 4 additions & 0 deletions configs/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
# ── Astation ─────────────────────────────────────────────────────────
# astation_ws = "ws://127.0.0.1:8080/ws"
# astation_relay_url = "https://station.agora.build"
# astation_relay_code = "astation-..." # learned automatically after authentication
#
# For an offline LAN connection, point astation_ws at the Mac running Astation:
# astation_ws = "ws://192.168.1.20:8080/ws"

# ── Diagram hosting server ───────────────────────────────────────────
# diagram_server_url = "http://localhost:8787"
Expand Down
172 changes: 41 additions & 131 deletions designs/connection-priority.md
Original file line number Diff line number Diff line change
@@ -1,147 +1,57 @@
# Connection Priority Architecture
# Astation Connection Priority

## Overview
Status: implemented.

Atem now uses a clear, simple priority cascade for connecting to Astation:
## Order

```
1. Local URL (configurable) ← Same machine / LAN / VPN
2. Relay (wss://station.agora.build/ws) ← Remote connection
```

The local URL can be configured to support:
- **Same machine**: `ws://127.0.0.1:8080/ws` (default)
- **LAN**: `ws://192.168.1.5:8080/ws` (auto-detected by Astation)
- **VPN**: `ws://100.x.x.x:8080/ws` (Netbird, Tailscale, ZeroTier, etc.)

## Changes Made

### Astation (Server)

**Listen on all interfaces:**
- Changed from `127.0.0.1` to `0.0.0.0` so LAN clients can connect
- Now accessible from:
- Same machine: `ws://127.0.0.1:8080/ws`
- LAN: `ws://<local-ip>:8080/ws` (e.g., `ws://192.168.1.5:8080/ws`)
- Remote: via relay server only

**UI Updates:**
- Settings window now shows detected local network IP addresses
- Astation always listens on all interfaces (`0.0.0.0:8080`)
- Only Station relay URL needs configuration
- Status shows: "Listening on: ws://127.0.0.1:8080/ws, ws://192.168.1.5:8080/ws"
- Info text explains VPN IP configuration for Atem

**Files Modified:**
- `Sources/Menubar/AstationApp.swift` - Listen on `0.0.0.0`, added `getLocalNetworkIP()`
- `Sources/Menubar/SettingsWindowController.swift` - Updated UI, added server status display

### Atem (Client)
1. Connect to `astation_ws`, defaulting to `ws://127.0.0.1:8080/ws`.
2. Complete device authentication v2 on that direct socket.
3. If direct connection fails and an Astation identity was learned previously,
connect to that identity room at `astation_relay_url`.
4. Send `hello`, receive Astation's challenge through the relay, and complete
the same v2 session proof or pairing flow.
5. If both paths fail, continue without Astation and retry later.

**Connection Priority (simplified):**
There is no separate session-ID URL attempt. `connect_with_session` is a
compatibility alias; authentication always happens after the WebSocket opens.

```rust
// 1. Try configured local URL with session (if available)
config.astation_ws()?session=<id>
// Default: ws://127.0.0.1:8080/ws?session=<id>
// VPN: ws://100.x.x.x:8080/ws?session=<id>

// 2. Try configured local URL direct
config.astation_ws()
// Default: ws://127.0.0.1:8080/ws
// VPN: ws://100.x.x.x:8080/ws

// 3. Try relay with session (if available)
wss://station.agora.build/ws?session=<id>

// 4. Try relay with pairing code
wss://station.agora.build/ws?role=atem&code=<pairing-code>
```

**Configuration:**

For VPN connections (Netbird, Tailscale, ZeroTier), configure the VPN IP in `~/.config/atem/config.toml`:

```toml
astation_ws = "ws://100.64.0.2:8080/ws" # Netbird IP
# or
astation_ws = "ws://100.100.100.5:8080/ws" # Tailscale IP
```
## Configuration

For custom relay servers:
```toml
astation_relay_url = "http://100.117.91.44:8080" # Custom relay
# HTTP/HTTPS base URL - automatically converted to ws:// or wss://
```

Or via environment variables:
```bash
export ASTATION_WS="ws://100.64.0.2:8080/ws"
export ASTATION_RELAY_URL="http://100.117.91.44:8080"
```

**Benefits:**
- Always tries configured local URL first (lowest latency, most stable)
- Supports VPN IPs that Astation can't auto-detect
- Falls back to relay only when needed
- Session-based auth is seamless (no pairing code needed)
- Pairing code is last resort for explicit approval
# Same Mac, also the default
astation_ws = "ws://127.0.0.1:8080/ws"

**Files Modified:**
- `src/app.rs` - Refactored `spawn_astation_connect()` to use `config.astation_ws()`
- `src/config.rs` - Already supports `astation_ws` configuration

## Connection Scenarios

### Same Machine
```
Atem → ws://127.0.0.1:8080/ws → Astation
✅ Direct, fast, no auth needed
✅ Default configuration (no setup required)
```
# Or direct LAN/VPN
# astation_ws = "ws://192.168.1.20:8080/ws"
# astation_ws = "ws://100.64.0.20:8080/ws"

### Same LAN (Different Machines)
# Remote fallback
astation_relay_url = "https://station.agora.build"
astation_relay_code = "astation-..."
```
Atem → ws://192.168.1.5:8080/ws → Astation
✅ Direct, fast, no relay needed
✅ Configure astation_ws = "ws://192.168.1.5:8080/ws" in Atem config
```

### VPN (Netbird, Tailscale, ZeroTier)
```
Atem → ws://100.64.0.2:8080/ws → Astation (via VPN tunnel)
✅ Direct through VPN, no relay needed
✅ Configure astation_ws = "ws://<vpn-ip>:8080/ws" in Atem config
✅ Astation listens on 0.0.0.0 so VPN interface is accessible
```

### Different Networks (Remote, No VPN)
```
Atem → wss://station.agora.build/ws → Relay → Astation
✅ Via relay server, pairing code or session auth
✅ Fallback when direct connection fails
```

## Security Model

1. **Local connections** - Trusted (localhost or LAN)
2. **Session-based** - After HTTP auth, 30-day TTL
3. **Pairing code** - Explicit approval, short-lived (5 minutes)
Environment overrides are `ASTATION_WS`, `ASTATION_RELAY_URL`, and
`ASTATION_RELAY_CODE`. Successful authentication persists the relay code
automatically; the explicit value is an override for provisioning or recovery.

## Testing
## Network behavior

**Astation:**
- Run on macOS: `swift build && .build/debug/Astation`
- Check logs for network IP: "Network: ws://192.168.1.x:8080"
- Open Settings → Server Info → verify IP displayed
| Scenario | Direct | Relay | Result |
|----------|--------|-------|--------|
| Same Mac, radios disabled | Loopback succeeds | Not used | Fully offline |
| Separate machine, same LAN, internet down | LAN IP succeeds | Not used | Fully offline after local routing works |
| Separate networks | Direct normally fails | WSS succeeds | Internet and relay required |
| Direct and relay both reachable | Direct wins | Standby fallback | Lowest-latency path |

**Atem:**
- Run: `cargo run`
- Watch connection attempts in status bar
- Verify local connection tried first (check Astation logs)
- Disconnect Astation → verify relay fallback works
Direct LAN clients still require first-use approval and later HMAC proofs. LAN
reachability never grants the loopback policy. See `session-auth.md` for the
protocol and the current plaintext-LAN limitation.

## Version
## Operational checks

- Atem: v0.4.27 (pending release)
- Astation: v0.4.13 (pending release)
- Disable Wi-Fi on the Astation Mac and verify the default loopback connection.
- Configure a real LAN address from a second host and verify operation with the
internet uplink unavailable.
- Stop the direct listener and verify identity-relay fallback.
- Restore direct service and verify the same Astation session is reused.
- Confirm an invalid saved proof falls back to pairing without reconnecting.
Loading