- Add Timestamp field (int64, nanoseconds since Unix epoch) to LogRecord - Import time package in api/types.go - Set timestamp using time.Now().UnixNano() in NewLogRecord() - Add test assertion to verify timestamp is set The timestamp is now included in all JSON log outputs Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
278 lines
9.2 KiB
Go
278 lines
9.2 KiB
Go
package api
|
|
|
|
import "time"
|
|
|
|
// ServiceLog represents internal service logging for diagnostics
|
|
type ServiceLog struct {
|
|
Level string `json:"level"`
|
|
Component string `json:"component"`
|
|
Message string `json:"message"`
|
|
Details map[string]string `json:"details,omitempty"`
|
|
Timestamp int64 `json:"timestamp,omitempty"` // Unix nanoseconds (auto-set by logger)
|
|
TraceID string `json:"trace_id,omitempty"` // Optional distributed tracing ID
|
|
ConnID string `json:"conn_id,omitempty"` // Optional TCP flow identifier
|
|
}
|
|
|
|
// Config holds basic network and TLS configuration
|
|
type Config struct {
|
|
Interface string `json:"interface"`
|
|
ListenPorts []uint16 `json:"listen_ports"`
|
|
BPFFilter string `json:"bpf_filter,omitempty"`
|
|
FlowTimeoutSec int `json:"flow_timeout_sec,omitempty"` // Timeout for TLS handshake extraction (default: 30)
|
|
PacketBufferSize int `json:"packet_buffer_size,omitempty"` // Buffer size for packet channel (default: 1000)
|
|
}
|
|
|
|
// IPMeta contains IP metadata for stack fingerprinting
|
|
type IPMeta struct {
|
|
TTL uint8 `json:"ttl"`
|
|
TotalLength uint16 `json:"total_length"`
|
|
IPID uint16 `json:"id"`
|
|
DF bool `json:"df"`
|
|
}
|
|
|
|
// TCPMeta contains TCP metadata for stack fingerprinting
|
|
type TCPMeta struct {
|
|
WindowSize uint16 `json:"window_size"`
|
|
MSS uint16 `json:"mss,omitempty"`
|
|
WindowScale uint8 `json:"window_scale,omitempty"`
|
|
Options []string `json:"options"`
|
|
}
|
|
|
|
// RawPacket represents a raw packet captured from the network
|
|
type RawPacket struct {
|
|
Data []byte `json:"-"` // Not serialized
|
|
Timestamp int64 `json:"timestamp"` // nanoseconds since epoch
|
|
}
|
|
|
|
// TLSClientHello represents a client-side TLS ClientHello with IP/TCP metadata
|
|
type TLSClientHello struct {
|
|
SrcIP string `json:"src_ip"`
|
|
SrcPort uint16 `json:"src_port"`
|
|
DstIP string `json:"dst_ip"`
|
|
DstPort uint16 `json:"dst_port"`
|
|
Payload []byte `json:"-"` // Not serialized
|
|
IPMeta IPMeta `json:"ip_meta"`
|
|
TCPMeta TCPMeta `json:"tcp_meta"`
|
|
}
|
|
|
|
// Fingerprints contains TLS fingerprints for a client flow
|
|
type Fingerprints struct {
|
|
JA4 string `json:"ja4"`
|
|
JA4Hash string `json:"ja4_hash"`
|
|
JA3 string `json:"ja3,omitempty"`
|
|
JA3Hash string `json:"ja3_hash,omitempty"`
|
|
}
|
|
|
|
// LogRecord is the final log record, serialized as a flat JSON object
|
|
type LogRecord struct {
|
|
SrcIP string `json:"src_ip"`
|
|
SrcPort uint16 `json:"src_port"`
|
|
DstIP string `json:"dst_ip"`
|
|
DstPort uint16 `json:"dst_port"`
|
|
|
|
// Flattened IPMeta fields
|
|
IPTTL uint8 `json:"ip_meta_ttl"`
|
|
IPTotalLen uint16 `json:"ip_meta_total_length"`
|
|
IPID uint16 `json:"ip_meta_id"`
|
|
IPDF bool `json:"ip_meta_df"`
|
|
|
|
// Flattened TCPMeta fields
|
|
TCPWindow uint16 `json:"tcp_meta_window_size"`
|
|
TCPMSS *uint16 `json:"tcp_meta_mss,omitempty"`
|
|
TCPWScale *uint8 `json:"tcp_meta_window_scale,omitempty"`
|
|
TCPOptions string `json:"tcp_meta_options"` // comma-separated list
|
|
|
|
// Fingerprints
|
|
JA4 string `json:"ja4"`
|
|
JA4Hash string `json:"ja4_hash"`
|
|
JA3 string `json:"ja3,omitempty"`
|
|
JA3Hash string `json:"ja3_hash,omitempty"`
|
|
|
|
// Timestamp in nanoseconds since Unix epoch
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
// OutputConfig defines configuration for a single log output
|
|
type OutputConfig struct {
|
|
Type string `json:"type"` // unix_socket, stdout, file, etc.
|
|
Enabled bool `json:"enabled"` // whether this output is active
|
|
Params map[string]string `json:"params"` // specific parameters like socket_path, path, etc.
|
|
}
|
|
|
|
// AppConfig is the complete ja4sentinel configuration
|
|
type AppConfig struct {
|
|
Core Config `json:"core"`
|
|
Outputs []OutputConfig `json:"outputs"`
|
|
}
|
|
|
|
// Loader defines the interface for loading application configuration.
|
|
// Implementations must read configuration from a YAML file, merge with
|
|
// environment variables (JA4SENTINEL_*), and validate the final result.
|
|
type Loader interface {
|
|
Load() (AppConfig, error)
|
|
}
|
|
|
|
// Capture defines the interface for capturing raw network packets.
|
|
// Implementations must listen on a configured network interface, apply
|
|
// BPF filters for specified ports, and emit RawPacket objects to a channel.
|
|
// The Close method must be called to release resources (e.g., pcap handle).
|
|
type Capture interface {
|
|
Run(cfg Config, out chan<- RawPacket) error
|
|
Close() error
|
|
}
|
|
|
|
// Parser defines the interface for extracting TLS ClientHello messages
|
|
// from raw network packets. Implementations must track TCP connection states,
|
|
// reassemble fragmented handshakes, and return TLSClientHello objects with
|
|
// IP/TCP metadata. Returns nil for non-TLS or non-ClientHello packets.
|
|
type Parser interface {
|
|
Process(pkt RawPacket) (*TLSClientHello, error)
|
|
Close() error
|
|
}
|
|
|
|
// Engine defines the interface for generating TLS fingerprints.
|
|
// Implementations must analyze TLS ClientHello payloads and produce
|
|
// JA4 (required) and optionally JA3 fingerprint strings.
|
|
type Engine interface {
|
|
FromClientHello(ch TLSClientHello) (*Fingerprints, error)
|
|
}
|
|
|
|
// Writer defines the generic interface for writing log records.
|
|
// Implementations must serialize LogRecord objects and send them to
|
|
// a destination (stdout, file, UNIX socket, etc.).
|
|
type Writer interface {
|
|
Write(rec LogRecord) error
|
|
}
|
|
|
|
// UnixSocketWriter extends Writer with a Close method for UNIX socket cleanup.
|
|
// Implementations must connect to a UNIX socket at the specified path and
|
|
// write JSON-encoded LogRecord objects. Reconnection logic should be
|
|
// implemented for transient socket failures.
|
|
type UnixSocketWriter interface {
|
|
Writer
|
|
Close() error
|
|
}
|
|
|
|
// MultiWriter extends Writer to support multiple output destinations.
|
|
// Implementations must write each LogRecord to all registered writers
|
|
// and provide methods to add writers and close all connections.
|
|
type MultiWriter interface {
|
|
Writer
|
|
Add(writer Writer)
|
|
CloseAll() error
|
|
}
|
|
|
|
// Builder defines the interface for constructing output writers from configuration.
|
|
// Implementations must parse AppConfig.Outputs and create appropriate Writer
|
|
// instances (StdoutWriter, FileWriter, UnixSocketWriter), combining them
|
|
// into a MultiWriter if multiple outputs are configured.
|
|
type Builder interface {
|
|
NewFromConfig(cfg AppConfig) (Writer, error)
|
|
}
|
|
|
|
// Logger defines the interface for structured service logging.
|
|
// Implementations must emit JSON-formatted log entries to stdout/stderr
|
|
// with support for multiple log levels (DEBUG, INFO, WARN, ERROR).
|
|
// Each log entry includes timestamp, level, component, message, and optional details.
|
|
type Logger interface {
|
|
Debug(component, message string, details map[string]string)
|
|
Info(component, message string, details map[string]string)
|
|
Warn(component, message string, details map[string]string)
|
|
Error(component, message string, details map[string]string)
|
|
}
|
|
|
|
// Helper functions for creating and converting records
|
|
|
|
// NewLogRecord creates a flattened LogRecord from TLSClientHello and Fingerprints.
|
|
// Converts TCPMeta options to a comma-separated string and creates pointer values
|
|
// for optional fields (MSS, WindowScale) to support proper JSON omitempty behavior.
|
|
// If fingerprints is nil, the JA4/JA3 fields will be empty strings.
|
|
func NewLogRecord(ch TLSClientHello, fp *Fingerprints) LogRecord {
|
|
opts := ""
|
|
if len(ch.TCPMeta.Options) > 0 {
|
|
opts = joinStringSlice(ch.TCPMeta.Options, ",")
|
|
}
|
|
|
|
// Helper to create pointer from value for optional fields
|
|
var mssPtr *uint16
|
|
if ch.TCPMeta.MSS != 0 {
|
|
mssPtr = &ch.TCPMeta.MSS
|
|
}
|
|
|
|
var wScalePtr *uint8
|
|
if ch.TCPMeta.WindowScale != 0 {
|
|
wScalePtr = &ch.TCPMeta.WindowScale
|
|
}
|
|
|
|
rec := LogRecord{
|
|
SrcIP: ch.SrcIP,
|
|
SrcPort: ch.SrcPort,
|
|
DstIP: ch.DstIP,
|
|
DstPort: ch.DstPort,
|
|
IPTTL: ch.IPMeta.TTL,
|
|
IPTotalLen: ch.IPMeta.TotalLength,
|
|
IPID: ch.IPMeta.IPID,
|
|
IPDF: ch.IPMeta.DF,
|
|
TCPWindow: ch.TCPMeta.WindowSize,
|
|
TCPMSS: mssPtr,
|
|
TCPWScale: wScalePtr,
|
|
TCPOptions: opts,
|
|
Timestamp: time.Now().UnixNano(),
|
|
}
|
|
|
|
if fp != nil {
|
|
rec.JA4 = fp.JA4
|
|
rec.JA4Hash = fp.JA4Hash
|
|
rec.JA3 = fp.JA3
|
|
rec.JA3Hash = fp.JA3Hash
|
|
}
|
|
|
|
return rec
|
|
}
|
|
|
|
// Helper to join string slice with separator
|
|
func joinStringSlice(slice []string, sep string) string {
|
|
if len(slice) == 0 {
|
|
return ""
|
|
}
|
|
result := slice[0]
|
|
for _, s := range slice[1:] {
|
|
result += sep + s
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Default values and constants
|
|
|
|
const (
|
|
DefaultInterface = "eth0"
|
|
DefaultPort = 443
|
|
DefaultBPFFilter = ""
|
|
DefaultFlowTimeout = 30 // seconds
|
|
DefaultPacketBuffer = 1000 // packet channel buffer size
|
|
|
|
// Logging levels
|
|
LogLevelDebug = "DEBUG"
|
|
LogLevelInfo = "INFO"
|
|
LogLevelWarn = "WARN"
|
|
LogLevelError = "ERROR"
|
|
)
|
|
|
|
// DefaultConfig returns an AppConfig with sensible default values.
|
|
// Uses eth0 as the default interface, port 443 for monitoring,
|
|
// no BPF filter, a 30-second flow timeout, and a 1000-packet
|
|
// channel buffer. Returns an empty outputs slice (caller must
|
|
// configure outputs explicitly).
|
|
func DefaultConfig() AppConfig {
|
|
return AppConfig{
|
|
Core: Config{
|
|
Interface: DefaultInterface,
|
|
ListenPorts: []uint16{DefaultPort},
|
|
BPFFilter: DefaultBPFFilter,
|
|
FlowTimeoutSec: DefaultFlowTimeout,
|
|
PacketBufferSize: DefaultPacketBuffer,
|
|
},
|
|
Outputs: []OutputConfig{},
|
|
}
|
|
}
|