3proxy/tests/cases/portmap.py
Vladimir Dubrovin 629a978c00
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
help: the features of the build listed, and the tests skip what they need
3proxy --help (and the usage message of a bad invocation) now lists
what the build was made with: TLS and its backend, RADIUS, PCRE, IPv6,
UNIX sockets, the built-in HTTP server, transparency, ODBC and the
optional proxy services. The test runner reads the list before the
first case and the cases state the features they need with
t.skip_unless(), so a build without a feature skips its cases instead
of failing them; a binary too old to report anything skips the cases
with needs and runs the rest.
2026-10-30 14:24:46 +03:00

126 lines
3.8 KiB
Python

"""The port mappers: tcppm forwards a TCP port, udppm a UDP one."""
import socket
import threading
import time
HALFCLOSE_SIZE = 20000000
def halfclose_origin(listener):
"""Answers each connection which asks for something with HALFCLOSE_SIZE
bytes, then waits for the client's end of the stream and closes. The
connections waiting for the service to start ask nothing."""
while True:
try:
c, _ = listener.accept()
except OSError:
return
try:
if c.recv(1000):
c.sendall(b"x" * HALFCLOSE_SIZE)
c.recv(10)
except OSError:
pass
c.close()
def run(t):
if not t.skip_unless("httpsrv"):
return
# --- tcppm ---------------------------------------------------------
origin = t.free_port()
mapped = t.free_port()
refused = t.free_port()
t.start("portmap_tcp", f"""
log
auth iponly
allow *
http echo * /echo**
http data * /data
httpsrv -p{origin}
flush
auth iponly
allow *
tcppm {mapped} 127.0.0.1 {origin}
flush
auth iponly
deny *
tcppm {refused} 127.0.0.1 {origin}
""", ports=[origin, mapped, refused])
r = t.http(f"http://127.0.0.1:{mapped}/echo")
t.eq(200, r.status, "a mapped TCP port reaches the target")
t.contains(r, "path=/echo", "the target sees the request")
t.contains(r, "peer.addr=127.0.0.1", "the mapper makes the connection")
t.eq(20000, t.http(f"http://127.0.0.1:{mapped}/data?size=20000").length,
"a body passes through the mapper")
# the mapper is a service like any other, so its rules apply
r = t.http(f"http://127.0.0.1:{refused}/echo")
t.ne(200, r.status, "a mapper whose rules deny the client answers nothing")
t.stop_all()
# A client which shuts down its sending side still gets the whole answer,
# even when it reads slowly: the server has finished and closed by then,
# and the rest of the answer waits in the proxy's buffer and the kernel's.
listener = socket.socket()
listener.bind(("127.0.0.1", 0))
listener.listen(1)
threading.Thread(target=halfclose_origin, args=(listener,), daemon=True).start()
halfclosed = t.free_port()
t.start("portmap_halfclose", f"""
log
auth none
tcppm {halfclosed} 127.0.0.1 {listener.getsockname()[1]}
""", ports=[halfclosed])
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 32768)
s.settimeout(30)
got = 0
try:
s.connect(("127.0.0.1", halfclosed))
s.sendall(b"request")
s.shutdown(socket.SHUT_WR)
time.sleep(1)
while True:
d = s.recv(65536)
if not d:
break
got += len(d)
except OSError:
pass
s.close()
listener.close()
t.eq(HALFCLOSE_SIZE, got, "a half-closed client reading slowly gets the whole answer")
t.stop_all()
# --- udppm ---------------------------------------------------------
# something has to be listening for the mapped datagrams to go anywhere
echo = t.udp_echo()
mapped = t.free_port()
t.start("portmap_udp", f"""
log
flush
auth iponly
allow *
udppm {mapped} 127.0.0.1 {echo}
""")
# a UDP service has no listening socket to wait for, so ask until it
# answers rather than racing it
t.wait_udp(mapped)
t.eq(b"echo:hello", t.udp_exchange(mapped, b"hello"),
"a datagram is relayed and the reply comes back")
t.eq(b"echo:second", t.udp_exchange(mapped, b"second"),
"a second datagram uses the mapping again")
big = b"x" * 2000
t.eq(b"echo:" + big, t.udp_exchange(mapped, big),
"a larger datagram survives the round trip")