You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TCPClientTunnel BindAddress registered as landlock.ConnectTCP instead of landlock.BindTCP — self-denied "bind: permission denied" on kernels with Landlock ABI v4 #218
TCPClientTunnel's listener can fail to bind with bind: permission denied, on any address/port, on any host (bare metal, Docker, cloud platforms) — caused by wireproxy's own Landlock self-sandboxing, not the host environment.
Root cause
In lockNetwork() (cmd/wireproxy/main.go), every routine type that binds a local listener registers a landlock.BindTCP rule — except TCPClientTunnelConfig, which registers landlock.ConnectTCP instead:
SpawnRoutine for TCPClientTunnelConfig (routine.go) then calls a plain net.ListenTCP("tcp", conf.BindAddress) on that same address — a real bind, not a connect. Since the self-imposed Landlock network ruleset only allowlists a connect rule for that port, the kernel denies the listener's own bind() syscall once Landlock actually enforces network rules — which only happens on kernels supporting Landlock ABI v4 (network rules), added in Linux 6.7 (Jan 2024).
This explains why the bug has been silent since it was introduced in v1.0.8 (#108, April 2024): landlock.V4.BestEffort() silently skips network restriction entirely on any kernel older than 6.7, so on most systems until recently this dead/wrong rule had no effect. As 6.7+ kernels become the default (e.g. current Docker Desktop VM, most current cloud container platforms), the bug now reliably reproduces.
Run on a host with Linux 6.7+ (verified on: Docker Desktop for Mac's LinuxKit VM, and independently on Railway's container runtime):
$ wireproxy --config config.conf
...
DEBUG: ... Interface state was Down, requested Up, now Up
2026/07/27 12:07:56 listen tcp 0.0.0.0:5432: bind: permission denied
The WireGuard interface itself comes up fine (handshake succeeds) — only the local TCPClientTunnel listener fails. This reproduces with any port number (tested 5432, 8123, 15432, and platform-injected $PORT values), on any bind address (0.0.0.0, :PORT), confirming it's not port/address-specific — it's the missing BindTCP allowlist entry.
As an independent control: a non-Go process (redis:8.2.1) binding a comparable port on the exact same host has no issue — ruling out host/platform-level restrictions.
Fix
One-line change, swap ConnectTCP for BindTCP for the TCPClientTunnelConfig case:
I can open a PR with this change (plus, if useful, a regression test that asserts a BindTCP rule is present for TCPClientTunnelConfig in lockNetwork's output) if that's welcome.
Summary
TCPClientTunnel's listener can fail to bind withbind: permission denied, on any address/port, on any host (bare metal, Docker, cloud platforms) — caused by wireproxy's own Landlock self-sandboxing, not the host environment.Root cause
In
lockNetwork()(cmd/wireproxy/main.go), every routine type that binds a local listener registers alandlock.BindTCPrule — exceptTCPClientTunnelConfig, which registerslandlock.ConnectTCPinstead:(https://github.com/windtf/wireproxy/blob/v1.1.3/cmd/wireproxy/main.go#L177-L183)
SpawnRoutineforTCPClientTunnelConfig(routine.go) then calls a plainnet.ListenTCP("tcp", conf.BindAddress)on that same address — a real bind, not a connect. Since the self-imposed Landlock network ruleset only allowlists a connect rule for that port, the kernel denies the listener's ownbind()syscall once Landlock actually enforces network rules — which only happens on kernels supporting Landlock ABI v4 (network rules), added in Linux 6.7 (Jan 2024).This explains why the bug has been silent since it was introduced in v1.0.8 (#108, April 2024):
landlock.V4.BestEffort()silently skips network restriction entirely on any kernel older than 6.7, so on most systems until recently this dead/wrong rule had no effect. As 6.7+ kernels become the default (e.g. current Docker Desktop VM, most current cloud container platforms), the bug now reliably reproduces.Reproduction
Minimal config:
Run on a host with Linux 6.7+ (verified on: Docker Desktop for Mac's LinuxKit VM, and independently on Railway's container runtime):
The WireGuard interface itself comes up fine (handshake succeeds) — only the local
TCPClientTunnellistener fails. This reproduces with any port number (tested 5432, 8123, 15432, and platform-injected$PORTvalues), on any bind address (0.0.0.0,:PORT), confirming it's not port/address-specific — it's the missingBindTCPallowlist entry.As an independent control: a non-Go process (
redis:8.2.1) binding a comparable port on the exact same host has no issue — ruling out host/platform-level restrictions.Fix
One-line change, swap
ConnectTCPforBindTCPfor theTCPClientTunnelConfigcase:I can open a PR with this change (plus, if useful, a regression test that asserts a
BindTCPrule is present forTCPClientTunnelConfiginlockNetwork's output) if that's welcome.Environment
go-landlockv0.6.0