95 lines
3.8 KiB
Python
95 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Real Tor SOCKS request from a disposable peer network namespace.
|
|
|
|
Run: sudo -n unshare --net python3 apps/lnd-tor/test_namespace.py
|
|
No host routes, service commands, wallet mounts or external network access.
|
|
"""
|
|
import ctypes
|
|
import os
|
|
from pathlib import Path
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
|
|
|
|
def ip(*args):
|
|
subprocess.run(["ip", *args], check=True)
|
|
|
|
|
|
def main():
|
|
# Refuse to operate in the host network. Also supports the locked PID1
|
|
# fixture wrapper used by the combined backend verification run.
|
|
assert os.getpid() == 1 or os.stat("/proc/self/ns/net").st_ino != os.stat("/proc/1/ns/net").st_ino, "requires disposable network namespace"
|
|
ip("link", "set", "lo", "up")
|
|
ready, child_ready = socket.socketpair()
|
|
pid = os.fork()
|
|
if pid == 0:
|
|
ready.close()
|
|
try:
|
|
assert ctypes.CDLL(None, use_errno=True).unshare(0x40000000) == 0
|
|
child_ready.sendall(b"N")
|
|
assert child_ready.recv(1) == b"G"
|
|
ip("link", "set", "lo", "up")
|
|
ip("addr", "add", "10.89.0.2/24", "dev", "requester")
|
|
ip("addr", "add", "198.51.100.2/24", "dev", "requester")
|
|
ip("link", "set", "requester", "up")
|
|
# Host loopback cannot serve this request namespace.
|
|
try:
|
|
socket.create_connection(("127.0.0.1", 9050), 1)
|
|
raise AssertionError("unexpected requester-loopback SOCKS")
|
|
except ConnectionRefusedError:
|
|
pass
|
|
with socket.create_connection(("10.89.0.1", 9050), 3) as client:
|
|
client.sendall(b"\x05\x01\x00")
|
|
assert client.recv(2) == b"\x05\x00", "SOCKS5 negotiation failed"
|
|
# A non-private source on the same disposable link is denied.
|
|
with socket.create_connection(("198.51.100.1", 9050), 3) as client:
|
|
try:
|
|
client.sendall(b"\x05\x01\x00")
|
|
assert client.recv(2) != b"\x05\x00", "public source accepted"
|
|
except ConnectionResetError:
|
|
pass
|
|
child_ready.sendall(b"P")
|
|
os._exit(0)
|
|
except BaseException:
|
|
import traceback
|
|
traceback.print_exc()
|
|
os._exit(1)
|
|
child_ready.close()
|
|
tor = None
|
|
try:
|
|
assert ready.recv(1) == b"N"
|
|
ip("link", "add", "transport", "type", "veth", "peer", "name", "requester")
|
|
ip("link", "set", "requester", "netns", str(pid))
|
|
ip("addr", "add", "10.89.0.1/24", "dev", "transport")
|
|
ip("addr", "add", "198.51.100.1/24", "dev", "transport")
|
|
ip("link", "set", "transport", "up")
|
|
with tempfile.TemporaryDirectory(prefix="lnd-tor-fixture-") as tmp:
|
|
with open(Path(tmp) / "tor.log", "w+") as log:
|
|
tor = subprocess.Popen(["tor", "-f", str(Path(__file__).with_name("torrc")),
|
|
"--DataDirectory", tmp], stdout=log, stderr=log)
|
|
for _ in range(100):
|
|
try:
|
|
with socket.create_connection(("127.0.0.1", 9050), .1):
|
|
break
|
|
except OSError:
|
|
assert tor.poll() is None, "disposable Tor exited"
|
|
time.sleep(.05)
|
|
else:
|
|
raise AssertionError("Tor SOCKS listener never became ready")
|
|
ready.sendall(b"G")
|
|
ready.settimeout(10)
|
|
assert ready.recv(1) == b"P"
|
|
assert os.waitpid(pid, 0)[1] == 0
|
|
print("PASS: real Tor SOCKS5 reachable across disposable request namespace; requester loopback refused; non-private source denied", flush=True)
|
|
finally:
|
|
if tor is not None:
|
|
tor.terminate()
|
|
tor.wait(timeout=10)
|
|
ready.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|