feat: version 1.0.0 avec corrections critiques et nommage de packages

Ajout du point d'entrée principal :
- cmd/ja4sentinel/main.go : pipeline complet avec gestion des signaux
- Intégration des modules (capture, tlsparse, fingerprint, output)
- Shutdown propre avec context.Context

Corrections du parsing TLS :
- Flow key unidirectionnel (client → serveur uniquement)
- Timeout de flux configurable via FlowTimeoutSec
- Structure ConnectionFlow simplifiée

Améliorations de l'API :
- Champs TCPMSS et TCPWScale en pointeurs (omitempty correct)
- NewLogRecord mis à jour pour les champs optionnels

Mise à jour de l'architecture :
- architecture.yml : documentation des champs optionnels
- Règles de flux unidirectionnel documentées

Système de packages :
- Version par défaut : 1.0.0
- Nommage cohérent : ja4sentinel_1.0.0_amd64.deb
- Scripts build-deb.sh et build-rpm.sh simplifiés
- Extraction correcte des checksums

Tests :
- TestFlowKey mis à jour pour le format unidirectionnel
- Tous les tests passent (go test ./...)
- go vet clean

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Jacquin Antoine
2026-02-26 23:24:42 +01:00
parent 410467f099
commit 9280cb545c
9 changed files with 201 additions and 179 deletions

View File

@ -2,11 +2,11 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
@ -19,190 +19,192 @@ import (
"ja4sentinel/internal/tlsparse"
)
// Version information (set via ldflags)
var (
Version = "dev"
// Version information (set via ldflags)
Version = "1.0.0"
BuildTime = "unknown"
GitCommit = "unknown"
)
func main() {
// Parse command line flags
// Parse command-line flags
configPath := flag.String("config", "", "Path to configuration file (YAML)")
version := flag.Bool("version", false, "Print version information")
version := flag.Bool("version", false, "Show 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)
fmt.Printf("ja4sentinel version %s (built %s, commit %s)\n", Version, BuildTime, GitCommit)
os.Exit(0)
}
// Initialize logger factory
// Create logger factory
loggerFactory := &logging.LoggerFactory{}
logger := loggerFactory.NewDefaultLogger()
appLogger := loggerFactory.NewDefaultLogger()
logger.Info("service", "Starting ja4sentinel", map[string]string{
"version": Version,
appLogger.Info("main", "Starting ja4sentinel", map[string]string{
"version": Version,
"build_time": BuildTime,
"git_commit": GitCommit,
})
// Load configuration
configLoader := config.NewLoader(*configPath)
cfg, err := configLoader.Load()
cfgLoader := config.NewLoader(*configPath)
appConfig, err := cfgLoader.Load()
if err != nil {
logger.Error("service", "Failed to load configuration", map[string]string{
appLogger.Error("main", "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)),
appLogger.Info("main", "Configuration loaded", map[string]string{
"interface": appConfig.Core.Interface,
"listen_ports": formatPorts(appConfig.Core.ListenPorts),
})
// 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 context with cancellation for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 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)
var wg sync.WaitGroup
// Setup signal handling for graceful shutdown
// Setup signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start capture goroutine
wg.Add(1)
go func() {
defer wg.Done()
defer close(packetChan)
// Create pipeline components
captureEngine := capture.New()
parser := tlsparse.NewParserWithTimeout(time.Duration(appConfig.Core.FlowTimeoutSec) * time.Second)
fingerprintEngine := fingerprint.NewEngine()
outputBuilder := output.NewBuilder()
logger.Info("capture", "Starting packet capture", map[string]string{
"interface": cfg.Core.Interface,
outputWriter, err := outputBuilder.NewFromConfig(appConfig)
if err != nil {
appLogger.Error("main", "Failed to create output writer", map[string]string{
"error": err.Error(),
})
os.Exit(1)
}
err := captureImpl.Run(cfg.Core, packetChan)
if err != nil {
// Create channel for raw packets
packetChan := make(chan api.RawPacket, 1000)
// Start capture goroutine
captureErrChan := make(chan error, 1)
go func() {
appLogger.Info("capture", "Starting packet capture", map[string]string{
"interface": appConfig.Core.Interface,
})
err := captureEngine.Run(appConfig.Core, packetChan)
captureErrChan <- err
}()
// Process packets
go func() {
for {
select {
case errorChan <- fmt.Errorf("capture error: %w", err):
default:
case <-ctx.Done():
appLogger.Info("main", "Packet processor shutting down", nil)
return
case pkt, ok := <-packetChan:
if !ok {
return
}
// Parse TLS ClientHello
clientHello, err := parser.Process(pkt)
if err != nil {
appLogger.Warn("tlsparse", "Failed to parse TLS ClientHello", map[string]string{
"error": err.Error(),
})
continue
}
if clientHello == nil {
continue // Not a TLS ClientHello packet
}
appLogger.Debug("tlsparse", "ClientHello extracted", map[string]string{
"src_ip": clientHello.SrcIP,
"src_port": fmt.Sprintf("%d", clientHello.SrcPort),
"dst_ip": clientHello.DstIP,
"dst_port": fmt.Sprintf("%d", clientHello.DstPort),
})
// Generate fingerprints
fingerprints, err := fingerprintEngine.FromClientHello(*clientHello)
if err != nil {
appLogger.Warn("fingerprint", "Failed to generate fingerprints", map[string]string{
"error": err.Error(),
})
continue
}
appLogger.Debug("fingerprint", "Fingerprints generated", map[string]string{
"src_ip": clientHello.SrcIP,
"ja4": fingerprints.JA4,
})
// Create log record
logRecord := api.NewLogRecord(*clientHello, fingerprints)
// Write output
if err := outputWriter.Write(logRecord); err != nil {
appLogger.Error("output", "Failed to write log record", map[string]string{
"error": err.Error(),
})
}
}
}
}()
// Start TLS parsing goroutine
wg.Add(1)
go func() {
defer wg.Done()
defer close(helloChan)
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
wg.Add(1)
go func() {
defer wg.Done()
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
// Wait for shutdown signal or capture error
select {
case sig := <-sigChan:
logger.Info("service", "Received shutdown signal", map[string]string{
appLogger.Info("main", "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)
if err := captureImpl.Close(); err != nil {
logger.Error("capture", "Error closing capture", map[string]string{
"error": err.Error(),
})
}
wg.Wait()
// Close parser (stops cleanup goroutine)
if err := parser.Close(); err != nil {
logger.Error("tlsparse", "Error closing parser", map[string]string{
"error": err.Error(),
})
}
// 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{
case err := <-captureErrChan:
if err != nil {
appLogger.Error("capture", "Capture engine failed", map[string]string{
"error": err.Error(),
})
}
}
logger.Info("service", "ja4sentinel stopped", nil)
// Graceful shutdown
appLogger.Info("main", "Shutting down...", nil)
cancel()
// Close components
if err := captureEngine.Close(); err != nil {
appLogger.Error("main", "Failed to close capture engine", map[string]string{
"error": err.Error(),
})
}
if err := parser.Close(); err != nil {
appLogger.Error("main", "Failed to close parser", map[string]string{
"error": err.Error(),
})
}
if closer, ok := outputWriter.(interface{ Close() error }); ok {
if err := closer.Close(); err != nil {
appLogger.Error("main", "Failed to close output writer", map[string]string{
"error": err.Error(),
})
}
}
appLogger.Info("main", "ja4sentinel stopped", nil)
}
// formatPorts formats a slice of ports as a comma-separated string
func formatPorts(ports []uint16) string {
if len(ports) == 0 {
return ""
}
result := fmt.Sprintf("%d", ports[0])
for _, port := range ports[1:] {
result += fmt.Sprintf(",%d", port)
}
return result
}