Build your own SSH client
OuterSSH is a C library. It essentially has two interface points:
- You call its C APIs directly, creating SSH sessions, proxy servers, and sometimes port forwarding.
- Your browser or web view connects to its proxy server or forwarded port.
Typical usage involves:
- Create a session pool (this involves authentication)
- Register that session pool with a local proxy server
Creating a session pool
Other SSH libraries use SSH sessions as their main primitive. OuterSSH is focused on laptops and mobile, so it focuses more on robustness and recovery, since laptops are always closing and phones are always suspending apps. So the session primitive of OuterSSH is a session pool. In today’s version, the pool typically has only one session, but the key change is that this session may disconnect and be replaced over time.
You’ll call outerssh_session_pool_create to create a pool, providing an address and credentials. You can then optionally call outerssh_session_pool_ensure_session to make it immediately connect, to verify. If connecting fails due to invalid credentials, you’ll destroy the pool and start over, presumably displaying appropriate UI to the user.
You now have SSH sessions where you can run commands. Typically, next, you’ll start a proxy server or a port forward, using one of the following paths:
- HTTP proxy:
outerssh_http_proxy_server_create,outerssh_http_proxy_server_start - SOCKS proxy:
outerssh_socks_proxy_server_create,outerssh_socks_proxy_server_start - Port forward:
outerssh_port_forward_create,outerssh_port_forward_start
Proxies and port forwards are two different ways for a browser web view to connect to remote endpoints. Using a proxy server is more powerful than port forwarding, because the browser can reach multiple apps/endpoints on the server. It’s also potentially more secure, because the proxy server can be guarded by a username and password (rather than being accessible to all users and processes).
Configure your browser or web view to use this server.
Special domains
The proxy server will now translate URLs into locations relative to the remote server. However:
- Browsers actually circumvent the proxy server for “localhost” URLs
- Chromium-based web views let you disable this. WebKit-based web views don’t.
- Browsers don’t support a protocol for providing a path to a Unix socket
So the proxy has two special domains:
http://4000.localhost.outerssh.invalid/pathreacheshttp://localhost:4000/pathhttp://<encoded-socket-path>.unix.outerssh.invalid/pathreaches the socket at, requesting path "/path". The encoded path is the unpadded lowercase base32 encoded socket path, with "." characters inserted every 50 characters to avoid any of these subdomains too long.
Use outerssh_proxy_encode_loopback_host and outerssh_proxy_encode_socket_host to generate these hostnames.
Extra complexity for WebKit web views
OuterSSH’s HTTP proxy has two transformations it can perform: rewrite_request_authorities and rewrite_loopback_urls_in_html. Currently, for WebKit based web views, both are necessary, and there’s also some JavaScript that you should inject into the web view. For Chromium based web views, only the rewrite_request_authorities is necessary.
Because WebKit doesn’t support using “localhost” URLs with a proxy, we have to use special domains, described above. When you navigate your web view / browser, you’ll simple translate it to use a special hostname. But that only solves top-level navigation; we also need to solve all the cases where web apps use absolute URLs internally, converting them both in HTML “href”s and in JavaScript APIs.
OuterSSH makes more changes for you at the HTTP proxy level, if you enable them. Absolute localhost URLs within pages need to be converted to use special domains, but then HTTP headers need to have these special domains converted back into localhost (apps like Jupyter require this). So the proxy converts incoming URLs from localhost URLs into special domains, and it converts outgoing URLs back into localhost URLs. OuterSSH handles all of this for you, you just need to use the HTTP proxy (not the SOCKS proxy) and enable rewrite_request_authorities and rewrite_loopback_urls_in_html.
Additional changes are needed in your web view code. You need to inject some JavaScript in the page. It will wrap various APIs and convert localhost URLs, similar to above, for dynamically constructed URLs. And it will add a MutationObserver that changes URLs back into “localhost” URLs before displaying them in the page (so a user never sees the special domain in the page). You can copy-paste this script from here.
I hope this all becomes unnecessary someday, the WebKit web view just needs to give us a way to make it stop circumventing the proxy for localhost URLs. In the meantime, Chromium web views can skip most of this; just make sure you disable Chromium’s implicit proxy bypass for localhost and loopback addresses with --proxy-bypass-list="<-loopback>", alongside your proxy configuration. In WebView2, pass this flag through AdditionalBrowserArguments when creating the environment.
Even in browser engines like Chromium which have proper localhost proxy support, we still use special domains for Unix socket support. In these cases, the web app is aware of the fact that it is using a Unix socket, so we don’t have to worry about absolute URLs needing to be transformed, but the backend (for example, Jupyter) can get confused when these special URLs are in HTTP headers. So our HTTP proxy converts the domain in HTTP headers to just “localhost”. Enable rewrite_request_authorities for this; Chromium clients can leave rewrite_loopback_urls_in_html disabled. (If browsers formally supported a “http+unix://” protocol, this whole section would become unnecessary.)
Root-owned sockets
The proxy will connect your web view to any socket on the server that your SSH credentials have the authority to connect to. You just put the path to the socket in the special domain. If this path is to a root-owned folder like /run/... or /var/run/... and you’re not logged in as root, OuterSSH will run a helper executable “outer-socket-bridge” under sudo, and this process will form a dedicated tunnel to that socket. OuterSSH will forward any sudo experience to your app, and it’s your app’s responsibility to show it to the user (i.e. showing a password input).
To show sudo password UI, register callbacks via outerssh_http_proxy_server_set_sudo_recovery_handler (or outerssh_socks_proxy_server_set_sudo_recovery_handler for SOCKS proxies). While OuterSSH waits for the password, it will keep the user’s connection pending. Your app will call outerssh_session_pool_ensure_sudo_socket_bridge to provide the password, then you’ll call the completion recovery handler completion. This flow doesn’t only occur during navigation, it can also occur in various recovery scenarios, like when the user opens their laptop lid and the connection is reestablished.
The socket allowlist
Unix domain sockets are like ports, except they live on the filesystem and hence can be restricted using filesystem permissions. It has been proposed multiple times that browsers could in theory connect to Unix domain sockets, so web app backends could live there. Every time this feature was discussed, they concluded that an allow list would be necessary, because some apps use Unix sockets explicitly because browsers can’t connect to them. OuterSSH introduces such an allow list:
Here’s where this allow list lives on the remote server:
- Linux, per user:
$XDG_CONFIG_HOME/outerssh/http-unix.allow, or~/.config/outerssh/http-unix.allowifXDG_CONFIG_HOMEis unset or empty. - Linux, system:
/etc/outerssh/http-unix.allow - macOS, per user:
~/Library/Application Support/org.outerssh/http-unix.allow - macOS, system:
/Library/Application Support/org.outerssh/http-unix.allow
It’s a newline-delimited list of socket paths. OuterSSH will check this list before connecting to a socket. Anybody hosting an app on a Unix domain socket should add that socket to this allow list.
The packaged “outer-socket-bridge” executable
You’ll typically bundle a few versions of this executable for different targets, for example Outer Loop bundles it for Linux using glibc and musl versions, for both x86 and arm.
Fingerprint verification
SSH clients have to decide how to ask the user whether they trust the server that they’ve connected to. This is a design decision for you. Set the host_key_validator and decide what user experience to provide.
Cookies
Your app should keep different cookie stores for different servers. Otherwise, your “localhost:8888” or “/run/user/MyApp” cookies will collide with each other across servers.
Showing error messages
Your SSH client should surface errors to the user. Hook into errors using:
outerssh_http_proxy_server_set_connection_failure_handlerouterssh_socks_proxy_server_set_connection_failure_handlerouterssh_port_forward_set_connection_failure_handler
END
Which type of proxy should I use?
Your app plugs in authentication information. Often, though, there’s some back-and-forth. (???)
END
A browser using OuterSSH loads normal HTTP or HTTPS URLs through a local proxy. For Unix sockets and remote loopback ports, the URL hostname encodes the destination. OuterSSH decodes it and prepares the connection when traffic arrives; the browser does not register a route before each navigation.
This contract has two layers:
- Browser-to-proxy: proxy configuration, authentication, destination URLs, HTTP behavior, and browser storage isolation.
- Host-to-library: creation and ownership of pools and frontends, callbacks, resource loading, error handling, and shutdown.
A browser can embed the C library or receive a proxy endpoint from another process that embeds it. Outer Loop’s macOS XPC protocol is an application adapter, not an OuterSSH wire protocol. The standalone outerssh tool is another host; its command-line defaults are not the browser defaults described here.
The pieces
| Piece | Responsibility |
|---|---|
OuterSSHContext |
Associates pools and frontends with a shared serial control queue. |
OuterSSHSessionPool |
Connections to one SSH server/account, authentication, commands, socket authorization, watches, and remote bridges. |
OuterSSHHTTPProxyServer |
A local HTTP/1.1 and CONNECT listener, with optional HTTP rewriting. This is the frontend all four current web browsers use. |
OuterSSHSOCKSProxyServer |
A local SOCKS5 listener. Carries raw bytes without HTTP rewriting. |
OuterSSHPortForward |
A local TCP listener forwarding to one fixed destination through one pool. |
| Browser adapter | Configures the web view, translates user-facing socket URLs, manages credentials and storage, handles prompts, and presents errors. |
Each proxy forwards through one session pool, supplied at creation. A pool can serve multiple HTTP or SOCKS frontends. The proxy selects the SSH server; the URL selects an endpoint on that server.
The HTTP frontend forwards directly into the shared SSH machinery. It does not need a SOCKS listener in between. Apple apps also start SOCKS for URLSession/outerframe traffic; Linux currently starts both listeners; Windows starts HTTP only.
A connection, from selection to navigation
- Resolve the saved server/account configuration and obtain authentication material.
- Create a context and a pool. Supply host-key validation and packaged bridge loading as needed.
- Install the pool’s socket-availability handler if the application will watch sockets.
- Optionally call
outerssh_session_pool_ensure_sessionto authenticate before presenting the browser. Forwarding and commands can also acquire a session on demand. - Create an HTTP frontend with the pool, configure
rewrite_request_authoritiesandrewrite_loopback_urls_in_htmlfor the browser, then install its failure/recovery handlers. - Start the listener, usually on port
0to obtain an available port. Copy the returned endpoint before its callback returns. - Configure the browser’s proxy and credential handler before loading a page.
- Translate socket URLs into ordinary HTTP URLs using the shared address helpers. Navigate; OuterSSH authorizes and connects on demand.
- On disconnect, stop browser submissions, destroy frontends, and then destroy the pool in the documented order.
Starting a listener is not proof that SSH authentication or a particular remote application has succeeded. Those are separate operations with separate failures.
Read the contract
- Library lifecycle and callbacks: inputs, ownership, queues, authentication, commands, and shutdown.
- URLs, destinations, and origins: endpoint encoding and pool selection.
- HTTP and browser networking: proxy credentials, rewriting, tunnels, and limitations.
- Remote sockets, recovery, and availability: allowlists, sudo, helper binaries, watches, and errors.
- The four browser adapters: what each application actually configures.
- Questions for API review: concrete asymmetries exposed by this documentation.
What the browser still owns
OuterSSH does not provide a Select Server window, address bar, history, tabs, cookies, a credential store, or an installation UI for remote applications. It does not know that Outer Shell is a special application. Finding or installing Outer Shell, interpreting friendly names and icons, and handling outerloop:// links belong to the host.
The library can execute arbitrary remote commands for its trusted host. A product that separates its browser and SSH processes must enforce its own IPC command policy; the local HTTP/SOCKS protocols do not expose the command-execution C API.