Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled
CRITICAL FIX: - Resolve crash in TLS parser with nil decode context - Use gopacket.NewPacket with LinkTypeIPv4/IPv6 - Fixes panic: runtime error: invalid memory address or nil pointer dereference - Properly handles raw IP packets after SLL header stripping Packaging: - Update RPM spec to version 1.1.8 - Update changelog with crash fix details Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
310 lines
8.6 KiB
Go
310 lines
8.6 KiB
Go
// Package main provides the entry point for ja4sentinel
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/coreos/go-systemd/v22/daemon"
|
|
"ja4sentinel/api"
|
|
"ja4sentinel/internal/capture"
|
|
"ja4sentinel/internal/config"
|
|
"ja4sentinel/internal/fingerprint"
|
|
"ja4sentinel/internal/logging"
|
|
"ja4sentinel/internal/output"
|
|
"ja4sentinel/internal/tlsparse"
|
|
)
|
|
|
|
var (
|
|
// Version information (set via ldflags)
|
|
Version = "1.1.8"
|
|
BuildTime = "unknown"
|
|
GitCommit = "unknown"
|
|
)
|
|
|
|
func main() {
|
|
// Parse command-line flags
|
|
configPath := flag.String("config", "", "Path to configuration file (YAML)")
|
|
version := flag.Bool("version", false, "Show version information")
|
|
flag.Parse()
|
|
|
|
if *version {
|
|
fmt.Printf("ja4sentinel version %s (built %s, commit %s)\n", Version, BuildTime, GitCommit)
|
|
os.Exit(0)
|
|
}
|
|
|
|
// Load configuration
|
|
cfgLoader := config.NewLoader(*configPath)
|
|
appConfig, err := cfgLoader.Load()
|
|
if err != nil {
|
|
// Create logger with default level for error reporting
|
|
loggerFactory := &logging.LoggerFactory{}
|
|
appLogger := loggerFactory.NewDefaultLogger()
|
|
appLogger.Error("main", "Failed to load configuration", map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Create logger factory with configured log level
|
|
loggerFactory := &logging.LoggerFactory{}
|
|
appLogger := loggerFactory.NewLogger(appConfig.Core.LogLevel)
|
|
|
|
appLogger.Info("main", "Starting ja4sentinel", map[string]string{
|
|
"version": Version,
|
|
"build_time": BuildTime,
|
|
"git_commit": GitCommit,
|
|
})
|
|
|
|
appLogger.Info("main", "Configuration loaded", map[string]string{
|
|
"interface": appConfig.Core.Interface,
|
|
"listen_ports": formatPorts(appConfig.Core.ListenPorts),
|
|
"log_level": appConfig.Core.LogLevel,
|
|
})
|
|
|
|
// Create context with cancellation for graceful shutdown
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Signal readiness to systemd
|
|
if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil {
|
|
appLogger.Warn("main", "Failed to send READY notification to systemd", map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
// Start watchdog goroutine if enabled
|
|
watchdogInterval, err := daemon.SdWatchdogEnabled(false)
|
|
if err != nil {
|
|
appLogger.Warn("main", "Failed to check watchdog status", map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
if watchdogInterval > 0 {
|
|
appLogger.Info("main", "systemd watchdog enabled", map[string]string{
|
|
"interval": watchdogInterval.String(),
|
|
})
|
|
go func() {
|
|
ticker := time.NewTicker(watchdogInterval / 2)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if _, err := daemon.SdNotify(false, daemon.SdNotifyWatchdog); err != nil {
|
|
appLogger.Warn("main", "Failed to send WATCHDOG notification", map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Setup signal handling for shutdown and log rotation
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
|
|
|
// Create pipeline components
|
|
captureEngine := capture.New()
|
|
parser := tlsparse.NewParserWithTimeout(time.Duration(appConfig.Core.FlowTimeoutSec) * time.Second)
|
|
fingerprintEngine := fingerprint.NewEngine()
|
|
|
|
// Create output builder with error callback for socket connection errors
|
|
outputBuilder := output.NewBuilder().WithErrorCallback(func(socketPath string, err error, attempt int) {
|
|
appLogger.Error("output", "UNIX socket connection failed", map[string]string{
|
|
"socket_path": socketPath,
|
|
"error": err.Error(),
|
|
"attempt": fmt.Sprintf("%d", attempt),
|
|
})
|
|
})
|
|
|
|
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)
|
|
}
|
|
|
|
// Create channel for raw packets (configurable buffer size)
|
|
bufferSize := appConfig.Core.PacketBufferSize
|
|
if bufferSize <= 0 {
|
|
bufferSize = 1000 // Default fallback
|
|
}
|
|
packetChan := make(chan api.RawPacket, bufferSize)
|
|
|
|
// 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)
|
|
close(packetChan) // Close channel to signal packet processor to shut down
|
|
captureErrChan <- err
|
|
}()
|
|
|
|
// Process packets
|
|
go func() {
|
|
for {
|
|
select {
|
|
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(),
|
|
"src_ip": "unknown",
|
|
"src_port": "unknown",
|
|
"dst_ip": "unknown",
|
|
"dst_port": "unknown",
|
|
})
|
|
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(),
|
|
"src_ip": clientHello.SrcIP,
|
|
"src_port": fmt.Sprintf("%d", clientHello.SrcPort),
|
|
"dst_ip": clientHello.DstIP,
|
|
"dst_port": fmt.Sprintf("%d", clientHello.DstPort),
|
|
"conn_id": clientHello.ConnID,
|
|
})
|
|
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(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Wait for shutdown signal or capture error
|
|
for {
|
|
select {
|
|
case sig := <-sigChan:
|
|
switch sig {
|
|
case syscall.SIGHUP:
|
|
// Handle log rotation - reopen output files
|
|
appLogger.Info("main", "Received SIGHUP, reopening log files", nil)
|
|
if mw, ok := outputWriter.(api.Reopenable); ok {
|
|
if err := mw.Reopen(); err != nil {
|
|
appLogger.Error("main", "Failed to reopen log files", map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
} else {
|
|
appLogger.Info("main", "Log files reopened successfully", nil)
|
|
}
|
|
} else {
|
|
appLogger.Warn("main", "Output writer does not support log rotation", nil)
|
|
}
|
|
case syscall.SIGINT, syscall.SIGTERM:
|
|
appLogger.Info("main", "Received shutdown signal", map[string]string{
|
|
"signal": sig.String(),
|
|
})
|
|
goto shutdown
|
|
}
|
|
case err := <-captureErrChan:
|
|
if err != nil {
|
|
appLogger.Error("capture", "Capture engine failed", map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
goto shutdown
|
|
}
|
|
}
|
|
|
|
shutdown:
|
|
// Graceful shutdown
|
|
appLogger.Info("main", "Shutting down...", nil)
|
|
|
|
// Signal stopping to systemd
|
|
if _, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil {
|
|
appLogger.Warn("main", "Failed to send STOPPING notification to systemd", map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
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 mw, ok := outputWriter.(interface{ CloseAll() error }); ok {
|
|
if err := mw.CloseAll(); err != nil {
|
|
appLogger.Error("main", "Failed to close output writers", map[string]string{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
} else 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
|
|
}
|