Skip to content

TCPClientTunnel BindAddress registered as landlock.ConnectTCP instead of landlock.BindTCP — self-denied "bind: permission denied" on kernels with Landlock ABI v4 #218

Description

@bkmz

Summary

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:

// cmd/wireproxy/main.go, v1.1.3
case *wireproxy.HTTPConfig:
    rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))
case *wireproxy.TCPClientTunnelConfig:
    rules = append(rules, landlock.ConnectTCP(uint16(section.BindAddress.Port))) // <-- wrong
case *wireproxy.Socks5Config:
    rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))
case *wireproxy.SNIConfig:
    rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))

(https://github.com/windtf/wireproxy/blob/v1.1.3/cmd/wireproxy/main.go#L177-L183)

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.

Reproduction

Minimal config:

[Interface]
PrivateKey = <redacted>
Address = 10.0.0.2/32

[Peer]
PublicKey = <redacted>
Endpoint = <redacted>
AllowedIPs = 0.0.0.0/0

[TCPClientTunnel]
BindAddress = 0.0.0.0:5432
Target = example.internal:5432

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:

 case *wireproxy.TCPClientTunnelConfig:
-    rules = append(rules, landlock.ConnectTCP(uint16(section.BindAddress.Port)))
+    rules = append(rules, landlock.BindTCP(uint16(section.BindAddress.Port)))

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.

Environment

  • wireproxy v1.1.3 (also present in v1.0.8 through v1.1.3 — same code since introduced in Limit wireproxy's permissions with landlock #108)
  • Reproduced on: Docker Desktop for Mac (Linux 6.7+ LinuxKit VM) and Railway's container platform
  • go-landlock v0.6.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions