rcon

package module
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2026 License: MIT Imports: 14 Imported by: 0

README

rcon

Release License Release Coverage

A Source RCON client for Go, and a command-line tool built on it.

go get github.com/SRS-Hosting/rcon

Why this exists

The readily available Go RCON clients read exactly one packet per command. A response larger than about 4KB is split across several, so anything long, a full player list on a busy server, say, comes back quietly truncated: a short response with no error to say it was cut. This client reassembles multi-packet responses, and when one is genuinely cut short it says so while still handing back what arrived.

Two ways a response gets split

Both are handled for you; Execute returns the whole thing either way.

The RCON protocol splits a body larger than one packet across several, and this client reassembles them. Separately, Path of Titans caps a response at 4000 characters and pages the remainder, marking each page [Page(Key 24) 1/5] and serving the rest only when asked for by Page:<key>-<index>. This client follows that automatically, strips the markers, and joins the pages byte for byte, which matters because the split can fall mid-word.

Two consequences worth planning for. A paged response costs one round trip per page against the game thread, so keep the timeout generous enough to cover them; the 10s default handles five pages comfortably. And the server holds pages only for PageTimeout, five seconds by default, so the pages are fetched back to back on the connection that is already open rather than dialling per page.

A page that expires or stops matching before it is fetched comes back as ErrTruncated with the pages that did arrive, never silently as a shorter response. The marker format is the game's to change, though, and a changed marker is indistinguishable from an unpaged response, so a consumer that can cross-check the result against a count the server itself reports should keep doing so.

Library

client := rcon.New("127.0.0.1:27015", password,
    rcon.WithTimeout(10*time.Second),
    rcon.WithMaxConcurrent(4),
)

output, err := client.Execute(ctx, "status")

A Client is safe for concurrent use. It opens a fresh connection per command: RCON connections do not survive a server restart and there is no state worth keeping warm, so reconnecting each time removes a whole class of stale-socket failures for the cost of one handshake.

One deadline covers a whole exchange, connect through response, rather than being re-armed at each step, where a server answering every step just inside the deadline could outlast the budget in aggregate.

Callers past WithMaxConcurrent get ErrBusy instead of queueing. Waiting would spend a caller's deadline in the queue and then hand it whatever was left. The limit is small by default because Source servers handle RCON on their main thread and cap or ban clients that pile on connections.

Errors

Each of these means something different to whoever has to fix it, so they are kept apart rather than flattened into one failure:

Error Meaning
ErrBusy Every concurrent slot was taken. Backpressure; worth retrying.
ErrAuthFailed The server rejected the password, by verdict or by hanging up during auth.
ErrNotRCON Something answered, but not with RCON. Almost always a wrong port.
ErrCommandTooLong Over MaxCommandLen. Returned before dialling.
ErrTruncated The response was cut short, in transit or mid-pagination. Returned with the partial body.
ErrProtocol The response was not valid RCON framing.
*TimeoutError The exchange exceeded its deadline. Match with errors.As.

ErrTruncated is the unusual one: it comes back with whatever did arrive, because a partial response is often still useful and the caller is better placed to judge that. It is the only case where a non-nil error carries a body.

output, err := client.Execute(ctx, "PlayerInfoAll")
if errors.Is(err, rcon.ErrTruncated) {
    // output holds what arrived; cross-check it before trusting it
}
Testing against a fake server

rcontest provides a scriptable RCON server so callers can test without a game server. Its framing is written independently of the client's, so a passing test means two separate readings of the wire format agree.

srv, err := rcontest.New(rcontest.Respond(password, 0, func(command string) string {
    return "Total Players: 0"
}))
defer srv.Close()

client := rcon.New(srv.Addr(), password)

Respond handles authentication and answers each command, splitting responses into chunks of a given size so multi-packet reassembly is exercised (0 sends each response whole). For anything else, pass a raw handler and drive the Framer directly, including malformed frames.

srv.Connections() reports how many connections were accepted, which is what a caching layer's test asserts on.

Command

rcon -a 127.0.0.1:27015 -p secret status
rcon -a game:7779 "Restart 600"
RCON_PASSWORD=secret rcon -a game:7779 status
echo status | rcon -a game:7779

With a command it runs and exits, which is the form to use from scripts and container lifecycle hooks. With no command it reads one per line from standard input until end of input, prompting when that is a terminal, so it works interactively and in a pipeline without a flag to switch between them. Ctrl-C ends an interactive session as cleanly as typing exit; a second Ctrl-C force-kills a wedged one.

Prefer RCON_PASSWORD over --password: an argument is visible to anyone who can read the process list.

Configuration

Settings come from a config file, then the environment, then flags, each overriding the last. Environment variables carry an RCON_ prefix, matching the names the sibling services already use, so one environment configures all of them.

Flag Environment Default Notes
-a, --address RCON_ADDRESS host:port; overrides host and port
-H, --host RCON_HOST 127.0.0.1
-P, --port RCON_PORT 27015
-p, --password RCON_PASSWORD
--timeoutSeconds RCON_TIMEOUTSECONDS 10 covers the whole exchange
-c, --config config.yaml
Exit codes

These are a contract. A container lifecycle hook branches on success to decide whether to wait out a graceful restart or shut down now, so a failure reported as success would hold a terminating pod open for the whole drain.

Code Meaning
0 The command ran and the server answered in full.
1 The command did not complete: no connection, rejected password, server error.
2 Bad invocation. Nothing was sent.
3 The server answered but the response was cut short, so whether the command finished is unknown.

Anything that only checks for zero treats 3 as the failure it might be.

Documentation

Overview

Package rcon is a Source RCON client.

It exists because the readily available Go clients read exactly one packet per command, and a response larger than about 4KB is split across several. That truncation is silent: a long response comes back looking like a short one, with no error to say otherwise. This client reassembles multi-packet responses and, when one is cut short anyway, says so explicitly while still handing back what did arrive.

The design assumes the far end is a game server running RCON on its game thread, so commands are bounded: one deadline covers a whole exchange, and callers past a configured concurrency limit fail fast rather than queue.

client := rcon.New("127.0.0.1:27015", password)
output, err := client.Execute(ctx, "status")

Index

Constants

View Source
const (
	// DefaultTimeout bounds a whole exchange: connect, authenticate, send, receive.
	DefaultTimeout = 10 * time.Second

	// DefaultMaxConcurrent is deliberately small. Source servers handle RCON on
	// their main thread and cap or ban clients that pile on connections, so this
	// is headroom for a few callers at once, not a throughput setting.
	DefaultMaxConcurrent = 4
)

Defaults applied by New when the corresponding option is not given.

View Source
const MaxCommandLen = 1000

MaxCommandLen is the longest command a Source server will accept. Anything longer is rejected before dialling, so an over-long command reports itself as such instead of as whatever the connection happened to do.

Variables

View Source
var ErrAuthFailed = errors.New("rcon: authentication failed, check the password")

ErrAuthFailed reports that the server rejected the password. Retrying cannot fix it, so callers should surface it rather than treat it as a transient.

View Source
var ErrBusy = errors.New("rcon: too many commands already in flight")

ErrBusy reports that every concurrent-command slot was taken. It is backpressure rather than a failure of the RCON server, and worth distinguishing: an ErrBusy is worth retrying in a moment, where most other errors mean something is actually wrong.

View Source
var ErrCommandTooLong = fmt.Errorf("rcon: command must be at most %d bytes", MaxCommandLen)

ErrCommandTooLong reports a command over MaxCommandLen bytes. It is returned before any connection is opened.

View Source
var ErrNotRCON = errors.New("rcon: the address is not an RCON server")

ErrNotRCON reports that something is listening and answering, but not with RCON. It is almost always a wrong port, and it is kept separate from ErrAuthFailed because the two send an operator to entirely different places.

View Source
var ErrProtocol = errors.New("rcon: malformed response")

ErrProtocol reports a response that is not valid Source RCON. It means the endpoint is not what we think it is, or the exchange has desynchronised; either way retrying the same way will not help.

View Source
var ErrTruncated = errors.New("rcon: response truncated before the end-of-response marker")

ErrTruncated reports that a response was cut short: the sentinel packet never arrived before the deadline, the response outgrew maxResponseBytes, or the game's own pagination could not be followed to the end because a page expired, stopped matching, or there were too many of them.

It is returned alongside the bytes that did arrive, because a partial response is still worth having. What must not happen is a truncated response being read as a complete one, so callers that can cross-check the result, against a count the server itself reported, for instance, should do so.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client executes commands against a Source RCON server.

A fresh connection is opened per command. RCON connections do not survive a server restart and there is no state worth keeping warm, so reconnecting each time removes a whole class of stale-socket failures for the cost of one TCP handshake and auth round trip.

A Client is safe for concurrent use.

func New

func New(addr, password string, opts ...Option) *Client

New returns a Client for the RCON server at addr, which must be in host:port form.

func (*Client) Addr

func (c *Client) Addr() string

Addr returns the address of the RCON server.

func (*Client) Execute

func (c *Client) Execute(ctx context.Context, command string) (string, error)

Execute runs command on the RCON server and returns its response.

The entire exchange, connect, authenticate, send, receive, shares a single deadline, the smaller of ctx and the configured timeout. The socket deadline derived from that is the primary bound; the select below is what stops the caller waiting past it. The connection watches ctx itself from the moment it is dialled (see dialAndAuth) and closes on ctx.Done, so a goroutine abandoned here, whether to the deadline or to an early cancellation, has its blocked read fail immediately and releases its slot and socket promptly, rather than holding both against the game server for up to another full timeout after the caller already gave up.

A response cut short still returns the part that arrived, paired with ErrTruncated. Every other error returns an empty body.

func (*Client) Timeout

func (c *Client) Timeout() time.Duration

Timeout returns the per-command deadline.

type Option

type Option func(*Client)

Option configures a Client.

func WithMaxConcurrent

func WithMaxConcurrent(maxConcurrent int) Option

WithMaxConcurrent sets how many exchanges may be in flight at once. Callers past the limit get ErrBusy rather than being queued: waiting would spend the caller's deadline on the queue and then hand it a truncated budget for the exchange itself.

Values below 1 are raised to 1, because a zero-capacity limit would report ErrBusy for every command rather than meaning "unlimited".

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout sets the deadline covering one complete exchange.

Values of zero or less are ignored, leaving DefaultTimeout in place: a client with no deadline at all hangs forever against a server that accepts a connection and then says nothing, which is a common way for a game server to fail.

type TimeoutError

type TimeoutError struct {
	Addr    string
	Timeout time.Duration
}

TimeoutError reports that an exchange did not finish within the deadline. Callers use errors.As to tell a slow or dead server apart from a server that answered with a failure:

var timeout *rcon.TimeoutError
if errors.As(err, &timeout) { ... }

It deliberately does not implement net.Error's Timeout() bool: that method name would collide with the Timeout field, and the field is the more useful of the two since it tells a caller what budget was actually exceeded.

func (*TimeoutError) Error

func (e *TimeoutError) Error() string

Directories

Path Synopsis
cmd
rcon command
Command rcon runs commands on a Source RCON server.
Command rcon runs commands on a Source RCON server.
internal
cli
Package cli implements the rcon command-line tool.
Package cli implements the rcon command-line tool.
config
Package config is the rcon command's configuration.
Package config is the rcon command's configuration.
Package rcontest provides a scriptable Source RCON server for tests.
Package rcontest provides a scriptable Source RCON server for tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL