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
66
README.md
66
README.md
@ -11,65 +11,69 @@ Routes Minecraft client connections to backend servers based upon the requested
|
||||
|
||||
```text
|
||||
-api-binding host:port
|
||||
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
|
||||
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
|
||||
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
|
||||
Enables CPU profiling and writes to given path (env CPU_PROFILE)
|
||||
Enables CPU profiling and writes to given path (env CPU_PROFILE)
|
||||
-debug
|
||||
Enable debug logs (env DEBUG)
|
||||
Enable debug logs (env DEBUG)
|
||||
-default string
|
||||
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")
|
||||
host:port of a default Minecraft server to use when mapping not found (env DEFAULT)
|
||||
-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
|
||||
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
|
||||
Use Docker service discovery (env IN_DOCKER)
|
||||
Use Docker service discovery (env IN_DOCKER)
|
||||
-in-docker-swarm
|
||||
Use Docker Swarm service discovery (env IN_DOCKER_SWARM)
|
||||
Use Docker Swarm service discovery (env IN_DOCKER_SWARM)
|
||||
-in-kube-cluster
|
||||
Use in-cluster Kubernetes config (env IN_KUBE_CLUSTER)
|
||||
Use in-cluster Kubernetes config (env IN_KUBE_CLUSTER)
|
||||
-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
|
||||
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
|
||||
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
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_ADDR)
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_ADDR)
|
||||
-metrics-backend-config-influxdb-database string
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_DATABASE)
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_DATABASE)
|
||||
-metrics-backend-config-influxdb-interval duration
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_INTERVAL) (default 1m0s)
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_INTERVAL) (default 1m0s)
|
||||
-metrics-backend-config-influxdb-password string
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_PASSWORD)
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_PASSWORD)
|
||||
-metrics-backend-config-influxdb-retention-policy string
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_RETENTION_POLICY)
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_RETENTION_POLICY)
|
||||
-metrics-backend-config-influxdb-tags value
|
||||
any extra tags to be included with all reported metrics (env METRICS_BACKEND_CONFIG_INFLUXDB_TAGS)
|
||||
any extra tags to be included with all reported metrics (env METRICS_BACKEND_CONFIG_INFLUXDB_TAGS)
|
||||
-metrics-backend-config-influxdb-username string
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_USERNAME)
|
||||
(env METRICS_BACKEND_CONFIG_INFLUXDB_USERNAME)
|
||||
-ngrok-token string
|
||||
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
|
||||
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)
|
||||
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)
|
||||
Name or full path to routes config file (env ROUTES_CONFIG)
|
||||
-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)
|
||||
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)
|
||||
Send PROXY protocol to backend servers (env USE_PROXY_PROTOCOL)
|
||||
-version
|
||||
Output version and exit (env VERSION)
|
||||
Output version and exit (env VERSION)
|
||||
```
|
||||
|
||||
|
||||
|
@ -53,6 +53,9 @@ type Config struct {
|
||||
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."`
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
@ -88,6 +91,7 @@ func main() {
|
||||
if err != nil {
|
||||
logrus.WithError(err).Fatal("trying to create cpu profile file")
|
||||
}
|
||||
//goland:noinspection GoUnhandledErrorResult
|
||||
defer cpuProfileFile.Close()
|
||||
|
||||
logrus.WithField("file", config.CpuProfile).Info("Starting cpu profiling")
|
||||
@ -131,7 +135,12 @@ func main() {
|
||||
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 != "" {
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
metrics: metrics,
|
||||
sendProxyProto: sendProxyProto,
|
||||
connectionsCond: sync.NewCond(&sync.Mutex{}),
|
||||
receiveProxyProto: receiveProxyProto,
|
||||
trustedProxyNets: trustedProxyNets,
|
||||
clientFilter: clientFilter,
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +55,7 @@ type Connector struct {
|
||||
activeConnections int32
|
||||
connectionsCond *sync.Cond
|
||||
ngrokToken string
|
||||
clientFilter *ClientFilter
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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.
|
||||
WithField("client", clientAddr).
|
||||
Info("Got connection")
|
||||
|
Loading…
Reference in New Issue
Block a user