Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ rules:
- match: tcp dst port 27017
type: conn_handler
target: mongodb
- match: tcp dst port 502
type: conn_handler
target: modbus
- match: tcp dst port 9889
type: proxy_tcp
target: 127.0.0.1:9889
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tevino/abool v1.2.0 // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/xiegeo/modbusone v1.0.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/sys v0.33.0 // indirect
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
Expand Down Expand Up @@ -53,10 +55,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
Expand Down Expand Up @@ -114,11 +121,15 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xiegeo/coloredgoroutine v0.1.1/go.mod h1:d3jyamWlthEBXOL5qUpKOaaKSJM75HuCIn/z9f4ylrs=
github.com/xiegeo/modbusone v1.0.1 h1:owliTRNAg9rP8RM62cMZD+iXu7Kzq4EM1i8zcL3//uY=
github.com/xiegeo/modbusone v1.0.1/go.mod h1:Q991qN56vRM6oQftesnNxOVd92fmHvjL0nrYEoAgUs0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand All @@ -137,6 +148,7 @@ golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand All @@ -145,10 +157,13 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
3 changes: 3 additions & 0 deletions protocols/protocols.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st
protocolHandlers["http"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
return tcp.HandleHTTP(ctx, conn, md, log, h)
}
protocolHandlers["modbus"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
return tcp.HandleModbus(ctx, conn, md, log, h)
}
protocolHandlers["proxy_tcp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
return tcp.HandleProxyTCP(ctx, conn, md, log, h)
}
Expand Down
219 changes: 219 additions & 0 deletions protocols/tcp/modbus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
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"

"github.com/xiegeo/modbusone"
)

type parsedModbus struct {
Direction string `json:"direction,omitempty"`
Header mongoMsgHeader `json:"header,omitempty"`
Payload []byte `json:"payload,omitempty"`
OpCodeStr string `json:"opcode_str,omitempty"`
}

type modbusServer struct {
events []parsedModbus
conn net.Conn
logger interfaces.Logger
handler modbusone.ProtocolHandler
}

func (s *modbusServer) read() ([]byte, error) {
data := make([]byte, modbusone.MBAPHeaderLength+modbusone.MaxPDUSize)
n, err := io.ReadFull(s.conn, data[:modbusone.TCPHeaderLength])
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil, io.EOF
}
return nil, err
}

if data[2] != 0 || data[3] != 0 {
return nil, fmt.Errorf("MBAP protocol of %X %X is unknown", data[2], data[3])
}

l := int(data[4])*256 + int(data[5])
if l <= 2 {
return nil, fmt.Errorf("MBAP data length of %v is too short", l)
}
if len(data) < l+modbusone.TCPHeaderLength {
return nil, fmt.Errorf("MBAP data length of %v is too long", l)
}
n, err = io.ReadFull(s.conn, data[modbusone.TCPHeaderLength:l+modbusone.TCPHeaderLength])
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil, io.EOF
}
return nil, err
}

s.events = append(s.events, parsedModbus{
Direction: "read",
Payload: data[:n+modbusone.TCPHeaderLength],
})
return data[:n+modbusone.TCPHeaderLength], nil
}

func (s *modbusServer) write(data []byte) error {
l := len(data) + 1 // PDU + byte of slaveID
bs := make([]byte, modbusone.TCPHeaderLength+l)
bs[4] = byte(l / 256)
bs[5] = byte(l)
copy(bs[modbusone.MBAPHeaderLength:], data)
if _, err := s.conn.Write(bs); err != nil {
return err
}
s.events = append(s.events, parsedModbus{
Direction: "write",
Payload: data,
})
return nil
}

func (s *modbusServer) writeError(req modbusone.PDU, err error) {
if err := s.write(modbusone.ExceptionReplyPacket(req, modbusone.ToExceptionCode(err))); err != nil {
s.logger.Error("Error writing Modbus exception reply", slog.String("protocol", "modbus"), producer.ErrAttr(err))
}

}

const size = 0x10000

var (
discretes [size]bool
coils [size]bool
inputRegisters [size]uint16
holdingRegisters [size]uint16
)

func HandleModbus(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error {
server := &modbusServer{
conn: conn,
logger: logger,
handler: &modbusone.SimpleHandler{
ReadDiscreteInputs: func(address, quantity uint16) ([]bool, error) {
fmt.Printf("ReadDiscreteInputs from %v, quantity %v\n", address, quantity)
return discretes[address : address+quantity], nil
},
WriteDiscreteInputs: func(address uint16, values []bool) error {
fmt.Printf("WriteDiscreteInputs from %v, quantity %v\n", address, len(values))
for i, v := range values {
discretes[address+uint16(i)] = v
}
return nil
},

ReadCoils: func(address, quantity uint16) ([]bool, error) {
fmt.Printf("ReadCoils from %v, quantity %v\n", address, quantity)
return coils[address : address+quantity], nil
},
WriteCoils: func(address uint16, values []bool) error {
fmt.Printf("WriteCoils from %v, quantity %v\n", address, len(values))
for i, v := range values {
coils[address+uint16(i)] = v
}
return nil
},

ReadInputRegisters: func(address, quantity uint16) ([]uint16, error) {
fmt.Printf("ReadInputRegisters from %v, quantity %v\n", address, quantity)
return inputRegisters[address : address+quantity], nil
},
WriteInputRegisters: func(address uint16, values []uint16) error {
fmt.Printf("WriteInputRegisters from %v, quantity %v\n", address, len(values))
for i, v := range values {
inputRegisters[address+uint16(i)] = v
}
return nil
},

ReadHoldingRegisters: func(address, quantity uint16) ([]uint16, error) {
fmt.Printf("ReadHoldingRegisters from %v, quantity %v\n", address, quantity)
return holdingRegisters[address : address+quantity], nil
},
WriteHoldingRegisters: func(address uint16, values []uint16) error {
fmt.Printf("WriteHoldingRegisters from %v, quantity %v\n", address, len(values))
for i, v := range values {
holdingRegisters[address+uint16(i)] = v
}
return nil
},

OnErrorImp: func(req modbusone.PDU, errRep modbusone.PDU) {
fmt.Printf("error received: %v from req: %v\n", errRep, req)
},
},
}

defer func() {
if err := conn.Close(); err != nil {
logger.Error("Failed to close connection", slog.String("protocol", "modbus"), producer.ErrAttr(err))
}
}()

for {
if err := h.UpdateConnectionTimeout(ctx, conn); err != nil {
logger.Debug("Failed to set connection timeout", slog.String("protocol", "modbus"), producer.ErrAttr(err))
return nil
}
data, err := server.read()
if err != nil {
if err != io.EOF {
logger.Error("Error reading from connection", slog.String("protocol", "modbus"), producer.ErrAttr(err))
}
break
}

if len(data) == 0 {
continue
}

p := modbusone.PDU(data[modbusone.MBAPHeaderLength:])
if err := p.ValidateRequest(); err != nil {
return fmt.Errorf("invalid modbus request: %w", err)
}

fc := p.GetFunctionCode()
switch {
case fc.IsReadToServer():
data, err := server.handler.OnRead(p)
if err != nil {
server.writeError(p, err)
continue
}
if err := server.write(p.MakeReadReply(data)); err != nil {
logger.Error("Error writing to connection", slog.String("protocol", "modbus"), producer.ErrAttr(err))
continue
}
case fc.IsWriteToServer():
data, err := p.GetRequestValues()
if err != nil {
server.writeError(p, err)
continue
}
err = server.handler.OnWrite(p, data)
if err != nil {
server.writeError(p, err)
continue
}
if err := server.write(p.MakeWriteReply()); err != nil {
logger.Error("Error writing to connection", slog.String("protocol", "modbus"), producer.ErrAttr(err))
continue
}
default:
logger.Warn("Unsupported Modbus function code", slog.String("protocol", "modbus"), slog.String("function_code", fmt.Sprintf("ExceptionCode:0x%02X", byte(fc))))
}
}

return nil
}
88 changes: 88 additions & 0 deletions protocols/tcp/modbus_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package tcp

import (
"context"
"net"
"testing"

"github.com/mushorg/glutton/connection"
"github.com/mushorg/glutton/protocols/mocks"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/xiegeo/modbusone"
)

type nopLogger struct{}

func (nopLogger) Debug(string, ...any) {}
func (nopLogger) Info(string, ...any) {}
func (nopLogger) Warn(string, ...any) {}
func (nopLogger) Error(string, ...any) {}

func TestModbusServer(t *testing.T) {
ctx := context.Background()

l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer l.Close()

accepted := make(chan net.Conn, 1)
go func() {
conn, err := l.Accept()
if err != nil {
close(accepted)
return
}
accepted <- conn
}()

clientConn, err := net.Dial("tcp", l.Addr().String())
require.NoError(t, err)
defer clientConn.Close()

serverConn, ok := <-accepted
require.True(t, ok, "failed to accept connection")
require.NotNil(t, serverConn)

h := &mocks.MockHoneypot{}
h.EXPECT().UpdateConnectionTimeout(mock.Anything, mock.Anything).Return(nil)

done := make(chan error, 1)
go func() {
done <- HandleModbus(ctx, serverConn, connection.Metadata{}, nopLogger{}, h)
}()

client := modbusone.NewTCPClient(clientConn, 0)
go client.Serve(&modbusone.SimpleHandler{
WriteCoils: func(uint16, []bool) error { return nil },
WriteDiscreteInputs: func(uint16, []bool) error { return nil },
WriteHoldingRegisters: func(uint16, []uint16) error { return nil },
WriteInputRegisters: func(uint16, []uint16) error { return nil },
})
defer client.Close()

tests := []struct {
name string
fc modbusone.FunctionCode
address uint16
quantity uint16
}{
{"ReadCoils", modbusone.FcReadCoils, 0, 1},
{"ReadDiscreteInputs", modbusone.FcReadDiscreteInputs, 0, 1},
{"ReadHoldingRegisters", modbusone.FcReadHoldingRegisters, 0, 1},
{"ReadInputRegisters", modbusone.FcReadInputRegisters, 0, 1},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pdu, err := tt.fc.MakeRequestHeader(tt.address, tt.quantity)
require.NoError(t, err)
require.NoError(t, client.DoTransaction(pdu))
})
}

require.NoError(t, clientConn.Close())
require.NoError(t, <-done)
h.AssertExpectations(t)
}
Loading
Loading