feat: implémentation complète du pipeline JA4 + Docker + tests

Nouveaux modules:
- cmd/ja4sentinel/main.go : point d'entrée avec pipeline capture→parse→fingerprint→output
- internal/config/loader.go : chargement YAML + env (JA4SENTINEL_*) + validation
- internal/tlsparse/parser.go : extraction ClientHello avec suivi d'état de flux (NEW/WAIT_CLIENT_HELLO/JA4_DONE)
- internal/fingerprint/engine.go : génération JA4/JA3 via psanford/tlsfingerprint
- internal/output/writers.go : StdoutWriter, FileWriter, UnixSocketWriter, MultiWriter

Infrastructure:
- Dockerfile (multi-stage), Dockerfile.dev, Dockerfile.test-server
- Makefile (build, test, lint, docker-build-*)
- docker-compose.test.yml pour tests d'intégration
- README.md (276 lignes) avec architecture, config, exemples

API (api/types.go):
- Ajout Close() aux interfaces Capture et Parser
- Ajout FlowTimeoutSec dans Config (défaut: 30s, env: JA4SENTINEL_FLOW_TIMEOUT)
- ServiceLog: +Timestamp, +TraceID, +ConnID
- LogRecord: champs flatten (ip_meta_*, tcp_meta_*, ja4*)
- Helper NewLogRecord() pour conversion TLSClientHello+Fingerprints→LogRecord

Architecture (architecture.yml):
- Documentation module logging + interfaces LoggerFactory/Logger
- Section service.systemd complète (unit, security, capabilities)
- Section logging.strategy (JSON lines, champs, règles)
- api.Config: +FlowTimeoutSec documenté

Fixes/cleanup:
- Suppression internal/api/types.go (consolidé dans api/types.go)
- Correction imports logging (ja4sentinel/api)
- .dockerignore / .gitignore
- config.yml.example

Tests:
- Tous les modules ont leurs tests (*_test.go)
- Tests unitaires : capture, config, fingerprint, output, tlsparse
- Tests d'intégration via docker-compose.test.yml

Build:
- Binaires dans dist/ (make build → dist/ja4sentinel)
- Docker runtime avec COPY --from=builder /app/dist/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Jacquin Antoine
2026-02-25 20:02:52 +01:00
parent 3b09f9416e
commit efd4481729
28 changed files with 2797 additions and 285 deletions

189
cmd/ja4sentinel/main.go Normal file
View File

@ -0,0 +1,189 @@
// Package main provides the entry point for ja4sentinel
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"ja4sentinel/api"
"ja4sentinel/internal/capture"
"ja4sentinel/internal/config"
"ja4sentinel/internal/fingerprint"
"ja4sentinel/internal/logging"
"ja4sentinel/internal/output"
"ja4sentinel/internal/tlsparse"
)
// Version information (set via ldflags)
var (
Version = "dev"
BuildTime = "unknown"
GitCommit = "unknown"
)
func main() {
// Parse command line flags
configPath := flag.String("config", "", "Path to configuration file (YAML)")
version := flag.Bool("version", false, "Print version information")
flag.Parse()
if *version {
fmt.Printf("ja4sentinel version %s\n", Version)
fmt.Printf("Build time: %s\n", BuildTime)
fmt.Printf("Git commit: %s\n", GitCommit)
os.Exit(0)
}
// Initialize logger factory
loggerFactory := &logging.LoggerFactory{}
logger := loggerFactory.NewDefaultLogger()
logger.Info("service", "Starting ja4sentinel", map[string]string{
"version": Version,
})
// Load configuration
configLoader := config.NewLoader(*configPath)
cfg, err := configLoader.Load()
if err != nil {
logger.Error("service", "Failed to load configuration", map[string]string{
"error": err.Error(),
})
os.Exit(1)
}
logger.Info("config", "Configuration loaded", map[string]string{
"interface": cfg.Core.Interface,
"ports": fmt.Sprintf("%v", cfg.Core.ListenPorts),
"bpf_filter": cfg.Core.BPFFilter,
"num_outputs": fmt.Sprintf("%d", len(cfg.Outputs)),
})
// Build output writer
outputBuilder := output.NewBuilder()
writer, err := outputBuilder.NewFromConfig(cfg)
if err != nil {
logger.Error("output", "Failed to create output writer", map[string]string{
"error": err.Error(),
})
os.Exit(1)
}
// Create pipeline components
captureImpl := capture.New()
parser := tlsparse.NewParserWithTimeout(time.Duration(cfg.Core.FlowTimeoutSec) * time.Second)
engine := fingerprint.NewEngine()
// Create channels for pipeline
packetChan := make(chan api.RawPacket, 1000)
helloChan := make(chan api.TLSClientHello, 1000)
errorChan := make(chan error, 100)
// Setup signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start capture goroutine
go func() {
logger.Info("capture", "Starting packet capture", map[string]string{
"interface": cfg.Core.Interface,
})
err := captureImpl.Run(cfg.Core, packetChan)
if err != nil {
errorChan <- fmt.Errorf("capture error: %w", err)
}
}()
// Start TLS parsing goroutine
go func() {
for pkt := range packetChan {
hello, err := parser.Process(pkt)
if err != nil {
logger.Warn("tlsparse", "Failed to parse packet", map[string]string{
"error": err.Error(),
})
continue
}
if hello != nil {
logger.Debug("tlsparse", "ClientHello extracted", map[string]string{
"src_ip": hello.SrcIP,
"src_port": fmt.Sprintf("%d", hello.SrcPort),
"dst_ip": hello.DstIP,
"dst_port": fmt.Sprintf("%d", hello.DstPort),
})
helloChan <- *hello
}
}
}()
// Start fingerprinting and output goroutine
go func() {
for hello := range helloChan {
fingerprints, err := engine.FromClientHello(hello)
if err != nil {
logger.Warn("fingerprint", "Failed to generate fingerprints", map[string]string{
"error": err.Error(),
"src_ip": hello.SrcIP,
})
continue
}
logger.Debug("fingerprint", "JA4 generated", map[string]string{
"src_ip": hello.SrcIP,
"ja4": fingerprints.JA4,
})
// Create log record and write
rec := api.NewLogRecord(hello, fingerprints)
if err := writer.Write(rec); err != nil {
logger.Error("output", "Failed to write log record", map[string]string{
"error": err.Error(),
})
}
}
}()
// Wait for shutdown signal or error
select {
case sig := <-sigChan:
logger.Info("service", "Received shutdown signal", map[string]string{
"signal": sig.String(),
})
case err := <-errorChan:
logger.Error("service", "Pipeline error", map[string]string{
"error": err.Error(),
})
}
// Graceful shutdown
logger.Info("service", "Shutting down", nil)
// Close output writer
if closer, ok := writer.(interface{ CloseAll() error }); ok {
if err := closer.CloseAll(); err != nil {
logger.Error("output", "Error closing writer", map[string]string{
"error": err.Error(),
})
}
}
// Close parser (stops cleanup goroutine)
if err := parser.Close(); err != nil {
logger.Error("tlsparse", "Error closing parser", map[string]string{
"error": err.Error(),
})
}
// Close capture
if err := captureImpl.Close(); err != nil {
logger.Error("capture", "Error closing capture", map[string]string{
"error": err.Error(),
})
}
logger.Info("service", "ja4sentinel stopped", nil)
}