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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ git clone https://github.com/sadgoodman/cli-proxy.git
cd cli-proxy && make build && ./cli-proxy
```

## Proxy without TLS interception

```sh
./cli-proxy -tunnel # terminal UI
./cli-proxy -tunnel -headless # no UI
./cli-proxy -tunnel -system-proxy # also enable the system proxy
curl -x http://127.0.0.1:8080 https://example.com
```

No CA certificate is created or required. HTTPS passes through CONNECT without
decryption: clients see the original server certificate, and only connection
endpoints and byte counts are recorded. Rules and breakpoints do not apply to
HTTPS contents. Plain HTTP capture and editing remain available. TLS interception
is still the default when `-tunnel` is omitted.

To switch while running, open **Cert** (`4`) and press `m` or click
**TLS off / TLS on**. Changes apply to new connections; reconnect existing
clients to use the new mode. Enabling interception creates the CA if needed
but does not automatically install it.

## Documentation

The full reference — every flag, the rule DSL, filter syntax, key bindings,
Expand Down
22 changes: 22 additions & 0 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,28 @@ git clone https://github.com/sadgoodman/cli-proxy.git
cd cli-proxy && make build && ./cli-proxy
```

## Прокси без перехвата TLS

Для работы без перехвата SSL/TLS используйте существующий режим `-tunnel`:

```sh
./cli-proxy -tunnel # с терминальным интерфейсом
./cli-proxy -tunnel -headless # без интерфейса
./cli-proxy -tunnel -system-proxy # также включить системный прокси
curl -x http://127.0.0.1:8080 https://example.com
```

CA-сертификат не создаётся и не требуется. HTTPS передаётся через CONNECT
без расшифровки: клиент получает исходный сертификат сервера, а в журнале
видны только адрес соединения и объём переданных данных. Правила и breakpoints
не применяются к содержимому HTTPS. Обычный HTTP по-прежнему доступен для
просмотра и изменения. По умолчанию, без `-tunnel`, включён перехват TLS.

Переключить режим во время работы можно во вкладке **Cert** (`4`): клавиша
`m` или кнопка **TLS off / TLS on**. Изменение действует на новые соединения;
для уже открытых требуется переподключить клиент. При первом включении
перехвата CA создаётся при необходимости, но автоматически не устанавливается.

## Документация

Полное руководство — все флаги, синтаксис правил и фильтров, горячие клавиши,
Expand Down
25 changes: 25 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,31 @@ Checking it from the same machine:
curl -x http://127.0.0.1:8080 --cacert ~/.cli-proxy/ca.pem https://example.com
```

### Proxy without SSL/TLS interception

Run `./cli-proxy -tunnel`, or `./cli-proxy -tunnel -headless` without the TUI.
This mode does not load or generate a CA, and clients need no proxy certificate.
To check it:

```sh
curl -x http://127.0.0.1:8080 https://example.com
```

HTTPS passes through an opaque CONNECT tunnel with the original server certificate.
Only endpoints and byte counts are visible, not HTTPS URL paths, headers or bodies.
Rules and breakpoints do not apply inside tunnels. Plain HTTP works as before.
You can combine `-tunnel` with `-system-proxy`. Standalone `-install-cert` and
`-uninstall-cert` commands still perform their explicit certificate action even
when combined with `-tunnel`.

In the TUI, open **Cert** (`4`) and press `m` or click **TLS off / TLS on**.
The header (`MITM` / `TUNNEL`) and Cert view show the current mode. Changes
affect new connections only; existing connections retain their mode until
the client reconnects. Enabling interception loads or creates the CA, while
trust-store installation remains a separate action. If loading the CA fails,
the proxy stays in tunnel mode. The selection lasts for the current run;
the next startup uses the `-tunnel` flag again.

## Changing the port

You can set the port with a flag, or change it right in the running interface:
Expand Down
26 changes: 26 additions & 0 deletions docs/guide.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,31 @@ GOOS=windows GOARCH=amd64 go build -o cli-proxy.exe .
curl -x http://127.0.0.1:8080 --cacert ~/.cli-proxy/ca.pem https://example.com
```

### Прокси без перехвата SSL/TLS

Запустите `./cli-proxy -tunnel` (или `./cli-proxy -tunnel -headless` без TUI).
В этом режиме CA не загружается и не создаётся; устанавливать сертификат
на клиентские устройства не нужно. Проверка:

```sh
curl -x http://127.0.0.1:8080 https://example.com
```

HTTPS проходит через непрозрачный CONNECT-туннель с исходным сертификатом
сервера. Видны адресаты и объёмы данных, но не URL-пути, заголовки или тела
HTTPS-запросов. Правила и breakpoints внутри туннеля не работают. Обычный HTTP
обрабатывается как прежде. Флаг `-system-proxy` можно сочетать с `-tunnel`.
Отдельные команды `-install-cert` и `-uninstall-cert` по-прежнему выполняют
явно запрошенное действие с сертификатом, даже при указании `-tunnel`.

В TUI откройте **Cert** (`4`) и нажмите `m` или кнопку **TLS off / TLS on**.
Текущий режим показан в шапке (`MITM` / `TUNNEL`) и во вкладке Cert.
Переключение действует только на новые соединения: существующие продолжают
работать в прежнем режиме до переподключения клиента. При первом включении
перехвата CA загружается или создаётся; установка в доверенные остаётся
отдельным действием. При ошибке загрузки CA сохраняется режим туннеля.
Выбор действует до выхода; при следующем запуске режим задаёт флаг `-tunnel`.

## Смена порта

Порт можно задать флагом, а можно поменять прямо в работающем интерфейсе:
Expand Down Expand Up @@ -390,6 +415,7 @@ filter`), а в заголовке списка — `+2 saved`. Фильтры
| `I` (в разделе Cert) | то же, но для всех пользователей |
| `u` (в разделе Cert) | убрать сертификат из доверенных |
| `s` (в разделе Cert) | включить/выключить системный прокси этой машины |
| `m` (в разделе Cert) | включить/выключить перехват TLS для новых соединений |
| `c` | очистить список |
| `Tab` / `Shift-Tab` | активная панель / таб панели (см. выше) |
| `h` `b` `r` | табы панели: заголовки, тело, сырое сообщение |
Expand Down
52 changes: 47 additions & 5 deletions internal/proxy/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"encoding/pem"
"fmt"
"html"
"net"
"net/http"
"sort"
Expand Down Expand Up @@ -84,6 +85,14 @@ func (p *Proxy) serveLocal(w http.ResponseWriter, r *http.Request, origin string
path = "/"
}
p.cfg.Log.Addf("%s request %s %s from %s", origin, r.Method, path, r.RemoteAddr)
if p.CA() == nil {
switch path {
case "/", "/index.html", "/help", "/status", "/status.json":
default:
http.Error(w, "not found; TLS pass-through is enabled, no CA certificate is available", http.StatusNotFound)
return
}
}
switch path {
case "/", "/index.html", "/help":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
Expand All @@ -94,9 +103,9 @@ func (p *Proxy) serveLocal(w http.ResponseWriter, r *http.Request, origin string
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
w.Header().Set("Content-Disposition", `attachment; filename="cli-proxy-ca.crt"`)
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(p.cfg.CA.CertPEM)
_, _ = w.Write(p.CA().CertPEM)
case "/cert.der", "/ca.der":
block, _ := pem.Decode(p.cfg.CA.CertPEM)
block, _ := pem.Decode(p.CA().CertPEM)
if block == nil {
http.Error(w, "corrupt CA", http.StatusInternalServerError)
return
Expand All @@ -110,6 +119,10 @@ func (p *Proxy) serveLocal(w http.ResponseWriter, r *http.Request, origin string
_, _ = w.Write([]byte(p.mobileconfig()))
case "/status", "/status.json":
active, total := p.Stats()
fingerprint := ""
if p.CA() != nil {
fingerprint = p.CA().Fingerprint()
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"proxy": p.Addr(),
Expand All @@ -121,7 +134,7 @@ func (p *Proxy) serveLocal(w http.ResponseWriter, r *http.Request, origin string
"breakpoints": p.cfg.Breaker.Len(),
"active_conns": active,
"total_conns": total,
"ca_fingerprint": p.cfg.CA.Fingerprint(),
"ca_fingerprint": fingerprint,
})
default:
http.Error(w, "not found\n\navailable: / /ssl /cert /cert.der /ca.mobileconfig /status", http.StatusNotFound)
Expand All @@ -143,6 +156,9 @@ func (p *Proxy) BaseURL() string {

// DeviceHint returns the short "point your phone here" instruction.
func (p *Proxy) DeviceHint() string {
if p.tunnelOnly.Load() {
return fmt.Sprintf("proxy %s | TLS pass-through; no CA certificate required", p.DisplayAddr())
}
return fmt.Sprintf("proxy %s | cert %s/cert", p.DisplayAddr(), p.BaseURL())
}

Expand Down Expand Up @@ -217,6 +233,23 @@ func (p *Proxy) CertSteps() string {
if len(ips) > 0 {
host = ips[0]
}
if p.tunnelOnly.Load() {
return fmt.Sprintf(`TLS pass-through

HTTPS is forwarded through CONNECT without TLS interception.
No CA certificate installation is required.
Only CONNECT endpoints and byte counts are visible for HTTPS.
Plain HTTP capture, rules and breakpoints still work.

Configure your device's HTTP and HTTPS proxy:
Server: %s Port: %s

For programs on this computer:
export HTTP_PROXY=http://127.0.0.1:%s
export HTTPS_PROXY=http://127.0.0.1:%s
curl -x http://127.0.0.1:%s https://example.com
`, host, port, port, port, port)
}
return fmt.Sprintf(certStepsText,
"http://"+LocalHostName, // 1 short base
p.BaseURL()+"/cert", // 2 full certificate URL
Expand All @@ -227,6 +260,15 @@ func (p *Proxy) CertSteps() string {
}

func (p *Proxy) indexHTML() string {
if p.tunnelOnly.Load() {
return `<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>cli-proxy</title></head><body>
<h1>cli-proxy</h1><p>Mode: tunnel</p>
<p>Proxy address: <code>` + html.EscapeString(p.DisplayAddr()) + `</code></p>
<pre>` + html.EscapeString(p.CertSteps()) + `</pre>
</body></html>`
}
_, port, _ := net.SplitHostPort(p.Addr())
ips := LocalIPs()
var hosts strings.Builder
Expand Down Expand Up @@ -257,7 +299,7 @@ func (p *Proxy) indexHTML() string {
<div>Proxy address: <code>` + p.DisplayAddr() + `</code></div>
<div>LAN addresses: ` + hosts.String() + `</div>
<div>Mode: <code>` + p.Mode() + `</code></div>
<div>CA: <code>` + p.cfg.CA.Summary() + `</code></div>
<div>CA: <code>` + p.CA().Summary() + `</code></div>
</div>
<h2>Short URL</h2>
<div>Any device whose proxy already points here can use
Expand All @@ -279,7 +321,7 @@ curl -x ` + p.BaseURL() + ` --cacert cli-proxy-ca.crt https://example.com</pre>

func (p *Proxy) mobileconfig() string {
uuid := "cli-proxy-ca-root"
block, _ := pem.Decode(p.cfg.CA.CertPEM)
block, _ := pem.Decode(p.CA().CertPEM)
der := ""
if block != nil {
der = base64Std(block.Bytes)
Expand Down
Loading
Loading