Skip to content

Library Usage

MEMOxiiii edited this page Sep 17, 2026 · 2 revisions

Library Usage

Portal's own binary (examples/main.go) is a regular consumer of the portal Go package — there's no separate "server mode." Embedding it yourself gives you the same proxy with direct access to its internals (sessions, servers, load balancer) instead of going through config.json and the Admin Console.

This page documents the actual package API. If you only need to run Portal, see Installation and Configuration instead — this page is for embedding it in your own Go program or forking its wiring.

Minimal working example

portal.New does not start listening by itself, and it does not return an error — you still need to call Listen and then loop on Accept:

package main

import (
	"github.com/paroxity/portal"
	"github.com/paroxity/portal/session"
	"github.com/sirupsen/logrus"
)

func main() {
	log := logrus.New()

	p := portal.New(portal.Options{
		Logger:  log,
		Address: ":19132",
	})

	if err := p.Listen(); err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	defer p.Close()

	for {
		s, err := p.Accept()
		if err != nil {
			log.Errorf("failed to accept connection: %v", err)
			continue
		}
		_ = s // session is already connected and load balanced onto a backend server
	}
}

This alone gets you a player listener. It's not useful yet, though — no server is registered for the load balancer to send players to. For that you need the socket API server described below, or you can register servers directly via p.ServerRegistry().AddServer(...) if you're managing that out of band.

For the full picture — resource packs, the socket server, health checking, clustering, metrics, the admin console, and graceful shutdown all wired together — read examples/main.go in the repository; it's the reference wiring the bundled binary uses; every piece below is a smaller excerpt of it.

portal.Options and portal.New

type Options struct {
	Logger       internal.Logger        // defaults to logrus.New() if nil
	Address      string                 // address players connect to; meaning depends on Transport
	Transport    portal.Transport       // TransportNetherNet (default) or TransportRakNet
	NetherNet    portal.NetherNetOptions // TLS, ICE servers, UDP port(s) — only used when Transport is TransportNetherNet
	ListenConfig minecraft.ListenConfig // MOTD, resource packs, flush rate, etc.
	LoadBalancer session.LoadBalancer   // defaults to NewSplitLoadBalancer(registry)
	Whitelist    session.Whitelist      // defaults to a disabled SimpleWhitelist
	IPGuard      session.IPGuard        // defaults to NopIPGuard (allow everything)
}

func New(opts Options) *Portal

New fills in defaults for any zero-valued field and returns a ready-to-Listen *Portal. Unlike some constructors elsewhere in the codebase, it never returns an error. Leaving Transport empty defaults to TransportNetherNet — see NetherNet Transport for what NetherNetOptions controls and what it takes to actually run it outside of localhost.

*Portal methods

Method Purpose
Listen() error Starts the player listener on Options.Address, using Options.Transport
Accept() (*session.Session, error) Blocks for the next fully-connected player, already passed through IPGuard/Whitelist and load balanced onto a backend. Call this in a loop.
Disconnect(conn *minecraft.Conn, message string) error Disconnects a raw minecraft.Conn; empty message sends straight to the player list instead of a disconnect screen
Close() error Closes the listener. Does not disconnect already-connected sessions — do that yourself first (see the shutdown pattern below)
Events() *event.Bus The proxy-wide event bus — see Event Bus
Logger() internal.Logger The logger passed in (or defaulted) via Options
SessionStore() *session.Store All currently connected sessions
ServerRegistry() *server.Registry All registered backend servers
LoadBalancer() session.LoadBalancer / SetLoadBalancer(lb session.LoadBalancer) Read or swap the load balancer used for new joins
HandleAdminCommand(line string) string / ServeAdminConsole(r io.Reader, w io.Writer) See Admin Console

Graceful shutdown

Close() only stops new connections — draining existing players is on you:

if err := p.Close(); err != nil {
	log.Errorf("failed to close proxy listener: %v", err)
}
for _, s := range p.SessionStore().All() {
	s.Disconnect("Proxy is shutting down.")
}

Load balancers (session package)

LoadBalancer is a one-method interface: FindServer(*Session) *server.Server, called once per newly joined player. Returning nil kicks the player.

  • session.NewSplitLoadBalancer(registry *server.Registry) — balances across every registered server, in proportion to weight. This is the default.
  • session.NewGroupedLoadBalancer(registry *server.Registry, primaryGroup string, fallbackGroups ...string) — balances within primaryGroup only, falling back through fallbackGroups in order if the primary group has no available servers. This is what routing.default_group/routing.fallback_groups in Configuration configure when using the bundled binary.

Both skip servers that are draining or unhealthy (see Socket Protocol), and both weight their picks by server.Server.Weight().

Write your own LoadBalancer implementation and pass it via Options.LoadBalancer (or SetLoadBalancer later) for custom routing logic — e.g. geographic routing, A/B testing, or permission-based server access.

Whitelist and IPGuard (session package)

  • Whitelist — one method, Authorize(conn *minecraft.Conn) (bool, string). session.NewSimpleWhitelist(enabled bool, players []string) is the built-in implementation backing whitelist.* in Configuration.
  • IPGuard — one method, Allow(addr net.Addr) (bool, string), checked before the whitelist or any game-layer authentication. session.NewSimpleIPGuard(bannedIPs []string, rateLimitEnabled bool, window time.Duration, limit int) backs security.* in Configuration. session.NopIPGuard{} (the default) allows everything through.

Both are small interfaces specifically so you can swap in your own — e.g. a whitelist backed by a database, or an IP guard backed by an external abuse-detection service.

The socket API server (socket package)

The player listener alone doesn't expose the Socket Protocol backend servers need — that's a separate component:

socketServer := socket.NewDefaultServer(
	":19131", "my-secret",
	p.SessionStore(), p.ServerRegistry(),
	log, true, // readerLimits
	p.Events(),
)
if err := socketServer.Listen(); err != nil {
	log.Fatalf("socket server failed to listen: %v", err)
}

Use socket.NewDefaultTLSServer(addr, secret, sessionStore, registry, log, readerLimits, tlsConfig, events) instead to serve it over TLS (matches network.communication.tls.* in Configuration).

Useful methods: Clients() []*Client / Client(name string) (*Client, bool) (connected backend servers), ReportPlayerLatency(interval time.Duration) (start the periodic UpdatePlayerLatency broadcast), and SetCluster(cluster.Backend) (wire in clustering, see below). SessionStore().PreTransfer is a hook you can set to a func(serverName, playerName string) — the bundled binary uses it to send DisconnectPlayer to the target server ahead of every transfer.

Resource packs (ResourcePackManager)

mgr, err := portal.NewResourcePackManager("resource_packs", encryptionKeys)
// mgr.ResourcePacks() -> []*resource.Pack, for minecraft.ListenConfig.ResourcePacks
// mgr.FetchResourcePacks -> matches the FetchResourcePacks callback signature directly

go mgr.StartHotReload(context.Background(), 30*time.Second, log)

StartHotReload polls the directory (hashing file paths, sizes, and mtimes) and swaps in a new snapshot only when something actually changed and the reload succeeds — a broken pack never replaces a working snapshot. This is what resource_packs.hot_reload.* in Configuration configures.

Health checking (server.HealthChecker)

checker := server.NewHealthChecker(
	p.ServerRegistry(),
	10*time.Second, // interval
	3*time.Second,  // timeout
	3,              // failureThreshold
	log,
	p.Events(), // may be nil — publishes TopicServerHealthChanged
)
go checker.Start(context.Background())

Pings every registered server on the configured interval — a real RakNet unconnected ping for a TransportRakNet server, or an HTTP GET of /v1/join for a TransportNetherNet one (see Backend Transports) — and after failureThreshold consecutive failures marks it unhealthy (both load balancers stop sending it new players) until a ping succeeds again. Backs health_check.* in Configuration.

Clustering (cluster package)

backend, err := cluster.NewRedisBackend(redisAddr, redisPassword, redisDB, ttl)
socketServer.SetCluster(backend)

cluster.Backend is a small interface (Announce, Remove, Lookup, Close) — RedisBackend is the only built-in implementation, storing each player under a portal:player:<name> key with a TTL as a crash safety net. Subscribe to Event Bus topics (TopicPlayerJoin, TopicPlayerQuit, TopicTransfer) to keep it updated, and re-Announce on a ttl/2 heartbeat to keep long sessions from expiring — see examples/main.go for the exact wiring the bundled binary uses. Full behavior described in Clustering.

Metrics (metrics package)

metrics.Default.RegisterGauge("portal_players_online", func() float64 {
	return float64(len(p.SessionStore().All()))
})

mux := http.NewServeMux()
mux.Handle("/metrics", metrics.Default.Handler())
go http.ListenAndServe(":9131", mux)

metrics.Default is a package-level *Registry with two kinds of output: gauges you register yourself via RegisterGauge(name, func() float64), evaluated fresh on every scrape, plus two counters Portal tracks internally regardless of whether you register anything — portal_transfers_total{result="success"|"failed"} and portal_transfer_duration_seconds_{sum,count}, recorded through Registry.RecordTransfer. Output is plain Prometheus text exposition format, no client library dependency required. Backs metrics.* in Configuration.

Clone this wiki locally