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
8 changes: 4 additions & 4 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ default-members = [
]

[workspace.package]
version = "0.0.79"
version = "0.0.80"
edition = "2024"
description = "SOCKS5/HTTP-proxy-over-QUIC split tunnel via iroh P2P"
description = "SOCKS5/HTTP proxy and port forwards over QUIC split tunnel via iroh P2P"

[workspace.dependencies]
anyhow = "1"
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# flextunnel

A SOCKS5/HTTP-proxy-over-QUIC split tunnel. The **client** runs optional local
SOCKS5 and HTTP proxy listeners. Each request is matched
A SOCKS5/HTTP-proxy- and port-forward-over-QUIC split tunnel. The **client**
runs optional local SOCKS5 and HTTP proxy listeners, plus optional port
forwards that send a local port straight to one server-side address. Each
proxy request is matched
against the server-pushed tunnel set: routed targets are tunneled as reliable
QUIC bi-streams to the **server**, which performs **DNS resolution and the
outbound TCP connection from its own network**, then pipes bytes back; off-list
targets are connected directly from the client device.
targets are connected directly from the client device. A forwarded port does no
matching on the client: everything it accepts goes to the server, which
enforces its routed set there.

This lets you reach hosts that are only reachable from the server side — a
private network, the server's own `localhost`, or names that only resolve via
Expand Down Expand Up @@ -618,12 +622,12 @@ Auto-reconnect is **enabled by default** (`auto_reconnect = true`); pass

- A failed connection attempt — the **first one included** — or a lost
connection is retried with **exponential backoff + jitter** (1s doubling to
5 min), indefinitely, unless `--max-reconnect-attempts` caps it or
60s), indefinitely, unless `--max-reconnect-attempts` caps it or
auto-reconnect is disabled. A server that is down, or not up yet, is the
ordinary case, not a reason to exit: the client waits it out and connects
when the server appears.
- A long outage is cheap to sit through: once the backoff reaches its cap the
client makes one bounded connect attempt every five minutes. Repeated
client makes one bounded connect attempt a minute. Repeated
failures escalate to rebuilding the iroh endpoint from scratch after the
third one, and then at most every 30 minutes for as long as the outage
lasts (see [`docs/architecture.md`](docs/architecture.md#reconnect-policy-client)).
Expand Down
5 changes: 3 additions & 2 deletions crates/flextunnel-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! flextunnel
//!
//! A SOCKS5/HTTP-proxy-over-QUIC split tunnel via iroh P2P connections. The
//! A SOCKS5/HTTP-proxy- and port-forward-over-QUIC split tunnel via iroh P2P
//! connections. The
//! client runs optional local SOCKS5/HTTP proxy listeners and server-direct
//! port forwards (declared in its config; `flextunnel client control` shows
//! their state); routed
Expand Down Expand Up @@ -40,7 +41,7 @@ use flextunnel_core::{auth, config, secret};
#[derive(Parser)]
#[command(name = "flextunnel")]
#[command(version)]
#[command(about = "SOCKS5/HTTP-proxy-over-QUIC split tunnel via iroh P2P")]
#[command(about = "SOCKS5/HTTP proxy and port forwards over QUIC split tunnel via iroh P2P")]
struct Args {
#[command(subcommand)]
command: Command,
Expand Down
2 changes: 1 addition & 1 deletion crates/flextunnel-cli/src/tui/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ fn header_lines(s: &StatusSnapshot) -> Vec<Line<'static>> {
first.push(Span::styled(format!(" for {}", format_uptime(secs)), DIM));
}
// While down, say how far the retry loop has got and when it tries again,
// so a backoff step of minutes reads as waiting, not stuck.
// so a backoff step reads as waiting, not stuck.
if s.failed_attempts > 0 {
let next = match s.next_attempt_secs {
Some(secs) if secs > 0 => format!("next in {}", format_uptime(secs)),
Expand Down
3 changes: 2 additions & 1 deletion crates/flextunnel-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! flextunnel
//!
//! A SOCKS5/HTTP-proxy-over-QUIC split tunnel via iroh P2P connections. The
//! A SOCKS5/HTTP-proxy- and port-forward-over-QUIC split tunnel via iroh P2P
//! connections. The
//! clients may run local SOCKS5/HTTP proxy listeners or server-direct loopback
//! forwards; routed targets are reliable QUIC bi-streams to the server, which
//! resolves DNS and connects from its own network. Uses a fixed ALPN for
Expand Down
23 changes: 12 additions & 11 deletions crates/flextunnel-core/src/proxy/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ use tokio::sync::{Semaphore, watch};

/// Reconnect backoff: base 1s, doubling per consecutive failed attempt, capped
/// here. The early steps (1s, 2s, 4s, …) catch a server restart within a
/// minute or two; past them the doubling runs on up to the cap, so a server
/// that stays down for hours or days is probed once every five minutes for as
/// long as it takes. Each probe is one bounded connect ([`CONNECT_TIMEOUT`])
/// on the endpoint the client already holds, so an outage of any length is
/// cheap to sit through — at the price of noticing the server's return up to
/// five minutes late. Events that make an earlier attempt worthwhile cut the
/// wait short (see [`ProxyClient::wait_backoff`]).
const RECONNECT_BACKOFF_MAX: Duration = Duration::from_secs(300);
/// minute or two; past them the doubling settles at the cap, so a server that
/// stays down for hours or days is probed once a minute for as long as it
/// takes. Each probe is one bounded connect ([`CONNECT_TIMEOUT`]) on the
/// endpoint the client already holds, so an outage of any length is cheap to
/// sit through while still noticing the server's return within a minute — a
/// wait of several minutes saves little and reads as a hang to anyone
/// watching. Events that make an earlier attempt worthwhile cut the wait
/// short (see [`ProxyClient::wait_backoff`]).
const RECONNECT_BACKOFF_MAX: Duration = Duration::from_secs(60);
/// Escalate to a full endpoint rebuild once this many consecutive attempts of
/// an outage have failed. The attempts before it get the cheap
/// `network_change()` nudge, which repairs dead UDP sockets; a wedge that
Expand Down Expand Up @@ -862,7 +863,7 @@ impl ProxyClient {
/// reconnect. While the path is up this is a plain backoff sleep, except
/// that a mid-sleep loss switches to parking, and that the embedding app
/// coming to the foreground ends the sleep the same way a restored path
/// does — a backoff step sized for an unattended outage (up to
/// does — a backoff step sized for a long outage (up to
/// [`RECONNECT_BACKOFF_MAX`]) must not keep a user who is looking waiting.
///
/// Returns whether the wait was cut short by one of those events (the
Expand Down Expand Up @@ -2180,12 +2181,12 @@ mod tests {
#[test]
fn backoff_doubles_to_the_cap() {
let jitter = Duration::from_millis(RECONNECT_JITTER_MAX_MS);
for (attempt, secs) in [(0, 1), (1, 1), (2, 2), (3, 4), (7, 64), (9, 256)] {
for (attempt, secs) in [(0, 1), (1, 1), (2, 2), (3, 4), (6, 32)] {
let b = calculate_backoff(attempt);
let base = Duration::from_secs(secs);
assert!(b >= base && b <= base + jitter, "attempt {attempt}: {b:?}");
}
for attempt in [10, 11, 20, u32::MAX] {
for attempt in [7, 9, 10, 20, u32::MAX] {
let b = calculate_backoff(attempt);
assert!(
b >= RECONNECT_BACKOFF_MAX && b <= RECONNECT_BACKOFF_MAX + jitter,
Expand Down
2 changes: 1 addition & 1 deletion crates/flextunnel-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ pub unsafe extern "C" fn flextunnel_close_listeners(handle: *const FlextunnelHan
/// heartbeat — the connection's only periodic traffic — slows from 10s to 60s,
/// keeping the cellular radio in its low-power state almost the whole time; the
/// foreground flip snaps it back and sends any overdue beat immediately, and if
/// the core is sitting out a reconnect backoff (up to 5 min once a long outage
/// the core is sitting out a reconnect backoff (up to 60s once a long outage
/// has pushed it to the cap) it ends that wait and attempts at once with a
/// fresh backoff series. Idempotent; safe to call with the same value
/// repeatedly.
Expand Down
8 changes: 4 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ Implemented in `ProxyClient::run` / `handle_failure`:

- Every recoverable failure (`ConnectionLost` / `Network` / `Signaling` — see
`ProxyError::is_recoverable`) is retried with **exponential backoff +
jitter** (1s doubling to `RECONNECT_BACKOFF_MAX`, 5 min), indefinitely,
jitter** (1s doubling to `RECONNECT_BACKOFF_MAX`, 60s), indefinitely,
unless `--max-reconnect-attempts` caps it or `--no-auto-reconnect` disables
it. The first attempt is no different from any later one: a server that is
down or not up yet is the ordinary case (boot order, maintenance), not an
Expand All @@ -233,11 +233,11 @@ Implemented in `ProxyClient::run` / `handle_failure`:
- Permanent errors (`AuthenticationFailed` / `Config`) never retry — the same
credential and config would fail the same way every time.
- A long outage costs one bounded connect (`CONNECT_TIMEOUT`) per attempt on
the endpoint the client already holds, once every five minutes at the cap.
the endpoint the client already holds, once a minute at the cap.
Two events cut a backoff step short with a fresh series: the device
reporting its network path back (`set_network_available`) and the embedding
app coming to the foreground (`set_background(false)`), so a user who is
looking never waits out a step sized for an unattended outage.
looking never waits out a step sized for a long outage.
- An outage's **third** consecutive failure escalates to a **full endpoint
rebuild** (`ClientEndpoint::rebuild`), repeated at most every
`REBUILD_ENDPOINT_MIN_INTERVAL` (30 min) for as long as the outage lasts; the
Expand Down Expand Up @@ -369,7 +369,7 @@ defenses.
| `TUNNEL_OPEN_TIMEOUT` | 30s | `proxy/client.rs` |
| `CONNECT_TIMEOUT` (server dial) | 10s | `proxy/dial.rs` |
| `MAX_CONCURRENT_CONNECTIONS` | 1024 | `proxy/server.rs` |
| reconnect backoff | 1s → 5 min + ≤500ms jitter | `proxy/client.rs` |
| reconnect backoff | 1s → 60s + ≤500ms jitter | `proxy/client.rs` |
| `REBUILD_ENDPOINT_ATTEMPTS` / `REBUILD_ENDPOINT_MIN_INTERVAL` (client endpoint rebuild) | 3rd failure, then ≥30 min apart | `proxy/client.rs` |
| `MAX_HANDSHAKE_SIZE` | 64 KiB | `proxy/signaling.rs` |
| `MAX_CONTROL_MSG_SIZE` | 16 KiB | `proxy/signaling.rs` |
Expand Down
6 changes: 3 additions & 3 deletions docs/systemd.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ The client already supervises itself where it matters:

- Auto-reconnect (on by default) retries every failed connection attempt and
every lost connection internally with exponential backoff (1s doubling to
5 min), indefinitely — the first attempt included. A server that is down
60s), indefinitely — the first attempt included. A server that is down
when the unit starts, or a network that isn't up yet at boot, is waited
out, not exited on: the client connects when the server appears, and there
is no user-manager `network-online.target` to order against nor any need
for one. A long outage costs one bounded connect attempt every five
minutes. Reconnects that keep failing escalate to rebuilding the iroh
for one. A long outage costs one bounded connect attempt a minute.
Reconnects that keep failing escalate to rebuilding the iroh
endpoint from scratch (after the third failure, then at most every 30
minutes) — the in-process equivalent of a unit restart, covering wedges (a
dead relay link, stale path state) that only a fresh endpoint repairs. The
Expand Down
2 changes: 1 addition & 1 deletion ios/flextunnel.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ int flextunnel_close_listeners(const FlextunnelHandle *handle);
* traffic — slows from 10s to 60s so an idle session wakes the cellular radio
* once a minute instead of six times; the foreground flip snaps it back and
* sends any overdue beat immediately, and ends any reconnect backoff in
* progress (up to 5 min once a long outage has pushed it to the cap) so the
* progress (up to 60s once a long outage has pushed it to the cap) so the
* next attempt runs at once with a fresh backoff series. Idempotent.
*
* Returns 1 on success and -1 for a NULL handle.
Expand Down
Loading