feat: add ability to receive proxy protocol (#307)

This commit is contained in:
iipanda 2024-07-07 18:13:12 +02:00 committed by GitHub
parent f32dfa3800
commit e38a054c46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 159 additions and 25 deletions

View File

@ -54,10 +54,14 @@ Routes Minecraft client connections to backend servers based upon the requested
If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable. (env NGROK_TOKEN)
-port port
The port bound to listen for Minecraft client connections (env PORT) (default 25565)
-receive-proxy-protocol
Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies (env RECEIVE_PROXY_PROTOCOL)
-routes-config string
Name or full path to routes config file (env ROUTES_CONFIG)
-simplify-srv
Simplify fully qualified SRV records for mapping (env SIMPLIFY_SRV)
-trusted-proxies value
Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol (env TRUSTED_PROXIES)
-use-proxy-protocol
Send PROXY protocol to backend servers (env USE_PROXY_PROTOCOL)
-version

View File

@ -45,6 +45,8 @@ type Config struct {
DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker Swarm integration"`
MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb"`
UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"`
ReceiveProxyProtocol bool `default:"false" usage:"Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies"`
TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"`
MetricsBackendConfig MetricsBackendConfig
RoutesConfig string `usage:"Name or full path to routes config file"`
NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."`
@ -117,7 +119,17 @@ func main() {
if config.ConnectionRateLimit < 1 {
config.ConnectionRateLimit = 1
}
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol)
trustedIpNets := make([]*net.IPNet, 0)
for _, ip := range config.TrustedProxies {
_, ipNet, err := net.ParseCIDR(ip)
if err != nil {
logrus.WithError(err).Fatal("Unable to parse trusted proxy CIDR block")
}
trustedIpNets = append(trustedIpNets, ipNet)
}
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets)
if config.NgrokToken != "" {
connector.UseNgrok(config.NgrokToken)
}

View File

@ -3,14 +3,15 @@ package server
import (
"bytes"
"context"
"golang.ngrok.com/ngrok"
"golang.ngrok.com/ngrok/config"
"io"
"net"
"sync"
"sync/atomic"
"time"
"golang.ngrok.com/ngrok"
"golang.ngrok.com/ngrok/config"
"github.com/go-kit/kit/metrics"
"github.com/itzg/mc-router/mcproto"
"github.com/juju/ratelimit"
@ -31,12 +32,13 @@ type ConnectorMetrics struct {
ActiveConnections metrics.Gauge
}
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool) *Connector {
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet) *Connector {
return &Connector{
metrics: metrics,
sendProxyProto: sendProxyProto,
connectionsCond: sync.NewCond(&sync.Mutex{}),
receiveProxyProto: receiveProxyProto,
trustedProxyNets: trustedProxyNets,
}
}
@ -44,6 +46,8 @@ type Connector struct {
state mcproto.State
metrics *ConnectorMetrics
sendProxyProto bool
receiveProxyProto bool
trustedProxyNets []*net.IPNet
activeConnections int32
connectionsCond *sync.Cond
@ -51,9 +55,17 @@ type Connector struct {
}
func (c *Connector) StartAcceptingConnections(ctx context.Context, listenAddress string, connRateLimit int) error {
ln, err := c.createListener(ctx, listenAddress)
if err != nil {
return err
}
var ln net.Listener
var err error
go c.acceptConnections(ctx, ln, connRateLimit)
return nil
}
func (c *Connector) createListener(ctx context.Context, listenAddress string) (net.Listener, error) {
if c.ngrokToken != "" {
ngrokTun, err := ngrok.Listen(ctx,
config.TCPEndpoint(),
@ -61,22 +73,51 @@ func (c *Connector) StartAcceptingConnections(ctx context.Context, listenAddress
)
if err != nil {
logrus.WithError(err).Fatal("Unable to start ngrok tunnel")
return err
return nil, err
}
ln = ngrokTun
logrus.WithField("ngrokUrl", ngrokTun.URL()).Info("Listening for Minecraft client connections via ngrok tunnel")
} else {
ln, err = net.Listen("tcp", listenAddress)
return ngrokTun, nil
}
listener, err := net.Listen("tcp", listenAddress)
if err != nil {
logrus.WithError(err).Fatal("Unable to start listening")
return err
return nil, err
}
logrus.WithField("listenAddress", listenAddress).Info("Listening for Minecraft client connections")
if c.receiveProxyProto {
proxyListener := &proxyproto.Listener{
Listener: listener,
Policy: c.createProxyProtoPolicy(),
}
logrus.Info("Using PROXY protocol listener")
return proxyListener, nil
}
go c.acceptConnections(ctx, ln, connRateLimit)
return listener, nil
}
return nil
func (c *Connector) createProxyProtoPolicy() func(upstream net.Addr) (proxyproto.Policy, error) {
return func(upstream net.Addr) (proxyproto.Policy, error) {
trustedIpNets := c.trustedProxyNets
if len(trustedIpNets) == 0 {
logrus.Debug("No trusted proxy networks configured, using the PROXY header by default")
return proxyproto.USE, nil
}
upstreamIP := upstream.(*net.TCPAddr).IP
for _, ipNet := range trustedIpNets {
if ipNet.Contains(upstreamIP) {
logrus.WithField("upstream", upstream).Debug("IP is in trusted proxies, using the PROXY header")
return proxyproto.USE, nil
}
}
logrus.WithField("upstream", upstream).Debug("IP is not in trusted proxies, discarding PROXY header")
return proxyproto.IGNORE, nil
}
}
func (c *Connector) WaitForConnections() {

77
server/connector_test.go Normal file
View File

@ -0,0 +1,77 @@
package server
import (
"net"
"testing"
"github.com/pires/go-proxyproto"
"github.com/stretchr/testify/assert"
)
func TestTrustedProxyNetworkPolicy(t *testing.T) {
tests := []struct {
name string
trustedNets []string
upstreamIP string
expectedPolicy proxyproto.Policy
}{
{
name: "trusted IP",
trustedNets: []string{"10.0.0.0/8"},
upstreamIP: "10.0.0.1",
expectedPolicy: proxyproto.USE,
},
{
name: "untrusted IP",
trustedNets: []string{"10.0.0.0/8"},
upstreamIP: "192.168.1.1",
expectedPolicy: proxyproto.IGNORE,
},
{
name: "multiple trusted nets",
trustedNets: []string{"10.0.0.0/8", "172.16.0.0/12"},
upstreamIP: "172.16.0.1",
expectedPolicy: proxyproto.USE,
},
{
name: "no trusted nets",
trustedNets: []string{},
upstreamIP: "148.184.129.202",
expectedPolicy: proxyproto.USE,
},
{
name: "remote trusted IP",
trustedNets: []string{"203.0.113.0/24"},
upstreamIP: "203.0.113.10",
expectedPolicy: proxyproto.USE,
},
{
name: "remote untrusted IP",
trustedNets: []string{"203.0.113.0/24"},
upstreamIP: "198.51.100.1",
expectedPolicy: proxyproto.IGNORE,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := &Connector{
trustedProxyNets: parseTrustedProxyNets(test.trustedNets),
}
policy := c.createProxyProtoPolicy()
upstreamAddr := &net.TCPAddr{IP: net.ParseIP(test.upstreamIP)}
policyResult, _ := policy(upstreamAddr)
assert.Equal(t, test.expectedPolicy, policyResult, "Unexpected policy result for %s", test.name)
})
}
}
func parseTrustedProxyNets(nets []string) []*net.IPNet {
parsedNets := make([]*net.IPNet, 0, len(nets))
for _, n := range nets {
_, ipNet, _ := net.ParseCIDR(n)
parsedNets = append(parsedNets, ipNet)
}
return parsedNets
}