Glutton has two output paths: process logs through Go slog, and optional producer events sent to HTTP or hpfeeds sinks. Handlers can emit both, but they're configured separately.
producer.NewLogger(...) creates a JSON slog logger that writes every record to:
- stdout
- the path configured by
--logpath
The file writer uses lumberjack rotation: 200 MB max size, 356 days max age, compression on. It is skipped (stdout only) when --logpath is empty, /dev/null (the default), or an existing non-regular file: lumberjack rotates by renaming the file and creating a new regular one, which on /dev/null replaced the device with a file in RAM-backed devtmpfs that every process on the host wrote into. Every record carries the sensorID attribute (read from / written to <var-dir>/glutton.id).
The --debug flag is parsed but not wired into slog.HandlerOptions, so it does not currently lower the log level.
Producer events follow the producer.Event schema:
| JSON field | Type | Meaning |
|---|---|---|
timestamp |
string (RFC 3339) | UTC time the event was produced. |
startedAt |
string (RFC 3339) | Start of what the event covers (md.Added): the TCP connection's accept time from the connection table, or the UDP datagram's receive time (for a SIP dialog, its INVITE). Sensors up to v1.0.1+b9e1a04 reused the first datagram seen from the same UDP source address and port, so UDP startedAt could be minutes early. Go does not omit a zero time.Time, so an unknown start is 0001-01-01T00:00:00Z, not absent. |
durationMs |
number, optional | Milliseconds from startedAt to produce time. Omitted when 0. |
transport |
string | tcp or udp. |
srcHost |
string | Source IP. |
srcPort |
string | Source port. Unlike dstPort, this is a string. |
srcPtr |
string, optional | First reverse-DNS name when a PTR lookup already ran (not on CIDR-matched scanners). |
dstHost |
string, optional | Original destination IP (TPROXY LocalAddr / UDP dest). This is the sensor's address; strip it before showing events publicly. |
dstPort |
number | Original destination port from metadata. |
sensorID |
string | Glutton sensor ID. |
sensorVersion |
string, optional | Build version (VERSION / sensor_version). |
rule |
string, optional | Rule match string when metadata includes a rule. |
ruleName |
string, optional | Optional name from rules.yaml. |
handler |
string | Handler name supplied by the protocol handler. |
handlers |
array of strings, optional | Protocol stack from the wire up, e.g. ["tcp", "tls", "socks", "tls", "http"]: the transport, a tls layer the sensor terminated before the handler (rule tls), the handler, then layers the handler decoded inside its own stream (a SOCKS tunnel carrying TLS and HTTP). Handlers that decode a nested layer record it: SOCKS (tls, http), the catch-all, RDP and Jabber (tls, from in-band, CredSSP-era or STARTTLS handshakes). When one handler hands the session to another the earlier layer comes before it (["tcp", "http", "mcp"], ["tcp", "tls", "http"] for HTTPS the catch-all decrypted). The last entry is the innermost protocol. Older sensors omit it; fall back to [transport, handler]. |
payload |
string (base64), optional | First-frame payload bytes. |
payloadHash |
string (hex), optional | SHA-256 of the (sanitized) top-level payload. |
frameCount |
number, optional | Number of decoded frames when decoded is a slice. Omitted when 0. |
endReason |
string, optional | Why the session ended (client_close, client_reset, timeout, handler_close, read_error, write_error, max_frames, evicted). client_reset means the client aborted the connection with a TCP RST instead of closing it. evicted means the handler's session table was full and the least recently active session was flushed early. Omitted by handlers that do not set it. |
mptcp |
boolean, optional | true when the TCP connection negotiated Multipath TCP (the client offered MP_CAPABLE and the sensor kernel supports MPTCP). The sensor's TCP listener is MPTCP-enabled (Go 1.24+ default). Omitted for plain TCP and UDP. |
tls |
object, optional | Present only when the sensor terminated TLS. See TLS details. |
scanner |
string, optional | Scanner classification from scanner.Classify(...), else a payload-hash fleet label, else a JA4 fleet label (from tls.ja4, per handler), else a behavioral label the handler set on the session (ssh-authkeys-implant, ssh-spoofed-openssh). Omitted when empty. |
decoded |
array, object or null, optional |
Handler-specific decoded data. See Decoded data. |
All optional fields use omitempty: an empty string, a zero number or a nil value is left out of the JSON instead of being sent as "", 0 or null.
Events are emitted only when (1) producers.enabled is true so a producer object exists, (2) a handler calls ProduceTCP(...) or ProduceUDP(...), (3) the matched rule does not set produce: false, and (4) at least one sink is enabled. Before output, configured addresses values are scrubbed from payload bytes (ASCII and UTF-16LE) and replaced with 1.2.3.4. The same sanitizer runs on every string and byte slice inside decoded (frame fields such as from, to, endpoint_url, nested structs, maps), so the sensor address never appears in decoded output; fixed-size byte arrays are left alone. Events from sensors built before commit 937dcd8 (after v1.0.1) only scrubbed payload and path.
tls is set when the rule has tls: true, or tls: auto and the client opened with a ClientHello, or when the tcp catch-all terminated a complete ClientHello itself (its first decoded frame is then the raw tls-clienthello read, followed by a tls-handshake write for the server flight; if the decrypted traffic is HTTP, the event is an http event instead and has no such frames). decoded and payload then hold the decrypted plaintext protocol, so a display should say that the session was TLS. When the handshake fails mid-flight there is no top-level tls object, but the catch-all still records the tls-handshake write. If the honeypot could not send its flight (endReason write_error), that write has an error field (reply over budget when the tcp_reply_limit guard dropped it, otherwise write failed) and may have no payload.
| Field | Type | Meaning |
|---|---|---|
serverName |
string, optional | SNI from the ClientHello. |
alpn |
array of strings, optional | Protocols offered by the client. |
version |
string, optional | Negotiated TLS version, e.g. TLS 1.3. |
cipher |
string, optional | Negotiated cipher suite. Empty (so omitted) when the handshake failed. |
clientHello |
string (base64), optional | Raw ClientHello records, capped at 4 KiB. |
truncated |
boolean, optional | Set when clientHello was cut at the cap. |
ja3 |
string, optional | JA3 MD5 of the ClientHello. Set whenever the recorded hello parses, also when the handshake failed. |
ja3n |
string, optional | JA3 MD5 with extensions sorted, stable for clients that randomize extension order (Chrome). |
ja4 |
string, optional | FoxIO JA4 fingerprint, e.g. t13d1516h2_8daaf6152771_e5627efa2ab1. |
ja4_r |
string, optional | JA4 raw form: the sorted cipher and extension lists and the signature algorithms in clear instead of hashed. |
Value encodings inside decoded follow Go's encoding/json:
- Byte slices (
[]byte) are base64 strings. Framepayloadfields are always base64. - Fields documented as "(hex)" are hex strings produced by the handler (
spi_i,spi_r,token,session_idonopenvpn,challenge, and so on). - Fixed-size byte arrays are JSON arrays of numbers 0–255. Today these are only the
bittorrentfieldsprotocol_identifier,reserved,info_hashandpeer_id; show them as hex. - Every other array of numbers is a list of IDs, not bytes:
cipher_suitesandextensions(dtls, uint16),etypes(kerberos),encodings(rfb, int32, can be negative). Show these as lists; joining them as hex gives wrong values. - Nested objects (
headeronsmb,mongodbandbgp,open/notificationonbgp,messageoncassandra,headerandoptionsoncql,questionsonmdns,submessagesonrtps) are JSON objects or arrays of objects.
Array-of-frames decoded entries share these JSON names when the handler fills them: direction, payload, command (leaf operation), path, status (writes), truncated. Handler-specific fields sit beside them. A display that shows only the shared fields hides the most useful data for most handlers, so show every key a frame carries and use the per-handler page linked in the table below to order them. Some fields are easy to miss but matter for reading a session:
http:dest_portandsrc_porton reads, because a session can span connections and ports while the top-leveldstPort/srcPortbelong to the first connection.rdp:ntlm_domain,ntlm_user,ntlm_workstationandntlm_versionon theNTLMAuthenticateframe, plusnetntlmv2(hashcat-m 5600) for NTLMv2;nla_acceptedwhen a password inrdp.credentialsorrdp.guess_passwordsmatched, thenntlm_userandnla_passwordfrom the decrypted authInfo;client_data(client name,client_name_ansi, build, screen, keyboard, channels) onMCSConnectInitial;client_info(domain, username,password, client time zonetz_bias/tz_standard_name/tz_daylight_name) onClientInfo. Most probes never do NLA: the Confirm prefers TLS, so the password arrives inclient_info. An unknown CredSSP password still getsSTATUS_LOGON_FAILURE.sip:from,to,call_id,username,called_number(normalized dialed number; a REFER's transfer target),tool(fingerprinted scanner/dialer or fleet tell), the digest-auth fieldsrealm/nonce/digest_uri/digest_response/algorithm/qop/nc/cnonce(the honeypot issued the nonce, so these crack the guessed password offline, hashcat-m 11400),variant/visiton writes, anddroppedon UDP writes the reply budget kept from leaving. UDP calls end with onecommandrtpframe per caller media stream:dtmfdigits,packets/lost,duration_ms, andpayload_hashof the WAV recording.ssh,telnet,jabber,minecraft,smtp,opcua,mqtt,dicom,pop3,ftp,http,rtsp,socks,hiflying,sadp,mssql,firebird,postgres,smb,rsync:username(oruser) andpasswordas the client sent them. Passwords are guessing attempts, not confidential data, so they are recorded in plaintext.smbalso setslm_response/ntlm_response(hex) for basic Session Setup challenge responses.rsyncsetsresponse(the base64 digest of the password and the AUTHREQD challenge) instead ofpassword.hl7:security(MSH-8) as the client sent it; some senders put a password or token there, so it is recorded in plaintext like other credentials.dtls,jabber:server_name.smb,tcp:shellcodeon a read, listing shellcode/PE heuristics matched in the captured payload;smbalso setsxor_keyanddecoded_hashwhen a DoublePulsar payload was de-obfuscated, andvariant/visiton emulated DoublePulsar replies.
Example shape:
{
"timestamp": "2026-05-15T12:00:00Z",
"transport": "tcp",
"srcHost": "203.0.113.10",
"srcPort": "54321",
"dstHost": "192.0.2.10",
"dstPort": 80,
"sensorID": "00000000-0000-0000-0000-000000000000",
"sensorVersion": "v0.0.0",
"rule": "Rule: tcp",
"frameCount": 1,
"endReason": "client_close",
"handler": "http",
"payload": "R0VUIC8gSFRUUC8xLjENCg0K",
"decoded": [
{
"direction": "read",
"command": "GET",
"path": "/",
"session_id": "11111111-1111-1111-1111-111111111111",
"payload": "R0VUIC8gSFRUUC8xLjENCg0K"
}
]
}decoded is handler-specific:
When producers.http.enabled is true, Glutton marshals each event as JSON and POSTs it to producers.http.remote with Content-Type: application/json. From source:
- HTTP client timeout: 10s; TLS handshake timeout: 5s.
- Events from private source IPs are skipped.
- Userinfo in the remote URL is used as HTTP Basic Auth.
- Query strings in the configured URL are preserved.
When producers.hpfeeds.enabled is true, Glutton connects to the broker at startup and publishes gob-encoded producer.Event values to producers.hpfeeds.channel.
producers:
hpfeeds:
enabled: false
host: 172.26.0.2
port: 20000
ident: ident
auth: auth
channel: testTreat all logged payloads as attacker-controlled data in downstream pipelines.