mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
[client] Set up signal to generate debug bundles (#3683)
This commit is contained in:
1022
client/internal/debug/debug.go
Normal file
1022
client/internal/debug/debug.go
Normal file
File diff suppressed because it is too large
Load Diff
682
client/internal/debug/debug_linux.go
Normal file
682
client/internal/debug/debug_linux.go
Normal file
@@ -0,0 +1,682 @@
|
||||
//go:build linux && !android
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// addFirewallRules collects and adds firewall rules to the archive
|
||||
func (g *BundleGenerator) addFirewallRules() error {
|
||||
log.Info("Collecting firewall rules")
|
||||
iptablesRules, err := collectIPTablesRules()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to collect iptables rules: %v", err)
|
||||
} else {
|
||||
if g.anonymize {
|
||||
iptablesRules = g.anonymizer.AnonymizeString(iptablesRules)
|
||||
}
|
||||
if err := g.addFileToZip(strings.NewReader(iptablesRules), "iptables.txt"); err != nil {
|
||||
log.Warnf("Failed to add iptables rules to bundle: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
nftablesRules, err := collectNFTablesRules()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to collect nftables rules: %v", err)
|
||||
} else {
|
||||
if g.anonymize {
|
||||
nftablesRules = g.anonymizer.AnonymizeString(nftablesRules)
|
||||
}
|
||||
if err := g.addFileToZip(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")
|
||||
}
|
||||
|
||||
builder.WriteString("=== iptables -v -n -L output ===\n")
|
||||
|
||||
tables := []string{"filter", "nat", "mangle", "raw", "security"}
|
||||
|
||||
for _, table := range tables {
|
||||
builder.WriteString(fmt.Sprintf("*%s\n", 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
|
||||
}
|
||||
|
||||
for _, chain := range chains {
|
||||
formatChain(conn, table, chain, &builder)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
7
client/internal/debug/debug_mobile.go
Normal file
7
client/internal/debug/debug_mobile.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build ios || android
|
||||
|
||||
package debug
|
||||
|
||||
func (g *BundleGenerator) addRoutes() error {
|
||||
return nil
|
||||
}
|
||||
8
client/internal/debug/debug_nonlinux.go
Normal file
8
client/internal/debug/debug_nonlinux.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build !linux || android
|
||||
|
||||
package debug
|
||||
|
||||
// collectFirewallRules returns nothing on non-linux systems
|
||||
func (g *BundleGenerator) addFirewallRules() error {
|
||||
return nil
|
||||
}
|
||||
25
client/internal/debug/debug_nonmobile.go
Normal file
25
client/internal/debug/debug_nonmobile.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build !ios && !android
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||
)
|
||||
|
||||
func (g *BundleGenerator) addRoutes() error {
|
||||
routes, err := systemops.GetRoutesFromTable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get routes: %w", err)
|
||||
}
|
||||
|
||||
// TODO: get routes including nexthop
|
||||
routesContent := formatRoutes(routes, g.anonymize, g.anonymizer)
|
||||
routesReader := strings.NewReader(routesContent)
|
||||
if err := g.addFileToZip(routesReader, "routes.txt"); err != nil {
|
||||
return fmt.Errorf("add routes file to zip: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
543
client/internal/debug/debug_test.go
Normal file
543
client/internal/debug/debug_test.go
Normal file
@@ -0,0 +1,543 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/anonymize"
|
||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||
)
|
||||
|
||||
func TestAnonymizeStateFile(t *testing.T) {
|
||||
testState := map[string]json.RawMessage{
|
||||
"null_state": json.RawMessage("null"),
|
||||
"test_state": mustMarshal(map[string]any{
|
||||
// Test simple fields
|
||||
"public_ip": "203.0.113.1",
|
||||
"private_ip": "192.168.1.1",
|
||||
"protected_ip": "100.64.0.1",
|
||||
"well_known_ip": "8.8.8.8",
|
||||
"ipv6_addr": "2001:db8::1",
|
||||
"private_ipv6": "fd00::1",
|
||||
"domain": "test.example.com",
|
||||
"uri": "stun:stun.example.com:3478",
|
||||
"uri_with_ip": "turn:203.0.113.1:3478",
|
||||
"netbird_domain": "device.netbird.cloud",
|
||||
|
||||
// Test CIDR ranges
|
||||
"public_cidr": "203.0.113.0/24",
|
||||
"private_cidr": "192.168.0.0/16",
|
||||
"protected_cidr": "100.64.0.0/10",
|
||||
"ipv6_cidr": "2001:db8::/32",
|
||||
"private_ipv6_cidr": "fd00::/8",
|
||||
|
||||
// Test nested structures
|
||||
"nested": map[string]any{
|
||||
"ip": "203.0.113.2",
|
||||
"domain": "nested.example.com",
|
||||
"more_nest": map[string]any{
|
||||
"ip": "203.0.113.3",
|
||||
"domain": "deep.example.com",
|
||||
},
|
||||
},
|
||||
|
||||
// Test arrays
|
||||
"string_array": []any{
|
||||
"203.0.113.4",
|
||||
"test1.example.com",
|
||||
"test2.example.com",
|
||||
},
|
||||
"object_array": []any{
|
||||
map[string]any{
|
||||
"ip": "203.0.113.5",
|
||||
"domain": "array1.example.com",
|
||||
},
|
||||
map[string]any{
|
||||
"ip": "203.0.113.6",
|
||||
"domain": "array2.example.com",
|
||||
},
|
||||
},
|
||||
|
||||
// Test multiple occurrences of same value
|
||||
"duplicate_ip": "203.0.113.1", // Same as public_ip
|
||||
"duplicate_domain": "test.example.com", // Same as domain
|
||||
|
||||
// Test URIs with various schemes
|
||||
"stun_uri": "stun:stun.example.com:3478",
|
||||
"turns_uri": "turns:turns.example.com:5349",
|
||||
"http_uri": "http://web.example.com:80",
|
||||
"https_uri": "https://secure.example.com:443",
|
||||
|
||||
// Test strings that might look like IPs but aren't
|
||||
"not_ip": "300.300.300.300",
|
||||
"partial_ip": "192.168",
|
||||
"ip_like_string": "1234.5678",
|
||||
|
||||
// Test mixed content strings
|
||||
"mixed_content": "Server at 203.0.113.1 (test.example.com) on port 80",
|
||||
|
||||
// Test empty and special values
|
||||
"empty_string": "",
|
||||
"null_value": nil,
|
||||
"numeric_value": 42,
|
||||
"boolean_value": true,
|
||||
}),
|
||||
"route_state": mustMarshal(map[string]any{
|
||||
"routes": []any{
|
||||
map[string]any{
|
||||
"network": "203.0.113.0/24",
|
||||
"gateway": "203.0.113.1",
|
||||
"domains": []any{
|
||||
"route1.example.com",
|
||||
"route2.example.com",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"network": "2001:db8::/32",
|
||||
"gateway": "2001:db8::1",
|
||||
"domains": []any{
|
||||
"route3.example.com",
|
||||
"route4.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Test map with IP/CIDR keys
|
||||
"refCountMap": map[string]any{
|
||||
"203.0.113.1/32": map[string]any{
|
||||
"Count": 1,
|
||||
"Out": map[string]any{
|
||||
"IP": "192.168.0.1",
|
||||
"Intf": map[string]any{
|
||||
"Name": "eth0",
|
||||
"Index": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"2001:db8::1/128": map[string]any{
|
||||
"Count": 1,
|
||||
"Out": map[string]any{
|
||||
"IP": "fe80::1",
|
||||
"Intf": map[string]any{
|
||||
"Name": "eth0",
|
||||
"Index": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"10.0.0.1/32": map[string]any{ // private IP should remain unchanged
|
||||
"Count": 1,
|
||||
"Out": map[string]any{
|
||||
"IP": "192.168.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
|
||||
// Pre-seed the domains we need to verify in the test assertions
|
||||
anonymizer.AnonymizeDomain("test.example.com")
|
||||
anonymizer.AnonymizeDomain("nested.example.com")
|
||||
anonymizer.AnonymizeDomain("deep.example.com")
|
||||
anonymizer.AnonymizeDomain("array1.example.com")
|
||||
|
||||
err := anonymizeStateFile(&testState, anonymizer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Helper function to unmarshal and get nested values
|
||||
var state map[string]any
|
||||
err = json.Unmarshal(testState["test_state"], &state)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test null state remains unchanged
|
||||
require.Equal(t, "null", string(testState["null_state"]))
|
||||
|
||||
// Basic assertions
|
||||
assert.NotEqual(t, "203.0.113.1", state["public_ip"])
|
||||
assert.Equal(t, "192.168.1.1", state["private_ip"]) // Private IP unchanged
|
||||
assert.Equal(t, "100.64.0.1", state["protected_ip"]) // Protected IP unchanged
|
||||
assert.Equal(t, "8.8.8.8", state["well_known_ip"]) // Well-known IP unchanged
|
||||
assert.NotEqual(t, "2001:db8::1", state["ipv6_addr"])
|
||||
assert.Equal(t, "fd00::1", state["private_ipv6"]) // Private IPv6 unchanged
|
||||
assert.NotEqual(t, "test.example.com", state["domain"])
|
||||
assert.True(t, strings.HasSuffix(state["domain"].(string), ".domain"))
|
||||
assert.Equal(t, "device.netbird.cloud", state["netbird_domain"]) // Netbird domain unchanged
|
||||
|
||||
// CIDR ranges
|
||||
assert.NotEqual(t, "203.0.113.0/24", state["public_cidr"])
|
||||
assert.Contains(t, state["public_cidr"], "/24") // Prefix preserved
|
||||
assert.Equal(t, "192.168.0.0/16", state["private_cidr"]) // Private CIDR unchanged
|
||||
assert.Equal(t, "100.64.0.0/10", state["protected_cidr"]) // Protected CIDR unchanged
|
||||
assert.NotEqual(t, "2001:db8::/32", state["ipv6_cidr"])
|
||||
assert.Contains(t, state["ipv6_cidr"], "/32") // IPv6 prefix preserved
|
||||
|
||||
// Nested structures
|
||||
nested := state["nested"].(map[string]any)
|
||||
assert.NotEqual(t, "203.0.113.2", nested["ip"])
|
||||
assert.NotEqual(t, "nested.example.com", nested["domain"])
|
||||
moreNest := nested["more_nest"].(map[string]any)
|
||||
assert.NotEqual(t, "203.0.113.3", moreNest["ip"])
|
||||
assert.NotEqual(t, "deep.example.com", moreNest["domain"])
|
||||
|
||||
// Arrays
|
||||
strArray := state["string_array"].([]any)
|
||||
assert.NotEqual(t, "203.0.113.4", strArray[0])
|
||||
assert.NotEqual(t, "test1.example.com", strArray[1])
|
||||
assert.True(t, strings.HasSuffix(strArray[1].(string), ".domain"))
|
||||
|
||||
objArray := state["object_array"].([]any)
|
||||
firstObj := objArray[0].(map[string]any)
|
||||
assert.NotEqual(t, "203.0.113.5", firstObj["ip"])
|
||||
assert.NotEqual(t, "array1.example.com", firstObj["domain"])
|
||||
|
||||
// Duplicate values should be anonymized consistently
|
||||
assert.Equal(t, state["public_ip"], state["duplicate_ip"])
|
||||
assert.Equal(t, state["domain"], state["duplicate_domain"])
|
||||
|
||||
// URIs
|
||||
assert.NotContains(t, state["stun_uri"], "stun.example.com")
|
||||
assert.NotContains(t, state["turns_uri"], "turns.example.com")
|
||||
assert.NotContains(t, state["http_uri"], "web.example.com")
|
||||
assert.NotContains(t, state["https_uri"], "secure.example.com")
|
||||
|
||||
// Non-IP strings should remain unchanged
|
||||
assert.Equal(t, "300.300.300.300", state["not_ip"])
|
||||
assert.Equal(t, "192.168", state["partial_ip"])
|
||||
assert.Equal(t, "1234.5678", state["ip_like_string"])
|
||||
|
||||
// Mixed content should have IPs and domains replaced
|
||||
mixedContent := state["mixed_content"].(string)
|
||||
assert.NotContains(t, mixedContent, "203.0.113.1")
|
||||
assert.NotContains(t, mixedContent, "test.example.com")
|
||||
assert.Contains(t, mixedContent, "Server at ")
|
||||
assert.Contains(t, mixedContent, " on port 80")
|
||||
|
||||
// Special values should remain unchanged
|
||||
assert.Equal(t, "", state["empty_string"])
|
||||
assert.Nil(t, state["null_value"])
|
||||
assert.Equal(t, float64(42), state["numeric_value"])
|
||||
assert.Equal(t, true, state["boolean_value"])
|
||||
|
||||
// Check route state
|
||||
var routeState map[string]any
|
||||
err = json.Unmarshal(testState["route_state"], &routeState)
|
||||
require.NoError(t, err)
|
||||
|
||||
routes := routeState["routes"].([]any)
|
||||
route1 := routes[0].(map[string]any)
|
||||
assert.NotEqual(t, "203.0.113.0/24", route1["network"])
|
||||
assert.Contains(t, route1["network"], "/24")
|
||||
assert.NotEqual(t, "203.0.113.1", route1["gateway"])
|
||||
domains := route1["domains"].([]any)
|
||||
assert.True(t, strings.HasSuffix(domains[0].(string), ".domain"))
|
||||
assert.True(t, strings.HasSuffix(domains[1].(string), ".domain"))
|
||||
|
||||
// Check map keys are anonymized
|
||||
refCountMap := routeState["refCountMap"].(map[string]any)
|
||||
hasPublicIPKey := false
|
||||
hasIPv6Key := false
|
||||
hasPrivateIPKey := false
|
||||
for key := range refCountMap {
|
||||
if strings.Contains(key, "203.0.113.1") {
|
||||
hasPublicIPKey = true
|
||||
}
|
||||
if strings.Contains(key, "2001:db8::1") {
|
||||
hasIPv6Key = true
|
||||
}
|
||||
if key == "10.0.0.1/32" {
|
||||
hasPrivateIPKey = true
|
||||
}
|
||||
}
|
||||
assert.False(t, hasPublicIPKey, "public IP in key should be anonymized")
|
||||
assert.False(t, hasIPv6Key, "IPv6 in key should be anonymized")
|
||||
assert.True(t, hasPrivateIPKey, "private IP in key should remain unchanged")
|
||||
}
|
||||
|
||||
func mustMarshal(v any) json.RawMessage {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestAnonymizeNetworkMap(t *testing.T) {
|
||||
networkMap := &mgmProto.NetworkMap{
|
||||
PeerConfig: &mgmProto.PeerConfig{
|
||||
Address: "203.0.113.5",
|
||||
Dns: "1.2.3.4",
|
||||
Fqdn: "peer1.corp.example.com",
|
||||
SshConfig: &mgmProto.SSHConfig{
|
||||
SshPubKey: []byte("ssh-rsa AAAAB3NzaC1..."),
|
||||
},
|
||||
},
|
||||
RemotePeers: []*mgmProto.RemotePeerConfig{
|
||||
{
|
||||
AllowedIps: []string{
|
||||
"203.0.113.1/32",
|
||||
"2001:db8:1234::1/128",
|
||||
"192.168.1.1/32",
|
||||
"100.64.0.1/32",
|
||||
"10.0.0.1/32",
|
||||
},
|
||||
Fqdn: "peer2.corp.example.com",
|
||||
SshConfig: &mgmProto.SSHConfig{
|
||||
SshPubKey: []byte("ssh-rsa AAAAB3NzaC2..."),
|
||||
},
|
||||
},
|
||||
},
|
||||
Routes: []*mgmProto.Route{
|
||||
{
|
||||
Network: "197.51.100.0/24",
|
||||
Domains: []string{"prod.example.com", "staging.example.com"},
|
||||
NetID: "net-123abc",
|
||||
},
|
||||
},
|
||||
DNSConfig: &mgmProto.DNSConfig{
|
||||
NameServerGroups: []*mgmProto.NameServerGroup{
|
||||
{
|
||||
NameServers: []*mgmProto.NameServer{
|
||||
{IP: "8.8.8.8"},
|
||||
{IP: "1.1.1.1"},
|
||||
{IP: "203.0.113.53"},
|
||||
},
|
||||
Domains: []string{"example.com", "internal.example.com"},
|
||||
},
|
||||
},
|
||||
CustomZones: []*mgmProto.CustomZone{
|
||||
{
|
||||
Domain: "custom.example.com",
|
||||
Records: []*mgmProto.SimpleRecord{
|
||||
{
|
||||
Name: "www.custom.example.com",
|
||||
Type: 1,
|
||||
RData: "203.0.113.10",
|
||||
},
|
||||
{
|
||||
Name: "internal.custom.example.com",
|
||||
Type: 1,
|
||||
RData: "192.168.1.10",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create anonymizer with test addresses
|
||||
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||
|
||||
// Anonymize the network map
|
||||
err := anonymizeNetworkMap(networkMap, anonymizer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test PeerConfig anonymization
|
||||
peerCfg := networkMap.PeerConfig
|
||||
require.NotEqual(t, "203.0.113.5", peerCfg.Address)
|
||||
|
||||
// Verify DNS and FQDN are properly anonymized
|
||||
require.NotEqual(t, "1.2.3.4", peerCfg.Dns)
|
||||
require.NotEqual(t, "peer1.corp.example.com", peerCfg.Fqdn)
|
||||
require.True(t, strings.HasSuffix(peerCfg.Fqdn, ".domain"))
|
||||
|
||||
// Verify SSH key is replaced
|
||||
require.Equal(t, []byte("ssh-placeholder-key"), peerCfg.SshConfig.SshPubKey)
|
||||
|
||||
// Test RemotePeers anonymization
|
||||
remotePeer := networkMap.RemotePeers[0]
|
||||
|
||||
// Verify FQDN is anonymized
|
||||
require.NotEqual(t, "peer2.corp.example.com", remotePeer.Fqdn)
|
||||
require.True(t, strings.HasSuffix(remotePeer.Fqdn, ".domain"))
|
||||
|
||||
// Check that public IPs are anonymized but private IPs are preserved
|
||||
for _, allowedIP := range remotePeer.AllowedIps {
|
||||
ip, _, err := net.ParseCIDR(allowedIP)
|
||||
require.NoError(t, err)
|
||||
|
||||
if ip.IsPrivate() || isInCGNATRange(ip) {
|
||||
require.Contains(t, []string{
|
||||
"192.168.1.1/32",
|
||||
"100.64.0.1/32",
|
||||
"10.0.0.1/32",
|
||||
}, allowedIP)
|
||||
} else {
|
||||
require.NotContains(t, []string{
|
||||
"203.0.113.1/32",
|
||||
"2001:db8:1234::1/128",
|
||||
}, allowedIP)
|
||||
}
|
||||
}
|
||||
|
||||
// Test Routes anonymization
|
||||
route := networkMap.Routes[0]
|
||||
require.NotEqual(t, "197.51.100.0/24", route.Network)
|
||||
for _, domain := range route.Domains {
|
||||
require.True(t, strings.HasSuffix(domain, ".domain"))
|
||||
require.NotContains(t, domain, "example.com")
|
||||
}
|
||||
|
||||
// Test DNS config anonymization
|
||||
dnsConfig := networkMap.DNSConfig
|
||||
nameServerGroup := dnsConfig.NameServerGroups[0]
|
||||
|
||||
// Verify well-known DNS servers are preserved
|
||||
require.Equal(t, "8.8.8.8", nameServerGroup.NameServers[0].IP)
|
||||
require.Equal(t, "1.1.1.1", nameServerGroup.NameServers[1].IP)
|
||||
|
||||
// Verify public DNS server is anonymized
|
||||
require.NotEqual(t, "203.0.113.53", nameServerGroup.NameServers[2].IP)
|
||||
|
||||
// Verify domains are anonymized
|
||||
for _, domain := range nameServerGroup.Domains {
|
||||
require.True(t, strings.HasSuffix(domain, ".domain"))
|
||||
require.NotContains(t, domain, "example.com")
|
||||
}
|
||||
|
||||
// Test CustomZones anonymization
|
||||
customZone := dnsConfig.CustomZones[0]
|
||||
require.True(t, strings.HasSuffix(customZone.Domain, ".domain"))
|
||||
require.NotContains(t, customZone.Domain, "example.com")
|
||||
|
||||
// Verify records are properly anonymized
|
||||
for _, record := range customZone.Records {
|
||||
require.True(t, strings.HasSuffix(record.Name, ".domain"))
|
||||
require.NotContains(t, record.Name, "example.com")
|
||||
|
||||
ip := net.ParseIP(record.RData)
|
||||
if ip != nil {
|
||||
if !ip.IsPrivate() {
|
||||
require.NotEqual(t, "203.0.113.10", record.RData)
|
||||
} else {
|
||||
require.Equal(t, "192.168.1.10", record.RData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if IP is in CGNAT range
|
||||
func isInCGNATRange(ip net.IP) bool {
|
||||
cgnat := net.IPNet{
|
||||
IP: net.ParseIP("100.64.0.0"),
|
||||
Mask: net.CIDRMask(10, 32),
|
||||
}
|
||||
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;")
|
||||
}
|
||||
Reference in New Issue
Block a user