Skip to content

socks: fix half-close from outbound side#97

Open
nlzy wants to merge 1 commit intoSagerNet:devfrom
nlzy:dev
Open

socks: fix half-close from outbound side#97
nlzy wants to merge 1 commit intoSagerNet:devfrom
nlzy:dev

Conversation

@nlzy
Copy link

@nlzy nlzy commented Mar 6, 2026

sing-box config
{
  "inbounds": [
    {
      "type": "socks",
      "tag": "socks-noauth",
      "listen": "127.0.0.1",
      "listen_port": 1080
    },
    {
      "type": "http",
      "tag": "http-noauth",
      "listen": "127.0.0.1",
      "listen_port": 8080
    }
  ],
  "outbounds": [
    {
      "type": "direct",
      "tag": "direct"
    }
  ],
  "route": {
    "rules": [
      {
        "outbound": "direct"
      }
    ]
  }
}
example server & client
#!/usr/bin/env python3
"""
TCP Half-Close Test Tool (Server-initiated)
===========================================

This script implements a TCP half-close test where the server initiates
a half-close (shutdown write end) while keeping the read end open.

Usage:
    python3 tcp_halfclose_sv.py -s <bind_addr> -p <port>    # Run as server
    python3 tcp_halfclose_sv.py -c <server_addr> -p <port>  # Run as client

Arguments:
    -s, --server <bind_addr>    Run in server mode, binding to specified address
    -c, --client <server_addr>  Run in client mode, connecting to specified server
    -p, --port <port>           Port number to use (default: 37777)

Test Flow:
                            Client                              Server
                              |                                    |
                              |-------- 1. send 'c' -------------->|
                              |         (DATA_SIZE bytes)          |
                              |                                    |-- 2. recv 'c'
                              |                                    |      print SERVER-FIRST-RECV-OK
                              |                                    |
                              |<------- 3. send 's' ---------------|
                              |         (DATA_SIZE bytes)          |
                              |         shutdown(SHUT_WR)          |
      4. recv 's' until EOF --|                                    |
 print CLIENT-FINAL-RECV-OK   |                                    |
                              |                                    |
                              |-------- 5. send 'C' -------------->|
                              |         (DATA_SIZE bytes)          |
                              |         close()                    |
                              |                                    |-- 6. recv 'C' until EOF
                              |                                    |      print SERVER-FINAL-RECV-OK
                              |                                    |

Output Messages:
    SERVER-FIRST-RECV-OK    - Server successfully received first data chunk
    CLIENT-FINAL-RECV-OK    - Client received data and detected server half-close (FIN)
    SERVER-FINAL-RECV-OK    - Server received second chunk and detected client close (FIN)

Exit Codes:
    0 - Success
    1 - Error (with details printed to stderr)
"""

import socket
import sys
import argparse

DATA_SIZE = 100000  # 100,000 bytes


def recv_exact(sock, size):
    data = bytearray()
    while len(data) < size:
        chunk = sock.recv(min(4096, size - len(data)))
        if not chunk:
            return None
        data.extend(chunk)
    return bytes(data)


def run_server(bind_addr, port):
    """Run as server"""
    server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_sock.bind((bind_addr, port))
    print(f"Server listening on {bind_addr}:{port}", flush=True)
    server_sock.listen(1)

    conn, addr = server_sock.accept()

    # diagram.2: Receive 'c' from client
    data1 = recv_exact(conn, DATA_SIZE)
    if data1 is None:
        print(
            f"ERROR: Failed to receive first data, connection closed", file=sys.stderr
        )
        sys.exit(1)
    if len(data1) != DATA_SIZE:
        print(
            f"ERROR: Failed to receive first data, got {len(data1)} bytes, expected {DATA_SIZE}",
            file=sys.stderr,
        )
        sys.exit(1)

    # Verify data content
    if data1 != b"c" * DATA_SIZE:
        print("ERROR: First data mismatch", file=sys.stderr)
        sys.exit(1)

    print("SERVER-FIRST-RECV-OK", flush=True)

    # diagram.3: Send 's' to client and shutdown write end (half-close)
    conn.sendall(b"s" * DATA_SIZE)
    conn.shutdown(socket.SHUT_WR)

    # diagram.6: Receive 'C' from client and detect client close
    data2 = recv_exact(conn, DATA_SIZE)
    if data2 is None:
        print(
            f"ERROR: Failed to receive second data, connection closed", file=sys.stderr
        )
        sys.exit(1)
    if len(data2) != DATA_SIZE:
        print(
            f"ERROR: Failed to receive second data, got {len(data2)} bytes, expected {DATA_SIZE}",
            file=sys.stderr,
        )
        sys.exit(1)

    # Verify data content
    if data2 != b"C" * DATA_SIZE:
        print("ERROR: Second data mismatch", file=sys.stderr)
        sys.exit(1)

    # Confirm client has stopped sending (must receive FIN, not RST)
    # Keep reading until EOF (empty bytes) which indicates FIN received
    conn.settimeout(5)
    while True:
        chunk = conn.recv(4096)
        if chunk == b"":
            # EOF received - this is the expected FIN
            break
        if chunk:
            print(
                f"ERROR: Received unexpected extra data: {len(chunk)} bytes",
                file=sys.stderr,
            )
            sys.exit(1)

    print("SERVER-FINAL-RECV-OK", flush=True)

    # Close connection
    conn.close()
    server_sock.close()


def run_client(server_addr, port):
    """Run as client"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((server_addr, port))

    # diagram.1: Send 'c' to server
    sock.sendall(b"c" * DATA_SIZE)

    # diagram.4: Receive 's' from server and detect server half-close
    data1 = recv_exact(sock, DATA_SIZE)
    if data1 is None:
        print(f"ERROR: Failed to receive data, connection closed", file=sys.stderr)
        sys.exit(1)
    if len(data1) != DATA_SIZE:
        print(
            f"ERROR: Failed to receive data, got {len(data1)} bytes, expected {DATA_SIZE}",
            file=sys.stderr,
        )
        sys.exit(1)

    # Verify data content
    if data1 != b"s" * DATA_SIZE:
        print("ERROR: Data mismatch", file=sys.stderr)
        sys.exit(1)

    # Confirm server has stopped sending (must receive FIN, not RST)
    # Keep reading until EOF (empty bytes) which indicates FIN received
    sock.settimeout(5)
    while True:
        chunk = sock.recv(4096)
        if chunk == b"":
            # EOF received - this is the expected FIN
            break
        if chunk:
            print(
                f"ERROR: Received unexpected extra data: {len(chunk)} bytes",
                file=sys.stderr,
            )
            sys.exit(1)

    print("CLIENT-FINAL-RECV-OK", flush=True)

    # diagram.5: Send 'C' to server and close connection
    sock.sendall(b"C" * DATA_SIZE)

    # Close connection
    sock.close()


def main():
    parser = argparse.ArgumentParser(
        description="TCP half-close test tool (server-initiated)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s -s 0.0.0.0 -p 37777          # Start server
  %(prog)s -c 127.0.0.1 -p 37777        # Start client
        """,
    )

    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument(
        "-s",
        "--server",
        metavar="BIND_ADDR",
        help="Run as server, binding to specified address",
    )
    group.add_argument(
        "-c",
        "--client",
        metavar="SERVER_ADDR",
        help="Run as client, connecting to specified server",
    )
    parser.add_argument(
        "-p", "--port", type=int, default=37777, help="Port number (default: 37777)"
    )

    args = parser.parse_args()

    if args.server:
        run_server(args.server, args.port)
    else:
        run_client(args.client, args.port)


if __name__ == "__main__":
    main()

Issue

TCP half-closing from outbound side is not working.

Affected only with SOCKS inbound, HTTP inbound is worked.

Reproduction steps

  1. Start sing-box with above config
  2. Start TCP server python3 tcp_halfclose_sv.py -s 0.0.0.0
  3. Launch TCP client with proxychains proxychains python3 tcp_halfclose_sv.py -c <YOUR_LOCAL_IP>

Note

Use 127.0.0.1 as your local IP may not work, please use IPs that attached in your net interface.

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

Successfully merging this pull request may close these issues.

1 participant