NTRIP (Networked Transport of RTCM via Internet Protocol) caster written in Go.
- NTRIP v1 and v2 for both clients (rovers) and servers (base stations).
- Client pull:
GET /<mountpoint>(ICY 200 OKfor v1, chunkedgnss/datafor v2). - Server push:
SOURCE <pw> /<mp>(v1) andPOST /<mp>with Basic auth (v2). - Sourcetable on
GET /or for unknown/offline mountpoints.
- Client pull:
- Handover endpoints. A client connects to a virtual endpoint and streams
its NMEA
GGA; the caster subscribes it to the nearest online member base station and re-switches as the client moves. Members are grouped manually in config so each endpoint serves one RTCM flavor (e.g. RTCM 3.0, 3.2 MSM4/5/7). - Username/password authentication for both clients and servers
(passwords stored in plaintext, per the chosen configuration model).
Individual mountpoints/handover endpoints can opt into anonymous client
access with
open: true(server push auth is unaffected). - Hot reload via
systemctl reload(SIGHUP): client users, mountpoint definitions, and base-station metadata/handover groups apply without dropping live connections. Changing the listen address requires a restart.
go build -o ntrip-caster ./cmd/ntrip-castercp config.example.yaml config.yaml # edit credentials and mountpoints
./ntrip-caster -config config.yaml
./ntrip-caster -config config.yaml -check # validate config and exit| Package | Responsibility |
|---|---|
internal/config |
YAML load, validation, hot-reload snapshot. |
internal/caster |
Runtime hub: mountpoints, source fan-out, subscribers. |
internal/server |
NTRIP v1/v2 wire protocol, auth, dispatch, handover. |
internal/handover |
Nearest-base selection (haversine) from a GGA fix. |
internal/nmea |
NMEA GGA parsing. |
internal/sourcetable |
Sourcetable (CAS/STR) rendering. |
Data flow: an NTRIP server attaches as the single source of a mountpoint; its bytes are copied and broadcast to every subscribed client. Slow clients whose queue overflows are disconnected rather than served a corrupted stream.
The central concept is the mountpoint: a named relay hub that ties together
one Source (the base station currently pushing) and N Subscribers
(the clients currently reading). A Subscriber is one reader's mailbox — a
buffered chan []byte — and what it is subscribed to is a mountpoint. The
Manager owns every mountpoint and the live config snapshot.
flowchart LR
subgraph MGR["caster.Manager (all mountpoints + config)"]
subgraph MP["Mountpoint "TOKYO""]
SRC["Source<br/>(1 base station, push)"]
SUBS["Subscribers<br/>(N readers, each a chan []byte)"]
SRC -->|"Broadcast(chunk)"| SUBS
end
end
SERVER["NTRIP server (push)"] -->|"SOURCE / POST"| SRC
SUBS -->|"streamToClient"| C1["client #1"]
SUBS -->|"streamToClient"| C2["client #2"]
Role mapping:
| Type | Role in one sentence |
|---|---|
caster.Manager |
Holds all mountpoints and the current config snapshot. |
caster.Mountpoint |
One relay hub = one Source + N Subscribers. |
caster.Source |
The single base station currently pushing into a mountpoint. |
caster.Subscriber |
One reading client's receive channel + done signal. |
server.* |
Speaks the NTRIP wire protocol and wires connections to the above. |
server.handoverSession |
A client whose subscription is moved between mountpoints by position. |
Handover then reads simply: it moves one subscriber's mountpoint from, say,
TOKYO to OSAKA as the client moves — the connection and channel stay the
same.
Every connection is one raw TCP socket. handleConn peeks the first bytes and
routes by request kind; versionOf then picks v1/v2 framing from the
Ntrip-Version header.
flowchart TD
A["handleConn<br/>peek first bytes"] -->|"SOURCE …"| B["handleSourceV1<br/>(v1 push)"]
A -->|"HTTP POST"| C["handleSourceV2<br/>(v2 push, Basic auth)"]
A -->|"HTTP GET"| D["handleClient"]
B --> E["runSource"]
C --> E
E --> F{"AttachSource<br/>already has a source?"}
F -->|yes| G["409 / ERROR — reject"]
F -->|no| H["ack, then read loop:<br/>copy chunk → Broadcast"]
D --> I{"lookup path"}
I -->|"root / unknown / offline"| J["writeSourcetable"]
I -->|"auth fails"| K["401 Unauthorized"]
I -->|"mountpoint"| L["handleMountpoint"]
I -->|"handover group"| M["handleHandover"]
One source feeds many clients. Broadcast non-blocking-sends each chunk to
every subscriber's buffered channel; a subscriber that falls behind is dropped.
flowchart LR
SRV["NTRIP server"] -->|"RTCM bytes"| RS["runSource<br/>read loop"]
RS -->|"copy chunk"| BC["Mountpoint.Broadcast"]
BC --> Q1["(sub ch #1)"]
BC --> Q2["(sub ch #2)"]
Q1 --> W1["streamToClient → rover #1"]
Q2 --> W2["streamToClient → rover #2"]
BC -. "buffer full" .-> X["drop slow client"]
handleHandover builds a handoverSession (in handover_session.go) whose
single control loop owns the client connection. The active member changes as
GGA fixes arrive or the current source drops, so the client connection survives
switches; it is closed only when no member is online.
A per-group switch_margin_km applies hysteresis to avoid flapping at a
boundary: once attached, a client only switches to a base that is more than the
margin closer than its current one. A source disconnect bypasses the margin
(immediate failover to the nearest remaining member).
sequenceDiagram
participant R as Rover
participant H as handleHandover
participant Sel as handover.Selector
participant MP as Mountpoints
R->>H: GET /AUTO_… (v1/v2)
alt no member online
H-->>R: SOURCETABLE (fail fast)
else at least one online
H-->>R: stream OK header
loop control loop
R->>H: $GxGGA (lat, lon)
H->>Sel: Nearest(group, lat, lon)
Sel-->>H: nearest online member ("" if none)
alt nearest == ""
H-->>R: disconnect
else member changed
H->>MP: Subscribe(new) / drop old
end
MP-->>R: RTCM from nearest base
Note over H,MP: active source drops → re-select<br/>(next-nearest) using last fix
end
end
See config.example.yaml. Reloadable keys:
client_users, mountpoints, handover.
See systemd/ntrip-caster.service.
sudo cp ntrip-caster /usr/local/bin/
sudo install -d /etc/ntrip-caster && sudo cp config.yaml /etc/ntrip-caster/
sudo cp systemd/ntrip-caster.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now ntrip-caster
# After editing the config:
sudo systemctl reload ntrip-caster- A handover connection fails immediately (sourcetable response) when no member base station is online. If the active base drops mid-stream, the client is automatically re-routed to the next-nearest online member using its last known position; the connection is closed only when no member remains online.
- Passwords are plaintext by design of the current config model.
- NTRIP v2 over TLS is not yet implemented (terminate TLS with a reverse proxy if required).