FPS can use ordinary browser or application TLS sessions as carriers. The debug
fps_carrier utility is useful for deterministic tests, but beta operators also
need to understand the real-origin flow.
Carrier sessions feed the generic FPS covert datagram core. In the current Linux VPN adapter, those datagrams are TUN packets plus control metadata; carrier setup is therefore required before the TUN lease is useful for traffic.
The application must still believe it is connecting to the real carrier origin.
Only the TCP destination is redirected to the local fps_client listener.
That means:
wss://carrier.example.net/stream;Host stay the real hostname;127.0.0.1:443 or whichever port fps_client
listens on;/etc/hosts is the simplest beta mechanism for browser/app flows.Do not open https://127.0.0.1/ for a hostname-based origin. That changes the
browser origin and usually breaks certificates, SNI, Host, cookies or CORS.
For a self-hosted carrier origin, fps_server should be treated as a
pre-reverse-proxy: it is the public TCP gate in front of the real origin path,
but it must not terminate the carrier TLS session itself.
A typical deployment can be:
Internet
-> fps_server
-> optional L4/TCP proxy, TLS passthrough load balancer or SNI router
-> real reverse proxy that terminates TLS
-> backend application
The real reverse proxy, application gateway or backend can terminate TLS only
after fps_server has already relayed the carrier byte stream. Ordinary HTTP
reverse proxies or CDNs that terminate TLS before the traffic reaches
fps_server break this model, because FPS no longer sees the same end-to-end
carrier TLS records.
L4/TCP proxying, TLS passthrough and SNI-routing products can fit this topology because they forward TCP/TLS without decrypting it. They are less common than standard HTTP reverse-proxy/CDN setups and may have different operational or pricing tradeoffs, so treat this as an explicit deployment requirement rather than an automatic property of “put it behind a proxy”.
For a default HTTPS/WSS origin on port 443:
echo '127.0.0.1 carrier.example.net' | sudo tee -a /etc/hosts
Then connect to the original URL:
wss://carrier.example.net/stream
DNS and /etc/hosts do not rewrite ports. If the local FPS client listens on a
non-default port, use a tool that can connect to that port while preserving SNI
and Host, or change the client listener to 127.0.0.1:443.
Remove the mapping after the test:
sudo sed -i '/carrier.example.net/d' /etc/hosts
A router can act as the FPS client entry point for a whole LAN. This is useful
when the operator wants applications on laptops, phones or TVs to create carrier
sessions without running fps_client on each device.
Conceptual shape:
LAN device opens wss://carrier.example.net
|
v
router DNS maps carrier.example.net -> router LAN IP
|
v
fps_client listens on router LAN IP:443
|
v
fps_server -> carrier origin
The same core rule still applies: the LAN application must use the real carrier
hostname in its URL, SNI and Host header. Only DNS resolution changes.
Router-side requirements:
fps_client must listen on a LAN-reachable address and port, commonly the
router LAN IP on :443; binding to 127.0.0.1 would only work for processes
running on the router itself.For OpenWrt-style DNS, the usual pattern is a per-domain dnsmasq/DHCP-DNS
override through LuCI or UCI rather than editing every LAN device. Exact syntax
depends on the router image and DNS stack, but the intent is:
carrier.example.net -> 192.168.1.1
where 192.168.1.1 is the router LAN address that fps_client listens on.
This pattern does not remove the live-carrier requirement. LAN clients can have
working DNS and a valid FPS lease while the tunnel is unusable because
sessions.carriers_current == 0. Keep at least one carrier session alive before
debugging SOCKS, routes or application traffic.
A leased TUN address does not mean the tunnel is usable by itself. It means the client identity authenticated and received an address. VPN/SOCKS traffic also needs at least one live carrier.
Check both sides:
docker compose run --rm --no-deps fps-server status
docker compose run --rm --no-deps fps-client status
Ready signals:
auth.authenticated > 0;sessions.carriers_current > 0;tun.leased_client_address;auth.*_failed and classified_record.*_failed counters stay at zero for a clean
smoke run.If tun.leased_client_address exists but sessions.carriers_current == 0, the
client has an address but no current cover session. Open or restart a carrier
client/browser tab before debugging routes or SOCKS.
Use an origin and application protocol that the operator controls or is already legitimately expected to use. Good carrier candidates are long-lived TLS sessions such as WebSocket streams, HTTPS long polling, keep-alive HTTPS flows, internet radio clients, game clients or other applications that naturally keep TLS connections open.
The exact carrier client depends on the chosen origin. For an
operator-controlled WebSocket echo endpoint, a minimal Python holder can keep
several sessions alive after the /etc/hosts mapping is in place:
python3 -m pip install --user websockets==12.0
export FPS_CARRIER_URL='wss://carrier.example.net/stream'
python3 - <<'PY'
import asyncio
import itertools
import os
import ssl
import websockets
URL = os.environ["FPS_CARRIER_URL"]
async def hold(index):
ssl_context = ssl.create_default_context()
async with websockets.connect(URL, ssl=ssl_context) as ws:
for counter in itertools.count():
message = f"fps-carrier-{index}-{counter}"
await ws.send(message)
echoed = await ws.recv()
if echoed != message:
raise RuntimeError(f"unexpected echo: {echoed!r}")
await asyncio.sleep(1.0)
async def main():
await asyncio.gather(*(hold(i) for i in range(3)))
asyncio.run(main())
PY
For websocat, use the real URL after adding the hosts entry:
websocat -v wss://carrier.example.net/stream
Type a message and confirm it is echoed. Keep the process running while testing TUN or SOCKS traffic. For non-echo origins, use the normal browser, game, streaming or application client that speaks that origin’s real protocol.
curl --resolve is useful for a short carrier sanity check without editing
/etc/hosts:
curl -vk --resolve carrier.example.net:443:127.0.0.1 \
https://carrier.example.net/
This preserves SNI and Host, but it is only a one-shot HTTPS request. Some
origins return 404 for non-WebSocket paths or close immediately after the
response. That can authenticate a short carrier, but it will not keep TUN/SOCKS
traffic alive.
Prefer an operator-controlled origin for repeatable deployments. FPS documentation intentionally avoids recommending third-party public test services as carrier origins: availability, rate limits, acceptable-use policy and protocol behavior are outside FPS control.