Skip to content

Commit 1caa9b8

Browse files
committed
feat: support upstream socks5 egress
1 parent 31dbe94 commit 1caa9b8

7 files changed

Lines changed: 389 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- First-run setup wizard covering both OS and language-runtime trust stores.
2525
- Application launcher mode for running HTTPS clients and Electron apps through
2626
Doppel without proxychains.
27+
- Upstream SOCKS5 proxy support for `run`, `launch`, and `verify`.
2728

2829
### Known limitations
2930

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ Useful flags:
178178
| `--force` | `init` | Regenerate the local CA |
179179
| `--export <path>` | `ca` | Write the CA certificate to a file |
180180
| `--insecure` | `run`, `launch` | Skip upstream certificate verification for debugging only |
181+
| `--upstream-proxy <url>` | `run`, `launch`, `verify` | Route Doppel egress through a SOCKS5 proxy |
181182
| `-v` | `run`, `launch` | Enable debug logging |
182183
| `--electron` | `launch` | Add Chromium/Electron proxy switches to the child process |
183184

@@ -231,6 +232,17 @@ Supported `client_hello` templates are `chrome`, `firefox`, `safari`,
231232

232233
## Usage examples
233234

235+
### Route Doppel through an upstream SOCKS5 proxy
236+
237+
Use `--upstream-proxy` when Doppel itself should egress through another proxy:
238+
239+
```sh
240+
doppel verify --upstream-proxy socks5://user:pass@proxy.example:1080
241+
```
242+
243+
The same value can be supplied with `DOPPEL_UPSTREAM_PROXY` for `run`, `launch`,
244+
and `verify`. Provider-style `socks5://host:port:user:pass` URLs are accepted.
245+
234246
### Launch an app through Doppel
235247

236248
`doppel launch` starts a temporary Doppel proxy, launches one child process with

cmd/doppel/main.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func cmdRun(args []string) error {
113113
dataDir := fs.String("data", cfg.DataDir, "data directory")
114114
verbose := fs.Bool("v", false, "verbose (debug) logging")
115115
insecure := fs.Bool("insecure", false, "skip upstream certificate verification (debugging only)")
116+
upstreamProxyURL := fs.String("upstream-proxy", os.Getenv("DOPPEL_UPSTREAM_PROXY"), "upstream SOCKS5 proxy URL")
116117
if err := fs.Parse(args); err != nil {
117118
return err
118119
}
@@ -132,9 +133,13 @@ func cmdRun(args []string) error {
132133
if err != nil {
133134
return err
134135
}
136+
upstreamProxy, err := upstream.ParseProxy(*upstreamProxyURL)
137+
if err != nil {
138+
return err
139+
}
135140

136141
transport := &upstream.RoundTripper{
137-
Dialer: &upstream.Dialer{SkipVerify: *insecure},
142+
Dialer: &upstream.Dialer{SkipVerify: *insecure, UpstreamProxy: upstreamProxy},
138143
Profile: selected,
139144
}
140145
defer transport.Close()
@@ -170,6 +175,7 @@ func cmdLaunch(args []string) error {
170175
dataDir := fs.String("data", cfg.DataDir, "data directory")
171176
verbose := fs.Bool("v", false, "verbose (debug) logging")
172177
insecure := fs.Bool("insecure", false, "skip upstream certificate verification (debugging only)")
178+
upstreamProxyURL := fs.String("upstream-proxy", os.Getenv("DOPPEL_UPSTREAM_PROXY"), "upstream SOCKS5 proxy URL")
173179
includeEnv := fs.Bool("env", true, "set HTTPS proxy and CA environment variables for the child")
174180
electron := fs.Bool("electron", false, "append Chromium/Electron proxy command-line switches")
175181
allSchemes := fs.Bool("all-schemes", false, "with -electron, proxy every Chromium URL scheme instead of HTTPS only")
@@ -197,9 +203,13 @@ func cmdLaunch(args []string) error {
197203
if err != nil {
198204
return err
199205
}
206+
upstreamProxy, err := upstream.ParseProxy(*upstreamProxyURL)
207+
if err != nil {
208+
return err
209+
}
200210

201211
transport := &upstream.RoundTripper{
202-
Dialer: &upstream.Dialer{SkipVerify: *insecure},
212+
Dialer: &upstream.Dialer{SkipVerify: *insecure, UpstreamProxy: upstreamProxy},
203213
Profile: selected,
204214
}
205215
defer transport.Close()
@@ -330,6 +340,7 @@ func cmdVerify(args []string) error {
330340
profileName := fs.String("profile", cfg.Profile, "identity profile to test")
331341
url := fs.String("url", "https://get.ja3.zone/", "fingerprint-reporting endpoint")
332342
dataDir := fs.String("data", cfg.DataDir, "data directory")
343+
upstreamProxyURL := fs.String("upstream-proxy", os.Getenv("DOPPEL_UPSTREAM_PROXY"), "upstream SOCKS5 proxy URL")
333344
if err := fs.Parse(args); err != nil {
334345
return err
335346
}
@@ -339,8 +350,12 @@ func cmdVerify(args []string) error {
339350
if err != nil {
340351
return err
341352
}
353+
upstreamProxy, err := upstream.ParseProxy(*upstreamProxyURL)
354+
if err != nil {
355+
return err
356+
}
342357

343-
rt := &upstream.RoundTripper{Dialer: &upstream.Dialer{}, Profile: selected}
358+
rt := &upstream.RoundTripper{Dialer: &upstream.Dialer{UpstreamProxy: upstreamProxy}, Profile: selected}
344359
defer rt.Close()
345360

346361
req, err := http.NewRequest(http.MethodGet, *url, nil)

docs/LAUNCHING_APPS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ child environment. Doppel is a TLS identity proxy and expects CONNECT/SOCKS5
4848
followed by a TLS stream. Plain HTTP forwarding is not part of the current
4949
design.
5050

51+
## Upstream SOCKS5 egress
52+
53+
`doppel launch` can also route Doppel's outbound side through a SOCKS5 proxy:
54+
55+
```sh
56+
doppel launch --upstream-proxy socks5://user:pass@proxy.example:1080 -- \
57+
curl https://example.com
58+
```
59+
60+
The child application still talks only to local Doppel. The upstream proxy URL
61+
is consumed by Doppel and should be provided through a secret manager or
62+
short-lived environment variable in automation.
63+
5164
## Electron and Chromium apps
5265

5366
Electron applications are Chromium-based, so environment variables are often

internal/upstream/dialer.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type Dialer struct {
3030
// It must remain false outside of debugging: skipping verification
3131
// would let an attacker between Doppel and the server go unnoticed.
3232
SkipVerify bool
33+
// UpstreamProxy routes Doppel's outbound TCP connection through a proxy.
34+
// Nil means direct egress.
35+
UpstreamProxy *ProxyConfig
3336
}
3437

3538
// Dial connects to host (host:port, defaulting to port 443) and performs a
@@ -47,8 +50,8 @@ func (d *Dialer) Dial(ctx context.Context, p *profile.Profile, host string) (*Co
4750
timeout = defaultTimeout
4851
}
4952

50-
tcpDialer := &net.Dialer{Timeout: timeout}
51-
raw, err := tcpDialer.DialContext(ctx, "tcp", net.JoinHostPort(hostname, port))
53+
target := net.JoinHostPort(hostname, port)
54+
raw, err := d.dialTCP(ctx, target, timeout)
5255
if err != nil {
5356
return nil, fmt.Errorf("dial %s: %w", host, err)
5457
}
@@ -80,6 +83,14 @@ func (d *Dialer) Dial(ctx context.Context, p *profile.Profile, host string) (*Co
8083
return &Conn{Conn: uconn, ALPN: uconn.ConnectionState().NegotiatedProtocol}, nil
8184
}
8285

86+
func (d *Dialer) dialTCP(ctx context.Context, target string, timeout time.Duration) (net.Conn, error) {
87+
if d.UpstreamProxy != nil {
88+
return d.UpstreamProxy.Dial(ctx, "tcp", target, timeout)
89+
}
90+
tcpDialer := &net.Dialer{Timeout: timeout}
91+
return tcpDialer.DialContext(ctx, "tcp", target)
92+
}
93+
8394
func splitHostPort(host string) (hostname, port string) {
8495
if h, p, err := net.SplitHostPort(host); err == nil {
8596
return h, p

0 commit comments

Comments
 (0)