Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c9c7764
feat: introduces Spicy runtime bridge and initial Spicy-HTTP parser
Boolean-Autocrat Jul 6, 2025
d9a6e3b
feat: adds basic and relevant function documentation for bridge and p…
Boolean-Autocrat Jul 6, 2025
722a49e
chore: hardens Spicy bridge - OOM-safe growth, parse timeout, HTTP si…
Boolean-Autocrat Jul 6, 2025
c7c1e39
chore: hardens C++ glue, adds depth guard & OOM-safe field handling
Boolean-Autocrat Jul 6, 2025
5a1ccd6
feat: adds tests for parser and Spicy http handler
Boolean-Autocrat Jul 7, 2025
49b6c58
chore: tightens up parser tests, adds relevant comments for helpers
Boolean-Autocrat Jul 7, 2025
7052c45
refactor: update GitHub Actions to install Spicy C++ runtime, bumps G…
Boolean-Autocrat Jul 7, 2025
60ca1d6
fix: adds CI step for generating Spicy files
Boolean-Autocrat Jul 7, 2025
aec7036
fix: adds Spicy CLI to actions runner path
Boolean-Autocrat Jul 7, 2025
e933fdb
chore: adds missing ldflags to make build
Boolean-Autocrat Jul 8, 2025
51ed6f2
chore: adds error handling for failed file read
Boolean-Autocrat Jul 8, 2025
a970c55
refactor: rename inner `raw` variable to `r` in drain goroutine
Boolean-Autocrat Jul 8, 2025
4001b4a
refactor: normalizes proto name before parser lookup
Boolean-Autocrat Jul 9, 2025
fd87964
Merge PR #186 into Spicy continuation branch
furusiyya Apr 26, 2026
cb612a4
refactor(spicy bridge): make bridge protocol-agnostic with combined l…
furusiyya May 8, 2026
d1991be
refactor(spicy http): parse path and query fields
furusiyya May 9, 2026
18a1f49
refactor(tcp): add Spicy protocol detection parser
furusiyya May 9, 2026
5a457c3
docs: Adds Spicy bridge architecture and configuration details
furusiyya May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,47 @@ jobs:
checks:
runs-on: ubuntu-latest
steps:
- name: Dependencies
run: sudo apt install libpcap-dev iptables
- name: Basic Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpcap-dev iptables zlib1g-dev build-essential

- name: Install spicyc
run: |
wget https://github.com/zeek/spicy/releases/download/v1.13.1/spicy_linux_ubuntu24.deb
sudo dpkg --install spicy_linux_ubuntu24.deb
sudo apt-get install -f -y # pulling in any missing deps
rm spicy_linux_ubuntu24.deb

- name: Add Spicy CLI to PATH
run: echo "/opt/spicy/bin" >> $GITHUB_PATH

- name: Install clang17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 17

- name: Checkout
uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "^1.21"
go-version: "^1.23"

- name: Build Spicy generated files
run: |
make spicy

- name: Build
env:
CC: clang
CXX: clang++
run: go build -v ./...

- name: Test
env:
CC: clang
CXX: clang++
run: go test -v ./...
12 changes: 7 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ poc/

# Dev
.vscode
openspec/
.cache
.codex
openspec/
docs/roadmap.md
.cache/
.codex/
docs/engineering-guidelines.md
docs/roadmap.md
openspec/

# Spicy generated files
protocols/spicy/*.cc
protocols/spicy/parsers/*.h
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ upx:
default: build

build:
go build -ldflags=$(LDFLAGS) -o bin/server app/server.go
CC=clang CXX=clang++ go build -ldflags=$(LDFLAGS) -o bin/server app/server.go

spicy:
cd protocols/spicy && make

static:
go build --ldflags '-extldflags "-static"' -o bin/server app/server.go
Expand Down
5 changes: 4 additions & 1 deletion config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ max_tcp_payload: 4096 # bytes
dial_timeout: 5 # timeout in seconds for proxy target connection.

capture_traffic:
enabled: false
enabled: false

spicy:
enabled: true
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This file holds the core settings for Glutton. Key configuration options include
- **max_tcp_payload:** Maximum TCP payload size in bytes (default: `4096`). Proxy TCP uses this as the per-direction captured payload cap.
- **dial_timeout:** Timeout, in seconds, for opening outbound proxy TCP target connections (default: `5`).
- **capture_traffic.enabled:** Enables raw payload capture in logs and produced decoded events. When disabled, proxy TCP still forwards traffic and logs metadata, but raw payload bytes are omitted from decoded events.
- **spicy.enabled:** Enables Spicy parser integration for supported protocols. When enabled, Glutton initializes the Spicy/HILTI runtime and uses Spicy-backed parsing paths where implemented. When disabled, Glutton uses the existing Go parser and TCP fallback paths.
- **confpath:** The directory path where the configuration file resides.
- **producers:**
- **enabled**: Boolean flag to enable or disable logging/producer functionality.
Expand Down Expand Up @@ -61,6 +62,9 @@ dial_timeout: 5

capture_traffic:
enabled: false

spicy:
enabled: true
```

### config/rules.yaml
Expand Down
51 changes: 50 additions & 1 deletion docs/extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,57 @@ For example:
- Write tests similar to those in `protocols/protocols_test.go` to verify your new handler’s functionality.
- Use `go test` to ensure that your changes do not break existing functionality.

## Spicy Parser Integration

Glutton can use [Spicy](https://docs.zeek.org/projects/spicy/en/latest/) parsers when `spicy.enabled: true` is set in `config/config.yaml`. Spicy parsing is currently used for:

- HTTP request parsing through `HTTP::Request`, with Glutton behavior implemented in `protocols/spicy/handlers/http.go`.
- TCP payload protocol detection through `TCP::Protocol`, which helps route raw TCP traffic to HTTP, RDP, MongoDB, or the fallback TCP handler.

Spicy does not replace Glutton's handler layer. The parser extracts structured fields from bytes; Go handlers still own connection handling, logging, producer events, responses, and fallback behavior.

### Architecture

The Spicy integration has three main parts:

- **C++ bridge:** `protocols/spicy/bridge.{h,cpp}` initializes and shuts down the Spicy/HILTI runtime, exposes the generic `spicy_parse_generic()` entry point, lists compiled parsers, and flattens parsed HILTI values into key-value fields.
- **Generated parser code:** `make spicy` compiles `protocols/spicy/parsers/*.spicy` into generated C++ files and creates the combined Spicy linker file used to register parser modules.
- **Go parser worker:** `protocols/spicy/parser.go` runs Spicy calls on a dedicated OS thread with `runtime.LockOSThread()`, communicates through a command channel, keeps the parser registry, and applies timeout protection around parsing.

The bridge is intentionally protocol-agnostic. New parsers should not add parser-specific includes or parser-specific logic to `bridge.cpp`; concrete parser modules are registered through the generated linker code.

### Parsed Field Shape

The bridge recursively flattens Spicy/HILTI values into a flat map:

- Struct fields become `field` or `parent.field`.
- Vectors and arrays become `field[0]`, `field[1]`, and so on.
- Maps become `field.key`.
- Nested values are depth-limited to prevent runaway recursion.

For example, an HTTP parser can expose fields such as `method`, `path`, `query`, `version.number`, or `headers[0].name`.

### TCP Protocol Detection

When Spicy is enabled, the raw TCP dispatcher can call `spicy.Parse("tcp", sample)` before falling back to the older byte-prefix checks. The `TCP::Protocol` parser inspects application payload bytes, not TCP/IP headers.

The detector currently recognizes:

- HTTP from the first request bytes.
- RDP from the initial connection bytes.
- MongoDB from a valid 16-byte message header.

Unknown or invalid samples continue through the normal fallback TCP handler.

### Adding a Spicy Parser

1. Add a grammar under `protocols/spicy/parsers/*.spicy`.
2. Run `make spicy` before building or testing so generated parser C++ files, parser headers, and the combined linker file are available.
3. Use the parser from Go through `spicy.Parse("<protocol>", payload)`. Parser names are registered from compiled Spicy module names, for example `HTTP::Request` is available as `http`.
4. Add or update Glutton routing and handler code when the parser should be used for live traffic. A compiled grammar makes parsing available, but it does not decide how much data to read, what handler to call, what response to send, or which producer event shape to emit.
5. Add tests for the parser and any handler or routing behavior that consumes it.

## Customizing Logging and Rules

- **Logging:** The logging mechanism is provided by the Producer (located in `producer/`). You can modify or extend it to suit your logging infrastructure.
- **Rules Engine:** The rules engine (found in `rules/`) can be extended to support additional matching criteria or custom rule types.

7 changes: 4 additions & 3 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ Ensure you have [Go](https://go.dev/dl/) installed (recommended version: **Go 1.

```bash
sudo apt-get update
sudo apt-get install gcc libpcap-dev iptables
sudo apt-get install gcc g++ libpcap-dev iptables
```

Spicy parser development also requires Spicy/HILTI headers and libraries under `/opt/spicy` and a C++20-capable compiler. After installing Spicy, run `make spicy` before `make build` or `go test ./...`. The `make spicy` target generates parser C++ files, parser headers, and the combined linker file used by the generic Spicy bridge.

### Arch Linux
```bash
sudo pacman -S gcc libpcap iptables
```

### Fedora
```bash
sudo dnf install gcc libpcap-devel iptables
sudo dnf install gcc gcc-c++ libpcap-devel iptables
```

## Building Glutton
Expand Down Expand Up @@ -103,4 +105,3 @@ To deploy using Docker:
```

The Docker container is preconfigured with the necessary dependencies (iptables, libpcap, etc.) and copies the configuration and rules files into the container.

15 changes: 14 additions & 1 deletion glutton.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/mushorg/glutton/connection"
"github.com/mushorg/glutton/producer"
"github.com/mushorg/glutton/protocols"
"github.com/mushorg/glutton/protocols/spicy"
"github.com/mushorg/glutton/rules"

"github.com/google/uuid"
Expand Down Expand Up @@ -134,6 +135,13 @@ func (g *Glutton) Init() error {
g.tcpProtocolHandlers = protocols.MapTCPProtocolHandlers(g.Logger, g)
g.udpProtocolHandlers = protocols.MapUDPProtocolHandlers(g.Logger, g)

// Initializing Spicy parsers
if viper.GetBool("spicy.enabled") {
if err := spicy.Initialize(g.Logger); err != nil {
return fmt.Errorf("failed to initialize Spicy: %w", err)
}
}

return nil
}

Expand Down Expand Up @@ -366,7 +374,12 @@ func (g *Glutton) Shutdown() {
if err := flushTProxyIPTables(viper.GetString("interface"), g.publicAddrs[0].String(), "udp", uint32(g.Server.udpPort), uint32(viper.GetInt("ports.ssh"))); err != nil {
g.Logger.Error("Failed to drop udp iptables", producer.ErrAttr(err))
}

if viper.GetBool("spicy.enabled") {
g.Logger.Info("Cleaning up and shutting down Spicy and HILTI runtimes")
if err := spicy.Cleanup(); err != nil {
g.Logger.Error("Failed to clean up Spicy and HILTI runtimes", producer.ErrAttr(err))
}
}
g.Logger.Info("All done")
}

Expand Down
61 changes: 31 additions & 30 deletions protocols/protocols.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package protocols

import (
"bytes"
"context"
"encoding/binary"
"net"
"strings"

"github.com/mushorg/glutton/connection"
"github.com/mushorg/glutton/producer"
"github.com/mushorg/glutton/protocols/interfaces"
"github.com/mushorg/glutton/protocols/spicy"
spicyHandlers "github.com/mushorg/glutton/protocols/spicy/handlers"
"github.com/mushorg/glutton/protocols/tcp"
"github.com/mushorg/glutton/protocols/udp"
"github.com/spf13/viper"
)

type TCPHandlerFunc func(ctx context.Context, conn net.Conn, md connection.Metadata) error
Expand Down Expand Up @@ -72,6 +73,9 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st
protocolHandlers["mongodb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
return tcp.HandleMongoDB(ctx, conn, md, log, h)
}
protocolHandlers["http"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
return tcp.HandleHTTP(ctx, conn, md, log, h)
}
protocolHandlers["proxy_tcp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
return tcp.HandleProxyTCP(ctx, conn, md, log, h)
}
Expand All @@ -84,18 +88,17 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st
log.Debug("failed to peek connection", producer.ErrAttr(err))
return nil
}
// poor mans check for HTTP request
httpMap := map[string]bool{"GET ": true, "POST": true, "HEAD": true, "OPTI": true, "CONN": true}
if _, ok := httpMap[strings.ToUpper(string(snip))]; ok {
return tcp.HandleHTTP(ctx, bufConn, md, log, h)
}
// poor mans check for RDP header
if bytes.Equal(snip, []byte{0x03, 0x00, 0x00, 0x2b}) {
return tcp.HandleRDP(ctx, bufConn, md, log, h)
}
// poor mans check for MongoDB header (checking msg length and validating opcodes)
messageLength := binary.LittleEndian.Uint32(snip)
if messageLength > 0 && messageLength <= 48*1024*1024 {

// Uses a basic spicy parser to detect application protocol from tcp payload
if viper.GetBool("spicy.enabled") {
if protocol, ok := parseTCPProtocol(snip, log); ok {
switch protocol {
case "http":
return spicyHandlers.HandleHTTP(ctx, bufConn, md, log, h)
case "rdp":
return tcp.HandleRDP(ctx, bufConn, md, log, h)
}
}
moreSample, bufConn, err := Peek(bufConn, 16)
if err != nil {
if err := conn.Close(); err != nil {
Expand All @@ -104,26 +107,24 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st
log.Debug("failed to peek connection", producer.ErrAttr(err))
return nil
}
if len(moreSample) == 16 {
opCode := binary.LittleEndian.Uint32(moreSample[12:16])
validOpCodes := map[uint32]bool{
1: true, // OP_REPLY
2001: true, // OP_UPDATE
2002: true, // OP_INSERT
2004: true, // OP_QUERY
2005: true, // OP_GET_MORE
2006: true, // OP_DELETE
2007: true, // OP_KILL_CURSORS
2012: true, // OP_COMPRESSED
2013: true, // OP_MSG
}
if _, ok := validOpCodes[opCode]; ok {
return tcp.HandleMongoDB(ctx, bufConn, md, log, h)
}
if protocol, ok := parseTCPProtocol(moreSample, log); ok && protocol == "mongodb" {
return tcp.HandleMongoDB(ctx, bufConn, md, log, h)
}
}
// fallback TCP handler
return tcp.HandleTCP(ctx, bufConn, md, log, h)
}
return protocolHandlers
}

func parseTCPProtocol(sample []byte, log interfaces.Logger) (string, bool) {
parsed, err := spicy.Parse("tcp", sample)
if err != nil {
log.Error("spicy tcp protocol parse error", producer.ErrAttr(err))
return "", false
}

protocol, ok := parsed.Fields["protocol"].(string)
protocol = strings.ToLower(strings.TrimSpace(protocol))
return protocol, ok && protocol != ""
}
Loading
Loading