v1.1.11: Fix exclude_source_ips config loading and debug logging
Major fixes: - Add exclude_source_ips to mergeConfigs() - config file values now properly loaded - Add validation for exclude_source_ips (IP/CIDR format validation) - Remove JA4SENTINEL_LOG_LEVEL env var from systemd service - Config file log_level now respected without env override Debug logging improvements: - Log IP filter entries at startup (debug mode) - Track filtered packet count with atomic counter - Display filter statistics at shutdown via GetFilterStats() - New debug logs in tlsparse component Testing: - Add 6 new unit tests for exclude_source_ips and log_level config loading - Test mergeConfigs() behavior with empty/override values - Test validation of invalid IPs and CIDR ranges Documentation: - Update architecture.yml with ipfilter module - Document config loading priority and notes - Update api.Config fields (LocalIPs, ExcludeSourceIPs, LogLevel) Files changed: - internal/config/loader.go (merge, validation, helpers) - internal/config/loader_test.go (6 new tests) - internal/tlsparse/parser.go (GetFilterStats, counter) - cmd/ja4sentinel/main.go (debug logging) - packaging/systemd/ja4sentinel.service (remove env var) - architecture.yml (ipfilter module, config_loading section) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@ -741,3 +741,262 @@ func TestMergeConfigs_OutputMerge(t *testing.T) {
|
||||
t.Errorf("Merged Outputs[0].Type = %q, want 'file'", result.Outputs[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMergeConfigs_ExcludeSourceIPs tests that exclude_source_ips is properly merged
|
||||
func TestMergeConfigs_ExcludeSourceIPs(t *testing.T) {
|
||||
base := api.AppConfig{
|
||||
Core: api.Config{
|
||||
Interface: "eth0",
|
||||
ListenPorts: []uint16{443},
|
||||
ExcludeSourceIPs: []string{}, // Empty base
|
||||
},
|
||||
}
|
||||
override := api.AppConfig{
|
||||
Core: api.Config{
|
||||
ExcludeSourceIPs: []string{"10.0.0.0/8", "192.168.1.1"},
|
||||
},
|
||||
}
|
||||
|
||||
result := mergeConfigs(base, override)
|
||||
|
||||
if len(result.Core.ExcludeSourceIPs) != 2 {
|
||||
t.Errorf("Merged ExcludeSourceIPs length = %d, want 2", len(result.Core.ExcludeSourceIPs))
|
||||
}
|
||||
if result.Core.ExcludeSourceIPs[0] != "10.0.0.0/8" {
|
||||
t.Errorf("Merged ExcludeSourceIPs[0] = %q, want '10.0.0.0/8'", result.Core.ExcludeSourceIPs[0])
|
||||
}
|
||||
if result.Core.ExcludeSourceIPs[1] != "192.168.1.1" {
|
||||
t.Errorf("Merged ExcludeSourceIPs[1] = %q, want '192.168.1.1'", result.Core.ExcludeSourceIPs[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestMergeConfigs_ExcludeSourceIPs_EmptyOverride tests that empty override doesn't replace
|
||||
func TestMergeConfigs_ExcludeSourceIPs_EmptyOverride(t *testing.T) {
|
||||
base := api.AppConfig{
|
||||
Core: api.Config{
|
||||
Interface: "eth0",
|
||||
ListenPorts: []uint16{443},
|
||||
ExcludeSourceIPs: []string{"10.0.0.0/8"},
|
||||
},
|
||||
}
|
||||
override := api.AppConfig{
|
||||
Core: api.Config{
|
||||
ExcludeSourceIPs: []string{}, // Empty override
|
||||
},
|
||||
}
|
||||
|
||||
result := mergeConfigs(base, override)
|
||||
|
||||
// Empty override should NOT replace base
|
||||
if len(result.Core.ExcludeSourceIPs) != 1 {
|
||||
t.Errorf("Merged ExcludeSourceIPs length = %d, want 1", len(result.Core.ExcludeSourceIPs))
|
||||
}
|
||||
if result.Core.ExcludeSourceIPs[0] != "10.0.0.0/8" {
|
||||
t.Errorf("Merged ExcludeSourceIPs[0] = %q, want '10.0.0.0/8'", result.Core.ExcludeSourceIPs[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadFromFile_ExcludeSourceIPs tests loading exclude_source_ips from YAML file
|
||||
func TestLoadFromFile_ExcludeSourceIPs(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
yamlContent := `
|
||||
core:
|
||||
interface: eth0
|
||||
listen_ports:
|
||||
- 443
|
||||
exclude_source_ips:
|
||||
- "10.0.0.0/8"
|
||||
- "192.168.1.100"
|
||||
log_level: debug
|
||||
outputs:
|
||||
- type: stdout
|
||||
enabled: true
|
||||
`
|
||||
if err := os.WriteFile(configPath, []byte(yamlContent), 0600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
loader := NewLoader(configPath)
|
||||
cfg, err := loader.Load()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Core.ExcludeSourceIPs) != 2 {
|
||||
t.Errorf("ExcludeSourceIPs length = %d, want 2", len(cfg.Core.ExcludeSourceIPs))
|
||||
}
|
||||
if cfg.Core.ExcludeSourceIPs[0] != "10.0.0.0/8" {
|
||||
t.Errorf("ExcludeSourceIPs[0] = %q, want '10.0.0.0/8'", cfg.Core.ExcludeSourceIPs[0])
|
||||
}
|
||||
if cfg.Core.ExcludeSourceIPs[1] != "192.168.1.100" {
|
||||
t.Errorf("ExcludeSourceIPs[1] = %q, want '192.168.1.100'", cfg.Core.ExcludeSourceIPs[1])
|
||||
}
|
||||
if cfg.Core.LogLevel != "debug" {
|
||||
t.Errorf("LogLevel = %q, want 'debug'", cfg.Core.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadFromFile_LogLevel tests loading log_level from YAML file
|
||||
func TestLoadFromFile_LogLevel(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
logLevel string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"debug level", "debug", "debug", false},
|
||||
{"info level", "info", "info", false},
|
||||
{"warn level", "warn", "warn", false},
|
||||
{"error level", "error", "error", false},
|
||||
{"invalid level", "invalid", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
yamlContent := `
|
||||
core:
|
||||
interface: eth0
|
||||
listen_ports:
|
||||
- 443
|
||||
log_level: ` + tt.logLevel + `
|
||||
outputs:
|
||||
- type: stdout
|
||||
enabled: true
|
||||
`
|
||||
if err := os.WriteFile(configPath, []byte(yamlContent), 0600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
loader := NewLoader(configPath)
|
||||
cfg, err := loader.Load()
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Load() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if !tt.wantErr && cfg.Core.LogLevel != tt.want {
|
||||
t.Errorf("LogLevel = %q, want %q", cfg.Core.LogLevel, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidate_ExcludeSourceIPs tests validation of exclude_source_ips entries
|
||||
func TestValidate_ExcludeSourceIPs(t *testing.T) {
|
||||
loader := &LoaderImpl{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ips []string
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
ips: []string{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid single IP",
|
||||
ips: []string{"192.168.1.1"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid CIDR",
|
||||
ips: []string{"10.0.0.0/8"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple valid entries",
|
||||
ips: []string{"10.0.0.0/8", "192.168.1.1", "172.16.0.0/12"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty entry",
|
||||
ips: []string{""},
|
||||
wantErr: true,
|
||||
errContains: "entry cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "invalid IP",
|
||||
ips: []string{"999.999.999.999"},
|
||||
wantErr: true,
|
||||
errContains: "invalid IP address",
|
||||
},
|
||||
{
|
||||
name: "invalid CIDR format",
|
||||
ips: []string{"10.0.0.0/33"},
|
||||
wantErr: true,
|
||||
errContains: "invalid CIDR",
|
||||
},
|
||||
{
|
||||
name: "invalid CIDR syntax",
|
||||
ips: []string{"10.0.0.0/"},
|
||||
wantErr: true,
|
||||
errContains: "invalid CIDR",
|
||||
},
|
||||
{
|
||||
name: "mixed valid and invalid",
|
||||
ips: []string{"10.0.0.0/8", "invalid"},
|
||||
wantErr: true,
|
||||
errContains: "invalid",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := api.AppConfig{
|
||||
Core: api.Config{
|
||||
Interface: "eth0",
|
||||
ListenPorts: []uint16{443},
|
||||
ExcludeSourceIPs: tt.ips,
|
||||
},
|
||||
Outputs: []api.OutputConfig{
|
||||
{Type: "stdout", Enabled: true},
|
||||
},
|
||||
}
|
||||
err := loader.validate(cfg)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if tt.wantErr && tt.errContains != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("validate() error = %v, should contain %q", err, tt.errContains)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadFromEnv_ExcludeSourceIPs tests that exclude_source_ips can be set via env (future feature)
|
||||
// Currently exclude_source_ips is NOT loaded from env, only from config file
|
||||
func TestLoadFromEnv_ExcludeSourceIPs_NotSupported(t *testing.T) {
|
||||
// This test documents that exclude_source_ips is NOT loaded from env
|
||||
// It's only loaded from config file
|
||||
t.Setenv("JA4SENTINEL_LOG_LEVEL", "debug")
|
||||
|
||||
loader := NewLoader("")
|
||||
cfg, err := loader.Load()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
// exclude_source_ips should be empty (not loaded from env)
|
||||
if len(cfg.Core.ExcludeSourceIPs) != 0 {
|
||||
t.Errorf("ExcludeSourceIPs should be empty from env, got %v", cfg.Core.ExcludeSourceIPs)
|
||||
}
|
||||
|
||||
// But log_level should be loaded from env
|
||||
if cfg.Core.LogLevel != "debug" {
|
||||
t.Errorf("LogLevel = %q, want 'debug'", cfg.Core.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user