feat: connectivity redesign phase 7 — cell-relay as a connection type
Unit Tests / test (push) Successful in 13m22s

cell exits surface as cell_relay connections via reconcile, bridged onto
the existing cell route_via mechanism, health from handshake, loop
detection, assignable in the unified UI

- CELL_RELAY_TYPE constant; not manually creatable
- reconcile_cell_relays() derives connections from cell links offering an
  exit (name "Cell: <cellname>", mark+table only, no iface/port/container)
- apply_routes bridges cell_relay to existing route_via path via
  apply_peer_route_via + cell firewall rules + set_exit_relay_active;
  keeps peer.route_via in sync
- _probe_cell_relay health from cell handshake + offer state
- _cell_relay_loops loop detection at assign and apply time
- FAILOPEN_DEFAULTS cell_relay=False
- set_peer_exit clears stale route_via on reassignment
- reconcile hooked into PUT /exit-offer and peer-sync/permissions handlers
- cell_link_manager + wireguard_manager wired into connectivity_manager
- UI: cell_relay in TYPE_META/GROUP_TYPES/GROUP_LABELS (Cells optgroup),
  removed "coming soon" placeholder
- 18 new tests in tests/test_connectivity_cell_relay.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 23:58:19 -04:00
parent 391d8ede48
commit 743b026b01
6 changed files with 688 additions and 14 deletions
@@ -70,7 +70,7 @@ function FailopenControl({ value, onChange, saving }) {
export default function AssignmentsPage() {
const toasts = useToasts();
const {
connections, peerExits, peerFailopen, serviceEgress, installed, peers, cells,
connections, peerExits, peerFailopen, serviceEgress, installed, peers,
loading, error, reload, setPeerExits, setPeerFailopen, setServiceEgress,
} = useConnectivityData();
@@ -78,7 +78,9 @@ export default function AssignmentsPage() {
const [savingFailopen, setSavingFailopen] = useState({});
const [savingService, setSavingService] = useState({});
// Build grouped options from connection instances + cell-relay placeholders.
// Build grouped options from connection instances. cell_relay connections
// (derived from cell links that offer an exit) flow through the same
// `connections` list and land in the "Cells" group automatically.
const options = (() => {
const groups = [];
Object.keys(GROUP_TYPES).forEach((group) => {
@@ -87,17 +89,6 @@ export default function AssignmentsPage() {
.map((c) => ({ id: c.id, label: `${c.name} (${typeMeta(c.type).short})` }));
if (items.length) groups.push({ label: GROUP_LABELS[group], items });
});
// Cell-relay: remote cells that offer their internet. Backend wiring for
// cell-relay exits lands in P7; surface them disabled so the option is
// discoverable without breaking assignment.
const relayItems = (cells || [])
.filter((c) => c.remote_exit_offered || c.exit_offered)
.map((c) => ({
id: `cell:${c.cell_name}`,
label: `${c.cell_name} (cell relay — coming soon)`,
disabled: true,
}));
if (relayItems.length) groups.push({ label: 'Cell relay', items: relayItems });
return { groups };
})();
+10
View File
@@ -50,6 +50,14 @@ export const TYPE_META = {
group: 'tor',
service: 'tor',
},
cell_relay: {
label: 'Cell relay',
short: 'Cell',
Icon: Network,
color: 'gray',
group: 'cells',
service: null,
},
};
// Subpage groups → which connection types they contain.
@@ -58,6 +66,7 @@ export const GROUP_TYPES = {
proxies: ['proxy'],
ssh: ['sshuttle'],
tor: ['tor'],
cells: ['cell_relay'],
};
export const GROUP_LABELS = {
@@ -65,6 +74,7 @@ export const GROUP_LABELS = {
proxies: 'Proxies',
ssh: 'SSH',
tor: 'Tor',
cells: 'Cells',
};
export function typeMeta(type) {