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
- Variables
- func DeviceName(ua string) string
- func HostMatch(pattern, host string) bool
- func HostMatchAny(patterns []string, host string) bool
- func NewTransport(tlsCfg *tls.Config) *http.Transport
- func NormalizeHost(s string) (string, error)
- type Decision
- type DecryptMode
- type DecryptPolicy
- type Device
- type Hooks
- type Options
- type RejectedHost
- type Server
- func (s *Server) ActiveConns() int
- func (s *Server) AddListener(ln net.Listener)
- func (s *Server) Addr() string
- func (s *Server) Capturing() bool
- func (s *Server) Close() error
- func (s *Server) Decrypt() DecryptPolicy
- func (s *Server) Device(addr string) (Device, bool)
- func (s *Server) Devices() []Device
- func (s *Server) Listen() error
- func (s *Server) Rejected() []RejectedHost
- func (s *Server) RemoveListener(ln net.Listener) error
- func (s *Server) Serve() error
- func (s *Server) SetCapturing(on bool)
- func (s *Server) SetDecrypt(p DecryptPolicy)
- func (s *Server) Shutdown(ctx context.Context) error
- type Sink
Constants ¶
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.
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.
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 ¶
var ErrBadPattern = errors.New("bad host pattern")
ErrBadPattern is wrapped by NormalizeHost for entries that cannot match anything.
Functions ¶
func DeviceName ¶
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 ¶
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 ¶
HostMatchAny reports whether host matches any pattern.
func NewTransport ¶
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 ¶
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.
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.
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 ¶
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 (*Server) ActiveConns ¶
ActiveConns is the number of open tunnels/requests.
func (*Server) AddListener ¶
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) Close ¶
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) Devices ¶
Devices lists remote clients seen by the proxy, most recent first. Loopback clients are never included.
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 ¶
RemoveListener closes a listener added with AddListener. Open tunnels on it keep running until they end.
func (*Server) SetCapturing ¶
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.
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.