Skip to content

Repository files navigation

Fugitive3dServerRepository

The server back end for Fugitive 3D's server browser.

Game servers register themselves here; game clients read the list to populate the in-game server browser. Registrations are held in memory only and expire: a server that stops sending heartbeats is pruned once it has not been seen for the stale threshold (30 seconds by default, set with -s).

Requirements

Go 1.26 or newer.

Build releases with a current Go patch release rather than the minimum. The go directive is a floor, and the oldest patch of a given minor version carries standard library advisories that later patches fix.

Running

go run . -a 0.0.0.0 -p 8080 -s 30 -logtostderr
Flag Default Meaning
-a 0.0.0.0 IP address to listen on
-p 8080 TCP port to listen on
-s 30 Seconds before a server is considered stale and pruned

Logging is glog, so -logtostderr and -log_dir=<path> are also accepted. Successful requests are not logged at the default verbosity, since every registered server heartbeats continuously; pass -v=2 to see them. Errors and rejections are always logged.

The server runs in gin's release mode unless GIN_MODE is set in the environment; GIN_MODE=debug restores gin's startup route dump and debug warnings.

Endpoints

A server is identified by its own address, so :server_id is a literal ip:port string, for example /servers/203.0.113.4:45677. The IP must be IPv4 and the port must be in the range 1024-65535.

GET /reflection/ip

Returns the caller's public IP, so a game server can learn the address it should register under.

200 { "ip": "203.0.113.4" }

GET /servers

Returns the full list of currently registered servers.

200 [
  {
    "ip": "203.0.113.4",
    "port": 45677,
    "name": "special server",
    "game_version": 1,
    "current_players": 2,
    "max_players": 8,
    "is_joinable": true,
    "last_seen": "2021-01-19T12:34:56Z"
  }
]

POST /servers/{ip:port}

First-time registration. Before accepting the server, the repository dials UDP back to the address being registered and waits up to 5 seconds for the game server to answer a ping with pong. This confirms the port is actually reachable from the outside.

The request body carries the server metadata:

{
  "name": "special server",
  "game_version": 1,
  "current_players": 0,
  "max_players": 8,
  "is_joinable": true
}
Status Body Cause
200 {"result": "registration complete"} Registered
400 {"result": "invalid request JSON"} Body did not parse
400 {"result": "<validation error>"} Name length or address out of range
403 {"result": "request IP address does not match client IP address"} Registering an address you are not calling from
406 {"result": "invalid server ID"} :server_id is not a valid ip:port
406 {"result": "Bad ping response"} Reply was not pong
412 {"result": "Repository could not ping you."} Could not open a UDP socket to the address
504 {"result": "no ping response received, is your port not properly forwarded?"} No reply within 5 seconds
500 {"result": "internal server error"} Registration failed

PUT /servers/{ip:port}

Heartbeat. Refreshes the last-seen timestamp and metadata, and must be called more often than the stale threshold to stay listed. There is no ping-back on this path.

This is an upsert, not an update: the repository keeps its registrations in memory, so a restart drops every server while game servers are still heartbeating. A PUT for a server the repository does not know about therefore recreates it rather than failing.

Status Body Cause
202 {"result": "updated"} Existing registration refreshed
201 {"result": "registered"} Server was not known, so it was recreated
400 {"result": "invalid request JSON"} Body did not parse
400 {"result": "invalid server ID"} :server_id is not a valid ip:port
400 {"result": "<validation error>"} Name length or address out of range
403 {"result": "request IP address does not match client IP address"} Updating an address you are not calling from
500 {"result": "internal server error"} Registration failed

DELETE /servers/{ip:port}

Deregisters a server.

Status Body Cause
200 {"result": "success"} Removed
403 {"result": "request IP address does not match client IP address"} Removing an address you are not calling from
404 {"result": "invalid server ID"} :server_id is not a valid ip:port
404 {"result": "failure"} No such server

Validation

  • ip must be IPv4, port must be within 1024-65535.
  • name is trimmed of surrounding whitespace and must then be 3-32 characters.
  • Every mutating call must originate from the IP it is registering, updating, or removing; this is the only authentication in the system.

Running behind a reverse proxy

Because a caller's source IP is the only authentication, the address the service sees has to be the game server's own. A forwarding header is honoured only when the connection comes from 127.0.0.1 or ::1, on the assumption that a reverse proxy runs on the same host; the same header from anywhere else is ignored, since believing it would let anyone claim another server's address and hijack or deregister its listing.

A proxy must therefore set X-Forwarded-For, and must not be reachable in a way that lets a client choose that header itself:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

To verify, call /reflection/ip from outside the host and confirm it returns the caller's public address rather than 127.0.0.1. If it returns 127.0.0.1, every registration will be rejected with a 403 while /servers keeps answering, so the service looks healthy from the outside while the browser stays empty.

If the proxy runs on a different host, trustedProxies in internal/httpapi/router.go has to be changed to match; widening it to all addresses reintroduces the hijack.

About

The server back end for Fugitive 3D's server browser

Resources

Stars

2 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages