mirror of
https://github.com/itzg/mc-router.git
synced 2024-12-21 16:07:34 +01:00
Add support for allow/deny clients by IP (#355)
This commit is contained in:
parent
513e0b86a7
commit
7526a7078a
14
README.md
14
README.md
@ -14,6 +14,10 @@ Routes Minecraft client connections to backend servers based upon the requested
|
|||||||
The host:port bound for servicing API requests (env API_BINDING)
|
The host:port bound for servicing API requests (env API_BINDING)
|
||||||
-auto-scale-up
|
-auto-scale-up
|
||||||
Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed (env AUTO_SCALE_UP)
|
Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed (env AUTO_SCALE_UP)
|
||||||
|
-clients-to-allow value
|
||||||
|
Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny. (env CLIENTS_TO_ALLOW)
|
||||||
|
-clients-to-deny value
|
||||||
|
Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow (env CLIENTS_TO_DENY)
|
||||||
-connection-rate-limit int
|
-connection-rate-limit int
|
||||||
Max number of connections to allow per second (env CONNECTION_RATE_LIMIT) (default 1)
|
Max number of connections to allow per second (env CONNECTION_RATE_LIMIT) (default 1)
|
||||||
-cpu-profile string
|
-cpu-profile string
|
||||||
@ -22,12 +26,12 @@ Routes Minecraft client connections to backend servers based upon the requested
|
|||||||
Enable debug logs (env DEBUG)
|
Enable debug logs (env DEBUG)
|
||||||
-default string
|
-default string
|
||||||
host:port of a default Minecraft server to use when mapping not found (env DEFAULT)
|
host:port of a default Minecraft server to use when mapping not found (env DEFAULT)
|
||||||
-docker-socket
|
|
||||||
Path to Docker socket to use (env DOCKER_SOCKET) (default "unix:///var/run/docker.sock")
|
|
||||||
-docker-refresh-interval int
|
-docker-refresh-interval int
|
||||||
Refresh interval in seconds for the Docker Swarm integration (env DOCKER_REFRESH_INTERVAL) (default 15)
|
Refresh interval in seconds for the Docker integrations (env DOCKER_REFRESH_INTERVAL) (default 15)
|
||||||
|
-docker-socket string
|
||||||
|
Path to Docker socket to use (env DOCKER_SOCKET) (default "unix:///var/run/docker.sock")
|
||||||
-docker-timeout int
|
-docker-timeout int
|
||||||
Timeout configuration in seconds for the Docker Swarm integration (env DOCKER_TIMEOUT)
|
Timeout configuration in seconds for the Docker integrations (env DOCKER_TIMEOUT)
|
||||||
-in-docker
|
-in-docker
|
||||||
Use Docker service discovery (env IN_DOCKER)
|
Use Docker service discovery (env IN_DOCKER)
|
||||||
-in-docker-swarm
|
-in-docker-swarm
|
||||||
@ -37,7 +41,7 @@ Routes Minecraft client connections to backend servers based upon the requested
|
|||||||
-kube-config string
|
-kube-config string
|
||||||
The path to a Kubernetes configuration file (env KUBE_CONFIG)
|
The path to a Kubernetes configuration file (env KUBE_CONFIG)
|
||||||
-mapping value
|
-mapping value
|
||||||
Comma-separated or repeated mappings of externalHostname=host:port (env MAPPING)
|
Comma or newline delimited or repeated mappings of externalHostname=host:port (env MAPPING)
|
||||||
-metrics-backend string
|
-metrics-backend string
|
||||||
Backend to use for metrics exposure/publishing: discard,expvar,influxdb (env METRICS_BACKEND) (default "discard")
|
Backend to use for metrics exposure/publishing: discard,expvar,influxdb (env METRICS_BACKEND) (default "discard")
|
||||||
-metrics-backend-config-influxdb-addr string
|
-metrics-backend-config-influxdb-addr string
|
||||||
|
@ -53,6 +53,9 @@ type Config struct {
|
|||||||
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."`
|
||||||
|
|
||||||
|
ClientsToAllow []string `usage:"Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny."`
|
||||||
|
ClientsToDeny []string `usage:"Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow"`
|
||||||
|
|
||||||
SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"`
|
SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,6 +91,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("trying to create cpu profile file")
|
logrus.WithError(err).Fatal("trying to create cpu profile file")
|
||||||
}
|
}
|
||||||
|
//goland:noinspection GoUnhandledErrorResult
|
||||||
defer cpuProfileFile.Close()
|
defer cpuProfileFile.Close()
|
||||||
|
|
||||||
logrus.WithField("file", config.CpuProfile).Info("Starting cpu profiling")
|
logrus.WithField("file", config.CpuProfile).Info("Starting cpu profiling")
|
||||||
@ -131,7 +135,12 @@ func main() {
|
|||||||
trustedIpNets = append(trustedIpNets, ipNet)
|
trustedIpNets = append(trustedIpNets, ipNet)
|
||||||
}
|
}
|
||||||
|
|
||||||
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets)
|
clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Fatal("Unable to create client filter")
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, clientFilter)
|
||||||
if config.NgrokToken != "" {
|
if config.NgrokToken != "" {
|
||||||
connector.UseNgrok(config.NgrokToken)
|
connector.UseNgrok(config.NgrokToken)
|
||||||
}
|
}
|
||||||
|
101
server/client_filter.go
Normal file
101
server/client_filter.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addrMatcher struct {
|
||||||
|
addrs []netip.Addr
|
||||||
|
prefixes []netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAddrMatcher(filters []string) (*addrMatcher, error) {
|
||||||
|
addrs := make([]netip.Addr, 0)
|
||||||
|
prefixes := make([]netip.Prefix, 0)
|
||||||
|
|
||||||
|
if filters != nil {
|
||||||
|
for _, filter := range filters {
|
||||||
|
if strings.Contains(filter, "/") {
|
||||||
|
prefix, err := netip.ParsePrefix(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prefixes = append(prefixes, prefix)
|
||||||
|
} else {
|
||||||
|
addr, err := netip.ParseAddr(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &addrMatcher{
|
||||||
|
addrs: addrs,
|
||||||
|
prefixes: prefixes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *addrMatcher) Match(addr netip.Addr) bool {
|
||||||
|
for _, a := range a.addrs {
|
||||||
|
|
||||||
|
// Before comparison, need to unmap addresses such as
|
||||||
|
// ::ffff:127.0.0.1
|
||||||
|
unmapped := addr.Unmap()
|
||||||
|
if a == unmapped {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, p := range a.prefixes {
|
||||||
|
if p.Contains(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *addrMatcher) Empty() bool {
|
||||||
|
return len(a.addrs) == 0 && len(a.prefixes) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientFilter performs allow/deny filtering of client IP addresses
|
||||||
|
type ClientFilter struct {
|
||||||
|
allow *addrMatcher
|
||||||
|
deny *addrMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientFilter provides a mechanism to evaluate client IP addresses and determine if
|
||||||
|
// they should be allowed access or not.
|
||||||
|
// The allows and denies can each or both be nil or netip.ParseAddr allowed values.
|
||||||
|
func NewClientFilter(allows []string, denies []string) (*ClientFilter, error) {
|
||||||
|
allow, err := newAddrMatcher(allows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid allow filter")
|
||||||
|
}
|
||||||
|
deny, err := newAddrMatcher(denies)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid deny filter")
|
||||||
|
}
|
||||||
|
return &ClientFilter{
|
||||||
|
allow: allow,
|
||||||
|
deny: deny,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow determines if the given address is allowed by this filter
|
||||||
|
// where addrStr is a netip.ParseAddr allowed address
|
||||||
|
func (f *ClientFilter) Allow(addrPort netip.AddrPort) bool {
|
||||||
|
if !f.allow.Empty() {
|
||||||
|
matched := f.allow.Match(addrPort.Addr())
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
if !f.deny.Empty() {
|
||||||
|
matched := f.deny.Match(addrPort.Addr())
|
||||||
|
return !matched
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
173
server/client_filter_test.go
Normal file
173
server/client_filter_test.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientFilter_Allow(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
allow []string
|
||||||
|
deny []string
|
||||||
|
input string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
assertErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "defaults",
|
||||||
|
args: args{
|
||||||
|
input: "192.168.1.1",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just allow - matches",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"192.168.1.1"},
|
||||||
|
input: "192.168.1.1",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just allow - not match",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"192.168.1.1"},
|
||||||
|
input: "192.168.1.2",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just allow cidr - matches",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"192.168.1.0/8"},
|
||||||
|
input: "192.168.1.2",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just allow cidr or specific - matches cidr",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"192.168.1.0/8", "192.168.2.5"},
|
||||||
|
input: "192.168.1.2",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just allow cidr or specific - matches specific",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"192.168.1.0/8", "192.168.2.5"},
|
||||||
|
input: "192.168.2.5",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just deny - matches",
|
||||||
|
args: args{
|
||||||
|
deny: []string{"192.168.2.5"},
|
||||||
|
input: "192.168.2.5",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "just deny - not match",
|
||||||
|
args: args{
|
||||||
|
deny: []string{"192.168.2.5"},
|
||||||
|
input: "192.168.1.1",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mix allow",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"192.168.1.6"},
|
||||||
|
deny: []string{"192.168.1.0/8"},
|
||||||
|
input: "192.168.1.6",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
f, err := NewClientFilter(tt.args.allow, tt.args.deny)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
addr, err := netip.ParseAddr(tt.args.input)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
got := f.Allow(netip.AddrPortFrom(addr, 25565))
|
||||||
|
assert.Equalf(t, tt.want, got, "Allow(%v)", tt.args.input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClientFilter(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
allow []string
|
||||||
|
deny []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
assertErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allow single",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"192.168.1.1"},
|
||||||
|
},
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allow cidr",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"192.168.1.0/8"},
|
||||||
|
},
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deny single",
|
||||||
|
args: args{
|
||||||
|
deny: []string{"192.168.1.1"},
|
||||||
|
},
|
||||||
|
assertErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allow invalid",
|
||||||
|
args: args{
|
||||||
|
allow: []string{"7"},
|
||||||
|
},
|
||||||
|
assertErr: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deny invalid",
|
||||||
|
args: args{
|
||||||
|
deny: []string{"7"},
|
||||||
|
},
|
||||||
|
assertErr: assert.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := NewClientFilter(tt.args.allow, tt.args.deny)
|
||||||
|
tt.assertErr(t, err, fmt.Sprintf("NewClientFilter(%v, %v)", tt.args.allow, tt.args.deny))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -33,13 +33,15 @@ type ConnectorMetrics struct {
|
|||||||
ActiveConnections metrics.Gauge
|
ActiveConnections metrics.Gauge
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet) *Connector {
|
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet,
|
||||||
|
clientFilter *ClientFilter) *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,
|
receiveProxyProto: receiveProxyProto,
|
||||||
trustedProxyNets: trustedProxyNets,
|
trustedProxyNets: trustedProxyNets,
|
||||||
|
clientFilter: clientFilter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +55,7 @@ type Connector struct {
|
|||||||
activeConnections int32
|
activeConnections int32
|
||||||
connectionsCond *sync.Cond
|
connectionsCond *sync.Cond
|
||||||
ngrokToken string
|
ngrokToken string
|
||||||
|
clientFilter *ClientFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connector) StartAcceptingConnections(ctx context.Context, listenAddress string, connRateLimit int) error {
|
func (c *Connector) StartAcceptingConnections(ctx context.Context, listenAddress string, connRateLimit int) error {
|
||||||
@ -164,6 +167,17 @@ func (c *Connector) HandleConnection(ctx context.Context, frontendConn net.Conn)
|
|||||||
defer frontendConn.Close()
|
defer frontendConn.Close()
|
||||||
|
|
||||||
clientAddr := frontendConn.RemoteAddr()
|
clientAddr := frontendConn.RemoteAddr()
|
||||||
|
|
||||||
|
if tcpAddr, ok := clientAddr.(*net.TCPAddr); ok {
|
||||||
|
allow := c.clientFilter.Allow(tcpAddr.AddrPort())
|
||||||
|
if !allow {
|
||||||
|
logrus.WithField("client", clientAddr).Debug("Client is blocked")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logrus.WithField("client", clientAddr).Warn("Remote address is not a TCP address, skipping filtering")
|
||||||
|
}
|
||||||
|
|
||||||
logrus.
|
logrus.
|
||||||
WithField("client", clientAddr).
|
WithField("client", clientAddr).
|
||||||
Info("Got connection")
|
Info("Got connection")
|
||||||
|
Loading…
Reference in New Issue
Block a user