diff --git a/conn_linux_integration_test.go b/conn_linux_integration_test.go index 2b69b83..d0a0456 100644 --- a/conn_linux_integration_test.go +++ b/conn_linux_integration_test.go @@ -13,6 +13,7 @@ import ( "os/exec" "os/user" "slices" + "strings" "sync" "testing" "time" @@ -1528,6 +1529,69 @@ func TestIntegrationConntrackGetMultiReplyFollowedByAcknowledgement(t *testing.T } } +func TestIntegrationConnExtendedAcknowledgeCapped(t *testing.T) { + for _, capped := range []bool{false, true} { + t.Run(fmt.Sprintf("capped %t", capped), func(t *testing.T) { + c, err := netlink.Dial(unix.NETLINK_ROUTE, nil) + if err != nil { + t.Fatalf("failed to dial netlink: %v", err) + } + defer c.Close() + + if err := c.SetOption(netlink.ExtendedAcknowledge, true); err != nil { + if errors.Is(err, unix.ENOPROTOOPT) { + t.Skipf("skipping, extended acknowledge not supported by this kernel: %v", err) + } + + t.Fatalf("failed to set extended acknowledge: %v", err) + } + + if err := c.SetOption(netlink.CapAcknowledge, capped); err != nil { + t.Fatalf("failed to set cap acknowledge: %v", err) + } + + // Send a RTM_GETLINK request with an IFLA_IFNAME attribute longer + // than IFNAMSIZ to trigger an ERANGE error with an extended + // acknowledgement message. + attrs, err := netlink.MarshalAttributes([]netlink.Attribute{{ + Type: unix.IFLA_IFNAME, + Data: []byte(strings.Repeat("x", 2*unix.IFNAMSIZ) + "\x00"), + }}) + if err != nil { + t.Fatalf("failed to marshal attributes: %v", err) + } + + ifi := make([]byte, unix.SizeofIfInfomsg) + + _, err = c.Execute(netlink.Message{ + Header: netlink.Header{ + Type: unix.RTM_GETLINK, + Flags: netlink.Request, + }, + Data: append(ifi, attrs...), + }) + + var oerr *netlink.OpError + if !errors.As(err, &oerr) { + t.Fatalf("expected OpError, but got: %v", err) + } + if !errors.Is(oerr, unix.ERANGE) { + t.Fatalf("expected ERANGE, but got: %v", oerr) + } + if oerr.Message == "" { + t.Fatalf("expected extended acknowledgement message, but got: %v", oerr) + } + + // Where the invalid attribute starts within the sent request + ifnameOffset := unix.SizeofNlMsghdr + len(ifi) + if oerr.Offset != ifnameOffset { + t.Fatalf("expected offset %d (IFLA_IFNAME), but got: %d", + ifnameOffset, oerr.Offset) + } + }) + } +} + func mustBeTimeoutNetError(t *testing.T, err error) { t.Helper() diff --git a/debug_linux.go b/debug_linux.go index bb6d041..3c1bb44 100644 --- a/debug_linux.go +++ b/debug_linux.go @@ -119,13 +119,19 @@ func nlmsgFprintf(fd io.Writer, m Message, colorize bool) { 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 + if m.Header.Flags&Capped != 0 { + // Only the request's nlmsghdr was echoed back, so the TLVs + // immediately follow the header. + off = endErrno + nlmsgHeaderLen + } else { + // 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. diff --git a/message.go b/message.go index bc74d31..c9657af 100644 --- a/message.go +++ b/message.go @@ -323,8 +323,6 @@ func checkMessage(m Message) error { Sequence: m.Header.Sequence, } - // TODO(mdlayher): investigate the Capped flag. - if m.Header.Flags&AcknowledgeTLVs == 0 { // No extended acknowledgement. return oerr @@ -339,14 +337,21 @@ func checkMessage(m Message) error { return newOpError("receive", errShortErrorMessage) } - // 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 newOpError("receive", errShortErrorMessage) + if m.Header.Flags&Capped != 0 { + // Only the request's nlmsghdr was echoed back, but its length + // still reflects the full request, so the TLVs immediately follow + // the header. + off = endErrno + nlmsgHeaderLen + } else { + // 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 newOpError("receive", errShortErrorMessage) + } } } else { // There is no nlmsghdr preceding the TLVs, parse them directly. diff --git a/message_linux_test.go b/message_linux_test.go index 6058291..cfae9ce 100644 --- a/message_linux_test.go +++ b/message_linux_test.go @@ -79,6 +79,7 @@ func Test_checkMessageExtendedAcknowledgementTLVs(t *testing.T) { }, Data: []byte{0xff, 0xff, 0xff, 0xff}, }, + false, // The actual extended acknowledgement TLVs. []Attribute{ { @@ -100,6 +101,46 @@ func Test_checkMessageExtendedAcknowledgementTLVs(t *testing.T) { Sequence: 3258842681, }, }, + { + name: "error capped", + m: Message{ + Header: Header{ + Type: Error, + // The kernel only echoes the request's header, but leaves + // the header's length set to the full request length. + Flags: Capped | AcknowledgeTLVs, + Sequence: 3258842681, + }, + Data: packExtACK( + -1, + // A large request, of which only the header is echoed. + &Message{ + Header: Header{ + Sequence: 3258842681, + }, + Data: make([]byte, 1024), + }, + true, + []Attribute{ + { + Type: 1, + Data: nlenc.Bytes("bad request"), + }, + { + Type: 2, + Data: nlenc.Uint32Bytes(2), + }, + }, + ), + }, + err: &OpError{ + Op: "receive", + Err: unix.Errno(1), + Message: "bad request", + Offset: 2, + Sequence: 3258842681, + }, + }, { name: "done multi", m: Message{ @@ -113,6 +154,7 @@ func Test_checkMessageExtendedAcknowledgementTLVs(t *testing.T) { -1, // No message, straight to TLVs. nil, + false, []Attribute{ { Type: 1, @@ -144,8 +186,9 @@ func Test_checkMessageExtendedAcknowledgementTLVs(t *testing.T) { } } -// packExtACK packs an extended acknowledgement response. -func packExtACK(errno int32, m *Message, tlvs []Attribute) []byte { +// packExtACK packs an extended acknowledgement response. If capped is set, only +// the header of m is echoed back, as the kernel does with NETLINK_CAP_ACK. +func packExtACK(errno int32, m *Message, capped bool, tlvs []Attribute) []byte { b := nlenc.Int32Bytes(errno) if m != nil { @@ -156,6 +199,10 @@ func packExtACK(errno int32, m *Message, tlvs []Attribute) []byte { panicf("failed to marshal message: %v", err) } + if capped { + mb = mb[:nlmsgHeaderLen] + } + b = append(b, mb...) }