proxy

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package proxy is pano's MITM engine: an HTTP/1.1 proxy listener that intercepts CONNECT tunnels, terminates TLS with certificates minted by the local CA, serves HTTP/1.1 or HTTP/2 on the decrypted connection, forwards to the origin over a pooled transport, and captures every exchange while streaming bodies in both directions. Rules and storage plug in through the Hooks and Sink interfaces so the engine has no knowledge of either.

Index

Constants

View Source
const (
	ReasonNever    = "never"    // host is on the never list
	ReasonUnlisted = "unlisted" // mode "only" and host is not on the only list
	ReasonOff      = "off"      // mode "off"
)

Tunnel reasons recorded as the tag on undecrypted tunnel flows.

View Source
const (
	HeaderReplayOf = "X-Pano-Replay-Of"
	HeaderNoRules  = "X-Pano-No-Rules"
	HeaderFlowID   = "X-Pano-Flow-Id"
)

Internal headers used by the daemon when replaying through the proxy. They are stripped before the request leaves pano.

View Source
const MagicHost = "pano.internal"

MagicHost is the name a client can open through the proxy — any scheme, any port — to reach pano's own setup site instead of an origin. It never resolves in DNS on purpose: a browser only sees it once its requests are already routed through pano, so the same URL works on every device and on every network. `.internal` is ICANN-reserved for private use.

Variables

View Source
var ErrBadPattern = errors.New("bad host pattern")

ErrBadPattern is wrapped by NormalizeHost for entries that cannot match anything.

Functions

func DeviceName

func DeviceName(ua string) string

DeviceName guesses a short human name for a client from its User-Agent: "iPhone · iOS 17.5", "Pixel 8 · Android 14", "iPad", "Mac", or "" when the agent says nothing useful.

func HostMatch

func HostMatch(pattern, host string) bool

HostMatch reports whether host matches pattern. A pattern containing '*' or '?' is a glob (see package glob). A bare domain matches itself and every subdomain: "whatsapp.net" covers "mmg.whatsapp.net". Matching is case-insensitive.

func HostMatchAny

func HostMatchAny(patterns []string, host string) bool

HostMatchAny reports whether host matches any pattern.

func NewTransport

func NewTransport(tlsCfg *tls.Config) *http.Transport

NewTransport builds the shared upstream transport. It never consults proxy environment variables (which could point back at pano) and never decodes bodies, so captured bytes are exactly what the origin sent.

func NormalizeHost

func NormalizeHost(s string) (string, error)

NormalizeHost canonicalises a list entry: trims space, lowercases, strips a trailing dot and a :port suffix. It rejects empty entries and entries with spaces or slashes (someone pasted a URL).

Types

type Decision

type Decision struct {
	// Mock, if set, is written to the client instead of contacting the origin.
	Mock *http.Response
	// Block: "" (none), "reset" (drop the connection), "timeout" (hang until
	// the client gives up or Deadline passes).
	Block string
	// Deadline bounds a "timeout" block.
	Deadline time.Duration
}

Decision is what a hook asks the engine to do instead of forwarding.

type DecryptMode

type DecryptMode string

DecryptMode says which CONNECT tunnels are TLS-terminated.

const (
	// DecryptAll decrypts every host except those on the never list.
	DecryptAll DecryptMode = "all"
	// DecryptOnly decrypts only hosts on the only list (never still wins).
	DecryptOnly DecryptMode = "only"
	// DecryptOff decrypts nothing: every tunnel is spliced through.
	DecryptOff DecryptMode = "off"
)

Decrypt modes.

func ParseDecryptMode

func ParseDecryptMode(s string) (DecryptMode, error)

ParseDecryptMode validates a mode string.

type DecryptPolicy

type DecryptPolicy struct {
	Mode  DecryptMode
	Only  []string
	Never []string
}

DecryptPolicy decides, per host, whether a tunnel is decrypted. Never wins in every mode; Only is consulted only in mode "only".

func (DecryptPolicy) Clone

func (p DecryptPolicy) Clone() DecryptPolicy

Clone returns a deep copy.

func (DecryptPolicy) Decide

func (p DecryptPolicy) Decide(host string) (decrypt bool, reason string)

Decide reports whether host is decrypted and, when it is not, the reason tag for the tunnel flow.

type Device

type Device struct {
	IP        string
	Name      string // derived from the first User-Agent seen, e.g. "iPhone · iOS 17.5"
	UserAgent string
	FirstSeen time.Time
	LastSeen  time.Time
	Requests  int // proxied requests and tunnels
	Decrypted int // TLS handshakes with pano's certificate that succeeded
	Rejected  int // handshakes the client refused (certificate not trusted yet, or pinning)
}

Device is one remote client of the proxy (a phone, a tablet, another machine), identified by its IP. It summarises how far that client has got through setup: has it sent anything via the proxy, has it accepted pano's certificate, or refused it.

func (Device) ProxyOK

func (d Device) ProxyOK() bool

ProxyOK reports whether the device has routed anything through pano.

func (Device) TLSOK

func (d Device) TLSOK() bool

TLSOK reports whether the device has ever accepted pano's certificate.

type Hooks

type Hooks interface {
	Request(ctx context.Context, f *flow.Flow, r *http.Request) Decision
	Response(ctx context.Context, f *flow.Flow, r *http.Request, resp *http.Response) Decision
}

Hooks lets a rules engine observe and mutate exchanges. Hooks may mutate the request/response in place (headers, body, URL) and append RuleHits to the flow. Both methods run synchronously on the request goroutine.

type Options

type Options struct {
	Addr        string      // listen address, e.g. 127.0.0.1:9091
	TLS         *tls.Config // MITM server config (from ca.Authority.TLSConfig)
	Sink        Sink
	Hooks       Hooks             // optional
	Transport   http.RoundTripper // optional; NewTransport(UpstreamTLS) if nil
	UpstreamTLS *tls.Config       // optional client TLS config for origins (tests, custom roots)
	MaxBody     int64             // per-body capture cap; 0 = 4 MiB
	MaxInflight int64             // total in-flight capture budget; 0 = 256 MiB
	MaxConns    int               // concurrent tunnels; 0 = 10000
	Decrypt     DecryptPolicy     // which tunnels are TLS-terminated; zero value = mode all, no lists
	CaptureWS   bool
	Session     func() string // current session id
	IDs         *flow.IDGen
	Logger      *slog.Logger
	CAPEM       []byte // served at /_pano/ca.pem on the proxy port
	DisableH2   bool
	// Local, if set, serves requests addressed to pano itself: a plain
	// request on a proxy port, an absolute-URI request for one of pano's own
	// addresses, and anything for MagicHost over http or https. It replaces
	// the built-in one-page site (which only offers the CA).
	Local http.Handler
}

Options configure a Server.

type RejectedHost

type RejectedHost struct {
	Host  string
	Count int
	First time.Time
	Last  time.Time
	Error string
}

RejectedHost is a host whose client refused pano's certificate recently — the usual sign of certificate pinning. It is a suggestion for the never list, never applied automatically.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the proxy.

func New

func New(opts Options) *Server

New creates a server (not yet listening).

func (*Server) ActiveConns

func (s *Server) ActiveConns() int

ActiveConns is the number of open tunnels/requests.

func (*Server) AddListener

func (s *Server) AddListener(ln net.Listener)

AddListener serves the proxy on one more listener — how `pano mobile` opens the proxy to the LAN without touching the loopback listener. The listener's address counts as pano itself (requests for it are served locally, never forwarded). Close it with RemoveListener.

func (*Server) Addr

func (s *Server) Addr() string

Addr returns the bound address (after Listen).

func (*Server) Capturing

func (s *Server) Capturing() bool

Capturing reports whether recording is on.

func (*Server) Close

func (s *Server) Close() error

Close force-closes the listeners and every open connection, including hijacked tunnels. Used after a short Shutdown drain: a user turning pano off should not wait for a browser's streaming request to end.

func (*Server) Decrypt

func (s *Server) Decrypt() DecryptPolicy

Decrypt returns a copy of the current policy.

func (*Server) Device

func (s *Server) Device(addr string) (Device, bool)

Device looks up one remote client by address or IP.

func (*Server) Devices

func (s *Server) Devices() []Device

Devices lists remote clients seen by the proxy, most recent first. Loopback clients are never included.

func (*Server) Listen

func (s *Server) Listen() error

Listen binds the address.

func (*Server) Rejected

func (s *Server) Rejected() []RejectedHost

Rejected lists hosts whose clients refused pano's certificate in the last hour, most frequent first — candidates for the never list.

func (*Server) RemoveListener

func (s *Server) RemoveListener(ln net.Listener) error

RemoveListener closes a listener added with AddListener. Open tunnels on it keep running until they end.

func (*Server) Serve

func (s *Server) Serve() error

Serve runs until Shutdown.

func (*Server) SetCapturing

func (s *Server) SetCapturing(on bool)

SetCapturing toggles recording (the proxy keeps forwarding).

func (*Server) SetDecrypt

func (s *Server) SetDecrypt(p DecryptPolicy)

SetDecrypt replaces the decrypt policy. Takes effect for the next CONNECT; open tunnels are unaffected. Hosts now covered by the never list are dropped from the rejected-host suggestions.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown stops accepting and waits for in-flight exchanges up to ctx.

type Sink

type Sink interface {
	// Started is called once request headers are known.
	Started(f *flow.Flow)
	// Updated is called when response headers are known or state changes.
	Updated(f *flow.Flow)
	// Done is called once with the final snapshot.
	Done(f *flow.Flow)
	// Blob stores body bytes and returns their content hash.
	Blob(b []byte) string
	// WS is called per captured WebSocket message.
	WS(m *flow.WSMessage)
}

Sink receives capture output. Implementations must not block for long.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL