A WireGuard implementation in Zig, ported from boringtun. Zero external dependencies — uses only Zig's standard library.
- Zig 0.15.x
- macOS (utun support) or Linux
zig buildThis produces both a library (zig-out/lib/libziguard.a) and a CLI binary (zig-out/bin/ziguard).
zig build testShow individual test names and timing:
zig build test --summary allGenerate a keypair using wg (from wireguard-tools) or any WireGuard key generator:
wg genkey | tee private.key | wg pubkey > public.keyStart the tunnel (requires root for utun access):
sudo zig-out/bin/ziguard \
--private-key $(cat private.key) \
--peer-public-key <PEER_PUBLIC_KEY> \
--endpoint <PEER_IP>:<PEER_PORT> \
--allowed-ips 10.0.0.0/24 \
--listen-port 51820 \
--tun utunThen configure the interface address and routing:
sudo ifconfig utun7 10.0.0.2/24 10.0.0.1
sudo route add -net 10.0.0.0/24 -interface utun7(Replace utun7 with the actual interface name printed at startup.)
Required:
--private-key <base64> Local private key
--peer-public-key <base64> Peer's public key
--endpoint <ip:port> Peer endpoint address
--allowed-ips <cidr> Comma-separated CIDR list (e.g. 10.0.0.0/24,0.0.0.0/0)
Optional:
--listen-port <port> UDP listen port (default: random)
--tun <name> TUN interface name (default: utun)
--keepalive <seconds> Persistent keepalive interval
--preshared-key <base64> Preshared key for additional security
--help Show this help
Press Ctrl+C to stop the tunnel.
src/
├── main.zig # CLI entry point
├── root.zig # Library entry point
├── crypto.zig # Crypto wrappers (X25519, ChaCha20Poly1305, BLAKE2s, HMAC)
├── tai64n.zig # TAI64N timestamps
├── serialization.zig # Base64/hex key encoding/decoding
├── noise/
│ ├── noise.zig # Module re-exports
│ ├── errors.zig # WireGuard error types
│ ├── index.zig # Session index management
│ ├── session.zig # Session encrypt/decrypt + replay prevention
│ ├── handshake.zig # Noise IKpsk2 handshake protocol
│ ├── timers.zig # Rekey/keepalive timer management
│ ├── rate_limiter.zig # Cookie-based DoS prevention
│ └── tunnel.zig # Tunnel state machine (encap/decap, handshake flow)
└── device/
├── device.zig # Device event loop (poll-based)
├── peer.zig # Peer state management
├── tun_darwin.zig # macOS utun TUN device
└── allowed_ips.zig # IP routing table (CIDR trie)
Ziguard also builds as a library. Import it from your Zig project:
const ziguard = @import("ziguard");
const Tunn = ziguard.noise.Tunn;
const serialization = ziguard.serialization;
// Parse a base64-encoded private key
const private_key = try serialization.parseKey("BASE64_ENCODED_KEY_HERE==");
// Derive public key
const public_key = try ziguard.crypto.x25519PublicKey(private_key);
// Create a tunnel to a peer
var tunnel = Tunn.init(
allocator,
private_key,
peer_public_key,
null, // preshared key (optional)
null, // persistent keepalive interval (optional)
0, // peer index
std.time.nanoTimestamp(),
);
defer tunnel.deinit();
// Encapsulate an outbound IP packet
var dst: [2048]u8 = undefined;
const result = tunnel.encapsulateAt(ip_packet, &dst, std.time.nanoTimestamp());
switch (result) {
.write_to_network => |data| {
// Send `data` as a UDP datagram to the peer
},
.err => |e| std.debug.print("error: {}\n", .{e}),
else => {},
}
// Decapsulate an inbound UDP datagram
var plain: [2048]u8 = undefined;
const recv = tunnel.decapsulateAt(null, udp_payload, &plain, std.time.nanoTimestamp());
switch (recv) {
.write_to_tunnel_v4 => |info| {
// Write `info.packet` to the TUN device
},
.write_to_network => |data| {
// Handshake/keepalive response — send back to peer
},
else => {},
}All crypto is provided by Zig's std.crypto:
| Primitive | Zig path |
|---|---|
| X25519 | std.crypto.dh.X25519 |
| ChaCha20-Poly1305 | std.crypto.aead.chacha_poly.ChaCha20Poly1305 |
| XChaCha20-Poly1305 | std.crypto.aead.chacha_poly.XChaCha20Poly1305 |
| BLAKE2s | std.crypto.hash.blake2.Blake2s256 |
| HMAC-BLAKE2s | std.crypto.auth.hmac.Hmac(Blake2s256) |
BSD-3-Clause