etcat is a netcat-like, rootless peer-to-peer tool built on
EasyTier. One process prints a
connection token; another process uses that token to pipe data, reach selected
local TCP services, open an SSH session, or use the server as a process-scoped
SOCKS exit.
It is deliberately not a VPN. Both peers run EasyTier with no_tun and a
userspace TCP/IP stack. etcat does not create a TUN device, alter routes or
DNS, bind privileged ports, or require root/administrator privileges.
This project is experimental. Tokens and the CLI may change before 1.0.
Linux and macOS:
curl --proto '=https' --tlsv1.2 -LsSf https://raw.githubusercontent.com/EasyTier/etcat/main/install.sh | shThe Unix installer selects the native platform archive and installs etcat to
/usr/local/bin, using sudo only when the directory is not writable. Linux
downloads are statically linked musl binaries.
Windows PowerShell:
irm https://raw.githubusercontent.com/EasyTier/etcat/main/install.ps1 | iexThe Windows installer writes to %LOCALAPPDATA%\Programs\etcat and adds that
directory to the user PATH. Both installers verify the release archive against
the published SHA256SUMS. Set ETCAT_VERSION to install a specific release,
or ETCAT_INSTALL_DIR to choose another destination:
curl --proto '=https' --tlsv1.2 -LsSf https://raw.githubusercontent.com/EasyTier/etcat/main/install.sh | ETCAT_VERSION=v0.1.0 ETCAT_INSTALL_DIR="$HOME/.local/bin" sh$env:ETCAT_VERSION = "v0.1.0"
$env:ETCAT_INSTALL_DIR = "C:\Tools\etcat"
irm https://raw.githubusercontent.com/EasyTier/etcat/main/install.ps1 | iexRelease archives are also available from the GitHub releases page for Linux x86-64/ARM64, macOS Intel/Apple Silicon, and Windows x64/ARM64.
Rust 1.95 or newer is required. EasyTier is pinned to commit
164e2db6aecd13117d840821d5b889b51cb7c463.
cargo build --release
./target/release/etcat --help
./target/release/etcat --readmeLinux, macOS, and Windows are supported, including the built-in SSH and SFTP server.
Run etcat COMMAND --help for every option. The main workflows are:
| Goal | Server | Client |
|---|---|---|
| One-shot pipe | etcat --key=new |
etcat TOKEN |
| Serve TCP ports | etcat serve 22,8080,9000-9010 |
etcat TOKEN 8080 |
| Forward listeners | etcat serve 8080 |
etcat forward TOKEN 18080:8080 |
| Temporary SSH | etcat serve no-auth-ssh |
etcat ssh TOKEN |
| Existing SSH daemon | etcat serve 22 |
etcat ssh TOKEN |
| Read-only files | etcat serve files --files=DIR:ro |
etcat ls TOKEN:. |
| Receive files | etcat recv DIR |
etcat cp FILE TOKEN: |
| SOCKS for a command | etcat serve exit-node |
etcat socks TOKEN curl https://example.com |
TOKEN means the etc2... address printed by the server. A DNS name with an
etcat=etc2... TXT record can be used in the same places.
Start an ephemeral server. The token is printed to stderr and becomes useless when this process exits:
server$ etcat --key=new
# Server listening with address: etc2...Send data from the client:
client$ printf 'hello\n' | etcat etc2...The server writes hello to stdout and exits.
While a one-shot raw stream is active, the server processes Ctrl-C after the peer closes that stream. Close the client side first when interrupting a hung raw transfer.
The server can allow individual ports, ranges, or every local TCP port. The
root --serve form and the serve command are equivalent:
server$ etcat --serve=22,8080,9000-9010
server$ etcat serve 22,8080 9000-9010
server$ etcat serve all
client$ etcat etc2... 8080All destinations are connected on the server as 127.0.0.1:<port>. EasyTier
ACLs expose only etcat's random internal gateway port; the signed application
handshake enforces the published service policy before opening a local socket.
forward keeps one or more local listeners open and sends each connection to
the selected server destination:
server$ etcat serve 8080,9000-9010
client$ etcat forward etc2... 8080
client$ etcat forward etc2... 18080:8080 19000:9000
client$ etcat forward --bind=0.0.0.0 etc2... 0:8080Mappings have these forms:
| Mapping | Meaning |
|---|---|
8080 |
Listen on 8080 and connect to server port 8080 |
18080:8080 |
Listen on 18080 and connect to server port 8080 |
0:8080 |
Pick a free local port and print it |
13306:192.0.2.10:3306 |
Through exit-node, connect to that IPv4 address |
13306:[2001:db8::10]:3306 |
Same for IPv6 |
The default bind address is 127.0.0.1. Exit-node mappings require the server
to run etcat serve exit-node.
Start an ephemeral SSH server that accepts any user without an SSH password or key:
server$ etcat serve no-auth-ssh
client$ etcat ssh etc2...
client$ etcat ssh etc2... uname -aInteractive sessions use a real PTY. The system OpenSSH client is launched with a ProxyCommand. Its host-key database is disabled because the etcat gateway handshake authenticates the server public key carried in the token.
ssh defaults to logical server port 22. -p accepts another server port, or
an IP/IP:port when the server has exit-node enabled:
client$ etcat ssh -p 2222 etc2...
client$ etcat ssh -p 192.0.2.10 etc2...
client$ etcat ssh -p 192.0.2.10:2222 etc2...To retain normal SSH authentication, expose the system SSH daemon instead:
server$ etcat serve 22
client$ etcat ssh user@etc2...The ssh command uses the system OpenSSH client. It preserves the child's exit
status, supports interactive PTYs and resize events, and passes TERM, LANG,
and LC_*. The built-in server uses one persistent SSH host key, while the
etcat gateway handshake authenticates the endpoint identified by the token.
The files service is rooted at one directory. Paths visible to a client
cannot escape that root, including through .., absolute paths, or symbolic
links.
server$ etcat serve files --files=/srv/releases:ro
client$ etcat ls etc2...
client$ etcat ls -l etc2...:subdirectory
client$ etcat cp etc2...:artifact.tar.zst .Available modes are:
| Mode | Access |
|---|---|
ro |
List, inspect, and download |
rw |
Full access inside the configured root |
wo |
Flat write-only drop box; uploaded names cannot overwrite files |
wo+ |
Recursive write-only drop box; directory uploads are allowed |
--files=DIR defaults to ro. etcat serve files defaults to the current
directory in ro mode.
For receiving uploads, recv is the shorter and safer spelling:
server$ etcat recv ./incoming
client$ etcat cp report.pdf etc2...:
server$ etcat recv --accept-dirs ./incoming
client$ etcat cp -r result-directory etc2...:cp delegates transport behavior to the system OpenSSH scp client and
supports -r, -p, and -P PORT|IP|IP:PORT. It accepts the usual local path
and TOKEN:PATH operands and returns the exact scp exit status.
When no-auth-ssh is enabled without an explicit --files, SFTP follows the
current user's normal filesystem permissions: relative paths begin at that
user's home directory, and absolute paths retain their operating-system
meaning. Supplying --files always restores the rooted restriction, even when
shell access is also enabled.
On Windows, pass an absolute drive path such as C:\\incoming to --files.
Drive-relative forms such as C: and C:incoming are not accepted.
Run a command with all_proxy pointing at a temporary local SOCKS5 listener:
server$ etcat serve 8080
client$ etcat socks curl http://etc2...:8080/
client$ etcat socks etc2... curl http://server.etcat:8080/The first form puts a compact registry-relative bearer token directly in the
hostname. The fixed-token server.etcat form also works with longer sealed or
--full-address tokens; server.tailcat is accepted as a compatibility alias.
To reach arbitrary TCP destinations visible from the server, explicitly enable
exit-node mode:
server$ etcat serve exit-node
client$ etcat socks etc2... curl https://example.com/Without a child command, socks keeps the local proxy running. Choose the
listener with --listen=PORT, --listen=IP, or --listen=IP:PORT:
client$ etcat socks --listen=127.0.0.1:1080 etc2...This affects only the child command or the explicitly printed local SOCKS
listener. It never installs a default route and never becomes a system VPN.
Only SOCKS5 TCP CONNECT is supported.
etcat ping etc2...
etcat ping --until-direct --timeout=20s etc2...
etcat parse etc2...
etcat resolve etc2...
etcat relaysping performs an authenticated application handshake and reports whether
EasyTier currently uses a direct peer path or the shared relay. A DNS name is
accepted anywhere a token is accepted when its TXT records contain:
etcat=etc2...
--full-address embeds the selected relay metadata. Otherwise the token stores
a permanent built-in relay number or the custom relay ID, and clients resolve
it through their local relays.toml. etc2 tokens use a fixed binary
layout and lowercase Base32 hostname labels. A normal bearer token using the
built-in relay is 65 characters. It carries a 128-bit credential seed and a
128-bit fingerprint of the server signing key; the gateway returns the full
public key and proves possession during its signed handshake.
The safe server default is a new in-memory identity and credential. Create a persistent server identity only when a stable token is required:
server$ etcat genkey --key=default
server$ etcat genkey --key=home --fixed-relay
server$ etcat genkey --key=office --relay=community-1
server$ etcat genkey --list
server$ etcat genkey --delete --key=defaultOnce the default server key exists, it is used automatically. --key=new
always forces an ephemeral server. --fixed-relay measures latency once and
stores the winner; an explicit --relay is also fixed. Without either option,
a saved server identity selects the nearest relay at every start.
Bearer tokens authorize one logical client identity by default. For a token that only named clients can open, generate a client key and give its public key to the server:
client$ etcat genkey --client --key=client-default
# prints the public key and saves client-default
server$ etcat --serve=22 --allow='<client public key>'Multiple --allow values are supported. Each recipient gets a distinct
EasyTier credential sealed with HPKE. Clients automatically use
client-default, or a named identity selected with --key. Key files and
ETCAT_ADDR_FILE token files are written with mode 0600 on Unix.
--allow=none starts a server that issues no usable client credential.
Key generation and deletion deliberately require an explicit --key; there is
no implicit file creation. etcat printpub prints client-default when it
exists and otherwise prints a temporary public key. Use
etcat --key=new printpub to force a temporary key.
--ttl=30m adds an absolute credential expiry. Without --ttl, an ephemeral
token is process-bound because its credential exists only in the running
server. A saved server key intentionally produces a reusable token across
server restarts.
The built-in list lives in relays.toml. It currently contains a
best-effort community relay that has passed an end-to-end etcat transfer test,
including through its browser-compatible WSS endpoint. It has no uptime
guarantee or independently published EasyTier public key. TLS authenticates the
WSS hostname, but EasyTier will still warn that the relay identity is not
pinned. Override the registry with --relay-file or ETCAT_RELAY_FILE:
version = 1
[[relay]]
id = "my-relay"
region = "Office"
probe = "relay.example.com:11010"
priority = 10
endpoints = [
"tcp://relay.example.com:11010",
"udp://relay.example.com:11010",
]
public_key = "base64-encoded-32-byte-EasyTier-public-key"Relay candidates are probed concurrently with three TCP samples and tried in
median-RTT order. Startup fails only after every reachable candidate fails its
EasyTier handshake. etcat prints the server token only after a connection to the
selected relay is established. When public_key is present, EasyTier must
authenticate that exact relay key. When it is absent, the relay is encrypted but
unpinned and etcat prints a warning.
- Treat a bearer
etc2...token like a password. Avoid shell history, public logs, issue trackers, and process arguments on shared machines. Prefer--allowwhen a token will be published in DNS or retained long term. - EasyTier secure mode encrypts the overlay and admits clients with managed credentials. Credential clients cannot advertise proxy routes, and client ACLs reject gateway traffic routed to another credential peer. An independent Ed25519-signed gateway handshake pins the exact server identity carried in the token and authorizes each logical destination. Requests also carry an HMAC whose key is derived separately from the same token credential, preventing local processes from bypassing the overlay through etcat's loopback gateway.
- Shared relays provide rendezvous and fallback transport. A pinned relay key authenticates the relay; an unpinned registry entry does not.
--serve=all,--serve=exit-node, and--serve=no-auth-sshgrant broad capabilities to anyone who can open the token. Use them intentionally.- No-availability guarantee is implied for community shared relays. A private, pinned EasyTier relay is recommended for durable use.
The package also exposes the transport as a Rust library. Client keeps one
EasyTier session alive for repeated dials; Server accepts authenticated
streams and reports the requested logical destination:
use etcat::{Client, ClientOptions, ConnectionToken, Destination, Server,
ServerOptions};
# async fn example(encoded: &str) -> anyhow::Result<()> {
let token = ConnectionToken::decode(encoded)?;
let client = Client::connect(token, &ClientOptions::default()).await?;
let stream = client.dial_port(8080).await?;
let server = Server::bind(&ServerOptions::default()).await?;
println!("{}", server.token().encode()?);
let incoming = server.accept().await?;
if let Destination::ServerPort { port } = incoming.destination {
println!("client requested port {port}");
}
# drop(stream);
# Ok(())
# }The public file-service types are FileService, FileMode, and FileSession.
Use them when embedding the rooted SFTP policy in another SSH server.
The native CLI covers Tailcat's pipe, serve, ping, socks, ssh, cp,
ls, forward, recv, parse, resolve, genkey, printpub, version, and
readme workflows. etcat has its own etc2 token and EasyTier relay protocol;
it does not read Tailcat tokens.
The web/ directory contains the browser app: a Vue 3 + Vite single-page UI
that sends and receives files or text through the EasyTier WebAssembly
runtime. It implements Tailcat's browser send/receive workflow: create a
receiver token, send text or a file to logical port 1, half-close the stream,
and wait for the receiver to confirm EOF. Browser-created bearer tokens work
with the native CLI:
browser$ Start receiving, then copy etc2...
client$ etcat etc2... < archive.tar.zstBrowser networking is relay-only. Its relay must publish a ws:// or
wss:// EasyTier endpoint, and an HTTPS page can use only wss://. The
checked-in community relay includes a publicly trusted WSS endpoint and is the
app's zero-config default. To send from the browser to a native receiver,
print a self-contained token and paste it into the app (or open the app's
share link, which fills the token in automatically):
server$ etcat --full-address --key=new
browser$ Paste the printed token under SendBrowser listeners use ephemeral keys unless the user explicitly enables
browser-local persistence under Advanced relay settings. Sealed
--allow/HPKE client tokens are not supported by the browser API yet; bearer
tokens are supported.
Node 22+ and pnpm are required. The vendored EasyTier runtime under
web/vendor/runtime/ and web/public/easytier_core.wasm is committed, so a
plain build works without an EasyTier checkout:
cd web
pnpm install
pnpm build
python3 -m http.server 8000 --directory distTo refresh the vendored runtime after upstream EasyTier changes, build the
browser-profile Wasm in an EasyTier checkout
(easytier-contrib/easytier-cloudflare-worker, pnpm build:browser-wasm) and
re-sync:
pnpm sync:runtime -- --from /path/to/EasyTierThe sync records the source revision and per-file SHA-256 in
web/vendor/runtime/MANIFEST.json.
The page keeps the query-string hooks used by the EasyTier browser smoke
harness: ?mode=listen starts a listener on load, ?mode=send&token=etc2... &bytes=N sends N random bytes, ?sink=hash hashes received payloads instead
of showing cards, and ?relay= / ?relayKey= override the relay settings.
Progress is reported on window.etcatTest
(ready/listenToken/recvBytes/recvHash/recvDone/sentBytes/
sentHash/sentDone/errors).
The original etcat source is licensed under Apache License 2.0; see
LICENSE. EasyTier is an LGPL-3.0 dependency. Distributors of
compiled binaries must also satisfy EasyTier's license and the licenses of all
other linked dependencies.