mirror of
https://github.com/jpillora/chisel.git
synced 2026-10-12 02:16:57 +00:00
feat(socks): add UDP ASSOCIATE with fixed-port udpsocks remotes (#624)
Co-authored-by: Codex GPT-6 <[email protected]>
This commit is contained in:
co-authored by
Codex GPT-6
parent
3c00f04ca1
commit
a5796f95d2
@@ -263,15 +263,18 @@ $ chisel client --help
|
||||
192.168.0.5:3000:google.com:80
|
||||
socks
|
||||
5000:socks
|
||||
udpsocks
|
||||
5000:udpsocks
|
||||
R:2222:localhost:22
|
||||
R:socks
|
||||
R:5000:socks
|
||||
R:5000:udpsocks
|
||||
stdio:example.com:22
|
||||
1.1.1.1:53/udp
|
||||
|
||||
When the chisel server has --socks5 enabled, remotes can
|
||||
specify "socks" in place of remote-host and remote-port.
|
||||
The default local host and port for a "socks" remote is
|
||||
specify "socks" or "udpsocks" in place of remote-host and
|
||||
remote-port. Both default to the local host and port
|
||||
127.0.0.1:1080. Connections to this remote will terminate
|
||||
at the server's internal SOCKS5 proxy. When the server also
|
||||
has --authfile set, SOCKS5 access requires an entry matching
|
||||
@@ -285,6 +288,17 @@ $ chisel client --help
|
||||
default socks port (1080) and terminate the connection at the
|
||||
client's internal SOCKS5 proxy.
|
||||
|
||||
The "socks" remote supports TCP CONNECT only. Use "udpsocks"
|
||||
for TCP CONNECT and UDP ASSOCIATE, including "R:udpsocks".
|
||||
It binds TCP and UDP on the same configured interface and port;
|
||||
the UDP relay never uses a randomly allocated listening port.
|
||||
UDP requires support on both Chisel peers. Destination names are
|
||||
resolved at the exit node. If UDP cannot bind, TCP remains available
|
||||
and UDP ASSOCIATE fails; no alternate port is used. Clients using
|
||||
a zero UDP source port allow one pending association per source IP
|
||||
until the first valid datagram pins it. BIND, UDP fragmentation,
|
||||
and udpsocks over stdio are not supported.
|
||||
|
||||
When stdio is used as local-host, the tunnel will connect standard
|
||||
input/output of this program with the remote. This is useful when
|
||||
combined with ssh ProxyCommand. You can use
|
||||
@@ -452,6 +466,14 @@ For mutual TLS, also pass `--tls-ca` to the server and `--tls-cert`/`--tls-key`
|
||||
|
||||
Note: if the server also uses `--authfile`, users need an entry matching the token `socks` to use the proxy (see [Authentication](#authentication)).
|
||||
|
||||
For setup commands, reverse mode, and a UDP client example, see the [SOCKS5 UDP user guide](example/udpsocks.md).
|
||||
|
||||
Ordinary `socks` remotes support TCP CONNECT only and bind no UDP socket. To enable both TCP CONNECT and UDP ASSOCIATE, use `udpsocks` (or `R:udpsocks` for reverse mode), for example `1080:udpsocks`. Both types default to `127.0.0.1:1080` and use the same SOCKS access permissions. A `udpsocks` listener binds TCP and UDP on the same configured interface and port, sharing one UDP relay socket across associations without allocating random listening ports. UDP clients keep their TCP control connection open and send SOCKS-encapsulated datagrams to that port. Datagram destination names are resolved at the exit node, and the UDP traffic travels through the encrypted SSH tunnel. Upgrade both Chisel peers for UDP; TCP SOCKS remains compatible with older peers.
|
||||
|
||||
When exposing a `udpsocks` listener through a firewall or container, allow both TCP and UDP on its configured port. UDP ASSOCIATE returns the local IP accepting the TCP connection, which must be reachable by the SOCKS client. For Linux Docker listeners, use `--network host` with the remote `0.0.0.0:1080:udpsocks` to avoid advertising an isolated container address. Container port mappings need both `-p 1080:1080/tcp` and `-p 1080:1080/udp`, plus reachability to the advertised relay IP. If the UDP port is occupied, Chisel logs that UDP is unavailable, retains TCP service, and rejects UDP ASSOCIATE without using an alternate port; restart the proxy after freeing the port.
|
||||
|
||||
An association accepts packets from its TCP peer's IP. A declared UDP source port reserves that endpoint; a zero source port is pinned by its first valid datagram. Only one unpinned association per IP is allowed, so clients sharing an IP should declare their UDP source ports or send the first datagram before opening another association. Duplicate source-port reservations are rejected. Closing the control connection or losing SSH ends the association; reconnecting requires a new UDP ASSOCIATE. BIND, UDP fragmentation, and `stdio:udpsocks` are unsupported.
|
||||
|
||||
### Reverse SOCKS with an Authfile
|
||||
|
||||
To let a specific client act as a SOCKS exit node, grant it the reverse-socks listener address (`R:socks` listens on the server's `127.0.0.1:1080`):
|
||||
@@ -469,6 +491,8 @@ chisel client --auth exituser:password <server-address> R:socks
|
||||
# and their traffic exits via the chisel client's network
|
||||
```
|
||||
|
||||
Use `R:udpsocks` in the client command to also enable UDP ASSOCIATE on the same server port; the authfile grant stays the same.
|
||||
|
||||
See also the step-by-step [reverse tunneling example](example/reverse-tunneling-authenticated.md).
|
||||
|
||||
### Running behind a CDN (Cloudflare)
|
||||
@@ -486,16 +510,18 @@ Less common knobs are environment variables, all read with a `CHISEL_` prefix (e
|
||||
| `CONFIG_TIMEOUT`| server | `10s` | wait for the client's config request |
|
||||
| `SSH_WAIT` | both | `35s` | how long new tunnels wait for an active connection |
|
||||
| `PING_TIMEOUT` | both | keepalive interval | keepalive ping reply timeout (no pings if `--keepalive 0`) |
|
||||
| `DIAL_TIMEOUT` | exit node | `30s` | tcp dial timeout for tunnel targets |
|
||||
| `DIAL_TIMEOUT` | both | `30s` | tcp target dial, SOCKS handshake, and SOCKS UDP DNS timeout |
|
||||
| `WS_READ_LIMIT` | both | `524288` | max inbound websocket message bytes (0 = no limit; negative = default) |
|
||||
| `WS_BUFF_SIZE` | both | go default | websocket read/write buffer sizes |
|
||||
| `UDP_MAX_SIZE` | both | `9012` | max udp packet bytes |
|
||||
| `UDP_DEADLINE` | both | `15s` | udp flow read deadline and return-peer expiry |
|
||||
| `UDP_MAX_CONNS` | both | `100` | max udp flows or return peers per tunnel |
|
||||
| `UDP_MAX_SIZE` | both | `9012` | max udp packet bytes (includes the SOCKS UDP header) |
|
||||
| `UDP_DEADLINE` | both | `15s` | udp flow read deadline and return-peer expiry; SOCKS UDP destination idle timeout |
|
||||
| `UDP_MAX_CONNS` | both | `100` | max udp flows or return peers; SOCKS UDP associations per listener and destinations per association |
|
||||
| `SHUTDOWN_GRACE`| server | `5s` | http request drain time on shutdown |
|
||||
|
||||
`HOST`, `PORT`, `AUTH`, and `CHISEL_KEY`/`CHISEL_KEY_FILE` are documented in the `--help` texts above.
|
||||
|
||||
SOCKS UDP uses hard association and destination limits and drops packets beyond them. Each association queues up to 16 outbound datagrams; full queues drop packets without blocking other clients. For SOCKS UDP, nonpositive size/connection limits or idle deadlines use the defaults, and packet size is capped at 65507 bytes. Operating systems can impose a smaller UDP send limit: macOS defaults to 9216 bytes (`net.inet.udp.maxdgram`). The default 9012-byte Chisel limit fits within that bound.
|
||||
|
||||
#### Caveats
|
||||
|
||||
Since WebSockets support is required:
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# udpsocks performance and limit tests
|
||||
|
||||
Measured on 2026-10-11 against the UDP reply-source fix in the working tree (base commit `e7a9879`). Each association now uses unconnected outgoing sockets shared across destinations, with at most one socket per address family.
|
||||
|
||||
## Results
|
||||
|
||||
With 1,200-byte payloads and sixteen concurrent clients, forward UDP delivered **17.17 MB/s (137.4 Mbps)** and reverse UDP delivered **16.88 MB/s (135.0 Mbps)**. Both measured zero loss. A single client had median round-trip latency of approximately **176–192 microseconds**.
|
||||
|
||||
At 8,192 bytes with sixteen clients, delivered throughput reached **40.74 MB/s forward and 43.29 MB/s reverse**, with **0.43–0.50% loss**. Direct UDP also lost packets in this case (**0.059%**). The benchmark records timeouts as lost packets.
|
||||
|
||||
The limit tests passed at the configured boundaries, including the default 100-association and 100-destination limits. Overload testing confirmed that unrelated clients still respond and associations recover after the flood.
|
||||
|
||||
## Method
|
||||
|
||||
- Linux/ARM64, four logical CPUs, Go 1.27.0, `GOMAXPROCS=4`.
|
||||
- Direct UDP, forward `udpsocks`, and reverse `R:udpsocks`, all over loopback. Both Chisel peers, the independent `txthinking/socks5` client, and the echo endpoint run in the benchmark process.
|
||||
- Three runs per combination, one-second benchmark target per run; the table shows the median of each metric across those runs.
|
||||
- Payloads of 1,200 and 8,192 bytes. One or sixteen independent clients, each with one request outstanding. Associations and destinations are warmed before timing.
|
||||
- Default Chisel limits and socket buffers. This host reports default Linux receive/send buffers of 212,992 bytes; these were not increased for the measurements.
|
||||
- A request counts as lost if its matching response does not arrive within 100 ms. Sequence numbers prevent late responses being counted against a later request.
|
||||
- RTT percentiles sample successful responses; lost requests are excluded from those percentiles. Throughput includes the time spent waiting for losses.
|
||||
- MB/s counts delivered application payload once per echo. It excludes SOCKS/SSH/WebSocket overhead and does not double-count the return direction.
|
||||
- Allocation figures include both Chisel peers, the independent client, and the endpoint. Race instrumentation was disabled for performance measurements.
|
||||
|
||||
These are local measurements of this host and workload. A WAN adds network latency, transport congestion, and TCP head-of-line blocking; these measurements do not establish maximum WAN throughput.
|
||||
|
||||
## Echo benchmark results
|
||||
|
||||
| Path | Payload | Clients | Payload MB/s | Echoes/s | RTT p50 (us) | RTT p95 (us) | Loss (%) | B/attempt | Allocs/attempt |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| direct | 1200 | 1 | 88.87 | 74056 | 12.6 | 14.6 | 0.0000 | 52 | 2 |
|
||||
| direct | 1200 | 16 | 137.76 | 114797 | 112.8 | 304.8 | 0.0000 | 52 | 2 |
|
||||
| direct | 8192 | 1 | 499.26 | 60945 | 15.5 | 19.1 | 0.0000 | 52 | 2 |
|
||||
| direct | 8192 | 16 | 712.51 | 86976 | 82.4 | 205.7 | 0.0589 | 52 | 1 |
|
||||
| forward | 1200 | 1 | 6.27 | 5225 | 175.9 | 319.6 | 0.0000 | 16827 | 65 |
|
||||
| forward | 1200 | 16 | 17.17 | 14310 | 924.6 | 2464.0 | 0.0000 | 16839 | 65 |
|
||||
| forward | 8192 | 1 | 21.32 | 2602 | 327.4 | 729.8 | 0.0000 | 111496 | 99 |
|
||||
| forward | 8192 | 16 | 40.74 | 4973 | 2195.0 | 5087.0 | 0.4958 | 111139 | 99 |
|
||||
| reverse | 1200 | 1 | 6.15 | 5123 | 191.7 | 309.1 | 0.0000 | 16827 | 65 |
|
||||
| reverse | 1200 | 16 | 16.88 | 14069 | 920.4 | 2458.0 | 0.0000 | 16840 | 65 |
|
||||
| reverse | 8192 | 1 | 22.36 | 2729 | 325.2 | 648.0 | 0.0000 | 111495 | 99 |
|
||||
| reverse | 8192 | 16 | 43.29 | 5285 | 2083.0 | 4716.0 | 0.4348 | 111201 | 99 |
|
||||
|
||||
The 1,200-byte tunnel paths allocate about 16.8 KB and 65 allocations per attempt in this whole-process benchmark. At 8,192 bytes, that rises to about 111 KB and 99 allocations. Allocation reduction is a useful candidate for subsequent profiling and optimization.
|
||||
|
||||
## Unpaced overload
|
||||
|
||||
Each path sends 20,000 datagrams of 1,200 bytes as quickly as its client can write them, with a concurrent response reader. Offered rates differ between paths. This measures burst loss and recovery rather than a sustainable constant-rate workload.
|
||||
|
||||
| Path | Offered packets/s | Received / 20,000 | Loss (%) | Successful RTT p50 (us) | Successful RTT p95 (us) |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| direct | 165172 | 3455 | 82.72 | 1630.2 | 10664.1 |
|
||||
| forward | 88916 | 2184 | 89.08 | 9035.8 | 17683.1 |
|
||||
| reverse | 98778 | 2103 | 89.48 | 9294.8 | 26451.9 |
|
||||
|
||||
All three paths passed 20 probes from an unrelated client during the workload and a recovery echo on the flooded connection afterward. No duplicated or corrupted received packets were detected. UDP socket buffers and the relay queue can both drop packets; this test does not attribute each loss to a specific queue.
|
||||
|
||||
## Limit coverage
|
||||
|
||||
| Limit or behavior | Verified |
|
||||
| --- | --- |
|
||||
| Associations per listener | Configured limit 2 and default 100, forward and reverse. The next association fails; existing UDP and CONNECT continue. Closing a control frees capacity. |
|
||||
| Destinations per association | Configured limit 2 and default 100, forward and reverse. Over-limit requests never reach the new destination. Other associations have independent capacity. |
|
||||
| Destination idle expiration | A 500 ms configured timeout frees capacity for a new destination without replacing its TCP control connection. Expiration keeps outgoing sockets and their source ports intact. |
|
||||
| Changed reply endpoint | Two-block TFTP exchanges switch server ports and preserve the client transfer ID, forward/reverse over IPv4/IPv6. A different reply source IP is also covered when the OS permits binding an alternate loopback address; that variant skips when the address is unavailable. Two simultaneous associations use separate exit sockets. |
|
||||
| Datagram size | Exact 64-byte and 9,012-byte SOCKS packets succeed. One extra byte is dropped before reaching the endpoint. IPv6 header overhead is included. |
|
||||
| Upper packet-size bound | Setting 70,000 clamps to 65,507. An exact 65,507-byte SOCKS datagram succeeds on Linux. This boundary case skips if an independent native UDP send reports a smaller OS limit, as macOS does by default. The setting clamp remains covered by unit tests on all platforms. Oversized return packets are tested separately. |
|
||||
| Oversized replies | Responses whose SOCKS header plus payload exceeds the limit are dropped; subsequent valid traffic still works. |
|
||||
| Shared-reader overflow | A full 16-packet association queue drops overflow and does not block another association. Covered deterministically by `TestSocksUDPSharedRoutingAndOverflow`. |
|
||||
| Invalid settings | Zero, negative, and malformed size/connection/deadline settings fall back to defaults; oversized size settings clamp. |
|
||||
| Existing protocol limits | Malformed/unassociated packets and fragmentation are rejected; source-port conflicts and the one-pending-association-per-IP rule remain covered. |
|
||||
|
||||
The association and destination limits are per listener and per association respectively; they are not a process-wide socket or memory limit. Each association retains at most two outgoing sockets (IPv4 and IPv6) until it closes. Destination expiry releases tracking capacity; incoming replies do not create new tracking entries.
|
||||
|
||||
Local Linux/ARM64 validation passed: `CHISEL_UDP_STRESS=1 GOMAXPROCS=4 go test -race ./...`, `go vet ./...`, and `git diff --check`.
|
||||
|
||||
## Reproduce
|
||||
|
||||
Run the benchmark without race instrumentation:
|
||||
|
||||
```sh
|
||||
GOMAXPROCS=4 go test ./test/e2e -run "^$" -bench '^BenchmarkSocksUDP/(direct|forward|reverse)/bytes=(1200|8192)/clients=(1|16)$' \
|
||||
-benchmem -benchtime=1s -count=3
|
||||
```
|
||||
|
||||
Run the boundary and recovery tests:
|
||||
|
||||
```sh
|
||||
go test -race ./test/e2e -run '^TestSocksUDP(AssociationLimit|DestinationLimit|PacketSizeLimit|TransferPort|SourcePortSurvivesIdle)$'
|
||||
go test -race ./share/tunnel -run '^TestSocksUDP(LimitSettings|SharedRoutingAndOverflow|ExitLimitsAndRecovery)$'
|
||||
```
|
||||
|
||||
Run the opt-in overload test:
|
||||
|
||||
```sh
|
||||
CHISEL_UDP_STRESS=1 GOMAXPROCS=4 go test -v ./test/e2e -run "^TestSocksUDPOverload$"
|
||||
```
|
||||
|
||||
Ordinary `go test ./...` runs the limit tests and skips the overload workload. Set `CHISEL_UDP_STRESS=1` to include it. Performance numbers above come from runs without the race detector; run the full suite with `-race` to check concurrency correctness.
|
||||
@@ -0,0 +1,177 @@
|
||||
# Using SOCKS5 UDP with Chisel
|
||||
|
||||
Use `udpsocks` to carry both TCP and UDP traffic through a Chisel SOCKS5 proxy. It supports TCP CONNECT and UDP ASSOCIATE, including reverse tunnels.
|
||||
|
||||
Both Chisel peers need builds that include `udpsocks`. To build this working tree, run `go build -o chisel .` and use the resulting binary on each machine (build separately for different operating systems or architectures).
|
||||
|
||||
## Choose your listener
|
||||
|
||||
| Remote | Listener location | Listening sockets | Supported commands |
|
||||
| --- | --- | --- | --- |
|
||||
| `1080:socks` | Chisel client | TCP 1080 | CONNECT |
|
||||
| `1080:udpsocks` | Chisel client | TCP 1080 and UDP 1080 | CONNECT and UDP ASSOCIATE |
|
||||
| `R:1080:udpsocks` | Chisel server | TCP 1080 and UDP 1080 | CONNECT and UDP ASSOCIATE |
|
||||
|
||||
Bare `udpsocks` defaults to `127.0.0.1:1080`. Use `5000:udpsocks` to change the port, or `192.168.1.10:5000:udpsocks` to choose an interface. IPv6 works too: quote the remote, for example `'[::1]:1080:udpsocks'`.
|
||||
|
||||
TCP and UDP always use the same configured interface and port number. One UDP relay socket serves all associations: Chisel does not allocate a random relay port for each client. Port zero is rejected. This applies to the exposed relay; normal outgoing UDP sockets and application source ports can still use operating-system-assigned ports.
|
||||
|
||||
## Forward proxy: traffic exits through the server
|
||||
|
||||
The path is:
|
||||
|
||||
```text
|
||||
SOCKS application → client TCP/UDP :1080 → Chisel tunnel → server → destination
|
||||
```
|
||||
|
||||
On the server, create `users.json`. Replace `CHANGE_ME` with your own password:
|
||||
|
||||
```json
|
||||
{
|
||||
"proxyuser:CHANGE_ME": ["^socks$"]
|
||||
}
|
||||
```
|
||||
|
||||
Generate a persistent server key once, then start the server:
|
||||
|
||||
```sh
|
||||
chisel server --keygen chisel-key.pem
|
||||
chisel server --keyfile chisel-key.pem --port 9312 --socks5 --authfile users.json
|
||||
```
|
||||
|
||||
Copy the fingerprint printed by the server. On the client machine, replace `SERVER_FINGERPRINT`, `server.example.com`, and the password:
|
||||
|
||||
```sh
|
||||
chisel client --fingerprint 'SERVER_FINGERPRINT' --auth 'proxyuser:CHANGE_ME' \
|
||||
http://server.example.com:9312 1080:udpsocks
|
||||
```
|
||||
|
||||
Point the SOCKS application at **127.0.0.1:1080 on the client machine**. For example, this checks TCP CONNECT:
|
||||
|
||||
```sh
|
||||
curl --socks5-hostname 127.0.0.1:1080 https://example.com
|
||||
```
|
||||
|
||||
The SOCKS application must support SOCKS5 UDP ASSOCIATE to send UDP. Configuring a browser or an HTTP-only SOCKS client does not automatically tunnel its UDP traffic. Chisel tunnel credentials authenticate the Chisel client; the local SOCKS listener uses no SOCKS username/password authentication.
|
||||
|
||||
## Reverse proxy: traffic exits through the client
|
||||
|
||||
Use this when applications on the server should access destinations reachable from the client's network:
|
||||
|
||||
```text
|
||||
SOCKS application → server TCP/UDP :1080 → Chisel tunnel → client → destination
|
||||
```
|
||||
|
||||
On the server, create `reverse-users.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"exituser:CHANGE_ME": ["^R:127\\.0\\.0\\.1:1080$"]
|
||||
}
|
||||
```
|
||||
|
||||
Start the server using the persistent key generated above:
|
||||
|
||||
```sh
|
||||
chisel server --keyfile chisel-key.pem --port 9312 --reverse \
|
||||
--authfile reverse-users.json
|
||||
```
|
||||
|
||||
Connect from the machine that should act as the exit:
|
||||
|
||||
```sh
|
||||
chisel client --fingerprint 'SERVER_FINGERPRINT' --auth 'exituser:CHANGE_ME' \
|
||||
http://server.example.com:9312 R:1080:udpsocks
|
||||
```
|
||||
|
||||
Point SOCKS applications at **127.0.0.1:1080 on the server machine**. Reverse mode needs `--reverse` on the server; it does not require `--socks5` there. The client supplies the SOCKS exit.
|
||||
|
||||
For forward mode, both `socks` and `udpsocks` use the authfile permission `socks`. For reverse mode, permission matches the server listener address, such as `R:127.0.0.1:1080`. Changing the listener interface or port also requires updating that reverse permission.
|
||||
|
||||
## Try UDP with an independent client
|
||||
|
||||
This example uses [PySocks](https://pypi.org/project/PySocks/), which performs the SOCKS negotiation and UDP encapsulation itself.
|
||||
|
||||
First, run a UDP echo endpoint on the **exit machine**: the server for forward mode, or the client for reverse mode. Leave it running:
|
||||
|
||||
```sh
|
||||
python3 - <<'PY'
|
||||
import socket
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as echo:
|
||||
echo.bind(('127.0.0.1', 9999))
|
||||
print('UDP echo listening on 127.0.0.1:9999', flush=True)
|
||||
while True:
|
||||
data, peer = echo.recvfrom(65535)
|
||||
echo.sendto(data, peer)
|
||||
PY
|
||||
```
|
||||
|
||||
On the **SOCKS listener machine**, install PySocks in a virtual environment:
|
||||
|
||||
This needs Python's virtual environment support (`python3-venv` on Debian/Ubuntu). On Windows, use `.venv\Scripts\python.exe` instead of `.venv/bin/python`.
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
.venv/bin/python -m pip install PySocks==1.7.1
|
||||
```
|
||||
|
||||
Then send a datagram through the proxy:
|
||||
|
||||
```sh
|
||||
.venv/bin/python - <<'PY'
|
||||
import socket
|
||||
import socks
|
||||
|
||||
with socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM) as client:
|
||||
client.set_proxy(socks.SOCKS5, '127.0.0.1', 1080)
|
||||
client.settimeout(5)
|
||||
client.bind(('127.0.0.1', 0))
|
||||
payload = b'hello through udpsocks'
|
||||
client.sendto(payload, ('127.0.0.1', 9999))
|
||||
response, source = client.recvfrom(65535)
|
||||
assert response == payload, (response, source)
|
||||
print('UDP round trip succeeded:', response.decode())
|
||||
PY
|
||||
```
|
||||
|
||||
The destination `127.0.0.1:9999` is reached from the exit machine. The application's source port may be allocated dynamically; the Chisel relay remains on port 1080.
|
||||
|
||||
The repository also includes interoperability tests using the independent Go client [txthinking/socks5](https://github.com/txthinking/socks5):
|
||||
|
||||
```sh
|
||||
go test -v ./test/e2e -run TestSocksUDPThirdPartyClient
|
||||
```
|
||||
|
||||
Those tests cover forward and reverse UDP, IPv4, IPv6, hostname destinations, multiple packet sizes, and the configured relay port.
|
||||
|
||||
For measured throughput, latency, overload behavior, and boundary tests, see the [performance and limit test report](udpsocks-performance.md). It includes commands to repeat the benchmarks and tests.
|
||||
|
||||
## Interfaces, firewalls, and Docker
|
||||
|
||||
Keep the SOCKS listener on loopback when applications run on the same machine. To accept applications on a trusted network, specify a reachable interface, for example `192.168.1.10:1080:udpsocks`. The listener has no SOCKS authentication, so control access to that address and port.
|
||||
|
||||
Allow **both TCP and UDP** to the configured SOCKS port when applications connect across a firewall. Between the two Chisel peers, allow the Chisel transport port (9312 in these examples); SOCKS UDP travels inside the tunnel, so no separate UDP tunnel port is required between peers.
|
||||
|
||||
UDP ASSOCIATE advertises the concrete local IP accepting the TCP control connection. The application must be able to reach that IP over UDP. Linux Docker listeners can use `--network host` and `0.0.0.0:1080:udpsocks` to avoid advertising an isolated container IP. With bridge networking, mapping both `1080:1080/tcp` and `1080:1080/udp` is necessary, but the advertised container IP must also be reachable; port mappings alone do not guarantee this.
|
||||
|
||||
## Troubleshooting and limits
|
||||
|
||||
| Symptom | Check |
|
||||
| --- | --- |
|
||||
| TCP works, UDP ASSOCIATE is rejected | Use `udpsocks`, upgrade both Chisel peers, and check logs for a UDP bind failure. Forward mode also requires server `--socks5`. |
|
||||
| UDP port is already occupied | Free the configured UDP port and restart the proxy. Chisel retains TCP service and rejects ASSOCIATE; it never switches to another relay port. |
|
||||
| Association succeeds but packets time out | Check the destination from the exit machine, UDP firewall rules, and reachability of the advertised relay IP. |
|
||||
| Another client sharing an IP cannot associate | Only one association with an unknown UDP source port may wait per source IP. Send the first client's first datagram before opening the next association, or use clients that declare distinct source ports. |
|
||||
| UDP stops after reconnecting | Keep the TCP control connection open. Losing that connection or the SSH tunnel ends the association; create a new UDP ASSOCIATE after reconnecting. |
|
||||
| Large or fragmented datagrams disappear | Fragmented SOCKS UDP packets are unsupported. The default packet limit is 9012 bytes, including the SOCKS UDP header. |
|
||||
|
||||
On the exposed relay, only client datagrams from the TCP peer's IP are accepted. A declared source port reserves that endpoint; otherwise, the first valid datagram pins the association to its source port. DNS names in UDP destination headers are resolved at the exit machine.
|
||||
|
||||
Each association uses at most one outgoing socket per address family, retaining its source port across destination changes and idle periods until the association closes. Replies may use a different source address or UDP port, as protocols such as TFTP require; the SOCKS reply header carries the actual source address and port. Incoming replies do not allocate additional destination tracking entries.
|
||||
|
||||
Defaults are 100 associations per listener, 100 destinations per association, and a 15-second idle timeout for destination tracking. Expired destinations free capacity and are resolved again on the next packet; the outgoing sockets remain available until the association closes. Each association queues up to 16 outbound datagrams and drops overflow. Tune these with `CHISEL_UDP_MAX_CONNS`, `CHISEL_UDP_DEADLINE`, and `CHISEL_UDP_MAX_SIZE` on the relevant peers.
|
||||
|
||||
Chisel caps SOCKS UDP packets at 65507 bytes, including the SOCKS header. The operating system can impose a smaller send limit: macOS defaults to 9216 bytes (`net.inet.udp.maxdgram`), which accommodates Chisel's 9012-byte default. Increasing `CHISEL_UDP_MAX_SIZE` does not increase the operating system's limit.
|
||||
|
||||
SOCKS BIND, UDP fragmentation, and `stdio:udpsocks` are unsupported. UDP is best-effort: queue and resource limits can drop packets, and reconnecting does not replay them.
|
||||
@@ -12,6 +12,7 @@ require (
|
||||
github.com/jpillora/opts v1.8.1
|
||||
github.com/jpillora/requestlog v1.0.0
|
||||
github.com/jpillora/sizestr v1.0.0
|
||||
github.com/txthinking/socks5 v0.0.0-20260601051520-339b044ab0eb
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
@@ -22,8 +23,10 @@ require (
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jpillora/ansi v1.0.3 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/posener/complete v1.2.3 // indirect
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
)
|
||||
|
||||
@@ -24,6 +24,10 @@ github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+Pp
|
||||
github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8=
|
||||
github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=
|
||||
github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0=
|
||||
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
|
||||
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||
@@ -33,18 +37,56 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=
|
||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
|
||||
github.com/txthinking/socks5 v0.0.0-20260601051520-339b044ab0eb h1:cFSmrCbLJPb87QrwDhUTnguVLezlIqDiJuY8xKcFZnw=
|
||||
github.com/txthinking/socks5 v0.0.0-20260601051520-339b044ab0eb/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -265,15 +265,18 @@ from the client to the server's <local-interface>:<local-port>.
|
||||
192.168.0.5:3000:google.com:80
|
||||
socks
|
||||
5000:socks
|
||||
udpsocks
|
||||
5000:udpsocks
|
||||
R:2222:localhost:22
|
||||
R:socks
|
||||
R:5000:socks
|
||||
R:5000:udpsocks
|
||||
stdio:example.com:22
|
||||
1.1.1.1:53/udp
|
||||
|
||||
When the chisel server has --socks5 enabled, remotes can
|
||||
specify "socks" in place of remote-host and remote-port.
|
||||
The default local host and port for a "socks" remote is
|
||||
specify "socks" or "udpsocks" in place of remote-host and
|
||||
remote-port. Both default to the local host and port
|
||||
127.0.0.1:1080. Connections to this remote will terminate
|
||||
at the server's internal SOCKS5 proxy. When the server also
|
||||
has --authfile set, SOCKS5 access requires an entry matching
|
||||
@@ -287,6 +290,17 @@ from the client to the server's <local-interface>:<local-port>.
|
||||
default socks port (1080) and terminate the connection at the
|
||||
client's internal SOCKS5 proxy.
|
||||
|
||||
The "socks" remote supports TCP CONNECT only. Use "udpsocks"
|
||||
for TCP CONNECT and UDP ASSOCIATE, including "R:udpsocks".
|
||||
It binds TCP and UDP on the same configured interface and port;
|
||||
the UDP relay never uses a randomly allocated listening port.
|
||||
UDP requires support on both Chisel peers. Destination names are
|
||||
resolved at the exit node. If UDP cannot bind, TCP remains available
|
||||
and UDP ASSOCIATE fails; no alternate port is used. Clients using
|
||||
a zero UDP source port allow one pending association per source IP
|
||||
until the first valid datagram pins it. BIND, UDP fragmentation,
|
||||
and udpsocks over stdio are not supported.
|
||||
|
||||
When stdio is used as local-host, the tunnel will connect standard
|
||||
input/output of this program with the remote. This is useful when
|
||||
combined with ssh ProxyCommand. You can use
|
||||
|
||||
+34
-13
@@ -25,6 +25,9 @@ import (
|
||||
// 127.0.0.1:1080:socks
|
||||
// local 127.0.0.1:1080
|
||||
// remote socks
|
||||
// 127.0.0.1:1080:udpsocks
|
||||
// local 127.0.0.1:1080 (TCP and UDP)
|
||||
// remote udpsocks
|
||||
// stdio:example.com:22
|
||||
// local stdio
|
||||
// remote example.com:22
|
||||
@@ -35,7 +38,7 @@ import (
|
||||
type Remote struct {
|
||||
LocalHost, LocalPort, LocalProto string
|
||||
RemoteHost, RemotePort, RemoteProto string
|
||||
Socks, Reverse, Stdio bool
|
||||
Socks, UDPSocks, Reverse, Stdio bool
|
||||
}
|
||||
|
||||
const revPrefix = "R:"
|
||||
@@ -57,8 +60,9 @@ func DecodeRemote(s string) (*Remote, error) {
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
p := parts[i][1]
|
||||
//remote portion is socks?
|
||||
if i == len(parts)-1 && p == "socks" {
|
||||
if i == len(parts)-1 && (p == "socks" || p == "udpsocks") {
|
||||
r.Socks = true
|
||||
r.UDPSocks = p == "udpsocks"
|
||||
continue
|
||||
}
|
||||
//local portion is stdio?
|
||||
@@ -75,12 +79,23 @@ func DecodeRemote(s string) (*Remote, error) {
|
||||
}
|
||||
}
|
||||
if isPort(p) {
|
||||
if r.UDPSocks && r.LocalPort != "" {
|
||||
return nil, errors.New("udpsocks accepts one shared TCP and UDP port")
|
||||
}
|
||||
if !r.Socks && r.RemotePort == "" {
|
||||
r.RemotePort = p
|
||||
}
|
||||
r.LocalPort = p
|
||||
continue
|
||||
}
|
||||
if r.UDPSocks {
|
||||
if _, err := strconv.Atoi(p); err == nil {
|
||||
return nil, errors.New("udpsocks port must be between 1 and 65535")
|
||||
}
|
||||
if r.LocalHost != "" {
|
||||
return nil, errors.New("udpsocks accepts one local interface")
|
||||
}
|
||||
}
|
||||
if !r.Socks && (r.RemotePort == "" && r.LocalPort == "") {
|
||||
return nil, errors.New("Missing ports")
|
||||
}
|
||||
@@ -124,7 +139,10 @@ func DecodeRemote(s string) (*Remote, error) {
|
||||
return nil, errors.New("cross-protocol remotes are not supported yet")
|
||||
}
|
||||
if r.Socks && r.RemoteProto != "tcp" {
|
||||
return nil, errors.New("only TCP SOCKS is supported")
|
||||
return nil, errors.New("SOCKS remotes require TCP; use udpsocks for UDP ASSOCIATE")
|
||||
}
|
||||
if r.UDPSocks && r.Stdio {
|
||||
return nil, errors.New("udpsocks requires a TCP and UDP listener; stdio is unsupported")
|
||||
}
|
||||
if r.Stdio && r.Reverse {
|
||||
return nil, errors.New("stdio cannot be reversed")
|
||||
@@ -153,7 +171,7 @@ func isHost(s string) bool {
|
||||
|
||||
var l4Proto = regexp.MustCompile(`(?i)\/(tcp|udp)$`)
|
||||
|
||||
//L4Proto extracts the layer-4 protocol from the given string
|
||||
// L4Proto extracts the layer-4 protocol from the given string
|
||||
func L4Proto(s string) (head, proto string) {
|
||||
if l4Proto.MatchString(s) {
|
||||
l := len(s)
|
||||
@@ -164,7 +182,7 @@ func L4Proto(s string) (head, proto string) {
|
||||
return s, ""
|
||||
}
|
||||
|
||||
//implement Stringer
|
||||
// implement Stringer
|
||||
func (r Remote) String() string {
|
||||
sb := strings.Builder{}
|
||||
if r.Reverse {
|
||||
@@ -179,7 +197,7 @@ func (r Remote) String() string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
//Encode remote to a string
|
||||
// Encode remote to a string
|
||||
func (r Remote) Encode() string {
|
||||
if r.LocalPort == "" {
|
||||
r.LocalPort = r.RemotePort
|
||||
@@ -195,7 +213,7 @@ func (r Remote) Encode() string {
|
||||
return local + ":" + remote
|
||||
}
|
||||
|
||||
//Local is the decodable local portion
|
||||
// Local is the decodable local portion
|
||||
func (r Remote) Local() string {
|
||||
if r.Stdio {
|
||||
return "stdio"
|
||||
@@ -206,8 +224,11 @@ func (r Remote) Local() string {
|
||||
return r.LocalHost + ":" + r.LocalPort
|
||||
}
|
||||
|
||||
//Remote is the decodable remote portion
|
||||
// Remote is the decodable remote portion
|
||||
func (r Remote) Remote() string {
|
||||
if r.UDPSocks {
|
||||
return "udpsocks"
|
||||
}
|
||||
if r.Socks {
|
||||
return "socks"
|
||||
}
|
||||
@@ -217,8 +238,8 @@ func (r Remote) Remote() string {
|
||||
return r.RemoteHost + ":" + r.RemotePort
|
||||
}
|
||||
|
||||
//UserAddr is checked when checking if a
|
||||
//user has access to a given remote
|
||||
// UserAddr is checked when checking if a
|
||||
// user has access to a given remote
|
||||
func (r Remote) UserAddr() string {
|
||||
if r.Reverse {
|
||||
return "R:" + r.LocalHost + ":" + r.LocalPort
|
||||
@@ -229,7 +250,7 @@ func (r Remote) UserAddr() string {
|
||||
return r.RemoteHost + ":" + r.RemotePort
|
||||
}
|
||||
|
||||
//CanListen checks if the port can be listened on
|
||||
// CanListen checks if the port can be listened on
|
||||
func (r Remote) CanListen() bool {
|
||||
//valid protocols
|
||||
switch r.LocalProto {
|
||||
@@ -258,7 +279,7 @@ func (r Remote) CanListen() bool {
|
||||
|
||||
type Remotes []*Remote
|
||||
|
||||
//Filter out forward reversed/non-reversed remotes
|
||||
// Filter out forward reversed/non-reversed remotes
|
||||
func (rs Remotes) Reversed(reverse bool) Remotes {
|
||||
subset := Remotes{}
|
||||
for _, r := range rs {
|
||||
@@ -270,7 +291,7 @@ func (rs Remotes) Reversed(reverse bool) Remotes {
|
||||
return subset
|
||||
}
|
||||
|
||||
//Encode back into strings
|
||||
// Encode back into strings
|
||||
func (rs Remotes) Encode() []string {
|
||||
s := make([]string, len(rs))
|
||||
for i, r := range rs {
|
||||
|
||||
@@ -2,6 +2,7 @@ package settings
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -148,3 +149,63 @@ func TestRemoteDecode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPSocksRemote(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
input, host, port string
|
||||
reverse bool
|
||||
}{
|
||||
{"udpsocks", "127.0.0.1", "1080", false},
|
||||
{"5000:udpsocks", "127.0.0.1", "5000", false},
|
||||
{"0.0.0.0:5000:udpsocks", "0.0.0.0", "5000", false},
|
||||
{"localhost:5000:udpsocks", "localhost", "5000", false},
|
||||
{"[::1]:5000:udpsocks", "[::1]", "5000", false},
|
||||
{"R:udpsocks", "127.0.0.1", "1080", true},
|
||||
{"R:5000:udpsocks", "127.0.0.1", "5000", true},
|
||||
{"R:[::]:5000:udpsocks", "[::]", "5000", true},
|
||||
} {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
r, err := DecodeRemote(test.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := &Remote{LocalHost: test.host, LocalPort: test.port, LocalProto: "tcp", RemoteProto: "tcp", Socks: true, UDPSocks: true, Reverse: test.reverse}
|
||||
if !reflect.DeepEqual(r, want) {
|
||||
t.Fatalf("decoded %#v, want %#v", r, want)
|
||||
}
|
||||
encoded := test.host + ":" + test.port + ":udpsocks"
|
||||
permission := "socks"
|
||||
if test.reverse {
|
||||
encoded = "R:" + encoded
|
||||
permission = "R:" + test.host + ":" + test.port
|
||||
}
|
||||
if r.Encode() != encoded || r.Remote() != "udpsocks" || !strings.HasSuffix(r.String(), "=>udpsocks") || r.UserAddr() != permission {
|
||||
t.Fatalf("encode=%q remote=%q string=%q permission=%q", r.Encode(), r.Remote(), r.String(), r.UserAddr())
|
||||
}
|
||||
decoded, err := DecodeRemote(r.Encode())
|
||||
if err != nil || !reflect.DeepEqual(decoded, r) {
|
||||
t.Fatalf("remote round trip: %#v, %v", decoded, err)
|
||||
}
|
||||
config := Config{Version: "test", Remotes: Remotes{r}}
|
||||
got, err := DecodeConfig(EncodeConfig(config))
|
||||
if err != nil || !reflect.DeepEqual(got, &config) {
|
||||
t.Fatalf("configuration round trip: %#v, %v", got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPSocksInvalidRemote(t *testing.T) {
|
||||
for _, input := range []string{
|
||||
"0:udpsocks", "127.0.0.1:0:udpsocks", "R:0:udpsocks",
|
||||
"-1:udpsocks", "65536:udpsocks", "1080:1081:udpsocks",
|
||||
"localhost:localhost:udpsocks", "1080/udp:udpsocks", "1080:udpsocks/udp",
|
||||
"stdio:udpsocks", "stdio:1080:udpsocks", "R:stdio:udpsocks",
|
||||
} {
|
||||
t.Run(input, func(t *testing.T) {
|
||||
if r, err := DecodeRemote(input); err == nil {
|
||||
t.Fatalf("invalid remote accepted: %#v", r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/chisel/share/settings"
|
||||
)
|
||||
|
||||
// A separate destination keeps legacy peers from accepting a UDP channel as
|
||||
// an ordinary SOCKS stream. The existing TCP SOCKS protocol stays unchanged.
|
||||
const socksUDPDestination = "socks-udp-v1"
|
||||
|
||||
// Older peers may accept a channel before attempting their TCP dial. Require
|
||||
// an explicit marker before reporting UDP ASSOCIATE success to the client.
|
||||
const socksUDPReady = "chisel-socks-udp-v1\n"
|
||||
|
||||
const (
|
||||
socksSuccess = 0
|
||||
socksFailure = 1
|
||||
socksDenied = 2
|
||||
socksUnsupported = 7
|
||||
socksBadAddress = 8
|
||||
)
|
||||
|
||||
var errSocksAddress = errors.New("unsupported SOCKS address type")
|
||||
|
||||
type socksAddress struct {
|
||||
host string
|
||||
port uint16
|
||||
wire []byte
|
||||
}
|
||||
|
||||
func readSocksAddress(r io.Reader) (socksAddress, error) {
|
||||
a := socksAddress{}
|
||||
head := make([]byte, 1)
|
||||
if _, err := io.ReadFull(r, head); err != nil {
|
||||
return a, err
|
||||
}
|
||||
n := 0
|
||||
switch head[0] {
|
||||
case 1:
|
||||
n = 4
|
||||
case 4:
|
||||
n = 16
|
||||
case 3:
|
||||
length := make([]byte, 1)
|
||||
if _, err := io.ReadFull(r, length); err != nil {
|
||||
return a, err
|
||||
}
|
||||
if length[0] == 0 {
|
||||
return a, errors.New("empty SOCKS hostname")
|
||||
}
|
||||
head = append(head, length[0])
|
||||
n = int(length[0])
|
||||
default:
|
||||
return a, errSocksAddress
|
||||
}
|
||||
body := make([]byte, n+2)
|
||||
if _, err := io.ReadFull(r, body); err != nil {
|
||||
return a, err
|
||||
}
|
||||
if head[0] == 3 {
|
||||
a.host = string(body[:n])
|
||||
} else {
|
||||
a.host = net.IP(body[:n]).String()
|
||||
}
|
||||
a.port = binary.BigEndian.Uint16(body[n:])
|
||||
a.wire = append(head, body...)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a socksAddress) destination() string {
|
||||
return net.JoinHostPort(a.host, fmt.Sprint(a.port))
|
||||
}
|
||||
|
||||
func socksIPPort(ip net.IP, port int) []byte {
|
||||
atyp := byte(4)
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
ip, atyp = v4, 1
|
||||
} else {
|
||||
ip = ip.To16()
|
||||
}
|
||||
b := append([]byte{atyp}, ip...)
|
||||
return binary.BigEndian.AppendUint16(b, uint16(port))
|
||||
}
|
||||
|
||||
func socksReply(w io.Writer, code byte, addr []byte) error {
|
||||
if addr == nil {
|
||||
addr = []byte{1, 0, 0, 0, 0, 0, 0}
|
||||
}
|
||||
return writeSocksBytes(w, append([]byte{5, code, 0}, addr...))
|
||||
}
|
||||
|
||||
func parseSocksUDP(b []byte) (socksAddress, []byte, error) {
|
||||
if len(b) < 4 || b[0] != 0 || b[1] != 0 || b[2] != 0 {
|
||||
return socksAddress{}, nil, errors.New("invalid or fragmented SOCKS UDP packet")
|
||||
}
|
||||
a, err := readSocksAddress(bytes.NewReader(b[3:]))
|
||||
if err != nil {
|
||||
return a, nil, err
|
||||
}
|
||||
if a.port == 0 {
|
||||
return a, nil, errors.New("zero SOCKS UDP destination port")
|
||||
}
|
||||
return a, b[3+len(a.wire):], nil
|
||||
}
|
||||
|
||||
func socksUDPTooLarge(n, limit int, err error) bool {
|
||||
// Unix truncates UDP reads; Windows may return WSAEMSGSIZE (10040)
|
||||
// instead. Treat both as dropped datagrams, not listener/flow failures.
|
||||
return (err == nil && n > limit) || errors.Is(err, syscall.EMSGSIZE) ||
|
||||
(runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(10040)))
|
||||
}
|
||||
|
||||
func writeSocksBytes(w io.Writer, b []byte) error {
|
||||
for len(b) > 0 {
|
||||
n, err := w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type socksUDPStream struct {
|
||||
io.ReadWriteCloser
|
||||
max int
|
||||
mu sync.Mutex // IPv4 and IPv6 readers share the reply stream
|
||||
}
|
||||
|
||||
func (s *socksUDPStream) readPacket() ([]byte, error) {
|
||||
var header [4]byte
|
||||
if _, err := io.ReadFull(s, header[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := binary.BigEndian.Uint32(header[:])
|
||||
if n == 0 || uint64(n) > uint64(s.max) {
|
||||
return nil, errors.New("invalid SOCKS UDP frame size")
|
||||
}
|
||||
b := make([]byte, int(n))
|
||||
_, err := io.ReadFull(s, b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (s *socksUDPStream) writePacket(b []byte) error {
|
||||
if len(b) == 0 || len(b) > s.max {
|
||||
return errors.New("invalid SOCKS UDP frame size")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var header [4]byte
|
||||
binary.BigEndian.PutUint32(header[:], uint32(len(b)))
|
||||
if err := writeSocksBytes(s, header[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeSocksBytes(s, b)
|
||||
}
|
||||
|
||||
func socksUDPLimits() (size, conns int, deadline time.Duration) {
|
||||
size = settings.EnvInt("UDP_MAX_SIZE", 9012)
|
||||
if size <= 0 {
|
||||
size = 9012
|
||||
}
|
||||
if size > 65507 {
|
||||
size = 65507
|
||||
}
|
||||
conns = settings.EnvInt("UDP_MAX_CONNS", 100)
|
||||
if conns <= 0 {
|
||||
conns = 100
|
||||
}
|
||||
deadline = settings.EnvDuration("UDP_DEADLINE", 15*time.Second)
|
||||
if deadline <= 0 {
|
||||
deadline = 15 * time.Second
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/chisel/share/cio"
|
||||
"github.com/jpillora/chisel/share/settings"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// socksProxy owns one UDP socket per TCP listener. Associations are isolated
|
||||
// by observed UDP endpoints, never by addresses supplied by the exit node.
|
||||
type socksProxy struct {
|
||||
proxy *Proxy
|
||||
udp *net.UDPConn
|
||||
maxSize, maxConns int
|
||||
mu sync.Mutex
|
||||
associations map[*socksAssociation]bool
|
||||
peers map[netip.AddrPort]*socksAssociation
|
||||
pending map[netip.Addr]*socksAssociation
|
||||
workers sync.WaitGroup
|
||||
}
|
||||
|
||||
type socksAssociation struct {
|
||||
ip netip.Addr
|
||||
key netip.AddrPort
|
||||
peer *net.UDPAddr
|
||||
packets chan []byte
|
||||
stream *socksUDPStream
|
||||
}
|
||||
|
||||
func newSocksProxy(p *Proxy) *socksProxy {
|
||||
size, conns, _ := socksUDPLimits()
|
||||
s := &socksProxy{
|
||||
proxy: p, maxSize: size, maxConns: conns,
|
||||
associations: make(map[*socksAssociation]bool),
|
||||
peers: make(map[netip.AddrPort]*socksAssociation),
|
||||
pending: make(map[netip.Addr]*socksAssociation),
|
||||
}
|
||||
addr := p.tcp.Addr().(*net.TCPAddr)
|
||||
var err error
|
||||
s.udp, err = net.ListenUDP("udp", &net.UDPAddr{IP: addr.IP, Port: addr.Port, Zone: addr.Zone})
|
||||
if err != nil {
|
||||
p.Infof("SOCKS UDP unavailable on %s: %s (TCP remains available)", addr, err)
|
||||
} else {
|
||||
p.Infof("Listening for SOCKS UDP on %s", s.udp.LocalAddr())
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *socksProxy) run(ctx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
stop := context.AfterFunc(ctx, func() { s.proxy.Close() })
|
||||
var udpWorkers sync.WaitGroup
|
||||
udpError := make(chan error, 1)
|
||||
if s.udp != nil {
|
||||
udpWorkers.Add(1)
|
||||
go func() {
|
||||
defer udpWorkers.Done()
|
||||
if err := s.readUDP(); err != nil && ctx.Err() == nil {
|
||||
s.proxy.Infof("SOCKS UDP read error: %s", err)
|
||||
udpError <- err
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
err := s.proxy.runTCP(ctx)
|
||||
cancel()
|
||||
stop()
|
||||
s.proxy.Close()
|
||||
udpWorkers.Wait()
|
||||
s.workers.Wait()
|
||||
if err == nil {
|
||||
select {
|
||||
case err = <-udpError:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func socksPeerIP(ip net.IP, zone string) netip.Addr {
|
||||
a, _ := netip.AddrFromSlice(ip)
|
||||
a = a.Unmap()
|
||||
if zone != "" && a.Is6() {
|
||||
a = a.WithZone(zone)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (s *socksProxy) readUDP() error {
|
||||
// The extra byte detects oversize packets even when ReadFromUDP truncates.
|
||||
buf := make([]byte, s.maxSize+1)
|
||||
for {
|
||||
n, peer, err := s.udp.ReadFromUDP(buf)
|
||||
if socksUDPTooLarge(n, s.maxSize, err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := parseSocksUDP(buf[:n]); err != nil {
|
||||
continue
|
||||
}
|
||||
ip := socksPeerIP(peer.IP, peer.Zone)
|
||||
key := netip.AddrPortFrom(ip, uint16(peer.Port))
|
||||
s.mu.Lock()
|
||||
a := s.peers[key]
|
||||
if a == nil {
|
||||
a = s.pending[ip]
|
||||
if a != nil {
|
||||
delete(s.pending, ip)
|
||||
a.key = key
|
||||
s.peers[key] = a
|
||||
}
|
||||
}
|
||||
if a != nil {
|
||||
if a.peer == nil {
|
||||
// ReadFromUDP's address is local evidence; retain our own copy.
|
||||
copyPeer := *peer
|
||||
copyPeer.IP = append(net.IP(nil), peer.IP...)
|
||||
a.peer = ©Peer
|
||||
}
|
||||
select {
|
||||
case a.packets <- append([]byte(nil), buf[:n]...):
|
||||
default: // a stalled association cannot block this shared listener
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *socksProxy) register(a *socksAssociation, port uint16) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.associations) >= s.maxConns {
|
||||
return false
|
||||
}
|
||||
if port == 0 {
|
||||
if s.pending[a.ip] != nil {
|
||||
return false
|
||||
}
|
||||
s.pending[a.ip] = a
|
||||
} else {
|
||||
a.key = netip.AddrPortFrom(a.ip, port)
|
||||
if s.peers[a.key] != nil {
|
||||
return false
|
||||
}
|
||||
s.peers[a.key] = a
|
||||
}
|
||||
s.associations[a] = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *socksProxy) unregister(a *socksAssociation) {
|
||||
s.mu.Lock()
|
||||
delete(s.associations, a)
|
||||
if s.pending[a.ip] == a {
|
||||
delete(s.pending, a.ip)
|
||||
}
|
||||
if s.peers[a.key] == a {
|
||||
delete(s.peers, a.key)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *socksProxy) serve(ctx context.Context, src *net.TCPConn) {
|
||||
defer s.workers.Done()
|
||||
defer src.Close()
|
||||
stop := context.AfterFunc(ctx, func() { src.Close() })
|
||||
defer stop()
|
||||
// Bound unfinished handshakes without imposing a TCP control idle timeout.
|
||||
src.SetDeadline(time.Now().Add(settings.EnvDuration("DIAL_TIMEOUT", 30*time.Second)))
|
||||
if err := s.handle(ctx, src); err != nil && ctx.Err() == nil {
|
||||
s.proxy.Debugf("SOCKS connection: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *socksProxy) handle(ctx context.Context, src *net.TCPConn) error {
|
||||
var greeting [2]byte
|
||||
if _, err := io.ReadFull(src, greeting[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if greeting[0] != 5 || greeting[1] == 0 {
|
||||
return errors.New("invalid SOCKS greeting")
|
||||
}
|
||||
methods := make([]byte, int(greeting[1]))
|
||||
if _, err := io.ReadFull(src, methods); err != nil {
|
||||
return err
|
||||
}
|
||||
noAuth := false
|
||||
for _, method := range methods {
|
||||
noAuth = noAuth || method == 0
|
||||
}
|
||||
if !noAuth {
|
||||
writeSocksBytes(src, []byte{5, 255})
|
||||
return errors.New("no supported SOCKS authentication method")
|
||||
}
|
||||
if err := writeSocksBytes(src, []byte{5, 0}); err != nil {
|
||||
return err
|
||||
}
|
||||
var header [3]byte
|
||||
if _, err := io.ReadFull(src, header[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if header[0] != 5 || header[2] != 0 {
|
||||
socksReply(src, socksFailure, nil)
|
||||
return errors.New("invalid SOCKS request")
|
||||
}
|
||||
addr, err := readSocksAddress(src)
|
||||
if err != nil {
|
||||
code := byte(socksFailure)
|
||||
if errors.Is(err, errSocksAddress) {
|
||||
code = socksBadAddress
|
||||
}
|
||||
socksReply(src, code, nil)
|
||||
return err
|
||||
}
|
||||
if header[1] != 1 && header[1] != 3 {
|
||||
return socksReply(src, socksUnsupported, nil)
|
||||
}
|
||||
if header[1] == 3 && s.udp == nil {
|
||||
return socksReply(src, socksFailure, nil)
|
||||
}
|
||||
conn := s.proxy.sshTun.getSSH(ctx)
|
||||
if conn == nil {
|
||||
return socksReply(src, socksFailure, nil)
|
||||
}
|
||||
destination := "socks"
|
||||
if header[1] == 3 {
|
||||
destination = socksUDPDestination
|
||||
}
|
||||
ch, reqs, err := conn.OpenChannel("chisel", []byte(destination))
|
||||
if err != nil {
|
||||
code := byte(socksFailure)
|
||||
var channelErr *ssh.OpenChannelError
|
||||
if errors.As(err, &channelErr) && channelErr.Reason == ssh.Prohibited {
|
||||
code = socksDenied
|
||||
} else if header[1] == 3 && errors.As(err, &channelErr) && channelErr.Reason == ssh.ConnectionFailed {
|
||||
code = socksUnsupported
|
||||
}
|
||||
if header[1] == 3 {
|
||||
s.proxy.Infof("SOCKS UDP channel unavailable: %s (UDP requires support on both Chisel peers)", err)
|
||||
} else {
|
||||
s.proxy.Infof("SOCKS TCP channel unavailable: %s", err)
|
||||
}
|
||||
socksReply(src, code, nil)
|
||||
return err
|
||||
}
|
||||
defer ch.Close()
|
||||
go ssh.DiscardRequests(reqs)
|
||||
stop := context.AfterFunc(ctx, func() { ch.Close() })
|
||||
defer stop()
|
||||
if header[1] == 1 {
|
||||
// Replay a no-auth greeting on the unchanged legacy TCP SOCKS channel.
|
||||
if err := writeSocksBytes(ch, []byte{5, 1, 0}); err != nil {
|
||||
return err
|
||||
}
|
||||
var reply [2]byte
|
||||
if _, err := io.ReadFull(ch, reply[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if reply != [2]byte{5, 0} {
|
||||
return errors.New("unexpected exit SOCKS authentication reply")
|
||||
}
|
||||
if err := writeSocksBytes(ch, append(header[:], addr.wire...)); err != nil {
|
||||
return err
|
||||
}
|
||||
src.SetDeadline(time.Time{})
|
||||
cio.Pipe(src, ch)
|
||||
return nil
|
||||
}
|
||||
// An old exit may accept the channel then fail its TCP dial. It must
|
||||
// never cause a successful UDP reply, even briefly. Bound a silent exit.
|
||||
timer := time.AfterFunc(settings.EnvDuration("DIAL_TIMEOUT", 30*time.Second), func() { ch.Close() })
|
||||
ready := make([]byte, len(socksUDPReady))
|
||||
_, err = io.ReadFull(ch, ready)
|
||||
timedOut := !timer.Stop()
|
||||
if err != nil || timedOut || string(ready) != socksUDPReady {
|
||||
s.proxy.Infof("SOCKS UDP unavailable: exit did not confirm support (upgrade both Chisel peers)")
|
||||
return socksReply(src, socksUnsupported, nil)
|
||||
}
|
||||
return s.associate(ctx, src, ch, addr.port)
|
||||
}
|
||||
|
||||
func (s *socksProxy) associate(ctx context.Context, src *net.TCPConn, ch ssh.Channel, port uint16) error {
|
||||
peer := src.RemoteAddr().(*net.TCPAddr)
|
||||
a := &socksAssociation{
|
||||
ip: socksPeerIP(peer.IP, peer.Zone), packets: make(chan []byte, 16),
|
||||
stream: &socksUDPStream{ReadWriteCloser: ch, max: s.maxSize},
|
||||
}
|
||||
if !s.register(a, port) {
|
||||
return socksReply(src, socksFailure, nil)
|
||||
}
|
||||
defer s.unregister(a)
|
||||
local := src.LocalAddr().(*net.TCPAddr)
|
||||
relay := s.udp.LocalAddr().(*net.UDPAddr)
|
||||
if err := socksReply(src, socksSuccess, socksIPPort(local.IP, relay.Port)); err != nil {
|
||||
return err
|
||||
}
|
||||
src.SetDeadline(time.Time{})
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
var wg sync.WaitGroup
|
||||
finish := func() { cancel(); ch.Close(); src.Close() }
|
||||
// All workers unblock on control EOF, channel EOF, or proxy cancellation.
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer finish()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case b := <-a.packets:
|
||||
if err := a.stream.writePacket(b); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer finish()
|
||||
for {
|
||||
b, err := a.stream.readPacket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, _, err := parseSocksUDP(b); err != nil {
|
||||
continue
|
||||
}
|
||||
s.mu.Lock()
|
||||
// Never resolve or route using the exit's packet address: it is
|
||||
// the responder encoded in the SOCKS header, not the local peer.
|
||||
if s.associations[a] && a.peer != nil && ctx.Err() == nil {
|
||||
s.udp.WriteToUDP(b, a.peer)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
io.Copy(io.Discard, src)
|
||||
finish()
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-socks5"
|
||||
"github.com/jpillora/chisel/share/cio"
|
||||
"github.com/jpillora/chisel/share/settings"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type socksTestRWC struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (s socksTestRWC) Close() error { return nil }
|
||||
|
||||
func TestSocksUDPCodec(t *testing.T) {
|
||||
addresses := [][]byte{
|
||||
socksIPPort(net.ParseIP("127.0.0.1"), 53),
|
||||
socksIPPort(net.ParseIP("::1"), 53),
|
||||
append([]byte{3, 9}, append([]byte("localhost"), 0, 53)...),
|
||||
}
|
||||
for _, address := range addresses {
|
||||
packet := append(append([]byte{0, 0, 0}, address...), []byte("data")...)
|
||||
a, payload, err := parseSocksUDP(packet)
|
||||
if err != nil || a.port != 53 || string(payload) != "data" || !bytes.Equal(a.wire, address) {
|
||||
t.Fatalf("decode %v: %v %q %v", packet, a, payload, err)
|
||||
}
|
||||
for n := 0; n < 3+len(address); n++ {
|
||||
if _, _, err := parseSocksUDP(packet[:n]); err == nil {
|
||||
t.Fatalf("accepted truncated packet of length %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, b := range [][]byte{
|
||||
{0, 0, 0, 2}, {0, 0, 0, 3, 0, 0, 53},
|
||||
{0, 0, 1, 1, 127, 0, 0, 1, 0, 53},
|
||||
{1, 0, 0, 1, 127, 0, 0, 1, 0, 53},
|
||||
{0, 0, 0, 1, 127, 0, 0, 1, 0, 0},
|
||||
} {
|
||||
if _, _, err := parseSocksUDP(b); err == nil {
|
||||
t.Fatalf("accepted malformed packet %v", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzSocksUDPCodec(f *testing.F) {
|
||||
f.Add([]byte{0, 0, 0, 1, 127, 0, 0, 1, 0, 53, 'x'})
|
||||
f.Add([]byte{0, 0, 0, 3, 1, 'a', 0, 53})
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
a, payload, err := parseSocksUDP(b)
|
||||
if err == nil && (a.port == 0 || len(a.wire)+3+len(payload) != len(b)) {
|
||||
t.Fatal("invalid decoded packet")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSocksUDPFrames(t *testing.T) {
|
||||
// A tiny packet limit can be smaller than an IPv6 reply header. Even
|
||||
// then, closed sockets and read deadlines must terminate the reader.
|
||||
if socksUDPTooLarge(0, -1, net.ErrClosed) {
|
||||
t.Fatal("small packet limit hid a closed socket")
|
||||
}
|
||||
for _, size := range []uint32{0, 65, 0xffffffff} {
|
||||
buf := binary.BigEndian.AppendUint32(nil, size)
|
||||
s := &socksUDPStream{ReadWriteCloser: socksTestRWC{Reader: bytes.NewReader(buf)}, max: 64}
|
||||
if _, err := s.readPacket(); err == nil {
|
||||
t.Fatalf("accepted frame size %d", size)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
s := &socksUDPStream{ReadWriteCloser: socksTestRWC{Reader: &buf, Writer: &buf}, max: 64}
|
||||
if err := s.writePacket([]byte("one")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.writePacket([]byte("two")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{"one", "two"} {
|
||||
b, err := s.readPacket()
|
||||
if err != nil || string(b) != want {
|
||||
t.Fatalf("read %q, want %q: %v", b, want, err)
|
||||
}
|
||||
}
|
||||
if err := s.writePacket(make([]byte, 65)); err == nil {
|
||||
t.Fatal("wrote oversized frame")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPLimitSettings(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name, size, conns, idle string
|
||||
wantSize, wantConns int
|
||||
wantIdle time.Duration
|
||||
}{
|
||||
{"defaults", "", "", "", 9012, 100, 15 * time.Second},
|
||||
{"configured", "64", "2", "250ms", 64, 2, 250 * time.Millisecond},
|
||||
{"zero", "0", "0", "0s", 9012, 100, 15 * time.Second},
|
||||
{"negative", "-1", "-1", "-1s", 9012, 100, 15 * time.Second},
|
||||
{"invalid", "invalid", "invalid", "invalid", 9012, 100, 15 * time.Second},
|
||||
{"size cap", "70000", "2", "1s", 65507, 2, time.Second},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Setenv("CHISEL_UDP_MAX_SIZE", test.size)
|
||||
t.Setenv("CHISEL_UDP_MAX_CONNS", test.conns)
|
||||
t.Setenv("CHISEL_UDP_DEADLINE", test.idle)
|
||||
size, conns, idle := socksUDPLimits()
|
||||
if size != test.wantSize || conns != test.wantConns || idle != test.wantIdle {
|
||||
t.Fatalf("limits=(%d,%d,%s), want (%d,%d,%s)", size, conns, idle, test.wantSize, test.wantConns, test.wantIdle)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzSocksUDPFrames(f *testing.F) {
|
||||
f.Add([]byte{0, 0, 0, 1, 'x'})
|
||||
f.Add([]byte{255, 255, 255, 255})
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
s := &socksUDPStream{ReadWriteCloser: socksTestRWC{Reader: bytes.NewReader(b)}, max: 9012}
|
||||
packet, err := s.readPacket()
|
||||
if err == nil && (len(packet) == 0 || len(packet) > 9012 || len(packet)+4 > len(b)) {
|
||||
t.Fatal("invalid frame decoded")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSocksUDPSharedRoutingAndOverflow(t *testing.T) {
|
||||
u, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer u.Close()
|
||||
s := &socksProxy{udp: u, maxSize: 100, maxConns: 2,
|
||||
associations: make(map[*socksAssociation]bool), peers: make(map[netip.AddrPort]*socksAssociation), pending: make(map[netip.Addr]*socksAssociation)}
|
||||
ip := netip.MustParseAddr("127.0.0.1")
|
||||
a := &socksAssociation{ip: ip, packets: make(chan []byte, 16)}
|
||||
b := &socksAssociation{ip: ip, packets: make(chan []byte, 16)}
|
||||
if !s.register(a, 0) || s.register(b, 0) {
|
||||
t.Fatal("pending association policy failed")
|
||||
}
|
||||
peer, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer peer.Close()
|
||||
readDone := make(chan error, 1)
|
||||
go func() { readDone <- s.readUDP() }()
|
||||
packet := append([]byte{0, 0, 0}, socksIPPort(net.IPv4(127, 0, 0, 1), 53)...)
|
||||
t.Run("different source IP", func(t *testing.T) {
|
||||
otherIP, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 2)})
|
||||
if err != nil {
|
||||
t.Skipf("alternate loopback unavailable: %v", err)
|
||||
}
|
||||
defer otherIP.Close()
|
||||
otherIP.WriteToUDP(packet, u.LocalAddr().(*net.UDPAddr))
|
||||
select {
|
||||
case <-a.packets:
|
||||
t.Fatal("different IP claimed a pending association")
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
})
|
||||
peer.WriteToUDP(packet, u.LocalAddr().(*net.UDPAddr))
|
||||
select {
|
||||
case <-a.packets:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("first packet did not pin")
|
||||
}
|
||||
if !s.register(b, 0) {
|
||||
t.Fatal("pinned association still blocks pending association")
|
||||
}
|
||||
if s.register(&socksAssociation{ip: ip}, 12345) {
|
||||
t.Fatal("association limit bypassed")
|
||||
}
|
||||
// Fill A's queue, then send enough packets to overflow it. B must still
|
||||
// receive its own packet, proving the shared reader does not block on A.
|
||||
for i := 0; i < cap(a.packets); i++ {
|
||||
a.packets <- packet
|
||||
}
|
||||
for i := 0; i < 20; i++ {
|
||||
peer.WriteToUDP(packet, u.LocalAddr().(*net.UDPAddr))
|
||||
}
|
||||
other, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer other.Close()
|
||||
other.WriteToUDP(packet, u.LocalAddr().(*net.UDPAddr))
|
||||
select {
|
||||
case <-b.packets:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("full queue blocked unrelated peer")
|
||||
}
|
||||
s.unregister(a)
|
||||
s.unregister(b)
|
||||
if !s.register(a, uint16(peer.LocalAddr().(*net.UDPAddr).Port)) {
|
||||
t.Fatal("closed association retained reservation")
|
||||
}
|
||||
u.Close()
|
||||
select {
|
||||
case <-readDone:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("UDP reader did not stop")
|
||||
}
|
||||
}
|
||||
|
||||
type socksTestSSH struct {
|
||||
*mockSSHConn
|
||||
open func(string, []byte) (ssh.Channel, <-chan *ssh.Request, error)
|
||||
}
|
||||
|
||||
func (s *socksTestSSH) OpenChannel(kind string, extra []byte) (ssh.Channel, <-chan *ssh.Request, error) {
|
||||
return s.open(kind, extra)
|
||||
}
|
||||
|
||||
type socksTestTunnel struct{ conn ssh.Conn }
|
||||
|
||||
func (s socksTestTunnel) getSSH(context.Context) ssh.Conn { return s.conn }
|
||||
|
||||
type socksTestChannel struct{ net.Conn }
|
||||
|
||||
func (s socksTestChannel) CloseWrite() error { return s.Close() }
|
||||
func (s socksTestChannel) SendRequest(string, bool, []byte) (bool, error) { return false, nil }
|
||||
func (s socksTestChannel) Stderr() io.ReadWriter { return s.Conn }
|
||||
|
||||
func newSocksTestProxy(t *testing.T, conn ssh.Conn) *Proxy {
|
||||
t.Helper()
|
||||
// Reserve a port in the test; udpsocks itself must never bind port zero.
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port)
|
||||
listener.Close()
|
||||
p, err := NewProxy(cio.NewLogger("test"), socksTestTunnel{conn}, 0, &settings.Remote{LocalHost: "127.0.0.1", LocalPort: port, LocalProto: "tcp", Socks: true, UDPSocks: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- p.Run(ctx) }()
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Error("SOCKS proxy did not stop")
|
||||
}
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
func TestUDPSocksRejectsEphemeralListener(t *testing.T) {
|
||||
r := &settings.Remote{LocalHost: "127.0.0.1", LocalPort: "0", LocalProto: "tcp", Socks: true, UDPSocks: true}
|
||||
p, err := NewProxy(cio.NewLogger("test"), socksTestTunnel{}, 0, r)
|
||||
if p != nil {
|
||||
defer p.Close()
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("udpsocks accepted an ephemeral listening port")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksLegacyPeerCompatibility(t *testing.T) {
|
||||
legacy, err := socks5.New(&socks5.Config{Logger: nil})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
conn := &socksTestSSH{mockSSHConn: newMockSSHConn(false)}
|
||||
conn.open = func(_ string, extra []byte) (ssh.Channel, <-chan *ssh.Request, error) {
|
||||
if string(extra) != "socks" {
|
||||
// Old Chisel treats unknown destinations as TCP host:port and
|
||||
// rejects this token because it lacks a port.
|
||||
return nil, nil, &ssh.OpenChannelError{Reason: ssh.ConnectionFailed, Message: "missing port in address"}
|
||||
}
|
||||
local, remote := net.Pipe()
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); legacy.ServeConn(remote) }()
|
||||
reqs := make(chan *ssh.Request)
|
||||
close(reqs)
|
||||
return socksTestChannel{local}, reqs, nil
|
||||
}
|
||||
p := newSocksTestProxy(t, conn)
|
||||
endpoint, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer endpoint.Close()
|
||||
go func() {
|
||||
c, err := endpoint.Accept()
|
||||
if err == nil {
|
||||
c.Write([]byte("legacy TCP"))
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
for _, command := range []byte{1, 3} {
|
||||
c, err := net.Dial("tcp", p.tcp.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.SetDeadline(time.Now().Add(time.Second))
|
||||
c.Write([]byte{5, 1, 0})
|
||||
greeting := make([]byte, 2)
|
||||
if _, err := io.ReadFull(c, greeting); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := endpoint.Addr().(*net.TCPAddr)
|
||||
c.Write(append([]byte{5, command, 0}, socksIPPort(a.IP, a.Port)...))
|
||||
header := make([]byte, 3)
|
||||
if _, err := io.ReadFull(c, header); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := readSocksAddress(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if command == 1 {
|
||||
b := make([]byte, len("legacy TCP"))
|
||||
if _, err := io.ReadFull(c, b); err != nil || string(b) != "legacy TCP" || header[1] != socksSuccess {
|
||||
t.Fatalf("legacy TCP failed: %q %v %v", b, header, err)
|
||||
}
|
||||
} else if header[1] != socksUnsupported {
|
||||
t.Fatalf("legacy UDP reply %d, want unsupported", header[1])
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSocksLegacyPeerAcceptsBeforeDial(t *testing.T) {
|
||||
conn := &socksTestSSH{mockSSHConn: newMockSSHConn(false)}
|
||||
conn.open = func(_ string, _ []byte) (ssh.Channel, <-chan *ssh.Request, error) {
|
||||
local, remote := net.Pipe()
|
||||
// Pre-1.12 Chisel accepted channels before dialing. An unknown
|
||||
// destination then failed the TCP dial and closed the accepted stream.
|
||||
remote.Close()
|
||||
reqs := make(chan *ssh.Request)
|
||||
close(reqs)
|
||||
return socksTestChannel{local}, reqs, nil
|
||||
}
|
||||
p := newSocksTestProxy(t, conn)
|
||||
c, err := net.Dial("tcp", p.tcp.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
c.SetDeadline(time.Now().Add(time.Second))
|
||||
c.Write([]byte{5, 1, 0})
|
||||
var greeting [2]byte
|
||||
if _, err := io.ReadFull(c, greeting[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Write(append([]byte{5, 3, 0}, socksIPPort(net.IPv4zero, 0)...))
|
||||
var reply [3]byte
|
||||
if _, err := io.ReadFull(c, reply[:]); err != nil || reply[1] != socksUnsupported {
|
||||
t.Fatalf("old peer produced reply %v: %v", reply, err)
|
||||
}
|
||||
p.socks.mu.Lock()
|
||||
defer p.socks.mu.Unlock()
|
||||
if len(p.socks.associations) != 0 {
|
||||
t.Fatal("unsupported peer retained an association")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPExitLimitsAndRecovery(t *testing.T) {
|
||||
t.Setenv("CHISEL_UDP_MAX_CONNS", "1")
|
||||
t.Setenv("CHISEL_UDP_DEADLINE", "50ms")
|
||||
echo := func() *net.UDPConn {
|
||||
c, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
go func() {
|
||||
b := make([]byte, 100)
|
||||
for {
|
||||
n, a, err := c.ReadFromUDP(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.WriteToUDP(b[:n], a)
|
||||
}
|
||||
}()
|
||||
return c
|
||||
}
|
||||
one, two := echo(), echo()
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
tun := New(Config{Logger: cio.NewLogger("test")})
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- tun.handleSocksUDP(ctx, tun.Logger, server) }()
|
||||
s := &socksUDPStream{ReadWriteCloser: client, max: 9012}
|
||||
packet := func(c *net.UDPConn) []byte {
|
||||
a := c.LocalAddr().(*net.UDPAddr)
|
||||
return append(append([]byte{0, 0, 0}, socksIPPort(a.IP, a.Port)...), 'x')
|
||||
}
|
||||
client.SetDeadline(time.Now().Add(time.Second))
|
||||
if err := s.writePacket(packet(one)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.readPacket(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.writePacket(packet(two)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
|
||||
if _, err := s.readPacket(); err == nil {
|
||||
t.Fatal("flow cap did not drop new destination")
|
||||
}
|
||||
// The flow cap must recover once the existing destination goes idle.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
client.SetDeadline(time.Now().Add(time.Second))
|
||||
if err := s.writePacket(packet(two)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.readPacket(); err != nil {
|
||||
t.Fatalf("idle flow retained capacity: %v", err)
|
||||
}
|
||||
// Close immediately while a reply reader exists: cancellation must join it.
|
||||
cancel()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("exit retained workers on cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPReturnsOnlyToObservedAssociation(t *testing.T) {
|
||||
conn := &socksTestSSH{mockSSHConn: newMockSSHConn(false)}
|
||||
remoteChannels := make(chan net.Conn, 1)
|
||||
conn.open = func(_ string, extra []byte) (ssh.Channel, <-chan *ssh.Request, error) {
|
||||
if string(extra) != socksUDPDestination {
|
||||
t.Errorf("unexpected destination %q", extra)
|
||||
}
|
||||
local, remote := net.Pipe()
|
||||
remoteChannels <- remote
|
||||
go writeSocksBytes(remote, []byte(socksUDPReady))
|
||||
reqs := make(chan *ssh.Request)
|
||||
close(reqs)
|
||||
return socksTestChannel{local}, reqs, nil
|
||||
}
|
||||
p := newSocksTestProxy(t, conn)
|
||||
udpClient := func() *net.UDPConn {
|
||||
c, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
return c
|
||||
}
|
||||
allowed, blocked := udpClient(), udpClient()
|
||||
control, err := net.Dial("tcp", p.tcp.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer control.Close()
|
||||
control.SetDeadline(time.Now().Add(time.Second))
|
||||
control.Write([]byte{5, 1, 0})
|
||||
var greeting [2]byte
|
||||
if _, err := io.ReadFull(control, greeting[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port := allowed.LocalAddr().(*net.UDPAddr).Port
|
||||
control.Write(append([]byte{5, 3, 0}, socksIPPort(net.IPv4zero, port)...))
|
||||
var reply [3]byte
|
||||
if _, err := io.ReadFull(control, reply[:]); err != nil || reply[1] != 0 {
|
||||
t.Fatalf("association failed: %v %v", reply, err)
|
||||
}
|
||||
if _, err := readSocksAddress(control); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remote := <-remoteChannels
|
||||
defer remote.Close()
|
||||
remote.SetDeadline(time.Now().Add(time.Second))
|
||||
s := &socksUDPStream{ReadWriteCloser: remote, max: 9012}
|
||||
blockedAddr := blocked.LocalAddr().(*net.UDPAddr)
|
||||
packet := append(append([]byte{0, 0, 0}, socksIPPort(blockedAddr.IP, blockedAddr.Port)...), []byte("response")...)
|
||||
// A declared port alone is insufficient evidence for returning a packet.
|
||||
if err := s.writePacket(packet); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range []*net.UDPConn{allowed, blocked} {
|
||||
c.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
|
||||
if _, _, err := c.ReadFromUDP(make([]byte, 100)); err == nil {
|
||||
t.Fatal("unobserved endpoint received a return packet")
|
||||
}
|
||||
}
|
||||
allowed.WriteToUDP(packet, p.socks.udp.LocalAddr().(*net.UDPAddr))
|
||||
if _, err := s.readPacket(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.writePacket(packet); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
allowed.SetReadDeadline(time.Now().Add(time.Second))
|
||||
buf := make([]byte, 100)
|
||||
n, _, err := allowed.ReadFromUDP(buf)
|
||||
if err != nil || !bytes.Equal(buf[:n], packet) {
|
||||
t.Fatalf("observed endpoint did not receive reply: %v", err)
|
||||
}
|
||||
blocked.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
|
||||
if _, _, err := blocked.ReadFromUDP(buf); err == nil {
|
||||
t.Fatal("exit-controlled address received a return packet")
|
||||
}
|
||||
control.Close()
|
||||
if _, err := remote.Read(make([]byte, 1)); err != io.EOF {
|
||||
t.Fatalf("control EOF did not close SSH channel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPInvalidFrameClosesAssociation(t *testing.T) {
|
||||
for _, size := range []uint32{0, 9013, 0xffffffff} {
|
||||
client, server := net.Pipe()
|
||||
tun := New(Config{Logger: cio.NewLogger("test")})
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- tun.handleSocksUDP(context.Background(), tun.Logger, server) }()
|
||||
client.SetDeadline(time.Now().Add(time.Second))
|
||||
if _, err := client.Write(binary.BigEndian.AppendUint32(nil, size)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
t.Fatal("invalid frame succeeded")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("invalid frame retained association")
|
||||
}
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/chisel/share/cio"
|
||||
"github.com/jpillora/chisel/share/settings"
|
||||
)
|
||||
|
||||
type socksUDPFlow struct {
|
||||
addr *net.UDPAddr
|
||||
lastSeen time.Time // protected by the handler mutex
|
||||
}
|
||||
|
||||
type socksUDPHandler struct {
|
||||
stream *socksUDPStream
|
||||
logger *cio.Logger
|
||||
ctx context.Context
|
||||
maxConns int
|
||||
deadline time.Duration
|
||||
mu sync.Mutex
|
||||
flows map[string]*socksUDPFlow
|
||||
sockets map[string]*net.UDPConn // one unconnected socket per address family
|
||||
workers sync.WaitGroup
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleSocksUDP(ctx context.Context, logger *cio.Logger, stream io.ReadWriteCloser) error {
|
||||
size, conns, deadline := socksUDPLimits()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
h := &socksUDPHandler{
|
||||
stream: &socksUDPStream{ReadWriteCloser: stream, max: size},
|
||||
logger: logger, ctx: ctx, maxConns: conns, deadline: deadline,
|
||||
flows: make(map[string]*socksUDPFlow), sockets: make(map[string]*net.UDPConn),
|
||||
}
|
||||
stop := context.AfterFunc(ctx, func() { stream.Close() })
|
||||
defer func() {
|
||||
cancel()
|
||||
stop()
|
||||
stream.Close()
|
||||
h.mu.Lock()
|
||||
for _, conn := range h.sockets {
|
||||
conn.Close()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
h.workers.Wait()
|
||||
}()
|
||||
for {
|
||||
b, err := h.stream.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr, payload, err := parseSocksUDP(b)
|
||||
if err != nil {
|
||||
continue // malformed datagrams do not end a valid association
|
||||
}
|
||||
h.write(addr, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// Expiration bounds destination state without closing the association's
|
||||
// sockets: protocols such as TFTP must retain their source port even when
|
||||
// the remote endpoint changes. Caller holds h.mu.
|
||||
func (h *socksUDPHandler) expire(now time.Time) {
|
||||
for key, f := range h.flows {
|
||||
if now.Sub(f.lastSeen) >= h.deadline {
|
||||
delete(h.flows, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *socksUDPHandler) resolve(addr socksAddress) (*net.UDPAddr, error) {
|
||||
ip := net.ParseIP(addr.host)
|
||||
zone := ""
|
||||
if ip == nil {
|
||||
ctx, cancel := context.WithTimeout(h.ctx, settings.EnvDuration("DIAL_TIMEOUT", 30*time.Second))
|
||||
defer cancel()
|
||||
ips, err := net.DefaultResolver.LookupIPAddr(ctx, addr.host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, &net.DNSError{Err: "no addresses", Name: addr.host, IsNotFound: true}
|
||||
}
|
||||
ip, zone = ips[0].IP, ips[0].Zone
|
||||
}
|
||||
return &net.UDPAddr{IP: ip, Port: int(addr.port), Zone: zone}, nil
|
||||
}
|
||||
|
||||
func (h *socksUDPHandler) write(addr socksAddress, payload []byte) {
|
||||
key := addr.destination()
|
||||
h.mu.Lock()
|
||||
h.expire(time.Now())
|
||||
f := h.flows[key]
|
||||
full := len(h.flows) >= h.maxConns
|
||||
h.mu.Unlock()
|
||||
if f == nil {
|
||||
if full {
|
||||
return // hard cap: no unbounded destination state
|
||||
}
|
||||
resolved, err := h.resolve(addr)
|
||||
if err != nil {
|
||||
h.logger.Debugf("SOCKS UDP resolve %s: %s", key, err)
|
||||
return
|
||||
}
|
||||
f = &socksUDPFlow{addr: resolved}
|
||||
}
|
||||
network := "udp6"
|
||||
if f.addr.IP.To4() != nil {
|
||||
network = "udp4"
|
||||
}
|
||||
h.mu.Lock()
|
||||
conn := h.sockets[network]
|
||||
if conn == nil {
|
||||
var err error
|
||||
conn, err = net.ListenUDP(network, nil)
|
||||
if err != nil {
|
||||
h.mu.Unlock()
|
||||
h.logger.Debugf("SOCKS UDP listen %s: %s", network, err)
|
||||
return
|
||||
}
|
||||
h.sockets[network] = conn
|
||||
h.workers.Add(1)
|
||||
go h.readReplies(network, conn)
|
||||
}
|
||||
f.lastSeen = time.Now()
|
||||
h.flows[key] = f
|
||||
h.mu.Unlock()
|
||||
conn.SetWriteDeadline(time.Now().Add(h.deadline))
|
||||
if _, err := conn.WriteToUDP(payload, f.addr); err != nil {
|
||||
h.logger.Debugf("SOCKS UDP write %s: %s", key, err)
|
||||
h.mu.Lock()
|
||||
delete(h.flows, key)
|
||||
h.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer an exact endpoint match when refreshing destination activity. For
|
||||
// replies from a new port, refresh the latest request to that host without
|
||||
// allocating additional destination state. Replies from other hosts are also
|
||||
// relayed but do not refresh destination state. Caller holds h.mu.
|
||||
func (h *socksUDPHandler) replyFlow(addr *net.UDPAddr) *socksUDPFlow {
|
||||
var latest *socksUDPFlow
|
||||
for _, f := range h.flows {
|
||||
if !f.addr.IP.Equal(addr.IP) || f.addr.Zone != addr.Zone {
|
||||
continue
|
||||
}
|
||||
if f.addr.Port == addr.Port {
|
||||
return f
|
||||
}
|
||||
if latest == nil || f.lastSeen.After(latest.lastSeen) {
|
||||
latest = f
|
||||
}
|
||||
}
|
||||
return latest
|
||||
}
|
||||
|
||||
func (h *socksUDPHandler) readReplies(network string, conn *net.UDPConn) {
|
||||
defer h.workers.Done()
|
||||
defer func() {
|
||||
h.mu.Lock()
|
||||
if h.sockets[network] == conn {
|
||||
delete(h.sockets, network)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
conn.Close()
|
||||
}()
|
||||
buf := make([]byte, h.stream.max+1)
|
||||
for {
|
||||
n, addr, err := conn.ReadFromUDP(buf)
|
||||
if socksUDPTooLarge(n, h.stream.max, err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
header := append([]byte{0, 0, 0}, socksIPPort(addr.IP, addr.Port)...)
|
||||
if n > h.stream.max-len(header) {
|
||||
continue
|
||||
}
|
||||
h.mu.Lock()
|
||||
now := time.Now()
|
||||
h.expire(now)
|
||||
f := h.replyFlow(addr)
|
||||
if f != nil {
|
||||
f.lastSeen = now
|
||||
}
|
||||
h.mu.Unlock()
|
||||
packet := make([]byte, len(header)+n)
|
||||
copy(packet, header)
|
||||
copy(packet[len(header):], buf[:n])
|
||||
if err := h.stream.writePacket(packet); err != nil {
|
||||
h.stream.Close() // unblock the association's request reader
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
//sshTunnel exposes a subset of Tunnel to subtypes
|
||||
// sshTunnel exposes a subset of Tunnel to subtypes
|
||||
type sshTunnel interface {
|
||||
getSSH(ctx context.Context) ssh.Conn
|
||||
}
|
||||
|
||||
//Proxy is the inbound portion of a Tunnel
|
||||
// Proxy is the inbound portion of a Tunnel
|
||||
type Proxy struct {
|
||||
*cio.Logger
|
||||
sshTun sshTunnel
|
||||
@@ -27,10 +27,11 @@ type Proxy struct {
|
||||
dialer net.Dialer
|
||||
tcp *net.TCPListener
|
||||
udp *udpListener
|
||||
socks *socksProxy
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
//NewProxy creates a Proxy
|
||||
// NewProxy creates a Proxy
|
||||
func NewProxy(logger *cio.Logger, sshTun sshTunnel, index int, remote *settings.Remote) (*Proxy, error) {
|
||||
id := index + 1
|
||||
p := &Proxy{
|
||||
@@ -50,12 +51,18 @@ func (p *Proxy) listen() error {
|
||||
if err != nil {
|
||||
return p.Errorf("resolve: %s", err)
|
||||
}
|
||||
if p.remote.UDPSocks && addr.Port == 0 {
|
||||
return p.Errorf("udpsocks requires a nonzero listening port")
|
||||
}
|
||||
l, err := net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
return p.Errorf("tcp: %s", err)
|
||||
}
|
||||
p.Infof("Listening")
|
||||
p.tcp = l
|
||||
if p.remote.UDPSocks {
|
||||
p.socks = newSocksProxy(p)
|
||||
}
|
||||
} else if p.remote.LocalProto == "udp" {
|
||||
l, err := listenUDP(p.Logger, p.sshTun, p.remote)
|
||||
if err != nil {
|
||||
@@ -69,24 +76,33 @@ func (p *Proxy) listen() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Close unbinds the proxy's listeners. proxies which have
|
||||
//been Run are instead closed by cancelling their context.
|
||||
// Close unbinds the proxy's listeners. proxies which have
|
||||
// been Run are instead closed by cancelling their context.
|
||||
func (p *Proxy) Close() error {
|
||||
var err error
|
||||
if p.tcp != nil {
|
||||
return p.tcp.Close()
|
||||
err = p.tcp.Close()
|
||||
}
|
||||
if p.socks != nil && p.socks.udp != nil {
|
||||
if e := p.socks.udp.Close(); err == nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
if p.udp != nil {
|
||||
return p.udp.inbound.Close()
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
//Run enables the proxy and blocks while its active,
|
||||
//close the proxy by cancelling the context.
|
||||
// Run enables the proxy and blocks while its active,
|
||||
// close the proxy by cancelling the context.
|
||||
func (p *Proxy) Run(ctx context.Context) error {
|
||||
if p.remote.Stdio {
|
||||
return p.runStdio(ctx)
|
||||
} else if p.remote.LocalProto == "tcp" {
|
||||
if p.socks != nil {
|
||||
return p.socks.run(ctx)
|
||||
}
|
||||
return p.runTCP(ctx)
|
||||
} else if p.remote.LocalProto == "udp" {
|
||||
return p.udp.run(ctx)
|
||||
@@ -130,7 +146,12 @@ func (p *Proxy) runTCP(ctx context.Context) error {
|
||||
close(done)
|
||||
return err
|
||||
}
|
||||
go p.pipeRemote(ctx, src)
|
||||
if p.socks != nil {
|
||||
p.socks.workers.Add(1)
|
||||
go p.socks.serve(ctx, src.(*net.TCPConn))
|
||||
} else {
|
||||
go p.pipeRemote(ctx, src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,14 @@ func (t *Tunnel) handleSSHChannel(ctx context.Context, ch ssh.NewChannel) {
|
||||
return
|
||||
}
|
||||
remote := string(ch.ExtraData())
|
||||
socksUDP := remote == socksUDPDestination
|
||||
//extract protocol
|
||||
hostPort, proto := settings.L4Proto(remote)
|
||||
udp := proto == "udp"
|
||||
socks := hostPort == "socks"
|
||||
socks := hostPort == "socks" || socksUDP
|
||||
if socksUDP {
|
||||
hostPort = "socks" // UDP associations use the same capability and ACL
|
||||
}
|
||||
if socks && t.socksServer == nil {
|
||||
t.Debugf("Denied socks request, please enable socks")
|
||||
ch.Reject(ssh.Prohibited, "SOCKS5 is not enabled")
|
||||
@@ -87,7 +91,11 @@ func (t *Tunnel) handleSSHChannel(ctx context.Context, ch ssh.NewChannel) {
|
||||
//ready to handle
|
||||
t.connStats.Open()
|
||||
l.Debugf("Open %s", t.connStats.String())
|
||||
if socks {
|
||||
if socksUDP {
|
||||
if err = writeSocksBytes(stream, []byte(socksUDPReady)); err == nil {
|
||||
err = t.handleSocksUDP(ctx, l, stream)
|
||||
}
|
||||
} else if socks {
|
||||
err = t.handleSocks(stream)
|
||||
} else if udp {
|
||||
err = t.handleUDP(l, stream, hostPort)
|
||||
|
||||
+10
-3
@@ -26,7 +26,7 @@ type testLayout struct {
|
||||
udpServer bool
|
||||
}
|
||||
|
||||
func (tl *testLayout) setup(t *testing.T) (server *chserver.Server, client *chclient.Client, teardown func()) {
|
||||
func (tl *testLayout) setup(t testing.TB) (server *chserver.Server, client *chclient.Client, teardown func()) {
|
||||
//start of the world
|
||||
// goroutines := runtime.NumGoroutine()
|
||||
//root cancel
|
||||
@@ -56,11 +56,15 @@ func (tl *testLayout) setup(t *testing.T) (server *chserver.Server, client *chcl
|
||||
}()
|
||||
}
|
||||
//server
|
||||
_, benchmarking := t.(*testing.B)
|
||||
server, err := chserver.NewServer(tl.server)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server.Debug = debug
|
||||
server.Debug = debug && !benchmarking
|
||||
if benchmarking {
|
||||
server.SetOutput(io.Discard)
|
||||
}
|
||||
port := availablePort()
|
||||
if err := server.StartContext(ctx, "127.0.0.1", port); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -88,7 +92,10 @@ func (tl *testLayout) setup(t *testing.T) (server *chserver.Server, client *chcl
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client.Debug = debug
|
||||
client.Debug = debug && !benchmarking
|
||||
if benchmarking {
|
||||
client.SetOutput(io.Discard)
|
||||
}
|
||||
if err := client.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/txthinking/socks5"
|
||||
)
|
||||
|
||||
// BenchmarkSocksUDP measures warmed UDP echo round trips over loopback.
|
||||
// The SOCKS paths use an independent client and the real encrypted tunnel.
|
||||
// MB/s counts delivered application payload once per echo, not both wire
|
||||
// directions. Timed-out requests count as loss, with a 100ms client timeout.
|
||||
func BenchmarkSocksUDP(b *testing.B) {
|
||||
b.Setenv("CHISEL_UDP_MAX_SIZE", "")
|
||||
b.Setenv("CHISEL_UDP_MAX_CONNS", "")
|
||||
b.Setenv("CHISEL_UDP_DEADLINE", "")
|
||||
for _, path := range []string{"direct", "forward", "reverse"} {
|
||||
for _, size := range []int{64, 1200, 8192} {
|
||||
for _, workers := range []int{1, 16} {
|
||||
b.Run(fmt.Sprintf("%s/bytes=%d/clients=%d", path, size, workers), func(b *testing.B) {
|
||||
echo := socksUDPEcho(b, "udp4", "127.0.0.1:0")
|
||||
var addr string
|
||||
if path != "direct" {
|
||||
addr, _ = setupSocksUDP(b, path == "reverse", availablePort())
|
||||
}
|
||||
payload := bytes.Repeat([]byte{'x'}, size)
|
||||
conns := make([]net.Conn, workers)
|
||||
for i := range conns {
|
||||
var err error
|
||||
if path == "direct" {
|
||||
conns[i], err = net.Dial("udp4", echo.String())
|
||||
} else {
|
||||
client, e := socks5.NewClient(addr, "", "", 5, 0)
|
||||
if e != nil {
|
||||
b.Fatal(e)
|
||||
}
|
||||
conns[i], err = client.Dial("udp", echo.String())
|
||||
}
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
c := conns[i]
|
||||
b.Cleanup(func() { c.Close() })
|
||||
// Pin each wildcard association before opening the next one.
|
||||
if err := benchmarkUDPEcho(c, payload, make([]byte, size+64)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
samples := make([][]int64, workers)
|
||||
buffers := make([][]byte, workers)
|
||||
payloads := make([][]byte, workers)
|
||||
losses := make([]int, workers)
|
||||
for i := range samples {
|
||||
samples[i] = make([]int64, 0, 2048)
|
||||
buffers[i] = make([]byte, size+64)
|
||||
payloads[i] = bytes.Clone(payload)
|
||||
}
|
||||
errors := make(chan error, workers)
|
||||
var wg sync.WaitGroup
|
||||
sampleEvery := max(1, b.N/workers/2048)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for worker := range workers {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i, n := worker, 0; i < b.N; i, n = i+workers, n+1 {
|
||||
binary.BigEndian.PutUint64(payloads[worker][:8], uint64(i))
|
||||
start := time.Now()
|
||||
if err := benchmarkUDPMeasured(conns[worker], payloads[worker], buffers[worker]); err != nil {
|
||||
if e, ok := err.(net.Error); ok && e.Timeout() {
|
||||
losses[worker]++
|
||||
continue
|
||||
}
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
if n%sampleEvery == 0 && len(samples[worker]) < cap(samples[worker]) {
|
||||
samples[worker] = append(samples[worker], time.Since(start).Nanoseconds())
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
b.StopTimer()
|
||||
close(errors)
|
||||
for err := range errors {
|
||||
b.Fatal(err)
|
||||
}
|
||||
var all []int64
|
||||
var lost int
|
||||
for i, sample := range samples {
|
||||
all = append(all, sample...)
|
||||
lost += losses[i]
|
||||
}
|
||||
if len(all) == 0 {
|
||||
b.Fatal("all measured datagrams were lost")
|
||||
}
|
||||
slices.Sort(all)
|
||||
b.ReportMetric(float64(all[len(all)*50/100])/1000, "p50-us")
|
||||
b.ReportMetric(float64(all[len(all)*95/100])/1000, "p95-us")
|
||||
b.ReportMetric(float64(b.N-lost)/b.Elapsed().Seconds(), "roundtrips/s")
|
||||
b.ReportMetric(float64(b.N-lost)*float64(size)/b.Elapsed().Seconds()/1e6, "MB/s")
|
||||
b.ReportMetric(100*float64(lost)/float64(b.N), "loss-percent")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkUDPMeasured(c net.Conn, payload, buf []byte) error {
|
||||
if err := c.SetDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
return err
|
||||
}
|
||||
if n, err := c.Write(payload); err != nil || n != len(payload) {
|
||||
return fmt.Errorf("UDP write n=%d: %v", n, err)
|
||||
}
|
||||
for {
|
||||
n, err := c.Read(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(payload) {
|
||||
return fmt.Errorf("UDP echo n=%d, want %d", n, len(payload))
|
||||
}
|
||||
if !bytes.Equal(buf[:8], payload[:8]) {
|
||||
continue // late reply to an operation already counted as lost
|
||||
}
|
||||
if !bytes.Equal(buf[:n], payload) {
|
||||
return fmt.Errorf("UDP echo payload corruption")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkUDPEcho(c net.Conn, payload, buf []byte) error {
|
||||
if err := c.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
|
||||
return err
|
||||
}
|
||||
if n, err := c.Write(payload); err != nil || n != len(payload) {
|
||||
return fmt.Errorf("UDP write n=%d: %v", n, err)
|
||||
}
|
||||
n, err := c.Read(buf)
|
||||
if err != nil || !bytes.Equal(buf[:n], payload) {
|
||||
return fmt.Errorf("UDP echo n=%d: %v", n, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/txthinking/socks5"
|
||||
)
|
||||
|
||||
// Exercise negotiation, UDP ASSOCIATE, and datagram encoding/decoding through
|
||||
// an independent client, without using our SOCKS wire helpers.
|
||||
func TestSocksUDPThirdPartyClient(t *testing.T) {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("reverse=%t", reverse), func(t *testing.T) {
|
||||
addr, _ := setupSocksUDP(t, reverse, availablePort())
|
||||
for _, target := range []struct {
|
||||
name, network, address, host string
|
||||
}{
|
||||
{"IPv4", "udp4", "127.0.0.1:0", "127.0.0.1"},
|
||||
{"domain", "udp", ":0", "localhost"},
|
||||
{"IPv6", "udp6", "[::1]:0", "::1"},
|
||||
} {
|
||||
t.Run(target.name, func(t *testing.T) {
|
||||
echo := socksUDPEcho(t, target.network, target.address)
|
||||
client, err := socks5.NewClient(addr, "", "", 3, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn, err := client.Dial("udp", net.JoinHostPort(target.host, strconv.Itoa(echo.Port)))
|
||||
if err != nil {
|
||||
t.Fatalf("third-party UDP ASSOCIATE: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
// Inspect the socket the client opened from Chisel's reply.
|
||||
// Every association must use our configured relay port.
|
||||
relay := conn.(*socks5.Client).UDPConn.RemoteAddr().String()
|
||||
if relay != addr {
|
||||
t.Fatalf("third-party client connected to relay %s, want %s", relay, addr)
|
||||
}
|
||||
buf := make([]byte, 65535)
|
||||
for _, size := range []int{1, 1024, 8192} {
|
||||
payload := bytes.Repeat([]byte{'x'}, size)
|
||||
if n, err := conn.Write(payload); err != nil || n != size {
|
||||
t.Fatalf("third-party UDP write (%d bytes): n=%d err=%v", size, n, err)
|
||||
}
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil || !bytes.Equal(buf[:n], payload) {
|
||||
t.Fatalf("third-party UDP echo (%d bytes): n=%d err=%v", size, n, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSocksUDPAssociationLimit(t *testing.T) {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
for _, limit := range []int{2, 100} {
|
||||
t.Run(fmt.Sprintf("reverse=%t/limit=%d", reverse, limit), func(t *testing.T) {
|
||||
value := ""
|
||||
if limit != 100 {
|
||||
value = strconv.Itoa(limit)
|
||||
}
|
||||
t.Setenv("CHISEL_UDP_MAX_CONNS", value)
|
||||
addr, _ := setupSocksUDP(t, reverse, availablePort())
|
||||
echo := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
controls := make([]net.Conn, limit)
|
||||
clients := make([]*net.UDPConn, limit)
|
||||
var relay *net.UDPAddr
|
||||
for i := range controls {
|
||||
clients[i] = socksUDPClient(t, "127.0.0.1")
|
||||
var code byte
|
||||
controls[i], relay, code = socksUDPControl(t, addr, clients[i].LocalAddr().(*net.UDPAddr).Port)
|
||||
if code != 0 || relay.String() != addr {
|
||||
t.Fatalf("association %d/%d code=%d relay=%s", i+1, limit, code, relay)
|
||||
}
|
||||
}
|
||||
extra := socksUDPClient(t, "127.0.0.1")
|
||||
_, _, code := socksUDPControl(t, addr, extra.LocalAddr().(*net.UDPAddr).Port)
|
||||
if code != 1 {
|
||||
t.Fatalf("association %d exceeded cap with reply %d", limit+1, code)
|
||||
}
|
||||
// Exhaustion must preserve existing UDP and ordinary CONNECT.
|
||||
socksUDPRoundTrip(t, clients[0], relay, echo.IP.String(), echo.Port, "at capacity")
|
||||
socksTCPRoundTrip(t, addr)
|
||||
controls[0].Close()
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for {
|
||||
c, nextRelay, code := socksUDPControl(t, addr, extra.LocalAddr().(*net.UDPAddr).Port)
|
||||
if code == 0 {
|
||||
socksUDPRoundTrip(t, extra, nextRelay, echo.IP.String(), echo.Port, "recovered capacity")
|
||||
c.Close()
|
||||
break
|
||||
}
|
||||
c.Close()
|
||||
if code != 1 || time.Now().After(deadline) {
|
||||
t.Fatalf("association capacity did not recover: code=%d", code)
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPDestinationLimit(t *testing.T) {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
for _, limit := range []int{2, 100} {
|
||||
t.Run(fmt.Sprintf("reverse=%t/limit=%d", reverse, limit), func(t *testing.T) {
|
||||
value, idle := "", 2*time.Second
|
||||
if limit != 100 {
|
||||
value, idle = strconv.Itoa(limit), 500*time.Millisecond
|
||||
}
|
||||
t.Setenv("CHISEL_UDP_MAX_CONNS", value)
|
||||
t.Setenv("CHISEL_UDP_DEADLINE", idle.String())
|
||||
addr, _ := setupSocksUDP(t, reverse, availablePort())
|
||||
u := socksUDPClient(t, "127.0.0.1")
|
||||
_, relay, code := socksUDPControl(t, addr, u.LocalAddr().(*net.UDPAddr).Port)
|
||||
if code != 0 {
|
||||
t.Fatal(code)
|
||||
}
|
||||
var first *net.UDPAddr
|
||||
for i := range limit {
|
||||
echo := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
if i == 0 {
|
||||
first = echo
|
||||
}
|
||||
socksUDPRoundTrip(t, u, relay, echo.IP.String(), echo.Port, fmt.Sprintf("destination %d", i))
|
||||
}
|
||||
blocked, observed := socksUDPLimitEcho(t, "udp4", "127.0.0.1:0", 0)
|
||||
packet := append(append([]byte{0, 0, 0}, socksWireAddress(blocked.IP.String(), blocked.Port)...), 'x')
|
||||
assertSocksUDPDrop(t, u, relay, packet, observed)
|
||||
// Existing flows continue; another association has its own cap.
|
||||
socksUDPRoundTrip(t, u, relay, first.IP.String(), first.Port, "existing destination")
|
||||
other := socksUDPClient(t, "127.0.0.1")
|
||||
_, otherRelay, code := socksUDPControl(t, addr, other.LocalAddr().(*net.UDPAddr).Port)
|
||||
if code != 0 {
|
||||
t.Fatal(code)
|
||||
}
|
||||
socksUDPRoundTrip(t, other, otherRelay, blocked.IP.String(), blocked.Port, "independent cap")
|
||||
<-observed
|
||||
if limit == 2 {
|
||||
// Expiration must free capacity without replacing the control.
|
||||
time.Sleep(idle + 150*time.Millisecond)
|
||||
socksUDPRoundTrip(t, u, relay, blocked.IP.String(), blocked.Port, "after idle expiry")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPPacketSizeLimit(t *testing.T) {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
for _, test := range []struct {
|
||||
name, setting, network, address string
|
||||
limit int
|
||||
}{
|
||||
{"small", "64", "udp4", "127.0.0.1:0", 64},
|
||||
{"default", "", "udp4", "127.0.0.1:0", 9012},
|
||||
{"IPv6", "64", "udp6", "[::1]:0", 64},
|
||||
{"clamped", "70000", "udp4", "127.0.0.1:0", 65507},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("reverse=%t/%s", reverse, test.name), func(t *testing.T) {
|
||||
if test.limit == 65507 {
|
||||
// Probe the OS independently of Chisel. macOS defaults to a
|
||||
// smaller UDP send limit; relay failures must still fail below.
|
||||
probe := socksUDPClient(t, "127.0.0.1")
|
||||
_, err := probe.WriteToUDP(make([]byte, test.limit), probe.LocalAddr().(*net.UDPAddr))
|
||||
probe.Close()
|
||||
if errors.Is(err, syscall.EMSGSIZE) ||
|
||||
(runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(10040))) {
|
||||
t.Skipf("OS cannot send a %d-byte UDP datagram: %v", test.limit, err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Setenv("CHISEL_UDP_MAX_SIZE", test.setting)
|
||||
echo, observed := socksUDPLimitEcho(t, test.network, test.address, test.limit)
|
||||
addr, _ := setupSocksUDP(t, reverse, availablePort())
|
||||
u := socksUDPClient(t, "127.0.0.1")
|
||||
_, relay, code := socksUDPControl(t, addr, u.LocalAddr().(*net.UDPAddr).Port)
|
||||
if code != 0 {
|
||||
t.Fatal(code)
|
||||
}
|
||||
header := append([]byte{0, 0, 0}, socksWireAddress(echo.IP.String(), echo.Port)...)
|
||||
boundary := bytes.Repeat([]byte{'x'}, test.limit-len(header))
|
||||
socksUDPRoundTrip(t, u, relay, echo.IP.String(), echo.Port, string(boundary))
|
||||
<-observed
|
||||
if test.limit < 65507 {
|
||||
packet := append(append([]byte{}, header...), append(boundary, 'x')...)
|
||||
assertSocksUDPDrop(t, u, relay, packet, observed)
|
||||
}
|
||||
// This request fits; the endpoint deliberately replies one byte
|
||||
// above the limit. The exit must drop the oversized response.
|
||||
packet := append(append([]byte{}, header...), []byte("oversized reply")...)
|
||||
if _, err := u.WriteToUDP(packet, relay); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case <-observed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("reply-size probe did not reach endpoint")
|
||||
}
|
||||
assertSocksUDPTimeout(t, u)
|
||||
socksUDPRoundTrip(t, u, relay, echo.IP.String(), echo.Port, "valid after oversized packets")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The endpoint records every request, so a missing reply cannot hide an
|
||||
// over-limit request that was incorrectly forwarded by the exit.
|
||||
func socksUDPLimitEcho(t *testing.T, network, address string, maxSize int) (*net.UDPAddr, <-chan []byte) {
|
||||
t.Helper()
|
||||
a, err := net.ResolveUDPAddr(network, address)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := net.ListenUDP(network, a)
|
||||
if err != nil {
|
||||
if network == "udp6" {
|
||||
t.Skipf("IPv6 unavailable: %v", err)
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
observed := make(chan []byte, 4)
|
||||
go func() {
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
n, peer, err := c.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
payload := append([]byte{}, buf[:n]...)
|
||||
select {
|
||||
case observed <- payload:
|
||||
default:
|
||||
}
|
||||
if maxSize > 0 && string(payload) == "oversized reply" {
|
||||
headerLen := 3 + len(socksWireAddress(c.LocalAddr().(*net.UDPAddr).IP.String(), c.LocalAddr().(*net.UDPAddr).Port))
|
||||
payload = bytes.Repeat([]byte{'y'}, maxSize-headerLen+1)
|
||||
}
|
||||
c.WriteToUDP(payload, peer)
|
||||
}
|
||||
}()
|
||||
return c.LocalAddr().(*net.UDPAddr), observed
|
||||
}
|
||||
|
||||
func assertSocksUDPDrop(t *testing.T, u *net.UDPConn, relay *net.UDPAddr, packet []byte, observed <-chan []byte) {
|
||||
t.Helper()
|
||||
if _, err := u.WriteToUDP(packet, relay); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case payload := <-observed:
|
||||
t.Fatalf("over-limit packet reached the destination: %d bytes", len(payload))
|
||||
case <-time.After(75 * time.Millisecond):
|
||||
}
|
||||
assertSocksUDPTimeout(t, u)
|
||||
}
|
||||
|
||||
func assertSocksUDPTimeout(t *testing.T, u *net.UDPConn) {
|
||||
t.Helper()
|
||||
u.SetReadDeadline(time.Now().Add(75 * time.Millisecond))
|
||||
_, _, err := u.ReadFromUDP(make([]byte, 65535))
|
||||
if e, ok := err.(net.Error); !ok || !e.Timeout() {
|
||||
t.Fatalf("expected dropped datagram, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/txthinking/socks5"
|
||||
)
|
||||
|
||||
// Opt-in because this deliberately overloads UDP sockets and the relay queue.
|
||||
// Loss is measured, not asserted: it depends on the host and socket buffers.
|
||||
func TestSocksUDPOverload(t *testing.T) {
|
||||
if os.Getenv("CHISEL_UDP_STRESS") != "1" {
|
||||
t.Skip("set CHISEL_UDP_STRESS=1 to run UDP overload measurements")
|
||||
}
|
||||
t.Setenv("CHISEL_UDP_MAX_SIZE", "")
|
||||
t.Setenv("CHISEL_UDP_MAX_CONNS", "")
|
||||
t.Setenv("CHISEL_UDP_DEADLINE", "")
|
||||
for _, path := range []string{"direct", "forward", "reverse"} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
const packets, size = 20000, 1200
|
||||
echo := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
quietEcho := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
var addr string
|
||||
if path != "direct" {
|
||||
addr, _ = setupSocksUDP(t, path == "reverse", availablePort())
|
||||
}
|
||||
dial := func(destination *net.UDPAddr) net.Conn {
|
||||
var c net.Conn
|
||||
var err error
|
||||
if path == "direct" {
|
||||
c, err = net.Dial("udp4", destination.String())
|
||||
} else {
|
||||
client, e := socks5.NewClient(addr, "", "", 5, 0)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
c, err = client.Dial("udp", destination.String())
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
if err := benchmarkUDPEcho(c, []byte("warmup"), make([]byte, 128)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
c, quiet := dial(echo), dial(quietEcho)
|
||||
var sendingDone atomic.Bool
|
||||
type result struct {
|
||||
latencies []int64
|
||||
duplicate int
|
||||
err error
|
||||
}
|
||||
results := make(chan result, 1)
|
||||
go func() {
|
||||
buf := make([]byte, size+64)
|
||||
tail := bytes.Repeat([]byte{'x'}, size-16)
|
||||
seen := make([]bool, packets)
|
||||
r := result{latencies: make([]int64, 0, packets)}
|
||||
for {
|
||||
c.SetReadDeadline(time.Now().Add(250 * time.Millisecond))
|
||||
n, err := c.Read(buf)
|
||||
if e, ok := err.(net.Error); ok && e.Timeout() {
|
||||
if sendingDone.Load() {
|
||||
results <- r
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil || n != size {
|
||||
r.err = fmt.Errorf("overload reply n=%d: %v", n, err)
|
||||
results <- r
|
||||
return
|
||||
}
|
||||
seq := binary.BigEndian.Uint64(buf[:8])
|
||||
if seq >= packets || !bytes.Equal(buf[16:n], tail) {
|
||||
r.err = fmt.Errorf("corrupted overload reply seq=%d", seq)
|
||||
results <- r
|
||||
return
|
||||
}
|
||||
if seen[seq] {
|
||||
r.duplicate++
|
||||
continue
|
||||
}
|
||||
seen[seq] = true
|
||||
sent := int64(binary.BigEndian.Uint64(buf[8:16]))
|
||||
r.latencies = append(r.latencies, time.Now().UnixNano()-sent)
|
||||
}
|
||||
}()
|
||||
quietDone := make(chan error, 1)
|
||||
go func() {
|
||||
for range 20 {
|
||||
if err := benchmarkUDPEcho(quiet, []byte("unrelated client"), make([]byte, 128)); err != nil {
|
||||
quietDone <- err
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
quietDone <- nil
|
||||
}()
|
||||
payload := bytes.Repeat([]byte{'x'}, size)
|
||||
start := time.Now()
|
||||
c.SetWriteDeadline(start.Add(10 * time.Second))
|
||||
for seq := range packets {
|
||||
binary.BigEndian.PutUint64(payload[:8], uint64(seq))
|
||||
binary.BigEndian.PutUint64(payload[8:16], uint64(time.Now().UnixNano()))
|
||||
if n, err := c.Write(payload); err != nil || n != size {
|
||||
t.Fatalf("overload send %d n=%d: %v", seq, n, err)
|
||||
}
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
sendingDone.Store(true)
|
||||
r := <-results
|
||||
if r.err != nil || r.duplicate != 0 || len(r.latencies) == 0 {
|
||||
t.Fatalf("invalid overload results: received=%d duplicates=%d err=%v", len(r.latencies), r.duplicate, r.err)
|
||||
}
|
||||
if err := <-quietDone; err != nil {
|
||||
t.Fatalf("overload blocked unrelated association: %v", err)
|
||||
}
|
||||
if err := benchmarkUDPEcho(c, []byte("after overload"), make([]byte, 128)); err != nil {
|
||||
t.Fatalf("association did not recover after overload: %v", err)
|
||||
}
|
||||
slices.Sort(r.latencies)
|
||||
t.Logf("sent=%d received=%d loss=%.2f%% offered=%.0f packets/s p50=%.1f us p95=%.1f us; 20 unrelated-client probes and recovery passed",
|
||||
packets, len(r.latencies), 100*float64(packets-len(r.latencies))/packets, float64(packets)/elapsed.Seconds(),
|
||||
float64(r.latencies[len(r.latencies)*50/100])/1000, float64(r.latencies[len(r.latencies)*95/100])/1000)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
chclient "github.com/jpillora/chisel/client"
|
||||
chserver "github.com/jpillora/chisel/server"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
func socksWireAddress(host string, port int) []byte {
|
||||
var b []byte
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
b = append([]byte{1}, v4...)
|
||||
} else {
|
||||
b = append([]byte{4}, ip.To16()...)
|
||||
}
|
||||
} else {
|
||||
b = append([]byte{3, byte(len(host))}, []byte(host)...)
|
||||
}
|
||||
return binary.BigEndian.AppendUint16(b, uint16(port))
|
||||
}
|
||||
|
||||
func readSocksWireAddress(r io.Reader) (string, int, error) {
|
||||
var atyp [1]byte
|
||||
if _, err := io.ReadFull(r, atyp[:]); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
n := 0
|
||||
switch atyp[0] {
|
||||
case 1:
|
||||
n = 4
|
||||
case 4:
|
||||
n = 16
|
||||
case 3:
|
||||
if _, err := io.ReadFull(r, atyp[:]); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
n = int(atyp[0])
|
||||
default:
|
||||
return "", 0, fmt.Errorf("unexpected SOCKS address type %d", atyp[0])
|
||||
}
|
||||
b := make([]byte, n+2)
|
||||
_, err := io.ReadFull(r, b)
|
||||
return net.IP(b[:n]).String(), int(binary.BigEndian.Uint16(b[n:])), err
|
||||
}
|
||||
|
||||
func socksUDPControl(t *testing.T, addr string, sourcePort int) (net.Conn, *net.UDPAddr, byte) {
|
||||
t.Helper()
|
||||
c, err := net.DialTimeout("tcp", addr, time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
c.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
if _, err := c.Write([]byte{5, 1, 0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var greeting [2]byte
|
||||
if _, err := io.ReadFull(c, greeting[:]); err != nil || greeting != [2]byte{5, 0} {
|
||||
t.Fatalf("SOCKS greeting %v: %v", greeting, err)
|
||||
}
|
||||
if _, err := c.Write(append([]byte{5, 3, 0}, socksWireAddress("0.0.0.0", sourcePort)...)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var reply [3]byte
|
||||
if _, err := io.ReadFull(c, reply[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
host, port, err := readSocksWireAddress(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.SetDeadline(time.Time{})
|
||||
return c, &net.UDPAddr{IP: net.ParseIP(host), Port: port}, reply[1]
|
||||
}
|
||||
|
||||
func socksUDPClient(t *testing.T, host string) *net.UDPConn {
|
||||
t.Helper()
|
||||
c, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP(host)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
return c
|
||||
}
|
||||
|
||||
// Reserve UDP on a TCP-available port. Choosing a random UDP port can collide
|
||||
// with an existing TCP socket and make a bind-failure test fail before it
|
||||
// reaches the UDP behavior it is intended to exercise.
|
||||
func socksUDPBlocker(t *testing.T) *net.UDPConn {
|
||||
t.Helper()
|
||||
for range 10 {
|
||||
addr, err := net.ResolveUDPAddr("udp4", "127.0.0.1:"+availablePort())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := net.ListenUDP("udp4", addr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
return c
|
||||
}
|
||||
t.Fatal("cannot reserve UDP on a TCP-available port")
|
||||
return nil
|
||||
}
|
||||
|
||||
func socksUDPEcho(t testing.TB, network, addr string) *net.UDPAddr {
|
||||
t.Helper()
|
||||
a, err := net.ResolveUDPAddr(network, addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := net.ListenUDP(network, a)
|
||||
if err != nil {
|
||||
if network == "udp6" {
|
||||
t.Skipf("IPv6 unavailable: %s", err)
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
go func() {
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
n, peer, err := c.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.WriteToUDP(buf[:n], peer)
|
||||
}
|
||||
}()
|
||||
return c.LocalAddr().(*net.UDPAddr)
|
||||
}
|
||||
|
||||
func socksUDPRoundTrip(t *testing.T, c *net.UDPConn, relay *net.UDPAddr, host string, port int, payload string) {
|
||||
t.Helper()
|
||||
packet := append(append([]byte{0, 0, 0}, socksWireAddress(host, port)...), []byte(payload)...)
|
||||
if _, err := c.WriteToUDP(packet, relay); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
buf := make([]byte, 65535)
|
||||
n, from, err := c.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if from.Port != relay.Port || len(buf[:n]) < 3 || !bytes.Equal(buf[:3], []byte{0, 0, 0}) {
|
||||
t.Fatalf("invalid UDP relay response: %v from %s", buf[:n], from)
|
||||
}
|
||||
r := bytes.NewReader(buf[3:n])
|
||||
_, responsePort, err := readSocksWireAddress(r)
|
||||
if err != nil || responsePort != port {
|
||||
t.Fatalf("responder port %d, want %d: %v", responsePort, port, err)
|
||||
}
|
||||
b, _ := io.ReadAll(r)
|
||||
if string(b) != payload {
|
||||
t.Fatalf("UDP response %q, want %q", b, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func setupSocksUDP(t testing.TB, reverse bool, port string) (string, *chclient.Client) {
|
||||
t.Helper()
|
||||
return setupSocksUDPOn(t, reverse, "127.0.0.1", port)
|
||||
}
|
||||
|
||||
func setupSocksUDPOn(t testing.TB, reverse bool, host, port string) (string, *chclient.Client) {
|
||||
t.Helper()
|
||||
return setupSocksRemoteOn(t, reverse, host, port, "udpsocks")
|
||||
}
|
||||
|
||||
func setupSocksRemoteOn(t testing.TB, reverse bool, host, port, remoteType string) (string, *chclient.Client) {
|
||||
t.Helper()
|
||||
remote := net.JoinHostPort(host, port) + ":" + remoteType
|
||||
if reverse {
|
||||
remote = "R:" + remote
|
||||
}
|
||||
layout := testLayout{
|
||||
server: &chserver.Config{Socks5: !reverse, Reverse: reverse},
|
||||
client: &chclient.Config{Remotes: []string{remote}},
|
||||
}
|
||||
_, c, teardown := layout.setup(t)
|
||||
t.Cleanup(teardown)
|
||||
if host == "0.0.0.0" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
return net.JoinHostPort(host, port), c
|
||||
}
|
||||
|
||||
func TestSocksTCPOnly(t *testing.T) {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
for _, occupied := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("reverse=%t/udpOccupied=%t", reverse, occupied), func(t *testing.T) {
|
||||
port := availablePort()
|
||||
if occupied {
|
||||
blocker := socksUDPBlocker(t)
|
||||
port = strconv.Itoa(blocker.LocalAddr().(*net.UDPAddr).Port)
|
||||
}
|
||||
addr, _ := setupSocksRemoteOn(t, reverse, "127.0.0.1", port, "socks")
|
||||
if !occupied {
|
||||
// A plain SOCKS proxy must leave this UDP port available.
|
||||
u, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
t.Fatalf("socks bound UDP: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { u.Close() })
|
||||
}
|
||||
_, _, code := socksUDPControl(t, addr, 0)
|
||||
if code != 7 {
|
||||
t.Fatalf("plain socks UDP ASSOCIATE code=%d, want unsupported (7)", code)
|
||||
}
|
||||
socksTCPRoundTrip(t, addr)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func socksTCPRoundTrip(t *testing.T, addr string) {
|
||||
t.Helper()
|
||||
endpoint, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer endpoint.Close()
|
||||
go func() {
|
||||
c, err := endpoint.Accept()
|
||||
if err == nil {
|
||||
c.Write([]byte("TCP"))
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
dialer, _ := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
|
||||
c, err := dialer.Dial("tcp", endpoint.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
c.SetDeadline(time.Now().Add(time.Second))
|
||||
b, err := io.ReadAll(c)
|
||||
if err != nil || string(b) != "TCP" {
|
||||
t.Fatalf("TCP response %q: %v", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDP(t *testing.T) {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("reverse=%t", reverse), func(t *testing.T) {
|
||||
addr, _ := setupSocksUDP(t, reverse, availablePort())
|
||||
udp := socksUDPClient(t, "127.0.0.1")
|
||||
_, relay, code := socksUDPControl(t, addr, 0)
|
||||
if code != 0 || relay.String() != addr {
|
||||
t.Fatalf("UDP ASSOCIATE code=%d relay=%s, want %s", code, relay, addr)
|
||||
}
|
||||
v4 := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
socksUDPRoundTrip(t, udp, relay, v4.IP.String(), v4.Port, "IPv4")
|
||||
// A dual-stack endpoint permits whichever localhost address the
|
||||
// exit resolver selects, without needing external DNS/networking.
|
||||
domain := socksUDPEcho(t, "udp", ":0")
|
||||
socksUDPRoundTrip(t, udp, relay, "localhost", domain.Port, "domain")
|
||||
t.Run("IPv6", func(t *testing.T) {
|
||||
v6 := socksUDPEcho(t, "udp6", "[::1]:0")
|
||||
socksUDPRoundTrip(t, udp, relay, "::1", v6.Port, "IPv6")
|
||||
})
|
||||
// TCP and UDP share the SOCKS port and work concurrently.
|
||||
endpoint, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer endpoint.Close()
|
||||
go func() {
|
||||
c, err := endpoint.Accept()
|
||||
if err == nil {
|
||||
c.Write([]byte("TCP"))
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
dialer, _ := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
|
||||
c, err := dialer.Dial("tcp", endpoint.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
c.SetDeadline(time.Now().Add(time.Second))
|
||||
b, err := io.ReadAll(c)
|
||||
if err != nil || string(b) != "TCP" {
|
||||
t.Fatalf("TCP response %q: %v", b, err)
|
||||
}
|
||||
socksUDPRoundTrip(t, udp, relay, v4.IP.String(), v4.Port, "still UDP")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPAssociations(t *testing.T) {
|
||||
addr, _ := setupSocksUDP(t, false, availablePort())
|
||||
echo := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
u1, u2 := socksUDPClient(t, "127.0.0.1"), socksUDPClient(t, "127.0.0.1")
|
||||
c1, relay, code := socksUDPControl(t, addr, 0)
|
||||
if code != 0 {
|
||||
t.Fatalf("first pending association rejected: %d", code)
|
||||
}
|
||||
_, _, code = socksUDPControl(t, addr, 0)
|
||||
if code != 1 {
|
||||
t.Fatalf("ambiguous pending association accepted: %d", code)
|
||||
}
|
||||
socksUDPRoundTrip(t, u1, relay, echo.IP.String(), echo.Port, "one")
|
||||
c2, relay2, code := socksUDPControl(t, addr, 0)
|
||||
if code != 0 {
|
||||
t.Fatalf("second association rejected after pinning: %d", code)
|
||||
}
|
||||
if relay.String() != addr || relay2.String() != addr {
|
||||
t.Fatalf("associations must share configured UDP port %s, got %s and %s", addr, relay, relay2)
|
||||
}
|
||||
socksUDPRoundTrip(t, u2, relay, echo.IP.String(), echo.Port, "two")
|
||||
socksUDPRoundTrip(t, u1, relay, echo.IP.String(), echo.Port, "one again")
|
||||
_, _, code = socksUDPControl(t, addr, u1.LocalAddr().(*net.UDPAddr).Port)
|
||||
if code != 1 {
|
||||
t.Fatalf("conflicting reservation accepted: %d", code)
|
||||
}
|
||||
c1.Close()
|
||||
// Wait for the source-port reservation to be released by control EOF.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for {
|
||||
c, _, result := socksUDPControl(t, addr, u1.LocalAddr().(*net.UDPAddr).Port)
|
||||
if result == 0 {
|
||||
c.Close()
|
||||
break
|
||||
}
|
||||
c.Close()
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("closed control retained its reservation")
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
socksUDPRoundTrip(t, u2, relay, echo.IP.String(), echo.Port, "surviving association")
|
||||
c2.Close()
|
||||
}
|
||||
|
||||
func TestSocksUDPRejectsUnassociatedAndMalformedPackets(t *testing.T) {
|
||||
addr, _ := setupSocksUDP(t, false, availablePort())
|
||||
echo := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
allowed, blocked := socksUDPClient(t, "127.0.0.1"), socksUDPClient(t, "127.0.0.1")
|
||||
_, relay, code := socksUDPControl(t, addr, allowed.LocalAddr().(*net.UDPAddr).Port)
|
||||
if code != 0 {
|
||||
t.Fatalf("declared source port rejected: %d", code)
|
||||
}
|
||||
packet := append(append([]byte{0, 0, 0}, socksWireAddress(echo.IP.String(), echo.Port)...), 'x')
|
||||
if _, err := blocked.WriteToUDP(packet, relay); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
blocked.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
if _, _, err := blocked.ReadFromUDP(make([]byte, 100)); err == nil {
|
||||
t.Fatal("unassociated source received a response")
|
||||
}
|
||||
for _, invalid := range [][]byte{{0}, {0, 0, 0, 2}, append([]byte{0, 0, 1}, packet[3:]...), append([]byte{1, 0, 0}, packet[3:]...), make([]byte, 10000)} {
|
||||
allowed.WriteToUDP(invalid, relay)
|
||||
}
|
||||
allowed.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
if _, _, err := allowed.ReadFromUDP(make([]byte, 11000)); err == nil {
|
||||
t.Fatal("malformed packet received a response")
|
||||
}
|
||||
socksUDPRoundTrip(t, allowed, relay, echo.IP.String(), echo.Port, "valid after invalid")
|
||||
}
|
||||
|
||||
func TestSocksUDPBindFailureKeepsTCP(t *testing.T) {
|
||||
blocker := socksUDPBlocker(t)
|
||||
port := strconv.Itoa(blocker.LocalAddr().(*net.UDPAddr).Port)
|
||||
addr, _ := setupSocksUDP(t, false, port)
|
||||
_, _, code := socksUDPControl(t, addr, 0)
|
||||
if code != 1 {
|
||||
t.Fatalf("UDP bind failure reply %d, want 1", code)
|
||||
}
|
||||
// The greeting and CONNECT must still reach the exit's existing TCP engine.
|
||||
endpoint, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer endpoint.Close()
|
||||
go func() {
|
||||
c, err := endpoint.Accept()
|
||||
if err == nil {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
dialer, _ := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
|
||||
c, err := dialer.Dial("tcp", endpoint.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func TestSocksUDPDisconnectClosesControl(t *testing.T) {
|
||||
addr, client := setupSocksUDP(t, false, availablePort())
|
||||
control, _, code := socksUDPControl(t, addr, 0)
|
||||
if code != 0 {
|
||||
t.Fatal(code)
|
||||
}
|
||||
client.Close()
|
||||
control.SetReadDeadline(time.Now().Add(time.Second))
|
||||
_, err := control.Read(make([]byte, 1))
|
||||
if err != io.EOF {
|
||||
t.Fatalf("control did not close on shutdown: %v", err)
|
||||
}
|
||||
// Both ports must be released after the proxy has stopped.
|
||||
tcp, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
t.Fatalf("TCP port retained: %v", err)
|
||||
}
|
||||
tcp.Close()
|
||||
udp, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
t.Fatalf("UDP port retained: %v", err)
|
||||
}
|
||||
udp.Close()
|
||||
}
|
||||
|
||||
func TestSocksUDPChannelACL(t *testing.T) {
|
||||
for _, grant := range []string{`^socks$`, `^127\.0\.0\.1:80$`} {
|
||||
t.Run(grant, func(t *testing.T) {
|
||||
addr := startSocksACLServer(t, "socks-udp-acl", grant)
|
||||
sc, _, _ := dialChiselSSH(t, addr, "user", "pass")
|
||||
defer sc.Close()
|
||||
sendConfig(t, sc, nil)
|
||||
ch, reqs, err := sc.OpenChannel("chisel", []byte("socks-udp-v1"))
|
||||
if grant != `^socks$` {
|
||||
if err == nil {
|
||||
ch.Close()
|
||||
t.Fatal("UDP channel bypassed socks ACL")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go ssh.DiscardRequests(reqs)
|
||||
defer ch.Close()
|
||||
})
|
||||
}
|
||||
server, err := chserver.NewServer(&chserver.Config{Socks5: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server.AddUser("user", "pass", `^socks$`)
|
||||
port := availablePort()
|
||||
if err := server.Start("127.0.0.1", port); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.Close()
|
||||
sc, _, _ := dialChiselSSH(t, "127.0.0.1:"+port, "user", "pass")
|
||||
defer sc.Close()
|
||||
sendConfig(t, sc, nil)
|
||||
ch, reqs, err := sc.OpenChannel("chisel", []byte("socks-udp-v1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go ssh.DiscardRequests(reqs)
|
||||
defer ch.Close()
|
||||
ready := make([]byte, len("chisel-socks-udp-v1\n"))
|
||||
if _, err := io.ReadFull(ch, ready); err != nil || string(ready) != "chisel-socks-udp-v1\n" {
|
||||
t.Fatalf("exit did not confirm UDP support: %q %v", ready, err)
|
||||
}
|
||||
echo := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
roundTripSocksUDPChannel(t, ch, echo, "before removal")
|
||||
server.DeleteUser("user")
|
||||
if next, _, err := sc.OpenChannel("chisel", []byte("socks-udp-v1")); err == nil {
|
||||
next.Close()
|
||||
t.Fatal("removed user opened a UDP channel")
|
||||
}
|
||||
roundTripSocksUDPChannel(t, ch, echo, "existing association after removal")
|
||||
}
|
||||
|
||||
func roundTripSocksUDPChannel(t *testing.T, ch ssh.Channel, echo *net.UDPAddr, payload string) {
|
||||
t.Helper()
|
||||
packet := append(append([]byte{0, 0, 0}, socksWireAddress(echo.IP.String(), echo.Port)...), []byte(payload)...)
|
||||
frame := binary.BigEndian.AppendUint32(nil, uint32(len(packet)))
|
||||
if _, err := ch.Write(append(frame, packet...)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type result struct {
|
||||
packet []byte
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
var header [4]byte
|
||||
if _, err := io.ReadFull(ch, header[:]); err != nil {
|
||||
done <- result{err: err}
|
||||
return
|
||||
}
|
||||
size := binary.BigEndian.Uint32(header[:])
|
||||
if size > 9012 {
|
||||
done <- result{err: fmt.Errorf("oversized reply %d", size)}
|
||||
return
|
||||
}
|
||||
b := make([]byte, size)
|
||||
_, err := io.ReadFull(ch, b)
|
||||
done <- result{b, err}
|
||||
}()
|
||||
select {
|
||||
case r := <-done:
|
||||
if r.err != nil || !bytes.Equal(r.packet, packet) {
|
||||
t.Fatalf("UDP channel response %v: %v", r.packet, r.err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("UDP channel timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPConcurrentDestinations(t *testing.T) {
|
||||
addr, _ := setupSocksUDP(t, false, availablePort())
|
||||
u := socksUDPClient(t, "127.0.0.1")
|
||||
_, relay, code := socksUDPControl(t, addr, 0)
|
||||
if code != 0 {
|
||||
t.Fatal(code)
|
||||
}
|
||||
echoes := []*net.UDPAddr{socksUDPEcho(t, "udp4", "127.0.0.1:0"), socksUDPEcho(t, "udp4", "127.0.0.1:0")}
|
||||
const packets = 12 // stay below the deliberately bounded queue
|
||||
for i := 0; i < packets; i++ {
|
||||
echo := echoes[i%len(echoes)]
|
||||
packet := append(append([]byte{0, 0, 0}, socksWireAddress(echo.IP.String(), echo.Port)...), []byte(fmt.Sprint(i))...)
|
||||
if _, err := u.WriteToUDP(packet, relay); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
u.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
seen := make(map[int]bool)
|
||||
for i := 0; i < packets; i++ {
|
||||
b := make([]byte, 100)
|
||||
n, _, err := u.ReadFromUDP(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := bytes.NewReader(b[3:n])
|
||||
_, port, err := readSocksWireAddress(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, _ := io.ReadAll(r)
|
||||
id, err := strconv.Atoi(string(payload))
|
||||
if err != nil || id < 0 || id >= packets || seen[id] || port != echoes[id%len(echoes)].Port {
|
||||
t.Fatalf("misrouted response %q from port %d", payload, port)
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPListenerAddresses(t *testing.T) {
|
||||
for _, host := range []string{"0.0.0.0", "::1"} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
if host == "::1" {
|
||||
probe, err := net.Listen("tcp6", "[::1]:0")
|
||||
if err != nil {
|
||||
t.Skipf("IPv6 unavailable: %v", err)
|
||||
}
|
||||
probe.Close()
|
||||
}
|
||||
addr, _ := setupSocksUDPOn(t, false, host, availablePort())
|
||||
controlHost, _, _ := net.SplitHostPort(addr)
|
||||
u := socksUDPClient(t, controlHost)
|
||||
_, relay, code := socksUDPControl(t, addr, 0)
|
||||
if code != 0 || relay.String() != addr {
|
||||
t.Fatalf("relay %s code %d, want concrete address %s", relay, code, addr)
|
||||
}
|
||||
echo := socksUDPEcho(t, "udp4", "127.0.0.1:0")
|
||||
socksUDPRoundTrip(t, u, relay, echo.IP.String(), echo.Port, "listener address")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPDisabledExit(t *testing.T) {
|
||||
port := availablePort()
|
||||
layout := testLayout{server: &chserver.Config{}, client: &chclient.Config{Remotes: []string{"127.0.0.1:" + port + ":udpsocks"}}}
|
||||
_, _, teardown := layout.setup(t)
|
||||
defer teardown()
|
||||
_, _, code := socksUDPControl(t, "127.0.0.1:"+port, 0)
|
||||
if code != 2 {
|
||||
t.Fatalf("SOCKS-disabled exit reply %d, want ruleset failure", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksLegacyTCPChannel(t *testing.T) {
|
||||
addr := startSocksACLServer(t, "legacy-socks-tcp", `^socks$`)
|
||||
sc, _, _ := dialChiselSSH(t, addr, "user", "pass")
|
||||
defer sc.Close()
|
||||
sendConfig(t, sc, nil)
|
||||
ch, reqs, err := sc.OpenChannel("chisel", []byte("socks"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go ssh.DiscardRequests(reqs)
|
||||
defer ch.Close()
|
||||
endpoint, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer endpoint.Close()
|
||||
go func() {
|
||||
c, err := endpoint.Accept()
|
||||
if err == nil {
|
||||
c.Write([]byte("old client"))
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
ch.Write([]byte{5, 1, 0})
|
||||
var greeting [2]byte
|
||||
if _, err := io.ReadFull(ch, greeting[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := endpoint.Addr().(*net.TCPAddr)
|
||||
ch.Write(append([]byte{5, 1, 0}, socksWireAddress(a.IP.String(), a.Port)...))
|
||||
var header [3]byte
|
||||
if _, err := io.ReadFull(ch, header[:]); err != nil || header[1] != 0 {
|
||||
t.Fatalf("CONNECT reply %v: %v", header, err)
|
||||
}
|
||||
if _, _, err := readSocksWireAddress(ch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := make([]byte, len("old client"))
|
||||
if _, err := io.ReadFull(ch, b); err != nil || string(b) != "old client" {
|
||||
t.Fatalf("legacy TCP response %q: %v", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksTCPHalfClose(t *testing.T) {
|
||||
addr, _ := setupSocksUDP(t, false, availablePort())
|
||||
endpoint, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer endpoint.Close()
|
||||
go func() {
|
||||
c, err := endpoint.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
c.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
b, err := io.ReadAll(c)
|
||||
if err == nil {
|
||||
c.Write(append(b, '!'))
|
||||
}
|
||||
}()
|
||||
dialer, _ := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
|
||||
c, err := dialer.Dial("tcp", endpoint.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
c.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
if _, err := c.Write([]byte("half-close")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.(*net.TCPConn).CloseWrite(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := io.ReadAll(c)
|
||||
if err != nil || string(b) != "half-close!" {
|
||||
t.Fatalf("SOCKS half-close response %q: %v", b, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TFTP starts at one server port, receives DATA from a new transfer port,
|
||||
// and sends ACKs to that port while retaining its own transfer ID (source
|
||||
// port). Exercise two associations to ensure their exit sockets stay isolated.
|
||||
func TestSocksUDPTransferPort(t *testing.T) {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
for _, kind := range []string{"udp4", "udp6", "different-IP"} {
|
||||
t.Run(fmt.Sprintf("reverse=%t/%s", reverse, kind), func(t *testing.T) {
|
||||
network := kind
|
||||
if kind == "different-IP" {
|
||||
network = "udp4"
|
||||
}
|
||||
host := "127.0.0.1"
|
||||
if network == "udp6" {
|
||||
host = "::1"
|
||||
}
|
||||
listen := func() *net.UDPConn {
|
||||
c, err := net.ListenUDP(network, &net.UDPAddr{IP: net.ParseIP(host)})
|
||||
if err != nil {
|
||||
if network == "udp6" {
|
||||
t.Skipf("IPv6 unavailable: %v", err)
|
||||
}
|
||||
if host == "127.0.0.2" && (errors.Is(err, syscall.EADDRNOTAVAIL) ||
|
||||
(runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(10049)))) {
|
||||
t.Skipf("alternate loopback unavailable: %v", err)
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { c.Close() })
|
||||
return c
|
||||
}
|
||||
service := listen()
|
||||
if kind == "different-IP" {
|
||||
host = "127.0.0.2"
|
||||
}
|
||||
transfer := listen()
|
||||
serviceAddr := service.LocalAddr().(*net.UDPAddr)
|
||||
transferAddr := transfer.LocalAddr().(*net.UDPAddr)
|
||||
proxyAddr, _ := setupSocksUDP(t, reverse, availablePort())
|
||||
clients := [2]*net.UDPConn{}
|
||||
relays := [2]*net.UDPAddr{}
|
||||
peers := [2]*net.UDPAddr{}
|
||||
read := func(c *net.UDPConn, want []byte) *net.UDPAddr {
|
||||
t.Helper()
|
||||
c.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
buf := make([]byte, 1024)
|
||||
n, from, err := c.ReadFromUDP(buf)
|
||||
if err != nil || !bytes.Equal(buf[:n], want) {
|
||||
t.Fatalf("datagram %x, want %x: %v", buf[:n], want, err)
|
||||
}
|
||||
return from
|
||||
}
|
||||
send := func(c *net.UDPConn, to *net.UDPAddr, data []byte) {
|
||||
t.Helper()
|
||||
if _, err := c.WriteToUDP(data, to); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
request := func(i int, to *net.UDPAddr, data []byte) {
|
||||
packet := append(append([]byte{0, 0, 0}, socksWireAddress(to.IP.String(), to.Port)...), data...)
|
||||
send(clients[i], relays[i], packet)
|
||||
}
|
||||
rrq := []byte("\x00\x01test\x00octet\x00")
|
||||
for i := range clients {
|
||||
clients[i] = socksUDPClient(t, "127.0.0.1")
|
||||
_, relay, code := socksUDPControl(t, proxyAddr, clients[i].LocalAddr().(*net.UDPAddr).Port)
|
||||
if code != 0 {
|
||||
t.Fatalf("ASSOCIATE failed: %d", code)
|
||||
}
|
||||
relays[i] = relay
|
||||
request(i, serviceAddr, rrq)
|
||||
peers[i] = read(service, rrq)
|
||||
}
|
||||
if peers[0].String() == peers[1].String() {
|
||||
t.Fatal("associations share an exit source socket")
|
||||
}
|
||||
for block := uint16(1); block <= 2; block++ {
|
||||
data := binary.BigEndian.AppendUint16([]byte{0, 3}, block)
|
||||
if block == 1 {
|
||||
data = append(data, bytes.Repeat([]byte{'x'}, 512)...)
|
||||
} else {
|
||||
data = append(data, 'y') // final short DATA block
|
||||
}
|
||||
for i := range clients {
|
||||
send(transfer, peers[i], data)
|
||||
clients[i].SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
buf := make([]byte, 1024)
|
||||
n, relay, err := clients[i].ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("DATA block %d reply from new server port: %v", block, err)
|
||||
}
|
||||
if relay.String() != relays[i].String() || n < 3 || !bytes.Equal(buf[:3], []byte{0, 0, 0}) {
|
||||
t.Fatalf("invalid relay packet from %s: %x", relay, buf[:n])
|
||||
}
|
||||
r := bytes.NewReader(buf[3:n])
|
||||
host, port, err := readSocksWireAddress(r)
|
||||
if err != nil || host != transferAddr.IP.String() || port != transferAddr.Port {
|
||||
t.Fatalf("reply source %s:%d, want %s: %v", host, port, transferAddr, err)
|
||||
}
|
||||
payload, _ := io.ReadAll(r)
|
||||
if !bytes.Equal(payload, data) {
|
||||
t.Fatalf("wrong DATA block: %x", payload)
|
||||
}
|
||||
ack := binary.BigEndian.AppendUint16([]byte{0, 4}, block)
|
||||
request(i, &net.UDPAddr{IP: net.ParseIP(host), Port: port}, ack)
|
||||
if peer := read(transfer, ack); peer.String() != peers[i].String() {
|
||||
t.Fatalf("source changed during transfer: %s -> %s", peers[i], peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksUDPSourcePortSurvivesIdle(t *testing.T) {
|
||||
for _, reverse := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("reverse=%t", reverse), func(t *testing.T) {
|
||||
t.Setenv("CHISEL_UDP_DEADLINE", "50ms")
|
||||
addr, _ := setupSocksUDP(t, reverse, availablePort())
|
||||
client := socksUDPClient(t, "127.0.0.1")
|
||||
service := socksUDPClient(t, "127.0.0.1")
|
||||
_, relay, code := socksUDPControl(t, addr, 0)
|
||||
if code != 0 {
|
||||
t.Fatal(code)
|
||||
}
|
||||
to := service.LocalAddr().(*net.UDPAddr)
|
||||
packet := append(append([]byte{0, 0, 0}, socksWireAddress(to.IP.String(), to.Port)...), 'x')
|
||||
probe := func() *net.UDPAddr {
|
||||
if _, err := client.WriteToUDP(packet, relay); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
_, peer, err := service.ReadFromUDP(make([]byte, 32))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return peer
|
||||
}
|
||||
before := probe()
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if after := probe(); after.String() != before.String() {
|
||||
t.Fatalf("idle expiration changed source: %s -> %s", before, after)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user