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:
189
cmd/ja4sentinel/main.go
Normal file
189
cmd/ja4sentinel/main.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user