Proxy filters the traffic you allow
link-sandbox-proxy: the sandbox's one way out, a filtering proxy Link
runs outside it, with a table of routes that names none of them. Every
sandboxed agent with [internet] runs behind it, unless it has
[network] mode = "host", and any sandbox can have it instead of the
network with lnk sandbox run --internet-host (or a spec's
[internet]): everything below but the home exit.
Where its traffic leaves
On this computer it leaves from here, upstream direct. On a box it's
always behind the proxy, whatever its [network]: lnk agent move sets
its route, and a new box's setup makes it the box's default, which every
agent made there starts with and one already there with no route takes.
From a box it leaves either from the box itself (lnk agent exit direct, upstream direct) or only through this computer (lnk agent exit home).
A SOCKS upstream on a Unix socket
It must be a socket owned and served by this user or root. Symlinks and other file types are refused. Linux pins its opened inode across the connection; every supported OS checks the connected peer's UID, so a path swapped to another user's server is refused before the SOCKS greeting is sent.
An HTTP proxy upstream
An upstream that's an HTTP proxy on this machine's loopback
(http://127.0.0.1:<port>) is handed each internet host's name,
unresolved, as CONNECT <name>:<port>: it resolves the name, and must
refuse what this proxy would (this machine, its network, a metadata
service), since nothing here checks the address it reaches.
What's still checked here
The hosts the sandbox may reach are still checked here, by name, before
it's asked, an address the sandbox names is checked here as any, and
[lan] * reaches no other name. A machine whose resolver must not be
asked a name a sandbox chose uses it: its DNS then answers only the
endpoints' own names, an allowed name has no records, and only a name
[lan] lists by itself, or one of no internet's, is dialed here.
Its only way out
Anything that doesn't use a proxy has no network at all, and no DNS but its proxy's where it names endpoints (Where It May Connect). The proxy carries TCP, not UDP.
On Linux
bubblewrap gives the harness a network namespace (--unshare-net): its
own loopback, nothing else. Its only way out is a relay inside the
sandbox, 127.0.0.1:40781 and its HTTPS_PROXY, to a unix socket
where Link's proxy, outside the sandbox, takes HTTP CONNECT and plain
http:// requests.
On macOS
With no network namespaces, the proxy listens on a free port of the
machine's loopback. The sandbox's Seatbelt rules allow connecting to
that port and the spec's own, nothing else: not ::ffff:127.0.0.1, not
the Mac's own address.
Other programs on a Mac
Other programs of this machine, and other users, can connect to that port. Through it they reach the internet as they could directly. Other users get no key and no bot: those routes take only the placeholder the sandbox was given, a tag made under a secret random for each run and holder, which nobody else can make.
Nor its tools
Nor its tools, its local model or open: their URLs
(LNK_TOOL_<NAME>, LNK_LOCAL_MODEL_<PORT>, LNK_SANDBOX_OPEN, and
those the harness is set up with) carry a secret of the agent's, which
the proxy checks, and the proxy writes neither in its log. So no other
user asks you to open a page in the sandbox's name.
Programs you run yourself
Outside a sandbox, they're not kept out. The placeholders, made anew at each start, and the URL secret, made once, are written in the harness's home and Link's settings, readable by your user. A program of yours that reads one can use it while it holds, as it could read the key itself in Link's settings.
A sandboxed program
Every sandbox hides agents' folders (~/Link/Agents, and
~/Link/History), --read ~ or not, so a sandboxed program reads no
agent's placeholder but its own, or one in a folder you open to it with
--read or --write.
What a web page can send
The proxy refuses it, whatever its path: a request to the proxy itself
(not CONNECT or http://...) with a Host that isn't this machine's
loopback, which a name pointed here (DNS rebinding) carries, or one a
browser marks as from another site (Origin, Sec-Fetch-Site).
Before it counts
It is refused before it counts toward a pause or the calls a minute,
and logged as browser once a minute at most, so a page the user visits
can't pause an agent or use up its calls.
Its sockets, and reaching the harness
An agent's sockets are in ~/.lnk/agents/.net/<agent id>/<harness>
(700), in Link's settings folder, which the sandbox opens alone to the
harness. A run's (lnk sandbox run) are in a temporary folder with a
random name, made by the run (700): no other user can make it first.
The reverse relay
Link reaches the harness for its health checks through it: Link listens on the harness's ports on the box's loopback and passes connections in through a second socket, as does a login's callback.
Never through a link
The sandbox makes that socket in the folder it writes, so Link connects to it without following a link, and only if it is a socket of the user's: a link the sandbox puts in its place, to the session bus or Docker's socket, reaches nothing.
A blank proxy (Linux)
For a machine that packs agents, a proxy, its sandbox and its harness can be started blank, holding nothing of anyone's, and bound to one agent later.
Started blank
lnk sandbox proxy --blank --control <socket> --dir <folder> (with
--hold, or a program after --) holds no rules, key, secret or wire.
It listens on its folder's out.sock and answers every call 503 (this proxy isn't bound to an agent yet), so a sandbox behind it reaches
nothing at all. --listen and --dns aren't taken with --blank.
Its control socket
<socket> is made 0600 and takes only its own user's connections, or,
with --binder <uid>, that one user's too, so a service running as
another user binds a proxy that isn't its own; every other user is
refused, root among them unless it's the one named.
Bound
It's bound by one line of JSON there, link_sandbox_client::ProxyBind:
what its command line takes, by the same names, but with its routes
(what each is given, a model's key and secrets among it), their caller's
key and the URL secret themselves in place of the files they'd be read
from, so none of them is ever written to the disk.
Its answer
A bind with routes names who calls them, or it's refused. It answers one
line, {} or {"error"}, and a bind that doesn't hold binds nothing.
From then on it's the proxy those arguments start. It's bound once:
another bind is refused, and there's no unbinding, since the binding
ends with the process.
A bridge reaching the harness
A bridge reaches the harness through the sandbox's own in.sock in the
same folder, its endpoint unix:<folder>/in.sock:<port> (the
connection
contract),
with no port on the machine's loopback.
As itself
The sandbox writes that folder, so the bridge reaches the socket as itself: never through a link put in its place (Linux opens it without following one and connects through the open file), only a socket owned by the folder's owner, and only when what listens on it runs as that user (its peer's credentials). A sandbox that swaps in a link to another socket, or a socket of another user's, gets nothing: the connection is refused before the proof is asked.
A blank sandbox's spec
It names no owner's path: an empty home of its own, the harness's files
read only, and the proxy's folder (reached). A test shows it reads
nothing of the folder beside its own, and that its only way out is that
proxy.
The proxy as another user (Linux)
A proxy can run as a user other than its agent's, such as a user that holds the machine's secrets and runs nothing else, so the agent's user can't read what the proxy holds. The two share the socket folder alone, by a group only they're in.
Its folder
lnk sandbox proxy --dir <folder> --agent-user <uid> --group <gid>
(blank or not) starts only when <folder> is of group <gid>, mode
2770: theirs, closed to everyone else, and set-group-ID, so what's
made in it takes its group. It makes out.sock and local.sock there
0660, of that group.
in.sock
The relay inside the sandbox makes in.sock 0660; it can't name the
group from inside its user namespace, so the socket takes the folder's.
The proxy connects to in.sock only as a socket of <uid>'s, never
through a link: not its own user's, which a sandbox can't make.
The folders a sandbox reaches
lnk sandbox wrap keeps a folder the sandbox reaches (reached) that
is its user's with mode 2770 as it is, and makes every other its
user's alone (0700). The proxies one sandbox reaches share their
folders alike, or it's refused.
Nothing else is shared
The agent's home is its user's alone, and the proxy's wire and control socket are the proxy's user's. A test packs two agents with their proxies each a second user, and checks that each sandbox's way out still works through its own proxy, that no user of one slot reaches anything of the other's, and that a proxy's user reads nothing of its own agent's but the socket folder.
With no sandbox
An agent with no sandbox (lnk agent start --no-sandbox) keeps its
proxy. Its tools (but run and file, which need
one),
its memory, its scheduler, its model's key and its sealed secrets added
outside, the wire's log, its counted usage, lnk sandbox stop and
pausing don't depend on the wall, so they stay; only the wall goes.
Unwalled
lnk sandbox wrap given unwalled (no wall, its proxy beside it) runs
the harness as it is under lnk sandbox proxy --listen <port>, on this
machine's loopback at the agent's own port (the last of its block, as on
a Mac), its routes behind its URL secret. The harness is pointed at it
(HTTPS_PROXY), and the proxy filters and logs what it's sent as the
agent's spec says. With [network] mode = "host" it's pointed at no
proxy, which then serves only Link's ways out.
Advice, not a wall
The proxy is then advice the program may ignore. Nothing keeps the
harness to it: it reaches the internet, your network and this machine
directly, and reads and writes what you can, Link's settings among
them, where the keys its proxy adds are kept. Its hosts and endpoints
filter only what it sends its proxy, and stop closes only that.
No exit, and harness run
An exit is refused with no sandbox: only a sandbox keeps a program to
its proxy, so with none, an exit would hold nothing back. lnk agent start says so. lnk agent harness run with no sandbox runs its command
beside a proxy of its own on a free port it holds and hands it open, as
on a Mac.
Home
The proxy connects through a SOCKS proxy on a Unix socket on the box,
~/.lnk/exit/exit.sock (the agent's upstream, socks5://<that path>),
that this computer offers with ssh -R over Link's SSH connection to
the box. The connection leaves from this computer; the harness's DNS
lookups happen on the box.
The exit
That is the exit plugin's exit, lnk exit start <box>, which lnk agent exit home holds for the agent and which stops once nobody holds it. For
a machine that isn't a box, it's ssh -R with your own SSH setup.
The socket
The socket is the box user's only, in the machine folder, ~/.lnk,
which every sandbox hides: the agent can't reach it but through its
proxy. Anything of your user on the machine can use that SOCKS proxy
while it's up, and on the port anything at all, so it's your home
address for whatever runs there, not only your agent.
One round trip
The proxy sends the SOCKS greeting and its request at once, one round trip to the exit. Of a host's addresses, the next is tried 1 s after the one before through the exit, not the quarter second it waits when connecting directly.
Fails closed
With this computer off or the SSH connection down, nothing answers on the socket and nothing gets out. The proxy never falls back to the box's own network.
Your agents alone
This computer as an exit serves only your own agents, through your own SSH connection. The SOCKS proxy on the box is reachable only by programs on your box, and forwards to one on this computer's loopback that reaches only the internet, never this computer or its networks (the exit's security).
What stays direct
The harness's install and configuration, such as downloading a harness,
leave from the box, through a proxy of their own limited to the hosts
the adapter lists, and lnk itself uses the box's network. Only the
running agent's traffic goes home.
Overlapping routes
When routes overlap, the most specific matching prefix wins. A typed action can therefore coexist with its namespace’s native carrier.
Docs
- Where It May Connect: What the filtering proxy refuses, and how it reaches the hosts, endpoints and ports a sandbox may (The Filtering Proxy)
- Decisions: Why the sandbox's filtering proxy,
link-sandbox-proxy, works the way it does (The Filtering Proxy) - The Wire: What the filtering proxy logs of every call, and how its wire is stopped or paused (The Filtering Proxy)