rconclient

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package rconclient is a high-level RCON client built on github.com/cbrgm/rcon.

It mirrors the shape of net/http: a DefaultClient, package-level helper functions that delegate to it, and an instantiable Client that is safe for concurrent use. For repeated commands to one server, open a Session.

Most servers work with the default multi-packet response mode. For servers that mishandle the multi-packet terminator sentinel, use WithSinglePacket; for servers that also split large replies across packets (such as Project Zomboid), use WithReadUntilIdle.

Example

For a one-off command, the package-level Execute dials, authenticates, runs the command, and closes the connection, all against DefaultClient.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/cbrgm/rcon/rconclient"
)

func main() {
	out, err := rconclient.Execute(context.Background(), "127.0.0.1:25575", "password", "list")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Example (ErrorHandling)

Classify failures with the re-exported sentinels, so you can tell a rejected password from a bad command or a transport problem without importing the core package. errors.Is matches through wrapping.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/cbrgm/rcon/rconclient"
)

func main() {
	_, err := rconclient.Execute(context.Background(), "127.0.0.1:25575", "wrong-password", "list")
	switch {
	case err == nil:
		fmt.Println("ok")
	case errors.Is(err, rconclient.ErrAuthFailed):
		fmt.Println("wrong password")
	case errors.Is(err, rconclient.ErrCommandTooLong):
		fmt.Println("command too long")
	default:
		fmt.Println("connection problem:", err)
	}
}
Example (RoundTrip)

A complete round trip against an in-process server: dial through the default client, run a command, and print the reply.

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net"

	"github.com/cbrgm/rcon/rconclient"
	"github.com/cbrgm/rcon/rconserver"
)

func main() {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	srv := &rconserver.Server{
		Password: "secret",
		Handler: rconserver.HandlerFunc(func(w rconserver.ResponseWriter, r *rconserver.Request) {
			_, _ = io.WriteString(w, "3/20 players online")
		}),
	}
	go func() { _ = srv.Serve(ln) }()
	defer srv.Close()

	out, err := rconclient.Execute(context.Background(), ln.Addr().String(), "secret", "list")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Output:
3/20 players online

Index

Examples

Constants

View Source
const (
	// DefaultTimeout bounds a single command round-trip.
	DefaultTimeout = 10 * time.Second
	// DefaultDialTimeout bounds establishing the connection.
	DefaultDialTimeout = 5 * time.Second
)

Default option values.

Variables

View Source
var (
	// ErrAuthFailed indicates the server rejected the password.
	ErrAuthFailed = rcon.ErrAuthFailed
	// ErrCommandEmpty indicates an empty command was supplied.
	ErrCommandEmpty = rcon.ErrCommandEmpty
	// ErrCommandTooLong indicates the command exceeded the maximum length.
	ErrCommandTooLong = rcon.ErrCommandTooLong
)

These sentinels are re-exported from github.com/cbrgm/rcon/rcon so callers can classify errors without importing the core package. errors.Is matches either name.

View Source
var DefaultClient = New()

DefaultClient is used by the package-level Execute helper.

Functions

func Execute

func Execute(ctx context.Context, address, password, command string) (string, error)

Execute runs a single command using DefaultClient.

Types

type BackoffFunc

type BackoffFunc func(attempt int) time.Duration

BackoffFunc computes the delay before retry attempt n (1-based).

func ExponentialBackoff

func ExponentialBackoff(base, max time.Duration) BackoffFunc

ExponentialBackoff returns a BackoffFunc that doubles from base up to max, with full jitter.

type Client

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

Client is a reusable, concurrency-safe high-level RCON client. Construct it with New; the zero value is not usable. A Client is safe for use by multiple goroutines, mirroring *http.Client.

Example

Construct a Client to configure timeouts, retries, or logging once, then reuse it across goroutines. It mirrors the shape of http.Client.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/cbrgm/rcon/rconclient"
)

func main() {
	client := rconclient.New(
		rconclient.WithTimeout(10*time.Second),
		rconclient.WithRetry(3, rconclient.ExponentialBackoff(100*time.Millisecond, 2*time.Second)),
	)

	out, err := client.Execute(context.Background(), "127.0.0.1:25575", "password", "list")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}
Example (CustomDialer)

WithDialer runs the client over any transport you supply, for example a dialer with custom timeouts, a SOCKS proxy, or a TLS tunnel, instead of plain TCP.

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"time"

	"github.com/cbrgm/rcon/rconclient"
)

func main() {
	dialer := &net.Dialer{Timeout: 2 * time.Second, KeepAlive: 30 * time.Second}

	client := rconclient.New(
		rconclient.WithDialer(func(ctx context.Context, address string) (net.Conn, error) {
			return dialer.DialContext(ctx, "tcp", address)
		}),
	)

	out, err := client.Execute(context.Background(), "127.0.0.1:25575", "password", "list")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}

func New

func New(opts ...Option) *Client

New returns a Client configured with opts.

func (*Client) Dial

func (c *Client) Dial(ctx context.Context, address, password string) (*Session, error)

Dial opens a Session to address, authenticated with password.

Example

For many commands against the same server, open a Session instead of dialing per command. It keeps one authenticated connection and reconnects on drop.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/cbrgm/rcon/rconclient"
)

func main() {
	ctx := context.Background()
	client := rconclient.New()

	session, err := client.Dial(ctx, "127.0.0.1:25575", "password")
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	for _, cmd := range []string{"list", "seed", "save-all"} {
		out, err := session.Execute(ctx, cmd)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(out)
	}
}

func (*Client) Execute

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

Execute dials address, authenticates with password, runs command, and closes the connection. It is the one-shot path; for many commands use Dial + Session. The whole call, including any retries, is bounded by the Client's timeout (see WithTimeout). Connection-level failures (dial error, io.EOF, net.Error) are retried per WithRetry; rcon.ErrAuthFailed, ErrCommandEmpty, and ErrCommandTooLong are never retried.

type DialFunc

type DialFunc func(ctx context.Context, address string) (net.Conn, error)

DialFunc establishes a raw connection to address. It lets the client run over TLS, an SSH tunnel, a proxy, or any custom transport instead of plain TCP.

type Option

type Option func(*Client)

Option configures a Client.

func WithDialTimeout

func WithDialTimeout(d time.Duration) Option

WithDialTimeout sets the connection dial timeout.

func WithDialer

func WithDialer(d DialFunc) Option

WithDialer makes the Client establish connections with d and wrap them via rcon.Open, instead of dialing plain TCP itself. When a dialer is set, WithDialTimeout does not apply, since d owns connection setup. The Client's WithTimeout still bounds the whole call.

d must return a non-nil connection when it returns a nil error. A dial error is retried by WithRetry only when it satisfies net.Error, so wrap transport errors accordingly if you want them retried.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the structured logger. The default logger discards output.

func WithReadUntilIdle added in v0.2.0

func WithReadUntilIdle(window time.Duration) Option

WithReadUntilIdle reads response packets until the connection is quiet for window, concatenating their bodies, instead of using the terminator sentinel. It handles servers that mishandle that sentinel but still split large responses across packets, such as Project Zomboid. A window of 0 or less uses rcon.DefaultIdleWindow. See rcon.WithReadUntilIdle for the tradeoffs.

Example

WithReadUntilIdle reads reply packets until the connection goes quiet, for servers like Project Zomboid that split large replies but mishandle the terminator. A window of 0 uses the default.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/cbrgm/rcon/rconclient"
)

func main() {
	client := rconclient.New(rconclient.WithReadUntilIdle(0))

	out, err := client.Execute(context.Background(), "127.0.0.1:27015", "changeme", "help")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}

func WithRetry

func WithRetry(attempts int, backoff BackoffFunc) Option

WithRetry sets how many attempts a one-shot Execute makes on connection-level failures and the backoff between them. Attempts of 0 or 1 disables retry.

func WithSinglePacket added in v0.2.0

func WithSinglePacket() Option

WithSinglePacket makes the client read exactly one response packet per command instead of using the multi-packet terminator sentinel. Use it for servers that mishandle that sentinel and never split a response across packets. Multi-packet mode (the default) is correct for Source-engine servers, whose large responses span several packets and would otherwise be truncated. For servers that mishandle the sentinel yet still split large responses (e.g. Project Zomboid), prefer WithReadUntilIdle, which takes precedence if both are set.

Example

WithSinglePacket reads one reply packet per command, for servers that mishandle the multi-packet terminator and never split a response.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/cbrgm/rcon/rconclient"
)

func main() {
	client := rconclient.New(rconclient.WithSinglePacket())

	out, err := client.Execute(context.Background(), "127.0.0.1:27015", "password", "players")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(out)
}

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the overall per-command deadline.

type Session

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

Session is a live authenticated connection to one server, for issuing many commands. Unlike Client.Execute, it keeps the connection open. A Session is not safe for concurrent use; use one per goroutine.

func (*Session) Close

func (s *Session) Close() error

Close closes the underlying connection.

func (*Session) Execute

func (s *Session) Execute(ctx context.Context, command string) (string, error)

Execute runs command on the session's connection. On a retryable connection-level error (dial error, io.EOF, net.Error), it reconnects once and retries the command; auth and command-validation errors are returned immediately. The whole call, including the reconnect, is bounded by the Client's timeout (see WithTimeout).

Example

A Session keeps one authenticated connection open and reconnects on drop, so repeated commands avoid re-dialing and re-authenticating each time.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/cbrgm/rcon/rconclient"
)

func main() {
	ctx := context.Background()

	session, err := rconclient.New().Dial(ctx, "127.0.0.1:25575", "password")
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	for _, cmd := range []string{"list", "seed"} {
		out, err := session.Execute(ctx, cmd)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(out)
	}
}

Jump to

Keyboard shortcuts

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