mirror of
https://github.com/onyx-and-iris/q3rcon-proxy.git
synced 2026-03-03 07:39:11 +00:00
176 lines
4.8 KiB
Go
176 lines
4.8 KiB
Go
// Package main implements a command-line application for a Quake 3 RCON proxy server.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/urfave/cli/v3"
|
|
|
|
udpproxy "github.com/onyx-and-iris/q3rcon-proxy"
|
|
)
|
|
|
|
var version string // Version holds the application version, set at build time using ldflags.
|
|
|
|
// versionFromBuild retrieves the version information from the build metadata.
|
|
func versionFromBuild() string {
|
|
if version != "" {
|
|
return version
|
|
}
|
|
|
|
info, ok := debug.ReadBuildInfo()
|
|
if !ok {
|
|
return "(unable to read version)"
|
|
}
|
|
return strings.Split(info.Main.Version, "-")[0]
|
|
}
|
|
|
|
// proxyConfig holds the configuration for a single UDP proxy server.
|
|
type proxyConfig struct {
|
|
proxyHost string
|
|
targetHost string
|
|
portsMapping []string
|
|
sessionTimeout int
|
|
}
|
|
|
|
func main() {
|
|
cli.VersionPrinter = func(cmd *cli.Command) {
|
|
fmt.Printf("q3rcon-proxy version: %s\n", cmd.Root().Version)
|
|
}
|
|
|
|
cmd := &cli.Command{
|
|
Name: "q3rcon-proxy",
|
|
Usage: "A Quake 3 RCON proxy server",
|
|
Version: versionFromBuild(),
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "proxy-host",
|
|
Value: "0.0.0.0",
|
|
Usage: "Proxy host address",
|
|
Sources: cli.EnvVars("Q3RCON_PROXY_HOST"),
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "target-host",
|
|
Value: "localhost",
|
|
Usage: "Target host address",
|
|
Sources: cli.EnvVars("Q3RCON_TARGET_HOST"),
|
|
},
|
|
&cli.StringSliceFlag{
|
|
Name: "port-map",
|
|
Usage: "Ports mapping in the format proxyPort:targetPort (e.g., 27950:27960)",
|
|
Sources: cli.EnvVars("Q3RCON_PORT_MAP"),
|
|
Required: true,
|
|
Action: func(_ context.Context, cmd *cli.Command, v []string) error {
|
|
// Validate the ports mapping format and values
|
|
for _, mapping := range v {
|
|
src, dst := func(m string) (string, string) {
|
|
parts := strings.Split(m, ":")
|
|
if len(parts) != 2 {
|
|
return "", ""
|
|
}
|
|
return parts[0], parts[1]
|
|
}(mapping)
|
|
|
|
if src == "" || dst == "" {
|
|
return fmt.Errorf(
|
|
"invalid ports mapping: %s (expected format: proxyPort:targetPort)",
|
|
mapping,
|
|
)
|
|
}
|
|
n, err := strconv.Atoi(src)
|
|
if err != nil || n <= 0 || n > 65535 {
|
|
return fmt.Errorf("invalid proxy port: %s", src)
|
|
}
|
|
n, err = strconv.Atoi(dst)
|
|
if err != nil || n <= 0 || n > 65535 {
|
|
return fmt.Errorf("invalid target port: %s", dst)
|
|
}
|
|
if src == dst {
|
|
return fmt.Errorf(
|
|
"proxy port and target port cannot be the same: %s",
|
|
src,
|
|
)
|
|
}
|
|
log.Debugf(
|
|
"Validated ports mapping: proxy port %s -> target port %s",
|
|
src,
|
|
dst,
|
|
)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
&cli.IntFlag{
|
|
Name: "session-timeout",
|
|
Value: 20,
|
|
Usage: "Session timeout in minutes",
|
|
Sources: cli.EnvVars("Q3RCON_SESSION_TIMEOUT"),
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "loglevel",
|
|
Value: "info",
|
|
Usage: "Log level (trace, debug, info, warn, error, fatal, panic)",
|
|
Sources: cli.EnvVars("Q3RCON_LOGLEVEL"),
|
|
},
|
|
},
|
|
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
|
logLevel, err := log.ParseLevel(cmd.String("loglevel"))
|
|
if err != nil {
|
|
return ctx, fmt.Errorf("invalid log level: %w", err)
|
|
}
|
|
log.SetLevel(logLevel)
|
|
return ctx, nil
|
|
},
|
|
Action: func(_ context.Context, cmd *cli.Command) error {
|
|
errChan := make(chan error)
|
|
|
|
for _, mapping := range cmd.StringSlice("port-map") {
|
|
cfg := proxyConfig{
|
|
proxyHost: cmd.String("proxy-host"),
|
|
targetHost: cmd.String("target-host"),
|
|
portsMapping: strings.Split(mapping, ":"),
|
|
sessionTimeout: cmd.Int("session-timeout"),
|
|
}
|
|
|
|
go launchProxy(cfg, errChan)
|
|
}
|
|
|
|
// Under normal circumstances, the main goroutine will block here.
|
|
// If we receive an error we will log it and exit
|
|
return <-errChan
|
|
},
|
|
}
|
|
|
|
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// launchProxy initialises the UDP proxy server with the given configuration.
|
|
// It listens on the specified proxy host and port, and forwards traffic to the target host and port.
|
|
// server.ListenAndServe blocks until the server is stopped or an error occurs.
|
|
func launchProxy(cfg proxyConfig, errChan chan<- error) {
|
|
proxyPort, targetPort := cfg.portsMapping[0], cfg.portsMapping[1]
|
|
|
|
hostAddr := fmt.Sprintf("%s:%s", cfg.proxyHost, proxyPort)
|
|
proxyAddr := fmt.Sprintf("%s:%s", cfg.targetHost, targetPort)
|
|
|
|
server, err := udpproxy.New(
|
|
hostAddr, proxyAddr,
|
|
udpproxy.WithSessionTimeout(time.Duration(cfg.sessionTimeout)*time.Minute))
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("failed to create proxy: %w", err)
|
|
return
|
|
}
|
|
|
|
log.Infof("q3rcon-proxy initialised: [proxy] (%s) [target] (%s)", hostAddr, proxyAddr)
|
|
|
|
errChan <- server.ListenAndServe()
|
|
}
|