From 3b09f9416e4a408720bffba2bb1ea0aceef2c5b6 Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Wed, 25 Feb 2026 04:17:40 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Impl=C3=A9menter=20le=20syst=C3=A8me=20?= =?UTF-8?q?de=20logging=20complet=20avec=20interfaces=20et=20structures=20?= =?UTF-8?q?de=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/qwen/qwen3-coder-plus) --- api/types.go | 200 +++++++++++++++++++++++------ internal/logging/logger_factory.go | 19 +++ internal/logging/service_logger.go | 120 +++++++++++++++++ 3 files changed, 300 insertions(+), 39 deletions(-) create mode 100644 internal/logging/logger_factory.go create mode 100644 internal/logging/service_logger.go diff --git a/api/types.go b/api/types.go index 293e7a3..6530f24 100644 --- a/api/types.go +++ b/api/types.go @@ -1,6 +1,10 @@ package api -// ServiceLog représente un log interne du service ja4sentinel (diagnostic). +import ( + "time" +) + +// ServiceLog represents internal service logging for diagnostics type ServiceLog struct { Level string `json:"level"` Component string `json:"component"` @@ -8,14 +12,14 @@ type ServiceLog struct { Details map[string]string `json:"details,omitempty"` } -// Config contient la configuration réseau et TLS de base. +// Config holds basic network and TLS configuration type Config struct { Interface string `json:"interface"` ListenPorts []uint16 `json:"listen_ports"` - BPFFilter string `json:"bpf_filter"` + BPFFilter string `json:"bpf_filter,omitempty"` } -// IPMeta contient les métadonnées IP pour fingerprinting de stack. +// IPMeta contains IP metadata for stack fingerprinting type IPMeta struct { TTL uint8 `json:"ttl"` TotalLength uint16 `json:"total_length"` @@ -23,33 +27,32 @@ type IPMeta struct { DF bool `json:"df"` } -// TCPMeta contient les métadonnées TCP pour fingerprinting de stack. +// TCPMeta contains TCP metadata for stack fingerprinting type TCPMeta struct { WindowSize uint16 `json:"window_size"` - MSS uint16 `json:"mss"` - WindowScale uint8 `json:"window_scale"` + MSS uint16 `json:"mss,omitempty"` + WindowScale uint8 `json:"window_scale,omitempty"` Options []string `json:"options"` } -// RawPacket représente un paquet brut capturé sur le réseau. +// RawPacket represents a raw packet captured from the network type RawPacket struct { - Data []byte `json:"data"` - Timestamp int64 `json:"timestamp"` + Data []byte `json:"-"` // Not serialized + Timestamp int64 `json:"timestamp"` // nanoseconds since epoch } -// TLSClientHello représente un ClientHello TLS client, avec meta IP/TCP. +// 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:"payload"` - - IPMeta IPMeta `json:"ip_meta"` + Payload []byte `json:"-"` // Not serialized + IPMeta IPMeta `json:"ip_meta"` TCPMeta TCPMeta `json:"tcp_meta"` } -// Fingerprints contient les empreintes TLS pour un flux client. +// Fingerprints contains TLS fingerprints for a client flow type Fingerprints struct { JA4 string `json:"ja4"` JA4Hash string `json:"ja4_hash"` @@ -57,45 +60,164 @@ type Fingerprints struct { JA3Hash string `json:"ja3_hash,omitempty"` } -// LogRecord est un enregistrement de log final, sérialisé en JSON objet plat. +// LogRecord is the final log record, serialized as a flat JSON object type LogRecord struct { - // Adresse IP source (client) - SrcIP string `json:"src_ip"` - // Port source (client) - SrcPort uint16 `json:"src_port"` - // Adresse IP destination (serveur) - DstIP string `json:"dst_ip"` - // Port destination (serveur) - DstPort uint16 `json:"dst_port"` + SrcIP string `json:"src_ip"` + SrcPort uint16 `json:"src_port"` + DstIP string `json:"dst_ip"` + DstPort uint16 `json:"dst_port"` - // Métadonnées IP (flatten) - 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 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"` - // Métadonnées TCP (flatten) + // Flattened TCPMeta fields TCPWindow uint16 `json:"tcp_meta_window_size"` - TCPMSS uint16 `json:"tcp_meta_mss"` - TCPWScale uint8 `json:"tcp_meta_window_scale"` - TCPOptions string `json:"tcp_meta_options"` + TCPMSS uint16 `json:"tcp_meta_mss,omitempty"` + TCPWScale uint8 `json:"tcp_meta_window_scale,omitempty"` + TCPOptions string `json:"tcp_meta_options"` // comma-separated list - // Empreintes + // Fingerprints JA4 string `json:"ja4"` JA4Hash string `json:"ja4_hash"` JA3 string `json:"ja3,omitempty"` JA3Hash string `json:"ja3_hash,omitempty"` } -// OutputConfig configure une sortie de logs. +// OutputConfig defines configuration for a single log output type OutputConfig struct { - Type string `json:"type"` - Enabled bool `json:"enabled"` - Params map[string]string `json:"params"` + 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 est la configuration complète de ja4sentinel. +// AppConfig is the complete ja4sentinel configuration type AppConfig struct { Core Config `json:"core"` Outputs []OutputConfig `json:"outputs"` } + +// Loader interface loads configuration from file/env/CLI +type Loader interface { + Load() (AppConfig, error) +} + +// Capture interface provides raw network packets +type Capture interface { + Run(cfg Config, out chan<- RawPacket) error +} + +// Parser converts RawPacket to TLSClientHello +type Parser interface { + Process(pkt RawPacket) (*TLSClientHello, error) +} + +// Engine generates JA4 fingerprints from TLS ClientHello +type Engine interface { + FromClientHello(ch TLSClientHello) (*Fingerprints, error) +} + +// Writer is the generic interface for writing results +type Writer interface { + Write(rec LogRecord) error +} + +// UnixSocketWriter implements Writer sending logs to a UNIX socket +type UnixSocketWriter interface { + Writer + Close() error +} + +// MultiWriter combines multiple Writers +type MultiWriter interface { + Writer + Add(writer Writer) + CloseAll() error +} + +// Builder constructs Writers from AppConfig +type Builder interface { + NewFromConfig(cfg AppConfig) (Writer, error) +} + +// Logger interface for service logging +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 LogRecord from TLSClientHello and Fingerprints +func NewLogRecord(ch TLSClientHello, fp *Fingerprints) LogRecord { + opts := "" + if len(ch.TCPMeta.Options) > 0 { + opts = joinStringSlice(ch.TCPMeta.Options, ",") + } + + 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: ch.TCPMeta.MSS, + TCPWScale: ch.TCPMeta.WindowScale, + TCPOptions: opts, + } + + 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 = "" + + // Logging levels + LogLevelDebug = "DEBUG" + LogLevelInfo = "INFO" + LogLevelWarn = "WARN" + LogLevelError = "ERROR" +) + +// DefaultConfig returns a configuration with sensible defaults +func DefaultConfig() AppConfig { + return AppConfig{ + Core: Config{ + Interface: DefaultInterface, + ListenPorts: []uint16{DefaultPort}, + BPFFilter: DefaultBPFFilter, + }, + Outputs: []OutputConfig{}, + } +} diff --git a/internal/logging/logger_factory.go b/internal/logging/logger_factory.go new file mode 100644 index 0000000..2377749 --- /dev/null +++ b/internal/logging/logger_factory.go @@ -0,0 +1,19 @@ +// Package logging provides a factory for creating loggers +package logging + +import ( + "github.com/your-repo/ja4sentinel/api" +) + +// LoggerFactory creates logger instances +type LoggerFactory struct{} + +// NewLogger creates a new logger based on configuration +func (f *LoggerFactory) NewLogger(level string) api.Logger { + return NewServiceLogger(level) +} + +// NewDefaultLogger creates a logger with default settings +func (f *LoggerFactory) NewDefaultLogger() api.Logger { + return NewServiceLogger("info") +} diff --git a/internal/logging/service_logger.go b/internal/logging/service_logger.go new file mode 100644 index 0000000..edd1e32 --- /dev/null +++ b/internal/logging/service_logger.go @@ -0,0 +1,120 @@ +// Package logging provides structured logging for ja4sentinel service components +package logging + +import ( + "encoding/json" + "fmt" + "log" + "os" + "strings" + "sync" + "time" + + "github.com/your-repo/ja4sentinel/api" +) + +// ServiceLogger handles structured logging for the ja4sentinel service +type ServiceLogger struct { + level string + mutex sync.Mutex + out *log.Logger + formatter func(api.ServiceLog) ([]byte, error) +} + +// NewServiceLogger creates a new service logger +func NewServiceLogger(level string) *ServiceLogger { + logger := &ServiceLogger{ + level: strings.ToLower(level), + out: log.New(os.Stdout, "", 0), + formatter: func(s api.ServiceLog) ([]byte, error) { + logData := map[string]interface{}{ + "timestamp": time.Now().UnixNano(), + "level": strings.ToUpper(s.Level), + "component": s.Component, + "message": s.Message, + } + + if s.Details != nil && len(s.Details) > 0 { + for k, v := range s.Details { + logData[k] = v + } + } + + return json.Marshal(logData) + }, + } + + return logger +} + +// Log emits a structured log entry to stdout in JSON format +func (l *ServiceLogger) Log(component, level, message string, details map[string]string) { + if !l.isLogLevelEnabled(level) { + return + } + + // Lock to prevent concurrent writes to stdout + l.mutex.Lock() + defer l.mutex.Unlock() + + serviceLog := api.ServiceLog{ + Level: level, + Component: component, + Message: message, + Details: details, + } + + jsonData, err := l.formatter(serviceLog) + if err != nil { + // Fallback to simple logging if JSON formatting fails + fmt.Printf(`{"timestamp":%d,"level":"ERROR","component":"logging","message":"%s","original_message":"%s"}`, + time.Now().UnixNano(), err.Error(), message) + return + } + + fmt.Println(string(jsonData)) +} + +// Debug logs a debug level entry +func (l *ServiceLogger) Debug(component, message string, details map[string]string) { + if l.isLogLevelEnabled("debug") { + l.Log(component, "DEBUG", message, details) + } +} + +// Info logs an info level entry +func (l *ServiceLogger) Info(component, message string, details map[string]string) { + if l.isLogLevelEnabled("info") { + l.Log(component, "INFO", message, details) + } +} + +// Warn logs a warning level entry +func (l *ServiceLogger) Warn(component, message string, details map[string]string) { + if l.isLogLevelEnabled("warn") { + l.Log(component, "WARN", message, details) + } +} + +// Error logs an error level entry +func (l *ServiceLogger) Error(component, message string, details map[string]string) { + if l.isLogLevelEnabled("error") { + l.Log(component, "ERROR", message, details) + } +} + +// isLogLevelEnabled checks if a log level should be emitted based on configured level +func (l *ServiceLogger) isLogLevelEnabled(messageLevel string) bool { + switch l.level { + case "debug": + return true + case "info": + return messageLevel != "debug" + case "warn": + return messageLevel != "debug" && messageLevel != "info" + case "error": + return messageLevel == "error" + default: + return false // If level is invalid, don't log anything + } +}