Files
ProxmoxDash/network_flows_test.go

190 lines
7.4 KiB
Go

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)
}
}