* fix(web): fence managed config runtime reconciliation Keep runtime reconciliation tied to the currently authorized session so stale connections cannot mutate a replacement session runtime. Accumulate only contiguous dirty IDs and load their latest SQLite state. Require the applied revision to match the earliest Patch base and the persisted revision to match the latest target. Otherwise, reconcile the full desired state. Use separate runtime-state and config-cache epochs. Managed updates can reuse observed configs; direct mutations invalidate them. Update sync documentation to match. * fix(web): interrupt validation retry on state changes Track meaningful validation state changes separately from periodic dirty signals. Applied revision changes wake a failed validation immediately, while heartbeat-driven revalidation retains the retry backoff. Treat Notify as a wake-up hint and recheck the state-change epoch after every wake so stored permits and periodic heartbeats cannot cause retry storms. * fix(web): retry unconfirmed connected webhooks Retry node-connected webhook delivery on retryable errors with a short 100ms/500ms backoff and give up immediately on non-retryable errors. Re-check that the session still owns the connection before every attempt and before recording the delivery, so a replaced session can no longer record a stale connected binding. * fix(web): fence disconnects by session ownership Return whether session removal actually removed the current route owner, and emit disconnected only for that owner. Replaced sessions can no longer invalidate a newer connected route. * fix(web): hot-patch managed hostnames Include hostname changes in the hot-patch path instead of falling back to a full restart. When a full overwrite run is required and the desired config has no hostname, inherit the current runtime hostname so an unmanaged value survives until it is explicitly cleared. Read back the runtime config after an overwrite run and verify it converged instead of assuming the desired state was applied. * fix(web): retry transient runtime reconciliation failures Keep the per-session managed runtime reconciliation worker alive when a single database round fails. Retry from the next heartbeat so persisted managed revisions can still converge after restart-time contention. Reserve terminal worker shutdown for destroyed session or storage state, and cover recovery after a transient revision read failure. * fix(web): accept omitted hostname after runtime apply Release 2.6.4 omits hostname from config readback when it matches the device hostname. Trust a successful hostname mutation only when the returned field is absent, while continuing to verify every other field and rejecting explicit mismatches. * fix(web): ignore unmanaged runtime device names Windows release 2.6.4 generates a random interface name when the managed config leaves dev_name empty. Exclude that runtime-owned value from reconciliation unless the desired config explicitly sets a non-empty device name, preventing endless overwrite restarts. * feat(web): report failed network instances to console Expose stopped Core instances with startup errors in heartbeats. Merge Core failures with direct managed-run RPC failures in easytier-web. Send failed instance IDs during token validation without error text. Prune local run failures when managed configs are deleted. * fix(web): distinguish unknown runtime application state Track whether the current session has observed its applied revision separately from the optional revision value. Report this fact through validate-token so Console can preserve application state across receiver restarts while recognizing deliberate pending mutations. * feat(web): configure heartbeat timing from server Heartbeat responses now provide the interval and RPC timeout. Legacy servers use local defaults and remote values are clamped. Web configuration and session receive timeout follow the policy. * fix(web): reject inactive control sessions Route control RPCs by machine id only to sessions whose RPC manager is still running, so a session that has been stopped or replaced can no longer receive control traffic addressed to the device. * fix(core): filter network info before collection When a collect-network-info request names specific instances, collect those instances only instead of collecting every instance and filtering the result afterwards, so unrequested instances no longer run per-collection work on every request. * feat(web): enable focused runtime diagnostics Enable easytier-web info logs by default while preserving explicit log configuration. Record startup settings, session lifecycle, failed instance changes, webhook queue and request latency, and managed runtime operation timings for production diagnosis. * fix(web): preserve managed revision across reconnects Keep one runtime identifier for each Core WebClient lifetime. Reuse its managed runtime state after transport reconnects. Retain applied revisions and reconcile hints while disconnected. Preserve runtime epochs so stale work cannot mark a revision applied. Reject stale sessions from reclaiming routes after reconnect. Core or Web restarts and legacy clients still use unknown state. Immediately revalidate a restored revision after authentication. Document local management RPC drift as an accepted trade-off. This lets Console converge without waiting for periodic validation. * fix(web): satisfy clippy across managed config sync tests Scope managed runtime guards to blocks in runtime revision tests so no std MutexGuard is held across await points, return the applied revision directly instead of through a let binding, and pass WebhookValidationInput to request_heartbeat_validation instead of expanding it into eight separate arguments. * fix(core): stop reporting failed instances as running in heartbeats A stopped instance with a startup error appeared in both running_network_instances and failed_network_instances, so the server treated it as running and never re-ran its managed config. Exclude failed instance ids when building the running list so the reconciler restarts them. * fix(core): close missed-wakeup race in instance state changes wait_for_change created the Notified future before reading the generation but only registered it when awaited. A change landing in between fired notify_waiters with no registered waiter and delayed the heartbeat by a full interval. Enable the future before reading the generation so every change wakes a waiting heartbeat. * fix(web): address review findings Fence webhook validation and connection transitions against stale state, redact credentials from default-level logs, and stabilize runtime reconciliation: - Record connected bindings only while the session still owns the machine route, and skip disconnect compensation once a replacement owns the route so a stale disconnect cannot revoke it. - Discard webhook validation results when the change epoch moved during the HTTP round, so a stale rejection cannot invalidate the current session. - Drop user_token fields from info and warn logs that became visible with info-level defaults. - Restore a hostname omitted by the 2.6.4 readback into the cached runtime config after a successful mutation, so later rounds stop re-sending the same hostname patch. - Reconcile running web configs when no revision is tracked so legacy unrevisioned updates converge, and wake sessions for unrevisioned full updates instead of waiting for the next heartbeat. * chore(go): regenerate web proto bindings for heartbeat fields Add failed_network_instances, support_heartbeat_policy, and the heartbeat policy response fields to the checked-in Go bindings. Other proto packages are left as-is because their drift predates this change. * fix(web): redact user tokens from positional log arguments Three runtime reconciliation info logs and the user lookup error contexts printed user_token through format arguments, which the earlier field-syntax redaction missed. The reconcile log now fires every round for unrevisioned machines, so remove the token from these messages as well. * fix(web): fence stale validation and runtime reconcile rounds Check webhook validation epochs while holding the session write lock, so stale success and rejection responses cannot change session state. Advance the runtime epoch for unrevisioned full config updates, and exclude failed instances from heartbeat and RPC reconciliation lists so stopped instances are restarted instead of repeatedly hot-patched. Release test read guards before awaiting validation apply calls. Set up the no-pending condition before asserting that an applied revision is a no-op, and verify that its runtime epoch remains unchanged. Validation: all 137 client_manager tests passed. * test(credentials): cover P2P with active VPN portal Model an admin and temporary credential peer connected as a foreign network through a public server with data relay disabled. Verify their direct connection can be replaced after a WireGuard portal client comes online. * test(credentials): stabilize two-admins failover assertions The two-admins non-reusable credential test could fail on slow convergence: after dropping the winning peer it relied on a single route sample passing a bare AND condition, then re-asserted the same expectations through one-shot checks seconds later. A transient route flap in that window (for example a briefly resurrected winner route from stale conn info) turned a passing convergence into a hard assert failure. This matches the 48.9s CI flake of credential_non_reusable_across_two_admins_allows_only_one_peer observed on 2026-08-12. Changes: - wait for bidirectional admin connectivity (AND) with a 20s budget before issuing the credential, instead of a one-directional OR - replace the failover wait_for_condition with wait_stable_failover_visibility_on_admins, which requires three consecutive samples of loser-present and winner-absent on both admins within the same 60s budget and logs every sample - enrich the stable-single-winner timeout message with per-admin visibility flags and elapsed time for triage All existing contracts are preserved; only observation windows and diagnostics change. Validated in the rust container: three passes at normal speed (54.1s / 53.8s / 53.1s) plus one slow-convergence round (172.7s) that would have raced the old one-shot sampling; it now passes with failover samples logged. cargo fmt and clippy -D warnings clean.
EasyTier
✨ A simple, secure, decentralized virtual private network solution powered by Rust and Tokio
📚 Full Documentation | 🖥️ Web Console | 📝 Download Releases | 🧩 Third Party Tools | ❤️ Sponsor
Features
Core Features
- 🔒 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
Advanced Capabilities
- 🔌 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
Network Optimization
- 📊 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
📥 Installation
Choose the installation method that best suits your needs:
Linux (Recommended):
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
Homebrew (MacOS/Linux):
brew tap brewforge/chinese
brew install --cask easytier-gui
Windows (Recommended, run with administrator privileges):
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
Install via cargo (Latest development version):
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
Install pre-built binary (Recommended, All platforms supported)
Additional steps:
One-Click Register Service (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):
- Run on Node A:
# Run with administrator privileges
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
- Run on Node B:
# 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:
| 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:
# 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:
# 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 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:
- Start First Node (Node A):
# 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
- Connect Second Node (Node B):
# Connect to the first node using its public IP
sudo easytier-core -i 10.144.144.2 -p udp://FIRST_NODE_PUBLIC_IP:11010
- Verify Connection:
# 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:
# 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:
flowchart LR
subgraph Node A Public IP 22.1.1.1
nodea[EasyTier<br/>10.144.144.1]
end
subgraph Node B
nodeb[EasyTier<br/>10.144.144.2]
end
id1[[10.1.1.0/24]]
nodea <--> nodeb <-.-> id1
To share a subnet, add the -n parameter when starting EasyTier:
# 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. You can verify the subnet proxy setup:
- Check if the routing information has been synchronized (the proxy_cidrs column shows the proxied subnets):
# View routing information
easytier-cli route
- Test if you can access nodes in the proxied subnet:
# Test connectivity to proxied subnet
ping 10.1.1.2
WireGuard Integration
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:
flowchart LR
ios[[iPhone<br/>WireGuard Installed]]
subgraph Node A Public IP 22.1.1.1
nodea[EasyTier<br/>10.144.144.1]
end
subgraph Node B
nodeb[EasyTier<br/>10.144.144.2]
end
id1[[10.1.1.0/24]]
ios <-.-> nodea <--> nodeb <-.-> id1
- Start EasyTier with WireGuard portal enabled:
# Register one WireGuard client as virtual peer 10.144.144.3
sudo easytier-core -i 10.144.144.1 \
--network-secret portal-secret \
--vpn-portal wg://0.0.0.0:11013 \
--vpn-portal-private-key "$(wg genkey)" \
--vpn-portal-client phone=10.144.144.3
- Get WireGuard client configuration:
# Get WireGuard client configuration
easytier-cli vpn-portal
- In the output configuration, replace a wildcard
Peer.Endpointwith the public IP/domain of your EasyTier node, then import it.Interface.Addressis local to that WireGuard client and may be changed to any IPv4 address; EasyTier translates it to the registered virtual-peer address.
Self-Hosted Public Shared Node
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.
To run a public shared node:
# No need to specify IPv4 address for public shared nodes
sudo easytier-core --network-name mysharednode --network-secret mysharednode
Related Projects
- ZeroTier: A global virtual network for connecting devices.
- TailScale: A VPN solution aimed at simplifying network configuration.
Contact Us
- 💬 Telegram Group
- 👥 [QQ Group]
License
EasyTier is released under the LGPL-3.0.
Responsible Use
Use EasyTier only for lawful purposes and in compliance with applicable laws and regulations. You are responsible for ensuring that you are authorized to connect to and administer the networks and devices involved.
Sponsor
CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
Special thanks to Langlang Cloud and RainCloud for sponsoring our public servers.
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.




