From 58b881c22eb8fecc16f6afa301031b657e659bb1 Mon Sep 17 00:00:00 2001 From: namay26 Date: Tue, 10 Jun 2025 19:31:54 +0530 Subject: [PATCH 1/5] Add initial passthrough implementation --- config/rules.yaml | 3 ++ protocols/protocols.go | 3 ++ protocols/tcp/passthrough.go | 91 ++++++++++++++++++++++++++++++++++++ rules/rules.go | 9 ++-- 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 protocols/tcp/passthrough.go diff --git a/config/rules.yaml b/config/rules.yaml index 6b5bb4c..0b9a653 100644 --- a/config/rules.yaml +++ b/config/rules.yaml @@ -35,6 +35,9 @@ rules: - match: tcp dst port 27017 type: conn_handler target: mongodb + - match: tcp dst port 9889 + type: passthrough + target: passthrough # Will switch to host:ip - match: tcp type: conn_handler target: tcp diff --git a/protocols/protocols.go b/protocols/protocols.go index d43b3ca..f41842e 100644 --- a/protocols/protocols.go +++ b/protocols/protocols.go @@ -72,6 +72,9 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st protocolHandlers["mongodb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { return tcp.HandleMongoDB(ctx, conn, md, log, h) } + protocolHandlers["passthrough"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { + return tcp.HandlePassThrough(ctx, conn, md, log, h) + } protocolHandlers["tcp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { snip, bufConn, err := Peek(conn, 4) if err != nil { diff --git a/protocols/tcp/passthrough.go b/protocols/tcp/passthrough.go new file mode 100644 index 0000000..9e38902 --- /dev/null +++ b/protocols/tcp/passthrough.go @@ -0,0 +1,91 @@ +package tcp + +import ( + "context" + "fmt" + "io" + "log/slog" + "net" + + "github.com/mushorg/glutton/connection" + "github.com/mushorg/glutton/producer" + "github.com/mushorg/glutton/protocols/interfaces" +) + +type parsedPassThrough struct { + Direction string `json:"direction,omitempty"` + Payload []byte `json:"payload,omitempty"` + PayloadHash string `json:"payload_hash,omitempty"` +} + +type passThroughServer struct { + events []parsedPassThrough + target string +} + +// Dial to the source ip, acting as a proxy between the client and real source by piping the data back and forth w/o interfering w it. +func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { + var err error + defer func() { + if err := h.ProduceTCP("passthrough", conn, md, nil, nil); err != nil { + logger.Error("failed to produce passthrough message", producer.ErrAttr(err)) + } + if err := conn.Close(); err != nil { + logger.Error("failed to close incoming connection", slog.String("handler", "passthrough"), producer.ErrAttr(err)) + } + }() + + srcAddr := conn.RemoteAddr().String() + + // Still figuring out on this. + // targetIP := conn.LocalAddr().(*net.TCPAddr).IP.String() + // targetPort := md.TargetPort + // destAddr := fmt.Sprintf("%s:%d", targetIP, targetPort) + + // Hardcoded for now + destAddr := "127.0.0.1:5000" + + fmt.Println("dst", destAddr, " src", srcAddr) + if destAddr == "" { + logger.Error("no target defined", slog.String("handler", "passthrough")) + return nil + } + + targetConn, err := net.Dial("tcp", string(destAddr)) + if err != nil { + logger.Error("failed to connect to the target", slog.String("handler", "passthrough"), slog.String("target", string(destAddr)), producer.ErrAttr(err)) + return nil + } + defer targetConn.Close() + + logger.Info("starting passthrough", slog.String("source", srcAddr), slog.String("target", string(destAddr)), slog.String("handler", "passthrough")) + + errChan := make(chan error, 2) + + // Source to target + go func() { + _, err := io.Copy(targetConn, conn) + errChan <- err + }() + + // Target to source + go func() { + _, err := io.Copy(conn, targetConn) + errChan <- err + }() + + // When either of the error is returned or no more data is left to be sent, the go routines exit. + select { + case err := <-errChan: + if err != nil && err != io.EOF { + logger.Error("transfer error", producer.ErrAttr(err)) + return err + } + case <-ctx.Done(): + logger.Info("context cancelled") + return ctx.Err() + } + + logger.Info("Passthrough completed successfully") + return nil +} diff --git a/rules/rules.go b/rules/rules.go index 0b4d595..13707dd 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -18,6 +18,7 @@ type RuleType int const ( UserConnHandler RuleType = iota Drop + Passthrough ) type Config struct { @@ -32,7 +33,7 @@ type Rule struct { Name string `yaml:"name,omitempty"` isInit bool - ruleType RuleType + RuleType RuleType index int matcher *pcap.BPF } @@ -59,9 +60,11 @@ func (rule *Rule) init(idx int) error { switch rule.Type { case "conn_handler": - rule.ruleType = UserConnHandler + rule.RuleType = UserConnHandler + case "passthrough": + rule.RuleType = Passthrough case "drop": - rule.ruleType = Drop + rule.RuleType = Drop default: return fmt.Errorf("unknown rule type: %s", rule.Type) } From 84d3ba6f2702a234b451ef5e9f22a2dbc471b96b Mon Sep 17 00:00:00 2001 From: namay26 Date: Thu, 26 Jun 2025 13:37:24 +0530 Subject: [PATCH 2/5] Add destination routing logic --- protocols/tcp/passthrough.go | 46 ++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/protocols/tcp/passthrough.go b/protocols/tcp/passthrough.go index 9e38902..a72ef8c 100644 --- a/protocols/tcp/passthrough.go +++ b/protocols/tcp/passthrough.go @@ -37,15 +37,11 @@ func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadat srcAddr := conn.RemoteAddr().String() - // Still figuring out on this. - // targetIP := conn.LocalAddr().(*net.TCPAddr).IP.String() - // targetPort := md.TargetPort - // destAddr := fmt.Sprintf("%s:%d", targetIP, targetPort) + targetIP := conn.LocalAddr() + destAddr := fmt.Sprintf("%s", targetIP) - // Hardcoded for now - destAddr := "127.0.0.1:5000" + fmt.Println("src : ", srcAddr, ", dest : ", destAddr) - fmt.Println("dst", destAddr, " src", srcAddr) if destAddr == "" { logger.Error("no target defined", slog.String("handler", "passthrough")) return nil @@ -64,14 +60,40 @@ func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadat // Source to target go func() { - _, err := io.Copy(targetConn, conn) - errChan <- err + buf := make([]byte, 4096) + for { + n, err := conn.Read(buf) + if err != nil { + errChan <- err + return + } + if n > 0 { + logger.Info("source to target", slog.String("payload", string(buf[:n]))) + if _, err := targetConn.Write(buf[:n]); err != nil { + errChan <- err + return + } + } + } }() - // Target to source go func() { - _, err := io.Copy(conn, targetConn) - errChan <- err + buf := make([]byte, 4096) + for { + n, err := targetConn.Read(buf) + if err != nil { + errChan <- err + return + } + if n > 0 { + logger.Info("target to source", slog.String("payload", string(buf[:n]))) + if _, err := conn.Write(buf[:n]); err != nil { + errChan <- err + return + } + } + + } }() // When either of the error is returned or no more data is left to be sent, the go routines exit. From 50bec0eabb6c1d66ee480736d8cdcfbb74dafbaf Mon Sep 17 00:00:00 2001 From: namay26 Date: Tue, 8 Jul 2025 19:05:27 +0530 Subject: [PATCH 3/5] Add traffic capture config for passthrough and host:port target for dest --- config/config.yaml | 3 +++ config/rules.yaml | 2 +- glutton.go | 23 ++++++++++++----- protocols/protocols.go | 8 +++++- protocols/tcp/passthrough.go | 50 ++++++++++++++++++++++++++++-------- 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index 222deb7..c8c4219 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -24,3 +24,6 @@ producers: conn_timeout: 45 max_tcp_payload: 4096 + +capture_traffic: + enabled: false \ No newline at end of file diff --git a/config/rules.yaml b/config/rules.yaml index 0b9a653..83e598f 100644 --- a/config/rules.yaml +++ b/config/rules.yaml @@ -37,7 +37,7 @@ rules: target: mongodb - match: tcp dst port 9889 type: passthrough - target: passthrough # Will switch to host:ip + target: 127.0.0.1:9889 # Can use hostip:port for the required destination. - match: tcp type: conn_handler target: tcp diff --git a/glutton.go b/glutton.go index c6ef637..619f414 100644 --- a/glutton.go +++ b/glutton.go @@ -222,12 +222,23 @@ func (g *Glutton) tcpListen() { g.Logger.Error("Failed to set connection timeout", producer.ErrAttr(err)) } - if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok { - go func() { - if err := hfunc(g.ctx, conn, md); err != nil { - g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target)) - } - }() + // If hostip:port is used as target, pass on to the passthrough handler. + if host, port, err := net.SplitHostPort(rule.Target); err == nil && host != "" && port != "" { + if hfunc, ok := g.tcpProtocolHandlers["passthrough"]; ok { + go func() { + if err := hfunc(g.ctx, conn, md); err != nil { + g.Logger.Error("Failed to handle TCP passthrough", producer.ErrAttr(err), slog.String("handler", "Passthrough")) + } + }() + } + } else { + if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok { + go func() { + if err := hfunc(g.ctx, conn, md); err != nil { + g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target)) + } + }() + } } } } diff --git a/protocols/protocols.go b/protocols/protocols.go index f41842e..2f0de84 100644 --- a/protocols/protocols.go +++ b/protocols/protocols.go @@ -12,6 +12,7 @@ import ( "github.com/mushorg/glutton/protocols/interfaces" "github.com/mushorg/glutton/protocols/tcp" "github.com/mushorg/glutton/protocols/udp" + "github.com/spf13/viper" ) type TCPHandlerFunc func(ctx context.Context, conn net.Conn, md connection.Metadata) error @@ -72,8 +73,13 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st protocolHandlers["mongodb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { return tcp.HandleMongoDB(ctx, conn, md, log, h) } + var capture bool + if viper.GetBool("capture_traffic.enabled") { + log.Info("Capturing traffic enabled.") + capture = true + } protocolHandlers["passthrough"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { - return tcp.HandlePassThrough(ctx, conn, md, log, h) + return tcp.HandlePassThrough(ctx, conn, md, log, h, capture) } protocolHandlers["tcp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { snip, bufConn, err := Peek(conn, 4) diff --git a/protocols/tcp/passthrough.go b/protocols/tcp/passthrough.go index a72ef8c..8fd1854 100644 --- a/protocols/tcp/passthrough.go +++ b/protocols/tcp/passthrough.go @@ -2,6 +2,7 @@ package tcp import ( "context" + "crypto/sha256" "fmt" "io" "log/slog" @@ -15,19 +16,51 @@ import ( type parsedPassThrough struct { Direction string `json:"direction,omitempty"` Payload []byte `json:"payload,omitempty"` - PayloadHash string `json:"payload_hash,omitempty"` + PayloadHash string `json:"payload_hash,omitempty"` // Used for easier identification, can remove } type passThroughServer struct { events []parsedPassThrough + conn net.Conn target string + source string +} + +func (srv *passThroughServer) recordEvent(dir string, buf []byte, capture bool) { + if !capture { + return + } + hash := sha256.Sum256(buf) + + payload := append([]byte(nil), buf...) // defensive copy + + srv.events = append(srv.events, parsedPassThrough{ + Direction: dir, + Payload: payload, + PayloadHash: fmt.Sprintf("%x", hash[:]), + }) } // Dial to the source ip, acting as a proxy between the client and real source by piping the data back and forth w/o interfering w it. -func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { +func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot, capture bool) error { var err error + + srcAddr := conn.RemoteAddr().String() + destAddr := md.Rule.Target + + server := &passThroughServer{ + events: []parsedPassThrough{}, + conn: conn, + target: destAddr, + source: srcAddr, + } + defer func() { - if err := h.ProduceTCP("passthrough", conn, md, nil, nil); err != nil { + var events []parsedPassThrough + if capture { + events = server.events + } + if err := h.ProduceTCP("passthrough", conn, md, nil, events); err != nil { logger.Error("failed to produce passthrough message", producer.ErrAttr(err)) } if err := conn.Close(); err != nil { @@ -35,19 +68,12 @@ func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadat } }() - srcAddr := conn.RemoteAddr().String() - - targetIP := conn.LocalAddr() - destAddr := fmt.Sprintf("%s", targetIP) - - fmt.Println("src : ", srcAddr, ", dest : ", destAddr) - if destAddr == "" { logger.Error("no target defined", slog.String("handler", "passthrough")) return nil } - targetConn, err := net.Dial("tcp", string(destAddr)) + targetConn, err := net.Dial("tcp", destAddr) if err != nil { logger.Error("failed to connect to the target", slog.String("handler", "passthrough"), slog.String("target", string(destAddr)), producer.ErrAttr(err)) return nil @@ -69,6 +95,7 @@ func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadat } if n > 0 { logger.Info("source to target", slog.String("payload", string(buf[:n]))) + server.recordEvent("source->target", buf[:n], capture) if _, err := targetConn.Write(buf[:n]); err != nil { errChan <- err return @@ -87,6 +114,7 @@ func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadat } if n > 0 { logger.Info("target to source", slog.String("payload", string(buf[:n]))) + server.recordEvent("target->source", buf[:n], capture) if _, err := conn.Write(buf[:n]); err != nil { errChan <- err return From de76bf41efd5842304212347908751b57d995a21 Mon Sep 17 00:00:00 2001 From: namay26 Date: Mon, 4 Aug 2025 00:07:47 +0530 Subject: [PATCH 4/5] Add tests and improve function structure --- glutton.go | 19 ++- protocols/protocols.go | 8 +- protocols/tcp/passthrough.go | 125 ++++++++++----- protocols/tcp/passthrough_test.go | 255 ++++++++++++++++++++++++++++++ 4 files changed, 349 insertions(+), 58 deletions(-) create mode 100644 protocols/tcp/passthrough_test.go diff --git a/glutton.go b/glutton.go index 619f414..a7c3707 100644 --- a/glutton.go +++ b/glutton.go @@ -222,22 +222,21 @@ func (g *Glutton) tcpListen() { g.Logger.Error("Failed to set connection timeout", producer.ErrAttr(err)) } - // If hostip:port is used as target, pass on to the passthrough handler. - if host, port, err := net.SplitHostPort(rule.Target); err == nil && host != "" && port != "" { + if rule.Type == "passthrough" { if hfunc, ok := g.tcpProtocolHandlers["passthrough"]; ok { go func() { if err := hfunc(g.ctx, conn, md); err != nil { g.Logger.Error("Failed to handle TCP passthrough", producer.ErrAttr(err), slog.String("handler", "Passthrough")) } }() - } - } else { - if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok { - go func() { - if err := hfunc(g.ctx, conn, md); err != nil { - g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target)) - } - }() + } else { + if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok { + go func() { + if err := hfunc(g.ctx, conn, md); err != nil { + g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target)) + } + }() + } } } } diff --git a/protocols/protocols.go b/protocols/protocols.go index 2f0de84..f41842e 100644 --- a/protocols/protocols.go +++ b/protocols/protocols.go @@ -12,7 +12,6 @@ import ( "github.com/mushorg/glutton/protocols/interfaces" "github.com/mushorg/glutton/protocols/tcp" "github.com/mushorg/glutton/protocols/udp" - "github.com/spf13/viper" ) type TCPHandlerFunc func(ctx context.Context, conn net.Conn, md connection.Metadata) error @@ -73,13 +72,8 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st protocolHandlers["mongodb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { return tcp.HandleMongoDB(ctx, conn, md, log, h) } - var capture bool - if viper.GetBool("capture_traffic.enabled") { - log.Info("Capturing traffic enabled.") - capture = true - } protocolHandlers["passthrough"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { - return tcp.HandlePassThrough(ctx, conn, md, log, h, capture) + return tcp.HandlePassThrough(ctx, conn, md, log, h) } protocolHandlers["tcp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { snip, bufConn, err := Peek(conn, 4) diff --git a/protocols/tcp/passthrough.go b/protocols/tcp/passthrough.go index 8fd1854..8435e9c 100644 --- a/protocols/tcp/passthrough.go +++ b/protocols/tcp/passthrough.go @@ -3,6 +3,7 @@ package tcp import ( "context" "crypto/sha256" + "encoding/hex" "fmt" "io" "log/slog" @@ -11,6 +12,7 @@ import ( "github.com/mushorg/glutton/connection" "github.com/mushorg/glutton/producer" "github.com/mushorg/glutton/protocols/interfaces" + "github.com/spf13/viper" ) type parsedPassThrough struct { @@ -26,6 +28,44 @@ type passThroughServer struct { source string } +// checks whether the payload can be converted to text, to prevent expensive hex coding. +func (srv *passThroughServer) isLikelyText(data []byte) bool { + if len(data) == 0 { + return false + } + + printable := 0 + for _, b := range data { + if b >= 32 && b <= 126 || b == '\n' || b == '\r' || b == '\t' { + printable++ + } + } + + return (printable*100)/len(data) > 80 // threshold value --> 80% +} + +// logs the payload hex or payload text. +func (srv *passThroughServer) logPayload(direction string, data []byte, logger interfaces.Logger) { + if len(data) == 0 { + return + } + + fields := []any{ + slog.String("direction", direction), + slog.Int("length", len(data)), + slog.String("sha256", fmt.Sprintf("%x", sha256.Sum256(data))), + } + + if srv.isLikelyText(data) { + fields = append(fields, slog.String("payload", string(data))) + } else { + fields = append(fields, slog.String("hex", hex.EncodeToString(data))) + } + + logger.Info("payload_transferred", fields...) +} + +// records the events in the server func (srv *passThroughServer) recordEvent(dir string, buf []byte, capture bool) { if !capture { return @@ -41,8 +81,44 @@ func (srv *passThroughServer) recordEvent(dir string, buf []byte, capture bool) }) } +// pipeBidirectional handles data transfer between the two connections +func pipeBidirectional(ctx context.Context, src, dst net.Conn, server *passThroughServer, logger interfaces.Logger, capture bool, errChan chan error) { + buf := make([]byte, 4096) + direction := getDirection(src, dst) + for { + select { + case <-ctx.Done(): + errChan <- ctx.Err() + return + default: + n, err := src.Read(buf) + if err != nil { + errChan <- err + return + } + + if n > 0 { + server.logPayload(direction, buf[:n], logger) + server.recordEvent(direction, buf[:n], capture) + + if _, err := dst.Write(buf[:n]); err != nil { + errChan <- err + return + } + } + } + } +} + +// getDirection returns the direction as a string +func getDirection(src, dst net.Conn) string { + srcAddr := src.RemoteAddr().String() + dstAddr := dst.RemoteAddr().String() + return fmt.Sprintf("%s -> %s", srcAddr, dstAddr) +} + // Dial to the source ip, acting as a proxy between the client and real source by piping the data back and forth w/o interfering w it. -func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot, capture bool) error { +func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { var err error srcAddr := conn.RemoteAddr().String() @@ -55,6 +131,11 @@ func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadat source: srcAddr, } + var capture bool + if viper.GetBool("capture_traffic.enabled") { + capture = true + } + defer func() { var events []parsedPassThrough if capture { @@ -84,47 +165,9 @@ func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadat errChan := make(chan error, 2) - // Source to target - go func() { - buf := make([]byte, 4096) - for { - n, err := conn.Read(buf) - if err != nil { - errChan <- err - return - } - if n > 0 { - logger.Info("source to target", slog.String("payload", string(buf[:n]))) - server.recordEvent("source->target", buf[:n], capture) - if _, err := targetConn.Write(buf[:n]); err != nil { - errChan <- err - return - } - } - } - }() - - go func() { - buf := make([]byte, 4096) - for { - n, err := targetConn.Read(buf) - if err != nil { - errChan <- err - return - } - if n > 0 { - logger.Info("target to source", slog.String("payload", string(buf[:n]))) - server.recordEvent("target->source", buf[:n], capture) - if _, err := conn.Write(buf[:n]); err != nil { - errChan <- err - return - } - } - - } - }() + go pipeBidirectional(ctx, conn, targetConn, server, logger, capture, errChan) // source to target + go pipeBidirectional(ctx, targetConn, conn, server, logger, capture, errChan) // target to source - // When either of the error is returned or no more data is left to be sent, the go routines exit. select { case err := <-errChan: if err != nil && err != io.EOF { diff --git a/protocols/tcp/passthrough_test.go b/protocols/tcp/passthrough_test.go new file mode 100644 index 0000000..c444b19 --- /dev/null +++ b/protocols/tcp/passthrough_test.go @@ -0,0 +1,255 @@ +package tcp + +import ( + "context" + "crypto/rand" + "io" + "net" + "testing" + + "github.com/mushorg/glutton/protocols/interfaces" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type MockLogger struct { + mock.Mock +} + +func (m *MockLogger) Info(msg string, attrs ...interface{}) { + m.Called(msg, attrs) +} + +func (m *MockLogger) Debug(msg string, attrs ...interface{}) { + m.Called(msg, attrs) +} + +func (m *MockLogger) Error(msg string, attrs ...interface{}) { + m.Called(msg, attrs) +} + +func (m *MockLogger) Warn(msg string, attrs ...interface{}) { + m.Called(msg, attrs) +} + +func TestIsLikelyText(t *testing.T) { + tests := []struct { + name string + input []byte + expected bool + }{ + { + name: "Empty input", + input: []byte(""), + expected: false, + }, + { + name: "Simple ASCII text", + input: []byte("This is plain text"), + expected: true, + }, + { + name: "Text with whitespace", + input: []byte("Text with\nnewlines\tand tabs\r\n"), + expected: true, + }, + { + name: "Binary data", + input: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + expected: false, + }, + { + name: "Mixed content with few non-printable", + input: []byte("Text\x00with\x01binary"), + expected: true, // checking threshold at 85.7% + }, + { + name: "Exactly 80% printable", + input: []byte("AAAA\x01"), // 4/5 = 80% + expected: false, + }, + { + name: "Just below 80% printable", + input: []byte("AAA\x01\x02"), // 3/5 = 60% + expected: false, + }, + } + + srv := &passThroughServer{} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := srv.isLikelyText(test.input) + require.Equal(t, test.expected, result, "unexpected result for test case: %s", test.name) + }) + } +} + +func TestRecording(t *testing.T) { + s := &passThroughServer{} + s.recordEvent("test", []byte("data"), true) + assert.Len(t, s.events, 1) +} + +func TestPipeBidirectional(t *testing.T) { + mockLogger := &MockLogger{} + mockLogger.On("Info", mock.Anything, mock.Anything).Return() + + mockServer := &passThroughServer{ + events: make([]parsedPassThrough, 0), + conn: nil, + target: "test-target:1234", + source: "test-source:5678", + } + + type args struct { + ctx context.Context + src net.Conn + dst net.Conn + server *passThroughServer + logger interfaces.Logger + capture bool + errChan chan error + } + tests := []struct { + name string + args args + setup func() (net.Conn, net.Conn) + wantErr bool + wantErrType error + verify func(t *testing.T, args args) + }{ + { + name: "successful data transfer with capture", + args: args{ + ctx: context.Background(), + server: mockServer, + logger: mockLogger, + capture: true, + errChan: make(chan error, 1), + }, + + setup: func() (net.Conn, net.Conn) { + client, server := net.Pipe() + go func() { + client.Write([]byte("test data")) + client.Close() + }() + return client, server + }, + verify: func(t *testing.T, args args) { + buf := make([]byte, 1024) + n, err := args.dst.Read(buf) + + require.NoError(t, err) + assert.Equal(t, "test data", string(buf[:n])) + + require.True(t, args.capture, "Capture should be enabled") + }, + }, + { + name: "read error from source", + args: args{ + ctx: context.Background(), + server: mockServer, + logger: mockLogger, + capture: false, + errChan: make(chan error, 1), + }, + setup: func() (net.Conn, net.Conn) { + client, server := net.Pipe() + client.Close() + return client, server + }, + wantErr: true, + wantErrType: io.EOF, + }, + { + name: "write error to destination", + args: args{ + ctx: context.Background(), + server: mockServer, + logger: mockLogger, + capture: false, + errChan: make(chan error, 1), + }, + setup: func() (net.Conn, net.Conn) { + client, server := net.Pipe() + server.Close() + return client, server + }, + wantErr: true, + }, + { + name: "zero byte read", + args: args{ + ctx: context.Background(), + server: mockServer, + logger: mockLogger, + capture: true, + errChan: make(chan error, 1), + }, + setup: func() (net.Conn, net.Conn) { + client, server := net.Pipe() + go func() { + client.Write([]byte{}) + client.Close() + }() + return client, server + }, + verify: func(t *testing.T, args args) { + assert.Empty(t, args.server.events) + }, + }, + { + name: "large data transfer", + args: args{ + ctx: context.Background(), + server: mockServer, + logger: mockLogger, + capture: true, + errChan: make(chan error, 1), + }, + setup: func() (net.Conn, net.Conn) { + client, server := net.Pipe() + largeData := make([]byte, 8192) + rand.Read(largeData) + go func() { + client.Write(largeData) + client.Close() + }() + return client, server + }, + verify: func(t *testing.T, args args) { + buf := make([]byte, 8192) + n, err := io.ReadFull(args.dst, buf) + require.NoError(t, err) + assert.Equal(t, 8192, n) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.args.src, tt.args.dst = tt.setup() + defer tt.args.src.Close() + defer tt.args.dst.Close() + } + + go pipeBidirectional( + tt.args.ctx, + tt.args.src, + tt.args.dst, + tt.args.server, + tt.args.logger, + tt.args.capture, + tt.args.errChan, + ) + + if tt.verify != nil { + tt.verify(t, tt.args) + } + }) + } +} From 797bb89575d3748088cedb744c52c35132e891df Mon Sep 17 00:00:00 2001 From: namay26 Date: Wed, 20 Aug 2025 23:21:43 +0530 Subject: [PATCH 5/5] Add io.copy and change to tcp_proxy --- config/rules.yaml | 2 +- glutton.go | 22 +++---- protocols/protocols.go | 2 +- protocols/tcp/passthrough.go | 98 ++++++++++++++++++------------- protocols/tcp/passthrough_test.go | 1 - rules/rules.go | 6 +- 6 files changed, 73 insertions(+), 58 deletions(-) diff --git a/config/rules.yaml b/config/rules.yaml index 83e598f..88ab2c2 100644 --- a/config/rules.yaml +++ b/config/rules.yaml @@ -36,7 +36,7 @@ rules: type: conn_handler target: mongodb - match: tcp dst port 9889 - type: passthrough + type: tcp_proxy target: 127.0.0.1:9889 # Can use hostip:port for the required destination. - match: tcp type: conn_handler diff --git a/glutton.go b/glutton.go index a7c3707..61e58e1 100644 --- a/glutton.go +++ b/glutton.go @@ -222,21 +222,21 @@ func (g *Glutton) tcpListen() { g.Logger.Error("Failed to set connection timeout", producer.ErrAttr(err)) } - if rule.Type == "passthrough" { - if hfunc, ok := g.tcpProtocolHandlers["passthrough"]; ok { + if rule.Type == "tcp_proxy" { + if hfunc, ok := g.tcpProtocolHandlers[rule.Type]; ok { go func() { if err := hfunc(g.ctx, conn, md); err != nil { - g.Logger.Error("Failed to handle TCP passthrough", producer.ErrAttr(err), slog.String("handler", "Passthrough")) + g.Logger.Error("Failed to handle TCP passthrough", producer.ErrAttr(err), slog.String("handler", "tcp_proxy")) + } + }() + } + } else { + if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok { + go func() { + if err := hfunc(g.ctx, conn, md); err != nil { + g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target)) } }() - } else { - if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok { - go func() { - if err := hfunc(g.ctx, conn, md); err != nil { - g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target)) - } - }() - } } } } diff --git a/protocols/protocols.go b/protocols/protocols.go index f41842e..7e8f6ba 100644 --- a/protocols/protocols.go +++ b/protocols/protocols.go @@ -72,7 +72,7 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st protocolHandlers["mongodb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { return tcp.HandleMongoDB(ctx, conn, md, log, h) } - protocolHandlers["passthrough"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { + protocolHandlers["tcp_proxy"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { return tcp.HandlePassThrough(ctx, conn, md, log, h) } protocolHandlers["tcp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { diff --git a/protocols/tcp/passthrough.go b/protocols/tcp/passthrough.go index 8435e9c..232b9a7 100644 --- a/protocols/tcp/passthrough.go +++ b/protocols/tcp/passthrough.go @@ -6,8 +6,10 @@ import ( "encoding/hex" "fmt" "io" + "log" "log/slog" "net" + "time" "github.com/mushorg/glutton/connection" "github.com/mushorg/glutton/producer" @@ -28,6 +30,20 @@ type passThroughServer struct { source string } +type loggingWriter struct { + dst net.Conn + server *passThroughServer + logger interfaces.Logger + capture bool + dir string +} + +func (lw *loggingWriter) Write(p []byte) (int, error) { + lw.server.logPayload(lw.dir, p, lw.logger) + lw.server.recordEvent(lw.dir, p, lw.capture) + return lw.dst.Write(p) +} + // checks whether the payload can be converted to text, to prevent expensive hex coding. func (srv *passThroughServer) isLikelyText(data []byte) bool { if len(data) == 0 { @@ -82,32 +98,24 @@ func (srv *passThroughServer) recordEvent(dir string, buf []byte, capture bool) } // pipeBidirectional handles data transfer between the two connections -func pipeBidirectional(ctx context.Context, src, dst net.Conn, server *passThroughServer, logger interfaces.Logger, capture bool, errChan chan error) { - buf := make([]byte, 4096) +func pipeBidirectional(src, dst net.Conn, server *passThroughServer, logger interfaces.Logger, capture bool, errChan chan error) { direction := getDirection(src, dst) - for { - select { - case <-ctx.Done(): - errChan <- ctx.Err() - return - default: - n, err := src.Read(buf) - if err != nil { - errChan <- err - return - } - - if n > 0 { - server.logPayload(direction, buf[:n], logger) - server.recordEvent(direction, buf[:n], capture) - - if _, err := dst.Write(buf[:n]); err != nil { - errChan <- err - return - } - } - } - } + writer := &loggingWriter{dst: dst, server: server, logger: logger, capture: capture, dir: direction} + + // source to target + go func() { + _, err := io.Copy(writer, src) + errChan <- err + }() + + revDirection := getDirection(dst, src) + revWriter := &loggingWriter{dst: src, server: server, logger: logger, capture: capture, dir: revDirection} + + // target to source + go func() { + _, err := io.Copy(revWriter, dst) + errChan <- err + }() } // getDirection returns the direction as a string @@ -120,10 +128,23 @@ func getDirection(src, dst net.Conn) string { // Dial to the source ip, acting as a proxy between the client and real source by piping the data back and forth w/o interfering w it. func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { var err error + handler := "tcp_proxy" srcAddr := conn.RemoteAddr().String() destAddr := md.Rule.Target + host, _, err := net.SplitHostPort(destAddr) + if err != nil { + logger.Error("invalid address format", producer.ErrAttr(err)) + return nil + } + + if ip := net.ParseIP(host); ip == nil { + if _, err := net.LookupHost(host); err != nil { + return fmt.Errorf("invalid host: %w", err) + } + } + server := &passThroughServer{ events: []parsedPassThrough{}, conn: conn, @@ -145,38 +166,33 @@ func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadat logger.Error("failed to produce passthrough message", producer.ErrAttr(err)) } if err := conn.Close(); err != nil { - logger.Error("failed to close incoming connection", slog.String("handler", "passthrough"), producer.ErrAttr(err)) + logger.Error("failed to close incoming connection", slog.String("handler", handler), producer.ErrAttr(err)) } }() if destAddr == "" { - logger.Error("no target defined", slog.String("handler", "passthrough")) + logger.Error("no target defined", slog.String("handler", handler)) return nil } - targetConn, err := net.Dial("tcp", destAddr) + timeout := 5 * time.Second + + targetConn, err := net.DialTimeout("tcp", destAddr, timeout) if err != nil { - logger.Error("failed to connect to the target", slog.String("handler", "passthrough"), slog.String("target", string(destAddr)), producer.ErrAttr(err)) + logger.Error("failed to connect to the target", slog.String("handler", handler), slog.String("target", string(destAddr)), producer.ErrAttr(err)) return nil } defer targetConn.Close() - logger.Info("starting passthrough", slog.String("source", srcAddr), slog.String("target", string(destAddr)), slog.String("handler", "passthrough")) + logger.Info("starting passthrough", slog.String("source", srcAddr), slog.String("target", string(destAddr)), slog.String("handler", handler)) errChan := make(chan error, 2) - go pipeBidirectional(ctx, conn, targetConn, server, logger, capture, errChan) // source to target - go pipeBidirectional(ctx, targetConn, conn, server, logger, capture, errChan) // target to source + go pipeBidirectional(conn, targetConn, server, logger, capture, errChan) - select { - case err := <-errChan: - if err != nil && err != io.EOF { - logger.Error("transfer error", producer.ErrAttr(err)) - return err - } - case <-ctx.Done(): - logger.Info("context cancelled") - return ctx.Err() + // wait for either side to close + if err := <-errChan; err != nil { + log.Printf("connection closed: %v", err) } logger.Info("Passthrough completed successfully") diff --git a/protocols/tcp/passthrough_test.go b/protocols/tcp/passthrough_test.go index c444b19..4f5569f 100644 --- a/protocols/tcp/passthrough_test.go +++ b/protocols/tcp/passthrough_test.go @@ -238,7 +238,6 @@ func TestPipeBidirectional(t *testing.T) { } go pipeBidirectional( - tt.args.ctx, tt.args.src, tt.args.dst, tt.args.server, diff --git a/rules/rules.go b/rules/rules.go index 13707dd..b28e234 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -18,7 +18,7 @@ type RuleType int const ( UserConnHandler RuleType = iota Drop - Passthrough + Tcp_Proxy ) type Config struct { @@ -61,8 +61,8 @@ func (rule *Rule) init(idx int) error { switch rule.Type { case "conn_handler": rule.RuleType = UserConnHandler - case "passthrough": - rule.RuleType = Passthrough + case "tcp_proxy": + rule.RuleType = Tcp_Proxy case "drop": rule.RuleType = Drop default: