From 09c15e090d4ab02b1bfae8f2767951b6e4170cd9 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Sat, 4 Jan 2025 21:49:00 +0000 Subject: [PATCH 1/3] debug: option to print using mnl format nftables uses the libmnl library that has a particular debugging format. This format is also used for nftables --debug=all. google/nftables golang library to interact with nftables uses this library to interface with netlink directly. Since these libraries are not well documented, is common to have to dump the netlink bits to compare. This commit implements an option to choose the same format used in nftables by adding a new option to the NLDEBUG environmenta variable and using the format=mnt key value. Change-Id: Ifcddb31d1561074942ad8d6af74b134540eca5fe Signed-off-by: Antonio Ojea --- debug.go | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- doc.go | 3 + 2 files changed, 199 insertions(+), 3 deletions(-) diff --git a/debug.go b/debug.go index d39d66c5..9f0e1e16 100644 --- a/debug.go +++ b/debug.go @@ -2,10 +2,15 @@ package netlink import ( "fmt" + "io" "log" "os" "strconv" "strings" + "syscall" + "unsafe" + + "github.com/mdlayher/netlink/nlenc" ) // Arguments used to create a debugger. @@ -23,8 +28,9 @@ func init() { // A debugger is used to provide debugging information about a netlink connection. type debugger struct { - Log *log.Logger - Level int + Log *log.Logger + Level int + Format string } // newDebugger creates a debugger by parsing key=value arguments. @@ -50,6 +56,8 @@ func newDebugger(args []string) *debugger { } d.Level = level + case "format": + d.Format = kv[1] } } @@ -60,10 +68,195 @@ func newDebugger(args []string) *debugger { // high enough to print the message. func (d *debugger) debugf(level int, format string, v ...interface{}) { if d.Level >= level { - d.Log.Printf(format, v...) + if d.Format == "mnl" { + for _, iface := range v { + if msg, ok := iface.(Message); ok { + nlmsgFprintf(d.Log.Writer(), msg) + } + } + } else { + d.Log.Printf(format, v...) + } } } func panicf(format string, a ...interface{}) { panic(fmt.Sprintf(format, a...)) } + +/* + nlmsgFprintf - print netlink message to file + - Based on https://git.netfilter.org/libmnl/tree/src/nlmsg.c + - This function prints the netlink header to a file handle. + - It may be useful for debugging purposes. One example of the output + - is the following: + +---------------- ------------------ +| 0000000040 | | message length | +| 00016 | R-A- | | type | flags | +| 1289148991 | | sequence number| +| 0000000000 | | port ID | +---------------- ------------------ +| 00 00 00 00 | | extra header | +| 00 00 00 00 | | extra header | +| 01 00 00 00 | | extra header | +| 01 00 00 00 | | extra header | +|00008|--|00003| |len |flags| type| +| 65 74 68 30 | | data | e t h 0 +---------------- ------------------ + + * + * This example above shows the netlink message that is send to kernel-space + * to set up the link interface eth0. The netlink and attribute header data + * are displayed in base 10 whereas the extra header and the attribute payload + * are expressed in base 16. The possible flags in the netlink header are: + * + * - R, that indicates that NLM_F_REQUEST is set. + * - M, that indicates that NLM_F_MULTI is set. + * - A, that indicates that NLM_F_ACK is set. + * - E, that indicates that NLM_F_ECHO is set. + * + * The lack of one flag is displayed with '-'. On the other hand, the possible + * attribute flags available are: + * + * - N, that indicates that NLA_F_NESTED is set. + * - B, that indicates that NLA_F_NET_BYTEORDER is set. +*/ +func nlmsgFprintfHeader(fd io.Writer, nlh Header) { + fmt.Fprintf(fd, "----------------\t------------------\n") + fmt.Fprintf(fd, "| %010d |\t| message length |\n", nlh.Length) + fmt.Fprintf(fd, "| %05d | %s%s%s%s |\t| type | flags |\n", + nlh.Type, + ternary(nlh.Flags&Request != 0, "R", "-"), + ternary(nlh.Flags&Multi != 0, "M", "-"), + ternary(nlh.Flags&Acknowledge != 0, "A", "-"), + ternary(nlh.Flags&Echo != 0, "E", "-"), + ) + fmt.Fprintf(fd, "| %010d |\t| sequence number|\n", nlh.Sequence) + fmt.Fprintf(fd, "| %010d |\t| port ID |\n", nlh.PID) + fmt.Fprintf(fd, "----------------\t------------------\n") +} + +// nlmsgFprintf checks a single Message for netlink errors. +func nlmsgFprintf(fd io.Writer, m Message) { + colorize := true + var hasHeader bool + nlmsgFprintfHeader(fd, m.Header) + switch { + case m.Header.Type == Error: + hasHeader = true + case m.Header.Type == Done && m.Header.Flags&Multi != 0: + if len(m.Data) == 0 { + return + } + default: + // Neither, nothing to do. + } + + // Errno occupies 4 bytes. + const endErrno = 4 + if len(m.Data) < endErrno { + return + } + + c := nlenc.Int32(m.Data[:endErrno]) + if c != 0 { + b := m.Data[0:4] + fmt.Fprintf(fd, "| %.2x %.2x %.2x %.2x |\t", + 0xff&b[0], 0xff&b[1], + 0xff&b[2], 0xff&b[3]) + fmt.Fprintf(fd, "| extra header |\n") + } + + // Flags indicate an extended acknowledgement. The type/flags combination + // checked above determines the offset where the TLVs occur. + var off int + if hasHeader { + // There is an nlmsghdr preceding the TLVs. + if len(m.Data) < endErrno+nlmsgHeaderLen { + return + } + + // The TLVs should be at the offset indicated by the nlmsghdr.length, + // plus the offset where the header began. But make sure the calculated + // offset is still in-bounds. + h := *(*Header)(unsafe.Pointer(&m.Data[endErrno : endErrno+nlmsgHeaderLen][0])) + off = endErrno + int(h.Length) + + if len(m.Data) < off { + return + } + } else { + // There is no nlmsghdr preceding the TLVs, parse them directly. + off = endErrno + } + + data := m.Data[off:] + for i := 0; i < len(data); { + // Make sure there's at least a header's worth + // of data to read on each iteration. + if len(data[i:]) < nlaHeaderLen { + break + } + + // Extract the length of the attribute. + l := int(nlenc.Uint16(data[i : i+2])) + // extract the type + t := nlenc.Uint16(data[i+2 : i+4]) + // print attribute header + if colorize { + fmt.Fprintf(fd, "|\033[1;31m%05d|\033[1;32m%s%s|\033[1;34m%05d\033[0m|\t", + l, + ternary(t&syscall.NLA_F_NESTED != 0, "N", "-"), + ternary(t&syscall.NLA_F_NET_BYTEORDER != 0, "B", "-"), + t&attrTypeMask) + fmt.Fprintf(fd, "|len |flags| type|\n") + } else { + fmt.Fprintf(fd, "|%05d|%s%s|%05d|\t", + l, + ternary(t&syscall.NLA_F_NESTED != 0, "N", "-"), + ternary(t&syscall.NLA_F_NET_BYTEORDER != 0, "B", "-"), + t&attrTypeMask) + fmt.Fprintf(fd, "|len |flags| type|\n") + } + + nextAttr := i + nlaAlign(l) + + // advance the pointer to the bytes after the header + i += nlaHeaderLen + + // Ignore zero-length attributes. + if l == 0 { + continue + } + // If nested check the next attribute + if t&syscall.NLA_F_NESTED != 0 { + continue + } + + // Print the remaining attributes bytes + for ; i < nextAttr; i += 4 { + fmt.Fprintf(fd, "| %.2x %.2x %.2x %.2x |\t", + 0xff&data[i], 0xff&data[i+1], + 0xff&data[i+2], 0xff&data[i+3]) + + fmt.Fprintf(fd, "| data |") + + fmt.Fprintf(fd, "\t %s %s %s %s\n", + ternary(strconv.IsPrint(rune(data[i])), string(data[i]), " "), + ternary(strconv.IsPrint(rune(data[i+1])), string(data[i+1]), " "), + ternary(strconv.IsPrint(rune(data[i+2])), string(data[i+2]), " "), + ternary(strconv.IsPrint(rune(data[i+3])), string(data[i+3]), " "), + ) + } + } + fmt.Fprintf(fd, "----------------\t------------------\n") +} + +func ternary(cond bool, iftrue string, iffalse string) string { + if cond { + return iftrue + } else { + return iffalse + } +} diff --git a/doc.go b/doc.go index 98c744a5..9828be85 100644 --- a/doc.go +++ b/doc.go @@ -27,7 +27,10 @@ // // $ NLDEBUG=level=1 ./nlctl // +// $ NLDEBUG=level=1,format=mnl ./nlctl +// // Available key/value debugger options include: // // level=N: specify the debugging level (only "1" is currently supported) +// format=mnl: specify the same format used by libmnl (nftables --debug=all) package netlink From 91acba8ada245f4dcb133866be48d532033b2901 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Sun, 5 Jan 2025 19:00:26 +0000 Subject: [PATCH 2/3] ci: bump actions and golang versions Change-Id: I0d067261c1c30c0dc25fbc92f162b0a3746158ef --- .github/workflows/linux-integration-test.yml | 4 ++-- .github/workflows/linux-test.yml | 6 +++--- .github/workflows/macos-test.yml | 4 ++-- .github/workflows/static-analysis.yml | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linux-integration-test.yml b/.github/workflows/linux-integration-test.yml index b29e7f1e..e0a70950 100644 --- a/.github/workflows/linux-integration-test.yml +++ b/.github/workflows/linux-integration-test.yml @@ -17,13 +17,13 @@ jobs: steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create a network namespace for privileged tests run: sudo ip netns add unpriv0 diff --git a/.github/workflows/linux-test.yml b/.github/workflows/linux-test.yml index aa7f76b5..9fe0b11c 100644 --- a/.github/workflows/linux-test.yml +++ b/.github/workflows/linux-test.yml @@ -12,18 +12,18 @@ jobs: build: strategy: matrix: - go-version: ["1.21", "1.22", "1.23"] + go-version: ["1.21", "1.22", "1.23"] runs-on: ubuntu-latest steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create a network namespace for unprivileged tests run: sudo ip netns add unpriv0 diff --git a/.github/workflows/macos-test.yml b/.github/workflows/macos-test.yml index c305b130..88381300 100644 --- a/.github/workflows/macos-test.yml +++ b/.github/workflows/macos-test.yml @@ -17,13 +17,13 @@ jobs: steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run tests run: go test -v -race ./... diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 319eeea0..d788c60d 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -17,13 +17,13 @@ jobs: steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest From e14b4c44d55ab93456cab8d6e184e086a799b9e9 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Sat, 4 Jan 2025 21:49:00 +0000 Subject: [PATCH 3/3] debug: option to print using mnl format nftables uses the libmnl library that has a particular debugging format. This format is also used for nftables --debug=all. google/nftables golang library to interact with nftables uses this library to interface with netlink directly. Since these libraries are not well documented, is common to have to dump the netlink bits to compare. This commit implements an option to choose the same format used in nftables by adding a new option to the NLDEBUG environmenta variable and using the format=mnl key value. For Linux systems mnl is the default format. Change-Id: I5f63753b3ada4d25565fe0bddac35e010510a34f --- debug.go | 47 -------- debug_linux.go | 252 +++++++++++++++++++++++++++++++++++++++ debug_linux_test.go | 285 ++++++++++++++++++++++++++++++++++++++++++++ debug_others.go | 45 +++++++ doc.go | 2 +- 5 files changed, 583 insertions(+), 48 deletions(-) create mode 100644 debug_linux.go create mode 100644 debug_linux_test.go create mode 100644 debug_others.go diff --git a/debug.go b/debug.go index 9f0e1e16..62cb03e7 100644 --- a/debug.go +++ b/debug.go @@ -33,53 +33,6 @@ type debugger struct { Format string } -// newDebugger creates a debugger by parsing key=value arguments. -func newDebugger(args []string) *debugger { - d := &debugger{ - Log: log.New(os.Stderr, "nl: ", 0), - Level: 1, - } - - for _, a := range args { - kv := strings.Split(a, "=") - if len(kv) != 2 { - // Ignore malformed pairs and assume callers wants defaults. - continue - } - - switch kv[0] { - // Select the log level for the debugger. - case "level": - level, err := strconv.Atoi(kv[1]) - if err != nil { - panicf("netlink: invalid NLDEBUG level: %q", a) - } - - d.Level = level - case "format": - d.Format = kv[1] - } - } - - return d -} - -// debugf prints debugging information at the specified level, if d.Level is -// high enough to print the message. -func (d *debugger) debugf(level int, format string, v ...interface{}) { - if d.Level >= level { - if d.Format == "mnl" { - for _, iface := range v { - if msg, ok := iface.(Message); ok { - nlmsgFprintf(d.Log.Writer(), msg) - } - } - } else { - d.Log.Printf(format, v...) - } - } -} - func panicf(format string, a ...interface{}) { panic(fmt.Sprintf(format, a...)) } diff --git a/debug_linux.go b/debug_linux.go new file mode 100644 index 00000000..2937008f --- /dev/null +++ b/debug_linux.go @@ -0,0 +1,252 @@ +//go:build linux + +package netlink + +import ( + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + "syscall" + "unsafe" + + "github.com/mdlayher/netlink/nlenc" + "golang.org/x/sys/unix" +) + +// newDebugger creates a debugger by parsing key=value arguments. +func newDebugger(args []string) *debugger { + d := &debugger{ + Log: log.New(os.Stderr, "nl: ", 0), + Level: 1, + Format: "mnl", + } + + for _, a := range args { + kv := strings.Split(a, "=") + if len(kv) != 2 { + // Ignore malformed pairs and assume callers wants defaults. + continue + } + + switch kv[0] { + // Select the log level for the debugger. + case "level": + level, err := strconv.Atoi(kv[1]) + if err != nil { + panicf("netlink: invalid NLDEBUG level: %q", a) + } + + d.Level = level + case "format": + d.Format = kv[1] + } + } + + return d +} + +// debugf prints debugging information at the specified level, if d.Level is +// high enough to print the message. +func (d *debugger) debugf(level int, format string, v ...interface{}) { + if d.Level < level { + return + } + + switch d.Format { + case "mnl": + colorize := true + _, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) + if err != nil { + colorize = false + } + + for _, iface := range v { + if msg, ok := iface.(Message); ok { + nlmsgFprintf(d.Log.Writer(), msg, colorize) + } else { + d.Log.Printf(format, v...) + } + } + default: + d.Log.Printf(format, v...) + } +} + +/* + nlmsgFprintf - print netlink message to file + - Based on https://git.netfilter.org/libmnl/tree/src/nlmsg.c + - This function prints the netlink header to a file handle. + - It may be useful for debugging purposes. One example of the output + - is the following: + +---------------- ------------------ +| 0000000040 | | message length | +| 00016 | R-A- | | type | flags | +| 1289148991 | | sequence number| +| 0000000000 | | port ID | +---------------- ------------------ +| 00 00 00 00 | | extra header | +| 00 00 00 00 | | extra header | +| 01 00 00 00 | | extra header | +| 01 00 00 00 | | extra header | +|00008|--|00003| |len |flags| type| +| 65 74 68 30 | | data | e t h 0 +---------------- ------------------ + + * + * This example above shows the netlink message that is send to kernel-space + * to set up the link interface eth0. The netlink and attribute header data + * are displayed in base 10 whereas the extra header and the attribute payload + * are expressed in base 16. The possible flags in the netlink header are: + * + * - R, that indicates that NLM_F_REQUEST is set. + * - M, that indicates that NLM_F_MULTI is set. + * - A, that indicates that NLM_F_ACK is set. + * - E, that indicates that NLM_F_ECHO is set. + * + * The lack of one flag is displayed with '-'. On the other hand, the possible + * attribute flags available are: + * + * - N, that indicates that NLA_F_NESTED is set. + * - B, that indicates that NLA_F_NET_BYTEORDER is set. +*/ +func nlmsgFprintfHeader(fd io.Writer, nlh Header) { + fmt.Fprintf(fd, "----------------\t------------------\n") + fmt.Fprintf(fd, "| %010d |\t| message length |\n", nlh.Length) + fmt.Fprintf(fd, "| %05d | %s%s%s%s |\t| type | flags |\n", + nlh.Type, + ternary(nlh.Flags&Request != 0, "R", "-"), + ternary(nlh.Flags&Multi != 0, "M", "-"), + ternary(nlh.Flags&Acknowledge != 0, "A", "-"), + ternary(nlh.Flags&Echo != 0, "E", "-"), + ) + fmt.Fprintf(fd, "| %010d |\t| sequence number|\n", nlh.Sequence) + fmt.Fprintf(fd, "| %010d |\t| port ID |\n", nlh.PID) + fmt.Fprintf(fd, "----------------\t------------------\n") +} + +// nlmsgFprintf checks a single Message for netlink errors. +func nlmsgFprintf(fd io.Writer, m Message, colorize bool) { + var hasHeader bool + nlmsgFprintfHeader(fd, m.Header) + switch { + case m.Header.Type == Error: + hasHeader = true + case m.Header.Type == Done && m.Header.Flags&Multi != 0: + if len(m.Data) == 0 { + return + } + default: + // Neither, nothing to do. + } + + // Errno occupies 4 bytes. + const endErrno = 4 + if len(m.Data) < endErrno { + return + } + + c := nlenc.Int32(m.Data[:endErrno]) + if c != 0 { + b := m.Data[0:4] + fmt.Fprintf(fd, "| %.2x %.2x %.2x %.2x |\t", + 0xff&b[0], 0xff&b[1], + 0xff&b[2], 0xff&b[3]) + fmt.Fprintf(fd, "| extra header |\n") + } + + // Flags indicate an extended acknowledgement. The type/flags combination + // checked above determines the offset where the TLVs occur. + var off int + if hasHeader { + // There is an nlmsghdr preceding the TLVs. + if len(m.Data) < endErrno+nlmsgHeaderLen { + return + } + + // The TLVs should be at the offset indicated by the nlmsghdr.length, + // plus the offset where the header began. But make sure the calculated + // offset is still in-bounds. + h := *(*Header)(unsafe.Pointer(&m.Data[endErrno : endErrno+nlmsgHeaderLen][0])) + off = endErrno + int(h.Length) + + if len(m.Data) < off { + return + } + } else { + // There is no nlmsghdr preceding the TLVs, parse them directly. + off = endErrno + } + + data := m.Data[off:] + for i := 0; i < len(data); { + // Make sure there's at least a header's worth + // of data to read on each iteration. + if len(data[i:]) < nlaHeaderLen { + break + } + + // Extract the length of the attribute. + l := int(nlenc.Uint16(data[i : i+2])) + // extract the type + t := nlenc.Uint16(data[i+2 : i+4]) + // print attribute header + if colorize { + fmt.Fprintf(fd, "|\033[1;31m%05d|\033[1;32m%s%s|\033[1;34m%05d\033[0m|\t", + l, + ternary(t&syscall.NLA_F_NESTED != 0, "N", "-"), + ternary(t&syscall.NLA_F_NET_BYTEORDER != 0, "B", "-"), + t&attrTypeMask) + fmt.Fprintf(fd, "|len |flags| type|\n") + } else { + fmt.Fprintf(fd, "|%05d|%s%s|%05d|\t", + l, + ternary(t&syscall.NLA_F_NESTED != 0, "N", "-"), + ternary(t&syscall.NLA_F_NET_BYTEORDER != 0, "B", "-"), + t&attrTypeMask) + fmt.Fprintf(fd, "|len |flags| type|\n") + } + + nextAttr := i + nlaAlign(l) + + // advance the pointer to the bytes after the header + i += nlaHeaderLen + + // Ignore zero-length attributes. + if l == 0 { + continue + } + // If nested check the next attribute + if t&syscall.NLA_F_NESTED != 0 { + continue + } + + // Print the remaining attributes bytes + for ; i < nextAttr; i += 4 { + fmt.Fprintf(fd, "| %.2x %.2x %.2x %.2x |\t", + 0xff&data[i], 0xff&data[i+1], + 0xff&data[i+2], 0xff&data[i+3]) + + fmt.Fprintf(fd, "| data |") + + fmt.Fprintf(fd, "\t %s %s %s %s\n", + ternary(strconv.IsPrint(rune(data[i])), string(data[i]), " "), + ternary(strconv.IsPrint(rune(data[i+1])), string(data[i+1]), " "), + ternary(strconv.IsPrint(rune(data[i+2])), string(data[i+2]), " "), + ternary(strconv.IsPrint(rune(data[i+3])), string(data[i+3]), " "), + ) + } + } + fmt.Fprintf(fd, "----------------\t------------------\n") +} + +func ternary(cond bool, iftrue string, iffalse string) string { + if cond { + return iftrue + } else { + return iffalse + } +} diff --git a/debug_linux_test.go b/debug_linux_test.go new file mode 100644 index 00000000..cfb55d5e --- /dev/null +++ b/debug_linux_test.go @@ -0,0 +1,285 @@ +//go:build linux +// +build linux + +package netlink + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/sys/unix" +) + +func TestNlmsgFprintf(t *testing.T) { + // netlink messages obtained from https://github.com/google/nftables/blob/e99829fb4f26d75fdd0cfce8ba4632744e72c2bc/nftables_test.go#L245C1-L246C94 + tests := []struct { + name string + m Message + colorize bool + want string + }{ + { + name: "nft add table ip nat", + m: Message{ + Header: Header{ + Length: 40, + Type: HeaderType(uint16(unix.NFNL_SUBSYS_NFTABLES)<<8 | uint16(unix.NFT_MSG_NEWTABLE)), + Flags: Request, + Sequence: 1, + PID: 1234, + }, + Data: []byte("\x02\x00\x00\x00\x08\x00\x01\x00\x6e\x61\x74\x00\x08\x00\x02\x00\x00\x00\x00\x00"), + }, + colorize: false, + want: `---------------- ------------------ +| 0000000040 | | message length | +| 02560 | R--- | | type | flags | +| 0000000001 | | sequence number| +| 0000001234 | | port ID | +---------------- ------------------ +| 02 00 00 00 | | extra header | +|00008|--|00001| |len |flags| type| +| 6e 61 74 00 | | data | n a t +|00008|--|00002| |len |flags| type| +| 00 00 00 00 | | data | +---------------- ------------------ +`, + }, + { + name: "nft add rule nat prerouting iifname uplink0 udp dport 4070-4090 dnat 192.168.23.2:4070-4090", + m: Message{ + Header: Header{ + Length: 40, + Type: HeaderType(uint16(unix.NFNL_SUBSYS_NFTABLES)<<8 | uint16(unix.NFT_MSG_NEWRULE)), + Flags: Request, + Sequence: 1, + PID: 1234, + }, + Data: []byte("\x02\x00\x00\x00\x08\x00\x01\x00\x6e\x61\x74\x00\x0f\x00\x02\x00\x70\x72\x65\x72\x6f\x75\x74\x69\x6e\x67\x00\x00\xf8\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x38\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x18\x00\x03\x80\x14\x00\x01\x00\x75\x70\x6c\x69\x6e\x6b\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x10\x08\x00\x01\x00\x00\x00\x00\x01\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x05\x00\x01\x00\x11\x00\x00\x00\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x02\x08\x00\x03\x00\x00\x00\x00\x02\x08\x00\x04\x00\x00\x00\x00\x02\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x05\x0c\x00\x03\x80\x06\x00\x01\x00\x0f\xe6\x00\x00\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x03\x0c\x00\x03\x80\x06\x00\x01\x00\x0f\xfa\x00\x00\x2c\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x18\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x0c\x00\x02\x80\x08\x00\x01\x00\xc0\xa8\x17\x02\x2c\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x18\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x02\x0c\x00\x02\x80\x06\x00\x01\x00\x0f\xe6\x00\x00\x2c\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x18\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x03\x0c\x00\x02\x80\x06\x00\x01\x00\x0f\xfa\x00\x00\x38\x00\x01\x80\x08\x00\x01\x00\x6e\x61\x74\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x02\x08\x00\x03\x00\x00\x00\x00\x01\x08\x00\x05\x00\x00\x00\x00\x02\x08\x00\x06\x00\x00\x00\x00\x03"), + }, + colorize: false, + want: `---------------- ------------------ +| 0000000040 | | message length | +| 02566 | R--- | | type | flags | +| 0000000001 | | sequence number| +| 0000001234 | | port ID | +---------------- ------------------ +| 02 00 00 00 | | extra header | +|00008|--|00001| |len |flags| type| +| 6e 61 74 00 | | data | n a t +|00015|--|00002| |len |flags| type| +| 70 72 65 72 | | data | p r e r +| 6f 75 74 69 | | data | o u t i +| 6e 67 00 00 | | data | n g +|00504|N-|00004| |len |flags| type| +|00036|N-|00001| |len |flags| type| +|00009|--|00001| |len |flags| type| +| 6d 65 74 61 | | data | m e t a +| 00 00 00 00 | | data | +|00020|N-|00002| |len |flags| type| +|00008|--|00002| |len |flags| type| +| 00 00 00 06 | | data | +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00056|N-|00001| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 63 6d 70 00 | | data | c m p +|00044|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00008|--|00002| |len |flags| type| +| 00 00 00 00 | | data | +|00024|N-|00003| |len |flags| type| +|00020|--|00001| |len |flags| type| +| 75 70 6c 69 | | data | u p l i +| 6e 6b 30 00 | | data | n k 0 +| 00 00 00 00 | | data | +| 00 00 00 00 | | data | +|00036|N-|00001| |len |flags| type| +|00009|--|00001| |len |flags| type| +| 6d 65 74 61 | | data | m e t a +| 00 00 00 00 | | data | +|00020|N-|00002| |len |flags| type| +|00008|--|00002| |len |flags| type| +| 00 00 00 10 | | data | +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00044|N-|00001| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 63 6d 70 00 | | data | c m p +|00032|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00008|--|00002| |len |flags| type| +| 00 00 00 00 | | data | +|00012|N-|00003| |len |flags| type| +|00005|--|00001| |len |flags| type| +| 11 00 00 00 | | data | +|00052|N-|00001| |len |flags| type| +|00012|--|00001| |len |flags| type| +| 70 61 79 6c | | data | p a y l +| 6f 61 64 00 | | data | o a d +|00036|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00008|--|00002| |len |flags| type| +| 00 00 00 02 | | data | +|00008|--|00003| |len |flags| type| +| 00 00 00 02 | | data | +|00008|--|00004| |len |flags| type| +| 00 00 00 02 | | data | +|00044|N-|00001| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 63 6d 70 00 | | data | c m p +|00032|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00008|--|00002| |len |flags| type| +| 00 00 00 05 | | data | +|00012|N-|00003| |len |flags| type| +|00006|--|00001| |len |flags| type| +| 0f e6 00 00 | | data | æ +|00044|N-|00001| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 63 6d 70 00 | | data | c m p +|00032|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00008|--|00002| |len |flags| type| +| 00 00 00 03 | | data | +|00012|N-|00003| |len |flags| type| +|00006|--|00001| |len |flags| type| +| 0f fa 00 00 | | data | ú +|00044|N-|00001| |len |flags| type| +|00014|--|00001| |len |flags| type| +| 69 6d 6d 65 | | data | i m m e +| 64 69 61 74 | | data | d i a t +| 65 00 00 00 | | data | e +|00024|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00012|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| c0 a8 17 02 | | data | À ¨ +|00044|N-|00001| |len |flags| type| +|00014|--|00001| |len |flags| type| +| 69 6d 6d 65 | | data | i m m e +| 64 69 61 74 | | data | d i a t +| 65 00 00 00 | | data | e +|00024|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 02 | | data | +|00012|N-|00002| |len |flags| type| +|00006|--|00001| |len |flags| type| +| 0f e6 00 00 | | data | æ +|00044|N-|00001| |len |flags| type| +|00014|--|00001| |len |flags| type| +| 69 6d 6d 65 | | data | i m m e +| 64 69 61 74 | | data | d i a t +| 65 00 00 00 | | data | e +|00024|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 03 | | data | +|00012|N-|00002| |len |flags| type| +|00006|--|00001| |len |flags| type| +| 0f fa 00 00 | | data | ú +|00056|N-|00001| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 6e 61 74 00 | | data | n a t +|00044|N-|00002| |len |flags| type| +|00008|--|00001| |len |flags| type| +| 00 00 00 01 | | data | +|00008|--|00002| |len |flags| type| +| 00 00 00 02 | | data | +|00008|--|00003| |len |flags| type| +| 00 00 00 01 | | data | +|00008|--|00005| |len |flags| type| +| 00 00 00 02 | | data | +|00008|--|00006| |len |flags| type| +| 00 00 00 03 | | data | +---------------- ------------------ +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + nlmsgFprintf(w, tt.m, tt.colorize) + got := w.String() + if got != tt.want { + t.Errorf("nlmsgFprintf() =\n%s,\nwant\n%s\ndiff:\n%s", got, tt.want, cmp.Diff(got, tt.want)) + } + }) + } +} + +func TestNlmsgFprintfHeader(t *testing.T) { + tests := []struct { + name string + h Header + want string + }{ + { + name: "Basic test", + h: Header{ + Length: 16 + 4, + Type: 0, + Flags: Request, + Sequence: 1, + PID: 123, + }, + want: `---------------- ------------------ +| 0000000020 | | message length | +| 00000 | R--- | | type | flags | +| 0000000001 | | sequence number| +| 0000000123 | | port ID | +---------------- ------------------ +`, + }, + { + name: "All flags", + h: Header{ + Length: 16, + Type: unix.NFNL_SUBSYS_IPSET << 8, + Flags: Request | Multi | Acknowledge | Echo | Dump | DumpFiltered | Create | Excl | Append, + Sequence: 123, + PID: 456, + }, + want: `---------------- ------------------ +| 0000000016 | | message length | +| 01536 | RMAE | | type | flags | +| 0000000123 | | sequence number| +| 0000000456 | | port ID | +---------------- ------------------ +`, + }, + { + name: "Unknown type", + h: Header{ + Length: 16, + Type: 0xffff, + Flags: Request | Acknowledge, + Sequence: 123, + PID: 456, + }, + want: `---------------- ------------------ +| 0000000016 | | message length | +| 65535 | R-A- | | type | flags | +| 0000000123 | | sequence number| +| 0000000456 | | port ID | +---------------- ------------------ +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + nlmsgFprintfHeader(w, tt.h) + if got := w.String(); got != tt.want { + t.Errorf("nlmsgFprintfHeader() =\n%s,\nwant\n%s\ndiff:\n%s", got, tt.want, cmp.Diff(got, tt.want)) + } + }) + } +} diff --git a/debug_others.go b/debug_others.go new file mode 100644 index 00000000..88804981 --- /dev/null +++ b/debug_others.go @@ -0,0 +1,45 @@ +//go:build !linux + +package netlink + +import ( + "log" + "os" + "strconv" + "strings" +) + +// newDebugger creates a debugger by parsing key=value arguments. +func newDebugger(args []string) *debugger { + d := &debugger{ + Log: log.New(os.Stderr, "nl: ", 0), + Level: 1, + } + for _, a := range args { + kv := strings.Split(a, "=") + if len(kv) != 2 { + // Ignore malformed pairs and assume callers wants defaults. + continue + } + switch kv[0] { + // Select the log level for the debugger. + case "level": + level, err := strconv.Atoi(kv[1]) + if err != nil { + panicf("netlink: invalid NLDEBUG level: %q", a) + } + + d.Level = level + } + } + + return d +} + +// debugf prints debugging information at the specified level, if d.Level is +// high enough to print the message. +func (d *debugger) debugf(level int, format string, v ...interface{}) { + if d.Level >= level { + d.Log.Printf(format, v...) + } +} diff --git a/doc.go b/doc.go index 9828be85..f0c51c7b 100644 --- a/doc.go +++ b/doc.go @@ -32,5 +32,5 @@ // Available key/value debugger options include: // // level=N: specify the debugging level (only "1" is currently supported) -// format=mnl: specify the same format used by libmnl (nftables --debug=all) +// format=mnl: specify the same format used by libmnl (nft --debug=all) package netlink