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.

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)