Where It May Connect

What the filtering proxy refuses, and how it reaches the hosts, endpoints and ports a sandbox may (The Filtering Proxy).

What the proxy refuses

It resolves each name itself and refuses the addresses below, and every address assigned to the machine whatever its range, such as a server's public address on its own interface. That covers the box, its network and the cloud's metadata service, 169.254.169.254, but not a public address the cloud's NAT holds for the box (known gaps).

The addresses

Loopback, private, link-local, carrier-grade NAT, unique-local, reserved (240.0.0.0/4), benchmarking (198.18.0.0/15) and 192.0.0.0/24 addresses, and Azure's host for its machines (168.63.129.16).

IPv4 inside IPv6

IPv4 ones written inside IPv6 (mapped, compatible, NAT64 64:ff9b::/96, 6to4 2002::/16) are refused as the IPv4 address they reach. check asks it for this machine, a metadata service and a private address in each form, the compatible ::a.b.c.d too.

A name resolving there

A name resolving there is refused the same way, so it can't point the exit at this computer either. It keeps a name's addresses for 30 seconds, and checks them on every connection.

Time and size

A request's head must arrive within 30 seconds, and is at most 16 KiB. A call that carries nothing either way for 10 minutes is ended: a tunnel (CONNECT), a plain http:// request, a hosted or local model's, or a Telegram call.

Connections at once

It serves at most 512 connections at once on each of its listeners; the next waits for one to end, so a program can't use up the proxy's file descriptors.

Ports

CONNECT reaches port 443 only, and a plain http:// request port 80 only, but for the endpoints a spec names (host:port, lnk agent allow --tcp). A list of hosts still applies to them. A host is only a name or an address.

Calls a minute

With a limit (per_minute, lnk agent allow --per-minute), it takes at most that many calls within a minute, of every verb, refused ones counted too, and answers the rest with a 429 and Retry-After. Those refusals are logged and count toward no pause.

A plain http:// request

It goes to its host alone: its Host header is set to the host checked, never the client's, the connection closes after one request, and a body in chunks is refused. So a CDN that routes by Host can't be pointed at another site past a list of hosts.

The TLS ClientHello

Over CONNECT to port 443, with a list of hosts, the TLS ClientHello must ask for the host checked (its SNI), or for none to a listed address, which TLS names none of, or the tunnel is closed. An encrypted ClientHello, or another Host inside the TLS, can still front another of a CDN's sites, which only the CDN can stop.

A request means one thing to every server

The proxy refuses a request head with a bare \r or \n, since every line must end in \r\n, or any other control character but tab. What follows a head is never read as its head. A local model's request with a folded header, or two lengths, is refused too.

Why

Some servers, such as Go's (which Ollama is) and Node's, end a line at a bare \n, so a header the proxy checked could otherwise hide another, or hide a second request, such as a pull behind a chat, that its filter never saw. The parsers are fuzzed: what they pass on is read back as such a server reads it.

Hosts, when listed

With [internet] hosts (--internet-host, lnk agent allow --internet-host), the proxy refuses by name any host not listed, before resolving it, with a 403 and no DNS query. Names are compared without case or a final dot. *.example.com covers the names under it, not example.com. An IP address is reached only if listed as one. Listed names still resolve here, and still never reach this machine or its networks.

Named TCP endpoints and git over SSH

The endpoints a spec names (host:port) are reached through the proxy, by ssh's ProxyCommand anywhere, and on Linux by an address of the sandbox's own.

Git over SSH

It gets out through GIT_SSH_COMMAND, whose ProxyCommand is lnk-sandbox sandbox connect. It runs inside the sandbox and tunnels through the proxy on its loopback, never anything else, once its host's port 22 is named. For ssh itself, run $GIT_SSH_COMMAND user@host.

An address of its own (Linux)

On Linux, a named host with a port from 1024 that its hosts allow also gets an address of its own in the sandbox, from 127.0.1.1 on, in the sandbox's own /etc/hosts. The proxy's end in the sandbox listens there and sends each connection to the proxy as CONNECT host:port, which the proxy checks and logs as any other. A port below 1024, an IP address, or macOS: only through a command such as ssh's ProxyCommand.

Its /etc/hosts

That file is written in a private folder beside its sockets' folder (<folder>.etc, which no other user can write or plant a link in), out of its reach.

Every name under a domain

An endpoint may hold every name under one domain on one port (*.abc.mongodb.net:27017), never the domain itself; its hosts still have to allow each name. check warns of one over a domain a provider shares among its customers (*.mongodb.net, a cloud region's *.us-east-1.rds.amazonaws.com), which reaches anyone's database.

DNS where a sandbox names endpoints (Linux)

A sandbox whose spec names endpoints gets a resolver: its own /etc/resolv.conf (in the same private folder as /etc/hosts) names 127.0.0.1:53 in its network, where its proxy answers, outside. Every other name is refused before anything is asked, so a query reaches only the nameservers of names the sandbox may reach anyway.

Listening there

The proxy has bubblewrap wait (--block-fd) until a helper, lnk-sandbox sandbox dns-socket, joins the sandbox's network as the owner of the user namespace bubblewrap made for it, binds 127.0.0.1:53, hands the sockets over and exits. It runs nothing in the sandbox and reads nothing of it. Where joining fails, the sandbox runs without DNS, and says so.

A name an endpoint holds

One its hosts allow gets an address of the sandbox's own: its /etc/hosts one, or one from 127.1.0.1 on for a name under a *. endpoint, where the proxy's end in the sandbox listens on the endpoint's ports (from 1024) on every loopback address. Such a name gets no IPv6 address.

Connecting to it

A connection there goes to the proxy as CONNECT to that address, which the proxy takes for the name it gave it, and checks as any.

A name its hosts allow

It gets its public addresses, never this machine's, its network's or a metadata service's; they reach nothing but through the proxy. A name whose only address refused is a metadata service's pauses the wire, as connecting to it does.

SRV and TXT

SRV and TXT for such a name, or a service of it (_mongodb._tcp.<name>), get what this machine's resolver answers, as records of that name only. An SRV answer's targets are names the program then looks up and connects to, each checked as any.

Never a local network's names

One label, or under .local, .localhost, .localdomain, .internal, .intranet, .private, .corp, .home, .lan or .arpa (home.arpa, reverse lookups), which this machine's resolver answers from its network (mDNS, a company's zone), nor those no public name has (.test, .example, .invalid, .alt, .onion).

A split view

With every host allowed, a name under a public domain that only your network's nameserver answers (a company's split view of corp.example.com, its _ldap._tcp records) still gets that answer.

Which resolver

Every lookup, these and a name's addresses, goes to this machine's resolver, never through the upstream: on a box with lnk agent exit home, the box's nameservers see the names, as they do for the proxy's own.

Logged and counted

Each query is a dns line in the wire's log. Over the limit it's refused (REFUSED). A refused one is logged once a minute at most and never counts toward a pause: looking up a name it may not reach gets the program nothing.

What counts

What asks outside counts toward its calls a minute: SRV, TXT, and an A or AAAA query whose name's addresses aren't kept from a lookup in the last 30 seconds, nor kept as having none from one in the last 5, nor being looked up already; not an endpoint's own name, nor a refused one.

Kept and shared

A name's addresses come from this machine's resolver as the proxy's connections do, kept 30 seconds and shared with them; a name with none is kept as such 5 seconds. A name is looked up once at a time: its A and AAAA, and connections at once to it, wait for the first lookup's answer rather than asking again.

Its limits

At most 16 lookups that wait on a nameserver run at once, for 5 seconds each (SRV and TXT, 10), so slow ones neither stall the proxy's own nor hold up the sandbox's answers for its endpoints. It answers 256 queries over UDP at once and drops the rest, and gives at most 65,000 names under *. endpoints an address for as long as its proxy runs; a name past them gets SERVFAIL.

What it reads

Queries are read as plain DNS of one question; anything else gets an error. An answer over UDP longer than 512 bytes is cut for the program to ask again over TCP. The reader is fuzzed.

The machine's own ports

The ports a spec names, such as a local model's, are reachable inside at the same port, relayed through a third socket. The port is checked outside, and no other port of the machine is reachable: every port ([localhost] ports = ["*"]) is taken only with [network] mode = "host", never behind a proxy. A port or model a sandbox names gives it its proxy, so on Linux the port is relayed in like any other.