diff --git a/.github/workflows/linux-integration-test.yml b/.github/workflows/linux-integration-test.yml index b29e7f1..e0a7095 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 aa7f76b..9fe0b11 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 c305b13..8838130 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 319eeea..d788c60 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 diff --git a/debug.go b/debug.go index d39d66c..62cb03e 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,47 +28,188 @@ 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 +} + +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") } -// 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, +// 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. } - for _, a := range args { - kv := strings.Split(a, "=") - if len(kv) != 2 { - // Ignore malformed pairs and assume callers wants defaults. - continue + // 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 } - 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) - } + // 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) - d.Level = level + if len(m.Data) < off { + return } + } else { + // There is no nlmsghdr preceding the TLVs, parse them directly. + off = endErrno } - return d -} + 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 |") -// 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...) + 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 panicf(format string, a ...interface{}) { - panic(fmt.Sprintf(format, a...)) +func ternary(cond bool, iftrue string, iffalse string) string { + if cond { + return iftrue + } else { + return iffalse + } } diff --git a/debug_linux.go b/debug_linux.go new file mode 100644 index 0000000..2937008 --- /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 0000000..cfb55d5 --- /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 0000000..8880498 --- /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 98c744a..f0c51c7 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 (nft --debug=all) package netlink