Link (origin tunnels)

The origin dials out.

A connector on your server opens a connection to a landing point we run and keeps it open. Requests arrive over that connection. Your server accepts nothing from the internet, and its address appears nowhere.

01

What this is for

Hiding an origin behind a CDN only works if the origin also refuses everybody who did not come through the CDN. That is a firewall rule that has to be kept correct forever, and an address that leaks once — an old DNS record, a mail header, a certificate transparency log — stays leaked.

A tunnel removes the question. There is no inbound port to protect, so there is no rule to get wrong, and an address that leaks reaches a server that will not answer it.

  • Works behind NAT, behind CGNAT, and on a connection with no fixed address — it dials out.
  • One connector can hold two landing points in different cities, so losing a city does not take the site down.
  • The origin is reached over an address that exists only inside our network; it is not routable from the internet.
A tunnel opened from your sideA visitor reaches the nearest location, which passes the request to a landing point inside our network. The connection to your server was dialled out by your server and is held open, so nothing ever dials in.VisitorNearest locationinside our networkLanding pointno public addressLanding pointno public addressYour serveraccepts nothing inbound
Nothing dials your server. The connector opens a connection to a landing point inside our network and holds it open, and requests arrive over it — so there is no inbound port to protect.
02

Set one up

  1. In the console open CDN → Link and create a tunnel: a name, the city it lands in, and the local address to forward to — usually http://127.0.0.1:8080.
  2. Copy the credential. It is shown once and is not stored anywhere we can read it back.
  3. Put the connector on the machine your site runs on and run it with the command the console gives you.
  4. Point a Shield site at the tunnel instead of at an origin address.
Install the connector
curl -fsSL https://dash.yunzheng.space/assets/orbit-link-linux-amd64 -o /usr/local/bin/orbit-link
chmod +x /usr/local/bin/orbit-link
Run it — the console fills in the endpoint and fingerprint for your tunnel
orbit-link --token lnk_… --endpoint 203.0.113.4:7844 --fingerprint sha256:… --local http://127.0.0.1:8080

Builds are published for Linux (x86-64, arm64 and ARMv7), macOS (Apple silicon and Intel) and Windows. The console links all of them.

Keep it running with systemd — the console fills in the command
[Unit]
Description=Orbit Link connector
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/orbit-link --token lnk_… --endpoint … --fingerprint … --local http://127.0.0.1:8080
Restart=always
RestartSec=5
DynamicUser=yes
StateDirectory=orbit-link

[Install]
WantedBy=multi-user.target
03

Run it in a container

The connector is published as a container image for Linux x86-64, arm64 and ARMv7. The image holds the same signed build as the downloads, runs as a non-root user on a read-only file system, and needs no shell.

Keep the settings in a file only you can read — the console fills it in for your tunnel
# orbit-link.env  (chmod 600 orbit-link.env)
ORBIT_LINK_TOKEN=lnk_…
ORBIT_LINK_ENDPOINT=203.0.113.4:7844
ORBIT_LINK_FINGERPRINT=…
ORBIT_LINK_LOCAL=http://127.0.0.1:8080
Run it with Docker
docker run -d --name orbit-link --restart unless-stopped --env-file orbit-link.env --network host \
  --read-only --cap-drop ALL --security-opt no-new-privileges \
  cr.yunzheng.space/orbit-link:1.3.0

Inside a container, 127.0.0.1 is the container itself. --network host lets the connector reach a service on the host; to reach another container, use its name instead.

Or next to your app with Docker Compose
services:
  app:
    image: your-app-image
  orbit-link:
    image: cr.yunzheng.space/orbit-link:1.3.0
    restart: unless-stopped
    env_file: orbit-link.env
    environment:
      ORBIT_LINK_LOCAL: http://app:8080
    read_only: true
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    depends_on: [app]
  • Keep the credential out of the image and out of the command line: pass it in an env file, or mount it as a secret and set ORBIT_LINK_TOKEN_FILE to its path.
  • The image does not update itself. Tags name an exact version (1.3.0), a minor line (1.3) and latest; to upgrade, change the tag.
  • Docker sees the connector's health: it reports healthy while a tunnel is up.
  • On docker stop the connector finishes the requests in flight, for up to 8 seconds (ORBIT_LINK_DRAIN), before it exits.
04

Kubernetes

Two Helm charts are published. orbit-link runs the connector for one tunnel as a Deployment. orbit-link-controller publishes Ingresses and Gateway API routes: each hostname you declare in the cluster is served through Orbit Link, with no inbound port and no load balancer.

The connector for one tunnel — the console fills in the values for your tunnel
# orbit-link-values.yaml  (chmod 600 orbit-link-values.yaml)
tunnel:
  token: lnk_…
  endpoint: "203.0.113.4:7844"
  fingerprint: "…"
  local: http://my-app.default.svc.cluster.local:8080

helm install orbit-link oci://cr.yunzheng.space/charts/orbit-link \
  --namespace orbit-link --create-namespace -f orbit-link-values.yaml
  • Two replicas by default, spread across nodes, each holding its own connection; several replicas behind one network address all stay connected.
  • Runs as a non-root user on a read-only file system; the credential is mounted as a file, not passed in the environment.
  • Readiness follows the tunnel: a replica is ready while its connection is up. On a rolling update each replica finishes the requests in flight before it stops.
The controller — with an API token limited to Orbit Link (Console → API → Orbit Link only)
kubectl create namespace orbit-link
kubectl -n orbit-link create secret generic orbit-api --from-file=token=./orbit-api-token
helm install orbit-link oci://cr.yunzheng.space/charts/orbit-link-controller -n orbit-link \
  --set clusterName=my-cluster --set orbit.existingSecret=orbit-api
Publish a service with an Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
spec:
  ingressClassName: orbit-link
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
  • Ingresses of class orbit-link, and HTTPRoutes attached to a Gateway of GatewayClass orbit-link, are published. Host and path rules (Prefix and Exact) are honoured.
  • The Ingress status, or the Gateway's addresses, show the name to point each hostname's CNAME record at. kubectl describe shows events for each hostname: published, already in use, or over the plan's limit.
  • Changing a host or a path takes effect at once; deleting the Ingress or route removes its hostnames from Orbit. helm uninstall removes everything the controller created.
  • TLS is terminated at the Orbit edge, which issues the certificate once the hostname's CNAME is in place.
  • Several controller replicas share the work: one of them makes changes, all of them serve traffic.
  • The controller and both charts are open source under the Apache License 2.0: github.com/yunzheng2006/orbit-link-controller.
05

Versions and updates

The current connector is 1.3.0 (2026-09-28). Download it for Linux x86-64, Linux arm64, Linux ARMv7, macOS Apple silicon, macOS Intel or Windows x86-64. orbit-link --version prints the version you have.

  • 1.3.0 (2026-09-28): several copies of the connector can run for one tunnel behind the same network address, for example as replicas in Kubernetes.
  • 1.2.0 (2026-09-28): finishes the requests in flight before it stops; can answer health checks; can read the credential from a file; a Linux ARMv7 build.
  • 1.1.2 (2026-09-25): on networks that block UDP, requests no longer fail after the connection has been idle.
  • 1.1.1 (2026-09-25): a connection that had to fall back from UDP returns to it once UDP works again.
  • 1.1.0 (2026-09-25): fixes the connector stopping after a few requests on networks that block UDP; connects over IPv4 and IPv6 at the same time; updates itself.
  • 1.0.0: first release.

From 1.1.0 the connector keeps itself up to date. It checks for a new version when it starts and every few hours, installs only builds signed with our release key, tries each new build before switching to it, and goes back to the previous build if the new one cannot bring the tunnel up. Under systemd it stages updates in its StateDirectory; run by hand, it replaces its own file when it may.

To pin a version, start it with --no-update (or set ORBIT_LINK_NO_UPDATE=1). The console marks tunnels whose connector is older than the current version. A 1.0.0 connector does not update itself: replace it once with the current build.
06

How the connection is secured

The connector dials an address, not a hostname, so there is no name for a public certificate to vouch for. Instead it pins the landing point's public key: the fingerprint on the command line is a SHA-256 of that key, and the connector refuses to talk to anything that does not present it.

The credential goes the other way: the landing point checks it and accepts the tunnel. A wrong credential and a tunnel that does not exist are refused identically — the difference between them is precisely what somebody guessing would like to learn.

Issuing a new credential disconnects the connector still using the old one, within about a minute. Have the new one ready to install before you rotate. Losing the install command is not a reason to rotate: the endpoint and fingerprint are on the tunnel's Install button at any time.
07

When UDP is blocked

The connector prefers QUIC on UDP 7844 and falls back to TCP on the same port. On a network that drops UDP entirely, add --tcp to skip straight to the fallback rather than waiting for QUIC to give up.

Only outbound connections are needed in both cases. Nothing ever dials your server.

08

Questions

Does my server need a public IP address?

No. That is the point. It needs to be able to make an outbound connection, which is all a home connection or a container on a private network can usually do.

What happens when the connector stops?

The tunnel goes away and the site behind it becomes unreachable. There is deliberately no public fallback address: keeping one would reintroduce exactly the leak the product exists to prevent. Run the connector under a supervisor — the console gives you a systemd unit.

Can one tunnel serve several sites?

Yes. Several Shield hostnames can pull from the same tunnel; the connector forwards them all to the same local address, and your own server decides what to do with each Host header.

Is it available on my account?

Link is switched on per platform. If the console says it is not switched on yet, the address pool has not been assigned — ask support.

Next

Related

Getting started

How an Orbit account is organised, what lives where in the console, and the shortest path from signing in to serving a name from our network.

DNS hosting

Add a zone, point your registrar at our name servers, manage records: weighted answers, health-checked records, ALIAS at the apex, zone-file import.

Connect a hostname

Delegate the whole domain, delegate one hostname, or add a CNAME and leave your DNS where it is. What each one costs you and when to pick it.

← All documentation