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) If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable. (env NGROK_TOKEN)
-port port -port port
The port bound to listen for Minecraft client connections (env PORT) (default 25565) 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 -routes-config string
Name or full path to routes config file (env ROUTES_CONFIG) Name or full path to routes config file (env ROUTES_CONFIG)
-simplify-srv -simplify-srv
Simplify fully qualified SRV records for mapping (env 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 -use-proxy-protocol
Send PROXY protocol to backend servers (env USE_PROXY_PROTOCOL) Send PROXY protocol to backend servers (env USE_PROXY_PROTOCOL)
-version -version
@ -361,4 +365,4 @@ docker run -it --rm \
## Related Projects ## Related Projects
* https://github.com/haveachin/infrared * https://github.com/haveachin/infrared

View File

@ -45,6 +45,8 @@ type Config struct {
DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker Swarm integration"` 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"` 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"` 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 MetricsBackendConfig MetricsBackendConfig
RoutesConfig string `usage:"Name or full path to routes config file"` 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."` 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 { if config.ConnectionRateLimit < 1 {
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 != "" { if config.NgrokToken != "" {
connector.UseNgrok(config.NgrokToken) connector.UseNgrok(config.NgrokToken)
} }

View File

@ -3,14 +3,15 @@ package server
import ( import (
"bytes" "bytes"
"context" "context"
"golang.ngrok.com/ngrok"
"golang.ngrok.com/ngrok/config"
"io" "io"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"golang.ngrok.com/ngrok"
"golang.ngrok.com/ngrok/config"
"github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics"
"github.com/itzg/mc-router/mcproto" "github.com/itzg/mc-router/mcproto"
"github.com/juju/ratelimit" "github.com/juju/ratelimit"
@ -31,19 +32,22 @@ type ConnectorMetrics struct {
ActiveConnections metrics.Gauge ActiveConnections metrics.Gauge
} }
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool) *Connector { func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet) *Connector {
return &Connector{ return &Connector{
metrics: metrics, metrics: metrics,
sendProxyProto: sendProxyProto, sendProxyProto: sendProxyProto,
connectionsCond: sync.NewCond(&sync.Mutex{}), connectionsCond: sync.NewCond(&sync.Mutex{}),
receiveProxyProto: receiveProxyProto,
trustedProxyNets: trustedProxyNets,
} }
} }
type Connector struct { type Connector struct {
state mcproto.State state mcproto.State
metrics *ConnectorMetrics metrics *ConnectorMetrics
sendProxyProto bool sendProxyProto bool
receiveProxyProto bool
trustedProxyNets []*net.IPNet
activeConnections int32 activeConnections int32
connectionsCond *sync.Cond connectionsCond *sync.Cond
@ -51,9 +55,17 @@ type Connector struct {
} }
func (c *Connector) StartAcceptingConnections(ctx context.Context, listenAddress string, connRateLimit int) error { 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 go c.acceptConnections(ctx, ln, connRateLimit)
var err error
return nil
}
func (c *Connector) createListener(ctx context.Context, listenAddress string) (net.Listener, error) {
if c.ngrokToken != "" { if c.ngrokToken != "" {
ngrokTun, err := ngrok.Listen(ctx, ngrokTun, err := ngrok.Listen(ctx,
config.TCPEndpoint(), config.TCPEndpoint(),
@ -61,22 +73,51 @@ func (c *Connector) StartAcceptingConnections(ctx context.Context, listenAddress
) )
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Unable to start ngrok tunnel") 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") logrus.WithField("ngrokUrl", ngrokTun.URL()).Info("Listening for Minecraft client connections via ngrok tunnel")
} else { return ngrokTun, nil
ln, err = net.Listen("tcp", listenAddress)
if err != nil {
logrus.WithError(err).Fatal("Unable to start listening")
return err
}
logrus.WithField("listenAddress", listenAddress).Info("Listening for Minecraft client connections")
} }
go c.acceptConnections(ctx, ln, connRateLimit) listener, err := net.Listen("tcp", listenAddress)
if err != nil {
logrus.WithError(err).Fatal("Unable to start listening")
return nil, err
}
logrus.WithField("listenAddress", listenAddress).Info("Listening for Minecraft client connections")
return nil if c.receiveProxyProto {
proxyListener := &proxyproto.Listener{
Listener: listener,
Policy: c.createProxyProtoPolicy(),
}
logrus.Info("Using PROXY protocol listener")
return proxyListener, nil
}
return listener, 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() { 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
}