From e670068cabfa4288455d34d3b917eaa5a5ae2f7a Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:37:09 +0100 Subject: [PATCH 1/2] [management] Run test sequential (#3101) --- .github/workflows/golang-test-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 50c6cba848c..15519039844 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -183,7 +183,7 @@ jobs: run: git --no-pager diff --exit-code - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) benchmark: needs: [ build-cache ] From 05930ee6b192fa9928e709a8fc74ef6e2cb779a1 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:57:15 +0100 Subject: [PATCH 2/2] [client] Add firewall rules to the debug bundle (#3089) Adds the following to the debug bundle: - iptables: `iptables-save`, `iptables -v -n -L` - nftables: `nft list ruleset` or if not available formatted output from netlink (WIP) --- client/server/debug.go | 24 ++ client/server/debug_linux.go | 693 ++++++++++++++++++++++++++++++++ client/server/debug_nonlinux.go | 15 + client/server/debug_test.go | 113 ++++++ 4 files changed, 845 insertions(+) create mode 100644 client/server/debug_linux.go create mode 100644 client/server/debug_nonlinux.go diff --git a/client/server/debug.go b/client/server/debug.go index c12fd99dbf2..9dfde0367f3 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -40,6 +40,8 @@ netbird.err: Most recent, anonymized stderr log file of the NetBird client. netbird.out: Most recent, anonymized stdout log file of the NetBird client. routes.txt: Anonymized system routes, if --system-info flag was provided. interfaces.txt: Anonymized network interface information, if --system-info flag was provided. +iptables.txt: Anonymized iptables rules with packet counters, if --system-info flag was provided. +nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided. config.txt: Anonymized configuration information of the NetBird client. network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules. state.json: Anonymized client state dump containing netbird states. @@ -106,6 +108,24 @@ The config.txt file contains anonymized configuration information of the NetBird - CustomDNSAddress Other non-sensitive configuration options are included without anonymization. + +Firewall Rules (Linux only) +The bundle includes two separate firewall rule files: + +iptables.txt: +- Complete iptables ruleset with packet counters using 'iptables -v -n -L' +- Includes all tables (filter, nat, mangle, raw, security) +- Shows packet and byte counters for each rule +- All IP addresses are anonymized +- Chain names, table names, and other non-sensitive information remain unchanged + +nftables.txt: +- Complete nftables ruleset obtained via 'nft -a list ruleset' +- Includes rule handle numbers and packet counters +- All tables, chains, and rules are included +- Shows packet and byte counters for each rule +- All IP addresses are anonymized +- Chain names, table names, and other non-sensitive information remain unchanged ` const ( @@ -172,6 +192,10 @@ func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleReques if err := s.addInterfaces(req, anonymizer, archive); err != nil { log.Errorf("Failed to add interfaces to debug bundle: %v", err) } + + if err := s.addFirewallRules(req, anonymizer, archive); err != nil { + log.Errorf("Failed to add firewall rules to debug bundle: %v", err) + } } if err := s.addNetworkMap(req, anonymizer, archive); err != nil { diff --git a/client/server/debug_linux.go b/client/server/debug_linux.go new file mode 100644 index 00000000000..60bc4056167 --- /dev/null +++ b/client/server/debug_linux.go @@ -0,0 +1,693 @@ +//go:build linux && !android + +package server + +import ( + "archive/zip" + "bytes" + "encoding/binary" + "fmt" + "os/exec" + "sort" + "strings" + + "github.com/google/nftables" + "github.com/google/nftables/expr" + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/proto" +) + +// addFirewallRules collects and adds firewall rules to the archive +func (s *Server) addFirewallRules(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + log.Info("Collecting firewall rules") + // Collect and add iptables rules + iptablesRules, err := collectIPTablesRules() + if err != nil { + log.Warnf("Failed to collect iptables rules: %v", err) + } else { + if req.GetAnonymize() { + iptablesRules = anonymizer.AnonymizeString(iptablesRules) + } + if err := addFileToZip(archive, strings.NewReader(iptablesRules), "iptables.txt"); err != nil { + log.Warnf("Failed to add iptables rules to bundle: %v", err) + } + } + + // Collect and add nftables rules + nftablesRules, err := collectNFTablesRules() + if err != nil { + log.Warnf("Failed to collect nftables rules: %v", err) + } else { + if req.GetAnonymize() { + nftablesRules = anonymizer.AnonymizeString(nftablesRules) + } + if err := addFileToZip(archive, strings.NewReader(nftablesRules), "nftables.txt"); err != nil { + log.Warnf("Failed to add nftables rules to bundle: %v", err) + } + } + + return nil +} + +// collectIPTablesRules collects rules using both iptables-save and verbose listing +func collectIPTablesRules() (string, error) { + var builder strings.Builder + + // First try using iptables-save + saveOutput, err := collectIPTablesSave() + if err != nil { + log.Warnf("Failed to collect iptables rules using iptables-save: %v", err) + } else { + builder.WriteString("=== iptables-save output ===\n") + builder.WriteString(saveOutput) + builder.WriteString("\n") + } + + // Then get verbose statistics for each table + builder.WriteString("=== iptables -v -n -L output ===\n") + + // Get list of tables + tables := []string{"filter", "nat", "mangle", "raw", "security"} + + for _, table := range tables { + builder.WriteString(fmt.Sprintf("*%s\n", table)) + + // Get verbose statistics for the entire table + stats, err := getTableStatistics(table) + if err != nil { + log.Warnf("Failed to get statistics for table %s: %v", table, err) + continue + } + builder.WriteString(stats) + builder.WriteString("\n") + } + + return builder.String(), nil +} + +// collectIPTablesSave uses iptables-save to get rule definitions +func collectIPTablesSave() (string, error) { + cmd := exec.Command("iptables-save") + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("execute iptables-save: %w (stderr: %s)", err, stderr.String()) + } + + rules := stdout.String() + if strings.TrimSpace(rules) == "" { + return "", fmt.Errorf("no iptables rules found") + } + + return rules, nil +} + +// getTableStatistics gets verbose statistics for an entire table using iptables command +func getTableStatistics(table string) (string, error) { + cmd := exec.Command("iptables", "-v", "-n", "-L", "-t", table) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("execute iptables -v -n -L: %w (stderr: %s)", err, stderr.String()) + } + + return stdout.String(), nil +} + +// collectNFTablesRules attempts to collect nftables rules using either nft command or netlink +func collectNFTablesRules() (string, error) { + // First try using nft command + rules, err := collectNFTablesFromCommand() + if err != nil { + log.Debugf("Failed to collect nftables rules using nft command: %v, falling back to netlink", err) + // Fall back to netlink + rules, err = collectNFTablesFromNetlink() + if err != nil { + return "", fmt.Errorf("collect nftables rules using both nft and netlink failed: %w", err) + } + } + return rules, nil +} + +// collectNFTablesFromCommand attempts to collect rules using nft command +func collectNFTablesFromCommand() (string, error) { + cmd := exec.Command("nft", "-a", "list", "ruleset") + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("execute nft list ruleset: %w (stderr: %s)", err, stderr.String()) + } + + rules := stdout.String() + if strings.TrimSpace(rules) == "" { + return "", fmt.Errorf("no nftables rules found") + } + + return rules, nil +} + +// collectNFTablesFromNetlink collects rules using netlink library +func collectNFTablesFromNetlink() (string, error) { + conn, err := nftables.New() + if err != nil { + return "", fmt.Errorf("create nftables connection: %w", err) + } + + tables, err := conn.ListTables() + if err != nil { + return "", fmt.Errorf("list tables: %w", err) + } + + sortTables(tables) + return formatTables(conn, tables), nil +} + +func formatTables(conn *nftables.Conn, tables []*nftables.Table) string { + var builder strings.Builder + + for _, table := range tables { + builder.WriteString(fmt.Sprintf("table %s %s {\n", formatFamily(table.Family), table.Name)) + + chains, err := getAndSortTableChains(conn, table) + if err != nil { + log.Warnf("Failed to list chains for table %s: %v", table.Name, err) + continue + } + + // Format chains + for _, chain := range chains { + formatChain(conn, table, chain, &builder) + } + + // Format sets + if sets, err := conn.GetSets(table); err != nil { + log.Warnf("Failed to get sets for table %s: %v", table.Name, err) + } else if len(sets) > 0 { + builder.WriteString("\n") + for _, set := range sets { + builder.WriteString(formatSet(conn, set)) + } + } + + builder.WriteString("}\n") + } + + return builder.String() +} + +func getAndSortTableChains(conn *nftables.Conn, table *nftables.Table) ([]*nftables.Chain, error) { + chains, err := conn.ListChains() + if err != nil { + return nil, err + } + + var tableChains []*nftables.Chain + for _, chain := range chains { + if chain.Table.Name == table.Name && chain.Table.Family == table.Family { + tableChains = append(tableChains, chain) + } + } + + sort.Slice(tableChains, func(i, j int) bool { + return tableChains[i].Name < tableChains[j].Name + }) + + return tableChains, nil +} + +func formatChain(conn *nftables.Conn, table *nftables.Table, chain *nftables.Chain, builder *strings.Builder) { + builder.WriteString(fmt.Sprintf("\tchain %s {\n", chain.Name)) + + if chain.Type != "" { + var policy string + if chain.Policy != nil { + policy = fmt.Sprintf("; policy %s", formatPolicy(*chain.Policy)) + } + builder.WriteString(fmt.Sprintf("\t\ttype %s hook %s priority %d%s\n", + formatChainType(chain.Type), + formatChainHook(chain.Hooknum), + chain.Priority, + policy)) + } + + rules, err := conn.GetRules(table, chain) + if err != nil { + log.Warnf("Failed to get rules for chain %s: %v", chain.Name, err) + } else { + sort.Slice(rules, func(i, j int) bool { + return rules[i].Position < rules[j].Position + }) + for _, rule := range rules { + builder.WriteString(formatRule(rule)) + } + } + + builder.WriteString("\t}\n") +} + +func sortTables(tables []*nftables.Table) { + sort.Slice(tables, func(i, j int) bool { + if tables[i].Family != tables[j].Family { + return tables[i].Family < tables[j].Family + } + return tables[i].Name < tables[j].Name + }) +} + +func formatFamily(family nftables.TableFamily) string { + switch family { + case nftables.TableFamilyIPv4: + return "ip" + case nftables.TableFamilyIPv6: + return "ip6" + case nftables.TableFamilyINet: + return "inet" + case nftables.TableFamilyARP: + return "arp" + case nftables.TableFamilyBridge: + return "bridge" + case nftables.TableFamilyNetdev: + return "netdev" + default: + return fmt.Sprintf("family-%d", family) + } +} + +func formatChainType(typ nftables.ChainType) string { + switch typ { + case nftables.ChainTypeFilter: + return "filter" + case nftables.ChainTypeNAT: + return "nat" + case nftables.ChainTypeRoute: + return "route" + default: + return fmt.Sprintf("type-%s", typ) + } +} + +func formatChainHook(hook *nftables.ChainHook) string { + if hook == nil { + return "none" + } + switch *hook { + case *nftables.ChainHookPrerouting: + return "prerouting" + case *nftables.ChainHookInput: + return "input" + case *nftables.ChainHookForward: + return "forward" + case *nftables.ChainHookOutput: + return "output" + case *nftables.ChainHookPostrouting: + return "postrouting" + default: + return fmt.Sprintf("hook-%d", *hook) + } +} + +func formatPolicy(policy nftables.ChainPolicy) string { + switch policy { + case nftables.ChainPolicyDrop: + return "drop" + case nftables.ChainPolicyAccept: + return "accept" + default: + return fmt.Sprintf("policy-%d", policy) + } +} + +func formatRule(rule *nftables.Rule) string { + var builder strings.Builder + builder.WriteString("\t\t") + + for i := 0; i < len(rule.Exprs); i++ { + if i > 0 { + builder.WriteString(" ") + } + i = formatExprSequence(&builder, rule.Exprs, i) + } + + builder.WriteString("\n") + return builder.String() +} + +func formatExprSequence(builder *strings.Builder, exprs []expr.Any, i int) int { + curr := exprs[i] + + // Handle Meta + Cmp sequence + if meta, ok := curr.(*expr.Meta); ok && i+1 < len(exprs) { + if cmp, ok := exprs[i+1].(*expr.Cmp); ok { + if formatted := formatMetaWithCmp(meta, cmp); formatted != "" { + builder.WriteString(formatted) + return i + 1 + } + } + } + + // Handle Payload + Cmp sequence + if payload, ok := curr.(*expr.Payload); ok && i+1 < len(exprs) { + if cmp, ok := exprs[i+1].(*expr.Cmp); ok { + builder.WriteString(formatPayloadWithCmp(payload, cmp)) + return i + 1 + } + } + + builder.WriteString(formatExpr(curr)) + return i +} + +func formatMetaWithCmp(meta *expr.Meta, cmp *expr.Cmp) string { + switch meta.Key { + case expr.MetaKeyIIFNAME: + name := strings.TrimRight(string(cmp.Data), "\x00") + return fmt.Sprintf("iifname %s %q", formatCmpOp(cmp.Op), name) + case expr.MetaKeyOIFNAME: + name := strings.TrimRight(string(cmp.Data), "\x00") + return fmt.Sprintf("oifname %s %q", formatCmpOp(cmp.Op), name) + case expr.MetaKeyMARK: + if len(cmp.Data) == 4 { + val := binary.BigEndian.Uint32(cmp.Data) + return fmt.Sprintf("meta mark %s 0x%x", formatCmpOp(cmp.Op), val) + } + } + return "" +} + +func formatPayloadWithCmp(p *expr.Payload, cmp *expr.Cmp) string { + if p.Base == expr.PayloadBaseNetworkHeader { + switch p.Offset { + case 12: // Source IP + if p.Len == 4 { + return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) + } else if p.Len == 2 { + return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) + } + case 16: // Destination IP + if p.Len == 4 { + return fmt.Sprintf("ip daddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) + } else if p.Len == 2 { + return fmt.Sprintf("ip daddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data)) + } + } + } + return fmt.Sprintf("%d reg%d [%d:%d] %s %v", + p.Base, p.DestRegister, p.Offset, p.Len, + formatCmpOp(cmp.Op), cmp.Data) +} + +func formatIPBytes(data []byte) string { + if len(data) == 4 { + return fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3]) + } else if len(data) == 2 { + return fmt.Sprintf("%d.%d.0.0/16", data[0], data[1]) + } + return fmt.Sprintf("%v", data) +} + +func formatCmpOp(op expr.CmpOp) string { + switch op { + case expr.CmpOpEq: + return "==" + case expr.CmpOpNeq: + return "!=" + case expr.CmpOpLt: + return "<" + case expr.CmpOpLte: + return "<=" + case expr.CmpOpGt: + return ">" + case expr.CmpOpGte: + return ">=" + default: + return fmt.Sprintf("op-%d", op) + } +} + +// formatExpr formats an expression in nft-like syntax +func formatExpr(exp expr.Any) string { + switch e := exp.(type) { + case *expr.Meta: + return formatMeta(e) + case *expr.Cmp: + return formatCmp(e) + case *expr.Payload: + return formatPayload(e) + case *expr.Verdict: + return formatVerdict(e) + case *expr.Counter: + return fmt.Sprintf("counter packets %d bytes %d", e.Packets, e.Bytes) + case *expr.Masq: + return "masquerade" + case *expr.NAT: + return formatNat(e) + case *expr.Match: + return formatMatch(e) + case *expr.Queue: + return fmt.Sprintf("queue num %d", e.Num) + case *expr.Lookup: + return fmt.Sprintf("@%s", e.SetName) + case *expr.Bitwise: + return formatBitwise(e) + case *expr.Fib: + return formatFib(e) + case *expr.Target: + return fmt.Sprintf("jump %s", e.Name) // Properly format jump targets + case *expr.Immediate: + if e.Register == 1 { + return formatImmediateData(e.Data) + } + return fmt.Sprintf("immediate %v", e.Data) + default: + return fmt.Sprintf("<%T>", exp) + } +} + +func formatImmediateData(data []byte) string { + // For IP addresses (4 bytes) + if len(data) == 4 { + return fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3]) + } + return fmt.Sprintf("%v", data) +} + +func formatMeta(e *expr.Meta) string { + // Handle source register case first (meta mark set) + if e.SourceRegister { + return fmt.Sprintf("meta %s set reg %d", formatMetaKey(e.Key), e.Register) + } + + // For interface names, handle register load operation + switch e.Key { + case expr.MetaKeyIIFNAME, + expr.MetaKeyOIFNAME, + expr.MetaKeyBRIIIFNAME, + expr.MetaKeyBRIOIFNAME: + // Simply the key name with no register reference + return formatMetaKey(e.Key) + + case expr.MetaKeyMARK: + // For mark operations, we want just "mark" + return "mark" + } + + // For other meta keys, show as loading into register + return fmt.Sprintf("meta %s => reg %d", formatMetaKey(e.Key), e.Register) +} + +func formatMetaKey(key expr.MetaKey) string { + switch key { + case expr.MetaKeyLEN: + return "length" + case expr.MetaKeyPROTOCOL: + return "protocol" + case expr.MetaKeyPRIORITY: + return "priority" + case expr.MetaKeyMARK: + return "mark" + case expr.MetaKeyIIF: + return "iif" + case expr.MetaKeyOIF: + return "oif" + case expr.MetaKeyIIFNAME: + return "iifname" + case expr.MetaKeyOIFNAME: + return "oifname" + case expr.MetaKeyIIFTYPE: + return "iiftype" + case expr.MetaKeyOIFTYPE: + return "oiftype" + case expr.MetaKeySKUID: + return "skuid" + case expr.MetaKeySKGID: + return "skgid" + case expr.MetaKeyNFTRACE: + return "nftrace" + case expr.MetaKeyRTCLASSID: + return "rtclassid" + case expr.MetaKeySECMARK: + return "secmark" + case expr.MetaKeyNFPROTO: + return "nfproto" + case expr.MetaKeyL4PROTO: + return "l4proto" + case expr.MetaKeyBRIIIFNAME: + return "briifname" + case expr.MetaKeyBRIOIFNAME: + return "broifname" + case expr.MetaKeyPKTTYPE: + return "pkttype" + case expr.MetaKeyCPU: + return "cpu" + case expr.MetaKeyIIFGROUP: + return "iifgroup" + case expr.MetaKeyOIFGROUP: + return "oifgroup" + case expr.MetaKeyCGROUP: + return "cgroup" + case expr.MetaKeyPRANDOM: + return "prandom" + default: + return fmt.Sprintf("meta-%d", key) + } +} + +func formatCmp(e *expr.Cmp) string { + ops := map[expr.CmpOp]string{ + expr.CmpOpEq: "==", + expr.CmpOpNeq: "!=", + expr.CmpOpLt: "<", + expr.CmpOpLte: "<=", + expr.CmpOpGt: ">", + expr.CmpOpGte: ">=", + } + return fmt.Sprintf("%s %v", ops[e.Op], e.Data) +} + +func formatPayload(e *expr.Payload) string { + var proto string + switch e.Base { + case expr.PayloadBaseNetworkHeader: + proto = "ip" + case expr.PayloadBaseTransportHeader: + proto = "tcp" + default: + proto = fmt.Sprintf("payload-%d", e.Base) + } + return fmt.Sprintf("%s reg%d [%d:%d]", proto, e.DestRegister, e.Offset, e.Len) +} + +func formatVerdict(e *expr.Verdict) string { + switch e.Kind { + case expr.VerdictAccept: + return "accept" + case expr.VerdictDrop: + return "drop" + case expr.VerdictJump: + return fmt.Sprintf("jump %s", e.Chain) + case expr.VerdictGoto: + return fmt.Sprintf("goto %s", e.Chain) + case expr.VerdictReturn: + return "return" + default: + return fmt.Sprintf("verdict-%d", e.Kind) + } +} + +func formatNat(e *expr.NAT) string { + switch e.Type { + case expr.NATTypeSourceNAT: + return "snat" + case expr.NATTypeDestNAT: + return "dnat" + default: + return fmt.Sprintf("nat-%d", e.Type) + } +} + +func formatMatch(e *expr.Match) string { + return fmt.Sprintf("match %s rev %d", e.Name, e.Rev) +} + +func formatBitwise(e *expr.Bitwise) string { + return fmt.Sprintf("bitwise reg%d = reg%d & %v ^ %v", + e.DestRegister, e.SourceRegister, e.Mask, e.Xor) +} + +func formatFib(e *expr.Fib) string { + var flags []string + if e.FlagSADDR { + flags = append(flags, "saddr") + } + if e.FlagDADDR { + flags = append(flags, "daddr") + } + if e.FlagMARK { + flags = append(flags, "mark") + } + if e.FlagIIF { + flags = append(flags, "iif") + } + if e.FlagOIF { + flags = append(flags, "oif") + } + if e.ResultADDRTYPE { + flags = append(flags, "type") + } + return fmt.Sprintf("fib reg%d %s", e.Register, strings.Join(flags, ",")) +} + +func formatSet(conn *nftables.Conn, set *nftables.Set) string { + var builder strings.Builder + builder.WriteString(fmt.Sprintf("\tset %s {\n", set.Name)) + builder.WriteString(fmt.Sprintf("\t\ttype %s\n", formatSetKeyType(set.KeyType))) + if set.ID > 0 { + builder.WriteString(fmt.Sprintf("\t\t# handle %d\n", set.ID)) + } + + elements, err := conn.GetSetElements(set) + if err != nil { + log.Warnf("Failed to get elements for set %s: %v", set.Name, err) + } else if len(elements) > 0 { + builder.WriteString("\t\telements = {") + for i, elem := range elements { + if i > 0 { + builder.WriteString(", ") + } + builder.WriteString(fmt.Sprintf("%v", elem.Key)) + } + builder.WriteString("}\n") + } + + builder.WriteString("\t}\n") + return builder.String() +} + +func formatSetKeyType(keyType nftables.SetDatatype) string { + switch keyType { + case nftables.TypeInvalid: + return "invalid" + case nftables.TypeIPAddr: + return "ipv4_addr" + case nftables.TypeIP6Addr: + return "ipv6_addr" + case nftables.TypeEtherAddr: + return "ether_addr" + case nftables.TypeInetProto: + return "inet_proto" + case nftables.TypeInetService: + return "inet_service" + case nftables.TypeMark: + return "mark" + default: + return fmt.Sprintf("type-%v", keyType) + } +} diff --git a/client/server/debug_nonlinux.go b/client/server/debug_nonlinux.go new file mode 100644 index 00000000000..c54ac9b6ebb --- /dev/null +++ b/client/server/debug_nonlinux.go @@ -0,0 +1,15 @@ +//go:build !linux || android + +package server + +import ( + "archive/zip" + + "github.com/netbirdio/netbird/client/anonymize" + "github.com/netbirdio/netbird/client/proto" +) + +// collectFirewallRules returns nothing on non-linux systems +func (s *Server) addFirewallRules(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + return nil +} diff --git a/client/server/debug_test.go b/client/server/debug_test.go index c8f7bae5d38..ebd0bffbc68 100644 --- a/client/server/debug_test.go +++ b/client/server/debug_test.go @@ -428,3 +428,116 @@ func isInCGNATRange(ip net.IP) bool { } return cgnat.Contains(ip) } + +func TestAnonymizeFirewallRules(t *testing.T) { + // TODO: Add ipv6 + + // Example iptables-save output + iptablesSave := `# Generated by iptables-save v1.8.7 on Thu Dec 19 10:00:00 2024 +*filter +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +-A INPUT -s 192.168.1.0/24 -j ACCEPT +-A INPUT -s 44.192.140.1/32 -j DROP +-A FORWARD -s 10.0.0.0/8 -j DROP +-A FORWARD -s 44.192.140.0/24 -d 52.84.12.34/24 -j ACCEPT +COMMIT + +*nat +:PREROUTING ACCEPT [0:0] +:INPUT ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +-A POSTROUTING -s 192.168.100.0/24 -j MASQUERADE +-A PREROUTING -d 44.192.140.10/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 192.168.1.10:80 +COMMIT` + + // Example iptables -v -n -L output + iptablesVerbose := `Chain INPUT (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + 0 0 ACCEPT all -- * * 192.168.1.0/24 0.0.0.0/0 + 100 1024 DROP all -- * * 44.192.140.1 0.0.0.0/0 + +Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination + 0 0 DROP all -- * * 10.0.0.0/8 0.0.0.0/0 + 25 256 ACCEPT all -- * * 44.192.140.0/24 52.84.12.34/24 + +Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) + pkts bytes target prot opt in out source destination` + + // Example nftables output + nftablesRules := `table inet filter { + chain input { + type filter hook input priority filter; policy accept; + ip saddr 192.168.1.1 accept + ip saddr 44.192.140.1 drop + } + chain forward { + type filter hook forward priority filter; policy accept; + ip saddr 10.0.0.0/8 drop + ip saddr 44.192.140.0/24 ip daddr 52.84.12.34/24 accept + } + }` + + anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) + + // Test iptables-save anonymization + anonIptablesSave := anonymizer.AnonymizeString(iptablesSave) + + // Private IP addresses should remain unchanged + assert.Contains(t, anonIptablesSave, "192.168.1.0/24") + assert.Contains(t, anonIptablesSave, "10.0.0.0/8") + assert.Contains(t, anonIptablesSave, "192.168.100.0/24") + assert.Contains(t, anonIptablesSave, "192.168.1.10") + + // Public IP addresses should be anonymized to the default range + assert.NotContains(t, anonIptablesSave, "44.192.140.1") + assert.NotContains(t, anonIptablesSave, "44.192.140.0/24") + assert.NotContains(t, anonIptablesSave, "52.84.12.34") + assert.Contains(t, anonIptablesSave, "198.51.100.") // Default anonymous range + + // Structure should be preserved + assert.Contains(t, anonIptablesSave, "*filter") + assert.Contains(t, anonIptablesSave, ":INPUT ACCEPT [0:0]") + assert.Contains(t, anonIptablesSave, "COMMIT") + assert.Contains(t, anonIptablesSave, "-j MASQUERADE") + assert.Contains(t, anonIptablesSave, "--dport 80") + + // Test iptables verbose output anonymization + anonIptablesVerbose := anonymizer.AnonymizeString(iptablesVerbose) + + // Private IP addresses should remain unchanged + assert.Contains(t, anonIptablesVerbose, "192.168.1.0/24") + assert.Contains(t, anonIptablesVerbose, "10.0.0.0/8") + + // Public IP addresses should be anonymized to the default range + assert.NotContains(t, anonIptablesVerbose, "44.192.140.1") + assert.NotContains(t, anonIptablesVerbose, "44.192.140.0/24") + assert.NotContains(t, anonIptablesVerbose, "52.84.12.34") + assert.Contains(t, anonIptablesVerbose, "198.51.100.") // Default anonymous range + + // Structure and counters should be preserved + assert.Contains(t, anonIptablesVerbose, "Chain INPUT (policy ACCEPT 0 packets, 0 bytes)") + assert.Contains(t, anonIptablesVerbose, "100 1024 DROP") + assert.Contains(t, anonIptablesVerbose, "pkts bytes target") + + // Test nftables anonymization + anonNftables := anonymizer.AnonymizeString(nftablesRules) + + // Private IP addresses should remain unchanged + assert.Contains(t, anonNftables, "192.168.1.1") + assert.Contains(t, anonNftables, "10.0.0.0/8") + + // Public IP addresses should be anonymized to the default range + assert.NotContains(t, anonNftables, "44.192.140.1") + assert.NotContains(t, anonNftables, "44.192.140.0/24") + assert.NotContains(t, anonNftables, "52.84.12.34") + assert.Contains(t, anonNftables, "198.51.100.") // Default anonymous range + + // Structure should be preserved + assert.Contains(t, anonNftables, "table inet filter {") + assert.Contains(t, anonNftables, "chain input {") + assert.Contains(t, anonNftables, "type filter hook input priority filter; policy accept;") +}