Skip to content

WireGuard endpoint (system: true) + tun: packets sent but no return traffic — routing loop #3858

Description

@Leadaxe

Operating system

Windows

System version

1.13.0

Installation type

Original sing-box Command Line

If you are using a graphical client, please provide the version of the client.

https://github.com/Leadaxe/singbox-launcher/releases/tag/v0.8.2

Version

Description

About

I'm the author of singbox-launcher
a cross-platform GUI launcher for sing-box written in Go (300+ ★).

I'm working on adding WireGuard endpoint support to the GUI,
and ran into this issue while testing WG configs via CLI before implementing the feature.

Environment

  • sing-box version: latest stable
  • OS: Windows 11
  • Mode: TUN (auto_route + strict_route) + WireGuard endpoint with system: true
  • Tool: singbox-launcher — GUI launcher for sing-box (Go, Fyne)

Config

config.json (keys redacted)
{
  "log": { "level": "info", "timestamp": true },
  "dns": {
    "servers": [
      { "type": "udp", "tag": "direct_dns_resolver", "server": "1.1.1.1", "server_port": 53 }
    ],
    "rules": [{ "clash_mode": "direct", "server": "direct_dns_resolver" }],
    "final": "direct_dns_resolver",
    "independent_cache": true
  },
  "inbounds": [
    {
      "type": "tun", "tag": "tun-in",
      "address": ["172.19.0.1/30"],
      "auto_route": true, "strict_route": true, "stack": "mixed",
      "route_exclude_address": ["212.x.x.x/32"]
    }
  ],
  "endpoints": [
    {
      "type": "wireguard", "tag": "wg-ep", "name": "singbox-wg0",
      "system": true, "mtu": 1420,
      "address": ["10.10.10.2/32"],
      "private_key": "...",
      "listen_port": 10000,
      "peers": [
        {
          "address": "212.x.x.x", "port": 51820,
          "public_key": "...",
          "allowed_ips": ["0.0.0.0/0", "::/0"],
          "persistent_keepalive_interval": 25
        }
      ]
    }
  ],
  "outbounds": [
    { "type": "direct", "tag": "wg-out", "bind_interface": "singbox-wg0" },
    { "type": "direct", "tag": "direct-out" }
  ],
  "route": {
    "rules": [
      { "action": "sniff" },
      { "protocol": "dns", "action": "hijack-dns" }
    ],
    "auto_detect_interface": true,
    "final": "wg-out"
  }
}

Expected behavior

All traffic routes through WireGuard tunnel (singbox-wg0). ping 1.1.1.1 works.

Actual behavior

  • ping 212.x.x.x (WG server) — works (4ms, physical route)
  • ping 1.1.1.1hangs, no response
  • ping 192.168.100.1 (WG peer network) — timeout

WG handshake appears to complete (logs show endpoint started, keepalive active), but return traffic never arrives.

Root cause (investigated)

The tun interface installs a default route 0.0.0.0/0 → 172.19.0.2 with metric 0. This captures all traffic including WireGuard handshake UDP packets destined for the WG server, creating a routing loop:
WG handshake → tun → sing-box → wg-out → singbox-wg0 → [needs to reach 212.x.x.x] → tun → loop

What doesn't work

route_exclude_address: ["212.x.x.x/32"] — does NOT work as expected.

Instead of adding a single /32 exclusion, sing-box tun creates many aggregate routes covering the surrounding address space, but not the exact address itself. The WG server IP still gets routed through tun.

Example of what actually appears in the routing table after adding route_exclude_address:
route print shows ~20 aggregate routes around 212.x.x.x, but 212.x.x.x/32 itself is absent

Workaround

Manual OS-level route after each sing-box start:

route add 212.x.x.x mask 255.255.255.255 192.168.1.1 metric 5

This is fragile — requires running after every restart and is not automatable within sing-box config.

Expected fix / question

Is there a way to properly exclude a single IP from tun routing so that WireGuard handshake traffic bypasses the tun interface entirely?

route_exclude_address behavior with exact /32 addresses seems like a bug — it should add a host route for that exact IP, not aggregate routes.

Reproduction

Reproduction steps

Prerequisites

  • Windows 10/11
  • sing-box latest version (run as Administrator)
  • Any working WireGuard server (use your own keys/endpoint)

Steps

  1. Create config.json, substituting your own WireGuard credentials:
{
  "log": { "level": "info", "timestamp": true },
  "inbounds": [
    {
      "type": "tun",
      "tag": "tun-in",
      "address": ["172.19.0.1/30"],
      "auto_route": true,
      "strict_route": true,
      "stack": "mixed",
      "route_exclude_address": ["<YOUR_WG_SERVER_IP>/32"]
    }
  ],
  "endpoints": [
    {
      "type": "wireguard",
      "tag": "wg-ep",
      "name": "singbox-wg0",
      "system": true,
      "mtu": 1420,
      "address": ["<YOUR_WG_CLIENT_IP>/32"],
      "private_key": "<YOUR_PRIVATE_KEY>",
      "listen_port": 10000,
      "peers": [
        {
          "address": "<YOUR_WG_SERVER_IP>",
          "port": <YOUR_WG_PORT>,
          "public_key": "<YOUR_SERVER_PUBLIC_KEY>",
          "allowed_ips": ["0.0.0.0/0", "::/0"],
          "persistent_keepalive_interval": 25
        }
      ]
    }
  ],
  "outbounds": [
    { "type": "direct", "tag": "wg-out", "bind_interface": "singbox-wg0" },
    { "type": "direct", "tag": "direct-out" }
  ],
  "route": {
    "rules": [
      { "action": "sniff" },
      { "protocol": "dns", "action": "hijack-dns" }
    ],
    "auto_detect_interface": true,
    "final": "wg-out"
  }
}
  1. Run as Administrator:
sing-box run -c config.json
  1. In a separate cmd window, check routing table:
route print
  1. Try to ping any public IP:
ping 1.1.1.1

Expected result

  • route print shows exact host route for <YOUR_WG_SERVER_IP>/32 bypassing tun
  • ping 1.1.1.1 replies through WireGuard tunnel

Observed result

  • route print shows aggregate routes around the server IP, but no exact /32 entry for it
  • ping 1.1.1.1 hangs — packets are sent but no replies received
  • ping <YOUR_WG_SERVER_IP> works (physical route exists)

Workaround (manual, required after every start)

route add <YOUR_WG_SERVER_IP> mask 255.255.255.255 <YOUR_GATEWAY_IP> metric 5

After adding this route manually, tunnel works correctly.

Logs

Supporter

Integrity requirements

  • I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
  • I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data.
  • I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
  • I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions