mirror of
https://github.com/itzg/mc-router.git
synced 2024-11-21 11:25:41 +01:00
feat: add ability to receive proxy protocol (#307)
This commit is contained in:
parent
f32dfa3800
commit
e38a054c46
@ -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
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
77
server/connector_test.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user