API Reference
Every public name, generated from the package's own docstrings.
The same surface in prose, with the bounds and the gotchas, is the API header
that ships inside the installed package, netproxy/AGENTS.md, readable with
importlib.resources.files("netproxy") and printed in the Guide of this site.
A rule-routed asyncio proxy server with per-destination mTLS.
TLSSource = Union[ssl.SSLContext, Credential]
module-attribute
A client context used as is, or a :class:Credential that builds one.
Upstream = Union[str, Any, None]
module-attribute
None: the server's egress; a string: a proxy URL, "direct", or a
PAC result such as "PROXY a:3128; DIRECT"; anything else: a proxylib
ProxyMap (indexed with the destination URL).
Connection(host, port, scheme='https', path=None, client=None, listener='')
dataclass
A connection the proxy is about to make, as rules see it.
scheme is "https" for a CONNECT tunnel, "http" for a forwarded
plain-HTTP request, "tcp" for a tunnel listener. path is set only
for forwarded HTTP requests. client is the peer address, listener
the name of the listener that accepted it.
authority
property
host:port, with IPv6 hosts bracketed.
url
property
The destination as a URL, the key an upstream ProxyMap is asked;
the scheme's default port is left out, as clients and PAC scripts write it.
Credential(identity=None, *, backend=None, pem=None, options=None, query=None, verify=None, cafile=None, capath=None, cadata=None, minimum_version=None, name='')
A client identity for mTLS, turned into a client SSLContext lazily.
Give one source: identity (an :class:sslcontext.Identity, or a
callable returning one); pem, a (certificate, key, password)
tuple for :func:sslcontext.load_pem (see :meth:from_pem); or a
keystore backend name with its constructor
options (store, module, pin, ...) and a query for
:meth:sslcontext.backends.Backend.find_identity (subject,
thumbprint, label, ...). verify, cafile, capath,
cadata and minimum_version shape how the destination's
certificate is checked, as in :func:sslcontext.create_context.
Building touches the keystore, so it may block: the server calls
:meth:context from a worker thread. The context is cached until
:meth:invalidate; a failure is not cached, so the next connection tries
again.
Source code in src/netproxy/_credential.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
from_pem(certificate, key=None, password=None, *, name='', **context_options)
classmethod
A credential from a certificate (+ chain) and key held in memory,
as :func:sslcontext.load_pem takes them: PEM or DER (key
omitted: both are in certificate), or a PKCS#12 bundle as
certificate with no key. password decrypts the key or
bundle (a value, a file, or a callable).
Source code in src/netproxy/_credential.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
invalidate()
Forget the built context; the next use looks the identity up again.
Source code in src/netproxy/_credential.py
99 100 101 102 | |
context()
The client context, built on first call.
Raises :class:~netproxy.exceptions.CredentialUnavailableError when
the identity cannot be found or loaded.
Source code in src/netproxy/_credential.py
104 105 106 107 108 109 110 111 112 113 | |
AsyncProxyServer(router=None, *, listen=None, egress=None, tunnels=(), timeout=30.0, idle_timeout=None, allow_remote=False, ca=None, recorders=(), keylog=None)
A proxy server on the running event loop.
router decides every connection (default: everything direct).
listen is where the HTTP proxy accepts, in netimps.parse_listen
syntax ("127.0.0.1:3128", a list of such); None means
127.0.0.1 on a free port, () no HTTP proxy listener at all.
egress is a proxylib ProxyMap (or proxy URL string) for routes
that set no upstream. tunnels are extra :class:Tunnel listeners.
timeout bounds each connection attempt and handshake, idle_timeout
ends a relayed direction that stays silent that long. Listening beyond
loopback needs allow_remote=True. ca (an
sslcontext.issuing.CertificateAuthority or a
:class:~netproxy.intercept.Interceptor) enables interception of
CONNECT requests whose route is :attr:~netproxy.Action.TLS.
recorders (see :mod:netproxy.recording) see every relayed
connection. keylog names a file that receives the TLS secrets of every
TLS connection the proxy makes or ends, in the SSLKEYLOGFILE format
Wireshark reads; treat it as a secret.
The constructor does no I/O; :meth:start binds, :meth:aclose stops
and waits for every connection to end.
Source code in src/netproxy/_server.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | |
addresses
property
(host, port) the HTTP proxy listens on, ports read back.
port
property
The HTTP proxy's first port (the free one picked for port 0).
start()
async
Bind every listener. Raises OSError (address in use) or
ValueError (a remote address without allow_remote).
Source code in src/netproxy/_server.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | |
tunnel_address(tunnel)
Where tunnel (one of tunnels) listens, port read back.
Source code in src/netproxy/_server.py
207 208 209 210 211 | |
serve_forever()
async
Start if needed, then wait until :meth:aclose (or cancellation).
Source code in src/netproxy/_server.py
213 214 215 216 | |
aclose()
async
Stop listening, end every relayed connection and wait for them.
Source code in src/netproxy/_server.py
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
open_upstream(connection, route, *, forward_http=False)
async
:func:netproxy.upstream.connect with this server's egress,
timeout and key log.
Source code in src/netproxy/_server.py
243 244 245 246 247 248 249 250 251 252 253 254 255 | |
start_recording(connection, route, client, upstream)
A recording of one session, or None with no recorders.
Source code in src/netproxy/_server.py
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | |
relay_session(connection, route, reader, writer, upstream, *, recording=None, before_server=None)
async
Relay reader/writer with upstream, recorded when the
server has recorders (recording passes one already started).
Source code in src/netproxy/_server.py
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | |
ProxyServer(router=None, *, server=None, **options)
:class:AsyncProxyServer for synchronous code: the server runs on its
own event loop in a daemon thread. Same arguments.
:meth:start returns once every listener is bound (or raises what
binding raised); :meth:close stops the server and joins the thread.
Source code in src/netproxy/_server.py
356 357 358 359 360 361 362 363 364 365 366 367 368 369 | |
Tunnel(listen, target, route=None, name='')
dataclass
A local port whose connections go to one fixed target.
listen is where to accept ("127.0.0.1:15432"); target is
host:port. Each connection is routed as a Connection with scheme
"tcp" and listener name (default "tunnel:<target>"); route
overrides the router for this tunnel.
Action
Bases: Enum
What the proxy does with a connection.
DIRECT = 'direct'
class-attribute
instance-attribute
Relay bytes to the destination unchanged.
TLS = 'tls'
class-attribute
instance-attribute
The proxy makes the TLS connection to the destination with the route's client context. Plain HTTP is upgraded to HTTPS; a CONNECT tunnel is intercepted (the client's TLS ends at the proxy).
REJECT = 'reject'
class-attribute
instance-attribute
Refuse the connection.
Route(action=Action.DIRECT, tls=None, upstream=None, name='')
dataclass
The decision for one connection.
tls is required for, and only allowed with, :attr:Action.TLS.
upstream overrides the server's egress for this route (see
:data:Upstream). name labels the route in logs and check.
Router(rules=(), *, default=None)
Ordered rules; the first that matches decides, else default.
Thread-safe for reading; :meth:add is meant for set-up.
Source code in src/netproxy/_route.py
129 130 131 132 133 134 135 | |
add(rule, route=None)
Append rule: a :class:Rule, a (match, route) pair, or
add(match, route). Returns the stored :class:Rule.
Source code in src/netproxy/_route.py
144 145 146 147 148 149 150 151 152 153 154 155 156 | |
explain(connection)
The rule that matched (None for the default) and its route.
Source code in src/netproxy/_route.py
161 162 163 164 165 166 | |
Routing
Bases: Protocol
Anything that routes connections; :class:Router is the built-in one.
Rule(match, route, name='')
dataclass
route applies to connections match accepts.
match is a :class:~netproxy.rules.Matcher, any callable taking a
:class:~netproxy.Connection, or a pattern string for
:func:~netproxy.rules.parse_matcher.
ConfigError(msg, path=None, lineno=None, colno=None)
Bases: NetProxyError, ValueError
A configuration document is malformed or inconsistent.
path, lineno and colno locate the problem when known; the
message never quotes the document.
Source code in src/netproxy/exceptions.py
43 44 45 46 47 48 49 50 51 52 53 54 55 | |
CredentialUnavailableError
Bases: NetProxyError
A route's client credential could not be loaded (keystore entry missing, no PIN, keystore failure).
NetProxyError
Bases: Exception
Base class for every error this package raises.
RejectedError
Bases: NetProxyError
A rule rejected the connection.
UpstreamError
Bases: NetProxyError, OSError
The destination, or an upstream proxy on the way to it, could not be reached or refused the connection.
Upstream connections
Reaching a destination: directly or through upstream proxies, then TLS if the route says so.
Upstream proxies come from a proxylib ProxyMap (the route's upstream,
else the server's egress). Its answers are tried in order, so a PAC
result such as PROXY a:3128; DIRECT fails over. HTTP and HTTPS proxies
are supported (CONNECT, Basic credentials from the proxy URL).
UpstreamStream(reader, writer, via, tls, forward=False, proxy_authorization=None)
dataclass
An open connection to a destination.
via
instance-attribute
host:port of the upstream proxy used, or None for direct.
tls
instance-attribute
Whether the proxy made a TLS connection to the destination.
forward = False
class-attribute
instance-attribute
The stream reaches an upstream HTTP proxy without a tunnel: send it
absolute-form requests (forward_http=True and a proxy answer).
proxy_authorization = field(default=None, repr=False)
class-attribute
instance-attribute
Proxy-Authorization value for a forward stream, from the proxy
URL's credentials.
resolve_upstream(connection, route, egress=None)
async
The upstream answers to try for connection: proxylib Proxy
objects and None (direct), in order.
Source code in src/netproxy/upstream.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
connect(connection, route, *, egress=None, timeout=30.0, forward_http=False, keylog=None)
async
Open the connection route describes to connection's destination.
Raises :class:~netproxy.exceptions.UpstreamError when no upstream
answer works (the message lists each attempt, never proxy credentials)
and :class:~netproxy.exceptions.CredentialUnavailableError when a TLS
route's credential cannot be loaded. A REJECT route is a ValueError:
the caller should not have asked.
forward_http=True (a plain http:// request being forwarded) makes
a proxy answer return a stream to the proxy itself, flagged forward,
instead of a CONNECT tunnel: proxies commonly refuse CONNECT to port 80.
keylog is set as the TLS context's keylog_filename.
Source code in src/netproxy/upstream.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | |
TLS interception
TLS interception: the proxy ends the client's TLS with a certificate it mints.
A CONNECT whose route is :attr:~netproxy.Action.TLS is intercepted: the
proxy connects to the destination first (with the route's client credential),
answers 200, then completes the client's handshake with a leaf issued by
its CA for the CONNECT host, and relays the plaintext between the two TLS
connections. Clients must trust the CA (:attr:CertificateAuthority.certificate).
Create the CA with :func:create_ca so it is name-constrained to exactly the
hosts the TLS rules intercept: a leaked key then cannot vouch for anything
else.
Interceptor(ca, *, cache_size=256, leaf_days=7)
Server-side TLS for intercepted hosts, from ca.
Leaves (one per host, valid leaf_days) are cached, at most
cache_size of them, least recently used first out.
Source code in src/netproxy/intercept.py
44 45 46 47 48 49 50 51 52 53 54 55 | |
keylog = None
instance-attribute
Where minted contexts log TLS secrets (set by the server's keylog).
server_context(host)
The context presenting a leaf for host (minted on first use).
Source code in src/netproxy/intercept.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
intercept(server, connection, route, reader, writer)
async
Serve one intercepted CONNECT (the request head already read).
Source code in src/netproxy/intercept.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
permitted_names(router)
The hosts and networks router's TLS routes can intercept, or
None when some TLS rule (or the default) is not bounded by
Host/Network matchers and no name constraint can cover it.
Source code in src/netproxy/intercept.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | |
create_ca(router, common_name='netproxy local CA', *, days=3650, constrain=True)
A new CA for intercepting router's TLS routes.
With constrain (the default) it is name-constrained to
:func:permitted_names; when the rules cannot be bounded that way this
raises ValueError rather than create an unconstrained CA. Pass
constrain=False to accept an unconstrained one knowingly.
Source code in src/netproxy/intercept.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
Recording
Recording what passes through the proxy: a hook, a log, and pcap files.
A :class:Recorder sees each relayed connection as a :class:Session: when
it opens, every chunk each side sends, when each direction ends, and when it
closes. What it sees is what crossed the proxy: the plaintext of a TLS route
(the proxy made, or ended, that TLS) and of the request and response heads
it rewrote; the ciphertext of a tunnel it only relayed. Plaintext of an mTLS
session is as sensitive as the credentials that opened it; store it as such.
A recorder that raises is logged and stops receiving that session; traffic is never affected.
Session(id, connection, route, client, server, started)
dataclass
One relayed connection, as recorders see it.
client
instance-attribute
The client's address.
server
instance-attribute
The address the proxy connected to: the destination, or the upstream proxy that tunnelled to it.
started
instance-attribute
Seconds since the epoch.
Recorder
Bases: Protocol
What the server calls for each relayed connection. Calls come from the event loop's thread, one session's in order; each must return quickly.
data(session, sender, data)
sender is "client" or "server".
Source code in src/netproxy/recording.py
55 56 | |
ended(session, sender)
sender's direction has no more data.
Source code in src/netproxy/recording.py
58 59 | |
LogRecorder(logger='netproxy.traffic', level=logging.INFO)
Logs each session's opening and closing, with the octets each way, at
level on logger (default netproxy.traffic, INFO).
Source code in src/netproxy/recording.py
68 69 70 71 72 73 74 75 | |
PcapRecorder(path, *, format='pcap')
Writes every session to a capture file as a TCP connection.
Each session becomes one connection between the client's address and the
address the proxy connected to, in the file at path (pcap, or pcapng
when format="pcapng"). The octets are those the proxy relayed, so a
TLS route is recorded in plaintext on its TLS port: in Wireshark, Decode
As HTTP. Needs the capture extra (pktcap). Thread-safe; close it
(or use with) to flush the file.
Source code in src/netproxy/recording.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | |
Configuration files
Configuration files: read a document, then build a server from it.
The file is netproxy.cfg; its first line may name its format
(#!toml, #!yaml, #!json, #!ini), and without one it is TOML
(see :func:loads). Documents are
read by anycfg. :func:loads and :func:load return plain data. :func:build_server turns that data into an
:class:~netproxy.AsyncProxyServer; :func:load_server does both. Relative
paths in the document are relative to the file's directory. Secrets never
live in the file: PINs and key passwords are named by environment variable
(pin_env, password_env), read when the credential is first used.
Document shape, in TOML (every table optional)::
[server]
listen = "127.0.0.1:3128" # or a list
egress = "system" # "system" | "env" | "auto" | "direct" | "pac:<url>" | a proxy URL
timeout = 30.0
idle_timeout = 300.0
allow_remote = false
record = "proxy.pcap" # every relayed session, as a capture (extra "capture")
keylog = "tls-keys.log" # TLS secrets for Wireshark; a secret itself
log_traffic = true # a log line per session opened and closed
ca = { certificate = "ca.pem", key = "ca.key", password_env = "NETPROXY_CA_PASSWORD" }
[credentials.work] # backend = "cng" | "pkcs11" | "pem"
backend = "cng"
options = { store = "MY" } # the backend's constructor options
query = { subject = "alice" } # find_identity() criteria
pin_env = "NETPROXY_WORK_PIN" # or pin = "keystore": the OS dialog / PIN pad
cafile = "corp-ca.pem" # how the destination is verified (also verify = false)
[[rules]]
match = "*.corp.example" # a pattern or a list of patterns (any of)
port = 443 # optional
path = "^/api/" # optional regex, forwarded HTTP only
action = "tls" # "tls" | "direct" | "reject"
credential = "work" # required for "tls"
upstream = "direct" # optional: "direct", a proxy URL or a PAC result
name = "corp"
[default]
action = "direct"
[[tunnels]]
listen = "127.0.0.1:15432"
target = "db.corp.example:5432"
credential = "work" # optional: makes the tunnel's route TLS
In INI a dotted section nests ([credentials.work], [server.ca]),
[rules.<name>] and [tunnels.<name>] are list entries in file order
with that name, and values are typed: true/false (also
yes/no/on/off), integers, decimals, and JSON lists, objects
or strings (match = ["*.corp.example", "corp"]); anything else is a
string.
loads(text, *, path=None, format=None)
Parse a configuration document into plain data. path labels errors
and, by its suffix, may name the format.
The format is the one a #!<format> first line names (#!toml,
#!yaml, #!json, #!ini; the line is not part of the document),
else format, else the one path's suffix selects (.toml,
.yaml/.yml, .json, .ini; not .cfg), else TOML. An INI
document is read as described in the module documentation. Bytes are strict UTF-8 (one leading BOM
ignored). Raises :class:ConfigError with lineno/colno when
known; the message never quotes the document.
Source code in src/netproxy/config.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
load(source, *, format=None)
Read and parse the configuration file at source (OSError if
unreadable); the format is chosen as :func:loads chooses it.
Source code in src/netproxy/config.py
210 211 212 213 214 215 216 | |
default_path()
NETPROXY_CONFIG, else netproxy/netproxy.cfg in the platform's
configuration directory (%APPDATA% on Windows, $XDG_CONFIG_HOME or
~/.config elsewhere).
Source code in src/netproxy/config.py
219 220 221 222 223 224 225 226 227 228 229 230 231 232 | |
build_router(document, *, base_dir=None)
The :class:~netproxy.Router the document's rules and default describe.
Source code in src/netproxy/config.py
544 545 546 547 548 549 550 551 | |
build_server(document, *, base_dir=None, label='<document>')
An :class:~netproxy.AsyncProxyServer for document (from :func:loads).
Raises :class:ConfigError naming the table and key at fault, or
OSError for an egress PAC that cannot be fetched.
Source code in src/netproxy/config.py
554 555 556 557 558 559 560 561 562 563 564 565 | |
load_server(source)
:func:load then :func:build_server, paths relative to the file.
Source code in src/netproxy/config.py
568 569 570 571 | |
PAC scripts
A proxy auto-config (PAC) script generated from a router's rules.
Clients that read a PAC (browsers, OS proxy settings) then send only what needs the proxy through it. A rule PAC JavaScript cannot express exactly (a path, an IPv6 network, a custom callable) sends everything still undecided to the proxy, which routes it exactly: the script errs towards the proxy, never away from it.
generate_pac(router, proxy, *, egress=None)
A PAC script sending to proxy (host:port) every request
router does not send straight to its destination.
A route goes DIRECT in the script only when the proxy itself would connect
directly: a DIRECT route whose upstream is "direct", or none with no
egress. Rules are evaluated in order, the default last.
Source code in src/netproxy/pac.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
Exceptions
Errors raised by :mod:netproxy.
A caller's bad argument raises plain :class:TypeError or :class:ValueError;
everything here is a failure found at run time.
NetProxyError
Bases: Exception
Base class for every error this package raises.
UpstreamError
Bases: NetProxyError, OSError
The destination, or an upstream proxy on the way to it, could not be reached or refused the connection.
RejectedError
Bases: NetProxyError
A rule rejected the connection.
CredentialUnavailableError
Bases: NetProxyError
A route's client credential could not be loaded (keystore entry missing, no PIN, keystore failure).
ConfigError(msg, path=None, lineno=None, colno=None)
Bases: NetProxyError, ValueError
A configuration document is malformed or inconsistent.
path, lineno and colno locate the problem when known; the
message never quotes the document.
Source code in src/netproxy/exceptions.py
43 44 45 46 47 48 49 50 51 52 53 54 55 | |