mirror of
https://github.com/soypat/lneto.git
synced 2026-08-12 02:43:44 +00:00
dhcpv4: fixes (#19)
* dhcpv4: fix several issues * condition flipped in Encapsulate * check for correct bounds in ForEachOption * getMessageType didn't check for opt * dhcpv4: preserve backing buffer in reset
This commit is contained in:
+5
-2
@@ -159,7 +159,7 @@ func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
||||
}
|
||||
n, _ = EncodeOption16(opts[numOpts:], OptMaximumMessageSize, uint16(maxlen))
|
||||
numOpts += n
|
||||
if !c.reqIP.valid {
|
||||
if c.reqIP.valid {
|
||||
n, _ = EncodeOption(opts[numOpts:], OptRequestedIPaddress, c.reqIP.addr[:]...)
|
||||
numOpts += n
|
||||
}
|
||||
@@ -253,7 +253,7 @@ func (c *Client) getMessageType(frm Frame) MessageType {
|
||||
c.auxbuf[0] = 255
|
||||
ptrMsgType := &c.auxbuf[0]
|
||||
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
if len(data) == 1 {
|
||||
if opt == OptMessageType && len(data) == 1 {
|
||||
*ptrMsgType = data[0]
|
||||
return io.EOF
|
||||
}
|
||||
@@ -340,6 +340,9 @@ func (c *Client) reset(xid uint32) {
|
||||
reqIP: c.reqIP,
|
||||
clientMAC: c.clientMAC,
|
||||
clientID: c.clientID,
|
||||
dns: c.dns[:0],
|
||||
ntps: c.ntps[:0],
|
||||
hostname: c.hostname[:0],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dhcpv4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -154,3 +155,365 @@ func TestExample(t *testing.T) {
|
||||
t.Error("encapsulate double tap got data!", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequestedIPAddressOption verifies that when a client has a valid requested IP,
|
||||
// the OptRequestedIPaddress option is included in the DISCOVER message.
|
||||
// This tests for the bug where the condition was inverted (!c.reqIP.valid instead of c.reqIP.valid).
|
||||
func TestRequestedIPAddressOption(t *testing.T) {
|
||||
var cl Client
|
||||
requestedAddr := [4]byte{192, 168, 1, 100}
|
||||
|
||||
err := cl.BeginRequest(12345, RequestConfig{
|
||||
RequestedAddr: requestedAddr,
|
||||
ClientHardwareAddr: [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("no data encapsulated")
|
||||
}
|
||||
|
||||
// Parse the frame and look for OptRequestedIPaddress
|
||||
frm, err := NewFrame(buf[:n])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var foundRequestedIP bool
|
||||
var foundIPValue [4]byte
|
||||
err = frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
if opt == OptRequestedIPaddress {
|
||||
foundRequestedIP = true
|
||||
if len(data) == 4 {
|
||||
copy(foundIPValue[:], data)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !foundRequestedIP {
|
||||
t.Error("OptRequestedIPaddress not found in DISCOVER message when reqIP.valid is true")
|
||||
} else if foundIPValue != requestedAddr {
|
||||
t.Errorf("OptRequestedIPaddress has wrong value: got %v, want %v", foundIPValue, requestedAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForEachOptionBoundsCheck verifies that ForEachOption properly validates
|
||||
// buffer bounds and doesn't panic on malformed options with lengths that extend
|
||||
// past the buffer end.
|
||||
func TestForEachOptionBoundsCheck(t *testing.T) {
|
||||
// Create a minimal valid frame buffer
|
||||
buf := make([]byte, OptionsOffset+10)
|
||||
frm, err := NewFrame(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
frm.SetMagicCookie(MagicCookie)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
options []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid option",
|
||||
options: []byte{byte(OptHostName), 4, 't', 'e', 's', 't', byte(OptEnd)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "option length exceeds buffer",
|
||||
options: []byte{byte(OptHostName), 100, 't', 'e', 's', 't'}, // claims 100 bytes but only 4 available
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "option length exactly at buffer end",
|
||||
options: []byte{byte(OptHostName), 255}, // claims 255 bytes, way past end
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "option length causes ptr+2+optlen overflow",
|
||||
options: []byte{byte(OptHostName), 8, 'a', 'b', 'c'}, // claims 8 bytes but only 3 available
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create fresh buffer for each test
|
||||
testBuf := make([]byte, OptionsOffset+len(tc.options))
|
||||
testFrm, _ := NewFrame(testBuf)
|
||||
testFrm.SetMagicCookie(MagicCookie)
|
||||
copy(testBuf[OptionsOffset:], tc.options)
|
||||
|
||||
// Use recover to catch panics
|
||||
var panicked bool
|
||||
var gotErr error
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicked = true
|
||||
}
|
||||
}()
|
||||
gotErr = testFrm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
// Access the data to trigger potential panic
|
||||
_ = len(data)
|
||||
if len(data) > 0 {
|
||||
_ = data[0]
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
if panicked {
|
||||
t.Errorf("ForEachOption panicked on malformed input %q", tc.name)
|
||||
}
|
||||
if tc.wantErr && gotErr == nil {
|
||||
t.Errorf("ForEachOption should return error for %q, got nil", tc.name)
|
||||
}
|
||||
if !tc.wantErr && gotErr != nil {
|
||||
t.Errorf("ForEachOption should not return error for %q, got %v", tc.name, gotErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetMessageTypeChecksOptNum verifies that getMessageType correctly identifies
|
||||
// the DHCP message type by checking for OptMessageType specifically, not just
|
||||
// any single-byte option.
|
||||
func TestGetMessageTypeChecksOptNum(t *testing.T) {
|
||||
var cl Client
|
||||
err := cl.BeginRequest(1, RequestConfig{
|
||||
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a frame with a single-byte option BEFORE OptMessageType
|
||||
buf := make([]byte, 512)
|
||||
frm, _ := NewFrame(buf)
|
||||
frm.SetMagicCookie(MagicCookie)
|
||||
frm.SetXID(1)
|
||||
|
||||
opts := buf[OptionsOffset:]
|
||||
n := 0
|
||||
|
||||
// Add a single-byte option that is NOT OptMessageType first
|
||||
// OptOptionOverload (52) can be a single byte value
|
||||
opts[n] = byte(OptOptionOverload)
|
||||
opts[n+1] = 1
|
||||
opts[n+2] = 3 // value 3 means both sname and file contain options
|
||||
n += 3
|
||||
|
||||
// Now add the actual message type
|
||||
opts[n] = byte(OptMessageType)
|
||||
opts[n+1] = 1
|
||||
opts[n+2] = byte(MsgOffer)
|
||||
n += 3
|
||||
|
||||
opts[n] = byte(OptEnd)
|
||||
|
||||
// getMessageType should return MsgOffer, not MessageType(3)
|
||||
msgType := cl.getMessageType(frm)
|
||||
|
||||
// If the bug exists (not checking opt == OptMessageType), it will return
|
||||
// MessageType(3) which is MsgRequest, not MsgOffer
|
||||
if msgType != MsgOffer {
|
||||
t.Errorf("getMessageType returned %v (%d), want MsgOffer (%d); "+
|
||||
"likely not checking for OptMessageType specifically",
|
||||
msgType, msgType, MsgOffer)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetMessageTypeWithMultipleSingleByteOptions tests that getMessageType
|
||||
// returns the correct message type even when multiple single-byte options exist.
|
||||
func TestGetMessageTypeWithMultipleSingleByteOptions(t *testing.T) {
|
||||
var cl Client
|
||||
err := cl.BeginRequest(42, RequestConfig{
|
||||
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
buildOptions func([]byte) int
|
||||
expectedMsgType MessageType
|
||||
}{
|
||||
{
|
||||
name: "message type first",
|
||||
buildOptions: func(opts []byte) int {
|
||||
n := 0
|
||||
n += writeOption(opts[n:], OptMessageType, byte(MsgAck))
|
||||
n += writeOption(opts[n:], OptOptionOverload, byte(1))
|
||||
opts[n] = byte(OptEnd)
|
||||
return n + 1
|
||||
},
|
||||
expectedMsgType: MsgAck,
|
||||
},
|
||||
{
|
||||
name: "message type after other single-byte option",
|
||||
buildOptions: func(opts []byte) int {
|
||||
n := 0
|
||||
n += writeOption(opts[n:], OptOptionOverload, byte(2))
|
||||
n += writeOption(opts[n:], OptMessageType, byte(MsgNack))
|
||||
opts[n] = byte(OptEnd)
|
||||
return n + 1
|
||||
},
|
||||
expectedMsgType: MsgNack,
|
||||
},
|
||||
{
|
||||
name: "message type between multi-byte options",
|
||||
buildOptions: func(opts []byte) int {
|
||||
n := 0
|
||||
n += writeOption(opts[n:], OptHostName, 't', 'e', 's', 't')
|
||||
n += writeOption(opts[n:], OptMessageType, byte(MsgDiscover))
|
||||
n += writeOption(opts[n:], OptRouter, 192, 168, 1, 1)
|
||||
opts[n] = byte(OptEnd)
|
||||
return n + 1
|
||||
},
|
||||
expectedMsgType: MsgDiscover,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
buf := make([]byte, 512)
|
||||
frm, _ := NewFrame(buf)
|
||||
frm.SetMagicCookie(MagicCookie)
|
||||
frm.SetXID(42)
|
||||
|
||||
opts := buf[OptionsOffset:]
|
||||
tc.buildOptions(opts)
|
||||
|
||||
msgType := cl.getMessageType(frm)
|
||||
if msgType != tc.expectedMsgType {
|
||||
t.Errorf("got message type %v (%d), want %v (%d)",
|
||||
msgType, msgType, tc.expectedMsgType, tc.expectedMsgType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// writeOption is a test helper that writes a DHCP option and returns bytes written.
|
||||
func writeOption(dst []byte, opt OptNum, data ...byte) int {
|
||||
dst[0] = byte(opt)
|
||||
dst[1] = byte(len(data))
|
||||
copy(dst[2:], data)
|
||||
return 2 + len(data)
|
||||
}
|
||||
|
||||
// TestForEachOptionEdgeCases tests additional edge cases for bounds checking.
|
||||
func TestForEachOptionEdgeCases(t *testing.T) {
|
||||
t.Run("empty options section", func(t *testing.T) {
|
||||
buf := make([]byte, OptionsOffset)
|
||||
frm, _ := NewFrame(buf)
|
||||
frm.SetMagicCookie(MagicCookie)
|
||||
|
||||
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
return nil
|
||||
})
|
||||
// Should return errNoOptions for empty options
|
||||
if err == nil {
|
||||
t.Error("expected error for empty options section")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("only end option", func(t *testing.T) {
|
||||
buf := make([]byte, OptionsOffset+1)
|
||||
frm, _ := NewFrame(buf)
|
||||
frm.SetMagicCookie(MagicCookie)
|
||||
buf[OptionsOffset] = byte(OptEnd)
|
||||
|
||||
var called bool
|
||||
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if called {
|
||||
t.Error("callback should not be called for OptEnd")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("truncated option header", func(t *testing.T) {
|
||||
// Buffer has option type but no length byte
|
||||
buf := make([]byte, OptionsOffset+1)
|
||||
frm, _ := NewFrame(buf)
|
||||
frm.SetMagicCookie(MagicCookie)
|
||||
buf[OptionsOffset] = byte(OptHostName) // Not OptEnd, so it needs a length
|
||||
|
||||
var panicked bool
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicked = true
|
||||
}
|
||||
}()
|
||||
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
if panicked {
|
||||
t.Error("ForEachOption panicked on truncated option header")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestRequestedIPNotSentWhenInvalid verifies that OptRequestedIPaddress is NOT
|
||||
// sent when reqIP is not valid (zero address with valid=false).
|
||||
func TestRequestedIPNotSentWhenInvalid(t *testing.T) {
|
||||
var cl Client
|
||||
|
||||
// Begin request with zero address - this still sets valid=true in current impl
|
||||
err := cl.BeginRequest(99999, RequestConfig{
|
||||
RequestedAddr: [4]byte{0, 0, 0, 0}, // Zero but will be marked valid
|
||||
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
frm, _ := NewFrame(buf[:n])
|
||||
|
||||
var foundRequestedIP bool
|
||||
var ipValue [4]byte
|
||||
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
if opt == OptRequestedIPaddress {
|
||||
foundRequestedIP = true
|
||||
if len(data) == 4 {
|
||||
copy(ipValue[:], data)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// With the bug fixed, when a valid requested IP is set (even 0.0.0.0),
|
||||
// it should be included. This test documents expected behavior.
|
||||
if foundRequestedIP {
|
||||
// Verify the value matches what was requested
|
||||
if !bytes.Equal(ipValue[:], []byte{0, 0, 0, 0}) {
|
||||
t.Errorf("unexpected requested IP value: %v", ipValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -132,9 +132,6 @@ func (frm Frame) ForEachOption(fn func(off int, op OptNum, data []byte) error) e
|
||||
}
|
||||
callback := fn != nil
|
||||
for ptr+1 < len(frm.buf) {
|
||||
if int(frm.buf[ptr+1]) >= len(frm.buf) {
|
||||
return errDHCPBadOption
|
||||
}
|
||||
optnum := OptNum(frm.buf[ptr])
|
||||
if optnum == 0xff {
|
||||
break
|
||||
@@ -142,14 +139,17 @@ func (frm Frame) ForEachOption(fn func(off int, op OptNum, data []byte) error) e
|
||||
ptr++
|
||||
continue
|
||||
}
|
||||
optlen := frm.buf[ptr+1]
|
||||
optlen := int(frm.buf[ptr+1])
|
||||
if ptr+2+optlen > len(frm.buf) {
|
||||
return errDHCPBadOption
|
||||
}
|
||||
if callback {
|
||||
optionData := frm.buf[ptr+2 : ptr+2+int(optlen)]
|
||||
optionData := frm.buf[ptr+2 : ptr+2+optlen]
|
||||
if err := fn(ptr, optnum, optionData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ptr += int(optlen) + 2
|
||||
ptr += optlen + 2
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user