From d278315485da36d67079bacd151b7513390fc9f3 Mon Sep 17 00:00:00 2001 From: haveachin Date: Sat, 3 Feb 2024 04:38:27 +0100 Subject: [PATCH 1/7] refactor: infrared tests --- .golangci.yml | 6 +- cmd/infrared/main.go | 13 ++- pkg/infrared/conn.go | 83 ++++++++++++------- pkg/infrared/filter.go | 4 - pkg/infrared/infrared.go | 89 ++++++++++++++------ pkg/infrared/infrared_test.go | 148 ++++++++++++++++++++++++++-------- pkg/infrared/rate_limiter.go | 2 +- pkg/infrared/server.go | 21 +++-- 8 files changed, 264 insertions(+), 102 deletions(-) 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/cmd/infrared/main.go b/cmd/infrared/main.go index 2eb6098..cbed84c 100644 --- a/cmd/infrared/main.go +++ b/cmd/infrared/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "os" "os/signal" "syscall" @@ -83,6 +84,8 @@ func main() { log.Info().Msg("Starting Infrared") + ir.AddServerConfig() + if err := run(); err != nil { log.Fatal(). Err(err). @@ -117,12 +120,18 @@ 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 { + if errors.Is(err, ir.ErrNoServers) { + log.Fatal(). + Str("docs", "https://infrared.dev/config/proxies"). + Msg("No proxy configs found; check the docs") + } else if err != nil { return err } } + log.Info().Msg("Bye") + return nil } 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..99775f5 100644 --- a/pkg/infrared/infrared.go +++ b/pkg/infrared/infrared.go @@ -61,7 +61,7 @@ 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,16 +70,23 @@ 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 { @@ -92,7 +99,7 @@ func New(fns ...ConfigFunc) *Infrared { } func NewWithConfigProvider(prv ConfigProvider) *Infrared { - cfg := MustConfig(prv.Config) + cfg := MustProvideConfig(prv.Config) return NewWithConfig(cfg) } @@ -105,21 +112,31 @@ 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) + } + } + + 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 +146,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 +183,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 +204,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 +217,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 +236,7 @@ func (ir *Infrared) handleConn(c *Conn) error { } c.reqDomain = ServerDomain(reqDomain) - resp, err := ir.sg.RequestServer(ServerRequest{ + resp, err := ir.sr.RequestServer(ServerRequest{ Domain: c.reqDomain, IsLogin: c.handshake.IsLoginRequest(), ProtocolVersion: protocol.Version(c.handshake.ProtocolVersion), @@ -217,7 +253,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 +270,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 +281,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 +302,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,8 +320,13 @@ 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) +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") + } + srcClosedChan <- struct{}{} } diff --git a/pkg/infrared/infrared_test.go b/pkg/infrared/infrared_test.go index 34f204d..6c98be6 100644 --- a/pkg/infrared/infrared_test.go +++ b/pkg/infrared/infrared_test.go @@ -1,43 +1,137 @@ -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 } -func (c *TestConn) RemoteAddr() net.Addr { +func (c VirtualConn) RemoteAddr() net.Addr { return &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 25565, } } -func TestInfrared_handlePipe_ProxyProtocol(t *testing.T) { - rcIn, rcOut := net.Pipe() - _, cOut := net.Pipe() +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 +} + +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 { + net.Listener + connChan <-chan net.Conn + errChan chan error +} - c := TestConn{Conn: cOut} - rc := TestConn{Conn: rcIn} +func (l *VirtualListener) Accept() (net.Conn, error) { + l.errChan = make(chan error) - srv := New() + select { + case c := <-l.connChan: + return c, nil + case err := <-l.errChan: + return nil, err + } +} + +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) NewConn() VirtualConn { + cIn, cOut := net.Pipe() + vi.connChan <- VirtualConn{Conn: cOut} + return VirtualConn{Conn: cIn} +} + +func (vi *VirtualInfrared) Close() { + vi.vl.Close() +} + +// 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(sendProxyProtocol bool) (*VirtualInfrared, net.Conn) { + vir := ir.New() + + connChan := make(chan net.Conn) + vir.NewListenerFunc = func(addr string) (net.Listener, error) { + return &VirtualListener{ + connChan: connChan, + }, 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 + } go func() { - resp := ServerResponse{ - ServerConn: newConn(&rc), - SendProxyProtocol: true, + if err := vir.ListenAndServe(); errors.Is(err, net.ErrClosed) { + return + } else if err != nil { + panic(err) } - - _ = srv.handlePipe(newConn(&c), resp) }() - r := bufio.NewReader(rcOut) + return &VirtualInfrared{ + vir: vir, + connChan: connChan, + }, rcOut +} + +func TestInfrared_SendProxyProtocol_True(t *testing.T) { + vi, srvOut := NewVirtualInfrared(true) + vc := vi.NewConn() + _ = vc.SendHandshake(handshaking.ServerBoundHandshake{}) + _ = vc.SendLoginStart(login.ServerBoundLoginStart{}, protocol.Version1_20_2) + + r := bufio.NewReader(srvOut) header, err := proxyproto.Read(r) if err != nil { t.Fatalf("Unexpected error reading proxy protocol header: %v", err) @@ -56,25 +150,13 @@ func TestInfrared_handlePipe_ProxyProtocol(t *testing.T) { } } -func TestInfrared_handlePipe_NoProxyProtocol(t *testing.T) { - rcIn, rcOut := net.Pipe() - _, cOut := net.Pipe() - - c := TestConn{Conn: cOut} - rc := TestConn{Conn: rcIn} - - srv := New() - - go func() { - resp := ServerResponse{ - ServerConn: newConn(&rc), - SendProxyProtocol: false, - } - - _ = srv.handlePipe(newConn(&c), resp) - }() +func TestInfrared_SendProxyProtocol_False(t *testing.T) { + vi, srvOut := NewVirtualInfrared(false) + vc := vi.NewConn() + _ = vc.SendHandshake(handshaking.ServerBoundHandshake{}) + _ = vc.SendLoginStart(login.ServerBoundLoginStart{}, protocol.Version1_20_2) - r := bufio.NewReader(rcOut) + r := bufio.NewReader(srvOut) if _, err := proxyproto.Read(r); err == nil { t.Fatal("Expected error reading proxy protocol header, but got nothing") } 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..adba923 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,15 +68,13 @@ 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 { @@ -84,7 +85,7 @@ type ServerRequest struct { } type ServerResponse struct { - ServerConn *Conn + ServerConn *ServerConn StatusResponse protocol.Packet SendProxyProtocol bool } @@ -93,6 +94,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 +107,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) From 7e87d71b16c1fdf2a2dcf14902d0bea40c3c48bb Mon Sep 17 00:00:00 2001 From: haveachin Date: Sat, 3 Feb 2024 21:38:01 +0100 Subject: [PATCH 2/7] 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 +} From 2fecd945d854fcf4fae00b7278ba3d961ef731d1 Mon Sep 17 00:00:00 2001 From: haveachin Date: Sun, 4 Feb 2024 15:50:15 +0100 Subject: [PATCH 3/7] fix: docs --- README.md | 2 +- docs/features/filters.md | 2 +- docs/features/proxy-protocol.md | 6 +++--- docs/features/rate-limiter.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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/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/proxy-protocol.md b/docs/features/proxy-protocol.md index e051b16..2bc29b9 100644 --- a/docs/features/proxy-protocol.md +++ b/docs/features/proxy-protocol.md @@ -1,11 +1,11 @@ # PROXY Protocol -Infrared supportes [PROXY Protocol v2](). +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.md): +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 @@ -28,7 +28,7 @@ proxyProtocol: ## 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): +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. diff --git a/docs/features/rate-limiter.md b/docs/features/rate-limiter.md index 15130e2..0900279 100644 --- a/docs/features/rate-limiter.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: From 9fcca8d87f7b97131480385706c3bd33d5b3357f Mon Sep 17 00:00:00 2001 From: haveachin Date: Mon, 5 Feb 2024 17:06:55 +0100 Subject: [PATCH 4/7] fix: send proxy protocol and tests --- cmd/infrared/main.go | 11 +++++++---- pkg/infrared/infrared.go | 1 + pkg/infrared/infrared_test.go | 18 +++++++++++++++++- pkg/infrared/server.go | 20 +++++++++++++++----- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/cmd/infrared/main.go b/cmd/infrared/main.go index cda1701..bbb8a43 100644 --- a/cmd/infrared/main.go +++ b/cmd/infrared/main.go @@ -120,16 +120,19 @@ func run() error { case sig := <-sigChan: log.Info().Msg("Received " + sig.String()) case err := <-errChan: - if errors.Is(err, ir.ErrNoServers) { + switch { + case errors.Is(err, ir.ErrNoServers): log.Fatal(). Str("docs", "https://infrared.dev/config/proxies"). Msg("No proxy configs found; Check the docs") - } else if errors.Is(err, ir.ErrNoTrustedCIDRs) { + 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") - } else if err != nil { - return err + default: + if err != nil { + return err + } } } diff --git a/pkg/infrared/infrared.go b/pkg/infrared/infrared.go index c5e9c1a..a174854 100644 --- a/pkg/infrared/infrared.go +++ b/pkg/infrared/infrared.go @@ -262,6 +262,7 @@ func (ir *Infrared) handleConn(c *clientConn) error { c.reqDomain = ServerDomain(reqDomain) resp, err := ir.sr.RequestServer(ServerRequest{ + ClientAddr: c.RemoteAddr(), Domain: c.reqDomain, IsLogin: c.handshake.IsLoginRequest(), ProtocolVersion: protocol.Version(c.handshake.ProtocolVersion), diff --git a/pkg/infrared/infrared_test.go b/pkg/infrared/infrared_test.go index fa447d1..6f9afd6 100644 --- a/pkg/infrared/infrared_test.go +++ b/pkg/infrared/infrared_test.go @@ -192,7 +192,7 @@ func TestInfrared_SendProxyProtocol_False(t *testing.T) { func TestInfrared_ReceiveProxyProtocol_True(t *testing.T) { cfg := ir.NewConfig(). WithProxyProtocolReceive(true). - WithProxyProtocolTrustedCIDRs() + WithProxyProtocolTrustedCIDRs("127.0.0.1/32") vi, _ := NewVirtualInfrared(cfg, false) vc := vi.NewConn() @@ -206,3 +206,19 @@ func TestInfrared_ReceiveProxyProtocol_True(t *testing.T) { t.Fatal(err) } } + +func TestInfrared_ReceiveProxyProtocol_False(t *testing.T) { + cfg := ir.NewConfig(). + WithProxyProtocolReceive(false). + WithProxyProtocolTrustedCIDRs("127.0.0.1/32") + + vi, _ := NewVirtualInfrared(cfg, false) + vc := vi.NewConn() + 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") +} diff --git a/pkg/infrared/server.go b/pkg/infrared/server.go index adba923..842884a 100644 --- a/pkg/infrared/server.go +++ b/pkg/infrared/server.go @@ -78,6 +78,7 @@ func (s Server) Dial() (*ServerConn, error) { } type ServerRequest struct { + ClientAddr net.Addr Domain ServerDomain IsLogin bool ProtocolVersion protocol.Version @@ -192,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 } @@ -203,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 { @@ -226,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() @@ -233,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 } @@ -257,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 @@ -273,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 } From 0b5eb1be56d0ff732675a908b9ff94568bf45057 Mon Sep 17 00:00:00 2001 From: haveachin Date: Wed, 7 Feb 2024 01:35:50 +0100 Subject: [PATCH 5/7] test: receive proxy protocol --- pkg/infrared/infrared_test.go | 132 +++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 26 deletions(-) diff --git a/pkg/infrared/infrared_test.go b/pkg/infrared/infrared_test.go index 6f9afd6..0795e40 100644 --- a/pkg/infrared/infrared_test.go +++ b/pkg/infrared/infrared_test.go @@ -15,13 +15,18 @@ import ( type VirtualConn struct { net.Conn + remoteAddr net.Addr } func (c VirtualConn) RemoteAddr() net.Addr { - return &net.TCPAddr{ - IP: net.IPv4(127, 0, 0, 1), - Port: 25565, + if c.remoteAddr == nil { + c.remoteAddr = &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 25565, + } } + + return c.remoteAddr } func (c VirtualConn) SendProxyProtocolHeader() error { @@ -61,14 +66,22 @@ func (c VirtualConn) SendLoginStart(ls login.ServerBoundLoginStart, v protocol.V } type VirtualListener struct { - net.Listener - connChan <-chan net.Conn - errChan chan error + 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: + } + }() + select { case c := <-l.connChan: return c, nil @@ -77,6 +90,10 @@ func (l *VirtualListener) Accept() (net.Conn, error) { } } +func (l *VirtualListener) AcceptTick() <-chan struct{} { + return l.acceptTickerChan +} + func (l *VirtualListener) Close() error { l.errChan <- net.ErrClosed return nil @@ -92,14 +109,33 @@ type VirtualInfrared struct { connChan chan<- net.Conn } -func (vi *VirtualInfrared) NewConn() VirtualConn { +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} + vi.connChan <- VirtualConn{ + Conn: cOut, + remoteAddr: remoteAddr, + } return VirtualConn{Conn: cIn} } func (vi *VirtualInfrared) Close() { - vi.vl.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. @@ -113,10 +149,13 @@ func NewVirtualInfrared( 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 &VirtualListener{ - connChan: connChan, - }, nil + return vl, nil } rcIn, rcOut := net.Pipe() @@ -130,23 +169,18 @@ func NewVirtualInfrared( }), nil } - go func() { - if err := vir.ListenAndServe(); errors.Is(err, net.ErrClosed) { - return - } else if err != nil { - panic(err) - } - }() - return &VirtualInfrared{ vir: vir, + vl: vl, connChan: connChan, }, rcOut } func TestInfrared_SendProxyProtocol_True(t *testing.T) { vi, srvOut := NewVirtualInfrared(ir.NewConfig(), true) - vc := vi.NewConn() + go vi.MustListenAndServe(t) + + vc := vi.NewConn(nil) if err := vc.SendHandshake(handshaking.ServerBoundHandshake{}); err != nil { t.Fatal(err) } @@ -175,7 +209,9 @@ func TestInfrared_SendProxyProtocol_True(t *testing.T) { func TestInfrared_SendProxyProtocol_False(t *testing.T) { vi, srvOut := NewVirtualInfrared(ir.NewConfig(), false) - vc := vi.NewConn() + go vi.MustListenAndServe(t) + + vc := vi.NewConn(nil) if err := vc.SendHandshake(handshaking.ServerBoundHandshake{}); err != nil { t.Fatal(err) } @@ -195,7 +231,9 @@ func TestInfrared_ReceiveProxyProtocol_True(t *testing.T) { WithProxyProtocolTrustedCIDRs("127.0.0.1/32") vi, _ := NewVirtualInfrared(cfg, false) - vc := vi.NewConn() + go vi.MustListenAndServe(t) + + vc := vi.NewConn(nil) if err := vc.SendProxyProtocolHeader(); err != nil { t.Fatal(err) } @@ -209,11 +247,12 @@ func TestInfrared_ReceiveProxyProtocol_True(t *testing.T) { func TestInfrared_ReceiveProxyProtocol_False(t *testing.T) { cfg := ir.NewConfig(). - WithProxyProtocolReceive(false). - WithProxyProtocolTrustedCIDRs("127.0.0.1/32") + WithProxyProtocolReceive(false) vi, _ := NewVirtualInfrared(cfg, false) - vc := vi.NewConn() + go vi.MustListenAndServe(t) + + vc := vi.NewConn(nil) if err := vc.SendProxyProtocolHeader(); err != nil { t.Fatal(err) } @@ -222,3 +261,44 @@ func TestInfrared_ReceiveProxyProtocol_False(t *testing.T) { } 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() { + 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) + } +} + +func TestInfrared_ReceiveProxyProtocol_True_UntrustedIP(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(&net.TCPAddr{ + IP: net.IPv4(12, 34, 56, 78), + Port: 12345, + }) + + if err := vc.SendProxyProtocolHeader(); err == nil { + t.Fatal("no disconnect after untrusted IP") + } +} From a945c9a92e97061e586f5b7067b915c8013b8aa6 Mon Sep 17 00:00:00 2001 From: haveachin Date: Wed, 7 Feb 2024 01:56:39 +0100 Subject: [PATCH 6/7] docs: add community projects page --- docs/.vitepress/config.mts | 1 + docs/community-projects.md | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 docs/community-projects.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 2679ff2..2683a39 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -47,6 +47,7 @@ export default defineConfig({ sidebar: [ { text: 'Getting Started', link: '/getting-started' }, + { text: 'Community Projects', link: '/community-projects' }, { text: 'Config', items: [ diff --git a/docs/community-projects.md b/docs/community-projects.md new file mode 100644 index 0000000..c188089 --- /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 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) From bb48be806f82ddcd31922bf38782c5fddff73aae Mon Sep 17 00:00:00 2001 From: haveachin Date: Wed, 7 Feb 2024 02:01:35 +0100 Subject: [PATCH 7/7] docs: fix typo --- docs/community-projects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/community-projects.md b/docs/community-projects.md index c188089..d4d57d6 100644 --- a/docs/community-projects.md +++ b/docs/community-projects.md @@ -3,7 +3,7 @@ > [!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 respective project. +> Please use their dedicated issue trackers or support channels provided by the respective project. > Thanks for understanding. ## Infrared for Pterodactyl