WGX shares its name with several other WireGuard tools, so the project becomes ihasvpn, alongside ihasmail. - Module github.com/Coffey-Labs/ihasvpn, command cmd/ihasvpn, image ghcr.io/coffey-labs/ihasvpn. - Environment variables move from WGX_* to IHASVPN_*. The default database is ihasvpn.db, the nftables table is `ihasvpn`, metrics are ihasvpn_*, and the session cookie and theme key are renamed, so existing sessions end. - The mark is the ihasmail cat peeking over the edge of a shield, drawn as a vector. docs/brand/generate.py builds the mark, mono mark, wordmarks, social card, favicons and app icons from that one drawing. - The console takes ihasmail's palette: the ihasmail.org teal-navy for dark, its contrast-checked light tiers with the site's light accent, received traffic in the cat's orange and sent in teal. The wordmark weight and font stack follow ihasmail.org. - Detail values wrap at spaces before breaking inside an address, so an IPv6 tunnel address no longer splits mid-number. - The README history note about the earlier WGX installer is gone with the name it explained. Screenshots retaken.
5.1 KiB
Performance
ihasvpn is built so the packet path is as short as it can be. This page explains what it does on its own, what only the host can do, and how to check the result.
What decides throughput
In rough order of importance:
- Kernel data plane. WireGuard in the kernel handles encryption in the
network stack with no copies to user space. ihasvpn creates a native
wireguardlink over netlink and configures it over the same channel; nothing sits between the NIC and the module. On a host without the module ihasvpn falls back towireguard-go, which works everywhere but moves every packet through a user-space process and is several times slower. The dashboard says which one is in use. Any Linux kernel from 5.6 has the module; on older kernels installwireguard-dkmson the host. - MTU and MSS. A tunnel packet has 60 bytes of overhead on IPv4 (80 on IPv6). The default MTU of 1420 fits a 1500-byte underlay. If the path to the server is smaller than that (PPPoE, a second tunnel, some mobile networks) packets fragment or vanish and downloads crawl. ihasvpn clamps the TCP MSS of forwarded connections to the route MTU, which removes the "connected but pages hang" failure outright; lower the MTU in Settings if UDP-heavy traffic still struggles.
- Socket buffers. Bursts on a fast link fill the default UDP receive buffer before WireGuard drains it, and the kernel drops the excess. These are host-wide sysctls, so see below.
- Port mapping. In the default bridged mode Docker DNATs the UDP port,
which costs a conntrack lookup per packet.
docker-compose.host.ymlputs the socket on the host network and skips it. - The CPU. WireGuard is ChaCha20-Poly1305 and scales across cores per peer; a single flow is bounded by one core. Machines with AVX2 or ARMv8 crypto extensions do markedly better.
What ihasvpn sets by itself
At startup ihasvpn writes these through /proc/sys and reports the outcome on
the dashboard under Show kernel tuning:
| sysctl | value | why |
|---|---|---|
net.ipv4.ip_forward |
1 | required; peers go nowhere without it |
net.ipv6.conf.all.forwarding |
1 | required when IHASVPN_SUBNET6 is set |
net.ipv4.conf.all.rp_filter, ...default.rp_filter |
2 | strict reverse-path filtering drops legitimate tunnel replies |
net.core.rmem_max, net.core.wmem_max |
26214400 | room for bursts on the UDP socket |
net.core.rmem_default, net.core.wmem_default |
1048576 | default socket buffers |
net.core.netdev_max_backlog |
16384 | deeper per-CPU input queue |
net.ipv4.udp_rmem_min, net.ipv4.udp_wmem_min |
16384 | UDP buffers under memory pressure |
The first three groups are network-namespaced and work inside the container
when the compose file passes them under sysctls: (forwarding) or the
container has NET_ADMIN (rp_filter). The net.core.* and udp_* ones are
global: the kernel refuses them from inside a container, ihasvpn logs a
warning, and they show as "not set" on the dashboard. That is expected; set
them on the host.
Host settings
Drop this into /etc/sysctl.d/99-ihasvpn.conf on the Docker host and run
sysctl --system:
# Socket buffers: let a 1-10 GbE burst queue rather than drop.
net.core.rmem_max = 26214400
net.core.wmem_max = 26214400
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576
net.core.netdev_max_backlog = 16384
net.ipv4.udp_rmem_min = 16384
net.ipv4.udp_wmem_min = 16384
# Fair queueing and BBR help the *host's own* TCP flows; forwarded peer
# traffic keeps the peers' congestion control. Harmless, often helpful.
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# Forwarding, in case you run the host-network compose file and want to own
# it yourself (ihasvpn sets it otherwise).
net.ipv4.ip_forward = 1
Two more things on the host that are worth checking on a busy server:
- UDP GRO/GSO offload on the physical NIC lets the kernel batch
WireGuard's UDP segments. It is on by default on modern drivers;
ethtool -k eth0 | grep -i udpshows the state. - IRQ affinity / multi-queue. WireGuard spreads decryption across all
CPUs, but the NIC's receive queues need to be spread too.
irqbalanceon most distributions does it; on single-queue virtual NICs there is nothing to gain.
Host networking
docker-compose.host.yml runs ihasvpn with network_mode: host. It removes the
Docker port mapping from the path and lets ihasvpn set the host's own
forwarding sysctls. The costs are listed at the top of that file; in short,
wg0 and the ihasvpn nftables table become visible on the host, and the admin
UI is bound to localhost so it is not exposed by accident.
Measuring
Run iperf3 -s on a machine behind the server (or on the server itself)
and iperf3 -c <tunnel address> -P 4 from a peer, then compare it with the
same test outside the tunnel. On a wired gigabit link a modern x86 host with
the kernel module should get within a few percent of line rate; with
wireguard-go expect a few hundred Mbit/s and a busy core.
The dashboard's live rates come from the interface counters every two seconds, so they show what the tunnel actually carries during the test.