---
title: "Connectors: Run fixedmarkd for Tunnels and Inbound"
description: "Run fixedmarkd behind NAT so partners reach your service by hostname: create a connector, install the service, state, certificates, status, and exit codes."
canonical: https://fixedmark.com/docs/connectors
updated: 2026-11-07
source: Fixedmark (https://fixedmark.com), BitMask LLP
---

Inbound

# Connectors: fixedmarkd on your machine

A connector is fixedmarkd, the program that serves your tunnel routes from a machine with no public IP. It dials out on port 443 to each Fixedmark IP that carries one of its routes and holds a session open, and the node sends each partner connection down that session to a local target such as 127.0.0.1:8080. For an http route it also orders the hostname's certificate and terminates TLS on your machine. 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](https://fixedmark.com/early-access) to get yours first.

## What a connector is

A connector is the origin of every [tunnel](https://fixedmark.com/docs/tunnels) route, and one of two kinds of origin on a [dedicated inbound IP](https://fixedmark.com/docs/inbound). A route's origin is either a host the node dials, which only a dedicated IP can do, or a connector and a target on the connector's machine. Use a connector when the service sits behind home NAT, office NAT, or carrier-grade NAT, or when you do not want to open a port to the internet.

- fixedmarkd dials each Fixedmark IP that carries one of its routes on TCP 443 with TLS 1.3, the server name `connector.fixedmark.net`, and the ALPN `fm-connector/1`. It needs outbound 443 only.
- It holds one session per IP, so two for a tunnel route on its two shared IPs, and opens a stream inside a session for each partner connection.
- The node checks the partner list before a stream opens, then meters the bytes as inbound and writes the connection log row, exactly as for a host origin.
- For a `tls` or `tcp` route the bytes pass through unchanged. For an `http` route fixedmarkd terminates TLS on your machine with a certificate it orders itself, as [Tunnels](https://fixedmark.com/docs/tunnels#passthrough) explains. No node ever terminates the partner's TLS.

## Before you start

- A plan with tunnels. Tunnels on Pro and up, with a dedicated inbound IP as an add-on at $9 per IP-month. Your plan also sets how many connectors your org can hold, as Limits lists.
- An app for the connector and its routes. `fm apps list` prints its id and name.
- A Linux or macOS machine on amd64 or arm64 that can reach the 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.

## Create a connector

Name the connector after the machine or the service it serves. A name is 1 to 64 letters, digits, dots, dashes, or underscores, starts with a letter or digit, and is unique within the app.

**Terminal**

```bash
fm connectors create --app APP_ID --name billing-db
```

`fm` writes the connector's token to a file in the current directory, readable by you only, and prints the install line. The token is shown this once. Fixedmark stores only its SHA-256 hash and cannot show it again.

```
connector billing-db created id=CONNECTOR_ID
token_prefix xxxxxxxx
token written to fm-connector-billing-db.token (mode 0600)
install sudo fixedmarkd install --token-file fm-connector-billing-db.token
Run the install line from this directory. The token is never shown again.
```

A token is `fmc_` followed by 52 characters. `fm connectors list` and the dashboard show only the first 8 characters after `fmc_`, as its prefix. To write the file somewhere else, add `--token-file PATH`. `fm` never replaces a file that exists. The dashboard's connectors page for the app creates a connector too, and shows the same install line once.

Keep the token in the file. Do not paste it into a command, since a command line lands in shell history and the process list. Anyone with the token can attach a connector to your routes, so rotate it if it leaks.

## Install fixedmarkd and the service

fixedmarkd is the connector program. It is separate from `fm`, needs no `fm` on the machine, and reads no `fm` configuration. The fixedmarkd download is not published yet.

On the machine that holds the service, from the directory with the token file, run the install line:

**Terminal**

```bash
sudo fixedmarkd install --token-file fm-connector-billing-db.token
```

1. `sudo fixedmarkd install` copies the token to a root-only file, writes the service, and starts it.
2. The service starts `fixedmarkd run`, which attaches within seconds once a route names the connector.
3. systemd also gives the service a state directory for its certificate keys. On macOS install creates one at mode 0700, owned by `nobody`.

After the install, delete the token file in your directory. The service holds its own copy.

## Point a route at the connector

A route takes one origin. Pass `--connector` and `--to` for a target on the connector's machine. The target is `host:port` as that machine sees it, so `127.0.0.1`, a private address, or a local name all work. Leave out `--dedicated-ip` and the route is a tunnel on the shared pool.

**fm**

```bash
fm routes create --app APP_ID --kind http --name billing \
  --connector billing-db --to 127.0.0.1:8080 \
  --partner 198.51.100.0/24
```

**API**

```bash
curl -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":"billing","origin":{"kind":"connector","connector_id":"CONNECTOR_ID","target":"127.0.0.1:8080"},"partner_cidrs":["198.51.100.0/24"]}'
```

Partners then reach `https://billing.<org-slug>.<zone>`, and each request reaches `127.0.0.1:8080` on your machine. `tls` and `tcp` routes take a connector the same way, with `--kind tls` or `--kind tcp`. fixedmarkd reads new routes from its plan within 60 seconds, so it needs no restart.

_Connector route refusals_

| Code                             | Meaning                                                      |
| -------------------------------- | ------------------------------------------------------------ |
| `route.origin_conflict`          | The request names both a host origin and a connector.        |
| `route.connector_not_found`      | No connector with that name or id exists in the route's app. |
| `route.connector_disabled`       | The connector is disabled. Contact support.                  |
| `route.target_invalid`           | The target is not `host:port` with a port from 1 to 65535.   |
| `route.proxy_protocol_host_only` | `--proxy-protocol` was set on a host origin.                 |

## Run it in the foreground

To try a connector before you install the service, run it in a terminal. It reads the token from the file, which must be readable by you only.

**Terminal**

```bash
chmod 600 fm-connector-billing-db.token
fixedmarkd run --token-file fm-connector-billing-db.token --log-format text
```

Without `--token-file`, `fixedmarkd run` reads the token from `FIXEDMARKD_TOKEN`, else from `~/.config/fixedmarkd/token`, or `~/Library/Application Support/fixedmarkd/token` on macOS. Stop it with Ctrl+C. It waits up to 10 seconds for open partner connections to finish, then exits 0.

_fixedmarkd run flags_

| Flag                | Default                                                                                 | Meaning                                                                                                                   |
| ------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `--token-file`      | `FIXEDMARKD_TOKEN`                                                                      | The token file. Refused when group or others can read it.                                                                 |
| `--log-format`      | `json`                                                                                  | `json` or `text`. `--log` is the same flag.                                                                               |
| `--api-url`         | `https://api.fixedmark.com`                                                             | The control plane. `FIXEDMARKD_API_URL` sets it too.                                                                      |
| `--connect-timeout` | `5s`                                                                                    | How long to wait for the local target to accept.                                                                          |
| `--max-streams`     | `256`                                                                                   | Partner connections open at once, from 1 to 2048. Past it a new one is refused with `busy`.                               |
| `--state-dir`       | `$STATE_DIRECTORY`, else `$XDG_STATE_HOME/fixedmarkd`, else `~/.local/state/fixedmarkd` | Where the ACME account and each `http` route's key and certificate live, at mode 0700. See Certificates.                  |
| `--status-file`     | `$XDG_RUNTIME_DIR/fixedmarkd/status.json`                                               | Where `fixedmarkd status` reads the state. Without `XDG_RUNTIME_DIR`, and for the service, `/run/fixedmarkd/status.json`. |

## Run it as a service

`sudo fixedmarkd install` sets up the service for your OS. Run it again with a new `--token-file` after a rotate. Add `--dry-run` to print the files and commands without writing anything.

_What fixedmarkd install writes_

| OS    | Service                                                            | Token                                                      | Logs                                      |
| ----- | ------------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------- |
| Linux | `fixedmarkd.service`, a systemd unit with its own dynamic user     | `/etc/fixedmarkd/token`, mode 0600                         | `journalctl -u fixedmarkd`                |
| macOS | `net.fixedmark.fixedmarkd`, a launchd daemon that runs as `nobody` | `/Library/Application Support/fixedmarkd/token`, mode 0600 | `/Library/Logs/fixedmarkd/fixedmarkd.log` |

**Linux**

```bash
systemctl is-active fixedmarkd
journalctl -u fixedmarkd -f
```

**macOS**

```bash
sudo launchctl print system/net.fixedmark.fixedmarkd
tail -f /Library/Logs/fixedmarkd/fixedmarkd.log
```

- systemd restarts the service 5 seconds after any exit except exit codes 2, 3, and 4, which a restart cannot fix.
- launchd restarts every exit, so on macOS the service waits 5 minutes before it exits 3 or 4. A revoked token then asks the control plane once every 5 minutes.
- `sudo fixedmarkd uninstall` stops the service and removes the files `fixedmarkd install` wrote. It leaves any file it did not write, and it keeps the state directory, so the ACME account and the certificate keys stay on the machine until you remove the directory. On another OS, install exits 2 with `fixedmarkd.install_unsupported`.

## Check the status

**Terminal**

```bash
fixedmarkd status
sudo fixedmarkd status --token-file /etc/fixedmarkd/token
```

`fixedmarkd status` prints one line per IP with its state, `connecting`, `attached`, `detached`, or `stopped`, the node, and the open streams. A CERTS table follows with one row per `http` route's hostname and its certificate's state. With `--token-file` it adds the control plane's view of each session. `--json` prints the status file, with the routes, the certificates, and each `http` route's counters.

No endpoints yet

Until a route names the connector, `fixedmarkd status` prints `no endpoints yet` and the log says `plan has no routes yet`. Create a route with `--connector` and the session attaches within a minute.

Fixedmark shows each placement's health beside the route. A placement reads `healthy` while a session to its IP is attached. When no session on that IP has been seen for 20 seconds, it reads `no_session`, the route's roll-up turns `degraded` or `down`, and the [webhook](https://fixedmark.com/docs/webhooks) `route.health` fires with `origin_kind` and `connector_id`. While no session is attached, a partner's connection opens and then closes, and your [connection log](https://fixedmark.com/docs/logs) shows `result` `connector_offline`.

## The plan

fixedmarkd learns its routes from a plan it reads from the control plane every 60 seconds. After a dial fails it reads the plan again within about 8 seconds, then backs off to once a minute. The plan is version 2. It names each route's id, name, kind, hostname, target, and placements, the IPs to dial, and for `http` routes the CA and the challenge relay to use.

- A field fixedmarkd does not know is ignored, so a control plane that adds an optional field never stops the fleet.
- A newer version than fixedmarkd reads stops it with exit code 4, `fixedmarkd.plan_version`. Upgrade fixedmarkd.
- An older version stops it with exit code 5, `fixedmarkd.plan_version_old`, and the service restarts until the control plane is upgraded.
- A plan with a bad entry is refused whole, logged, and read again. The last good plan keeps serving.

## Certificates

For each `http` route, fixedmarkd orders a certificate for the hostname by ACME DNS-01 and keeps the key in its state directory. Fixedmark only publishes the challenge record. No key leaves the machine. [Tunnels](https://fixedmark.com/docs/tunnels#certificates) covers the order, the renewal, and the weekly budget.

- Without a state directory fixedmarkd issues no certificate, and an `http` route refuses partners with `cert_unavailable`.
- Run one fixedmarkd per state directory. Two on one directory would order the same certificates twice.
- Keep the state directory when you move the service. With an empty one, fixedmarkd makes a new key and orders each certificate again. Within 90 days of a hostname's last certificate, Fixedmark counts that order as a renewal, so it does not use the weekly budget. Let's Encrypt still allows only 5 certificates for the same names a week, so repeated moves can hit that limit. The route then refuses partners with `cert_unavailable` until the limit clears.

## Exit codes

_fixedmarkd exit codes_

| Code | Meaning                                                                                                                                                                                                                                                                                                                                          |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `0`  | Stopped by SIGINT or SIGTERM, after up to 10 seconds for open streams to finish.                                                                                                                                                                                                                                                                 |
| `1`  | An unexpected error, such as `fixedmarkd.api_not_found` when the API URL serves no plan, or `fixedmarkd.install_foreign_file` when a service file exists that fixedmarkd did not write. Also `fixedmarkd.state_dir` when the state directory cannot be read or written.                                                                          |
| `2`  | Bad usage. Also `fixedmarkd.token_file_mode` (the token file is readable by group or others), `fixedmarkd.token_missing`, `fixedmarkd.token_malformed`, `fixedmarkd.api_url_insecure`, `fixedmarkd.install_unsupported`, `fixedmarkd.install_needs_root`, and `fixedmarkd.state_dir` (the state directory is a symbolic link or another user's). |
| `3`  | The control plane refused the token with `connector.token_invalid`, so install a new token. Or Fixedmark disabled the connector with `connector.disabled`, which a new token does not change, so contact [support](https://fixedmark.com/docs/support) .                                                                                         |
| `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 it, and the macOS service waits 5 minutes first.                                                                                                                                       |
| `5`  | The control plane sent an older plan version than this fixedmarkd reads, `fixedmarkd.plan_version_old`. The control plane is behind, and the service restarts until it catches up.                                                                                                                                                               |

`fixedmarkd run --help` prints the same table.

## Log lines

fixedmarkd writes one JSON line per event to standard output, or text with `--log-format text`. Every line carries `service` (`fixedmarkd`), `version`, and `direction`, every line after the first plan read adds `connector_id`, and no line carries the token.

_fixedmarkd log lines_

| Line                                      | When                                                                   |
| ----------------------------------------- | ---------------------------------------------------------------------- |
| `connector starting`                      | At start.                                                              |
| `plan refreshed`                          | Each plan read, every 60 seconds, with `routes` and `endpoints`.       |
| `plan has no routes yet`                  | The connector exists, but no route names it.                           |
| `connector attached`                      | A session is up, with `ip`, `node_id`, and `routes`.                   |
| `connector detached`                      | The session ended, with the `reason` and `retry_in_ms`.                |
| `connector dial failed`                   | The machine could not reach the IP on 443.                             |
| `stream opened`, `stream closed`          | One partner connection, with `partner`, `target`, bytes, and `result`. |
| `stream cap reached`                      | Every stream slot is in use. New partners are refused with `busy`.     |
| `connector token rejected`                | The control plane refused the token. Exit 3 follows.                   |
| `connector draining`, `connector stopped` | Shutdown.                                                              |

## PROXY protocol v2

Your service sees each partner connection arrive from fixedmarkd on its own machine. To give a `tcp` or `tls` target the partner's address, create the route with `--proxy-protocol`, or `proxy_protocol: true` in the API origin. fixedmarkd then sends a PROXY protocol v2 header before the first byte. The source is the partner's address and port, and the destination is the Fixedmark IP and the route's port. An `http` route takes no PROXY header: fixedmarkd sets `X-Forwarded-For` instead.

**Terminal**

```bash
fm routes create --app APP_ID --kind tcp --name db \
  --connector billing-db --to 127.0.0.1:5432 --proxy-protocol \
  --partner 198.51.100.0/24
```

Turn it on only when the service expects the header, as HAProxy, NGINX with `proxy_protocol`, and PostgreSQL poolers such as PgBouncer can. A service that does not expect it reads the header as data and fails the connection.

## Limits

_Connectors by plan_

| Plan     | Connectors per org |
| -------- | ------------------ |
| Pro      | 5                  |
| Business | 20                 |
| Scale    | 50                 |

These counts are proposed and may change before connectors leave early access. `connector.quota` answers a create past your plan's count, and `connector.not_entitled` (402) answers one on a plan without tunnels.

- fixedmarkd holds up to 256 partner connections open at once by default. Past that it refuses new ones with the reason `busy`, and the partner's connection closes. `--max-streams` changes the count.
- The node gives one session 1000 streams. Past that a partner gets `connector_busy`.
- One session per connector and IP. A second fixedmarkd with the same token replaces the first, so run one per token. For more capacity, create more connectors.
- fixedmarkd pings the node every 15 seconds. The node closes a session that sends nothing for 45 seconds, and fixedmarkd reconnects.

What a connector cannot do:

- Carry UDP. Routes are TCP, and `tls` and `http` routes are TLS over TCP.
- Open a port outside the route ranges. Partners reach ports 20000 to 29999 for `tcp` routes, and 443 for every other kind, as [inbound limits](https://fixedmark.com/docs/limits#inbound) lists.
- Bypass the partner list. The node admits only the partner addresses on the route, before anything reaches your machine.
- Reach a target that no route names. fixedmarkd opens only the targets in its plan and refuses any other with `target_not_in_plan`.

## Troubleshooting by close reason

Find the `reason` in the `connector detached` line, or the `result` in the connection log row, then follow its row.

_Session close reasons_

| Reason               | Meaning                                                                                           | What to do                                                                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connector_replaced` | A second session with the same token attached to the IP. The newest one wins.                     | Stop the other fixedmarkd, or create a second connector for it.                                                                                       |
| `token_rotated`      | The token was rotated. The old token now exits 3.                                                 | Run `sudo fixedmarkd install --token-file` with the new token file.                                                                                   |
| `connector_disabled` | Fixedmark staff disabled the connector.                                                           | Contact [support](https://fixedmark.com/docs/support) .                                                                                               |
| `connector_unbound`  | No route on the IP names the connector anymore, for example after a route moved to another group. | Nothing when the route moved: fixedmarkd reads its plan and dials the new IPs. Otherwise create a route with `--connector`, or uninstall the service. |
| `staff_disconnect`   | Fixedmark staff reset the session.                                                                | Nothing. fixedmarkd reconnects on its own.                                                                                                            |
| `keepalive_timeout`  | The node heard nothing for 45 seconds.                                                            | Check the machine's network. If it repeats every few minutes, a NAT or firewall on its path drops idle flows.                                         |
| `shutdown`           | The node restarted for an update.                                                                 | Nothing. fixedmarkd reconnects within seconds.                                                                                                        |

_Partner connection results on a connector route_

| Result               | Meaning                                                                                                     | What to do                                                                            |
| -------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `connector_offline`  | No session was attached on the IP.                                                                          | Check `fixedmarkd status` and the service's logs for `connector dial failed`.         |
| `target_unreachable` | fixedmarkd could not open the target, with a reason such as `connect_refused`, `timeout`, `dns`, or `busy`. | Check the service listens on the route's `--to` address from the connector's machine. |
| `connector_busy`     | The session's 1000 streams were in use.                                                                     | Set idle timeouts on the partner side, or spread partners over more connectors.       |

When you write to [support](https://fixedmark.com/docs/support), include the `connector_id` and the reason from your log. Never include the token.

## Rotate or delete a connector

**Terminal**

```bash
fm connectors rotate --app APP_ID billing-db
sudo fixedmarkd install --token-file fm-connector-billing-db.token
fm connectors delete --app APP_ID billing-db
```

A rotate writes a new token file and closes the session at once, so install the new token right away. A delete is refused with `connector.in_use` while a route names the connector. Delete or repoint those routes first.

## Frequently asked questions

### Does a connector need an open inbound port on my machine?

No. fixedmarkd dials out to your Fixedmark IP on port 443 and holds the session open. Partner connections come back down that session, so the machine needs outbound 443 only.

### Do partner allowlists still work with a connector?

Yes. Partners connect to the route's hostname or IP as with any inbound route, and the node admits only the partner addresses you list before anything reaches your machine.

### Can I run fixedmarkd on Windows?

Not yet. fixedmarkd runs on Linux and macOS.
