shadowgate

module
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT

README

shadowgate

CI Go Reference Go Report Card

shadowgate is a lightweight, point-to-multipoint encrypted IP tunnel for Linux. It creates a TUN interface on both ends and carries IPv4 frames between them, running both a TCP and a UDP transport at the same time and adapting between them.

  • Dual transport, always on — the server listens on TCP and UDP at once, and the client opens both. There is no transport flag to set.
  • Adaptive client — the client probes each path with keepalives, sends over the healthy path with the lowest latency, and switches automatically as conditions change — falling back to whichever path works if one is blocked or fails.
  • Authenticated encryption — TCP uses a ChaCha20-Poly1305 record layer (Shadowsocks-AEAD style: a per-direction random salt, HKDF session keys, and a counter nonce per record). UDP uses per-packet XChaCha20-Poly1305. Both derive keys from the same password via PBKDF2, and both authenticate every record or packet so tampering and wrong passwords are rejected.
  • Obfuscated UDP — each UDP datagram is random-nonce || AEAD-ciphertext with no handshake, no plaintext header, and randomized length padding, so a passive observer sees only high-entropy datagrams of varying size.
  • Optional compression — TCP frames can be Snappy-compressed with --compress (off by default; compression is usually wasted on already- encrypted traffic and can leak length information).
  • Small — a single static binary, no configuration files, no daemon dependencies beyond the ip command.
  • Multi-client — a shared router learns client addresses from the traffic it sees and routes frames between connected clients (even across transports) as well as to its own TUN interface.

Note shadowgate is a personal project and has not received a formal security audit. See Security before relying on it.

How it works

        client                                   server
 ┌──────────────────┐                    ┌──────────────────┐
 │       tun0       │                    │       tun0       │
 │   172.18.0.2     │                    │    172.18.0.1    │
 └────────┬─────────┘                    └─────────┬────────┘
          │            TCP (AEAD records)          │
          ├───────────────────────────────────────┤
          │      UDP (obfuscated datagrams)        │
          │  ── adaptively picks the faster path ──│

The server owns one address on the tunnel subnet and binds the same port on both TCP and UDP; a shared router moves frames between the TUN device and whichever transport a given client uses. Each client opens both transports to the server, measures their round-trip time with keepalives, and sends tunnel traffic over the healthiest, lowest-latency path — re-evaluating continuously so a path that becomes slow, blocked, or dead is abandoned in favour of the other.

Installation

From source

Requires Go 1.25 or newer.

git clone https://github.com/ziyan/shadowgate.git
cd shadowgate
make build      # produces ./shadowgate
With Docker
docker build -t shadowgate .

The container needs NET_ADMIN and access to /dev/net/tun to create the interface (see below).

Usage

shadowgate must run as root (or with CAP_NET_ADMIN) because it opens /dev/net/tun and shells out to ip to configure the interface.

Server
sudo shadowgate server \
  --ip 172.18.0.1/24 \
  --listen :3389 \
  --password "correct horse battery staple"
Client
sudo shadowgate client \
  --ip 172.18.0.2/24 \
  --connect server.example.com:3389 \
  --password "correct horse battery staple"

The server binds both TCP and UDP on the given port, and the client opens both to --connect — no transport selection is needed. Once both ends are up, the two hosts can reach each other over the tunnel subnet (e.g. ping 172.18.0.1 from the client). Raise --loglevel NOTICE on the client to see it announce which transport it is actively using as it adapts.

Options

Global:

Flag Default Description
--loglevel INFO DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL

server / client:

Flag Default (server / client) Description
--ip 172.18.0.1/24 / 172.18.0.2/24 Tunnel address in CIDR notation
--listen / --connect :3389 / 127.0.0.1:3389 Address (TCP+UDP) to listen on / connect to
--password (empty) Shared secret used to derive the session keys
--compress false TCP: Snappy-compress the stream
--padding 256 UDP: max random padding bytes per datagram
--mtu 0 (kernel default) TUN interface MTU; lower it to avoid UDP fragmentation
--gateway (server only; unset) Tunnel address of a client to route otherwise-unroutable egress through
--ifname (kernel-assigned) TUN interface name to create
--persist false Keep the TUN interface after exit
--timeout 2s Dial / network operation timeout
The obfuscated UDP datagram

Each UDP datagram is 24-byte random nonce || XChaCha20-Poly1305 ciphertext; the encrypted payload includes a sequence number (for replay protection), the IPv4 frame, and 0..--padding random bytes so datagram sizes vary. There is no handshake and no plaintext field, so an on-path observer cannot fingerprint the protocol by content or by a fixed packet size. This defends against passive DPI; it does not attempt to defeat active probing (which would require mimicking a real protocol such as HTTPS), and a censor doing entropy analysis may still flag uniformly-random UDP.

Because each datagram adds ~54 bytes of AEAD overhead plus up to --padding bytes, a full-size (1500-byte) tunnel packet can exceed the path MTU and fragment. Set --mtu below path-MTU − 54 − padding (for example --mtu 1150 with the default padding on a 1500-byte path) so datagrams fit in one packet.

Routing egress through a client

The server forwards a frame read from its own tun to a connected client in three steps, stopping at the first match: a client whose tunnel address is the destination; the client that the server host's own routing table names as the next hop toward the destination (so a route such as ip route add default via 172.18.0.2 dev sg0 on the server sends its egress through that client); and finally the --gateway client, if configured. A frame that matches none is dropped rather than looped back to the tun.

The routing-table step honors policy routing (it queries the kernel with the frame's own source address), so a rule like from 10.9.0.0/24 lookup wgexit is respected. The client at the far end must forward and NAT the traffic itself — for example:

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -o eth0 -j MASQUERADE

Use --gateway <client-tunnel-ip> when you want a fixed fallback client for any destination the server host has no explicit route for.

Running in Docker
docker run --rm -it \
  --cap-add NET_ADMIN \
  --device /dev/net/tun \
  shadowgate client --connect server.example.com:3389 --password secret

Security

  • Confidentiality and integrity rest entirely on the shared --password. Use a long, high-entropy secret.
  • Both transports use authenticated encryption (AEAD): the wrong password, and any tampering with a record or datagram, are detected and rejected. There is no forward secrecy — the keys are derived from the password alone — so a compromised password exposes past captured traffic.
  • shadowgate has not undergone a professional security review; it is not a substitute for a formally audited VPN such as WireGuard in adversarial environments.
  • Transparent forwarding trusts every peer. Because all peers share one password, the server cannot tell them apart: any peer can source frames from, and thus advertise a route for, any address (including one another's), and a forwarding-enabled client relays whatever it is handed. Run shadowgate only among mutually trusted machines, do not rely on tunnel source addresses for access control on the server, and constrain a relay client with host firewall rules. The routing table is size-bounded so a single peer cannot exhaust server memory, but it cannot prevent a trusted peer from hijacking another's route.

Development

See CONTRIBUTING.md for the development setup, coding conventions, and how to run the linters and tests.

make build      # build ./shadowgate
make test       # run unit tests
make lint       # golangci-lint (+ mulint if installed)
make coverage   # tests with an HTML coverage report

License

Released under the MIT License.

Directories

Path Synopsis
internal
cli
client
Package client implements the shadowgate client.
Package client implements the shadowgate client.
compress
Package compress wraps a stream connection with Snappy compression.
Package compress wraps a stream connection with Snappy compression.
core
Package core holds the transport-agnostic heart of a shadowgate server: the tun device and the routing table that maps a tunnel address to the transport peer that owns it.
Package core holds the transport-agnostic heart of a shadowgate server: the tun device and the routing table that maps a tunnel address to the transport peer that owns it.
deferutil
Package deferutil holds small helpers meant to be deferred at the top of a function or goroutine.
Package deferutil holds small helpers meant to be deferred at the top of a function or goroutine.
e2e
Package e2e exercises the full client/server stack over real loopback sockets using an in-memory tun device.
Package e2e exercises the full client/server stack over real loopback sockets using an in-memory tun device.
ipv4
Package ipv4 provides a thin, allocation-free view over raw IPv4 packets.
Package ipv4 provides a thin, allocation-free view over raw IPv4 packets.
obfuscate
Package obfuscate implements shadowgate's headerless, fully-encrypted UDP packet format.
Package obfuscate implements shadowgate's headerless, fully-encrypted UDP packet format.
secure
Package secure provides an authenticated, encrypted record layer for a stream connection (the TCP transport).
Package secure provides an authenticated, encrypted record layer for a stream connection (the TCP transport).
server
Package server orchestrates a shadowgate server: it owns the shared router (tun device + routing table) and starts the enabled transports (TCP, UDP, or both), which all route through that single router so clients on different transports can reach each other.
Package server orchestrates a shadowgate server: it owns the shared router (tun device + routing table) and starts the enabled transports (TCP, UDP, or both), which all route through that single router so clients on different transports can reach each other.
tun
Package tun opens and manages a Linux TUN network interface.
Package tun opens and manages a Linux TUN network interface.
tuntest
Package tuntest provides an in-memory tun.TUN implementation for tests.
Package tuntest provides an in-memory tun.TUN implementation for tests.
udp

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL