From 3fc6169e0248c00b53c1ada44c1950429b6d3275 Mon Sep 17 00:00:00 2001 From: Andrew Chen Date: Fri, 4 Sep 2026 07:07:31 -0700 Subject: [PATCH 1/3] Back off between endpoint rebuilds that never register on a home relay A rebuild only helps when iroh's relay bookkeeping went stale. When the relay itself is unreachable the fresh endpoint never registers either (the rebuild factory tolerates a failed online wait), so the watchdog tripped again 180s later and every LAN client was dropped every few minutes for as long as the relay outage lasted. The watchdog now returns a RelayOutage that says whether a home relay was connected at any point of the watch, and takes its rebuild deadline as a parameter. run_server doubles that deadline for each consecutive endpoint that never registered (180s, 6m, 12m, 24m, capped at 30m) and resets it once an endpoint does register. The 60s nudge and the 30s bind-failure retry are unchanged. Same change as ezvpn's relay-watchdog branch; the module stays identical between the two repos. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PFw1C9hzWKyoieoxWVSqbA --- CLAUDE.md | 1 + crates/flextunnel-cli/src/main.rs | 55 +++++- .../flextunnel-core/src/transport/endpoint.rs | 4 +- .../src/transport/relay_watchdog.rs | 172 ++++++++++++++---- docs/architecture.md | 12 +- 5 files changed, 199 insertions(+), 45 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f4e3d2f..199a187 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,7 @@ - run cargo clippy and test after rust code changes - to run the CI workflow's clippy + test steps on all three host platforms against the working tree, use `ci/all.sh` — see `docs/local-ci.md`. Worth doing before a release, or after touching platform-gated code (`flextunnel-desktop`'s macOS/Windows backends), which Linux-only checks never compile. If host is Linux, run linux ci on the same host. - no cargo fmt +- no cargo test for flextunnel-desktop for linux because it is not available for linux - always use uv to run python scripts if needed - clients and server are expected to be trusted and error detections, for example, duplicate id detections are meant for preventing accidental misconfigurations such as running two clients or servers with the same id. - the desktop client (`flextunnel-desktop`) normally stores its config in the system keychain; set `FLEXTUNNEL_DEV_CONFIG=1` (or a file path) to store it as plaintext JSON instead, avoiding the macOS keychain access prompt on every unsigned rebuild. Development only — never set it for a real install (the auth secret key is stored unencrypted). diff --git a/crates/flextunnel-cli/src/main.rs b/crates/flextunnel-cli/src/main.rs index b2f9c37..9b6be6d 100644 --- a/crates/flextunnel-cli/src/main.rs +++ b/crates/flextunnel-cli/src/main.rs @@ -36,7 +36,7 @@ use flextunnel_core::transport::endpoint::{ EndpointAllowlists, RelayConfig, create_server_endpoint, secret_to_endpoint_id, server_rebuild_factory, }; -use flextunnel_core::transport::relay_watchdog; +use flextunnel_core::transport::relay_watchdog::{self, RelayOutage}; use flextunnel_core::{auth, config, secret}; #[derive(Parser)] @@ -623,6 +623,22 @@ const SHUTDOWN_CLOSE_TIMEOUT: Duration = Duration::from_secs(5); /// short — there is nothing to lose by trying again soon. const REBUILD_RETRY: Duration = Duration::from_secs(30); +/// Cap on the watchdog's rebuild deadline once consecutive rebuilt endpoints +/// keep failing to register on any home relay. +const REBUILD_DEADLINE_MAX: Duration = Duration::from_secs(30 * 60); + +/// The watchdog's rebuild deadline for the next serve pass, given how many +/// endpoints in a row never registered on a home relay: the usual +/// [`relay_watchdog::RELAY_OUTAGE_REBUILD`] after an endpoint that did +/// register, doubling per unregistered endpoint up to [`REBUILD_DEADLINE_MAX`] +/// (180s, 6m, 12m, 24m, 30m). Rebuilding while the relay itself is down +/// gains nothing and drops every LAN client, so it is done less and less +/// often; a relay that comes back resets the escalation. +fn rebuild_deadline(unregistered_endpoints: u32) -> Duration { + let factor = 1u32 << unregistered_endpoints.min(4); + (relay_watchdog::RELAY_OUTAGE_REBUILD * factor).min(REBUILD_DEADLINE_MAX) +} + /// Build the ephemeral `ServerConfig` for `server start --quick`: a full-tunnel /// routed set (`routed_domains = ["*"]`, `routed_cidrs = ["0.0.0.0/0", "::/0"]`) /// plus a freshly generated in-memory identity, returned *alongside* the config — @@ -885,8 +901,8 @@ async fn run_server( enum Pass { /// The server is done (clean or failed): close the endpoint and return. Exit(Result<()>), - /// The relay watchdog gave up on the endpoint after an outage this long. - Rebuild(Duration), + /// The relay watchdog gave up on the endpoint. + Rebuild(RelayOutage), } // Serve loop. A pass serves on the current endpoint until the server ends, @@ -897,13 +913,22 @@ async fn run_server( // one with the same identity, and serve again. The `ProxyServer` (its // registries, blocklist, status state) carries over; the old endpoint's // connections and bridge tasks end with it. + // + // A rebuild only helps when iroh's relay bookkeeping went stale. When the + // relay itself is unreachable the fresh endpoint never registers either, + // and rebuilding it again every few minutes would keep dropping the LAN + // clients that still work. So consecutive endpoints that never saw a home + // relay lengthen the watchdog's deadline (`rebuild_deadline`); one that + // did register resets the escalation. let mut endpoint = endpoint; + let mut unregistered_endpoints: u32 = 0; let res = loop { let pass = { let run = Arc::clone(&server).run(&endpoint); + let deadline = rebuild_deadline(unregistered_endpoints); let outage = async { if relay_watchdog_armed { - relay_watchdog::watch_home_relay(&endpoint).await + relay_watchdog::watch_home_relay(&endpoint, deadline).await } else { std::future::pending().await } @@ -925,12 +950,21 @@ async fn run_server( Pass::Rebuild(outage) => outage, }; + unregistered_endpoints = if outage.relay_seen { 0 } else { unregistered_endpoints + 1 }; log::error!( "No connected home relay for {:.0}s despite a network re-check; rebuilding the \ endpoint from scratch (server id stays {})", - outage.as_secs_f64(), + outage.duration.as_secs_f64(), endpoint.id() ); + if unregistered_endpoints > 0 { + log::error!( + "{unregistered_endpoints} endpoint(s) in a row never registered on any home \ + relay; the relay itself is probably unreachable. If the rebuilt endpoint does \ + not register either, the next rebuild waits {}s", + rebuild_deadline(unregistered_endpoints).as_secs() + ); + } close_endpoint_or_exit(&endpoint).await; endpoint = loop { match rebuild().await { @@ -991,6 +1025,17 @@ mod tests { use super::*; use std::collections::HashMap; + #[test] + fn rebuild_deadline_doubles_per_unregistered_endpoint_up_to_the_cap() { + let base = relay_watchdog::RELAY_OUTAGE_REBUILD; + assert_eq!(rebuild_deadline(0), base); + assert_eq!(rebuild_deadline(1), base * 2); + assert_eq!(rebuild_deadline(2), base * 4); + assert_eq!(rebuild_deadline(3), base * 8); + assert_eq!(rebuild_deadline(4), REBUILD_DEADLINE_MAX); + assert_eq!(rebuild_deadline(50), REBUILD_DEADLINE_MAX); + } + fn forwarder(suffix: &str) -> DnsForwarder { let mut m = HashMap::new(); m.insert(suffix.to_string(), vec!["10.0.0.53".to_string()]); diff --git a/crates/flextunnel-core/src/transport/endpoint.rs b/crates/flextunnel-core/src/transport/endpoint.rs index 9fa332b..e6e0c05 100644 --- a/crates/flextunnel-core/src/transport/endpoint.rs +++ b/crates/flextunnel-core/src/transport/endpoint.rs @@ -473,7 +473,9 @@ async fn bind_server_endpoint( /// recovery through the one relay that still answers. /// - **The online wait is tolerated failing.** A fresh endpoint is no worse /// than the wedged one it replaces — LAN clients can still find it over -/// mDNS — and the watchdog trips again if the relays stay unreachable. +/// mDNS — and the watchdog trips again if the relays stay unreachable +/// (with a lengthening deadline, so a dead relay does not churn the endpoint +/// every few minutes; see `run_server`). pub fn server_rebuild_factory( relay_config: RelayConfig, secret: SecretKey, diff --git a/crates/flextunnel-core/src/transport/relay_watchdog.rs b/crates/flextunnel-core/src/transport/relay_watchdog.rs index b7a5e64..4bf2829 100644 --- a/crates/flextunnel-core/src/transport/relay_watchdog.rs +++ b/crates/flextunnel-core/src/transport/relay_watchdog.rs @@ -16,16 +16,26 @@ //! 1. after [`RELAY_OUTAGE_NUDGE`] without a connected home relay it calls //! [`Endpoint::network_change`], which forces a fresh net report and relay //! re-selection (enough when only the bookkeeping went stale); -//! 2. after [`RELAY_OUTAGE_REBUILD`] it resolves, telling the caller to -//! replace the endpoint — the in-process equivalent of the restart that is -//! known to fix it. The caller (`run_server`) closes the wedged endpoint, -//! binds a fresh one with the same identity, and serves on again. +//! 2. after the caller's rebuild deadline ([`RELAY_OUTAGE_REBUILD`] by +//! default) it resolves, telling the caller to replace the endpoint — the +//! in-process equivalent of the restart that is known to fix it. The caller +//! (`run_server`) closes the wedged endpoint, binds a fresh one with the +//! same identity, and serves on again. +//! +//! The resolution also says whether a home relay was connected at *any* point +//! of the watch ([`RelayOutage::relay_seen`]). A rebuilt endpoint that never +//! registers is a sign the relay itself is unreachable, not that iroh's +//! bookkeeping went stale; rebuilding it again drops every LAN client for +//! nothing, so the caller backs off between such rebuilds by passing a longer +//! deadline. //! //! Only the *home* relay matters: non-home relays are connected on demand and //! dropped after a minute idle, which is normal and not an outage. +//! +//! Shared verbatim with ezvpn (`src/transport/relay_watchdog.rs`); keep the two in sync. -use iroh::{Endpoint, Watcher}; use iroh::endpoint::RelayStatus; +use iroh::{Endpoint, Watcher}; use std::future::Future; use std::time::Duration; use tokio::time::Instant; @@ -36,22 +46,34 @@ use tokio::time::Instant; /// ~25s cadence of its periodic net report. pub const RELAY_OUTAGE_NUDGE: Duration = Duration::from_secs(60); -/// How long from the start of the outage before the watchdog gives up on the -/// endpoint and asks for a rebuild. Leaves the nudge two minutes to take -/// effect (a net report through slow relays can take tens of seconds). +/// Default for how long from the start of the outage before the watchdog +/// gives up on the endpoint and asks for a rebuild. Leaves the nudge two +/// minutes to take effect (a net report through slow relays can take tens of +/// seconds). pub const RELAY_OUTAGE_REBUILD: Duration = Duration::from_secs(180); -/// Watch `endpoint`'s home-relay status and resolve — with the outage's -/// duration — once it has had no connected home relay for -/// [`RELAY_OUTAGE_REBUILD`], having nudged it with `network_change()` at -/// [`RELAY_OUTAGE_NUDGE`]. Never resolves while the home relay stays -/// connected; a reconnect at any point resets the clock. Pending forever once -/// the endpoint is gone. -pub async fn watch_home_relay(endpoint: &Endpoint) -> Duration { +/// A tripped watchdog: the endpoint should be replaced. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RelayOutage { + /// How long the endpoint has had no connected home relay. + pub duration: Duration, + /// Whether a home relay was connected at any point during the watch. + /// `false` means this endpoint never registered at all. + pub relay_seen: bool, +} + +/// Watch `endpoint`'s home-relay status and resolve once it has had no +/// connected home relay for `rebuild_after` (at least +/// [`RELAY_OUTAGE_NUDGE`]; [`RELAY_OUTAGE_REBUILD`] is the usual value), +/// having nudged it with `network_change()` at [`RELAY_OUTAGE_NUDGE`]. Never +/// resolves while the home relay stays connected; a reconnect at any point +/// resets the clock. Pending forever once the endpoint is gone. +pub async fn watch_home_relay(endpoint: &Endpoint, rebuild_after: Duration) -> RelayOutage { watch_outage( endpoint.home_relay_status(), |statuses| describe_statuses(statuses), || endpoint.network_change(), + rebuild_after, ) .await } @@ -77,20 +99,29 @@ fn describe_statuses(statuses: &[RelayStatus]) -> Result<(), String> { /// The watchdog proper, generic over the status source so it can be driven by /// a plain watchable in tests. `describe` classifies a status value -/// (`Ok` = connected); `nudge` is the first-stage remedy. -async fn watch_outage(mut watcher: W, describe: D, mut nudge: N) -> Duration +/// (`Ok` = connected); `nudge` is the first-stage remedy; `rebuild_after` is +/// the outage duration at which the watchdog trips. +async fn watch_outage( + mut watcher: W, + describe: D, + mut nudge: N, + rebuild_after: Duration, +) -> RelayOutage where W: Watcher, D: Fn(&W::Value) -> Result<(), String>, N: FnMut() -> Fut, Fut: Future, { + let rebuild_after = rebuild_after.max(RELAY_OUTAGE_NUDGE); let mut outage_since: Option = None; let mut nudged = false; + let mut relay_seen = false; let mut value = watcher.get(); loop { match describe(&value) { Ok(()) => { + relay_seen = true; if let Some(since) = outage_since.take() { log::info!( "Home relay connection restored after {:.0}s", @@ -119,11 +150,19 @@ where continue; }; - let deadline = since + if nudged { RELAY_OUTAGE_REBUILD } else { RELAY_OUTAGE_NUDGE }; + let deadline = since + + if nudged { + rebuild_after + } else { + RELAY_OUTAGE_NUDGE + }; tokio::select! { _ = tokio::time::sleep_until(deadline) => { if nudged { - return since.elapsed(); + return RelayOutage { + duration: since.elapsed(), + relay_seen, + }; } nudged = true; log::warn!( @@ -157,22 +196,41 @@ mod tests { /// Test double for the home-relay status: `true` = a home relay is /// connected. fn describe(connected: &bool) -> Result<(), String> { - if *connected { Ok(()) } else { Err("down".into()) } + if *connected { + Ok(()) + } else { + Err("down".into()) + } } - /// Run the watchdog on `status`, counting nudges. Returns the watchdog - /// future's resolution wrapped in a bounded wait so a test never hangs. + /// Run the watchdog on `status` with the default rebuild deadline, + /// counting nudges. Returns the watchdog future's resolution wrapped in a + /// bounded wait so a test never hangs. async fn run_for( status: &Watchable, nudges: Arc, bound: Duration, - ) -> Option { - let watchdog = watch_outage(status.watch(), describe, || { - let nudges = nudges.clone(); - async move { - nudges.fetch_add(1, Ordering::SeqCst); - } - }); + ) -> Option { + run_with_deadline(status, nudges, bound, RELAY_OUTAGE_REBUILD).await + } + + async fn run_with_deadline( + status: &Watchable, + nudges: Arc, + bound: Duration, + rebuild_after: Duration, + ) -> Option { + let watchdog = watch_outage( + status.watch(), + describe, + || { + let nudges = nudges.clone(); + async move { + nudges.fetch_add(1, Ordering::SeqCst); + } + }, + rebuild_after, + ); tokio::time::timeout(bound, watchdog).await.ok() } @@ -181,7 +239,10 @@ mod tests { let status = Watchable::new(true); let nudges = Arc::new(AtomicUsize::new(0)); let tripped = run_for(&status, nudges.clone(), RELAY_OUTAGE_REBUILD * 3).await; - assert!(tripped.is_none(), "healthy relay must never request a rebuild"); + assert!( + tripped.is_none(), + "healthy relay must never request a rebuild" + ); assert_eq!(nudges.load(Ordering::SeqCst), 0); } @@ -189,12 +250,40 @@ mod tests { async fn sustained_outage_nudges_then_requests_rebuild() { let status = Watchable::new(false); let nudges = Arc::new(AtomicUsize::new(0)); - let elapsed = run_for(&status, nudges.clone(), RELAY_OUTAGE_REBUILD * 2) + let outage = run_for(&status, nudges.clone(), RELAY_OUTAGE_REBUILD * 2) .await .expect("a sustained outage must request a rebuild"); - assert_eq!(nudges.load(Ordering::SeqCst), 1, "exactly one nudge before the rebuild"); - assert!(elapsed >= RELAY_OUTAGE_REBUILD); - assert!(elapsed < RELAY_OUTAGE_REBUILD + Duration::from_secs(1)); + assert_eq!( + nudges.load(Ordering::SeqCst), + 1, + "exactly one nudge before the rebuild" + ); + assert!(outage.duration >= RELAY_OUTAGE_REBUILD); + assert!(outage.duration < RELAY_OUTAGE_REBUILD + Duration::from_secs(1)); + assert!( + !outage.relay_seen, + "a relay that was never connected must be reported as never seen" + ); + } + + #[tokio::test(start_paused = true)] + async fn a_longer_rebuild_deadline_delays_the_trip_but_not_the_nudge() { + let status = Watchable::new(false); + let nudges = Arc::new(AtomicUsize::new(0)); + let rebuild_after = RELAY_OUTAGE_REBUILD * 4; + let nudge_count = nudges.clone(); + let (outage, ()) = tokio::join!( + run_with_deadline(&status, nudges.clone(), rebuild_after * 2, rebuild_after), + async move { + // The nudge still comes at the fixed first-stage deadline. + tokio::time::sleep(RELAY_OUTAGE_NUDGE + Duration::from_secs(1)).await; + assert_eq!(nudge_count.load(Ordering::SeqCst), 1); + } + ); + let outage = outage.expect("a sustained outage must request a rebuild"); + assert!(outage.duration >= rebuild_after); + assert!(outage.duration < rebuild_after + Duration::from_secs(1)); + assert_eq!(nudges.load(Ordering::SeqCst), 1); } #[tokio::test(start_paused = true)] @@ -236,7 +325,10 @@ mod tests { run_for(&status, nudges.clone(), RELAY_OUTAGE_REBUILD * 2), flipper ); - assert!(tripped.is_none(), "a relay that came back must not be rebuilt"); + assert!( + tripped.is_none(), + "a relay that came back must not be rebuilt" + ); assert_eq!(nudges.load(Ordering::SeqCst), 1); } @@ -260,10 +352,14 @@ mod tests { run_for(&status, nudges.clone(), RELAY_OUTAGE_REBUILD * 3), flipper ); - let elapsed = tripped.expect("second outage must eventually trip"); + let outage = tripped.expect("second outage must eventually trip"); assert_eq!(nudges.load(Ordering::SeqCst), 2); - assert!(elapsed >= RELAY_OUTAGE_REBUILD); - assert!(elapsed < RELAY_OUTAGE_REBUILD + Duration::from_secs(1)); + assert!(outage.duration >= RELAY_OUTAGE_REBUILD); + assert!(outage.duration < RELAY_OUTAGE_REBUILD + Duration::from_secs(1)); + assert!( + outage.relay_seen, + "the relay was connected between the outages, so it was seen" + ); // Second outage began at nudge + 20s; the trip comes a full window after that. let total = start.elapsed(); assert!(total >= RELAY_OUTAGE_NUDGE + Duration::from_secs(20) + RELAY_OUTAGE_REBUILD); diff --git a/docs/architecture.md b/docs/architecture.md index 7ab176b..307c5ff 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -273,6 +273,15 @@ client's reconnect loop: the previous `run` future (they are owned by a `JoinSet` per run). A failed rebuild is retried every `REBUILD_RETRY` (30s). +A rebuild only helps when iroh's bookkeeping went stale; when the relay itself +is unreachable the fresh endpoint never registers either, and rebuilding again +every three minutes would keep dropping the LAN clients that still work. The +watchdog therefore reports whether the endpoint held a home relay at any point +(`RelayOutage::relay_seen`), and the serve loop doubles the rebuild deadline +for each consecutive endpoint that never did (`rebuild_deadline`: 180s, 6m, +12m, 24m, then capped at `REBUILD_DEADLINE_MAX`, 30m). An endpoint that +registers resets the escalation to the usual 180s. The 60s nudge is unaffected. + A reconnect at any point resets the outage clock. Non-home relays are connected on demand and dropped after a minute idle, which is normal and never counts as an outage. With the default relays the watchdog is not armed: @@ -336,7 +345,8 @@ defenses. | `LIVENESS_WINDOW` | 33s | `transport/mod.rs` | | `RELAY_CONNECT_TIMEOUT` (`endpoint.online()`) | 10s | `transport/endpoint.rs` | | `RELAY_OUTAGE_NUDGE` (server relay watchdog) | 60s | `transport/relay_watchdog.rs` | -| `RELAY_OUTAGE_REBUILD` (server relay watchdog) | 180s | `transport/relay_watchdog.rs` | +| `RELAY_OUTAGE_REBUILD` (server relay watchdog, default deadline) | 180s | `transport/relay_watchdog.rs` | +| `REBUILD_DEADLINE_MAX` (server relay watchdog, escalated deadline cap) | 30m | `flextunnel-cli/src/main.rs` | | `REBUILD_RETRY` (server endpoint rebuild) | 30s | `flextunnel-cli/src/main.rs` | | `CONNECT_TIMEOUT` (client server connect) | 30s | `proxy/client.rs` | | `HANDSHAKE_TIMEOUT` | 10s | `proxy/client.rs`, `proxy/server.rs`, `proxy/bridge.rs` | From 81bfd5ce7d4381030fe5307604733f0c71ae0a87 Mon Sep 17 00:00:00 2001 From: Andrew Chen Date: Fri, 4 Sep 2026 07:09:48 -0700 Subject: [PATCH 2/3] fix --- AGENTS.md | 11 ++++++++++- CLAUDE.md | 11 +---------- 2 files changed, 11 insertions(+), 11 deletions(-) mode change 120000 => 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 681311e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..199a187 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +- strict no backward compatibility +- run cargo clippy and test after rust code changes +- to run the CI workflow's clippy + test steps on all three host platforms against the working tree, use `ci/all.sh` — see `docs/local-ci.md`. Worth doing before a release, or after touching platform-gated code (`flextunnel-desktop`'s macOS/Windows backends), which Linux-only checks never compile. If host is Linux, run linux ci on the same host. +- no cargo fmt +- no cargo test for flextunnel-desktop for linux because it is not available for linux +- always use uv to run python scripts if needed +- clients and server are expected to be trusted and error detections, for example, duplicate id detections are meant for preventing accidental misconfigurations such as running two clients or servers with the same id. +- the desktop client (`flextunnel-desktop`) normally stores its config in the system keychain; set `FLEXTUNNEL_DEV_CONFIG=1` (or a file path) to store it as plaintext JSON instead, avoiding the macOS keychain access prompt on every unsigned rebuild. Development only — never set it for a real install (the auth secret key is stored unencrypted). +- after rust changes that affect iOS (flextunnel-core or flextunnel-ffi, including the FFI config schema and `ios/flextunnel.h`), run `./build-ios.sh release` to rebuild `libflextunnel.xcframework` into `dist/ios/` (this script no longer writes into `../flextunnel-ios`). The iOS app links via its own Swift package (`../flextunnel-ios/Packages/Flextunnel`), which **defaults to the pinned GitHub release**, so it won't see local changes unless you build the app with `FLEXTUNNEL_LOCAL_XCFRAMEWORK=1` — that links this fresh `dist/ios` build through a committed symlink (set it for both `xcodegen generate` and `xcodebuild`, then clean-rebuild). This is an **extra step only needed when actively working on the iOS app side by side** (and only possible on macOS with Xcode + the iOS Rust targets); otherwise just skip it. +- the iroh transport layer shared with tunnel-rs and ezvpn — relays and address lookup, the per-relay startup probe, relay auth tokens, relay self-hosting — is documented once in https://github.com/flexaccessdev/iroh-common-architecture. Do not duplicate it in this repo; update it there and link to it. diff --git a/CLAUDE.md b/CLAUDE.md index 199a187..eef4bd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1 @@ -- strict no backward compatibility -- run cargo clippy and test after rust code changes -- to run the CI workflow's clippy + test steps on all three host platforms against the working tree, use `ci/all.sh` — see `docs/local-ci.md`. Worth doing before a release, or after touching platform-gated code (`flextunnel-desktop`'s macOS/Windows backends), which Linux-only checks never compile. If host is Linux, run linux ci on the same host. -- no cargo fmt -- no cargo test for flextunnel-desktop for linux because it is not available for linux -- always use uv to run python scripts if needed -- clients and server are expected to be trusted and error detections, for example, duplicate id detections are meant for preventing accidental misconfigurations such as running two clients or servers with the same id. -- the desktop client (`flextunnel-desktop`) normally stores its config in the system keychain; set `FLEXTUNNEL_DEV_CONFIG=1` (or a file path) to store it as plaintext JSON instead, avoiding the macOS keychain access prompt on every unsigned rebuild. Development only — never set it for a real install (the auth secret key is stored unencrypted). -- after rust changes that affect iOS (flextunnel-core or flextunnel-ffi, including the FFI config schema and `ios/flextunnel.h`), run `./build-ios.sh release` to rebuild `libflextunnel.xcframework` into `dist/ios/` (this script no longer writes into `../flextunnel-ios`). The iOS app links via its own Swift package (`../flextunnel-ios/Packages/Flextunnel`), which **defaults to the pinned GitHub release**, so it won't see local changes unless you build the app with `FLEXTUNNEL_LOCAL_XCFRAMEWORK=1` — that links this fresh `dist/ios` build through a committed symlink (set it for both `xcodegen generate` and `xcodebuild`, then clean-rebuild). This is an **extra step only needed when actively working on the iOS app side by side** (and only possible on macOS with Xcode + the iOS Rust targets); otherwise just skip it. -- the iroh transport layer shared with tunnel-rs and ezvpn — relays and address lookup, the per-relay startup probe, relay auth tokens, relay self-hosting — is documented once in https://github.com/flexaccessdev/iroh-common-architecture. Do not duplicate it in this repo; update it there and link to it. +@AGENTS.md \ No newline at end of file From fa022ff5e30701bce9ac60dfda1838b4481e2628 Mon Sep 17 00:00:00 2001 From: Andrew Chen Date: Fri, 4 Sep 2026 07:13:29 -0700 Subject: [PATCH 3/3] Bump version to 0.0.72 for flextunnel packages --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e350cc..44e57c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1929,7 +1929,7 @@ dependencies = [ [[package]] name = "flextunnel-cli" -version = "0.0.71" +version = "0.0.72" dependencies = [ "anyhow", "clap", @@ -1945,7 +1945,7 @@ dependencies = [ [[package]] name = "flextunnel-core" -version = "0.0.71" +version = "0.0.72" dependencies = [ "anyhow", "askama", @@ -1977,7 +1977,7 @@ dependencies = [ [[package]] name = "flextunnel-desktop" -version = "0.0.71" +version = "0.0.72" dependencies = [ "aes-gcm", "anyhow", @@ -2006,7 +2006,7 @@ dependencies = [ [[package]] name = "flextunnel-ffi" -version = "0.0.71" +version = "0.0.72" dependencies = [ "flextunnel-core", "iroh", diff --git a/Cargo.toml b/Cargo.toml index ca64865..2d81dfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ default-members = [ ] [workspace.package] -version = "0.0.71" +version = "0.0.72" edition = "2024" description = "SOCKS5/HTTP-proxy-over-QUIC split tunnel via iroh P2P"