feat: Implémenter le système de logging complet avec interfaces et structures de données
Co-authored-by: aider (openrouter/qwen/qwen3-coder-plus) <aider@aider.chat>
This commit is contained in:
182
api/types.go
182
api/types.go
@ -1,6 +1,10 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
// ServiceLog représente un log interne du service ja4sentinel (diagnostic).
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceLog represents internal service logging for diagnostics
|
||||||
type ServiceLog struct {
|
type ServiceLog struct {
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
Component string `json:"component"`
|
Component string `json:"component"`
|
||||||
@ -8,14 +12,14 @@ type ServiceLog struct {
|
|||||||
Details map[string]string `json:"details,omitempty"`
|
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 {
|
type Config struct {
|
||||||
Interface string `json:"interface"`
|
Interface string `json:"interface"`
|
||||||
ListenPorts []uint16 `json:"listen_ports"`
|
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 {
|
type IPMeta struct {
|
||||||
TTL uint8 `json:"ttl"`
|
TTL uint8 `json:"ttl"`
|
||||||
TotalLength uint16 `json:"total_length"`
|
TotalLength uint16 `json:"total_length"`
|
||||||
@ -23,33 +27,32 @@ type IPMeta struct {
|
|||||||
DF bool `json:"df"`
|
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 {
|
type TCPMeta struct {
|
||||||
WindowSize uint16 `json:"window_size"`
|
WindowSize uint16 `json:"window_size"`
|
||||||
MSS uint16 `json:"mss"`
|
MSS uint16 `json:"mss,omitempty"`
|
||||||
WindowScale uint8 `json:"window_scale"`
|
WindowScale uint8 `json:"window_scale,omitempty"`
|
||||||
Options []string `json:"options"`
|
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 {
|
type RawPacket struct {
|
||||||
Data []byte `json:"data"`
|
Data []byte `json:"-"` // Not serialized
|
||||||
Timestamp int64 `json:"timestamp"`
|
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 {
|
type TLSClientHello struct {
|
||||||
SrcIP string `json:"src_ip"`
|
SrcIP string `json:"src_ip"`
|
||||||
SrcPort uint16 `json:"src_port"`
|
SrcPort uint16 `json:"src_port"`
|
||||||
DstIP string `json:"dst_ip"`
|
DstIP string `json:"dst_ip"`
|
||||||
DstPort uint16 `json:"dst_port"`
|
DstPort uint16 `json:"dst_port"`
|
||||||
Payload []byte `json:"payload"`
|
Payload []byte `json:"-"` // Not serialized
|
||||||
|
|
||||||
IPMeta IPMeta `json:"ip_meta"`
|
IPMeta IPMeta `json:"ip_meta"`
|
||||||
TCPMeta TCPMeta `json:"tcp_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 {
|
type Fingerprints struct {
|
||||||
JA4 string `json:"ja4"`
|
JA4 string `json:"ja4"`
|
||||||
JA4Hash string `json:"ja4_hash"`
|
JA4Hash string `json:"ja4_hash"`
|
||||||
@ -57,45 +60,164 @@ type Fingerprints struct {
|
|||||||
JA3Hash string `json:"ja3_hash,omitempty"`
|
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 {
|
type LogRecord struct {
|
||||||
// Adresse IP source (client)
|
|
||||||
SrcIP string `json:"src_ip"`
|
SrcIP string `json:"src_ip"`
|
||||||
// Port source (client)
|
|
||||||
SrcPort uint16 `json:"src_port"`
|
SrcPort uint16 `json:"src_port"`
|
||||||
// Adresse IP destination (serveur)
|
|
||||||
DstIP string `json:"dst_ip"`
|
DstIP string `json:"dst_ip"`
|
||||||
// Port destination (serveur)
|
|
||||||
DstPort uint16 `json:"dst_port"`
|
DstPort uint16 `json:"dst_port"`
|
||||||
|
|
||||||
// Métadonnées IP (flatten)
|
// Flattened IPMeta fields
|
||||||
IPTTL uint8 `json:"ip_meta_ttl"`
|
IPTTL uint8 `json:"ip_meta_ttl"`
|
||||||
IPTotalLen uint16 `json:"ip_meta_total_length"`
|
IPTotalLen uint16 `json:"ip_meta_total_length"`
|
||||||
IPID uint16 `json:"ip_meta_id"`
|
IPID uint16 `json:"ip_meta_id"`
|
||||||
IPDF bool `json:"ip_meta_df"`
|
IPDF bool `json:"ip_meta_df"`
|
||||||
|
|
||||||
// Métadonnées TCP (flatten)
|
// Flattened TCPMeta fields
|
||||||
TCPWindow uint16 `json:"tcp_meta_window_size"`
|
TCPWindow uint16 `json:"tcp_meta_window_size"`
|
||||||
TCPMSS uint16 `json:"tcp_meta_mss"`
|
TCPMSS uint16 `json:"tcp_meta_mss,omitempty"`
|
||||||
TCPWScale uint8 `json:"tcp_meta_window_scale"`
|
TCPWScale uint8 `json:"tcp_meta_window_scale,omitempty"`
|
||||||
TCPOptions string `json:"tcp_meta_options"`
|
TCPOptions string `json:"tcp_meta_options"` // comma-separated list
|
||||||
|
|
||||||
// Empreintes
|
// Fingerprints
|
||||||
JA4 string `json:"ja4"`
|
JA4 string `json:"ja4"`
|
||||||
JA4Hash string `json:"ja4_hash"`
|
JA4Hash string `json:"ja4_hash"`
|
||||||
JA3 string `json:"ja3,omitempty"`
|
JA3 string `json:"ja3,omitempty"`
|
||||||
JA3Hash string `json:"ja3_hash,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 OutputConfig struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"` // unix_socket, stdout, file, etc.
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"` // whether this output is active
|
||||||
Params map[string]string `json:"params"`
|
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 {
|
type AppConfig struct {
|
||||||
Core Config `json:"core"`
|
Core Config `json:"core"`
|
||||||
Outputs []OutputConfig `json:"outputs"`
|
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{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
19
internal/logging/logger_factory.go
Normal file
19
internal/logging/logger_factory.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
120
internal/logging/service_logger.go
Normal file
120
internal/logging/service_logger.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user