Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential UDP Packet Handling Issue in TUN Mode (sing-box 1.11.4) #2655

Open
4 of 5 tasks
kexichan opened this issue Feb 28, 2025 · 2 comments
Open
4 of 5 tasks

Potential UDP Packet Handling Issue in TUN Mode (sing-box 1.11.4) #2655

kexichan opened this issue Feb 28, 2025 · 2 comments

Comments

@kexichan
Copy link

Operating system

Linux

System version

Ubuntu 24.04.1 LTS x86_64

Installation type

Original sing-box Command Line

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

No response

Version

1.11.4

Description

When running sing-box 1.11.4 with TUN mode enabled for UDP traffic, if a specific outbound route is configured for UDP port 443, there is a high likelihood (approximately 90% or more) that the software will incorrectly forward UDP packets directly out of the WAN interface without proper processing. This behavior may expose raw packet data and bypass intended routing rules.
And after I downgraded sing-box to version 1.10.7, the issue disappeared.

Reproduction

Steps to Reproduce:

Configure sing-box 1.11.4 to use TUN mode for UDP inbound traffic.
Set up a specific outbound rule within the software for UDP port 443.
Initiate UDP traffic on port 443.
Observe that in a majority of cases (around 90% or higher), the UDP packets are forwarded directly via the WAN interface without modification or proper routing.

you may use this python code to send quic traffic

import asyncio
import logging
import urllib.parse

from aioquic.asyncio import connect, QuicConnectionProtocol
from aioquic.h3.connection import H3Connection, H3_ALPN
from aioquic.h3.events import DataReceived
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import QuicEvent


class HttpClientProtocol(QuicConnectionProtocol):
    """
    An HTTP/3 client protocol that sends a GET request using aioquic and collects the response.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._http = H3Connection(self._quic)
        self._response = b""
        self._response_received = asyncio.Event()
        self._stream_id = None

    def send_request(self, host: str, path: str):
        self._stream_id = self._quic.get_next_available_stream_id()
        headers = [
            (b":method", b"GET"),
            (b":authority", host.encode()),
            (b":scheme", b"https"),
            (b":path", path.encode() if path else b"/"),
        ]
        # Use send_headers to encode and send the HTTP/3 headers
        self._http.send_headers(self._stream_id, headers, end_stream=True)

    def quic_event_received(self, event: QuicEvent):
        events = self._http.handle_event(event)
        for http_event in events:
            if hasattr(http_event, "stream_id") and http_event.stream_id == self._stream_id:
                if isinstance(http_event, DataReceived):
                    self._response += http_event.data
                # Signal when the stream is ended
                if getattr(http_event, "stream_ended", False):
                    self._response_received.set()


async def fetch(url: str) -> str:
    """
    Opens an HTTP/3 connection to the given URL, sends a GET request, and returns the response body.
    """
    parsed = urllib.parse.urlparse(url)
    host = parsed.hostname
    path = parsed.path or "/"
    port = parsed.port or 443

    configuration = QuicConfiguration(is_client=True, alpn_protocols=H3_ALPN)
    configuration.server_name = host  # set SNI

    async with connect(
        host,
        port,
        configuration=configuration,
        create_protocol=HttpClientProtocol
    ) as client:
        client: HttpClientProtocol  # type hint for clarity
        client.send_request(host, path)
        await client._response_received.wait()
        return client._response.decode()


async def main():
    url = "https://www.cloudflare.com/cdn-cgi/trace"
    while True:
        try:
            response = await fetch(url)
            ip_line = None
            # Look for the line that starts with "ip="
            for line in response.splitlines():
                if line.startswith("ip="):
                    ip_line = line
                    break
            if ip_line:
                print(ip_line)
            else:
                print("No ip= line found")
            await asyncio.sleep(1)
        except KeyboardInterrupt:
            print("\nExiting...")
            break

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    asyncio.run(main())

Logs

The logs show that the packet was sent out from the expected outbound (proxy). However, based on the pwru results and the results returned by the aforementioned Python code, the packet was actually sent directly from the WAN interface. The pwru results are consistent with the tcpdump results.

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.
@kexichan
Copy link
Author

Image
This is the result from pwru. Clearly, we observed that the traffic entered sing-box, but it was sent out as-is. The "mysrc" field is the result of using sed to replace my local IPv6 address. I have already disabled IPv6 NAT.

@kexichan
Copy link
Author

Here is a summary of the configuration file:

{
  "log": {
    "level": "debug",
    "timestamp": true
  },
  "inbounds": [
    {
      "type": "redirect",
      "tag": "redirect",
      "listen": "::",
      "listen_port": 2000,
      "sniff": true,
      "sniff_override_destination": true
    },
    {
      "type": "tun",
      "tag": "tun-in",
      "interface_name": "singbox",
      "sniff": true,
      "sniff_override_destination": true,
      "address": [
        "172.18.0.1/30",
        "fdfe:dcba:9876::1/126"
      ],
      "mtu": 1500,
      "endpoint_independent_nat": true,
      "udp_timeout": "5m",
      "stack": "system"
    }
  ],
  "outbounds": [
  ],
  "route": {
    "auto_detect_interface": false,
    "rules": [
      {
        "network": "udp",
        "port": [
          443
        ],
        "outbound": "QUIC_OUTBOUND"
      },
      {
        "network": "udp",
        "outbound": "ANOTHER_UDP_OUTBOUND" //this works well
      }
    ],
    "final": "proxy"
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant