Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/proxies.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ under its own device name (`cashpilot-box-p3` for proxy 3).

| Type | What you give it | Notes |
|---|---|---|
| SOCKS5 | host, port, optional user and password | Carries UDP only if the provider supports SOCKS5 UDP ASSOCIATE. |
| SOCKS5 | host, port, optional user and password | TCP only: the tunnel lets out only TCP to the proxy's port, so SOCKS5 UDP relays are dropped. Name lookups still work (see [DNS](#dns)). |
| HTTP CONNECT | host, port, optional user and password | TCP only. Name lookups still work (see [DNS](#dns)). |
| Tailscale exit node | an auth key and the exit node's name or 100.x address | Traffic leaves from the exit node's connection. See [Tailscale exit nodes](#tailscale-exit-nodes). |
| Tailscale exit node | an auth key and the exit node's name or 100.x address | Traffic leaves from the exit node's connection, UDP included. See [Tailscale exit nodes](#tailscale-exit-nodes). |

Any provider works. Nothing in CashPilot is specific to one.

Expand Down Expand Up @@ -168,8 +168,9 @@ A service is refused when its catalog entry needs the host:
- terms that forbid proxies or VPNs: `requirements.proxy_prohibited` in the
catalog, with a link to the provider's own terms.

Services that need UDP do not work through HTTP CONNECT, and through SOCKS5
only when the provider supports UDP ASSOCIATE.
Services that need UDP do not work through SOCKS5 or HTTP CONNECT: the
tunnel lets out only TCP to the proxy's address and port. A Tailscale exit
node carries UDP.

## Exit IPs and grouping

Expand Down
44 changes: 44 additions & 0 deletions tests/test_docs_proxy_udp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""docs/proxies.md must not promise UDP through a SOCKS5 or HTTP proxy.

tunnel/cp-tunnel lets a socks5 or http tunnel out only over TCP to the
proxy's own address and port. A SOCKS5 UDP ASSOCIATE relay sends UDP to the
proxy, so the firewall drops it. The page used to say SOCKS5 carries UDP when
the provider supports UDP ASSOCIATE.
"""

from __future__ import annotations

import re
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
TUNNEL = ROOT / "tunnel" / "cp-tunnel"
PAGE = ROOT / "docs" / "proxies.md"

UDP_PROMISE = re.compile(r"(carries|works?|through) [^.|]*udp associate", re.IGNORECASE)


def _socks_egress_rule() -> str:
branch = TUNNEL.read_text(encoding="utf-8").split("socks5 | http)", 1)[1].split("tailscale)", 1)[0]
rules = [line.strip() for line in branch.splitlines() if line.strip().startswith("EGRESS_RULE=")]
assert len(rules) == 1, "expected one EGRESS_RULE in the socks5 | http branch of cp-tunnel"
return rules[0]


def promises_udp(text: str) -> bool:
return bool(UDP_PROMISE.search(re.sub(r"\s+", " ", text)))


def test_the_socks_tunnel_lets_out_only_tcp():
rule = _socks_egress_rule()
assert "tcp dport $PROXY_PORT" in rule
assert "udp" not in rule, "cp-tunnel now lets UDP out; this test's premise no longer holds"


def test_the_page_does_not_promise_udp_through_socks5():
assert not promises_udp(PAGE.read_text(encoding="utf-8"))


def test_the_check_catches_the_old_claims():
assert promises_udp("| SOCKS5 | host | Carries UDP only if the provider supports SOCKS5 UDP ASSOCIATE. |")
assert promises_udp("and through SOCKS5\nonly when the provider supports UDP ASSOCIATE.")
Loading