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