Files
shadowsocks-libev/tests/test_half_close.py
T
Max LvandClaude 50f1ae54f7 Fix findings from the security audit (#3083)
* Fix findings from the security audit

Remote:
- TCP: handshake/connect deadlines follow --timeout instead of a 24h
  minimum, so idle sockets can no longer fill the connection pool.
- ACL: IPv4-mapped IPv6 addresses are matched against the IPv4 lists.
- UDP server now enforces the peer ACL and the outbound block list.
- ACL loading fails closed on bad files or invalid entries.
- TCP half-close is relayed instead of tearing the connection down.
- AEAD-2022 UDP sessions are created only after timestamp/replay checks.

ss-manager:
- Owner-only conf files (deleted on remove), 0600 control socket,
  umask 022, safe working directory, loopback-only IP addresses,
  per-user sender check on Linux, plugins only over the UNIX socket.
- Port canonicalisation, list() overflow, add/remove waits for the
  server, stale pid files can't signal unrelated processes.

Also: plugins drop to --user, daemonize readiness/pid file handling,
resolver cancellation, replay-filter fail-closed, key wiping, spec
strictness in AEAD/2022 parsing, JSON surrogate validation, ss:// plugin
name checks, embedding API state reset, Android protect_socket replies.

Adds tests/test_server_acl.py and tests/test_half_close.py and extends
the manager, ACL, AEAD-2022 and ssurl tests.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix CI on Ubuntu 26.04 runners; use string_view over raw string pointers

- Define _GLIBCXX_NO_ASSERTIONS: libstdc++ 15 enables assertions at -O0,
  which pulls __glibcxx_assert_fail (C++ runtime) into the programs.
- test_redir_qemu: boot the guest with -cpu max so the runner's glibc
  loader runs under TCG.
- find_program: std::string_view PATH splitting into a fixed buffer, no
  heap and no null snprintf destination (GCC -Wformat-truncation).
- manager: parse /proc/net/udp numbers with strtoul (cert-err34-c);
  walk /proc/<pid>/cmdline with std::string_view.
- ssurl: sanitise the tag through std::span.

Co-Authored-By: Claude <noreply@anthropic.com>

* coverage: tolerate gcov 15 lambda inconsistencies in lcov 2

Co-Authored-By: Claude <noreply@anthropic.com>

* resolver: replace init's cleanup lambda with a member; lcov --list ignores inconsistent

gcov 15 marks the lambda as not hit while its lines are, which lcov 2
rejects.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-28 09:34:36 +08:00

64 lines
2.4 KiB
Python

#!/usr/bin/env python3
"""TCP half-close through ss-local and ss-server: a client that sends its
request and then shuts down its write side still gets the whole response,
and the origin sees the EOF."""
import argparse
import hashlib
import os
from pathlib import Path
import socket
import socketserver
import threading
from interop import free_port, peers, socks
class ReplyAfterEOF(socketserver.BaseRequestHandler):
"""Reads to EOF, then answers with the digest and a large body."""
def handle(self):
self.request.settimeout(15)
digest = hashlib.sha256()
while chunk := self.request.recv(65536):
digest.update(chunk)
body = digest.digest() * 32768 # 1 MiB
self.request.sendall(body)
class Origin(socketserver.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--bin", required=True, type=Path)
args = parser.parse_args()
bins = args.bin.resolve()
with Origin(("127.0.0.1", 0), ReplyAfterEOF) as origin:
threading.Thread(target=origin.serve_forever, daemon=True).start()
for method in ("aes-256-gcm", "2022-blake3-aes-128-gcm"):
key = ["-k", "pw"] if not method.startswith("2022") else ["-k", "AAAAAAAAAAAAAAAAAAAAAA=="]
sport, lport = free_port(), free_port()
server = [str(bins / "ss-server"), "-s", "127.0.0.1", "-p", str(sport), "-m", method] + key
local = [str(bins / "ss-local"), "-s", "127.0.0.1", "-p", str(sport), "-l", str(lport), "-m",
method] + key
with peers([server, local], [sport, lport]):
sock, _ = socks(lport, 1, ("127.0.0.1", origin.server_address[1]))
with sock:
sock.settimeout(15)
request = os.urandom(200000)
sock.sendall(request)
sock.shutdown(socket.SHUT_WR)
received = bytearray()
while chunk := sock.recv(65536):
received += chunk
expected = hashlib.sha256(request).digest() * 32768
assert bytes(received) == expected, \
f"{method}: got {len(received)} of {len(expected)} bytes after a half-close"
print(f"PASS half-close: {method}")
if __name__ == "__main__":
main()