From 7e87d71b16c1fdf2a2dcf14902d0bea40c3c48bb Mon Sep 17 00:00:00 2001 From: haveachin Date: Sat, 3 Feb 2024 21:38:01 +0100 Subject: [PATCH] feat: add receive proxy protocol support --- cmd/infrared/main.go | 8 +- configs/config.yml | 17 +++ configs/haproxy.cfg | 12 +- configs/proxy.yml | 2 +- deployments/docker-compose.dev.yml | 11 +- docs/.vitepress/config.mts | 8 +- docs/features/forward-player-ips.md | 16 --- docs/features/proxy-protocol.md | 43 ++++++ .../{rate-limit-ips.md => rate-limiter.md} | 0 pkg/infrared/infrared.go | 126 +++++++++--------- pkg/infrared/infrared_test.go | 61 +++++++-- pkg/infrared/protocol/packet.go | 17 --- pkg/infrared/proxy_protocol.go | 78 +++++++++++ 13 files changed, 267 insertions(+), 132 deletions(-) delete mode 100644 docs/features/forward-player-ips.md create mode 100644 docs/features/proxy-protocol.md rename docs/features/{rate-limit-ips.md => rate-limiter.md} (100%) create mode 100644 pkg/infrared/proxy_protocol.go diff --git a/cmd/infrared/main.go b/cmd/infrared/main.go index cbed84c..cda1701 100644 --- a/cmd/infrared/main.go +++ b/cmd/infrared/main.go @@ -84,8 +84,6 @@ func main() { log.Info().Msg("Starting Infrared") - ir.AddServerConfig() - if err := run(); err != nil { log.Fatal(). Err(err). @@ -125,7 +123,11 @@ func run() error { if errors.Is(err, ir.ErrNoServers) { log.Fatal(). Str("docs", "https://infrared.dev/config/proxies"). - Msg("No proxy configs found; check the docs") + Msg("No proxy configs found; Check the docs") + } else if errors.Is(err, ir.ErrNoTrustedCIDRs) { + log.Fatal(). + Str("docs", "https://infrared.dev/features/proxy-protocol#receive-proxy-protocol"). + Msg("Receive PROXY Protocol enabled, but no CIDRs specified; Check the docs") } else if err != nil { return err } diff --git a/configs/config.yml b/configs/config.yml index 4af56ff..4f84158 100644 --- a/configs/config.yml +++ b/configs/config.yml @@ -4,6 +4,23 @@ # bind: 0.0.0.0:25565 +# This is for receiving PROXY Protocol Headers +# +proxyProtocol: + # Set this to true to enable it. + # You also need to set trusted CIDRs to use this feature. + # You can only receive PROXY Protocol Headers from trusted CIDRs. + # + receive: false + + # List all your trusted CIDRs here. + # A CIDR is basically a way to talk about a whole range of IPs + # instead of just one. See here for more info: + # https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#IPv4_CIDR_blocks + # + trustedCIDRs: + - 127.0.0.1/32 + # Maximum duration between packets before the client gets timed out. # keepAliveTimeout: 30s diff --git a/configs/haproxy.cfg b/configs/haproxy.cfg index b5cefef..a599503 100644 --- a/configs/haproxy.cfg +++ b/configs/haproxy.cfg @@ -8,10 +8,6 @@ global maxconn 20000 log stdout local0 debug - user haproxy - chroot /usr/share/haproxy - pidfile /run/haproxy.pid - daemon defaults log global @@ -20,11 +16,6 @@ resolvers nameserver nameserver ns1 1.1.1.1:53 nameserver ns2 8.8.8.8:53 -#listen minecraft -# bind :25500 -# mode tcp -# server s1 127.0.0.1:25565 send-proxy-v2 resolvers nameserver - frontend minecraft_fe maxconn 2000 mode tcp @@ -33,5 +24,4 @@ frontend minecraft_fe backend minecraft_be mode tcp -# server s1 185.232.71.248:25565 send-proxy-v2 resolvers nameserver - server s1 127.0.0.1:25565 send-proxy-v2 resolvers nameserver \ No newline at end of file + server s1 127.0.0.1:25565 send-proxy-v2 resolvers nameserver diff --git a/configs/proxy.yml b/configs/proxy.yml index eab5d5d..d4de154 100644 --- a/configs/proxy.yml +++ b/configs/proxy.yml @@ -10,7 +10,7 @@ domains: addresses: - 127.0.0.1:25565 -# Send a Proxy Protocol v2 Header to the server to +# Send a PROXY Protocol Header to the server to # forward the players IP address # #sendProxyProtocol: true \ No newline at end of file diff --git a/deployments/docker-compose.dev.yml b/deployments/docker-compose.dev.yml index 9a4e1f3..744e7f1 100644 --- a/deployments/docker-compose.dev.yml +++ b/deployments/docker-compose.dev.yml @@ -21,19 +21,14 @@ services: - infrared.java.servers.devserver.address=:25566 haproxy: - image: haproxy + image: haproxy:alpine container_name: infrared-dev-haproxy - sysctls: - - net.ipv4.ip_unprivileged_port_start=0 volumes: - ../.dev/haproxy:/usr/local/etc/haproxy:ro - ports: - - 25567:25565/tcp - networks: - - infrared + network_mode: host redis: - image: redis + image: redis:alpine container_name: infrared-dev-redis ports: - 6379:6379/tcp diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 527e8a2..2679ff2 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -23,8 +23,8 @@ export default defineConfig({ { text: 'Features', items: [ - { text: 'PROXY Protocol', link: '/features/forward-player-ips' }, - { text: 'Rate Limiter', link: '/features/rate-limit-ips' }, + { text: 'PROXY Protocol', link: '/features/proxy-protocol' }, + { text: 'Rate Limiter', link: '/features/rate-limiter' }, ] }, { @@ -58,12 +58,12 @@ export default defineConfig({ { text: 'Features', items: [ - { text: 'Forward Player IPs', link: '/features/forward-player-ips' }, + { text: 'PROXY Protocol', link: '/features/proxy-protocol' }, { text: 'Filters', link: '/features/filters', items: [ - { text: 'Rate Limit IPs', link: '/features/rate-limit-ips' }, + { text: 'Rate Limiter', link: '/features/rate-limiter' }, ] } ] diff --git a/docs/features/forward-player-ips.md b/docs/features/forward-player-ips.md deleted file mode 100644 index 7fb6e96..0000000 --- a/docs/features/forward-player-ips.md +++ /dev/null @@ -1,16 +0,0 @@ -# Forward Player IPs - -You can forward the player IPs via proxy protocol. -To enable it in Infrared you just have to change this in you [**proxy config**](../config/proxies.md): -```yml -# Send a Proxy Protocol v2 Header to the server to -# forward the players IP address. -# -#sendProxyProtocol: true // [!code --] -sendProxyProtocol: true // [!code ++] -``` - -## Paper - -In Paper you have to enable it also to work. -See [the Paper documentation on Proxy Protocol](https://docs.papermc.io/paper/reference/global-configuration#proxies_proxy_protocol) for more. \ No newline at end of file diff --git a/docs/features/proxy-protocol.md b/docs/features/proxy-protocol.md new file mode 100644 index 0000000..e051b16 --- /dev/null +++ b/docs/features/proxy-protocol.md @@ -0,0 +1,43 @@ +# PROXY Protocol + +Infrared supportes [PROXY Protocol v2](). + +## Receive PROXY Protocol + +You can receive PROXY Protocol Headers, but you **need** to specify your trusted [CIDRs](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#IPv4_CIDR_blocks). +To enable it in Infrared you just have to change this in you [global config](../config/index.md): + +```yml +# This is for receiving PROXY Protocol Headers +# +proxyProtocol: + # Set this to true to enable it. + # You also need to set trusted CIDRs to use this feature. + # You can only receive PROXY Protocol Headers from trusted CIDRs. + # + receive: false + + # List all your trusted CIDRs here. + # A CIDR is basically a way to talk about a whole range of IPs + # instead of just one. + # + trustedCIDRs: + - 127.0.0.1/32 +``` + +## Forward Player IPs + +You can forward the player IPs via PROXY Protocol. +To enable it in Infrared you just have to change this in you [**proxy config**](../config/proxies.md): +```yml +# Send a PROXY Protocol Header to the server to +# forward the players IP address. +# +#sendProxyProtocol: true // [!code --] +sendProxyProtocol: true // [!code ++] +``` + +## Paper + +In Paper you have to enable it also to work. +See [the Paper documentation on PROXY Protocol](https://docs.papermc.io/paper/reference/global-configuration#proxies_proxy_protocol) for more. \ No newline at end of file diff --git a/docs/features/rate-limit-ips.md b/docs/features/rate-limiter.md similarity index 100% rename from docs/features/rate-limit-ips.md rename to docs/features/rate-limiter.md diff --git a/pkg/infrared/infrared.go b/pkg/infrared/infrared.go index 99775f5..c5e9c1a 100644 --- a/pkg/infrared/infrared.go +++ b/pkg/infrared/infrared.go @@ -9,42 +9,18 @@ import ( "time" "github.com/haveachin/infrared/pkg/infrared/protocol" - "github.com/pires/go-proxyproto" "github.com/rs/zerolog" ) type Config struct { - BindAddr string `yaml:"bind"` - ServerConfigs []ServerConfig `yaml:"servers"` - FiltersConfig FiltersConfig `yaml:"filters"` - KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"` + BindAddr string `yaml:"bind"` + KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"` + ServerConfigs []ServerConfig `yaml:"servers"` + FiltersConfig FiltersConfig `yaml:"filters"` + ProxyProtocolConfig ProxyProtocolConfig `yaml:"proxyProtocol"` } -type ConfigFunc func(cfg *Config) - -func WithBindAddr(bindAddr string) ConfigFunc { - return func(cfg *Config) { - cfg.BindAddr = bindAddr - } -} - -func AddServerConfig(fns ...ServerConfigFunc) ConfigFunc { - return func(cfg *Config) { - var sCfg ServerConfig - for _, fn := range fns { - fn(&sCfg) - } - cfg.ServerConfigs = append(cfg.ServerConfigs, sCfg) - } -} - -func WithKeepAliveTimeout(d time.Duration) ConfigFunc { - return func(cfg *Config) { - cfg.KeepAliveTimeout = d - } -} - -func DefaultConfig() Config { +func NewConfig() Config { return Config{ BindAddr: ":25565", KeepAliveTimeout: 30 * time.Second, @@ -54,7 +30,49 @@ func DefaultConfig() Config { WindowLength: time.Second, }, }, + ProxyProtocolConfig: ProxyProtocolConfig{ + TrustedCIDRs: make([]string, 0), + }, + } +} + +func (cfg Config) WithBindAddr(bindAddr string) Config { + cfg.BindAddr = bindAddr + return cfg +} + +func (cfg Config) AddServerConfig(fns ...ServerConfigFunc) Config { + var sCfg ServerConfig + for _, fn := range fns { + fn(&sCfg) } + cfg.ServerConfigs = append(cfg.ServerConfigs, sCfg) + return cfg +} + +func (cfg Config) WithKeepAliveTimeout(d time.Duration) Config { + cfg.KeepAliveTimeout = d + return cfg +} + +func (cfg Config) WithProxyProtocolReceive(receive bool) Config { + cfg.ProxyProtocolConfig.Receive = receive + return cfg +} + +func (cfg Config) WithProxyProtocolTrustedCIDRs(trustedCIDRs ...string) Config { + cfg.ProxyProtocolConfig.TrustedCIDRs = trustedCIDRs + return cfg +} + +func (cfg Config) WithRateLimiterWindowLength(windowLength time.Duration) Config { + cfg.FiltersConfig.RateLimiter.WindowLength = windowLength + return cfg +} + +func (cfg Config) WithRateLimiterRequestLimit(requestLimit int) Config { + cfg.FiltersConfig.RateLimiter.RequestLimit = requestLimit + return cfg } type ConfigProvider interface { @@ -89,13 +107,8 @@ type Infrared struct { sr ServerRequester } -func New(fns ...ConfigFunc) *Infrared { - cfg := DefaultConfig() - for _, fn := range fns { - fn(&cfg) - } - - return NewWithConfig(cfg) +func New() *Infrared { + return NewWithConfig(NewConfig()) } func NewWithConfigProvider(prv ConfigProvider) *Infrared { @@ -127,6 +140,18 @@ func (ir *Infrared) initListener() error { } } + if ir.cfg.ProxyProtocolConfig.Receive { + fn := ir.NewListenerFunc + ir.NewListenerFunc = func(addr string) (net.Listener, error) { + l, err := fn(addr) + if err != nil { + return nil, err + } + + return newProxyProtocolListener(l, ir.cfg.ProxyProtocolConfig.TrustedCIDRs) + } + } + l, err := ir.NewListenerFunc(ir.cfg.BindAddr) if err != nil { return err @@ -329,30 +354,3 @@ func (ir *Infrared) pipe(dst io.WriteCloser, src io.ReadCloser, srcClosedChan ch srcClosedChan <- struct{}{} } - -func writeProxyProtocolHeader(addr net.Addr, rc net.Conn) error { - rcAddr := rc.RemoteAddr() - tcpAddr, ok := rcAddr.(*net.TCPAddr) - if !ok { - panic("not a tcp connection") - } - - tp := proxyproto.TCPv4 - if tcpAddr.IP.To4() == nil { - tp = proxyproto.TCPv6 - } - - header := &proxyproto.Header{ - Version: 2, - Command: proxyproto.PROXY, - TransportProtocol: tp, - SourceAddr: addr, - DestinationAddr: rcAddr, - } - - if _, err := header.WriteTo(rc); err != nil { - return err - } - - return nil -} diff --git a/pkg/infrared/infrared_test.go b/pkg/infrared/infrared_test.go index 6c98be6..fa447d1 100644 --- a/pkg/infrared/infrared_test.go +++ b/pkg/infrared/infrared_test.go @@ -24,6 +24,22 @@ func (c VirtualConn) RemoteAddr() net.Addr { } } +func (c VirtualConn) SendProxyProtocolHeader() error { + header := &proxyproto.Header{ + Version: 2, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: c.RemoteAddr(), + DestinationAddr: c.RemoteAddr(), + } + + if _, err := header.WriteTo(c); err != nil { + return err + } + + return nil +} + func (c VirtualConn) SendHandshake(hs handshaking.ServerBoundHandshake) error { pk := protocol.Packet{} if err := hs.Marshal(&pk); err != nil { @@ -90,8 +106,11 @@ func (vi *VirtualInfrared) Close() { // Connections are simulated via synchronous, in-memory, full duplex network connection (see net.Pipe). // It returns a the virtual Infrared instance and the output pipe to the virutal external server. // Use the out pipe to see what is actually sent to the server. Like the PROXY Protocol header. -func NewVirtualInfrared(sendProxyProtocol bool) (*VirtualInfrared, net.Conn) { - vir := ir.New() +func NewVirtualInfrared( + cfg ir.Config, + sendProxyProtocol bool, +) (*VirtualInfrared, net.Conn) { + vir := ir.NewWithConfig(cfg) connChan := make(chan net.Conn) vir.NewListenerFunc = func(addr string) (net.Listener, error) { @@ -126,10 +145,14 @@ func NewVirtualInfrared(sendProxyProtocol bool) (*VirtualInfrared, net.Conn) { } func TestInfrared_SendProxyProtocol_True(t *testing.T) { - vi, srvOut := NewVirtualInfrared(true) + vi, srvOut := NewVirtualInfrared(ir.NewConfig(), true) vc := vi.NewConn() - _ = vc.SendHandshake(handshaking.ServerBoundHandshake{}) - _ = vc.SendLoginStart(login.ServerBoundLoginStart{}, protocol.Version1_20_2) + if err := vc.SendHandshake(handshaking.ServerBoundHandshake{}); err != nil { + t.Fatal(err) + } + if err := vc.SendLoginStart(login.ServerBoundLoginStart{}, protocol.Version1_20_2); err != nil { + t.Fatal(err) + } r := bufio.NewReader(srvOut) header, err := proxyproto.Read(r) @@ -151,13 +174,35 @@ func TestInfrared_SendProxyProtocol_True(t *testing.T) { } func TestInfrared_SendProxyProtocol_False(t *testing.T) { - vi, srvOut := NewVirtualInfrared(false) + vi, srvOut := NewVirtualInfrared(ir.NewConfig(), false) vc := vi.NewConn() - _ = vc.SendHandshake(handshaking.ServerBoundHandshake{}) - _ = vc.SendLoginStart(login.ServerBoundLoginStart{}, protocol.Version1_20_2) + if err := vc.SendHandshake(handshaking.ServerBoundHandshake{}); err != nil { + t.Fatal(err) + } + if err := vc.SendLoginStart(login.ServerBoundLoginStart{}, protocol.Version1_20_2); err != nil { + t.Fatal(err) + } r := bufio.NewReader(srvOut) if _, err := proxyproto.Read(r); err == nil { t.Fatal("Expected error reading proxy protocol header, but got nothing") } } + +func TestInfrared_ReceiveProxyProtocol_True(t *testing.T) { + cfg := ir.NewConfig(). + WithProxyProtocolReceive(true). + WithProxyProtocolTrustedCIDRs() + + vi, _ := NewVirtualInfrared(cfg, false) + vc := vi.NewConn() + if err := vc.SendProxyProtocolHeader(); err != nil { + t.Fatal(err) + } + if err := vc.SendHandshake(handshaking.ServerBoundHandshake{}); err != nil { + t.Fatal(err) + } + if err := vc.SendLoginStart(login.ServerBoundLoginStart{}, protocol.Version1_20_2); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/infrared/protocol/packet.go b/pkg/infrared/protocol/packet.go index 79f16cd..6e9f37f 100644 --- a/pkg/infrared/protocol/packet.go +++ b/pkg/infrared/protocol/packet.go @@ -99,20 +99,3 @@ func (pk *Packet) ReadFrom(r io.Reader) (int64, error) { return n, nil } - -type Builder struct { - buf bytes.Buffer -} - -func (p *Builder) WriteField(fields ...FieldEncoder) { - for _, f := range fields { - _, err := f.WriteTo(&p.buf) - if err != nil { - panic(err) - } - } -} - -func (p *Builder) Packet(id int32) Packet { - return Packet{ID: id, Data: p.buf.Bytes()} -} diff --git a/pkg/infrared/proxy_protocol.go b/pkg/infrared/proxy_protocol.go new file mode 100644 index 0000000..afcf372 --- /dev/null +++ b/pkg/infrared/proxy_protocol.go @@ -0,0 +1,78 @@ +package infrared + +import ( + "errors" + "net" + + "github.com/pires/go-proxyproto" +) + +var ( + ErrUpstreamNotTrusted = errors.New("upstream not trusted") + ErrNoTrustedCIDRs = errors.New("no trusted CIDRs") +) + +type ProxyProtocolConfig struct { + Receive bool `yaml:"receive"` + TrustedCIDRs []string `yaml:"trustedCIDRs"` +} + +func newProxyProtocolListener(l net.Listener, trustedCIDRs []string) (net.Listener, error) { + if len(trustedCIDRs) == 0 { + return nil, ErrNoTrustedCIDRs + } + + cidrs := make([]*net.IPNet, len(trustedCIDRs)) + for i, trustedCIDR := range trustedCIDRs { + _, cidr, err := net.ParseCIDR(trustedCIDR) + if err != nil { + return nil, err + } + cidrs[i] = cidr + } + + return &proxyproto.Listener{ + Listener: l, + Policy: func(upstream net.Addr) (proxyproto.Policy, error) { + tcpAddr, ok := upstream.(*net.TCPAddr) + if !ok { + return proxyproto.REJECT, errors.New("not a tcp conn") + } + + for _, cidr := range cidrs { + if cidr.Contains(tcpAddr.IP) { + return proxyproto.REQUIRE, nil + } + } + + return proxyproto.REJECT, ErrUpstreamNotTrusted + }, + }, nil +} + +func writeProxyProtocolHeader(addr net.Addr, rc net.Conn) error { + rcAddr := rc.RemoteAddr() + tcpAddr, ok := rcAddr.(*net.TCPAddr) + if !ok { + panic("not a tcp connection") + } + + tp := proxyproto.TCPv4 + if tcpAddr.IP.To4() == nil { + tp = proxyproto.TCPv6 + } + + header := &proxyproto.Header{ + Version: 2, + Command: proxyproto.PROXY, + TransportProtocol: tp, + SourceAddr: addr, + DestinationAddr: rcAddr, + } + + if _, err := header.WriteTo(rc); err != nil { + return err + } + + return nil +}