133 lines
3.9 KiB
Go

package main
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"time"
udpproxy "github.com/onyx-and-iris/q3rcon-proxy"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v3"
)
// proxyConfig holds the configuration for a single UDP proxy server.
type proxyConfig struct {
proxyHost string
targetHost string
portsMapping []string
sessionTimeout int
}
func main() {
cmd := &cli.Command{
Name: "q3rcon-proxy",
Usage: "A Quake 3 RCON proxy server",
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.StringFlag{
Name: "ports-mapping",
Usage: "Proxy and target ports (proxy:target)",
Sources: cli.EnvVars("Q3RCON_PORTS_MAPPING"),
Required: true,
Action: func(ctx context.Context, cmd *cli.Command, v string) error {
// Validate the ports mapping
for mapping := range strings.SplitSeq(v, ";") {
ports := strings.Split(mapping, ":")
if len(ports) != 2 {
return fmt.Errorf("invalid ports mapping: %s", mapping)
}
proxyPort, err := strconv.Atoi(ports[0])
if err != nil || proxyPort < 1 || proxyPort > 65535 {
return fmt.Errorf("invalid proxy port: %s", ports[0])
}
targetPort, err := strconv.Atoi(ports[1])
if err != nil || targetPort < 1 || targetPort > 65535 {
return fmt.Errorf("invalid target port: %s", ports[1])
}
if proxyPort == targetPort {
return fmt.Errorf("proxy and target ports cannot be the same: %s", mapping)
}
}
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 strings.SplitSeq(cmd.String("ports-mapping"), ";") {
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
},
}
log.Fatal(cmd.Run(context.Background(), os.Args))
}
// launchProxy initializes 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.Printf("q3rcon-proxy initialized: [proxy] (%s) [target] (%s)", hostAddr, proxyAddr)
errChan <- server.ListenAndServe()
}