parseFirewallLog parses a firewall log file and returns analysis
(logPath string, verbose bool)
| 270 | |
| 271 | // parseFirewallLog parses a firewall log file and returns analysis |
| 272 | func parseFirewallLog(logPath string, verbose bool) (*FirewallAnalysis, error) { |
| 273 | firewallLogLog.Printf("Parsing firewall log: %s", logPath) |
| 274 | file, err := os.Open(logPath) |
| 275 | if err != nil { |
| 276 | firewallLogLog.Printf("Failed to open firewall log: %v", err) |
| 277 | return nil, fmt.Errorf("failed to open firewall log: %w", err) |
| 278 | } |
| 279 | defer file.Close() |
| 280 | |
| 281 | analysis := &FirewallAnalysis{ |
| 282 | RequestsByDomain: make(map[string]DomainRequestStats), |
| 283 | } |
| 284 | |
| 285 | allowedDomainsSet := make(map[string]struct{}) |
| 286 | blockedDomainsSet := make(map[string]struct{}) |
| 287 | |
| 288 | scanner := bufio.NewScanner(file) |
| 289 | for scanner.Scan() { |
| 290 | line := scanner.Text() |
| 291 | |
| 292 | entry := parseFirewallLogLine(line) |
| 293 | if entry == nil { |
| 294 | continue |
| 295 | } |
| 296 | |
| 297 | // Skip internal Squid error entries (client IP ::1, no domain, no destination) |
| 298 | // These are internal Squid connection errors (e.g., error:transaction-end-before-headers) |
| 299 | // and are not actual external network requests. |
| 300 | // Example: 1773003472.027 ::1:52010 - -:- 0.0 - 0 NONE_NONE:HIER_NONE error:transaction-end-before-headers "-" |
| 301 | if strings.HasPrefix(entry.ClientIPPort, "::1:") && entry.Domain == "-" && (entry.DestIPPort == "-:-" || entry.DestIPPort == "-") { |
| 302 | continue |
| 303 | } |
| 304 | |
| 305 | analysis.TotalRequests++ |
| 306 | |
| 307 | // Determine if request was allowed or blocked |
| 308 | isAllowed := isRequestAllowed(entry.Decision, entry.Status) |
| 309 | |
| 310 | // Extract domain - when domain is "-" (iptables-dropped traffic not visible to Squid), |
| 311 | // fall back to dest IP:port so blocked requests show their actual destination instead of "-" |
| 312 | // Only fall back if destIPPort is a valid host:port (not "-" or "-:-" which are placeholder values) |
| 313 | domain := entry.Domain |
| 314 | if domain == "-" && entry.DestIPPort != "-" && entry.DestIPPort != "-:-" { |
| 315 | domain = entry.DestIPPort |
| 316 | } else if domain == "-" { |
| 317 | // Both domain and destIPPort are placeholders: iptables dropped the traffic before |
| 318 | // Squid could identify the destination. Use a sentinel so the entry appears in |
| 319 | // RequestsByDomain for informational purposes, but do NOT add it to the domain sets |
| 320 | // since "-" is not an actionable domain name. |
| 321 | domain = unknownDomain |
| 322 | } |
| 323 | |
| 324 | if isAllowed { |
| 325 | analysis.AllowedRequests++ |
| 326 | if domain != unknownDomain { |
| 327 | allowedDomainsSet[domain] = struct{}{} |
| 328 | } |
| 329 | } else { |