Summary
Reconcile the Android system VPN after the GUI has finished initializing the EasyTier core.
Retry reconciliation while network information or the virtual IPv4 address is not ready yet.
Serialize reconciliation work and de-duplicate concurrent VPN permission requests.
Root cause
On Android, the network instance can report that it has started before collectNetworkInfo exposes the instance state and virtual IPv4 address. The previous startup path treated that temporary state as a terminal failure, stopped VPN setup, and relied on another event to retry it. If no later event arrived, peers could connect successfully while the Android VpnService remained inactive until the user stopped and started the network again.
PR #1628 added polling for the DHCP-specific empty-IP case. The same race can occur earlier, while network information is still unavailable, and can also affect static-IP configurations.
* feat(peer): echo liveness probes on data traffic
Advertise a liveness-echo capability during classic and Noise
handshakes. After a ping failure, tag outgoing peer packets with a
short probe token and accept only the matching echoed token as
round-trip proof.
Keep one ping request outstanding and coalesce scheduler triggers so
high traffic cannot reorder timeout results. Preserve one-way failure
detection because unrelated ingress never clears the loss counter.
* test(three_node): relax disconnect wait for sequential pingpong
proxy_three_node_disconnect_test assumed the old pingpong timing,
where overlapping pings failed fast and the connection closed well
inside the 11s wait (see the old [4, 9)s comment).
The liveness-echo change keeps one ping outstanding: each failure
now takes a full 2s timeout, so the fifth consecutive failure and
the connection close land at ~11s. Both proto variants timed out at
the 11s bound in CI. Widen the wait to 15s and update the timing
comment.
tcp_stun_servers explicitly controls TCP STUN servers.
If tcp_stun_servers is not configured, TCP STUN falls back to configured stun_servers.
If neither is configured, TCP STUN uses the built-in default TCP STUN list.
Empty lists explicitly disable the corresponding STUN server list.
Empty CLI/env overrides now clear existing configured STUN servers instead of appending nothing.
Use AF_PACKET SOCK_DGRAM so Ethernet, TUN, and point-to-point
interfaces expose the same IP payload to the BPF filter. Rebuild the
synthetic Ethernet envelope expected by fake TCP on receive and strip
it before transmitting through the cooked socket.
Bind sockets to the selected IP protocol and reject non-initial IPv4
fragments before reading TCP ports. Preserve peer MAC addresses on
Ethernet links.
Add privileged TUN and veth tests for IPv4/IPv6 receive, send, tuple
filtering, and fragment rejection, and enable them in Linux CI.
Co-authored-by: KKRainbow <443152178@qq.com>
Co-authored-by: Max Sum <4883681+Max-Sum@users.noreply.github.com>
* feat(credentials): support managed credential synchronization
Allow managed callers to upsert credentials with an exact ID, secret,
permissions, reuse policy, and expiry.
Return non-secret attributes plus a public-key fingerprint so callers can
verify relay credential consistency.
Persist imported credentials atomically and preserve identity and expiry
across restarts.
* fix(credentials): make managed upserts durable
Write the candidate credential snapshot before committing it to memory.
Propagate storage failures so controllers can retry instead of observing
false convergence.
Cover a transient storage failure to verify that memory stays unchanged
and the retry persists the credential.
* fix(credentials): atomically replace stored snapshots
Define CredentialStorage::store as an atomic replacement boundary and
use atomic-write-file in the management adapter. This keeps the last
committed credential JSON readable when a replacement fails.
Cover replacement of an existing credential snapshot and keep the
dependency scoped to the management feature.
* feat(ohos): complete nearby console integration
Use the core management RPC surface for ephemeral nearby deployments, harden session lifecycle and packet validation, support tunnel-to-NIC packet conversion, and timestamp HarmonyOS traffic samples.
* fix(core): prefer verified UDP hole-punch paths
Treat zero latency as unmeasured so a newly admitted UDP path cannot replace a working relay before liveness is confirmed.
---------
Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local>
Add a native EasyTier proof-of-concept binary with TCP and UDP
transports, TUN, UDP hole punching, AES-GCM, and a read-only RPC
portal.
Introduce a release-derived mini profile and musl linker policy so
x86_64, big-endian MIPS, and little-endian MIPS stay below the strict
5,000,000-byte target without UPX.
* feat(wasi): expose protobuf RPC request ABI
Add an instance-scoped asynchronous RPC session backed by the shared
operation broker. Reuse the existing dispatcher and management handlers.
WASI hosts can call PeerManageRpc and ConnectorManageRpc with the same
protobuf payloads as easytier-cli.
Export ABI version, submit, take, and free functions. Bind selectors to
the WASM instance handle and keep method errors in RpcResponse. Enable
management RPC explicitly in the Go-host WASM build.
* fix(gateway): serialize UDP client eviction
Serialize UDP client admission across forwarding rules so only one
eviction can claim and wait for a released semaphore permit. Retry
when cleanup concurrently removes the selected client.
Add a multithreaded regression test for the permit handoff while the
evicted client is still referenced.
* fix(gateway): publish UDP client admission atomically
Hold the admission guard through client and response-task publication
so a concurrent eviction cannot leave an orphan task holding the slot
permit.
Open the data-plane flow before entering the critical section and extend
the multithreaded regression test across the publication window.
Add a small Vite workspace that builds and publishes independently
of the dashboard. Reuse frontend-lib for the form and expose the
existing NetworkConfig conversions through wasm-bindgen.
Initialize the Aura theme in the standalone entry, detect the browser
language, and provide a persistent selector in the form header. Present
Generate Config and Copy Config as the page actions.
Keep the shared network secret field fluid so both form columns align.
Bundle the generator under dist/config-generator in the dashboard
artifact while preserving its standalone build output.
Build the optimized WASM module with the project and remove the
API-backed generator route from the dashboard.
Require matching pong responses before resetting consecutive liveness
failures so half-open direct connections leave the peer map.
Carry latency-first policy on relay handshakes and route replies around
stale direct peers, including handshakes started during decryption.
Store peer-center reports as atomic per-peer snapshots and include
topology costs in the digest so removals and latency updates propagate.
Drop data packets at a saturated host egress boundary instead of
blocking the shared peer packet router and shutdown path.
Add regressions for asymmetric traffic, relay ACK routing, peer-center
invalidation, and bounded host egress.
Replace periodic refill tasks with on-demand accounting to avoid waking
idle token buckets.
Keep balance, refill time, and fractional credit in one locked state so
concurrent consumers cannot observe partially published refills or
exceed the configured burst capacity. Track credit in nanoseconds and
discard excess credit at capacity to preserve precise limiter behavior.
Use a one-second default burst capacity to preserve the existing
limiter behavior while supporting explicit capacity configuration. Keep
limiter capacity and fill rate in a local config instead of an unused
protobuf message.
Charge only logical EasyTier data payload, unwrap foreign network
packets before accounting, and leave control traffic outside the
limiter. Reject forged payload lengths by accounting from actual packet
boundaries.
Split oversized blocking consumes into capacity-sized chunks and cover
concurrency, refill precision, burst caps, payload accounting, and
bandwidth integration behavior.
Replace pnet_packet parsing and mutation across gateway packet paths with the existing smoltcp wire APIs. Preserve length validation, fragmentation classification, TCP flags, and checksum behavior while removing the core pnet_packet feature dependency.
Reject stale non-initiator OSPF sync sessions: only initiator requests may create missing sessions, and a rejection clears the old initiator role only when the remote session generation is unchanged. This fixes an unbounded RPC storm caused by a delayed route sync recreating a session after both peers relinquished the initiator role, with regression tests for session creation and response reordering.
Overhaul the WASI guest data plane for throughput and add the host
capabilities it relies on. The externally driven Tokio runtime now
runs its timer pre-turn only when a tracked deadline has expired,
and all WASI-reachable timers (STUN, port mapping, WebClient, UDP
flow cleanup) go through the portable time facade so conditional
timer driving cannot starve them.
Data plane:
- Move read/write deadlines onto TCP and UDP resources with one ABI
setter per direction, reuse a single expiration timer per
resource, and drop timeout arguments from the four hot data-plane
submissions (ABI v3). Checked absolute instants treat
unrepresentable finite timeouts as unbounded instead of panicking.
- Batch host traffic: vectored TCP frame writes combine queued
slices into one host operation, and reads request a bounded 64
KiB while retaining excess bytes in the stream buffer.
- Complete TCP writes inside the guest with cancellation-safe
writes, reporting the completed prefix before honoring
cancellation or timeout so hosts never replay bytes.
- Repoll smoltcp egress immediately on zero poll delay, enlarge
virtual UDP receive queues to 128 KiB payload with 128 metadata
slots, and bound UDP session receive buffers to 8 KiB plus one
byte while keeping oversized-datagram detection.
Host integration:
- Add optional algorithm-neutral AEAD seal/open imports with the
ring backend as fallback, and pin the ring AES-128-GCM wire vector
so the Go host stays interoperable.
- Forward instance events to hosts through one best-effort,
synchronous, non-blocking import.
- Add a repository-owned build entry point for the Go host artifact:
Binaryen 131 at -O4 with cached, SHA-256-verified official
archives.
* perf(stats): avoid per-update clock reads
Perf profiles show quanta::get_now consuming 2.9-4.3% of data-plane
CPU because every counter update refreshes a high-resolution timestamp.
Track metric activity with the existing 60-second cleanup cadence instead.
Relaxed 32-bit epochs preserve the three-minute retention window, support
32-bit targets, and remove repeated clock reads from packet processing.
* perf(data-plane): reduce per-packet synchronization
Perf profiles showed per-packet config Arc cloning, bounded-channel
permit futures, duplicate peer lookups, and default connection UUID
lookups consuming CPU in both TCP and UDP data paths.
Borrow stable config snapshots, use nonblocking channel fast paths with
the existing backpressure fallback, reuse direct peer lookups, and cache
the selected connection while preserving close and reselection behavior.
Add focused tests for channel backpressure and cached connection
invalidation.
* fix(peer): serialize default connection cache updates
The profile-guided default connection cache could republish a connection
after the close task removed it, leaving a stale cache while another
connection remained live.
Serialize only cache-miss selection/publication and connection removal.
The per-packet cache-hit path remains lock-free, while close and selection
can no longer race to resurrect a removed connection.
* perf(core): make data-plane idle check constant time
Avoid scanning every DashMap shard for each peer packet when no data-plane flows are active.
Publish the flow count before insertion and release it after removal so an Acquire load is a safe O(1) idle signal. Reject count overflow and underflow instead of silently saturating.
* test(perf): add repeatable two-node netns benchmark
Create isolated underlay namespaces, pin both EasyTier cores and iperf3 endpoints, and measure a single TCP flow in both directions over either UDP or TCP peer transport.
Keep every iperf3 JSON result and emit directional medians while cleaning up processes and namespaces on every exit path.
* perf(tcp): preserve native owned stream halves
Let each VirtualTcpSocket adapter consume itself into independent read and write halves. Portable adapters retain the generic shared split as a default.
Use lock-free Tokio owned halves for native TCP and Unix streams so tunnel I/O no longer takes the generic split mutex on every poll. Cover full-duplex traffic and write-half shutdown.
* perf(packet): preserve ownership across the Host seam
Introduce an opaque, move-only HostPacket that retains core packet storage while exposing only the raw IP payload. Clear private headers before handing storage back to a native TUN adapter.
Use an ownership-preserving bounded channel for native ingress and egress. Keep explicit copy adapters for Vec and WASI boundaries, and verify allocation identity, backpressure, shutdown, and end-to-end delivery.
* perf(udp): preserve packet ownership through sessions
Carry EasyTier tunnel packets through UDP session queues as owned values. Reuse the existing tunnel header for session framing instead of copying payloads into a second packet and rebuilding them on receive.
Keep completion delivery for the public datagram socket API while removing the unused completion channel from streaming tunnel sends. Avoid the unconditional receive-side clone before QUIC routing is known.
* perf(peer): publish packet filters as immutable snapshots
Replace per-packet async and synchronous registry locks with ArcSwap snapshots. Permanent filters now need no activity checks, while managed registrations retain explicit acquire/release visibility.
Closing a managed registration marks it inactive before atomically removing it. Existing snapshots keep in-flight filters alive, and registration mutations prune inactive entries while preserving newest-first order.
* perf(instance): give native hosts direct packet egress
Let the core create one bounded HostPacket channel and transfer its receiver directly to a PacketEgressHost during startup. Native TUN runtimes now consume that receiver without the intermediate PacketSink channel and forwarding task.
Keep PacketSinkEgress as the compatibility adapter for callback and test hosts, and make receiver installation one-shot across desktop, mobile, and disabled runtimes.
* perf(crypto): restore accelerated native AEAD backends
Move Ring and OpenSSL implementations behind the core Encryptor seam.
Portable builds continue selecting only supported backends.
Restore historical precedence: OpenSSL, Ring, then RustCrypto. Keep
backend availability consistent across secure transports and cover
fixed-nonce wire compatibility between implementations.
* perf(udp): receive native datagrams into owned buffers
Extend the portable UDP socket seam with an owned-datagram receive path.
Keep a compatible default for portable hosts. Native Unix sockets write
recvmsg output directly into the final BytesMut allocation.
This removes the per-packet stack-to-heap copy introduced by the portable
socket boundary without exposing native socket resources to core.
* perf(data-plane): remove portable hot-path overhead
Restore native throughput lost while generalizing the host and UDP
session layers.
Read packet policy once per send, update traffic counters through
registry guards, and preserve packet ownership while UDP dispatch
borrows stable session state.
Move UDP shutdown monitoring into a control task so forwarding avoids
a select future per packet. Bound native datagram storage to 8 KiB,
reject oversized sends, and drop truncated Unix receives.
Keep accelerated AEAD selection warning-free when portable crypto
features are also built. Cover session bounds, truncation, and idle
shutdown with regression tests.
* fix(udp): preserve portable datagram receive semantics
Keep the public portable receive capacity at the theoretical UDP
maximum instead of silently shrinking it to the native fast-path limit.
Apply the 8 KiB session boundary after a complete portable receive,
so Windows cannot turn an oversized datagram into a fatal listener
error and other adapters cannot dispatch a truncated prefix.
Cover dropping an oversized packet while the same portable socket
continues to deliver the following valid datagram.
* fix(ci): align feature gating with backend selection
Compile the Ring implementation in production only when OpenSSL is not
selected, while retaining it for cross-backend unit tests.
Remove stale test imports and assert UDP dispatch results so the strict
workspace Clippy job passes without suppressing diagnostics.
Create easytier-core as the portable owner of configuration,
connectivity, tunnels, peer and routing state, gateways, management,
the data plane, and instance lifecycle. Keep operating-system
integration, native protocol engines, process startup, and presentation
in easytier behind explicit Host capability adapters.
Create easytier-proto to own schemas, generated RPC types, descriptors,
and feature-scoped protocol slices. Remove runtime protobuf reflection
from core while preserving unknown route-peer fields across forwarding.
Normalize instance construction through CoreInstance, CoreHostAdapters,
CoreProcessRuntime, and InstanceManager. Make the runtime config store
the only authoritative mutable configuration after startup.
Move the portable TCP/UDP data plane into core and extract a generic
OperationBroker for completion, cancellation, disposal, and capacity
accounting. Expose the session-based FFI v2 completion API and keep the
WASI guest ABI, wire schemas, and adapters with core.
Migrate CLI, GUI, web, FFI, Android JNI, OHOS, uptime, and mobile
consumers to the shared manager and core state. Add explicit user/web
config ownership and revision-aware web reconciliation.
Preserve configuration, wire, and management behavior while fixing
regressions discovered by the full platform and integration matrix:
- inherit advertised relay capabilities in foreign networks;
- refresh OSPF peer state immediately after runtime config changes;
- restore CLI GlobalCtx event output without forcing GUI logging;
- retain legacy encryption names and standalone RPC tunnel metadata;
- restore ICMP host composition and fragmented UDP handling;
- use portable 64-bit atomics on 32-bit MIPS targets; and
- retain discarded operations until late cancellation completes.
Validate the refactor across 45 GitHub checks, including Linux, macOS,
Windows, FreeBSD, web, GUI, Android, OHOS, feature profiles, and
three-node and subnet-proxy integration tests.
BREAKING CHANGE: internal Rust module paths are not preserved. Legacy
native data-plane APIs are replaced by the session-based FFI v2 API.
The dedicated Android data-plane wrapper is removed.
handle_list_all_sessions returned client_mgr.list_sessions(), which
iterates user_clients_map across ALL users and returns every session's
StorageToken (token, client_url, machine_id, user_id). The handler is
mounted under login_required! but performed no per-user authorization:
it fetched get_group_permissions() only to println! the result, then
returned the full cross-user list. Any authenticated user could read
every other user's device token and public client_url.
Scope the result to the caller by adding
Storage::list_user_client_tokens(user_id) /
ClientManager::list_sessions_by_user_id(user_id), mirroring the existing
per-user pattern in handle_get_summary (list_machine_by_user_id). Also
drop the leftover debug println! and the unwrap() on the current user
(return 401 instead).
* bench: add packet bytes extraction Criterion benchmark
Adds a Criterion benchmark under easytier/benches/ covering
ZCPacket::payload_bytes and tunnel_payload_bytes at 1280/4096-byte payload
sizes, using iter_batched so ZCPacket construction stays in the setup phase
and is excluded from the timed region.
- Register the [[bench]] entry in easytier/Cargo.toml.
- Document the bench and PACKET_BYTES_* env vars in benches/README.md.
* perf: reduce packet buffer slicing churn
Replace BytesMut::split_off with Buf::advance in ZCPacket bytes
extraction paths (payload_bytes, tunnel_payload_bytes, convert_type,
drop_foreign_header) and in TunZCPacketToBytes, and simplify the
copy_from_slice in new_from_payload.
When the buffer is in its unique (VEC) representation, split_off promotes
it to the shared (ARC) representation, allocating a Shared control block
and bumping the refcount on every call, and pins the buffer in shared
mode. advance only mutates the in-place ptr/len/cap fields, avoiding that
allocation/refcount churn on the TX hot path. The byte data itself is not
copied by either path.
Fix web/frontend compat bugs in managed config & runtime status
- Preserve [[peer]].peer_public_key when TOML configs round-trip
through the web/managed NetworkConfig path
- Keep old peer_urls clients working while adding structured peer
metadata for new clients
- Make frontend protobuf JSON normalization preserve omitted-field
semantics instead of turning missing data into misleading defaults
- Harden runtime status rendering against omitted or string-encoded
backend fields
- Expose peer-route feature flags in the web status UI
Fixes SOCKS5/port-forward handling for peer data packets
whose source endpoint was rewritten by the KCP or QUIC proxy path.
Keep SOCKS5 entry accounting consistent by centralizing insert/remove
operations, decrementing only for actual removals, avoiding underflow,
and resetting counts when entries are retained or cleared after IPv4 changes.
This PR fixes IPv6 UDP hole punching for peers with multiple public IPv6
addresses by adding two RPC signals:
- connector_addrs: connector-side candidate public IPv6 socket addresses
that the remote peer should punch back to.
- preferred_src_ipv6: remote listener IPv6 address that the remote peer
should use as the UDP source when sending hole-punch packets back.
Together, these let the connector try all usable local IPv6 candidates
while keeping the remote punch-back
packet sourced from the same IPv6 address that the connector is dialing.
Replace std::time::Instant with quanta::Instant on per-packet, per-RPC,
and per-session paths. TSC-based, ~5ns vs ~25ns per now() call.
Reuses the existing `extern crate self as hotpath` alias so
`use hotpath::instant::Instant;` resolves to the same quanta type with
or without the hotpath feature. Leaves tokio::time::Instant and
smoltcp::time::Instant untouched.
* refactor(web): use generated proto network types
* fix(core): preserve dumped config flags
* test(web): cover config flag save paths
* fix(ci): use system protoc before frontend codegen
* fix(ci): serialize frontend-lib builds
Handle TUN receive errors by marking the fake TCP stack closed and
clearing registered sockets instead of panicking.
Refuse new sockets on closed stacks and let listeners recreate stacks
when the reader task exits.
Remove quinn-plaintext to fix connection errors caused
by different hash values across platforms.
On x64, maintain compatibility with quinn-plaintext.