Reaching the relay through an SSH tunnel
The relay can open an SSH connection to a server of yours and ask that server
to forward a port back to the relay. You then reach the relay at the server's
address, and nothing at home listens for the internet. The relay starts,
watches and restarts the ssh process itself.
Choose this when you already have a server with a public address and SSH access. If you have no server, the built-in Cloudflare Tunnel needs none.
What you need #
- The relay host has an
sshprogram on itsPATH(or you name another withcommand, see below). - An SSH login on the server that works with a key and no prompt. The
relay runs
sshin batch mode, so it cannot type a password or a key passphrase. A key with a passphrase works only if an SSH agent holds it. Test it first from the relay host:ssh relay@relay.must log in without asking anything.example. com - A free port on the server for the tunnel (
remote).Port - Something on the server that serves that port over HTTPS: a web server or reverse proxy such as Caddy or nginx. The tunnel carries plain HTTP, so without it your token and your conversations cross the internet unencrypted once the port is reachable. See Running the relay behind a reverse proxy.
The relay needs no special sshd setting. With remote at loopback it
asks for the forward without a bind address, so the server's own Gateway
setting decides where the port opens. At its default, no, that is the server
itself only, and the reverse proxy is the only thing that faces the internet.
With public the relay asks for all interfaces (*:), and that request
takes effect only if the server's sshd allows it (Gateway set to
clientspecified or yes).
Set it up #
Add an ssh block to config. (see
Where the file is) and restart the
relay with oar restart.
"sshTunnel": {
"mode": "managed",
"required": false,
"user": "relay",
"host": "relay.example.com",
"remotePort": 4444,
"remoteBind": "loopback",
"identityFile": "~/.ssh/id_ed25519"
}
On the server, send HTTPS to the tunnel port. With Caddy:
relay.example.com {
reverse_proxy localhost:4444
}
Open https:// in a browser and sign in with your access
token. The relay's start-up banner prints a Remote: line and oar url
prints a Tunnel URL: line with a QR code. Both are https:// plus the host
value, with no port and no check that the tunnel is up, so they are right only
when your proxy serves HTTPS on the standard port.
The sshTunnel keys #
All keys sit in the ssh block. The table agrees with
config.json, which has the same list.
| Key | Type | Default | What it does |
|---|---|---|---|
mode |
"disabled" or "managed" |
"disabled" |
managed makes the relay start and restart the tunnel. |
user |
string | none | The SSH user on the server. Required when managed. |
host |
string | none | The server's host name or address. Required when managed. |
remote |
number | none | The port that opens on the server, 1 to 65535. Required when managed. |
remote |
"loopback" or "public" |
"loopback" |
loopback asks for the port without a bind address (the server's Gateway decides, see What you need); public asks for all interfaces. Any other value counts as loopback. Independent of localhost. |
identity |
string | none | The private key file; ~ is expanded. Without it ssh uses your agent and its default keys. |
command |
string | ssh |
The SSH program, also used for the cleanup on the server. A name is looked up on the PATH; a path with a slash is used as given, and a relative one counts from the relay's program folder. |
required |
boolean | false |
While the tunnel is not connected, the relay takes no messages from the queue. See Required. |
auto |
boolean | true |
When the remote port is already taken, run a cleanup on the server and try again. Only false turns it off. |
reclaim |
boolean | false |
As part of that cleanup, also end your own idle SSH sessions on the server. |
remote |
string | built in | Your own cleanup command, run on the server instead of the built-in one. |
The old form "enabled": true still works and means "mode": "managed", but
only when mode is not set to disabled or managed.
The relay forwards the server's remote to its own port on 127.
(3333 unless you changed port). With public the forward is requested as
*:, otherwise as remote.
These settings are fixed and have no keys: the relay runs ssh -N with
Server, Server, Batch,
Strict and Exit. ssh gives
up on a connection after three unanswered checks, 30 seconds apart.
If a required value is missing (user, host or a valid remote in
managed mode), the relay starts as usual, logs which one, and leaves the tunnel
off.
How it behaves #
Start. The relay starts the tunnel once it is listening. It counts the
tunnel as connected when ssh has run for 1.2 seconds and the server has not
refused the forward. The log then says Tunnel up to user@host remote port N.
Reconnecting. When ssh exits, the relay starts it again after a wait that
grows: 5, 10, 20, 40 and then 60 seconds, and stays at 60. The count starts over
once a connection that lasted more than 30 seconds drops. Stopping the relay
stops the tunnel and the retries.
Host keys. A server the relay host has never seen is accepted and
remembered on the first connection. After that, a changed host key makes ssh
refuse to connect until you remove the old entry from known_.
Required #
With required: true the queue stays paused from start-up until the tunnel is
connected, and again whenever it drops. Messages you send wait in the queue and
run once the tunnel is up. Use it when the relay is useless to you without the
tunnel. A tunnel that cannot start because of a missing value keeps the queue
paused too.
When the remote port is taken #
If the server says the port cannot be bound, the usual cause is a leftover
session of an earlier tunnel that the server has not noticed is dead. Unless
auto is false, the relay then connects to the server a second
time and runs a cleanup command there.
These options run commands on your server
The cleanup runs under your SSH login and kills processes. It ends whatever
process of that user listens on the remote port, whether or not it is a
leftover tunnel. With reclaim it also ends every idle SSH
session of that user, which includes other ssh -N -R tunnels of the same
login on other ports. remote runs whatever you write. Read what
each step does before you leave them on.
The built-in cleanup does this, in order:
- It checks whether the port is in use (with
ss, or else by reading the kernel's list of listening sockets). If not, it stops and reports success. - It finds the processes listening on the port (with
lsof, or elsefuser, whichever exists), sends them a termination signal (kill) and waits one second. A process your login may not signal stays. If the port is free now, it reports success. - Only with
reclaim, and only if the port is still in use: it sends a termination signal to everyStale Ssh Sessions: true sshdsession of your user that has no child processes, then waits one second. These are the sessions anssh -N -Rleaves behind. Sessions that run a shell, an editor's remote server or any other program have children and are skipped, as is the session running the cleanup itself. - If the port is still in use, the cleanup exits with code 3, which the relay
logs as
is still held.
With remote set, your command runs instead of all of that, and
reclaim has no effect. The relay runs it as sh -lc '<command>' over a
second ssh login with the same key, in batch mode. It must exit with 0 when the port is free and with another code when it
is still held.
After a cleanup that freed the port, the relay retries after one second, up to
three times in a row. After a failed cleanup, or with auto off, it
uses the normal wait above.
Seeing the tunnel's state #
The SSH tunnel is not shown in the places the Cloudflare tunnel is:
oar status and oar doctor only report the Cloudflare tunnel, and the status
dot does not turn amber for an SSH tunnel (see
The status dot). Look here instead:
- The relay's log. Every line starts with
[ssh-. It shows thetunnel] sshcommand line it runs, whatsshprints,Tunnel up to ...,Reconnecting in Ns (attempt N)...and the reclaim steps. Where the log is depends on how the relay runs; see Files and folders. - The start-up banner, with a
Remote:line and aTunnel: mode=(orloopback public) line when the managed tunnel is configured. They show the configuration, not whether the tunnel is up.oar urlprints the same address asTunnel URL:. /, which has anapi/ status sshobject withTunnel connected,connected,Since reconnect,Attempts last,Error blocking,required,mode,valid,host,remoteandPort remote. Fetch it with your token.Bind Mode
last is exit:<code> after ssh ended, remote-
after the server refused the port, invalid- or the missing-value message
when the block is incomplete, and empty while the tunnel is up.
Security #
Once the tunnel is up, the relay can be reached from the internet, and the access token is all that stands between a stranger and your agents, which run commands on your machine. So:
- Keep
remoteatBind loopbackand put HTTPS in front on the server. Withpublic, anyone who can reach the server's tunnel port reaches the relay over plain HTTP. - The access token is the only lock. See The access token and who can reach the relay and What OAR exposes.
- The relay refuses requests to its internal session-worker paths when they
carry the Cloudflare marker header
cf-or a header you list inray tunnel. A plain reverse proxy adds none of these, so list the header your proxy adds, if it adds one you would like blocked. See config.json.Marker Headers
Troubleshooting #
Look at the [ssh- lines in the log first. Lines starting stderr:
are what ssh itself printed.
| You see | What it means | What to do |
|---|---|---|
ssh (also for host) |
A required value is missing. The tunnel stays off. | Set it and run oar restart. |
ssh |
The port is missing or not a number from 1 to 65535. | Fix it and restart. |
Tunnel mode disabled; running direct relay only. |
mode is not managed. |
Set "mode": "managed". |
stderr: ... Permission denied (publickey) |
The server did not accept your key. Batch mode means no password prompt. | Set identity, and test the same ssh user@host by hand on the relay host. |
Process exited (code= repeating |
ssh could not connect or log in. |
Run the logged ssh command by hand and read its error. |
stderr: ... remote port forwarding failed for listen port N, then Attempting remote reclaim ... |
The port on the server is in use. | Wait for the reclaim. If it ends with is still held, free the port yourself or pick another remote. |
Remote reclaim finished: listen port N is still held (code= |
The cleanup could not free the port. | Find the holder on the server, try reclaim, or set remote. |
stderr: ... Host key verification failed or REMOTE HOST IDENTIFICATION HAS CHANGED |
The server's key differs from the one saved. | If the change is expected, remove the old entry from known_. If not, do not connect. |
Reconnecting in 60s (attempt N) over and over, and the tunnel never says Tunnel up |
The connection fails each time. | Read the stderr: lines before each one. |
The tunnel says Tunnel up but the address does not answer |
The relay side is fine; the server side is not. | Check that the reverse proxy runs and points to the remote, and that remote matches how the proxy reaches it. |
| The tunnel connects, drops and reconnects in a loop | Something on the server or the network ends the connection. | The log shows the exit code and ssh messages. Check the server's sshd log. |
| Messages stay queued | required is true and the tunnel is not connected. |
Fix the tunnel, or set required to false. |