Compare commits

...
Author SHA1 Message Date
KKRainbow 0f2f019f16 ci(core): keep matrix jobs running after transient failures
Disable fail-fast for the Core build matrix so one flaky external
download does not cancel every other platform build. This preserves
successful matrix results and makes failed jobs independently
retryable.
2026-08-13 06:40:33 +08:00
KKRainbow 86bf3df0fd fix(shared-tun): restore routed IPv6 replies
Forward route-owned packets unchanged when a member has no address
suitable for source translation. This preserves public IPv6 provider
replies while keeping cross-member source translation when an address
is available.

Keep the shared DNS route backend available in tun-only builds and
use the current error conversion in the FreeBSD IPv4 setup path.
2026-08-13 02:51:40 +08:00
KKRainbow daf30bff6e test(shared-tun): satisfy strict clippy check
Return the shared route owner count expression directly so the new root
integration helper passes the workspace -D warnings lint gate.
2026-08-13 01:59:44 +08:00
KKRainbow b5bc4a853d fix(shared-tun): close mobile routing gaps
Translate IPv6 traffic across shared mobile members, including
transport checksums and reverse-flow handling.

Normalize saved Android TUN configs and detach runtimes before the
VPN fd closes. Restore the mainline lockfile after resolving the merge.
2026-08-13 01:43:40 +08:00
KKRainbow 144e306562 Merge upstream main into shared virtual NIC work
Port shared TUN ownership, routing, mobile source dispatch, and
Magic DNS route claims onto the native runtime-host architecture.

Preserve per-member lifecycle and add focused desktop, mobile, netlink,
GUI, FFI, and root integration validation for the merged code.
2026-08-13 00:56:52 +08:00
52bb26680a fix(fake-tcp): normalize Linux packet sockets to layer 3 (#2255)
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>
2026-08-12 20:21:42 +08:00
KKRainbow 0b27ac2885 feat(credentials): support managed credential synchronization (#2490)
* 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.
2026-08-10 23:20:36 +08:00
韩嘉乐andFrankHan 23d55373a4 feat(ohos): add nearby console integration and improve UDP path selection (#2486)
* 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>
2026-08-10 14:15:09 +08:00
KKRainbow d375d7e455 feat(mini): add compact native EasyTier client (#2479)
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.
2026-08-09 19:43:30 +08:00
KKRainbow 1e40350c89 feat(wasi): expose protobuf RPC request ABI (#2477)
* 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.
2026-08-07 18:34:53 +08:00
KKRainbow e31bde1836 feat(web): add standalone WASM config generator (#2480)
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.
2026-08-07 16:25:33 +08:00
KKRainbow 86222771c5 fix(peer): recover from asymmetric direct connections (#2476)
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.
2026-08-07 10:51:15 +08:00
刚刚and225284228a-droid d114cdd20f fix(web-client): time out stalled config-server dials (#2461)
Co-authored-by: 225284228a-droid <239500008+225284228a-droid@users.noreply.github.com>
2026-08-04 10:08:57 +08:00
Chenx Dust df874b85be refactor(core): use linearizable lazy token bucket (#2421)
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.
2026-08-04 10:08:37 +08:00
韩嘉乐andFrankHan 8d475dc3fc fix(tunnel): preserve websocket cleanup after send errors (#2468)
Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local>
2026-08-03 10:15:42 +08:00
韩嘉乐 40c857748f feat(ohos): 接入 Pro 运行时并自动化 HAR 交付 (#2462) 2026-08-01 16:35:14 +08:00
KKRainbow afbba5d928 refactor(core): migrate packet processing from pnet to smoltcp (#2456)
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.
2026-08-01 10:57:23 +08:00
刚刚and刚刚 fc3914baf5 Require a full config server URL (#2459)
Co-authored-by: 刚刚 <239500008+225284228a-droid@users.noreply.github.com>
2026-07-31 16:47:40 +08:00
zyflypzlc 4923a13c5d 增加easytier_ffi对Windows7的兼容 (#2458) 2026-07-30 20:32:08 +08:00
KKRainbow d55e63b88e perf(wasi): optimize data plane and extend host ABI to v3 (#2455)
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.
2026-07-28 00:29:08 +08:00
KKRainbow 7b506e25a7 perf(data-plane): reduce per-packet synchronization overhead (#2453)
* 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.
2026-07-27 21:32:57 +08:00
KKRainbow 7fb42c3b73 perf(data-plane): restore native throughput after host portability (#2452)
* 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.
2026-07-26 22:54:43 +08:00
KKRainbow dc11298558 ci: sign and notarize macOS GUI builds (#2418) 2026-07-26 15:44:19 +08:00
KKRainbow 021f523431 refactor(core): separate portable core from native runtime (#2451)
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.
2026-07-26 15:41:55 +08:00
Moder Steven 346f32d3d0 fix(web): scope GET /api/v1/sessions to the authenticated user (#2445)
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).
2026-07-20 13:47:15 +08:00
fanyang f24735a86f perf: reduce packet buffer slicing churn (#2381)
* 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.
2026-07-01 23:19:34 +08:00
7756a15cbe feat: add interface address fallback for IPv6 prefix detection (DHCPv6 IA_NA / SLAAC) (#2334)
feat: add IPv6 prefix fallback and NDP proxy for SLAAC/IA_NA

Add interface address fallback for IPv6 prefix detection when
route-based detection fails (no delegated prefix on LAN). Covers
DHCPv6 IA_NA / SLAAC where prefix is assigned to WAN without PD.
Uses getifaddrs() and Ipv6Inet to handle host bits.

Add NDP proxy sync for on-link prefixes. ISP router uses NDP to
resolve MACs; EasyTier /128 addresses on tun0 won't answer on
physical WAN. Periodic sync (30s) reads /128 routes via netlink
and manages proxy neigh entries. Auto-enables proxy_ndp sysctl.
Returns WAN ifindex from detection; static NDP_WAN_IFINDEX for
sync task. Spawns from provider reconcile when auto-detection
enabled.

Add tests: fallback default route interface, on-link NDP proxy,
interface prefix selection.

Fixes #2333

Co-authored-by: ririyeye <200610237@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: KKRainbow <443152178@qq.com>
2026-07-01 12:34:46 +08:00
fanyang 741460e1e4 bench: add Criterion TX throughput benchmark (#2397) 2026-06-30 22:10:34 +08:00
fanyang 13412895c5 refactor(core): remove hotpath profiling (#2394)
hotpath adds noticeable overhead on hot paths, so remove the optional
profiling integration while keeping direct quanta::Instant timing.
2026-06-30 09:50:27 +08:00
KKRainbow 425a24273b fix: preserve secure relay sessions during cleanup (#2393)
* fix: skip secure relay packets in peer conn filter
* fix: keep active secure relay sessions during GC
2026-06-30 00:04:17 +08:00
KKRainbow 4e61612944 fix(web): preserve managed config and status compatibility (#2389)
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
2026-06-29 12:40:04 +08:00
KKRainbow 15e5d89f70 Fix SOCKS5 port forwarding for modified peer data packets (#2391)
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.
2026-06-29 10:24:57 +08:00
KKRainbow 46f1b57367 perf(ipv6_hole_punch): handle multiple ipv6 public ip correctly (#2387)
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.
2026-06-28 20:42:16 +08:00
KKRainbow 9cb3833216 perf(easytier-web): improve easytier-web webhook performance (#2383)
* feat(web): reconcile managed config revisions
* feat(web): cache managed runtime configs per session
* test: cover managed web config delivery
2026-06-28 13:14:40 +08:00
fanyang 7205517160 perf(core): use quanta::Instant for hot-path timing (#2384)
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.
2026-06-28 10:43:55 +08:00
fanyang be2034dd06 feat: add optional hotpath profiling support (#2380)
* feat: add hotpath profiling support
* perf(hotpath): make hotpath an optional dependency
2026-06-27 13:12:28 +08:00
KKRainbow f0d00d6161 refactor(web): use generated proto network types (#2373)
* 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
2026-06-27 13:09:28 +08:00
fanyang 034f5066cd fix(faketcp): handle closed tun reader without panic (#2308)
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.
2026-06-22 10:54:48 +08:00
fanyang 9869ddaa4b fix: clarify config parse errors (#2360)
* fix: improve config parse diagnostics
* fix: polish config error context
* test: cover non-ascii config diagnostics
2026-06-21 21:56:49 +08:00
Luna Yao 5ea6766238 fix: raise max_headers in ws handshake to 128 (#2366) 2026-06-21 21:54:07 +08:00
Luna Yao 5efbc8587f upgrade guarden to 0.2.0 (#2365) 2026-06-18 23:45:28 +08:00
HYec 7632cd64da Fix latency-first routing for direct peers (#2358) 2026-06-16 20:58:07 +08:00
sijie.sun ba86e47cc6 fix: adapt mobile ffi tun fd API 2026-06-16 20:31:22 +08:00
sijie.sun d742fa34aa refactor: simplify shared virtual nic helpers 2026-06-16 12:46:28 +08:00
sijie.sun 800c840bb5 shared-tun: preserve member ownership on mobile
Introduce shared NIC source ownership and dispatcher handling so a
single dev_name can be shared by multiple tun-enabled instances while
keeping per-member IP and route claims distinct.

Pass Android VpnService fd registration with per-instance source and
route claims. Keep the VPN address list limited to real member
addresses and allow AF_INET6 without installing hidden fd00::1.

Invalidate dispatcher flow and NAT state when source ownership changes
or a member unregisters. Avoid rewriting non-first IPv4 fragment
payloads, and adjust fragmented TCP/UDP checksums without recomputing
over partial fragment bodies.

Preserve source-owner routing for equal-prefix route conflicts, keep
ICMP echo NAT entries distinct by echo id, and retry stale flow-owner
send failures from the original packet. Only record NAT state after a
translated packet is accepted by its member.

Apply Linux IPv4 route preferred-source hints for shared routes and keep
route repair paths source-aware. Keep Darwin ifcfg access scoped to
cleanup-only paths where netns is not available.
2026-06-16 02:16:25 +08:00
韩嘉乐 16b666ad25 fix: route_update message is not lag (#2355) 2026-06-16 00:00:48 +08:00
sijie.sun ca17e856a6 fix: tighten shared virtual nic lifecycle
Key shared virtual NIC registry entries by both netns and device
name. Instances in different namespaces can no longer reuse the same
underlying device by accident.

Store each member's configured MTU separately and publish it as
the initial MTU claim after device creation. Later members no longer
inherit only the first member's MTU.

Shut down the dispatcher when the last current member leaves, and
invalidate the shared NIC if member cleanup fails. The registry entry
remains available for a later clean recreate.

Keep Magic DNS fake-IP routes owned by shared TUN members through
the shared backend. Stopping one member no longer removes the route
while another shared member still owns it, and Linux retained routes
are re-applied after address deletion.

Extend the shared TUN Magic DNS integration test to drop one shared
member first, verify DNS still works through the remaining member,
and then verify final cleanup after the last member exits.
2026-06-15 01:34:29 +08:00
sijie.sun d87624b353 fix: address shared tun ci failures 2026-06-14 23:14:45 +08:00
sijie.sun a7ab60e0e4 fix: support android shared tun fd groups
Android previously treated setTunFd as a single-instance update, and the
VpnService plugin could only expose one IPv4 address. That made shared
TUN members disable each other or leave only one address configured.

Group enabled Android TUN instances by shared dev_name, send the fd to
every compatible member, and only disable incompatible TUN users. Build
the Android VPN request from the whole running shared group and pass
every IPv4 address to VpnService.

The shared mobile dispatcher now owns current fd device state on a
process-level runtime. New setTunFd calls replace that state even when
the raw fd number is reused, and mobile TUN read/write/create failures
rebuild with backoff while preserving member registrations.

Protect shared member cleanup with per-registration ownership tokens, so
old async cleanup cannot unregister a recreated member or remove its
source claims. Mobile source addresses are registered in the dispatcher
without applying OS ifcfg changes, so Android-originated packets return
through the owning instance.

When one shared member stops while another remains, notify the frontend
to recalculate the VpnService config instead of leaving stale addresses
and routes. Serialize Android VpnService config recalculation so stale
async events cannot overwrite newer shared-group state.

If one shared member is not ready, rebuild from the healthy members and
retry the missing member later. If no healthy member remains, stop the
Android VPN service instead of keeping stale routes active.
2026-06-14 16:24:08 +08:00
Luna Yao 8909e88484 do not panic when fail to parse flags (#2349) 2026-06-14 13:12:20 +08:00
sijie.sun 3d0d2bed9e fix: preserve macos shared tun ipv4 aliases 2026-06-14 11:38:12 +08:00
sijie.sun 7ef023fcdd test: cover shared tun dynamic proxy routes
Cover shared tun duplicate proxy CIDR failover and runtime proxy
CIDR add/remove through netns integration tests.

Start IP proxy from config patch events so a node that did not have
proxy CIDRs at startup can serve a later proxy network patch.

Ignore NotFound for shared NIC remove-side ifcfg cleanup so member
teardown does not poison shared owner state when the OS item is
already gone.

Add dispatcher coverage for TUN read failure invalidation and member
close notification.
2026-06-14 02:37:19 +08:00
sijie.sun 69d476ad13 test: cover shared tun subnet proxy paths
Add a shared tun subnet proxy topology with shared members on both
the source and destination sides. The test exercises ICMP, TCP
subnet proxy, and UDP subnet proxy against a real netns target.

Add focused KCP and QUIC TCP proxy variants that verify the selected
TcpProxyConnect transport label. The helper waits for real proxy CIDR
reachability before running transport-specific checks to avoid racing
route installation.
2026-06-14 01:26:19 +08:00
sijie.sun cf3fcb75b5 test: cover shared tun magic dns in netns
Add an end-to-end shared tun Magic DNS test with two members
sharing one device in the same namespace. The test checks real
netns ping paths and queries the Magic DNS fake IP through dig.

Teach DnsRunner to pass the instance netns into
MagicDnsServerInstance so fake DNS route add/remove operations run
where the tun device exists. Keep the existing constructors as
wrappers so current callers stay source-compatible.
2026-06-14 01:26:19 +08:00
sijie.sun 17195a5cc4 fix: dispatch shared tun packets by source owner
Track each shared tun member's claimed IPv4 and IPv6 addresses in
the dispatcher.

When a packet arrives from the shared device, keep the existing flow
affinity lookup first, then route by source address owner before
falling back to an arbitrary member. This keeps public IPv6 auto
traffic on the member that owns the leased address while preserving
the simple local-origin fallback.

Migrate high-value shared tun coverage from the backup branch: real
member communication, no_tun isolation, proxy CIDR forwarding, and
public IPv6 auto through a shared device.
2026-06-14 01:26:19 +08:00
sijie.sun 22bcfebe3e test: cover shared nic instance selection
Add focused tests for Instance nic context creation without creating a
real tun device.

Verify empty dev_name keeps the dedicated backend, while matching
shared dev_name values reuse the same shared virtual nic and get fresh
member ids on each recreation.
2026-06-14 01:26:19 +08:00
sijie.sun 47a2fb19a6 feat: wire shared nic into instance setup
Route normal, DHCP, and mobile nic creation through one helper that
selects the shared backend when dev_name is configured.

Keep dedicated nic creation as the default for configs without a
dev_name, and keep no_tun handling in the existing callers.

Store a process-wide shared virtual nic registry behind each instance
so networks with the same dev_name attach to the same shared device.
2026-06-14 01:26:19 +08:00
sijie.sun 62e43855cb feat: support shared nic mobile fd
Create the shared virtual nic dispatcher from the mobile tun fd.

Later shared members attach through their ring tunnel and do not
consume another fd. Dedicated mobile nic creation stays unchanged.

Fix mobile no-magic-dns cfg so the mobile tun path checks without
enabling magic-dns.
2026-06-14 01:26:19 +08:00
sijie.sun 5efbf4853a feat: apply shared nic member ifcfg
Route shared nic IP and route configuration through member claims.
Repeated addresses or routes now touch the OS device only when the
first owner appears, and are removed only after the last owner leaves.

Forward NicBackend shared operations to the member claim layer.
Dynamic proxy and public IPv6 route updates now go through NicBackend,
so they use the same ownership merge behavior.

Clean member claims when shared member registration is dropped. Keep
raw ifcfg access for non-Linux platform cleanup only, and document
that this raw path does not carry the netns guard.
2026-06-14 01:26:19 +08:00
sijie.sun 3e0bde68b8 perf: bound shared nic flow eviction
Replace full flow table clears with a small FIFO order queue so overflow evicts only older flow ownership hints.

Keep lookup read-only on the packet hot path and prune stale queue entries when a shared member unregisters.
2026-06-14 01:26:19 +08:00
sijie.sun bda06ebb61 perf: speed up shared nic flow lookup
Parse shared virtual nic flow keys directly from IP payload bytes instead of building pnet packet wrappers on the dispatcher hot path.

Use a local HashMap for flow owner lookup and clear the bounded table on overflow instead of removing a BTreeMap entry by key order.
2026-06-14 01:26:19 +08:00
sijie.sun be0859aca6 refactor: keep shared dispatcher state local
Move shared member and flow ownership state into the dispatcher task.

Use control messages for member register and unregister events.

Keep the member table lock off the packet forwarding path.
2026-06-14 01:26:19 +08:00
sijie.sun b2f1b37336 refactor: add shared virtual nic dispatcher
Add the shared NIC dispatcher that owns the underlying VirtualNic tunnel.

Connect member ring tunnels through a private member table.

Track reverse flow ownership from packets written by a member.

Replies read from the shared device go back to the same member.

Packets without an owner fall back to any active member.

Mark the shared NIC invalid when the underlying tunnel closes or fails.

Notify members so their NicCtx can rebuild.
2026-06-14 01:26:19 +08:00
sijie.sun 583fb45939 refactor: reuse ring tunnel for shared members
Use create_ring_tunnel_pair for member-level shared NIC tunnels.

That replaces the custom packet stream and sink implementation.

Keep one side of each ring tunnel in SharedVirtualNic's member map.

Later dispatcher code can connect those endpoints to the shared device.
2026-06-14 01:26:19 +08:00
sijie.sun a343a907a9 refactor: add shared nic ctx constructor
Add a shared NicCtx constructor that obtains a SharedVirtualNicMember from the registry and wraps it in NicBackend::Shared.

Keep NicCtx::new on the dedicated backend path so existing runtime behavior does not change before the shared tunnel implementation is ready.
2026-06-14 01:26:19 +08:00
sijie.sun b1ac64f40f refactor: route nic ctx through nic backend
Replace NicCtx's direct VirtualNic field with NicBackend while keeping the constructor on the dedicated backend path.

Move existing device creation, ifcfg lookup, IP assignment, and public IPv6 updater calls through backend helpers so shared mode can be wired in a later commit without changing the dedicated flow.
2026-06-14 01:26:19 +08:00
sijie.sun 122fb9b54e refactor: add shared nic backend skeleton
Introduce SharedVirtualNicMember as the per-instance handle for a shared virtual NIC and add a registry helper to create members.

Add NicBackend so NicCtx can later choose between dedicated VirtualNic and shared member-backed tunnel creation without changing the existing dedicated path.
2026-06-14 01:26:19 +08:00
sijie.sun 09780c1698 refactor: add shared virtual nic registry
Add a small registry keyed by dev_name so shared-mode setup can reuse one SharedVirtualNic per device name.

Track invalid shared NICs through a shared flag and replace them on the next get_or_create call.
2026-06-14 01:26:19 +08:00
sijie.sun e533b48244 refactor: decouple virtual nic from global ctx
Move TUN-specific configuration into VirtualNicConfig so VirtualNic and SharedVirtualNic no longer depend on ArcGlobalCtx.

Keep global context side effects in NicCtx, including the Windows generated device-name writeback.
2026-06-14 01:26:19 +08:00
sijie.sun c0bd050742 add shared virtual nic 2026-06-14 01:26:19 +08:00
Luna Yao 5edc4cb1cd fix: remove quinn-plaintext (#2345)
Remove quinn-plaintext to fix connection errors caused
by different hash values ​​across platforms.

On x64, maintain compatibility with quinn-plaintext.
2026-06-14 01:21:35 +08:00
韩嘉乐 e7709f1cb5 [OHOS] feat: improve status manager (#2343)
* fix: improve status manager
2026-06-11 17:27:10 +08:00
韩嘉乐 c0f42ebe8c fix: improve log manager (#2329) 2026-06-07 23:32:22 +08:00
KKRainbow 9d965cae64 Add FFI JNI JSON RPC bridge (#2326)
* Add FFI JNI JSON RPC bridge
* Add FFI instance list API
2026-06-07 17:48:58 +08:00
韩嘉乐 da28c8badc [OHOS] fix: 修复内存泄露问题,并重构日志管理,预防性修复数据库初始化异常问题 (#2328)
* fix: leak memory
feat: new log manager

* fix: fail to init db

* fix: fail to init db

* fix: cargo format
2026-06-07 16:18:54 +08:00
KKRainbow e38b1354b3 Fix credential ospf logic, fix udp subnet proxy loop protection (#2315) 2026-06-07 12:40:09 +08:00
KKRainbow 793b57c2a1 feat(ffi): add async data plane API (#2321)
* feat(ffi): add async data plane API
* feat(ffi): add async data plane examples
* test(ffi): make async Go dataplane tests self-contained
* docs(ffi): document Go async dataplane API
* docs(android): document dataplane JNI API
2026-06-06 21:52:50 +08:00
深鸣 4a25ca934b build: update pnpm config for v11 compatibility (#2322)
pnpm v11 introduces breaking changes that cause frozen installations
to fail:

1. The "pnpm" field in package.json is no longer read. Moved
   `overrides` to `pnpm-workspace.yaml` to fix
   `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`.
2. `strictDepBuilds` is now enabled by default. Added required
   dependencies (esbuild, unrs-resolver, vue-demi) to `allowBuilds` in
   the workspace config to fix `ERR_PNPM_IGNORED_BUILDS`.
2026-06-06 21:49:05 +08:00
KKRainbow 13f2ebfe12 feat(ffi): add config server client bindings (#2320)
Add config server client support for the C FFI and Android JNI bindings.

Reuse the existing easytier::web_client::run_web_client path and 
NetworkInstanceManager; OHOS is unchanged.

Report successful remote config apply/delete operations through a 
callback, with one JSON event per affected instance.

Keep the config server client and FFI data plane mutually exclusive: once 
either side is in use, the other side returns an error instead of sharing
lifecycle state.
2026-06-06 01:52:27 +08:00
Luna Yao 9ba364ff60 feat: string deserialization for prost enums (#2316)
Use pbjson to support string deserialization for enum fields

This allows TOML configs like:
    chainType = "Inbound"
instead of:
    chainType = 1

- Maintain backward compatibility with integer values
- Default serialization format is now string
2026-06-06 00:21:22 +08:00
Neil ba653da9a0 fix: detect credential mode in TOML config loader (#2301)
TomlConfigLoader::new_from_str() always calls NetworkIdentity::new()
with unwrap_or_default() on network_secret, converting None to ''.
This creates a non-zero SHA256 digest, causing credential nodes loaded
from TOML to be misidentified as regular nodes (with network_secret),
which breaks Noise handshake authentication.

Fix: check if secure_mode is enabled AND network_secret is absent/empty,
and call NetworkIdentity::new_credential() in that case.

The same detection already exists in:
- core.rs (CLI path, via --credential flag)
- launcher.rs (GUI/web path, via gen_config)

This makes TOML config loading consistent with the other two entry points.
2026-06-04 22:38:40 +08:00
Luna Yao 64c4d73044 fix QuicSocket payload offset (#2306) 2026-06-04 18:04:39 +08:00
w568w e0745f4bab feat: Add data plane support to FFI (#2287)
1. Overview

This PR adds data plane APIs to easytier-ffi:

TCP Outbound:

- data_plane_tcp_connect
- data_plane_tcp_read
- data_plane_tcp_write
- data_plane_tcp_close

TCP Listener:

- data_plane_tcp_bind
- data_plane_tcp_accept
- data_plane_tcp_listener_close

UDP:

- data_plane_udp_bind
- data_plane_udp_send_to
- data_plane_udp_recv_from
- data_plane_udp_close

2. Key Changes

The main changes are focused on:

- easytier-contrib/easytier-ffi/src/lib.rs: Added FFI interfaces;
  made ERROR_MSG thread-safe.

- easytier/src/gateway/socks5.rs: Bridges the data plane to the
  existing Socks5 server logic.
  - Added EasyTierUdpSocket, mainly wrapping ref-counting and
    critical object (e.g., Socks5EntrySet) hold & drop logic,
    and exposing common fields (e.g., local_addr).
  - Extended Socks5Server functionality to expose TCP and UDP
    socket creation interfaces for FFI calls.

- Other files: Mostly pass-through logic.

- Added a relatively large Go usage example.
2026-06-04 17:17:41 +08:00
XandClaude e3ca7ffa54 feat(socket): add Linux SO_MARK (fwmark) support for underlay sockets (#2288)
Adds a Linux-only socket_mark u32 config flag (CLI: --socket-mark, env:
ET_SOCKET_MARK, TOML/proto: flags.socket_mark, 0 = disabled) that is
applied as SO_MARK to every outbound underlay socket EasyTier creates:
TCP, UDP, QUIC, WebSocket, WireGuard connectors and listeners, plus the
FakeTCP decoy socket. Lets the host policy-route or filter EasyTier
underlay traffic with 'ip rule fwmark ...' or iptables -m mark.

Plumbing mirrors the existing bind_device pattern:
- FlagsInConfig.socket_mark (proto) + default 0 in gen_default_flags
- bind() builder gets a socket_mark arg; setup_socket2_ext calls
  apply_socket_mark which is a no-op for mark=0 and on non-Linux
- TunnelConnector trait gets set_socket_mark(u32) default-no-op method
- IP-based connectors override; create_listener_by_url and the connector
  factory pass mark from global_ctx flags
- QUIC threads mark through QuicEndpointManager::{server,connect}
- WebSocket/FakeTCP/TCP default-bind bypass paths apply mark via
  socket2::SockRef::from(&tokio_socket)
- ForeignNetworkEntry propagates parent socket_mark into its derived ctx

Includes a Linux smoke test plus a CAP_NET_ADMIN-gated test that does a
getsockopt(SO_MARK) round-trip to confirm the kernel applied the value.

SO_MARK requires CAP_NET_ADMIN; ignored silently on non-Linux. FakeTCP's
TUN-written segments are not covered (kernel doesn't tag raw TUN
writes); operators relying on fwmark for FakeTCP must apply an iptables
rule on the FakeTCP TUN device separately.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 09:57:18 +08:00
韩嘉乐 df97f3a64d fix: make ohos snapshot sync and config id validation safer (#2283)
* feat: add the management of config_store_snapshot

* fix: make ohrs snapshot sync and config id validation safer
2026-06-02 22:40:48 +08:00
fanyang 00957e5f9d feat: add Docker healthcheck (#2279) 2026-05-24 23:12:51 +08:00
fanyang bfa3383aaa chore: update kcp-sys (#2277) 2026-05-24 23:11:16 +08:00
fanyang 73bea01f40 fix: support env vars for easytier-web (#2280)
* fix: support env vars for easytier-web
* fix: hide sensitive web env values
2026-05-22 23:59:16 +08:00
韩嘉乐 0378191783 feat: add the management of config_store_snapshot (#2271) 2026-05-22 01:54:43 +08:00
ParkGarden d5fa6a608d fix: Magisk module incorrectly matches the lookup main rule in Android 15 (#2259)
Fix the issue where the Magisk module incorrectly matches the lookup main rule in Android 15's cellular network rules, causing data plane connectivity failure
2026-05-18 12:51:49 +08:00
Luna Yao 811f151155 refactor: rpc build (#2244)
rewrite rpc build with quota crate
2026-05-15 14:01:56 +08:00
Luna Yao 8428a89d2d refactor: introduce HedgeExt for task hedging; rewrite NatDstQuicConnector (#2229) 2026-05-12 20:26:16 +08:00
韩嘉乐andFrankHan 513695297c [OHOS] feat: Enhance Rust kernel with config management and routing improvements (#2227)
* [OHOS.with ai] 将配置管理/配置分享/路由聚合/实例状态解析下沉至 Rust 内核,收敛职责并提升性能 (#2209)

* feat: add ohrs config store and startup error logging

* feat: full ability core for ohos

* feat: full ability core for ohos

* feat: clean code

---------

Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local>

* fix: 添加缺失文件

* fix: 修复更新路由启动两次TUN问题,并调整日志

* fix: rustfmt

* fix: 适配Cidr忽略/32格式路由

* fix: 修复Option适配错误

* fix: rustfmt

* fix: rustfmt

---------

Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local>
2026-05-10 14:15:31 +08:00
21paradox bfbfa2ef8d fix: reuse conn by dst_peer_id, every peer use only 1 quic conn, to fix nat lost problem (#2216) 2026-05-09 22:33:44 +08:00
KKRainbow 8e1d079142 feat: add Windows UDP broadcast relay (#2222)
This may helps games to find rooms in virtual network.

- add opt-in Windows UDP broadcast relay config flag and CLI/env plumbing
- capture local UDP broadcasts with Windows raw sockets, normalize packets, and inject them via PeerManager
2026-05-09 09:56:31 +08:00
fanyang 55f15bb6f0 fix(connector): classify manual reconnect timeouts by stage (#2062) 2026-05-08 22:08:51 +08:00
Luna Yao 96fd39649a revert UPX version to 4.2.4 in core.yml (#2221) 2026-05-07 18:49:40 +08:00
KKRainbow 74fc8b300d chore: bump version to 2.6.4 (#2219) 2026-05-07 13:48:51 +08:00
KKRainbow baeee40b79 fix machine uid and easytier-web panic (#2215)
1. fix(web-client): persist and migrate machine id
2. fix panic when easytier-web session receive malformat packet
2026-05-07 00:57:42 +08:00
fanyang 4342c8d7a2 fix: add missing CLI help text (#2213) 2026-05-05 17:05:34 +08:00
KKRainbow 1178b312fa fix foreign network entry leak (#2211) 2026-05-05 11:01:44 +08:00
fanyang 362aa7a9cd fix: allow omitted ACL config fields (#2206) 2026-05-04 00:47:24 +08:00
KKRainbow 12a7b5a5c5 fix: scope peer center server data to instance (#2198)
Stop sharing PeerCenterServer state through a process-global map so local and foreign-network services cannot mix peer-center data when peer ids overlap.
2026-05-02 01:43:01 +08:00
fanyang 4eba9b07b6 fix(web-client): keep retrying unreachable config server (#2140)
Defer config-server connector creation into the web client retry loop so
service startup does not fail when network or DNS is unavailable.
2026-05-02 00:09:48 +08:00
KKRainbow 1b48029bdc fix: clean stale foreign network state (#2197)
- clear foreign-network traffic metric peer caches on peer removal and network cleanup
- release reserved foreign-network peer IDs on handshake/add-peer error paths
- avoid creating no-op foreign-network token buckets when limits are unlimited
- shrink relay/session maps after cleanup and remove unused peer-center global data entries
2026-05-01 23:30:51 +08:00
KKRainbow 3542e944cb fix(quic): prune stopped endpoints from pool (#2195)
* remove wss port 0 compatibility code
* fix(quic): prune stopped endpoints from pool
2026-05-01 18:51:39 +08:00
KKRainbow 852d1c9e14 feat(gui): add UPnP and public IPv6 advanced options (#2194)
Expose disable-upnp and ipv6_public_addr_auto in the shared web/GUI config editor
bump release metadata to 2.6.3.
2026-05-01 13:45:19 +08:00
KKRainbow 4958394469 fix: protect self peer during credential refresh and allow need-p2p peers through public server (#2192)
* fix: protect self peer during credential refresh

* fix: allow need-p2p peers through public server
2026-05-01 06:59:30 +08:00
KKRainbow 41b6d65604 fix faketcp filter on windows (#2190) 2026-04-30 23:55:56 +08:00
KKRainbow aae30894dd fix: keep file logger disabled by default (#2189) 2026-04-30 21:42:30 +08:00
fanyang 81d169abfc fix: fall back when CLI manage service is unavailable (#2185) 2026-04-30 19:50:50 +08:00
Luna Yao 9c6c210e89 fix: disable SO_EXCLUSIVEADDRUSE on Windows (#2180) 2026-04-30 19:48:54 +08:00
Mg Pig d1c6dcf754 fix: prevent URL input layout flicker with container queries (#2186) 2026-04-30 19:45:01 +08:00
KKRainbow 97c8c4f55a feat: support disabling relay data forwarding (#2188)
- add a disable_relay_data runtime/config patch option
- reuse the existing avoid_relay_data feature flag when relay data forwarding is disabled
2026-04-30 19:44:40 +08:00
KKRainbowandClaude Opus 4.7 ed8df2d58f prevent EasyTier-managed IPv6 from being used as underlay connections (#2181)
When a node has public IPv6 addresses allocated by EasyTier, those addresses
are installed on the host's network interfaces. The system would then pick
them up as candidate source/destination addresses for underlay connections
(direct peer, UDP hole punch, bind addresses), causing overlay traffic to
loop back into the overlay itself.

Add a central predicate is_ip_easytier_managed_ipv6() and apply it at every
point where IPv6 addresses are selected for underlay use:
- Filter managed IPv6 from DNS-resolved connector addresses, including a
  UDP socket getsockname check to detect whether the OS would route through
  the overlay to reach a destination
- Skip managed IPv6 in bind address selection and STUN candidate filtering
- Strip managed IPv6 from GetIpListResponse RPC so peers never learn them
- Pass pre-resolved addresses to tunnel connectors to avoid re-resolution

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 12:17:22 +08:00
lurenjiaandKKRainbow f66010e6f9 fix: preserve URL type in matches_scheme (#2179)
Avoid resolving Url::as_ref() to the full URL string before TunnelScheme
conversion. Add regression coverage for owned/borrowed URLs and the UDP
IPv6 hole-punch branch condition.

Co-authored-by: KKRainbow <443152178@qq.com>
2026-04-28 23:23:41 +08:00
Luna Yao d5c4700d32 utils: replace defer, ContextGuard, DetachableTask with guarden crate (#2163) 2026-04-27 18:29:46 +08:00
KKRainbow 969ecfc4ca fix(gui): refresh service after core version upgrade (#2172) 2026-04-27 15:54:52 +08:00
KKRainbowandClaude Opus 4.7 8f862997eb feat: support allocating public IPv6 addresses from a provider (#2162)
* feat: support allocating public IPv6 addresses from a provider

Add a provider/leaser architecture for public IPv6 address allocation
between nodes in the same network:

- A node with `--ipv6-public-addr-provider` advertises a delegable
  public IPv6 prefix (auto-detected from kernel routes or manually
  configured via `--ipv6-public-addr-prefix`).
- Other nodes with `--ipv6-public-addr-auto` request a /128 lease from
  the selected provider via a new RPC service (PublicIpv6AddrRpc).
- Leases have a 30s TTL, renewed every 10s by the client routine.
- The provider allocates addresses deterministically from its prefix
  using instance-UUID-based hashing to prefer stable assignments.
- Routes to peer leases are installed on the TUN device, and each
  client's own /128 is assigned as its IPv6 address.

Also includes netlink IPv6 route table inspection, integration tests,
and event-driven route/address reconciliation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 21:37:34 +08:00
KKRainbow b20075e3dc fix: allow self virtual IP loopback (#2161) 2026-04-25 21:26:16 +08:00
Luna Yao eb3b5aae51 utils: add DetachableTask & ContextGuard (#2138) 2026-04-25 18:24:36 +08:00
datayurei af6b6ab6f1 fix: avoid panic when validating mapped listeners (#2153) 2026-04-25 17:45:57 +08:00
Luna Yao 5a1668c753 refactor: remove ScopedTask (#2125)
* replace ScopedTask with AbortOnDropHandle
2026-04-25 15:20:25 +08:00
Luna Yao 820d9095d3 replace AsyncRuntime with simpler CancellableTask (#2136) 2026-04-25 10:29:53 +08:00
KKRainbow 2fb41ccbba bump version 262 (#2158) 2026-04-25 10:22:24 +08:00
Luna Yao b4666be696 fix: disable SO_REUSEADDR & enable SO_EXCLUSIVEADDRUSE on Windows (#2128) 2026-04-25 00:37:34 +08:00
KKRainbow 4688ad74ad Honor credential reusable flag (#2157)
- propagate reusable through credential storage, CLI, RPC, routing, and tests
- enforce reusable=false owner election with current topology
- preserve proof-backed groups when refreshing credential ACL groups
2026-04-25 00:22:40 +08:00
Luna Yao f7ea78d4f0 lower max_udp_payload_size to 1200 (#2156) 2026-04-24 21:20:37 +08:00
james.zhang ac112440c3 fix(UrlInput): update parseUrl and buildUrlValue to handle null ports correctly (#2146) 2026-04-23 13:45:09 +08:00
KKRainbow 958b246f05 improve webclient (#2151) 2026-04-23 13:44:18 +08:00
james.zhang 263f4c3bc9 fix(peer_route): exclude current peer ID from proxy CIDR lists (#2149) 2026-04-22 20:30:38 +08:00
Luna Yao ffddc517e1 fix: listener parsing (#2143)
Fixes a CLI listener parsing regression where url crate special-casing for ws/wss could misinterpret inputs like ws:11011, and adds coverage to prevent future regressions.

Changes:

Refactors listener parsing to avoid url::Url parsing for proto:port forms and to support additional shorthand inputs (port-only / IP-only / SocketAddr).
Centralizes “expand to all IpScheme variants” logic in a helper (gen_listeners) while preserving the “port=0 is dynamic” behavior.
Adds unit tests covering valid/invalid listener inputs and expansion behavior.
2026-04-21 23:45:22 +08:00
Debugger Chen 5cd0a3e846 feat: add upnp support (#1449) 2026-04-21 17:19:04 +08:00
Luna Yao f4319c4d4f ci(test): always check everything (#2142)
* ci(test): always check everything
* move Cargo.lock check to the last step
2026-04-21 10:08:27 +08:00
Luna Yao 0091a535d5 use mimalloc for FreeBSD (#2144) 2026-04-21 08:40:21 +08:00
Luna Yao d7a5fb8d66 remove --no-deps from lock check (#2134) 2026-04-20 00:46:26 +08:00
KKRainbow f63054e937 fix: resolve Android APK version fallback to 1.0 on CI (#2131) 2026-04-19 19:06:37 +08:00
KKRainbow efc043abbb bump version to v2.6.1 (#2129) 2026-04-19 16:49:45 +08:00
Mg Pig 40c6de8e31 fix(core): restrict implicit config merge to explicit config files (#2127) 2026-04-19 10:39:04 +08:00
KKRainbow 2db655bd6d fix: refresh ACL groups and enable TCP_NODELAY for WebSocket (#2118)
* fix: refresh ACL groups and enable TCP_NODELAY for WebSocket
* add remove_peers to remove list of peer id in ospf route
* fix secure tunnel for unreliable udp tunnel
* fix(web-client): timeout secure tunnel handshake
* fix(web-server): tolerate delayed secure hello
* fix quic endpoint panic
* fix replay check
2026-04-19 10:37:39 +08:00
Mg Pig c49c56612b feat(ui): add ACL graphical configuration interface (#1815) 2026-04-18 20:23:53 +08:00
Mg Pig 6ca074abae feat(nix): 添加 rustfmt 和 clippy 到 Rust 工具链扩展 (#2126) 2026-04-18 20:23:26 +08:00
Luna Yao 84430055ab remove hashbrown (#2108) 2026-04-18 11:06:34 +08:00
Mg Pig 432fcb3fc3 build(nix): add mold to the flake dev shell (#2122) 2026-04-18 09:06:45 +08:00
Luna Yao fae32361f2 chore: update Rust to 1.95; replace cfg_if with cfg_select (#2121) 2026-04-17 23:41:31 +08:00
Luna Yao bcb2e512d4 utils: move code to a dedicated mod; add AsyncRuntime (#2072) 2026-04-16 23:32:07 +08:00
Luna Yao 82ca04a8a7 proto(utils): add MessageModel & RepeatedMessageModel (#2068)
* add FromIterator, Extend, AsRef, AsMut, TryFrom<[Message]>
2026-04-15 19:40:09 +08:00
Luna Yao 2ef3b72224 proto: add some conversion for Url (#2067) 2026-04-15 19:39:24 +08:00
Luna Yao 6d319cba1d tests(relay_peer_e2e_encryption): wait for the key of inst3 before ping test (#2069) 2026-04-15 19:39:00 +08:00
Luna YaoandKKRainbow 3687519ef3 turn off ansi for file log (#2110)
Co-authored-by: KKRainbow <443152178@qq.com>
2026-04-15 19:38:27 +08:00
Luna Yao 3a4ac59467 log: change default log level of tests to WARNING (#2113) 2026-04-14 18:10:38 +08:00
Luna YaoandKKRainbow 1cfc135df3 ci: remove -D warnings from test (#2109)
Co-authored-by: KKRainbow <443152178@qq.com>
2026-04-14 12:35:05 +08:00
KKRainbow 5b35c51da9 fix packet split on udp tunnel and avoid tcp proxy access rpc portal (#2107)
* distinct control / data when forward packets
* fix rpc split for udp tunnel
* feat(easytier-web): pass public ip in validate token webhook
* protect rpc port from subnet proxy
2026-04-13 11:03:09 +08:00
Luna Yao ec7ddd3bad fix: filter overlapped proxy cidrs in ProxyCidrsMonitor (#2079)
* feat(route): add async methods to list proxy CIDRs for IPv4 and IPv6
* refactor(ProxyCidrsMonitor): get proxy cidrs from list_proxy_cidrs
2026-04-12 22:18:54 +08:00
Luna Yao 6f3e708679 tunnel(bind): gather all bind logic to a single function (#2070)
* extract a Bindable trait for binding TcpSocket, TcpListener, and UdpSocket
2026-04-12 22:16:58 +08:00
Luna Yao 869e1b89f5 fix: remove log (file) when level is explicitly set to OFF (#2083)
* fix level filter for OFF
* remove unwrap of file appender creation
2026-04-12 22:16:30 +08:00
Luna Yao 9e0a3b6936 ci: rewrite build workflows (#2089) 2026-04-12 22:14:41 +08:00
Luna Yao c6cb1a77d0 chore: clippy fix some code on Windows (#2106) 2026-04-12 22:13:58 +08:00
deddeyandKKRainbow 83010861ba Optimize network interface configuration for macOS and FreeBSD to avoid hard-coded IP addresses (#1853)
Co-authored-by: KKRainbow <443152178@qq.com>
2026-04-12 21:00:59 +08:00
Luna Yao daa53e5168 log: auto-init log for tests (#2073) 2026-04-12 13:04:21 +08:00
fanyang 51befdbf87 fix(faketcp): harden packet parsing against malformed frames (#2103)
Discard malformed fake TCP frames instead of panicking so OpenWrt
nodes can survive unexpected or truncated packets.

Also emit the correct IPv6 ethertype and cover the parser with
round-trip and truncation regression tests.
2026-04-12 13:02:23 +08:00
Luna Yao 8311b11713 refactor: remove NoGroAsyncUdpSocket (#1867) 2026-04-10 23:22:08 +08:00
Luna Yao 19c80c7b9c cli: do not add offset when port = 0 (#2085) 2026-04-10 23:21:15 +08:00
Luna Yao a879dd1b14 chore: update Rust to 2024 edition (#2066) 2026-04-10 00:22:12 +08:00
Luna Yao a8feb9ac2b chore: use Debug to print errors (#2086) 2026-04-09 09:45:55 +08:00
Luna Yao c5fbd29c0e ci: fix skip condition for draft pull requests in CI workflows (#2088)
* ci: run xxx-result only when pre_job is run successfully
* fix get-result steps
2026-04-09 09:45:04 +08:00
Luna Yao 26b1794723 ci: accecelerate pipeline (#2078)
* enable concurrency

pr

* do not run build on draft PRs

pr

* enable fail-fast for build workflows
2026-04-08 08:43:03 +08:00
Luna Yao 371b4b70a3 proto(utils): add TransientDigest trait (#2071) 2026-04-08 00:06:48 +08:00
Luna Yao b2cc38ee63 chore(clippy): disallow some methods from itertools (#2075) 2026-04-07 16:27:33 +08:00
Luna Yao 79b562cdc9 drop peer_mgr in time (#2064) 2026-04-06 11:31:05 +08:00
fanyang e3f089251c fix(ospf): mitigate route sync storm under connection flapping (#2063)
Addresses issue #2016 where nodes behind unstable networks
(e.g. campus firewalls) cause excessive traffic that can freeze
the remote node.

Two changes in peer_ospf_route.rs:

- Make do_sync_route_info only trigger reverse sync_now when
  incoming data actually changed the route table or foreign
  network state.  The previous unconditional sync_now created
  an A->B->A->B ping-pong cycle on every RPC exchange.

- Add exponential backoff (50ms..5s) to session_task retry loop.
  The previous fixed 50ms retry produced ~20 RPCs/s during
  sustained network instability.
2026-04-06 11:26:20 +08:00
fanyang cf6dcbc054 Fix IPv6 TCP tunnel display formatting (#1980)
Normalize composite tunnel display values before rendering peer and
debug output so IPv6 tunnel types no longer append `6` to the port.

- Preserve prefixes like `txt-` while converting tunnel schemes to
  their IPv6 form.
- Recover malformed values such as `txt-tcp://...:110106` into
  `txt-tcp6://...:11010`.
- Reuse the normalized remote address display in CLI debug output.
2026-04-05 22:12:55 +08:00
fanyang 2cf2b0fcac feat(cli): implement connector add/remove, drop peer stubs (#2058)
Implement the previously stubbed connector add/remove CLI commands
using PatchConfig RPC with InstanceConfigPatch.connectors, and
remove the peer add/remove stubs that had incorrect semantics.
2026-04-05 13:56:17 +08:00
dependabot[bot] aa0cca3bb6 build(deps): bump quinn-proto in /easytier-contrib/easytier-ohrs (#2059)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.13 to 0.11.14.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.13...quinn-proto-0.11.14)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-05 13:16:33 +08:00
KKRainbow fb59f01058 fix: reconcile webhook-managed configs and make disable_p2p more intelligent (#2057)
* reconcile infra configs on webhook validate
* make disable_p2p more intelligent
* fix stats
2026-04-04 23:41:57 +08:00
Luna Yao e91a0da70a refactor: listener/connector protocol abstraction (#2026)
* fix listener protocol detection
* replace IpProtocol with IpNextHeaderProtocol
* use an enum to gather all listener schemes
* rename ListenerScheme to TunnelScheme; replace IpNextHeaderProtocols with socket2::Protocol
* move TunnelScheme to tunnel
* add IpScheme, simplify connector creation
* format; fix some typos; remove check_scheme_...;
* remove PROTO_PORT_OFFSET
* rename WSTunnel.. -> WsTunnel.., DNSTunnel.. -> DnsTunnel..
2026-04-04 10:55:58 +08:00
Luna Yao 9cc617ae4c ci: build rpm package (#2044)
* add rpm to ci
* rename build_filter to build-filter
* use prepare-pnpm action
2026-04-04 10:32:08 +08:00
韩嘉乐 e4b0f1f1bb Rename libeasytier_ohrs.so to libeasytier_release.so when build release package (#2056)
Rename shared library file for release.
2026-04-04 10:29:37 +08:00
Luna Yao 443c3ca0b3 fix: append address of reverse proxy to remote_addr (#2034)
* append address of reverse proxy to remote_addr
* validate proxy address in test
2026-03-30 16:48:23 +08:00
Luna Yao 55a0e5952c chore: use cfg_aliases for mobile (#2033) 2026-03-30 16:38:39 +08:00
KKRainbow 1dff388717 bump version to v2.6.0 (#2039) 2026-03-30 15:50:07 +08:00
Luna Yao 61c741f887 add BoxExt trait (#2036) 2026-03-30 13:25:53 +08:00
ParkGarden 01dd9a05c3 fix: 重构了 Magisk 模块的 easytier_core.sh, action.sh, uninstall.sh 三个脚本的逻辑,优化参数解析与进程管理,调整措辞 (#1964) 2026-03-30 13:18:42 +08:00
KKRainbow 8c19a2293c fix(windows): avoid pnet interface enumeration panic (#2031) 2026-03-29 23:16:44 +08:00
KKRainbow a1bec48dc9 fix android vpn permission grant (#2023)
* fix android vpn permission grant
* fix url input behaviour
2026-03-29 23:16:32 +08:00
KKRainbow 7e289865b2 fix(faketcp): avoid pnet interface lookup on windows (#2029) 2026-03-29 19:26:29 +08:00
fanyang 742c7edd57 fix: use default connection loss rate for peer stats (#2030) 2026-03-29 19:25:25 +08:00
Luna Yao b71a2889ef suppress clippy warnings when no feature flags are enabled (#2028) 2026-03-29 11:02:23 +08:00
KKRainbow bcd75d6ce3 Add instance recv limiter in peer conn (#2027) 2026-03-29 10:28:02 +08:00
Luna Yao d4c1b0e867 fix: read X-Forwarded-For from HTTP header of WS/WSS (#2019) 2026-03-28 22:20:46 +08:00
KKRainbow b037ea9c3f Relax private mode foreign network secret checks (#2022) 2026-03-28 22:19:23 +08:00
Luna Yao b5f475cd4c filter overlapped proxy cidr (#2024) 2026-03-28 09:40:05 +08:00
Luna Yao eaa4d2c7b8 test: use taiki-e/install-action for cargo-hack (#2020) 2026-03-28 00:07:59 +08:00
Luna Yao e160d9b048 ci: remove aes-gcm from check (#1925) 2026-03-27 22:48:22 +08:00
KKRainbow 0aeea39fbe refactor(gui): collapse public server and standalone into initial peer list (#2017)
The GUI exposed three networking modes: public server, manual, and standalone. In practice EasyTier does not have a server/client role distinction here. Those options only mapped to different peer bootstrap shapes, which made the product model misleading and pushed users toward a non-existent "public server" concept.

This change rewrites the shared configuration UX around initial nodes. Users now add or remove one or more initial node URLs directly, and the UI explains that EasyTier networking works like plugging in a cable: once a node connects to one or more existing nodes, it can join the mesh. Initial nodes may be self-hosted or shared by others.

To preserve compatibility, the frontend keeps the legacy fields and adds normalization helpers in the shared NetworkConfig layer. Old configs are read as initial_node_urls, while saves, runs, validation, config generation, and persisted GUI config sync still denormalize back into the current backend shape: zero initial nodes -> Standalone, one -> PublicServer, many -> Manual. This avoids any proto or backend API change while making old saved configs and imported TOML files load cleanly in the new UI.

Code changes:

- add initial_node_urls plus normalize/denormalize helpers in the shared frontend NetworkConfig model

- remove the mode switch and public-server/manual specific inputs from the shared Config component and replace them with a single initial-node list plus explanatory copy

- update Chinese and English locale strings for the new terminology

- normalize configs received from GUI/web backends and denormalize them before outbound API calls

- normalize GUI save-config events before storing them in localStorage so legacy payloads remain editable under the new model
2026-03-27 11:37:09 +08:00
KKRainbow e000636d83 feat(stats): add by-instance traffic metrics (#2011) 2026-03-26 13:46:33 +08:00
Luna Yao 8e4dc508bb test: improve test_txt_public_stun_server with timeout and retry mechanism (#2014) 2026-03-26 09:32:07 +08:00
Luna Yao e2684a93de refactor: use strum on EncryptionAlgorithm, use Xor as default when AesGcm not available (#1923) 2026-03-25 18:42:34 +08:00
KKRainbow 1d89ddbb16 Add lazy P2P demand tracking and need_p2p override (#2003)
- add lazy_p2p so nodes only start background P2P for peers that actually have recent business traffic
- add need_p2p so specific peers can still request eager background P2P even when other nodes enable lazy mode
- cover the new behavior with focused connector/peer-manager tests plus three-node integration tests that verify relay-to-direct route transition
2026-03-23 09:38:57 +08:00
KKRainbow 2bfdd44759 multi_fix: harden peer/session handling, tighten foreign-network trust, and improve web client metadata (#1999)
* machine-id should be scoped unbder same user-id
* feat: report device os metadata to console
* fix sync root key cause packet loss
* fix tun packet not invalid
* fix faketcp cause lat jitter
* fix some packet not decrypt
* fix peer info patch, improve performance of update self info
* fix foreign credential identity mismatch handling
2026-03-21 21:06:07 +08:00
Luna Yao 77966916c4 cargo: add used features for windows-sys (#1924) 2026-03-17 14:10:50 +08:00
TsXor 26b7455c1e ignores eol difference for auto-generated files (#1997) 2026-03-16 23:40:38 +08:00
KKRainbow 8922e7b991 fix: foreign credential handling and trusted key visibility (#1993)
* fix foreign credential handling
* allow list foreign network trusted keys
* fix(gui): delete removed config-server networks
* fix(web): reset managed instances on first sync
2026-03-16 22:19:31 +08:00
KKRainbow e6ac31fb20 feat(web): add webhook-managed machine access and multi-instance CLI support (#1989)
* feat: add webhook-managed access and multi-instance CLI support
* fix(foreign): verify credential of foreign credential peer
2026-03-15 12:08:50 +08:00
KKRainbow c8f3c5d6aa feat(credential): support custom credential ID generation (#1984)
introduces support for custom credential ID generation, allowing users to specify their own credential IDs instead of relying solely on auto-generated UUIDs.
2026-03-12 00:48:24 +08:00
KKRainbow 330659e449 feat(web): full-power RPC access + typed JSON proxy endpoint (#1983)
- extend web controller bindings to cover full RPC service set
- update rpc_service API wiring and session/controller integration
- generate trait-level json_call_method in rpc codegen
- route restful proxy-rpc requests via scoped typed clients
- add json-call regression tests and required Sync bound fixes~
2026-03-11 20:32:37 +08:00
Maxwell 80043df292 script: introduce EasyTier powershell installer (#1975) 2026-03-11 11:57:03 +08:00
KKRainbow ecd1ea6f8c feat(web): implement secure core-web tunnel with Noise protocol (#1976)
Implement end-to-end encryption for core-web connections using the
Noise protocol framework with the following changes:

Client-side (easytier/src/web_client/):
- Add security.rs module with Noise handshake implementation
- Add upgrade_client_tunnel() for client-side handshake
- Add Noise frame encryption/decryption via TunnelFilter
- Integrate GetFeature RPC for capability negotiation
- Support secure_mode option to enforce encrypted connections
- Handle graceful fallback for backward compatibility

Server-side (easytier-web/):
- Accept Noise handshake in client_manager
- Expose encryption support via GetFeature RPC

The implementation uses Noise_NN_25519_ChaChaPoly_SHA256 pattern for
encryption without authentication. Provides backward compatibility
with automatic fallback to plaintext connections.
2026-03-10 08:48:08 +08:00
KKRainbow 694b8d349d feat(credential): enforce signed credential distribution across mixed admin/shared topology (#1972) 2026-03-10 08:37:33 +08:00
KKRainbow ef44027f57 feat(credential): improve credential peer routing and visibility (#1971)
- improve credential peer filtering and related route lookup behavior
- expose credential peer information through CLI and API definitions
- add and refine tests for credential routing and peer interactions
2026-03-08 14:06:33 +08:00
KKRainbow f3db348b01 fix: resolve slow exit and reduce test timeouts (#1970)
- Explicitly shutdown tokio runtime on launcher cleanup to fix slow exit
- Add timeout to tunnel connector in tests to prevent hanging
- Reduce test wait durations from 5s to 100ms for faster test execution
- Bump num-bigint-dig from 0.8.4 to 0.8.6
2026-03-08 12:27:42 +08:00
KKRainbow c4eacf4591 feat(credential): implement credential peer auth and trust propagation (#1968)
- add credential manager and RPC/CLI for generate/list/revoke
- support credential-based Noise authentication and revocation handling
- propagate trusted credential metadata through OSPF route sync
- classify direct peers by auth level in session maintenance
- normalize sender credential flag for legacy non-secure compatibility
- add unit/integration tests for credential join, relay and revocation
2026-03-07 22:58:15 +08:00
59d4475743 feat: relay peer end-to-end encryption via Noise IK handshake (#1960)
Enable encryption for non-direct nodes requiring relay forwarding.
When secure_mode is enabled, peers perform Noise IK handshake to
establish an encrypted PeerSession. Relay packets are encrypted at
the sender and decrypted at the receiver. Intermediate forwarding
nodes cannot read plaintext data.

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com>
2026-03-07 14:47:22 +08:00
KKRainbowandClaude Opus 4.6 22b4c4be2c fix: guard macos-ne feature with target_os = "macos" in cfg expressions (#1962)
All 13 occurrences of `any(target_os = "ios", feature = "macos-ne")` are
replaced with `any(target_os = "ios", all(target_os = "macos", feature = "macos-ne"))`.

Previously, enabling `macos-ne` on non-macOS platforms (e.g. `--all-features`
on Linux) would incorrectly compile macOS/mobile-specific code paths, causing
build failures or wrong runtime behavior.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 00:06:21 +08:00
Luna Yao 5f31583a84 refactor: 使用 tracing 输出日志 (#1856)
* change all println to tracing
2026-03-04 09:52:23 +08:00
Mg Pig 1d25240d8c refactor(ui): extract URL input components and enhance UI responsiveness (#1819) 2026-03-04 09:49:15 +08:00
eeb507d6ea fix: register PeerCenterRpc in management API server so CLI peer-center works (#1929)
PeerCenterRpc was only registered in the per-instance peer-to-peer RPC
manager (domain = network_name), but not in the management API server
(domain = ""). The CLI connects to the management API with an empty
domain, causing "Invalid service name: PeerCenterRpc" errors.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 09:37:37 +08:00
9e9916efa5 fix(connector): skip self-connection when peer shares local interface IPs (#1941)
When two EasyTier instances run on the same machine and share the same
network, the direct connector would expand a remote peer's 0.0.0.0
listener into local interface IPs and then attempt to connect to
itself, causing an infinite loop of failed connection attempts.

The existing `peer_id != my_peer_id` guard does not cover this case
because the two instances have different peer IDs despite sharing the
same physical network interfaces.

Fix by adding a self-connection check in `spawn_direct_connect_task`:
before spawning a connect task, compare the candidate (scheme, IP,
port) against the local running listeners. If a local listener matches
on all three dimensions — accounting for 0.0.0.0/:: wildcards by
checking membership in the local interface IP sets — the candidate is
silently dropped with a DEBUG log message.

The fix covers all four code paths:
- IPv4 unspecified (0.0.0.0) expansion loop
- IPv4 specific-address branch
- IPv6 unspecified (::) expansion loop
- IPv6 specific-address branch

The TESTING flag logic is untouched so existing unit tests are
unaffected.

* refactor(connector): replace is_self_connect closure with GlobalCtx::should_deny_proxy (#1954)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-03-04 09:36:35 +08:00
hello db6b9e3684 feat: core config server use last path segment as user name (#1931) 2026-03-03 18:24:28 +08:00
Mg Pig ff24332e23 feat(web): add OIDC SSO login support (#1943) 2026-03-03 18:23:31 +08:00
fanyang d4ff0b1767 build(deps): upgrade vite to 5.4.21 in frontend and gui packages (#1950) 2026-03-01 13:47:02 +08:00
Mg Pig 5716f7f16b fix(web): allow configuring listen address for API and web servers (#1919) (#1948) 2026-03-01 01:02:31 +08:00
fanyang e5bd8f9e24 build(deps): upgrade minimatch to 10.2.4 (#1949) 2026-02-28 22:40:47 +08:00
sky96111 b56bcfb4b0 fix: increase websocket peer connection timeout to 20 seconds (#1939)
- Add ws/wss protocols to long timeout list
2026-02-28 18:26:19 +08:00
fanyangandClaude Sonnet 4.6 fb95b4827c build(deps): bump axios from 1.11.0 to 1.13.6 in frontend packages (#1947)
Addresses security vulnerabilities in axios <1.13.5. Updates the
declared specifier to ^1.13.5 in all three frontend package.json
files and regenerates both npm and pnpm lock files (resolved: 1.13.6).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 11:17:18 +08:00
fanyang a8f7226195 fix(foreign_network): set avoid_relay_data when relay_data is false (#1935) 2026-02-25 09:30:24 +08:00
dependabot[bot] e6ee485352 build(deps-dev): bump vite from 5.4.10 to 5.4.21 in /easytier-web/frontend-lib (#1922)
* build(deps-dev): bump vite in /easytier-web/frontend-lib

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.10 to 5.4.21.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.21/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.21
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 22:47:29 +08:00
hello 73291a3a1c feat: Update Cargo.toml to add support for tls1.2 when use wss (#1917) 2026-02-20 18:01:21 +08:00
fanyang f737708f45 fix: avoid panic on malformed short tunnel packets (#1904) 2026-02-18 00:04:30 +08:00
fanyang aa24d09aa2 fix: replace stale magic DNS records on IP change (#1906)
Magic DNS updates are full snapshots, so appending routes keeps old IPs and returns duplicate A records. Replace each client's previous routes on update and add a regression test to ensure hostname resolution keeps only the latest IP.
2026-02-16 13:20:11 +08:00
fanyangandCopilot fe4e77979d fix: avoid panic for quic peer urls using port 0 (#1905)
Prevent crashes when users input quic://...:0 by rejecting port 0 explicitly and propagating connect setup errors. Add a regression test to ensure invalid QUIC targets fail gracefully.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-14 17:10:29 +08:00
Chenx Dust 7a26640c26 feat: support macOS Network Extension (#1902)
* feat: support macOS Network Extension
* fix: disable macOS NE feature in cargo hack check
2026-02-14 14:54:36 +08:00
Mg Pig 5a777959e3 ui: clarify encryption checkbox description in locales (#1841) 2026-02-13 16:04:26 +08:00
Mg Pig 3512a80597 feat(web): add --disable-registration flag to disable user registration (#1881) 2026-02-13 16:03:11 +08:00
Zkitefly 011770a601 Update http_connector.rs (#1900) 2026-02-13 16:02:32 +08:00
Chenx Dust 6475724d2e fix: toggle_window_visibility with focus check (#1888)
* refactor: better logics for toggle_window_visibility
2026-02-11 16:50:36 +08:00
Mg Pig 85e9029577 feat: add Nix CI workflow and update flake.lock dependencies (#1872) 2026-02-10 18:11:35 +08:00
Luna Yao b6e292cce3 ci: use shared key for build workflow (#1868) 2026-02-04 09:48:55 +08:00
KKRainbow c58140fb47 update rust to 1.93 (#1865) 2026-02-04 09:48:43 +08:00
Luna Yao aebb7facfa drop permit reserved by poll_reserve (#1858) 2026-02-03 11:14:11 +08:00
Chenx Dust 1e2124cb99 fix: force set tun fd when received (#1860) 2026-02-03 11:13:31 +08:00
Chenx Dust e1cbd07d1f feat: separate zstd and faketcp into features (#1861)
* feat: separate faketcp into a feature
* fix: no need to initialize out_len
* feat: separate zstd into a feature
* clippy: remove unnecessary cast, because for unix size_t always equals usize
2026-02-03 11:12:33 +08:00
韩嘉乐 7750e81168 CI(ohos): add a condition to check for the publish code (#1863)
Added a condition to check for the presence of a release code when running the publish step
2026-02-03 11:11:45 +08:00
KKRainbow bf3edbd28f remove src modified flag from pm hdr (#1857) 2026-02-02 16:47:26 +08:00
Luna Yao cd2cf56358 refactor: handle quic proxy internally instead of use external udp port (#1743)
* deprecate quic_listen_port, add disable_relay_quic and enable_relay_foreign_network_quic
* add set_src_modified to TcpProxyForWrappedSrcTrait
* prioritize quic over kcp
2026-02-02 11:53:40 +08:00
KKRainbow 21f4a944a7 fix perf degraded because of impact of is_empty() of dashmap (#1854) 2026-02-01 08:51:18 +08:00
KKRainbow 9617005136 make udp->ring transmit reliable (#1851) 2026-01-31 17:23:45 +08:00
deddey c85d1d41b3 allow set TUN dev name on FreeBSD (#1823)
Also rename stale interfaces from previous runs before creating new ones.
Works around rust-tun reusing existing tun0 instead of configured name.

Tested on FreeBSD 14.1
2026-01-30 23:51:52 +08:00
KKRainbow 9e3c9228bb improve perf of remove_network in foreign net mgr (#1847) 2026-01-30 23:04:31 +08:00
Luna Yao acd7c85ff6 ci: speed up test with matrix (#1830)
* add an action to install pnpm packages
* add an action to prepare build environment
* rewrite test workflow, using composite actions and matrix
2026-01-30 22:21:27 +08:00
KKRainbow 8727221513 call remove_peer instead of remove_network when peer id not match (#1844) 2026-01-30 16:01:52 +08:00
Luna YaoandCopilot cdedaf3f63 refactor(quic): remove quinn encryption (#1831)
* use quinn-plaintext
* remove server_cert in QUICTunnelListener
* remove some customized transport config
* leave max_concurrent_bidi_streams as default

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 10:21:59 +08:00
KKRainbow ffe5644ddc add token bucket limiter on peer conn recv (#1842)
We should limit peer conn recv to make sure we don't recv too much from peers.
2026-01-29 16:12:26 +08:00
Chenx Dust ccc684a9ab Fix: Fixed compilation issue after partially removing the feature flag (#1835) 2026-01-28 21:38:34 +08:00
fanyangandCopilot 977e502150 feat(cli): add column truncation controls (#1838)
- drop low-priority columns when tables exceed terminal width
- truncate optional columns to fit remaining width
- add --no-trunc flag to disable truncation
- compute column widths using unicode display width

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 14:50:14 +08:00
Mg Pig 518d26b25f feat: add X-Network-Name header to HTTP connector requests (#1839)
This allows HTTP redirect servers to provide network-specific node
lists based on the client's network identity. Updated unit tests
to verify the header is correctly sent.
2026-01-28 14:48:45 +08:00
KKRainbow 101f416268 Introduce secure mode (part 1) (#1808)
Use noise protocol on handshake. Check peer's public key if needed. Also support rekey and replay attack prevention.

E2EE and temporary password will be implemented based on this.
2026-01-25 20:16:51 +08:00
Chenx Dust ffa08d1c43 feat: add peer_id in MyNodeInfo (#1821) 2026-01-22 22:44:37 +08:00
韩嘉乐 cf3f9169b7 CI(ohos): Enhance CI workflow for release package builds (#1812)
Added support for building and publishing release packages based on tags.
2026-01-20 12:25:10 +08:00
KKRainbow 8343cd5e76 fix config loss when run network (#1802) 2026-01-17 00:58:42 +08:00
KKRainbow 005b321f62 allow open rpc port in gui normal mode (#1795)
* allow open rpc port for gui normal mode
* downgrade dev tool console
2026-01-16 11:12:32 +08:00
KKRainbow 53264f67bf fix peer establish direct conn with subnet proxy to one of local interface (#1782)
* fix peer establish direct conn with subnet proxy to one of local interface

* fix peer mgr ref loop
2026-01-15 01:00:32 +08:00
韩嘉乐 f8b34e3c86 Merge pull request #1787 from EasyTier/FrankHan052176-patch-1
action[ohos] fix the cnt of commit in ohos.yml
2026-01-13 23:58:26 +08:00
韩嘉乐 ce1bdac2bc action[ohos] fix the cnt of commit in ohos.yml 2026-01-13 22:57:43 +08:00
Copilot bd8f01fb26 Add Nushell completion script generation support (#1756)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-11 18:41:02 +08:00
Chenx DustandPage Chen b590700540 feat: support unix socket tunnel (for ios) (#1779)
Co-authored-by: Page Chen <pagechen04@gmail.com>
2026-01-11 16:37:32 +08:00
Chenx Dust 48c5c23f9b feat: support compile for iOS (#1777) 2026-01-11 16:36:58 +08:00
朝倉水希 f4f591d14c fix: outbound packet not dropped by acl (#1766) 2026-01-08 19:58:23 +08:00
Mg Pig 0c16e2211b feat(gui): persist and restore last used network instance ID (#1762) 2026-01-08 17:03:51 +08:00
4bfea06a12 docs: update locales (#1755)
Co-authored-by: KKRainbow <443152178@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 11:08:32 +08:00
桜井 ホタル 057ee9f2c5 Resolves the issue of DNS resolution failure after installing KSU modules, resulting in inability to connect to nodes. (#1761) 2026-01-08 11:07:52 +08:00
Burning_TNTandCopilot 7f48ca54a3 Implement requesting tun_fd with tokio channel. (#1734)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-04 21:04:43 +08:00
hello ee5227130c feat: Update Cargo.toml for easytier-gui and android app to support tls1.2 (#1744) 2026-01-04 21:03:34 +08:00
韩嘉乐 2e0d9a2b54 Refactor EasyTier version resolution in workflow (#1747)
Updated the workflow to resolve the EasyTier version based on the latest commit and tag information.
2026-01-04 21:02:55 +08:00
编程小白 c5d732773f Convert dead URL to ASCII before socket address lookup (#1739) 2026-01-02 18:49:23 +08:00
狂男风 88a45d1156 use 80/443 as ws/wss default port (#1700) 2026-01-01 01:31:38 +08:00
KKRainbow 4e651a72f7 allow loopback src address in listener (#1730) 2026-01-01 00:41:56 +08:00
Mg Pig 7c563153ae fix: ensure proxy routes update correctly on NIC (#1729) 2025-12-31 22:36:45 +08:00
KKRainbow cb81c0df85 respond packet should not be dropped if request packet is already allowed (#1725) 2025-12-31 08:14:39 +08:00
21paradoxandsijie.sun 9c316ea01c fix socks5 and tcp forward mem leak (#1721)
Co-authored-by: sijie.sun <sijie.sun@smartx.com>
2025-12-31 00:01:44 +08:00
XuDaojie 541fc664e3 update[gui]:将macOS端的应用图标修改为Big Sur风格圆角图标 (#1723) 2025-12-30 22:22:39 +08:00
Mg Pig 18478b7c4b fix(android): update vpn routes when proxy cidrs change (#1717) 2025-12-30 19:26:42 +08:00
韩嘉乐 650323faef [Ohos] 仅在push时执行发布操作,避免流水线运行错误 (#1718) 2025-12-29 13:57:02 +08:00
狂男风 ed131272d4 fix(gui): open_log_dir not working (#1714)
* fix(mobile): open_log_dir not working on android
2025-12-28 23:20:29 +08:00
KKRainbow 39b056c87a bump version to v2.5.0 (#1715) 2025-12-28 23:19:30 +08:00
KKRainbow c19cd1bff3 add tcp hole punching (#1713)
add tcp hole punching and tcp stun test
2025-12-28 21:35:30 +08:00
狂男风 37531507db fix(mobile): logs unreachable on android (#1710) 2025-12-27 20:04:18 +08:00
KKRainbow ca9b4c58b1 fix windivert cause stack overflow (#1711) 2025-12-27 19:31:42 +08:00
KKRainbow 4341bcba5d improve faketcp, handle tcp GSO correctly (#1708)
Current implementation falsely drop GSO-merged tcp packet, and cause unexpected packet loss.
2025-12-26 23:46:17 +08:00
韩嘉乐 0be4ac1fa5 [Ohos] 使用Commit计数器替代Commit Hash作为版本尾缀 (#1703) 2025-12-25 20:42:43 +08:00
KKRainbow 28cd6da502 Add fake tcp tunnel (experimental) (#1673)
support faketcp to avoid tcp-over-tcp problem.
linux/macos/windows are supported.

better to be used in internet env, the maximum 
performance is majorly limited by windivert/raw socket.
2025-12-25 00:10:32 +08:00
狂男风 0712ef762d Fix logic error in relay network whitelist resolving (#1692) 2025-12-23 08:25:45 +08:00
韩嘉乐 eee7d7a1ed 增加Ohos流水线发布步骤执行条件 (#1695) 2025-12-22 21:40:28 +08:00
Burning_TNT 4c58def0db Make release.yml available in forks (#1689) 2025-12-21 21:13:53 +08:00
Momo c6a32e4467 fix: magic dns tld_dns_zone were not working properly (#1686)
* fix: magic dns tld_dns_zone failed to get updated
2025-12-21 21:13:39 +08:00
韩嘉乐 30f0ff16ca Merge pull request #1678 from EasyTier/ohpm
[鸿蒙] 在流水线中增加上传中心仓与上传华为云私仓流程,增加华为云流水线Webhook
2025-12-16 12:06:19 +08:00
FrankHan 38d117ee44 [鸿蒙] 在流水线中增加上传中心仓与上传华为云私仓流程,增加华为云流水线Webhook 2025-12-16 11:50:05 +08:00
KKRainbow 7aba65ea32 enhance port forward (#1662) 2025-12-09 22:16:16 +08:00
Tunglies fe4dff5df0 perf: simplify method signatures and reduce clone across multiple files (#1663) 2025-12-09 16:47:57 +08:00
KKRainbow 2bc51daa98 fix whitelist cause packets of other protocal dropped (#1660) 2025-12-08 21:56:27 +08:00
KKRainbowandCopilot 838b6101b9 Make ospf route more effiencient (#1512)
Avoid iterate all peer info and conn list when building sync request.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 13:14:47 +08:00
韩嘉乐 056c9da781 [EasyTier-ohrs] Use NetworkConfig instead of TomlConfig, and add CompressionAlgorithm and EncryptionAlgorithm to NetworkConfig. (#1654) 2025-12-06 23:23:22 +08:00
datasoneandsijie.sun 2a656d6a0c fix(core): Fix sleep-wake reconnect by resetting alive_conn_urls (#1593)
Co-authored-by: sijie.sun <sijie.sun@smartx.com>
2025-12-05 14:31:08 +08:00
KKRainbow 43a650f9ab set FORCE_USE_CONN_LIST default to false (#1652)
this is falsely set to true and will casue compatibility issue
2025-12-05 00:26:04 +08:00
C.C. 88a55859ac fix(web): remove trailing slash from api base url (#1621) 2025-12-04 23:06:28 +08:00
dawn-lc d686c8721f feat(install): enhance installation script functionality (#1641)
* feat(install): enhance installation script functionality
* fix temp file extname
2025-12-04 23:06:06 +08:00
Mg Pig 0a718163fd feat(gui): GUI add support to connect to config server (#1596) 2025-12-04 23:05:36 +08:00
Mg Pig 53f279f5ff feat(core): Support environment variable parsing in config files (#1640) 2025-12-02 17:54:31 +08:00
Mg Pig ae6d929f4a fix(mobile): Add DHCP polling to fix Android VPN startup failure (#1628) 2025-12-01 01:13:05 +08:00
starrain bb82b3a5b0 fix(elevate): fix panic on NixOS (#1634) 2025-12-01 01:12:08 +08:00
Mg Pig 70b122fb91 feat(gui): macOS UX Improvements (#1631) 2025-12-01 01:11:36 +08:00
狂男风 67cba2c326 feat(mobile): Enhance the Magic DNS support via VpnService on Android (#1617)
* Add DNS route if accept_dns is enabled
* Update doStartVpn to accept optional DNS parameter
2025-11-27 16:53:40 +08:00
sky96111 b86692d009 fix(android): use network-assigned DNS when no DNS is provided (#1612) 2025-11-26 18:24:05 +08:00
狂男风 28e645a277 Add IPv6 address to VPN service (#1615) 2025-11-26 17:15:19 +08:00
Mg Pig 1f2517c731 feat(gui): add service and remote mode support (#1578)
This PR fundamentally restructures the EasyTier GUI, introducing support for service mode and remote mode, transforming it from a simple desktop application into a powerful network management terminal. This change allows users to persistently run the EasyTier core as a background service or remotely manage multiple EasyTier instances, greatly improving deployment flexibility and manageability.
2025-11-25 13:59:27 +08:00
Sijie.Sun b44053f496 support p2p-only mode (#1598) 2025-11-20 08:20:27 +08:00
Sijie.Sun 5b9ac65477 update readme (#1594) 2025-11-17 16:00:32 +08:00
Mg Pig d726d46a00 fix: Preserve disable_sym_hole_punching setting on edit (#1589) 2025-11-15 18:57:59 +08:00
Mg Pig 1273426009 feat: Enable core to use local config files while being managed via the web (#1540) 2025-11-08 20:32:00 +08:00
Sijie.Sun b50744690e easytier-web and uptime use mimalloc as allocator (#1559) 2025-11-08 11:07:33 +08:00
TungliesandCopilot 55b93454dc fix: clippy errors with stable toolchain and default features (#1553)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-07 20:08:39 +08:00
Mg Pig 89cc75f674 refactor: replace ConfigSource with bool parameter (#1516) 2025-11-04 13:48:10 +08:00
Mg Pig 6bb2fd9a15 feat(core): Refactor IDN and URL handling logic (#1533)
* feat(core): Refactor IDN and URL handling logic

* feat(tests): add dual_convert option for URL serialization in IDN tests
2025-11-03 22:15:40 +08:00
Mg Pig 8ab98bba8f feat(ui): make port forward config responsive (#1530) 2025-10-31 23:23:36 +08:00
韩嘉乐 26d002bc2b The flowback solution of HarmonyOS 5 failed due to the anti-loop mechanism. (#1514) 2025-10-25 00:17:24 +08:00
Sijie.Sun 71679e889a allow sync conn with conn list when conn bitmap is too large (#1508) 2025-10-23 08:11:36 +08:00
Sijie.Sun 7485f5f64e make sure event is triggered when peer conn remove (#1507) 2025-10-22 23:37:19 +08:00
Mg Pig bbe8f9f810 feat(ui): Display network names and optimize list loading (#1503) 2025-10-22 13:40:36 +08:00
Mg Pig eba9504fc2 refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component (#1489)
* refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component
* feat(gui): Add network config saving and refactor RemoteManagement
2025-10-20 22:07:01 +08:00
kuaifan 67ac9b00ff feat(gui): Optimize the data table column header style to prevent line breaks (#1497) 2025-10-19 16:50:17 +08:00
Sijie.Sun 3ffa6214ca fix subnet proxy deadloop (#1492)
* use LPM to determine subnet proxy dst.
* never allow subnet proxy traffic sending to self.
2025-10-19 15:46:51 +08:00
Mg Pig 6f278ab167 chore: update flake configuration (#1490) 2025-10-19 00:25:40 +08:00
Sijie.Sun f10b45a67c [easytier-uptime] support tag in node list (#1487) 2025-10-18 23:19:53 +08:00
Sijie.Sun cc8f35787e release dashmap memory (#1485) 2025-10-18 12:48:04 +08:00
Sijie.Sun 8f1786fa23 replace tachyonix with tokio mpsc in MpscTunnel (#1483)
tachyonix cannot correctly wakeup senders when the receiver is closed
and causing tasks deadlock and memory leak.
2025-10-17 00:09:13 +08:00
编程小白 70dddeace3 Fix support for Chinese domain names (#1462) 2025-10-15 21:00:05 +08:00
Mg Pig 8cc9da9d6d fix(web): fix generate and parse config methods broken in #1465 (#1476) 2025-10-14 15:13:20 +08:00
Luna Yao 5292b87275 Add quic-listen-port flag for customization of the port used by QUIC proxy (#1473) 2025-10-14 09:43:50 +08:00
Mg Pig 87b7b7ed7c refactor(web): Refactor web logic to extract reusable remote client management module (#1465) 2025-10-13 23:59:46 +08:00
imdingtalk 999a486928 Improve update in installation script, decrease downtime(#1422) 2025-10-13 23:52:37 +08:00
TaurusXin 627e989faa feat: show NAT type of all nodes in GUI (#1464) 2025-10-13 11:40:57 +08:00
Mg Pig af95312949 fix(acl): acl group cache add self group info (#1445) 2025-10-07 23:56:26 +08:00
Mg Pig a452c34390 fix(ohrs): update collect_network_infos to use synchronous method (#1444) 2025-10-04 23:12:38 +08:00
Mg Pig 4d5330fa0a refactor: get_running_info fn replace status polling with direct calls (#1441) 2025-10-04 21:43:34 +08:00
agusti moll 5e48626cb9 add tld-dns-zone for customizing top-level domain (TLD) zone (#1436) 2025-10-04 00:18:10 +08:00
阿瓦 ad7dc3a129 use plist as macos service management config generator (#1439) 2025-10-04 00:14:45 +08:00
92fab5aafa feat(ohos) build har package (#1440)
Co-authored-by: niuhuan <20847533+niuhuan@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-02 22:29:49 +08:00
Mg Pig 841d525913 refactor(rpc): Centralize RPC service and unify API (#1427)
This change introduces a major refactoring of the RPC service layer to improve modularity, unify the API, and simplify the overall architecture.

Key changes:
- Replaced per-network-instance RPC services with a single global RPC server, reducing resource usage and simplifying management.
- All clients (CLI, Web UI, etc.) now interact with EasyTier core through a unified RPC entrypoint, enabling consistent authentication and control.
- RPC implementation logic has been moved to `easytier/src/rpc_service/` and organized by functionality (e.g., `instance_manage.rs`, `peer_manage.rs`, `config.rs`) for better maintainability.
- Standardized Protobuf API definitions under `easytier/src/proto/` with an `api_` prefix (e.g., `cli.proto` → `api_instance.proto`) to provide a consistent interface.
- CLI commands now require explicit `--instance-id` or `--instance-name` when multiple network instances are running; the parameter is optional when only one instance exists.

BREAKING CHANGE:  
RPC portal configuration (`rpc_portal` and `rpc_portal_whitelist`) has been removed from per-instance configs and the Web UI. The RPC listen address must now be specified globally via the `--rpc-portal` command-line flag or the `ET_RPC_PORTAL` environment variable, as there is only one RPC service for the entire application.
2025-10-02 20:30:39 +08:00
Luna YaoandSijie.Sun d2efbbef04 refactor: change magicdns to internal redirect (#1428)
To resolve issue #1419, DNS request packets are read directly and responses are sent back internally instead of being forwarded to the listening port.

The DNS service on fake_ip (100.100.100.101) no longer supports DNS-over-TCP.

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-10-02 20:19:12 +08:00
Sijie.Sun 971ef82679 fix data not encrypted when no tun is enabled (#1435) 2025-10-01 11:16:24 +08:00
Mg Pig 020bf04ec4 refactor(config): unify runtime configuration management via ConfigRpc (#1397)
* refactor(config): unify runtime configuration management via ConfigRpc
* feat(tests): add config patch test and fix problem
2025-10-01 00:32:28 +08:00
韩嘉乐 4d91582fd8 Update ohos-rs (#1434) 2025-09-30 23:51:58 +08:00
Sijie.Sun e9b4dbce6e use cargo ndk in jni build script (#1424) 2025-09-28 23:18:51 +08:00
R0S 00fd02c739 正确的hostname 2025-09-26 21:17:14 +08:00
sijie.sun c0d2045e52 bump version to v2.4.5 2025-09-26 00:48:10 +08:00
ThermalEng 835cd407bf Update hotspot_iprule.sh, Support subnet forward for usb shared network (#1411) 2025-09-25 16:25:53 +08:00
Sijie.Sun f5ba5bb146 show traffic stats chart in web/gui (#1410) 2025-09-25 13:43:11 +08:00
Sijie.Sun 7a694257d9 add test for ipv6 wireguard vpn portal (#1408) 2025-09-25 08:24:56 +08:00
Sijie.Sun 67abf4446d fix socks5 panic (#1409) 2025-09-25 08:24:50 +08:00
Sijie.Sun 7035a3fef4 fix firewall rule not specify interface (#1407) 2025-09-25 00:11:26 +08:00
Sijie.Sun 4445916ba7 fix open log dir not work on gui (#1403) 2025-09-21 23:17:31 +08:00
Sijie.Sun a102a8bfc7 fix macos bind failed when addr is v6 (#1398) 2025-09-21 21:47:03 +08:00
Sijie.Sun c9e8c35e77 fix log dir not work; fix stun config from file not work; (#1393) 2025-09-20 00:20:08 +08:00
Sijie.Sun 1a1be8138a bump version to v2.4.4 (#1386) 2025-09-18 19:49:10 +08:00
Sijie.Sun e06e8a9e8a allow enable log with cli, limit log size (#1384)
* impl logger rpc
* use size based appender
* add log args
2025-09-18 16:35:12 +08:00
Sijie.SunandCopilot 56fd6e4ab6 fix wireguard listener (#1382)
* listen both v4 and v6 for wireguard portal
* fix panic when getting udp local addr

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-17 23:45:05 +08:00
Sijie.Sun 215db09925 avoid packets sending to non-exist peer causing route loop (#1378) 2025-09-17 09:52:53 +08:00
fanyang 9fff5e4fec Add config validation flag (#1376)
Add `--check-config` CLI option to validate configuration without
starting network
2025-09-16 22:58:07 +08:00
Sijie.Sun 802d3f78d7 distinct v6 and v4 tunnel in gui and cli (#1373) 2025-09-16 07:24:31 +08:00
Sijie.Sun 3593035eb9 fix networksetup timeout on some machine (#1372) 2025-09-15 23:33:43 +08:00
Sijie.Sun 757d76c9da fix stun server list empty when config is from web (#1371) 2025-09-15 22:52:58 +08:00
fanyang 445e68ddd1 Read config from stdin (#1354) 2025-09-13 21:21:30 +08:00
Sijie.Sun b540ec3f46 improve uptime (#1365) 2025-09-13 19:14:13 +08:00
Sijie.Sun 5c90431876 fix smoltcp attempt to subtract sequence numbers with underflow (#1360) 2025-09-13 15:03:04 +08:00
Sijie.Sun 793889c3b7 fix ospf ipv4 map error when ipv4 conflicted and changed (#1359) 2025-09-13 08:48:50 +08:00
Sijie.Sun eb42086f9c set correct route policy for udp/icmp (#1361) 2025-09-13 08:48:37 +08:00
Sijie.Sun d0efc40efb fix foreign network direct conn with mapped listeners (#1363) 2025-09-13 08:48:12 +08:00
fanyang ae704d1d5f Fix jemalloc warning on macOS (#1344)
fix:
```
-> % easytier-core
<jemalloc>: option background_thread currently supports pthread only
```

Reference: https://github.com/apache/arrow/pull/5729
2025-09-08 21:53:40 +08:00
fanyang 525dfd9fc1 cli: improve peer table display with shorter columns for small display (#1342)
- Add short column names for latency, loss rate, rx/tx bytes, tunnel protocol and NAT type
- Format loss rate as percentage with one decimal place
- Change table style from modern to markdown for better readability
2025-09-08 21:52:53 +08:00
Sijie.Sun 18bd178bbd update readme to fix script installation command (#1341) 2025-09-06 17:02:31 +08:00
fanyang 088155f6f3 core: hide default STUN servers from cli (#1334) 2025-09-06 15:53:34 +08:00
Sijie.Sun b750faa66f add android jni (#1340) 2025-09-06 13:49:42 +08:00
Sijie.Sun ef3309814d fix cli add port forward failed if initial forward list is empty (#1324) 2025-09-02 22:03:57 +08:00
fanyang b87a05b457 refactor: update custom STUN server settings (#1310)
* refactor: update global context STUN server initialization

Modified global context initialization to use a single StunInfoCollector
instance with properly configured IPv4 and IPv6 servers instead of
creating separate instances.

feat: add IPv6 STUN server configuration support

Added interface methods and config struct fields to support both IPv4
and IPv6 STUN server configuration. Modified getter and setter methods
to handle Option<Vec<String>> type for both server types.

feat: enhance StunInfoCollector with IPv6 support

Updated StunInfoCollector to support both IPv4 and IPv6 STUN servers.
Added new constructor that accepts both server types and methods to set
them independently.

feat: add CLI argument for IPv6 STUN servers

Added command line argument support for configuring IPv6 STUN servers.
Updated configuration setup to handle both IPv4 and IPv6 STUN server
settings.

docs: add localization for STUN server configuration

Added English and Chinese localization strings for the new STUN server
configuration options, including both IPv4 and IPv6 variants.
2025-09-02 21:46:37 +08:00
Joel Stodolski 754439f03c feat(gui): add macOS dock icon visibility control (#1328) 2025-09-02 17:32:18 +08:00
Sijie.Sun 2145ef40b9 fix ospf route panic (#1304) 2025-08-27 13:22:29 +08:00
Sijie.Sun a3806e0190 fix set ipv6 mtu may cause tun init error (#1300) 2025-08-27 09:57:32 +08:00
Sijie.Sun 0ceb58586b fix keepalive on accepted tcp proxy connection (#1302) 2025-08-26 23:30:30 +08:00
Sijie.Sun 719a1fe7cf bump version to 2.4.3 (#1296) 2025-08-26 12:22:08 +08:00
Sijie.Sun 671b8d5a0c fix quic transport (#1293) 2025-08-26 08:37:31 +08:00
fanyang e29206aef9 tray: place the exit menu item at bottom (#1291) 2025-08-25 12:47:43 +08:00
Sijie.Sun 3299a77da3 make magic dns domain check robust (#1288) 2025-08-24 18:24:42 +08:00
Sijie.Sun 0804fd6632 retry create tun device if it closed (#1279) 2025-08-24 15:25:09 +08:00
Sijie.Sun ea76114d50 fix kcp not work as expect (#1285) 2025-08-24 14:33:11 +08:00
Mg Pig 9304d3b227 feat(nix): refactor Flake and Migrate Android Support (#1280) 2025-08-24 00:53:42 +08:00
fanyang 78004de5e5 gui: sort peer list (#1278) 2025-08-24 00:53:32 +08:00
Sijie.Sun 5b7384fddd disable nat4 hole punch (#1277) 2025-08-22 23:33:21 +08:00
08a92a53c3 feat(acl): add group-based ACL rules and related structures (#1265)
* feat(acl): add group-based ACL rules and related structures

* refactor(acl): optimize group handling with Arc and improve cache management

* refactor(acl): clippy

* feat(tests): add performance tests for generate_with_proof and verify methods

* feat: update group_trust_map to use HashMap for more secure group proofs

* refactor: refactor the logic of the trusted group getting and setting

* feat(acl): support kcp/quic use group acl

* feat(proxy): optimize group retrieval by IP in Kcp and Quic proxy handlers

* feat(tests): add group-based ACL tree node test

* always allow quic proxy traffic

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
Co-authored-by: sijie.sun <sijie.sun@smartx.com>
2025-08-22 22:25:00 +08:00
fanyang 34560af141 cli: put the local IP at the front (#1256) 2025-08-22 20:40:28 +08:00
Sijie.Sun 2e7e0088dd Revise QQ group details in README_CN.md (#1274) 2025-08-22 12:52:01 +08:00
Sijie.Sun d23366ea84 Update QQ Group contact information in README (#1275) 2025-08-22 12:51:50 +08:00
fanyang df7eb47593 Support tokio-console (#1259) 2025-08-21 11:41:42 +08:00
Sijie.Sun 839a28a3d5 avoid panic on smoltcp socket accept (#1272) 2025-08-21 09:30:51 +08:00
Sijie.Sun 9c6d1dabdf fix dead lock in tokio smoltcp (#1270) 2025-08-21 00:16:11 +08:00
Sijie.Sun e6ec7f405c introduce uptime monitor for easytier public nodes (#1250) 2025-08-20 22:59:44 +08:00
ThermalEng 8f37d4ef7c 增加magisk模块功能:热点局域网转发。 (#1252)
* 增加magisk模块功能:热点局域网转发。该功能由后台监控,热点打开,可自动增加转发规则。在三星fold3测试通过。

* 增加了默认tun名称的识别

1.防止配置文件没有配置dev_name的情况,按默认名称tun+在网络设备中查找;
2.一旦热点关闭,自动删除规则。

* 考虑到主程序已可通过模块开关来重新加载,将操作按钮用于转发开关。此外对状态栏信息进行了一些修饰,加入了转发状态的显示。
2025-08-20 20:49:25 +08:00
TigerBeanst c37af8c1be feat(easytier-magisk): add support for custom command args. (#1236) 2025-08-19 00:04:54 +08:00
Glavo 489661a2ce Fix #1255: Using mimalloc for Linux LoongArch64 (#1257) 2025-08-19 00:04:11 +08:00
Sijie.Sun fa3e208668 fix panic of std::Instant overflow (#1243) 2025-08-15 22:54:58 +08:00
21paradox 4d240efde9 add a android flake.nix for local development/test/build (#1237) 2025-08-15 16:59:11 +08:00
Sijie.SunandCopilot d9bcbd9b31 fix proxy traffic not count into traffic (#1229)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-14 00:05:12 +08:00
fanyangandCopilot 35ff9b82fc Support custom STUN servers configuration (#1212)
* Support custom STUN servers

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-13 10:35:59 +08:00
Sijie.Sun a511abb613 fix docker file (#1219) 2025-08-11 18:09:16 +08:00
Sijie.Sun 1eec27b5ff bump version to 2.4.2 (#1218) 2025-08-11 09:03:13 +08:00
Sijie.Sun 1de7777a71 fix quic transport panic (#1216) 2025-08-11 08:30:59 +08:00
Sijie.Sun 975ca8bd9c Update docker workflow (#1217)
1. push all supported platform
2. support unstable tag
2025-08-10 23:36:50 +08:00
Sijie.Sun e43537939a clippy all codes (#1214)
1. clippy code
2. add fmt and clippy check in ci
2025-08-10 22:56:41 +08:00
CyiceKandSijie.Sun 0087ac3ffc feat(encrypt): Add XOR and ChaCha20 encryption with low-end device optimization and openssl support. (#1186)
Add ChaCha20 XOR algorithm, extend AES-GCM-256 capabilities, and integrate OpenSSL support.

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-08-09 18:53:55 +08:00
21paradoxandCopilot 7de4b33dd1 add FOREGROUND_SERVICE for no_tun mode, not using vpn service (#1203)
1. add FOREGROUND_SERVICE related code, connection not to be **blocked by android system** when apps running in background
2. no_tun mode not enabling vpnservice, makeing other app to use vpnservice

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-09 18:34:45 +08:00
Sijie.Sun 8ffc2f12e4 optimize the condition of enabling kcp (#1210) 2025-08-09 16:16:09 +08:00
FuturePrayer 37b24164b6 add portforward config to gui (#1198)
* Added port forwarding to the GUI interface
* Separated port forwarding into a separate drop-down menu
2025-08-09 09:50:09 +08:00
Sijie.Sun 8cdb27d43d add stats metrics (#1207)
support new cli command `easytier-cli stats`

It's useful to find out which components are consuming bandwidth.
2025-08-09 00:06:35 +08:00
Sijie.Sun efa17a7c10 fix dead loop in direct connecto if disable-p2p is enabled in dst (#1206) 2025-08-08 22:30:39 +08:00
Sijie.Sun 6d14e9e441 fix jemalloc prof feature (#1201) 2025-08-08 17:54:39 +08:00
fanyangandCopilot e3e406dcde cli: sort peers by IPv4 and hostname (#1191)
* cli: sort entries by IPv4 and hostname

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-04 21:18:49 +08:00
sijie.sun d0a6c93c2c fix ipv6 packet routing and avoid route looping
properly handle ipv6 link local address and exit node.
2025-08-03 18:10:27 +08:00
sijie.sun 84bfac144c bump version to 2.4.1 2025-08-02 10:48:17 +08:00
Sijie.Sun 9eddb4b072 fix readme assets (#1182) 2025-08-01 23:58:01 +08:00
Sijie.Sun 4fca0f40fe update readme (#1181) 2025-08-01 23:52:27 +08:00
Sijie.Sun 43b9e6e6e9 fix macos elevate (#1177) 2025-08-01 09:36:10 +08:00
Sijie.Sun 583c768f40 fix exit code when error occcurs (#1173) 2025-07-30 23:05:22 +08:00
Tunglies b1b2421561 fix: compiling with socket2::Type::RAW not found on macOS #1168 (#1169) 2025-07-30 00:33:38 +08:00
Sijie.Sun 3d610c0f0f Some Improvements (#1172)
1. do not exit when dns query failed on et startup.
2. do not send secret digest to client when secret mismatch.
2025-07-29 23:05:38 +08:00
Sijie.Sun 2ec88da823 cli for port forward and tcp whitelist (#1165) 2025-07-29 09:30:47 +08:00
Mg Pig 5514de1187 chore: update flake configuration (#1163) 2025-07-29 00:26:05 +08:00
Glavo e70eed74e2 Add support for Linux RISC-V 64 (#1159) 2025-07-27 22:07:07 +08:00
Sijie.Sun 7dc5988620 avoid udp hole punch go through tun (#1155) 2025-07-26 14:39:03 +08:00
Sijie.Sun 354a4e1d7b fix acl not work with kcp&quic (#1152) 2025-07-26 14:38:10 +08:00
Sijie.Sun 5409c5bbe7 port range should not be converted to single port (#1154) 2025-07-26 14:13:13 +08:00
Sijie.Sun 33ff9554cd need encrypt rpc if dst is in peer map (#1151) 2025-07-25 22:28:47 +08:00
Sijie.Sun 975b4e7664 support loongarch (#1146) 2025-07-25 01:53:49 +08:00
Sijie.Sun 1f6a715939 releases/v2.4.0 (#1145)
* bump version to v2.4.0
* update tauri.
* allow try direct connect to public server
2025-07-25 00:16:15 +08:00
Sijie.Sun 8e7a8de5e5 Implement ACL (#1140)
1. get acl stats
```
./easytier-cli acl stats
AclStats:
  Global:
    CacheHits: 4
    CacheMaxSize: 10000
    CacheSize: 5
    DefaultAllows: 3
    InboundPacketsAllowed: 2
    InboundPacketsTotal: 2
    OutboundPacketsAllowed: 7
    OutboundPacketsTotal: 7
    PacketsAllowed: 9
    PacketsTotal: 9
    RuleMatches: 2
  ConnTrack:
    [src: 10.14.11.1:57444, dst: 10.14.11.2:1000, proto: Tcp, state: New, pkts: 1, bytes: 60, created: 2025-07-24 10:13:39 +08:00, last_seen: 2025-07-24 10:13:39 +08:00]
  Rules:
    [name: 'tcp_whitelist', prio: 1000, action: Allow, enabled: true, proto: Tcp, ports: ["1000"], src_ports: [], src_ips: [], dst_ips: [], stateful: true, rate: 0, burst: 0] [pkts: 2, bytes: 120]

  ```
2. use tcp/udp whitelist to block unexpected traffic.
   `sudo ./easytier-core -d --tcp-whitelist 1000`

3. use complete acl ability with config file:

```
[[acl.acl_v1.chains]]
name = "inbound_whitelist"
chain_type = 1
description = "Auto-generated inbound whitelist from CLI"
enabled = true
default_action = 2

[[acl.acl_v1.chains.rules]]
name = "tcp_whitelist"
description = "Auto-generated TCP whitelist rule"
priority = 1000
enabled = true
protocol = 1
ports = ["1000"]
source_ips = []
destination_ips = []
source_ports = []
action = 1
rate_limit = 0
burst_limit = 0
stateful = true

```
2025-07-24 22:13:45 +08:00
Sijie.Sun 4f53fccd25 fix bugs (#1138)
1. avoid dns query hangs the thread
2. avoid deadloop when stun query failed because of no ipv4 addr.
3. make quic input error non-fatal.
4. remove ring tunnel from connection map to avoid mem leak.
5. limit listener retry count.
2025-07-21 23:18:38 +08:00
Sijie.Sun 876d550f68 reduce memory usage (#1133)
Large memory usage comes from:

Mimalloc hold large thread cache, causing abort 13M+ usage.
QUIC endpoint occupy 3M when GRO is enabled.
Smoltcp 64 tcp listener use 2MB.
2025-07-20 19:15:28 +08:00
Sijie.Sun 2660ed5fda try create tun device if not exist (#1131) 2025-07-19 22:56:19 +08:00
Sijie.Sun 50c6f5ae6c add windows firewall for tun interface (#1130)
allow all icmp/tcp/udp on tun interface.
2025-07-19 20:38:44 +08:00
Sijie.Sun 85f0091056 fix latency first route of public server (#1129) 2025-07-19 18:16:53 +08:00
Sijie.Sun e25cd9be37 add disable ipv6 option to gui/web (#1127) 2025-07-19 11:07:57 +08:00
Sijie.Sun 1fb5ca9475 update issue template (#1126) 2025-07-18 23:50:02 +08:00
Sijie.Sun 7f3a9c021c close peer conn if remote addr is from virtual network (#1123) 2025-07-18 03:29:48 +08:00
liusen373andSijie.Sun 0427b48d75 Allows to modify Easytier's mapped listener at runtime via RPC (#1107)
* Add proto definition
* Implement and register the corresponding rpc service
* Parse command line parameters and call remote rpc service

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-07-17 20:37:05 +08:00
Jiangqiu ShenandSijie.Sun 0b729b99e7 add options to generate completions (#1103)
* add options to generate completions

use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-07-17 20:35:49 +08:00
Sijie.Sun 940238f158 socks5 and port forwarding (#1118) 2025-07-17 10:09:25 +08:00
Sijie.Sun 3f6c7ba1d2 update readme (#1102) 2025-07-10 00:34:34 +08:00
lazebird 0025973453 fix: cannot start gui on linux (#1090) 2025-07-07 22:59:11 +08:00
Rene Leonhardt c3a217c9d2 chore(ci): update GitHub Actions (#1088)
* chore(ci): update GitHub Actions
* update gradle-wrapper and revert UPX
* exclude cargo from dependabot and remove empty .gitmodules
2025-07-07 22:55:30 +08:00
Sijie.Sun 13c2e72871 fix incorrect config check (#1086) 2025-07-06 14:20:49 +08:00
Sijie.Sun 3c65594030 smoltcp use larger tx/rx buf size (#1085)
* smoltcp use larger tx/rx buf size
* fix direct conn check
2025-07-06 10:53:01 +08:00
Sijie.Sun f85b031402 handle close peer conn correctly (#1082) 2025-07-06 09:16:13 +08:00
Sijie.Sun ac3e994682 contributing.md (#1084) 2025-07-06 00:08:21 +08:00
Sijie.Sun 139f6b3c4c exclude ohos from workspace (#1080) 2025-07-05 18:44:37 +08:00
Sijie.Sun a4bb555fac use winapi to config ip and route (remove dep on netsh) (#1079)
On some windows machines can not execut netsh.
Also this avoid black cmd window when using gui.
2025-07-05 16:50:09 +08:00
DavHauandsijie.sun d0cfc49806 Add support for IPv6 within VPN (#1061)
* add flake.nix with nix based dev shell
* add support for IPv6
* update thunk

---------

Co-authored-by: sijie.sun <sijie.sun@smartx.com>
2025-07-04 23:43:30 +08:00
韩嘉乐andFrankHan 01e491ec07 support ohos (#974)
* support ohos

---------

Co-authored-by: FrankHan <2777926911@qq.com>
2025-07-02 09:44:45 +08:00
Sijie.Sun bf021a9ead update gui placeholder text (#1062) 2025-06-27 08:29:44 +08:00
Sijie.Sun 70e69a382e allow set multithread count (#1056) 2025-06-26 02:19:33 +08:00
Sijie.Sun cd26d9f669 fix mem leak of token bucket (#1055) 2025-06-26 02:19:26 +08:00
Sijie.Sun 4fd0253e99 fix cargo install failure (#1054) 2025-06-25 21:55:44 +08:00
Sijie.Sun ebab70ca3b add geo info for in web device list (#1052) 2025-06-25 09:03:47 +08:00
Sijie.Sun ae4a158e36 web improve (#1047) 2025-06-24 09:09:52 +08:00
Mg Pig 760a1e6306 fix rpc_portal_whitelist from config file not working (#1042) 2025-06-23 00:50:41 +08:00
Sijie.Sun fded8b1de0 limit max conn count in foreign network manager (#1041) 2025-06-22 19:11:27 +08:00
Sijie.Sun 762d5cd392 blacklist the peers which disable p2p in hole-punching client (#1038) 2025-06-22 14:39:24 +08:00
dawn-lc 09ac79b9f3 fix uninstall.cmd (#1036) 2025-06-22 12:06:16 +08:00
dawn-lc 16f6fb0c59 add Windows Service install script 2025-06-21 15:57:55 +08:00
xzzpig 385e790600 simplify Textarea class in ConfigGenerator.vue 2025-06-21 14:56:40 +08:00
liusen373 95e4e5a931 Implement custom fmt::Debug for some prost_build generated structs
Currently implemented for:
1. common.Ipv4Addr
2. common.Ipv6Addr
3. common.UUID
2025-06-21 14:56:28 +08:00
sijie.sun e1bfec6fe2 add api_meta.js to frontend public 2025-06-19 23:40:57 +08:00
sijie.sun dde7a4dff1 bps limit should throttle kcp packet 2025-06-19 22:53:41 +08:00
Sijie.Sun 40601bd05b add bps limiter (#1015)
* add token bucket
* remove quinn-proto
2025-06-19 21:15:04 +08:00
chenxudong2020 72d5ed908e quic uses the bbr congestion control algorithm (#1010) 2025-06-18 23:17:52 +08:00
liusen373 72673a9d52 Add is_hole_punched flag to PeerConn (#1001) 2025-06-18 12:14:57 +08:00
tianxiayu007 327ccdcf38 installing by homebrew should use easytier-gui (#1004) 2025-06-18 11:06:26 +08:00
Sijie.Sun 8c2f96d1aa allow set machine uid with command line (#1009) 2025-06-18 11:02:29 +08:00
Sijie.Sun 34ba0bc95b add keepalive option for quic proxy (#1008)
avoid connection loss when idle
2025-06-17 23:39:56 +08:00
Mg Pig ed162c2e66 Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990)
* add method to create NetworkConfig from TomlConfigLoader
* allow web export/import toml config file and gui edit toml config
* Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web
2025-06-15 23:41:42 +08:00
Sijie.Sun 40b5fe9a54 support quic proxy (#993)
QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC.
QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. 
QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`.
2025-06-15 19:43:45 +08:00
Sijie.Sun 5a98fac395 Update core.yml,use upx4.2.4 (#991) 2025-06-14 23:04:55 +08:00
Sijie.Sun 0bab14cd72 use bulk compress instead of streaming to reduce mem usage (#985) 2025-06-14 14:55:48 +08:00
Mg PigandSijie.Sun b407cfd9d4 Fixed the issue where the GUI would panic after using InstanceManager (#982)
Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-06-14 13:06:53 +08:00
Sijie.Sun 25dcdc652a support mapping subnet proxy (#978)
- **support mapping subproxy network cidr**
- **add command line option for proxy network mapping**
- **fix Instance leak in tests.
2025-06-14 11:42:45 +08:00
Sijie.Sun 950cb04534 remove macos default route on utun device (#976) 2025-06-12 22:24:34 +08:00
Sijie.Sun c07d1286ef internal stun server should use xor mapped addr (#975) 2025-06-12 08:09:59 +08:00
Mg Pig 8ddd153022 easytier-core支持多配置文件 (#964)
* 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中

* easytier-core支持多配置文件

* FFI复用instance manager

* 添加instance manager 单元测试
2025-06-11 23:17:09 +08:00
Sijie.Sun 870353c499 fix ospf route (#970)
- **fix deadlock in ospf route introducd by #958 **
- **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual**
- **allow multiple nodes broadcast same network ranges for subnet proxy**
- **bump version to v2.3.2**
2025-06-11 09:44:03 +08:00
BlackLuny ecebbecd3b add check for rpc packet fix #963 (#969) 2025-06-09 19:35:29 +08:00
Sijie.Sun f39fbb2ce2 ipv4-peerid table should use peer with least hop (#958)
sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list.
when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes.
2025-06-08 11:28:59 +08:00
Kiva ec56c0bc45 feat: allow using --proxy-forward-by-system together with --enable-exit-node (#957) 2025-06-07 22:27:57 +08:00
Mg Pig 20a6025075 Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) 2025-06-07 22:05:47 +08:00
BlackLuny 707963c0d9 Web dual stack (#953)
* reimplement easytier-web dual stack
* add protocol check for dual stack listener current only support tcp and udp
2025-06-07 22:05:11 +08:00
Kiva 3c7837692e fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) 2025-06-07 21:19:03 +08:00
Sijie.Sun f890812577 kcp connect retry (#952) 2025-06-07 12:24:11 +08:00
Sijie.Sun 47f3efe71b Create LICENSE (#951) 2025-06-07 10:56:54 +08:00
Sijie.Sun 6d88b10b14 remove LICENSE (#950) 2025-06-07 10:39:42 +08:00
Zisu Zhang d34a51739f Update default_port and sni logic to improve reverse proxy reachability (#947) 2025-06-07 08:19:31 +08:00
851 changed files with 224544 additions and 47870 deletions
+50 -43
View File
@@ -1,17 +1,50 @@
[target.x86_64-unknown-linux-musl]
linker = "rust-lld"
rustflags = ["-C", "linker-flavor=ld.lld"]
# region Native
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-unknown-linux-musl-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]
[target.wasm32-unknown-unknown]
rustflags = [
"-C",
"opt-level=z",
"--cfg",
'getrandom_backend="wasm_js"',
]
# region
# region CI
[target.x86_64-unknown-linux-musl]
rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-unknown-linux-musl]
rustflags = ["-C", "target-feature=+crt-static"]
[target.riscv64gc-unknown-linux-musl]
rustflags = ["-C", "target-feature=+crt-static"]
[target.armv7-unknown-linux-musleabihf]
rustflags = ["-C", "target-feature=+crt-static"]
[target.armv7-unknown-linux-musleabi]
rustflags = ["-C", "target-feature=+crt-static"]
[target.arm-unknown-linux-musleabihf]
rustflags = ["-C", "target-feature=+crt-static"]
[target.arm-unknown-linux-musleabi]
rustflags = ["-C", "target-feature=+crt-static"]
[target.loongarch64-unknown-linux-musl]
rustflags = ["-C", "target-feature=+crt-static"]
[target.mipsel-unknown-linux-musl]
linker = "mipsel-unknown-linux-muslsf-gcc"
rustflags = [
@@ -50,40 +83,14 @@ rustflags = [
"gcc",
]
[target.armv7-unknown-linux-musleabihf]
linker = "armv7-unknown-linux-musleabihf-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-unknown-linux-ohos]
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
[target.armv7-unknown-linux-musleabi]
linker = "armv7-unknown-linux-musleabi-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-unknown-linux-ohos.env]
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
[target.arm-unknown-linux-musleabihf]
linker = "arm-unknown-linux-musleabihf-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/arm-unknown-linux-musleabihf/arm-unknown-linux-musleabihf/lib",
"-L",
"./musl_gcc/arm-unknown-linux-musleabihf/lib/gcc/arm-unknown-linux-musleabihf/15.1.0",
"-l",
"atomic",
"-l",
"gcc",
]
[target.arm-unknown-linux-musleabi]
linker = "arm-unknown-linux-musleabi-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/arm-unknown-linux-musleabi/arm-unknown-linux-musleabi/lib",
"-L",
"./musl_gcc/arm-unknown-linux-musleabi/lib/gcc/arm-unknown-linux-musleabi/15.1.0",
"-l",
"atomic",
"-l",
"gcc",
]
# endregion
+2
View File
@@ -0,0 +1,2 @@
PROFILE=$(cat .flake-profile 2>/dev/null)
use flake .#${PROFILE}
+95 -13
View File
@@ -23,31 +23,113 @@ body:
- type: textarea
id: description
attributes:
label: 描述问题 / Describe the bug
description: bug 的明确描述。如果条件允许,请包括屏幕截图。 / A clear description of what the bug is. Include screenshots if applicable.
placeholder: 问题描述 / Bug description
label: 问题简要描述 / Brief Description
description: 问题的简要描述,包括期望的行为和实际发生的情况。 / A brief description of the issue, including expected vs actual behavior.
placeholder: |
例如:节点 A 无法连接到节点 B,期望能够正常建立连接
Example: Node A cannot connect to Node B, expected to establish connection normally
validations:
required: true
- type: textarea
id: environment-info
attributes:
label: 环境信息 / Environment Information
description: 请提供网络拓扑、节点信息和系统环境详情。 / Please provide network topology, node information and system environment details.
placeholder: |
**EasyTier 版本(非常重要)/ EasyTier Version (Very Important):** v1.2.0
**网络拓扑 / Network Topology:**
- 节点 A (10.1.1.1): Windows 11 Pro 22H2, Wifi,有 IPV6 地址
- 节点 B (10.1.1.2): Ubuntu 22.04.3 LTS (Linux 5.15.0-72-generic), 公网 IP
- 节点 C (10.1.1.3): macOS Ventura 13.4.1, 5G 流量,无 IPV6 地址
**Network Topology:**
- Node A (10.1.1.1): Windows 11 Pro 22H2, Wifi, has IPV6 address
- Node B (10.1.1.2): Ubuntu 22.04.3 LTS (Linux 5.15.0-72-generic), public IP
- Node C (10.1.1.3): macOS Ventura 13.4.1, 5G traffic, no IPV6 address
validations:
required: true
- type: textarea
id: node-configs
attributes:
label: 节点配置 / Node Configurations
description: 请提供每个节点的配置文件或启动参数。 / Please provide configuration files or startup parameters for each node.
placeholder: |
**节点 A 配置 / Node A Config:**
```
easytier-core --config-file config.toml
```
**节点 B 配置 / Node B Config:**
```
easytier-core --ipv4 10.1.1.2 --peers tcp://1.2.3.4:11010
```
请贴出完整的配置文件内容或命令行参数
Please paste complete configuration file contents or command line arguments
validations:
required: true
- type: textarea
id: logs
attributes:
label: 日志信息 / Log Information
description: 请提供相关的日志信息,包括 GUI 的事件日志或命令行的控制台输出。 / Please provide relevant log information, including GUI event logs or command line console output.
placeholder: |
请粘贴相关的日志信息:
- GUI 用户:请提供事件日志中的错误信息
- 命令行用户:请提供控制台输出的详细日志
- 一般情况下,提供默认输出的事件日志即可
- 如果能提供 --file-log-level debug 输出的日志,会更方便 debug
Please paste relevant log information:
- GUI users: Please provide error messages from event logs
- CLI users: Please provide detailed console output logs
- Default log output is usually sufficient
- If possible, logs with --file-log-level debug would be more helpful for debugging
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: 重现步骤 / Reproduction
description: 能够重现行为的步骤或指向能够复现的存储库链接。 / A link to a reproduction repo or steps to reproduce the behaviour.
label: 重现步骤 / Reproduction Steps
description: 请提供详细的步骤来重现这个问题。 / Please provide detailed steps to reproduce this issue.
placeholder: |
请提供一个最小化的复现示例或复现步骤,请参考这个指南 https://stackoverflow.com/help/minimal-reproducible-example
Please provide a minimal reproduction or steps to reproduce, see this guide https://stackoverflow.com/help/minimal-reproducible-example
为什么需要重现(问题)?请参阅这篇文章 https://antfu.me/posts/why-reproductions-are-required
Why reproduction is required? see this article https://antfu.me/posts/why-reproductions-are-required
1. 启动节点 A,使用配置 xxx / Start Node A with config xxx
2. 启动节点 B,使用配置 yyy / Start Node B with config yyy
3. 尝试从节点 A ping 节点 B / Try to ping Node B from Node A
4. 观察到错误:xxx / Observe error: xxx
请提供详细的操作步骤,以便我们能够重现问题
Please provide detailed steps so we can reproduce the issue
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: 预期结果 / Expected behavior
label: 预期结果 / Expected Behavior
description: 清楚地描述您期望发生的事情。 / A clear description of what you expected to happen.
placeholder: |
例如:节点 A 应该能够成功 ping 通节点 B,延迟在 100ms 以内
Example: Node A should be able to ping Node B successfully with latency under 100ms
- type: textarea
id: context
id: additional-context
attributes:
label: 额外上下文 / Additional context
description: 在这里添加关于问题的任何其他上下文。 / Add any other context about the problem here.
label: 额外信息 / Additional Context
description: 在这里添加关于问题的任何其他上下文信息。 / Add any other context about the problem here.
placeholder: |
例如:
- 这个问题是否在特定时间出现?
- 是否有网络环境的特殊配置?
- 是否尝试过其他解决方案?
Example:
- Does this issue occur at specific times?
- Are there any special network environment configurations?
- Have you tried any other solutions?
+156 -15
View File
@@ -3,36 +3,177 @@
name: 💡 新功能请求 / Feature Request
title: '[feat] '
description: 提出一个想法 / Suggest an idea
description: 提出一个想法 / Suggest an idea
labels: ['type: feature request']
body:
- type: textarea
id: problem
- type: markdown
attributes:
label: 描述问题 / Describe the problem
description: 明确描述此功能将解决的问题 / A clear description of the problem this feature would solve
placeholder: "我总是在...感觉困惑 / I'm always frustrated when..."
value: |
## 提交功能请求前请注意 / Before Submitting
1. 请先搜索 [现有的功能请求](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue+label%3A%22type%3A+feature+request%22) 确保您的想法尚未被提出。
1. Please search [existing feature requests](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue+label%3A%22type%3A+feature+request%22) to ensure your idea hasn't been suggested already.
2. 请确保这个功能确实适合 EasyTier 项目的目标和范围。
2. Please ensure this feature fits within EasyTier's goals and scope.
3. 考虑这个功能是否能让更多用户受益,而不只是解决个人需求。
3. Consider whether this feature would benefit many users, not just personal needs.
- type: dropdown
id: feature-category
attributes:
label: 功能类别 / Feature Category
description: 请选择这个功能请求属于哪个类别 / Please select which category this feature request belongs to
options:
- 网络连接 / Network Connectivity
- 安全和加密 / Security & Encryption
- 性能优化 / Performance Optimization
- 用户界面 / User Interface
- 配置管理 / Configuration Management
- 监控和日志 / Monitoring & Logging
- 平台支持 / Platform Support
- API 和集成 / API & Integration
- 其他 / Other
validations:
required: true
- type: textarea
id: solution
id: use-case
attributes:
label: "描述您想要的解决方案 / Describe the solution you'd like"
description: 明确说明您希望做出的改变 / A clear description of what change you would like
placeholder: '我希望... / I would like to...'
label: 使用场景 / Use Case
description: 描述您希望这个功能解决的具体使用场景或问题 / Describe the specific use case or problem you want this feature to solve
placeholder: |
例如:
- 作为企业用户,我需要在多个分支机构之间建立安全的网络连接
- 作为开发者,我希望能够通过 API 监控网络状态
- 作为系统管理员,我需要更详细的连接日志来排查问题
Example:
- As an enterprise user, I need to establish secure network connections between multiple branch offices
- As a developer, I want to monitor network status through APIs
- As a system administrator, I need more detailed connection logs for troubleshooting
validations:
required: true
- type: textarea
id: current-limitations
attributes:
label: 当前限制 / Current Limitations
description: 描述当前 EasyTier 的哪些限制阻止了您实现这个使用场景 / Describe what current limitations in EasyTier prevent you from achieving this use case
placeholder: |
例如:
- 目前不支持基于用户角色的访问控制
- 缺少对 IPv6 的完整支持
- 没有提供 REST API 来获取网络状态
Example:
- Currently lacks role-based access control
- Missing complete IPv6 support
- No REST API available for network status
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: 建议的解决方案 / Proposed Solution
description: 详细描述您希望添加的功能以及它应该如何工作 / Describe in detail the feature you'd like to add and how it should work
placeholder: |
请描述:
- 功能的具体实现方式
- 用户界面或 API 设计
- 配置选项和参数
- 与现有功能的集成方式
Please describe:
- Specific implementation approach
- User interface or API design
- Configuration options and parameters
- Integration with existing features
validations:
required: true
- type: textarea
id: benefits
attributes:
label: 预期收益 / Expected Benefits
description: 说明这个功能会带来什么好处,会影响哪些用户群体 / Explain what benefits this feature would bring and which user groups it would affect
placeholder: |
例如:
- 提高网络连接的稳定性和性能
- 简化大规模部署的管理复杂度
- 增强企业用户的安全性需求
- 降低新用户的学习成本
Example:
- Improve network connection stability and performance
- Simplify management complexity for large-scale deployments
- Enhance security requirements for enterprise users
- Reduce learning curve for new users
- type: textarea
id: technical-considerations
attributes:
label: 技术考虑 / Technical Considerations
description: 如果您了解技术细节,请分享相关的技术考虑或约束 / If you have technical knowledge, please share relevant technical considerations or constraints
placeholder: |
例如:
- 可能需要修改网络协议栈
- 需要考虑跨平台兼容性
- 可能影响现有性能
- 依赖第三方库或协议
Example:
- May require modifications to network protocol stack
- Cross-platform compatibility needs consideration
- Potential impact on existing performance
- Dependencies on third-party libraries or protocols
- type: textarea
id: alternatives
attributes:
label: 替代方案 / Alternatives considered
description: "您考虑过的任何替代解决方案 / Any alternative solutions you've considered"
label: 备选方案 / Alternative Solutions
description: 您是否考虑过其他解决方案?是否有现有的替代方案 / Have you considered other solutions? Are there existing alternatives?
placeholder: |
例如:
- 使用第三方工具 X 可以部分解决,但缺少 Y 功能
- 通过脚本workaround可以实现,但不够优雅
- 其他类似项目 Z 有这个功能,可以参考其实现
Example:
- Third-party tool X can partially solve this, but lacks Y functionality
- Can be achieved through script workarounds, but not elegant
- Similar project Z has this feature, could reference its implementation
- type: textarea
id: context
id: implementation-priority
attributes:
label: 额外上下文 / Additional context
description: 在此处添加有关问题的任何其他上下文。 / Add any other context about the problem here.
label: 实现优先级 / Implementation Priority
description: 这个功能对您有多重要?是否有时间要求? / How important is this feature to you? Any time requirements?
placeholder: |
例如:
- 高优先级:阻碍了我们的生产部署
- 中优先级:会显著改善用户体验
- 低优先级:锦上添花的功能
Example:
- High priority: Blocking our production deployment
- Medium priority: Would significantly improve user experience
- Low priority: Nice-to-have feature
- type: textarea
id: additional-context
attributes:
label: 补充信息 / Additional Context
description: 添加任何其他相关信息,如截图、链接、参考资料等 / Add any other relevant information such as screenshots, links, or references
placeholder: |
例如:
- 相关的 RFC 或技术规范
- 其他项目的实现示例
- 用户调研或反馈数据
- 设计草图或流程图
Example:
- Relevant RFCs or technical specifications
- Implementation examples from other projects
- User research or feedback data
- Design sketches or flowcharts
+98
View File
@@ -0,0 +1,98 @@
name: prepare-build
author: Luna
description: Prepare build environment
inputs:
target:
description: 'The target to build for'
required: false
pnpm:
description: 'Whether to run pnpm build'
required: true
default: 'true'
pnpm-build-filter:
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
required: false
default: './easytier-web/*'
gui:
description: 'Whether to prepare the GUI build environment'
required: true
default: 'true'
token:
description: 'GitHub token, used by setup-protoc action'
required: false
runs:
using: 'composite'
steps:
- run: mkdir -p easytier-gui/dist
shell: bash
- name: Install dependencies
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt-get update
sudo apt-get install -qqy build-essential mold musl-tools
shell: bash
- name: Setup protoc
uses: arduino/setup-protoc@v3
with:
version: '35.1'
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ inputs.token }}
- name: Verify protoc version
run: |
version="$(protoc --version | tr -d '\r')"
test "$version" = "libprotoc 35.1"
shell: bash
- name: Setup Frontend Environment
if: ${{ inputs.pnpm == 'true' }}
uses: ./.github/actions/prepare-pnpm
with:
token: ${{ inputs.token }}
build-filter: ${{ inputs.pnpm-build-filter }}
- name: Install GUI dependencies (Linux)
if: ${{ inputs.gui == 'true' && runner.os == 'Linux' }}
run: |
sudo apt-get install -qq xdg-utils \
libappindicator3-dev \
libgtk-3-dev \
librsvg2-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev
shell: bash
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.95
target: ${{ !contains(inputs.target, 'mips') && inputs.target || '' }}
components: ${{ contains(inputs.target, 'mips') && 'rust-src' || '' }}
cache: false
rustflags: ''
- name: Install Rust (MIPS)
if: ${{ contains(inputs.target, 'mips') }}
run: |
MUSL_TARGET=${{ inputs.target }}sf
mkdir -p ./musl_gcc
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
sudo chmod -R a+rwx ./musl_gcc
if [[ -d "./musl_gcc/sysroot" ]]; then
echo "BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)" >> $GITHUB_ENV
fi
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
# for panic-abort
cp libgcc_eh.a libunwind.a
# for mimalloc
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
shell: bash
+75
View File
@@ -0,0 +1,75 @@
name: 'Setup pnpm'
author: Luna
description: 'Setup Node.js, pnpm, and install dependencies'
inputs:
token:
description: 'GitHub token, used by setup-protoc action'
required: false
build-filter:
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
required: false
default: ''
runs:
using: "composite"
steps:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.95
target: wasm32-unknown-unknown
cache: false
rustflags: ''
- uses: taiki-e/install-action@v2
with:
tool: wasm-pack
- uses: arduino/setup-protoc@v3
with:
version: '35.1'
repo-token: ${{ inputs.token }}
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 22
- name: Install pnpm
uses: pnpm/action-setup@v5
with:
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install and build
shell: bash
run: |
pnpm -r install
if [ -n "${{ inputs.build-filter }}" ]; then
echo "Building with filter: ${{ inputs.build-filter }}"
pnpm -r --workspace-concurrency=1 --filter "${{ inputs.build-filter }}" build
else
echo "No build filter provided, building all packages"
pnpm -r --workspace-concurrency=1 build
fi
- name: Bundle config generator with web frontend
shell: bash
run: |
if [ -f easytier-web/frontend/dist/index.html ] && [ -f easytier-web/config-generator/dist/index.html ]; then
mkdir -p easytier-web/frontend/dist/config-generator
cp -R easytier-web/config-generator/dist/. easytier-web/frontend/dist/config-generator/
fi
+16 -7
View File
@@ -1,29 +1,35 @@
FROM alpine:latest AS builder
FROM alpine:latest AS base
FROM base AS builder
ARG TARGETPLATFORM
COPY . /tmp/artifacts
RUN mkdir -p /tmp/output; \
cd /tmp/artifacts; \
ARTIFACT_ARCH=""; \
WORKDIR /tmp/output
RUN ARTIFACT_ARCH=""; \
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
ARTIFACT_ARCH="x86_64"; \
elif [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then \
ARTIFACT_ARCH="armhf"; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
ARTIFACT_ARCH="armv7hf"; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
ARTIFACT_ARCH="aarch64"; \
elif [ "$TARGETPLATFORM" = "linux/riscv64" ]; then \
ARTIFACT_ARCH="riscv64"; \
else \
echo "Unsupported architecture: $TARGETARCH"; \
echo "Unsupported architecture: $TARGETPLATFORM"; \
exit 1; \
fi; \
cp /tmp/artifacts/easytier-linux-${ARTIFACT_ARCH}/* /tmp/output;
FROM alpine:latest
FROM base
RUN apk add --no-cache tzdata tini
WORKDIR /app
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
# users can use "-e TZ=xxx" to adjust it
ENV TZ Asia/Shanghai
ENV TZ=Asia/Shanghai
# tcp
EXPOSE 11010/tcp
@@ -36,4 +42,7 @@ EXPOSE 11011/tcp
# wss
EXPOSE 11012/tcp
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
CMD ["/usr/local/bin/easytier-cli", "--rpc-portal", "127.0.0.1:15888", "--output", "json", "node", "info"]
ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]
+145 -162
View File
@@ -2,9 +2,14 @@ name: EasyTier Core
on:
push:
branches: ["develop", "main", "releases/**"]
branches: [ "develop", "main", "releases/**" ]
pull_request:
branches: ["develop", "main"]
branches: [ "develop", "main" ]
types: [ opened, synchronize, reopened, ready_for_review ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
@@ -18,6 +23,7 @@ jobs:
pre_job:
# continue-on-error: true # Uncomment once integration is finished
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
# Map a step output to a job output
outputs:
# do not skip push on branch starts with releases/
@@ -30,44 +36,22 @@ jobs:
concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true'
cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-proto/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
build_web:
runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- name: Setup Frontend Environment
uses: ./.github/actions/prepare-pnpm
with:
node-version: 21
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
pnpm -r install
pnpm -r --filter "./easytier-web/*" build
token: ${{ github.token }}
build-filter: './easytier-web/*'
- name: Archive artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: easytier-web-dashboard
path: |
@@ -77,31 +61,45 @@ jobs:
fail-fast: false
matrix:
include:
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-22.04
ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-22.04
OS: ubuntu-24.04
ARTIFACT_NAME: linux-x86_64
- TARGET: mips-unknown-linux-musl
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mips
- TARGET: mipsel-unknown-linux-musl
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mipsel
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-24.04-arm
ARTIFACT_NAME: linux-aarch64
- TARGET: riscv64gc-unknown-linux-musl
OS: ubuntu-24.04
ARTIFACT_NAME: linux-riscv64
- TARGET: loongarch64-unknown-linux-musl
OS: ubuntu-24.04
ARTIFACT_NAME: linux-loongarch64
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
OS: ubuntu-22.04
OS: ubuntu-24.04
ARTIFACT_NAME: linux-armv7hf
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
OS: ubuntu-22.04
OS: ubuntu-24.04
ARTIFACT_NAME: linux-armv7
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
OS: ubuntu-22.04
OS: ubuntu-24.04
ARTIFACT_NAME: linux-armhf
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
OS: ubuntu-22.04
OS: ubuntu-24.04
ARTIFACT_NAME: linux-arm
- TARGET: mips-unknown-linux-musl
OS: ubuntu-24.04
ARTIFACT_NAME: linux-mips
- TARGET: mipsel-unknown-linux-musl
OS: ubuntu-24.04
ARTIFACT_NAME: linux-mipsel
- TARGET: x86_64-unknown-freebsd
OS: ubuntu-24.04
ARTIFACT_NAME: freebsd-13.2-x86_64
BSD_VERSION: 13.2
- TARGET: x86_64-apple-darwin
OS: macos-latest
ARTIFACT_NAME: macos-x86_64
@@ -112,17 +110,12 @@ jobs:
- TARGET: x86_64-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-i686
- TARGET: x86_64-unknown-freebsd
OS: ubuntu-22.04
ARTIFACT_NAME: freebsd-13.2-x86_64
BSD_VERSION: 13.2
- TARGET: aarch64-pc-windows-msvc
OS: windows-11-arm
ARTIFACT_NAME: windows-arm64
runs-on: ${{ matrix.OS }}
env:
@@ -135,7 +128,7 @@ jobs:
- build_web
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Set current ref as env variable
run: |
@@ -147,149 +140,131 @@ jobs:
name: easytier-web-dashboard
path: easytier-web/frontend/dist/
- name: Cargo cache
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
uses: actions/cache@v4
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
target: ${{ matrix.TARGET }}
gui: true
pnpm: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup protoc
uses: arduino/setup-protoc@v2
- uses: Swatinem/rust-cache@v2
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
# The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust"
prefix-key: ""
shared-key: "core-registry"
cache-targets: "false"
- name: Build Core & Cli
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
run: |
bash ./.github/workflows/install_rust.sh
- uses: mlugg/setup-zig@v2
if: ${{ contains(matrix.OS, 'ubuntu') }}
with:
version: 0.16.0
use-cache: true
# we set the sysroot when sysroot is a dir
# this dir is a soft link generated by install_rust.sh
# kcp-sys need this to gen ffi bindings. without this clang may fail to find some libc headers such as bits/libc-header-start.h
if [[ -d "./musl_gcc/sysroot" ]]; then
export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)
fi
- uses: taiki-e/install-action@v2
if: ${{ contains(matrix.OS, 'ubuntu') }}
with:
tool: cargo-zigbuild
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
- name: Build
if: ${{ !contains(matrix.TARGET, 'mips') }}
run: |
if [[ "$TARGET" == *windows* ]]; then
SUFFIX=.exe
else
if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe
fi
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
cargo build --release --verbose --target $TARGET
SUFFIX=""
fi
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
- name: Build Core & Cli (X86_64 FreeBSD)
uses: vmactions/freebsd-vm@v1
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
if [[ "$TARGET" =~ (x86_64-unknown-linux-musl|aarch64-unknown-linux-musl|windows|darwin) ]]; then
BUILD=build
else
BUILD=zigbuild
fi
if [[ "$TARGET" =~ ^(riscv64|loongarch64|aarch64).*$ || "$TARGET" =~ (freebsd|windows) ]]; then
FEATURES="mimalloc"
else
FEATURES="jemalloc"
fi
cargo $BUILD --release --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
cargo $BUILD --release --target $TARGET --features=$FEATURES
- name: Build (MIPS)
if: ${{ contains(matrix.TARGET, 'mips') }}
env:
TARGET: ${{ matrix.TARGET }}
with:
envs: TARGET
release: ${{ matrix.BSD_VERSION }}
arch: x86_64
usesh: true
mem: 6144
cpu: 4
run: |
uname -a
echo $SHELL
pwd
ls -lah
whoami
env | sort
pkg install -y git protobuf llvm-devel sudo curl
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. $HOME/.cargo/env
rustup set auto-self-update disable
rustup install 1.86
rustup default 1.86
export CC=clang
export CXX=clang++
export CARGO_TERM_COLOR=always
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web ./target/$TARGET/release/easytier-web-embed
cargo build --release --verbose --target $TARGET
RUSTC_BOOTSTRAP: 1
run: |
cargo build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
- name: Compress
run: |
mkdir -p ./artifacts/objects/
# windows is the only OS using a different convention for executable file name
if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe
cp easytier/third_party/*.dll ./artifacts/objects/
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^i686.*$ ]]; then
SUFFIX=.exe
cp easytier/third_party/i686/*.dll ./artifacts/objects/
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
SUFFIX=.exe
cp easytier/third_party/arm64/*.dll ./artifacts/objects/
case $TARGET in
x86_64*) ARCH_DIR=x86_64 ;;
i686*) ARCH_DIR=i686 ;;
aarch64*) ARCH_DIR=arm64 ;;
esac
if [[ -n "$ARCH_DIR" ]]; then
find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \;
fi
fi
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME
else
TAG=$GITHUB_SHA
fi
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ (loongarch|freebsd) ]]; then
HOST_ARCH=$(uname -m)
case $HOST_ARCH in
x86_64) UPX_ARCH="amd64" ;;
aarch64) UPX_ARCH="arm64" ;;
*) UPX_ARCH="amd64" ;;
esac
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
UPX_VERSION=5.0.1
curl -L https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -s | tar xJvf -
cp upx-${UPX_VERSION}-amd64_linux/upx .
./upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
./upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
UPX_VERSION=4.2.4
UPX_PKG="upx-${UPX_VERSION}-${UPX_ARCH}_linux"
curl -L "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_PKG}.tar.xz" -s | tar xJvf -
cp "${UPX_PKG}/upx" .
UPX_BIN=./upx
fi
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
if [[ ! $TARGET =~ ^mips.*$ ]]; then
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
fi
for BIN in ./target/$TARGET/release/easytier-{core,cli,web,web-embed}"$SUFFIX"; do
if [[ -f "$BIN" ]]; then
if [[ -n "$UPX_BIN" ]]; then
$UPX_BIN --lzma --best "$BIN" || true
fi
mv "$BIN" ./artifacts/objects/
fi
done
mv ./artifacts/objects/* ./artifacts/
rm -rf ./artifacts/objects/
- name: Archive artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: easytier-${{ matrix.ARTIFACT_NAME }}
path: |
./artifacts/*
core-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
needs:
- pre_job
- build_web
- build
steps:
- name: Mark result as failed
if: needs.build.result != 'success'
run: exit 1
magisk_build:
needs:
- pre_job
- build_web
- build
if: needs.pre_job.outputs.should_skip != 'true' && always()
build_magisk:
runs-on: ubuntu-latest
needs: [ pre_job, build_web, build ]
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
steps:
- name: Checkout Code
uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
# 下载二进制文件到独立目录
- name: Download Linux aarch64 binaries
@@ -306,14 +281,22 @@ jobs:
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
# 上传生成的模块
- name: Upload Magisk Module
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: Easytier-Magisk
path: |
./easytier-contrib/easytier-magisk
!./easytier-contrib/easytier-magisk/build.sh
!./easytier-contrib/easytier-magisk/magisk_update.json
if-no-files-found: error
if-no-files-found: error
core-result:
runs-on: ubuntu-latest
needs: [ pre_job, build_web, build, build_magisk ]
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
steps:
- name: Mark result as failed
if: contains(needs.*.result, 'failure')
run: exit 1
+41 -7
View File
@@ -11,13 +11,18 @@ on:
image_tag:
description: 'Tag for this image build'
type: string
default: 'v2.3.1'
default: 'v2.6.4'
required: true
mark_latest:
description: 'Mark this image as latest'
type: boolean
default: false
required: true
mark_unstable:
description: 'Mark this image as unstable'
type: boolean
default: false
required: true
jobs:
docker:
@@ -26,7 +31,14 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
-
name: Validate inputs
run: |
if [[ "${{ inputs.mark_latest }}" == "true" && "${{ inputs.mark_unstable }}" == "true" ]]; then
echo "Error: mark_latest and mark_unstable cannot both be true"
exit 1
fi
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -47,7 +59,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download artifact
id: download-artifact
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.run_id }}
@@ -56,14 +68,36 @@ jobs:
- name: List files
run: |
ls -l -R .
- name: Prepare Docker tags
id: tags
run: |
# Base tags with version
DOCKERHUB_TAGS="easytier/easytier:${{ inputs.image_tag }}"
GHCR_TAGS="ghcr.io/easytier/easytier:${{ inputs.image_tag }}"
# Add latest tags if requested
if [[ "${{ inputs.mark_latest }}" == "true" ]]; then
DOCKERHUB_TAGS="${DOCKERHUB_TAGS},easytier/easytier:latest"
GHCR_TAGS="${GHCR_TAGS},ghcr.io/easytier/easytier:latest"
fi
# Add unstable tags if requested
if [[ "${{ inputs.mark_unstable }}" == "true" ]]; then
DOCKERHUB_TAGS="${DOCKERHUB_TAGS},easytier/easytier:unstable"
GHCR_TAGS="${GHCR_TAGS},ghcr.io/easytier/easytier:unstable"
fi
# Combine all tags
ALL_TAGS="${DOCKERHUB_TAGS},${GHCR_TAGS}"
echo "tags=${ALL_TAGS}" >> $GITHUB_OUTPUT
echo "Generated tags: ${ALL_TAGS}"
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: ./docker_context
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/riscv64
push: true
file: .github/workflows/Dockerfile
tags: |
easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
ghcr.io/easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
tags: ${{ steps.tags.outputs.tags }}
+132 -130
View File
@@ -5,7 +5,12 @@ on:
branches: ["develop", "main", "releases/**"]
pull_request:
branches: ["develop", "main"]
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
@@ -18,6 +23,7 @@ jobs:
pre_job:
# continue-on-error: true # Uncomment once integration is finished
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
@@ -29,20 +35,20 @@ jobs:
concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true'
cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh"]'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
build-gui:
strategy:
fail-fast: false
fail-fast: true
matrix:
include:
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-22.04
GUI_TARGET: aarch64-unknown-linux-gnu
ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-22.04
OS: ubuntu-24.04
GUI_TARGET: x86_64-unknown-linux-gnu
ARTIFACT_NAME: linux-x86_64
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-24.04-arm
GUI_TARGET: aarch64-unknown-linux-gnu
ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-apple-darwin
OS: macos-latest
@@ -57,16 +63,14 @@ jobs:
OS: windows-latest
GUI_TARGET: x86_64-pc-windows-msvc
ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
GUI_TARGET: aarch64-pc-windows-msvc
ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc
OS: windows-latest
GUI_TARGET: i686-pc-windows-msvc
ARTIFACT_NAME: windows-i686
- TARGET: aarch64-pc-windows-msvc
OS: windows-11-arm
GUI_TARGET: aarch64-pc-windows-msvc
ARTIFACT_NAME: windows-arm64
runs-on: ${{ matrix.OS }}
env:
@@ -78,127 +82,128 @@ jobs:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- name: Install GUI dependencies (x86 only)
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
run: |
sudo apt update
sudo apt install -qq libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
- name: Install GUI cross compile (aarch64 only)
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
run: |
# see https://tauri.app/v1/guides/building/linux/
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install aptitude
sudo aptitude install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64 \
libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64 \
libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- uses: actions/setup-node@v4
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
node-version: 21
target: ${{ matrix.TARGET }}
gui: true
pnpm: true
pnpm-build-filter: ''
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install pnpm
uses: pnpm/action-setup@v3
- uses: Swatinem/rust-cache@v2
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
pnpm -r install
pnpm -r build
- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install rust target
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc
uses: arduino/setup-protoc@v2
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
# The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust"
prefix-key: ""
shared-key: "gui-registry"
cache-targets: "false"
- name: copy correct DLLs
if: ${{ matrix.OS == 'windows-latest' }}
if: ${{ contains(matrix.GUI_TARGET, 'windows') }}
run: |
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/
elif [[ $GUI_TARGET =~ ^i686.*$ ]]; then
cp ./easytier/third_party/i686/*.dll ./easytier-gui/src-tauri/
else
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
case $TARGET in
x86_64*) ARCH_DIR=x86_64 ;;
i686*) ARCH_DIR=i686 ;;
aarch64*) ARCH_DIR=arm64 ;;
esac
if [[ -n "$ARCH_DIR" ]]; then
find "./easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./easytier-gui/src-tauri/ \;
fi
- name: Validate macOS signing secrets
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
missing=()
for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
if [[ -z "${!name}" ]]; then
missing+=("$name")
fi
done
if (( ${#missing[@]} )); then
printf 'Missing macOS signing secret(s): %s\n' "${missing[*]}" >&2
exit 1
fi
- name: Build GUI
if: ${{ matrix.GUI_TARGET != '' }}
if: ${{ matrix.GUI_TARGET != '' && (!contains(matrix.GUI_TARGET, 'darwin') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) }}
uses: tauri-apps/tauri-action@v0
with:
projectPath: ./easytier-gui
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-22.04' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
args: --verbose --target ${{ matrix.GUI_TARGET }}
- name: Compress
- name: Build GUI (signed and notarized)
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
timeout-minutes: 60
uses: tauri-apps/tauri-action@v0
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
projectPath: ./easytier-gui
args: --verbose --target ${{ matrix.GUI_TARGET }}
- name: Notarize and staple macOS DMG
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
timeout-minutes: 45
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
dmg_dir="./target/$GUI_TARGET/release/bundle/dmg"
if [[ ! -d "$dmg_dir" ]]; then
printf 'macOS DMG directory not found: %s\n' "$dmg_dir" >&2
exit 1
fi
dmgs=()
while IFS= read -r dmg; do
dmgs+=("$dmg")
done < <(find "$dmg_dir" -maxdepth 1 -type f -name "*.dmg" | sort)
if (( ${#dmgs[@]} == 0 )); then
printf 'No macOS DMG found in %s\n' "$dmg_dir" >&2
exit 1
fi
for dmg in "${dmgs[@]}"; do
printf 'Verifying signed DMG: %s\n' "$dmg"
codesign --verify --verbose=4 "$dmg"
codesign -dv --verbose=4 "$dmg"
printf 'Notarizing DMG: %s\n' "$dmg"
xcrun notarytool submit "$dmg" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait \
--timeout 40m
printf 'Stapling DMG: %s\n' "$dmg"
xcrun stapler staple "$dmg"
xcrun stapler validate "$dmg"
done
- name: Collect artifact
run: |
mkdir -p ./artifacts/objects/
@@ -207,36 +212,33 @@ jobs:
else
TAG=$GITHUB_SHA
fi
# copy gui bundle, gui is built without specific target
if [[ $OS =~ ^windows.*$ ]]; then
if [[ $GUI_TARGET =~ windows ]]; then
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
elif [[ $OS =~ ^macos.*$ ]]; then
elif [[ $GUI_TARGET =~ darwin ]]; then
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
elif [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^mips.*$ ]]; then
elif [[ $GUI_TARGET =~ linux ]]; then
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
if [[ $GUI_TARGET =~ ^x86_64.*$ ]]; then
# currently only x86 appimage is supported
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
fi
mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
fi
mv ./artifacts/objects/* ./artifacts/
rm -rf ./artifacts/objects/
- name: Archive artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
path: |
./artifacts/*
gui-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
needs:
- pre_job
- build-gui
needs: [ pre_job, build-gui ]
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
steps:
- name: Mark result as failed
if: needs.build-gui.result != 'success'
if: contains(needs.*.result, 'failure')
run: exit 1
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env bash
# env needed:
# - TARGET
# - GUI_TARGET
# - OS
# dependencies are only needed on ubuntu as that's the only place where
# we make cross-compilation
if [[ $OS =~ ^ubuntu.*$ ]]; then
sudo apt-get update && sudo apt-get install -qq musl-tools libappindicator3-dev llvm clang
# https://github.com/cross-tools/musl-cross/releases
# if "musl" is a substring of TARGET, we assume that we are using musl
MUSL_TARGET=$TARGET
# if target is mips or mipsel, we should use soft-float version of musl
if [[ $TARGET =~ ^mips.*$ || $TARGET =~ ^mipsel.*$ ]]; then
MUSL_TARGET=${TARGET}sf
fi
if [[ $MUSL_TARGET =~ musl ]]; then
mkdir -p ./musl_gcc
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
sudo chmod -R a+rwx ./musl_gcc
fi
fi
# see https://github.com/rust-lang/rustup/issues/3709
rustup set auto-self-update disable
rustup install 1.86
rustup default 1.86
# mips/mipsel cannot add target from rustup, need compile by ourselves
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
# for panic-abort
cp libgcc_eh.a libunwind.a
# for mimalloc
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
rustup toolchain install nightly-x86_64-unknown-linux-gnu
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
# https://github.com/rust-lang/rust/issues/128808
# remove it after Cargo or rustc fix this.
RUST_LIB_SRC=$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/
if [[ -f $RUST_LIB_SRC/library/Cargo.lock && ! -f $RUST_LIB_SRC/Cargo.lock ]]; then
cp -f $RUST_LIB_SRC/library/Cargo.lock $RUST_LIB_SRC/Cargo.lock
fi
else
rustup target add $TARGET
if [[ $GUI_TARGET != '' ]]; then
rustup target add $GUI_TARGET
fi
fi
+47 -71
View File
@@ -5,7 +5,12 @@ on:
branches: ["develop", "main", "releases/**"]
pull_request:
branches: ["develop", "main"]
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
@@ -18,6 +23,7 @@ jobs:
pre_job:
# continue-on-error: true # Uncomment once integration is finished
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
@@ -29,25 +35,30 @@ jobs:
concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true'
cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
build-mobile:
strategy:
fail-fast: false
fail-fast: true
matrix:
include:
- TARGET: android
OS: ubuntu-22.04
ARTIFACT_NAME: android
runs-on: ${{ matrix.OS }}
- TARGET: aarch64-linux-android
ARCH: aarch64
- TARGET: armv7-linux-androideabi
ARCH: armv7
- TARGET: i686-linux-android
ARCH: i686
- TARGET: x86_64-linux-android
ARCH: x86_64
runs-on: ubuntu-latest
env:
NAME: easytier
TARGET: ${{ matrix.TARGET }}
OS: ${{ matrix.OS }}
ARCH: ${{ matrix.ARCH }}
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Set current ref as env variable
run: |
@@ -56,79 +67,46 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: '20'
java-version: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
cmdline-tools-version: 11076708
packages: 'build-tools;34.0.0 ndk;26.0.10792818 tools platform-tools platforms;android-34 '
cmdline-tools-version: 12.0
packages: 'build-tools;34.0.0 ndk;26.0.10792818 platform-tools platforms;android-34 '
- name: Setup Android Environment
run: |
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
echo "$ANDROID_HOME/ndk/26.0.10792818/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" > $GITHUB_ENV
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" >> $GITHUB_ENV
- uses: actions/setup-node@v4
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
node-version: 21
target: ${{ matrix.TARGET }}
gui: false
pnpm: true
pnpm-build-filter: ''
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install pnpm
uses: pnpm/action-setup@v3
- uses: Swatinem/rust-cache@v2
with:
version: 9
run_install: false
# The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust"
prefix-key: ""
shared-key: "gui-registry"
cache-targets: "false"
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
pnpm -r install
pnpm -r build
- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install rust target
run: |
bash ./.github/workflows/install_rust.sh
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add i686-linux-android
rustup target add x86_64-linux-android
- name: Setup protoc
uses: arduino/setup-protoc@v2
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Android
- name: Build
run: |
cd easytier-gui
pnpm tauri android build
pnpm tauri android build --apk --target "$ARCH" --split-per-abi
- name: Compress
- name: Collect artifact
run: |
mkdir -p ./artifacts/objects/
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk ./artifacts/objects/
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/*/release/*.apk ./artifacts/objects/
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME
@@ -136,23 +114,21 @@ jobs:
TAG=$GITHUB_SHA
fi
mv ./artifacts/objects/* ./artifacts
mv ./artifacts/objects/* ./artifacts/
rm -rf ./artifacts/objects/
- name: Archive artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
name: easytier-mobile-android-${{ matrix.ARCH }}
path: |
./artifacts/*
mobile-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
needs:
- pre_job
- build-mobile
needs: [ pre_job, build-mobile ]
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
steps:
- name: Mark result as failed
if: needs.build-mobile.result != 'success'
if: contains(needs.*.result, 'failure')
run: exit 1
+44
View File
@@ -0,0 +1,44 @@
name: Nix Check
on:
push:
branches: ["main", "develop"]
paths:
- "**/*.nix"
- "flake.lock"
- "rust-toolchain.toml"
pull_request:
branches: ["main", "develop"]
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "**/*.nix"
- "flake.lock"
- "rust-toolchain.toml"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
check-full-shell:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@v6
- name: Warm up full devShell
run: nix develop .#full --command true
- name: Cargo check in flake environment
run: nix develop .#full --command cargo check
- name: Cargo build in flake environment
run: nix develop .#full --command cargo build
+205
View File
@@ -0,0 +1,205 @@
name: ohos
on:
push:
branches: [develop, main, "releases/**", "ohos/**"]
tags:
- "v*"
- "!*-pre"
pull_request:
branches: [develop, main, "ohos/**"]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
inputs:
publish:
description: Publish this non-main branch and dispatch downstream builds
required: false
default: false
type: boolean
permissions:
contents: read
pull-requests: read
env:
CARGO_TERM_COLOR: always
defaults:
run:
shell: bash
jobs:
ohos:
name: ohos
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Rust
uses: ./.github/actions/prepare-build
with:
target: aarch64-unknown-linux-ohos
gui: false
pnpm: false
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up HarmonyOS
uses: ErBWs/setup-ohos@v1
- name: Install ohrs
uses: taiki-e/install-action@v2
with:
tool: ohrs
- name: Build HAR
id: package
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
sudo apt-get install -qqy \
pkg-config curl libgl1-mesa-dev expect llvm clang lldb lld
rustup component add rustfmt
cargo fmt --all --manifest-path \
easytier-contrib/easytier-ohrs/Cargo.toml -- --check
cargo_version=$(cargo metadata --format-version 1 --no-deps \
--manifest-path easytier/Cargo.toml | jq -r '.packages[0].version')
last_tag=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || true)
if [ -n "$last_tag" ]; then
base_version=$(printf '%s\n' "$cargo_version" "${last_tag#v}" \
| sort -V | tail -n 1)
commit_count=$(git rev-list --count "$last_tag..HEAD")
else
base_version=$cargo_version
commit_count=0
fi
source_branch=${GITHUB_HEAD_REF:-}
if [ -z "$source_branch" ]; then
if [ "$GITHUB_REF_TYPE" = branch ]; then
source_branch=$GITHUB_REF_NAME
else
source_branch=${DEFAULT_BRANCH:-main}
fi
fi
branch_id=$(printf '%s' "$source_branch" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//' \
| cut -c1-64)
branch_id=${branch_id:-main}
package_name=easytier-ohrs
package_version="${base_version}-${branch_id}-${commit_count}-${GITHUB_RUN_NUMBER}-${GITHUB_RUN_ATTEMPT}-g$(git rev-parse --short=8 HEAD)"
echo "name=$package_name" >> "$GITHUB_OUTPUT"
echo "EASYTIER_PACKAGE_NAME=$package_name" >> "$GITHUB_ENV"
echo "EASYTIER_VERSION=$package_version" >> "$GITHUB_ENV"
package_dir=easytier-contrib/easytier-ohrs/package
jq --arg name "$package_name" --arg version "$package_version" \
'.name = $name | .version = $version' \
"$package_dir/oh-package.json5" > "$package_dir/oh-package.tmp.json5"
mv "$package_dir/oh-package.tmp.json5" "$package_dir/oh-package.json5"
{
echo "## $package_name $package_version"
echo
echo "- Core version: $base_version"
echo "- Core commit: $GITHUB_SHA"
git log -1 --pretty=format:'- %s'
echo
} > "$package_dir/CHANGELOG.md"
sudo mkdir -p "$OHOS_NDK_HOME/native/llvm"
sudo tee "$OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh" >/dev/null <<'EOF'
#!/bin/sh
exec "$OHOS_NDK_HOME/native/llvm/bin/clang" \
-target aarch64-linux-ohos \
--sysroot="$OHOS_NDK_HOME/native/sysroot" \
-D__MUSL__ "$@"
EOF
sudo chmod +x \
"$OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
cd easytier-contrib/easytier-ohrs
source env.sh
ohrs build --release --arch aarch
ohrs artifact
mv package.har "$package_name.har"
- name: Upload HAR
uses: actions/upload-artifact@v5
with:
name: ${{ steps.package.outputs.name }}
path: easytier-contrib/easytier-ohrs/${{ steps.package.outputs.name }}.har
retention-days: 5
if-no-files-found: error
- name: Publish and dispatch
if: >-
(github.event_name == 'push' &&
github.ref_type == 'branch' &&
github.ref_name == 'main' &&
github.event.forced != true) ||
(github.event_name == 'workflow_dispatch' &&
github.ref_type == 'branch' &&
(github.ref_name == 'main' || inputs.publish))
working-directory: easytier-contrib/easytier-ohrs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CODEARTS_PRIVATE_OHPM: ${{ secrets.CODEARTS_PRIVATE_OHPM }}
DOWNSTREAM_DISPATCH_TOKEN: ${{ secrets.DOWNSTREAM_DISPATCH_TOKEN }}
run: |
set -euo pipefail
if [ "$GITHUB_EVENT_NAME" = push ]; then
pull_requests=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls")
if ! jq -e \
--arg repository "$GITHUB_REPOSITORY" \
--arg branch "$GITHUB_REF_NAME" \
--arg sha "$GITHUB_SHA" \
'any(.[];
.merged_at != null and
.base.repo.full_name == $repository and
.base.ref == $branch and
.merge_commit_sha == $sha)' \
<<< "$pull_requests" >/dev/null; then
echo "Direct push: HAR built without publishing."
exit 0
fi
fi
mkdir -p "$HOME/.ohpm"
umask 077
printf '%s' "$CODEARTS_PRIVATE_OHPM" > "$HOME/.ohpm/.ohpmrc"
trap 'rm -f "$HOME/.ohpm/.ohpmrc"' EXIT
ohpm publish "$EASYTIER_PACKAGE_NAME.har"
payload=$(jq -nc \
--arg repository "$GITHUB_REPOSITORY" \
--arg ref "refs/heads/$GITHUB_REF_NAME" \
--arg package "$EASYTIER_PACKAGE_NAME" \
'{
event_type: "core-har-published",
client_payload: {
core_repository: $repository,
core_ref: $ref,
package_name: $package
}
}')
for repository in \
FrankHan052176/EasyTier-ArkTS \
FrankHan052176/easytier-pro-app; do
curl --fail-with-body --silent --show-error \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $DOWNSTREAM_DISPATCH_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$GITHUB_API_URL/repos/$repository/dispatches" \
--data "$payload"
done
+9 -13
View File
@@ -6,22 +6,19 @@ on:
core_run_id:
description: 'The run id of EasyTier-Core Action in EasyTier repo'
type: number
default: 10322498549
required: true
gui_run_id:
description: 'The run id of EasyTier-GUI Action in EasyTier repo'
type: number
default: 10322498557
required: true
mobile_run_id:
description: 'The run id of EasyTier-Mobile Action in EasyTier repo'
type: number
default: 10322498555
required: true
version:
description: 'Version for this release'
type: string
default: 'v2.3.1'
default: 'v2.6.4'
required: true
make_latest:
description: 'Mark this release as latest'
@@ -34,35 +31,34 @@ permissions:
jobs:
release:
if: contains('["KKRainbow"]', github.actor)
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Download Core Artifact
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.core_run_id }}
repo: EasyTier/EasyTier
repo: ${{ github.repository }}
path: release_assets
- name: Download GUI Artifact
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.gui_run_id }}
repo: EasyTier/EasyTier
repo: ${{ github.repository }}
path: release_assets_nozip
- name: Download Mobile Artifact
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.mobile_run_id }}
repo: EasyTier/EasyTier
repo: ${{ github.repository }}
path: release_assets_nozip
- name: Zip release assets
@@ -96,4 +92,4 @@ jobs:
files: |
./zipped_assets/*
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ inputs.version }}
tag_name: ${{ inputs.version }}
+125 -50
View File
@@ -2,12 +2,18 @@ name: EasyTier Test
on:
push:
branches: ["develop", "main"]
branches: [ "develop", "main" ]
pull_request:
branches: ["develop", "main"]
branches: [ "develop", "main" ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
# RUSTC_WRAPPER: "sccache"
# SCCACHE_GHA_ENABLED: "true"
defaults:
run:
@@ -28,69 +34,138 @@ jobs:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
test:
runs-on: ubuntu-22.04
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-proto/**", "easytier-web/**", "easytier-gui/src-tauri/**", "easytier-contrib/**", ".github/workflows/test.yml", ".github/actions/**"]'
- name: Setup protoc
uses: arduino/setup-protoc@v2
check:
name: Run linters & check
runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v5
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
gui: true
pnpm: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt,clippy
rustflags: ''
- uses: taiki-e/install-action@cargo-hack
- name: Check formatting
if: ${{ !cancelled() }}
run: cargo fmt --all -- --check
- name: Check Clippy
if: ${{ !cancelled() }}
run: cargo clippy --all-targets --features full --all -- -D warnings
- name: Check features
if: ${{ !cancelled() }}
run: cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose
- name: Check Cargo.lock is up to date
if: ${{ !cancelled() }}
run: |
if ! cargo metadata --format-version 1 --locked > /dev/null; then
echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock."
exit 1
fi
pre-test:
name: Build test
runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v5
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
gui: true
pnpm: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@nextest
- name: Archive test
run: >-
cargo nextest archive --archive-file tests.tar.zst
--package easytier --package easytier-core --features full
- uses: actions/upload-artifact@v5
with:
name: tests
path: tests.tar.zst
retention-days: 1
test_matrix:
name: Test (${{ matrix.name }})
runs-on: ubuntu-latest
needs: [ pre_job, pre-test ]
if: needs.pre_job.outputs.should_skip != 'true'
strategy:
fail-fast: false
matrix:
include:
- name: "easytier"
opts: "-E 'not test(tests::three_node)' --test-threads 1 --no-fail-fast"
- name: "three_node"
opts: "-E 'test(tests::three_node) and not test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
- name: "three_node::subnet_proxy_three_node_test"
opts: "-E 'test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
steps:
- uses: actions/checkout@v5
- name: Setup tools for test
run: sudo apt install bridge-utils
- name: Setup upnpd for test
run: |
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y miniupnpd miniupnpd-iptables iptables
- name: Setup system for test
run: |
sudo modprobe br_netfilter
sudo modprobe tun
if [ ! -e /dev/net/tun ]; then
sudo mkdir -p /dev/net
sudo mknod /dev/net/tun c 10 200
fi
sudo sysctl net.bridge.bridge-nf-call-iptables=0
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
sudo ip addr add 2001:db8::2/64 dev lo
- uses: actions/setup-node@v4
- uses: taiki-e/install-action@nextest
- name: Download tests
uses: actions/download-artifact@v4
with:
node-version: 21
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
pnpm -r install
pnpm -r --filter "./easytier-web/*" build
- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
name: tests
- name: Run tests
run: |
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1 --nocapture
sudo chown -R $USER:$USER ./target
sudo chown -R $USER:$USER ~/.cargo
sudo prlimit --pid $$ --nofile=1048576:1048576
sudo -E env "PATH=$PATH" EASYTIER_LINUX_BPF_INTEGRATION=required \
cargo nextest run --archive-file tests.tar.zst ${{ matrix.opts }}
test:
runs-on: ubuntu-latest
needs: [ pre_job, check, test_matrix ]
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
steps:
- name: Mark result as failed
if: contains(needs.*.result, 'failure')
run: exit 1
+12
View File
@@ -12,6 +12,7 @@ target-*/
.vscode
/.idea
/.direnv/
# perf & flamegraph
perf.data
@@ -33,7 +34,18 @@ easytier-panic.log
# web
node_modules
easytier-web/frontend-lib/src/generated/
easytier-web/config-generator/dist/
easytier-web/config-generator/src/generated/
.vite
easytier-gui/src-tauri/*.dll
easytier-gui/src-tauri/*.sys
/easytier-contrib/easytier-ohrs/dist/
.direnv
.flake-profile
# contrib
go.sum
View File
+30
View File
@@ -0,0 +1,30 @@
# EasyTier Domain Context
## Module layers
`easytier-core` layers dependencies from `foundation` upward through the
portable networking domains. `foundation` contains infrastructure Modules
that have no dependency on a networking domain and may be used by any higher
layer.
## Operation broker
An operation broker owns the lifecycle of asynchronous work submitted by an
external caller to core. It allocates opaque operation IDs, arbitrates
completion, cancellation, and disposal, retains terminal outcomes, and
publishes a batch-drainable completion queue.
The broker does not interpret operation kinds, outcomes, resources, wire
formats, or domain errors. Each domain Module owns those semantics and composes
the broker under the same lock as any state that must change atomically with an
operation transition.
Host capability operations use a separate seam. They turn Host readiness into
Rust task wakeups and do not share the caller-to-core broker state machine.
## Compact compatibility Host
A compact compatibility Host retains accepted values in the authoritative TOML
model for management readback, while the shared host-aware normalization path
omits capabilities that the compact runtime cannot execute. Omitted settings
are silent no-ops and must not be advertised as live network capabilities.
+236
View File
@@ -0,0 +1,236 @@
# Contributing to EasyTier
[中文版](CONTRIBUTING_zh.md)
Thank you for your interest in contributing to EasyTier! This document provides guidelines and instructions for contributing to the project.
## Table of Contents
- [Development Environment Setup](#development-environment-setup)
- [Prerequisites](#prerequisites)
- [Installation Steps](#installation-steps)
- [Project Structure](#project-structure)
- [Build Guide](#build-guide)
- [Building Core](#building-core)
- [Building GUI](#building-gui)
- [Building Mobile](#building-mobile)
- [Development Workflow](#development-workflow)
- [Testing Guidelines](#testing-guidelines)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Additional Resources](#additional-resources)
## Development Environment Setup
### Prerequisites
#### Required Tools
- Node.js v21 or higher
- pnpm v9 or higher
- Rust toolchain (version 1.95)
- LLVM and Clang
- Protoc (Protocol Buffers compiler)
#### Platform-Specific Dependencies
**Linux (Ubuntu/Debian)**
```bash
# Core build dependencies
sudo apt-get update && sudo apt-get install -y \
musl-tools \
llvm \
clang \
protobuf-compiler
# GUI build dependencies
sudo apt install -y \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
libappindicator3-dev \
patchelf
# Testing dependencies
sudo apt install -y bridge-utils
```
**For Cross-Compilation**
- musl-cross toolchain (for MIPS and other architectures)
- Additional setup may be required (see `.github/workflows/` for details)
**For Android Development**
- Java 20
- Android SDK (Build Tools 34.0.0)
- Android NDK (26.0.10792818)
### Installation Steps
1. Clone the repository:
```bash
git clone https://github.com/EasyTier/EasyTier.git
cd EasyTier
```
2. Install dependencies:
```bash
# Install Rust toolchain
rustup install 1.95
rustup default 1.95
# Install project dependencies
pnpm -r install
```
## Project Structure
```
easytier/ # Core functionality and libraries
easytier-web/ # Web dashboard and frontend
easytier-gui/ # Desktop GUI application
.github/workflows/ # CI/CD configuration files
```
## Build Guide
### Building Core
```bash
# Standard build
cargo build --release
# Platform-specific builds
cargo build --release --target x86_64-unknown-linux-musl # Linux x86_64
cargo build --release --target aarch64-unknown-linux-musl # Linux ARM64
cargo build --release --target x86_64-apple-darwin # macOS x86_64
cargo build --release --target aarch64-apple-darwin # macOS M1/M2
cargo build --release --target x86_64-pc-windows-msvc # Windows x86_64
```
Build artifacts: `target/[target-triple]/release/`
### Building the WASI core
```bash
script/build-wasi-core.sh
```
This builds the `easytier-core` Go-host profile for `wasm32-wasip1`, then
optimizes it with the pinned official Binaryen release. Binaryen is downloaded
once into `target/binaryen/` and verified by SHA-256; set `WASM_OPT` to use an
existing matching binary.
### Building GUI
```bash
# 1. Build frontend
pnpm -r build
# 2. Build GUI application
cd easytier-gui
# Linux
pnpm tauri build --target x86_64-unknown-linux-gnu
# macOS
pnpm tauri build --target x86_64-apple-darwin # Intel
pnpm tauri build --target aarch64-apple-darwin # Apple Silicon
# Windows
pnpm tauri build --target x86_64-pc-windows-msvc # x64
```
Build artifacts: `easytier-gui/src-tauri/target/release/bundle/`
### Building Mobile
```bash
# 1. Install Android targets
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add i686-linux-android
rustup target add x86_64-linux-android
# 2. Build Android application
cd easytier-gui
pnpm tauri android build
```
Build artifacts: `easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/`
### Build Notes
1. Cross-compilation for ARM/MIPS requires additional setup
2. Windows builds need correct DLL files
3. Check `.github/workflows/` for detailed build configurations
## Development Workflow
1. Create a feature branch from `develop`:
```bash
git checkout develop
git checkout -b feature/your-feature-name
```
2. Make your changes following our coding standards
3. Write or update tests as needed
4. Use conventional commit messages:
```
feat: add new feature
fix: resolve bug
docs: update documentation
test: add tests
chore: update dependencies
```
5. Submit a pull request to `develop`
## Testing Guidelines
### Running Tests
```bash
# Configure system (Linux)
sudo modprobe br_netfilter
sudo sysctl net.bridge.bridge-nf-call-iptables=0
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
# Run tests
cargo test --no-default-features --features=full --verbose
```
### Test Requirements
- Write tests for new features
- Maintain existing test coverage
- Tests should be isolated and repeatable
- Include both unit and integration tests
## Pull Request Guidelines
1. Target the `develop` branch
2. Ensure all tests pass
3. Include clear description and purpose
4. Reference related issues
5. Keep changes focused and atomic
6. Update documentation as needed
## Additional Resources
- [Issue Tracker](https://github.com/EasyTier/EasyTier/issues)
- [Project Documentation](https://github.com/EasyTier/EasyTier/wiki)
## Questions or Need Help?
Feel free to:
- Open an issue for questions
- Join our community discussions
- Reach out to maintainers
Thank you for contributing to EasyTier!
+233
View File
@@ -0,0 +1,233 @@
# EasyTier 贡献指南
[English Version](CONTRIBUTING.md)
感谢您对 EasyTier 项目的关注!本文档提供了参与项目贡献的指南和说明。
## 目录
- [EasyTier 贡献指南](#easytier-贡献指南)
- [目录](#目录)
- [开发环境配置](#开发环境配置)
- [前置要求](#前置要求)
- [必需工具](#必需工具)
- [平台特定依赖](#平台特定依赖)
- [安装步骤](#安装步骤)
- [项目结构](#项目结构)
- [构建指南](#构建指南)
- [构建核心组件](#构建核心组件)
- [构建桌面应用](#构建桌面应用)
- [构建移动应用](#构建移动应用)
- [构建注意事项](#构建注意事项)
- [开发工作流](#开发工作流)
- [测试指南](#测试指南)
- [运行测试](#运行测试)
- [测试要求](#测试要求)
- [Pull Request 规范](#pull-request-规范)
- [其他资源](#其他资源)
- [需要帮助?](#需要帮助)
## 开发环境配置
### 前置要求
#### 必需工具
- Node.js v21 或更高版本
- pnpm v9 或更高版本
- Rust 工具链(版本 1.95
- LLVM 和 Clang
- ProtocProtocol Buffers 编译器)
#### 平台特定依赖
**Linux (Ubuntu/Debian)**
```bash
# 核心构建依赖
sudo apt-get update && sudo apt-get install -y \
musl-tools \
llvm \
clang \
protobuf-compiler
# GUI 构建依赖
sudo apt install -y \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
libappindicator3-dev \
patchelf
# 测试依赖
sudo apt install -y bridge-utils
```
**交叉编译依赖**
- musl-cross 工具链(用于 MIPS 和其他架构)
- 可能需要额外配置(详见 `.github/workflows/` 目录)
**Android 开发依赖**
- Java 20
- Android SDKBuild Tools 34.0.0
- Android NDK26.0.10792818
### 安装步骤
1. 克隆仓库:
```bash
git clone https://github.com/EasyTier/EasyTier.git
cd EasyTier
```
2. 安装依赖:
```bash
# 安装 Rust 工具链
rustup install 1.95
rustup default 1.95
# 安装项目依赖
pnpm -r install
```
## 项目结构
```
easytier/ # 核心功能和库
easytier-web/ # Web 仪表盘和前端
easytier-gui/ # 桌面 GUI 应用
.github/workflows/ # CI/CD 配置文件
```
## 构建指南
### 构建核心组件
```bash
# 标准构建
cargo build --release
# 特定平台构建
cargo build --release --target x86_64-unknown-linux-musl # Linux x86_64
cargo build --release --target aarch64-unknown-linux-musl # Linux ARM64
cargo build --release --target x86_64-apple-darwin # macOS x86_64
cargo build --release --target aarch64-apple-darwin # macOS M1/M2
cargo build --release --target x86_64-pc-windows-msvc # Windows x86_64
```
构建产物位置:`target/[target-triple]/release/`
### 构建桌面应用
```bash
# 1. 构建前端
pnpm -r build
# 2. 构建 GUI 应用
cd easytier-gui
# Linux
pnpm tauri build --target x86_64-unknown-linux-gnu
# macOS
pnpm tauri build --target x86_64-apple-darwin # Intel
pnpm tauri build --target aarch64-apple-darwin # Apple Silicon
# Windows
pnpm tauri build --target x86_64-pc-windows-msvc # x64
```
构建产物位置:`easytier-gui/src-tauri/target/release/bundle/`
### 构建移动应用
```bash
# 1. 安装 Android 目标平台
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add i686-linux-android
rustup target add x86_64-linux-android
# 2. 构建 Android 应用
cd easytier-gui
pnpm tauri android build
```
构建产物位置:`easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/`
### 构建注意事项
1. ARM/MIPS 的交叉编译需要额外配置
2. Windows 构建需要正确的 DLL 文件
3. 详细构建配置请参考 `.github/workflows/` 目录
## 开发工作流
1. 从 `develop` 分支创建特性分支:
```bash
git checkout develop
git checkout -b feature/your-feature-name
```
2. 按照代码规范进行修改
3. 编写或更新测试
4. 使用规范的提交信息:
```
feat: 添加新功能
fix: 修复问题
docs: 更新文档
test: 添加测试
chore: 更新依赖
```
5. 提交 Pull Request 到 `develop` 分支
## 测试指南
### 运行测试
```bash
# 配置系统(Linux
sudo modprobe br_netfilter
sudo sysctl net.bridge.bridge-nf-call-iptables=0
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
# 运行测试
cargo test --no-default-features --features=full --verbose
```
### 测试要求
- 为新功能编写测试
- 维护现有测试覆盖率
- 测试应该是独立且可重复的
- 包含单元测试和集成测试
## Pull Request 规范
1. 目标分支为 `develop`
2. 确保所有测试通过
3. 包含清晰的描述和目的
4. 关联相关的 issues
5. 保持变更的原子性和聚焦性
6. 及时更新相关文档
## 其他资源
- [问题追踪](https://github.com/EasyTier/EasyTier/issues)
- [项目文档](https://github.com/EasyTier/EasyTier/wiki)
## 需要帮助?
欢迎:
- 提出问题
- 参与社区讨论
- 联系维护者
感谢您为 EasyTier 做出贡献!
Generated
+3740 -1594
View File
File diff suppressed because it is too large Load Diff
+18 -1
View File
@@ -1,16 +1,28 @@
[workspace]
resolver = "2"
members = [
"easytier-core",
"easytier-proto",
"easytier",
"easytier-gui/src-tauri",
"easytier-rpc-build",
"easytier-web",
"easytier-contrib/easytier-mini",
"easytier-contrib/easytier-ffi",
"easytier-contrib/easytier-uptime",
"easytier-contrib/easytier-android-jni",
]
default-members = ["easytier", "easytier-web"]
exclude = [
"easytier-contrib/easytier-ohrs", # it needs ohrs sdk
]
[workspace.package]
edition = "2024"
rust-version = "1.95"
[profile.dev]
panic = "unwind"
debug = 2
[profile.release]
panic = "abort"
@@ -18,3 +30,8 @@ lto = true
codegen-units = 1
opt-level = 3
strip = true
[profile.mini]
inherits = "release"
opt-level = "z"
strip = "symbols"
+29 -2
View File
@@ -3,13 +3,33 @@
{
"path": "."
},
{
"name": "core",
"path": "easytier"
},
{
"name": "gui",
"path": "easytier-gui"
},
{
"name": "core",
"path": "easytier"
"name": "web",
"path": "easytier-web"
},
{
"name": "ffi",
"path": "easytier-contrib/easytier-ffi"
},
{
"name": "magisk",
"path": "easytier-contrib/easytier-magisk"
},
{
"name": "openharmony",
"path": "easytier-contrib/easytier-ohrs"
},
{
"name": "uptime",
"path": "easytier-contrib/easytier-uptime"
},
{
"name": "vpnservice",
@@ -26,5 +46,12 @@
"i18n-ally.sortKeys": true,
// Disable the default formatter
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modifications",
"editor.formatOnPaste": false,
"editor.formatOnType": true,
"[nix]": {
"editor.formatOnSave": false,
},
}
}
+133 -41
View File
@@ -1,73 +1,165 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
0. Additional Definitions.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
1. Exception to Section 3 of the GNU GPL.
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
2. Conveying Modified Versions.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
3. Object Code Incorporating Material from Library Header Files.
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
4. Combined Works.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
END OF TERMS AND CONDITIONS
d) Do one of the following:
APPENDIX: How to apply the Apache License to your work.
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
Copyright 2023 sunsijie
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
5. Combined Libraries.
http://www.apache.org/licenses/LICENSE-2.0
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+235 -269
View File
@@ -11,263 +11,237 @@
[简体中文](/README_CN.md) | [English](/README.md)
**Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.**
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
> ✨ A simple, secure, decentralized virtual private network solution powered by Rust and Tokio
<p align="center">
<img src="assets/image-5.png" width="300">
<img src="assets/image-4.png" width="300">
<img src="assets/config-page.png" width="300" alt="config page">
<img src="assets/running-page.png" width="300" alt="running page">
</p>
📚 **[Full Documentation](https://easytier.cn/en/)** | 🖥️ **[Web Console](https://easytier.cn/web)** | 📝 **[Download Releases](https://github.com/EasyTier/EasyTier/releases)** | 🧩 **[Third Party Tools](https://easytier.cn/en/guide/installation_gui.html#third-party-graphical-interfaces)** | ❤️ **[Sponsor](#sponsor)**
## Features
- **Decentralized**: No need to rely on centralized services, nodes are equal and independent.
- **Safe**: Use WireGuard protocol to encrypt data.
- **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software.
- **Cross-platform**: Supports MacOS/Linux/Windows/Android, will support IOS in the future. The executable file is statically linked, making deployment simple.
- **Networking without public IP**: Supports networking using shared public nodes, refer to [Configuration Guide](#Networking-without-public-IP)
- **NAT traversal**: Supports UDP-based NAT traversal, able to establish stable connections even in complex network environments.
- **Subnet Proxy (Point-to-Network)**: Nodes can expose accessible network segments as proxies to the VPN subnet, allowing other nodes to access these subnets through the node.
- **Smart Routing**: Selects links based on traffic to reduce latency and increase throughput.
- **TCP Support**: Provides reliable data transmission through concurrent TCP links when UDP is limited, optimizing performance.
- **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected.
- **IPv6 Support**: Supports networking using IPv6.
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
- **Web Management Interface**: Provides a [web-based management](https://easytier.cn/web) interface for easy configuration and monitoring.
### Core Features
## Installation
- 🔒 **Decentralized**: Nodes are equal and independent, no centralized services required
- 🚀 **Easy to Use**: Multiple operation methods via web, client, and command line
- 🌍 **Cross-Platform**: Supports Win/MacOS/Linux/FreeBSD/Android and X86/ARM/MIPS architectures
- 🔐 **Secure**: AES-GCM or WireGuard encryption, prevents man-in-the-middle attacks
1. **Download the precompiled binary file**
### Advanced Capabilities
Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
- 🔌 **Efficient NAT Traversal**: Supports UDP and IPv6 traversal, works with NAT4-NAT4 networks
- 🌐 **Subnet Proxy**: Nodes can share subnets for other nodes to access
- 🔄 **Intelligent Routing**: Latency priority and automatic route selection for best network experience
-**High Performance**: Zero-copy throughout the entire link, supports TCP/UDP/WSS/WG protocols
2. **Install via crates.io**
### Network Optimization
```sh
cargo install easytier
```
3. **Install from source code**
```sh
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
```
4. **Install by Docker Compose**
Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.
5. **Install by script (For Linux Only)**
```sh
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
```
The script supports the following commands and options:
Commands:
- `install`: Install EasyTier
- `uninstall`: Uninstall EasyTier
- `update`: Update EasyTier to the latest version
- `help`: Show help message
Options:
- `--skip-folder-verify`: Skip folder verification during installation
- `--skip-folder-fix`: Skip automatic folder path fixing
- `--no-gh-proxy`: Disable GitHub proxy
- `--gh-proxy`: Set custom GitHub proxy URL (default: https://ghfast.top/)
Examples:
```sh
# Show help
bash /tmp/easytier.sh help
# Install with options
bash /tmp/easytier.sh install --skip-folder-verify
bash /tmp/easytier.sh install --no-gh-proxy
bash /tmp/easytier.sh install --gh-proxy https://your-proxy.com/
# Update EasyTier
bash /tmp/easytier.sh update
# Uninstall EasyTier
bash /tmp/easytier.sh uninstall
```
6. **Install by Homebrew (For MacOS Only)**
```sh
brew tap brewforge/chinese
brew install --cask easytier
```
- 📊 **UDP Loss Resistance**: KCP/QUIC proxy optimizes latency and bandwidth in high packet loss environments
- 🔧 **Web Management**: Easy configuration and monitoring through web interface
- 🛠️ **Zero Config**: Simple deployment with statically linked executables
## Quick Start
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
### 📥 Installation
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
Choose the installation method that best suits your needs:
### Two-node Networking
Linux (Recommended):
```bash
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
```
Assuming the network topology of the two nodes is as follows
Homebrew (MacOS/Linux):
```bash
brew tap brewforge/chinese
brew install --cask easytier-gui
```
Windows (Recommended, run with administrator privileges):
```powershell
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
```
Install via cargo (Latest development version):
```bash
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
```
[Install pre-built binary](https://github.com/EasyTier/EasyTier/releases) (Recommended, All platforms supported)
[Install via Docker](https://easytier.cn/en/guide/installation.html#installation-methods)
[Install OpenWrt ipk package](https://github.com/EasyTier/luci-app-easytier)
Additional steps:
[One-Click Register Service](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) (Automatically start when the system boots and run in the background)
### 🚀 Basic Usage
#### Quick Networking with Shared Nodes
EasyTier supports quick networking using shared public nodes. When you don't have a public IP, you can use the free shared nodes provided by the EasyTier community. Nodes will automatically attempt NAT traversal and establish P2P connections. When P2P fails, data will be relayed through shared nodes.
When using shared nodes, each node entering the network needs to provide the same `--network-name` and `--network-secret` parameters as the unique identifier of the network.
Taking two nodes as an example (Please use more complex network name to avoid conflicts):
1. Run on Node A:
```bash
# Run with administrator privileges
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
```
2. Run on Node B:
```bash
# Run with administrator privileges
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
```
After successful execution, you can check the network status using `easytier-cli`:
```text
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.2-70e69a38~ |
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.2-70e69a38~ |
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
```
You can test connectivity between nodes:
```bash
# Test connectivity
ping 10.126.126.1
ping 10.126.126.2
```
Note: If you cannot ping through, it may be that the firewall is blocking incoming traffic. Please turn off the firewall or add allow rules.
To improve availability, you can connect to multiple shared nodes simultaneously:
```bash
# Connect to multiple shared nodes
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP1>:11010 -p udp://<SharedNodeIP2>:11010
```
Once your network is set up successfully, you can easily configure it to start automatically on system boot. Refer to the [One-Click Register Service guide](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) for step-by-step instructions on registering EasyTier as a system service.
#### Decentralized Networking
EasyTier is fundamentally decentralized, with no distinction between server and client. As long as one device can communicate with any node in the virtual network, it can join the virtual network. Here's how to set up a decentralized network:
1. Start First Node (Node A):
```bash
# Start the first node
sudo easytier-core -i 10.144.144.1
```
After startup, this node will listen on the following ports by default:
- TCP: 11010
- UDP: 11010
- WebSocket: 11011
- WebSocket SSL: 11012
- WireGuard: 11013
2. Connect Second Node (Node B):
```bash
# Connect to the first node using its public IP
sudo easytier-core -i 10.144.144.2 -p udp://FIRST_NODE_PUBLIC_IP:11010
```
3. Verify Connection:
```bash
# Test connectivity
ping 10.144.144.2
# View connected peers
easytier-cli peer
# View routing information
easytier-cli route
# View local node information
easytier-cli node
```
For more nodes to join the network, they can connect to any existing node in the network using the `-p` parameter:
```bash
# Connect to any existing node using its public IP
sudo easytier-core -i 10.144.144.3 -p udp://ANY_EXISTING_NODE_PUBLIC_IP:11010
```
### 🔍 Advanced Features
#### Subnet Proxy
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes:
```mermaid
flowchart LR
subgraph Node A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
subgraph Node A Public IP 22.1.1.1
nodea[EasyTier<br/>10.144.144.1]
end
subgraph Node B
nodeb[EasyTier\n10.144.144.2]
end
nodea <-----> nodeb
```
1. Execute on Node A:
```sh
sudo easytier-core --ipv4 10.144.144.1
```
Successful execution of the command will print the following.
![alt text](/assets/image-2.png)
2. Execute on Node B
```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
```
3. Test Connectivity
The two nodes should connect successfully and be able to communicate within the virtual subnet
```sh
ping 10.144.144.2
```
Use easytier-cli to view node information in the subnet
```sh
easytier-cli peer
```
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](/assets/image-1.png)
```sh
easytier-cli node
```
![alt text](assets/image-10.png)
---
### Multi-node Networking
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
```
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
---
### Subnet Proxy (Point-to-Network) Configuration
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
```mermaid
flowchart LR
subgraph Node A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
end
subgraph Node B
nodeb[EasyTier\n10.144.144.2]
nodeb[EasyTier<br/>10.144.144.2]
end
id1[[10.1.1.0/24]]
nodea <--> nodeb <-.-> id1
```
Then the startup parameters for Node B's easytier are (new -n parameter)
To share a subnet, add the `-n` parameter when starting EasyTier:
```sh
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
```bash
# Share subnet 10.1.1.0/24 with other nodes
sudo easytier-core -i 10.144.144.2 -n 10.1.1.0/24
```
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. Node A can check whether the subnet proxy is effective through the following command.
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. You can verify the subnet proxy setup:
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
1. Check if the routing information has been synchronized (the proxy_cidrs column shows the proxied subnets):
```sh
easytier-cli route
```
![alt text](/assets/image-3.png)
2. Test whether Node A can access nodes under the proxied subnet
```sh
ping 10.1.1.2
```
---
### Networking without Public IP
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.cn:11010``.
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
Taking two nodes as an example, Node A executes:
```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
```bash
# View routing information
easytier-cli route
```
Node B executes
![Routing Information](/assets/image-3.png)
```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
2. Test if you can access nodes in the proxied subnet:
```bash
# Test connectivity to proxied subnet
ping 10.1.1.2
```
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
#### WireGuard Integration
### Use EasyTier with WireGuard Client
EasyTier can be used as a WireGuard server to allow any device with WireGuard client installed to access the EasyTier network. For platforms currently unsupported by EasyTier (such as iOS, Android, etc.), this method can be used to connect to the EasyTier network.
Assuming the network topology is as follows:
EasyTier can act as a WireGuard server, allowing any device with a WireGuard client (including iOS and Android) to access the EasyTier network. Here's an example setup:
```mermaid
flowchart LR
ios[[iPhone \n WireGuard Installed]]
ios[[iPhone<br/>WireGuard Installed]]
subgraph Node A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
subgraph Node A Public IP 22.1.1.1
nodea[EasyTier<br/>10.144.144.1]
end
subgraph Node B
nodeb[EasyTier\n10.144.144.2]
nodeb[EasyTier<br/>10.144.144.2]
end
id1[[10.1.1.0/24]]
@@ -275,86 +249,78 @@ id1[[10.1.1.0/24]]
ios <-.-> nodea <--> nodeb <-.-> id1
```
To enable an iPhone to access the EasyTier network through Node A, the following configuration can be applied:
1. Start EasyTier with WireGuard portal enabled:
Include the --vpn-portal parameter in the easytier-core command on Node A to specify the port that the WireGuard service listens on and the subnet used by the WireGuard network.
```sh
# The following parameters mean: listen on port 0.0.0.0:11013, and use the 10.14.14.0/24 subnet for WireGuard
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
```bash
# Listen on 0.0.0.0:11013 and use 10.14.14.0/24 subnet for WireGuard clients
sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
```
After successfully starting easytier-core, use easytier-cli to obtain the WireGuard client configuration.
2. Get WireGuard client configuration:
```sh
$> easytier-cli vpn-portal
portal_name: wireguard
############### client_config_start ###############
[Interface]
PrivateKey = 9VDvlaIC9XHUvRuE06hD2CEDrtGF+0lDthgr9SZfIho=
Address = 10.14.14.0/32 # should assign an ip from this cidr manually
[Peer]
PublicKey = zhrZQg4QdPZs8CajT3r4fmzcNsWpBL9ImQCUsnlXyGM=
AllowedIPs = 10.144.144.0/24,10.14.14.0/24
Endpoint = 0.0.0.0:11013 # should be the public ip(or domain) of the vpn server
PersistentKeepalive = 25
############### client_config_end ###############
connected_clients:
[]
```bash
# Get WireGuard client configuration
easytier-cli vpn-portal
```
Before using the Client Config, you need to modify the Interface Address and Peer Endpoint to the client's IP and the IP of the EasyTier node, respectively. Import the configuration file into the WireGuard client to access the EasyTier network.
3. In the output configuration:
- Set `Interface.Address` to an available IP from the WireGuard subnet
- Set `Peer.Endpoint` to the public IP/domain of your EasyTier node
- Import the modified configuration into your WireGuard client
### Self-Hosted Public Server
#### Self-Hosted Public Shared Node
Every virtual network (with same network name and secret) can act as a public server cluster. Nodes of other network can connect to arbitrary nodes in public server cluster to discover each other without public IP.
You can run your own public shared node to help other nodes discover each other. A public shared node is just a regular EasyTier network (with same network name and secret) that other networks can connect to.
Run you own public server cluster is exactly same as running an virtual network, except that you can skip config the ipv4 addr.
To run a public shared node:
You can also join the official public server cluster with following command:
```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
```bash
# No need to specify IPv4 address for public shared nodes
sudo easytier-core --network-name mysharednode --network-secret mysharednode
```
### Configurations
You can use ``easytier-core --help`` to view all configuration items
## Roadmap
- [ ] Support features such TCP hole punching, KCP, FEC etc.
- [ ] Support iOS.
## Community and Contribution
We welcome and encourage community contributions! If you want to get involved, please submit a [GitHub PR](https://github.com/EasyTier/EasyTier/pulls). Detailed contribution guidelines can be found in [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md).
## Related Projects and Resources
## Related Projects
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
### Contact Us
- 💬 **[Telegram Group](https://t.me/easytier)**
- 👥 **[QQ Group]**
- No.1 [949700262](https://qm.qq.com/q/wFoTUChqZW)
- No.2 [837676408](https://qm.qq.com/q/4V33DrfgHe)
- No.3 [957189589](https://qm.qq.com/q/YNyTQjwlai)
## License
EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
## Contact
- Ask questions or report problems: [GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
- Discussion and exchange: [GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
- Telegramhttps://t.me/easytier
- QQ Group: 949700262
EasyTier is released under the [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
## Sponsor
<img src="assets/image-8.png" width="300">
<img src="assets/image-9.png" width="300">
CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
<p align="center">
<a href="https://edgeone.ai/?from=github" target="_blank">
<img src="assets/edgeone.png" width="200" alt="EdgeOne Logo">
</a>
</p>
Special thanks to [Langlang Cloud](https://langlangy.cn/?i26c5a5) and [RainCloud](https://www.rainyun.com/NjM0NzQ1_) for sponsoring our public servers.
<p align="center">
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
<img src="assets/langlang.png" width="200">
</a>
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
<img src="assets/raincloud.png" width="200">
</a>
</p>
If you find EasyTier helpful, please consider sponsoring us. Software development and maintenance require a lot of time and effort, and your sponsorship will help us better maintain and improve EasyTier.
<p align="center">
<img src="assets/wechat.png" width="200">
<img src="assets/alipay.png" width="200">
</p>
+236 -271
View File
@@ -1,271 +1,245 @@
# EasyTier
[![Github release](https://img.shields.io/github/v/tag/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/releases)
[![GitHub](https://img.shields.io/github/license/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/commits/main)
[![GitHub issues](https://img.shields.io/github/issues/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/issues)
[![GitHub Core Actions](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
[![GitHub GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
[![GitHub Test Actions](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/EasyTier/EasyTier)
[简体中文](/README_CN.md) | [English](/README.md)
**请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。**
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
> ✨ 一个由 Rust 和 Tokio 驱动的简单、安全、去中心化的异地组网方案
<p align="center">
<img src="assets/image-6.png" width="300">
<img src="assets/image-7.png" width="300">
<img src="assets/config-page.png" width="300" alt="配置页面">
<img src="assets/running-page.png" width="300" alt="运行页面">
</p>
## 特点
📚 **[完整文档](https://easytier.cn)** | 🖥️ **[Web 控制台](https://easytier.cn/web)** | 📝 **[下载发布版本](https://github.com/EasyTier/EasyTier/releases)** | 🧩 **[第三方工具](https://easytier.cn/guide/installation_gui.html#%E7%AC%AC%E4%B8%89%E6%96%B9%E5%9B%BE%E5%BD%A2%E7%95%8C%E9%9D%A2)** | ❤️ **[赞助](#赞助)**
- **去中心化**:无需依赖中心化服务,节点平等且独立。
- **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。
- **高性能**:全链路零拷贝,性能与主流组网软件相当。
- **跨平台**:支持 MacOS/Linux/Windows/Android,未来将支持 IOS。可执行文件静态链接,部署简单。
- **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网)
- **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。
- **子网代理(点对网)**:节点可以将可访问的网段作为代理暴露给 VPN 子网,允许其他节点通过该节点访问这些子网。
- **智能路由**:根据流量智能选择链路,减少延迟,提高吞吐量。
- **TCP 支持**:在 UDP 受限的情况下,通过并发 TCP 链接提供可靠的数据传输,优化性能。
- **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。
- **IPV6 支持**:支持利用 IPV6 组网。
- **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。
- **Web 管理界面**:支持通过 [Web 界面](https://easytier.cn)管理节点。
## 特性
## 安装
### 核心特性
1. **下载预编译的二进制文件**
- 🔒 **去中心化**:节点平等且独立,无需中心化服务
- 🚀 **易于使用**:支持通过网页、客户端和命令行多种操作方式
- 🌍 **跨平台**:支持 Win/MacOS/Linux/FreeBSD/Android 和 X86/ARM/MIPS 架构
- 🔐 **安全**AES-GCM 或 WireGuard 加密,防止中间人攻击
访问 [GitHub Release 页面](https://github.com/EasyTier/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。
### 高级功能
2. **通过 crates.io 安装**
- 🔌 **高效 NAT 穿透**:支持 UDP 和 IPv6 穿透,可在 NAT4-NAT4 网络中工作
- 🌐 **子网代理**:节点可以共享子网供其他节点访问
- 🔄 **智能路由**:延迟优先和自动路由选择,提供最佳网络体验
-**高性能**:整个链路零拷贝,支持 TCP/UDP/WSS/WG 协议
```sh
cargo install easytier
```
### 网络优化
3. **通过源码安装**
```sh
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
```
4. **通过Docker Compose安装**
请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。
5. **使用一键脚本安装 (仅适用于 Linux)**
```sh
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
```
脚本支持以下命令和选项:
命令:
- `install`: 安装 EasyTier
- `uninstall`: 卸载 EasyTier
- `update`: 更新 EasyTier 到最新版本
- `help`: 显示帮助信息
选项:
- `--skip-folder-verify`: 跳过安装过程中的文件夹验证
- `--skip-folder-fix`: 跳过自动修复文件夹路径
- `--no-gh-proxy`: 禁用 GitHub 代理
- `--gh-proxy`: 设置自定义 GitHub 代理 URL (默认值: https://ghfast.top/)
示例:
```sh
# 查看帮助
bash /tmp/easytier.sh help
# 安装(带选项)
bash /tmp/easytier.sh install --skip-folder-verify
bash /tmp/easytier.sh install --no-gh-proxy
bash /tmp/easytier.sh install --gh-proxy https://your-proxy.com/
# 更新 EasyTier
bash /tmp/easytier.sh update
# 卸载 EasyTier
bash /tmp/easytier.sh uninstall
```
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
```sh
brew tap brewforge/chinese
brew install --cask easytier
```
- 📊 **UDP 丢包抗性**:KCP/QUIC 代理在高丢包环境下优化延迟和带宽
- 🔧 **Web 管理**:通过 Web 界面轻松配置和监控
- 🛠️ **零配置**:静态链接的可执行文件,简单部署
## 快速开始
> 下文仅描述命令行工具的使用,图形界面程序可参考下述概念自行配置。
### 📥 安装
确保已按照 [安装指南](#安装) 安装 EasyTier,并且 easytier-core 和 easytier-cli 两个命令都已经可用。
选择最适合您需求的安装方式:
### 双节点组网
Linux(推荐):
```bash
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
```
假设双节点的网络拓扑如下
HomebrewMacOS/Linux):
```bash
brew tap brewforge/chinese
brew install --cask easytier-gui
```
Windows(推荐,请以管理员权限运行):
```powershell
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
```
通过 cargo 安装(最新开发版本):
```bash
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
```
[下载预编译文件](https://github.com/EasyTier/EasyTier/releases)(推荐,支持所有平台)
[通过 Docker 安装](https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F)
[安装 OpenWrt ipk 软件包](https://github.com/EasyTier/luci-app-easytier)
附加步骤:
[一键注册系统服务](https://easytier.cn/guide/network/oneclick-install-as-service.html)(系统启动时自动后台运行)
### 🚀 基本用法
#### 使用共享节点快速组网
EasyTier 支持使用共享节点快速组网。当您没有公网 IP 时,可以使用公共共享节点。节点会自动尝试 NAT 穿透并建立 P2P 连接。当 P2P 失败时,数据将通过共享节点中继。
使用共享节点时,每个进入网络的节点需要提供相同的 `--network-name``--network-secret` 参数作为网络的唯一标识符。
以两个节点为例(请使用更复杂的网络名称以避免冲突):
1. 在节点 A 上运行:
```bash
# 以管理员权限运行
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
```
2. 在节点 B 上运行:
```bash
# 以管理员权限运行
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
```
执行成功后,可以使用 `easytier-cli` 检查网络状态:
```text
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.2-70e69a38~ |
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.2-70e69a38~ |
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
```
您可以测试节点之间的连通性:
```bash
# 测试连通性
ping 10.126.126.1
ping 10.126.126.2
```
注意:如果无法 ping 通,可能是防火墙阻止了入站流量。请关闭防火墙或添加允许规则。
为了提高可用性,您可以同时连接多个共享节点:
```bash
# 连接多个共享节点
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<公共节点IP>:11010 -p udp://<公共节点IP>:11010
```
#### 去中心化组网
EasyTier 本质上是去中心化的,没有服务器和客户端的区分。只要一个设备能与虚拟网络中的任何节点通信,它就可以加入虚拟网络。以下是如何设置去中心化网络:
1. 启动第一个节点(节点 A):
```bash
# 启动第一个节点
sudo easytier-core -i 10.144.144.1
```
启动后,该节点将默认监听以下端口:
- TCP11010
- UDP11010
- WebSocket11011
- WebSocket SSL11012
- WireGuard11013
2. 连接第二个节点(节点 B):
```bash
# 使用第一个节点的公网 IP 连接
sudo easytier-core -i 10.144.144.2 -p udp://第一个节点的公网IP:11010
```
3. 验证连接:
```bash
# 测试连通性
ping 10.144.144.2
# 查看已连接的对等节点
easytier-cli peer
# 查看路由信息
easytier-cli route
# 查看本地节点信息
easytier-cli node
```
更多节点要加入网络,可以使用 `-p` 参数连接到网络中的任何现有节点:
```bash
# 使用任何现有节点的公网 IP 连接
sudo easytier-core -i 10.144.144.3 -p udp://任何现有节点的公网IP:11010
```
### 🔍 高级功能
#### 子网代理
假设网络拓扑如下,节点 B 想要与其他节点共享其可访问的子网 10.1.1.0/24
```mermaid
flowchart LR
subgraph 节点 A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
subgraph 节点 A 公网 IP 22.1.1.1
nodea[EasyTier<br/>10.144.144.1]
end
subgraph 节点 B
nodeb[EasyTier\n10.144.144.2]
end
nodea <-----> nodeb
```
1. 在节点 A 上执行:
```sh
sudo easytier-core --ipv4 10.144.144.1
```
命令执行成功会有如下打印。
![alt text](/assets/image-2.png)
2. 在节点 B 执行
```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
```
3. 测试联通性
两个节点应成功连接并能够在虚拟子网内通信
```sh
ping 10.144.144.2
```
使用 easytier-cli 查看子网中的节点信息
```sh
easytier-cli peer
```
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](/assets/image-1.png)
```sh
easytier-cli node
```
![alt text](assets/image-10.png)
---
### 多节点组网
基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
```
其中 `--peers` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
---
### 子网代理(点对网)配置
假设网络拓扑如下,节点 B 想将其可访问的子网 10.1.1.0/24 共享给其他节点。
```mermaid
flowchart LR
subgraph 节点 A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
end
subgraph 节点 B
nodeb[EasyTier\n10.144.144.2]
nodeb[EasyTier<br/>10.144.144.2]
end
id1[[10.1.1.0/24]]
nodea <--> nodeb <-.-> id1
```
则节点 B 的 easytier 启动参数为(新增 -n 参数)
要共享子网,在启动 EasyTier 时添加 `-n` 参数:
```sh
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
```bash
# 与其他节点共享子网 10.1.1.0/24
sudo easytier-core -i 10.144.144.2 -n 10.1.1.0/24
```
子网代理信息自动同步到虚拟网络的每个节点,个节点自动配置相应的路由,节点 A 可以通过如下命令检查子网代理是否生效。
子网代理信息自动同步到虚拟网络的每个节点,个节点自动配置相应的路由。您可以验证子网代理设置:
1. 检查路由信息是否已同步proxy_cidrs 列展示了被代理的子网
1. 检查路由信息是否已同步proxy_cidrs 列显示代理的子网):
```sh
easytier-cli route
```
![alt text](/assets/image-3.png)
2. 测试节点 A 是否可访问被代理子网下的节点
```sh
ping 10.1.1.2
```
---
### 无公网IP组网
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.cn:11010``。
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
以双节点为例,节点 A 执行:
```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
```bash
# 查看路由信息
easytier-cli route
```
节点 B 执行
![路由信息](/assets/image-3.png)
```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
2. 测试是否可以访问代理子网中的节点:
```bash
# 测试到代理子网的连通性
ping 10.1.1.2
```
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
#### WireGuard 集成
---
### 使用 WireGuard 客户端接入
EasyTier 可以用作 WireGuard 服务端,让任意安装了 WireGuard 客户端的设备访问 EasyTier 网络。对于目前 EasyTier 不支持的平台 (如 iOS、Android 等),可以使用这种方式接入 EasyTier 网络。
假设网络拓扑如下:
EasyTier 可以作为 WireGuard 服务器,允许任何安装了 WireGuard 客户端的设备(包括 iOS 和 Android)访问 EasyTier 网络。以下是设置示例:
```mermaid
flowchart LR
ios[[iPhone \n 安装 WireGuard]]
ios[[iPhone<br/>已安装 WireGuard]]
subgraph 节点 A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
subgraph 节点 A 公网 IP 22.1.1.1
nodea[EasyTier<br/>10.144.144.1]
end
subgraph 节点 B
nodeb[EasyTier\n10.144.144.2]
nodeb[EasyTier<br/>10.144.144.2]
end
id1[[10.1.1.0/24]]
@@ -273,88 +247,79 @@ id1[[10.1.1.0/24]]
ios <-.-> nodea <--> nodeb <-.-> id1
```
我们需要 iPhone 通过节点 A 访问 EasyTier 网络,则可进行如下配置
1. 启动启用 WireGuard 门户的 EasyTier
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
```sh
# 以下参数的含义为: 监听 0.0.0.0:11013 端口,WireGuard 使用 10.14.14.0/24 网段
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
```bash
# 在 0.0.0.0:11013 上监听,并使用 10.14.14.0/24 子网作为 WireGuard 客户端
sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
```
easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
2. 获取 WireGuard 客户端配置:
```sh
$> easytier-cli vpn-portal
portal_name: wireguard
############### client_config_start ###############
[Interface]
PrivateKey = 9VDvlaIC9XHUvRuE06hD2CEDrtGF+0lDthgr9SZfIho=
Address = 10.14.14.0/32 # should assign an ip from this cidr manually
[Peer]
PublicKey = zhrZQg4QdPZs8CajT3r4fmzcNsWpBL9ImQCUsnlXyGM=
AllowedIPs = 10.144.144.0/24,10.14.14.0/24
Endpoint = 0.0.0.0:11013 # should be the public ip(or domain) of the vpn server
PersistentKeepalive = 25
############### client_config_end ###############
connected_clients:
[]
```bash
# 获取 WireGuard 客户端配置
easytier-cli vpn-portal
```
使用 Client Config 前,需要将 Interface Address 和 Peer Endpoint 分别修改为客户端的 IP 和 EasyTier 节点的 IP。将配置文件导入 WireGuard 客户端,即可访问 EasyTier 网络。
3. 在输出配置中:
-`Interface.Address` 设置为 WireGuard 子网中的可用 IP
-`Peer.Endpoint` 设置为您的 EasyTier 节点的公网 IP/域名
- 将修改后的配置导入到您的 WireGuard 客户端
---
#### 自建公共共享节点
### 自建公共中转服务器
您可以运行自己的公共共享节点来帮助其他节点相互发现。公共共享节点只是一个普通的 EasyTier 网络(具有相同的网络名称和密钥),其他网络可以连接到它。
每个虚拟网络(通过相同的网络名称和密钥建链)都可以充当公共服务器集群。其他网络的节点可以连接到公共服务器集群中的任意节点,无需公共 IP 即可发现彼此。
要运行公共共享节点:
运行自建的公共服务器集群与运行虚拟网络完全相同,不过可以跳过配置 ipv4 地址。
也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡:
```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
```bash
# 公共共享节点无需指定 IPv4 地址
sudo easytier-core --network-name mysharednode --network-secret mysharednode
```
### 其他配置
网络设置成功后,您可以轻松配置它以在系统启动时自动启动。请参阅 [一键注册服务指南](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) 了解如何将 EasyTier 注册为系统服务。
可使用 ``easytier-core --help`` 查看全部配置项
## 相关项目
## 路线图
- [ZeroTier](https://www.zerotier.com/):用于连接设备的全球虚拟网络。
- [TailScale](https://tailscale.com/):旨在简化网络配置的 VPN 解决方案。
- [ ] 完善文档和用户指南。
- [ ] 支持 TCP 打洞、KCP、FEC 等特性。
- [ ] 支持 iOS。
### 联系我们
## 社区和贡献
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
## 相关项目和资源
- [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。
- [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。
- [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN
- [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络
- 💬 **[Telegram 群组](https://t.me/easytier)**
- 👥 **QQ 群**
- 一群 [949700262](https://qm.qq.com/q/wFoTUChqZW)
- 二群 [837676408](https://qm.qq.com/q/4V33DrfgHe)
- 三群 [957189589](https://qm.qq.com/q/YNyTQjwlai)
## 许可证
EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可发布。
## 联系方式
- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
- QQ 群: 949700262
- Telegramhttps://t.me/easytier
EasyTier 在 [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可发布。
## 赞助
<img src="assets/image-8.png" width="300">
<img src="assets/image-9.png" width="300">
本项目的 CDN 加速和安全防护由腾讯云 EdgeOne 赞助。
<p align="center">
<a href="https://edgeone.ai/?from=github" target="_blank">
<img src="assets/edgeone.png" width="200">
</a>
</p>
特别感谢 [浪浪云](https://langlangy.cn/?i26c5a5) 和 [雨云](https://www.rainyun.com/NjM0NzQ1_) 赞助我们的公共服务器。
<p align="center">
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
<img src="assets/langlang.png" width="200">
</a>
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
<img src="assets/raincloud.png" width="200">
</a>
</p>
如果您觉得 EasyTier 有帮助,请考虑赞助我们。软件开发和维护需要大量的时间和精力,您的赞助将帮助我们更好地维护和改进 EasyTier。
<p align="center">
<img src="assets/wechat.png" width="200">
<img src="assets/alipay.png" width="200">
</p>
+116
View File
@@ -0,0 +1,116 @@
# Android build environment
{
pkgs,
nixpkgs,
system,
}:
let
androidEnv = pkgs.callPackage "${nixpkgs}/pkgs/development/mobile/androidenv" {
inherit pkgs;
licenseAccepted = true;
};
includeAuto = pkgs.stdenv.hostPlatform.isx86_64 || pkgs.stdenv.hostPlatform.isDarwin;
ndkVersion = "26.1.10909125";
ndkVersions = [ ndkVersion ];
sdkArgs = {
includeNDK = true;
includeSources = true;
includeSystemImages = false;
includeEmulator = false;
inherit ndkVersions;
useGoogleAPIs = true;
useGoogleTVAddOns = true;
buildToolsVersions = [ "34.0.0" ];
numLatestPlatformVersions = 10;
includeExtras = [
"extras;google;gcm"
]
++ pkgs.lib.optionals includeAuto [
"extras;google;auto"
];
extraLicenses = [
"android-sdk-preview-license"
"android-googletv-license"
"android-sdk-arm-dbt-license"
"google-gdk-license"
"intel-android-extra-license"
"intel-android-sysimage-license"
"mips-android-sysimage-license"
];
};
androidComposition = androidEnv.composeAndroidPackages sdkArgs;
androidSdk = androidComposition.androidsdk;
platformTools = androidComposition.platform-tools;
cmake = androidComposition.cmake;
ndkHostTag =
if pkgs.stdenv.isLinux then
"linux-x86_64"
else if pkgs.stdenv.isDarwin then
"darwin-x86_64"
else
"";
ndkToolchain = "${androidSdk}/libexec/android-sdk/ndk/${ndkVersion}/toolchains/llvm/prebuilt/${ndkHostTag}";
in
{
inherit
androidSdk
platformTools
cmake
ndkToolchain
ndkVersion
;
# List of packages required for Android development
packages = [
pkgs.jdk # openjdk 21
androidSdk
platformTools
cmake
pkgs.glibc_multi.dev
];
# Provide Rust extensions/targets for use by the upper-level flake
rust = {
extensions = [ "rust-std" ];
targets = [
"aarch64-linux-android"
"armv7-linux-androideabi"
"i686-linux-android"
"x86_64-linux-android"
];
};
# Android environment variables and shellHook
envVars = {
LANG = "C.UTF-8";
LC_ALL = "C.UTF-8";
JAVA_HOME = "${pkgs.jdk}/lib/openjdk";
ANDROID_SDK_ROOT = "${androidSdk}/libexec/android-sdk";
ANDROID_NDK_ROOT = "\${ANDROID_SDK_ROOT}/ndk-bundle";
NDK_HOME = "${androidSdk}/libexec/android-sdk/ndk/${ndkVersion}";
LIBCLANG_PATH = "${ndkToolchain}/lib";
KCP_SYS_EXTRA_HEADER_PATH = "${ndkToolchain}/lib/clang/19/include:${pkgs.glibc_multi.dev}/include";
ZSTD_SYS_STATIC = "1";
BINDGEN_EXTRA_CLANG_ARGS = "--sysroot=${ndkToolchain}/sysroot -I${ndkToolchain}/lib/clang/17/include ";
shellHook = ''
echo "Android environment activated"
export GRADLE_OPTS="-Dorg.gradle.project.android.aapt2FromMavenOverride=$(echo "$ANDROID_SDK_ROOT/build-tools/"*"/aapt2")"
cmake_root="$(echo "$ANDROID_SDK_ROOT/cmake/"*/)"
export PATH="$cmake_root/bin:$PATH"
unset NIX_CFLAGS_COMPILE
unset NIX_CFLAGS_COMPILE_FOR_BUILD
cat <<EOF > easytier-gui/local.properties
sdk.dir=$ANDROID_SDK_ROOT
ndk.dir=$ANDROID_NDK_ROOT
cmake.dir=$cmake_root
EOF
'';
};
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

+529
View File
@@ -0,0 +1,529 @@
# EasyTier Core Architecture
## Status and scope
This document describes the current architecture after the portable-core
refactor. It is the source of truth for ownership, dependency direction,
feature boundaries, and validation. It intentionally records the resulting
design rather than the migration history.
The refactor has three principal crate roles:
- `easytier-core` owns portable EasyTier configuration, protocol state,
routing, peer state, connectivity orchestration, packet processing, and
instance lifecycle.
- `easytier` is the native composition root. It owns operating-system
resources, native protocol engines, process integration, CLI and native
presentation.
- `easytier-proto` owns generated protobuf and RPC types, descriptor data, and
the feature slices needed by core and presentation users.
`easytier-core` is designed to compile without direct operating-system network
access. It supports native hosts through Rust traits and has a target-only WASI
adapter and ABI implementation under `easytier-core/src/wasi`.
This architecture does not require compatibility with old internal module
paths. Wire compatibility, configuration compatibility, management semantics,
and externally used application behaviour remain compatibility requirements.
## Architectural vocabulary
The following terms have specific meanings in this document:
- **Module**: an interface and the implementation hidden behind it.
- **Host**: the process or runtime embedding core and owning platform
resources.
- **Host capability**: an operation core may request but must not implement
with direct OS calls.
- **Adapter**: a concrete implementation of a Host capability or protocol
extension.
- **Composition root**: code that creates core configuration, Host Adapters,
instances, and process-level services.
- **Runtime configuration**: the authoritative normalized state used after an
instance starts.
- **Packet plane**: portable packet classification, routing, transformation,
proxy/NAT state, and forwarding decisions.
New abstractions should pass a deletion test: deleting a useful deep Module
should force non-trivial policy or lifecycle logic to reappear in multiple
callers. A pass-through wrapper with no independent invariant is not an
architectural boundary.
## Crate dependency direction
The principal dependency direction is:
```text
easytier-proto <- easytier-core <- easytier
```
Presentation crates and platform integrations consume these crates. Portable
policy must not move outward merely because one current consumer is native.
Conversely, core must not absorb an OS mechanism or a protocol engine whose
dependencies cannot satisfy the core target contract.
### `easytier-proto`
The protobuf crate is split by public Cargo features:
- `core` provides the common wire messages, peer RPC messages, generated RPC
runtime, and descriptor bytes needed by core.
- `api` adds management API messages.
- protocol-specific features add only their generated message modules.
- `json-rpc` enables the well-known protobuf JSON types used by the management
plane.
- `full` is the compatibility aggregate used by complete products.
The core crate depends on `easytier-proto` with default features disabled and
enables only `core`, adding API or JSON-RPC types through its own management
features.
The main core/native path has no `prost-reflect` dependency. OSPF route
reflection uses the focused wire editor in
`peers/route/route_peer_wire.rs`. It retains the original encoded
`RoutePeerInfo`, replaces only the fields credential filtering is allowed to
change, and leaves all other top-level and nested fields intact. This is
required so unknown fields survive mixed-version, multi-hop propagation.
Generated Rust types remain responsible for normal message construction and
validation.
Descriptor sets are still generated and embedded by `easytier-proto`; removing
runtime reflection did not remove descriptor data used by configuration and
RPC tooling. The OHOS integration has its own schema service and dependency
policy and is outside this replacement.
### `easytier-core`
Core owns portable behaviour and exposes capability seams. Its normal
dependencies use Tokio runtime, time, synchronization, and I/O traits without
requiring the full Tokio feature set.
Core may depend on optional portable engines when their owning feature is
enabled. It does not create real native TCP/UDP sockets, alter routes, open a
TUN device, enter a network namespace, configure system DNS, manage a service,
or invoke UPnP/NAT-PMP directly.
### `easytier`
The native crate owns:
- process startup, shutdown, signals, service management, and allocators;
- filesystem configuration input and persistence;
- real TCP/UDP, DNS, TUN, raw-socket, route, interface, namespace, and socket
option operations;
- UPnP and NAT-PMP operations;
- Unix and FakeTCP resources;
- WebSocket/WSS, QUIC, WireGuard, and KCP concrete engines;
- native Magic DNS serving and system DNS integration;
- CLI, web, GUI, FFI, and native management presentation.
Native code may translate values and assemble Adapters. It must not maintain a
second peer graph, reproduce core routing or hole-punch policy, or invent an
alternative instance lifecycle.
## Internal core layers
The physical module layout follows this downward order:
```text
foundation
<- config / packet
<- socket
<- host
<- tunnel
<- listener / connectivity
<- peers / rpc
<- gateway
<- instance
<- management
```
`process_runtime` is a process- or module-scoped owner shared by instances.
`wasi` is target integration and is compiled only for tests or the WASI target;
it is not an additional portable domain layer.
### Foundation
`foundation/` contains task supervision, the time facade, rate limiting,
statistics primitives, and the domain-neutral external operation broker. The
broker owns asynchronous operation lifecycle and completion storage while the
calling domain owns operation kinds, outcomes, resources, and errors.
Foundation must not depend on a domain layer.
### Configuration and packets
`config/` owns:
- the complete `TomlConfig` model;
- parsing, serialization, and validation;
- OS-independent defaults;
- peer, encryption, gateway, and API input models;
- normalized runtime snapshots and the live runtime configuration store.
The Host supplies platform facts through `CoreInstanceHostConfig`. Core applies
the policy that combines those facts with TOML input. This is especially
important for a WASI build: the compile-time guest target cannot be used as a
proxy for the Host operating system.
`packet/` owns EasyTier packet structures, compression, STUN and hole-punch
wire codecs. It does not own socket I/O or connection policy.
### Socket and Host seams
`socket/` contains transport-neutral primitives:
- `SocketContext`, including IP-family policy, optional socket mark, and an
opaque network-namespace token;
- virtual TCP socket, listener, and factory traits;
- virtual UDP socket and factory traits;
- UDP session multiplexing, classification, and lifecycle;
- in-process Ring sockets.
`host/` is the single home of Host capability seams:
- DNS and DNS record resolution;
- connector environment observations;
- packet ingress and egress;
- Host socket operation bridges and handle-based TCP/UDP/listener adapters.
Core owns scheduling, backpressure, cancellation, UDP session state, and
protocol state even when each actual operation crosses a Host Adapter. A Host
Adapter owns the real resource and performs the OS operation.
The native `NativeHostRuntime` is process-wide and does not retain an instance
`GlobalCtx`, namespace guard, socket mark, or connectivity state. Differences
between instances travel in each request's `SocketContext`. A narrow
instance-host projection may expose listener and interface facts, but it does
not become another socket factory.
### Tunnel and listener
A socket is a raw communication endpoint. A Tunnel is an EasyTier connection
created by adding framing, metadata, handshakes, and protocol lifecycle.
Core owns:
- raw TCP framing and upgrade;
- UDP tunnel/session framing and classification;
- Ring Tunnel identity and registry state;
- encryption and secure-datagram policy that is portable;
- client/server protocol selection interfaces;
- listener planning, optional/required listener policy, retry, accept
scheduling, running-listener registry, and orderly shutdown.
Native protocol Adapters own WebSocket/WSS, QUIC, WireGuard, and KCP engines.
Unix and FakeTCP are socket resources that feed a core protocol upgrader; they
are not independent owners of EasyTier peer state.
Each protocol registration must provide a coherent client/server Adapter.
Unavailable configured transports must be rejected during validation or
protocol selection in the standard runtime, rather than silently falling back
to another transport. A compact compatibility Host may instead retain the
desired value for management readback and omit it from normalized runtime
state; it must not advertise or partially activate the unavailable transport.
### Connectivity
`connectivity/` owns:
- manual connection and endpoint discovery policy;
- direct candidate selection;
- retry, backoff, blacklists, and listener reuse;
- STUN requests, responses, probing, NAT inference, and published endpoint
state;
- TCP and UDP hole-punch state machines;
- UDP port-mapping policy and lease lifecycle;
- conversion of successful sockets into protocol-upgrade requests.
The Host owns DNS execution, socket syscalls, interface enumeration, bind
device/mark/namespace operations, and concrete UPnP/NAT-PMP calls. STUN-only
hole punching remains available when the Host does not supply a port-mapping
Adapter.
Some connectivity files intentionally implement peer-facing adapter traits for
`PeerManagerCore`. These are localized integration edges between adjacent
domains, not permission for lower socket or Host layers to depend on peers.
### Peers and RPC
`peers/` is the authoritative owner of:
- admission and connection sessions;
- peer maps and connection lifecycle;
- ACL and whitelist decisions;
- OSPF route calculation and graph algorithms;
- peer and credential RPC registration;
- foreign-network admission, identity, relay, and lifecycle;
- peer-center state and public IPv6 policy;
- traffic metrics and peer snapshots.
Submodules progress from kernel types and utilities, through ACL/context,
connection state, route state, manager services, and finally foreign-network
and peer-center composition. Callers consume the public surface declared by
the domain rather than reaching into a parallel native peer owner.
`rpc/` owns the peer-flavoured RPC transport, packet fragmentation, client and
server lifecycle, handler registry, and standalone listener/client lifecycle.
Generated service descriptors and message types remain in `easytier-proto`.
### Gateway
`gateway/` owns portable packet-plane features:
- proxy CIDR state and monitoring policy;
- packet parsing, reassembly, NAT/proxy state, and TCP/UDP/ICMP decisions;
- the smoltcp-backed portable dataplane selected by its feature;
- SOCKS5 framing, authentication, association, routing, and session state;
- wrapped-transport planning and session state used by KCP and QUIC Adapters;
- DHCP allocation policy;
- Magic DNS route and response policy;
- VPN portal client/session policy;
- UDP broadcast classification and rewrite policy.
TUN, raw sockets, transparent-destination lookup, concrete protocol engines,
native DNS servers, namespace operations, and route application stay in native
Adapters.
Optional gateway capabilities are selected by cohesive Modules. Disabled
implementations retain stable lifecycle calls and report unsupported
configuration in the standard runtime. A compact compatibility Host may
silently normalize those settings to no-ops while preserving the desired TOML
model; disabled implementations do not duplicate portable policy.
The instance-scoped `DataPlaneSession` composes the foundation operation broker
under the same session lock as its resource and quota state. The broker owns
generic completion, cancellation, free, drain, and take transitions. The data
plane retains TCP/UDP resource ownership, operation metadata, route deadlines,
and error semantics.
The proposed restructuring of the smoltcp data plane, SOCKS5 and port-forward
Adapters, portable KCP engine, event-driven FFI/WASI completion model, and Go
Host integration is tracked in
[`data-plane-runtime-plan.md`](data-plane-runtime-plan.md). That document is a
future implementation plan; this document remains the source of truth for the
currently implemented architecture until the plan is completed.
### Instance and management
`CoreInstance::new(CoreInstanceConfig, CoreHostAdapters)` is the sole direct
construction path for a normalized instance. `CoreInstance::from_toml` uses
the same normalization and construction path. Core constructs the peer graph,
runtime store, STUN collector, connectivity managers, listener runtime, packet
plane, gateway runtimes, and lifecycle owners.
A core instance:
- owns all mutable portable state for one network;
- is one-shot after `stop`;
- exposes one complete `start` and one `stop` lifecycle interface;
- starts Modules in a fixed serial composition order without cross-Module
started flags or staged activation;
- installs initial ACL, proxy CIDR, and manual-peer inputs before startup;
- serializes lifecycle operations with one instance-level operation lock;
- owns cooperative cancellation and component shutdown order;
- exposes `CorePacketPlane` as the narrow packet/route projection used by Host
dataplane Adapters;
- treats its normalized runtime store as authoritative after construction.
`CoreHostAdapters` contains the required Host, DNS, packet sink, and
`CoreProcessRuntime`, plus optional protocol and platform capabilities. The
bundle carries capabilities, not preconstructed portable managers.
Each Module owns partial-start cleanup for its internal resources.
`CoreInstance` has one outer cancellation and recovery path for the complete
serial startup. `Running` therefore means the Host runtime and every enabled
portable Module have started successfully; there is no separate post-Host
activation state. Host packet tasks stop before PeerManager resources are
cleared.
`InstanceManager<F>` is the canonical UUID-indexed instance collection for one
Host composition. Its `InstanceFactory` constructs one complete record before
the manager performs an atomic uniqueness check. The manager owns collection
membership; it does not own startup order, persistence, daemon policy, cached
errors, ABI handles, or RPC projections.
`management/` consumes the canonical manager and instances. It owns:
- stable UUID/name selection;
- read-only instance and peer management RPC;
- full process mutation and configuration transactions when enabled;
- persistence and logger-control capability interfaces;
- management listener/client lifecycle and JSON-RPC presentation.
There is one process-level management entry. Instances and the manager do not
depend on management response projections.
## Process-scoped state
`CoreProcessRuntime` owns portable resources shared across instances in one
process or instantiated module:
- the Ring Tunnel registry and namespace;
- a reference-counted protected TCP-port registry.
The composition root creates and shares one runtime. Management listener ports
are protected before bind and held by leases after the concrete port is known.
Native and target adapters supply bound resources but do not implement a
second protected-port registry.
Process-global capability objects may contain stateless or shared platform
mechanisms. They must not contain instance-specific peer, route,
configuration, or connectivity state.
## Runtime configuration authority
`TomlConfig` is the authoritative desired configuration used for management
readback and patch transactions. Compact Hosts keep unsupported accepted values
there so controllers observe the configuration they submitted.
The separately typed, normalized core runtime store is authoritative for live
behavior:
- peer feature flags and routing policy;
- listeners and initial peers;
- ACL and whitelist inputs;
- manual and VPN portal CIDRs;
- gateway and connectivity settings;
- runtime configuration patches.
Host persistence is an effect following a successful core transaction. A Host
Adapter must not call back into an instance to obtain a hidden configuration
snapshot while core is applying an operation.
Non-serializable resources such as TUN descriptors, packet sinks, execution
domains, and native protocol engines are construction context, not TOML
fields.
## Logging
The main native runtime uses a small logger implemented in
`easytier/src/common/log`:
- `log` records and `tracing` events share console and file sinks;
- timestamps, compact formatting, optional terminal colours, `NO_COLOR`, and
basic `RUST_LOG` target/level filters are implemented directly;
- file rotation uses the existing EasyTier rolling appender;
- management RPC can reload the file level;
- an atomic maximum-level gate rejects disabled events before target matching
or file-filter locking;
- concurrent file-level reload serializes the filter and atomic-level update.
File logging and no-file logging are separate selected backends. The default
tracing backend records events and deliberately ignores span trees. The
optional `tracing` feature selects the tokio-console subscriber integration;
only that diagnostic profile pulls the main crate's `tracing-subscriber` and
`console-subscriber` dependencies.
Contrib applications and platform integrations may have independent logging
requirements and are not implicitly wired to the native process logger.
## Feature model
Features represent coherent capabilities, not arbitrary source fragments.
Important core feature relationships are:
- `management-rpc` enables generated management API types and read-only
management services.
- `management` adds configuration writes, full management composition, rich
errors, and JSON-RPC.
- `proxy-packet` enables portable packet parsing/proxy machinery and the
required smoltcp packet features.
- `proxy-smoltcp-stack` adds the async TCP/UDP smoltcp stack.
- `dns-resolver` is the shared Hickory resolver leaf used by endpoint
discovery and Magic DNS without coupling either capability to the other.
- `endpoint-discovery` adds HTTPS endpoint discovery dependencies.
- `magic-dns` enables its DNS server, management wire messages, and portable
packet-query integration.
- `tcp-hole-punch` enables the TCP hole-punch runtime.
- `dhcp-ipv4`, `public-ipv6-provider`, `vpn-portal`,
`wrapped-transport`, and `proxy-cidr-monitor` are independent gateway or
platform-policy leaves.
- `extended-services` is the compatibility aggregate for those leaves.
- encryption and compression engines remain independently selectable.
The native crate maps product features to the core and protocol features it
actually consumes. A protocol feature must not accidentally enable unrelated
gateway or management capabilities.
Production feature and platform selection belongs at Module or Adapter
boundaries rather than inside shared implementations. The logger demonstrates
the intended pattern: file and tracing variants are complete backend modules
with one stable interface, so shared event processing contains no feature
branches.
## Module boundaries
The dependency directions in this document define the intended module
boundaries. Changes that require a new upward edge must first define a stable
lower-layer interface or explicitly revise this architecture.
Modules are `pub(crate)` by default. Each domain's `mod.rs` declares its
outward surface. Public visibility is used for real cross-crate Host,
configuration, management, packet-plane, or test-support interfaces.
## Architectural invariants
1. Portable EasyTier policy has one owner in `easytier-core`.
2. Core does not perform real OS socket, DNS, TUN, route, filesystem
configuration, process, or service-manager operations.
3. Host-OS policy is runtime input; a WASI compile target is not Host policy.
4. Every real socket and DNS operation crosses a Host capability seam.
5. Core owns socket scheduling, backpressure, protocol state, and cancellation.
6. Dial, accept, and hole-punch paths produce sockets before protocol upgrade.
7. Peer admission consumes upgraded transports and does not create OS
resources.
8. Each instance owns its mutable peer, route, connectivity, gateway, and
runtime configuration state.
9. One Host composition has one canonical UUID-to-instance manager.
10. Process-level runtimes do not capture instance state.
11. `CoreInstance::new` is the sole normalized direct construction entry.
12. The manager owns membership, not lifecycle or presentation.
13. Management consumes the manager; the manager does not return management
projections.
14. Unknown protobuf fields in reflected route information survive forwarding
and credential filtering.
15. Feature selection is localized at cohesive Module/Adapter boundaries.
16. The standard runtime rejects unsupported configured capabilities. Compact
compatibility Hosts may preserve them as runtime no-ops, but never change
wire protocol, advertise them, or silently fall back to an unsafe mode.
## Validation
Changes to these boundaries should run, at minimum:
```text
cargo fmt --all -- --check
cargo check -p easytier-core -p easytier-proto -p easytier --features full
cargo test -p easytier-core --lib
```
Feature work should add focused checks for the changed no-default, isolated,
default, full, and cross-target profiles. Socket, TUN, namespace, protocol
engine, and multi-node changes require the relevant Docker integration tests.
WASI ABI or Adapter changes require a `wasm32-wasip1` build and target-side
tests. These compiler-resolved profiles are the authority for feature and
target boundaries.
CI path filters include `easytier-core`, `easytier-proto`, native, web, GUI
Tauri, and contrib. The archived Rust test suite contains both `easytier` and
`easytier-core`.
## Known limitations and debt
- Some production feature and platform gates still select fields or statements
inside shared implementations. New code should prefer complete Module or
Adapter variants, and existing cases should move only when their owning
Module is changed.
- Connectivity retains localized Adapter implementations that name
`PeerManagerCore`; further decoupling requires an interface extraction, not
a visibility-only move.
- Native Linux namespace guards exist in paths that can cross async suspension.
Because `setns` is thread-local, those operations should eventually be kept
on one non-migrating execution context.
- QUIC session retirement after failed or exhausted accepted sessions remains
separate native-engine correctness work; it must preserve multiple
connections sharing one QUIC endpoint/session.
These limitations are not reasons to add fallback owners or parallel state.
Fixes should preserve the ownership rules above and address the responsible
Module directly.
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
# HarmonyOS HAR delivery
The `ohos` workflow builds the Core HAR on pushes, pull requests, tags, and
manual runs. Every successful run retains a short-lived HAR artifact, while
publication to the private OHPM registry is deliberately restricted:
- A push to `main` publishes only when the pushed SHA is the merge commit of a
pull request targeting `main` and the push is not forced.
- A manual run on `main` publishes by default.
- A manual run on another branch publishes only when its `publish` input is
enabled.
- Direct pushes, pull requests, tags, and ordinary non-main branch builds do
not publish.
## Package identity
All branches publish the same private package name, `easytier-ohrs`. The
source branch is encoded in the package version instead of the package name:
```text
<core-version>-<branch-id>-<commits-since-tag>-<run-number>-<run-attempt>-g<short-sha>
```
`branch-id` is a lowercase, OHPM-safe form of the source branch. Publishing a
new version advances the registry's `latest` version. After publication, Core
sends the `core-har-published` repository dispatch to the ArkTS and Pro
repositories. The payload contains only `core_repository`, `core_ref`, and
`package_name`.
## App install sequence
ArkTS and Pro use the same three OHPM commands:
```bash
ohpm uninstall "$CORE_HAR_PACKAGE"
ohpm install "$CORE_HAR_PACKAGE@latest" \
--registry "$CORE_HAR_REGISTRY"
ohpm install
```
The App workflow then reads the installed version from:
```text
oh_modules/<package_name>/oh-package.json5
```
The existing `oh-package-lock.json5` and `oh_modules` directory are not
manually deleted. Because the package name remains `easytier-ohrs`, downstream
source imports do not need to be rewritten.
## Secrets
Core requires:
- `CODEARTS_PRIVATE_OHPM`: publish-capable OHPM configuration.
- `DOWNSTREAM_DISPATCH_TOKEN`: permission to dispatch both App repositories.
ArkTS and Pro require:
- `CODEARTS_PRIVATE_OHPM_READ`: read-only private OHPM authentication.
- `SIGNING_REPOSITORY_TOKEN`: read access to the corresponding private signing
repository.
Signing and AppGallery Connect credentials remain downstream application
concerns and are not passed through the Core dispatch payload.
@@ -0,0 +1,176 @@
# QUIC TCP Proxy 内存对比(2026-07-27
## 结论
在相同的双节点 network namespace 环境中,当前分支相对 2.6.4:
- 空闲且未建立 TCP proxy 连接时,两端合计 USS 从 15.95 MiB
降至 11.66 MiB,下降 26.9%
- 66 条空闲 TCP proxy 连接时,两端合计 USS 从 19.45 MiB
降至 13.85 MiB,下降 28.8%
- 固定 1 Gbit/s 的单流 TCP proxy 传输中,两端平均 USS 从
20.69 MiB 降至 14.75 MiB,下降 28.7%,同步峰值从
21.50 MiB 降至 15.02 MiB
- 从 0 增长到 66 条空闲连接推算,每条连接在两个 core 上合计
增加约 33.9 KiB USS2.6.4 为 54.2 KiB,下降 37.4%
- 当前分支的匿名内存下降约 40% 至 44%,说明堆和连接缓冲区开销
确实降低。
当前分支的 RSS 比 2.6.4 高约 5% 至 11%,但这部分差异没有出现在
Anonymous 中,主要体现为非匿名或共享驻留页。PSS 在高连接数及
固定吞吐场景基本持平,USS 和 Anonymous 则显著更低。因此不能
只根据 RSS 判断发生了内存回退。
## 测试对象
| 版本 | 标识 | 二进制 |
|---|---|---|
| 当前分支 | commit `9e2ed33aeb37`,版本 `2.6.4-9e2ed33a` | `target/x86_64-unknown-linux-musl/release/easytier-core` |
| 2.6.4 | 版本 `2.6.4-8428a89d` | `/data/tickets/easytier/easytier-linux-x86_64/easytier-core` |
当前分支使用以下命令重新构建,确保被测二进制准确对应 HEAD:
```console
cargo build --release \
--target x86_64-unknown-linux-musl \
-p easytier \
--features jemalloc \
--bin easytier-core \
--bin easytier-cli
```
两个二进制均为 stripped static PIE。当前分支明确使用 musl 和
jemalloc。
## 测试拓扑
- 两个 `easytier-core` 分别运行在独立的 network namespace
- namespace 通过 Linux bridge 和 veth 连接;
- underlay 地址为 `10.251.89.10/24``10.251.89.11/24`
- EasyTier 虚拟地址为 `10.144.144.1/24`
`10.144.144.2/24`
- 两个节点之间使用 UDP listener 建立 EasyTier peer 连接;
- 源节点启用 `--enable-quic-proxy true`
- 两个节点均保留默认 QUIC input;
- TCP client 从 `10.144.144.1` 访问绑定在
`10.144.144.2` 上的 server
- `tcp_proxy_connect` 指标的 `protocol` 标签确认为 `QUIC`
- 66 条连接场景通过两端各 132 个 established TCP socket 条目
确认当前连接数。
## 采样口径
数据读取自 `/proc/<pid>/smaps_rollup`
- RSS:进程映射的全部驻留页,包含共享代码页;
- PSS:共享页按共享进程数量分摊后的驻留内存;
- USS`Private_Clean + Private_Dirty`,表示进程独占内存;
- Anonymous:匿名页,主要反映堆、栈和运行时缓冲区。
空闲场景每隔 2 秒采样一次,共 5 次,表格记录均值。固定吞吐场景
持续 20 秒,每隔 2 秒采样一次,共 8 次,同时记录均值和峰值。
所有容量单位均为 MiB。
## 空闲连接结果
以下数据均为两个 EasyTier core 的合计值:
| 当前连接数 | 当前 RSS | 2.6.4 RSS | 当前 PSS | 2.6.4 PSS | 当前 USS | 2.6.4 USS | USS 变化 | 当前 Anonymous | 2.6.4 Anonymous |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 0 | 40.42 | 36.33 | 26.03 | 24.50 | 11.66 | 15.95 | -26.9% | 8.89 | 15.62 |
| 1 | 41.52 | 38.46 | 26.49 | 25.98 | 11.46 | 16.82 | -31.9% | 9.08 | 16.23 |
| 10 | 41.63 | 38.79 | 26.59 | 26.32 | 11.56 | 17.15 | -32.6% | 9.19 | 16.55 |
| 66 | 43.68 | 41.11 | 28.76 | 28.63 | 13.85 | 19.45 | -28.8% | 11.22 | 18.79 |
0 条和 1 条连接之间的小幅反向波动属于分配器回收和采样时序噪声,
不能解释为连接产生负开销。使用 0 到 66 条连接的跨度估算单位
连接成本更稳定。
### 分节点 USS
| 当前连接数 | 当前源端 | 当前目的端 | 2.6.4 源端 | 2.6.4 目的端 |
|---:|---:|---:|---:|---:|
| 0 | 5.45 | 6.21 | 8.07 | 7.88 |
| 1 | 5.43 | 6.03 | 8.68 | 8.14 |
| 10 | 5.49 | 6.07 | 8.90 | 8.25 |
| 66 | 6.60 | 7.25 | 10.19 | 9.25 |
### 单位连接增量
以 0 到 66 条连接的 USS 增量计算:
| 版本 | 两端 USS 增量 | 每连接两端合计 | 每连接单端平均 |
|---|---:|---:|---:|
| 当前分支 | 2.19 MiB | 33.9 KiB | 17.0 KiB |
| 2.6.4 | 3.50 MiB | 54.2 KiB | 27.1 KiB |
当前分支的每连接独占内存增量下降约 37.4%。
## 固定 1 Gbit/s 活跃流量
为排除两个版本最大吞吐不同造成的缓冲区差异,使用
`iperf3 -b 1G -P 1 -t 20` 将两个版本都限制为 1 Gbit/s。
两次测试均实际完成 2.33 GiB 传输,接收端报告 1000 Mbit/s。
### 平均值
| 版本 | 节点 | RSS | PSS | USS | Anonymous |
|---|---|---:|---:|---:|---:|
| 当前分支 | 源端 | 22.89 | 15.41 | 7.94 | 6.88 |
| 当前分支 | 目的端 | 21.77 | 14.29 | 6.82 | 5.27 |
| 当前分支 | 两端合计 | 44.67 | 29.70 | 14.75 | 12.14 |
| 2.6.4 | 源端 | 22.59 | 16.34 | 11.70 | 11.35 |
| 2.6.4 | 目的端 | 19.95 | 13.67 | 8.99 | 8.77 |
| 2.6.4 | 两端合计 | 42.53 | 30.01 | 20.69 | 20.13 |
### 对比
| 指标 | 当前分支 | 2.6.4 | 变化 |
|---|---:|---:|---:|
| 两端平均 RSS | 44.67 | 42.53 | +5.0% |
| 两端平均 PSS | 29.70 | 30.01 | -1.0% |
| 两端平均 USS | 14.75 | 20.69 | -28.7% |
| 两端平均 Anonymous | 12.14 | 20.13 | -39.7% |
| 两端同步峰值 USS | 15.02 | 21.50 | -30.2% |
## 分节点原始统计
下表保留各场景所有样本计算出的均值;`max_uss` 是该节点采样期间
的最大 USS。
| 版本 | 场景 | 节点 | 样本数 | mean_rss | mean_pss | mean_uss | mean_anon | max_uss |
|---|---|---|---:|---:|---:|---:|---:|---:|
| 当前 | 0 连接 | 源端 | 5 | 19.830 | 12.636 | 5.451 | 4.314 | 5.582 |
| 当前 | 0 连接 | 目的端 | 5 | 20.587 | 13.393 | 6.208 | 4.579 | 6.320 |
| 当前 | 1 连接 | 源端 | 5 | 20.463 | 12.944 | 5.432 | 4.401 | 5.465 |
| 当前 | 1 连接 | 目的端 | 5 | 21.061 | 13.541 | 6.030 | 4.682 | 6.051 |
| 当前 | 10 连接 | 源端 | 5 | 20.522 | 13.002 | 5.491 | 4.459 | 5.496 |
| 当前 | 10 连接 | 目的端 | 5 | 21.105 | 13.585 | 6.073 | 4.726 | 6.086 |
| 当前 | 66 连接 | 源端 | 5 | 21.513 | 14.050 | 6.595 | 5.498 | 6.672 |
| 当前 | 66 连接 | 目的端 | 5 | 22.169 | 14.706 | 7.251 | 5.723 | 7.375 |
| 当前 | 1 Gbit/s | 源端 | 8 | 22.893 | 15.410 | 7.936 | 6.877 | 8.188 |
| 当前 | 1 Gbit/s | 目的端 | 8 | 21.773 | 14.291 | 6.816 | 5.266 | 6.832 |
| 2.6.4 | 0 连接 | 源端 | 5 | 18.278 | 12.355 | 8.071 | 7.876 | 8.328 |
| 2.6.4 | 0 连接 | 目的端 | 5 | 18.048 | 12.144 | 7.880 | 7.747 | 8.203 |
| 2.6.4 | 1 连接 | 源端 | 5 | 19.535 | 13.279 | 8.676 | 8.262 | 8.727 |
| 2.6.4 | 1 连接 | 目的端 | 5 | 18.920 | 12.705 | 8.143 | 7.971 | 8.191 |
| 2.6.4 | 10 连接 | 源端 | 5 | 19.762 | 13.506 | 8.902 | 8.473 | 8.910 |
| 2.6.4 | 10 连接 | 目的端 | 5 | 19.027 | 12.812 | 8.250 | 8.078 | 8.297 |
| 2.6.4 | 66 连接 | 源端 | 5 | 21.069 | 14.805 | 10.194 | 9.702 | 10.320 |
| 2.6.4 | 66 连接 | 目的端 | 5 | 20.045 | 13.822 | 9.252 | 9.088 | 9.293 |
| 2.6.4 | 1 Gbit/s | 源端 | 8 | 22.588 | 16.343 | 11.697 | 11.354 | 12.258 |
| 2.6.4 | 1 Gbit/s | 目的端 | 8 | 19.946 | 13.670 | 8.993 | 8.774 | 9.277 |
## 解释和限制
1. 以固定 1 Gbit/s 场景为例,当前分支 RSS 增加 5.0%,但
Anonymous 下降 39.7%PSS 下降 1.0%。这说明差异主要体现
在非匿名或共享驻留页;本次没有保存逐 VMA 数据,因此不进一步
将它归因到某一个具体映射。
2. 两个相同版本进程运行在同一宿主机时会共享可执行文件代码页,
所以 PSS 比 RSS 更适合估算该测试拓扑的宿主机总成本,USS 和
Anonymous 更适合判断 EasyTier 私有堆及缓冲区的变化。
3. 这是一轮受控 A/B 测试,而不是长期统计分布。数值可用于确认
差异方向和量级;若作为发布门禁,应固定机器负载并增加多轮重复。
4. 本文只比较 QUIC TCP proxy 内存,不使用未限速吞吐结果推断性能,
避免吞吐差异污染内存结论。
@@ -0,0 +1,17 @@
[package]
name = "easytier-android-jni"
version = "0.1.0"
edition.workspace = true
[lib]
crate-type = ["cdylib"]
[dependencies]
jni = "0.21"
once_cell = "1.18.0"
log = "0.4"
android_logger = "0.13"
serde = { version = "1.0.220", features = ["derive"] }
serde_json = "1.0"
easytier = { path = "../../easytier" }
easytier-ffi = { path = "../easytier-ffi", default-features = false }
@@ -0,0 +1,282 @@
# EasyTier Android JNI
这是 EasyTier 的 Android JNI 绑定库,允许 Android 应用程序调用 EasyTier 的网络功能。
## 功能特性
- 🚀 完整的 EasyTier FFI 接口封装
- 📱 原生 Android JNI 支持
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
- 🛡️ 类型安全的 Java 接口
- 🔌 支持通过 JSON 调用已暴露的 EasyTier RPC 查询/管理接口
- 📝 详细的错误处理和日志记录
## 支持的架构
- `arm64-v8a` (aarch64-linux-android)
- `armeabi-v7a` (armv7-linux-androideabi)
- `x86` (i686-linux-android)
- `x86_64` (x86_64-linux-android)
## 构建要求
### 系统要求
- Rust 1.70+
- Android NDK r21+
- Linux/macOS 开发环境
### 环境设置
1. **安装 Rust**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
```
2. **安装 Android NDK**
- 下载 Android NDK: https://developer.android.com/ndk/downloads
- 解压到合适的目录
- 设置环境变量:
```bash
export ANDROID_NDK_ROOT=/path/to/android-ndk
```
3. **添加 Android 目标**
```bash
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add i686-linux-android
rustup target add x86_64-linux-android
```
## 构建步骤
1. **克隆项目并进入目录**
```bash
cd /path/to/EasyTier/easytier-contrib/easytier-android-jni
```
2. **运行构建脚本**
```bash
./build.sh
```
3. **构建完成后,库文件将生成在 `target/android/` 目录下**
```
target/android/
├── arm64-v8a/
│ └── libeasytier_android_jni.so
├── armeabi-v7a/
│ └── libeasytier_android_jni.so
├── x86/
│ └── libeasytier_android_jni.so
└── x86_64/
└── libeasytier_android_jni.so
```
## Android 项目集成
### 1. 复制库文件
将生成的 `.so` 文件复制到您的 Android 项目中:
```
your-android-project/
└── src/main/
├── jniLibs/
│ ├── arm64-v8a/
│ │ └── libeasytier_android_jni.so
│ ├── armeabi-v7a/
│ │ └── libeasytier_android_jni.so
│ ├── x86/
│ │ └── libeasytier_android_jni.so
│ └── x86_64/
│ └── libeasytier_android_jni.so
└── java/
└── com/easytier/jni/
└── EasyTierJNI.java
```
### 2. 复制 Java 接口
将 `java/com/easytier/jni/EasyTierJNI.java` 复制到您的 Android 项目的相应包路径下。
### 3. 添加权限
在 `AndroidManifest.xml` 中添加必要的权限:
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
```
## 使用示例
### 基本使用
```java
import com.easytier.jni.EasyTierJNI;
import java.util.Map;
public class EasyTierManager {
// 初始化网络实例
public void startNetwork() {
String config = """
inst_name = "my_instance"
network = "my_network"
""";
try {
// 解析配置
int result = EasyTierJNI.parseConfig(config);
if (result != 0) {
String error = EasyTierJNI.getLastError();
throw new RuntimeException("配置解析失败: " + error);
}
// 启动网络实例
result = EasyTierJNI.runNetworkInstance(config);
if (result != 0) {
String error = EasyTierJNI.getLastError();
throw new RuntimeException("网络实例启动失败: " + error);
}
System.out.println("EasyTier 网络实例启动成功");
} catch (RuntimeException e) {
System.err.println("启动失败: " + e.getMessage());
}
}
// 获取网络信息
public void getNetworkInfo() {
try {
Map<String, String> infos = EasyTierJNI.collectNetworkInfosAsMap(10);
for (Map.Entry<String, String> entry : infos.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
} catch (RuntimeException e) {
System.err.println("获取网络信息失败: " + e.getMessage());
}
}
// 停止所有实例
public void stopNetwork() {
try {
int result = EasyTierJNI.stopAllInstances();
if (result == 0) {
System.out.println("所有网络实例已停止");
}
} catch (RuntimeException e) {
System.err.println("停止网络失败: " + e.getMessage());
}
}
}
```
### 通用 JSON RPC
`EasyTierJNI.callJsonRpc(serviceName, methodName, domainName, payloadJson)` 可以调用已暴露的
EasyTier RPC 服务,payload 和返回值均为 protobuf JSON。该接口不支持
`api.manage.WebClientService`;实例启动、保留、删除、信息收集仍使用专用 JNI API。
```java
String response = EasyTierJNI.callJsonRpc(
"api.logger.LoggerRpcService",
"get_logger_config",
"{}"
);
```
### VPN 服务集成
如果您要在 Android VPN 服务中使用:
```java
public class EasyTierVpnService extends VpnService {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 建立 VPN 连接
ParcelFileDescriptor vpnInterface = establishVpnInterface();
if (vpnInterface != null) {
int fd = vpnInterface.getFd();
// 设置 TUN 文件描述符
try {
EasyTierJNI.setTunFd("my_instance", fd);
} catch (RuntimeException e) {
Log.e("EasyTier", "设置 TUN FD 失败", e);
}
}
return START_STICKY;
}
private ParcelFileDescriptor establishVpnInterface() {
Builder builder = new Builder();
builder.setMtu(1500);
builder.addAddress("10.0.0.2", 24);
builder.addRoute("0.0.0.0", 0);
builder.setSession("EasyTier VPN");
return builder.establish();
}
}
```
## API 参考
### EasyTierJNI 类方法
| 方法 | 描述 | 参数 | 返回值 |
|------|------|------|--------|
| `parseConfig(String config)` | 解析 TOML 配置 | config: 配置字符串 | 0=成功, -1=失败 |
| `runNetworkInstance(String config)` | 启动网络实例 | config: 配置字符串 | 0=成功, -1=失败 |
| `setTunFd(String instanceName, int fd)` | 设置 TUN 文件描述符 | instanceName: 实例名, fd: 文件描述符 | 0=成功, -1=失败 |
| `retainNetworkInstance(String[] names)` | 保留指定实例 | names: 实例名数组 | 0=成功, -1=失败 |
| `collectNetworkInfos(int maxLength)` | 收集网络信息 | maxLength: 最大条目数 | 信息字符串数组 |
| `collectNetworkInfosAsMap(int maxLength)` | 收集网络信息为 Map | maxLength: 最大条目数 | Map<String, String> |
| `getLastError()` | 获取最后错误 | 无 | 错误消息字符串 |
| `stopAllInstances()` | 停止所有实例 | 无 | 0=成功, -1=失败 |
| `retainSingleInstance(String name)` | 保留单个实例 | name: 实例名 | 0=成功, -1=失败 |
## 故障排除
### 常见问题
1. **构建失败: "Android NDK not found"**
- 确保设置了 `ANDROID_NDK_ROOT` 环境变量
- 检查 NDK 路径是否正确
2. **运行时错误: "java.lang.UnsatisfiedLinkError"**
- 确保 `.so` 文件放在正确的 `jniLibs` 目录下
- 检查目标架构是否匹配
3. **配置解析失败**
- 检查 TOML 配置格式是否正确
- 使用 `getLastError()` 获取详细错误信息
### 调试技巧
- 启用 Android 日志查看 JNI 层的日志输出
- 使用 `adb logcat -s EasyTier-JNI` 查看相关日志
- 检查 `getLastError()` 返回的错误信息
## 许可证
本项目遵循与 EasyTier 主项目相同的许可证。
## 贡献
欢迎提交 Issue 和 Pull Request 来改进这个项目。
## 相关链接
- [EasyTier 主项目](https://github.com/EasyTier/EasyTier)
- [Android NDK 文档](https://developer.android.com/ndk)
- [Rust JNI 文档](https://docs.rs/jni/)
@@ -0,0 +1,17 @@
use std::{env, path::PathBuf};
fn main() {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if !matches!(target_os.as_str(), "android" | "linux") {
return;
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let exports = manifest_dir.join("exports.map");
println!("cargo:rerun-if-changed={}", exports.display());
println!(
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={}",
exports.display()
);
println!("cargo:rustc-cdylib-link-arg=-Wl,--exclude-libs,ALL");
}
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# EasyTier Android JNI 构建脚本
# 用于编译适用于 Android 平台的 JNI 库
# 使用 cargo-ndk 工具简化 Android 编译过程
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
REPO_ROOT=$(git rev-parse --show-toplevel)
echo -e "${GREEN}EasyTier Android JNI 构建脚本 (使用 cargo-ndk)${NC}"
echo "=============================================="
# 检查 Rust 是否安装
if ! command -v rustc &> /dev/null; then
echo -e "${RED}错误: 未找到 Rust 编译器,请先安装 Rust${NC}"
exit 1
fi
# 检查 cargo 是否安装
if ! command -v cargo &> /dev/null; then
echo -e "${RED}错误: 未找到 Cargo,请先安装 Rust 工具链${NC}"
exit 1
fi
# 检查 cargo-ndk 是否安装
if ! cargo ndk --version &> /dev/null; then
echo -e "${YELLOW}cargo-ndk 未安装,正在安装...${NC}"
cargo install cargo-ndk
if ! cargo ndk --version &> /dev/null; then
echo -e "${RED}错误: cargo-ndk 安装失败${NC}"
exit 1
fi
fi
echo -e "${GREEN}cargo-ndk 版本: $(cargo ndk --version)${NC}"
# Android 目标架构映射 (cargo-ndk 使用的架构名称)
# ANDROID_TARGETS=("arm64-v8a" "armeabi-v7a" "x86" "x86_64")
ANDROID_TARGETS=("arm64-v8a")
# Android 架构到 Rust target 的映射
declare -A TARGET_MAP
TARGET_MAP["arm64-v8a"]="aarch64-linux-android"
TARGET_MAP["armeabi-v7a"]="armv7-linux-androideabi"
TARGET_MAP["x86"]="i686-linux-android"
TARGET_MAP["x86_64"]="x86_64-linux-android"
# 检查并安装所需的 Rust target
echo -e "${YELLOW}检查并安装 Android 目标架构...${NC}"
for android_target in "${ANDROID_TARGETS[@]}"; do
rust_target="${TARGET_MAP[$android_target]}"
if ! rustup target list --installed | grep -q "$rust_target"; then
echo -e "${YELLOW}安装目标架构: $rust_target (for $android_target)${NC}"
rustup target add "$rust_target"
else
echo -e "${GREEN}目标架构已安装: $rust_target (for $android_target)${NC}"
fi
done
# 创建输出目录
OUTPUT_DIR="./target/android"
mkdir -p "$OUTPUT_DIR"
# 构建函数
build_for_target() {
local android_target=$1
echo -e "${YELLOW}构建目标: $android_target${NC}"
# 首先构建 easytier-ffi
echo -e "${YELLOW}构建 easytier-ffi for $android_target${NC}"
(cd $REPO_ROOT/easytier-contrib/easytier-ffi && cargo ndk -t $android_target build --release)
# 构建 JNI 库
cargo ndk -t $android_target build --release
# 复制库文件到输出目录
# cargo-ndk 使用 Rust target 名称作为目录名,而不是 Android 架构名称
rust_target="${TARGET_MAP[$android_target]}"
mkdir -p "$OUTPUT_DIR/$android_target"
cp "$REPO_ROOT/target/$rust_target/release/libeasytier_android_jni.so" "$OUTPUT_DIR/$android_target/"
cp "$REPO_ROOT/target/$rust_target/release/libeasytier_ffi.so" "$OUTPUT_DIR/$android_target/"
echo -e "${GREEN}库文件已复制到: $OUTPUT_DIR/$android_target/${NC}"
}
# 检查 Android NDK (cargo-ndk 会自动处理 NDK 路径)
if [ -z "$ANDROID_NDK_ROOT" ] && [ -z "$ANDROID_NDK_HOME" ] && [ -z "$NDK_HOME" ]; then
echo -e "${YELLOW}警告: 未设置 Android NDK 环境变量${NC}"
echo "cargo-ndk 将尝试自动检测 NDK 路径"
echo "如果构建失败,请设置以下环境变量之一:"
echo " - ANDROID_NDK_ROOT"
echo " - ANDROID_NDK_HOME"
echo " - NDK_HOME"
else
if [ -n "$ANDROID_NDK_ROOT" ]; then
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_ROOT${NC}"
elif [ -n "$ANDROID_NDK_HOME" ]; then
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_HOME${NC}"
elif [ -n "$NDK_HOME" ]; then
echo -e "${GREEN}使用 Android NDK: $NDK_HOME${NC}"
fi
fi
# 构建所有目标
echo -e "${YELLOW}开始构建所有目标架构...${NC}"
for target in "${ANDROID_TARGETS[@]}"; do
build_for_target "$target"
done
echo -e "${GREEN}构建完成!${NC}"
echo -e "${GREEN}所有库文件已生成到: $OUTPUT_DIR${NC}"
echo ""
echo "目录结构:"
ls -la "$OUTPUT_DIR"/*/
echo ""
echo -e "${YELLOW}使用说明:${NC}"
echo "1. 将生成的 .so 文件复制到您的 Android 项目的 src/main/jniLibs/ 目录下"
echo "2. 将 java/com/easytier/jni/EasyTierJNI.java 复制到您的 Android 项目中"
echo "3. 在您的 Android 代码中调用 EasyTierJNI 类的方法"
echo ""
echo -e "${GREEN}注意: 此脚本使用 cargo-ndk 工具,无需手动设置复杂的环境变量${NC}"
echo -e "${GREEN}cargo-ndk 会自动处理交叉编译所需的工具链配置${NC}"
@@ -0,0 +1,56 @@
# EasyTier Android JNI 示例配置文件
# 这是一个基本的配置示例,展示如何配置 EasyTier 网络实例
# 实例名称 (必需)
inst_name = "android_instance"
# 网络名称 (必需)
network = "my_easytier_network"
# 网络密钥 (可选,用于网络加密)
# network_secret = "your_secret_key_here"
# 监听地址 (可选)
# listeners = ["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"]
# 对等节点地址 (可选)
# peers = ["tcp://peer1.example.com:11010", "udp://peer2.example.com:11010"]
# 虚拟 IP 地址 (可选)
# ipv4 = "10.144.144.1"
# 主机名 (可选)
# hostname = "android-device"
# 启用 IPv6 (可选)
# ipv6 = "fd00::1"
# 代理网络 (可选)
# proxy_networks = ["192.168.1.0/24"]
# 退出节点 (可选)
# exit_nodes = ["peer1"]
# 启用加密 (可选)
# enable_encryption = true
# 启用 IPv4 转发 (可选)
# enable_ipv4 = true
# 启用 IPv6 转发 (可选)
# enable_ipv6 = false
# MTU 设置 (可选)
# mtu = 1420
# 日志级别 (可选: error, warn, info, debug, trace)
# log_level = "info"
# 禁用 P2P (可选)
# disable_p2p = false
# 使用多路径 (可选)
# use_multi_path = true
# 延迟优先 (可选)
# latency_first = false
@@ -0,0 +1,6 @@
{
global:
Java_com_easytier_jni_EasyTierJNI_*;
local:
*;
};
@@ -0,0 +1,147 @@
package com.easytier.jni
fun interface ConfigServerEventCallback {
fun onEvent(eventJson: String)
}
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 核心网络功能的接口 */
object EasyTierJNI {
init {
// 加载本地库
System.loadLibrary("easytier_android_jni")
}
/**
* 设置 TUN 文件描述符
* @param instanceName 实例名称
* @param fd TUN 文件描述符
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic external fun setTunFd(instanceName: String, fd: Int): Int
/**
* 解析配置字符串
* @param config TOML 格式的配置字符串
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当配置解析失败时抛出异常
*/
@JvmStatic external fun parseConfig(config: String): Int
/**
* 运行网络实例
* @param config TOML 格式的配置字符串
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当实例启动失败时抛出异常
*/
@JvmStatic external fun runNetworkInstance(config: String): Int
/**
* 启动配置服务器客户端
* @param url 配置服务器 URL
* @param hostname 主机名,传入 null 使用系统主机名
* @param machineId 稳定机器 ID,由调用方负责持久化
* @param secureMode 是否启用 secure mode
* @param callback 远程配置应用/删除事件回调
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当客户端启动失败时抛出异常
*/
@JvmStatic
external fun startConfigServerClient(
url: String,
hostname: String?,
machineId: String,
secureMode: Boolean,
callback: ConfigServerEventCallback?
): Int
/**
* 停止配置服务器客户端
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当客户端停止失败时抛出异常
*/
@JvmStatic external fun stopConfigServerClient(): Int
/** 查询配置服务器客户端是否已连接 */
@JvmStatic external fun isConfigServerClientConnected(): Boolean
/**
* 保留指定的网络实例,停止其他实例
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic external fun retainNetworkInstance(instanceNames: Array<String>?): Int
/**
* 收集网络信息
* @param maxLength 最大返回条目数
* @return 包含网络信息的 JSON 字符串
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
/**
* 列出当前运行的实例名称和实例 ID。
* @param maxLength 最大返回条目数
* @return JSON 对象,key 为 instance namevalue 为 instance id
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic external fun listInstances(maxLength: Int): String?
/**
* 调用暴露的 EasyTier RPC 方法,输入和输出均为 protobuf JSON 字符串。
*
* 不支持 api.manage.WebClientService;实例启动、保留、删除、信息收集请继续使用专用 JNI API。
* payloadJson 需要包含目标 RPC 所需的 instance selector。
*
* @param serviceName RPC 服务名,例如 api.instance.PeerManageRpcService
* @param methodName RPC 方法名,支持 snake_case 或 proto 方法名
* @param domainName 仅 TcpProxyRpcService 使用;传 null 或空字符串默认 tcp
* @param payloadJson protobuf JSON 请求体
* @return protobuf JSON 响应体
* @throws RuntimeException 当 RPC 调用失败时抛出异常
*/
@JvmStatic
external fun callJsonRpc(
serviceName: String,
methodName: String,
domainName: String?,
payloadJson: String
): String?
/**
* 调用不需要 domainName 的 EasyTier RPC 方法。
*/
@JvmStatic
fun callJsonRpc(serviceName: String, methodName: String, payloadJson: String): String? {
return callJsonRpc(serviceName, methodName, null, payloadJson)
}
/**
* 获取最后的错误消息
* @return 错误消息字符串,如果没有错误则返回 null
*/
@JvmStatic external fun getLastError(): String?
/**
* 便利方法:停止所有网络实例
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic
fun stopAllInstances(): Int {
return retainNetworkInstance(null)
}
/**
* 便利方法:停止指定实例外的所有实例
* @param instanceName 要保留的实例名称
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic
fun retainSingleInstance(instanceName: String): Int {
return retainNetworkInstance(arrayOf(instanceName))
}
}
@@ -0,0 +1,252 @@
package com.easytier.jni
import android.app.Activity
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.squareup.moshi.Moshi
import com.squareup.wire.WireJsonAdapterFactory
import common.Ipv4Inet
import web.NetworkInstanceRunningInfoMap
fun parseIpv4InetToString(inet: Ipv4Inet?): String? {
val addr = inet?.address?.addr ?: return null
val networkLength = inet.network_length
// 将 int32 转换为 IPv4 字符串
val ip =
String.format(
"%d.%d.%d.%d",
(addr shr 24) and 0xFF,
(addr shr 16) and 0xFF,
(addr shr 8) and 0xFF,
addr and 0xFF
)
return "$ip/$networkLength"
}
/** EasyTier 管理类 负责管理 EasyTier 实例的生命周期、监控网络状态变化、控制 VpnService */
class EasyTierManager(
private val activity: Activity,
private val instanceName: String,
private val networkConfig: String
) {
companion object {
private const val TAG = "EasyTierManager"
private const val MONITOR_INTERVAL = 3000L // 3秒监控间隔
}
private val handler = Handler(Looper.getMainLooper())
private var isRunning = false
private var currentIpv4: String? = null
private var currentProxyCidrs: List<String> = emptyList()
private var vpnServiceIntent: Intent? = null
// JSON 解析器
private val moshi = Moshi.Builder().add(WireJsonAdapterFactory()).build()
private val adapter = moshi.adapter(NetworkInstanceRunningInfoMap::class.java)
// 监控任务
private val monitorRunnable =
object : Runnable {
override fun run() {
if (isRunning) {
monitorNetworkStatus()
handler.postDelayed(this, MONITOR_INTERVAL)
}
}
}
/** 启动 EasyTier 实例和监控 */
fun start() {
if (isRunning) {
Log.w(TAG, "EasyTier 实例已经在运行中")
return
}
try {
// 启动 EasyTier 实例
val result = EasyTierJNI.runNetworkInstance(networkConfig)
if (result == 0) {
isRunning = true
Log.i(TAG, "EasyTier 实例启动成功: $instanceName")
// 开始监控网络状态
handler.post(monitorRunnable)
} else {
Log.e(TAG, "EasyTier 实例启动失败: $result")
val error = EasyTierJNI.getLastError()
Log.e(TAG, "错误信息: $error")
}
} catch (e: Exception) {
Log.e(TAG, "启动 EasyTier 实例时发生异常", e)
}
}
/** 停止 EasyTier 实例和监控 */
fun stop() {
if (!isRunning) {
Log.w(TAG, "EasyTier 实例未在运行")
return
}
isRunning = false
// 停止监控任务
handler.removeCallbacks(monitorRunnable)
try {
// 停止 VpnService
stopVpnService()
// 停止 EasyTier 实例
EasyTierJNI.stopAllInstances()
Log.i(TAG, "EasyTier 实例已停止: $instanceName")
// 重置状态
currentIpv4 = null
currentProxyCidrs = emptyList()
} catch (e: Exception) {
Log.e(TAG, "停止 EasyTier 实例时发生异常", e)
}
}
/** 监控网络状态 */
private fun monitorNetworkStatus() {
try {
val infosJson = EasyTierJNI.collectNetworkInfos(10)
if (infosJson.isNullOrEmpty()) {
Log.d(TAG, "未获取到网络信息")
return
}
val networkInfoMap = parseNetworkInfo(infosJson)
val networkInfo = networkInfoMap?.map?.get(instanceName)
if (networkInfo == null) {
Log.d(TAG, "未找到实例 $instanceName 的网络信息")
return
}
Log.d(TAG, "网络信息: $networkInfo")
// 检查实例是否正在运行
if (!networkInfo.running) {
Log.w(TAG, "EasyTier 实例未运行: ${networkInfo.error_msg}")
return
}
val newIpv4Inet = networkInfo.my_node_info?.virtual_ipv4
if (newIpv4Inet == null) {
Log.w(TAG, "EasyTier No Ipv4: $networkInfo")
return
}
// 获取当前节点的 IPv4 地址
val newIpv4 = parseIpv4InetToString(newIpv4Inet)
// 获取所有节点的 proxy_cidrs
val newProxyCidrs = mutableListOf<String>()
networkInfo.routes?.forEach { route ->
route.proxy_cidrs?.let { cidrs -> newProxyCidrs.addAll(cidrs) }
}
// 检查是否有变化
val ipv4Changed = newIpv4 != currentIpv4
val proxyCidrsChanged = newProxyCidrs != currentProxyCidrs
if (ipv4Changed || proxyCidrsChanged) {
Log.i(TAG, "网络状态发生变化:")
Log.i(TAG, " IPv4: $currentIpv4 -> $newIpv4")
Log.i(TAG, " Proxy CIDRs: $currentProxyCidrs -> $newProxyCidrs")
// 更新状态
currentIpv4 = newIpv4
currentProxyCidrs = newProxyCidrs.toList()
// 重启 VpnService
if (newIpv4 != null) {
restartVpnService(newIpv4, newProxyCidrs)
}
} else {
Log.d(TAG, "网络状态无变化 - IPv4: $currentIpv4, Proxy CIDRs: ${currentProxyCidrs.size}")
}
} catch (e: Exception) {
Log.e(TAG, "监控网络状态时发生异常", e)
}
}
/** 解析网络信息 JSON */
private fun parseNetworkInfo(jsonString: String): NetworkInstanceRunningInfoMap? {
return try {
adapter.fromJson(jsonString)
} catch (e: Exception) {
Log.e(TAG, "解析网络信息失败", e)
null
}
}
/** 重启 VpnService */
private fun restartVpnService(ipv4: String, proxyCidrs: List<String>) {
try {
// 先停止现有的 VpnService
stopVpnService()
// 启动新的 VpnService
startVpnService(ipv4, proxyCidrs)
} catch (e: Exception) {
Log.e(TAG, "重启 VpnService 时发生异常", e)
}
}
/** 启动 VpnService */
private fun startVpnService(ipv4: String, proxyCidrs: List<String>) {
try {
val intent = Intent(activity, EasyTierVpnService::class.java)
intent.putExtra("ipv4_address", ipv4)
intent.putStringArrayListExtra("proxy_cidrs", ArrayList(proxyCidrs))
intent.putExtra("instance_name", instanceName)
activity.startService(intent)
vpnServiceIntent = intent
Log.i(TAG, "VpnService 已启动 - IPv4: $ipv4, Proxy CIDRs: $proxyCidrs")
} catch (e: Exception) {
Log.e(TAG, "启动 VpnService 时发生异常", e)
}
}
/** 停止 VpnService */
private fun stopVpnService() {
try {
vpnServiceIntent?.let { intent ->
activity.stopService(intent)
Log.i(TAG, "VpnService 已停止")
}
vpnServiceIntent = null
} catch (e: Exception) {
Log.e(TAG, "停止 VpnService 时发生异常", e)
}
}
/** 获取当前状态信息 */
fun getStatus(): EasyTierStatus {
return EasyTierStatus(
isRunning = isRunning,
instanceName = instanceName,
currentIpv4 = currentIpv4,
currentProxyCidrs = currentProxyCidrs.toList()
)
}
/** 状态数据类 */
data class EasyTierStatus(
val isRunning: Boolean,
val instanceName: String,
val currentIpv4: String?,
val currentProxyCidrs: List<String>
)
}
@@ -0,0 +1,143 @@
package com.easytier.jni
import android.content.Intent
import android.net.VpnService
import android.os.ParcelFileDescriptor
import android.util.Log
import kotlin.concurrent.thread
class EasyTierVpnService : VpnService() {
private var vpnInterface: ParcelFileDescriptor? = null
private var isRunning = false
private var instanceName: String? = null
companion object {
private const val TAG = "EasyTierVpnService"
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "VPN Service created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 获取传入的参数
val ipv4Address = intent?.getStringExtra("ipv4_address")
val proxyCidrs = intent?.getStringArrayListExtra("proxy_cidrs") ?: arrayListOf()
instanceName = intent?.getStringExtra("instance_name")
if (ipv4Address == null || instanceName == null) {
Log.e(TAG, "缺少必要参数: ipv4Address=$ipv4Address, instanceName=$instanceName")
stopSelf()
return START_NOT_STICKY
}
Log.i(
TAG,
"启动 VPN Service - IPv4: $ipv4Address, Proxy CIDRs: $proxyCidrs, Instance: $instanceName"
)
thread {
try {
setupVpnInterface(ipv4Address, proxyCidrs)
} catch (t: Throwable) {
Log.e(TAG, "VPN 设置失败", t)
stopSelf()
}
}
return START_STICKY
}
private fun setupVpnInterface(ipv4Address: String, proxyCidrs: List<String>) {
try {
// 解析 IPv4 地址和网络长度
val (ip, networkLength) = parseIpv4Address(ipv4Address)
// 1. 准备 VpnService.Builder
val builder = Builder()
builder.setSession("EasyTier VPN")
.addAddress(ip, networkLength)
.addDnsServer("223.5.5.5")
.addDnsServer("114.114.114.114")
.addDisallowedApplication("com.easytier.easytiervpn")
// 2. 添加路由表 - 为每个 proxy CIDR 添加路由
proxyCidrs.forEach { cidr ->
try {
val (routeIp, routeLength) = parseCidr(cidr)
builder.addRoute(routeIp, routeLength)
Log.d(TAG, "添加路由: $routeIp/$routeLength")
} catch (e: Exception) {
Log.w(TAG, "解析 CIDR 失败: $cidr", e)
}
}
// 3. 构建虚拟网络接口
vpnInterface = builder.establish()
if (vpnInterface == null) {
Log.e(TAG, "创建 VPN 接口失败")
return
}
Log.i(TAG, "VPN 接口创建成功")
// 4. 将 TUN 文件描述符传递给 EasyTier
instanceName?.let { name ->
val fd = vpnInterface!!.fd
val result = EasyTierJNI.setTunFd(name, fd)
if (result == 0) {
Log.i(TAG, "TUN 文件描述符设置成功: $fd")
} else {
Log.e(TAG, "TUN 文件描述符设置失败: $result")
}
}
isRunning = true
// 5. 保持服务运行
while (isRunning && vpnInterface != null) {
Thread.sleep(1000)
}
} catch (t: Throwable) {
Log.e(TAG, "VPN 接口设置过程中发生错误", t)
} finally {
cleanup()
}
}
/** 解析 IPv4 地址,返回 IP 和网络长度 */
private fun parseIpv4Address(ipv4Address: String): Pair<String, Int> {
return if (ipv4Address.contains("/")) {
val parts = ipv4Address.split("/")
Pair(parts[0], parts[1].toInt())
} else {
// 默认使用 /24 网络
Pair(ipv4Address, 24)
}
}
/** 解析 CIDR,返回 IP 和网络长度 */
private fun parseCidr(cidr: String): Pair<String, Int> {
val parts = cidr.split("/")
if (parts.size != 2) {
throw IllegalArgumentException("无效的 CIDR 格式: $cidr")
}
return Pair(parts[0], parts[1].toInt())
}
private fun cleanup() {
isRunning = false
vpnInterface?.close()
vpnInterface = null
Log.i(TAG, "VPN 接口已清理")
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "VPN Service destroyed")
cleanup()
}
}
@@ -0,0 +1,41 @@
# 使用说明
1. 需要将 proto 文件放入 app/src/main/proto
2. android/gradle/libs.versions.toml 中加入依赖
```
# Wire 核心运行时
android-wire-runtime = { group = "com.squareup.wire", name = "wire-runtime", version = "5.3.11" }
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
android-wire-moshi-adapter = { group = "com.squareup.wire", name = "wire-moshi-adapter", version = "5.3.11" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.9.0" }
```
3. build.gradle.kts 中加入
```
plugins {
...
alias(libs.plugins.wire)
}
dependencies {
...
implementation(libs.android.wire.runtime)
implementation(libs.android.wire.moshi.adapter)
implementation(libs.moshi)
}
...
wire {
kotlin {
rpcRole = "none"
}
}
```
4. 调用 easytier-contrib/easytier-android-jni/build.sh 生成 jni 和 ffi 的 so 文件。
并将生成的 so 文件放到 android/app/src/main/jniLibs/arm64-v8a 目录下。
5. 使用 EasyTierManager 可以拉起 EasyTier 实例并启动 Android VpnService 组件。
@@ -0,0 +1,124 @@
use std::{
ffi::{CStr, c_char, c_void},
sync::{Arc, Mutex, MutexGuard},
};
use easytier_ffi::ConfigServerEventCallback;
use jni::JNIEnv;
use jni::objects::{GlobalRef, JObject, JValue};
use once_cell::sync::Lazy;
use crate::error;
pub(crate) struct JniConfigServerCallback {
java_vm: jni::JavaVM,
callback: GlobalRef,
}
static CONFIG_SERVER_CALLBACK: Lazy<Mutex<Option<Arc<JniConfigServerCallback>>>> =
Lazy::new(|| Mutex::new(None));
pub(crate) fn lock_callback_storage()
-> Result<MutexGuard<'static, Option<Arc<JniConfigServerCallback>>>, String> {
CONFIG_SERVER_CALLBACK
.lock()
.map_err(|e| format!("Failed to lock config server callback: {}", e))
}
pub(crate) fn new_callback(
env: &mut JNIEnv,
callback: &JObject,
) -> Result<Arc<JniConfigServerCallback>, String> {
let java_vm = env
.get_java_vm()
.map_err(|e| format!("Failed to get JavaVM: {:?}", e))?;
let callback = env
.new_global_ref(callback)
.map_err(|e| format!("Failed to create callback global ref: {:?}", e))?;
Ok(Arc::new(JniConfigServerCallback { java_vm, callback }))
}
pub(crate) fn callback_fn(
callback: &Option<Arc<JniConfigServerCallback>>,
) -> ConfigServerEventCallback {
callback
.as_ref()
.map(|_| config_server_event_callback as unsafe extern "C" fn(*const c_char, *mut c_void))
}
pub(crate) fn user_data(callback: &Option<Arc<JniConfigServerCallback>>) -> *mut c_void {
callback
.as_ref()
.map(|callback| Arc::as_ptr(callback) as *mut c_void)
.unwrap_or(std::ptr::null_mut())
}
impl JniConfigServerCallback {
fn clear_pending_exception(
env: &mut JNIEnv,
context: &str,
error: &dyn std::fmt::Debug,
) -> String {
match env.exception_check() {
Ok(true) => {
if let Err(clear_err) = env.exception_clear() {
return format!(
"{}: {:?}; failed to clear pending Java exception: {:?}",
context, error, clear_err
);
}
}
Ok(false) => {}
Err(check_err) => {
return format!(
"{}: {:?}; failed to check pending Java exception: {:?}",
context, error, check_err
);
}
}
format!("{}: {:?}", context, error)
}
fn on_event(&self, event_json: *const c_char) -> Result<(), String> {
let event_json = unsafe { CStr::from_ptr(event_json) }
.to_str()
.map_err(|e| format!("Invalid config server event JSON: {:?}", e))?;
let mut env = self
.java_vm
.attach_current_thread()
.map_err(|e| format!("Failed to attach callback thread: {:?}", e))?;
let event_json = env.new_string(event_json).map_err(|e| {
Self::clear_pending_exception(&mut env, "Failed to create event string", &e)
})?;
if let Err(e) = env.call_method(
self.callback.as_obj(),
"onEvent",
"(Ljava/lang/String;)V",
&[JValue::from(&event_json)],
) {
return Err(Self::clear_pending_exception(
&mut env,
"Failed to call config server callback",
&e,
));
}
Ok(())
}
}
unsafe extern "C" fn config_server_event_callback(
event_json: *const c_char,
user_data: *mut c_void,
) {
if event_json.is_null() || user_data.is_null() {
return;
}
let callback = unsafe { &*(user_data as *const JniConfigServerCallback) };
if let Err(error) = callback.on_event(event_json) {
error::set_callback_error(error);
}
}
@@ -0,0 +1,140 @@
use std::ptr;
use easytier_ffi::{
in_config_server_callback, is_config_server_client_connected, start_config_server_client,
stop_config_server_client,
};
use jni::JNIEnv;
use jni::objects::{JClass, JObject, JString};
use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean, jint};
use crate::{
callback, error,
strings::{jstring_to_cstring, optional_jstring_to_cstring},
};
pub(crate) fn start_config_server_client_jni(
env: &mut JNIEnv,
config_server_url: JString,
hostname: JString,
machine_id: JString,
secure_mode: jboolean,
callback_obj: JObject,
) -> jint {
if in_config_server_callback() {
error::throw_exception(
env,
"Cannot start config server client from config server callback",
);
return -1;
}
let config_server_url = match jstring_to_cstring(env, &config_server_url) {
Ok(cstr) => cstr,
Err(e) => {
error::throw_exception(env, &format!("Invalid config server URL: {}", e));
return -1;
}
};
let hostname = match optional_jstring_to_cstring(env, &hostname) {
Ok(cstr) => cstr,
Err(e) => {
error::throw_exception(env, &format!("Invalid hostname: {}", e));
return -1;
}
};
let machine_id = match jstring_to_cstring(env, &machine_id) {
Ok(cstr) => cstr,
Err(e) => {
error::throw_exception(env, &format!("Invalid machine ID: {}", e));
return -1;
}
};
let callback_ref = if callback_obj.is_null() {
None
} else {
match callback::new_callback(env, &callback_obj) {
Ok(state) => Some(state),
Err(e) => {
error::throw_exception(env, &e);
return -1;
}
}
};
let mut callback_guard = match callback::lock_callback_storage() {
Ok(guard) => guard,
Err(e) => {
error::throw_exception(env, &e);
return -1;
}
};
if callback_guard.is_none() {
error::clear_callback_error();
}
let callback_fn = callback::callback_fn(&callback_ref);
let user_data = callback::user_data(&callback_ref);
let result = unsafe {
start_config_server_client(
config_server_url.as_ptr(),
hostname
.as_ref()
.map(|value| value.as_ptr())
.unwrap_or(ptr::null()),
machine_id.as_ptr(),
secure_mode == JNI_TRUE,
callback_fn,
user_data,
)
};
if result != 0 {
if let Some(error_msg) = error::get_last_error() {
error::throw_exception(env, &error_msg);
}
return result;
}
*callback_guard = callback_ref;
result
}
pub(crate) fn stop_config_server_client_jni(mut env: JNIEnv, _class: JClass) -> jint {
if in_config_server_callback() {
let result = stop_config_server_client();
if result != 0
&& let Some(error_msg) = error::get_last_error()
{
error::throw_exception(&mut env, &error_msg);
}
return result;
}
let mut callback_guard = match callback::lock_callback_storage() {
Ok(guard) => guard,
Err(e) => {
error::throw_exception(&mut env, &e);
return -1;
}
};
let result = stop_config_server_client();
if result != 0 {
if let Some(error_msg) = error::get_last_error() {
error::throw_exception(&mut env, &error_msg);
}
return result;
}
*callback_guard = None;
result
}
pub(crate) fn is_config_server_client_connected_jni(_env: JNIEnv, _class: JClass) -> jboolean {
if is_config_server_client_connected() != 0 {
JNI_TRUE
} else {
JNI_FALSE
}
}
@@ -0,0 +1,74 @@
use std::{
ffi::{CStr, c_char},
ptr,
sync::Mutex,
};
use easytier_ffi::{free_string, get_error_msg};
use jni::JNIEnv;
use jni::objects::JClass;
use jni::sys::jstring;
use once_cell::sync::Lazy;
static JNI_CALLBACK_ERROR: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
pub(crate) fn set_callback_error(error: String) {
log::error!("{}", error);
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
*guard = Some(error);
}
}
pub(crate) fn clear_callback_error() {
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
*guard = None;
}
}
fn take_callback_error() -> Option<String> {
JNI_CALLBACK_ERROR
.lock()
.ok()
.and_then(|mut guard| guard.take())
}
fn get_ffi_last_error() -> Option<String> {
unsafe {
let mut error_ptr: *const c_char = ptr::null();
get_error_msg(&mut error_ptr);
if error_ptr.is_null() {
None
} else {
let error_cstr = CStr::from_ptr(error_ptr);
let error_str = error_cstr.to_string_lossy().into_owned();
free_string(error_ptr);
Some(error_str)
}
}
}
pub(crate) fn get_last_error() -> Option<String> {
match (get_ffi_last_error(), take_callback_error()) {
(Some(ffi_error), Some(callback_error)) => Some(format!(
"{}; config server callback error: {}",
ffi_error, callback_error
)),
(Some(ffi_error), None) => Some(ffi_error),
(None, Some(callback_error)) => Some(callback_error),
(None, None) => None,
}
}
pub(crate) fn throw_exception(env: &mut JNIEnv, message: &str) {
let _ = env.throw_new("java/lang/RuntimeException", message);
}
pub(crate) fn get_last_error_jni(env: JNIEnv, _class: JClass) -> jstring {
match get_last_error() {
Some(error) => match env.new_string(&error) {
Ok(jstr) => jstr.into_raw(),
Err(_) => ptr::null_mut(),
},
None => ptr::null_mut(),
}
}
@@ -0,0 +1,91 @@
use std::{
ffi::{CStr, c_char},
ptr,
};
use easytier_ffi::{call_json_rpc, free_string};
use jni::JNIEnv;
use jni::objects::{JClass, JString};
use jni::sys::jstring;
use crate::{
error::{get_last_error, throw_exception},
strings::{jstring_to_cstring, optional_jstring_to_cstring},
};
pub(crate) fn call_json_rpc_jni(
mut env: JNIEnv,
_class: JClass,
service_name: JString,
method_name: JString,
domain_name: JString,
payload_json: JString,
) -> jstring {
let service_name_cstr = match jstring_to_cstring(&mut env, &service_name) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid service name: {}", e));
return ptr::null_mut();
}
};
let method_name_cstr = match jstring_to_cstring(&mut env, &method_name) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid method name: {}", e));
return ptr::null_mut();
}
};
let domain_name_cstr = match optional_jstring_to_cstring(&mut env, &domain_name) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid domain name: {}", e));
return ptr::null_mut();
}
};
let payload_json_cstr = match jstring_to_cstring(&mut env, &payload_json) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid payload JSON: {}", e));
return ptr::null_mut();
}
};
let domain_name_ptr = domain_name_cstr
.as_ref()
.map_or(ptr::null(), |cstr| cstr.as_ptr());
let mut response_ptr: *const c_char = ptr::null();
let result = unsafe {
call_json_rpc(
service_name_cstr.as_ptr(),
method_name_cstr.as_ptr(),
domain_name_ptr,
payload_json_cstr.as_ptr(),
&mut response_ptr,
)
};
if result != 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
return ptr::null_mut();
}
if response_ptr.is_null() {
throw_exception(&mut env, "JSON RPC returned a null response");
return ptr::null_mut();
}
let response = unsafe { CStr::from_ptr(response_ptr) }
.to_string_lossy()
.into_owned();
free_string(response_ptr);
match env.new_string(&response) {
Ok(jstr) => jstr.into_raw(),
Err(_) => {
throw_exception(&mut env, "Failed to create JSON RPC response string");
ptr::null_mut()
}
}
}
@@ -0,0 +1,253 @@
//! JNI facade for Android callers of EasyTier.
//!
//! This file intentionally lists every Java-visible native method exported by
//! `libeasytier_android_jni.so`. The implementation details live in sibling
//! modules so this facade stays readable as an API map.
//!
//! Network management APIs:
//! - `setTunFd(instanceName, fd)`: attach an Android TUN fd to an instance.
//! - `parseConfig(config)`: validate TOML config text.
//! - `runNetworkInstance(config)`: start a local network instance.
//! - `retainNetworkInstance(instanceNames)`: retain named instances and stop the rest.
//! - `listInstances()`: return running instance names and IDs as JSON.
//! - `collectNetworkInfos()`: return running instance info as a JSON string.
//! - `callJsonRpc(...)`: call an exposed EasyTier RPC service with JSON payload.
//!
//! Config server client APIs:
//! - `startConfigServerClient(url, hostname, machineId, secureMode, callback)`:
//! start the managed remote config client.
//! - `stopConfigServerClient()`: stop the managed client and release its Java callback.
//! - `isConfigServerClientConnected()`: return whether the managed client is connected.
//!
//! Error API:
//! - `getLastError()`: return the latest FFI/JNI error string for the calling thread.
//!
mod callback;
mod config_server_api;
mod error;
mod json_rpc_api;
mod logger;
mod network_api;
mod strings;
use jni::JNIEnv;
use jni::objects::{JClass, JObject, JObjectArray, JString};
use jni::sys::{jboolean, jint, jstring};
/// Attach a TUN file descriptor to an EasyTier network instance.
///
/// Java signature:
/// `EasyTierJNI.setTunFd(instanceName: String, fd: Int): Int`
///
/// `instanceName` must name an instance known to the shared FFI instance cache.
/// The `fd` must be a valid Android TUN file descriptor. On failure this
/// returns `-1` and throws `RuntimeException` with the FFI error message when
/// one is available.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
env: JNIEnv,
class: JClass,
inst_name: JString,
fd: jint,
) -> jint {
logger::init();
network_api::set_tun_fd_jni(env, class, inst_name, fd)
}
/// Validate a TOML network config string.
///
/// Java signature:
/// `EasyTierJNI.parseConfig(config: String): Int`
///
/// This only validates the config text; it does not start or mutate any
/// instance. On failure this returns `-1` and throws `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
env: JNIEnv,
class: JClass,
config: JString,
) -> jint {
logger::init();
network_api::parse_config_jni(env, class, config)
}
/// Start one local EasyTier network instance from TOML config text.
///
/// Java signature:
/// `EasyTierJNI.runNetworkInstance(config: String): Int`
///
/// The instance name in the config must be unique in the FFI instance cache.
/// On failure this returns `-1` and throws `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
env: JNIEnv,
class: JClass,
config: JString,
) -> jint {
logger::init();
network_api::run_network_instance_jni(env, class, config)
}
/// Retain the named network instances and stop all other instances.
///
/// Java signature:
/// `EasyTierJNI.retainNetworkInstance(instanceNames: Array<String>?): Int`
///
/// Passing `null` or an empty array stops all instances. Null elements inside a
/// non-empty array are invalid. On failure this returns `-1` and throws
/// `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
env: JNIEnv,
class: JClass,
instance_names: JObjectArray,
) -> jint {
logger::init();
network_api::retain_network_instance_jni(env, class, instance_names)
}
/// Collect running network instance information.
///
/// Java signature:
/// `EasyTierJNI.collectNetworkInfos(maxLength: Int): String?`
///
/// Returns a JSON string containing `NetworkInstanceRunningInfoMap`, or null if
/// collection fails. `maxLength` limits how many FFI entries are collected. On
/// failure this throws `RuntimeException` when an error message is available.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
env: JNIEnv,
class: JClass,
max_length: jint,
) -> jstring {
logger::init();
network_api::collect_network_infos_jni(env, class, max_length)
}
/// List running network instance names and IDs.
///
/// Java signature:
/// `EasyTierJNI.listInstances(maxLength: Int): String?`
///
/// Returns a JSON object whose keys are instance names and whose values are
/// instance ID strings. On failure this returns null and throws
/// `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_listInstances(
env: JNIEnv,
class: JClass,
max_length: jint,
) -> jstring {
logger::init();
network_api::list_instances_jni(env, class, max_length)
}
/// Call an exposed EasyTier RPC method using protobuf JSON.
///
/// Java signature:
/// `EasyTierJNI.callJsonRpc(serviceName, methodName, domainName, payloadJson): String?`
///
/// Instance lifecycle management RPCs are intentionally not exposed here. Use
/// the dedicated EasyTierJNI instance APIs for start/retain/delete/collect.
/// `payloadJson` must include any `instance` selector required by the target
/// RPC. On failure this returns null and throws `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_callJsonRpc(
env: JNIEnv,
class: JClass,
service_name: JString,
method_name: JString,
domain_name: JString,
payload_json: JString,
) -> jstring {
logger::init();
json_rpc_api::call_json_rpc_jni(
env,
class,
service_name,
method_name,
domain_name,
payload_json,
)
}
/// Return the latest FFI/JNI error string for the calling thread.
///
/// Java signature:
/// `EasyTierJNI.getLastError(): String?`
///
/// This combines the FFI thread-local error with any pending config-server Java
/// callback error. It returns null when no error is available.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
env: JNIEnv,
class: JClass,
) -> jstring {
error::get_last_error_jni(env, class)
}
/// Start the managed config-server client.
///
/// Java signature:
/// `EasyTierJNI.startConfigServerClient(url, hostname, machineId, secureMode, callback): Int`
///
/// JNI only converts Java values and keeps the Java callback alive. The FFI
/// layer owns singleton lifecycle, config-server/data-plane mutual exclusion,
/// remote instance tracking, and callback event timing. If `callback` is
/// non-null, each remote apply/delete event is delivered to
/// `ConfigServerEventCallback.onEvent(eventJson)`.
///
/// On failure this returns `-1` and throws `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_startConfigServerClient(
mut env: JNIEnv,
_class: JClass,
config_server_url: JString,
hostname: JString,
machine_id: JString,
secure_mode: jboolean,
callback: JObject,
) -> jint {
logger::init();
config_server_api::start_config_server_client_jni(
&mut env,
config_server_url,
hostname,
machine_id,
secure_mode,
callback,
)
}
/// Stop the managed config-server client.
///
/// Java signature:
/// `EasyTierJNI.stopConfigServerClient(): Int`
///
/// The FFI layer performs the actual stop and managed instance cleanup. JNI
/// releases the Java callback reference after FFI stop succeeds. On failure
/// this returns `-1` and throws `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_stopConfigServerClient(
env: JNIEnv,
class: JClass,
) -> jint {
logger::init();
config_server_api::stop_config_server_client_jni(env, class)
}
/// Report whether the managed config-server client is connected.
///
/// Java signature:
/// `EasyTierJNI.isConfigServerClientConnected(): Boolean`
///
/// Returns `JNI_TRUE` only when the FFI config-server client exists and reports
/// connected.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_isConfigServerClientConnected(
env: JNIEnv,
class: JClass,
) -> jboolean {
logger::init();
config_server_api::is_config_server_client_connected_jni(env, class)
}
@@ -0,0 +1,13 @@
use once_cell::sync::Lazy;
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Debug)
.with_tag("EasyTier-JNI"),
);
});
pub(crate) fn init() {
Lazy::force(&LOGGER_INIT);
}
@@ -0,0 +1,261 @@
use std::{ffi::CStr, ptr};
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
use easytier_ffi::{
KeyValuePair, collect_network_infos, free_string, list_instance, parse_config,
retain_network_instance, run_network_instance, set_tun_fd,
};
use jni::JNIEnv;
use jni::objects::{JClass, JObjectArray, JString};
use jni::sys::{jint, jstring};
use crate::{
error::{get_last_error, throw_exception},
strings::jstring_to_cstring,
};
pub(crate) fn set_tun_fd_jni(
mut env: JNIEnv,
_class: JClass,
inst_name: JString,
fd: jint,
) -> jint {
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
return -1;
}
};
unsafe {
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
if result != 0
&& let Some(error) = get_last_error()
{
throw_exception(&mut env, &error);
}
result
}
}
pub(crate) fn parse_config_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
let config_cstr = match jstring_to_cstring(&mut env, &config) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid config string: {}", e));
return -1;
}
};
unsafe {
let result = parse_config(config_cstr.as_ptr());
if result != 0
&& let Some(error) = get_last_error()
{
throw_exception(&mut env, &error);
}
result
}
}
pub(crate) fn run_network_instance_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
let config_cstr = match jstring_to_cstring(&mut env, &config) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid config string: {}", e));
return -1;
}
};
unsafe {
let result = run_network_instance(config_cstr.as_ptr());
if result != 0
&& let Some(error) = get_last_error()
{
throw_exception(&mut env, &error);
}
result
}
}
pub(crate) fn retain_network_instance_jni(
mut env: JNIEnv,
_class: JClass,
instance_names: JObjectArray,
) -> jint {
if instance_names.is_null() {
return retain_all(&mut env);
}
let array_length = match env.get_array_length(&instance_names) {
Ok(len) => len as usize,
Err(e) => {
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
return -1;
}
};
if array_length == 0 {
return retain_all(&mut env);
}
let mut c_strings = Vec::with_capacity(array_length);
let mut c_string_ptrs = Vec::with_capacity(array_length);
for i in 0..array_length {
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
Ok(obj) => obj,
Err(e) => {
throw_exception(
&mut env,
&format!("Failed to get array element {}: {:?}", i, e),
);
return -1;
}
};
if java_string.is_null() {
throw_exception(
&mut env,
&format!("Invalid instance name at index {}: null", i),
);
return -1;
}
let jstring = JString::from(java_string);
let c_string = match jstring_to_cstring(&mut env, &jstring) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(
&mut env,
&format!("Invalid instance name at index {}: {}", i, e),
);
return -1;
}
};
c_string_ptrs.push(c_string.as_ptr());
c_strings.push(c_string);
}
unsafe {
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
if result != 0
&& let Some(error) = get_last_error()
{
throw_exception(&mut env, &error);
}
result
}
}
fn retain_all(env: &mut JNIEnv) -> jint {
unsafe {
let result = retain_network_instance(ptr::null(), 0);
if result != 0
&& let Some(error) = get_last_error()
{
throw_exception(env, &error);
}
result
}
}
pub(crate) fn collect_network_infos_jni(
mut env: JNIEnv,
_class: JClass,
max_length: jint,
) -> jstring {
let max_length = max_length.max(0) as usize;
let mut infos = vec![
KeyValuePair {
key: ptr::null(),
value: ptr::null(),
};
max_length
];
unsafe {
let count = collect_network_infos(infos.as_mut_ptr(), max_length);
if count < 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
return ptr::null_mut();
}
let mut ret = NetworkInstanceRunningInfoMap::default();
for info in infos.iter().take(count as usize) {
let key_ptr = info.key;
let val_ptr = info.value;
if key_ptr.is_null() || val_ptr.is_null() {
break;
}
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
free_string(key_ptr);
free_string(val_ptr);
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(&val) {
Ok(v) => v,
Err(_) => {
throw_exception(&mut env, "Failed to parse JSON");
continue;
}
};
ret.map.insert(key, value);
}
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
match env.new_string(&json_str) {
Ok(jstr) => jstr.into_raw(),
Err(_) => {
throw_exception(&mut env, "Failed to create JSON string");
ptr::null_mut()
}
}
}
}
pub(crate) fn list_instances_jni(mut env: JNIEnv, _class: JClass, max_length: jint) -> jstring {
let max_length = max_length.max(0) as usize;
let mut infos = vec![
KeyValuePair {
key: ptr::null(),
value: ptr::null(),
};
max_length
];
unsafe {
let count = list_instance(infos.as_mut_ptr(), max_length);
if count < 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
return ptr::null_mut();
}
let mut ret = serde_json::Map::new();
for info in infos.iter().take(count as usize) {
let key_ptr = info.key;
let val_ptr = info.value;
if key_ptr.is_null() || val_ptr.is_null() {
break;
}
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
free_string(key_ptr);
free_string(val_ptr);
ret.insert(key, serde_json::Value::String(val));
}
let json_str = serde_json::Value::Object(ret).to_string();
match env.new_string(&json_str) {
Ok(jstr) => jstr.into_raw(),
Err(_) => {
throw_exception(&mut env, "Failed to create instance list JSON string");
ptr::null_mut()
}
}
}
}
@@ -0,0 +1,23 @@
use std::ffi::CString;
use jni::JNIEnv;
use jni::objects::JString;
pub(crate) fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
let java_str = env
.get_string(jstr)
.map_err(|e| format!("Failed to get string: {:?}", e))?;
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
}
pub(crate) fn optional_jstring_to_cstring(
env: &mut JNIEnv,
jstr: &JString,
) -> Result<Option<CString>, String> {
if jstr.is_null() {
return Ok(None);
}
jstring_to_cstring(env, jstr).map(Some)
}
+22 -4
View File
@@ -1,16 +1,34 @@
[package]
name = "easytier-ffi"
version = "0.1.0"
edition = "2021"
edition.workspace = true
[lib]
crate-type = ["cdylib"]
crate-type = ["cdylib", "rlib"]
[features]
default = ["c-abi", "ffi-dataplane"]
c-abi = []
ffi-dataplane = [
"easytier/ffi-dataplane",
"easytier-core/proxy-smoltcp-stack",
]
macos-ne = ["easytier/macos-ne"]
[dependencies]
easytier = { path = "../../easytier" }
easytier = { path = "../../easytier", features = ["tracing-log"] }
easytier-core = { path = "../../easytier-core" }
once_cell = "1.18.0"
dashmap = "6.0"
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
async-trait = "0.1"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
uuid = "1.17.0"
[build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
"win7",
] }
@@ -0,0 +1,108 @@
# Native data-plane ABI v3
The native data-plane ABI is a thin adapter over the instance-owned
`DataPlaneSession`. It does not own sockets, operation state, completion
queues, routing policy, or timeouts.
## Conventions
- Every immediate call returns `0` on success or a negative
`DataPlaneErrorKind` value on failure.
- `data_plane_completion_wait` returns `1` when a completion is ready, `0` on
timeout or session close, and a negative error value on failure.
- `data_plane_completion_drain` returns a non-negative descriptor count or a
negative error value.
- Handle zero is invalid.
- `timeout_ms == UINT64_MAX` means no deadline.
- TCP connect/bind/accept and UDP bind timeouts start when submission is
accepted.
- TCP streams and UDP sockets have persistent read and write deadlines.
`data_plane_resource_deadline_set` replaces the selected directions'
deadlines immediately, including for active operations. An expired deadline
remains expired until it is replaced or cleared with `UINT64_MAX`.
- Deadline direction `1` selects reads, `2` selects writes, and `3` selects
both.
- Request and write bytes are copied before a submit call returns.
- Socket-address fields use native-endian integers. Address bytes are in
network order. ABI v3 accepts IPv4 only.
`DataPlaneSocketAddr` is:
```c
typedef struct {
uint16_t family; /* 4 */
uint16_t port;
uint8_t address[16]; /* IPv4 uses the first four bytes */
} DataPlaneSocketAddr;
```
`DataPlaneCompletion` is:
```c
typedef struct {
uint64_t operation_id;
uint16_t operation_kind;
uint16_t status; /* 0 or DataPlaneErrorKind */
} DataPlaneCompletion;
```
## Lifecycle
One native session may be open for an EasyTier instance at a time:
```text
data_plane_session_open
-> set resource deadlines
-> submit operations
-> completion_wait
-> completion_drain
-> typed result_take
-> resource_close / operation_free
data_plane_session_close
```
Closing a native session cancels and discards its outstanding operations and
resources and wakes a thread blocked in `data_plane_completion_wait`.
The resource and operation IDs returned by the ABI belong to that session.
They must always be passed together with the same session handle.
## Completion and result ownership
Submission returns an operation ID immediately. Completion descriptors carry
only the operation ID, operation kind, and terminal status. Draining a
descriptor makes its typed result available but does not consume it.
`data_plane_result_size` reports the TCP-read or UDP-receive payload size.
Typed result-take functions consume the result exactly once. If a supplied
buffer is too small, they return `-BufferTooSmall` and leave the result
available for a later call.
Call `data_plane_operation_free` when a drained result is intentionally
abandoned. Call `data_plane_resource_close` for TCP streams, listeners, and
UDP sockets.
## Operation kinds
| Value | Operation |
| ---: | --- |
| 1 | TCP connect |
| 2 | TCP bind |
| 3 | TCP accept |
| 4 | TCP read |
| 5 | TCP write |
| 6 | UDP bind |
| 7 | UDP receive |
| 8 | UDP send |
The exported function families are:
- `data_plane_tcp_*_submit`
- `data_plane_udp_*_submit`
- `data_plane_resource_deadline_set`
- `data_plane_completion_wait`
- `data_plane_completion_drain`
- `data_plane_*_result_take`
- `data_plane_operation_cancel`
- `data_plane_operation_free`
- `data_plane_resource_close`
+8
View File
@@ -0,0 +1,8 @@
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
thunk::thunk();
}
}
@@ -0,0 +1,100 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h> // for sleep
// FFI struct and function declarations
typedef struct {
const char* key;
const char* value;
} KeyValuePair;
typedef void (*config_server_event_callback)(
const char* event_json,
void* user_data
);
extern int parse_config(const char* cfg_str);
extern int run_network_instance(const char* cfg_str);
extern void get_error_msg(const char** out);
extern void free_string(const char* s);
extern int collect_network_infos(KeyValuePair* infos, size_t max_length);
extern int start_config_server_client(
const char* config_server_url,
const char* hostname,
const char* machine_id,
bool secure_mode,
config_server_event_callback callback,
void* user_data
);
extern int stop_config_server_client(void);
extern int is_config_server_client_connected(void);
static void on_config_server_event(const char* event_json, void* user_data) {
(void)user_data;
printf("config server event: %s\n", event_json);
}
int main() {
const char* config = "inst_name = \"test\"\nnetwork = \"test_network\"\n";
int ret;
// 调用 parse_config
ret = parse_config(config);
if (ret != 0) {
const char* err = NULL;
get_error_msg(&err);
if (err) {
printf("parse_config error: %s\n", err);
free_string(err);
}
return 1;
}
printf("parse_config success\n");
// 调用 run_network_instance
ret = run_network_instance(config);
if (ret != 0) {
const char* err = NULL;
get_error_msg(&err);
if (err) {
printf("run_network_instance error: %s\n", err);
free_string(err);
}
return 1;
}
printf("run_network_instance success\n");
// 周期性调用 collect_network_infos 并打印
const size_t max_infos = 8;
KeyValuePair* infos = (KeyValuePair*)malloc(sizeof(KeyValuePair) * max_infos);
if (!infos) {
fprintf(stderr, "malloc failed\n");
return 1;
}
for (int i = 0; i < 5; ++i) { // 循环5次作为示例
memset(infos, 0, sizeof(KeyValuePair) * max_infos);
int count = collect_network_infos(infos, max_infos);
if (count < 0) {
const char* err = NULL;
get_error_msg(&err);
if (err) {
printf("collect_network_infos error: %s\n", err);
free_string(err);
}
break;
}
printf("collect_network_infos: %d instance(s)\n", count);
for (int j = 0; j < count; ++j) {
printf(" [%d] key: %s\n value: %s\n", j, infos[j].key, infos[j].value);
free_string(infos[j].key);
free_string(infos[j].value);
}
sleep(1);
}
free(infos);
return 0;
}
@@ -0,0 +1,2 @@
github.com/go-webgpu/goffi v0.4.1 h1:2hQH5XXloxTyTtIleYv+Rajlwzp6UOETURhSZ5+zJxU=
github.com/go-webgpu/goffi v0.4.1/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
@@ -0,0 +1,482 @@
use std::{
cell::Cell,
collections::HashSet,
ffi::{CString, c_char, c_int, c_void},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
use easytier::{
common::{
MachineIdOptions,
config::{ConfigLoader as _, TomlConfigLoader},
},
web_client::{WebClient, WebClientHooks, parse_config_server_endpoint, run_web_client},
};
use uuid::Uuid;
use crate::{
data_plane::remove_data_plane_sessions_by_instance_ids,
error::set_error_msg,
state::{ffi_context, resolve_instance_id_by_name},
strings::{c_str_to_string, optional_c_str_to_string},
types::ConfigServerEventCallback,
};
thread_local! {
static IN_CONFIG_SERVER_CALLBACK: Cell<bool> = const { Cell::new(false) };
}
static CONFIG_SERVER_CLIENT: once_cell::sync::Lazy<Mutex<Option<ManagedConfigServerClient>>> =
once_cell::sync::Lazy::new(|| Mutex::new(None));
static CONFIG_SERVER_CLIENT_ACTIVE: once_cell::sync::Lazy<AtomicBool> =
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
static CONFIG_SERVER_CLIENT_STOPPING: once_cell::sync::Lazy<AtomicBool> =
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
static LAST_CONFIG_SERVER_CALLBACK_ERROR: once_cell::sync::Lazy<Mutex<Option<String>>> =
once_cell::sync::Lazy::new(|| Mutex::new(None));
pub(crate) struct ConfigServerCallbackScope;
impl ConfigServerCallbackScope {
pub(crate) fn enter() -> Self {
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(true));
Self
}
}
impl Drop for ConfigServerCallbackScope {
fn drop(&mut self) {
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(false));
}
}
pub fn in_config_server_callback() -> bool {
IN_CONFIG_SERVER_CALLBACK.with(Cell::get)
}
fn config_server_machine_id_options(machine_id: String) -> MachineIdOptions {
MachineIdOptions {
explicit_machine_id: Some(machine_id),
state_dir: None,
}
}
pub fn validate_config_server_client_options(
config_server_url_s: &str,
machine_id: &str,
) -> Result<(), String> {
if machine_id.trim().is_empty() {
return Err("machine_id is empty".to_string());
}
parse_config_server_endpoint(config_server_url_s)
.map(|_| ())
.map_err(|error| error.to_string())
}
struct ManagedConfigServerClient {
client: WebClient,
hooks: Arc<ManagedConfigServerClientHooks>,
}
pub(crate) struct ManagedConfigServerClientHooks {
pub(crate) instance_ids: Mutex<HashSet<Uuid>>,
callback_delivery: Mutex<()>,
stopping: AtomicBool,
callback: ConfigServerEventCallback,
user_data: usize,
}
impl ManagedConfigServerClientHooks {
pub(crate) fn new(callback: ConfigServerEventCallback, user_data: *mut c_void) -> Self {
Self {
instance_ids: Mutex::new(HashSet::new()),
callback_delivery: Mutex::new(()),
stopping: AtomicBool::new(false),
callback,
user_data: user_data as usize,
}
}
#[cfg(test)]
pub(crate) fn tracked_instance_ids(&self) -> Vec<Uuid> {
self.instance_ids
.lock()
.map(|guard| guard.iter().copied().collect())
.unwrap_or_default()
}
fn remove_tracked_instance_ids(&self, ids: &[Uuid]) -> Result<Vec<Uuid>, String> {
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
Ok(ids
.iter()
.filter_map(|id| guard.remove(id).then_some(*id))
.collect())
}
fn validate_instance_name(&self, inst_name: &str, inst_id: Uuid) -> Result<(), String> {
if let Some(existing_id) =
resolve_instance_id_by_name(inst_name).map_err(|error| error.to_string())?
&& existing_id != inst_id
{
return Err(format!("instance name {} already exists", inst_name));
}
Ok(())
}
pub(crate) fn start_stopping(&self) -> Vec<Uuid> {
let _delivery_guard = if in_config_server_callback() {
None
} else {
self.callback_delivery.lock().ok()
};
let mut guard = match self.instance_ids.lock() {
Ok(guard) => guard,
Err(_) => return Vec::new(),
};
self.stopping.store(true, Ordering::Release);
guard.drain().collect()
}
pub(crate) fn note_callback_error(&self, error: String) {
log::warn!("config server event callback failed: {}", error);
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
*guard = Some(error);
}
}
fn emit_event_with_delivery_locked(
&self,
event: &str,
instance_id: Uuid,
) -> Result<(), String> {
if self.stopping.load(Ordering::Acquire) {
return Ok(());
}
let Some(callback) = self.callback else {
return Ok(());
};
let instance_name = ffi_context()
.manager
.instance(instance_id)
.map(|instance| instance.instance_name().to_owned())
.unwrap_or_default();
let network_name = ffi_context()
.manager
.config(instance_id)
.map(|config| config.get_network_identity().network_name)
.unwrap_or_default();
let event_json = serde_json::json!({
"event": event,
"success": true,
"instance_id": instance_id.to_string(),
"instance_name": instance_name,
"network_name": network_name,
"error": null,
})
.to_string();
let event_json = CString::new(event_json).map_err(|err| err.to_string())?;
let _callback_scope = ConfigServerCallbackScope::enter();
unsafe {
callback(event_json.as_ptr(), self.user_data as *mut c_void);
}
Ok(())
}
fn emit_event(&self, event: &str, instance_id: Uuid) -> Result<(), String> {
let _delivery_guard = self
.callback_delivery
.lock()
.map_err(|err| err.to_string())?;
self.emit_event_with_delivery_locked(event, instance_id)
}
fn wait_for_callback_delivery(&self) {
if in_config_server_callback() {
return;
}
if let Ok(guard) = self.callback_delivery.lock() {
drop(guard);
}
}
}
#[async_trait::async_trait]
impl WebClientHooks for ManagedConfigServerClientHooks {
fn manages_remote_config_instances(&self) -> bool {
true
}
async fn pre_run_network_instance(&self, cfg: &TomlConfigLoader) -> Result<(), String> {
if self.stopping.load(Ordering::Acquire) {
return Err("config server client is stopping".to_string());
}
let inst_name = cfg.get_inst_name();
let inst_id = cfg.get_id();
self.validate_instance_name(&inst_name, inst_id)
}
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
let _delivery_guard = self
.callback_delivery
.lock()
.map_err(|err| err.to_string())?;
if self.stopping.load(Ordering::Acquire) {
return Err("config server client is stopping".to_string());
}
let Some(inst_name) = ffi_context()
.manager
.instance(*id)
.map(|instance| instance.instance_name().to_owned())
else {
return Err(format!("instance {} not found after start", id));
};
self.instance_ids
.lock()
.map_err(|err| err.to_string())?
.insert(*id);
if let Err(error) = self.validate_instance_name(&inst_name, *id) {
self.remove_tracked_instance_ids(&[*id])?;
return Err(error);
}
remove_data_plane_sessions_by_instance_ids(&[*id]);
if let Err(err) = self.emit_event_with_delivery_locked("run_network_instance", *id) {
self.note_callback_error(err);
}
Ok(())
}
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
let removed_ids = self.remove_tracked_instance_ids(ids)?;
remove_data_plane_sessions_by_instance_ids(&removed_ids);
for id in removed_ids {
if let Err(err) = self.emit_event("delete_network_instance", id) {
self.note_callback_error(err);
}
}
Ok(())
}
}
pub(crate) fn remove_config_server_tracked_instance_ids(ids: &[Uuid]) {
if ids.is_empty() {
return;
}
if let Ok(guard) = CONFIG_SERVER_CLIENT.lock()
&& let Some(managed) = guard.as_ref()
&& let Err(err) = managed.hooks.remove_tracked_instance_ids(ids)
{
log::warn!("failed to remove config server tracked ids: {}", err);
}
}
pub(crate) fn wait_for_config_server_delivery() {
let hooks = CONFIG_SERVER_CLIENT
.lock()
.ok()
.and_then(|guard| guard.as_ref().map(|managed| managed.hooks.clone()));
if let Some(hooks) = hooks {
hooks.wait_for_callback_delivery();
}
}
pub(crate) fn last_callback_error() -> Option<String> {
LAST_CONFIG_SERVER_CALLBACK_ERROR
.lock()
.ok()
.and_then(|guard| guard.clone())
}
pub(crate) fn clear_last_callback_error() {
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
*guard = None;
}
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn is_config_server_active_or_stopping() -> bool {
CONFIG_SERVER_CLIENT_ACTIVE.load(Ordering::Acquire)
|| CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire)
}
#[cfg(test)]
pub(crate) fn set_active_for_test(active: bool) {
CONFIG_SERVER_CLIENT_ACTIVE.store(active, Ordering::Release);
}
/// # Safety
/// Start the config server client.
///
/// `config_server_url` must be a valid null-terminated UTF-8 string.
/// `hostname` may be null; if non-null it must be a valid null-terminated UTF-8 string.
/// `machine_id` must be a valid null-terminated UTF-8 string.
/// `event_json` passed to `callback` is valid only during that callback invocation.
pub(crate) unsafe fn start_config_server_client(
config_server_url: *const c_char,
hostname: *const c_char,
machine_id: *const c_char,
secure_mode: bool,
callback: ConfigServerEventCallback,
user_data: *mut c_void,
) -> c_int {
if in_config_server_callback() {
set_error_msg("cannot start config server client from config server callback");
return -1;
}
let config_server_url = match unsafe { c_str_to_string(config_server_url, "config_server_url") }
{
Ok(value) => value,
Err(err) => {
set_error_msg(&err);
return -1;
}
};
let hostname = match unsafe { optional_c_str_to_string(hostname, "hostname") } {
Ok(value) => value,
Err(err) => {
set_error_msg(&err);
return -1;
}
};
let machine_id = match unsafe { c_str_to_string(machine_id, "machine_id") } {
Err(err) => {
set_error_msg(&err);
return -1;
}
Ok(value) => value,
};
if let Err(err) = validate_config_server_client_options(&config_server_url, &machine_id) {
set_error_msg(&err);
return -1;
}
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
Ok(guard) => guard,
Err(err) => {
set_error_msg(&format!("failed to lock config server client: {}", err));
return -1;
}
};
if guard.is_some() {
set_error_msg("config server client already exists");
return -1;
}
if CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire) {
set_error_msg("config server client is stopping");
return -1;
}
clear_last_callback_error();
#[cfg(feature = "ffi-dataplane")]
let data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() {
Ok(guard) => guard,
Err(err) => {
set_error_msg(&err);
return -1;
}
};
CONFIG_SERVER_CLIENT_ACTIVE.store(true, Ordering::Release);
#[cfg(feature = "ffi-dataplane")]
drop(data_plane_usage_guard);
let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data));
let client = match ffi_context().runtime.block_on(run_web_client(
&config_server_url,
config_server_machine_id_options(machine_id),
hostname,
secure_mode,
ffi_context().manager.clone(),
Some(hooks.clone()),
)) {
Ok(client) => client,
Err(err) => {
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
set_error_msg(&format!("failed to start config server client: {}", err));
return -1;
}
};
*guard = Some(ManagedConfigServerClient { client, hooks });
0
}
pub(crate) fn stop_config_server_client() -> c_int {
if in_config_server_callback() {
set_error_msg("cannot stop config server client from config server callback");
return -1;
}
let guard = match CONFIG_SERVER_CLIENT.lock() {
Ok(guard) => guard,
Err(err) => {
set_error_msg(&format!("failed to lock config server client: {}", err));
return -1;
}
};
let Some(managed) = guard.as_ref() else {
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
return 0;
};
if CONFIG_SERVER_CLIENT_STOPPING.swap(true, Ordering::AcqRel) {
set_error_msg("config server client is stopping");
return -1;
}
let hooks = managed.hooks.clone();
// Keep the client discoverable until the canonical transaction drains its
// tracking. Earlier removals must still retire IDs from these same hooks.
drop(guard);
let delete_result = ffi_context().runtime.block_on(
ffi_context()
.process_management
.delete_owned_network_instances_selected_by(|| hooks.start_stopping()),
);
let managed = match CONFIG_SERVER_CLIENT.lock() {
Ok(mut guard) => guard.take(),
Err(err) => {
hooks.wait_for_callback_delivery();
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
set_error_msg(&format!("failed to lock config server client: {err}"));
return -1;
}
};
drop(managed);
hooks.wait_for_callback_delivery();
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
if let Err(err) = delete_result {
set_error_msg(&format!(
"failed to delete config server instances: {}",
err
));
return -1;
}
0
}
pub(crate) fn is_config_server_client_connected() -> c_int {
CONFIG_SERVER_CLIENT
.lock()
.ok()
.and_then(|guard| guard.as_ref().map(|managed| managed.client.is_connected()))
.map(i32::from)
.unwrap_or(0)
}
@@ -0,0 +1,685 @@
use std::{
ffi::{c_char, c_int, c_uchar},
net::{IpAddr, Ipv4Addr, SocketAddr},
ptr,
};
use easytier_core::gateway::DataPlaneErrorKind;
use super::session::{self, NativeDataPlaneError, NativeDataPlaneResult};
use crate::{
error::set_error_msg,
strings::c_str_to_string,
types::{DataPlaneCompletion, DataPlaneSocketAddr},
};
pub const DATA_PLANE_DEADLINE_READ: u32 = 1 << 0;
pub const DATA_PLANE_DEADLINE_WRITE: u32 = 1 << 1;
fn failure(error: NativeDataPlaneError) -> c_int {
set_error_msg(&error.message);
-(error.kind as c_int)
}
fn status(result: NativeDataPlaneResult<()>) -> c_int {
match result {
Ok(()) => 0,
Err(error) => failure(error),
}
}
fn invalid(message: impl Into<String>) -> NativeDataPlaneError {
NativeDataPlaneError {
kind: DataPlaneErrorKind::Io,
message: message.into(),
}
}
fn socket_addr(address: DataPlaneSocketAddr) -> NativeDataPlaneResult<SocketAddr> {
let ip = match address.family {
4 => IpAddr::V4(Ipv4Addr::new(
address.address[0],
address.address[1],
address.address[2],
address.address[3],
)),
6 => {
return Err(NativeDataPlaneError {
kind: DataPlaneErrorKind::AddressFamilyUnsupported,
message: "IPv6 is not supported by data-plane ABI v3".to_string(),
});
}
family => {
return Err(NativeDataPlaneError {
kind: DataPlaneErrorKind::AddressFamilyUnsupported,
message: format!("unsupported address family {family}"),
});
}
};
Ok(SocketAddr::new(ip, address.port))
}
fn ffi_socket_addr(address: SocketAddr) -> DataPlaneSocketAddr {
match address.ip() {
IpAddr::V4(ip) => {
let mut bytes = [0; 16];
bytes[..4].copy_from_slice(&ip.octets());
DataPlaneSocketAddr {
family: 4,
port: address.port(),
address: bytes,
}
}
IpAddr::V6(ip) => DataPlaneSocketAddr {
family: 6,
port: address.port(),
address: ip.octets(),
},
}
}
unsafe fn copy_input(ptr: *const c_uchar, len: u32) -> NativeDataPlaneResult<Vec<u8>> {
if len == 0 {
return Ok(Vec::new());
}
if ptr.is_null() {
return Err(invalid("input buffer is null"));
}
Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec())
}
unsafe fn output_slice<'a>(ptr: *mut c_uchar, len: u32) -> NativeDataPlaneResult<&'a mut [u8]> {
if len == 0 {
return Ok(&mut []);
}
if ptr.is_null() {
return Err(invalid("output buffer is null"));
}
Ok(unsafe { std::slice::from_raw_parts_mut(ptr, len as usize) })
}
fn write_operation(
out_operation: *mut u64,
submit: impl FnOnce() -> NativeDataPlaneResult<u64>,
) -> c_int {
if out_operation.is_null() {
return failure(invalid("out_operation is null"));
}
match submit() {
Ok(operation) => {
unsafe {
*out_operation = operation;
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// If non-null, `inst_name` must point to a valid NUL-terminated string.
/// `out_session` must be null or point to writable, properly aligned storage
/// for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_session_open(
inst_name: *const c_char,
out_session: *mut u64,
) -> c_int {
if out_session.is_null() {
return failure(invalid("out_session is null"));
}
unsafe {
*out_session = 0;
}
let inst_name = match unsafe { c_str_to_string(inst_name, "inst_name") } {
Ok(inst_name) => inst_name,
Err(error) => return failure(invalid(error)),
};
match session::open(&inst_name) {
Ok(handle) => {
unsafe {
*out_session = handle;
}
0
}
Err(error) => failure(error),
}
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_session_close(session: u64) -> c_int {
status(super::session::close(session))
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_connect_submit(
session: u64,
peer_addr: DataPlaneSocketAddr,
timeout_ms: u64,
out_operation: *mut u64,
) -> c_int {
let peer_addr = match socket_addr(peer_addr) {
Ok(address) => address,
Err(error) => return failure(error),
};
write_operation(out_operation, || {
super::session::submit_tcp_connect(session, peer_addr, timeout_ms)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_bind_submit(
session: u64,
local_port: u16,
timeout_ms: u64,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_tcp_bind(session, local_port, timeout_ms)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_accept_submit(
session: u64,
listener: u64,
timeout_ms: u64,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_tcp_accept(session, listener, timeout_ms)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_read_submit(
session: u64,
stream: u64,
max_len: u32,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_tcp_read(session, stream, max_len)
})
}
/// # Safety
///
/// When `len` is nonzero, `data` must point to `len` readable bytes.
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_write_submit(
session: u64,
stream: u64,
data: *const c_uchar,
len: u32,
out_operation: *mut u64,
) -> c_int {
let data = match unsafe { copy_input(data, len) } {
Ok(data) => data,
Err(error) => return failure(error),
};
write_operation(out_operation, || {
super::session::submit_tcp_write(session, stream, data)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_bind_submit(
session: u64,
local_port: u16,
timeout_ms: u64,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_udp_bind(session, local_port, timeout_ms)
})
}
/// # Safety
///
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_receive_submit(
session: u64,
socket: u64,
max_len: u32,
out_operation: *mut u64,
) -> c_int {
write_operation(out_operation, || {
super::session::submit_udp_receive(session, socket, max_len)
})
}
/// # Safety
///
/// When `len` is nonzero, `data` must point to `len` readable bytes.
/// `out_operation` must be null or point to writable, properly aligned
/// storage for one `u64`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_send_submit(
session: u64,
socket: u64,
peer_addr: DataPlaneSocketAddr,
data: *const c_uchar,
len: u32,
out_operation: *mut u64,
) -> c_int {
let peer_addr = match socket_addr(peer_addr) {
Ok(address) => address,
Err(error) => return failure(error),
};
let data = match unsafe { copy_input(data, len) } {
Ok(data) => data,
Err(error) => return failure(error),
};
write_operation(out_operation, || {
super::session::submit_udp_send(session, socket, peer_addr, data)
})
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_resource_deadline_set(
session: u64,
resource: u64,
direction: u32,
timeout_ms: u64,
) -> c_int {
let read = direction & DATA_PLANE_DEADLINE_READ != 0;
let write = direction & DATA_PLANE_DEADLINE_WRITE != 0;
if direction == 0 || direction & !(DATA_PLANE_DEADLINE_READ | DATA_PLANE_DEADLINE_WRITE) != 0 {
return failure(invalid(format!("invalid deadline direction {direction}")));
}
status(super::session::set_resource_deadline(
session, resource, read, write, timeout_ms,
))
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_operation_cancel(session: u64, operation: u64) -> c_int {
status(super::session::cancel_operation(session, operation))
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_operation_free(session: u64, operation: u64) -> c_int {
status(super::session::free_operation(session, operation))
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_resource_close(session: u64, resource: u64) -> c_int {
status(super::session::close_resource(session, resource))
}
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_completion_wait(session: u64, timeout_ms: u64) -> c_int {
match super::session::completion_wait(session, timeout_ms) {
Ok(true) => 1,
Ok(false) => 0,
Err(error) => failure(error),
}
}
/// # Safety
///
/// When `capacity` is nonzero, `completions` must point to writable, properly
/// aligned storage for `capacity` consecutive [`DataPlaneCompletion`] values.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_completion_drain(
session: u64,
completions: *mut DataPlaneCompletion,
capacity: u32,
) -> c_int {
if capacity != 0 && completions.is_null() {
return failure(invalid("completions is null"));
}
let drained = match super::session::drain_completions(session, capacity as usize) {
Ok(drained) => drained,
Err(error) => return failure(error),
};
for (index, completion) in drained.iter().enumerate() {
unsafe {
ptr::write(
completions.add(index),
DataPlaneCompletion {
operation_id: completion.operation_id.get(),
operation_kind: completion.kind as u16,
status: completion.status.code(),
},
);
}
}
drained.len() as c_int
}
/// # Safety
///
/// `out_size` must be null or point to writable, properly aligned storage for
/// one `u32`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_result_size(
session: u64,
operation: u64,
out_size: *mut u32,
) -> c_int {
if out_size.is_null() {
return failure(invalid("out_size is null"));
}
match super::session::result_size(session, operation) {
Ok(size) => match u32::try_from(size) {
Ok(size) => {
unsafe {
*out_size = size;
}
0
}
Err(_) => failure(invalid("data-plane result size exceeds u32")),
},
Err(error) => failure(error),
}
}
/// # Safety
///
/// Each output pointer must be null or point to writable, properly aligned
/// storage for its pointee type. Non-null output locations must not overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_connect_result_take(
session: u64,
operation: u64,
out_stream: *mut u64,
out_local_addr: *mut DataPlaneSocketAddr,
out_peer_addr: *mut DataPlaneSocketAddr,
) -> c_int {
if out_stream.is_null() || out_local_addr.is_null() || out_peer_addr.is_null() {
return failure(invalid("TCP connect result output pointer is null"));
}
match super::session::take_tcp_connect(session, operation) {
Ok(result) => {
unsafe {
*out_stream = result.stream;
*out_local_addr = ffi_socket_addr(result.local_addr);
*out_peer_addr = ffi_socket_addr(result.peer_addr);
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// Each output pointer must be null or point to writable, properly aligned
/// storage for its pointee type. Non-null output locations must not overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_bind_result_take(
session: u64,
operation: u64,
out_listener: *mut u64,
out_local_addr: *mut DataPlaneSocketAddr,
) -> c_int {
if out_listener.is_null() || out_local_addr.is_null() {
return failure(invalid("TCP bind result output pointer is null"));
}
match super::session::take_tcp_bind(session, operation) {
Ok(result) => {
unsafe {
*out_listener = result.listener;
*out_local_addr = ffi_socket_addr(result.local_addr);
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// Each output pointer must be null or point to writable, properly aligned
/// storage for its pointee type. Non-null output locations must not overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_accept_result_take(
session: u64,
operation: u64,
out_stream: *mut u64,
out_local_addr: *mut DataPlaneSocketAddr,
out_peer_addr: *mut DataPlaneSocketAddr,
) -> c_int {
if out_stream.is_null() || out_local_addr.is_null() || out_peer_addr.is_null() {
return failure(invalid("TCP accept result output pointer is null"));
}
match super::session::take_tcp_accept(session, operation) {
Ok(result) => {
unsafe {
*out_stream = result.stream;
*out_local_addr = ffi_socket_addr(result.local_addr);
*out_peer_addr = ffi_socket_addr(result.peer_addr);
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// When `capacity` is nonzero, `data` must point to `capacity` writable bytes.
/// Each scalar output pointer must be null or point to writable, properly
/// aligned storage for its pointee type. Non-null output ranges must not
/// overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_read_result_take(
session: u64,
operation: u64,
data: *mut c_uchar,
capacity: u32,
out_len: *mut u32,
out_eof: *mut bool,
) -> c_int {
if out_len.is_null() || out_eof.is_null() {
return failure(invalid("TCP read result output pointer is null"));
}
let data = match unsafe { output_slice(data, capacity) } {
Ok(data) => data,
Err(error) => return failure(error),
};
match super::session::take_tcp_read(session, operation, data) {
Ok(result) => {
unsafe {
*out_len = result.len as u32;
*out_eof = result.eof;
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// `out_len` must be null or point to writable, properly aligned storage for
/// one `u32`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_write_result_take(
session: u64,
operation: u64,
out_len: *mut u32,
) -> c_int {
if out_len.is_null() {
return failure(invalid("out_len is null"));
}
match super::session::take_tcp_write(session, operation) {
Ok(len) => match u32::try_from(len) {
Ok(len) => {
unsafe {
*out_len = len;
}
0
}
Err(_) => failure(invalid("TCP write result exceeds u32")),
},
Err(error) => failure(error),
}
}
/// # Safety
///
/// Each output pointer must be null or point to writable, properly aligned
/// storage for its pointee type. Non-null output locations must not overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_bind_result_take(
session: u64,
operation: u64,
out_socket: *mut u64,
out_local_addr: *mut DataPlaneSocketAddr,
) -> c_int {
if out_socket.is_null() || out_local_addr.is_null() {
return failure(invalid("UDP bind result output pointer is null"));
}
match super::session::take_udp_bind(session, operation) {
Ok(result) => {
unsafe {
*out_socket = result.socket;
*out_local_addr = ffi_socket_addr(result.local_addr);
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// When `capacity` is nonzero, `data` must point to `capacity` writable bytes.
/// Each scalar output pointer must be null or point to writable, properly
/// aligned storage for its pointee type. Non-null output ranges must not
/// overlap.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_receive_result_take(
session: u64,
operation: u64,
data: *mut c_uchar,
capacity: u32,
out_len: *mut u32,
out_peer_addr: *mut DataPlaneSocketAddr,
out_truncated: *mut bool,
) -> c_int {
if out_len.is_null() || out_peer_addr.is_null() || out_truncated.is_null() {
return failure(invalid("UDP receive result output pointer is null"));
}
let data = match unsafe { output_slice(data, capacity) } {
Ok(data) => data,
Err(error) => return failure(error),
};
match super::session::take_udp_receive(session, operation, data) {
Ok(result) => {
unsafe {
*out_len = result.len as u32;
*out_peer_addr = ffi_socket_addr(result.peer_addr);
*out_truncated = result.truncated;
}
0
}
Err(error) => failure(error),
}
}
/// # Safety
///
/// `out_len` must be null or point to writable, properly aligned storage for
/// one `u32`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_send_result_take(
session: u64,
operation: u64,
out_len: *mut u32,
) -> c_int {
if out_len.is_null() {
return failure(invalid("out_len is null"));
}
match super::session::take_udp_send(session, operation) {
Ok(len) => match u32::try_from(len) {
Ok(len) => {
unsafe {
*out_len = len;
}
0
}
Err(_) => failure(invalid("UDP send result exceeds u32")),
},
Err(error) => failure(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn socket_address_round_trip() {
let address = "127.0.0.1:1234".parse::<SocketAddr>().unwrap();
assert_eq!(socket_addr(ffi_socket_addr(address)).unwrap(), address);
}
#[test]
fn ipv6_is_rejected_by_v3() {
let error = socket_addr(ffi_socket_addr(
"[2001:db8::1]:4321".parse::<SocketAddr>().unwrap(),
))
.unwrap_err();
assert_eq!(error.kind, DataPlaneErrorKind::AddressFamilyUnsupported);
}
#[test]
fn invalid_address_family_is_stable() {
let error = socket_addr(DataPlaneSocketAddr {
family: 9,
..Default::default()
})
.unwrap_err();
assert_eq!(error.kind, DataPlaneErrorKind::AddressFamilyUnsupported);
}
#[test]
fn invalid_deadline_direction_is_rejected_before_session_lookup() {
let invalid = -(DataPlaneErrorKind::Io as c_int);
assert_eq!(data_plane_resource_deadline_set(u64::MAX, 1, 0, 0), invalid);
assert_eq!(data_plane_resource_deadline_set(u64::MAX, 1, 4, 0), invalid);
}
#[test]
fn null_operation_output_does_not_submit() {
let submitted = std::cell::Cell::new(false);
assert_eq!(
write_operation(std::ptr::null_mut(), || {
submitted.set(true);
Ok(1)
}),
-(DataPlaneErrorKind::Io as c_int)
);
assert!(!submitted.get());
}
}
@@ -0,0 +1,16 @@
//! Native C ABI adapter for the instance-scoped data-plane operation broker.
#[cfg(feature = "ffi-dataplane")]
mod abi;
#[cfg(feature = "ffi-dataplane")]
mod session;
#[cfg(feature = "ffi-dataplane")]
pub use abi::*;
#[cfg(feature = "ffi-dataplane")]
pub(crate) use session::{
lock_for_config_server_start, remove_data_plane_sessions_by_instance_ids,
};
#[cfg(not(feature = "ffi-dataplane"))]
pub(crate) fn remove_data_plane_sessions_by_instance_ids(_ids: &[uuid::Uuid]) {}
@@ -0,0 +1,646 @@
use std::{
collections::HashMap,
net::SocketAddr,
sync::{
Arc, Mutex, RwLock,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::Duration,
};
use easytier::instance::host::NativeInstanceHost;
use easytier_core::gateway::{
DataPlaneCompletionDescriptor, DataPlaneError, DataPlaneErrorKind, DataPlaneOperationId,
DataPlaneOperationKind, DataPlaneOperationResult, DataPlaneResourceId, DataPlaneSession,
};
use uuid::Uuid;
use crate::{
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
state::{ffi_context, resolve_instance_id_by_name},
};
type CoreDataPlaneSession = DataPlaneSession<NativeInstanceHost>;
static NEXT_SESSION_HANDLE: AtomicU64 = AtomicU64::new(1);
static SESSIONS: once_cell::sync::Lazy<Mutex<HashMap<u64, Arc<NativeDataPlaneSession>>>> =
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
once_cell::sync::Lazy::new(|| RwLock::new(()));
#[derive(Debug)]
pub(super) struct NativeDataPlaneError {
pub(super) kind: DataPlaneErrorKind,
pub(super) message: String,
}
impl NativeDataPlaneError {
fn new(kind: DataPlaneErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
fn invalid(message: impl Into<String>) -> Self {
Self::new(DataPlaneErrorKind::Io, message)
}
fn closed(message: impl Into<String>) -> Self {
Self::new(DataPlaneErrorKind::HandleClosed, message)
}
}
impl From<DataPlaneError> for NativeDataPlaneError {
fn from(error: DataPlaneError) -> Self {
Self::new(error.kind(), error.message())
}
}
pub(super) type NativeDataPlaneResult<T> = Result<T, NativeDataPlaneError>;
pub(super) struct TcpConnectResult {
pub(super) stream: u64,
pub(super) local_addr: SocketAddr,
pub(super) peer_addr: SocketAddr,
}
pub(super) struct TcpBindResult {
pub(super) listener: u64,
pub(super) local_addr: SocketAddr,
}
pub(super) struct TcpAcceptResult {
pub(super) stream: u64,
pub(super) local_addr: SocketAddr,
pub(super) peer_addr: SocketAddr,
}
pub(super) struct TcpReadResult {
pub(super) len: usize,
pub(super) eof: bool,
}
pub(super) struct UdpBindResult {
pub(super) socket: u64,
pub(super) local_addr: SocketAddr,
}
pub(super) struct UdpReceiveResult {
pub(super) len: usize,
pub(super) peer_addr: SocketAddr,
pub(super) truncated: bool,
}
struct NativeDataPlaneSession {
instance_id: Uuid,
runtime: tokio::runtime::Handle,
core: Arc<CoreDataPlaneSession>,
submit_gate: Mutex<()>,
closed: AtomicBool,
}
impl NativeDataPlaneSession {
fn close(&self) {
let _gate = self
.submit_gate
.lock()
.unwrap_or_else(|error| error.into_inner());
if self.closed.swap(true, Ordering::AcqRel) {
return;
}
self.core.discard_all();
}
fn call<T>(
&self,
call: impl FnOnce(&Arc<CoreDataPlaneSession>) -> Result<T, DataPlaneError>,
) -> NativeDataPlaneResult<T> {
let _gate = self
.submit_gate
.lock()
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
if self.closed.load(Ordering::Acquire) {
return Err(NativeDataPlaneError::closed(
"native data-plane session is closed",
));
}
let _runtime = self.runtime.enter();
call(&self.core).map_err(Into::into)
}
fn submit(
&self,
submit: impl FnOnce(&Arc<CoreDataPlaneSession>) -> Result<DataPlaneOperationId, DataPlaneError>,
) -> NativeDataPlaneResult<u64> {
self.call(submit).map(DataPlaneOperationId::get)
}
}
fn sessions()
-> NativeDataPlaneResult<std::sync::MutexGuard<'static, HashMap<u64, Arc<NativeDataPlaneSession>>>>
{
SESSIONS
.lock()
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))
}
fn get_session(handle: u64) -> NativeDataPlaneResult<Arc<NativeDataPlaneSession>> {
if handle == 0 {
return Err(NativeDataPlaneError::closed(
"native data-plane session handle is invalid",
));
}
let session = sessions()?
.get(&handle)
.cloned()
.ok_or_else(|| NativeDataPlaneError::closed("native data-plane session is closed"))?;
if session.closed.load(Ordering::Acquire) {
return Err(NativeDataPlaneError::closed(
"native data-plane session is closed",
));
}
Ok(session)
}
fn next_session_handle(
sessions: &HashMap<u64, Arc<NativeDataPlaneSession>>,
) -> NativeDataPlaneResult<u64> {
for _ in 0..sessions.len().saturating_add(2) {
let handle = NEXT_SESSION_HANDLE.fetch_add(1, Ordering::Relaxed);
if handle != 0 && !sessions.contains_key(&handle) {
return Ok(handle);
}
}
Err(NativeDataPlaneError::new(
DataPlaneErrorKind::ResourceLimit,
"native data-plane session handle space is exhausted",
))
}
fn reject_data_plane_use() -> NativeDataPlaneResult<()> {
if in_config_server_callback() {
Err(NativeDataPlaneError::invalid(
"cannot use data plane from config server callback",
))
} else if is_config_server_active_or_stopping() {
Err(NativeDataPlaneError::invalid(
"cannot use data plane while config server client is active",
))
} else {
Ok(())
}
}
pub(super) fn open(inst_name: &str) -> NativeDataPlaneResult<u64> {
reject_data_plane_use()?;
let _usage = DATA_PLANE_USAGE_LOCK
.read()
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
reject_data_plane_use()?;
let instance_id = resolve_instance_id_by_name(inst_name)
.map_err(NativeDataPlaneError::invalid)?
.ok_or_else(|| NativeDataPlaneError::closed("instance not found"))?;
let manager = &ffi_context().manager;
let core = manager.data_plane_session(&instance_id).ok_or_else(|| {
NativeDataPlaneError::closed("instance data-plane session is unavailable")
})?;
let runtime = manager
.data_plane_runtime_handle(&instance_id)
.ok_or_else(|| NativeDataPlaneError::closed("instance runtime is unavailable"))?;
let mut sessions = sessions()?;
if sessions
.values()
.any(|session| session.instance_id == instance_id)
{
return Err(NativeDataPlaneError::new(
DataPlaneErrorKind::ResourceLimit,
"instance already has an open native data-plane session",
));
}
let handle = next_session_handle(&sessions)?;
sessions.insert(
handle,
Arc::new(NativeDataPlaneSession {
instance_id,
runtime,
core,
submit_gate: Mutex::new(()),
closed: AtomicBool::new(false),
}),
);
Ok(handle)
}
pub(super) fn close(handle: u64) -> NativeDataPlaneResult<()> {
let _usage = DATA_PLANE_USAGE_LOCK
.read()
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
let mut sessions = sessions()?;
let session = sessions
.remove(&handle)
.ok_or_else(|| NativeDataPlaneError::closed("native data-plane session is closed"))?;
// Keep the registry locked until the shared core namespace is empty. An
// open for the same instance must not publish a replacement session before
// this old wrapper finishes discarding its operations and resources.
session.close();
Ok(())
}
fn timeout(timeout_ms: u64) -> Option<Duration> {
(timeout_ms != u64::MAX).then(|| Duration::from_millis(timeout_ms))
}
fn operation_id(raw: u64) -> NativeDataPlaneResult<DataPlaneOperationId> {
DataPlaneOperationId::from_raw(raw)
.ok_or_else(|| NativeDataPlaneError::closed("data-plane operation handle is invalid"))
}
fn resource_id(raw: u64) -> NativeDataPlaneResult<DataPlaneResourceId> {
DataPlaneResourceId::from_raw(raw)
.ok_or_else(|| NativeDataPlaneError::closed("data-plane resource handle is invalid"))
}
pub(super) fn submit_tcp_connect(
session: u64,
peer_addr: SocketAddr,
timeout_ms: u64,
) -> NativeDataPlaneResult<u64> {
get_session(session)?.submit(|core| core.submit_tcp_connect(peer_addr, timeout(timeout_ms)))
}
pub(super) fn submit_tcp_bind(
session: u64,
local_port: u16,
timeout_ms: u64,
) -> NativeDataPlaneResult<u64> {
get_session(session)?.submit(|core| core.submit_tcp_bind(local_port, timeout(timeout_ms)))
}
pub(super) fn submit_tcp_accept(
session: u64,
listener: u64,
timeout_ms: u64,
) -> NativeDataPlaneResult<u64> {
let listener = resource_id(listener)?;
get_session(session)?.submit(|core| core.submit_tcp_accept(listener, timeout(timeout_ms)))
}
pub(super) fn submit_tcp_read(
session: u64,
stream: u64,
max_len: u32,
) -> NativeDataPlaneResult<u64> {
let stream = resource_id(stream)?;
get_session(session)?.submit(|core| core.submit_tcp_read(stream, max_len as usize))
}
pub(super) fn submit_tcp_write(
session: u64,
stream: u64,
data: Vec<u8>,
) -> NativeDataPlaneResult<u64> {
let stream = resource_id(stream)?;
get_session(session)?.submit(|core| core.submit_tcp_write(stream, data))
}
pub(super) fn submit_udp_bind(
session: u64,
local_port: u16,
timeout_ms: u64,
) -> NativeDataPlaneResult<u64> {
get_session(session)?.submit(|core| core.submit_udp_bind(local_port, timeout(timeout_ms)))
}
pub(super) fn submit_udp_receive(
session: u64,
socket: u64,
max_len: u32,
) -> NativeDataPlaneResult<u64> {
let socket = resource_id(socket)?;
get_session(session)?.submit(|core| core.submit_udp_receive(socket, max_len as usize))
}
pub(super) fn submit_udp_send(
session: u64,
socket: u64,
peer_addr: SocketAddr,
data: Vec<u8>,
) -> NativeDataPlaneResult<u64> {
let socket = resource_id(socket)?;
get_session(session)?.submit(|core| core.submit_udp_send(socket, peer_addr, data))
}
pub(super) fn set_resource_deadline(
session: u64,
resource: u64,
read: bool,
write: bool,
timeout_ms: u64,
) -> NativeDataPlaneResult<()> {
let resource = resource_id(resource)?;
get_session(session)?
.call(|core| core.set_resource_deadline(resource, read, write, timeout(timeout_ms)))
}
pub(super) fn cancel_operation(session: u64, operation: u64) -> NativeDataPlaneResult<()> {
let operation = operation_id(operation)?;
get_session(session)?.core.cancel_operation(operation);
Ok(())
}
pub(super) fn free_operation(session: u64, operation: u64) -> NativeDataPlaneResult<()> {
let operation = operation_id(operation)?;
get_session(session)?.core.free_operation(operation);
Ok(())
}
pub(super) fn close_resource(session: u64, resource: u64) -> NativeDataPlaneResult<()> {
let resource = resource_id(resource)?;
get_session(session)?.core.close_resource(resource);
Ok(())
}
pub(super) fn completion_wait(session: u64, timeout_ms: u64) -> NativeDataPlaneResult<bool> {
let session = get_session(session)?;
let ready = session.core.completion_wait(timeout(timeout_ms));
Ok(ready && !session.closed.load(Ordering::Acquire))
}
pub(super) fn drain_completions(
session: u64,
max_count: usize,
) -> NativeDataPlaneResult<Vec<DataPlaneCompletionDescriptor>> {
Ok(get_session(session)?.core.drain_completions(max_count))
}
pub(super) fn result_size(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
let operation = operation_id(operation)?;
get_session(session)?
.core
.result_payload_bytes(operation)
.map_err(Into::into)
}
fn take_result<T>(
session: u64,
operation: u64,
expected: DataPlaneOperationKind,
take: impl FnOnce(&DataPlaneOperationResult) -> Option<T>,
) -> NativeDataPlaneResult<T> {
let operation = operation_id(operation)?;
let session = get_session(session)?;
let actual = session.core.operation_kind(operation)?;
if actual != expected {
return Err(NativeDataPlaneError::invalid(format!(
"operation kind mismatch: expected {expected:?}, got {actual:?}"
)));
}
let result = session.core.take_result_with(operation, |outcome| {
Some(match outcome {
Ok(result) => take(result).ok_or_else(|| {
NativeDataPlaneError::invalid("data-plane result variant does not match operation")
}),
Err(kind) => Err(NativeDataPlaneError::new(
*kind,
format!("data-plane operation failed with {kind:?}"),
)),
})
})?;
result
.ok_or_else(|| NativeDataPlaneError::invalid("data-plane result could not be consumed"))?
}
pub(super) fn take_tcp_connect(
session: u64,
operation: u64,
) -> NativeDataPlaneResult<TcpConnectResult> {
take_result(
session,
operation,
DataPlaneOperationKind::TcpConnect,
|result| match result {
DataPlaneOperationResult::TcpConnected {
stream,
local_addr,
peer_addr,
} => Some(TcpConnectResult {
stream: stream.get(),
local_addr: *local_addr,
peer_addr: *peer_addr,
}),
_ => None,
},
)
}
pub(super) fn take_tcp_bind(session: u64, operation: u64) -> NativeDataPlaneResult<TcpBindResult> {
take_result(
session,
operation,
DataPlaneOperationKind::TcpBind,
|result| match result {
DataPlaneOperationResult::TcpBound {
listener,
local_addr,
} => Some(TcpBindResult {
listener: listener.get(),
local_addr: *local_addr,
}),
_ => None,
},
)
}
pub(super) fn take_tcp_accept(
session: u64,
operation: u64,
) -> NativeDataPlaneResult<TcpAcceptResult> {
take_result(
session,
operation,
DataPlaneOperationKind::TcpAccept,
|result| match result {
DataPlaneOperationResult::TcpAccepted {
stream,
local_addr,
peer_addr,
} => Some(TcpAcceptResult {
stream: stream.get(),
local_addr: *local_addr,
peer_addr: *peer_addr,
}),
_ => None,
},
)
}
pub(super) fn take_tcp_read(
session: u64,
operation: u64,
output: &mut [u8],
) -> NativeDataPlaneResult<TcpReadResult> {
let required = result_size(session, operation)?;
if output.len() < required {
return Err(NativeDataPlaneError::new(
DataPlaneErrorKind::BufferTooSmall,
format!(
"TCP read result requires {required} bytes, buffer has {}",
output.len()
),
));
}
take_result(
session,
operation,
DataPlaneOperationKind::TcpRead,
|result| match result {
DataPlaneOperationResult::TcpRead { data, eof } => {
output[..data.len()].copy_from_slice(data);
Some(TcpReadResult {
len: data.len(),
eof: *eof,
})
}
_ => None,
},
)
}
pub(super) fn take_tcp_write(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
take_result(
session,
operation,
DataPlaneOperationKind::TcpWrite,
|result| match result {
DataPlaneOperationResult::TcpWritten { len } => Some(*len),
_ => None,
},
)
}
pub(super) fn take_udp_bind(session: u64, operation: u64) -> NativeDataPlaneResult<UdpBindResult> {
take_result(
session,
operation,
DataPlaneOperationKind::UdpBind,
|result| match result {
DataPlaneOperationResult::UdpBound { socket, local_addr } => Some(UdpBindResult {
socket: socket.get(),
local_addr: *local_addr,
}),
_ => None,
},
)
}
pub(super) fn take_udp_receive(
session: u64,
operation: u64,
output: &mut [u8],
) -> NativeDataPlaneResult<UdpReceiveResult> {
let required = result_size(session, operation)?;
if output.len() < required {
return Err(NativeDataPlaneError::new(
DataPlaneErrorKind::BufferTooSmall,
format!(
"UDP receive result requires {required} bytes, buffer has {}",
output.len()
),
));
}
take_result(
session,
operation,
DataPlaneOperationKind::UdpReceive,
|result| match result {
DataPlaneOperationResult::UdpReceived {
data,
peer_addr,
truncated,
} => {
output[..data.len()].copy_from_slice(data);
Some(UdpReceiveResult {
len: data.len(),
peer_addr: *peer_addr,
truncated: *truncated,
})
}
_ => None,
},
)
}
pub(super) fn take_udp_send(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
take_result(
session,
operation,
DataPlaneOperationKind::UdpSend,
|result| match result {
DataPlaneOperationResult::UdpSent { len } => Some(*len),
_ => None,
},
)
}
pub(crate) fn remove_data_plane_sessions_by_instance_ids(ids: &[Uuid]) {
if ids.is_empty() {
return;
}
let _usage = DATA_PLANE_USAGE_LOCK
.write()
.unwrap_or_else(|error| error.into_inner());
let removed = {
let mut sessions = SESSIONS.lock().unwrap_or_else(|error| error.into_inner());
let handles = sessions
.iter()
.filter_map(|(handle, session)| ids.contains(&session.instance_id).then_some(*handle))
.collect::<Vec<_>>();
handles
.into_iter()
.filter_map(|handle| sessions.remove(&handle))
.collect::<Vec<_>>()
};
for session in removed {
session.close();
}
}
pub(crate) fn lock_for_config_server_start()
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
let guard = DATA_PLANE_USAGE_LOCK
.write()
.map_err(|error| format!("failed to lock data plane usage: {error}"))?;
if !SESSIONS
.lock()
.map_err(|error| format!("failed to lock data-plane sessions: {error}"))?
.is_empty()
{
return Err("cannot start config server client while data plane is in use".to_string());
}
Ok(guard)
}
#[cfg(test)]
mod tests {
use std::{sync::mpsc, time::Duration};
use super::*;
#[test]
fn config_server_start_waits_for_session_open_or_close() {
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
let (done_tx, done_rx) = mpsc::channel();
let waiter = std::thread::spawn(move || {
let _write_guard = lock_for_config_server_start().unwrap();
done_tx.send(()).unwrap();
});
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
drop(read_guard);
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
waiter.join().unwrap();
}
}
@@ -0,0 +1,65 @@
use std::{
cell::RefCell,
ffi::{CString, c_char},
};
thread_local! {
// # Thread Safety
// set_error_msg and get_error_msg must be called on the same thread to
// get correct error. And since `Handle::block_on` polls the top-level
// future on the calling thread, set_error_msg always runs on the same
// thread as the corresponding get_error_msg.
static ERROR_MSG: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn set_error_msg(msg: &str) {
ERROR_MSG.with(|cell| {
let mut buf = cell.borrow_mut();
buf.clear();
buf.extend_from_slice(msg.as_bytes());
});
}
fn thread_local_error_msg() -> Option<String> {
ERROR_MSG.with(|cell| {
let buf = cell.borrow();
if buf.is_empty() {
None
} else {
Some(String::from_utf8_lossy(&buf).into_owned())
}
})
}
pub(crate) unsafe fn get_error_msg(out: *mut *const c_char) {
let msg = match (
thread_local_error_msg(),
crate::config_server::last_callback_error(),
) {
(Some(error), Some(callback_error)) => Some(format!(
"{}; config server callback error: {}",
error, callback_error
)),
(Some(error), None) => Some(error),
(None, Some(callback_error)) => {
Some(format!("config server callback error: {}", callback_error))
}
(None, None) => None,
};
let cstr = msg.and_then(|msg| CString::new(msg).ok());
unsafe {
*out = match cstr {
Some(s) => s.into_raw() as *const c_char,
None => std::ptr::null(),
};
}
}
pub(crate) fn free_string(s: *const c_char) {
if s.is_null() {
return;
}
unsafe {
let _ = CString::from_raw(s as *mut c_char);
}
}
@@ -0,0 +1,350 @@
use std::ffi::{CString, c_char, c_int};
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
use easytier::common::config::ConfigLoader as _;
use easytier::common::config::{ConfigFileControl, TomlConfigLoader};
use crate::{
config_server::{in_config_server_callback, wait_for_config_server_delivery},
error::set_error_msg,
state::{ffi_context, resolve_instance_id_by_name},
types::KeyValuePair,
};
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
fn mobile_tun_sources_for_legacy_set_tun_fd(inst_id: uuid::Uuid) -> Result<(), String> {
let config = ffi_context()
.manager
.config(inst_id)
.ok_or_else(|| format!("instance config unavailable: {inst_id}"))?;
let flags = config.get_flags();
if flags.dev_name.is_empty() {
return Ok(());
}
Err(format!(
"set_tun_fd legacy API cannot attach shared mobile TUN dev_name={} without tun sources",
flags.dev_name
))
}
/// # Safety
/// Set the tun fd
pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
let inst_name = unsafe {
assert!(!inst_name.is_null());
std::ffi::CStr::from_ptr(inst_name)
.to_string_lossy()
.into_owned()
};
let inst_id = match resolve_instance_id_by_name(&inst_name) {
Ok(Some(instance_id)) => instance_id,
Ok(None) => {
set_error_msg(&format!("instance not found: {inst_name}"));
return -1;
}
Err(error) => {
set_error_msg(&error.to_string());
return -1;
}
};
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
if let Err(error) = mobile_tun_sources_for_legacy_set_tun_fd(inst_id) {
set_error_msg(&error);
return -1;
}
match ffi_context().manager.attach_tun_fd(inst_id, fd) {
Ok(_) => 0,
Err(e) => {
set_error_msg(&format!("failed to set tun fd: {}", e));
-1
}
}
}
/// # Safety
/// Parse the config
pub(crate) unsafe fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
.to_string_lossy()
.into_owned()
};
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
set_error_msg(&format!("failed to parse config: {:?}", e));
return -1;
}
0
}
/// # Safety
/// Run the network instance
pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
if in_config_server_callback() {
set_error_msg("cannot run network instance from config server callback");
return -1;
}
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
.to_string_lossy()
.into_owned()
};
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
Ok(cfg) => cfg,
Err(e) => {
set_error_msg(&format!("failed to parse config: {}", e));
return -1;
}
};
wait_for_config_server_delivery();
if let Err(e) = ffi_context().runtime.block_on(
ffi_context()
.process_management
.run_owned_network_instance(cfg, ConfigFileControl::STATIC_CONFIG),
) {
set_error_msg(&format!("failed to start instance: {}", e));
return -1;
}
0
}
unsafe fn parse_instance_names(
inst_names: *const *const c_char,
length: usize,
) -> Option<Vec<String>> {
if length == 0 {
return Some(Vec::new());
}
if inst_names.is_null() {
set_error_msg("inst_names is null");
return None;
}
let names = unsafe { std::slice::from_raw_parts(inst_names, length) };
let mut parsed = Vec::with_capacity(length);
for (index, &name) in names.iter().enumerate() {
if name.is_null() {
set_error_msg(&format!("inst_names[{}] is null", index));
return None;
}
parsed.push(
unsafe { std::ffi::CStr::from_ptr(name) }
.to_string_lossy()
.into_owned(),
);
}
Some(parsed)
}
/// # Safety
/// Retain the network instance
pub(crate) unsafe fn retain_network_instance(
inst_names: *const *const std::ffi::c_char,
length: usize,
) -> std::ffi::c_int {
if in_config_server_callback() {
set_error_msg("cannot retain network instances from config server callback");
return -1;
}
wait_for_config_server_delivery();
let retained_names = if length == 0 {
Vec::new()
} else {
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
return -1;
};
inst_names
};
if let Err(error) = ffi_context().runtime.block_on(
ffi_context()
.process_management
.retain_owned_network_instances_by_name(retained_names),
) {
set_error_msg(&format!("failed to retain instances: {error}"));
return -1;
}
0
}
/// # Safety
/// Delete named network instances.
pub(crate) unsafe fn delete_network_instance(
inst_names: *const *const std::ffi::c_char,
length: usize,
) -> std::ffi::c_int {
if in_config_server_callback() {
set_error_msg("cannot delete network instances from config server callback");
return -1;
}
wait_for_config_server_delivery();
if length == 0 {
return 0;
}
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
return -1;
};
if let Err(error) = ffi_context().runtime.block_on(
ffi_context()
.process_management
.delete_owned_network_instances_by_name(inst_names),
) {
set_error_msg(&format!("failed to delete instances: {error}"));
return -1;
}
0
}
/// # Safety
/// Collect the network infos
pub(crate) unsafe fn collect_network_infos(
infos: *mut KeyValuePair,
max_length: usize,
) -> std::ffi::c_int {
if in_config_server_callback() {
set_error_msg("cannot collect network infos from config server callback");
return -1;
}
if max_length == 0 {
return 0;
}
let infos = unsafe {
assert!(!infos.is_null());
std::slice::from_raw_parts_mut(infos, max_length)
};
let collected_infos = match ffi_context().manager.collect_network_infos_sync() {
Ok(infos) => infos,
Err(e) => {
set_error_msg(&format!("failed to collect network infos: {}", e));
return -1;
}
};
let mut index = 0;
for (instance_id, value) in collected_infos.iter() {
if index >= max_length {
break;
}
let Some(key) = ffi_context()
.manager
.instance(*instance_id)
.map(|instance| instance.instance_name().to_owned())
else {
continue;
};
// convert value to json string
let value = match serde_json::to_string(&value) {
Ok(value) => value,
Err(e) => {
set_error_msg(&format!("failed to serialize instance info: {}", e));
return -1;
}
};
infos[index] = KeyValuePair {
key: std::ffi::CString::new(key).unwrap().into_raw(),
value: std::ffi::CString::new(value).unwrap().into_raw(),
};
index += 1;
}
index as std::ffi::c_int
}
/// # Safety
/// List the instance names and IDs known by the FFI instance manager.
pub(crate) unsafe fn list_instance(infos: *mut KeyValuePair, max_length: usize) -> std::ffi::c_int {
if in_config_server_callback() {
set_error_msg("cannot list instances from config server callback");
return -1;
}
if max_length == 0 {
return 0;
}
if infos.is_null() {
set_error_msg("infos is null");
return -1;
}
let infos = unsafe { std::slice::from_raw_parts_mut(infos, max_length) };
let mut instances = ffi_context()
.manager
.instance_ids()
.into_iter()
.filter_map(|id| {
ffi_context()
.manager
.instance(id)
.map(|instance| (instance.instance_name().to_owned(), id))
})
.collect::<Vec<_>>();
instances.sort_by(|(left_name, left_id), (right_name, right_id)| {
left_name
.cmp(right_name)
.then_with(|| left_id.to_string().cmp(&right_id.to_string()))
});
let encoded_instances = match instances
.into_iter()
.take(max_length)
.map(|(name, id)| {
let key = CString::new(name)
.map_err(|err| format!("failed to encode instance name: {}", err))?;
let value = CString::new(id.to_string())
.map_err(|err| format!("failed to encode instance id: {}", err))?;
Ok((key, value))
})
.collect::<Result<Vec<_>, String>>()
{
Ok(value) => value,
Err(err) => {
set_error_msg(&err);
return -1;
}
};
let count = encoded_instances.len();
for (index, (key, value)) in encoded_instances.into_iter().enumerate() {
infos[index] = KeyValuePair {
key: key.into_raw(),
value: value.into_raw(),
};
}
count as std::ffi::c_int
}
@@ -0,0 +1,107 @@
use std::{
ffi::{CString, c_char, c_int},
sync::Arc,
};
use crate::{
config_server::in_config_server_callback,
error::set_error_msg,
state::ffi_context,
strings::{c_str_to_string, optional_c_str_to_string},
};
/// # Safety
/// See `crate::call_json_rpc`.
pub(crate) unsafe fn call_json_rpc(
service_name: *const c_char,
method_name: *const c_char,
domain_name: *const c_char,
payload_json: *const c_char,
out_response_json: *mut *const c_char,
) -> c_int {
if out_response_json.is_null() {
set_error_msg("out_response_json is null");
return -1;
}
unsafe {
*out_response_json = std::ptr::null();
}
if in_config_server_callback() {
set_error_msg("cannot call JSON RPC from config server callback");
return -1;
}
let service_name = match unsafe { c_str_to_string(service_name, "service_name") } {
Ok(value) => value,
Err(err) => {
set_error_msg(&err);
return -1;
}
};
let method_name = match unsafe { c_str_to_string(method_name, "method_name") } {
Ok(value) => value,
Err(err) => {
set_error_msg(&err);
return -1;
}
};
let domain_name = match unsafe { optional_c_str_to_string(domain_name, "domain_name") } {
Ok(value) => value,
Err(err) => {
set_error_msg(&err);
return -1;
}
};
let payload_json = match unsafe { c_str_to_string(payload_json, "payload_json") } {
Ok(value) => value,
Err(err) => {
set_error_msg(&err);
return -1;
}
};
let payload = match serde_json::from_str::<serde_json::Value>(&payload_json) {
Ok(value) => value,
Err(err) => {
set_error_msg(&format!("failed to parse payload_json: {}", err));
return -1;
}
};
let response =
match ffi_context()
.runtime
.block_on(easytier_core::management::call_management_json_rpc(
&ffi_context().manager,
Arc::new(easytier::rpc_service::logger::NativeLoggerControl),
&service_name,
&method_name,
domain_name.as_deref(),
payload,
)) {
Ok(value) => value,
Err(err) => {
set_error_msg(&format!("RPC Error: {}", err));
return -1;
}
};
let response_json = match serde_json::to_string(&response) {
Ok(value) => value,
Err(err) => {
set_error_msg(&format!("failed to serialize RPC response: {}", err));
return -1;
}
};
let response_json = match CString::new(response_json) {
Ok(value) => value,
Err(err) => {
set_error_msg(&format!("failed to allocate RPC response: {}", err));
return -1;
}
};
unsafe {
*out_response_json = response_json.into_raw();
}
0
}
+351 -192
View File
@@ -1,199 +1,358 @@
use std::sync::Mutex;
//! C ABI facade for EasyTier.
//!
//! The exported API is intentionally kept in this file so C users and JNI
//! bindings can see the full callable surface without reading the internal
//! implementation modules.
//!
//! Network management APIs:
//! - `parse_config`: validate a TOML network config string.
//! - `run_network_instance`: start one local network instance from TOML.
//! - `retain_network_instance`: keep named instances and stop all others.
//! - `delete_network_instance`: stop named local network instances.
//! - `list_instance`: list running instance names and IDs.
//! - `collect_network_infos`: collect running instance info as key/value pairs.
//! - `set_tun_fd`: attach a TUN file descriptor to a named instance.
//! - `call_json_rpc`: call an exposed EasyTier RPC service with JSON payload.
//!
//! Config server client APIs:
//! - `start_config_server_client`: start the managed remote config client.
//! - `stop_config_server_client`: stop the remote config client and its managed instances.
//! - `is_config_server_client_connected`: report whether the client is connected.
//!
//! Data plane APIs, enabled by the `ffi-dataplane` feature:
//! - `data_plane_session_open` / `data_plane_session_close`: own one instance session.
//! - `data_plane_*_submit`: submit non-blocking TCP and UDP operations.
//! - `data_plane_completion_wait` / `data_plane_completion_drain`: await completions.
//! - `data_plane_*_result_take`: consume typed operation results.
//! - `data_plane_operation_cancel` / `data_plane_operation_free`: control operations.
//! - `data_plane_resource_close`: close streams, listeners, and UDP sockets.
//!
//! Shared FFI helper APIs:
//! - `get_error_msg`: copy the last FFI or config-server callback error message.
//! - `free_string`: release strings allocated by this library.
use dashmap::DashMap;
use easytier::{
common::config::{ConfigLoader as _, TomlConfigLoader},
launcher::NetworkInstance,
};
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
once_cell::sync::Lazy::new(DashMap::new);
static ERROR_MSG: once_cell::sync::Lazy<Mutex<Vec<u8>>> =
once_cell::sync::Lazy::new(|| Mutex::new(Vec::new()));
#[repr(C)]
pub struct KeyValuePair {
pub key: *const std::ffi::c_char,
pub value: *const std::ffi::c_char,
}
fn set_error_msg(msg: &str) {
let bytes = msg.as_bytes();
let mut msg_buf = ERROR_MSG.lock().unwrap();
let len = bytes.len();
msg_buf.resize(len, 0);
msg_buf[..len].copy_from_slice(bytes);
}
#[no_mangle]
pub extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
let msg_buf = ERROR_MSG.lock().unwrap();
if msg_buf.is_empty() {
unsafe {
*out = std::ptr::null();
}
return;
}
let cstr = std::ffi::CString::new(&msg_buf[..]).unwrap();
unsafe {
*out = cstr.into_raw();
}
}
#[no_mangle]
pub extern "C" fn free_string(s: *const std::ffi::c_char) {
if s.is_null() {
return;
}
unsafe {
let _ = std::ffi::CString::from_raw(s as *mut std::ffi::c_char);
}
}
#[no_mangle]
pub extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
.to_string_lossy()
.into_owned()
};
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
set_error_msg(&format!("failed to parse config: {:?}", e));
return -1;
}
0
}
#[no_mangle]
pub extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
.to_string_lossy()
.into_owned()
};
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
Ok(cfg) => cfg,
Err(e) => {
set_error_msg(&format!("failed to parse config: {}", e));
return -1;
}
};
let inst_name = cfg.get_inst_name();
if INSTANCE_MAP.contains_key(&inst_name) {
set_error_msg("instance already exists");
return -1;
}
let mut instance = NetworkInstance::new(cfg);
if let Err(e) = instance.start().map_err(|e| e.to_string()) {
set_error_msg(&format!("failed to start instance: {}", e));
return -1;
}
INSTANCE_MAP.insert(inst_name, instance);
0
}
#[no_mangle]
pub extern "C" fn retain_network_instance(
inst_names: *const *const std::ffi::c_char,
length: usize,
) -> std::ffi::c_int {
if length == 0 {
INSTANCE_MAP.clear();
return 0;
}
let inst_names = unsafe {
assert!(!inst_names.is_null());
std::slice::from_raw_parts(inst_names, length)
.iter()
.map(|&name| {
assert!(!name.is_null());
std::ffi::CStr::from_ptr(name)
.to_string_lossy()
.into_owned()
})
.collect::<Vec<_>>()
};
let _ = INSTANCE_MAP.retain(|k, _| inst_names.contains(k));
0
}
#[no_mangle]
pub extern "C" fn collect_network_infos(
infos: *mut KeyValuePair,
max_length: usize,
) -> std::ffi::c_int {
if max_length == 0 {
return 0;
}
let infos = unsafe {
assert!(!infos.is_null());
std::slice::from_raw_parts_mut(infos, max_length)
};
let mut index = 0;
for instance in INSTANCE_MAP.iter() {
if index >= max_length {
break;
}
let key = instance.key();
let Some(value) = instance.get_running_info() else {
continue;
};
// convert value to json string
let value = match serde_json::to_string(&value) {
Ok(value) => value,
Err(e) => {
set_error_msg(&format!("failed to serialize instance info: {}", e));
return -1;
}
};
infos[index] = KeyValuePair {
key: std::ffi::CString::new(key.clone()).unwrap().into_raw(),
value: std::ffi::CString::new(value).unwrap().into_raw(),
};
index += 1;
}
index as std::ffi::c_int
}
mod config_server;
mod data_plane;
mod error;
mod instance_api;
mod json_rpc;
mod state;
mod strings;
mod types;
#[cfg(test)]
mod tests {
use super::*;
mod tests;
#[test]
fn test_parse_config() {
let cfg_str = r#"
inst_name = "test"
network = "test_network"
fdsafdsa
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
assert_eq!(parse_config(cstr.as_ptr()), 0);
}
pub use config_server::{in_config_server_callback, validate_config_server_client_options};
pub use types::{
ConfigServerEventCallback, DataPlaneCompletion, DataPlaneSocketAddr, KeyValuePair,
};
#[test]
fn test_run_network_instance() {
let cfg_str = r#"
inst_name = "test"
network = "test_network"
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
use std::ffi::{c_char, c_int, c_void};
// ===== Network Management API =====
/// Validate a TOML network config string.
///
/// This only parses and validates the config. It does not start an instance and
/// does not change global FFI state.
///
/// # Safety
/// `cfg_str` must be a non-null pointer to a null-terminated UTF-8 string.
///
/// # Return
/// Returns `0` if the config parses successfully, or `-1` on failure. On
/// failure, call `get_error_msg` on the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn parse_config(cfg_str: *const c_char) -> c_int {
unsafe { instance_api::parse_config(cfg_str) }
}
/// Start one local EasyTier network instance from a TOML config string.
///
/// The config's `inst_name` must be unique among instances started through this
/// FFI layer. This API is mutually exclusive with config-server callback
/// execution and will fail if called from a config-server event callback.
///
/// # Safety
/// `cfg_str` must be a non-null pointer to a null-terminated UTF-8 string.
///
/// # Return
/// Returns `0` after the instance is started and registered in the FFI name
/// cache, or `-1` on failure. On failure, call `get_error_msg` on the same
/// thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn run_network_instance(cfg_str: *const c_char) -> c_int {
unsafe { instance_api::run_network_instance(cfg_str) }
}
/// Keep the named network instances and stop all other instances.
///
/// Passing `length == 0` stops all instances. When `length > 0`, `inst_names`
/// must point to an array of `length` non-null C strings. Instances that are not
/// retained are removed from the FFI name cache and any related data-plane
/// handles are closed.
///
/// This API fails if called from a config-server event callback.
///
/// # Safety
/// If `length > 0`, `inst_names` must be a non-null pointer to an array of
/// `length` non-null pointers to null-terminated UTF-8 strings.
///
/// # Return
/// Returns `0` on success, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn retain_network_instance(
inst_names: *const *const c_char,
length: usize,
) -> c_int {
unsafe { instance_api::retain_network_instance(inst_names, length) }
}
/// Stop the named network instances.
///
/// Passing `length == 0` is a no-op. When `length > 0`, `inst_names` must point
/// to an array of `length` non-null C strings. Unknown names are ignored.
/// Removed instances are also removed from the FFI name cache and any related
/// data-plane handles are closed.
///
/// This API fails if called from a config-server event callback.
///
/// # Safety
/// If `length > 0`, `inst_names` must be a non-null pointer to an array of
/// `length` non-null pointers to null-terminated UTF-8 strings.
///
/// # Return
/// Returns `0` on success, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn delete_network_instance(
inst_names: *const *const c_char,
length: usize,
) -> c_int {
unsafe { instance_api::delete_network_instance(inst_names, length) }
}
/// List running network instance names and IDs.
///
/// Writes up to `max_length` entries into `infos`. Each returned key is the
/// instance name and each returned value is the instance ID string. Returned
/// key/value strings are allocated by this library and must be released with
/// `free_string`.
///
/// This API fails if called from a config-server event callback.
///
/// # Safety
/// If `max_length > 0`, `infos` must be a non-null pointer to writable storage
/// for at least `max_length` `KeyValuePair` values.
///
/// # Return
/// Returns the number of entries written, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn list_instance(infos: *mut KeyValuePair, max_length: usize) -> c_int {
unsafe { instance_api::list_instance(infos, max_length) }
}
/// Collect running network instance information.
///
/// Writes up to `max_length` entries into `infos`. Each returned key is the
/// instance name and each returned value is a JSON string containing that
/// instance's running information. Returned key/value strings are allocated by
/// this library and must be released with `free_string`.
///
/// This API fails if called from a config-server event callback.
///
/// # Safety
/// If `max_length > 0`, `infos` must be a non-null pointer to writable storage
/// for at least `max_length` `KeyValuePair` values.
///
/// # Return
/// Returns the number of entries written, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn collect_network_infos(
infos: *mut KeyValuePair,
max_length: usize,
) -> c_int {
unsafe { instance_api::collect_network_infos(infos, max_length) }
}
/// Attach a TUN file descriptor to a named network instance.
///
/// The instance must already have been registered in the FFI name cache by
/// `run_network_instance` or by a managed config-server remote start event.
///
/// # Safety
/// `inst_name` must be a non-null pointer to a null-terminated UTF-8 string.
/// `fd` must be a valid TUN file descriptor owned by the caller.
///
/// # Return
/// Returns `0` if the descriptor is accepted by the instance, or `-1` on
/// failure.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
unsafe { instance_api::set_tun_fd(inst_name, fd) }
}
/// Call an exposed EasyTier RPC method using protobuf JSON.
///
/// This generic bridge intentionally excludes instance lifecycle management
/// RPCs. Use the dedicated FFI APIs for starting, retaining, deleting, and
/// collecting instances. `payload_json` must contain the protobuf JSON request,
/// including any `instance` selector required by the target RPC.
///
/// `domain_name` may be null or empty. It is only used by
/// `api.instance.TcpProxyRpcService`; null or empty defaults to `tcp`, and the
/// only accepted explicit values are `tcp`, `kcp_src`, `kcp_dst`, `quic_src`,
/// and `quic_dst`.
///
/// On success, writes a newly allocated JSON response string to
/// `out_response_json`. The caller must release it with `free_string`.
///
/// This API fails if called from a config-server event callback.
///
/// # Safety
/// `service_name`, `method_name`, `payload_json`, and `out_response_json` must
/// be non-null. String pointers must point to null-terminated UTF-8 strings.
/// `domain_name` may be null.
///
/// # Return
/// Returns `0` on success, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn call_json_rpc(
service_name: *const c_char,
method_name: *const c_char,
domain_name: *const c_char,
payload_json: *const c_char,
out_response_json: *mut *const c_char,
) -> c_int {
unsafe {
json_rpc::call_json_rpc(
service_name,
method_name,
domain_name,
payload_json,
out_response_json,
)
}
}
// ===== Config Server Client API =====
/// Start the managed config-server client.
///
/// The client reuses EasyTier's web-client path and applies remote config
/// changes through the shared `NativeInstanceManager`. Successful remote run
/// and delete operations are delivered to `callback` as JSON event strings, one
/// callback per affected instance. The event string is valid only for the
/// duration of the callback; callers must copy it if they need to keep it.
///
/// The config-server client is mutually exclusive with the FFI data plane. If a
/// data-plane handle exists or is being created, this function returns `-1`.
///
/// # Safety
/// `config_server_url` and `machine_id` must be non-null pointers to
/// null-terminated UTF-8 strings. `hostname` may be null; when non-null it must
/// also point to a null-terminated UTF-8 string. `user_data` is passed back to
/// `callback` unchanged and must remain valid for the callback's expectations.
///
/// # Return
/// Returns `0` after the client starts successfully, or `-1` on failure. On
/// failure, call `get_error_msg` on the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn start_config_server_client(
config_server_url: *const c_char,
hostname: *const c_char,
machine_id: *const c_char,
secure_mode: bool,
callback: ConfigServerEventCallback,
user_data: *mut c_void,
) -> c_int {
unsafe {
config_server::start_config_server_client(
config_server_url,
hostname,
machine_id,
secure_mode,
callback,
user_data,
)
}
}
/// Stop the managed config-server client.
///
/// This stops the client, removes instances tracked as remote config-server
/// instances, waits for in-flight callback delivery when safe to do so, and
/// releases the config-server/data-plane mutual exclusion state.
///
/// # Return
/// Returns `0` if no client exists or if the active client is stopped
/// successfully. Returns `-1` on failure. On failure, call `get_error_msg` on
/// the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn stop_config_server_client() -> c_int {
config_server::stop_config_server_client()
}
/// Report whether the managed config-server client is currently connected.
///
/// # Return
/// Returns `1` when a client exists and reports connected, otherwise returns
/// `0`.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn is_config_server_client_connected() -> c_int {
config_server::is_config_server_client_connected()
}
// ===== Data Plane API =====
#[cfg(feature = "ffi-dataplane")]
pub use data_plane::{
DATA_PLANE_DEADLINE_READ, DATA_PLANE_DEADLINE_WRITE, data_plane_completion_drain,
data_plane_completion_wait, data_plane_operation_cancel, data_plane_operation_free,
data_plane_resource_close, data_plane_resource_deadline_set, data_plane_result_size,
data_plane_session_close, data_plane_session_open, data_plane_tcp_accept_result_take,
data_plane_tcp_accept_submit, data_plane_tcp_bind_result_take, data_plane_tcp_bind_submit,
data_plane_tcp_connect_result_take, data_plane_tcp_connect_submit,
data_plane_tcp_read_result_take, data_plane_tcp_read_submit, data_plane_tcp_write_result_take,
data_plane_tcp_write_submit, data_plane_udp_bind_result_take, data_plane_udp_bind_submit,
data_plane_udp_receive_result_take, data_plane_udp_receive_submit,
data_plane_udp_send_result_take, data_plane_udp_send_submit,
};
// ===== Shared FFI Helper API =====
/// Return the last FFI error message.
///
/// API failures are stored in a thread-local buffer, so call this on the same
/// thread that received a negative status or another documented failure
/// sentinel. Config-server
/// callback delivery failures may happen on a runtime thread; those are stored
/// globally and are included here so direct FFI callers can still retrieve the
/// last callback error. If there is no error message, this writes a null pointer
/// to `out`.
///
/// The returned string is allocated by this library and must be released with
/// `free_string`.
///
/// # Safety
/// `out` must be a non-null pointer to writable storage for one C string
/// pointer.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn get_error_msg(out: *mut *const c_char) {
unsafe { error::get_error_msg(out) }
}
/// Release a C string allocated by this library.
///
/// Use this for strings returned through `get_error_msg`,
/// `collect_network_infos`, and data-plane address output parameters. Passing a
/// null pointer is allowed and has no effect.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn free_string(s: *const c_char) {
error::free_string(s)
}
@@ -0,0 +1,66 @@
use std::sync::Arc;
use easytier::instance::factory::{
NativeInstanceManager, NativeProcessManagement, native_instance_manager_with_runtime,
native_process_management,
};
use tokio::runtime::{Builder, Runtime};
struct FfiOwnedInstanceHooks;
#[async_trait::async_trait]
impl easytier_core::management::InstanceMutationHooks for FfiOwnedInstanceHooks {
async fn post_remove_network_instances(
&self,
instance_ids: &[uuid::Uuid],
) -> Result<(), String> {
crate::config_server::remove_config_server_tracked_instance_ids(instance_ids);
crate::data_plane::remove_data_plane_sessions_by_instance_ids(instance_ids);
Ok(())
}
}
pub(crate) struct FfiContext {
pub(crate) runtime: Runtime,
pub(crate) manager: Arc<NativeInstanceManager>,
pub(crate) process_management: NativeProcessManagement,
}
impl FfiContext {
fn new() -> Self {
let runtime = Builder::new_multi_thread()
.enable_all()
.build()
.expect("tokio runtime for easytier-ffi");
let manager = Arc::new(native_instance_manager_with_runtime(
runtime.handle().clone(),
));
let process_management =
native_process_management(manager.clone(), Arc::new(FfiOwnedInstanceHooks));
Self {
runtime,
manager,
process_management,
}
}
}
static FFI_CONTEXT: once_cell::sync::Lazy<FfiContext> = once_cell::sync::Lazy::new(FfiContext::new);
pub(crate) fn ffi_context() -> &'static FfiContext {
&FFI_CONTEXT
}
pub(crate) fn resolve_instance_id_by_name(inst_name: &str) -> Result<Option<uuid::Uuid>, String> {
easytier_core::management::resolve_optional_instance_by_name(
ffi_context().manager.as_ref(),
inst_name,
)
.map(|instance| instance.map(|instance| instance.instance_id()))
.map_err(|error| error.to_string())
}
#[cfg(test)]
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<uuid::Uuid> {
resolve_instance_id_by_name(inst_name).ok().flatten()
}
@@ -0,0 +1,23 @@
use std::ffi::{CStr, c_char};
pub(crate) unsafe fn c_str_to_string(ptr: *const c_char, name: &str) -> Result<String, String> {
if ptr.is_null() {
return Err(format!("{} is null", name));
}
unsafe { CStr::from_ptr(ptr) }
.to_str()
.map(|value| value.to_string())
.map_err(|err| format!("{} is not valid UTF-8: {}", name, err))
}
pub(crate) unsafe fn optional_c_str_to_string(
ptr: *const c_char,
name: &str,
) -> Result<Option<String>, String> {
if ptr.is_null() {
return Ok(None);
}
unsafe { c_str_to_string(ptr, name) }.map(Some)
}
+746
View File
@@ -0,0 +1,746 @@
use crate::{
config_server::{
ConfigServerCallbackScope, ManagedConfigServerClientHooks, set_active_for_test,
},
state::{ffi_context, find_instance_id_by_name},
*,
};
use easytier::{
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
web_client::WebClientHooks,
};
use serde_json::Value;
use std::{
collections::HashSet,
ffi::{CStr, CString, c_char, c_int, c_void},
sync::{Mutex, mpsc},
time::Duration,
};
use uuid::Uuid;
#[test]
fn test_parse_config() {
let cfg_str = r#"
inst_name = "test"
network = "test_network"
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
unsafe {
assert_eq!(parse_config(cstr.as_ptr()), 0);
}
}
#[test]
fn test_run_network_instance() {
let cfg_str = r#"
inst_name = "test"
network = "test_network"
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
unsafe {
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
}
}
#[test]
fn get_error_msg_returns_config_server_callback_error() {
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
let callback_error = format!("callback delivery failed {}", Uuid::new_v4());
crate::config_server::clear_last_callback_error();
hooks.note_callback_error(callback_error.clone());
unsafe {
let mut error_ptr: *const c_char = std::ptr::null();
get_error_msg(&mut error_ptr);
assert!(!error_ptr.is_null());
let error_msg = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
free_string(error_ptr);
assert!(error_msg.contains(&callback_error));
}
crate::config_server::clear_last_callback_error();
}
unsafe extern "C" fn record_config_server_event(event_json: *const c_char, user_data: *mut c_void) {
let events = unsafe { &*(user_data as *const Mutex<Vec<String>>) };
events.lock().unwrap().push(
unsafe { CStr::from_ptr(event_json) }
.to_string_lossy()
.into_owned(),
);
}
fn take_last_error() -> Option<String> {
unsafe {
let mut error_ptr: *const c_char = std::ptr::null();
get_error_msg(&mut error_ptr);
if error_ptr.is_null() {
None
} else {
let error = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
free_string(error_ptr);
Some(error)
}
}
}
fn free_key_value_pairs(infos: &[KeyValuePair]) {
for info in infos {
free_string(info.key);
free_string(info.value);
}
}
#[test]
fn list_instance_returns_instance_names_and_ids() {
let instance_id = Uuid::new_v4();
let instance_name = format!("list-instance-{}", instance_id);
let cfg = TomlConfigLoader::default();
cfg.set_id(instance_id);
cfg.set_inst_name(instance_name.clone());
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
let mut infos = vec![
KeyValuePair {
key: std::ptr::null(),
value: std::ptr::null(),
};
16
];
let count = unsafe { list_instance(infos.as_mut_ptr(), infos.len()) };
assert!(count > 0);
let mut found = false;
for info in infos.iter().take(count as usize) {
let key = unsafe { CStr::from_ptr(info.key) }.to_string_lossy();
let value = unsafe { CStr::from_ptr(info.value) }.to_string_lossy();
if key == instance_name {
assert_eq!(value, instance_id.to_string());
found = true;
}
}
free_key_value_pairs(&infos[..count as usize]);
ffi_context()
.runtime
.block_on(
ffi_context()
.manager
.delete_network_instances([instance_id]),
)
.unwrap();
assert!(found);
}
#[test]
fn list_instance_allows_zero_length() {
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, 0);
}
#[test]
fn list_instance_rejects_null_output_pointer() {
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 1) }, -1);
assert!(take_last_error().unwrap().contains("infos is null"));
}
#[test]
fn call_json_rpc_returns_logger_response() {
let service = CString::new("api.logger.LoggerRpcService").unwrap();
let method = CString::new("get_logger_config").unwrap();
let payload = CString::new("{}").unwrap();
let mut response_ptr: *const c_char = std::ptr::null();
assert_eq!(
unsafe {
call_json_rpc(
service.as_ptr(),
method.as_ptr(),
std::ptr::null(),
payload.as_ptr(),
&mut response_ptr,
)
},
0
);
assert!(!response_ptr.is_null());
let response = unsafe { CStr::from_ptr(response_ptr) }
.to_string_lossy()
.into_owned();
free_string(response_ptr);
let response: Value = serde_json::from_str(&response).unwrap();
assert!(response.get("level").is_some());
}
#[test]
fn call_json_rpc_rejects_instance_management_service() {
let service = CString::new("api.manage.WebClientService").unwrap();
let method = CString::new("list_network_instance").unwrap();
let payload = CString::new("{}").unwrap();
let mut response_ptr: *const c_char = std::ptr::null();
assert_eq!(
unsafe {
call_json_rpc(
service.as_ptr(),
method.as_ptr(),
std::ptr::null(),
payload.as_ptr(),
&mut response_ptr,
)
},
-1
);
assert!(response_ptr.is_null());
assert!(take_last_error().unwrap().contains("not exposed"));
}
#[test]
fn call_json_rpc_rejects_malformed_payload_json() {
let service = CString::new("api.logger.LoggerRpcService").unwrap();
let method = CString::new("get_logger_config").unwrap();
let payload = CString::new("{").unwrap();
let mut response_ptr: *const c_char = std::ptr::null();
assert_eq!(
unsafe {
call_json_rpc(
service.as_ptr(),
method.as_ptr(),
std::ptr::null(),
payload.as_ptr(),
&mut response_ptr,
)
},
-1
);
assert!(response_ptr.is_null());
assert!(
take_last_error()
.unwrap()
.contains("failed to parse payload_json")
);
}
#[test]
fn call_json_rpc_rejects_null_output_pointer() {
let service = CString::new("api.logger.LoggerRpcService").unwrap();
let method = CString::new("get_logger_config").unwrap();
let payload = CString::new("{}").unwrap();
assert_eq!(
unsafe {
call_json_rpc(
service.as_ptr(),
method.as_ptr(),
std::ptr::null(),
payload.as_ptr(),
std::ptr::null_mut(),
)
},
-1
);
assert!(
take_last_error()
.unwrap()
.contains("out_response_json is null")
);
}
#[tokio::test]
async fn config_server_hooks_emit_run_event() {
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
let hooks = ManagedConfigServerClientHooks::new(
Some(record_config_server_event),
&events as *const _ as *mut c_void,
);
let instance_id = Uuid::new_v4();
let cfg = TomlConfigLoader::default();
cfg.set_id(instance_id);
let inst_name = format!("test-{}", instance_id);
cfg.set_inst_name(inst_name.clone());
hooks.pre_run_network_instance(&cfg).await.unwrap();
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
hooks.post_run_network_instance(&instance_id).await.unwrap();
let duplicate_cfg = TomlConfigLoader::default();
duplicate_cfg.set_inst_name(inst_name);
duplicate_cfg.set_id(Uuid::new_v4());
assert!(
hooks
.pre_run_network_instance(&duplicate_cfg)
.await
.is_err()
);
assert_eq!(hooks.tracked_instance_ids(), vec![instance_id]);
let events = events.lock().unwrap().clone();
assert_eq!(events.len(), 1);
let event: Value = serde_json::from_str(&events[0]).unwrap();
assert_eq!(event["event"], "run_network_instance");
assert_eq!(event["success"], true);
assert_eq!(event["instance_id"], instance_id.to_string());
assert!(event["error"].is_null());
ffi_context()
.manager
.delete_network_instances([instance_id])
.await
.unwrap();
}
#[tokio::test]
async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
let hooks = ManagedConfigServerClientHooks::new(
Some(record_config_server_event),
&events as *const _ as *mut c_void,
);
let instance_id_1 = Uuid::new_v4();
let instance_id_2 = Uuid::new_v4();
let unknown_instance_id = Uuid::new_v4();
for id in [instance_id_1, instance_id_2] {
let cfg = TomlConfigLoader::default();
cfg.set_id(id);
cfg.set_inst_name(format!("test-{}", id));
hooks.pre_run_network_instance(&cfg).await.unwrap();
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
}
hooks
.post_run_network_instance(&instance_id_1)
.await
.unwrap();
hooks
.post_run_network_instance(&instance_id_2)
.await
.unwrap();
events.lock().unwrap().clear();
hooks
.post_remove_network_instances(&[instance_id_1, unknown_instance_id, instance_id_2])
.await
.unwrap();
assert!(hooks.tracked_instance_ids().is_empty());
let events = events.lock().unwrap().clone();
assert_eq!(events.len(), 2);
let event_ids = events
.iter()
.map(|event| {
let event: Value = serde_json::from_str(event).unwrap();
assert_eq!(event["event"], "delete_network_instance");
assert_eq!(event["success"], true);
assert!(event["error"].is_null());
event["instance_id"].as_str().unwrap().to_string()
})
.collect::<HashSet<_>>();
assert_eq!(
event_ids,
HashSet::from([instance_id_1.to_string(), instance_id_2.to_string()])
);
ffi_context()
.manager
.delete_network_instances([instance_id_1, instance_id_2])
.await
.unwrap();
}
#[tokio::test]
async fn config_server_hooks_ignore_untracked_instance_without_event() {
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
let hooks = ManagedConfigServerClientHooks::new(
Some(record_config_server_event),
&events as *const _ as *mut c_void,
);
let local_id = Uuid::new_v4();
hooks
.post_remove_network_instances(&[local_id])
.await
.unwrap();
assert!(events.lock().unwrap().is_empty());
}
#[tokio::test]
async fn config_server_hooks_reject_duplicate_instance_name() {
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
let inst_name = format!("test-{}", Uuid::new_v4());
let existing_id = Uuid::new_v4();
let new_id = Uuid::new_v4();
let existing_cfg = TomlConfigLoader::default();
existing_cfg.set_inst_name(inst_name.clone());
existing_cfg.set_id(existing_id);
ffi_context()
.manager
.run_network_instance(existing_cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
let cfg = TomlConfigLoader::default();
cfg.set_inst_name(inst_name.clone());
cfg.set_id(new_id);
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
assert_eq!(find_instance_id_by_name(&inst_name), Some(existing_id));
ffi_context()
.manager
.delete_network_instances([existing_id])
.await
.unwrap();
}
#[tokio::test]
async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error() {
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
let hooks = ManagedConfigServerClientHooks::new(
Some(record_config_server_event),
&events as *const _ as *mut c_void,
);
let old_name = format!("old-{}", Uuid::new_v4());
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
let overwritten_id = Uuid::new_v4();
let duplicate_id = Uuid::new_v4();
hooks.instance_ids.lock().unwrap().insert(overwritten_id);
for (id, name) in [
(overwritten_id, old_name.clone()),
(duplicate_id, duplicate_name.clone()),
] {
let cfg = TomlConfigLoader::default();
cfg.set_id(id);
cfg.set_inst_name(name);
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
}
ffi_context()
.manager
.delete_network_instances([overwritten_id])
.await
.unwrap();
hooks
.post_remove_network_instances(&[overwritten_id])
.await
.unwrap();
let cfg = TomlConfigLoader::default();
cfg.set_inst_name(duplicate_name.clone());
cfg.set_id(overwritten_id);
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
assert!(hooks.tracked_instance_ids().is_empty());
assert!(find_instance_id_by_name(&old_name).is_none());
assert_eq!(
find_instance_id_by_name(&duplicate_name),
Some(duplicate_id)
);
assert_eq!(events.lock().unwrap().len(), 1);
ffi_context()
.manager
.delete_network_instances([duplicate_id])
.await
.unwrap();
}
#[tokio::test]
async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
let inst_name = format!("test-{}", Uuid::new_v4());
let instance_id = Uuid::new_v4();
hooks.instance_ids.lock().unwrap().insert(instance_id);
let cfg = TomlConfigLoader::default();
cfg.set_inst_name(inst_name.clone());
cfg.set_id(instance_id);
ffi_context()
.manager
.run_network_instance(cfg.clone(), ConfigFileControl::STATIC_CONFIG)
.unwrap();
ffi_context()
.manager
.delete_network_instances([instance_id])
.await
.unwrap();
hooks
.post_remove_network_instances(&[instance_id])
.await
.unwrap();
hooks.pre_run_network_instance(&cfg).await.unwrap();
assert!(hooks.tracked_instance_ids().is_empty());
assert!(find_instance_id_by_name(&inst_name).is_none());
}
#[tokio::test]
async fn config_server_hooks_reject_post_run_after_external_delete() {
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
let instance_id = Uuid::new_v4();
let cfg = TomlConfigLoader::default();
cfg.set_id(instance_id);
cfg.set_inst_name(format!("test-{}", instance_id));
hooks.pre_run_network_instance(&cfg).await.unwrap();
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
ffi_context()
.manager
.delete_network_instances([instance_id])
.await
.unwrap();
assert!(hooks.post_run_network_instance(&instance_id).await.is_err());
}
#[test]
fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() {
let instance_id = Uuid::new_v4();
let inst_name = format!("test-{}", instance_id);
let cfg = TomlConfigLoader::default();
cfg.set_id(instance_id);
cfg.set_inst_name(inst_name.clone());
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
assert_eq!(find_instance_id_by_name(&inst_name), Some(instance_id));
ffi_context()
.runtime
.block_on(
ffi_context()
.manager
.delete_network_instances([instance_id]),
)
.unwrap();
}
#[test]
fn delete_network_instance_removes_only_named_instances() {
let keep_id = Uuid::new_v4();
let delete_id = Uuid::new_v4();
let keep_name = format!("keep-{}", keep_id);
let delete_name = format!("delete-{}", delete_id);
for (id, name) in [
(keep_id, keep_name.clone()),
(delete_id, delete_name.clone()),
] {
let cfg = TomlConfigLoader::default();
cfg.set_id(id);
cfg.set_inst_name(name.clone());
ffi_context()
.manager
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
.unwrap();
}
let delete_name = CString::new(delete_name.clone()).unwrap();
let inst_names = [delete_name.as_ptr()];
assert_eq!(
unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) },
0
);
assert_eq!(find_instance_id_by_name(&keep_name), Some(keep_id));
assert!(find_instance_id_by_name(delete_name.to_str().unwrap()).is_none());
ffi_context()
.runtime
.block_on(ffi_context().manager.delete_network_instances([keep_id]))
.unwrap();
}
#[test]
fn retain_and_delete_network_instance_reject_invalid_name_pointers() {
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 1) }, -1);
assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 1) }, -1);
let inst_names = [std::ptr::null()];
assert_eq!(
unsafe { retain_network_instance(inst_names.as_ptr(), inst_names.len()) },
-1
);
assert_eq!(
unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) },
-1
);
}
#[test]
fn ffi_process_management_uses_manager_mutation_lock() {
let manager_guard = ffi_context().manager.mutation_lock().blocking_lock_owned();
let (done_tx, done_rx) = mpsc::channel();
let waiter = std::thread::spawn(move || {
ffi_context()
.runtime
.block_on(
ffi_context()
.process_management
.delete_owned_network_instances(Vec::new()),
)
.unwrap();
done_tx.send(()).unwrap();
});
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
drop(manager_guard);
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
waiter.join().unwrap();
}
#[tokio::test]
async fn config_server_hooks_reject_late_runs_for_core_rollback() {
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
let hooks = ManagedConfigServerClientHooks::new(
Some(record_config_server_event),
&events as *const _ as *mut c_void,
);
hooks.start_stopping();
assert!(
hooks
.post_run_network_instance(&Uuid::new_v4())
.await
.is_err()
);
assert!(hooks.tracked_instance_ids().is_empty());
assert!(events.lock().unwrap().is_empty());
}
#[test]
fn delete_network_instance_rejects_an_ambiguous_name() {
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
let instance_ids = [Uuid::new_v4(), Uuid::new_v4()];
for instance_id in instance_ids {
let config = TomlConfigLoader::default();
config.set_id(instance_id);
config.set_inst_name(duplicate_name.clone());
ffi_context()
.manager
.run_network_instance(config, ConfigFileControl::STATIC_CONFIG)
.unwrap();
}
let duplicate_name = CString::new(duplicate_name).unwrap();
let names = [duplicate_name.as_ptr()];
assert_eq!(
unsafe { delete_network_instance(names.as_ptr(), names.len()) },
-1
);
assert!(take_last_error().unwrap().contains("2 instances match"));
assert!(
instance_ids
.iter()
.all(|id| ffi_context().manager.instance(*id).is_some())
);
ffi_context()
.runtime
.block_on(
ffi_context()
.process_management
.delete_owned_network_instances(instance_ids.to_vec()),
)
.unwrap();
}
#[test]
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
let _callback_scope = ConfigServerCallbackScope::enter();
assert_eq!(is_config_server_client_connected(), 0);
let service = CString::new("api.logger.LoggerRpcService").unwrap();
let method = CString::new("get_logger_config").unwrap();
let payload = CString::new("{}").unwrap();
let mut response_ptr: *const c_char = std::ptr::null();
assert_eq!(
unsafe {
call_json_rpc(
service.as_ptr(),
method.as_ptr(),
std::ptr::null(),
payload.as_ptr(),
&mut response_ptr,
)
},
-1
);
assert!(response_ptr.is_null());
assert_eq!(
unsafe { collect_network_infos(std::ptr::null_mut(), 0) },
-1
);
assert_eq!(unsafe { list_instance(std::ptr::null_mut(), 0) }, -1);
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 0) }, -1);
let url = CString::new("ring://test/token").unwrap();
let machine_id = CString::new("test-machine").unwrap();
assert_eq!(
unsafe {
start_config_server_client(
url.as_ptr(),
std::ptr::null(),
machine_id.as_ptr(),
false,
None,
std::ptr::null_mut(),
)
},
-1
);
assert_eq!(stop_config_server_client(), -1);
#[cfg(feature = "ffi-dataplane")]
{
let mut session = 0;
assert_eq!(
unsafe { data_plane_session_open(std::ptr::null(), &mut session) },
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
);
assert_eq!(session, 0);
}
}
#[cfg(feature = "ffi-dataplane")]
#[test]
fn active_config_server_rejects_data_plane() {
set_active_for_test(true);
let name = CString::new("missing").unwrap();
let mut session = 0;
assert_eq!(
unsafe { data_plane_session_open(name.as_ptr(), &mut session) },
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
);
assert_eq!(session, 0);
set_active_for_test(false);
}
#[cfg(feature = "ffi-dataplane")]
#[test]
fn data_plane_invalid_handle_errors_are_stable() {
let closed = -(easytier_core::gateway::DataPlaneErrorKind::HandleClosed as c_int);
assert_eq!(data_plane_completion_wait(u64::MAX, 0), closed);
assert_eq!(data_plane_operation_cancel(u64::MAX, 1), closed);
assert_eq!(data_plane_operation_free(u64::MAX, 1), closed);
assert_eq!(data_plane_resource_close(u64::MAX, 1), closed);
assert_eq!(
data_plane_resource_deadline_set(u64::MAX, 1, DATA_PLANE_DEADLINE_READ, 0),
closed
);
}
@@ -0,0 +1,30 @@
use std::ffi::{c_char, c_void};
#[repr(C)]
#[derive(Clone, Copy)]
pub struct KeyValuePair {
pub key: *const c_char,
pub value: *const c_char,
}
pub type ConfigServerEventCallback = Option<unsafe extern "C" fn(*const c_char, *mut c_void)>;
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DataPlaneSocketAddr {
/// `4` for IPv4. Other families are reserved for later ABI versions.
pub family: u16,
/// Native-endian port number.
pub port: u16,
/// Network-order address bytes. IPv4 uses the first four bytes.
pub address: [u8; 16],
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DataPlaneCompletion {
pub operation_id: u64,
pub operation_kind: u16,
/// `0` for success, otherwise a stable `DataPlaneErrorKind` value.
pub status: u16,
}
+69 -9
View File
@@ -1,14 +1,74 @@
#!/data/adb/magisk/busybox sh
MODDIR=${0%/*}
MODULE_PROP="${MODDIR}/module.prop"
IP_RULE_SCRIPT="${MODDIR}/hotspot_iprule.sh"
# 查找 easytier-core 进程的 PID
PID=$(pgrep easytier-core)
ET_STATUS=""
REDIR_STATUS=""
IS_RUNNING=false
# 检查是否找到了进程
if [ -z "$PID" ]; then
echo "easytier-core 进程未找到"
else
# 结束进程
kill $PID
echo "已结束 easytier-core 进程 (PID: $PID)"
# 确保辅助脚本有执行权限
chmod +x "${IP_RULE_SCRIPT}" 2>/dev/null
# 更新 module.prop 文件中的 description
update_module_description() {
local status_message=$1
# 检查 module.prop 文件存在且 description 发生变化了再写入
if [ -f "${MODULE_PROP}" ]; then
local current_desc=$(grep "^description=" "${MODULE_PROP}")
local new_desc="description=[状态] ${status_message}"
if [ "${current_desc}" != "${new_desc}" ]; then
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
fi
fi
}
# 判断程序启动状态
if [ -f "${MODDIR}/disable" ]; then
IS_RUNNING=false
ET_STATUS="主程序已关闭"
elif pgrep -f "${MODDIR}/easytier-core" >/dev/null; then
IS_RUNNING=true
if [ -f "${MODDIR}/config/command_args" ]; then
ET_STATUS="主程序正在运行(启动参数模式)"
else
ET_STATUS="主程序正在运行(配置文件模式)"
fi
elif [ -z "$ET_STATUS" ]; then
# 既没 disable 也没运行,说明是异常停止或未启动
ET_STATUS="主程序启动失败或未运行"
fi
# 无论主程序是否运行,都允许切换“开关文件”的状态,以便下次生效
if [ -f "${MODDIR}/enable_IP_rule" ]; then
rm -f "${MODDIR}/enable_IP_rule"
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
REDIR_STATUS="转发已禁用"
echo "热点子网转发已禁用"
echo "[ET-NAT] Action: IP rule disabled." >> "${MODDIR}/log.log"
else
touch "${MODDIR}/enable_IP_rule"
if [ "$IS_RUNNING" = true ]; then
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
"${IP_RULE_SCRIPT}" add_once
echo "转发规则将立即生效,无需重启"
else
echo "主程序未运行,转发规则将在下次启动时生效"
fi
REDIR_STATUS="转发已激活"
echo "----------------------------------"
echo "热点子网转发已激活"
echo "热点开启后将自动将热点加入转发网络"
echo "需要在配置中提前配置好 cidr 参数"
echo "----------------------------------"
echo "[ET-NAT] Action: IP rule enabled." >> "${MODDIR}/log.log"
fi
sync
update_module_description "${ET_STATUS}| ${REDIR_STATUS}"
@@ -0,0 +1 @@
--config-server udp://127.0.0.1:22020/admin --machine-id easytier-magisk
@@ -33,5 +33,6 @@ foreign_network_whitelist = "*"
disable_p2p = false
relay_all_peer_rpc = false
disable_udp_hole_punching = false
disable_tcp_hole_punching = false
+19 -7
View File
@@ -1,7 +1,19 @@
ui_print '安装完成'
ui_print '当前架构为' + $ARCH
ui_print '当前系统版本为' + $API
ui_print '安装目录为: /data/adb/modules/easytier_magisk'
ui_print '配置文件位置: /data/adb/modules/easytier_magisk/config/config.toml'
ui_print '修改后配置文件后在magisk app点击操作按钮即可生效'
ui_print '记得重启'
SKIPMOUNT=false
PROPFILE=true
POSTFSDATA=true
LATESTARTSERVICE=true
set_perm_recursive $MODPATH 0 0 0777 0777
ui_print "系统架构为:$ARCH"
ui_print "系统 SDK 版本:$API"
ui_print "EasyTier 安装位置:/data/adb/modules/easytier_magisk"
ui_print "配置文件位置:/data/adb/modules/easytier_magisk/config/config.toml"
ui_print "如需使用启动参数模式,请将 /data/adb/modules/easytier_magisk/config/command_args_sample 重命名为 command_args,并修改其中的内容"
ui_print "config 目录中存在 command_args 文件时,模块会自动忽略 config.toml 文件"
ui_print "----------------------------------"
ui_print "注意!启动参数文件中不能存在 \" 和 ',配置文件则没有这个限制"
ui_print "----------------------------------"
ui_print "修改配置后无需重启设备,在 Magisk 中禁用 EasyTier 模块,等待 10 秒后重新启用即可让新配置生效"
ui_print "点击 Magisk 中模块左下角的“操作”按钮可以禁用或激活热点子网转发,使用该功能前需要在配置中提前配置好 cidr 参数"
ui_print "模块安装完成,重启设备生效"
@@ -2,47 +2,111 @@
MODDIR=${0%/*}
CONFIG_FILE="${MODDIR}/config/config.toml"
COMMAND_ARGS="${MODDIR}/config/command_args"
LOG_FILE="${MODDIR}/log.log"
MODULE_PROP="${MODDIR}/module.prop"
EASYTIER="${MODDIR}/easytier-core"
# 更新module.prop文件中的description
# 处理获取到的设备型号中可能出现的空格
BRAND=$(getprop ro.product.brand | tr ' ' '-')
MODEL=$(getprop ro.product.model | tr ' ' '-')
DEVICE_HOSTNAME="${BRAND}-${MODEL}"
REDIR_STATUS=""
# 更新 module.prop 文件中的 description
update_module_description() {
local status_message=$1
sed -i "/^description=/c\description=[状态]${status_message}" ${MODULE_PROP}
# 检查 module.prop 文件存在且 description 发生变化了再写入
if [ -f "${MODULE_PROP}" ]; then
local current_desc=$(grep "^description=" "${MODULE_PROP}")
local new_desc="description=[状态] ${status_message}"
if [ "${current_desc}" != "${new_desc}" ]; then
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
fi
fi
}
# 检查并初始化 TUN 设备
if [ ! -e /dev/net/tun ]; then
if [ ! -d /dev/net ]; then
mkdir -p /dev/net
fi
ln -s /dev/tun /dev/net/tun
fi
while true; do
if ls $MODDIR | grep -q "disable"; then
update_module_description "关闭中"
if pgrep -f 'easytier-core' >/dev/null; then
echo "开关控制$(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭 ..."
pkill easytier-core # 关闭进程
fi
# 获取子网转发激活状态
if [ -f "${MODDIR}/enable_IP_rule" ]; then
REDIR_STATUS="转发已激活"
else
if ! pgrep -f 'easytier-core' >/dev/null; then
if [ ! -f "$CONFIG_FILE" ]; then
update_module_description "config.toml不存在"
sleep 3s
continue
fi
REDIR_STATUS="转发已禁用"
fi
TZ=Asia/Shanghai ${EASYTIER} -c ${CONFIG_FILE} > ${LOG_FILE} &
sleep 5s # 等待easytier-core启动完成
update_module_description "已开启(不一定运行成功)"
ip rule add from all lookup main
else
echo "开关控制$(date "+%Y-%m-%d %H:%M:%S") 进程已存在"
# 检查模块是否被禁用
if [ -f "${MODDIR}/disable" ]; then
update_module_description "主程序已关闭 | ${REDIR_STATUS}"
if pgrep -f "${EASYTIER}" >/dev/null; then
echo "开关控制 $(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭"
pkill -f "${EASYTIER}"
fi
sleep 10s
continue
fi
sleep 3s # 暂停3秒后再次执行循环
# 检查进程是否已经在运行
if pgrep -f "${EASYTIER}" >/dev/null; then
sleep 10s
continue
fi
# 检查配置文件是否存在
if [ ! -f "${CONFIG_FILE}" ] && [ ! -f "${COMMAND_ARGS}" ]; then
update_module_description "缺少配置文件或启动参数文件"
sleep 10s
continue
fi
# 如果 config 目录下存在 command_args 文件,则读取其中的内容作为启动参数
if [ -f "${COMMAND_ARGS}" ]; then
# 启动参数模式
CMD_CONTENT=$(tr '\r\n' ' ' < "${COMMAND_ARGS}")
if echo "${CMD_CONTENT}" | grep -q "\-\-hostname"; then
FINAL_ARGS="${CMD_CONTENT}"
else
FINAL_ARGS="${CMD_CONTENT} --hostname ${DEVICE_HOSTNAME}"
fi
TZ=Asia/Shanghai "${EASYTIER}" ${FINAL_ARGS} > "${LOG_FILE}" 2>&1 &
STR_MODE="启动参数模式"
# 否则读取 config.toml 的内容作为启动参数
else
# 配置文件模式
if grep -q "^[[:space:]]*hostname[[:space:]]*=" "${CONFIG_FILE}"; then
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" > "${LOG_FILE}" 2>&1 &
else
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" --hostname "${DEVICE_HOSTNAME}" > "${LOG_FILE}" 2>&1 &
fi
STR_MODE="配置文件模式"
fi
# 等待进程启动
sleep 5s
# 启动后的扫尾工作
if pgrep -f "${EASYTIER}" >/dev/null; then
if ! ip rule show | grep -qE '^[0-9]+:[[:space:]]+from all lookup main$'; then
ip rule add from all lookup main
fi
update_module_description "主程序正在运行(${STR_MODE}| ${REDIR_STATUS}"
else
update_module_description "主程序启动失败,请检查配置文件或启动参数"
fi
sleep 10s
done
@@ -0,0 +1,104 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_FILE="${MODDIR}/config/config.toml"
LOG_FILE="${MODDIR}/log.log"
ACTION="$1" # 参数:add add_once del
# 获取接口/IP
get_et_iface() {
awk '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*dev_name[[:space:]]*=/ {
val = $0
sub(/^[^=]*=[[:space:]]*/, "", val)
gsub(/[" \t]/, "", val)
print val
exit
}
' "$CONFIG_FILE"
}
get_tun_iface() {
ip link | awk -F': ' '/ tun[[:alnum:]]+/ {print $2; exit}'
}
get_hot_iface() {
ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|p2p-wlan[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
}
get_usb_iface() {
ip link | awk -F': ' '/(^| )(usb[[:alnum:]_]*|rndis[[:alnum:]_]*|eth[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
}
get_hot_cidr() {
ip -4 addr show dev "$1" | awk '/inet /{print $2; exit}'
}
set_nat_rules() {
ET_IFACE=$(get_et_iface)
[ -z "$ET_IFACE" ] && ET_IFACE="$(get_tun_iface)"
HOT_IFACE=$(get_hot_iface)
USB_IFACE=$(get_usb_iface)
HOT_CIDR=$(get_hot_cidr "$HOT_IFACE")
USB_CIDR=$(get_hot_cidr "$USB_IFACE")
# 如果热点关闭就删除自定义链
[ -n "$ET_IFACE" ] && { [ -n "$HOT_CIDR" ] || [ -n "$USB_CIDR" ]; } || return 1
# 创建自定义链(如不存在)
iptables -t nat -N ET_NAT 2>/dev/null
iptables -N ET_FWD 2>/dev/null
# 确保主链首条跳转到自定义链
iptables -t nat -C POSTROUTING -j ET_NAT 2>/dev/null || \
iptables -t nat -I POSTROUTING 1 -j ET_NAT
iptables -C FORWARD -j ET_FWD 2>/dev/null || \
iptables -I FORWARD 1 -j ET_FWD
# 添加规则
if [ -n "$HOT_CIDR" ]; then
iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE
iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \
-m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR$ET_IFACE" >> "$LOG_FILE"
fi
if [ -n "$USB_CIDR" ]; then
iptables -t nat -A ET_NAT -s "$USB_CIDR" -o "$ET_IFACE" -j MASQUERADE
iptables -A ET_FWD -i "$USB_IFACE" -o "$ET_IFACE" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A ET_FWD -i "$ET_IFACE" -o "$USB_IFACE" \
-m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[ET-NAT] Rules applied: $USB_IFACE $USB_CIDR$ET_IFACE" >> "$LOG_FILE"
fi
}
flush_rules() {
iptables -t nat -F ET_NAT 2>/dev/null
iptables -F ET_FWD 2>/dev/null
echo "[ET-NAT] Custom chains flushed." >> "$LOG_FILE"
}
case "$ACTION" in
add)
set_nat_rules
echo "[ET-NAT] Guard started." >> "$LOG_FILE"
ip monitor link addr | while read -r _; do
if [ -f "${MODDIR}/enable_IP_rule" ]; then
flush_rules
set_nat_rules
fi
done
;;
add_once)
flush_rules
set_nat_rules
echo "[ET-NAT] One-time rules applied." >> "$LOG_FILE"
;;
del)
flush_rules
;;
*)
echo "Usage: $0 [add|del]"
exit 1
;;
esac
+1 -1
View File
@@ -1,6 +1,6 @@
id=easytier_magisk
name=EasyTier_Magisk
version=v2.3.1
version=v2.6.4
versionCode=1
author=EasyTier
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
+3 -6
View File
@@ -18,10 +18,7 @@ sed -i 's/$(description=)$[^"]*/\1[状态]关闭中/' "$MODDIR/module.prop"
sleep 3s
"${MODDIR}/easytier_core.sh" &
"${MODDIR}/hotspot_iprule.sh" add &
# 检查是否启用模块
while [ ! -f ${MODDIR}/disable ]; do
sleep 2
done
pkill easytier-core
# easytier_core.sh 和 hotspot_iprule.sh 都有内部循环做守护,
# 所以这里不需要再做守护了
@@ -1,3 +1,5 @@
MODDIR=${0%/*}
pkill easytier-core # 结束 easytier-core 进程
rm -rf $MODDIR/*
pkill -f "${MODDIR}/easytier-core"
# 使用 ${MODDIR:?} 确保变量非空,避免执行 rm -rf /*
rm -rf "${MODDIR:?}/"*
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "easytier-mini"
description = "Minimal native EasyTier node with TCP/UDP tunnels, TUN and UDP hole punching."
version = "2.6.4"
edition.workspace = true
rust-version.workspace = true
license-file = "../../LICENSE"
build = "build.rs"
[dependencies]
anyhow = "1.0"
easytier = { path = "../../easytier", version = "2.6.4", default-features = false, features = [
"aes-gcm",
"dhcp-ipv4",
"logging",
"proxy-cidr-monitor",
"smoltcp",
"tun",
"web-client",
] }
tokio = { version = "1", default-features = false, features = ["macros", "rt", "signal"] }
+113
View File
@@ -0,0 +1,113 @@
# easytier-mini
`easytier-mini` is a native EasyTier POC binary. It shares EasyTier's TOML
configuration model, peer protocol, TCP/UDP tunnel implementations, TUN,
dynamic IPv4 allocation, the smoltcp userspace path and STUN/UDP hole-punching
core with the full binary. It includes AES-GCM so its default encryption
setting interoperates with the full binary's default configuration.
Build it with:
```sh
cargo build --release -p easytier-mini
```
For the static size target used by this POC:
```sh
cargo build --profile mini --target x86_64-unknown-linux-musl -p easytier-mini
```
MIPS targets use the repository's existing musl-cross toolchains. The helper
builds the standard library for size, applies immediate-abort only to the mini
MIPS target graph, and can build either or both byte orders:
```sh
./easytier-contrib/easytier-mini/build-mips.sh all
./easytier-contrib/easytier-mini/build-mips.sh mips
./easytier-contrib/easytier-mini/build-mips.sh mipsel
```
The `mini` profile derives from `release` and applies `opt-level=z` to the
entire compact binary dependency graph. Full EasyTier release builds retain
their normal `opt-level=3` profile. The musl builds use a mini-only static
linker policy to stay below 5,000,000 bytes on x86-64 and 5,500,000 bytes on
MIPS without UPX or another executable compressor. The compact x86-64 linker
policy retains static PIE, packs relative relocations and folds identical code.
MIPS builds omit standard-library backtrace support and use immediate abort;
normal workspace MIPS builds are not affected. Compact linker policies omit
unwind tables.
Start it with a normal EasyTier TOML file:
```sh
easytier-mini --config mini.toml
```
`-c` is accepted as the short form of `--config`.
Start it as an EasyTier Web managed node with a complete config-server URL:
```sh
easytier-mini --config-server udp://config-server.easytier.cn:22020/TOKEN
```
`--machine-id`, `--hostname`, and `--secure-mode` match the full client's Web
identity and transport options. `--config` and `--config-server` may be used
together: the local instance remains static while Web-owned instances are
created, updated, retained, and deleted independently.
The node also exposes the native EasyTier management RPC protocol on
`127.0.0.1:15888`, so the full `easytier-cli` can inspect it:
```sh
easytier-cli node info
easytier-cli peer
easytier-cli route
easytier-cli connector list
```
For example:
```toml
instance_name = "mini"
ipv4 = "10.147.0.2"
listeners = ["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"]
[network_identity]
network_name = "mini-poc"
network_secret = "change-me"
[[peer]]
uri = "tcp://example.net:11010"
```
Local TOML and Web configuration both retain the complete authoritative model.
The compact runtime silently omits unsupported capabilities while normalizing
that model into live runtime state. EasyTier Web therefore sees every accepted
configuration value unchanged and its consistency checks converge. This also
applies to hot patches: for example, a port-forward patch remains visible to
the controller while no port-forward service starts in mini. ChaCha20 falls
back to AES-GCM rather than plaintext.
The compact runtime supports `tcp://` and `udp://` listener, mapped-listener
and peer URLs. `no_tun = true` runs through smoltcp without an OS TUN device,
and `dhcp = true` allocates the virtual IPv4 address dynamically.
The mini feature set keeps STUN collection, UDP hole punching, Web heartbeats,
Web instance lifecycle management and the config hot-patch RPC. It omits TCP
hole punching, endpoint discovery (`http://`, `https://`, `txt://` and
`srv://` peers), protobuf reflection, logger control and the rest of the full
management surface. Unsupported connector URLs are accepted as no-ops. Its
local RPC surface remains read-only for node, peer, route and connector
queries. OSPF route messages keep their original protobuf wire data, so fields
added by future EasyTier versions are forwarded without requiring
`prost-reflect`.
For size, this POC reads one file directly and does not support configuration
from stdin or `${VAR}` expansion. It omits the process-management event journal,
while the console logger still reports runtime events such as peer, connection,
listener, TUN and DHCP changes. The RPC address is currently fixed, so only one
mini process can use the default portal on a host. The x86-64 musl POC cannot
provide reliable stack backtraces because its release binary has no unwind
tables.
+61
View File
@@ -0,0 +1,61 @@
#!/bin/sh
set -eu
# Cargo invokes this same file as a rustc wrapper during compact MIPS builds.
# Applying immediate-abort here keeps the size policy scoped to easytier-mini;
# normal MIPS builds elsewhere in the workspace retain their panic behavior.
if [ "${EASYTIER_MINI_MIPS_RUSTC_WRAPPER:-}" = "1" ]; then
mini_rustc=$1
shift
for mini_rustc_arg in "$@"; do
case "$mini_rustc_arg" in
mips-unknown-linux-musl|mipsel-unknown-linux-musl)
exec "$mini_rustc" "$@" \
-Zunstable-options \
-Cpanic=immediate-abort
;;
esac
done
exec "$mini_rustc" "$@"
fi
mini_script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
mini_repo_dir=$(CDPATH= cd -- "$mini_script_dir/../.." && pwd)
mini_requested_target=${1:-all}
cd "$mini_repo_dir"
build_mips_target() {
mini_target=$1
mini_toolchain=$2
PATH="$mini_repo_dir/musl_gcc/$mini_toolchain/bin:$PATH" \
EASYTIER_MINI_MIPS_RUSTC_WRAPPER=1 \
RUSTC_BOOTSTRAP=1 \
RUSTC_WRAPPER="$mini_script_dir/build-mips.sh" \
cargo build \
--manifest-path "$mini_repo_dir/Cargo.toml" \
--profile mini \
--target "$mini_target" \
-Z build-std=std \
-Z build-std-features=optimize_for_size \
-p easytier-mini
}
case "$mini_requested_target" in
all)
build_mips_target mips-unknown-linux-musl mips-unknown-linux-muslsf
build_mips_target mipsel-unknown-linux-musl mipsel-unknown-linux-muslsf
;;
mips|mips-unknown-linux-musl)
build_mips_target mips-unknown-linux-musl mips-unknown-linux-muslsf
;;
mipsel|mipsel-unknown-linux-musl)
build_mips_target mipsel-unknown-linux-musl mipsel-unknown-linux-muslsf
;;
-h|--help)
echo "usage: $0 [all|mips|mipsel]"
;;
*)
echo "unsupported MIPS target: $mini_requested_target" >&2
exit 2
;;
esac
+32
View File
@@ -0,0 +1,32 @@
use std::env;
use std::path::PathBuf;
fn main() {
let target = env::var("TARGET").unwrap_or_default();
let profile = env::var("PROFILE").unwrap_or_default();
if !matches!(profile.as_str(), "release" | "mini")
|| !matches!(
target.as_str(),
"x86_64-unknown-linux-musl" | "mips-unknown-linux-musl" | "mipsel-unknown-linux-musl"
)
{
return;
}
let script =
PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("easytier-mini-musl.ld");
println!("cargo:rerun-if-changed={}", script.display());
// The release-derived mini profile already aborts panics. Keep the compact
// binary's linker policy local so full EasyTier musl builds retain their
// normal PIE/unwind settings.
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--build-id=none");
if target == "x86_64-unknown-linux-musl" {
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--pack-dyn-relocs=relr");
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--icf=all");
}
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--no-eh-frame-hdr");
println!(
"cargo:rustc-link-arg-bin=easytier-mini=-Wl,-T,{}",
script.display()
);
}
@@ -0,0 +1,14 @@
SECTIONS
{
.eh_frame :
{
KEEP(*crtbegin.o(.eh_frame))
KEEP(*crtend.o(.eh_frame))
}
/DISCARD/ :
{
*(EXCLUDE_FILE (*crtbegin.o *crtend.o) .eh_frame)
*(.eh_frame_hdr)
}
}
INSERT AFTER .data;
+264
View File
@@ -0,0 +1,264 @@
use std::{ffi::OsString, path::PathBuf, sync::Arc};
use anyhow::Context as _;
use easytier::common::MachineIdOptions;
use easytier::{
common::config::{ConfigFileControl, load_toml_config_from_path},
instance::factory::native_compact_instance_manager_with_runtime,
rpc_service::ReadOnlyApiRpcServer,
web_client::{WebClientHooks, parse_config_server_endpoint, run_web_client},
};
enum Command {
Run(RunOptions),
Exit,
}
#[derive(Debug, Default, PartialEq, Eq)]
struct RunOptions {
config: Option<PathBuf>,
config_server: Option<String>,
machine_id: Option<String>,
hostname: Option<String>,
secure_mode: bool,
}
const USAGE: &str = "usage: easytier-mini [--config <FILE>] [--config-server <URL>] \
[--machine-id <ID>] [--hostname <NAME>] [--secure-mode]";
fn required_value(
args: &mut impl Iterator<Item = OsString>,
option: &str,
) -> anyhow::Result<OsString> {
args.next()
.with_context(|| format!("{option} requires a value"))
}
fn parse_args(mut args: impl Iterator<Item = OsString>) -> anyhow::Result<Command> {
let mut options = RunOptions::default();
while let Some(arg) = args.next() {
if arg == "-h" || arg == "--help" {
println!(
"easytier-mini {}\n\nUsage: {USAGE}",
env!("CARGO_PKG_VERSION")
);
return Ok(Command::Exit);
}
if arg == "-V" || arg == "--version" {
println!("easytier-mini {}", env!("CARGO_PKG_VERSION"));
return Ok(Command::Exit);
}
if arg == "-c" || arg == "--config" {
if options.config.is_some() {
anyhow::bail!("--config may only be specified once");
}
options.config = Some(PathBuf::from(required_value(&mut args, "--config")?));
continue;
}
if arg == "-w" || arg == "--config-server" {
if options.config_server.is_some() {
anyhow::bail!("--config-server may only be specified once");
}
options.config_server = Some(
required_value(&mut args, "--config-server")?
.into_string()
.map_err(|_| anyhow::anyhow!("--config-server must be valid UTF-8"))?,
);
continue;
}
if arg == "--machine-id" {
options.machine_id = Some(
required_value(&mut args, "--machine-id")?
.into_string()
.map_err(|_| anyhow::anyhow!("--machine-id must be valid UTF-8"))?,
);
continue;
}
if arg == "--hostname" {
options.hostname = Some(
required_value(&mut args, "--hostname")?
.into_string()
.map_err(|_| anyhow::anyhow!("--hostname must be valid UTF-8"))?,
);
continue;
}
if arg == "--secure-mode" {
options.secure_mode = true;
continue;
}
anyhow::bail!("unknown argument {arg:?}; {USAGE}");
}
if options.config.is_none() && options.config_server.is_none() {
anyhow::bail!("either --config or --config-server is required; {USAGE}");
}
Ok(Command::Run(options))
}
fn require_tcp_or_udp(scheme: &str, source: &str) -> anyhow::Result<()> {
match scheme {
"tcp" | "udp" => Ok(()),
scheme => anyhow::bail!(
"{source} uses unsupported tunnel scheme {scheme:?}; easytier-mini supports only tcp:// and udp://"
),
}
}
fn validate_config_server(config_server: &str) -> anyhow::Result<()> {
let endpoint = parse_config_server_endpoint(config_server)?;
require_tcp_or_udp(endpoint.connect_url().scheme(), "config server")
}
struct MiniWebClientHooks;
impl WebClientHooks for MiniWebClientHooks {
fn manages_remote_config_instances(&self) -> bool {
true
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let Command::Run(options) = parse_args(std::env::args_os().skip(1))? else {
return Ok(());
};
easytier::common::log::init_console()?;
let local_config = options
.config
.as_ref()
.map(|config_path| {
load_toml_config_from_path(config_path)
.with_context(|| format!("failed to load {}", config_path.display()))
})
.transpose()?;
if let Some(config_server) = options.config_server.as_deref() {
validate_config_server(config_server)?;
}
let instances = Arc::new(native_compact_instance_manager_with_runtime(
tokio::runtime::Handle::current(),
));
let local_instance_id = local_config
.map(|config| instances.run_network_instance(config, ConfigFileControl::STATIC_CONFIG))
.transpose()?;
let _web_client = if let Some(config_server) = options.config_server.as_deref() {
Some(
run_web_client(
config_server,
MachineIdOptions {
explicit_machine_id: options.machine_id,
state_dir: None,
},
options.hostname,
options.secure_mode,
instances.clone(),
Some(Arc::new(MiniWebClientHooks)),
)
.await?,
)
} else {
None
};
let _rpc_server =
ReadOnlyApiRpcServer::new(Some("127.0.0.1:15888".to_owned()), None, instances.clone())?
.serve()
.await?;
eprintln!(
"easytier-mini started: local={local_instance_id:?}, web={}; RPC: 127.0.0.1:15888",
options.config_server.is_some()
);
let stopped_unexpectedly = tokio::select! {
signal = tokio::signal::ctrl_c() => {
signal.context("failed to listen for Ctrl-C")?;
false
},
_ = instances.wait() => true,
};
for instance in instances.instances() {
instance.stop().await;
}
if stopped_unexpectedly {
anyhow::bail!("EasyTier instance stopped unexpectedly");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use easytier::common::config::{ConfigLoader as _, TomlConfigLoader};
#[test]
fn parses_minimal_config_argument() {
let Command::Run(options) =
parse_args([OsString::from("--config"), OsString::from("mini.toml")].into_iter())
.unwrap()
else {
panic!("expected run command");
};
assert_eq!(options.config, Some(PathBuf::from("mini.toml")));
}
#[test]
fn parses_web_client_arguments_without_a_local_config() {
let Command::Run(options) = parse_args(
[
OsString::from("--config-server"),
OsString::from("token"),
OsString::from("--machine-id"),
OsString::from("machine"),
OsString::from("--hostname"),
OsString::from("mini"),
OsString::from("--secure-mode"),
]
.into_iter(),
)
.unwrap() else {
panic!("expected run command");
};
assert_eq!(options.config_server.as_deref(), Some("token"));
assert_eq!(options.machine_id.as_deref(), Some("machine"));
assert_eq!(options.hostname.as_deref(), Some("mini"));
assert!(options.secure_mode);
}
#[test]
fn rejects_unknown_arguments() {
let result = parse_args([OsString::from("extra")].into_iter());
assert!(result.is_err());
}
#[test]
fn accepts_tcp_udp_config_server() {
assert!(validate_config_server("udp://127.0.0.1:22020/token").is_ok());
assert!(validate_config_server("quic://127.0.0.1:22020/token").is_err());
}
#[tokio::test]
async fn compact_factory_accepts_unsupported_config_without_changing_it() {
let config = TomlConfigLoader::new_from_str(
r#"
dhcp = true
listeners = ["quic://127.0.0.1:11010"]
proxy_network = [{ cidr = "10.20.0.0/16" }]
[flags]
encryption_algorithm = "chacha20"
data_compress_algo = "Zstd"
"#,
)
.unwrap();
config.get_id();
let before = config.dump();
let manager =
native_compact_instance_manager_with_runtime(tokio::runtime::Handle::current());
let instance = manager.create(config, ()).unwrap();
assert_eq!(instance.toml_config().unwrap().dump(), before);
}
}
@@ -0,0 +1,9 @@
dist/
target/
.DS_Store
.idea/
package/libs
*.har
Cargo.lock
File diff suppressed because it is too large Load Diff
+69
View File
@@ -0,0 +1,69 @@
[package]
name = "easytier-ohrs"
version = "0.1.0"
edition = "2024"
[lib]
crate-type=["cdylib"]
[dependencies]
anyhow = "1.0"
async-trait = "0.1"
base64 = "0.22"
bytes = "1.5"
easytier-core = { path = "../../easytier-core", default-features = false }
easytier-proto = { path = "../../easytier-proto", default-features = false, features = [
"api",
"core",
"json-rpc",
] }
flate2 = "1.1"
futures = "0.3"
gethostname = "1.1"
easytier = { path = "../../easytier" }
napi-derive-ohos = "1.1"
napi-ohos = { version = "1.1", default-features = false, features = [
"serde-json",
"latin1",
"chrono_date",
"object_indexmap",
"tokio",
"async",
"tokio_rt",
"tokio_macros",
"tokio_io_util",
"deferred_trace",
"napi8",
"node_version_detect",
"web_stream",
] }
once_cell = "1.21.3"
ipnet = "2.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.125"
prost-reflect = { version = "0.14.5", default-features = false, features = ["derive"] }
rusqlite = { version = "0.32", features = ["bundled"] }
tracing-subscriber = "0.3.19"
tracing-core = "0.1.33"
tracing = "0.1.41"
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
url = "2.5"
uuid = { version = "1.5.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"serde",
] }
[build-dependencies]
napi-build-ohos = "1.1"
[profile.dev]
panic = "unwind"
debug = true
[profile.release]
panic = "abort"
lto = true
codegen-units = 1
opt-level = 3
strip = true
+65
View File
@@ -0,0 +1,65 @@
# OpenHarmonyOS 项目构建说明
本项目需要 OpenHarmonyOS SDK 和多个基础库支持才能成功编译。请按照以下步骤准备构建环境。
如存在任何编译问题,请前往[Easytier for OHOS](https://github.com/FrankHan052176/EasyTier)
## 前置要求
### 1. 安装 OpenHarmonyOS SDK
**SDK 下载链接**
[OpenHarmony 每日构建版本](https://ci.openharmony.cn/workbench/cicd/dailybuild/dailylist)
**版本要求**
请选择版本号 **小于 OpenHarmony_5.1.0.58** 的 ohos-sdk-full 版本
下载后请解压到适当位置(如 `/usr/local/ohos-sdk`),并记下安装路径。
### 2. 编译依赖库
在编译本项目前,需要先自行编译以下四个基础库:
- glib
- libffi
- pcre2
- zlib
这些库需要使用 OpenHarmonyOS 的工具链进行交叉编译。
## 环境配置
### 1. 设置环境变量
创建并运行以下脚本设置环境变量(请根据您的实际 SDK 安装路径修改):
```bash
#!/bin/bash
# 请修改为您的实际 SDK 路径
export OHOS_SDK_PATH="/usr/local/ohos-sdk/linux"
export OHOS_TOOLCHAIN_DIR="${OHOS_SDK_PATH}/native/llvm"
export TARGET_ARCH="aarch64-linux-ohos"
export OHOS_SYSROOT="${OHOS_SDK_PATH}/native/sysroot"
export CC="${OHOS_TOOLCHAIN_DIR}/bin/aarch64-unknown-linux-ohos-clang"
export CXX="${OHOS_TOOLCHAIN_DIR}/bin/aarch64-unknown-linux-ohos-clang++"
export AS="${OHOS_TOOLCHAIN_DIR}/bin/llvm-as"
export AR="${OHOS_TOOLCHAIN_DIR}/bin/llvm-ar"
export LD="${OHOS_TOOLCHAIN_DIR}/bin/ld.lld"
export RANLIB="${OHOS_TOOLCHAIN_DIR}/bin/llvm-ranlib"
export STRIP="${OHOS_TOOLCHAIN_DIR}/bin/llvm-strip"
export OBJDUMP="${OHOS_TOOLCHAIN_DIR}/bin/llvm-objdump"
export OBJCOPY="${OHOS_TOOLCHAIN_DIR}/bin/llvm-objcopy"
export NM="${OHOS_TOOLCHAIN_DIR}/bin/llvm-nm"
export CFLAGS="-fPIC -D__MUSL__=1 -march=armv8-a --target=${TARGET_ARCH} -Wno-error --sysroot=${OHOS_SYSROOT} -I${OHOS_SYSROOT}/usr/include/${TARGET_ARCH}"
export CXXFLAGS="${CFLAGS}"
export LDFLAGS="--sysroot=${OHOS_SYSROOT} -L${OHOS_SYSROOT}/usr/lib/${TARGET_ARCH} -fuse-ld=${LD}"
export PKG_CONFIG_PATH="${OHOS_SYSROOT}/usr/lib/pkgconfig:${OHOS_SYSROOT}/usr/local/lib/pkgconfig"
export PKG_CONFIG_LIBDIR="${OHOS_SYSROOT}/usr/lib:${OHOS_SYSROOT}/usr/local/lib"
export PKG_CONFIG_SYSROOT_DIR="${OHOS_SYSROOT}"
export HOST_TRIPLET="${TARGET_ARCH}"
export BUILD_TRIPLET="$(dpkg-architecture -qDEB_BUILD_GNU_TYPE)"
export PATH="${OHOS_TOOLCHAIN_DIR}/bin:${PATH}"
echo "OpenHarmonyOS 环境变量已设置:"
echo "OHOS_SDK_PATH: ${OHOS_SDK_PATH}"
echo "OHOS_TOOLCHAIN_DIR: ${OHOS_TOOLCHAIN_DIR}"
echo "OHOS_SYSROOT: ${OHOS_SYSROOT}"
echo "PKG_CONFIG_PATH: ${PKG_CONFIG_PATH}"
echo "PATH: ${PATH}"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
napi_build_ohos::setup();
}
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# 请修改为您的实际 SDK 路径
export OHOS_TOOLCHAIN_DIR="${OHOS_NDK_HOME}/native/llvm"
export TARGET_ARCH="aarch64-linux-ohos"
export OHOS_SYSROOT="${OHOS_NDK_HOME}/native/sysroot"
export CC="${OHOS_TOOLCHAIN_DIR}/bin/aarch64-unknown-linux-ohos-clang"
export CXX="${OHOS_TOOLCHAIN_DIR}/bin/aarch64-unknown-linux-ohos-clang++"
export AS="${OHOS_TOOLCHAIN_DIR}/bin/llvm-as"
export AR="${OHOS_TOOLCHAIN_DIR}/bin/llvm-ar"
export LD="${OHOS_TOOLCHAIN_DIR}/bin/ld.lld"
export RANLIB="${OHOS_TOOLCHAIN_DIR}/bin/llvm-ranlib"
export STRIP="${OHOS_TOOLCHAIN_DIR}/bin/llvm-strip"
export OBJDUMP="${OHOS_TOOLCHAIN_DIR}/bin/llvm-objdump"
export OBJCOPY="${OHOS_TOOLCHAIN_DIR}/bin/llvm-objcopy"
export NM="${OHOS_TOOLCHAIN_DIR}/bin/llvm-nm"
export CFLAGS="-fPIC -D__MUSL__=1 -march=armv8-a --target=${TARGET_ARCH} -Wno-error --sysroot=${OHOS_SYSROOT} -I${OHOS_SYSROOT}/usr/include/${TARGET_ARCH}"
export CXXFLAGS="${CFLAGS}"
export LDFLAGS="--sysroot=${OHOS_SYSROOT} -L${OHOS_SYSROOT}/usr/lib/${TARGET_ARCH} -fuse-ld=${LD}"
export PKG_CONFIG_PATH="${OHOS_SYSROOT}/usr/lib/pkgconfig:${OHOS_SYSROOT}/usr/local/lib/pkgconfig"
export PKG_CONFIG_LIBDIR="${OHOS_SYSROOT}/usr/lib:${OHOS_SYSROOT}/usr/local/lib"
export PKG_CONFIG_SYSROOT_DIR="${OHOS_SYSROOT}"
export HOST_TRIPLET="${TARGET_ARCH}"
export BUILD_TRIPLET="$(dpkg-architecture -qDEB_BUILD_GNU_TYPE)"
export PATH="${OHOS_TOOLCHAIN_DIR}/bin:${PATH}"
echo "OpenHarmonyOS 环境变量已设置:"
echo "OHOS_SDK_PATH: ${OHOS_NDK_HOME}"
echo "OHOS_TOOLCHAIN_DIR: ${OHOS_TOOLCHAIN_DIR}"
echo "OHOS_SYSROOT: ${OHOS_SYSROOT}"
echo "PKG_CONFIG_PATH: ${PKG_CONFIG_PATH}"
echo "PATH: ${PATH}"
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
# 0.0.1
- init package
+165
View File
@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+162
View File
@@ -0,0 +1,162 @@
# `easytier-ohrs`
## Install
use `ohpm` to install package.
```shell
ohpm install easytier-ohrs
```
## API
### collectNetworkInfos
```ts
collectNetworkInfos(): Array<KeyValuePair>
````
---
### collectRunningNetwork
```ts
collectRunningNetwork(): Array<string>
```
获取当前正在运行的网络实例名称列表。
---
### convertTomlToNetworkConfig
```ts
convertTomlToNetworkConfig(cfgStr: string): string
```
将 TOML 配置转换为 NetworkConfig。
* `cfgStr`TOML 配置内容
---
### defaultNetworkConfig
```ts
defaultNetworkConfig(): string
```
获取默认的网络配置(JSON 字符串),用于转换为object进行赋值。
---
### easytierVersion
```ts
easytierVersion(): string
```
获取 EasyTier 当前版本号。
---
### hilogGlobalOptions
```ts
hilogGlobalOptions(domain: number, tag: string): void
```
设置全局日志选项。
* `domain`:日志域 ID
* `tag`:日志标签
---
### initPanicHook
```ts
initPanicHook(): void
```
初始化 panic 钩子,用于将Rust侧的panic输出到hilog中,请先通过 hilogGlobalOptions 设置hilog的参数。
---
### initTracingSubscriber
```ts
initTracingSubscriber(): void
```
初始化 tracing 日志订阅器,用于将Rust侧日志同步输出到hilog中,请先通过 hilogGlobalOptions 设置hilog的参数。
---
### isRunningNetwork
```ts
isRunningNetwork(instId: string): boolean
```
判断指定网络实例是否正在运行。
* `instId`:网络实例 ID
---
### parseNetworkConfig
```ts
parseNetworkConfig(cfgJson: string): boolean
```
校验网络配置(JSON 格式)是否合法。
* `cfgJson`:网络配置内容
---
### runNetworkInstance
```ts
runNetworkInstance(cfgJson: string): boolean
```
启动网络实例。
* `cfgJson`:网络配置(JSON
---
### setTunFd
```ts
setTunFd(instId: string, fd: number): boolean
```
为指定网络实例设置 TUN 设备文件描述符。
* `instId`:网络实例 ID
* `fd`TUN 设备文件描述符
---
### stopNetworkInstance
```ts
stopNetworkInstance(instNames: Array<string>): void
```
停止指定的网络实例。
* `instNames`:网络实例名称列表
## Usage
```ts
// todo
```
+4
View File
@@ -0,0 +1,4 @@
import * as api from "libeasytier_ohrs.so";
export * from 'libeasytier_ohrs.so';
export default api;

Some files were not shown because too many files have changed in this diff Show More