Commit Graph
114 Commits
Author SHA1 Message Date
KKRainbow c66076b8f3 fix(udp): repair symmetric hole-punch socket handoff and retries (#2774)
* fix(hole-punch): repair symmetric UDP candidate and handshake flow

Move punched sockets out of the candidate array before publishing them,
and keep transaction registration and results in one map. This prevents
receiver cleanup from racing with socket reuse or port rebinding and
removes the unsafe reference-count assertion.

Refill hard-symmetric candidates consumed before transport admission.
Refresh easy-symmetric candidates before taking a new STUN baseline so
prediction covers their new mappings. Keep probing a selected both-easy
socket during session setup so a peer still receiving raw HolePunch
packets can switch to its listener even after one-way probe loss.

Keep predicted ports within 1..=65535, permit a single remaining port,
and cover the full prediction interval for every advertised public IP.
Preserve the existing random scan budgets, retry policy and wire format.

Add regression coverage for candidate depletion, prediction retries,
receiver handoff, continued probing, multiple IPs and port boundaries.
Validate all 877 core tests, full workspace Clippy and NAT mapping
probes that exchange payloads through the production UDP session code.

Related to #2766; its field root cause remains unconfirmed.

* fix(hole-punch): retain candidate reuse and prediction budgets

Remove the unconditional easy-symmetric array reset so retries reuse the
existing 84-socket pool. Keep refilling consumed slots to avoid
permanent depletion after failed transport admission.

Preserve the predictable scan budget for multiple public IPs instead of
multiplying it by address count. Defer prediction freshness and coverage
changes until they can be addressed within the existing pressure limits.

Verify that two failed easy-symmetric attempts reuse all candidates,
that consumed candidates still recover, and that a 50-target prediction
budget with two public IPs retains the original 300-datagram total.
2026-10-29 12:31:38 +08:00
Alrcatraz 4bdba58ca9 feat(web): enable tld_dns_zone end-to-end from Web Console to MagicDNS (#2425) 2026-10-28 20:37:54 +08:00
6487a5e463 fix(peers): don't report unmeasured connections as 1 ms to peer center (#2768)
* fix(peers): don't report unmeasured connections as 1 ms to peer center

A freshly established connection has no latency sample yet, so its
WindowLatency reports 0 and direct_peer_info advertised the peer as
max(1, 0) = 1 ms. With latency_first enabled, every node could then
route through a slow or lossy link until the next report (up to ~60 s),
and a flapping link re-advertised 1 ms on every reconnect.

Skip connections that have no latency sample yet. A peer with no
measured connection is left out of the report; it is reported once its
first sample arrives, because the peer list changes then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(mobile): use the Android command-line tools build ID

setup-android expects a download build ID, not the displayed tools
version. The value 12.0 becomes 12 and produces a URL that returns 404
when the runner cannot reuse a preinstalled SDK.

Request build 11076708, which provides the same command-line tools
12.0. Keep the runner and other tool versions unchanged.

Validate the workflow YAML and confirm the archive URL returns HTTP 200.

---------

Co-authored-by: imoulei <188999141+imoulei@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com>
2026-10-28 12:27:23 +08:00
FaandKKRainbow 3463190838 fix(dataplane): finish TCP teardown after stream close (#2754)
Dropping a data-plane TCP stream removed its socket without sending FIN,
leaving peer reads and exit-node forwarding tasks waiting indefinitely.

Transfer released smoltcp sockets, flow registrations, and stack leases
to the existing reactor. Preserve FIN retransmissions and TIME-WAIT
after application handles close, drain unread peer data, and reclaim
sockets on completion, an absolute close deadline, or reactor shutdown.

Reuse session resource closure to cancel pending reads and writes. Keep
the Go host on the existing resource-close ABI and refresh its embedded
WASI guest, protobuf bindings, and matching provenance together.

Cover blocked writes, expired deadlines, lost FIN and ACK packets, close
deadlines, reactor shutdown, and final flow and stack lease release.
Validate with 77 Rust tests, the Go suite, fmt, and strict workspace
clippy.

Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com>
2026-10-27 00:32:23 +08:00
KKRainbow cf6531b302 fix(peer): keep active IP flows on their selected connection (#2753)
Periodic minimum-RTT reselection can move a busy UDP stream 
to an idle TCP connection and back as their queueing delays 
change. Bind active unfragmented TCP/UDP flows to their selected
connection so the five-second reselection applies to new flows and 
unkeyed packets.
2026-10-26 23:31:06 +08:00
KKRainbow 4ae9deb72b fix(net): recover peer, Web, and WireGuard portal listeners (#2760)
Keep peer listener reconstruction alive after runtime bind failures.
Withdraw failed bindings from the running registry before retrying.
Web configuration listeners retry listen after accept errors with a
fixed 100 ms delay, until their owner is stopped.

Make TCP and UDP listen replace the old binding while retaining the
assigned port. Keep accept ownership unchanged for other consumers.
Release closed UDP sessions' layer references and wake classified
accepts on receive shutdown so peer listeners can be reconstructed.

Let the existing WireGuard Portal reader replace its socket through a
shared endpoint slot, retaining the engine and client sessions. Discard
oversized Windows UDP datagrams before they reach listener recovery.

Cover repeated binding failure, cancellation, fixed ports, old socket
release, classified shutdown ordering, and successful Web recovery.
2026-10-26 21:34:50 +08:00
KKRainbow 6a5507b41e fix(icmp): open proxy sockets on demand and activate dynamic subnets (#2759)
* fix(icmp): open proxy sockets only when forwarding echo requests

Keep local no-TUN echo replies independent of raw socket permissions.
Create the socket and receiver on the first outward request, so newly
added proxy networks and exit-node traffic work without eager sockets.

Serialize concurrent initialization and socket publication with stop.
Roll back partial resources on failure or cancellation, and retain an
opened socket until shutdown for in-flight replies and later requests.

Cover local replies, dynamic CIDR updates, concurrent first requests,
exit-node forwarding, failed opens, cancellation and shutdown.

* fix(proxy): start forwarding after the first dynamic subnet update

A TUN instance started without proxy networks skips the packet proxy.
Runtime updates previously published new CIDRs without starting it, so
the first dynamically added subnet could not forward packets.

Reapply the existing startup predicate after configuration publication.
Run proxy startup on the instance executor so its sockets and tasks
survive a temporary configuration caller. Roll back partial startup on
cancellation as well as errors, allowing a subsequent update to retry.

Cover first-subnet forwarding, mapped CIDRs, removal and re-addition,
system forwarding, bind failure, cancellation and executor ownership.
2026-10-25 23:29:37 +08:00
79dd97dd7f fix(wasi): advertise the compiled smoltcp stack so subnet proxy can forward (#2734)
* fix(wasi): advertise the compiled smoltcp stack and keep ring out of the guest

A WASI guest has no OS TUN device, so the TCP subnet proxy can only forward
through the user-space stack. The capability probe in the WASI runtime only
set smoltcp_available under wasm-host-tunnel-outbound, so a plain core build
left force_smoltcp false and the proxy fell back to the kernel-listener path,
which rewrites the SYN onto a device that does not exist and lets every entry
expire as "syn nat entry expired".

- Add CoreInstanceHostConfig::set_smoltcp_stack and call it from the WASI
  runtime whenever the stack is compiled in. The setter only raises the flag,
  so the host-tunnel mode keeps working independently of the feature.
- wasi-crypto-offload no longer pulls ring-crypto: the host performs AEAD for
  the guest (easytier_host.crypto_aead_seal/open), and a software backend adds
  no value inside WASI.
- build-wasi-core.sh now builds with --no-default-features and an explicit
  list. endpoint-discovery was the only default that pulled rustls/ring; ring
  compiles C shims for wasm32-wasip1 that surface as env.ring_core_* imports,
  which the Go host cannot provide.

Covered by two unit tests for the setter.

* style(wasi): remove extra blank line from runtime module

Remove the extra blank line at the end of the WASI runtime module.
This restores the repository-wide cargo fmt check for PR #2734.

---------

Co-authored-by: 刘金发 <liujinfa9242@dingtalk.com>
Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com>
2026-10-25 19:34:33 +08:00
fe18f32c8b fix(peer_manager): 按 NIC 过滤器改写后的目的地址路由 (#2740)
* fix(peer_manager): route by destination rewritten in the nic packet pipeline

NIC packet filters receive &mut ZCPacket and may rewrite the IP header (e.g. a NAT filter that maps a virtual peer address to an address on the local subnet). send_msg_by_ip parsed the destination before running the filter pipeline and then routed by that stale address, so rewritten packets were looked up by the pre-filter destination and dropped as unreachable.

Re-parse the destination after run_nic_packet_process_pipeline and route by the rewritten address. Expose instance::packet_io as pub(crate) so peers can reuse the parser.

* test(peer-manager): cover NIC destination rewrite routing

Exercise IPv4 and IPv6 delivery through three peers connected by memory
tunnels. A NIC filter rewrites the destination without setting a peer
ID; verify delivery to the rewritten peer and no delivery to the
original.

Cover original destinations with and without an existing route. Wait for
route convergence before sending each packet once, without data retries.

Both regression tests fail with the PR routing fix removed and pass with
it restored. All 37 peer-manager tests and formatting checks pass.
Scoped Clippy completes with warnings in pre-existing code.

---------

Co-authored-by: ldoubil <ldoubil@users.noreply.github.com>
Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com>
2026-10-25 13:13:32 +08:00
KKRainbow 5565251562 test(limiter): stop racing the token bucket refill clock (#2752)
* test(limiter): stop racing the token bucket refill clock

test_initial_state drains a 1000 byte bucket that refills at 1000
bytes/s and then asserts that one more token is rejected. try_consume
refills from Instant::now() first, so the assertion only holds while
the thread runs within one millisecond of the drain; one millisecond
of scheduling delay is enough for the token to appear.

Freeze the lazy refill by moving last_refill into the future so no
token can appear between the two calls, and apply the same freeze to
test_consumption, which had the same race with a 100 ms window.

* test(limiter): pass the refill instant explicitly instead of faking the clock

Moving last_refill into the future only widened the window: once that
offset passes the bucket refills again, so the tests still depended on
the wall clock.

Split the locked consumption into try_consume_locked(state, tokens, now)
and have production take the lock first, then read the clock. Both
tests take one instant and reuse it for every call, so no refill can
happen between the assertions.
2026-10-25 12:57:13 +08:00
KKRainbow 887c701ee8 test(connector): fix foreign listener discovery readiness and isolation (#2750)
* test(connector): label the convergence waits of the direct connect test

foreign_network_advertises_listeners_for_direct_connect failed in run
39903378976 with a bare "Timeout" from wait_for_condition, so the log
cannot tell which convergence gate was hit: each of the two node pairs
has four waits (remote sees client-net, client learns the route,
remote learns the client peer, direct connection established).

Add wait_for_condition_with_reason and use it for all four waits, with
the node pair and the step in the message. Timeouts and assertions are
unchanged.

* test(connector): wait for the reply route the router actually uses

The reverse-route wait added in #2738 checked the foreign network peer
list, which only holds direct connections. The packet router also
routes through the route table, so the wait could time out although a
reply would have been routed, and a direct connection often appears
only later in the flow.

Call the router lookup (get_gateway_peer_id for the client peer on the
remote foreign network) through a test accessor and wait until any
gateway is available.

CI run 39911533854 timed out on the previous condition for both tcp
and udp.

* test(connector): wait for the reply route the router actually uses

The reverse-route wait added in #2738 checked the foreign network peer
list, which only holds direct connections. The packet router routes
through the route table as well (get_gateway_peer_id), so the wait
could time out although a reply would have been routed, and a direct
connection often appears only later in the flow.

Add a test accessor that performs the router lookup on the remote
foreign network and wait until any gateway is available.

Drops the gate labels from this branch, they are covered by #2749.

CI run 39911533854 timed out on the previous condition for both tcp
and udp.

* test(connector): report remote routing state when get_ip_list times out

CI run 39921692334 failed after the gateway wait succeeded: the RPC
itself timed out with all waits green, so the gateway was available
but the reply still did not arrive.

On RPC error, print the remote client-net state next to the error:
the gateway chosen for the client, whether that gateway is a direct
peer, and the peer ids currently connected. This makes the next
failure attributable without changing any assertion or timeout.

* test(connector): also report the origin mapping of the chosen gateway

A wrapped foreign network packet is forwarded by mapping the gateway
foreign peer id to its origin (parent network) peer id and sending it
through the parent peer map, so a gateway that is not directly
connected does not by itself mean the reply is dropped.

Extend the RPC failure snapshot with get_origin_my_peer_id for the
chosen gateway and whether the parent peer map can route to that
origin id.

* test(connector): capture the foreign network forwarding decisions

The routing snapshot taken after the timeout cannot explain a lost RPC
reply: it is sampled after the timeout, re-queries the state (which may
converge later) and does not show whether the request reached the
remote at all.

Install a test-scoped tracing subscriber that keeps debug events from
easytier_core::peers, so a failing run prints the forwarding decisions
(dropped relay data, missing origin peer id, failed direct send) when
they happen. Only this test process is affected.

* test(connector): isolate foreign listener discovery

The foreign DirectConnectorRpc handler collects interface addresses in
the default network namespace. Although the three-node fixture gives
each instance its own namespace, the RPC still observes host bridges
and veth interfaces created and removed by concurrent tests.

Add both protocol cases to the existing exclusive override for host
interfaces. Preserve the reply route readiness check, RPC deadline,
listener assertion, and final direct connection assertion.

Diagnostics with 32 test threads on four CPUs showed interface
enumeration delaying the RPC handler by over two seconds. With this
override, the same mixed workload kept all observed handlers below
1.1 ms.

Remove the temporary routing snapshot accessors, failure dump, and
test tracing setup. Keep only the helpers used by the readiness check.
2026-10-25 11:49:06 +08:00
KKRainbow 82dd835c26 feat(tls): make system certificates opt-in for HTTPS discovery (#2748) 2026-10-24 04:06:58 +08:00
KKRainbow 411faae131 test(limiter): assert bps limits on metered bytes (#2741)
The relay and instance-receive rate limit tests compared TCP
application payload against the configured limit, but the limiter
charges EasyTier payload including the inner IP and transport
headers. That left the 800 KiB/s case only about 2% above its lower
bound, so ordinary variance pushed it under (717 vs 720).

Count the bytes each limiter charges and assert that rate within 5%
instead. TokenBucket now tracks metered bytes, exposed through the
limiter trait and hidden test accessors, and the benchmark samples
the counter once the initial burst is drained. Keep the payload
upper bound, which the encapsulation mismatch does not invalidate,
so a limiter whose rejection the forwarding path ignores still
fails the test.
2026-10-23 18:00:58 +08:00
KKRainbow 6d9adc4fb9 fix(core): expose smoltcp feature for auditable builds (#2747)
Cargo exposes an enabled optional dependency as a rustc cfg even when
dep:smoltcp hides the implicit feature. cargo-auditable later passes
that cfg to cargo metadata, which rejects it.

Make smoltcp a core feature that selects proxy-packet, including the
dependency settings required when enabled on its own.

Fixes #2745
2026-10-23 13:51:04 +08:00
KKRainbow e307dadb48 fix(connectivity): advertise shared-node running listeners (#2733) 2026-10-21 09:47:03 +08:00
KKRainbow 318a0b0686 fix(gateway): make port-forward reload preserve live listeners (#2710)
Reuse listeners by protocol and bind address when their destination
changes, keeping established TCP streams connected to the old target.
Prepare all new sockets before persistence or configuration publication
so bind and storage failures leave committed forwarding usable.

Commit prepared resources with shared TOML and runtime publication.
Serialize UDP target changes with flow insertion, and track response
tasks through eviction so removal waits for every socket reference.
Keep cleanup and its operation lock alive if the caller stops waiting.

Add TCP and UDP regressions for target changes, failed updates, retries,
established streams, immediate rebinds, and cancelled cleanup waiters.
2026-10-21 00:23:10 +08:00
KKRainbow 5af90471bf fix(vpn): inherit portal exit nodes for attached clients (#2726) 2026-10-19 00:04:29 +08:00
KKRainbow a144597d02 fix(core): show complete instance startup errors (#2724)
Restore the CLI's default instance-manager diagnostics without enabling
unrelated module logs. Include failed instance IDs and their saved error
chains in the final exit error.

Attach the listener URL to required-listener failures and render full
error chains in startup logs, so bind failures expose their OS cause.

Extend listener regression coverage and document the default CLI targets.
2026-10-18 22:28:41 +08:00
KKRainbow f1b910da4e test: make secure datagram grace expiry deterministic (#2723)
The expiry test sampled time before sync_root_key established its grace
period. Scheduling delays over 3 ms could leave the asserted timestamp
inside the grace window and fail the test.

Use the actual grace deadline to check acceptance just before expiry and
rejection at expiry. Use a fresh sequence number for the rejection check
so replay detection cannot hide a broken expiry boundary.
2026-10-18 12:36:54 +08:00
KKRainbowandCodex 99db92a3ae fix(config): preserve and patch empty manual route mode (#2709)
* fix(config): preserve empty manual route mode in API conversion

Treat present TOML routes as manual mode even when the list is empty.
Both Core readback and Web normalization must distinguish routes=[]
from automatic discovery; otherwise Web can skip a required overwrite
and Core can report a different mode from its running configuration.

Cover API/TOML round trips and receiver reconcile mode transitions.
Verify default-feature tests, clippy, formatting and real system routes
in three isolated Linux network namespaces.

* fix(config): patch manual route mode and verify legacy readback

Add an explicit manual-route mode to route patches and advertise typed
readback support through GetConfig. Apply the mode and route edits in the
existing route transaction without restarting the instance; keep released
callers that omit the new field on their existing route semantics.

Web gates route patches on the observed capability. For old clients with
ambiguous empty routes, recover the actual mode from GetConfig TOML or
instance-scoped ShowNodeInfo before acknowledging application. Supporting
clients use typed readback directly. Keep capability evidence in the
existing session cache and preserve its invalidation rules.

Cover mode transitions, persistence failure, stable peer identity, legacy
readback, compact clients, failed observations and cache capability gates.
Document the runtime contract and compatibility boundary.

* refactor(web): advertise route patch support in heartbeats

Declare manual-route patch and typed readback support on the WebClient
heartbeat alongside existing client capabilities. Select route updates
and legacy readback from the current authenticated Session heartbeat,
including when reconciliation reuses an observed configuration.

Remove the unreleased GetConfig capability field and per-instance
capability wrapper. Keep only observed configuration in the existing
cache, and retain strict application confirmation after full Run.

Cover the client declaration, legacy default and capability changes
against the same cached configuration. Update the runtime contract.

---------

Co-authored-by: Codex <codex@users.noreply.github.com>
2026-10-16 00:47:43 +08:00
Fa 7588062f7e fix(wasi): initialize and control embedded runtime logging (#2655) 2026-10-15 20:37:32 +08:00
KKRainbow 03078b4791 feat(dhcp): support automatic IPv6 address assignment (#2688)
Accept a boolean or an IPv6 CIDR in dhcpv6 and --dhcpv6, using
fd6b:8c2d:91e4::/64 when no initial subnet is supplied. Keep IPv4
configuration serialization compatible and DHCPv6 disabled by default.

Share the DHCP lifecycle and host update between both address families,
with typed allocation state, peer subnet selection, conflict recovery,
and bounded IPv6 probing. Preserve static addresses of the other family
and prevent static config patches from replacing DHCP-owned leases.

Carry IPv6 settings, leases, and events through management, web config,
and mobile VPN setup. Reuse the shared NIC registry and mobile address
list for IPv6-only and dual-stack operation without extra FD owners.

Cover config round trips, allocation and recovery, mobile
reconciliation, three-node IPv6 and dual-stack connectivity, and shared
TUN member exit. Validate with workspace clippy, Docker TUN tests,
frontend tests/builds, WASI and Android Rust checks, and Android VPN
Kotlin compilation.
2026-10-14 00:36:47 +08:00
KKRainbow 7c8ca07812 refactor(tun): consolidate shared NIC configuration and tests (#2690)
* refactor(tun): consolidate shared NIC configuration and tests

Separate member claims from OS deltas so mobile admission updates do
not compute unused diffs. Reuse effective configurations for desktop
address, route, and MTU updates, and share the platform claims paths.

Remove redundant runtime dispatch and test-only public accessors while
preserving registration cleanup and platform device creation ordering.

Move configuration and lifecycle tests into focused modules. Reuse
setup and table-driven cases, and verify route replacement order,
source hints, stale registrations, and failed cleanup through behavior.

Validation: shared NIC unit and Docker integration tests, mobile claim
selection, TUN and public IPv6 tests, full-workspace clippy, Android
arm64 library check, and formatting of the changed files.

* docs(tun): describe shared Ring dispatch lifecycle

Document direct member Ring polling, mobile descriptor replacement,
and cooperative scheduling in the shared dispatcher. The dispatcher
implementation is already provided by the base branch in #2689.

* test(core): remove completion race from timeout budget test

Use a pending future to exercise stage-specific timeout errors.
A delayed CI runner can observe the old 20 ms sleep and 1 ms timeout
as ready together. Tokio polls the inner future first, returning Ok
instead of the error expected by the test.

Keep the timeout assertion without a competing completion timer.
Production timeout behavior is unchanged.
2026-10-13 23:53:35 +08:00
KKRainbow 9bc1a0941f feat(dhcp): accept an IPv4 CIDR for the initial address subnet (#2686)
* feat(dhcp): accept an IPv4 CIDR for the initial address subnet

Accept booleans or an IPv4 CIDR in --dhcp, ET_DHCP, and TOML. Preserve
existing boolean behavior and reject invalid subnets and /31 or /32.

Carry the bootstrap subnet through management config conversions and
runtime snapshots. Keep peer-advertised subnets authoritative and use
10.126.126.0/24 when no custom bootstrap subnet is supplied.

Cover CLI overrides, config round trips, validation, address allocation,
and subnet precedence. Update English and Chinese CLI help.

* refactor(dhcp): select the bootstrap subnet during construction

Read the optional DHCP CIDR once when creating the service and pass it
to the existing allocator constructor. Restore reconciliation without
rewriting the bootstrap subnet on every round.

Initialize test configurations before constructing their DHCP services.
The PatchConfig API does not expose the DHCP policy; management changes
recreate the network instance.
2026-10-13 00:34:26 +08:00
KKRainbow c01e2baafa perf(core): cap shards in per-network and per-peer maps (#2685)
Public relays create many mostly empty maps for each foreign network.
DashMap defaults to 128 shards on a 24-CPU host, allocating about
1.26 GiB of shard arrays for 2,421 networks before peer/session state.

Cap these maps at eight shards and retain the four-shard default on
single-CPU hosts. Cache the shard count once and keep global indexes
on their existing default. Apply the same cap to peer connection maps
and route synchronization version maps.

Replace nested peer/network DashSets with HashSets protected by their
existing outer map or an entry mutex. Keep cross-index updates under
the manager lock. Use a plain HashMap for the local route graph index.

Validation: 245 peer/RPC tests and eight three-node integration tests
passed, along with workspace fmt and clippy with full features.
2026-10-13 00:18:49 +08:00
KKRainbow d31396f04c fix(hole-punch): connect UDP when STUN mapping differs (#2680)
* fix(hole-punch): connect UDP without reverse probe gating

Start cone UDP handshakes on the same punched socket without waiting
for reverse HolePunch packets. Keep outgoing probes while the existing
session layer retries SYN, allowing a reachable listener to reply to the
actual source when the STUN candidate uses a different mapping.

Register source observers on the listener's existing receive loop for
the active cone RPC. Send bounded batches to the signaled endpoint and
the first matching observed source, deduplicating equal endpoints.
Unregister observers on completion, cancellation, or listener shutdown.

Cover missing reverse probes, dropped SYN, late observed sources,
transaction isolation, and cleanup. Document socket and connectivity
ownership of observation and reply policy.

* fix(hole-punch): prepare source observation during listener selection

Carry an optional transaction ID in SelectPunchListener and echo it only
after registering the listener's existing UDP receive loop. Preserve the
first source arriving before SendPunchPacketCone, then transfer cleanup
ownership to that RPC without clearing the observed address.

Keep prepared and active registrations in the same transaction table.
Expire abandoned preparations after 30 seconds through listener cleanup;
active observers still unregister on completion or cancellation.
Preserve legacy requests without a transaction ID and tolerate older
responses without a registration acknowledgement.

Cover early packets, transaction ID zero, preparation expiry, active RPC
cleanup, legacy protobuf interoperability, and SYN retries against peers
with and without registration confirmation. Document the lifetime and
compatibility contract in the core architecture.

Validation: 820 core tests; workspace fmt and full-feature clippy
passed.
2026-10-12 23:19:46 +08:00
lightfirstandKKRainbow 7592951b80 fix(dataplane): route UDP by bound local endpoints (#2652)
Register one UDP flow at bind so sockets can receive from new peers
before their first send. Allocate and reserve ports within each instance
independently of TUN and Host sockets. Release the endpoint on close.
Search the ephemeral range once and preserve concurrent cursor progress.

Route local UDP fragments by the first fragment's bound endpoint. Buffer
earlier tails in a bounded shallow cache and forward original packets to
one recipient. Preserve cached ownership across binding changes, expire
idle entries, and clear state on address changes and runtime shutdown.
This avoids orphan fragments exhausting smoltcp's reassembly slots while
keeping original packet sizes for TUN and proxy consumers.

Randomize each new stack's seed to avoid replaying IP IDs on rebuild.
Document accepted residual collision-related UDP loss or downstream
delivery instead of adding fragment completion tracking.

Allow peer filters to return a batch, resuming each packet at the next
filter without repeating earlier filters. The single-packet path does
not allocate a batch queue. Keep ZCPacket and IpReassembler unchanged.

Cover endpoint ownership, ephemeral allocation, out-of-order fragments,
cache limits and expiry, binding changes, batch continuation, and large
UDP datagrams after more than 16 complete unrelated fragmented
datagrams.

Exercise two recreated sending stacks with controlled seeds to verify
that new fragments do not inherit an old downstream route.

Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com>
2026-10-11 01:35:33 +08:00
fanyang 83baead059 fix(runtime): restore per-instance native thread configuration (#2658)
Restore per-instance Tokio executors for standard native instances,
honoring multi_thread and multi_thread_count with a two-worker minimum
in multi-thread mode. Preserve external execution for compact hosts.

Run construction, lifecycle operations, FFI data-plane tasks, and
hot-reloaded port forwards on the selected instance executor. Keep
shutdown cleanup running when its caller is cancelled, and release
owned executors without blocking their workers on runtime teardown.

Add runtime ownership, cancellation, cross-runtime connectivity, and
hot-reload regression coverage, and document the execution model.

Validation: all 45 PR checks passed, including linting, integration
tests, and cross-platform builds.
2026-10-11 01:17:32 +08:00
Luna YaoandClaude Code 250c2ad8d6 refactor(config): preserve explicit flags with FlagsPatch and derive CLI arguments from schema (#2585)
This PR refactors EasyTier's configuration flags system across protobuf definitions, TOML/API persistence, runtime reconciliation, and the CLI.

It introduces a synthesized FlagsPatch protobuf message with optionize and pbjson support to track field presence reliably, replaces manual CLI flag boilerplate with dynamic schema-driven clap argument generation, and
establishes Flags::resolve(patch) as the single source of truth for runtime defaults.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-10 22:11:08 +08:00
fanyangandCopilot Autofix powered by AI 018750a785 fix: replace deprecated atomic fetch_update calls (#2665)
* fix: replace deprecated atomic fetch_update calls

Use try_update, available since the workspace's Rust 1.95 minimum, for all standard-library atomic update calls. Preserve closure behavior and memory ordering, and leave arc-swap's unrelated API unchanged.

Cover smoltcp port allocation's previous-value return and wrap boundary.

Fixes #2614

* Add unused_async allowance for tokio test

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-10 21:49:43 +08:00
KKRainbow 7af7bdc16a build: consolidate dependencies and select rustls ring explicitly (#2648)
* build(core): consolidate crypto dependencies and trim features

Align AES-GCM, ChaCha20Poly1305, HMAC, SHA2 and HKDF with the versions
already required by Snow and STUN. Align base64 with pbjson, and explain
the coordinated upgrade constraints beside the manifest entries. Adapt
AEAD and HMAC calls without changing packet or key formats, and pin the
WireGuard client key derivation with an independent HKDF vector.

Limit Snow to the Noise algorithms used by the core. Use crossbeam-utils
directly, keep the futures executor in tests, and remove UUID fast-rng
from the core while retaining existing features in native consumers.
Enable browser entropy and certificate time for the rustls backend.

Core default normal/build dependencies fall from 255 to 224 packages;
duplicated package names fall from 24 to 5 against upstream 4837468d.

Validation: Docker tests pass: 970 core, 33 proto (2 ignored), 5
WireGuard, and TCP/UDP three-node encrypted relays. Clippy passes with
warnings denied for core, proto, native and web targets. Browser
default/ChaCha20, WASI and minimal native compile checks pass. Workspace
formatting passes.

* fix(tls): select ring explicitly across application entry points

Building easytier and easytier-web together enabled both ring and aws-lc
through reqwest 0.13. HTTPS endpoint discovery used rustls automatic
provider selection and could panic before sending a ClientHello.

Use reqwest rustls-no-provider to share ring, and explicitly install the
process default before starting CLI, GUI and web services. Initialize it
for standalone webhook construction too, retaining any provider already
selected by the host.

Pass ring directly to HTTPS discovery and WebSocket TLS builders so
library use is independent of application initialization order. Keep
existing certificate verification, SNI and protocol settings.

Add a duplex-stream regression that reproduces the original panic with
both backends enabled, and run it in CI. Explain provider selection and
feature-unification constraints next to the relevant code.

Validation: the new regression fails before the fix and passes after it.
Docker tests pass: 10 discovery, 13 webhook, 2 WebSocket, WSS three-node
AES-GCM relay, and WSS credential connectivity. Clippy with warnings
denied and fmt pass. GUI, browser, WASI, minimal native, endpoint-only
and WebSocket-only compile checks pass. Linux and Windows release
dependency trees contain only the ring runtime backend.

Fixes #2607

* ci: remove the separate rustls backend regression step

Keep the HTTPS discovery regression in the existing test archive and
runner. Avoid an extra core test build solely to enable both rustls
backends; that combination was verified locally before the TLS fix.

* build(web): share reqwest 0.13 with the OIDC client

Disable the reqwest 0.12 client bundled with openidconnect/oauth2 and
use the official oauth2-reqwest adapter around the workspace reqwest
0.13 client. Pin the pre-release adapter version until its API
stabilizes.

Keep the existing redirect policy and 30-second timeout. Initialize the
ring provider when constructing OIDC configuration, including outside
the web application entry point.

Exercise discovery, JWKS fetching, token success and OAuth error
responses against a local mock provider. Verify redirects are not
followed. Refresh Cargo.lock to remove reqwest 0.12 without unrelated
upgrades.

Validation: Docker OIDC tests (2) and webhook tests (13) pass. Clippy
with warnings denied for native/core/proto/web all targets and full
features passes, as does workspace formatting. The default core/web
dependency graph has one reqwest version and 45 multi-version names
instead of 46; the TLS runtime still selects ring only.
2026-10-09 10:22:32 +08:00
Luna Yao eb32d2a930 fix(utils): fix some bugs in BufPoolWriter/BufList (#2634)
fix bugs introduced by feat(buf): add BufPool, BufMargins, and BufList for zero-copy packet buffering #2625
cf. perf(quic-proxy): use BufPool and BufMargins in QuicSocket #2626 (comment)
2026-10-07 21:58:46 +08:00
KKRainbow 728ba94b50 Link easytier-mini downloads and fix UDP admission test ordering (#2638) 2026-10-07 01:03:35 +08:00
KKRainbow d3a5ae684a fix(peers): keep TCP hole-punched connections alive with 1s pings (#2632)
Disable ping interval backoff for TCP hole-punched connections so idle
connections continue to send keepalive traffic every second. Preserve
the existing backoff and loss handling for other connections.

Keep randomized backoff above zero and cover the one-second schedule
and the existing backoff and loss retry behavior with tests.

* refactor(peers): model connection origins at admission

Record manual, direct, listener, TCP/UDP hole-punch, and attached
origins when constructing peer connections. Derive hole-punch state
from this origin instead of maintaining separate mutable flags.

Choose the one-second TCP hole-punch ping limit in PeerConn and pass
only a maximum interval to the pinger. Keep other origins on the
existing backoff schedule without inspecting tunnel type strings.

Keep origin selection internal and preserve the dedicated attached
admission paths. Update public admission callers and cover origin
propagation, relay restrictions, and ping interval limits.
2026-10-05 16:25:08 +08:00
KKRainbow 651a8d9e25 feat(web): add central network management console (#2622)
Persist central network intent and compile complete per-device configs
with secure credentials, ACL policy, and dedicated Gateway runtimes.
Expose network, credential, device registry, and runtime observation
APIs.

Treat central management and external Console as alternative consumers
of the upstream ClientManager. Register central devices through a local
webhook handler and publish through existing runtime reconciliation.
Serialize public mutations with enrollment, reject direct credential
changes to managed instances, and retain REST revision invalidation.

Keep Gateway lifecycle publication in the central service and remove
obsolete incremental result bookkeeping. Restore persisted networks and
retire orphan runtimes through the same serialized publisher.

Add Core and protocol support for Gateway and WireGuard management,
including GUI bindings and serialized GUI config writes. Bootstrap IPv4
for DHCP-only networks and retain assigned addresses without peer IPv4.

Cover intent transactions, complete configuration publication, offline
recovery, authentication, revocation, Gateway lifecycle, and DHCP.

Validate central candidates before persistence using Core URL, config,
and portal-client rules. Reject unsupported peer schemes, unconvertible
proxy subnets, and invalid portal clients without changing live intent.
Expose existing pure Core validators without changing runtime behavior.

Gate API-facing credential validation on API-enabled Core builds so
minimal WASM targets do not reference omitted management types.

Preserve session-backed device views in external Console mode. Bound
Gateway admissions without cancelling transport upgrades, and cancel
pending peer handshakes before retiring runtimes. Batch registry reads
and bound runtime observation concurrency.

Randomize DHCP bootstrap and conflict retries, keep an advertised
current subnet, and select fallback subnets deterministically. Cover
concurrent startup, Gateway admission lifetime, and external Console
regressions.

Allow DHCP bootstrap with no remote routes. Count network members
through one tenant-scoped query without write transactions. Cover zero-
peer allocation and tenant/empty-network counts.

Preserve direct Web configurations and disabled states in central device
snapshots. Remove obsolete central rows atomically with membership
changes. Stop online managed instances before deleting or blocking
devices, retaining intent when shutdown or deletion fails.

Persist automatic IPv4 allocations separately from manual overrides so
subnet changes reallocate only automatic members. Normalize mapped proxy
routes to their advertised CIDRs when granting temporary credentials.
Cover publication, deletion rollback, subnet migration, and grant
updates.

Read central intent in deferred transactions so polling does not reserve
the SQLite writer lock. Test reads with an active writer and assert the
temporary-member secret constraint using a valid device fixture.

Persist patched WireGuard clients from the saved Web or GUI candidate
without a follow-up RPC. Preserve pending settings and ownership, and
cover disconnects and failed patches.

Resolve named credential and config mutations against the live Core
instance before checking central ownership. Forward authorized mutations
by UUID on the same session, including while network renames are
pending. Cover all mutation methods and preserve direct Console
behavior.

* feat(web-ui): add central network console and WireGuard management

Add network and device views with central membership, credentials, ACL
policy, temporary peers, and per-instance runtime details. Update
console navigation, styling, theme handling, and API clients.

Extend shared configuration and status components for central networks.
Add a WireGuard portal dialog for setup and running-device management,
with matching translations and network configuration types.

Use secure UUID generation on HTTP, discard stale member configuration
responses, and stop node-detail polling when the component unmounts.

Update frontend workspace dependencies and include component, dashboard,
configuration serialization, and central console end-to-end tests.

Add isolated real-Core E2E coverage for central data-plane traffic, ACL,
DHCP, recovery, device lifecycle, and native WireGuard clients. Record
all 59 functional checks with evidence and confirmed validation/UI
defects; keep runtime artifacts and test credentials out of Git.

Normalize protobuf logger levels and render translated labels correctly.
Cover all six levels across setting, reload, and language changes. Add
real configuration-rejection E2E checks for database and Core stability
and uninterrupted traffic, and record the resolved audit findings.

Gate central navigation and registry actions by console mode. Refresh
WireGuard settings on mounted status views and preserve explicit portal
listener endpoints. Explain the trusted permanent-member ACL boundary.

Cover external Console rendering, portal refresh retries and teardown,
and explicit IPv4/IPv6 listener exports.

Preserve PublicServer discovery when saving its settings, including when
its URL matches the Gateway. Cover renaming and endpoint edits in the
browser.

Display automatic member addresses without converting them to manual
overrides during edits. Derive offline address prefixes from the network
subnet. Add browser regression coverage and real-Core checks for direct
configuration preservation, temporary proxy mappings, automatic subnet
migration, and shutdown before device deletion.

Build enrollment commands from the configured API hostname, including
IPv6 and relative endpoints. Use the PublicServer connector in temporary
credential CLI and TOML exports. Add browser regression coverage.

Preserve form credentials across repeated normalization and GUI storage
reloads. Keep explicit form values authoritative over backend keys and
cover JSON persistence, idempotence, replacement, and clearing.
2026-10-05 11:06:47 +08:00
Luna Yao a017dc133c feat(buf): add BufPool, BufMargins, and BufList for zero-copy packet buffering (#2625) 2026-10-01 09:32:45 +08:00
KKRainbow 3da349f4ee fix(relay): preserve forwarded packets in secure relay paths (#2618)
Forward remote packets without starting a relay session at the
intermediate hop. Encrypting forwarded Noise handshake replies with
that hop's session corrupts the end-to-end handshake and causes
credential connectivity tests to time out.

Disable automatic P2P in the admin/shared/credential test so direct
connections cannot mask broken forwarding. Cover unchanged forwarding
of handshake requests, replies, and encrypted data packets.
2026-09-29 22:52:10 +08:00
刚刚and225284228a-droid 0a783c8e04 fix(listener): reap completed handlers during continuous accepts (#2598)
Continuous incoming connections recreate the one-second sleep in run_accepted_task_runner, so completed handlers are only reaped after a one-second gap in accepts. Busy public nodes can therefore accumulate completed Tokio tasks and JoinSet entries even when their established connection count stays stable.

Keep a persistent interval outside the select loop so incoming accepts cannot reset the reaping deadline. Add a paused-clock regression test that accepts an immediately completing handler every 100 ms, checks that completed entries remain bounded, and verifies shutdown drains the set. Enable Tokio's test clock utilities only for development builds.

Co-authored-by: 225284228a-droid <239500008+225284228a-droid@users.noreply.github.com>
2026-09-22 23:38:59 +08:00
KKRainbow ebf0b947b9 fix(ospf): resync missing peer data after metadata expiry (#2599)
Report missing peer metadata and connection rows in sync responses.
Resend those records through the existing per-session pending queues
without resetting the incremental cursor or rebuilding sessions.

Keep requests across lost responses, distinguish missing connection rows
from metadata, and avoid requesting non-relaying credential rows.
Deduplicate records selected by both incremental and pending paths.

Cover selective expiry, multihop recovery, retries, partial availability,
and unchanged-record suppression. All 40 route module tests pass.

Fixes #2597
2026-09-22 23:38:34 +08:00
KKRainbow 0f3d8e4434 fix(core): keep peer liveness responsive under receive limits (#2590) 2026-09-19 08:22:12 +08:00
Luna Yao 7223677264 cargo: upgrade (#2116)
update dependencies
2026-09-16 20:50:43 +08:00
KKRainbow 5477d4bca2 fix(proxy): isolate TCP flows and recover KCP control loss (#2569)
* fix(proxy): distinguish TCP flows sharing source ports

Key TCP NAT entries by source and destination so connections sharing a
source port can reach different targets independently. Allocate a unique
translated source port for each flow and restore the original addresses
and ports on the return path.

Keep connection identity during accept and cleanup so a replaced flow
cannot lose its new mapping. Give each smoltcp proxy mode its own
listener port and retain the original destination for mapped subnets.

Include regression coverage for SYN retries, concurrent insertion,
stale cleanup, translated port wraparound and exhaustion, plus the
recorded cross-platform and six-mode traffic validation.

* fix(proxy): preserve half-closed streams and pending KCP accepts

Propagate EOF to the opposite writer while keeping reverse traffic
alive until both directions finish. This allows a server to reply after
reading request EOF and a client to send after reading response EOF.

Pin kcp-sys to b37ee660 for retained pending accepts, receive-buffer
draining before EOF, early FIN handling, atomic state cleanup, and
keepalives that start only after the handshake completes.

Cover both shutdown directions, empty requests and buffered transfers
in gateway tests and the TCP/KCP/QUIC kernel/smoltcp integration matrix.
Include the half-close validation record and its remaining loss limits.

* fix(proxy): enable backward-compatible KCP control recovery

Pin kcp-sys to 26853356 and update both the root and OHOS workspace
lockfiles. The dependency negotiates reliable control per connection
while preserving legacy behavior when either endpoint lacks support.

New peers recover lost handshake and FIN control packets, acknowledge
FIN without closing the reverse writer, and retain bounded close state
for duplicate packets. Queue saturation cannot block reliable stateless
responses. Compatibility tests retain one historical baseline.

Include the protocol design and validation records for deterministic
loss, real old endpoints, three platforms, the full integration matrix,
and proxy traffic. Preserve mixed-version failures and the documented
minor scheduling issue rather than claiming legacy loss recovery.

 Validate the OHOS workspace with --locked: 12 library tests and the
binding test build check pass, along with formatting.
2026-09-16 12:50:08 +08:00
MRK 286e0f4a0d fix(proxy): do not wrap TCP packets destined to own virtual IP (#2572)
The wrapped TCP proxy (KCP/QUIC) claims every SYN emitted by the local host,
including packets addressed to the node's own virtual IP. Those packets were
marked for the wrapped path and self-delivered, then the wrapped destination
lookup failed ("no peer found for wrapped TCP dst"). This broke local access
to the node's own virtual IP on systems that route own-address traffic through
the tun (macOS), and, with the proxy enabled on both sides, also inbound
wrapped flows to a macOS node, because the receiving side dials its own
virtual IP locally.

Skip the wrapped path when the SYN destination equals the local virtual IPv4.
Feed the NIC filter context the instance virtual IPv4 (snapshot.virtual_ipv4)
instead of the smoltcp fake inet so the check works with smoltcp enabled too;
the net-to-net guard is unaffected because it only runs when smoltcp is off,
where both values are identical.

Add a regression test for the KCP and QUIC transports.
2026-09-15 14:37:47 +08:00
KKRainbow dd013e6a2a fix(acl): prevent bidirectional ping from bypassing inbound drop rules (#2570)
* fix(acl): match ICMP replies in outbound allow records

Only outbound echo requests with code zero create ICMP response records.
Require inbound complete packets and first fragments to be echo replies
with code zero before using those records, so reverse echo requests
remain subject to inbound ACL rules.

Recognize ICMP fragment tails without reading their payload as an ICMP
header. Allow them to use existing address and protocol records without
creating new records. Walk IPv6 extension headers to locate ICMPv6 and
avoid inferring an unknown tail protocol from its payload.

Add IPv4 and IPv6 unit coverage for message direction, code, truncation,
and fragment parsing. Add a three-node regression for bidirectional
requests and ordinary and fragmented replies under inbound default-drop.

Fixes #2545

* fix(acl): keep IPv6 parse failures subject to rules

Retain source and destination addresses when an IPv6 extension header
is truncated, exceeds the payload, or repeats a fragment header. Treat
these packets as unspecified protocol instead of entering the global
parse-failure allow path. Apply the same handling to truncated transport
headers after an extension header.

Classify non-ICMPv6 fragment tails as unspecified so their data cannot
be parsed as TCP/UDP ports or create temporary response records. Keep
ICMPv6 tail authorization through existing response records.

Exercise the ACL entry point with malformed headers and short and long
fragment tails under default-drop and default-allow policies, including
port allow rules that must not match tail payload bytes.
2026-09-15 10:03:15 +08:00
KKRainbow c96b6c1961 fix(web): harden managed config sync between console and clients (#2567)
* fix(web): fence managed config runtime reconciliation

Keep runtime reconciliation tied to the currently authorized session so
stale connections cannot mutate a replacement session runtime.

Accumulate only contiguous dirty IDs and load their latest SQLite state.
Require the applied revision to match the earliest Patch base and the
persisted revision to match the latest target. Otherwise, reconcile the
full desired state.

Use separate runtime-state and config-cache epochs. Managed updates can
reuse observed configs; direct mutations invalidate them. Update sync
documentation to match.

* fix(web): interrupt validation retry on state changes

Track meaningful validation state changes separately from periodic dirty signals. Applied revision changes wake a failed validation immediately, while heartbeat-driven revalidation retains the retry backoff.

Treat Notify as a wake-up hint and recheck the state-change epoch after every wake so stored permits and periodic heartbeats cannot cause retry storms.

* fix(web): retry unconfirmed connected webhooks

Retry node-connected webhook delivery on retryable errors with a
short 100ms/500ms backoff and give up immediately on non-retryable
errors. Re-check that the session still owns the connection before
every attempt and before recording the delivery, so a replaced
session can no longer record a stale connected binding.

* fix(web): fence disconnects by session ownership

Return whether session removal actually removed the current route owner, and emit disconnected only for that owner. Replaced sessions can no longer invalidate a newer connected route.

* fix(web): hot-patch managed hostnames

Include hostname changes in the hot-patch path instead of falling
back to a full restart. When a full overwrite run is required and
the desired config has no hostname, inherit the current runtime
hostname so an unmanaged value survives until it is explicitly
cleared.

Read back the runtime config after an overwrite run and verify it
converged instead of assuming the desired state was applied.

* fix(web): retry transient runtime reconciliation failures

Keep the per-session managed runtime reconciliation worker alive when a
single database round fails. Retry from the next heartbeat so persisted
managed revisions can still converge after restart-time contention.

Reserve terminal worker shutdown for destroyed session or storage state,
and cover recovery after a transient revision read failure.

* fix(web): accept omitted hostname after runtime apply

Release 2.6.4 omits hostname from config readback when it matches the device hostname. Trust a successful hostname mutation only when the returned field is absent, while continuing to verify every other field and rejecting explicit mismatches.

* fix(web): ignore unmanaged runtime device names

Windows release 2.6.4 generates a random interface name when the managed config leaves dev_name empty. Exclude that runtime-owned value from reconciliation unless the desired config explicitly sets a non-empty device name, preventing endless overwrite restarts.

* feat(web): report failed network instances to console

Expose stopped Core instances with startup errors in heartbeats.

Merge Core failures with direct managed-run RPC failures in easytier-web.

Send failed instance IDs during token validation without error text.

Prune local run failures when managed configs are deleted.

* fix(web): distinguish unknown runtime application state

Track whether the current session has observed its applied revision
separately from the optional revision value. Report this fact through
validate-token so Console can preserve application state across
receiver restarts while recognizing deliberate pending mutations.

* feat(web): configure heartbeat timing from server

Heartbeat responses now provide the interval and RPC timeout.

Legacy servers use local defaults and remote values are clamped.

Web configuration and session receive timeout follow the policy.

* fix(web): reject inactive control sessions

Route control RPCs by machine id only to sessions whose RPC manager
is still running, so a session that has been stopped or replaced
can no longer receive control traffic addressed to the device.

* fix(core): filter network info before collection

When a collect-network-info request names specific instances,
collect those instances only instead of collecting every instance
and filtering the result afterwards, so unrequested instances no
longer run per-collection work on every request.

* feat(web): enable focused runtime diagnostics

Enable easytier-web info logs by default while preserving explicit log configuration. Record startup settings, session lifecycle, failed instance changes, webhook queue and request latency, and managed runtime operation timings for production diagnosis.

* fix(web): preserve managed revision across reconnects

Keep one runtime identifier for each Core WebClient lifetime.

Reuse its managed runtime state after transport reconnects.

Retain applied revisions and reconcile hints while disconnected.

Preserve runtime epochs so stale work cannot mark a revision applied.

Reject stale sessions from reclaiming routes after reconnect.

Core or Web restarts and legacy clients still use unknown state.

Immediately revalidate a restored revision after authentication.

Document local management RPC drift as an accepted trade-off.

This lets Console converge without waiting for periodic validation.

* fix(web): satisfy clippy across managed config sync tests

Scope managed runtime guards to blocks in runtime revision tests so
no std MutexGuard is held across await points, return the applied
revision directly instead of through a let binding, and pass
WebhookValidationInput to request_heartbeat_validation instead of
expanding it into eight separate arguments.

* fix(core): stop reporting failed instances as running in heartbeats

A stopped instance with a startup error appeared in both
running_network_instances and failed_network_instances, so the
server treated it as running and never re-ran its managed config.
Exclude failed instance ids when building the running list so the
reconciler restarts them.

* fix(core): close missed-wakeup race in instance state changes

wait_for_change created the Notified future before reading the
generation but only registered it when awaited. A change landing in
between fired notify_waiters with no registered waiter and delayed
the heartbeat by a full interval. Enable the future before reading
the generation so every change wakes a waiting heartbeat.

* fix(web): address review findings

Fence webhook validation and connection transitions against stale
state, redact credentials from default-level logs, and stabilize
runtime reconciliation:

- Record connected bindings only while the session still owns the
  machine route, and skip disconnect compensation once a replacement
  owns the route so a stale disconnect cannot revoke it.
- Discard webhook validation results when the change epoch moved
  during the HTTP round, so a stale rejection cannot invalidate the
  current session.
- Drop user_token fields from info and warn logs that became
  visible with info-level defaults.
- Restore a hostname omitted by the 2.6.4 readback into the cached
  runtime config after a successful mutation, so later rounds stop
  re-sending the same hostname patch.
- Reconcile running web configs when no revision is tracked so
  legacy unrevisioned updates converge, and wake sessions for
  unrevisioned full updates instead of waiting for the next
  heartbeat.

* chore(go): regenerate web proto bindings for heartbeat fields

Add failed_network_instances, support_heartbeat_policy, and the
heartbeat policy response fields to the checked-in Go bindings.
Other proto packages are left as-is because their drift predates
this change.

* fix(web): redact user tokens from positional log arguments

Three runtime reconciliation info logs and the user lookup error
contexts printed user_token through format arguments, which the
earlier field-syntax redaction missed. The reconcile log now fires
every round for unrevisioned machines, so remove the token from
these messages as well.

* fix(web): fence stale validation and runtime reconcile rounds

Check webhook validation epochs while holding the session write lock,
so stale success and rejection responses cannot change session state.
Advance the runtime epoch for unrevisioned full config updates, and
exclude failed instances from heartbeat and RPC reconciliation lists
so stopped instances are restarted instead of repeatedly hot-patched.

Release test read guards before awaiting validation apply calls. Set
up the no-pending condition before asserting that an applied revision
is a no-op, and verify that its runtime epoch remains unchanged.

Validation: all 137 client_manager tests passed.

* test(credentials): cover P2P with active VPN portal

Model an admin and temporary credential peer connected as a foreign network through a public server with data relay disabled. Verify their direct connection can be replaced after a WireGuard portal client comes online.

* test(credentials): stabilize two-admins failover assertions

The two-admins non-reusable credential test could fail on slow
convergence: after dropping the winning peer it relied on a single
route sample passing a bare AND condition, then re-asserted the same
expectations through one-shot checks seconds later. A transient route
flap in that window (for example a briefly resurrected winner route
from stale conn info) turned a passing convergence into a hard assert
failure. This matches the 48.9s CI flake of
credential_non_reusable_across_two_admins_allows_only_one_peer
observed on 2026-08-12.

Changes:

- wait for bidirectional admin connectivity (AND) with a 20s budget
  before issuing the credential, instead of a one-directional OR
- replace the failover wait_for_condition with
  wait_stable_failover_visibility_on_admins, which requires three
  consecutive samples of loser-present and winner-absent on both
  admins within the same 60s budget and logs every sample
- enrich the stable-single-winner timeout message with per-admin
  visibility flags and elapsed time for triage

All existing contracts are preserved; only observation windows and
diagnostics change. Validated in the rust container: three passes at
normal speed (54.1s / 53.8s / 53.1s) plus one slow-convergence round
(172.7s) that would have raced the old one-shot sampling; it now
passes with failover samples logged. cargo fmt and clippy -D warnings
clean.
2026-09-13 01:13:28 +08:00
KKRainbow e0bdb516b6 fix(quic): bind proxy packet checksum to packet number via ETQ1 version (#2565)
* fix(quic): bind proxy packet checksum to packet number via ETQ1 version

QUIC proxy connections die with quinn PROTOCOL_VIOLATION "unsent
packet acked" under bursty traffic with reordering, and the affected
peer pair keeps failing for every new connection until the source node
restarts.

Root cause: the custom crypto checksums the packet bytes but not the
packet number, while quinn decodes truncated packet numbers by
proximity to the largest received (RFC 9000 Appendix A). A 1-byte
encoded packet delayed beyond the +/-128 decode window is decoded as
a future packet number, still passes the checksum, gets ACKed, and
the peer aborts because it never sent that number. Real QUIC survives
this because the AEAD nonce is derived from the packet number, so a
misdecode fails authentication.

Fix: negotiate a custom QUIC version ETQ1 (0x45545131) for the proxy.
Connections on ETQ1 mix the packet number into the SeaHash checksum,
so an out-of-window misdecode fails authentication and the packet is
handled as ordinary loss. The mode is derived statelessly from the
negotiated version in QuicSession and ServerConfig::initial_keys.

Compatibility: the proxy endpoint accepts both ETQ1 and version 1.
NatDstQuicConnector dials ETQ1 first; on ConnectionError::VersionMismatch
from a legacy peer it retries with version 1 and remembers the peer in
legacy_version_peers to skip the rejected version afterwards. The
quic:// tunnel keeps version 1 only.

Verified with 13 docker nodes under netem jitter and bursty iperf
load: the previously-poisoned pair survived 16 minutes on ETQ1 with
zero violations while all legacy-version pairs kept dying; mixed
new-to-old and old-to-new connections work.

* fix(quic): extend the ETQ1 checksum fix to the quic:// tunnel

The quic:// tunnel shares CryptoKey with the proxy, so after the proxy
moved to ETQ1 the tunnel still carried the unsent-packet-acked exposure.
Make endpoint_config() dual-version so tunnel listeners accept both
ETQ1 and legacy peers, and dial ETQ1 first in upgrade_connected with a
transparent fallback to version 1 on VersionMismatch, via a shared
connect_with_etq1 helper. The proxy keeps its hedged dialer with the
per-peer legacy memory; tunnel connections are established once per
session, so the fallback there costs a single extra round trip.
2026-09-11 23:33:46 +08:00
a7383114d0 fix(core): do not allocate DHCP IP without an assigned peer IPv4 (#2551)
Fixes the DHCP allocator treating its hard-coded fallback
 subnet as an interface lease when no peer IPv4 has been 
observed. With DHCP enabled and no devices carrying a 
virtual IPv4, the allocator now waits instead of pulling 
10.126.126.0/24 onto the TUN interface.

Behavior preserved when an explicit allocator subnet is 
supplied or when peers with assigned IPv4 exist.

---------

Co-authored-by: 225284228a-droid <225284228a-droid@users.noreply.github.com>
Co-authored-by: Codex <codex@users.noreply.github.com>
2026-09-11 23:06:18 +08:00
KKRainbow 19e5c49ba3 chore: bump version to 2.7.0 (#2566)
* chore: bump version to 2.7.0

Update version strings across the workspace:
- crate versions and internal dependency requirements for
  easytier, easytier-core, easytier-proto, easytier-web,
  easytier-gui, and easytier-mini, plus Cargo.lock
- GUI package.json and tauri.conf.json
- Magisk module.prop
- default release/docker workflow tags (v2.7.0)

* fix(ohos): sync easytier-ohrs Cargo.lock with bumped workspace versions

The ohos workflow builds easytier-ohrs with --locked, so its
lockfile must record the new 2.7.0 versions of the easytier,
easytier-core, and easytier-proto path dependencies.
2026-09-10 22:38:24 +08:00
KKRainbow 974c3270e8 feat(magisk): add module WebUI configuration (#2563)
* feat(magisk): add module WebUI configuration

Reuse the existing config generator in KernelSU-compatible module
managers. Validate and atomically persist TOML through a module helper
before restarting EasyTier, and package the generated assets in CI.

Closes #1915

* fix(magisk): preserve existing WebUI configuration

Merge form-managed fields into the original TOML so advanced module settings remain intact. Resolve the running core by its exact executable path before restart.
2026-09-10 20:13:21 +08:00
KKRainbow 993640b1dd fix(config): normalize [secure_mode] when loading TOML config files (#2562) 2026-09-10 08:56:57 +08:00