feat: add comprehensive HTTP e2e tests covering connector, TLS, probeResist, idleTimeout, and UDP relay
Adds 12 new HTTP handler test scenarios: HTTP connector (no-auth, auth, TLS upstream), CONNECT tunnel (no-sniffing, bypass-403), probeResist (host, web, file, knock), idleTimeout, and UDP relay over HTTP. Introduces RunGostContainerWithFiles helper for mounting extra files into test containers.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
UDP_ECHO_HOST = "udp-echo"
|
||||
UDP_ECHO_PORT = 5679
|
||||
|
||||
|
||||
def encode_socks5_addr(host, port):
|
||||
"""Encode (host, port) as SOCKS5 ADDRESS + PORT.
|
||||
|
||||
Uses ATYP=3 (domain name) so Docker DNS resolves the host.
|
||||
Returns (atyp, address_bytes, port_bytes).
|
||||
"""
|
||||
host_bytes = host.encode()
|
||||
addr_bytes = struct.pack("!B", len(host_bytes)) + host_bytes
|
||||
port_bytes = struct.pack("!H", port)
|
||||
return 0x03, addr_bytes, port_bytes
|
||||
|
||||
|
||||
def build_udp_frame(payload, host, port):
|
||||
"""Build a SOCKS5 UDP relay frame over TCP.
|
||||
|
||||
gost HTTP UDP relay uses RSV=data-length and FRAG=0xff.
|
||||
Frame: [RSV:2][FRAG:1][ATYP:1][DST.ADDR][DST.PORT:2][DATA]
|
||||
"""
|
||||
atyp, addr_bytes, port_bytes = encode_socks5_addr(host, port)
|
||||
rsv = struct.pack("!H", len(payload))
|
||||
frag = b"\xff"
|
||||
atyp_byte = struct.pack("!B", atyp)
|
||||
return rsv + frag + atyp_byte + addr_bytes + port_bytes + payload
|
||||
|
||||
|
||||
def recvn(sock, n):
|
||||
"""Receive exactly n bytes from socket."""
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise ConnectionError("connection closed while reading")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
|
||||
def read_socks5_frame(sock):
|
||||
"""Read one SOCKS5 UDP relay frame from the TCP connection.
|
||||
|
||||
Returns (addr, port, data).
|
||||
"""
|
||||
# Read header: RSV(2) + FRAG(1) + ATYP(1)
|
||||
header = recvn(sock, 4)
|
||||
rsv = struct.unpack("!H", header[:2])[0]
|
||||
frag = header[2]
|
||||
atyp = header[3]
|
||||
|
||||
# Read address based on ATYP
|
||||
if atyp == 1: # IPv4
|
||||
addr_bytes = recvn(sock, 4)
|
||||
addr = socket.inet_ntoa(addr_bytes)
|
||||
elif atyp == 3: # Domain name
|
||||
domain_len = recvn(sock, 1)
|
||||
addr = recvn(sock, domain_len[0]).decode()
|
||||
elif atyp == 4: # IPv6
|
||||
addr_bytes = recvn(sock, 16)
|
||||
addr = socket.inet_ntop(socket.AF_INET6, addr_bytes)
|
||||
else:
|
||||
raise ValueError(f"unknown ATYP: {atyp}")
|
||||
|
||||
port_bytes = recvn(sock, 2)
|
||||
port = struct.unpack("!H", port_bytes)[0]
|
||||
|
||||
# Read data
|
||||
if rsv > 0:
|
||||
data = recvn(sock, rsv)
|
||||
else:
|
||||
# Standard SOCKS5 UDP: read remaining
|
||||
data = b""
|
||||
while True:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
|
||||
return addr, port, data
|
||||
|
||||
|
||||
def main():
|
||||
host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
|
||||
port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(10)
|
||||
s.connect((host, port))
|
||||
|
||||
# Send CONNECT with X-Gost-Protocol: udp
|
||||
req = (
|
||||
b"CONNECT 0.0.0.0:0 HTTP/1.1\r\n"
|
||||
b"Host: 0.0.0.0:0\r\n"
|
||||
b"X-Gost-Protocol: udp\r\n"
|
||||
b"Proxy-Connection: keep-alive\r\n"
|
||||
b"\r\n"
|
||||
)
|
||||
s.sendall(req)
|
||||
|
||||
# Read 200 OK
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
chunk = s.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
resp += chunk
|
||||
|
||||
if b"200" not in resp:
|
||||
print(f"FAIL: expected 200, got {resp.decode(errors='replace')}")
|
||||
sys.exit(1)
|
||||
|
||||
# Build and send UDP relay frame
|
||||
payload = b"hello-gost"
|
||||
frame = build_udp_frame(payload, UDP_ECHO_HOST, UDP_ECHO_PORT)
|
||||
s.sendall(frame)
|
||||
|
||||
# Read response frame
|
||||
addr, rport, data = read_socks5_frame(s)
|
||||
|
||||
if b"hello-gost" in data:
|
||||
print(f"PASS: received expected data from {addr}:{rport}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"FAIL: expected hello-gost in response, got {data!r}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user