Skip to content

Commit 9917095

Browse files
author
yosebyte
authored
Add SOCKS5 support for outbound dialing
1 parent 0283ceb commit 9917095

27 files changed

Lines changed: 1603 additions & 95 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ The default `tls=1` creates an ephemeral self-signed certificate. Long-lived
6060
deployments should use `tls=2` with a PEM certificate and private key:
6161

6262
```bash
63-
nowhere 'portal://change-me@:2077?spec=nightfall&tls=2&crt=/etc/nowhere/cert.pem&key=/etc/nowhere/key.pem'
63+
nowhere 'portal://change-me@:2077?tls=2&crt=/etc/nowhere/cert.pem&key=/etc/nowhere/key.pem&spec=nightfall'
6464
```
6565

6666
## Ecosystem

docs/configuration.md

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,46 @@
33
The Portal is configured by one URL.
44

55
```text
6-
portal://<shared-key>@<listen-host>:<listen-port>?tls=<mode>&spec=<spec>&alpn=<alpn>&net=<mode>&dial=<ip-or-auto>&rate=<mbps>&etar=<mbps>&crt=<path>&key=<path>&log=<level>
6+
portal://<shared-key>@<listen-host>:<listen-port>?log=<level>&tls=<mode>&crt=<path>&key=<path>&net=<mode>&spec=<spec>&alpn=<alpn>&rate=<mbps>&etar=<mbps>&dial=<ip-or-auto>&socks=<proxy>
77
```
88

99
The URL username is the shared key. A password component is not supported. The
1010
listen port and a non-empty shared key are required. Unknown query parameters
1111
are ignored.
1212

1313
Use percent encoding for reserved URL characters in the shared key, `spec`,
14-
`alpn`, and file paths.
14+
`alpn`, SOCKS credentials, and file paths.
1515

1616
## Input Rules
1717

1818
The shared key, `spec`, and `alpn` are percent-decoded as UTF-8. A literal `+`
1919
in `spec` or `alpn` remains `+`; it is not converted to a space. If a query key
20-
appears more than once, the first occurrence is used.
20+
appears more than once, the first occurrence is used, except that duplicate
21+
`socks` parameters are rejected.
2122

2223
| Input | Requirement | Decoded UTF-8 byte length |
2324
| --- | --- | --- |
2425
| shared key | Required and non-empty | `1..255` |
2526
| `spec` | Optional; empty means omitted | `1..255` when non-empty |
2627
| `alpn` | Optional; empty means omitted | `1..255` when non-empty |
28+
| SOCKS username | Required when SOCKS authentication is configured | `1..255` |
29+
| SOCKS password | Required when SOCKS authentication is configured | `1..255` |
2730

2831
## Parameters
2932

3033
| Parameter | Default | Semantics |
3134
| --- | --- | --- |
35+
| `log` | `info` | `none`, `debug`, `info`, `warn`, `error`, or `event`. An unknown value selects `info`. |
3236
| `tls` | `1` | `1` creates an in-memory self-signed certificate. `2` loads PEM files from `crt` and `key`. `0` and all other values are rejected. |
37+
| `crt` | Empty | PEM certificate chain used by `tls=2`. |
38+
| `key` | Empty | PEM private key used by `tls=2`. |
39+
| `net` | `mix` | Selects ingress transports: `tcp` enables TLS/TCP, `udp` enables QUIC/UDP, and `mix` enables both. Missing and empty values select `mix`. |
3340
| `spec` | `auto` | Seed for v1 authentication material, deterministic padding, and field order. |
3441
| `alpn` | `now/1` | TLS and QUIC ALPN value. It does not alter authentication, padding, or frame layout. |
35-
| `net` | `mix` | Selects ingress transports: `tcp` enables TLS/TCP, `udp` enables QUIC/UDP, and `mix` enables both. Missing and empty values select `mix`. |
36-
| `dial` | `auto` | Local IP literal for outbound TCP and UDP sockets. Empty, invalid, hostname, and `auto` values select the operating-system default. |
3742
| `rate` | `0` | Client-to-target traffic limit in Mbps. |
3843
| `etar` | `0` | Target-to-client traffic limit in Mbps. |
39-
| `crt` | Empty | PEM certificate chain used by `tls=2`. |
40-
| `key` | Empty | PEM private key used by `tls=2`. |
41-
| `log` | `info` | `none`, `debug`, `info`, `warn`, `error`, or `event`. An unknown value selects `info`. |
44+
| `dial` | `auto` | Local IP literal for outbound TCP and UDP sockets. Empty, invalid, hostname, and `auto` values select the operating-system default. |
45+
| `socks` | `none` | SOCKS5 outbound proxy as `host:port` or `user:pass@host:port`. Missing, empty, and `none` disable proxying. IPv6 endpoints require brackets. |
4246

4347
`rate` and `etar` accept positive decimal integers. Zero, a negative value, an
4448
invalid value, or omission disables the corresponding direction. The conversion
@@ -135,6 +139,36 @@ invalid address lets the operating system select the source address. When an IP
135139
is set, the Portal considers only target addresses from the same address
136140
family.
137141

142+
When `socks` is enabled, `dial` instead binds the TCP control/CONNECT
143+
connections and UDP relay sockets used to reach the proxy. It does not bind a
144+
direct connection to the final target.
145+
146+
## SOCKS5 Outbound Proxy
147+
148+
SOCKS5 proxying applies to every outbound target, including loopback and
149+
private addresses:
150+
151+
```text
152+
portal://secret@:2077?socks=proxy.example:1080
153+
portal://secret@:2077?socks=user:pass@proxy.example:1080
154+
portal://secret@:2077?socks=user:p%40ss@[2001:db8::10]:1080
155+
```
156+
157+
Without credentials, the Portal offers only the SOCKS5 no-authentication
158+
method. With credentials, it offers only username/password authentication and
159+
does not permit a downgrade to no authentication. Target hostnames are sent to
160+
the proxy without local resolution. The proxy endpoint itself is resolved by
161+
the Portal.
162+
163+
TCP relays use CONNECT. Each QUIC DATAGRAM or UoT flow owns a separate UDP
164+
ASSOCIATE control connection and relay socket. SOCKS5 UDP fragmentation is not
165+
supported; fragmented responses are discarded. A proxy error closes the
166+
current flow and never falls back to a direct target connection.
167+
168+
The startup URL displays only the SOCKS endpoint. Credentials are omitted, so
169+
an authenticated startup URL is intentionally not a round-trippable copy of
170+
the command line.
171+
138172
## Logging Level
139173

140174
The default level is `info`:
@@ -175,11 +209,17 @@ portal://secret@:2077?net=udp
175209
PEM certificate with event logs:
176210

177211
```text
178-
portal://secret@:2077?tls=2&crt=/etc/nowhere/cert.pem&key=/etc/nowhere/key.pem&log=event
212+
portal://secret@:2077?log=event&tls=2&crt=/etc/nowhere/cert.pem&key=/etc/nowhere/key.pem
179213
```
180214

181215
Directional limits:
182216

183217
```text
184218
portal://secret@:2077?rate=100&etar=200
185219
```
220+
221+
Authenticated SOCKS5 outbound routing:
222+
223+
```text
224+
portal://secret@:2077?socks=user:pass@proxy.example:1080
225+
```

docs/integrations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ For a long-lived Portal, use a stable certificate and an explicit public
6767
configuration:
6868

6969
```text
70-
portal://change-me@:2077?tls=2&crt=/etc/nowhere/cert.pem&key=/etc/nowhere/key.pem&spec=nightfall&alpn=now%2F1&net=mix
70+
portal://change-me@:2077?tls=2&crt=/etc/nowhere/cert.pem&key=/etc/nowhere/key.pem&net=mix&spec=nightfall&alpn=now%2F1
7171
```
7272

7373
Certificate and private-key paths are resolved by the Nowhere child process
@@ -172,7 +172,7 @@ A Portal using `net=mix` accepts either client mode. A Portal restricted to
172172
Run a public Portal with a stable certificate:
173173

174174
```text
175-
portal://change-me@:2077?tls=2&crt=/etc/nowhere/cert.pem&key=/etc/nowhere/key.pem&spec=nightfall&alpn=now%2F1&net=mix
175+
portal://change-me@:2077?tls=2&crt=/etc/nowhere/cert.pem&key=/etc/nowhere/key.pem&net=mix&spec=nightfall&alpn=now%2F1
176176
```
177177

178178
Import a QUIC/UDP client configuration in Anywhere:

docs/operations.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,30 @@ and UDP listeners must both bind successfully. The process logs the effective
99
startup URL after defaults are applied:
1010

1111
```text
12-
portal::run: starting: portal://:<port>?tls=1&spec=auto&alpn=now/1&net=mix&dial=auto&rate=0&etar=0
12+
portal::run: starting: portal://:<port>?tls=1&net=mix&spec=auto&alpn=now/1&rate=0&etar=0&dial=auto&socks=none
1313
```
1414

1515
Use this line when comparing intended configuration with runtime behavior. It
16-
prints effective values, not necessarily the exact command-line URL.
16+
prints effective values, not necessarily the exact command-line URL. When
17+
SOCKS5 authentication is configured, this line prints only the proxy endpoint
18+
and never prints the username or password.
19+
20+
## SOCKS5 Outbound Behavior
21+
22+
When `socks` is configured, all TCP and UDP target traffic is routed through
23+
that SOCKS5 server. TCP uses CONNECT. Every QUIC DATAGRAM and UoT flow uses its
24+
own UDP ASSOCIATE control connection and relay socket. Closing the control
25+
connection closes the associated flow.
26+
27+
Target domain names are resolved by the SOCKS5 server. The Portal resolves only
28+
the proxy endpoint and any domain returned by the proxy for its UDP relay. If
29+
`dial` specifies a local IP, proxy addresses and relay sockets must use the same
30+
address family.
31+
32+
Proxy connection, authentication, command, or relay failures close only the
33+
affected Nowhere flow. The Portal never bypasses the configured proxy by
34+
falling back to a direct target connection. TCP proxy setup shares
35+
`NOW_TCP_DIAL_TIMEOUT`; UDP proxy setup shares `NOW_UDP_DIAL_TIMEOUT`.
1736

1837
## Logging
1938

docs/protocol.md

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ and **MAY** are to be interpreted as normative requirements.
3333
The Portal is configured with one URL:
3434

3535
```text
36-
portal://<shared-key>@<listen-host>:<listen-port>?tls=<mode>&spec=<spec>&alpn=<alpn>&net=<mode>&dial=<ip-or-auto>&rate=<mbps>&etar=<mbps>&crt=<path>&key=<path>&log=<level>
36+
portal://<shared-key>@<listen-host>:<listen-port>?log=<level>&tls=<mode>&crt=<path>&key=<path>&net=<mode>&spec=<spec>&alpn=<alpn>&rate=<mbps>&etar=<mbps>&dial=<ip-or-auto>&socks=<proxy>
3737
```
3838

3939
Minimal configuration:
@@ -50,28 +50,32 @@ Unknown query parameters are ignored.
5050

5151
The shared key, `spec`, and `alpn` are percent-decoded as UTF-8. A literal `+`
5252
in `spec` or `alpn` remains `+`; it is not converted to a space. If a query key
53-
occurs more than once, the first occurrence is used.
53+
occurs more than once, the first occurrence is used. Duplicate `socks`
54+
parameters MUST be rejected.
5455

5556
| Input | Requirement | Decoded UTF-8 byte length |
5657
| --- | --- | --- |
5758
| Shared key | Required and non-empty | `1..255` |
5859
| `spec` | Optional; empty is treated as omitted | `1..255` when non-empty |
5960
| `alpn` | Optional; empty is treated as omitted | `1..255` when non-empty |
61+
| SOCKS username | Required when SOCKS authentication is configured | `1..255` |
62+
| SOCKS password | Required when SOCKS authentication is configured | `1..255` |
6063

6164
### 3.2 Parameters
6265

6366
| Parameter | Default | Semantics |
6467
| --- | --- | --- |
68+
| `log` | `info` | `none`, `debug`, `info`, `warn`, `error`, or `event`. An unknown value selects `info`. |
6569
| `tls` | `1` | `1` creates a self-signed certificate. `2` loads PEM files from `crt` and `key`. Other values are invalid. |
70+
| `crt` | Empty | PEM certificate chain used by `tls=2`. |
71+
| `key` | Empty | PEM private key used by `tls=2`. |
72+
| `net` | `mix` | Selects ingress transports. `tcp` enables TLS/TCP, `udp` enables QUIC/UDP, and `mix` enables both. Missing and empty values select `mix`; other values are invalid. |
6673
| `spec` | `auto` | Deterministic seed for v1 authentication, padding, and frame layouts. |
6774
| `alpn` | `now/1` | QUIC/TLS ALPN override. It does not alter any other protocol field. |
68-
| `net` | `mix` | Selects ingress transports. `tcp` enables TLS/TCP, `udp` enables QUIC/UDP, and `mix` enables both. Missing and empty values select `mix`; other values are invalid. |
69-
| `dial` | `auto` | Local IP literal for outbound TCP and UDP sockets. Empty, invalid, hostname, and `auto` values select the operating-system default. |
7075
| `rate` | `0` | Client-to-target rate limit in Mbps. |
7176
| `etar` | `0` | Target-to-client rate limit in Mbps. |
72-
| `crt` | Empty | PEM certificate chain used by `tls=2`. |
73-
| `key` | Empty | PEM private key used by `tls=2`. |
74-
| `log` | `info` | `none`, `debug`, `info`, `warn`, `error`, or `event`. An unknown value selects `info`. |
77+
| `dial` | `auto` | Local IP literal for outbound TCP and UDP sockets. Empty, invalid, hostname, and `auto` values select the operating-system default. |
78+
| `socks` | `none` | SOCKS5 proxy as `host:port` or `user:pass@host:port`. Missing, empty, and `none` disable proxying. |
7579

7680
`rate` and `etar` accept positive decimal integers. Zero, a negative value, an
7781
invalid value, or omission disables the corresponding limit. The conversion is:
@@ -87,6 +91,12 @@ has independent client-to-target and target-to-client buckets.
8791
relay and UoT. QUIC supports TCP relay on bidirectional streams and UDP relay
8892
in DATAGRAM frames.
8993

94+
When `socks` is configured, every target MUST use that proxy. TCP uses SOCKS5
95+
CONNECT. Each UDP flow uses a separate UDP ASSOCIATE and retains its control
96+
connection for the flow lifetime. Target domain names are encoded in SOCKS5
97+
requests and resolved by the proxy. Proxy failure MUST NOT fall back to direct
98+
target access. `dial` binds connections and relay sockets toward the proxy.
99+
90100
### 3.3 Listen Address
91101

92102
An empty listen host binds separate IPv4 and IPv6 wildcard sockets on the same
@@ -359,10 +369,12 @@ The receiver MUST reject a version other than `1`, an invalid target, an
359369
incorrect padding length, or incorrect padding bytes. The request padding is
360370
not forwarded to the target.
361371

362-
After parsing the request, the Portal resolves and connects to the target,
363-
optionally binding the outbound socket to `dial`. It then relays bytes in both
364-
directions. When one direction reaches EOF, the other direction may continue
365-
for at most `NOW_TCP_READ_TIMEOUT`.
372+
After parsing the request, the Portal either resolves and connects directly to
373+
the target or sends the target unchanged in a configured SOCKS5 CONNECT
374+
request. `dial` binds the direct target socket or the connection to the SOCKS5
375+
server. The Portal then relays bytes in both directions. When one direction
376+
reaches EOF, the other direction may continue for at most
377+
`NOW_TCP_READ_TIMEOUT`.
366378

367379
## 9. UDP Relay
368380

@@ -429,7 +441,9 @@ target_len_u16 || target_utf8
429441
`target_len_u16` MUST be from 1 through 512, and `target_utf8` MUST satisfy
430442
Section 10. The Portal bounds reading the complete setup target by
431443
`NOW_HANDSHAKE_TIMEOUT`. It then resolves the target and opens one connected UDP
432-
socket, optionally binding its source address according to `dial`.
444+
socket, optionally binding its source address according to `dial`. With SOCKS5
445+
enabled, it instead creates a per-flow UDP ASSOCIATE, keeps the associated TCP
446+
control connection open, and sends the target in each SOCKS5 UDP packet.
433447

434448
After setup, both directions consist only of packet frames:
435449

docs/quick-start.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@ This starts the Portal with these effective defaults:
4040
| shared key | `secret` |
4141
| listen host | empty host, dual-stack wildcard |
4242
| listen port | `2077` |
43+
| `log` | `info` |
4344
| `tls` | `1`, generated self-signed certificate |
4445
| `net` | `mix`, TLS/TCP and QUIC/UDP listeners |
4546
| `spec` | `auto` |
4647
| `alpn` | `now/1` |
47-
| `dial` | `auto` |
4848
| `rate` | `0`, disabled |
4949
| `etar` | `0`, disabled |
50-
| `log` | `info` |
50+
| `dial` | `auto` |
51+
| `socks` | `none`, direct outbound connections |
5152

5253
The startup log prints the effective URL after parsing. Treat that line as the
5354
operator-facing source of truth for the running process.

src/common/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
mod config;
77
mod logger;
88
mod network;
9+
mod socks;
910
mod tls;
1011

1112
pub use config::{
@@ -16,4 +17,5 @@ pub use config::{
1617
};
1718
pub use logger::{LogLevel, Logger};
1819
pub use network::{bind_udp_addrs, dial_tcp_from_local_ip, dial_udp_from_local_ip};
20+
pub(crate) use socks::{OutboundDialer, OutboundUdpSocket, SocksConfig};
1921
pub use tls::{TLSMode, new_server_configs};

src/common/network.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,15 @@ pub async fn dial_udp_from_local_ip(
9595
.map_err(|_| anyhow!("common::util::dial_udp_from_local_ip: dial timeout"))?
9696
}
9797

98-
fn parse_local_ip(dialer_ip: &str) -> Option<IpAddr> {
98+
pub(super) fn parse_local_ip(dialer_ip: &str) -> Option<IpAddr> {
9999
if dialer_ip == DEFAULT_DIALER_IP {
100100
None
101101
} else {
102102
dialer_ip.parse::<IpAddr>().ok()
103103
}
104104
}
105105

106-
fn filter_addrs(
106+
pub(super) fn filter_addrs(
107107
addrs: impl Iterator<Item = SocketAddr>,
108108
local_ip: Option<IpAddr>,
109109
) -> Vec<SocketAddr> {
@@ -117,7 +117,10 @@ fn filter_addrs(
117117
.collect()
118118
}
119119

120-
async fn connect_tcp_addr(local_ip: Option<IpAddr>, target: SocketAddr) -> Result<TcpStream> {
120+
pub(super) async fn connect_tcp_addr(
121+
local_ip: Option<IpAddr>,
122+
target: SocketAddr,
123+
) -> Result<TcpStream> {
121124
if let Some(ip) = local_ip {
122125
let socket = if target.is_ipv4() {
123126
TcpSocket::new_v4()
@@ -138,7 +141,10 @@ async fn connect_tcp_addr(local_ip: Option<IpAddr>, target: SocketAddr) -> Resul
138141
}
139142
}
140143

141-
async fn connect_udp_addr(local_ip: Option<IpAddr>, target: SocketAddr) -> Result<UdpSocket> {
144+
pub(super) async fn connect_udp_addr(
145+
local_ip: Option<IpAddr>,
146+
target: SocketAddr,
147+
) -> Result<UdpSocket> {
142148
let bind_addr = match local_ip {
143149
Some(ip) => SocketAddr::new(ip, 0),
144150
None if target.is_ipv4() => SocketAddr::from(([0, 0, 0, 0], 0)),

src/common/socks.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (C) 2026 NodePassProject <https://github.com/NodePassProject>
2+
// SPDX-License-Identifier: GPL-3.0-only
3+
4+
//! SOCKS5 configuration and direct-or-proxied outbound dialing.
5+
6+
#[path = "socks/config.rs"]
7+
mod config;
8+
#[path = "socks/outbound.rs"]
9+
mod outbound;
10+
#[path = "socks/protocol.rs"]
11+
mod protocol;
12+
#[path = "socks/udp.rs"]
13+
mod udp;
14+
15+
pub(crate) use config::SocksConfig;
16+
pub(crate) use outbound::OutboundDialer;
17+
pub(crate) use udp::OutboundUdpSocket;
18+
19+
#[cfg(test)]
20+
use protocol::{
21+
ADDRESS_IPV4, AUTH_NONE, AUTH_PASSWORD, COMMAND_CONNECT, COMMAND_UDP_ASSOCIATE, SOCKS_VERSION,
22+
SocksAddress, encode_address, parse_udp_header, read_address,
23+
};
24+
25+
#[cfg(test)]
26+
#[path = "../tests/common/socks.rs"]
27+
mod tests;

0 commit comments

Comments
 (0)