├── .devcontainer └── devcontainer.json ├── .github ├── FUNDING.yml └── workflows │ └── workflow.yml ├── .gitignore ├── .mockery.yaml ├── .readthedocs.yaml ├── Dockerfile ├── Dockerfile.local ├── LICENSE ├── Makefile ├── README.md ├── app └── server.go ├── config ├── config.yaml ├── rules.yaml └── schema.json ├── connection ├── connection.go └── connection_test.go ├── docs ├── configuration.md ├── extension.md ├── faq.md ├── index.md └── setup.md ├── glutton.go ├── glutton_test.go ├── go.mod ├── go.sum ├── iptables.go ├── mkdocs.yml ├── producer ├── logger.go ├── producer.go └── producer_test.go ├── protocols ├── helpers │ └── helpers.go ├── interfaces │ └── interfaces.go ├── mocks │ ├── mock_honeypot.go │ └── mock_logger.go ├── peek.go ├── peek_test.go ├── protocols.go ├── protocols_test.go ├── tcp │ ├── adb.go │ ├── bittorrent.go │ ├── ethereum_rpc.go │ ├── ftp.go │ ├── http.go │ ├── http_test.go │ ├── iscsi.go │ ├── iscsi │ │ ├── iscsi.go │ │ └── iscsi_test.go │ ├── jabber.go │ ├── memcache.go │ ├── mongodb.go │ ├── mongodb_test.go │ ├── mqtt.go │ ├── rdp.go │ ├── rdp │ │ ├── rdp.go │ │ └── rdp_test.go │ ├── resources.go │ ├── resources │ │ └── docker_api.json │ ├── rfb.go │ ├── sip.go │ ├── smb.go │ ├── smb │ │ ├── command_codes.go │ │ ├── smb.go │ │ └── smb_test.go │ ├── smtp.go │ ├── smtp_test.go │ ├── tcp.go │ └── telnet.go └── udp │ └── udp.go ├── rules ├── rules.go ├── rules_test.go └── test.yaml ├── scanner ├── scanner.go └── scanner_test.go ├── server.go ├── server_test.go ├── system.go └── system_test.go /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Glutton Dev Environment", 3 | "dockerFile": "../Dockerfile.local", 4 | "postCreateCommand": "go mod download", 5 | "capAdd": ["NET_ADMIN"], 6 | "customizations": { 7 | "vscode": { 8 | "settings": { 9 | "go.useLanguageServer": true 10 | }, 11 | "extensions": [ 12 | "ms-vscode.go", 13 | "golang.go", 14 | "streetsidesoftware.code-spell-checker" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mushorg,] 4 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | checks: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Dependencies 14 | run: sudo apt install libpcap-dev iptables 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: "^1.21" 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | server 6 | 7 | # Folders 8 | _obj 9 | _test 10 | build 11 | vendor 12 | bin 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | 30 | # Artifacts 31 | glutton.log* 32 | payloads/ 33 | samples/ 34 | 35 | # PoCs 36 | poc/ 37 | 38 | # Dev 39 | .vscode 40 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | with-expecter: true 2 | packages: 3 | github.com/mushorg/glutton/protocols/interfaces: 4 | # place your package-specific config here 5 | config: 6 | all: true 7 | dir: protocols/mocks/ 8 | filename: mock_{{.InterfaceName | lower}}.go 9 | outpkg: mocks 10 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version, and other tools you might need 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.13" 12 | 13 | # Build documentation with Mkdocs 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build container 2 | FROM golang:1.23-alpine AS build-env 3 | RUN set -ex && apk add --no-cache gcc musl-dev git make iptables-dev libpcap-dev 4 | 5 | RUN mkdir -p /opt/glutton 6 | WORKDIR /opt/glutton 7 | 8 | RUN cd $WORKDIR 9 | 10 | ADD go.mod go.sum ./ 11 | RUN go mod download 12 | 13 | ADD . . 14 | 15 | RUN make build 16 | 17 | # run container 18 | FROM alpine:3.21 19 | 20 | RUN apk add iptables iptables-dev libpcap-dev 21 | WORKDIR /opt/glutton 22 | 23 | COPY --from=build-env /opt/glutton/bin/server /opt/glutton/bin/server 24 | COPY --from=build-env /opt/glutton/config /opt/glutton/config 25 | COPY --from=build-env /opt/glutton/rules /opt/glutton/rules 26 | 27 | CMD ["./bin/server", "-i", "eth0", "-l", "/var/log/glutton.log", "-d", "true"] 28 | -------------------------------------------------------------------------------- /Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-alpine 2 | 3 | RUN apk update 4 | RUN apk add -q --update --progress --no-cache g++ git make iptables iptables-dev libpcap-dev 5 | 6 | RUN go install golang.org/x/tools/gopls@latest 2>&1 7 | RUN go install -v github.com/go-delve/delve/cmd/dlv@latest 2>&1 8 | 9 | RUN mkdir -p /opt/glutton 10 | WORKDIR /opt/glutton 11 | 12 | RUN cd $WORKDIR 13 | 14 | ADD go.mod go.sum ./ 15 | RUN go mod download 16 | 17 | ADD . . 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 MushMush 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := v1.0.1 2 | NAME := glutton 3 | BUILDSTRING := $(shell git log --pretty=format:'%h' -n 1) 4 | VERSIONSTRING := $(NAME) version $(VERSION)+$(BUILDSTRING) 5 | BUILDDATE := $(shell date -u -Iseconds) 6 | 7 | LDFLAGS := "-X \"main.VERSION=$(VERSIONSTRING)\" -X \"main.BUILDDATE=$(BUILDDATE)\"" 8 | 9 | .PHONY: all test clean build 10 | 11 | .PHONY: tag 12 | tag: 13 | git tag $(VERSION) 14 | git push origin --tags 15 | 16 | .PHONY: upx 17 | upx: 18 | cd bin; find . -type f -exec upx "{}" \; 19 | 20 | default: build 21 | 22 | build: 23 | go build -ldflags=$(LDFLAGS) -o bin/server app/server.go 24 | 25 | static: 26 | go build --ldflags '-extldflags "-static"' -o bin/server app/server.go 27 | upx -1 bin/server 28 | 29 | clean: 30 | rm -rf bin/ 31 | 32 | run: build 33 | sudo bin/server 34 | docker: 35 | docker build -t glutton . 36 | docker run --rm --cap-add=NET_ADMIN -it glutton 37 | 38 | test: 39 | go test -v ./... 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glutton 2 | ![Tests](https://github.com/mushorg/glutton/actions/workflows/workflow.yml/badge.svg) 3 | [![GoDoc](https://godoc.org/github.com/mushorg/glutton?status.svg)](https://godoc.org/github.com/mushorg/glutton) 4 | 5 | Glutton is a protocol-agnostic, low-interaction honeypot that intercepts network traffic and logs interactions to help analyze malicious activities. It's built using Golang and leverages iptables and TPROXY to redirect all traffic to specific protocol handlers. 6 | 7 | ## Documentation 8 | For more details, please read the [documentation](https://go-glutton.readthedocs.io/en/latest/), which provides the following sections: 9 | 10 | * [Introduction](https://go-glutton.readthedocs.io/en/latest/) 11 | * [Setup](https://go-glutton.readthedocs.io/en/latest/setup/) 12 | * [Configuration](https://go-glutton.readthedocs.io/en/latest/configuration/) 13 | * [Extension](https://go-glutton.readthedocs.io/en/latest/extension/) 14 | * [FAQs](https://go-glutton.readthedocs.io/en/latest/faq/) -------------------------------------------------------------------------------- /app/server.go: -------------------------------------------------------------------------------- 1 | package main // import "github.com/mushorg/glutton/app" 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "runtime/debug" 10 | "syscall" 11 | 12 | "github.com/mushorg/glutton" 13 | 14 | "github.com/spf13/pflag" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | var ( 19 | // VERSION is set by the makefile 20 | VERSION = "v0.0.0" 21 | // BUILDDATE is set by the makefile 22 | BUILDDATE = "" 23 | ) 24 | 25 | func main() { 26 | fmt.Println(` 27 | _____ _ _ _ 28 | / ____| | | | | | 29 | | | __| |_ _| |_| |_ ___ _ __ 30 | | | |_ | | | | | __| __/ _ \| '_ \ 31 | | |__| | | |_| | |_| || (_) | | | | 32 | \_____|_|\__,_|\__|\__\___/|_| |_| 33 | 34 | `) 35 | fmt.Printf("%s %s\n\n", VERSION, BUILDDATE) 36 | 37 | pflag.StringP("interface", "i", "eth0", "Bind to this interface") 38 | pflag.IntP("ssh", "s", 22, "Override SSH port") 39 | pflag.StringP("logpath", "l", "/dev/null", "Log file path") 40 | pflag.StringP("confpath", "c", "config/", "Configuration file path") 41 | pflag.BoolP("debug", "d", false, "Enable debug mode") 42 | pflag.Bool("version", false, "Print version") 43 | pflag.String("var-dir", "/var/lib/glutton", "Set var-dir") 44 | 45 | pflag.Parse() 46 | viper.BindPFlags(pflag.CommandLine) 47 | 48 | if viper.IsSet("ssh") { 49 | viper.Set("ports.ssh", viper.GetInt("ssh")) 50 | } 51 | 52 | if viper.GetBool("version") { 53 | return 54 | } 55 | 56 | g, err := glutton.New(context.Background()) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | if err := g.Init(); err != nil { 62 | log.Fatal("Failed to initialize Glutton:", err) 63 | } 64 | 65 | exit := func() { 66 | // See if there was a panic... 67 | if r := recover(); r != nil { 68 | fmt.Fprintln(os.Stderr, r) 69 | fmt.Println("stacktrace from panic: \n" + string(debug.Stack())) 70 | } 71 | g.Shutdown() 72 | } 73 | 74 | // capture and handle signals 75 | sig := make(chan os.Signal, 1) 76 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 77 | go func() { 78 | <-sig 79 | fmt.Print("\r") 80 | exit() 81 | fmt.Println() 82 | os.Exit(0) 83 | }() 84 | 85 | if err := g.Start(); err != nil { 86 | exit() 87 | log.Fatal("Failed to start Glutton server:", err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | ports: 2 | tcp: 5000 3 | udp: 5001 4 | ssh: 2222 5 | 6 | rules_path: config/rules.yaml 7 | 8 | addresses: ["1.2.3.4", "5.4.3.2"] 9 | 10 | interface: eth0 11 | 12 | producers: 13 | enabled: false 14 | http: 15 | enabled: false 16 | remote: https://localhost:9000 17 | hpfeeds: 18 | enabled: false 19 | host: 172.26.0.2 20 | port: 20000 21 | ident: ident 22 | auth: auth 23 | channel: test 24 | 25 | conn_timeout: 45 26 | max_tcp_payload: 4096 27 | -------------------------------------------------------------------------------- /config/rules.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - match: tcp dst port 23 or port 2323 or port 23231 3 | type: conn_handler 4 | target: telnet 5 | - match: tcp dst port 1883 6 | type: conn_handler 7 | target: mqtt 8 | - match: tcp dst port 6969 9 | type: conn_handler 10 | target: bittorrent 11 | - match: tcp dst port 25 12 | type: conn_handler 13 | target: smtp 14 | - match: tcp dst port 3389 15 | type: conn_handler 16 | target: rdp 17 | - match: tcp dst port 445 18 | type: conn_handler 19 | target: smb 20 | - match: tcp dst port 21 21 | type: conn_handler 22 | target: ftp 23 | - match: tcp dst port 5060 24 | type: conn_handler 25 | target: sip 26 | - match: tcp dst port 5222 or port 5223 27 | type: conn_handler 28 | target: jabber 29 | - match: tcp dst port 11211 30 | type: conn_handler 31 | target: memcache 32 | - match: tcp dst port 3260 33 | type: conn_handler 34 | target: iscsi 35 | - match: tcp dst port 27017 36 | type: conn_handler 37 | target: mongodb 38 | - match: tcp 39 | type: conn_handler 40 | target: tcp 41 | - match: udp 42 | type: conn_handler 43 | target: udp 44 | -------------------------------------------------------------------------------- /config/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$ref": "#/definitions/Glutton", 4 | "definitions": { 5 | "Glutton": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "rules": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/Rule" 13 | } 14 | } 15 | }, 16 | "required": [ 17 | "rules" 18 | ], 19 | "title": "Glutton" 20 | }, 21 | "Rule": { 22 | "type": "object", 23 | "additionalProperties": false, 24 | "properties": { 25 | "match": { 26 | "type": "string" 27 | }, 28 | "type": { 29 | "$ref": "#/definitions/Type" 30 | }, 31 | "name": { 32 | "type": "string" 33 | }, 34 | "target": { 35 | "type": "string" 36 | } 37 | }, 38 | "required": [ 39 | "match", 40 | "type" 41 | ], 42 | "title": "Rule" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /connection/connection.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strconv" 9 | "sync" 10 | "time" 11 | 12 | "github.com/mushorg/glutton/rules" 13 | 14 | "github.com/google/gopacket" 15 | "github.com/google/gopacket/layers" 16 | ) 17 | 18 | type CKey [2]uint64 19 | 20 | func newConnKey(clientAddr gopacket.Endpoint, clientPort gopacket.Endpoint) (CKey, error) { 21 | if clientAddr.EndpointType() != layers.EndpointIPv4 { 22 | return CKey{}, errors.New("clientAddr endpoint must be of type layers.EndpointIPv4") 23 | } 24 | 25 | if clientPort.EndpointType() != layers.EndpointTCPPort { 26 | return CKey{}, errors.New("clientPort endpoint must be of type layers.EndpointTCPPort") 27 | } 28 | 29 | return CKey{clientAddr.FastHash(), clientPort.FastHash()}, nil 30 | } 31 | 32 | func NewConnKeyByString(host, port string) (CKey, error) { 33 | clientAddr := layers.NewIPEndpoint(net.ParseIP(host).To4()) 34 | p, err := strconv.Atoi(port) 35 | if err != nil { 36 | return CKey{}, err 37 | } 38 | clientPort := layers.NewTCPPortEndpoint(layers.TCPPort(p)) 39 | return newConnKey(clientAddr, clientPort) 40 | } 41 | 42 | func NewConnKeyFromNetConn(conn net.Conn) (CKey, error) { 43 | host, port, _ := net.SplitHostPort(conn.RemoteAddr().String()) 44 | return NewConnKeyByString(host, port) 45 | } 46 | 47 | type Metadata struct { 48 | Added time.Time 49 | Rule *rules.Rule 50 | TargetPort uint16 51 | //TargetIP net.IP 52 | } 53 | 54 | type ConnTable struct { 55 | table map[CKey]Metadata 56 | mtx sync.RWMutex 57 | } 58 | 59 | func New(ctx context.Context) *ConnTable { 60 | ct := &ConnTable{ 61 | table: make(map[CKey]Metadata, 1024), 62 | } 63 | // every 5 minutes using a ticker, flush the table 64 | 65 | go func() { 66 | ticker := time.NewTicker(5 * time.Minute) 67 | defer ticker.Stop() 68 | for { 69 | select { 70 | case <-ctx.Done(): 71 | return 72 | case <-ticker.C: 73 | ct.FlushOlderThan(5 * time.Minute) 74 | } 75 | } 76 | }() 77 | 78 | return ct 79 | } 80 | 81 | // RegisterConn a connection in the table 82 | func (t *ConnTable) RegisterConn(conn net.Conn, rule *rules.Rule) (Metadata, error) { 83 | srcIP, srcPort, err := net.SplitHostPort(conn.RemoteAddr().String()) 84 | if err != nil { 85 | return Metadata{}, fmt.Errorf("failed to split remote address: %w", err) 86 | } 87 | 88 | _, dstPort, err := net.SplitHostPort(conn.LocalAddr().String()) 89 | if err != nil { 90 | return Metadata{}, fmt.Errorf("failed to split local address: %w", err) 91 | } 92 | port, err := strconv.Atoi(dstPort) 93 | if err != nil { 94 | return Metadata{}, fmt.Errorf("failed to parse dstPort: %w", err) 95 | } 96 | return t.Register(srcIP, srcPort, uint16(port), rule) 97 | } 98 | 99 | // Register a connection in the table 100 | func (t *ConnTable) Register(srcIP, srcPort string, dstPort uint16, rule *rules.Rule) (Metadata, error) { 101 | t.mtx.Lock() 102 | defer t.mtx.Unlock() 103 | 104 | ck, err := NewConnKeyByString(srcIP, srcPort) 105 | if err != nil { 106 | return Metadata{}, err 107 | } 108 | if md, ok := t.table[ck]; ok { 109 | return md, nil 110 | } 111 | 112 | md := Metadata{ 113 | Added: time.Now(), 114 | TargetPort: dstPort, 115 | Rule: rule, 116 | } 117 | t.table[ck] = md 118 | return md, nil 119 | } 120 | 121 | func (t *ConnTable) FlushOlderThan(s time.Duration) { 122 | t.mtx.Lock() 123 | defer t.mtx.Unlock() 124 | 125 | threshold := time.Now().Add(-1 * s) 126 | 127 | for ck, md := range t.table { 128 | if md.Added.Before(threshold) { 129 | delete(t.table, ck) 130 | } 131 | } 132 | } 133 | 134 | // TODO: what happens when I return a *Metadata and then FlushOlderThan() deletes it? 135 | func (t *ConnTable) Get(ck CKey) Metadata { 136 | t.mtx.RLock() 137 | defer t.mtx.RUnlock() 138 | return t.table[ck] 139 | } 140 | -------------------------------------------------------------------------------- /connection/connection_test.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/mushorg/glutton/rules" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var testck = CKey([2]uint64{9580489724559085892, 13327978790310486453}) 14 | 15 | func TestNewConnKeyByString(t *testing.T) { 16 | ck, err := NewConnKeyByString("127.0.0.1", "1234") 17 | require.NoError(t, err) 18 | require.Equal(t, testck, ck) 19 | } 20 | 21 | func TestNewConnKeyFromNetConn(t *testing.T) { 22 | ln, err := net.Listen("tcp4", "127.0.0.1:1234") 23 | require.NoError(t, err) 24 | require.NotNil(t, ln) 25 | defer ln.Close() 26 | conn, err := net.Dial(ln.Addr().Network(), ln.Addr().String()) 27 | require.NoError(t, err) 28 | require.NotNil(t, conn) 29 | defer conn.Close() 30 | ck, err := NewConnKeyFromNetConn(conn) 31 | require.NoError(t, err) 32 | require.Equal(t, testck, ck) 33 | } 34 | 35 | func TestNewConnTable(t *testing.T) { 36 | table := New(context.Background()) 37 | require.NotNil(t, table) 38 | } 39 | 40 | func TestRegister(t *testing.T) { 41 | table := New(context.Background()) 42 | targetPort := 4321 43 | m1, err := table.Register("127.0.0.1", "1234", uint16(targetPort), &rules.Rule{}) 44 | require.NoError(t, err) 45 | require.NotNil(t, m1) 46 | m2 := table.Get(testck) 47 | require.NotNil(t, m1) 48 | require.Equal(t, targetPort, int(m2.TargetPort)) 49 | require.Equal(t, m1, m2) 50 | } 51 | 52 | func TestRegisterConn(t *testing.T) { 53 | ln, err := net.Listen("tcp4", "127.0.0.1:1234") 54 | require.NoError(t, err) 55 | require.NotNil(t, ln) 56 | defer ln.Close() 57 | conn, err := net.Dial(ln.Addr().Network(), ln.Addr().String()) 58 | require.NoError(t, err) 59 | require.NotNil(t, conn) 60 | defer conn.Close() 61 | table := New(context.Background()) 62 | md, err := table.RegisterConn(conn, &rules.Rule{Target: "tcp"}) 63 | require.NoError(t, err) 64 | require.NotNil(t, md) 65 | m := table.Get(testck) 66 | require.NotNil(t, m) 67 | require.Equal(t, "tcp", m.Rule.Target) 68 | } 69 | 70 | func TestFlushOlderThan(t *testing.T) { 71 | table := New(context.Background()) 72 | targetPort := 4321 73 | md, err := table.Register("127.0.0.1", "1234", uint16(targetPort), &rules.Rule{}) 74 | require.NoError(t, err) 75 | require.NotNil(t, md) 76 | table.FlushOlderThan(time.Duration(0)) 77 | m := table.Get(testck) 78 | require.Empty(t, m) 79 | } 80 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Glutton’s behavior is controlled by several configuration files written in YAML (and JSON for schema validation). This page details the available configuration options, how they’re loaded, and best practices for customizing your setup. 4 | 5 | ## Configuration Files 6 | 7 | ### config/config.yaml 8 | 9 | This file holds the core settings for Glutton. Key configuration options include: 10 | 11 | - **ports:** Defines the network ports used for traffic interception. 12 | - **tcp:** The TCP port for intercepted connections (default: `5000`). 13 | - **udp:** The UDP port for intercepted packets (default: `5001`). 14 | - **ssh:** Typically excluded from redirection to avoid interfering with SSH (default: `22`). 15 | - **interface:** The network interface Glutton listens on (default: `eth0`). 16 | - **max_tcp_payload:** Maximum TCP payload size in bytes (default: `4096`). 17 | - **conn_timeout:** The connection timeout duration in seconds (default: `45`). 18 | - **confpath:** The directory path where the configuration file resides. 19 | - **producers:** 20 | - **enabled**: Boolean flag to enable or disable logging/producer functionality. 21 | - **http:** HTTP producer for sending logs to a remote endpoint, like [Ochi](https://github.com/honeynet/ochi). 22 | - **hpfeeds:** [HPFeeds](https://github.com/hpfeeds/hpfeeds) producer for sharing data with other security tools. 23 | - **addresses:** A list of additional public IP addresses for traffic handling. 24 | 25 | Example configuration: 26 | 27 | ```yaml 28 | # config/config.yaml 29 | 30 | ports: 31 | tcp: 5000 32 | udp: 5001 33 | ssh: 22 34 | 35 | rules_path: config/rules.yaml 36 | 37 | addresses: ["1.2.3.4"] 38 | 39 | interface: eth0 40 | 41 | producers: 42 | enabled: true # enables producers 43 | http: 44 | enabled: true # enables http producer 45 | # Connect with Ochi here or other remote log aggregation servers 46 | remote: http://localhost:3000/publish?token=token 47 | hpfeeds: 48 | enabled: false # disables HPFeeds 49 | host: 172.26.0.2 50 | port: 20000 51 | # HPFeeds specific details go here 52 | ident: ident 53 | auth: auth 54 | channel: test 55 | 56 | conn_timeout: 45 57 | max_tcp_payload: 4096 58 | ``` 59 | 60 | ### config/rules.yaml 61 | 62 | This file defines the rules that Glutton uses to determine which protocol handler should process incoming traffic. 63 | 64 | Key elements include: 65 | 66 | - **type**: `conn_handler` to pass off to the appropriate protocol handler or `drop` to ignore packets. 67 | - **target**: Indicates the protocol handler (e.g., "http", "ftp") to be used. 68 | - **match**: Define criteria such as source IP ranges or destination ports to match incoming traffic, according to [BPF syntax](https://biot.com/capstats/bpf.html). 69 | 70 | Example rule: 71 | 72 | ```yaml 73 | # config/rules.yaml 74 | 75 | rules: 76 | - name: Telnet filter 77 | match: tcp dst port 23 or port 2323 or port 23231 78 | type: conn_handler # will find the appropriate target protocol handler 79 | target: telnet 80 | - match: tcp dst port 6969 81 | type: drop # drops any matching packets 82 | target: bittorrent 83 | ``` 84 | 85 | ## Configuration Loading Process 86 | Glutton uses the [Viper](https://github.com/spf13/viper) library to load configuration settings. The process works as follows: 87 | 88 | - **Default Settings**: Glutton initializes with default values for critical parameters. 89 | - **File-based Overrides**: Viper looks for `config.yaml` in the directory specified by confpath. If found, the settings from the file override the defaults. 90 | - **Additional Sources**: Environment variables or command-line flags can further override file-based configurations, allowing for flexible deployments. 91 | 92 | ## Best Practices 93 | 94 | - **Backup Your Files**: Always save a backup of your configuration files before making changes. 95 | - **Validate Configurations**: Use YAML validators and the provided JSON schema to ensure your configuration is error-free. 96 | - **Test Changes**: After modifying your configuration, restart Glutton and review the logs to confirm that your changes have been applied as expected. 97 | -------------------------------------------------------------------------------- /docs/extension.md: -------------------------------------------------------------------------------- 1 | # Extension 2 | 3 | Glutton is built to be easily extensible. Developers can add new protocol handlers or modify existing behavior to suit custom requirements. 4 | 5 | ## Adding a New Protocol Handler 6 | 7 | ### Create a New Module 8 | 9 | - Add your new protocol handler in the appropriate subdirectory under `protocols/` (e.g., `protocols/tcp` or `protocols/udp`). 10 | - Implement the handler function conforming to the expected signature: 11 | - For TCP: `func(context.Context, net.Conn, connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error` 12 | - For UDP: `func(context.Context, *net.UDPAddr, *net.UDPAddr, []byte, connection.Metadata) error` 13 | 14 | For example: 15 | 16 | 17 | // protocols/tcp/new_protocol.go 18 | 19 | package tcp 20 | 21 | import ( 22 | "context" 23 | "net" 24 | 25 | "github.com/mushorg/glutton/connection" 26 | "github.com/mushorg/glutton/protocols/interfaces" 27 | ) 28 | 29 | // HandleNewProtocol handles incoming connections. 30 | func HandleNewProtocol(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 31 | // Log the connection for demonstration purposes. 32 | logger.Info("Received NewProtocol connection from %s", conn.RemoteAddr().String()) 33 | // Here you could add protocol-specific handling logic. 34 | // For now, simply close the connection. 35 | return conn.Close() 36 | } 37 | 38 | 39 | ### Register the Handler 40 | - Modify the mapping function (e.g., `protocols.MapTCPProtocolHandlers` or `protocols.MapUDPProtocolHandlers` in `protocols/protocols.go`) to include your new handler. 41 | - Update configuration or rules (in `config/rules.yaml` or `rules/rules.yaml`) if needed to route specific traffic to your handler. 42 | 43 | For example: 44 | 45 | func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[string]TCPHandlerFunc { 46 | protocolHandlers := map[string]TCPHandlerFunc{} 47 | protocolHandlers["smtp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 48 | return tcp.HandleSMTP(ctx, conn, md, log, h) 49 | } 50 | ... 51 | protocolHandlers["new"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 52 | return tcp.HandleNewProtocol(ctx, conn, md, log, h) 53 | } 54 | ... 55 | } 56 | 57 | 3. **Test Your Extension:** 58 | - Write tests similar to those in `protocols/protocols_test.go` to verify your new handler’s functionality. 59 | - Use `go test` to ensure that your changes do not break existing functionality. 60 | 61 | ## Customizing Logging and Rules 62 | 63 | - **Logging:** The logging mechanism is provided by the Producer (located in `producer/`). You can modify or extend it to suit your logging infrastructure. 64 | - **Rules Engine:** The rules engine (found in `rules/`) can be extended to support additional matching criteria or custom rule types. 65 | 66 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ### Q: What is Glutton? 4 | **A:** Glutton is a protocol-agnostic honeypot that intercepts network traffic, applies customizable rules, and logs interactions to help analyze malicious activities. 5 | 6 | ### Q: Which protocols does Glutton support? 7 | **A:** Out of the box, Glutton supports multiple protocols via its TCP and UDP handlers. The repository includes handlers for protocols such as HTTP, FTP, SMTP, Telnet, MQTT, and more. You can extend support to additional protocols as needed. 8 | 9 | ### Q: How do I add a new protocol or extend existing functionality? 10 | **A:** You can add a new protocol handler by implementing the appropriate interface in the `protocols/` directory and registering your handler in the corresponding mapping function. See the [Extension](extension.md) section for detailed instructions. 11 | 12 | ### Q: How do I configure Glutton? 13 | **A:** Configuration is managed through YAML files: 14 | 15 | - **config/config.yaml:** General settings such as port numbers and interface names. 16 | - **config/rules.yaml:** Defines rules for matching and processing network traffic. 17 | 18 | See the [Configuration](configuration.md) section for detailed instructions. 19 | 20 | ### Q: What are the system prerequisites? 21 | **A:** Glutton requires a Linux system, so if you're using a different OS, you'll have to use Docker to set it up. Specific installation commands are provided in the [Setup](setup.md) section. 22 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Glutton is a protocol-agnostic honeypot designed to emulate various network services and protocols. It is built in Go and aims to capture and analyze malicious network activity by intercepting connections and logging detailed interaction data. Glutton is highly configurable, allowing users to define custom rules and extend its functionality by adding new protocol handlers. 4 | 5 | Key features include: 6 | 7 | - **Protocol Agnostic:** Supports TCP and UDP traffic across a variety of protocols. 8 | - **Rule-Based Processing:** Uses configurable rules to determine how incoming connections are handled. 9 | - **Extensibility:** Easily extend the tool by adding new protocol handlers. 10 | - **Logging and Analysis:** Captures connection metadata and payloads for further analysis. 11 | 12 | This documentation will guide you through the installation, deployment, understanding its network interactions, extending its functionality, and addressing common questions. 13 | 14 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Follow these steps to install Glutton on your system. 4 | 5 | ## Environment Requirements 6 | 7 | 8 | - **Linux Required:** Glutton must be built and run on a Linux system. 9 | - **Non-Linux Users:** For Windows or macOS, use Docker or the VSCode Dev Container Extension. 10 | - **WSL Users:** When using WOS, we recommend running glutton with the [xanmod-kernel-WSL2](https://github.com/Locietta/xanmod-kernel-WSL2) 11 | - For setting up the development environment using VS Code Dev Containers, refer to: 12 | - [Install Dev Container](https://code.visualstudio.com/docs/devcontainers/containers) 13 | - [Learn More](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) 14 | 15 | ## Prerequisites 16 | 17 | Ensure you have [Go](https://go.dev/dl/) installed (recommended version: **Go 1.21** or later). In addition, you will need system packages for building and running Glutton: 18 | 19 | ### Debian/Ubuntu 20 | 21 | ```bash 22 | sudo apt-get update 23 | sudo apt-get install gcc libpcap-dev iptables 24 | ``` 25 | 26 | ### Arch Linux 27 | ```bash 28 | sudo pacman -S gcc libpcap iptables 29 | ``` 30 | 31 | ### Fedora 32 | ```bash 33 | sudo dnf install gcc libpcap-devel iptables 34 | ``` 35 | 36 | ## Building Glutton 37 | 38 | Clone the repository and build the project: 39 | 40 | ```bash 41 | git clone https://github.com/mushorg/glutton.git 42 | cd glutton 43 | make build 44 | ``` 45 | 46 | This will compile the project and place the server binary in the `bin/` directory. 47 | 48 | ## Testing the Installation 49 | 50 | ```bash 51 | bin/server --version 52 | ``` 53 | You should see output similar to: 54 | 55 | ```bash 56 | _____ _ _ _ 57 | / ____| | | | | | 58 | | | __| |_ _| |_| |_ ___ _ __ 59 | | | |_ | | | | | __| __/ _ \| '_ \ 60 | | |__| | | |_| | |_| || (_) | | | | 61 | \_____|_|\__,_|\__|\__\___/|_| |_| 62 | 63 | 64 | glutton version v1.0.1+d2503ba 2025-02-21T05:48:07+00:00 65 | ``` 66 | 67 | ## Usage 68 | 69 | Glutton can be configured using several command-line flags: 70 | 71 | - **--interface, -i**: `string` - Specifies the network interface (default: `eth0`) 72 | - **--ssh, -s**: `int` - If set, it overrides the default SSH port 73 | - **--logpath, -l**: `string` - Sets the file path for logging (default: `/dev/null`) 74 | - **--confpath, -c**: `string` - Defines the path to the configuration directory (default: `config/`) 75 | - **--debug, -d**: `bool` - Enables debug mode (default: `false`) 76 | - **--version**: `bool` - Prints the version and exits 77 | - **--var-dir**: `string` - Sets the directory for variable data storage (default: `/var/lib/glutton`) 78 | 79 | For example, to run Glutton with a custom interface and enable debug mode, you might use the following command: 80 | 81 | ```bash 82 | bin/server --interface --debug 83 | ``` 84 | 85 | Replace `` (e.g., `eth0`) with the interface you want to monitor. The command starts the Glutton server, which sets up TCP/UDP listeners and applies iptables rules for transparent proxying. 86 | 87 | **Configuration:** Before deployment, ensure your configuration files, in the `config/` folder by default, are properly set up. For detailed instructions, refer to the [Configuration](configuration.md) page. 88 | 89 | ## Docker 90 | 91 | To deploy using Docker: 92 | 93 | 1. Build the Docker image: 94 | 95 | ``` 96 | docker build -t glutton . 97 | ``` 98 | 99 | 2. Run the Container: 100 | 101 | ``` 102 | docker run --rm --cap-add=NET_ADMIN -it glutton 103 | ``` 104 | 105 | The Docker container is preconfigured with the necessary dependencies (iptables, libpcap, etc.) and copies the configuration and rules files into the container. 106 | 107 | -------------------------------------------------------------------------------- /glutton.go: -------------------------------------------------------------------------------- 1 | package glutton 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net" 11 | "os" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | "github.com/mushorg/glutton/connection" 19 | "github.com/mushorg/glutton/producer" 20 | "github.com/mushorg/glutton/protocols" 21 | "github.com/mushorg/glutton/rules" 22 | 23 | "github.com/google/uuid" 24 | "github.com/seud0nym/tproxy-go/tproxy" 25 | "github.com/spf13/viper" 26 | ) 27 | 28 | // Glutton struct 29 | type Glutton struct { 30 | id uuid.UUID 31 | Logger *slog.Logger 32 | Server *Server 33 | rules rules.Rules 34 | Producer *producer.Producer 35 | connTable *connection.ConnTable 36 | tcpProtocolHandlers map[string]protocols.TCPHandlerFunc 37 | udpProtocolHandlers map[string]protocols.UDPHandlerFunc 38 | ctx context.Context 39 | cancel context.CancelFunc 40 | publicAddrs []net.IP 41 | } 42 | 43 | //go:embed config/rules.yaml 44 | var defaultRules []byte 45 | 46 | //go:embed config/config.yaml 47 | var defaultConfig []byte 48 | 49 | func (g *Glutton) initConfig() error { 50 | viper.SetConfigName("config") 51 | viper.AddConfigPath(viper.GetString("confpath")) 52 | if _, err := os.Stat(viper.GetString("confpath")); !os.IsNotExist(err) { 53 | g.Logger.Info("Using configuration file", slog.String("path", viper.GetString("confpath")), slog.String("reporter", "glutton")) 54 | return viper.ReadInConfig() 55 | } 56 | 57 | g.Logger.Info("No configuration file found, using default configuration", slog.String("reporter", "glutton")) 58 | return viper.ReadConfig(bytes.NewBuffer(defaultConfig)) 59 | } 60 | 61 | // New creates a new Glutton instance 62 | func New(ctx context.Context) (*Glutton, error) { 63 | g := &Glutton{ 64 | tcpProtocolHandlers: make(map[string]protocols.TCPHandlerFunc), 65 | udpProtocolHandlers: make(map[string]protocols.UDPHandlerFunc), 66 | } 67 | g.ctx, g.cancel = context.WithCancel(ctx) 68 | 69 | g.connTable = connection.New(ctx) 70 | if err := g.makeID(); err != nil { 71 | return nil, err 72 | } 73 | g.Logger = producer.NewLogger(g.id.String()) 74 | 75 | // Loading the configuration 76 | g.Logger.Info("Loading configurations", slog.String("reporter", "glutton")) 77 | if err := g.initConfig(); err != nil { 78 | return nil, err 79 | } 80 | 81 | var rulesFile io.ReadCloser 82 | 83 | rulesPath := viper.GetString("rules_path") 84 | if _, err := os.Stat(rulesPath); !os.IsNotExist(err) { 85 | rulesFile, err = os.Open(rulesPath) 86 | if err != nil { 87 | return nil, err 88 | } 89 | defer rulesFile.Close() 90 | } else { 91 | g.Logger.Warn("No rules file found, using default rules", slog.String("reporter", "glutton")) 92 | rulesFile = io.NopCloser(bytes.NewBuffer(defaultRules)) 93 | } 94 | 95 | var err error 96 | g.rules, err = rules.Init(rulesFile) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return g, nil 102 | } 103 | 104 | // Init initializes server and handles 105 | func (g *Glutton) Init() error { 106 | var err error 107 | g.publicAddrs, err = getNonLoopbackIPs(viper.GetString("interface")) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | for _, sIP := range viper.GetStringSlice("addresses") { 113 | if ip := net.ParseIP(sIP); ip != nil { 114 | g.publicAddrs = append(g.publicAddrs, ip) 115 | } 116 | } 117 | 118 | // Start the Glutton server 119 | tcpServerPort := uint(viper.GetInt("ports.tcp")) 120 | udpServerPort := uint(viper.GetInt("ports.udp")) 121 | g.Server = NewServer(tcpServerPort, udpServerPort) 122 | if err := g.Server.Start(); err != nil { 123 | return err 124 | } 125 | 126 | // Initiating log producers 127 | if viper.GetBool("producers.enabled") { 128 | g.Producer, err = producer.New(g.id.String()) 129 | if err != nil { 130 | return err 131 | } 132 | } 133 | // Initiating protocol handlers 134 | g.tcpProtocolHandlers = protocols.MapTCPProtocolHandlers(g.Logger, g) 135 | g.udpProtocolHandlers = protocols.MapUDPProtocolHandlers(g.Logger, g) 136 | 137 | return nil 138 | } 139 | 140 | func (g *Glutton) udpListen(wg *sync.WaitGroup) { 141 | defer func() { 142 | wg.Done() 143 | }() 144 | buffer := make([]byte, 1024) 145 | for { 146 | select { 147 | case <-g.ctx.Done(): 148 | if err := g.Server.udpConn.Close(); err != nil { 149 | g.Logger.Error("Failed to close UDP listener", producer.ErrAttr(err)) 150 | } 151 | return 152 | default: 153 | } 154 | g.Server.udpConn.SetReadDeadline(time.Now().Add(1 * time.Second)) 155 | n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(g.Server.udpConn, buffer) 156 | if err != nil { 157 | if nerr, ok := err.(net.Error); ok && nerr.Timeout() { 158 | continue 159 | } 160 | g.Logger.Error("Failed to read UDP packet", producer.ErrAttr(err)) 161 | } 162 | 163 | rule, err := g.applyRules("udp", srcAddr, dstAddr) 164 | if err != nil { 165 | g.Logger.Error("Failed to apply rules", producer.ErrAttr(err)) 166 | } 167 | if rule == nil { 168 | rule = &rules.Rule{Target: "udp"} 169 | } 170 | md, err := g.connTable.Register(srcAddr.IP.String(), strconv.Itoa(int(srcAddr.AddrPort().Port())), dstAddr.AddrPort().Port(), rule) 171 | if err != nil { 172 | g.Logger.Error("Failed to register UDP packet", producer.ErrAttr(err)) 173 | } 174 | 175 | if hfunc, ok := g.udpProtocolHandlers[rule.Target]; ok { 176 | data := buffer[:n] 177 | go func() { 178 | if err := hfunc(g.ctx, srcAddr, dstAddr, data, md); err != nil { 179 | g.Logger.Error("Failed to handle UDP payload", producer.ErrAttr(err)) 180 | } 181 | }() 182 | } 183 | } 184 | } 185 | 186 | func (g *Glutton) tcpListen() { 187 | for { 188 | select { 189 | case <-g.ctx.Done(): 190 | if err := g.Server.tcpListener.Close(); err != nil { 191 | g.Logger.Error("Failed to close TCP listener", producer.ErrAttr(err)) 192 | } 193 | return 194 | default: 195 | } 196 | 197 | conn, err := g.Server.tcpListener.Accept() 198 | if err != nil { 199 | g.Logger.Error("Failed to accept connection", producer.ErrAttr(err)) 200 | continue 201 | } 202 | 203 | rule, err := g.applyRulesOnConn(conn) 204 | if err != nil { 205 | g.Logger.Error("Failed to apply rules", producer.ErrAttr(err)) 206 | continue 207 | } 208 | if rule == nil { 209 | rule = &rules.Rule{Target: "default"} 210 | } 211 | 212 | md, err := g.connTable.RegisterConn(conn, rule) 213 | if err != nil { 214 | g.Logger.Error("Failed to register connection", producer.ErrAttr(err)) 215 | continue 216 | } 217 | 218 | g.Logger.Debug("new connection", slog.String("addr", conn.LocalAddr().String()), slog.String("handler", rule.Target)) 219 | 220 | g.ctx = context.WithValue(g.ctx, ctxTimeout("timeout"), int64(viper.GetInt("conn_timeout"))) 221 | if err := g.UpdateConnectionTimeout(g.ctx, conn); err != nil { 222 | g.Logger.Error("Failed to set connection timeout", producer.ErrAttr(err)) 223 | } 224 | 225 | if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok { 226 | go func() { 227 | if err := hfunc(g.ctx, conn, md); err != nil { 228 | g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target)) 229 | } 230 | }() 231 | } 232 | } 233 | } 234 | 235 | // Start the listener, this blocks for new connections 236 | func (g *Glutton) Start() error { 237 | g.startMonitor() 238 | 239 | sshPort := viper.GetUint32("ports.ssh") 240 | if err := setTProxyIPTables(viper.GetString("interface"), g.publicAddrs[0].String(), "tcp", uint32(g.Server.tcpPort), sshPort); err != nil { 241 | return err 242 | } 243 | 244 | if err := setTProxyIPTables(viper.GetString("interface"), g.publicAddrs[0].String(), "udp", uint32(g.Server.udpPort), sshPort); err != nil { 245 | return err 246 | } 247 | 248 | wg := &sync.WaitGroup{} 249 | 250 | wg.Add(1) 251 | go g.udpListen(wg) 252 | go g.tcpListen() 253 | 254 | wg.Wait() 255 | 256 | return nil 257 | } 258 | 259 | func (g *Glutton) makeID() error { 260 | filePath := filepath.Join(viper.GetString("var-dir"), "glutton.id") 261 | if err := os.MkdirAll(viper.GetString("var-dir"), 0744); err != nil { 262 | return fmt.Errorf("failed to create var-dir: %w", err) 263 | } 264 | if _, err := os.Stat(filePath); err != nil { 265 | if os.IsNotExist(err) { 266 | g.id = uuid.New() 267 | data, err := g.id.MarshalBinary() 268 | if err != nil { 269 | return fmt.Errorf("failed to marshal UUID: %w", err) 270 | } 271 | if err := os.WriteFile(filePath, data, 0744); err != nil { 272 | return fmt.Errorf("failed to create new PID file: %w", err) 273 | } 274 | return nil 275 | } 276 | return fmt.Errorf("failed to access PID file: %w", err) 277 | } 278 | 279 | f, err := os.Open(filePath) 280 | if err != nil { 281 | return fmt.Errorf("failed to open PID file: %w", err) 282 | } 283 | buff, err := io.ReadAll(f) 284 | if err != nil { 285 | return fmt.Errorf("failed to read PID file: %w", err) 286 | } 287 | g.id, err = uuid.FromBytes(buff) 288 | if err != nil { 289 | return fmt.Errorf("failed to create UUID from PID filed content: %w", err) 290 | } 291 | 292 | return nil 293 | } 294 | 295 | type ctxTimeout string 296 | 297 | // UpdateConnectionTimeout increase connection timeout limit on connection I/O operation 298 | func (g *Glutton) UpdateConnectionTimeout(ctx context.Context, conn net.Conn) error { 299 | if timeout, ok := ctx.Value(ctxTimeout("timeout")).(int64); ok { 300 | if err := conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second)); err != nil { 301 | return err 302 | } 303 | } 304 | return nil 305 | } 306 | 307 | // ConnectionByFlow returns connection metadata by connection key 308 | func (g *Glutton) ConnectionByFlow(ckey [2]uint64) connection.Metadata { 309 | return g.connTable.Get(ckey) 310 | } 311 | 312 | // MetadataByConnection returns connection metadata by connection 313 | func (g *Glutton) MetadataByConnection(conn net.Conn) (connection.Metadata, error) { 314 | host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) 315 | if err != nil { 316 | return connection.Metadata{}, fmt.Errorf("faild to split remote address: %w", err) 317 | } 318 | ckey, err := connection.NewConnKeyByString(host, port) 319 | if err != nil { 320 | return connection.Metadata{}, err 321 | } 322 | md := g.ConnectionByFlow(ckey) 323 | return md, nil 324 | } 325 | 326 | func (g *Glutton) sanitizePayload(payload []byte) []byte { 327 | for _, ip := range g.publicAddrs { 328 | payload = []byte(strings.ReplaceAll(string(payload), ip.String(), "1.2.3.4")) 329 | } 330 | return payload 331 | } 332 | 333 | func (g *Glutton) ProduceTCP(handler string, conn net.Conn, md connection.Metadata, payload []byte, decoded interface{}) error { 334 | if g.Producer != nil { 335 | payload = g.sanitizePayload(payload) 336 | return g.Producer.LogTCP(handler, conn, md, payload, decoded) 337 | } 338 | return nil 339 | } 340 | 341 | func (g *Glutton) ProduceUDP(handler string, srcAddr, dstAddr *net.UDPAddr, md connection.Metadata, payload []byte, decoded interface{}) error { 342 | if g.Producer != nil { 343 | payload = g.sanitizePayload(payload) 344 | return g.Producer.LogUDP("udp", srcAddr, md, payload, decoded) 345 | } 346 | return nil 347 | } 348 | 349 | // Shutdown the packet processor 350 | func (g *Glutton) Shutdown() { 351 | g.cancel() // close all connection 352 | 353 | g.Logger.Info("Flushing TCP iptables") 354 | if err := flushTProxyIPTables(viper.GetString("interface"), g.publicAddrs[0].String(), "tcp", uint32(g.Server.tcpPort), uint32(viper.GetInt("ports.ssh"))); err != nil { 355 | g.Logger.Error("Failed to drop tcp iptables", producer.ErrAttr(err)) 356 | } 357 | g.Logger.Info("Flushing UDP iptables") 358 | if err := flushTProxyIPTables(viper.GetString("interface"), g.publicAddrs[0].String(), "udp", uint32(g.Server.udpPort), uint32(viper.GetInt("ports.ssh"))); err != nil { 359 | g.Logger.Error("Failed to drop udp iptables", producer.ErrAttr(err)) 360 | } 361 | 362 | g.Logger.Info("All done") 363 | } 364 | 365 | func (g *Glutton) applyRules(network string, srcAddr, dstAddr net.Addr) (*rules.Rule, error) { 366 | match, err := g.rules.Match(network, srcAddr, dstAddr) 367 | if err != nil { 368 | return nil, err 369 | } 370 | if match != nil { 371 | return match, err 372 | } 373 | return nil, nil 374 | } 375 | 376 | func (g *Glutton) applyRulesOnConn(conn net.Conn) (*rules.Rule, error) { 377 | return g.applyRules("tcp", conn.RemoteAddr(), conn.LocalAddr()) 378 | } 379 | -------------------------------------------------------------------------------- /glutton_test.go: -------------------------------------------------------------------------------- 1 | package glutton 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/spf13/viper" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestPort2Protocol(t *testing.T) { 12 | } 13 | 14 | func TestNewGlutton(t *testing.T) { 15 | viper.Set("var-dir", "/tmp/glutton") 16 | viper.Set("confpath", "./config") 17 | g, err := New(context.Background()) 18 | require.NoError(t, err, "error initializing glutton") 19 | require.NotNil(t, g, "nil instance but no error") 20 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mushorg/glutton 2 | 3 | go 1.23.5 4 | 5 | require ( 6 | github.com/coreos/go-iptables v0.8.0 7 | github.com/d1str0/hpfeeds v0.1.6 8 | github.com/ghettovoice/gosip v0.0.0-20250512091045-f65af91fc833 9 | github.com/glaslos/lsof v0.0.0-20230723212405-b3baf9409e4b 10 | github.com/google/gopacket v1.1.19 11 | github.com/google/uuid v1.6.0 12 | github.com/seud0nym/tproxy-go v0.0.0-20250208031739-d6105fbee268 13 | github.com/spf13/pflag v1.0.6 14 | github.com/spf13/viper v1.20.1 15 | github.com/stretchr/testify v1.10.0 16 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 17 | gopkg.in/yaml.v2 v2.4.0 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/fsnotify/fsnotify v1.9.0 // indirect 23 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 24 | github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect 25 | github.com/mattn/go-colorable v0.1.14 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mattn/go-runewidth v0.0.16 // indirect 28 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 30 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 31 | github.com/rivo/uniseg v0.4.7 // indirect 32 | github.com/sagikazarmark/locafero v0.9.0 // indirect 33 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect 34 | github.com/sirupsen/logrus v1.9.3 // indirect 35 | github.com/sourcegraph/conc v0.3.0 // indirect 36 | github.com/spf13/afero v1.14.0 // indirect 37 | github.com/spf13/cast v1.8.0 // indirect 38 | github.com/stretchr/objx v0.5.2 // indirect 39 | github.com/subosito/gotenv v1.6.0 // indirect 40 | github.com/tevino/abool v1.2.0 // indirect 41 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect 42 | go.uber.org/multierr v1.11.0 // indirect 43 | golang.org/x/crypto v0.38.0 // indirect 44 | golang.org/x/sys v0.33.0 // indirect 45 | golang.org/x/term v0.32.0 // indirect 46 | golang.org/x/text v0.25.0 // indirect 47 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 48 | gopkg.in/yaml.v3 v3.0.1 // indirect 49 | ) 50 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= 2 | github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= 3 | github.com/d1str0/hpfeeds v0.1.6 h1:zT6FTvr6sVNgDkF+QMUy5Xzt9OFmNTN7qGbLMm31ZwI= 4 | github.com/d1str0/hpfeeds v0.1.6/go.mod h1:q2zw8+yzZONfUm8x3U6yNBjV+2xq+dkpkujNVWIntNs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0= 10 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 11 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 14 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 15 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 16 | github.com/ghettovoice/gosip v0.0.0-20250512091045-f65af91fc833 h1:LXR6wFjKbEFQwwmv2cZjoQxOpPd+TfpwqBlGx2Q7Rz4= 17 | github.com/ghettovoice/gosip v0.0.0-20250512091045-f65af91fc833/go.mod h1:rlD1yLOErWYohWTryG/2bTTpmzB79p52ntLA/uIFXeI= 18 | github.com/glaslos/lsof v0.0.0-20230723212405-b3baf9409e4b h1:jBsfkYu3JYFMhOx2X8BE6646Ks2GD1BCLtuJ/RpHja4= 19 | github.com/glaslos/lsof v0.0.0-20230723212405-b3baf9409e4b/go.mod h1:vqLD96WcPdyQOph4KcQjyMtkdSUQkCzulx7agyQjBdI= 20 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 21 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 22 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 23 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 24 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 25 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 26 | github.com/gobwas/ws v1.1.0-rc.1 h1:VK3aeRXMI8osaS6YCDKNZhU6RKtcP3B2wzqxOogNDz8= 27 | github.com/gobwas/ws v1.1.0-rc.1/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 30 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 31 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 32 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 33 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 34 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 35 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 36 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 39 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 40 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 41 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 42 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 43 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 44 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 45 | github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= 46 | github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= 47 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 48 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 49 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 50 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 51 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 52 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 53 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 54 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 55 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 56 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 57 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 58 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 59 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 60 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 61 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 62 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 63 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 64 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 65 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 66 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 67 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 68 | github.com/nxadm/tail v1.4.5 h1:obHEce3upls1IBn1gTw/o7bCv7OJb6Ib/o7wNO+4eKw= 69 | github.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 70 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 71 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 72 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 73 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 74 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 75 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 76 | github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= 77 | github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= 78 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 79 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 80 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 81 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 82 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 83 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 84 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 85 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 86 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 87 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 88 | github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= 89 | github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= 90 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= 91 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 92 | github.com/seud0nym/tproxy-go v0.0.0-20250208031739-d6105fbee268 h1:AxDZ5LSts+ZlZB3kNAWMmGkTCUuiFzi3drxWkFDJ6qw= 93 | github.com/seud0nym/tproxy-go v0.0.0-20250208031739-d6105fbee268/go.mod h1:wofECC0kWjN6y2oYHrfujMrhSP40dGf0FfduR4gdTRY= 94 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 95 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 96 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 97 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 98 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 99 | github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= 100 | github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 101 | github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= 102 | github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 103 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 104 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 105 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 106 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 107 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 108 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 109 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 110 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 111 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 112 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 113 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 114 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 115 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 116 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 117 | github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0= 118 | github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= 119 | github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= 120 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= 121 | github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= 122 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 123 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 124 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 125 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 126 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 127 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 128 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 129 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 130 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 131 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 132 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 133 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 134 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 135 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 136 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 137 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 138 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 142 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 156 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 157 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 158 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 159 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 160 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 161 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 162 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 163 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 164 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 165 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 166 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 169 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 170 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 171 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 172 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 173 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 174 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 175 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 176 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 177 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 178 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 179 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 180 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 181 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 182 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 183 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 184 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 185 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 186 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 187 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 188 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 189 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 190 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 191 | -------------------------------------------------------------------------------- /iptables.go: -------------------------------------------------------------------------------- 1 | package glutton 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/coreos/go-iptables/iptables" 8 | ) 9 | 10 | var ( 11 | // iptables -t mangle -I PREROUTING -p tcp ! --dport 22 -m state ! --state ESTABLISHED,RELATED -j TPROXY --on-port 5000 --on-ip 127.0.0.1 12 | specTCP = "-p;%s;-m;state;!;--state;ESTABLISHED,RELATED;!;--dport;%d;-j;TPROXY;--on-port;%d;--on-ip;127.0.0.1" 13 | specUDP = "-p;%s;-m;state;!;--state;ESTABLISHED,RELATED;!;--dport;%d;-j;TPROXY;--on-port;%d;--on-ip;127.0.0.1" 14 | ) 15 | 16 | func genRuleSpec(chain, iface, protocol, _ string, sshPort, dport uint32) []string { 17 | var spec string 18 | switch protocol { 19 | case "udp": 20 | spec = specUDP 21 | case "tcp": 22 | spec = specTCP 23 | } 24 | switch chain { 25 | case "PREROUTING": 26 | spec = "-i;%s;" + spec 27 | case "OUTPUT": 28 | spec = "-o;%s;" + spec 29 | } 30 | return strings.Split(fmt.Sprintf(spec, iface, protocol, sshPort, dport), ";") 31 | } 32 | 33 | func setTProxyIPTables(iface, srcIP, protocol string, port, sshPort uint32) error { 34 | ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) 35 | if err != nil { 36 | return err 37 | } 38 | return ipt.AppendUnique("mangle", "PREROUTING", genRuleSpec("PREROUTING", iface, protocol, srcIP, sshPort, port)...) 39 | } 40 | 41 | func flushTProxyIPTables(iface, srcIP, protocol string, port, sshPort uint32) error { 42 | ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return ipt.Delete("mangle", "PREROUTING", genRuleSpec("PREROUTING", iface, protocol, srcIP, sshPort, port)...) 48 | } 49 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Glutton Documentation 2 | version: 1.0 3 | nav: 4 | - Introduction: index.md 5 | - Setup: setup.md 6 | - Configuration: configuration.md 7 | - Extension: extension.md 8 | - FAQs: faq.md 9 | theme: 10 | name: readthedocs 11 | 12 | -------------------------------------------------------------------------------- /producer/logger.go: -------------------------------------------------------------------------------- 1 | package producer 2 | 3 | import ( 4 | "io" 5 | "log/slog" 6 | "os" 7 | 8 | "github.com/spf13/viper" 9 | lumberjack "gopkg.in/natefinch/lumberjack.v2" 10 | ) 11 | 12 | func ErrAttr(err error) slog.Attr { 13 | return slog.Any("error", err) 14 | } 15 | 16 | type TeeWriter struct { 17 | stdout *os.File 18 | file io.Writer 19 | } 20 | 21 | func (t *TeeWriter) Write(p []byte) (n int, err error) { 22 | n, err = t.stdout.Write(p) 23 | if err != nil { 24 | return n, err 25 | } 26 | n, err = t.file.Write(p) 27 | return n, err 28 | } 29 | 30 | // NewLogger creates a logger instance 31 | func NewLogger(id string) *slog.Logger { 32 | writer := &TeeWriter{ 33 | stdout: os.Stdout, 34 | file: &lumberjack.Logger{ 35 | Filename: viper.GetString("logpath"), 36 | MaxSize: 200, // megabyte 37 | MaxAge: 356, //days 38 | Compress: true, // disabled by default 39 | }, 40 | } 41 | handler := slog.NewJSONHandler(writer, &slog.HandlerOptions{}) 42 | return slog.New(handler.WithAttrs([]slog.Attr{slog.String("sensorID", id)})) 43 | } 44 | -------------------------------------------------------------------------------- /producer/producer.go: -------------------------------------------------------------------------------- 1 | package producer 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/gob" 7 | "encoding/json" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "time" 13 | 14 | "github.com/mushorg/glutton/connection" 15 | "github.com/mushorg/glutton/scanner" 16 | 17 | "github.com/d1str0/hpfeeds" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | const ( 22 | httpTimeout = 10 * time.Second 23 | tlsTimeout = 5 * time.Second 24 | ) 25 | 26 | // Producer for the producer 27 | type Producer struct { 28 | sensorID string 29 | httpClient *http.Client 30 | hpfClient hpfeeds.Client 31 | hpfChannel chan []byte 32 | } 33 | 34 | // Event is a struct for glutton events 35 | type Event struct { 36 | Timestamp time.Time `json:"timestamp,omitempty"` 37 | Transport string `json:"transport,omitempty"` 38 | SrcHost string `json:"srcHost,omitempty"` 39 | SrcPort string `json:"srcPort,omitempty"` 40 | DstPort uint16 `json:"dstPort,omitempty"` 41 | SensorID string `json:"sensorID,omitempty"` 42 | Rule string `json:"rule,omitempty"` 43 | Handler string `json:"handler,omitempty"` 44 | Payload string `json:"payload,omitempty"` 45 | Scanner string `json:"scanner,omitempty"` 46 | Decoded interface{} `json:"decoded,omitempty"` 47 | } 48 | 49 | func makeEventTCP(handler string, conn net.Conn, md connection.Metadata, payload []byte, decoded interface{}, sensorID string) (*Event, error) { 50 | host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | _, scannerName, err := scanner.IsScanner(net.ParseIP(host)) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | event := Event{ 61 | Timestamp: time.Now().UTC(), 62 | Transport: "tcp", 63 | SrcHost: host, 64 | SrcPort: port, 65 | DstPort: uint16(md.TargetPort), 66 | SensorID: sensorID, 67 | Handler: handler, 68 | Payload: base64.StdEncoding.EncodeToString(payload), 69 | Scanner: scannerName, 70 | Decoded: decoded, 71 | } 72 | if md.Rule != nil { 73 | event.Rule = md.Rule.String() 74 | } 75 | return &event, nil 76 | } 77 | 78 | func makeEventUDP(handler string, srcAddr *net.UDPAddr, md connection.Metadata, payload []byte, decoded interface{}, sensorID string) (*Event, error) { 79 | _, scannerName, err := scanner.IsScanner(net.ParseIP(srcAddr.IP.String())) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | event := Event{ 85 | Timestamp: time.Now().UTC(), 86 | Transport: "udp", 87 | SrcHost: srcAddr.IP.String(), 88 | SrcPort: strconv.Itoa(int(srcAddr.AddrPort().Port())), 89 | DstPort: uint16(md.TargetPort), 90 | SensorID: sensorID, 91 | Handler: handler, 92 | Payload: base64.StdEncoding.EncodeToString(payload), 93 | Scanner: scannerName, 94 | Decoded: decoded, 95 | } 96 | if md.Rule != nil { 97 | event.Rule = md.Rule.String() 98 | } 99 | return &event, nil 100 | } 101 | 102 | // New initializes the producers 103 | func New(sensorID string) (*Producer, error) { 104 | producer := &Producer{ 105 | sensorID: sensorID, 106 | httpClient: &http.Client{ 107 | Transport: &http.Transport{ 108 | TLSHandshakeTimeout: tlsTimeout, 109 | }, 110 | Timeout: httpTimeout, 111 | }, 112 | } 113 | if viper.GetBool("producers.hpfeeds.enabled") { 114 | producer.hpfClient = hpfeeds.NewClient( 115 | viper.GetString("producers.hpfeeds.host"), 116 | viper.GetInt("producers.hpfeeds.port"), 117 | viper.GetString("producers.hpfeeds.ident"), 118 | viper.GetString("producers.hpfeeds.auth"), 119 | ) 120 | if err := producer.hpfClient.Connect(); err != nil { 121 | return producer, err 122 | } 123 | producer.hpfChannel = make(chan []byte) 124 | producer.hpfClient.Publish(viper.GetString("producers.hpfeeds.channel"), producer.hpfChannel) 125 | } 126 | return producer, nil 127 | } 128 | 129 | // LogTCP is a meta caller for all producers 130 | func (p *Producer) LogTCP(handler string, conn net.Conn, md connection.Metadata, payload []byte, decoded interface{}) error { 131 | event, err := makeEventTCP(handler, conn, md, payload, decoded, p.sensorID) 132 | if err != nil { 133 | return err 134 | } 135 | if viper.GetBool("producers.hpfeeds.enabled") { 136 | if err := p.logHPFeeds(event); err != nil { 137 | return err 138 | } 139 | } 140 | if viper.GetBool("producers.http.enabled") { 141 | if err := p.logHTTP(event); err != nil { 142 | return err 143 | } 144 | } 145 | return nil 146 | } 147 | 148 | // LogUDP is a meta caller for all producers 149 | func (p *Producer) LogUDP(handler string, srcAddr *net.UDPAddr, md connection.Metadata, payload []byte, decoded interface{}) error { 150 | event, err := makeEventUDP(handler, srcAddr, md, payload, decoded, p.sensorID) 151 | if err != nil { 152 | return err 153 | } 154 | if viper.GetBool("producers.hpfeeds.enabled") { 155 | if err := p.logHPFeeds(event); err != nil { 156 | return err 157 | } 158 | } 159 | if viper.GetBool("producers.http.enabled") { 160 | if err := p.logHTTP(event); err != nil { 161 | return err 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | // logHPFeeds logs an event to a hpfeeds broker 168 | func (p *Producer) logHPFeeds(event *Event) error { 169 | var buf bytes.Buffer 170 | if err := gob.NewEncoder(&buf).Encode(event); err != nil { 171 | return err 172 | } 173 | p.hpfChannel <- buf.Bytes() 174 | return nil 175 | } 176 | 177 | // Check if a ip is private. 178 | func isPrivateIP(ip string) bool { 179 | ipAddress := net.ParseIP(ip) 180 | return ipAddress.IsPrivate() 181 | } 182 | 183 | // logHTTP send logs to HTTP endpoint 184 | func (p *Producer) logHTTP(event *Event) error { 185 | if isPrivateIP(event.SrcHost) { 186 | return nil 187 | } 188 | url, err := url.Parse(viper.GetString("producers.http.remote")) 189 | if err != nil { 190 | return err 191 | } 192 | data, err := json.Marshal(event) 193 | if err != nil { 194 | return err 195 | } 196 | req, err := http.NewRequest("POST", url.Scheme+"://"+url.Host+url.Path, bytes.NewBuffer(data)) 197 | if err != nil { 198 | return err 199 | } 200 | req.URL.RawQuery = url.RawQuery 201 | if password, ok := url.User.Password(); ok { 202 | req.SetBasicAuth(url.User.Username(), password) 203 | } 204 | req.Header.Set("Content-Type", "application/json") 205 | resp, err := p.httpClient.Do(req) 206 | if err != nil { 207 | return err 208 | } 209 | return resp.Body.Close() 210 | } 211 | -------------------------------------------------------------------------------- /producer/producer_test.go: -------------------------------------------------------------------------------- 1 | package producer 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/mushorg/glutton/connection" 10 | "github.com/mushorg/glutton/rules" 11 | 12 | "github.com/spf13/viper" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestNew(t *testing.T) { 17 | p, err := New("test") 18 | require.NoError(t, err) 19 | require.NotNil(t, p) 20 | } 21 | 22 | func TestProducerLog(t *testing.T) { 23 | p, err := New("test") 24 | require.NoError(t, err) 25 | require.NotNil(t, p) 26 | 27 | l, err := net.Listen("tcp", ":1234") 28 | require.NoError(t, err) 29 | require.NotNil(t, l) 30 | defer l.Close() 31 | conn, err := net.Dial("tcp", ":1234") 32 | require.NoError(t, err) 33 | require.NoError(t, conn.Close()) 34 | md := connection.Metadata{ 35 | Rule: &rules.Rule{}, 36 | } 37 | 38 | viper.Set("producers.http.enabled", true) 39 | 40 | svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) 41 | defer svr.Close() 42 | 43 | viper.Set("producers.http.remote", svr.URL) 44 | 45 | err = p.LogTCP("test", conn, md, []byte{123}, nil) 46 | require.NoError(t, err) 47 | 48 | err = p.LogUDP("test", &net.UDPAddr{}, md, []byte{123}, nil) 49 | require.NoError(t, err) 50 | } 51 | -------------------------------------------------------------------------------- /protocols/helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func FirstOrEmpty[T any](s []T) T { 11 | if len(s) > 0 { 12 | return s[0] 13 | } 14 | var t T 15 | return t 16 | } 17 | 18 | func Store(data []byte, folder string) (string, error) { 19 | sum := sha256.Sum256(data) 20 | if err := os.MkdirAll(folder, os.ModePerm); err != nil { 21 | return "", err 22 | } 23 | sha256Hash := hex.EncodeToString(sum[:]) 24 | path := filepath.Join(folder, sha256Hash) 25 | if _, err := os.Stat(path); err == nil { 26 | // file already exists 27 | return "", nil 28 | } 29 | out, err := os.Create(path) 30 | if err != nil { 31 | return "", err 32 | } 33 | defer out.Close() 34 | if _, err = out.Write(data); err != nil { 35 | return "", err 36 | } 37 | return sha256Hash, nil 38 | } 39 | -------------------------------------------------------------------------------- /protocols/interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/mushorg/glutton/connection" 8 | ) 9 | 10 | type Logger interface { 11 | Debug(msg string, fields ...any) 12 | Info(msg string, fields ...any) 13 | Warn(msg string, fields ...any) 14 | Error(msg string, fields ...any) 15 | } 16 | 17 | type Honeypot interface { 18 | ProduceTCP(protocol string, conn net.Conn, md connection.Metadata, payload []byte, decoded interface{}) error 19 | ProduceUDP(handler string, srcAddr, dstAddr *net.UDPAddr, md connection.Metadata, payload []byte, decoded interface{}) error 20 | ConnectionByFlow([2]uint64) connection.Metadata 21 | UpdateConnectionTimeout(ctx context.Context, conn net.Conn) error 22 | MetadataByConnection(net.Conn) (connection.Metadata, error) 23 | } 24 | -------------------------------------------------------------------------------- /protocols/mocks/mock_honeypot.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.44.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | connection "github.com/mushorg/glutton/connection" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | 12 | net "net" 13 | ) 14 | 15 | // MockHoneypot is an autogenerated mock type for the Honeypot type 16 | type MockHoneypot struct { 17 | mock.Mock 18 | } 19 | 20 | type MockHoneypot_Expecter struct { 21 | mock *mock.Mock 22 | } 23 | 24 | func (_m *MockHoneypot) EXPECT() *MockHoneypot_Expecter { 25 | return &MockHoneypot_Expecter{mock: &_m.Mock} 26 | } 27 | 28 | // ConnectionByFlow provides a mock function with given fields: _a0 29 | func (_m *MockHoneypot) ConnectionByFlow(_a0 [2]uint64) connection.Metadata { 30 | ret := _m.Called(_a0) 31 | 32 | if len(ret) == 0 { 33 | panic("no return value specified for ConnectionByFlow") 34 | } 35 | 36 | var r0 connection.Metadata 37 | if rf, ok := ret.Get(0).(func([2]uint64) connection.Metadata); ok { 38 | r0 = rf(_a0) 39 | } else { 40 | r0 = ret.Get(0).(connection.Metadata) 41 | } 42 | 43 | return r0 44 | } 45 | 46 | // MockHoneypot_ConnectionByFlow_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConnectionByFlow' 47 | type MockHoneypot_ConnectionByFlow_Call struct { 48 | *mock.Call 49 | } 50 | 51 | // ConnectionByFlow is a helper method to define mock.On call 52 | // - _a0 [2]uint64 53 | func (_e *MockHoneypot_Expecter) ConnectionByFlow(_a0 interface{}) *MockHoneypot_ConnectionByFlow_Call { 54 | return &MockHoneypot_ConnectionByFlow_Call{Call: _e.mock.On("ConnectionByFlow", _a0)} 55 | } 56 | 57 | func (_c *MockHoneypot_ConnectionByFlow_Call) Run(run func(_a0 [2]uint64)) *MockHoneypot_ConnectionByFlow_Call { 58 | _c.Call.Run(func(args mock.Arguments) { 59 | run(args[0].([2]uint64)) 60 | }) 61 | return _c 62 | } 63 | 64 | func (_c *MockHoneypot_ConnectionByFlow_Call) Return(_a0 connection.Metadata) *MockHoneypot_ConnectionByFlow_Call { 65 | _c.Call.Return(_a0) 66 | return _c 67 | } 68 | 69 | func (_c *MockHoneypot_ConnectionByFlow_Call) RunAndReturn(run func([2]uint64) connection.Metadata) *MockHoneypot_ConnectionByFlow_Call { 70 | _c.Call.Return(run) 71 | return _c 72 | } 73 | 74 | // MetadataByConnection provides a mock function with given fields: _a0 75 | func (_m *MockHoneypot) MetadataByConnection(_a0 net.Conn) (connection.Metadata, error) { 76 | ret := _m.Called(_a0) 77 | 78 | if len(ret) == 0 { 79 | panic("no return value specified for MetadataByConnection") 80 | } 81 | 82 | var r0 connection.Metadata 83 | var r1 error 84 | if rf, ok := ret.Get(0).(func(net.Conn) (connection.Metadata, error)); ok { 85 | return rf(_a0) 86 | } 87 | if rf, ok := ret.Get(0).(func(net.Conn) connection.Metadata); ok { 88 | r0 = rf(_a0) 89 | } else { 90 | r0 = ret.Get(0).(connection.Metadata) 91 | } 92 | 93 | if rf, ok := ret.Get(1).(func(net.Conn) error); ok { 94 | r1 = rf(_a0) 95 | } else { 96 | r1 = ret.Error(1) 97 | } 98 | 99 | return r0, r1 100 | } 101 | 102 | // MockHoneypot_MetadataByConnection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MetadataByConnection' 103 | type MockHoneypot_MetadataByConnection_Call struct { 104 | *mock.Call 105 | } 106 | 107 | // MetadataByConnection is a helper method to define mock.On call 108 | // - _a0 net.Conn 109 | func (_e *MockHoneypot_Expecter) MetadataByConnection(_a0 interface{}) *MockHoneypot_MetadataByConnection_Call { 110 | return &MockHoneypot_MetadataByConnection_Call{Call: _e.mock.On("MetadataByConnection", _a0)} 111 | } 112 | 113 | func (_c *MockHoneypot_MetadataByConnection_Call) Run(run func(_a0 net.Conn)) *MockHoneypot_MetadataByConnection_Call { 114 | _c.Call.Run(func(args mock.Arguments) { 115 | run(args[0].(net.Conn)) 116 | }) 117 | return _c 118 | } 119 | 120 | func (_c *MockHoneypot_MetadataByConnection_Call) Return(_a0 connection.Metadata, _a1 error) *MockHoneypot_MetadataByConnection_Call { 121 | _c.Call.Return(_a0, _a1) 122 | return _c 123 | } 124 | 125 | func (_c *MockHoneypot_MetadataByConnection_Call) RunAndReturn(run func(net.Conn) (connection.Metadata, error)) *MockHoneypot_MetadataByConnection_Call { 126 | _c.Call.Return(run) 127 | return _c 128 | } 129 | 130 | // ProduceTCP provides a mock function with given fields: protocol, conn, md, payload, decoded 131 | func (_m *MockHoneypot) ProduceTCP(protocol string, conn net.Conn, md connection.Metadata, payload []byte, decoded interface{}) error { 132 | ret := _m.Called(protocol, conn, md, payload, decoded) 133 | 134 | if len(ret) == 0 { 135 | panic("no return value specified for ProduceTCP") 136 | } 137 | 138 | var r0 error 139 | if rf, ok := ret.Get(0).(func(string, net.Conn, connection.Metadata, []byte, interface{}) error); ok { 140 | r0 = rf(protocol, conn, md, payload, decoded) 141 | } else { 142 | r0 = ret.Error(0) 143 | } 144 | 145 | return r0 146 | } 147 | 148 | // MockHoneypot_ProduceTCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProduceTCP' 149 | type MockHoneypot_ProduceTCP_Call struct { 150 | *mock.Call 151 | } 152 | 153 | // ProduceTCP is a helper method to define mock.On call 154 | // - protocol string 155 | // - conn net.Conn 156 | // - md connection.Metadata 157 | // - payload []byte 158 | // - decoded interface{} 159 | func (_e *MockHoneypot_Expecter) ProduceTCP(protocol interface{}, conn interface{}, md interface{}, payload interface{}, decoded interface{}) *MockHoneypot_ProduceTCP_Call { 160 | return &MockHoneypot_ProduceTCP_Call{Call: _e.mock.On("ProduceTCP", protocol, conn, md, payload, decoded)} 161 | } 162 | 163 | func (_c *MockHoneypot_ProduceTCP_Call) Run(run func(protocol string, conn net.Conn, md connection.Metadata, payload []byte, decoded interface{})) *MockHoneypot_ProduceTCP_Call { 164 | _c.Call.Run(func(args mock.Arguments) { 165 | run(args[0].(string), args[1].(net.Conn), args[2].(connection.Metadata), args[3].([]byte), args[4].(interface{})) 166 | }) 167 | return _c 168 | } 169 | 170 | func (_c *MockHoneypot_ProduceTCP_Call) Return(_a0 error) *MockHoneypot_ProduceTCP_Call { 171 | _c.Call.Return(_a0) 172 | return _c 173 | } 174 | 175 | func (_c *MockHoneypot_ProduceTCP_Call) RunAndReturn(run func(string, net.Conn, connection.Metadata, []byte, interface{}) error) *MockHoneypot_ProduceTCP_Call { 176 | _c.Call.Return(run) 177 | return _c 178 | } 179 | 180 | // ProduceUDP provides a mock function with given fields: handler, srcAddr, dstAddr, md, payload, decoded 181 | func (_m *MockHoneypot) ProduceUDP(handler string, srcAddr *net.UDPAddr, dstAddr *net.UDPAddr, md connection.Metadata, payload []byte, decoded interface{}) error { 182 | ret := _m.Called(handler, srcAddr, dstAddr, md, payload, decoded) 183 | 184 | if len(ret) == 0 { 185 | panic("no return value specified for ProduceUDP") 186 | } 187 | 188 | var r0 error 189 | if rf, ok := ret.Get(0).(func(string, *net.UDPAddr, *net.UDPAddr, connection.Metadata, []byte, interface{}) error); ok { 190 | r0 = rf(handler, srcAddr, dstAddr, md, payload, decoded) 191 | } else { 192 | r0 = ret.Error(0) 193 | } 194 | 195 | return r0 196 | } 197 | 198 | // MockHoneypot_ProduceUDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProduceUDP' 199 | type MockHoneypot_ProduceUDP_Call struct { 200 | *mock.Call 201 | } 202 | 203 | // ProduceUDP is a helper method to define mock.On call 204 | // - handler string 205 | // - srcAddr *net.UDPAddr 206 | // - dstAddr *net.UDPAddr 207 | // - md connection.Metadata 208 | // - payload []byte 209 | // - decoded interface{} 210 | func (_e *MockHoneypot_Expecter) ProduceUDP(handler interface{}, srcAddr interface{}, dstAddr interface{}, md interface{}, payload interface{}, decoded interface{}) *MockHoneypot_ProduceUDP_Call { 211 | return &MockHoneypot_ProduceUDP_Call{Call: _e.mock.On("ProduceUDP", handler, srcAddr, dstAddr, md, payload, decoded)} 212 | } 213 | 214 | func (_c *MockHoneypot_ProduceUDP_Call) Run(run func(handler string, srcAddr *net.UDPAddr, dstAddr *net.UDPAddr, md connection.Metadata, payload []byte, decoded interface{})) *MockHoneypot_ProduceUDP_Call { 215 | _c.Call.Run(func(args mock.Arguments) { 216 | run(args[0].(string), args[1].(*net.UDPAddr), args[2].(*net.UDPAddr), args[3].(connection.Metadata), args[4].([]byte), args[5].(interface{})) 217 | }) 218 | return _c 219 | } 220 | 221 | func (_c *MockHoneypot_ProduceUDP_Call) Return(_a0 error) *MockHoneypot_ProduceUDP_Call { 222 | _c.Call.Return(_a0) 223 | return _c 224 | } 225 | 226 | func (_c *MockHoneypot_ProduceUDP_Call) RunAndReturn(run func(string, *net.UDPAddr, *net.UDPAddr, connection.Metadata, []byte, interface{}) error) *MockHoneypot_ProduceUDP_Call { 227 | _c.Call.Return(run) 228 | return _c 229 | } 230 | 231 | // UpdateConnectionTimeout provides a mock function with given fields: ctx, conn 232 | func (_m *MockHoneypot) UpdateConnectionTimeout(ctx context.Context, conn net.Conn) error { 233 | ret := _m.Called(ctx, conn) 234 | 235 | if len(ret) == 0 { 236 | panic("no return value specified for UpdateConnectionTimeout") 237 | } 238 | 239 | var r0 error 240 | if rf, ok := ret.Get(0).(func(context.Context, net.Conn) error); ok { 241 | r0 = rf(ctx, conn) 242 | } else { 243 | r0 = ret.Error(0) 244 | } 245 | 246 | return r0 247 | } 248 | 249 | // MockHoneypot_UpdateConnectionTimeout_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateConnectionTimeout' 250 | type MockHoneypot_UpdateConnectionTimeout_Call struct { 251 | *mock.Call 252 | } 253 | 254 | // UpdateConnectionTimeout is a helper method to define mock.On call 255 | // - ctx context.Context 256 | // - conn net.Conn 257 | func (_e *MockHoneypot_Expecter) UpdateConnectionTimeout(ctx interface{}, conn interface{}) *MockHoneypot_UpdateConnectionTimeout_Call { 258 | return &MockHoneypot_UpdateConnectionTimeout_Call{Call: _e.mock.On("UpdateConnectionTimeout", ctx, conn)} 259 | } 260 | 261 | func (_c *MockHoneypot_UpdateConnectionTimeout_Call) Run(run func(ctx context.Context, conn net.Conn)) *MockHoneypot_UpdateConnectionTimeout_Call { 262 | _c.Call.Run(func(args mock.Arguments) { 263 | run(args[0].(context.Context), args[1].(net.Conn)) 264 | }) 265 | return _c 266 | } 267 | 268 | func (_c *MockHoneypot_UpdateConnectionTimeout_Call) Return(_a0 error) *MockHoneypot_UpdateConnectionTimeout_Call { 269 | _c.Call.Return(_a0) 270 | return _c 271 | } 272 | 273 | func (_c *MockHoneypot_UpdateConnectionTimeout_Call) RunAndReturn(run func(context.Context, net.Conn) error) *MockHoneypot_UpdateConnectionTimeout_Call { 274 | _c.Call.Return(run) 275 | return _c 276 | } 277 | 278 | // NewMockHoneypot creates a new instance of MockHoneypot. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 279 | // The first argument is typically a *testing.T value. 280 | func NewMockHoneypot(t interface { 281 | mock.TestingT 282 | Cleanup(func()) 283 | }) *MockHoneypot { 284 | mock := &MockHoneypot{} 285 | mock.Mock.Test(t) 286 | 287 | t.Cleanup(func() { mock.AssertExpectations(t) }) 288 | 289 | return mock 290 | } 291 | -------------------------------------------------------------------------------- /protocols/mocks/mock_logger.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.44.1. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // MockLogger is an autogenerated mock type for the Logger type 8 | type MockLogger struct { 9 | mock.Mock 10 | } 11 | 12 | type MockLogger_Expecter struct { 13 | mock *mock.Mock 14 | } 15 | 16 | func (_m *MockLogger) EXPECT() *MockLogger_Expecter { 17 | return &MockLogger_Expecter{mock: &_m.Mock} 18 | } 19 | 20 | // Debug provides a mock function with given fields: msg, fields 21 | func (_m *MockLogger) Debug(msg string, fields ...interface{}) { 22 | var _ca []interface{} 23 | _ca = append(_ca, msg) 24 | _ca = append(_ca, fields...) 25 | _m.Called(_ca...) 26 | } 27 | 28 | // MockLogger_Debug_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Debug' 29 | type MockLogger_Debug_Call struct { 30 | *mock.Call 31 | } 32 | 33 | // Debug is a helper method to define mock.On call 34 | // - msg string 35 | // - fields ...interface{} 36 | func (_e *MockLogger_Expecter) Debug(msg interface{}, fields ...interface{}) *MockLogger_Debug_Call { 37 | return &MockLogger_Debug_Call{Call: _e.mock.On("Debug", 38 | append([]interface{}{msg}, fields...)...)} 39 | } 40 | 41 | func (_c *MockLogger_Debug_Call) Run(run func(msg string, fields ...interface{})) *MockLogger_Debug_Call { 42 | _c.Call.Run(func(args mock.Arguments) { 43 | variadicArgs := make([]interface{}, len(args)-1) 44 | for i, a := range args[1:] { 45 | if a != nil { 46 | variadicArgs[i] = a.(interface{}) 47 | } 48 | } 49 | run(args[0].(string), variadicArgs...) 50 | }) 51 | return _c 52 | } 53 | 54 | func (_c *MockLogger_Debug_Call) Return() *MockLogger_Debug_Call { 55 | _c.Call.Return() 56 | return _c 57 | } 58 | 59 | func (_c *MockLogger_Debug_Call) RunAndReturn(run func(string, ...interface{})) *MockLogger_Debug_Call { 60 | _c.Call.Return(run) 61 | return _c 62 | } 63 | 64 | // Error provides a mock function with given fields: msg, fields 65 | func (_m *MockLogger) Error(msg string, fields ...interface{}) { 66 | var _ca []interface{} 67 | _ca = append(_ca, msg) 68 | _ca = append(_ca, fields...) 69 | _m.Called(_ca...) 70 | } 71 | 72 | // MockLogger_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' 73 | type MockLogger_Error_Call struct { 74 | *mock.Call 75 | } 76 | 77 | // Error is a helper method to define mock.On call 78 | // - msg string 79 | // - fields ...interface{} 80 | func (_e *MockLogger_Expecter) Error(msg interface{}, fields ...interface{}) *MockLogger_Error_Call { 81 | return &MockLogger_Error_Call{Call: _e.mock.On("Error", 82 | append([]interface{}{msg}, fields...)...)} 83 | } 84 | 85 | func (_c *MockLogger_Error_Call) Run(run func(msg string, fields ...interface{})) *MockLogger_Error_Call { 86 | _c.Call.Run(func(args mock.Arguments) { 87 | variadicArgs := make([]interface{}, len(args)-1) 88 | for i, a := range args[1:] { 89 | if a != nil { 90 | variadicArgs[i] = a.(interface{}) 91 | } 92 | } 93 | run(args[0].(string), variadicArgs...) 94 | }) 95 | return _c 96 | } 97 | 98 | func (_c *MockLogger_Error_Call) Return() *MockLogger_Error_Call { 99 | _c.Call.Return() 100 | return _c 101 | } 102 | 103 | func (_c *MockLogger_Error_Call) RunAndReturn(run func(string, ...interface{})) *MockLogger_Error_Call { 104 | _c.Call.Return(run) 105 | return _c 106 | } 107 | 108 | // Info provides a mock function with given fields: msg, fields 109 | func (_m *MockLogger) Info(msg string, fields ...interface{}) { 110 | var _ca []interface{} 111 | _ca = append(_ca, msg) 112 | _ca = append(_ca, fields...) 113 | _m.Called(_ca...) 114 | } 115 | 116 | // MockLogger_Info_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Info' 117 | type MockLogger_Info_Call struct { 118 | *mock.Call 119 | } 120 | 121 | // Info is a helper method to define mock.On call 122 | // - msg string 123 | // - fields ...interface{} 124 | func (_e *MockLogger_Expecter) Info(msg interface{}, fields ...interface{}) *MockLogger_Info_Call { 125 | return &MockLogger_Info_Call{Call: _e.mock.On("Info", 126 | append([]interface{}{msg}, fields...)...)} 127 | } 128 | 129 | func (_c *MockLogger_Info_Call) Run(run func(msg string, fields ...interface{})) *MockLogger_Info_Call { 130 | _c.Call.Run(func(args mock.Arguments) { 131 | variadicArgs := make([]interface{}, len(args)-1) 132 | for i, a := range args[1:] { 133 | if a != nil { 134 | variadicArgs[i] = a.(interface{}) 135 | } 136 | } 137 | run(args[0].(string), variadicArgs...) 138 | }) 139 | return _c 140 | } 141 | 142 | func (_c *MockLogger_Info_Call) Return() *MockLogger_Info_Call { 143 | _c.Call.Return() 144 | return _c 145 | } 146 | 147 | func (_c *MockLogger_Info_Call) RunAndReturn(run func(string, ...interface{})) *MockLogger_Info_Call { 148 | _c.Call.Return(run) 149 | return _c 150 | } 151 | 152 | // Warn provides a mock function with given fields: msg, fields 153 | func (_m *MockLogger) Warn(msg string, fields ...interface{}) { 154 | var _ca []interface{} 155 | _ca = append(_ca, msg) 156 | _ca = append(_ca, fields...) 157 | _m.Called(_ca...) 158 | } 159 | 160 | // MockLogger_Warn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Warn' 161 | type MockLogger_Warn_Call struct { 162 | *mock.Call 163 | } 164 | 165 | // Warn is a helper method to define mock.On call 166 | // - msg string 167 | // - fields ...interface{} 168 | func (_e *MockLogger_Expecter) Warn(msg interface{}, fields ...interface{}) *MockLogger_Warn_Call { 169 | return &MockLogger_Warn_Call{Call: _e.mock.On("Warn", 170 | append([]interface{}{msg}, fields...)...)} 171 | } 172 | 173 | func (_c *MockLogger_Warn_Call) Run(run func(msg string, fields ...interface{})) *MockLogger_Warn_Call { 174 | _c.Call.Run(func(args mock.Arguments) { 175 | variadicArgs := make([]interface{}, len(args)-1) 176 | for i, a := range args[1:] { 177 | if a != nil { 178 | variadicArgs[i] = a.(interface{}) 179 | } 180 | } 181 | run(args[0].(string), variadicArgs...) 182 | }) 183 | return _c 184 | } 185 | 186 | func (_c *MockLogger_Warn_Call) Return() *MockLogger_Warn_Call { 187 | _c.Call.Return() 188 | return _c 189 | } 190 | 191 | func (_c *MockLogger_Warn_Call) RunAndReturn(run func(string, ...interface{})) *MockLogger_Warn_Call { 192 | _c.Call.Return(run) 193 | return _c 194 | } 195 | 196 | // NewMockLogger creates a new instance of MockLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 197 | // The first argument is typically a *testing.T value. 198 | func NewMockLogger(t interface { 199 | mock.TestingT 200 | Cleanup(func()) 201 | }) *MockLogger { 202 | mock := &MockLogger{} 203 | mock.Mock.Test(t) 204 | 205 | t.Cleanup(func() { mock.AssertExpectations(t) }) 206 | 207 | return mock 208 | } 209 | -------------------------------------------------------------------------------- /protocols/peek.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "time" 7 | ) 8 | 9 | // BufferedConn provides an interface to peek at a connection 10 | type BufferedConn struct { 11 | r *bufio.Reader 12 | net.Conn // So that most methods are embedded 13 | } 14 | 15 | func newBufferedConn(c net.Conn) BufferedConn { 16 | return BufferedConn{bufio.NewReader(c), c} 17 | } 18 | 19 | func (b BufferedConn) peek(n int) ([]byte, error) { 20 | return b.r.Peek(n) 21 | } 22 | 23 | func (b BufferedConn) Read(p []byte) (int, error) { 24 | return b.r.Read(p) 25 | } 26 | 27 | // Peek reads `length` amount of data from the connection 28 | func Peek(conn net.Conn, length int) ([]byte, BufferedConn, error) { 29 | bufConn := newBufferedConn(conn) 30 | if err := bufConn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil { 31 | return nil, bufConn, err 32 | } 33 | snip, err := bufConn.peek(length) 34 | return snip, bufConn, err 35 | } 36 | -------------------------------------------------------------------------------- /protocols/peek_test.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestPeek(t *testing.T) { 12 | conn, close := testConn(t) 13 | defer close() 14 | snip, _, err := Peek(conn, 1) 15 | var netErr net.Error 16 | ok := errors.As(err, &netErr) 17 | require.True(t, ok) 18 | require.True(t, netErr.Timeout()) 19 | require.Empty(t, snip) 20 | } 21 | -------------------------------------------------------------------------------- /protocols/protocols.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "net" 8 | "strings" 9 | 10 | "github.com/mushorg/glutton/connection" 11 | "github.com/mushorg/glutton/producer" 12 | "github.com/mushorg/glutton/protocols/interfaces" 13 | "github.com/mushorg/glutton/protocols/tcp" 14 | "github.com/mushorg/glutton/protocols/udp" 15 | ) 16 | 17 | type TCPHandlerFunc func(ctx context.Context, conn net.Conn, md connection.Metadata) error 18 | 19 | type UDPHandlerFunc func(ctx context.Context, srcAddr, dstAddr *net.UDPAddr, data []byte, md connection.Metadata) error 20 | 21 | // MapUDPProtocolHandlers map protocol handlers to corresponding protocol 22 | func MapUDPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[string]UDPHandlerFunc { 23 | protocolHandlers := map[string]UDPHandlerFunc{} 24 | protocolHandlers["udp"] = func(ctx context.Context, srcAddr, dstAddr *net.UDPAddr, data []byte, md connection.Metadata) error { 25 | return udp.HandleUDP(ctx, srcAddr, dstAddr, data, md, log, h) 26 | } 27 | return protocolHandlers 28 | } 29 | 30 | // MapTCPProtocolHandlers map protocol handlers to corresponding protocol 31 | func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[string]TCPHandlerFunc { 32 | protocolHandlers := map[string]TCPHandlerFunc{} 33 | protocolHandlers["smtp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 34 | return tcp.HandleSMTP(ctx, conn, md, log, h) 35 | } 36 | protocolHandlers["rdp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 37 | return tcp.HandleRDP(ctx, conn, md, log, h) 38 | } 39 | protocolHandlers["smb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 40 | return tcp.HandleSMB(ctx, conn, md, log, h) 41 | } 42 | protocolHandlers["ftp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 43 | return tcp.HandleFTP(ctx, conn, md, log, h) 44 | } 45 | protocolHandlers["sip"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 46 | return tcp.HandleSIP(ctx, conn, md, log, h) 47 | } 48 | protocolHandlers["rfb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 49 | return tcp.HandleRFB(ctx, conn, md, log, h) 50 | } 51 | protocolHandlers["telnet"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 52 | return tcp.HandleTelnet(ctx, conn, md, log, h) 53 | } 54 | protocolHandlers["mqtt"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 55 | return tcp.HandleMQTT(ctx, conn, md, log, h) 56 | } 57 | protocolHandlers["iscsi"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 58 | return tcp.HandleISCSI(ctx, conn, md, log, h) 59 | } 60 | protocolHandlers["bittorrent"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 61 | return tcp.HandleBittorrent(ctx, conn, md, log, h) 62 | } 63 | protocolHandlers["memcache"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 64 | return tcp.HandleMemcache(ctx, conn, md, log, h) 65 | } 66 | protocolHandlers["jabber"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 67 | return tcp.HandleJabber(ctx, conn, md, log, h) 68 | } 69 | protocolHandlers["adb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 70 | return tcp.HandleADB(ctx, conn, md, log, h) 71 | } 72 | protocolHandlers["mongodb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 73 | return tcp.HandleMongoDB(ctx, conn, md, log, h) 74 | } 75 | protocolHandlers["tcp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error { 76 | snip, bufConn, err := Peek(conn, 4) 77 | if err != nil { 78 | if err := conn.Close(); err != nil { 79 | log.Error("failed to close connection", producer.ErrAttr(err)) 80 | } 81 | log.Debug("failed to peek connection", producer.ErrAttr(err)) 82 | return nil 83 | } 84 | // poor mans check for HTTP request 85 | httpMap := map[string]bool{"GET ": true, "POST": true, "HEAD": true, "OPTI": true, "CONN": true} 86 | if _, ok := httpMap[strings.ToUpper(string(snip))]; ok { 87 | return tcp.HandleHTTP(ctx, bufConn, md, log, h) 88 | } 89 | // poor mans check for RDP header 90 | if bytes.Equal(snip, []byte{0x03, 0x00, 0x00, 0x2b}) { 91 | return tcp.HandleRDP(ctx, bufConn, md, log, h) 92 | } 93 | // poor mans check for MongoDB header (checking msg length and validating opcodes) 94 | messageLength := binary.LittleEndian.Uint32(snip) 95 | if messageLength > 0 && messageLength <= 48*1024*1024 { 96 | moreSample, bufConn, err := Peek(bufConn, 16) 97 | if err != nil { 98 | if err := conn.Close(); err != nil { 99 | log.Error("failed to close connection", producer.ErrAttr(err)) 100 | } 101 | log.Debug("failed to peek connection", producer.ErrAttr(err)) 102 | return nil 103 | } 104 | if len(moreSample) == 16 { 105 | opCode := binary.LittleEndian.Uint32(moreSample[12:16]) 106 | validOpCodes := map[uint32]bool{ 107 | 1: true, // OP_REPLY 108 | 2001: true, // OP_UPDATE 109 | 2002: true, // OP_INSERT 110 | 2004: true, // OP_QUERY 111 | 2005: true, // OP_GET_MORE 112 | 2006: true, // OP_DELETE 113 | 2007: true, // OP_KILL_CURSORS 114 | 2012: true, // OP_COMPRESSED 115 | 2013: true, // OP_MSG 116 | } 117 | if _, ok := validOpCodes[opCode]; ok { 118 | return tcp.HandleMongoDB(ctx, bufConn, md, log, h) 119 | } 120 | } 121 | } 122 | // fallback TCP handler 123 | return tcp.HandleTCP(ctx, bufConn, md, log, h) 124 | } 125 | return protocolHandlers 126 | } 127 | -------------------------------------------------------------------------------- /protocols/protocols_test.go: -------------------------------------------------------------------------------- 1 | package protocols 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/mushorg/glutton/connection" 10 | "github.com/mushorg/glutton/protocols/mocks" 11 | "github.com/stretchr/testify/mock" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func testConn(t *testing.T) (net.Conn, func() error) { 16 | l, err := net.Listen("tcp", ":1235") 17 | require.NoError(t, err) 18 | require.NotNil(t, l) 19 | conn, err := net.Dial("tcp", ":1235") 20 | require.NoError(t, err) 21 | err = conn.SetDeadline(time.Now().Add(time.Millisecond)) 22 | require.NoError(t, err) 23 | return conn, l.Close 24 | } 25 | 26 | func TestMapUDPProtocolHandlers(t *testing.T) { 27 | h := &mocks.MockHoneypot{} 28 | h.EXPECT().ProduceUDP(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() 29 | 30 | l := &mocks.MockLogger{} 31 | l.EXPECT().Info(mock.Anything).Return().Maybe() 32 | 33 | m := MapUDPProtocolHandlers(l, h) 34 | require.NotEmpty(t, m, "should get a non-empty map") 35 | h.AssertExpectations(t) 36 | l.AssertExpectations(t) 37 | require.Contains(t, m, "udp", "expected UDP handler") 38 | ctx := context.Background() 39 | err := m["udp"](ctx, &net.UDPAddr{}, &net.UDPAddr{}, []byte{}, connection.Metadata{}) 40 | require.NoError(t, err, "expected no error from connection handler") 41 | } 42 | 43 | func TestMapTCPProtocolHandlers(t *testing.T) { 44 | h := &mocks.MockHoneypot{} 45 | l := &mocks.MockLogger{} 46 | l.EXPECT().Debug(mock.Anything, mock.Anything).Return().Maybe() 47 | 48 | m := MapTCPProtocolHandlers(l, h) 49 | require.NotEmpty(t, m, "should get a non-empty map") 50 | h.AssertExpectations(t) 51 | l.AssertExpectations(t) 52 | require.Contains(t, m, "tcp", "expected TCP handler") 53 | ctx := context.Background() 54 | conn, close := testConn(t) 55 | defer close() 56 | err := m["tcp"](ctx, conn, connection.Metadata{}) 57 | require.NoError(t, err, "expected no error from connection handler") 58 | } 59 | -------------------------------------------------------------------------------- /protocols/tcp/adb.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "net" 9 | "strconv" 10 | 11 | "github.com/mushorg/glutton/connection" 12 | "github.com/mushorg/glutton/producer" 13 | "github.com/mushorg/glutton/protocols/interfaces" 14 | ) 15 | 16 | // readHexLength reads the next 4 bytes from r as an ASCII hex-encoded length and parses them into an int. 17 | func readHexLength(r io.Reader) (int, error) { 18 | lengthHex := make([]byte, 4) 19 | _, err := io.ReadFull(r, lengthHex) 20 | if err != nil { 21 | return 0, err 22 | } 23 | 24 | length, err := strconv.ParseInt(string(lengthHex), 16, 64) 25 | if err != nil { 26 | return 0, err 27 | } 28 | // Clip the length to 255, as per the Google implementation. 29 | if length > 255 { 30 | length = 255 31 | } 32 | 33 | return int(length), nil 34 | } 35 | 36 | // HandleADB Android Debug bridge handler 37 | func HandleADB(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 38 | defer func() { 39 | if err := conn.Close(); err != nil { 40 | logger.Error("Failed to close ADB connection", slog.String("handler", "adb"), producer.ErrAttr(err)) 41 | } 42 | }() 43 | length, err := readHexLength(conn) 44 | if err != nil { 45 | return err 46 | } 47 | data := make([]byte, length) 48 | n, err := io.ReadFull(conn, data) 49 | if err != nil && err != io.ErrUnexpectedEOF { 50 | return fmt.Errorf("error reading message data: %w", err) 51 | } else if err == io.ErrUnexpectedEOF { 52 | return fmt.Errorf("incomplete message data: got %d, want %d. Error: %w", n, length, err) 53 | } 54 | 55 | if err = h.ProduceTCP("adb", conn, md, data, nil); err != nil { 56 | logger.Error("Failed to produce message", producer.ErrAttr(err), slog.String("handler", "adb")) 57 | } 58 | 59 | logger.Info("handled adb request", slog.Int("data_read", n)) 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /protocols/tcp/bittorrent.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "log/slog" 8 | "net" 9 | 10 | "github.com/mushorg/glutton/connection" 11 | "github.com/mushorg/glutton/producer" 12 | "github.com/mushorg/glutton/protocols/helpers" 13 | "github.com/mushorg/glutton/protocols/interfaces" 14 | ) 15 | 16 | type bittorrentMsg struct { 17 | Length uint8 `json:"length,omitempty"` 18 | ProtocolIdentifier [19]uint8 `json:"protocol_identifier,omitempty"` 19 | Reserved [8]uint8 `json:"reserved,omitempty"` 20 | InfoHash [20]uint8 `json:"info_hash,omitempty"` 21 | PeerID [20]uint8 `json:"peer_id,omitempty"` 22 | } 23 | 24 | type parsedBittorrent struct { 25 | Direction string `json:"direction,omitempty"` 26 | Message bittorrentMsg `json:"message,omitempty"` 27 | Payload []byte `json:"payload,omitempty"` 28 | } 29 | 30 | type bittorrentServer struct { 31 | events []parsedBittorrent 32 | } 33 | 34 | // HandleBittorrent handles a Bittorrent connection 35 | func HandleBittorrent(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 36 | server := bittorrentServer{ 37 | events: []parsedBittorrent{}, 38 | } 39 | defer func() { 40 | if err := h.ProduceTCP("bittorrent", conn, md, helpers.FirstOrEmpty[parsedBittorrent](server.events).Payload, server.events); err != nil { 41 | logger.Error("Failed to produce message", producer.ErrAttr(err), slog.String("handler", "bittorrent")) 42 | } 43 | if err := conn.Close(); err != nil { 44 | logger.Error("Failed to close connection", producer.ErrAttr(err), slog.String("handler", "bittorrent")) 45 | return 46 | } 47 | }() 48 | 49 | buffer := make([]byte, 1024) 50 | for { 51 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 52 | logger.Debug("Failed to set connection timeout", producer.ErrAttr(err), slog.String("handler", "bittorrent")) 53 | return nil 54 | } 55 | n, err := conn.Read(buffer) 56 | if err != nil { 57 | logger.Debug("Failed to read data", producer.ErrAttr(err), slog.String("handler", "bittorrent")) 58 | break 59 | } 60 | 61 | if n <= 0 { 62 | break 63 | } 64 | 65 | msg := bittorrentMsg{} 66 | if err := binary.Read(bytes.NewReader(buffer[:n]), binary.BigEndian, &msg); err != nil { 67 | logger.Error("Failed to read message", producer.ErrAttr(err), slog.String("handler", "bittorrent")) 68 | break 69 | } 70 | 71 | server.events = append(server.events, parsedBittorrent{ 72 | Direction: "read", 73 | Message: msg, 74 | Payload: buffer[:n], 75 | }) 76 | 77 | logger.Info( 78 | "bittorrent received", 79 | slog.String("handler", "bittorrent"), 80 | slog.Any("peer_id", msg.PeerID[:]), 81 | slog.Any("inf_hash", msg.InfoHash[:]), 82 | ) 83 | 84 | server.events = append(server.events, parsedBittorrent{ 85 | Direction: "write", 86 | Message: msg, 87 | Payload: buffer[:n], 88 | }) 89 | if err = binary.Write(conn, binary.BigEndian, msg); err != nil { 90 | logger.Error("Failed to write message", producer.ErrAttr(err), slog.String("handler", "bittorrent")) 91 | break 92 | } 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /protocols/tcp/ethereum_rpc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The go-ethereum Authors 2 | // This file is part of the go-ethereum library. 3 | // 4 | // The go-ethereum library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The go-ethereum library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | 17 | package tcp 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | ) 23 | 24 | type jsonError struct { 25 | Code int `json:"code"` 26 | Message string `json:"message"` 27 | Data interface{} `json:"data,omitempty"` 28 | } 29 | 30 | type jsonrpcMessage struct { 31 | Version string `json:"jsonrpc,omitempty"` 32 | ID int `json:"id,omitempty"` 33 | Method string `json:"method,omitempty"` 34 | Params json.RawMessage `json:"params,omitempty"` 35 | Error *jsonError `json:"error,omitempty"` 36 | Result json.RawMessage `json:"result,omitempty"` 37 | } 38 | 39 | // isBatch returns true when the first non-whitespace characters is '[' 40 | func isBatch(raw json.RawMessage) bool { 41 | for _, c := range raw { 42 | // skip insignificant whitespace (http://www.ietf.org/rfc/rfc4627.txt) 43 | if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d { 44 | continue 45 | } 46 | return c == '[' 47 | } 48 | return false 49 | } 50 | 51 | func parseMessage(raw json.RawMessage) ([]*jsonrpcMessage, bool) { 52 | if !isBatch(raw) { 53 | msgs := []*jsonrpcMessage{{}} 54 | json.Unmarshal(raw, &msgs[0]) 55 | return msgs, false 56 | } 57 | dec := json.NewDecoder(bytes.NewReader(raw)) 58 | dec.Token() // skip '[' 59 | var msgs []*jsonrpcMessage 60 | for dec.More() { 61 | msgs = append(msgs, new(jsonrpcMessage)) 62 | dec.Decode(&msgs[len(msgs)-1]) 63 | } 64 | return msgs, true 65 | } 66 | 67 | func blockNumber(message *jsonrpcMessage) (*jsonrpcMessage, error) { 68 | res, err := json.Marshal("0x4b7") 69 | return &jsonrpcMessage{ 70 | ID: message.ID, 71 | Version: message.Version, 72 | Result: res, 73 | }, err 74 | } 75 | 76 | func accounts(message *jsonrpcMessage) (*jsonrpcMessage, error) { 77 | res, err := json.Marshal([]string{"0x407d73d8a49eeb85d32cf465507dd71d507100c1"}) 78 | return &jsonrpcMessage{ 79 | ID: message.ID, 80 | Version: message.Version, 81 | Result: res, 82 | }, err 83 | } 84 | 85 | func getBlockByNumber(message *jsonrpcMessage) (*jsonrpcMessage, error) { 86 | result := struct { 87 | Difficulty string `json:"difficulty"` 88 | ExtraData string `json:"extraData"` 89 | GasLimit string `json:"gasLimit"` 90 | GasUsed string `json:"gasUsed"` 91 | Hash string `json:"hash"` 92 | LogsBloom string `json:"logsBloom"` 93 | Miner string `json:"miner"` 94 | MixHash string `json:"mixHash"` 95 | Nonce string `json:"nonce"` 96 | Number string `json:"number"` 97 | ParentHash string `json:"parentHash"` 98 | ReceiptsRoot string `json:"receiptsRoot"` 99 | Sha3Uncles string `json:"sha3Uncles"` 100 | Size string `json:"size"` 101 | StateRoot string `json:"stateRoot"` 102 | Timestamp string `json:"timestamp"` 103 | TotalDifficulty string `json:"totalDifficulty"` 104 | Transactions []interface{} `json:"transactions"` 105 | TransactionsRoot string `json:"transactionsRoot"` 106 | Uncles []interface{} `json:"uncles"` 107 | }{ 108 | Difficulty: "0x4ea3f27bc", 109 | ExtraData: "0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32", 110 | GasLimit: "0x1388", 111 | GasUsed: "0x0", 112 | Hash: "0xdc0818cf78f21a8e70579cb46a43643f78291264dda342ae31049421c82d21ae", 113 | LogsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 114 | Miner: "0xbb7b8287f3f0a933474a79eae42cbca977791171", 115 | MixHash: "0x4fffe9ae21f1c9e15207b1f472d5bbdd68c9595d461666602f2be20daf5e7843", 116 | Nonce: "0x689056015818adbe", 117 | Number: "0x1b4", 118 | ParentHash: "0xe99e022112df268087ea7eafaf4790497fd21dbeeb6bd7a1721df161a6657a54", 119 | ReceiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", 120 | Sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", 121 | Size: "0x220", 122 | StateRoot: "0xddc8b0234c2e0cad087c8b389aa7ef01f7d79b2570bccb77ce48648aa61c904d", 123 | Timestamp: "0x55ba467c", 124 | TotalDifficulty: "0x78ed983323d", 125 | Transactions: []interface{}{}, 126 | TransactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", 127 | Uncles: []interface{}{}, 128 | } 129 | res, err := json.Marshal(result) 130 | return &jsonrpcMessage{ 131 | ID: message.ID, 132 | Version: message.Version, 133 | Result: res, 134 | }, err 135 | } 136 | 137 | func handleEthereumRPC(body []byte) ([]byte, error) { 138 | var rawmsg json.RawMessage 139 | err := json.NewDecoder(bytes.NewReader(body)).Decode(&rawmsg) 140 | if err != nil { 141 | return nil, err 142 | } 143 | messages, _ := parseMessage(rawmsg) 144 | for i, msg := range messages { 145 | if msg == nil { 146 | messages[i] = new(jsonrpcMessage) 147 | } 148 | } 149 | 150 | responses := []*jsonrpcMessage{} 151 | var response *jsonrpcMessage 152 | for _, message := range messages { 153 | switch message.Method { 154 | case "eth_blockNumber": 155 | response, err = blockNumber(message) 156 | case "eth_getBlockByNumber": 157 | response, err = getBlockByNumber(message) 158 | case "eth_accounts": 159 | response, err = accounts(message) 160 | default: 161 | continue 162 | } 163 | if err != nil { 164 | return nil, err 165 | } 166 | responses = append(responses, response) 167 | } 168 | return json.Marshal(responses) 169 | } 170 | -------------------------------------------------------------------------------- /protocols/tcp/ftp.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "net" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/mushorg/glutton/connection" 14 | "github.com/mushorg/glutton/producer" 15 | "github.com/mushorg/glutton/protocols/helpers" 16 | "github.com/mushorg/glutton/protocols/interfaces" 17 | ) 18 | 19 | type parsedFTP struct { 20 | Direction string `json:"direction,omitempty"` 21 | Payload []byte `json:"payload,omitempty"` 22 | } 23 | 24 | type ftpServer struct { 25 | events []parsedFTP 26 | conn net.Conn 27 | } 28 | 29 | func (s *ftpServer) read(_ interfaces.Logger, _ interfaces.Honeypot) (string, error) { 30 | msg, err := bufio.NewReader(s.conn).ReadString('\n') 31 | if err != nil { 32 | return msg, err 33 | } 34 | s.events = append(s.events, parsedFTP{ 35 | Direction: "read", 36 | Payload: []byte(msg), 37 | }) 38 | return msg, nil 39 | } 40 | 41 | func (s *ftpServer) write(msg string) error { 42 | _, err := s.conn.Write([]byte(msg)) 43 | if err != nil { 44 | return err 45 | } 46 | s.events = append(s.events, parsedFTP{ 47 | Direction: "write", 48 | Payload: []byte(msg), 49 | }) 50 | return nil 51 | 52 | } 53 | 54 | // HandleFTP takes a net.Conn and does basic FTP communication 55 | func HandleFTP(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 56 | server := ftpServer{ 57 | conn: conn, 58 | } 59 | defer func() { 60 | if err := h.ProduceTCP("ftp", conn, md, helpers.FirstOrEmpty[parsedFTP](server.events).Payload, server.events); err != nil { 61 | logger.Error("Failed to produce events", slog.String("protocol", "ftp"), producer.ErrAttr(err)) 62 | } 63 | if err := conn.Close(); err != nil { 64 | logger.Error("Failed to close FTP connection", slog.String("protocol", "ftp"), producer.ErrAttr(err)) 65 | } 66 | }() 67 | 68 | host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if _, err := conn.Write([]byte("220 Welcome!\r\n")); err != nil { 74 | return err 75 | } 76 | for { 77 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 78 | logger.Debug("Failed to set connection timeout", slog.String("protocol", "ftp"), producer.ErrAttr(err)) 79 | return nil 80 | } 81 | msg, err := server.read(logger, h) 82 | if err != nil || err != io.EOF { 83 | logger.Debug("Failed to read data", slog.String("protocol", "ftp"), producer.ErrAttr(err)) 84 | break 85 | } 86 | if len(msg) < 4 { 87 | continue 88 | } 89 | cmd := strings.ToUpper(msg[:4]) 90 | 91 | logger.Info( 92 | "ftp payload received", 93 | slog.String("dest_port", strconv.Itoa(int(md.TargetPort))), 94 | slog.String("src_ip", host), 95 | slog.String("src_port", port), 96 | slog.String("message", fmt.Sprintf("%q", msg)), 97 | slog.String("handler", "ftp"), 98 | ) 99 | 100 | var resp string 101 | switch cmd { 102 | case "USER": 103 | resp = "331 OK.\r\n" 104 | case "PASS": 105 | resp = "230 OK.\r\n" 106 | default: 107 | resp = "200 OK.\r\n" 108 | } 109 | if err := server.write(resp); err != nil { 110 | return err 111 | } 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /protocols/tcp/http.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "log/slog" 11 | "net" 12 | "net/http" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/mushorg/glutton/connection" 17 | "github.com/mushorg/glutton/producer" 18 | "github.com/mushorg/glutton/protocols/interfaces" 19 | ) 20 | 21 | // formatRequest generates ascii representation of a request 22 | func formatRequest(r *http.Request) string { 23 | // Create return string 24 | var request []string 25 | // Add the request string 26 | url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto) 27 | request = append(request, url) 28 | // Add the host 29 | request = append(request, fmt.Sprintf("Host: %v", r.Host)) 30 | // Loop through headers 31 | for name, headers := range r.Header { 32 | name = strings.ToLower(name) 33 | for _, h := range headers { 34 | request = append(request, fmt.Sprintf("%v: %v", name, h)) 35 | } 36 | } 37 | 38 | // If this is a POST, add post data 39 | if r.Method == "POST" { 40 | r.ParseForm() 41 | request = append(request, "\n") 42 | request = append(request, r.Form.Encode()) 43 | } 44 | // Return the request as a string 45 | return strings.Join(request, "\n") 46 | } 47 | 48 | func sendJSON(data []byte, conn net.Conn) error { 49 | _, err := conn.Write(append([]byte(fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Length:%d\r\n\r\n", len(data))), data...)) 50 | return err 51 | } 52 | 53 | func handlePOST(req *http.Request, conn net.Conn, buf *bytes.Buffer, logger interfaces.Logger) error { 54 | body := buf.Bytes() 55 | // Ethereum RPC call 56 | if strings.Contains(string(body), "eth_blockNumber") { 57 | data, err := handleEthereumRPC(body) 58 | if err != nil { 59 | return err 60 | } 61 | return sendJSON(data, conn) 62 | } 63 | // Hadoop YARN hack 64 | if strings.Contains(req.RequestURI, "cluster/apps/new-application") { 65 | resp, err := json.Marshal( 66 | &struct { 67 | ApplicationID string `json:"application-id"` 68 | MaximumResourceCapability interface{} `json:"maximum-resource-capability"` 69 | }{ 70 | ApplicationID: "application_1527144634877_20465", 71 | MaximumResourceCapability: struct { 72 | Memory int `json:"memory"` 73 | VCores int `json:"vCores"` 74 | }{ 75 | Memory: 16384, 76 | VCores: 8, 77 | }, 78 | }, 79 | ) 80 | if err != nil { 81 | return err 82 | } 83 | logger.Info("sending hadoop yarn hack response") 84 | return sendJSON(resp, conn) 85 | } 86 | return nil 87 | } 88 | 89 | // scanning attempts for CVE-2019-19781 90 | // based on https://github.com/x1sec/citrix-honeypot/ 91 | func smbHandler(conn net.Conn, _ *http.Request) error { 92 | // if strings.ContainsRune(r.URL.RawPath, '%') { 93 | // with IDS evasion." 94 | // } 95 | 96 | headers := `Server: Apache 97 | X-Frame-Options: SAMEORIGIN 98 | Last-Modified: Thu, 28 Nov 2019 20:19:22 GMT 99 | ETag: "53-5986dd42b0680" 100 | Accept-Ranges: bytes 101 | Content-Length: 93 102 | X-XSS-Protection: 1; mode=block 103 | X-Content-Type-Options: nosniff 104 | Content-Type: text/plain; charset=UTF-8` 105 | 106 | smbConfig := "\r\n\r\n[global]\r\n\tencrypt passwords = yes\r\n\tname resolve order = lmhosts wins host bcast\r\n" 107 | _, err := conn.Write([]byte("HTTP/1.1 200 OK\r\n" + headers + smbConfig)) 108 | return err 109 | } 110 | 111 | type decodedHTTP struct { 112 | Method string `json:"method,omitempty"` 113 | URL string `json:"url,omitempty"` 114 | Path string `json:"path,omitempty"` 115 | Query string `json:"query,omitempty"` 116 | } 117 | 118 | // HandleHTTP takes a net.Conn and does basic HTTP communication 119 | func HandleHTTP(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 120 | defer func() { 121 | err := conn.Close() 122 | if err != nil { 123 | logger.Error("Failed to close the HTTP connection", producer.ErrAttr(err)) 124 | } 125 | }() 126 | 127 | req, err := http.ReadRequest(bufio.NewReader(conn)) 128 | if err != nil { 129 | return fmt.Errorf("failed to read the HTTP request: %w", err) 130 | } 131 | 132 | host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) 133 | if err != nil { 134 | return fmt.Errorf("failed to split the host: %w", err) 135 | } 136 | 137 | logger.Info( 138 | fmt.Sprintf("HTTP %s request handled: %s", req.Method, req.URL.EscapedPath()), 139 | slog.String("handler", "http"), 140 | slog.String("dest_port", strconv.Itoa(int(md.TargetPort))), 141 | slog.String("src_ip", host), 142 | slog.String("src_port", port), 143 | slog.String("path", req.URL.EscapedPath()), 144 | slog.String("method", req.Method), 145 | slog.String("query", req.URL.Query().Encode()), 146 | ) 147 | 148 | buf := &bytes.Buffer{} 149 | if req.ContentLength > 0 { 150 | defer req.Body.Close() 151 | buf = bytes.NewBuffer(make([]byte, 0, req.ContentLength)) 152 | length, err := buf.ReadFrom(req.Body) 153 | if err != nil { 154 | return fmt.Errorf("failed to read the HTTP body: %w", err) 155 | } 156 | logger.Info(fmt.Sprintf("HTTP payload:\n%s", hex.Dump(buf.Bytes()[:length%1024]))) 157 | } 158 | 159 | if err := h.ProduceTCP("http", conn, md, buf.Bytes(), decodedHTTP{ 160 | Method: req.Method, 161 | URL: req.URL.EscapedPath(), 162 | Path: req.URL.EscapedPath(), 163 | Query: req.URL.Query().Encode(), 164 | }); err != nil { 165 | logger.Error("Failed to produce message", slog.String("protocol", "http"), producer.ErrAttr(err)) 166 | } 167 | 168 | switch req.Method { 169 | case http.MethodPost: 170 | return handlePOST(req, conn, buf, logger) 171 | } 172 | 173 | if strings.Contains(req.RequestURI, "wallet") { 174 | logger.Info( 175 | "HTTP wallet request", 176 | slog.String("handler", "http"), 177 | ) 178 | _, err = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length:20\r\n\r\n[[\"\"]]\r\n\r\n")) 179 | return err 180 | } 181 | 182 | if strings.Contains(req.RequestURI, "/v1.16/version") { 183 | data, err := res.ReadFile("resources/docker_api.json") 184 | if err != nil { 185 | return fmt.Errorf("failed to read embedded file: %w", err) 186 | } 187 | _, err = conn.Write(append([]byte(fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Length:%d\r\n\r\n", len(data))), data...)) 188 | return err 189 | } 190 | 191 | if strings.HasPrefix(req.RequestURI, "/vpn/") { 192 | return smbHandler(conn, req) 193 | } 194 | 195 | // Handler for VMWare Attack 196 | if strings.Contains(req.RequestURI, "hyper/send") { 197 | body := string(buf.Bytes()[:]) 198 | parts := strings.Split(body, " ") 199 | if len(parts) >= 11 { 200 | conn, err := net.Dial("tcp", parts[9]+":"+parts[10]) 201 | if err != nil { 202 | return err 203 | } 204 | go func() { 205 | if err := HandleTCP(ctx, conn, md, logger, h); err != nil { 206 | logger.Error("Failed to handle vmware attack", producer.ErrAttr(err)) 207 | } 208 | }() 209 | } 210 | } 211 | _, err = conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) 212 | if err != nil { 213 | return fmt.Errorf("failed to send HTTP response: %w", err) 214 | } 215 | return nil 216 | } 217 | -------------------------------------------------------------------------------- /protocols/tcp/http_test.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFormatRequest(t *testing.T) { 11 | mockReq, err := http.NewRequest("GET", "http://example.com", nil) 12 | require.NoError(t, err) 13 | require.Equal(t, "GET http://example.com HTTP/1.1\nHost: example.com", formatRequest(mockReq)) 14 | } 15 | -------------------------------------------------------------------------------- /protocols/tcp/iscsi.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "io" 7 | "log/slog" 8 | "net" 9 | 10 | "github.com/mushorg/glutton/connection" 11 | "github.com/mushorg/glutton/producer" 12 | "github.com/mushorg/glutton/protocols/interfaces" 13 | "github.com/mushorg/glutton/protocols/tcp/iscsi" 14 | ) 15 | 16 | type ParsedIscsi struct { 17 | Direction string `json:"direction,omitempty"` 18 | Message iscsi.IscsiMsg `json:"message,omitempty"` 19 | Payload []byte `json:"payload,omitempty"` 20 | } 21 | type iscsiServer struct { 22 | events []ParsedIscsi 23 | conn net.Conn 24 | } 25 | 26 | // iSCSI messages contain a 48 byte header. The first byte contains the Opcode(Operation Code) which defines the type of operation that is to be performed. 27 | func (si *iscsiServer) handleISCSIMessage(conn net.Conn, md connection.Metadata, buffer []byte, logger interfaces.Logger, h interfaces.Honeypot, n int) error { 28 | defer func() { 29 | if err := h.ProduceTCP("iscsi", conn, md, buffer, si.events); err != nil { 30 | logger.Error("failed to produce message", slog.String("handler", "iscsi"), producer.ErrAttr(err)) 31 | } 32 | }() 33 | 34 | msg, res, _, err := iscsi.ParseISCSIMessage(buffer) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | si.events = append(si.events, ParsedIscsi{ 40 | Direction: "read", 41 | Message: msg, 42 | Payload: buffer[:n], 43 | }) 44 | 45 | logger.Info("received iSCSI message", slog.String("opcode", string(msg.Opcode)), slog.String("handler", "iscsi")) 46 | 47 | if err := binary.Write(conn, binary.BigEndian, res); err != nil { 48 | return err 49 | } 50 | 51 | si.events = append(si.events, ParsedIscsi{ 52 | Direction: "write", 53 | Message: res, 54 | Payload: buffer[:], 55 | }) 56 | 57 | return nil 58 | } 59 | 60 | func HandleISCSI(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 61 | var err error 62 | defer func() { 63 | err = conn.Close() 64 | if err != nil { 65 | logger.Error("failed to close iSCSI connection", slog.String("protocol", "iscsi"), producer.ErrAttr(err)) 66 | } 67 | }() 68 | 69 | server := &iscsiServer{ 70 | events: []ParsedIscsi{}, 71 | conn: conn, 72 | } 73 | 74 | buffer := make([]byte, 4096) 75 | for { 76 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 77 | logger.Error("failed to set connection timeout", slog.String("protocol", "iscsi"), producer.ErrAttr(err)) 78 | break 79 | } 80 | n, err := conn.Read(buffer) 81 | if err != nil && err != io.ErrUnexpectedEOF { 82 | logger.Error("failed to read from connection", slog.String("protocol", "iscsi"), producer.ErrAttr(err)) 83 | break 84 | } 85 | if err == io.ErrUnexpectedEOF { 86 | logger.Error("failed to read the complete file", slog.String("protocol", "iscsi"), producer.ErrAttr(err)) 87 | } 88 | err = server.handleISCSIMessage(conn, md, buffer, logger, h, n) 89 | if err != nil { 90 | logger.Error("failed to handle iSCSI message", slog.String("protocol", "iscsi"), producer.ErrAttr(err)) 91 | break 92 | } 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /protocols/tcp/iscsi/iscsi.go: -------------------------------------------------------------------------------- 1 | package iscsi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | ) 8 | 9 | type IscsiMsg struct { 10 | Opcode uint8 11 | Flags uint8 12 | TaskTag uint32 13 | Data uint32 14 | CID uint32 15 | LUN uint64 16 | } 17 | 18 | func ParseISCSIMessage(buffer []byte) (IscsiMsg, IscsiMsg, []byte, error) { 19 | if len(buffer) < 48 { 20 | return IscsiMsg{}, IscsiMsg{}, nil, fmt.Errorf("Incomplete iSCSI message") 21 | } 22 | msg := IscsiMsg{} 23 | r := bytes.NewReader(buffer) 24 | if err := binary.Read(r, binary.BigEndian, &msg); err != nil { 25 | return IscsiMsg{}, IscsiMsg{}, nil, fmt.Errorf("error reading iSCSI message: %w", err) 26 | } 27 | 28 | var res IscsiMsg 29 | switch msg.Opcode { 30 | case 0x03: 31 | res = IscsiMsg{ 32 | Opcode: 0x23, // Login response 33 | Flags: 0x00, 34 | TaskTag: msg.TaskTag, 35 | Data: 0, 36 | CID: msg.CID, 37 | LUN: msg.LUN, 38 | } 39 | case 0x01: //Initiator SCSI Command 40 | res = IscsiMsg{ 41 | Opcode: 0x21, // Target SCSI response 42 | Flags: 0x00, 43 | TaskTag: msg.TaskTag, 44 | Data: 8, //Can vary 45 | CID: msg.CID, 46 | LUN: msg.LUN, 47 | } 48 | 49 | case 0x06: // Logout Request 50 | res = IscsiMsg{ 51 | Opcode: 0x26, // Logout Response 52 | Flags: 0x00, 53 | TaskTag: msg.TaskTag, 54 | Data: 0, 55 | CID: msg.CID, 56 | LUN: msg.LUN, 57 | } 58 | default: 59 | res = IscsiMsg{ 60 | Opcode: 0x20, // No Operation response 61 | Flags: 0x00, 62 | TaskTag: msg.TaskTag, 63 | Data: 0x00000001, // A generic error data 64 | CID: msg.CID, 65 | LUN: msg.LUN, 66 | } 67 | } 68 | 69 | buf := new(bytes.Buffer) 70 | if err := binary.Write(buf, binary.BigEndian, res); err != nil { 71 | return IscsiMsg{}, IscsiMsg{}, nil, fmt.Errorf("failed to write response: %w", err) 72 | } 73 | return msg, res, buf.Bytes(), nil 74 | } 75 | -------------------------------------------------------------------------------- /protocols/tcp/iscsi/iscsi_test.go: -------------------------------------------------------------------------------- 1 | package iscsi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestHandleISCSIMessage(t *testing.T) { 10 | 11 | data := []byte{ 12 | 0x03, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x40, 0x00, 0x01, 0x37, 13 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 14 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 15 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x69, 0x74, 16 | 0x69, 0x61, 0x74, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x3d, 0x69, 0x71, 17 | 0x6e, 0x2e, 0x31, 0x39, 0x39, 0x31, 0x2d, 0x30, 0x35, 0x2e, 0x63, 0x6f, 18 | 0x6d, 0x2e, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x3a, 19 | 0x6e, 0x6d, 0x61, 0x70, 0x5f, 0x69, 0x73, 0x63, 0x73, 0x69, 0x5f, 0x70, 20 | 0x72, 0x6f, 0x62, 0x65, 0x00, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 21 | 0x54, 0x79, 0x70, 0x65, 0x3d, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 22 | 0x72, 0x79, 0x00, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 23 | 0x64, 0x3d, 0x4e, 0x6f, 0x6e, 0x65, 0x00, 0x00, 24 | } 25 | _, res, _, err := ParseISCSIMessage(data) 26 | require.NoError(t, err, "Error occurred while parsing ISCSI message") 27 | 28 | expectedRes := IscsiMsg{ 29 | Opcode: 0x23, 30 | Flags: 0x00, 31 | TaskTag: 0, 32 | Data: 0, 33 | CID: 20381696, 34 | LUN: 65537, 35 | } 36 | 37 | if res != expectedRes { 38 | require.Equal(t, expectedRes, res, "Expected response does not match the actual response") 39 | } 40 | 41 | } 42 | 43 | // Response: {Opcode:35 Flags:0 TaskTag:0 Data:0 CID:20381696 LUN:65537 Status:0} 44 | -------------------------------------------------------------------------------- /protocols/tcp/jabber.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/xml" 7 | "fmt" 8 | "log/slog" 9 | "net" 10 | "strconv" 11 | 12 | "github.com/mushorg/glutton/connection" 13 | "github.com/mushorg/glutton/producer" 14 | "github.com/mushorg/glutton/protocols/interfaces" 15 | ) 16 | 17 | // ServersJabber defines servers structure 18 | type ServersJabber struct { 19 | XMLName xml.Name `xml:"servers"` 20 | Version string `xml:"version,attr"` 21 | Svs []serverJabber `xml:"server"` 22 | } 23 | 24 | // define server structure in Jabber protocol 25 | type serverJabber struct { 26 | ServerName string `xml:"serverName"` 27 | ServerIP string `xml:"serverIP"` 28 | } 29 | 30 | // JabberClient structure in Jabber protocol 31 | type JabberClient struct { 32 | STo string `xml:"to,attr"` 33 | Version string `xml:"version,attr"` 34 | XMLns string `xml:"xmlns,attr"` 35 | ID string `xml:"id,attr"` 36 | XMLnsStream string `xml:"xmlns stream,attr"` 37 | XMLName xml.Name `xml:"http://etherx.jabber.org/streams stream"` 38 | } 39 | 40 | // parse Jabber client 41 | func parseJabberClient(conn net.Conn, md connection.Metadata, dataClient []byte, logger interfaces.Logger, h interfaces.Honeypot) error { 42 | v := JabberClient{STo: "none", Version: "none"} 43 | if err := xml.Unmarshal(dataClient, &v); err != nil { 44 | return err 45 | } 46 | 47 | host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if err = h.ProduceTCP("jabber", conn, md, dataClient, v); err != nil { 53 | logger.Error("Failed to produce message", producer.ErrAttr(err), slog.String("handler", "jabber")) 54 | } 55 | 56 | logger.Info( 57 | fmt.Sprintf("STo : %v Version: %v XMLns: %v XMLName: %v", v.STo, v.Version, v.XMLns, v.XMLName), 58 | slog.String("handler", "jabber"), 59 | slog.String("dest_port", strconv.Itoa(int(md.TargetPort))), 60 | slog.String("src_ip", host), 61 | slog.String("src_port", port), 62 | ) 63 | return nil 64 | } 65 | 66 | // read client msg 67 | func readMsgJabber(conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 68 | r := bufio.NewReader(conn) 69 | line, _, err := r.ReadLine() 70 | if err != nil { 71 | logger.Debug("Failed to read line", slog.String("handler", "jabber"), producer.ErrAttr(err)) 72 | return nil 73 | } 74 | return parseJabberClient(conn, md, line[:1024], logger, h) 75 | } 76 | 77 | // HandleJabber main handler 78 | func HandleJabber(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 79 | defer func() { 80 | if err := conn.Close(); err != nil { 81 | logger.Error("Failed to close connection", slog.String("handler", "jabber"), producer.ErrAttr(err)) 82 | } 83 | }() 84 | 85 | v := &ServersJabber{Version: "1"} 86 | v.Svs = append(v.Svs, serverJabber{"Test_VPN", "127.0.0.1"}) 87 | 88 | output, err := xml.MarshalIndent(v, " ", " ") 89 | if err != nil { 90 | return err 91 | } 92 | if _, err := conn.Write(output); err != nil { 93 | return err 94 | } 95 | if err := readMsgJabber(conn, md, logger, h); err != nil { 96 | return err 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /protocols/tcp/memcache.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/mushorg/glutton/connection" 11 | "github.com/mushorg/glutton/producer" 12 | "github.com/mushorg/glutton/protocols/interfaces" 13 | ) 14 | 15 | func HandleMemcache(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 16 | var dataMap = map[string]string{} 17 | buffer := make([]byte, 1024) 18 | for { 19 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 20 | return err 21 | } 22 | n, err := conn.Read(buffer) 23 | if err != nil { 24 | return err 25 | } 26 | if n == 0 { 27 | break 28 | } 29 | 30 | if err = h.ProduceTCP("memcache", conn, md, buffer, nil); err != nil { 31 | logger.Error("Failed to produce message", producer.ErrAttr(err), slog.String("handler", "memcache")) 32 | } 33 | 34 | parts := strings.Split(string(buffer[:]), " ") 35 | switch parts[0] { 36 | case "set": 37 | if len(parts) < 6 { 38 | break 39 | } 40 | dataMap[parts[1]] = parts[5] 41 | case "get": 42 | if len(parts) < 2 { 43 | break 44 | } 45 | val := dataMap[parts[1]] 46 | _, err := conn.Write([]byte(parts[1] + " 0 " + strconv.Itoa(len(val)) + " " + val + "\r\n")) 47 | if err != nil { 48 | return err 49 | } 50 | } 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /protocols/tcp/mongodb.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "fmt" 9 | "io" 10 | "log/slog" 11 | "net" 12 | "strconv" 13 | 14 | "github.com/mushorg/glutton/connection" 15 | "github.com/mushorg/glutton/producer" 16 | "github.com/mushorg/glutton/protocols/helpers" 17 | "github.com/mushorg/glutton/protocols/interfaces" 18 | ) 19 | 20 | type mongoMsgHeader struct { 21 | MessageLength int32 22 | RequestID int32 23 | ResponseTo int32 24 | OpCode int32 25 | } 26 | 27 | // OpCode values for the MongoDB wire protocol 28 | const ( 29 | OpReply = 1 30 | OpUpdate = 2001 31 | OpInsert = 2002 32 | OpQuery = 2004 33 | OpGetMore = 2005 34 | OpDelete = 2006 35 | OpKillCursors = 2007 36 | OpCompressed = 2012 37 | OpMsg = 2013 38 | ) 39 | 40 | var opCodeNames = map[int32]string{ 41 | OpReply: "OP_REPLY", 42 | OpUpdate: "OP_UPDATE", 43 | OpInsert: "OP_INSERT", 44 | OpQuery: "OP_QUERY", 45 | OpGetMore: "OP_GET_MORE", 46 | OpDelete: "OP_DELETE", 47 | OpKillCursors: "OP_KILL_CURSORS", 48 | OpCompressed: "OP_COMPRESSED", 49 | OpMsg: "OP_MSG", 50 | } 51 | 52 | type parsedMongoDB struct { 53 | Direction string `json:"direction,omitempty"` 54 | Header mongoMsgHeader `json:"header,omitempty"` 55 | Payload []byte `json:"payload,omitempty"` 56 | OpCodeStr string `json:"opcode_str,omitempty"` 57 | } 58 | 59 | type mongoDBServer struct { 60 | events []parsedMongoDB 61 | conn net.Conn 62 | } 63 | 64 | func (s *mongoDBServer) read() ([]byte, error) { 65 | // read message header 66 | headerBytes := make([]byte, 16) 67 | if _, err := io.ReadFull(s.conn, headerBytes); err != nil { 68 | return nil, err 69 | } 70 | 71 | var header mongoMsgHeader 72 | if err := binary.Read(bytes.NewReader(headerBytes), binary.LittleEndian, &header); err != nil { 73 | return nil, err 74 | } 75 | 76 | // check to prevent excessive mem alloc 77 | if header.MessageLength <= 0 || header.MessageLength > 48*1024*1024 { 78 | return nil, fmt.Errorf("invalid MongoDB message length: %d", header.MessageLength) 79 | } 80 | 81 | fullMessage := make([]byte, header.MessageLength) 82 | 83 | copy(fullMessage, headerBytes) 84 | 85 | if _, err := io.ReadFull(s.conn, fullMessage[16:]); err != nil { 86 | return nil, err 87 | } 88 | 89 | return fullMessage, nil 90 | } 91 | 92 | // writes a Mongo message to the connection 93 | func (s *mongoDBServer) write(header mongoMsgHeader, data []byte) error { 94 | _, err := s.conn.Write(data) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | s.events = append(s.events, parsedMongoDB{ 100 | Direction: "write", 101 | Header: header, 102 | Payload: data, 103 | OpCodeStr: opCodeNames[header.OpCode], 104 | }) 105 | 106 | return nil 107 | } 108 | 109 | // creates a basic "ok" response for Mongo queries 110 | func createOkResponse(requestHeader mongoMsgHeader) (mongoMsgHeader, []byte, error) { 111 | buffer := new(bytes.Buffer) 112 | 113 | responseHeader := mongoMsgHeader{ 114 | MessageLength: 0, // will fill in later 115 | RequestID: requestHeader.RequestID + 1, 116 | ResponseTo: requestHeader.RequestID, 117 | OpCode: OpMsg, // using OP_MSG for responses 118 | } 119 | 120 | // write placeholder for header 121 | if err := binary.Write(buffer, binary.LittleEndian, responseHeader); err != nil { 122 | return responseHeader, nil, err 123 | } 124 | 125 | // OP_MSG flags - no special flags set 126 | flagBits := uint32(0) 127 | if err := binary.Write(buffer, binary.LittleEndian, flagBits); err != nil { 128 | return responseHeader, nil, err 129 | } 130 | 131 | // section kind 0 (Body) 132 | sectionKind := byte(0) 133 | if err := binary.Write(buffer, binary.LittleEndian, sectionKind); err != nil { 134 | return responseHeader, nil, err 135 | } 136 | 137 | // simple document with ok:1 138 | document := []byte{ 139 | 0x11, 0x00, 0x00, 0x00, // doc size - 17 bytes 140 | 0x01, 'o', 'k', 0x00, // "ok" (type double) 141 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, // double value 1.0 142 | 0x00, // terminator 143 | } 144 | 145 | if _, err := buffer.Write(document); err != nil { 146 | return responseHeader, nil, err 147 | } 148 | 149 | response := buffer.Bytes() 150 | 151 | messageLength := int32(len(response)) 152 | binary.LittleEndian.PutUint32(response[0:4], uint32(messageLength)) 153 | 154 | responseHeader.MessageLength = messageLength 155 | 156 | return responseHeader, response, nil 157 | } 158 | 159 | func HandleMongoDB(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 160 | server := &mongoDBServer{ 161 | events: []parsedMongoDB{}, 162 | conn: conn, 163 | } 164 | 165 | defer func() { 166 | if err := h.ProduceTCP("mongodb", conn, md, helpers.FirstOrEmpty[parsedMongoDB](server.events).Payload, server.events); err != nil { 167 | logger.Error("Failed to produce MongoDB event", producer.ErrAttr(err), slog.String("protocol", "mongodb")) 168 | } 169 | 170 | if err := conn.Close(); err != nil { 171 | logger.Debug("Failed to close MongoDB connection", producer.ErrAttr(err), slog.String("protocol", "mongodb")) 172 | } 173 | }() 174 | 175 | host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) 176 | if err != nil { 177 | return fmt.Errorf("failed to split remote address: %w", err) 178 | } 179 | 180 | for { 181 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 182 | logger.Debug("Failed to update connection timeout", producer.ErrAttr(err), slog.String("protocol", "mongodb")) 183 | return nil 184 | } 185 | 186 | message, err := server.read() 187 | if err != nil { 188 | if err != io.EOF { 189 | logger.Debug("Failed to read MongoDB message", producer.ErrAttr(err), slog.String("protocol", "mongodb")) 190 | } 191 | break 192 | } 193 | 194 | var header mongoMsgHeader 195 | if err := binary.Read(bytes.NewReader(message[:16]), binary.LittleEndian, &header); err != nil { 196 | logger.Error("Failed to parse MongoDB header", producer.ErrAttr(err), slog.String("protocol", "mongodb")) 197 | break 198 | } 199 | 200 | server.events = append(server.events, parsedMongoDB{ 201 | Direction: "read", 202 | Header: header, 203 | Payload: message, 204 | OpCodeStr: opCodeNames[header.OpCode], 205 | }) 206 | 207 | logger.Info( 208 | "MongoDB message received", 209 | slog.String("dest_port", strconv.Itoa(int(md.TargetPort))), 210 | slog.String("src_ip", host), 211 | slog.String("src_port", port), 212 | slog.String("opcode", opCodeNames[header.OpCode]), 213 | slog.Int("message_length", int(header.MessageLength)), 214 | slog.Int("request_id", int(header.RequestID)), 215 | slog.String("handler", "mongodb"), 216 | ) 217 | 218 | logger.Debug(fmt.Sprintf("MongoDB payload:\n%s", hex.Dump(message))) 219 | 220 | responseHeader, response, err := createOkResponse(header) 221 | if err != nil { 222 | logger.Error("Failed to create MongoDB response", producer.ErrAttr(err), slog.String("protocol", "mongodb")) 223 | break 224 | } 225 | 226 | if err := server.write(responseHeader, response); err != nil { 227 | logger.Error("Failed to write MongoDB response", producer.ErrAttr(err), slog.String("protocol", "mongodb")) 228 | break 229 | } 230 | } 231 | 232 | return nil 233 | } 234 | -------------------------------------------------------------------------------- /protocols/tcp/mongodb_test.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // tests the Mongo response creation function 12 | func TestCreateOkResponse(t *testing.T) { 13 | requestHeader := mongoMsgHeader{ 14 | MessageLength: 100, 15 | RequestID: 12345, 16 | ResponseTo: 0, 17 | OpCode: OpMsg, 18 | } 19 | 20 | responseHeader, response, err := createOkResponse(requestHeader) 21 | require.NoError(t, err) 22 | require.NotNil(t, response) 23 | 24 | require.Equal(t, int32(len(response)), responseHeader.MessageLength) 25 | require.Equal(t, requestHeader.RequestID+1, responseHeader.RequestID) 26 | require.Equal(t, requestHeader.RequestID, responseHeader.ResponseTo) 27 | require.Equal(t, int32(OpMsg), responseHeader.OpCode) 28 | 29 | require.Equal(t, int32(len(response)), int32(binary.LittleEndian.Uint32(response[0:4]))) 30 | 31 | // check flags - should be 0 32 | flags := binary.LittleEndian.Uint32(response[16:20]) 33 | require.Equal(t, uint32(0), flags) 34 | 35 | // check section type - should be 0 (body) 36 | require.Equal(t, byte(0), response[20]) 37 | } 38 | 39 | // tests the Mongo handler with a mocked connection 40 | func TestHandleMongoDB(t *testing.T) { 41 | var requestID int32 = 12345 42 | var buffer bytes.Buffer 43 | 44 | header := mongoMsgHeader{ 45 | MessageLength: 39, // length of the entire message including header 46 | RequestID: requestID, 47 | ResponseTo: 0, 48 | OpCode: OpMsg, 49 | } 50 | binary.Write(&buffer, binary.LittleEndian, header) 51 | 52 | // Add flags 53 | binary.Write(&buffer, binary.LittleEndian, uint32(0)) 54 | 55 | // Body 56 | buffer.WriteByte(0) 57 | 58 | // document - {"hello": "mongodb"} 59 | docBytes := []byte{ 60 | 0x16, 0x00, 0x00, 0x00, // doc size: 22 bytes 61 | 0x02, // string type 62 | 'h', 'e', 'l', 'l', 'o', 0x00, 63 | 0x08, 0x00, 0x00, 0x00, // string length including null terminator 64 | 'm', 'o', 'n', 'g', 'o', 'd', 'b', 0x00, 65 | 0x00, // terminator 66 | } 67 | buffer.Write(docBytes) 68 | 69 | message := buffer.Bytes() 70 | var msgHeader mongoMsgHeader 71 | err := binary.Read(bytes.NewReader(message[:16]), binary.LittleEndian, &msgHeader) 72 | require.NoError(t, err) 73 | 74 | responseHeader, response, err := createOkResponse(msgHeader) 75 | require.NoError(t, err) 76 | require.NotNil(t, response) 77 | 78 | require.Equal(t, int32(len(response)), responseHeader.MessageLength) 79 | require.Equal(t, requestID+1, responseHeader.RequestID) 80 | require.Equal(t, requestID, responseHeader.ResponseTo) 81 | require.Equal(t, int32(OpMsg), responseHeader.OpCode) 82 | 83 | var respHeader mongoMsgHeader 84 | err = binary.Read(bytes.NewReader(response[:16]), binary.LittleEndian, &respHeader) 85 | require.NoError(t, err) 86 | 87 | require.Equal(t, byte(0), response[20]) 88 | 89 | // document starts at position 21 90 | document := response[21:] 91 | require.True(t, len(document) >= 17, "Document too short") 92 | 93 | // first 4 bytes are document size 94 | // then type code 0x01 (double) followed by "ok" 95 | require.Equal(t, byte(0x01), document[4], "Expected double type code") 96 | require.Equal(t, byte('o'), document[5], "Expected 'o' from 'ok' field name") 97 | require.Equal(t, byte('k'), document[6], "Expected 'k' from 'ok' field name") 98 | require.Equal(t, byte(0x00), document[7], "Expected null terminator") 99 | 100 | // next 8 bytes should be a double with value 1.0 101 | expectedValue := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F} 102 | require.Equal(t, expectedValue, document[8:16], "Expected double value 1.0") 103 | 104 | // doc should end with a null byte 105 | require.Equal(t, byte(0x00), document[16], "Expected document terminator") 106 | } 107 | -------------------------------------------------------------------------------- /protocols/tcp/mqtt.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "fmt" 8 | "log/slog" 9 | "net" 10 | 11 | "github.com/mushorg/glutton/connection" 12 | "github.com/mushorg/glutton/producer" 13 | "github.com/mushorg/glutton/protocols/interfaces" 14 | ) 15 | 16 | type mqttMsg struct { 17 | HeaderFlag uint8 18 | Length uint8 19 | } 20 | 21 | type mqttRes struct { 22 | HeaderFlag uint8 23 | Length uint8 24 | Flags uint8 25 | RetCode uint8 26 | } 27 | 28 | // HandleMQTT handles a MQTT connection 29 | func HandleMQTT(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 30 | var err error 31 | defer func() { 32 | err = conn.Close() 33 | if err != nil { 34 | logger.Error(fmt.Sprintf("[mqtt ] error: %v", err)) 35 | } 36 | }() 37 | buffer := make([]byte, 1024) 38 | for { 39 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 40 | return err 41 | } 42 | n, err := conn.Read(buffer) 43 | if err == nil || n > 0 { 44 | msg := mqttMsg{} 45 | r := bytes.NewReader(buffer) 46 | if err := binary.Read(r, binary.LittleEndian, &msg); err != nil { 47 | break 48 | } 49 | 50 | if err = h.ProduceTCP("mqtt", conn, md, buffer, msg); err != nil { 51 | logger.Error("Failed to produce message", producer.ErrAttr(err), slog.String("handler", "mqtt")) 52 | } 53 | 54 | logger.Info(fmt.Sprintf("new mqqt packet with header flag: %d", msg.HeaderFlag), slog.String("handler", "mqtt")) 55 | var res mqttRes 56 | switch msg.HeaderFlag { 57 | case 0x10: 58 | res = mqttRes{ 59 | HeaderFlag: 0x20, 60 | Length: 2, 61 | } 62 | case 0x82: 63 | res = mqttRes{ 64 | HeaderFlag: 0x90, 65 | Length: 3, 66 | } 67 | case 0xc0: 68 | res = mqttRes{ 69 | HeaderFlag: 0xd0, 70 | Length: 0, 71 | } 72 | } 73 | if err = binary.Write(conn, binary.LittleEndian, res); err != nil { 74 | logger.Error("Failed to write message", producer.ErrAttr(err), slog.String("handler", "mqtt")) 75 | break 76 | } 77 | } else { 78 | break 79 | } 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /protocols/tcp/rdp.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "fmt" 7 | "log/slog" 8 | "net" 9 | 10 | "github.com/mushorg/glutton/connection" 11 | "github.com/mushorg/glutton/producer" 12 | "github.com/mushorg/glutton/protocols/helpers" 13 | "github.com/mushorg/glutton/protocols/interfaces" 14 | "github.com/mushorg/glutton/protocols/tcp/rdp" 15 | ) 16 | 17 | type parsedRDP struct { 18 | Direction string `json:"direction,omitempty"` 19 | Header rdp.TKIPHeader `json:"header,omitempty"` 20 | Payload []byte `json:"payload,omitempty"` 21 | } 22 | 23 | type rdpServer struct { 24 | events []parsedRDP 25 | conn net.Conn 26 | } 27 | 28 | func (rs *rdpServer) write(header rdp.TKIPHeader, data []byte) error { 29 | rs.events = append(rs.events, parsedRDP{ 30 | Header: header, 31 | Direction: "write", 32 | Payload: data, 33 | }) 34 | _, err := rs.conn.Write(data) 35 | return err 36 | } 37 | 38 | // HandleRDP takes a net.Conn and does basic RDP communication 39 | func HandleRDP(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 40 | server := &rdpServer{ 41 | events: []parsedRDP{}, 42 | conn: conn, 43 | } 44 | defer func() { 45 | if err := h.ProduceTCP("rdp", conn, md, helpers.FirstOrEmpty[parsedRDP](server.events).Payload, server.events); err != nil { 46 | logger.Error("Failed to produce message", slog.String("protocol", "rdp"), producer.ErrAttr(err)) 47 | } 48 | if err := conn.Close(); err != nil { 49 | logger.Debug("Failed to close RDP connection", slog.String("protocol", "rdp"), producer.ErrAttr(err)) 50 | } 51 | }() 52 | 53 | buffer := make([]byte, 1024) 54 | for { 55 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 56 | logger.Debug("Failed to set connection timeout", slog.String("protocol", "rdp"), producer.ErrAttr(err)) 57 | return nil 58 | } 59 | n, err := conn.Read(buffer) 60 | if err != nil && n <= 0 { 61 | logger.Debug("Failed to read from connection", slog.String("protocol", "rdp"), producer.ErrAttr(err)) 62 | return nil 63 | } 64 | if n > 0 && n < 1024 { 65 | logger.Debug(fmt.Sprintf("rdp \n%s", hex.Dump(buffer[0:n]))) 66 | pdu, err := rdp.ParseCRPDU(buffer[0:n]) 67 | if err != nil { 68 | return err 69 | } 70 | server.events = append(server.events, parsedRDP{ 71 | Direction: "read", 72 | Header: pdu.Header, 73 | Payload: buffer[0:n], 74 | }) 75 | logger.Debug(fmt.Sprintf("rdp req pdu: %+v", pdu)) 76 | if len(pdu.Data) > 0 { 77 | logger.Debug(fmt.Sprintf("rdp data: %s", string(pdu.Data))) 78 | } 79 | header, resp, err := rdp.ConnectionConfirm(pdu.TPDU) 80 | if err != nil { 81 | return err 82 | } 83 | logger.Debug(fmt.Sprintf("rdp resp pdu: %+v", resp)) 84 | if err := server.write(header, resp); err != nil { 85 | return err 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /protocols/tcp/rdp/rdp.go: -------------------------------------------------------------------------------- 1 | package rdp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // TKIPHeader see http://go.microsoft.com/fwlink/?LinkId=90541 section 8 9 | type TKIPHeader struct { 10 | Version byte 11 | Reserved byte 12 | Length [2]byte 13 | } 14 | 15 | // CRTPDU see http://go.microsoft.com/fwlink/?LinkId=90588 section 13.3 16 | type CRTPDU struct { 17 | Length byte 18 | ConnectionRequestCode byte 19 | DstRef [2]byte 20 | SrcRef [2]byte 21 | ClassOption byte 22 | } 23 | 24 | type RDPNegReq struct { 25 | Type byte 26 | Flags byte 27 | Length [2]byte 28 | RequestedProtocols [4]byte 29 | } 30 | 31 | type ConnectionRequestPDU struct { 32 | Header TKIPHeader 33 | TPDU CRTPDU 34 | Data []byte 35 | RDPNegReq RDPNegReq 36 | } 37 | 38 | // CCTPDU Connection Confirm see http://go.microsoft.com/fwlink/?LinkId=90588 section 13.3 39 | type CCTPDU struct { 40 | Length byte // header length including parameters 41 | CCCDT byte 42 | DstRef [2]byte 43 | SrcRef [2]byte 44 | ClassOption byte 45 | } 46 | 47 | type NegotiationResponse struct { 48 | Type byte 49 | Flags byte 50 | Length [2]byte 51 | SelectedProtocol [4]byte 52 | } 53 | 54 | type ConnectionConfirmPDU struct { 55 | Header TKIPHeader 56 | TPDU CCTPDU 57 | Response NegotiationResponse 58 | } 59 | 60 | func ConnectionConfirm(cr CRTPDU) (TKIPHeader, []byte, error) { 61 | cc := ConnectionConfirmPDU{ 62 | Header: TKIPHeader{ 63 | Version: 3, 64 | }, 65 | TPDU: CCTPDU{ 66 | Length: 6, 67 | CCCDT: 0xc, // 1101-xxxx 68 | DstRef: cr.DstRef, 69 | SrcRef: cr.SrcRef, 70 | }, 71 | Response: NegotiationResponse{ 72 | Type: 0x02, 73 | SelectedProtocol: [4]byte{0x3}, 74 | }, 75 | } 76 | binary.BigEndian.PutUint16(cc.Header.Length[:], 12) 77 | binary.LittleEndian.PutUint16(cc.Response.Length[:], 8) 78 | buf := new(bytes.Buffer) 79 | err := binary.Write(buf, binary.LittleEndian, cc) 80 | return cc.Header, buf.Bytes(), err 81 | } 82 | 83 | // ParsePDU takes raw data and parses into struct 84 | func ParseCRPDU(data []byte) (ConnectionRequestPDU, error) { 85 | pdu := ConnectionRequestPDU{} 86 | buffer := bytes.NewBuffer(data) 87 | if err := binary.Read(buffer, binary.LittleEndian, &pdu.Header); err != nil { 88 | return pdu, err 89 | } 90 | 91 | // I wonder if we should be more lenient here 92 | if len(data) != int(binary.BigEndian.Uint16(pdu.Header.Length[:])) { 93 | return pdu, nil 94 | } 95 | if err := binary.Read(buffer, binary.LittleEndian, &pdu.TPDU); err != nil { 96 | return pdu, err 97 | } 98 | 99 | // Not sure if this is the best way to get the offset... 100 | offset := bytes.Index(data, []byte("\r\n")) 101 | switch { 102 | case offset < 4: 103 | return pdu, nil 104 | case offset < 4+7: 105 | if offset-4 == 0 { 106 | return pdu, nil 107 | } 108 | pdu.Data = make([]byte, offset-4) 109 | default: 110 | if offset-4-7 <= 0 { 111 | return pdu, nil 112 | } 113 | pdu.Data = make([]byte, offset-4-7) 114 | } 115 | 116 | if err := binary.Read(buffer, binary.LittleEndian, &pdu.Data); err != nil { 117 | return pdu, err 118 | } 119 | if buffer.Len() >= 8 { 120 | if err := binary.Read(buffer, binary.LittleEndian, &pdu.RDPNegReq); err != nil { 121 | return pdu, err 122 | } 123 | } 124 | return pdu, nil 125 | } 126 | -------------------------------------------------------------------------------- /protocols/tcp/rdp/rdp_test.go: -------------------------------------------------------------------------------- 1 | package rdp 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func processRawCR(raw string, t *testing.T) ConnectionRequestPDU { 12 | data, err := hex.DecodeString(raw) 13 | require.NoError(t, err) 14 | 15 | pdu, err := ParseCRPDU(data) 16 | require.NoError(t, err) 17 | return pdu 18 | } 19 | 20 | func TestRDPParseHeader1(t *testing.T) { 21 | raw := "0300002b26e00000000000436f6f6b69653a206d737473686173683d68656c6c6f0d0a0100080003000000" 22 | pdu := processRawCR(raw, t) 23 | if string(pdu.Data) != "Cookie: mstshash=hello" { 24 | fmt.Printf("%q\n", string(pdu.Data)) 25 | t.Error("Infalid data field") 26 | } 27 | fmt.Printf("Parsed data: %+v\n", pdu) 28 | } 29 | 30 | func TestRDPParseHeader2(t *testing.T) { 31 | raw := "0300001f1ae00000000000436f6f6b69653a206d737473686173683d610d0a" 32 | pdu := processRawCR(raw, t) 33 | if string(pdu.Data) != "Cookie: mstshash=a" { 34 | fmt.Printf("%q\n", string(pdu.Data)) 35 | t.Error("Infalid data field") 36 | } 37 | fmt.Printf("Parsed data: %+v\n", pdu) 38 | } 39 | 40 | func TestConnectionConfirm(t *testing.T) { 41 | cr := CRTPDU{} 42 | _, cc, err := ConnectionConfirm(cr) 43 | require.NoError(t, err) 44 | fmt.Printf("Parsed data: %+v\n", cc) 45 | } 46 | 47 | /* 48 | |e: mstshash=a..| 49 | */ 50 | -------------------------------------------------------------------------------- /protocols/tcp/resources.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import "embed" 4 | 5 | var ( 6 | //go:embed resources 7 | res embed.FS 8 | ) 9 | -------------------------------------------------------------------------------- /protocols/tcp/resources/docker_api.json: -------------------------------------------------------------------------------- 1 | {"Platform":{"Name":""},"Components":[{"Name":"Engine","Version":"20.10.9","Details":{"ApiVersion":"1.41","Arch":"amd64","BuildTime":"2021-10-04T19:12:03.000000000+00:00","Experimental":"false","GitCommit":"79ea9d3080","GoVersion":"go1.17.1","KernelVersion":"5.14.10-1-amd64","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"containerd","Version":"v1.5.7","Details":{"GitCommit":"8686ededfc90076914c5238eb96c883ea093a8ba.m"}},{"Name":"runc","Version":"1.0.2","Details":{"GitCommit":"v1.0.2-0-g52b36a2d"}},{"Name":"docker-init","Version":"0.19.0","Details":{"GitCommit":"de40ad0"}}],"Version":"20.10.9","ApiVersion":"1.41","MinAPIVersion":"1.12","GitCommit":"79ea9d3080","GoVersion":"go1.17.1","Os":"linux","Arch":"amd64","KernelVersion":"5.14.10-1-amd64","BuildTime":"2021-10-04T19:12:03.000000000+00:00"} -------------------------------------------------------------------------------- /protocols/tcp/rfb.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/binary" 7 | "log/slog" 8 | "net" 9 | 10 | "github.com/mushorg/glutton/connection" 11 | "github.com/mushorg/glutton/producer" 12 | "github.com/mushorg/glutton/protocols/interfaces" 13 | ) 14 | 15 | func readRFB(conn net.Conn, logger interfaces.Logger) error { 16 | msg, err := bufio.NewReader(conn).ReadString('\n') 17 | if err != nil { 18 | return err 19 | } 20 | logger.Debug("RFB message", slog.String("msg", msg), slog.String("protocol", "rfb")) 21 | return nil 22 | } 23 | 24 | // PixelFormat represents a RFB communication unit 25 | type PixelFormat struct { 26 | Width, Heigth uint16 27 | BPP, Depth uint8 28 | BigEndian, TrueColour uint8 // flags; 0 or non-zero 29 | RedMax, GreenMax, BlueMax uint16 30 | RedShift, GreenShift, BlueShift uint8 31 | Padding [3]uint8 32 | ServerNameLength int32 33 | } 34 | 35 | // HandleRFB takes a net.Conn and does basic RFB/VNC communication 36 | func HandleRFB(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 37 | defer func() { 38 | if err := conn.Close(); err != nil { 39 | logger.Debug("Failed to close RFB connection", slog.String("protocol", "rfb"), producer.ErrAttr(err)) 40 | } 41 | }() 42 | 43 | if _, err := conn.Write([]byte("RFB 003.008\n")); err != nil { 44 | return err 45 | } 46 | if err := readRFB(conn, logger); err != nil { 47 | logger.Debug("Failed to read RFB", slog.String("protocol", "rfb"), producer.ErrAttr(err)) 48 | return nil 49 | } 50 | var authNone uint32 = 1 51 | bs := make([]byte, 4) 52 | binary.LittleEndian.PutUint32(bs, authNone) 53 | if _, err := conn.Write(bs); err != nil { 54 | return err 55 | } 56 | 57 | serverName := "rfb-go" 58 | lenName := int32(len(serverName)) 59 | 60 | f := PixelFormat{ 61 | Width: 1, 62 | Heigth: 1, 63 | BPP: 16, 64 | Depth: 16, 65 | BigEndian: 0, 66 | TrueColour: 1, 67 | RedMax: 0x1f, 68 | GreenMax: 0x1f, 69 | BlueMax: 0x1f, 70 | RedShift: 0xa, 71 | GreenShift: 0x5, 72 | BlueShift: 0, 73 | ServerNameLength: lenName, 74 | } 75 | if err := binary.Write(conn, binary.LittleEndian, f); err != nil { 76 | return err 77 | } 78 | return readRFB(conn, logger) 79 | } 80 | -------------------------------------------------------------------------------- /protocols/tcp/sip.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/mushorg/glutton/connection" 10 | "github.com/mushorg/glutton/producer" 11 | "github.com/mushorg/glutton/protocols/helpers" 12 | "github.com/mushorg/glutton/protocols/interfaces" 13 | 14 | "github.com/ghettovoice/gosip/log" 15 | "github.com/ghettovoice/gosip/sip" 16 | "github.com/ghettovoice/gosip/sip/parser" 17 | ) 18 | 19 | const maxBufferSize = 1024 20 | 21 | type parsedSIP struct { 22 | Direction string 23 | Payload []byte 24 | Message sip.Message 25 | } 26 | 27 | type sipServer struct { 28 | events []parsedSIP 29 | } 30 | 31 | // HandleSIP takes a net.Conn and does basic SIP communication 32 | func HandleSIP(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 33 | server := sipServer{ 34 | events: []parsedSIP{}, 35 | } 36 | defer func() { 37 | if err := h.ProduceTCP("sip", conn, md, helpers.FirstOrEmpty[parsedSIP](server.events).Payload, server.events); err != nil { 38 | logger.Error("Failed to produce message", slog.String("protocol", "sip"), producer.ErrAttr(err)) 39 | } 40 | if err := conn.Close(); err != nil { 41 | logger.Debug("Failed to close SIP connection", slog.String("protocol", "sip"), producer.ErrAttr(err)) 42 | } 43 | }() 44 | 45 | buffer := make([]byte, maxBufferSize) 46 | l := log.NewDefaultLogrusLogger() 47 | pp := parser.NewPacketParser(l) 48 | 49 | for { 50 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 51 | logger.Debug("Failed to set connection timeout", slog.String("protocol", "sip"), producer.ErrAttr(err)) 52 | return nil 53 | } 54 | n, err := conn.Read(buffer) 55 | if err != nil { 56 | logger.Debug("Failed to read data", slog.String("protocol", "sip"), producer.ErrAttr(err)) 57 | break 58 | } 59 | 60 | msg, err := pp.ParseMessage(buffer[:n]) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | server.events = append(server.events, parsedSIP{ 66 | Direction: "read", 67 | Message: msg, 68 | Payload: buffer[:n], 69 | }) 70 | 71 | switch msg := msg.(type) { 72 | case sip.Request: 73 | switch msg.Method() { 74 | case sip.REGISTER: 75 | logger.Info("handling SIP register") 76 | case sip.INVITE: 77 | logger.Info("handling SIP invite") 78 | case sip.OPTIONS: 79 | logger.Info("handling SIP options") 80 | resp := sip.NewResponseFromRequest( 81 | msg.MessageID(), 82 | msg, 83 | http.StatusOK, 84 | "", 85 | "", 86 | ) 87 | server.events = append(server.events, parsedSIP{ 88 | Direction: "write", 89 | Message: resp, 90 | Payload: []byte(resp.String()), 91 | }) 92 | if _, err := conn.Write([]byte(resp.String())); err != nil { 93 | return err 94 | } 95 | } 96 | } 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /protocols/tcp/smb.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "log/slog" 7 | "net" 8 | 9 | "github.com/mushorg/glutton/connection" 10 | "github.com/mushorg/glutton/producer" 11 | "github.com/mushorg/glutton/protocols/helpers" 12 | "github.com/mushorg/glutton/protocols/interfaces" 13 | "github.com/mushorg/glutton/protocols/tcp/smb" 14 | ) 15 | 16 | type parsedSMB struct { 17 | Direction string `json:"direction,omitempty"` 18 | Header smb.SMBHeader `json:"header,omitempty"` 19 | Payload []byte `json:"payload,omitempty"` 20 | } 21 | 22 | type smbServer struct { 23 | events []parsedSMB 24 | conn net.Conn 25 | } 26 | 27 | func (ss *smbServer) write(header smb.SMBHeader, data []byte) error { 28 | _, err := ss.conn.Write(data) 29 | if err != nil { 30 | return err 31 | } 32 | ss.events = append(ss.events, parsedSMB{ 33 | Direction: "write", 34 | Header: header, 35 | Payload: data, 36 | }) 37 | return nil 38 | } 39 | 40 | // HandleSMB takes a net.Conn and does basic SMB communication 41 | func HandleSMB(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 42 | server := &smbServer{ 43 | events: []parsedSMB{}, 44 | conn: conn, 45 | } 46 | defer func() { 47 | if err := h.ProduceTCP("smb", conn, md, helpers.FirstOrEmpty[parsedSMB](server.events).Payload, server.events); err != nil { 48 | logger.Error("Failed to produce message", slog.String("protocol", "smb"), producer.ErrAttr(err)) 49 | } 50 | 51 | if err := conn.Close(); err != nil { 52 | logger.Debug("Failed to close SMB connection", producer.ErrAttr(err), slog.String("protocol", "smb")) 53 | } 54 | }() 55 | 56 | buffer := make([]byte, 4096) 57 | for { 58 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 59 | logger.Debug("Failed to set connection timeout", slog.String("protocol", "smb"), producer.ErrAttr(err)) 60 | return nil 61 | } 62 | n, err := conn.Read(buffer) 63 | if err != nil { 64 | logger.Debug("Failed to read data", slog.String("protocol", "smb"), producer.ErrAttr(err)) 65 | break 66 | } 67 | if n > 0 && n < 4096 { 68 | logger.Debug("SMB Payload", slog.String("payload", hex.Dump(buffer[0:n])), slog.String("protocol", "smb")) 69 | buffer, err := smb.ValidateData(buffer[0:n]) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | header := smb.SMBHeader{} 75 | err = smb.ParseHeader(buffer, &header) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | server.events = append(server.events, parsedSMB{ 81 | Direction: "read", 82 | Header: header, 83 | Payload: buffer.Bytes(), 84 | }) 85 | 86 | logger.Debug("SMB Header", slog.Any("header", header), slog.String("protocol", "smb")) 87 | switch header.Command { 88 | case 0x72, 0x73, 0x75: 89 | responseHeader, resp, err := smb.MakeNegotiateProtocolResponse(header) 90 | if err != nil { 91 | return err 92 | } 93 | if err := server.write(responseHeader, resp); err != nil { 94 | return err 95 | } 96 | case 0x32: 97 | responseHeader, resp, err := smb.MakeComTransaction2Response(header) 98 | if err != nil { 99 | return err 100 | } 101 | if err := server.write(responseHeader, resp); err != nil { 102 | return err 103 | } 104 | case 0x25: 105 | responseHeader, resp, err := smb.MakeComTransactionResponse(header) 106 | if err != nil { 107 | return err 108 | } 109 | if err := server.write(responseHeader, resp); err != nil { 110 | return err 111 | } 112 | } 113 | } 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /protocols/tcp/smb/command_codes.go: -------------------------------------------------------------------------------- 1 | package smb 2 | 3 | // var codes = map[int]string{ 4 | // 0x25: "SMB_COM_TRANSACTION", //37 5 | // 0x32: "SMB_COM_TRANSACTION2", //50 6 | // 0x72: "SMB_COM_NEGOTIATE", //114 7 | // 0x73: "SMB_COM_SESSION_SETUP_ANDX", //115 8 | // 0x75: "SMB_COM_TREE_CONNECT_ANDX", //117 9 | // } 10 | -------------------------------------------------------------------------------- /protocols/tcp/smb/smb.go: -------------------------------------------------------------------------------- 1 | package smb 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "errors" 8 | "math/big" 9 | "time" 10 | 11 | "github.com/google/uuid" 12 | ) 13 | 14 | type SMBHeader struct { 15 | Protocol [4]byte 16 | Command byte 17 | Status [4]byte 18 | Flags byte 19 | Flags2 [2]byte 20 | PIDHigh [2]byte 21 | SecurityFeatures [8]byte 22 | Reserved [2]byte 23 | TID [2]byte 24 | PIDLow [2]byte 25 | UID [2]byte 26 | MID [2]byte 27 | } 28 | 29 | type SMBParameters struct { 30 | WordCount byte 31 | } 32 | 33 | type SMBData struct { 34 | ByteCount [2]byte 35 | DialectString []byte 36 | } 37 | 38 | type NegotiateProtocolRequest struct { 39 | Header SMBHeader 40 | Param SMBParameters 41 | Data SMBData 42 | } 43 | 44 | type NegotiateProtocolResponse struct { 45 | Header SMBHeader 46 | StructureSize [2]byte 47 | SecurityMode [2]byte 48 | DialectRevision [2]byte 49 | NegotiateContextCount [2]byte 50 | ServerGUID [16]byte 51 | Capabilities [4]byte 52 | MaxTransactSize [4]byte 53 | MaxReadSize [4]byte 54 | MaxWriteSize [4]byte 55 | SystemTime Filetime 56 | ServerStartTime Filetime 57 | SecurityBufferOffset [2]byte 58 | SecurityBufferLength [2]byte 59 | NegotiateContextOffset [4]byte 60 | //Buffer []byte 61 | //Padding []byte 62 | //NegotiateContextList []byte 63 | } 64 | 65 | type Filetime struct { 66 | low uint32 67 | high uint32 68 | } 69 | 70 | func ValidateData(data []byte) (*bytes.Buffer, error) { 71 | // HACK: Not sure what the data in front is supposed to be... 72 | if !bytes.Contains(data, []byte("\xff")) { 73 | return nil, errors.New("packet is unrecognizable") 74 | } 75 | 76 | start := bytes.Index(data, []byte("\xff")) 77 | buffer := bytes.NewBuffer(data[start:]) 78 | return buffer, nil 79 | } 80 | 81 | func filetime(offset time.Duration) Filetime { 82 | epochAsFiletime := int64(116444736000000000) // January 1, 1970 as MS file time 83 | hundredsOfNanoseconds := int64(10000000) 84 | fileTime := epochAsFiletime + time.Now().Add(offset).Unix()*hundredsOfNanoseconds 85 | return Filetime{ 86 | low: uint32(fileTime), 87 | high: uint32(fileTime << 32), 88 | } 89 | } 90 | 91 | func random(min, max int) (int, error) { 92 | rn, err := rand.Int(rand.Reader, big.NewInt(int64(max-min))) 93 | if err != nil { 94 | return 0, err 95 | } 96 | return int(rn.Int64()) + min, nil 97 | } 98 | 99 | func toBytes(smb interface{}) ([]byte, error) { 100 | var buf bytes.Buffer 101 | err := binary.Write(&buf, binary.LittleEndian, smb) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return buf.Bytes(), nil 106 | } 107 | 108 | func MakeHeaderResponse(header SMBHeader) (SMBHeader, []byte, error) { 109 | smb := NegotiateProtocolResponse{} 110 | smb.Header.Protocol = header.Protocol 111 | smb.Header.Command = header.Command 112 | smb.Header.Status = [4]byte{0, 0, 0, 0} 113 | smb.Header.Flags = 0x98 114 | smb.Header.Flags2 = [2]byte{28, 1} 115 | 116 | data, err := toBytes(smb) 117 | return smb.Header, data, err 118 | } 119 | 120 | type ComTransaction2Response struct { 121 | Header SMBHeader 122 | WordCount byte 123 | TotalParameterCount [2]byte 124 | TotalDataCount [2]byte 125 | Reserved1 [2]byte 126 | ParameterCount [2]byte 127 | ParameterOffset [2]byte 128 | ParameterDisplacement [2]byte 129 | DataCount [2]byte 130 | DataOffset [2]byte 131 | DataDisplacement [2]byte 132 | SetupCount byte 133 | Reserved2 byte 134 | ByteCount [2]byte 135 | Pad1 byte 136 | SearchID [2]byte 137 | SearchCount [2]byte 138 | EndofSearch [2]byte 139 | ErrorOffset [2]byte 140 | LastNameOffset [2]byte 141 | Pad2 [2]byte 142 | Data [16]byte 143 | Data1 [16]byte 144 | Data2 [16]byte 145 | Data3 [16]byte 146 | Data4 [16]byte 147 | Data5 [16]byte 148 | Data6 [16]byte 149 | Data7 [16]byte 150 | Data8 [16]byte 151 | Data9 [16]byte 152 | Data10 [16]byte 153 | Data11 [16]byte 154 | Data12 [4]byte 155 | } 156 | 157 | func MakeComTransaction2Response(header SMBHeader) (SMBHeader, []byte, error) { 158 | smb := ComTransaction2Response{} 159 | smb.Header = header 160 | smb.WordCount = 0x0A 161 | smb.TotalParameterCount = [2]byte{0x0A} 162 | smb.TotalDataCount = [2]byte{196} 163 | smb.Reserved1 = [2]byte{0} 164 | smb.ParameterCount = [2]byte{10} 165 | smb.ParameterOffset = [2]byte{56} 166 | smb.ParameterDisplacement = [2]byte{} 167 | smb.DataCount = [2]byte{196} 168 | smb.DataOffset = [2]byte{68} 169 | smb.DataDisplacement = [2]byte{0} 170 | smb.SetupCount = 0 171 | smb.Reserved2 = 2 172 | smb.ByteCount = [2]byte{209} 173 | smb.Pad1 = 0 174 | smb.SearchID = [2]byte{0xff, 0xfd} 175 | smb.SearchCount = [2]byte{2} 176 | smb.SearchID = [2]byte{1} 177 | smb.ErrorOffset = [2]byte{} 178 | smb.LastNameOffset = [2]byte{96} 179 | smb.Pad2 = [2]byte{} 180 | smb.Data = [16]byte{0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0xa3, 0xda, 0x08, 0x01, 0xd6, 0xd2, 0x01} 181 | smb.Data1 = [16]byte{0xba, 0xb0, 0x6e, 0x0a, 0x01, 0xd6, 0xd2, 0x01, 0x39, 0xa3, 0xda, 0x08, 0x01, 0xd6, 0xd2, 0x01} 182 | smb.Data2 = [16]byte{0x39, 0xa3, 0xda, 0x08, 0x01, 0xd6, 0xd2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 183 | smb.Data3 = [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00} 184 | smb.Data4 = [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 185 | smb.Data5 = [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e, 0x00} 186 | smb.Data6 = [16]byte{0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc2, 0xaf, 0xca, 0x06, 0xcc, 0xd5, 0xd2, 0x01} 187 | smb.Data7 = [16]byte{0x9d, 0x76, 0x46, 0x90, 0xcc, 0xd5, 0xd2, 0x01, 0xc2, 0xaf, 0xca, 0x06, 0xcc, 0xd5, 0xd2, 0x01} 188 | smb.Data8 = [16]byte{0xc2, 0xaf, 0xca, 0x06, 0xcc, 0xd5, 0xd2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 189 | smb.Data9 = [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00} 190 | smb.Data10 = [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 191 | smb.Data11 = [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e, 0x2e} 192 | smb.Data12 = [4]byte{0x00, 0x00, 0x00, 0x00} 193 | 194 | data, err := toBytes(smb) 195 | return smb.Header, data, err 196 | } 197 | 198 | type ComTransaction2Error struct { 199 | Header SMBHeader 200 | WordCount byte 201 | ByteCount [2]byte 202 | } 203 | 204 | type ComTransactionResponse struct { 205 | Header SMBHeader 206 | WordCount byte 207 | TotalParameterCount [2]byte 208 | TotalDataCount [2]byte 209 | Reserved1 [2]byte 210 | ParameterCount [2]byte 211 | ParameterOffset [2]byte 212 | ParameterDisplacement [2]byte 213 | DataCount [2]byte 214 | DataOffset [2]byte 215 | DataDisplacement [2]byte 216 | SetupCount byte 217 | Reserved2 byte 218 | } 219 | 220 | func MakeComTransactionResponse(header SMBHeader) (SMBHeader, []byte, error) { 221 | smb := ComTransactionResponse{} 222 | smb.Header = header 223 | smb.WordCount = 10 224 | smb.ParameterOffset = [2]byte{56} 225 | smb.DataOffset = [2]byte{56} 226 | 227 | data, err := toBytes(smb) 228 | return smb.Header, data, err 229 | } 230 | 231 | func MakeComTransaction2Error(header SMBHeader) (SMBHeader, []byte, error) { 232 | smb := ComTransaction2Error{} 233 | smb.Header = header 234 | smb.Header.Status = [4]byte{0x02, 0x00, 0x00, 0xc0} 235 | smb.WordCount = 0x00 236 | smb.ByteCount = [2]byte{} 237 | 238 | data, err := toBytes(smb) 239 | return smb.Header, data, err 240 | } 241 | 242 | func MakeNegotiateProtocolResponse(header SMBHeader) (SMBHeader, []byte, error) { 243 | id := uuid.New() 244 | smb := NegotiateProtocolResponse{} 245 | smb.Header.Protocol = header.Protocol 246 | smb.Header.Command = header.Command 247 | smb.Header.Status = [4]byte{0, 0, 0, 0} 248 | smb.Header.Flags = 0x98 249 | smb.Header.Flags2 = [2]byte{28, 1} 250 | smb.StructureSize = [2]byte{65} 251 | smb.SecurityMode = [2]byte{0x0003} 252 | smb.DialectRevision = [2]byte{0x03, 0x00} 253 | b, err := id.MarshalBinary() 254 | if err != nil { 255 | return SMBHeader{}, nil, err 256 | } 257 | copy(smb.ServerGUID[:], b) 258 | smb.Capabilities = [4]byte{0x80, 0x01, 0xe3, 0xfc} 259 | smb.MaxTransactSize = [4]byte{0x04, 0x11} 260 | smb.MaxReadSize = [4]byte{0x00, 0x00, 0x01} 261 | smb.SystemTime = filetime(0) 262 | randomTime, err := random(1000, 2000) 263 | if err != nil { 264 | return SMBHeader{}, nil, err 265 | } 266 | smb.ServerStartTime = filetime(time.Duration(randomTime) * time.Hour) 267 | 268 | data, err := toBytes(smb) 269 | return smb.Header, data, err 270 | } 271 | 272 | func ParseHeader(buffer *bytes.Buffer, header *SMBHeader) error { 273 | return binary.Read(buffer, binary.LittleEndian, header) 274 | } 275 | 276 | func ParseParam(buffer *bytes.Buffer, param *SMBParameters) error { 277 | return binary.Read(buffer, binary.LittleEndian, param) 278 | } 279 | 280 | func ParseNegotiateProtocolRequest(buffer *bytes.Buffer, header SMBHeader) (NegotiateProtocolRequest, error) { 281 | smb := NegotiateProtocolRequest{} 282 | smb.Header = header 283 | err := ParseParam(buffer, &smb.Param) 284 | if err != nil { 285 | return smb, err 286 | } 287 | err = binary.Read(buffer, binary.LittleEndian, &smb.Data.ByteCount) 288 | if err != nil { 289 | return smb, err 290 | } 291 | smb.Data.DialectString = make([]byte, buffer.Len()) 292 | err = binary.Read(buffer, binary.LittleEndian, &smb.Data.DialectString) 293 | return smb, err 294 | } 295 | -------------------------------------------------------------------------------- /protocols/tcp/smb/smb_test.go: -------------------------------------------------------------------------------- 1 | package smb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | /* 12 | 00000000 00 00 00 85 ff 53 4d 42 72 00 00 00 00 18 53 c8 |.....SMBr.....S.| 13 | 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff fe |................| 14 | 00000020 00 00 00 00 00 62 00 02 50 43 20 4e 45 54 57 4f |.....b..PC NETWO| 15 | 00000030 52 4b 20 50 52 4f 47 52 41 4d 20 31 2e 30 00 02 |RK PROGRAM 1.0..| 16 | 00000040 4c 41 4e 4d 41 4e 31 2e 30 00 02 57 69 6e 64 6f |LANMAN1.0..Windo| 17 | 00000050 77 73 20 66 6f 72 20 57 6f 72 6b 67 72 6f 75 70 |ws for Workgroup| 18 | 00000060 73 20 33 2e 31 61 00 02 4c 4d 31 2e 32 58 30 30 |s 3.1a..LM1.2X00| 19 | 00000070 32 00 02 4c 41 4e 4d 41 4e 32 2e 31 00 02 4e 54 |2..LANMAN2.1..NT| 20 | 00000080 20 4c 4d 20 30 2e 31 32 00 | LM 0.12.| 21 | */ 22 | 23 | func TestParseSMB(t *testing.T) { 24 | raw := "00000085ff534d4272000000001853c80000000000000000000000000000fffe00000000006200025043204e4554574f524b" + 25 | "2050524f4752414d20312e3000024c414e4d414e312e30000257696e646f777320666f7220576f726b67726f75707320332e31610" + 26 | "0024c4d312e325830303200024c414e4d414e322e3100024e54204c4d20302e313200" 27 | data, _ := hex.DecodeString(raw) 28 | 29 | buffer, err := ValidateData(data) 30 | require.NoError(t, err) 31 | 32 | header := SMBHeader{} 33 | err = ParseHeader(buffer, &header) 34 | require.NoError(t, err) 35 | 36 | parsed, err := ParseNegotiateProtocolRequest(buffer, header) 37 | require.NoError(t, err) 38 | require.Equal(t, string(parsed.Header.Protocol[1:]), "SMB") 39 | 40 | dialectString := bytes.Split(parsed.Data.DialectString, []byte("\x00")) 41 | require.Equal(t, string(dialectString[0][:]), "\x02PC NETWORK PROGRAM 1.0", "dialect string mismatch") 42 | } 43 | 44 | func TestMakeResponses(t *testing.T) { 45 | tests := []struct { 46 | name string 47 | header SMBHeader 48 | f func(SMBHeader) (SMBHeader, []byte, error) 49 | }{ 50 | {name: "MakeHeaderResponse", header: SMBHeader{}, f: MakeHeaderResponse}, 51 | {name: "MakeComTransaction2Response", header: SMBHeader{Command: 0x32}, f: MakeComTransaction2Response}, 52 | {name: "MakeComTransactionResponse", header: SMBHeader{Command: 0x25}, f: MakeComTransactionResponse}, 53 | {name: "MakeComTransaction2Error", header: SMBHeader{}, f: MakeComTransaction2Error}, 54 | {name: "MakeNegotiateProtocolResponse", header: SMBHeader{Command: 0x25}, f: MakeNegotiateProtocolResponse}, 55 | } 56 | for _, test := range tests { 57 | t.Run(test.name, func(t *testing.T) { 58 | responseHeader, data, err := test.f(test.header) 59 | require.NoError(t, err) 60 | require.NotEmpty(t, data) 61 | require.NotEmpty(t, responseHeader, "invalid response header") 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /protocols/tcp/smtp.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/rand" 7 | "log/slog" 8 | "math/big" 9 | "net" 10 | "regexp" 11 | "strings" 12 | "time" 13 | 14 | "github.com/mushorg/glutton/connection" 15 | "github.com/mushorg/glutton/producer" 16 | "github.com/mushorg/glutton/protocols/interfaces" 17 | ) 18 | 19 | // maximum lines that can be read after the "DATA" command 20 | const maxDataRead = 500 21 | 22 | // Client is a connection container 23 | type Client struct { 24 | conn net.Conn 25 | bufin *bufio.Reader 26 | bufout *bufio.Writer 27 | } 28 | 29 | func (c *Client) w(s string) { 30 | c.bufout.WriteString(s + "\r\n") 31 | c.bufout.Flush() 32 | } 33 | func (c *Client) read() (string, error) { 34 | return c.bufin.ReadString('\n') 35 | } 36 | 37 | func randomSleep() error { 38 | // between 0.5 - 1.5 seconds 39 | rtime, err := rand.Int(rand.Reader, big.NewInt(1500)) 40 | if err != nil { 41 | return err 42 | } 43 | duration := time.Duration(rtime.Int64()+500) * time.Millisecond 44 | time.Sleep(duration) 45 | return nil 46 | } 47 | func validateMail(query string) bool { 48 | email := regexp.MustCompile("^MAIL FROM:<.+@.+>$") // naive regex 49 | return email.MatchString(query) 50 | } 51 | func validateRCPT(query string) bool { 52 | rcpt := regexp.MustCompile("^RCPT TO:<.+@.+>$") 53 | return rcpt.MatchString(query) 54 | } 55 | 56 | // HandleSMTP takes a net.Conn and does basic SMTP communication 57 | func HandleSMTP(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 58 | defer func() { 59 | if err := conn.Close(); err != nil { 60 | logger.Debug("Failed to close SMTP connection", slog.String("protocol", "smtp"), producer.ErrAttr(err)) 61 | } 62 | }() 63 | 64 | client := &Client{ 65 | conn: conn, 66 | bufin: bufio.NewReader(conn), 67 | bufout: bufio.NewWriter(conn), 68 | } 69 | if err := randomSleep(); err != nil { 70 | return err 71 | } 72 | client.w("220 Welcome!") 73 | 74 | for { 75 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 76 | return err 77 | } 78 | data, err := client.read() 79 | if err != nil { 80 | break 81 | } 82 | query := strings.Trim(data, "\r\n") 83 | logger.Debug("SMTP Query", slog.String("query", query), slog.String("protocol", "smtp")) 84 | if strings.HasPrefix(query, "HELO ") { 85 | if err := randomSleep(); err != nil { 86 | return err 87 | } 88 | client.w("250 Hello! Pleased to meet you.") 89 | } else if validateMail(query) { 90 | if err := randomSleep(); err != nil { 91 | return err 92 | } 93 | client.w("250 OK") 94 | } else if validateRCPT(query) { 95 | if err := randomSleep(); err != nil { 96 | return err 97 | } 98 | client.w("250 OK") 99 | } else if strings.Compare(query, "DATA") == 0 { 100 | client.w("354 End data with .") 101 | for readctr := maxDataRead; readctr >= 0; readctr-- { 102 | data, err = client.read() 103 | if err != nil { 104 | break 105 | } 106 | if err := h.ProduceTCP("smtp", conn, md, []byte(data), struct { 107 | Message string `json:"message,omitempty"` 108 | }{Message: query}); err != nil { 109 | logger.Error("Failed to produce message", slog.String("protocol", "smpt"), producer.ErrAttr(err)) 110 | } 111 | logger.Debug("SMTP Data", slog.String("data", data), slog.String("protocol", "smtp")) 112 | // exit condition 113 | if strings.Compare(data, ".\r\n") == 0 { 114 | break 115 | } 116 | } 117 | if err := randomSleep(); err != nil { 118 | return err 119 | } 120 | client.w("250 OK") 121 | } else if strings.Compare(query, "QUIT") == 0 { 122 | client.w("Bye") 123 | break 124 | } else { 125 | client.w("Recheck the command you entered.") 126 | } 127 | } 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /protocols/tcp/smtp_test.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestValidateMail(t *testing.T) { 10 | require.True(t, validateMail("MAIL FROM:"), "email regex validation failed") 11 | require.False(t, validateMail("MAIL FROM:"), "email regex validation failed") 12 | } 13 | 14 | func TestValidateRCPT(t *testing.T) { 15 | require.True(t, validateRCPT("RCPT TO:"), "validate rcpt regex failed") 16 | require.False(t, validateRCPT("RCPT TO:"), "validate rcpt regex failed") 17 | } 18 | -------------------------------------------------------------------------------- /protocols/tcp/tcp.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "fmt" 9 | "log/slog" 10 | "math/big" 11 | "net" 12 | "strconv" 13 | 14 | "github.com/mushorg/glutton/connection" 15 | "github.com/mushorg/glutton/producer" 16 | "github.com/mushorg/glutton/protocols/helpers" 17 | "github.com/mushorg/glutton/protocols/interfaces" 18 | 19 | "github.com/spf13/viper" 20 | ) 21 | 22 | type parsedTCP struct { 23 | Direction string `json:"direction,omitempty"` 24 | Payload []byte `json:"payload,omitempty"` 25 | PayloadHash string `json:"payload_hash,omitempty"` 26 | } 27 | 28 | type tcpServer struct { 29 | events []parsedTCP 30 | } 31 | 32 | func (s *tcpServer) sendRandom(conn net.Conn) error { 33 | randomInt, err := rand.Int(rand.Reader, big.NewInt(500)) 34 | if err != nil { 35 | return err 36 | } 37 | randomBytes := make([]byte, 12+randomInt.Int64()) 38 | if _, err := rand.Read(randomBytes); err != nil { 39 | return err 40 | } 41 | sum := sha256.Sum256(randomBytes) 42 | s.events = append(s.events, parsedTCP{ 43 | Direction: "write", 44 | PayloadHash: hex.EncodeToString(sum[:]), 45 | Payload: randomBytes, 46 | }) 47 | if _, err := conn.Write(randomBytes); err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | // HandleTCP takes a net.Conn and peeks at the data send 54 | func HandleTCP(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 55 | server := tcpServer{ 56 | events: []parsedTCP{}, 57 | } 58 | 59 | host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) 60 | if err != nil { 61 | return fmt.Errorf("faild to split remote address: %w", err) 62 | } 63 | 64 | msgLength := 0 65 | data := []byte{} 66 | buffer := make([]byte, 1024) 67 | 68 | defer func() { 69 | if msgLength > 0 { 70 | payloadHash, err := helpers.Store(data, "payloads") 71 | if err != nil { 72 | logger.Error("Failed to store payload", slog.String("handler", "tcp"), producer.ErrAttr(err)) 73 | } 74 | logger.Info( 75 | "Packet got handled by TCP handler", 76 | slog.String("dest_port", strconv.Itoa(int(md.TargetPort))), 77 | slog.String("src_ip", host), 78 | slog.String("src_port", port), 79 | slog.String("handler", "tcp"), 80 | slog.String("payload_hash", payloadHash), 81 | ) 82 | logger.Info(fmt.Sprintf("TCP payload:\n%s", hex.Dump(data[:msgLength%1024]))) 83 | 84 | server.events = append(server.events, parsedTCP{ 85 | Direction: "read", 86 | PayloadHash: payloadHash, 87 | Payload: data[:msgLength%1024], 88 | }) 89 | } 90 | 91 | if err := h.ProduceTCP("tcp", conn, md, helpers.FirstOrEmpty[parsedTCP](server.events).Payload, server.events); err != nil { 92 | logger.Error("Failed to produce message", slog.String("protocol", "tcp"), producer.ErrAttr(err)) 93 | } 94 | if err := conn.Close(); err != nil { 95 | logger.Error("Failed to close TCP connection", slog.String("handler", "tcp"), producer.ErrAttr(err)) 96 | } 97 | }() 98 | 99 | for { 100 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 101 | return err 102 | } 103 | n, err := conn.Read(buffer) 104 | if err != nil { 105 | logger.Error("read error", slog.String("handler", "tcp"), producer.ErrAttr(err)) 106 | break 107 | } 108 | msgLength += n 109 | data = append(data, buffer[:n]...) 110 | if n < 1024 { 111 | break 112 | } 113 | if msgLength > viper.GetInt("max_tcp_payload") { 114 | logger.Debug("max message length reached", slog.String("handler", "tcp")) 115 | break 116 | } 117 | } 118 | 119 | // sending some random data 120 | if err := server.sendRandom(conn); err != nil { 121 | logger.Error("write error", slog.String("handler", "tcp"), producer.ErrAttr(err)) 122 | } 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /protocols/tcp/telnet.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/rand" 7 | "errors" 8 | "io" 9 | "log/slog" 10 | "math/big" 11 | "net" 12 | "net/http" 13 | "regexp" 14 | "strings" 15 | "time" 16 | 17 | "github.com/mushorg/glutton/connection" 18 | "github.com/mushorg/glutton/producer" 19 | "github.com/mushorg/glutton/protocols/helpers" 20 | "github.com/mushorg/glutton/protocols/interfaces" 21 | ) 22 | 23 | // Mirai botnet - https://github.com/CymmetriaResearch/MTPot/blob/master/mirai_conf.json 24 | // Hajime botnet - https://security.rapiditynetworks.com/publications/2016-10-16/hajime.pdf 25 | var miraiCom = map[string][]string{ 26 | "ps": {"1 pts/21 00:00:00 init"}, 27 | "cat /proc/mounts": {"rootfs / rootfs rw 0 0\r\n/dev/root / ext2 rw,relatime,errors=continue 0 0\r\nproc /proc proc rw,relatime 0 0\r\nsysfs /sys sysfs rw,relatime 0 0\r\nudev /dev tmpfs rw,relatime 0 0\r\ndevpts /dev/pts devpts rw,relatime,mode=600,ptmxmode=000 0 0\r\n/dev/mtdblock1 /home/hik jffs2 rw,relatime 0 0\r\ntmpfs /run tmpfs rw,nosuid,noexec,relatime,size=3231524k,mode=755 0 0\r\n"}, 28 | "(cat .s || cp /bin/echo .s)": {"cat: .s: No such file or directory"}, 29 | "nc": {"nc: command not found"}, 30 | "wget": {"wget: missing URL"}, 31 | "(dd bs=52 count=1 if=.s || cat .s)": {"\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00\x01\x00\x00\x00\xbc\x14\x01\x00\x34\x00\x00\x00"}, 32 | "sh": {"$"}, 33 | "sh || shell": {"$"}, 34 | "enable\x00": {"-bash: enable: command not found"}, 35 | "system\x00": {"-bash: system: command not found"}, 36 | "shell\x00": {"-bash: shell: command not found"}, 37 | "sh\x00": {"$"}, 38 | // "fgrep XDVR /mnt/mtd/dep2.sh\x00": {"cd /mnt/mtd && ./XDVRStart.hisi ./td3500 &"}, 39 | "busybox": {"BusyBox v1.16.1 (2014-03-04 16:00:18 CST) built-it shell (ash)\r\nEnter 'help' for a list of built-in commands.\r\n"}, 40 | "echo -ne '\\x48\\x6f\\x6c\\x6c\\x61\\x46\\x6f\\x72\\x41\\x6c\\x6c\\x61\\x68\\x0a'\r\n": {"\x48\x6f\x6c\x6c\x61\x46\x6f\x72\x41\x6c\x6c\x61\x68\x0arn"}, 41 | "cat | sh": {""}, 42 | "echo -e \\x6b\\x61\\x6d\\x69/dev > /dev/.nippon": {""}, 43 | "cat /dev/.nippon": {"kami/dev"}, 44 | "rm /dev/.nippon": {""}, 45 | "echo -e \\x6b\\x61\\x6d\\x69/run > /run/.nippon": {""}, 46 | "cat /run/.nippon": {"kami/run"}, 47 | "rm /run/.nippon": {""}, 48 | "cat /bin/sh": {"\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x28\x00\x01\x00\x00\x00\x98\x30\x00\x00\x34\x00\x00\x00"}, 49 | "/bin/busybox ps": {"1 pts/21 00:00:00 init"}, 50 | "/bin/busybox cat /proc/mounts": {"tmpfs /run tmpfs rw,nosuid,noexec,relatime,size=3231524k,mode=755 0 0"}, 51 | "/bin/busybox echo -e \\x6b\\x61\\x6d\\x69/dev > /dev/.nippon": {""}, 52 | "/bin/busybox cat /dev/.nippon": {"kami/dev"}, 53 | "/bin/busybox rm /dev/.nippon": {""}, 54 | "/bin/busybox echo -e \\x6b\\x61\\x6d\\x69/run > /run/.nippon": {""}, 55 | "/bin/busybox cat /run/.nippon": {"kami/run"}, 56 | "/bin/busybox rm /run/.nippon": {""}, 57 | "/bin/busybox cat /bin/sh": {""}, 58 | "/bin/busybox cat /bin/echo": {"/bin/busybox cat /bin/echo\r\n\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00\x01\x00\x00\x00\x6c\xb9\x00\x00\x34\x00\x00\x00"}, 59 | "rm /dev/.human": {"rm: can't remove '/.t': No such file or directory\r\nrm: can't remove '/.sh': No such file or directory\r\nrm: can't remove '/.human': No such file or directory\r\ncd /dev"}, 60 | } 61 | 62 | type parsedTelnet struct { 63 | Direction string `json:"direction,omitempty"` 64 | Message string `json:"message,omitempty"` 65 | } 66 | 67 | type telnetServer struct { 68 | events []parsedTelnet 69 | client *http.Client 70 | } 71 | 72 | // write writes a telnet message to the connection 73 | func (s *telnetServer) write(conn net.Conn, msg string) error { 74 | if _, err := conn.Write([]byte(msg)); err != nil { 75 | return err 76 | } 77 | s.events = append(s.events, parsedTelnet{Direction: "write", Message: msg}) 78 | return nil 79 | } 80 | 81 | // read reads a telnet message from a connection 82 | func (s *telnetServer) read(conn net.Conn) (string, error) { 83 | msg, err := bufio.NewReader(conn).ReadString('\n') 84 | if err != nil { 85 | return msg, err 86 | } 87 | s.events = append(s.events, parsedTelnet{Direction: "read", Message: msg}) 88 | return msg, nil 89 | } 90 | 91 | func (s *telnetServer) getSample(cmd string, logger interfaces.Logger) error { 92 | url := cmd[strings.Index(cmd, "http"):] 93 | url = strings.Split(url, " ")[0] 94 | url = strings.TrimSpace(url) 95 | logger.Debug("Fetching sample", slog.String("url", url), slog.String("handler", "telnet")) 96 | resp, err := s.client.Get(url) 97 | if err != nil { 98 | return err 99 | } 100 | if resp.StatusCode != 200 { 101 | return errors.New("failed to fetch sample: " + resp.Status) 102 | } 103 | defer resp.Body.Close() 104 | if resp.ContentLength <= 0 { 105 | return errors.New("content length is 0") 106 | } 107 | 108 | data, err := io.ReadAll(resp.Body) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | if len(data) == 0 { 114 | return errors.New("empty response body") 115 | } 116 | 117 | sha256Hash, err := helpers.Store(data, "samples") 118 | if err != nil { 119 | return err 120 | } 121 | 122 | logger.Info( 123 | "New sample fetched", 124 | slog.String("handler", "telnet"), 125 | slog.String("sample_hash", sha256Hash), 126 | slog.String("source", url), 127 | ) 128 | return nil 129 | } 130 | 131 | // HandleTelnet handles telnet communication on a connection 132 | func HandleTelnet(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { 133 | s := &telnetServer{ 134 | events: []parsedTelnet{}, 135 | client: &http.Client{ 136 | Timeout: time.Duration(5 * time.Second), 137 | }, 138 | } 139 | defer func() { 140 | if err := h.ProduceTCP("telnet", conn, md, []byte(helpers.FirstOrEmpty[parsedTelnet](s.events).Message), s.events); err != nil { 141 | logger.Error("Failed to produce message", producer.ErrAttr(err)) 142 | } 143 | if err := conn.Close(); err != nil { 144 | logger.Debug("Failed to close telnet connection", producer.ErrAttr(err)) 145 | } 146 | }() 147 | 148 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 149 | logger.Debug("Failed to set connection timeout", slog.String("protocol", "telnet"), producer.ErrAttr(err)) 150 | return nil 151 | } 152 | 153 | // TODO (glaslos): Add device banner 154 | 155 | // telnet window size negotiation response 156 | if err := s.write(conn, "\xff\xfd\x18\xff\xfd\x20\xff\xfd\x23\xff\xfd\x27"); err != nil { 157 | return err 158 | } 159 | 160 | // User name prompt 161 | if err := s.write(conn, "Username: "); err != nil { 162 | return err 163 | } 164 | if _, err := s.read(conn); err != nil { 165 | logger.Debug("Failed to read from connection", slog.String("protocol", "telnet"), producer.ErrAttr(err)) 166 | return nil 167 | } 168 | if err := s.write(conn, "Password: "); err != nil { 169 | return err 170 | } 171 | if _, err := s.read(conn); err != nil { 172 | return err 173 | } 174 | if err := s.write(conn, "welcome\r\n> "); err != nil { 175 | return err 176 | } 177 | 178 | for { 179 | if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { 180 | return err 181 | } 182 | msg, err := s.read(conn) 183 | if err != nil { 184 | return err 185 | } 186 | for _, cmd := range strings.Split(msg, ";") { 187 | if strings.Contains(strings.Trim(cmd, " "), "wget http") { 188 | go func() { 189 | err := s.getSample(strings.Trim(cmd, " "), logger) 190 | if err != nil { 191 | logger.Error("Failed to get sample", slog.String("handler", "telnet"), producer.ErrAttr(err)) 192 | } 193 | }() 194 | } 195 | if strings.TrimRight(cmd, "") == " rm /dev/.t" { 196 | continue 197 | } 198 | if strings.TrimRight(cmd, "\r\n") == " rm /dev/.sh" { 199 | continue 200 | } 201 | if strings.TrimRight(cmd, "\r\n") == "cd /dev/" { 202 | if err := s.write(conn, "ECCHI: applet not found\r\n"); err != nil { 203 | return err 204 | } 205 | 206 | if err := s.write(conn, "\r\nBusyBox v1.16.1 (2014-03-04 16:00:18 CST) built-it shell (ash)\r\nEnter 'help' for a list of built-in commands.\r\n"); err != nil { 207 | return err 208 | } 209 | continue 210 | } 211 | 212 | if resp := miraiCom[strings.TrimSpace(cmd)]; len(resp) > 0 { 213 | n, err := rand.Int(rand.Reader, big.NewInt(int64(len(resp)))) 214 | if err != nil { 215 | return err 216 | } 217 | if err := s.write(conn, resp[n.Int64()]+"\r\n"); err != nil { 218 | return err 219 | } 220 | } else { 221 | // /bin/busybox YDKBI 222 | re := regexp.MustCompile(`\/bin\/busybox (?P[A-Za-z]+)`) 223 | match := re.FindStringSubmatch(cmd) 224 | if len(match) > 1 { 225 | if err := s.write(conn, match[1]+": applet not found\r\n"); err != nil { 226 | return err 227 | } 228 | 229 | if err := s.write(conn, "BusyBox v1.16.1 (2014-03-04 16:00:18 CST) built-in shell (ash)\r\nEnter 'help' for a list of built-in commands.\r\n"); err != nil { 230 | return err 231 | } 232 | } 233 | } 234 | } 235 | if err := s.write(conn, "> "); err != nil { 236 | return err 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /protocols/udp/udp.go: -------------------------------------------------------------------------------- 1 | package udp 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "fmt" 7 | "net" 8 | 9 | "github.com/mushorg/glutton/connection" 10 | "github.com/mushorg/glutton/producer" 11 | "github.com/mushorg/glutton/protocols/helpers" 12 | "github.com/mushorg/glutton/protocols/interfaces" 13 | ) 14 | 15 | func HandleUDP(ctx context.Context, srcAddr, dstAddr *net.UDPAddr, data []byte, md connection.Metadata, log interfaces.Logger, h interfaces.Honeypot) error { 16 | log.Info(fmt.Sprintf("UDP payload:\n%s", hex.Dump(data[:min(len(data), 1024)]))) 17 | if _, err := helpers.Store(data[:min(len(data), 1024)], "payloads"); err != nil { 18 | log.Error("failed to store UDP payload", producer.ErrAttr(err)) 19 | } 20 | if err := h.ProduceUDP("udp", srcAddr, dstAddr, md, data[:min(len(data), 1024)], nil); err != nil { 21 | log.Error("failed to produce UDP payload", producer.ErrAttr(err)) 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /rules/rules.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/google/gopacket" 11 | "github.com/google/gopacket/layers" 12 | "github.com/google/gopacket/pcap" 13 | yaml "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type RuleType int 17 | 18 | const ( 19 | UserConnHandler RuleType = iota 20 | Drop 21 | ) 22 | 23 | type Config struct { 24 | Version int `yaml:"version"` 25 | Rules Rules `yaml:"rules"` 26 | } 27 | 28 | type Rule struct { 29 | Match string `yaml:"match"` 30 | Type string `yaml:"type"` 31 | Target string `yaml:"target,omitempty"` 32 | Name string `yaml:"name,omitempty"` 33 | 34 | isInit bool 35 | ruleType RuleType 36 | index int 37 | matcher *pcap.BPF 38 | } 39 | 40 | func (r *Rule) String() string { 41 | return fmt.Sprintf("Rule: %s", r.Match) 42 | } 43 | 44 | func Init(file io.Reader) (Rules, error) { 45 | config := &Config{} 46 | if err := yaml.NewDecoder(file).Decode(config); err != nil { 47 | return nil, err 48 | } 49 | if err := config.Rules.init(); err != nil { 50 | return nil, err 51 | } 52 | return config.Rules, nil 53 | } 54 | 55 | func (rule *Rule) init(idx int) error { 56 | if rule.isInit { 57 | return nil 58 | } 59 | 60 | switch rule.Type { 61 | case "conn_handler": 62 | rule.ruleType = UserConnHandler 63 | case "drop": 64 | rule.ruleType = Drop 65 | default: 66 | return fmt.Errorf("unknown rule type: %s", rule.Type) 67 | } 68 | 69 | var err error 70 | if len(rule.Match) > 0 { 71 | rule.matcher, err = pcap.NewBPF(layers.LinkTypeEthernet, 65535, rule.Match) 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | 77 | rule.index = idx 78 | rule.isInit = true 79 | 80 | return nil 81 | } 82 | 83 | func splitAddr(addr string) (string, uint16, error) { 84 | ip, port, err := net.SplitHostPort(addr) 85 | if err != nil { 86 | return "", 0, err 87 | } 88 | dPort, err := strconv.Atoi(port) 89 | if err != nil { 90 | return "", 0, err 91 | } 92 | return ip, uint16(dPort), nil 93 | } 94 | 95 | func fakePacketBytes(network, srcIP, dstIP string, srcPort, dstPort uint16) ([]byte, error) { 96 | buf := gopacket.NewSerializeBuffer() 97 | eth := &layers.Ethernet{ 98 | SrcMAC: net.HardwareAddr{0x0, 0x11, 0x22, 0x33, 0x44, 0x55}, 99 | DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 100 | EthernetType: layers.EthernetTypeIPv4, 101 | } 102 | ipv4 := &layers.IPv4{ 103 | SrcIP: net.ParseIP(srcIP), 104 | DstIP: net.ParseIP(dstIP), 105 | Version: 4, 106 | } 107 | 108 | var transport gopacket.SerializableLayer 109 | switch network { 110 | case "tcp": 111 | ipv4.Protocol = layers.IPProtocolTCP 112 | tcp := &layers.TCP{ 113 | SrcPort: layers.TCPPort(srcPort), 114 | DstPort: layers.TCPPort(dstPort), 115 | } 116 | if err := tcp.SetNetworkLayerForChecksum(ipv4); err != nil { 117 | return nil, err 118 | } 119 | transport = tcp 120 | 121 | case "udp": 122 | ipv4.Protocol = layers.IPProtocolUDP 123 | udp := &layers.UDP{ 124 | SrcPort: layers.UDPPort(srcPort), 125 | DstPort: layers.UDPPort(dstPort), 126 | } 127 | if err := udp.SetNetworkLayerForChecksum(ipv4); err != nil { 128 | return nil, err 129 | } 130 | transport = udp 131 | } 132 | 133 | if err := gopacket.SerializeLayers(buf, gopacket.SerializeOptions{ 134 | FixLengths: true, 135 | ComputeChecksums: true, 136 | }, 137 | eth, 138 | ipv4, 139 | transport, 140 | gopacket.Payload([]byte{})); err != nil { 141 | return nil, err 142 | } 143 | return buf.Bytes(), nil 144 | } 145 | 146 | type Rules []*Rule 147 | 148 | func (rs Rules) Match(network string, srcAddr, dstAddr net.Addr) (*Rule, error) { 149 | srcIP, srcPort, err := splitAddr(srcAddr.String()) 150 | if err != nil { 151 | return nil, err 152 | } 153 | dstIP, dstPort, err := splitAddr(dstAddr.String()) 154 | if err != nil { 155 | return nil, err 156 | } 157 | b, err := fakePacketBytes(network, srcIP, dstIP, srcPort, dstPort) 158 | if err != nil { 159 | return nil, fmt.Errorf("failed to fake packet: %w", err) 160 | } 161 | 162 | for _, rule := range rs { 163 | if rule.matcher != nil { 164 | n := len(b) 165 | if rule.matcher.Matches(gopacket.CaptureInfo{ 166 | InterfaceIndex: 0, 167 | CaptureLength: n, 168 | Length: n, 169 | Timestamp: time.Now(), 170 | }, b) { 171 | return rule, nil 172 | } 173 | } 174 | } 175 | 176 | return nil, nil 177 | } 178 | 179 | // Init initializes the rules 180 | func (rs Rules) init() error { 181 | for i, rule := range rs { 182 | if err := rule.init(i); err != nil { 183 | return err 184 | } 185 | rs[i] = rule 186 | } 187 | return nil 188 | } 189 | -------------------------------------------------------------------------------- /rules/rules_test.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/google/gopacket" 10 | "github.com/google/gopacket/layers" 11 | "github.com/google/gopacket/pcap" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func parseRules(t *testing.T) Rules { 16 | fh, err := os.Open("test.yaml") 17 | require.NoError(t, err) 18 | rules, err := Init(fh) 19 | require.NoError(t, err) 20 | return rules 21 | } 22 | 23 | func TestParseRuleSpec(t *testing.T) { 24 | rules := parseRules(t) 25 | require.NotEmpty(t, rules) 26 | } 27 | 28 | func TestInitRule(t *testing.T) { 29 | rules := parseRules(t) 30 | require.NotEmpty(t, rules) 31 | 32 | for _, rule := range rules { 33 | require.True(t, rule.isInit) 34 | require.NotNil(t, rule.matcher) 35 | require.NotEmpty(t, rule.Type) 36 | } 37 | } 38 | 39 | func TestSplitAddr(t *testing.T) { 40 | ip, port, err := splitAddr("192.168.1.1:8080") 41 | require.NoError(t, err) 42 | require.Equal(t, "192.168.1.1", ip) 43 | require.Equal(t, uint16(8080), port) 44 | } 45 | 46 | func testConn(t *testing.T) (net.Conn, net.Listener) { 47 | ln, err := net.Listen("tcp", "127.0.0.1:1234") 48 | require.NoError(t, err) 49 | require.NotNil(t, ln) 50 | con, err := net.Dial(ln.Addr().Network(), ln.Addr().String()) 51 | require.NoError(t, err) 52 | require.NotNil(t, con) 53 | return con, ln 54 | } 55 | 56 | func TestFakePacketBytes(t *testing.T) { 57 | conn, ln := testConn(t) 58 | defer func() { 59 | conn.Close() 60 | ln.Close() 61 | }() 62 | b, err := fakePacketBytes("tcp", "1.1.1.1", "2.2.2.2", 12, 21) 63 | require.NoError(t, err) 64 | require.NotEmpty(t, b) 65 | } 66 | 67 | func TestRunMatchTCP(t *testing.T) { 68 | rules := parseRules(t) 69 | require.NotEmpty(t, rules) 70 | conn, ln := testConn(t) 71 | defer func() { 72 | conn.Close() 73 | ln.Close() 74 | }() 75 | var ( 76 | match *Rule 77 | err error 78 | ) 79 | 80 | match, err = rules.Match("tcp", conn.LocalAddr(), conn.RemoteAddr()) 81 | require.NoError(t, err) 82 | require.NotNil(t, match) 83 | require.Equal(t, "test", match.Target) 84 | } 85 | 86 | func TestRunMatchUDP(t *testing.T) { 87 | rules := parseRules(t) 88 | require.NotEmpty(t, rules) 89 | conn, ln := testConn(t) 90 | defer func() { 91 | conn.Close() 92 | ln.Close() 93 | }() 94 | var ( 95 | match *Rule 96 | err error 97 | ) 98 | 99 | match, err = rules.Match("udp", conn.LocalAddr(), conn.RemoteAddr()) 100 | require.NoError(t, err) 101 | require.NotNil(t, match) 102 | require.Equal(t, "test", match.Target) 103 | } 104 | 105 | func TestBPF(t *testing.T) { 106 | buf := make([]byte, 65535) 107 | bpfi, err := pcap.NewBPF(layers.LinkTypeEthernet, 65535, "icmp") 108 | require.NoError(t, err) 109 | fh, err := os.Open("test.yaml") 110 | require.NoError(t, err) 111 | n, err := fh.Read(buf) 112 | require.NoError(t, err) 113 | ci := gopacket.CaptureInfo{CaptureLength: n, Length: n, Timestamp: time.Now()} 114 | if bpfi.Matches(ci, buf) { 115 | t.Error("shouldn't match") 116 | } 117 | } 118 | 119 | func TestWorkingMatch(t *testing.T) { 120 | snaplen := 65535 121 | packet := [...]byte{ 122 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // dst mac 123 | 0x0, 0x11, 0x22, 0x33, 0x44, 0x55, // src mac 124 | 0x08, 0x0, // ether type 125 | 0x45, 0x0, 0x0, 0x3c, 0xa6, 0xc3, 0x40, 0x0, 0x40, 0x06, 0x3d, 0xd8, // ip header 126 | 0xc0, 0xa8, 0x50, 0x2f, // src ip 127 | 0xc0, 0xa8, 0x50, 0x2c, // dst ip 128 | 0xaf, 0x14, // src port 129 | 0x0, 0x50, // dst port 130 | } 131 | 132 | bpfi, _ := pcap.NewBPF(layers.LinkTypeEthernet, snaplen, "ip and tcp and port 80") 133 | ci := gopacket.CaptureInfo{ 134 | InterfaceIndex: 0, 135 | CaptureLength: len(packet), 136 | Length: len(packet), 137 | Timestamp: time.Now(), 138 | } 139 | if !bpfi.Matches(ci, packet[:]) { 140 | t.Fatal("didn't match") 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /rules/test.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - match: tcp dst port 22 or port 2222 3 | type: conn_handler 4 | name: proxy_ssh 5 | target: tcp://172.17.0.2:22 6 | - match: tcp dst port 1234 7 | type: conn_handler 8 | target: test 9 | - match: udp dst port 1234 10 | type: conn_handler 11 | target: test 12 | - match: tcp 13 | type: conn_handler 14 | target: tcp 15 | -------------------------------------------------------------------------------- /scanner/scanner.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | scannerSubnet = map[string][]string{ 10 | "censys": { 11 | "162.142.125.0/24", 12 | "167.94.138.0/24", 13 | "167.94.145.0/24", 14 | "167.94.146.0/24", 15 | "167.248.133.0/24", 16 | }, 17 | "shadowserver": { 18 | "64.62.202.96/27", 19 | "66.220.23.112/29", 20 | "74.82.47.0/26", 21 | "184.105.139.64/26", 22 | "184.105.143.128/26", 23 | "184.105.247.192/26", 24 | "216.218.206.64/26", 25 | "141.212.0.0/16", 26 | }, 27 | "PAN Expanse": { 28 | "144.86.173.0/24", 29 | }, 30 | "rwth": { 31 | "137.226.113.56/26", 32 | }, 33 | } 34 | ) 35 | 36 | func IsScanner(ip net.IP) (bool, string, error) { 37 | for scanner, subnets := range scannerSubnet { 38 | for _, subnet := range subnets { 39 | _, net, err := net.ParseCIDR(subnet) 40 | if err != nil { 41 | return false, "", err 42 | } 43 | if net.Contains(ip) { 44 | return true, scanner, nil 45 | } 46 | } 47 | } 48 | names, err := net.LookupAddr(ip.String()) 49 | if err != nil { 50 | return false, "", nil 51 | } 52 | for _, name := range names { 53 | if strings.HasSuffix(name, "shodan.io.") { 54 | return true, "shodan", nil 55 | } 56 | if strings.HasSuffix(name, "binaryedge.ninja.") { 57 | return true, "binaryedge", nil 58 | } 59 | if strings.HasSuffix(name, "rwth-aachen.de.") { 60 | return true, "rwth", nil 61 | } 62 | } 63 | return false, "", nil 64 | } 65 | -------------------------------------------------------------------------------- /scanner/scanner_test.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIsScanner(t *testing.T) { 11 | matched, _, err := IsScanner(net.ParseIP("162.142.125.1")) 12 | require.NoError(t, err) 13 | require.True(t, matched, "IP should be a scanner") 14 | } 15 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package glutton 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/seud0nym/tproxy-go/tproxy" 9 | ) 10 | 11 | type Server struct { 12 | tcpListener net.Listener 13 | udpConn *net.UDPConn 14 | tcpPort uint 15 | udpPort uint 16 | } 17 | 18 | func NewServer(tcpPort, udpPort uint) *Server { 19 | s := &Server{ 20 | tcpPort: tcpPort, 21 | udpPort: udpPort, 22 | } 23 | return s 24 | } 25 | 26 | func (s *Server) Start() error { 27 | tcpAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%d", s.tcpPort)) 28 | if err != nil { 29 | return err 30 | } 31 | if s.tcpListener, err = tproxy.ListenTCP("tcp4", tcpAddr); err != nil { 32 | return err 33 | } 34 | 35 | udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("127.0.0.1:%d", s.udpPort)) 36 | if err != nil { 37 | return err 38 | } 39 | if s.udpConn, err = tproxy.ListenUDP("udp4", udpAddr); err != nil { 40 | return err 41 | } 42 | if s.udpConn == nil { 43 | return errors.New("nil udp listener") 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package glutton 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestServer(t *testing.T) { 10 | server := NewServer(1234, 1235) 11 | require.NotNil(t, server) 12 | } 13 | -------------------------------------------------------------------------------- /system.go: -------------------------------------------------------------------------------- 1 | package glutton 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "runtime" 9 | "strings" 10 | "time" 11 | 12 | "github.com/glaslos/lsof" 13 | "github.com/google/gopacket/pcap" 14 | ) 15 | 16 | func countOpenFiles() (int, error) { 17 | if runtime.GOOS == "linux" { 18 | lines, err := lsof.ReadPID(os.Getpid()) 19 | return len(lines) - 1, err 20 | } 21 | return 0, errors.New("operating system type not supported for this command") 22 | } 23 | 24 | func (g *Glutton) startMonitor() { 25 | ticker := time.NewTicker(10 * time.Second) 26 | go func() { 27 | for { 28 | select { 29 | case <-ticker.C: 30 | openFiles, err := countOpenFiles() 31 | if err != nil { 32 | fmt.Printf("Failed :%s", err) 33 | } 34 | runningRoutines := runtime.NumGoroutine() 35 | g.Logger.Debug(fmt.Sprintf("running Go routines: %d, open files: %d", openFiles, runningRoutines)) 36 | case <-g.ctx.Done(): 37 | g.Logger.Info("Monitoring stopped...") 38 | ticker.Stop() 39 | return 40 | } 41 | } 42 | }() 43 | } 44 | 45 | func getNonLoopbackIPs(ifaceName string) ([]net.IP, error) { 46 | nonLoopback := []net.IP{} 47 | 48 | ifs, err := pcap.FindAllDevs() 49 | if err != nil { 50 | return nonLoopback, err 51 | } 52 | 53 | for _, iface := range ifs { 54 | if strings.EqualFold(iface.Name, ifaceName) { 55 | for _, addr := range iface.Addresses { 56 | if !addr.IP.IsLoopback() && addr.IP.To4() != nil { 57 | nonLoopback = append(nonLoopback, addr.IP) 58 | } 59 | } 60 | } 61 | } 62 | 63 | if len(nonLoopback) == 0 { 64 | return nonLoopback, fmt.Errorf("unable to find any non-loopback addresses for: %s", ifaceName) 65 | } 66 | 67 | return nonLoopback, nil 68 | } 69 | -------------------------------------------------------------------------------- /system_test.go: -------------------------------------------------------------------------------- 1 | package glutton 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCountOpenFiles(t *testing.T) { 11 | openFiles, err := countOpenFiles() 12 | require.NoError(t, err, "failed to count open files") 13 | require.NotEmpty(t, openFiles, "unexpected number of open files") 14 | } 15 | 16 | func TestCountRunningRoutines(t *testing.T) { 17 | runningRoutines := runtime.NumGoroutine() 18 | require.NotEmpty(t, runningRoutines, "expected running routines") 19 | } 20 | --------------------------------------------------------------------------------