diff --git a/.golangci.yml b/.golangci.yml index dea3674..63e2d3a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -252,6 +252,9 @@ linters: - wastedassign # finds wasted assignment statements - whitespace # detects leading and trailing whitespace - prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated + - zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event + - testpackage # makes you use a separate _test package + - tagalign # checks that struct tags are well aligned ## you may want to enable #- decorder # checks declaration order and count of types, constants, variables and functions @@ -263,13 +266,10 @@ linters: #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters #- interfacebloat # checks the number of methods inside an interface #- ireturn # accept interfaces, return concrete types - #- tagalign # checks that struct tags are well aligned #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope #- wrapcheck # checks that errors returned from external packages are wrapped - #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event #- gochecknoglobals # checks that no global variables exist #- gomnd # detects magic numbers - #- testpackage # makes you use a separate _test package ## disabled #- containedctx # detects struct contained context.Context field diff --git a/README.md b/README.md index 5d44ae0..cf67cb0 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

> [!WARNING] -> Infrared is currently under active development: breaking changes can happen. +> Infrared is currently under active development: bugs and breaking changes can happen. > Feedback and contributions are welcome. An ultra lightweight Minecraft reverse proxy and status placeholder: diff --git a/cmd/infrared/main.go b/cmd/infrared/main.go index 2eb6098..bbb8a43 100644 --- a/cmd/infrared/main.go +++ b/cmd/infrared/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "os" "os/signal" "syscall" @@ -117,12 +118,25 @@ func run() error { select { case sig := <-sigChan: - log.Printf("Received %s", sig.String()) + log.Info().Msg("Received " + sig.String()) case err := <-errChan: - if err != nil { - return err + switch { + case errors.Is(err, ir.ErrNoServers): + log.Fatal(). + Str("docs", "https://infrared.dev/config/proxies"). + Msg("No proxy configs found; Check the docs") + case 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") + default: + if err != nil { + return err + } } } + log.Info().Msg("Bye") + return nil } 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..2683a39 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' }, ] }, { @@ -47,6 +47,7 @@ export default defineConfig({ sidebar: [ { text: 'Getting Started', link: '/getting-started' }, + { text: 'Community Projects', link: '/community-projects' }, { text: 'Config', items: [ @@ -58,12 +59,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/community-projects.md b/docs/community-projects.md new file mode 100644 index 0000000..d4d57d6 --- /dev/null +++ b/docs/community-projects.md @@ -0,0 +1,13 @@ +# Community Projects + +> [!NOTE] +> These projects are managed by the Infrared Community. +> We do **not** provide official support for these projects. +> Please use their dedicated issue trackers or support channels provided by the respective project. +> Thanks for understanding. + +## Infrared for Pterodactyl + +An egg to run Infrared in Pterodactyl. \ +Repo: [Shadowner/Infrared-Pterodactyl-egg](https://github.com/Shadowner/Infrared-Pterodactyl-egg) \ +Owner: [Shadowner](https://github.com/Shadowner) diff --git a/docs/features/filters.md b/docs/features/filters.md index ec8abf6..437b149 100644 --- a/docs/features/filters.md +++ b/docs/features/filters.md @@ -17,4 +17,4 @@ filters: Now you actually need to add filters to your config. This is a list of all the filters that currently exist: -- [Rate Limiter](rate-limit-ips) \ No newline at end of file +- [Rate Limiter](rate-limiter) \ No newline at end of file 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..2bc29b9 --- /dev/null +++ b/docs/features/proxy-protocol.md @@ -0,0 +1,43 @@ +# PROXY Protocol + +Infrared supportes [PROXY Protocol v2](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). + +## 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): + +```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): +```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 94% rename from docs/features/rate-limit-ips.md rename to docs/features/rate-limiter.md index 15130e2..0900279 100644 --- a/docs/features/rate-limit-ips.md +++ b/docs/features/rate-limiter.md @@ -1,7 +1,7 @@ # Rate Limit IPs You can rate limit by IP address using the `rateLimit` filter. -This can be easily activated in your [**global config**](../config/index.md) by adding this: +This can be easily activated in your [**global config**](../config/index) by adding this: ```yml{2-16} filters: diff --git a/pkg/infrared/conn.go b/pkg/infrared/conn.go index b58ab2e..5bc786c 100644 --- a/pkg/infrared/conn.go +++ b/pkg/infrared/conn.go @@ -12,64 +12,55 @@ import ( "github.com/haveachin/infrared/pkg/infrared/protocol/login" ) -var connPool = sync.Pool{ +var cliConnPool = sync.Pool{ New: func() any { - return &Conn{ + return &clientConn{ readPks: [2]protocol.Packet{}, } }, } -type Conn struct { +type conn struct { net.Conn - r *bufio.Reader - w io.Writer - timeout time.Duration - readPks [2]protocol.Packet - handshake handshaking.ServerBoundHandshake - loginStart login.ServerBoundLoginStart - reqDomain ServerDomain + r *bufio.Reader + w io.Writer + timeout time.Duration } -func newConn(c net.Conn) *Conn { +func newConn(c net.Conn) conn { if c == nil { panic("c cannot be nil") } - conn, ok := connPool.Get().(*Conn) - if !ok { - panic("connPool contains other implementations of net.Conn") + return conn{ + Conn: c, + r: bufio.NewReader(c), + w: c, + timeout: time.Second * 10, } - - conn.Conn = c - conn.r = bufio.NewReader(c) - conn.w = c - conn.reqDomain = "" - conn.timeout = time.Second * 10 - return conn } -func (c *Conn) Read(b []byte) (int, error) { +func (c *conn) Read(b []byte) (int, error) { if err := c.SetReadDeadline(time.Now().Add(c.timeout)); err != nil { return 0, err } return c.r.Read(b) } -func (c *Conn) Write(b []byte) (int, error) { +func (c *conn) Write(b []byte) (int, error) { if err := c.SetWriteDeadline(time.Now().Add(c.timeout)); err != nil { return 0, err } return c.w.Write(b) } -func (c *Conn) ReadPacket(pk *protocol.Packet) error { +func (c *conn) ReadPacket(pk *protocol.Packet) error { _, err := pk.ReadFrom(c.r) return err } -func (c *Conn) ReadPackets(pks ...*protocol.Packet) error { +func (c *conn) ReadPackets(pks ...*protocol.Packet) error { for i := 0; i < len(pks); i++ { if err := c.ReadPacket(pks[i]); err != nil { return err @@ -78,12 +69,12 @@ func (c *Conn) ReadPackets(pks ...*protocol.Packet) error { return nil } -func (c *Conn) WritePacket(pk protocol.Packet) error { +func (c *conn) WritePacket(pk protocol.Packet) error { _, err := pk.WriteTo(c.w) return err } -func (c *Conn) WritePackets(pks ...protocol.Packet) error { +func (c *conn) WritePackets(pks ...protocol.Packet) error { for _, pk := range pks { if err := c.WritePacket(pk); err != nil { return err @@ -92,7 +83,7 @@ func (c *Conn) WritePackets(pks ...protocol.Packet) error { return nil } -func (c *Conn) ForceClose() error { +func (c *conn) ForceClose() error { if conn, ok := c.Conn.(*net.TCPConn); ok { if err := conn.SetLinger(0); err != nil { return err @@ -100,3 +91,39 @@ func (c *Conn) ForceClose() error { } return c.Close() } + +func (c *conn) Close() error { + return c.Conn.Close() +} + +type ServerConn struct { + conn +} + +func NewServerConn(c net.Conn) *ServerConn { + return &ServerConn{ + conn: newConn(c), + } +} + +type clientConn struct { + conn + + readPks [2]protocol.Packet + handshake handshaking.ServerBoundHandshake + loginStart login.ServerBoundLoginStart + reqDomain ServerDomain +} + +func newClientConn(c net.Conn) (*clientConn, func()) { + conn, ok := cliConnPool.Get().(*clientConn) + if !ok { + panic("connPool contains other implementations of net.Conn") + } + + conn.conn = newConn(c) + conn.reqDomain = "" + return conn, func() { + cliConnPool.Put(conn) + } +} diff --git a/pkg/infrared/filter.go b/pkg/infrared/filter.go index d78cffa..275b9af 100644 --- a/pkg/infrared/filter.go +++ b/pkg/infrared/filter.go @@ -14,10 +14,6 @@ func (f FilterFunc) Filter(c net.Conn) error { return f(c) } -type ( - FilterID string -) - type FilterConfigFunc func(cfg *FiltersConfig) func WithFilterConfig(c FiltersConfig) FilterConfigFunc { diff --git a/pkg/infrared/infrared.go b/pkg/infrared/infrared.go index cb6adbb..a174854 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,14 +30,56 @@ 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 { Config() (Config, error) } -func MustConfig(fn func() (Config, error)) Config { +func MustProvideConfig(fn func() (Config, error)) Config { cfg, err := fn() if err != nil { panic(err) @@ -70,29 +88,31 @@ func MustConfig(fn func() (Config, error)) Config { return cfg } +type ( + NewListenerFunc func(addr string) (net.Listener, error) + NewServerRequesterFunc func([]*Server) (ServerRequester, error) +) + type Infrared struct { - Logger zerolog.Logger + Logger zerolog.Logger + NewListenerFunc NewListenerFunc + NewServerRequesterFunc NewServerRequesterFunc cfg Config l net.Listener - sg *ServerGateway filter Filter bufPool sync.Pool - conns map[net.Addr]*Conn + conns map[net.Addr]*clientConn + 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 { - cfg := MustConfig(prv.Config) + cfg := MustProvideConfig(prv.Config) return NewWithConfig(cfg) } @@ -105,21 +125,43 @@ func NewWithConfig(cfg Config) *Infrared { return &b }, }, - conns: make(map[net.Addr]*Conn), + conns: make(map[net.Addr]*clientConn), } } -func (ir *Infrared) init() error { +func (ir *Infrared) initListener() error { ir.Logger.Info(). Str("bind", ir.cfg.BindAddr). Msg("Starting listener") - l, err := net.Listen("tcp", ir.cfg.BindAddr) + if ir.NewListenerFunc == nil { + ir.NewListenerFunc = func(addr string) (net.Listener, error) { + return net.Listen("tcp", addr) + } + } + + 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 } ir.l = l + return nil +} + +func (ir *Infrared) initServerGateway() error { srvs := make([]*Server, 0) for _, sCfg := range ir.cfg.ServerConfigs { srv, err := NewServer(WithServerConfig(sCfg)) @@ -129,12 +171,31 @@ func (ir *Infrared) init() error { srvs = append(srvs, srv) } - ir.filter = NewFilter(WithFilterConfig(ir.cfg.FiltersConfig)) - sg, err := NewServerGateway(srvs, nil) + if ir.NewServerRequesterFunc == nil { + ir.NewServerRequesterFunc = func(s []*Server) (ServerRequester, error) { + return NewServerGateway(srvs, nil) + } + } + + sr, err := ir.NewServerRequesterFunc(srvs) if err != nil { return err } - ir.sg = sg + ir.sr = sr + + return nil +} + +func (ir *Infrared) init() error { + if err := ir.initListener(); err != nil { + return err + } + + if err := ir.initServerGateway(); err != nil { + return err + } + + ir.filter = NewFilter(WithFilterConfig(ir.cfg.FiltersConfig)) return nil } @@ -147,7 +208,7 @@ func (ir *Infrared) ListenAndServe() error { for { c, err := ir.l.Accept() if errors.Is(err, net.ErrClosed) { - return err + return nil } else if err != nil { ir.Logger.Debug(). Err(err). @@ -168,10 +229,10 @@ func (ir *Infrared) handleNewConn(c net.Conn) { return } - conn := newConn(c) + conn, cleanUp := newClientConn(c) defer func() { - conn.ForceClose() - connPool.Put(conn) + _ = conn.ForceClose() + cleanUp() }() if err := ir.handleConn(conn); err != nil { @@ -181,7 +242,7 @@ func (ir *Infrared) handleNewConn(c net.Conn) { } } -func (ir *Infrared) handleConn(c *Conn) error { +func (ir *Infrared) handleConn(c *clientConn) error { if err := c.ReadPackets(&c.readPks[0], &c.readPks[1]); err != nil { return err } @@ -200,7 +261,8 @@ func (ir *Infrared) handleConn(c *Conn) error { } c.reqDomain = ServerDomain(reqDomain) - resp, err := ir.sg.RequestServer(ServerRequest{ + resp, err := ir.sr.RequestServer(ServerRequest{ + ClientAddr: c.RemoteAddr(), Domain: c.reqDomain, IsLogin: c.handshake.IsLoginRequest(), ProtocolVersion: protocol.Version(c.handshake.ProtocolVersion), @@ -217,7 +279,7 @@ func (ir *Infrared) handleConn(c *Conn) error { return ir.handleLogin(c, resp) } -func handleStatus(c *Conn, resp ServerResponse) error { +func handleStatus(c *clientConn, resp ServerResponse) error { if err := c.WritePacket(resp.StatusResponse); err != nil { return err } @@ -234,7 +296,7 @@ func handleStatus(c *Conn, resp ServerResponse) error { return nil } -func (ir *Infrared) handleLogin(c *Conn, resp ServerResponse) error { +func (ir *Infrared) handleLogin(c *clientConn, resp ServerResponse) error { hsVersion := protocol.Version(c.handshake.ProtocolVersion) if err := c.loginStart.Unmarshal(c.readPks[1], hsVersion); err != nil { return err @@ -245,7 +307,7 @@ func (ir *Infrared) handleLogin(c *Conn, resp ServerResponse) error { return ir.handlePipe(c, resp) } -func (ir *Infrared) handlePipe(c *Conn, resp ServerResponse) error { +func (ir *Infrared) handlePipe(c *clientConn, resp ServerResponse) error { rc := resp.ServerConn defer rc.Close() @@ -266,8 +328,8 @@ func (ir *Infrared) handlePipe(c *Conn, resp ServerResponse) error { rc.timeout = ir.cfg.KeepAliveTimeout ir.conns[c.RemoteAddr()] = c - go ir.copy(rc, c, cClosedChan) - go ir.copy(c, rc, rcClosedChan) + go ir.pipe(rc, c, cClosedChan) + go ir.pipe(c, rc, rcClosedChan) var waitChan chan struct{} select { @@ -284,34 +346,12 @@ func (ir *Infrared) handlePipe(c *Conn, resp ServerResponse) error { return nil } -func (ir *Infrared) copy(dst io.WriteCloser, src io.ReadCloser, srcClosedChan chan struct{}) { - _, _ = io.Copy(dst, src) - 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 +func (ir *Infrared) pipe(dst io.WriteCloser, src io.ReadCloser, srcClosedChan chan struct{}) { + if _, err := io.Copy(dst, src); err != nil && !errors.Is(err, io.EOF) { + ir.Logger.Debug(). + Err(err). + Msg("Connection closed unexpectedly") } - return nil + srcClosedChan <- struct{}{} } diff --git a/pkg/infrared/infrared_test.go b/pkg/infrared/infrared_test.go index 34f204d..0795e40 100644 --- a/pkg/infrared/infrared_test.go +++ b/pkg/infrared/infrared_test.go @@ -1,43 +1,194 @@ -package infrared +package infrared_test import ( "bufio" + "errors" "net" "testing" + ir "github.com/haveachin/infrared/pkg/infrared" + "github.com/haveachin/infrared/pkg/infrared/protocol" + "github.com/haveachin/infrared/pkg/infrared/protocol/handshaking" + "github.com/haveachin/infrared/pkg/infrared/protocol/login" "github.com/pires/go-proxyproto" ) -type TestConn struct { +type VirtualConn struct { net.Conn + remoteAddr net.Addr } -func (c *TestConn) RemoteAddr() net.Addr { - return &net.TCPAddr{ - IP: net.IPv4(127, 0, 0, 1), - Port: 25565, +func (c VirtualConn) RemoteAddr() net.Addr { + if c.remoteAddr == nil { + c.remoteAddr = &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 25565, + } } + + return c.remoteAddr } -func TestInfrared_handlePipe_ProxyProtocol(t *testing.T) { - rcIn, rcOut := net.Pipe() - _, cOut := net.Pipe() +func (c VirtualConn) SendProxyProtocolHeader() error { + header := &proxyproto.Header{ + Version: 2, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: c.RemoteAddr(), + DestinationAddr: c.RemoteAddr(), + } - c := TestConn{Conn: cOut} - rc := TestConn{Conn: rcIn} + if _, err := header.WriteTo(c); err != nil { + return err + } - srv := New() + return nil +} - go func() { - resp := ServerResponse{ - ServerConn: newConn(&rc), - SendProxyProtocol: true, - } +func (c VirtualConn) SendHandshake(hs handshaking.ServerBoundHandshake) error { + pk := protocol.Packet{} + if err := hs.Marshal(&pk); err != nil { + return err + } + + _, err := pk.WriteTo(c.Conn) + return err +} - _ = srv.handlePipe(newConn(&c), resp) +func (c VirtualConn) SendLoginStart(ls login.ServerBoundLoginStart, v protocol.Version) error { + pk := protocol.Packet{} + if err := ls.Marshal(&pk, v); err != nil { + return err + } + + _, err := pk.WriteTo(c.Conn) + return err +} + +type VirtualListener struct { + connChan <-chan net.Conn + errChan chan error + acceptTickerChan chan struct{} +} + +func (l *VirtualListener) Accept() (net.Conn, error) { + l.errChan = make(chan error) + + l.acceptTickerChan <- struct{}{} + defer func() { + select { + case <-l.acceptTickerChan: + default: + } }() - r := bufio.NewReader(rcOut) + select { + case c := <-l.connChan: + return c, nil + case err := <-l.errChan: + return nil, err + } +} + +func (l *VirtualListener) AcceptTick() <-chan struct{} { + return l.acceptTickerChan +} + +func (l *VirtualListener) Close() error { + l.errChan <- net.ErrClosed + return nil +} + +func (l *VirtualListener) Addr() net.Addr { + return nil +} + +type VirtualInfrared struct { + vir *ir.Infrared + vl *VirtualListener + connChan chan<- net.Conn +} + +func (vi *VirtualInfrared) ListenAndServe() error { + return vi.vir.ListenAndServe() +} + +func (vi *VirtualInfrared) MustListenAndServe(t *testing.T) { + if err := vi.ListenAndServe(); errors.Is(err, net.ErrClosed) { + return + } else if err != nil { + t.Error(err) + } +} + +func (vi *VirtualInfrared) NewConn(remoteAddr net.Addr) VirtualConn { + cIn, cOut := net.Pipe() + vi.connChan <- VirtualConn{ + Conn: cOut, + remoteAddr: remoteAddr, + } + return VirtualConn{Conn: cIn} +} + +func (vi *VirtualInfrared) Close() { + _ = vi.vl.Close() +} + +func (vi *VirtualInfrared) AcceptTick() <-chan struct{} { + return vi.vl.AcceptTick() +} + +// NewVirtualInfrared sets up a virtualized Infrared instance that is ready to accept new virutal connections. +// 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( + cfg ir.Config, + sendProxyProtocol bool, +) (*VirtualInfrared, net.Conn) { + vir := ir.NewWithConfig(cfg) + + connChan := make(chan net.Conn) + vl := &VirtualListener{ + connChan: connChan, + errChan: make(chan error), + acceptTickerChan: make(chan struct{}, 1), + } + vir.NewListenerFunc = func(addr string) (net.Listener, error) { + return vl, nil + } + + rcIn, rcOut := net.Pipe() + rc := VirtualConn{Conn: rcIn} + vir.NewServerRequesterFunc = func(s []*ir.Server) (ir.ServerRequester, error) { + return ir.ServerRequesterFunc(func(sr ir.ServerRequest) (ir.ServerResponse, error) { + return ir.ServerResponse{ + ServerConn: ir.NewServerConn(&rc), + SendProxyProtocol: sendProxyProtocol, + }, nil + }), nil + } + + return &VirtualInfrared{ + vir: vir, + vl: vl, + connChan: connChan, + }, rcOut +} + +func TestInfrared_SendProxyProtocol_True(t *testing.T) { + vi, srvOut := NewVirtualInfrared(ir.NewConfig(), true) + go vi.MustListenAndServe(t) + + vc := vi.NewConn(nil) + 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) if err != nil { t.Fatalf("Unexpected error reading proxy protocol header: %v", err) @@ -56,26 +207,98 @@ func TestInfrared_handlePipe_ProxyProtocol(t *testing.T) { } } -func TestInfrared_handlePipe_NoProxyProtocol(t *testing.T) { - rcIn, rcOut := net.Pipe() - _, cOut := net.Pipe() +func TestInfrared_SendProxyProtocol_False(t *testing.T) { + vi, srvOut := NewVirtualInfrared(ir.NewConfig(), false) + go vi.MustListenAndServe(t) - c := TestConn{Conn: cOut} - rc := TestConn{Conn: rcIn} + vc := vi.NewConn(nil) + 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) + } - srv := New() + 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("127.0.0.1/32") + vi, _ := NewVirtualInfrared(cfg, false) + go vi.MustListenAndServe(t) + + vc := vi.NewConn(nil) + 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) + } +} + +func TestInfrared_ReceiveProxyProtocol_False(t *testing.T) { + cfg := ir.NewConfig(). + WithProxyProtocolReceive(false) + + vi, _ := NewVirtualInfrared(cfg, false) + go vi.MustListenAndServe(t) + + vc := vi.NewConn(nil) + if err := vc.SendProxyProtocolHeader(); err != nil { + t.Fatal(err) + } + if err := vc.SendHandshake(handshaking.ServerBoundHandshake{}); err != nil { + return + } + t.Fatal("no disconnect after invalid proxy protocol header") +} + +func TestInfrared_ReceiveProxyProtocol_True_ErrNoTrustedCIDRs(t *testing.T) { + cfg := ir.NewConfig(). + WithProxyProtocolReceive(true). + WithProxyProtocolTrustedCIDRs() + + vi, _ := NewVirtualInfrared(cfg, false) + + errChan := make(chan error, 1) go func() { - resp := ServerResponse{ - ServerConn: newConn(&rc), - SendProxyProtocol: false, + errChan <- vi.ListenAndServe() + }() + + select { + case err := <-errChan: + if !errors.Is(err, ir.ErrNoTrustedCIDRs) { + t.Fatalf("got: %s; want: %s", err, ir.ErrNoTrustedCIDRs) } + case <-vi.AcceptTick(): + vi.Close() + t.Fatalf("got: no error during init; want: %s", ir.ErrNoTrustedCIDRs) + } +} - _ = srv.handlePipe(newConn(&c), resp) - }() +func TestInfrared_ReceiveProxyProtocol_True_UntrustedIP(t *testing.T) { + cfg := ir.NewConfig(). + WithProxyProtocolReceive(true). + WithProxyProtocolTrustedCIDRs("127.0.0.1/32") - r := bufio.NewReader(rcOut) - if _, err := proxyproto.Read(r); err == nil { - t.Fatal("Expected error reading proxy protocol header, but got nothing") + vi, _ := NewVirtualInfrared(cfg, false) + go vi.MustListenAndServe(t) + + vc := vi.NewConn(&net.TCPAddr{ + IP: net.IPv4(12, 34, 56, 78), + Port: 12345, + }) + + if err := vc.SendProxyProtocolHeader(); err == nil { + t.Fatal("no disconnect after untrusted IP") } } 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 +} diff --git a/pkg/infrared/rate_limiter.go b/pkg/infrared/rate_limiter.go index e040269..106ef2a 100644 --- a/pkg/infrared/rate_limiter.go +++ b/pkg/infrared/rate_limiter.go @@ -22,7 +22,7 @@ func RateLimit(requestLimit int, windowLength time.Duration, options ...RateLimi } func RateLimitByIP(requestLimit int, windowLength time.Duration) Filterer { - return RateLimit(requestLimit, windowLength, WithKeyFuncs(KeyByIP)) + return RateLimit(requestLimit, windowLength, WithKeyByIP()) } func KeyByIP(c net.Conn) string { diff --git a/pkg/infrared/server.go b/pkg/infrared/server.go index b7d7aaa..842884a 100644 --- a/pkg/infrared/server.go +++ b/pkg/infrared/server.go @@ -14,8 +14,11 @@ import ( "github.com/haveachin/infrared/pkg/infrared/protocol/status" ) +var ( + ErrNoServers = errors.New("no servers to route to") +) + type ( - ServerID string ServerAddress string ServerDomain string ) @@ -65,18 +68,17 @@ func NewServer(fns ...ServerConfigFunc) (*Server, error) { }, nil } -func (s Server) Dial() (*Conn, error) { +func (s Server) Dial() (*ServerConn, error) { c, err := net.Dial("tcp", string(s.cfg.Addresses[0])) if err != nil { return nil, err } - conn := newConn(c) - conn.timeout = time.Second * 10 - return conn, nil + return NewServerConn(c), nil } type ServerRequest struct { + ClientAddr net.Addr Domain ServerDomain IsLogin bool ProtocolVersion protocol.Version @@ -84,7 +86,7 @@ type ServerRequest struct { } type ServerResponse struct { - ServerConn *Conn + ServerConn *ServerConn StatusResponse protocol.Packet SendProxyProtocol bool } @@ -93,6 +95,12 @@ type ServerRequester interface { RequestServer(ServerRequest) (ServerResponse, error) } +type ServerRequesterFunc func(ServerRequest) (ServerResponse, error) + +func (fn ServerRequesterFunc) RequestServer(req ServerRequest) (ServerResponse, error) { + return fn(req) +} + type ServerGateway struct { responder ServerRequestResponder servers map[ServerDomain]*Server @@ -100,7 +108,7 @@ type ServerGateway struct { func NewServerGateway(servers []*Server, responder ServerRequestResponder) (*ServerGateway, error) { if len(servers) == 0 { - return nil, errors.New("server gateway: no servers to route to") + return nil, ErrNoServers } srvs := make(map[ServerDomain]*Server) @@ -185,7 +193,7 @@ func (r DialServerResponder) respondeToStatusRequest(req ServerRequest, srv *Ser r.respProvs[srv] = respProv } - _, pk, err := respProv.StatusResponse(req.ProtocolVersion, req.ReadPackets) + _, pk, err := respProv.StatusResponse(req.ClientAddr, req.ProtocolVersion, req.ReadPackets) if err != nil { return ServerResponse{}, err } @@ -196,7 +204,7 @@ func (r DialServerResponder) respondeToStatusRequest(req ServerRequest, srv *Ser } type StatusResponseProvider interface { - StatusResponse(protocol.Version, [2]protocol.Packet) (status.ResponseJSON, protocol.Packet, error) + StatusResponse(net.Addr, protocol.Version, [2]protocol.Packet) (status.ResponseJSON, protocol.Packet, error) } type statusCacheEntry struct { @@ -219,6 +227,7 @@ type statusResponseProvider struct { } func (s *statusResponseProvider) requestNewStatusResponseJSON( + cliAddr net.Addr, readPks [2]protocol.Packet, ) (status.ResponseJSON, protocol.Packet, error) { rc, err := s.server.Dial() @@ -226,6 +235,12 @@ func (s *statusResponseProvider) requestNewStatusResponseJSON( return status.ResponseJSON{}, protocol.Packet{}, err } + if s.server.cfg.SendProxyProtocol { + if err := writeProxyProtocolHeader(cliAddr, rc); err != nil { + return status.ResponseJSON{}, protocol.Packet{}, err + } + } + if err := rc.WritePackets(readPks[0], readPks[1]); err != nil { return status.ResponseJSON{}, protocol.Packet{}, err } @@ -250,11 +265,12 @@ func (s *statusResponseProvider) requestNewStatusResponseJSON( } func (s *statusResponseProvider) StatusResponse( + cliAddr net.Addr, protVer protocol.Version, readPks [2]protocol.Packet, ) (status.ResponseJSON, protocol.Packet, error) { if s.cacheTTL <= 0 { - return s.requestNewStatusResponseJSON(readPks) + return s.requestNewStatusResponseJSON(cliAddr, readPks) } // Prunes all expired status reponses @@ -266,17 +282,18 @@ func (s *statusResponseProvider) StatusResponse( hash, okHash := s.statusHash[protVer] entry, okCache := s.statusResponseCache[hash] if !okHash || !okCache { - return s.cacheResponse(protVer, readPks) + return s.cacheResponse(cliAddr, protVer, readPks) } return entry.responseJSON, entry.responsePk, nil } func (s *statusResponseProvider) cacheResponse( + cliAddr net.Addr, protVer protocol.Version, readPks [2]protocol.Packet, ) (status.ResponseJSON, protocol.Packet, error) { - newStatusResp, pk, err := s.requestNewStatusResponseJSON(readPks) + newStatusResp, pk, err := s.requestNewStatusResponseJSON(cliAddr, readPks) if err != nil { return status.ResponseJSON{}, protocol.Packet{}, err }