Skip to content

Commit 31dbe94

Browse files
committed
feat: launch applications through doppel
1 parent 9b243b4 commit 31dbe94

6 files changed

Lines changed: 472 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Single-port SOCKS5 and HTTP CONNECT proxy with protocol auto-detection and a
2020
client-negotiation deadline.
2121
- TLS interception that re-originates requests through the chosen profile.
22-
- `doppel` command-line interface: `init`, `run`, `profiles`, `ca`, `verify`.
22+
- `doppel` command-line interface: `init`, `run`, `launch`, `profiles`,
23+
`ca`, `verify`.
2324
- First-run setup wizard covering both OS and language-runtime trust stores.
25+
- Application launcher mode for running HTTPS clients and Electron apps through
26+
Doppel without proxychains.
2427

2528
### Known limitations
2629

README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ protocol: HTTP/2.0
160160
| --- | --- |
161161
| `doppel init` | Generate or reuse the local CA and print setup guidance |
162162
| `doppel run` | Start the local proxy |
163+
| `doppel launch` | Start Doppel and run one application through it |
163164
| `doppel profiles` | List built-in and user-supplied identity profiles |
164165
| `doppel ca` | Show or export the local CA certificate |
165166
| `doppel verify` | Check the selected profile against a remote endpoint |
@@ -171,13 +172,14 @@ Useful flags:
171172

172173
| Flag | Commands | Purpose |
173174
| --- | --- | --- |
174-
| `--profile <name>` | `run`, `verify` | Select the identity profile |
175-
| `--addr <host:port>` | `init`, `run` | Change the proxy address; default is `127.0.0.1:8080` |
176-
| `--data <path>` | `init`, `run`, `profiles`, `ca`, `verify` | Use a custom data directory |
175+
| `--profile <name>` | `run`, `launch`, `verify` | Select the identity profile |
176+
| `--addr <host:port>` | `init`, `run`, `launch` | Change the proxy address; default is `127.0.0.1:8080` |
177+
| `--data <path>` | `init`, `run`, `launch`, `profiles`, `ca`, `verify` | Use a custom data directory |
177178
| `--force` | `init` | Regenerate the local CA |
178179
| `--export <path>` | `ca` | Write the CA certificate to a file |
179-
| `--insecure` | `run` | Skip upstream certificate verification for debugging only |
180-
| `-v` | `run` | Enable debug logging |
180+
| `--insecure` | `run`, `launch` | Skip upstream certificate verification for debugging only |
181+
| `-v` | `run`, `launch` | Enable debug logging |
182+
| `--electron` | `launch` | Add Chromium/Electron proxy switches to the child process |
181183

182184
## Built-in profiles
183185

@@ -229,6 +231,26 @@ Supported `client_hello` templates are `chrome`, `firefox`, `safari`,
229231

230232
## Usage examples
231233

234+
### Launch an app through Doppel
235+
236+
`doppel launch` starts a temporary Doppel proxy, launches one child process with
237+
HTTPS proxy environment variables, and stops the proxy when the child exits:
238+
239+
```sh
240+
doppel launch --profile chrome-windows -- curl https://example.com
241+
```
242+
243+
For Electron or Chromium-based desktop apps, add `--electron`. Doppel appends
244+
Chromium proxy switches so the app does not need proxychains or LD_PRELOAD:
245+
246+
```sh
247+
doppel launch --profile chrome-windows --electron -- /path/to/electron-app
248+
```
249+
250+
By default the Chromium switch proxies HTTPS URLs only, because Doppel expects a
251+
TLS stream after CONNECT. See [Launching applications](docs/LAUNCHING_APPS.md)
252+
for Windows, macOS, Linux, and Electron notes.
253+
232254
### curl
233255

234256
```sh

cmd/doppel/main.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import (
99
"fmt"
1010
"io"
1111
"log/slog"
12+
"net"
1213
"net/http"
1314
"os"
1415
"os/signal"
1516
"sort"
17+
"time"
1618

1719
"github.com/redstone-md/Doppel/internal/ca"
1820
"github.com/redstone-md/Doppel/internal/config"
21+
"github.com/redstone-md/Doppel/internal/launch"
1922
"github.com/redstone-md/Doppel/internal/mitm"
2023
"github.com/redstone-md/Doppel/internal/profile"
2124
"github.com/redstone-md/Doppel/internal/proxy"
@@ -38,6 +41,8 @@ func main() {
3841
err = cmdInit(os.Args[2:])
3942
case "run":
4043
err = cmdRun(os.Args[2:])
44+
case "launch":
45+
err = cmdLaunch(os.Args[2:])
4146
case "profiles":
4247
err = cmdProfiles(os.Args[2:])
4348
case "ca":
@@ -152,6 +157,104 @@ func cmdRun(args []string) error {
152157
return server.ListenAndServe(ctx)
153158
}
154159

160+
// cmdLaunch starts the proxy, launches a child application configured to use it,
161+
// and stops the proxy when the child exits.
162+
func cmdLaunch(args []string) error {
163+
cfg, err := config.Default()
164+
if err != nil {
165+
return err
166+
}
167+
fs := flag.NewFlagSet("launch", flag.ExitOnError)
168+
addr := fs.String("addr", cfg.Addr, "proxy listen address")
169+
profileName := fs.String("profile", cfg.Profile, "identity profile to emulate")
170+
dataDir := fs.String("data", cfg.DataDir, "data directory")
171+
verbose := fs.Bool("v", false, "verbose (debug) logging")
172+
insecure := fs.Bool("insecure", false, "skip upstream certificate verification (debugging only)")
173+
includeEnv := fs.Bool("env", true, "set HTTPS proxy and CA environment variables for the child")
174+
electron := fs.Bool("electron", false, "append Chromium/Electron proxy command-line switches")
175+
allSchemes := fs.Bool("all-schemes", false, "with -electron, proxy every Chromium URL scheme instead of HTTPS only")
176+
bypass := fs.String("bypass", "<local>", "Chromium proxy bypass list used with -electron")
177+
if err := fs.Parse(args); err != nil {
178+
return err
179+
}
180+
argv := fs.Args()
181+
if len(argv) == 0 {
182+
return fmt.Errorf("usage: doppel launch [flags] -- <command> [args...]")
183+
}
184+
cfg.DataDir, cfg.Addr, cfg.Profile = *dataDir, *addr, *profileName
185+
186+
logger := newLogger(*verbose)
187+
188+
if !cfg.CAExists() {
189+
return fmt.Errorf("no CA found in %s; run 'doppel init' first", cfg.DataDir)
190+
}
191+
authority, err := ca.Load(cfg.CACertPath(), cfg.CAKeyPath())
192+
if err != nil {
193+
return err
194+
}
195+
196+
selected, err := loadProfile(cfg, cfg.Profile)
197+
if err != nil {
198+
return err
199+
}
200+
201+
transport := &upstream.RoundTripper{
202+
Dialer: &upstream.Dialer{SkipVerify: *insecure},
203+
Profile: selected,
204+
}
205+
defer transport.Close()
206+
207+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
208+
defer stop()
209+
210+
ln, err := net.Listen("tcp", cfg.Addr)
211+
if err != nil {
212+
return fmt.Errorf("listen on %s: %w", cfg.Addr, err)
213+
}
214+
215+
server := &proxy.Server{
216+
Addr: cfg.Addr,
217+
Logger: logger,
218+
Interceptor: &mitm.Interceptor{
219+
CA: authority,
220+
Profile: selected,
221+
Transport: transport,
222+
Logger: logger,
223+
},
224+
}
225+
226+
serveErr := make(chan error, 1)
227+
go func() { serveErr <- server.Serve(ctx, ln) }()
228+
229+
proxyAddr := ln.Addr().String()
230+
cmd, err := launch.Command(ctx, launch.Options{
231+
ProxyAddr: proxyAddr,
232+
CACertPath: cfg.CACertPath(),
233+
IncludeEnv: *includeEnv,
234+
IncludeChromium: *electron,
235+
ProxyAllSchemes: *allSchemes,
236+
BypassList: *bypass,
237+
}, argv)
238+
if err != nil {
239+
stop()
240+
return err
241+
}
242+
243+
logger.Info("launching app", "profile", selected.Name, "proxy", proxyAddr, "command", argv[0])
244+
runErr := cmd.Run()
245+
stop()
246+
247+
select {
248+
case err := <-serveErr:
249+
if runErr == nil && err != nil {
250+
return err
251+
}
252+
case <-time.After(5 * time.Second):
253+
logger.Warn("proxy shutdown timed out; exiting with child status")
254+
}
255+
return runErr
256+
}
257+
155258
// cmdProfiles lists every available identity profile.
156259
func cmdProfiles(args []string) error {
157260
cfg, err := config.Default()
@@ -320,6 +423,7 @@ Usage:
320423
Commands:
321424
init Generate the local CA and print setup instructions
322425
run Start the proxy
426+
launch Start the proxy and run an application through it
323427
profiles List available identity profiles
324428
ca Show or export the local CA certificate
325429
verify Check the emulated TLS fingerprint against a remote service

docs/LAUNCHING_APPS.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Launching Applications Through Doppel
2+
3+
`doppel launch` is the recommended way to run a single application through
4+
Doppel without proxychains-ng, LD_PRELOAD, or global OS proxy changes.
5+
6+
It starts Doppel, launches the child process with proxy-related environment
7+
variables, and stops Doppel when the child process exits.
8+
9+
## Basic usage
10+
11+
Initialize the local CA once:
12+
13+
```sh
14+
doppel init
15+
```
16+
17+
Launch an application:
18+
19+
```sh
20+
doppel launch --profile chrome-windows -- <command> [args...]
21+
```
22+
23+
Example with curl:
24+
25+
```sh
26+
doppel launch --profile chrome-windows -- curl https://example.com
27+
```
28+
29+
The `--` separator matters when the child command has its own flags.
30+
31+
## What the launcher sets
32+
33+
By default, `doppel launch` sets HTTPS-oriented environment variables for the
34+
child process:
35+
36+
```text
37+
HTTPS_PROXY=http://127.0.0.1:8080
38+
https_proxy=http://127.0.0.1:8080
39+
NO_PROXY=localhost,127.0.0.1,::1
40+
no_proxy=localhost,127.0.0.1,::1
41+
REQUESTS_CA_BUNDLE=<doppel ca.pem>
42+
NODE_EXTRA_CA_CERTS=<doppel ca.pem>
43+
SSL_CERT_FILE=<doppel ca.pem>
44+
```
45+
46+
`HTTP_PROXY`, `http_proxy`, `ALL_PROXY`, and `all_proxy` are removed from the
47+
child environment. Doppel is a TLS identity proxy and expects CONNECT/SOCKS5
48+
followed by a TLS stream. Plain HTTP forwarding is not part of the current
49+
design.
50+
51+
## Electron and Chromium apps
52+
53+
Electron applications are Chromium-based, so environment variables are often
54+
not enough. Use `--electron` to append Chromium proxy switches to the launched
55+
process:
56+
57+
```sh
58+
doppel launch --profile chrome-windows --electron -- /path/to/electron-app
59+
```
60+
61+
The launcher appends:
62+
63+
```text
64+
--proxy-server=https=http://127.0.0.1:8080
65+
--proxy-bypass-list=<local>
66+
```
67+
68+
The HTTPS-only proxy rule is deliberate: Doppel handles HTTPS requests where the
69+
client opens a CONNECT tunnel and then starts TLS. If you need to force Chromium
70+
to send all URL schemes to Doppel for debugging, add `--all-schemes`, but plain
71+
HTTP requests will not be rewritten into TLS and may fail.
72+
73+
```sh
74+
doppel launch --electron --all-schemes -- /path/to/electron-app
75+
```
76+
77+
For Electron applications you control, the equivalent in the main process is:
78+
79+
```js
80+
const { app } = require('electron')
81+
82+
app.commandLine.appendSwitch('proxy-server', 'https=http://127.0.0.1:8080')
83+
app.commandLine.appendSwitch('proxy-bypass-list', '<local>')
84+
```
85+
86+
Append these switches before `app.whenReady()`.
87+
88+
## Windows examples
89+
90+
PowerShell:
91+
92+
```powershell
93+
doppel launch --profile chrome-windows --electron -- "C:\Users\you\AppData\Local\Programs\App\App.exe"
94+
```
95+
96+
If the launched app is not Electron/Chromium-based, omit `--electron`:
97+
98+
```powershell
99+
doppel launch --profile chrome-windows -- python .\client.py
100+
```
101+
102+
## macOS examples
103+
104+
Electron `.app` bundles usually need the real executable inside the bundle:
105+
106+
```sh
107+
doppel launch --electron -- \
108+
"/Applications/Example.app/Contents/MacOS/Example"
109+
```
110+
111+
## Linux examples
112+
113+
```sh
114+
doppel launch --electron -- /usr/bin/example-app
115+
```
116+
117+
For AppImage builds:
118+
119+
```sh
120+
doppel launch --electron -- ./Example.AppImage
121+
```
122+
123+
## When to still use system proxy settings
124+
125+
Some closed-source desktop apps sanitize command-line arguments or spawn helper
126+
processes without inheriting the parent environment. For those apps, use OS
127+
proxy settings or the app's own proxy settings and point HTTPS traffic at
128+
`127.0.0.1:8080`.
129+
130+
System proxy mode is broader than `doppel launch`, so use it carefully and turn
131+
it off after the test. `doppel launch` is preferred when you want one target app
132+
without affecting the rest of the machine.
133+
134+
## Known limits
135+
136+
- Doppel handles HTTPS/TLS traffic, not arbitrary TCP protocols.
137+
- Plain HTTP URLs are not converted to HTTPS.
138+
- Apps with certificate pinning may reject Doppel's generated leaf
139+
certificates even after the CA is trusted.
140+
- Electron apps that ignore Chromium command-line switches need in-app proxy
141+
configuration or OS proxy settings.
142+
- Child processes that drop inherited environment variables may need explicit
143+
app-level proxy configuration.

0 commit comments

Comments
 (0)