-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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) *PortalNew 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.
| 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 |
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.")
}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 withinprimaryGrouponly, falling back throughfallbackGroupsin order if the primary group has no available servers. This is whatrouting.default_group/routing.fallback_groupsin 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— one method,Authorize(conn *minecraft.Conn) (bool, string).session.NewSimpleWhitelist(enabled bool, players []string)is the built-in implementation backingwhitelist.*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)backssecurity.*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 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.
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.
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.
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.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.
Getting started
Integrating a backend
Embedding Portal