package main import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" ) const networkNATLine = "ipv4 2 tcp 6 431999 ESTABLISHED src=8.8.8.8 dst=192.168.0.12 sport=51000 dport=443 packets=12 bytes=1500 src=172.18.0.2 dst=8.8.8.8 sport=443 dport=51000 packets=16 bytes=2500 [ASSURED] mark=0 zone=2 use=1" func TestNetworkConntrackTuples(t *testing.T) { f, ok := parseNetworkFlow(networkNATLine) if !ok || f.Source != "8.8.8.8" || f.Destination != "192.168.0.12" || f.ReplySource != "172.18.0.2" || f.SourcePort != 51000 || f.ReplySourcePort != 443 || f.State != "ESTABLISHED" || !f.Accounting || f.Bytes != 1500 || f.ReplyBytes != 2500 || f.Zone != "2" { t.Fatalf("NAT tuple corrupted: %+v", f) } udp, ok := parseNetworkFlow("ipv6 10 udp 17 29 src=2001:4860:4860::8888 dst=fd00::2 sport=51000 dport=53 [UNREPLIED] src=fd00::2 dst=2001:4860:4860::8888 sport=53 dport=51000 mark=0 use=1") if !ok || udp.State != "UNREPLIED" || udp.Accounting || udp.Protocol != "udp" { t.Fatalf("UDP: %+v", udp) } icmp, ok := parseNetworkFlow("icmp 1 29 src=8.8.8.8 dst=192.168.0.12 type=8 code=0 id=3 src=192.168.0.12 dst=8.8.8.8 type=0 code=0 id=3 use=1") if !ok || icmp.SourcePort != 0 || icmp.State != "TRACKED" { t.Fatalf("ICMP: %+v", icmp) } if _, ok := parseNetworkFlow("tcp 6 20 SYN_SENT src=not-an-ip dst=192.168.0.12"); ok { t.Fatal("invalid accepted") } } func TestNetworkSnapshotLimit(t *testing.T) { var s NetworkSnapshot appendNetworkFlows(&s, []byte(strings.Repeat(networkNATLine+"\n", 5001))) if len(s.Flows) != 5000 || !s.Limited { t.Fatalf("limit not enforced %d", len(s.Flows)) } b := networkBuffer{limit: 2} if _, err := b.Write([]byte("123")); err == nil || b.Len() != 0 { t.Fatal("output unbounded") } } func TestFirewallOnlyConfirmedDropCounters(t *testing.T) { raw := `{"nftables":[{"rule":{"family":"ip","table":"crowdsec","chain":"crowdsec-chain","handle":3,"expr":[{"match":{}},{"counter":{"packets":10,"bytes":1000}},{"drop":null}]}},{"rule":{"table":"crowdsec","expr":[{"counter":{"packets":99}},{"accept":null}]}},{"rule":{"table":"other","expr":[{"counter":{"packets":99}},{"drop":null}]}},{"rule":{"table":"crowdsec","expr":[{"counter":{"packets":99}},{"match":{}},{"drop":null}]}},{"rule":{"table":"crowdsec","expr":[{"counter":"named-counter"},{"drop":null}]}}]}` counters, err := parseFirewallCounters([]byte(raw)) if err != nil || len(counters) != 1 || counters[0].Packets != 10 || counters[0].Bytes != 1000 { t.Fatalf("invented blocks: %+v %v", counters, err) } if _, err := parseFirewallCounters([]byte("invalid")); err == nil { t.Fatal("invalid nft JSON accepted") } } func TestNetworkRouteAuthorizationAndSourceSelection(t *testing.T) { s, a := trafficFixture(t) s.SaveTrafficSettings(TrafficSettings{Enabled: true, AgentID: a.ID}) mux := http.NewServeMux() trafficRoutes(mux, s) flow, _ := parseNetworkFlow(networkNATLine) snapshot := NetworkSnapshot{At: time.Now().Unix(), Flows: []NetworkFlow{flow}} raw, _ := json.Marshal(snapshot) post := func(token string) int { r := httptest.NewRequest("POST", "/api/agent/network", bytes.NewReader(raw)) r.Header.Set("Authorization", "Bearer "+token) w := httptest.NewRecorder() mux.ServeHTTP(w, r) return w.Code } if got := post(a.ID + "." + a.Secret); got != 403 { t.Fatalf("disabled accepts data: %d", got) } s.SaveTrafficSettings(TrafficSettings{Enabled: true, NetworkEnabled: true, AgentID: a.ID}) if got := post("wrong.token"); got != 401 { t.Fatalf("unauthenticated: %d", got) } if got := post(a.ID + "." + a.Secret); got != 200 { t.Fatalf("valid: %d", got) } w := httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest("GET", "/api/network", nil)) var got NetworkSnapshot json.Unmarshal(w.Body.Bytes(), &got) if w.Code != 200 || len(got.Flows) != 1 || got.Flows[0].Country == "" || got.ReceivedAt == 0 { t.Fatalf("bad snapshot: %s", w.Body.String()) } token, _ := s.CreateAgentEnrollmentToken() other, _ := s.EnrollAgent(agentEnrollmentRequest{Token: token, Name: "other"}) s.SaveTrafficSettings(TrafficSettings{Enabled: true, NetworkEnabled: true, AgentID: other.ID}) if code := post(a.ID + "." + a.Secret); code != 403 { t.Fatalf("old source accepted: %d", code) } w = httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest("GET", "/api/network", nil)) json.Unmarshal(w.Body.Bytes(), &got) if len(got.Flows) != 0 { t.Fatal("old source data leaked") } s.SaveTrafficSettings(TrafficSettings{Enabled: false, NetworkEnabled: true, AgentID: a.ID}) for _, path := range []string{"/network", "/api/network"} { w = httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil)) if w.Code != 404 { t.Fatalf("disabled page: %s", path) } } } func TestNetworkRejectBadSnapshots(t *testing.T) { s, a := trafficFixture(t) if err := s.SaveNetwork(a.ID, NetworkSnapshot{At: time.Now().Unix() - 300}); err == nil { t.Fatal("stale timestamp") } f, _ := parseNetworkFlow(networkNATLine) f.SourcePort = 99999 if err := s.SaveNetwork(a.ID, NetworkSnapshot{At: time.Now().Unix(), Flows: []NetworkFlow{f}}); err == nil { t.Fatal("invalid port") } } func TestNetworkCommandCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := networkCommand(ctx, "conntrack", "-L"); err == nil { t.Fatal("cancelled command ran") } } func TestNetworkExternalAddresses(t *testing.T) { for _, ip := range []string{"192.168.1.1", "127.0.0.1", "100.64.0.1", "224.0.0.1", "ff02::1", "fe80::1", "fd00::1", "::"} { if networkExternalIP(ip) { t.Fatalf("not external: %s", ip) } } for _, ip := range []string{"8.8.8.8", "2001:4860:4860::8888"} { if !networkExternalIP(ip) { t.Fatalf("external: %s", ip) } } } func TestConntrackInstallerOnlyInstallsRequestedPackage(t *testing.T) { var calls [][]string err := installConntrackPackage(func(name string, args ...string) error { calls = append(calls, append([]string{name}, args...)) return nil }, func() bool { return true }) if err != nil || len(calls) != 2 { t.Fatalf("installer: %v %v", calls, err) } if calls[0][len(calls[0])-1] != "update" || strings.Join(calls[1][len(calls[1])-5:], " ") != "-y --no-install-recommends --no-remove install conntrack" { t.Fatalf("unexpected apt operation: %v", calls) } calls = nil err = installConntrackPackage(func(name string, args ...string) error { calls = append(calls, args); return fmt.Errorf("apt locked") }, func() bool { return true }) if err == nil || len(calls) != 1 { t.Fatal("continued after failed apt update") } } func TestConntrackDependencySingleInstallAndBackoff(t *testing.T) { var d networkDependencyState now := time.Now() started := make(chan struct{}) finish := make(chan struct{}) installer := func() error { close(started); <-finish; return fmt.Errorf("offline") } if got := d.ensure(now, true, installer); got != "" { t.Fatal("installed tool should be skipped") } d.ensure(now, false, installer) <-started // A second call must not start a second installer (which would panic closing started). d.ensure(now, false, installer) close(finish) deadline := time.Now().Add(time.Second) for { d.Lock() running := d.running d.Unlock() if !running { break } if time.Now().After(deadline) { t.Fatal("installer stuck") } time.Sleep(time.Millisecond) } got := d.ensure(now.Add(time.Minute), false, installer) if !strings.Contains(got, "offline") || !strings.Contains(got, "Повторная") { t.Fatalf("backoff missing: %s", got) } }