Inbound
Updated
Tunnels: hostname routes to your machine
A tunnel gives a service on your own machine a public hostname. You run fixedmarkd next to the service, and partners reach it at that hostname on port 443, or at a TCP port, through two shared Fixedmark ingress IPs on two nodes. TLS passes through end to end. No Fixedmark node terminates it, and your machine opens no inbound port.
Docs preview: Fixedmark is not live yet. Endpoints are issued at launch, and these pages describe planned launch behavior. Anything marked planned may change. Join early access to get yours first.
What a tunnel route is
- A route belongs to an app. Its IPs are placements, chosen by Fixedmark. A route on the shared pool is placed on two shared ingress IPs, one on each of two nodes in its region.
- fixedmarkd on your machine dials each of those IPs on 443 and holds one session to each. Each partner connection comes down one of those sessions to the local target you name, such as
127.0.0.1:8080. - A node reads only the server name in the partner's TLS ClientHello, or the port for a TCP route, and splices the bytes. It never holds a key for your traffic.
- Every control you know from inbound IPs works the same: partner allowlists, inbound metering, connection logs, webhooks, and the ingress block.
- A shared route takes a connector origin, since a shared IP never dials out to a host. For a host origin, or for traffic that must arrive only at your own address, use the dedicated inbound IP add-on.
Route kinds
| Kind | Partners reach | TLS | Use it for |
|---|---|---|---|
http | https://<hostname> on 443 | fixedmarkd terminates it on your machine with a certificate it orders itself | A plain-HTTP service, such as a web app or an API on 127.0.0.1:8080 |
tls | <hostname> on 443 | Passed through to your target, which terminates it | A service that already speaks TLS and holds its own certificate |
tcp | <hostname> on a port from 20000 to 29999 | None added. The bytes pass unchanged | Databases, SSH, MQTT, and other raw TCP |
access | Signed-in people through fm-access | Passed through | Staff reaching an internal service. It takes no partner list |
The kinds are spelled http, tls, tcp, and access in the CLI, the API, and fixedmarkd's plan. The API refuses any other kind with route.kind_invalid.
Before you start
- Tunnels on Pro and up, with a dedicated inbound IP as an add-on at $9 per IP-month. Your plan's routes, hostnames, connectors, and inbound bandwidth are in Limits. A plan without tunnels refuses a route with
route.tunnel_not_entitled(402). - An app.
fm apps listprints its id and name.--apptakes either. - A Linux or macOS machine on amd64 or arm64 that can reach your service and make outbound connections to port 443. fixedmarkd does not run on Windows yet.
- The partner's source addresses, as IPv4 addresses or CIDRs. Every route except
accessadmits only the addresses you list.
Quick start: a first hostname route
Create a connector, install fixedmarkd as a service on the machine that holds your service, then create an http route that names the connector.
fm connectors create --app shop --name laptop
sudo fixedmarkd install --token-file fm-connector-laptop.tokenfm connectors create writes the token to a file readable by you only and prints the install line. Run the install line on the service's machine, from the directory that holds the file, then delete the file. Connectors covers the service and its files.
fm routes create --app shop --kind http --name web \
--connector laptop --to 127.0.0.1:8080 \
--partner 198.51.100.0/24curl -X POST "https://api.fixedmark.com/v1/apps/APP_ID/routes" \
-H "Authorization: Bearer $FIXEDMARK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"kind":"http","name":"web","origin":{"kind":"connector","connector_id":"CONNECTOR_ID","target":"127.0.0.1:8080"},"partner_cidrs":["198.51.100.0/24"]}'The API answers 202 with the route and a job_id. fm prints the route:
route ROUTE_ID
name web
kind http
hostname web.acme.fmtunnel.test
mode shared
region REGION
state pending
dns unpublished
placements 2
IP_ID 203.0.113.20:443
IP_ID 203.0.113.21:443
job JOB_ID
watch it with: fm jobs watch JOB_IDWithin about a minute fixedmarkd reads the route from its plan, attaches a session to each IP, and orders the hostname's certificate. Then, from a partner address:
curl -sv https://web.acme.fmtunnel.test/The request reaches 127.0.0.1:8080 on your machine as plain HTTP. fm routes show web reads the route's state, health, and DNS record, and fixedmarkd status shows the sessions and the certificate.
fmtunnel.test is a placeholder. The tunnel domain is not chosen yet. It will be a domain of its own, never under fixedmark.net. Hostnames on this page show it in the zone's place.Hostnames
Every route except access gets the hostname <name>.<org-slug>.<zone>, such as web.acme.fmtunnel.test. Partners use that name for every kind. A tcp route adds its port, such as db.acme.fmtunnel.test:20000.
<name>is the route's name, from--name. It is 1 to 40 lowercase letters, digits, and single hyphens, starts and ends with a letter or digit, and does not start withxn. It is unique among your org's live routes. Without--namethe route is named its kind and the first 8 hex digits of its id, such astcp-5f0c2b1e.- Names such as
www,api,app,login, andstatus, and any name startingpg-orip-, are reserved and refused withroute.name_reserved. So is any name containingfixedmark. The same rules hold for the org slug. <org-slug>is set by your org's first hostname route, from--org-slugor your org's name, and it is fixed after that. A later route always gets that slug. A first route that asks for a slug another org holds is refused withroute.slug_taken.- Your org's slug is its own label under the zone, but that label does not keep cookies apart between orgs yet. Browsers treat each org's hostnames as separate sites only once the tunnel domain is on the Public Suffix List, and that entry is pending. Until then, set cookies without a
Domainattribute, so each one stays on the hostname that set it. - Every org shares the weekly certificate budget, with a share for each org. See Certificates.
fmtunnel.test is a placeholder. The tunnel domain is not chosen yet. It will be a domain of its own, never under fixedmark.net. Hostnames on this page show it in the zone's place.Passthrough and HTTP termination
A tls or tcp route is passthrough. The partner's bytes reach your target unchanged, and your target holds the certificate, if any. fixedmarkd does not hand its own certificates to a local TLS server yet, so a tls target needs a certificate of its own that its partners accept.
An http route terminates TLS in fixedmarkd, on your machine. The node still passes the encrypted bytes through, and the key never leaves your machine. fixedmarkd then sends each request to your target as plain HTTP.
- Partners speak HTTP/2 or HTTP/1.1, picked by ALPN. Your target always gets HTTP/1.1.
- WebSockets work over HTTP/1.1 upgrades and over HTTP/2 extended CONNECT. Other upgrade tokens are not passed.
- The
Hostheader is kept as the partner sent it. A request for any other host than the route's hostname gets421. AHostthat is not all visible ASCII gets400. - An upgrade request that declares a body, by a
Content-Lengthother than 0 or by aTransfer-Encoding, gets400. - A plain
CONNECTgets405, so no partner can tunnel past your target. - A target that refuses or fails the request gets the partner a bare
502, and fixedmarkd logsupstream failed. - Up to 256 requests and WebSockets go to your targets at once. A request past that cap gets
503, and so does one that needs a new target connection while the connection cap is full. - A connection with nothing in flight closes after 75 seconds. A request's headers must arrive within 30 seconds and fit in 64 KiB, else the partner gets
431for size. fixedmarkd waits 10 seconds for your target to accept.
fixedmarkd drops these headers from the partner's request: every header whose name starts X-Forwarded, plus Forwarded, X-Real-IP, Fixedmark-Tunnel-Request, and the client-address headers X-Client-IP, True-Client-IP, CF-Connecting-IP, X-Cluster-Client-IP, Fastly-Client-IP, X-Original-Forwarded-For. A name matches in any case and with _ in place of -, so X_Forwarded_For is dropped too. CGI, WSGI, Rack, and PHP read that name as X-Forwarded-For. Then fixedmarkd sets its own headers. It never appends to a partner's value.
| Header | Value |
|---|---|
X-Forwarded-For, X-Real-IP | The partner's address |
X-Forwarded-Proto | https |
X-Forwarded-Host | The route's hostname |
Forwarded | for=<partner>;proto=https;host=<hostname> |
Fixedmark-Tunnel-Request | ?1 |
fixedmarkd sets no other header. Any header it does not drop, such as Authorization or Cookie, reaches your target as the partner sent it.
127.0.0.1, or check the source address, so no one else on your network can send a request with a forged X-Forwarded-For.Certificates
fixedmarkd orders the certificate for each http route's hostname from Let's Encrypt, by the ACME DNS-01 challenge. It makes the key on your machine. Fixedmark only writes the challenge's TXT record in the tunnel zone for it, so no key and no certificate ever leaves your machine.
- fixedmarkd makes a key, opens an order, and asks the control plane to publish the challenge value at
_acme-challenge.<hostname>. - It waits until every Fixedmark nameserver answers the value, then asks the CA to validate.
- It stores the certificate and serves it from the next handshake. The challenge record is removed.
- It renews in the window the CA's renewal information names, else at two thirds of the lifetime, and checks once an hour. A renewal never counts against the budget below.
Until the first certificate is issued, a partner's connection to the route is refused with cert_unavailable, and fixedmarkd status shows the certificate as queued or issuing.
| How it runs | State directory |
|---|---|
| Linux service | /var/lib/private/fixedmarkd, systemd's StateDirectory, mode 0700, owned by the service's dynamic user |
| macOS service | /Library/Application Support/fixedmarkd/state, mode 0700, owned by nobody |
fixedmarkd run in a terminal | --state-dir, else $XDG_STATE_HOME/fixedmarkd, else ~/.local/state/fixedmarkd |
- The directory holds the ACME account and one key and certificate per hostname. Every file is mode 0600.
- fixedmarkd refuses a state directory that is a symbolic link or that another user owns, and exits 2 with
fixedmarkd.state_dir. sudo fixedmarkd uninstallleaves the state directory in place, so the keys stay on the machine. Remove the directory yourself when you retire the machine.- On macOS the directory is owned by
nobody, which other services on the machine may also run as. Run fixedmarkd on a machine where that is acceptable.
Until the tunnel domain is on the Public Suffix List, Let's Encrypt allows the zone a small number of new certificates a week. Fixedmark shares it out: at most 45 first certificates across every org in 7 days, and at most 9 for one org. A first certificate past either share waits, and fixedmarkd retries after the time the control plane names. Renewals are exempt.
Two sessions per route
- fixedmarkd holds one session to each IP that carries one of its routes. A shared route has two placements, so it has two sessions.
- A partner reaches either IP. Each node sends the connection down its own session, so both sessions serve traffic all the time.
- fixedmarkd reads its plan every 60 seconds. When a dial to an IP fails, it reads the plan again within about 8 seconds, then backs off to once a minute, so it follows a route that moved without a restart. It stops these extra reads 10 minutes after the first failure, or as soon as the plan changes.
- One fixedmarkd per token. A second one with the same token replaces the first one's sessions.
Partners
Every route except access admits only the partner addresses you list. The node checks the list before anything reaches your machine. Setting partners replaces the whole list.
fm routes partners set web --partner 198.51.100.0/24 --partner 192.0.2.44A public website takes --partner 0.0.0.0/0 --allow-any. Without --allow-any, that entry, or any entry wider than a /8, is refused with route.partner_open_to_all, and with it the route carries the warning open_to_all.
DNS records
Each hostname has one CNAME record in the tunnel zone. A shared route's record points at its placement group's name, pg-<id>.<zone>, which holds one A record for each of the group's two IPs. Every record has a TTL of 60 seconds.
web.acme.fmtunnel.test. 60 IN CNAME pg-0123456789ab.fmtunnel.test.
pg-0123456789ab.fmtunnel.test. 60 IN A 203.0.113.20
pg-0123456789ab.fmtunnel.test. 60 IN A 203.0.113.21fmtunnel.test is a placeholder. The tunnel domain is not chosen yet. It will be a domain of its own, never under fixedmark.net. Hostnames on this page show it in the zone's place.- The record is written about two seconds after the route is created. The route's
dns.statereadsunpublishedright after the create, asfmprints it, thenpending, thensyncedwithin seconds. - When only one of the route's placements is healthy, the CNAME points at that IP's own name,
ip-<id>.<zone>, and returns to the group when both are. - Health never removes the record. It goes only while the route is disabled or has no placement, and when you delete the route.
dns.staterefusedmeans the DNS provider refused the record. Fixedmark retries it when the route changes and at an hourly check.dns.errorsays why.
Health and failover
Fixedmark judges each placement from its node's heartbeat, its listener, and your connector's session, and rolls those up into the route's health. Failover is by DNS, in three layers.
| What fails | Within | What happens |
|---|---|---|
| One node | About 47 seconds, plus up to 60 seconds of a resolver's cache | Its IP leaves the group's A records. A client that cannot connect to one A record tries the other one, so most partners see no failed connection. |
| Your session to one IP | Held 30 seconds, then the next DNS write | The route's CNAME points at the other IP's name. A session that comes back within the hold leaves the record alone, so a flapping session moves nothing. |
| Both IPs of the group | The route moves after 120 seconds down, and every route of a dead group within about 3 minutes | Fixedmark places the route on another healthy group in the same region and sends the route.placement webhook. The hostname follows. A tcp route keeps its port. If every other healthy group in the region already holds that port for another route, the route waits where it is and Fixedmark staff are paged. |
- These bounds are measured with 2 second heartbeats in Fixedmark's test lanes and scaled to the 10 second heartbeats production nodes send.
- A route never moves to another region by default. When every group of its region is down, it waits there, and Fixedmark is paged. Cross-region moves are off until Fixedmark turns them on.
- A failure on your side, such as
no_sessionororigin_down, never moves a route, since a move would not fix it. - A route on a dedicated IP is never re-placed. It follows its IP.
- Partners that dial the hostname follow every move. A partner that dials an address it looked up once must look it up again.
Status fields
fm routes show web
fixedmarkd status| Field | Values |
|---|---|
state | pending until a node binds a listener, bound, then active once the node's firewall admits the partners. disabled when Fixedmark staff stopped the route, paused when its IP is suspended. |
state_reason | Why a route is still pending, such as plan_lapsed, pool_draining, ip_retired, or placement_changed. |
health | The roll-up: healthy, degraded when one placement is healthy and another is not, down when none is, or unknown before the first check. |
placements[].health | healthy, no_session when fixedmarkd holds no session on that IP, origin_down, listener_down when Fixedmark cannot reach the port, ip_down when the node stopped answering, or unknown. |
dns.state | unpublished, pending, synced, or refused, with dns.target and dns.error. |
cert | An http route's certificate as fixedmarkd last reported it, and null on every other kind. cert.state is none before the first report, then queued, issuing, valid, renewing, renewal_failing when a renewal failed while the current certificate still serves, or failed when there is none to serve. It comes with serial, not_after, error, and reported_at. |
fixedmarkd status prints one line per IP with its session state, connecting, attached, detached, or stopped, and a CERTS table with one row per hostname: queued, issuing, valid, renewing, renewal_failing, or failed, with the expiry and the last error. --json prints the same file, with each http route's request and error counters.
Move a route to a dedicated IP
A route keeps its name and hostname when it moves between the shared pool and a dedicated IP of its app.
fm routes placement web --dedicated-ip IP_ID
fm routes placement web --sharedPUT /v1/apps/APP_ID/routes/ROUTE_ID/placementtakes{"mode":"shared"}or{"mode":"dedicated","ip_id":"IP_ID"}. It answers202when the route moves.- Asking for the placement the route already has changes nothing. The API answers
200withchangedfalse, andfmprintsunchanged: the route is already there. - A
tcproute with partners gets a new address and maybe a new port, which its partners dial. It moves only with--confirm, orconfirm: true, elseroute.placement_unconfirmed(409). - A dedicated placement needs the add-on. A plan without it refuses with
route.dedicated_not_entitled(402).
fm routes reference
A route is named by its id or its name. Commands that take one route search every app your key reads unless you pass --app.
| Command | What it does |
|---|---|
fm routes create --app <app> --kind http|tls|tcp|access | Creates a route. Add --name, --connector and --to, and --partner for each source. --org-slug sets your org's slug on its first route. --region picks a shared route's region. --dedicated-ip, --origin, --sni, and --port are for a dedicated IP. |
fm routes list --app <app> | Lists the app's routes with hostname, mode, port, state, and DNS. |
fm routes show [--app <app>] <route> | Prints one route with each placement. |
fm routes partners set [--app <app>] <route> --partner <cidr> | Replaces the partner list. Add --allow-any for 0.0.0.0/0. |
fm routes placement [--app <app>] <route> --shared|--dedicated-ip <id> | Moves the route. Add --confirm for a tcp route with partners. |
fm routes delete [--app <app>] <route> | Deletes the route. Its hostname's record goes too. |
A tcp route takes the same flags with --kind tcp, such as fm routes create --app shop --kind tcp --name db --connector laptop --to 127.0.0.1:5432 --partner 198.51.100.0/24. Partners then reach db.acme.fmtunnel.test on the port the route prints.
Limits and quotas
| Plan | Routes | Hostnames | Connectors | Inbound bandwidth |
|---|---|---|---|---|
| Pro | 10 | 10 | 5 | 50 GB |
| Business | 50 | 50 | 20 | 250 GB |
| Scale | 200 | 200 | 50 | 1 TB |
- Every route with a hostname counts against
hostnames, and every route againstroutes. Past either, a create is refused withroute.hostname_quotaorroute.quota(409), and the error'slimitnames the quota. - Tunnels come with paid plans only. The trial has none.
- A
tcproute takes one port from 20000 to 29999, the same port on both IPs of its group. - A route admits 1 to 256 partner entries, IPv4 only.
- fixedmarkd holds up to 256 partner connections at once by default, set by
--max-streams. - Inbound bytes count toward your plan's inbound bandwidth, apart from outbound. Ingress packs add more for one period.
See inbound limits for the rest. Each count comes from the plan catalog; 3 plans on sale include tunnels.
Troubleshooting
| Exit | Meaning | What to do |
|---|---|---|
0 | Stopped by SIGINT or SIGTERM. | Nothing. |
1 | An unexpected error, or a state directory fixedmarkd could not read or write (fixedmarkd.state_dir). | Read the last log line. The service restarts it. |
2 | Bad usage, a token file group or others can read (fixedmarkd.token_file_mode), an install on an OS with no service support (fixedmarkd.install_unsupported), or a refused state directory (fixedmarkd.state_dir). | chmod 600 the token file, or fix the directory's owner, then start it again. |
3 | The token was refused (connector.token_invalid) or the connector disabled (connector.disabled). | Install a new token, or contact support for a disabled connector. |
4 | The control plane sent a plan version this fixedmarkd does not read (fixedmarkd.plan_version). | Upgrade fixedmarkd. The Linux service does not restart on exit 4. |
5 | The control plane sent an older plan version than this fixedmarkd reads (fixedmarkd.plan_version_old). | Nothing on your side. The service restarts and tries again. |
| You see | Meaning | What to do |
|---|---|---|
placements[].health no_session | fixedmarkd holds no session on that IP. | Check fixedmarkd status and look for connector dial failed. The machine needs outbound 443 to both IPs. |
Connection refused with cert_unavailable | The http route has no valid certificate yet. | Read the CERTS table. queued or issuing clears in a few minutes. A budget wait names its retry time. |
CERTS last error fixedmarkd.acme_io | The control plane or the CA did not answer in time while fixedmarkd ordered the certificate. | Nothing. fixedmarkd retries with backoff, and a challenge record it left behind is removed within an hour. |
dns.state refused | The DNS provider refused the hostname's record. | Read dns.error, and contact support if it stays. |
state pending, state_reason plan_lapsed | The plan no longer includes tunnels, and the route lost its placements after the grace period. | Renew or upgrade. The route is placed again within 15 minutes. |
421 on every request | The partner sent another Host than the route's hostname. | Call the route's hostname, not an IP or another name. |
502 with upstream failed in the log | The target refused or dropped the request. | Check the service listens on the route's --to address on the connector's machine. |
| Code | Meaning |
|---|---|
route.tunnel_not_entitled (402) | Your plan does not include tunnels. |
route.dedicated_not_entitled (402) | A dedicated placement needs the dedicated inbound IP add-on. |
route.kind_origin (422) | A shared route or an http route needs a connector origin. |
route.name_invalid, route.name_reserved, route.name_taken | The name breaks the rules in Hostnames, or another live route has it. |
route.slug_taken | Another org holds the slug you sent with your org's first hostname route. |
route.quota, route.hostname_quota (409) | Your plan's routes or hostnames are all in use. |
route.pool_full (503) | The region's shared pool has no room. Retry later. |
route.tunnel_disabled (409) | Hostname routes are not on yet on this control plane. |
fm exits 2 on a malformed request (400 or 422) and 1 on any other refusal, such as 402, 409, or 503. Either way it prints the code. When you write to support, include the route id and the connector_id, never the token.
Frequently asked questions
Does Fixedmark see my traffic in clear text?
No. Nodes pass TLS through. For an http route, fixedmarkd terminates TLS on your own machine with a key that never leaves it.
Do I need a public IP or an open port?
No. fixedmarkd dials out to Fixedmark on port 443 and partner connections come back down that session.
Can I use my own domain?
Not yet. Every route gets a name under the tunnel zone, such as web.acme.<zone> for the acme org. Custom domains are planned.
When do I need the dedicated inbound IP add-on?
When partners must allowlist one address of your own, when traffic must arrive only at that address, or when the origin is a host Fixedmark dials rather than a connector.