From 9c0e250cf62b6f46ad7deb74177a428bbd274146 Mon Sep 17 00:00:00 2001 From: soypat Date: Fri, 4 Jul 2025 00:26:25 -0300 Subject: [PATCH 1/2] switch to encoding DHCP options instead of appending --- dhcpv4/client.go | 50 +++++++++++++++++++++------------- dhcpv4/definitions.go | 24 ++++++++++------- dhcpv4/dhcp_test.go | 62 +++++++++++++++++++++++++++++++++++++++++++ dhcpv4/server.go | 32 +++++++++++++++++----- 4 files changed, 132 insertions(+), 36 deletions(-) diff --git a/dhcpv4/client.go b/dhcpv4/client.go index c544ddf..26656be 100644 --- a/dhcpv4/client.go +++ b/dhcpv4/client.go @@ -42,6 +42,7 @@ type RequestConfig struct { ClientHardwareAddr [6]byte // Optional hostname to request. Hostname string + ClientID string } func (c *Client) BeginRequest(xid uint32, cfg RequestConfig) error { @@ -108,24 +109,29 @@ func (c *Client) Encapsulate(carrierFrame []byte, frameOffset int) (int, error) if err != nil { return 0, err } + opts := frm.OptionsPayload() + if len(opts) < 255 { + return 0, errors.New("too short packet for options") + } - // var options []Option - // var nextState ClientState - optBuf := c.auxbuf[:0] var nextState ClientState + var numOpts int switch c.state { case StateInit: // Send out discover. - optBuf = AppendOption(optBuf, OptMessageType, byte(MsgDiscover)) - optBuf = AppendOption(optBuf, OptParameterRequestList, defaultParamReqList...) - optBuf = AppendOption(optBuf, OptClientIdentifier, c.clientMAC[:]...) + n, _ := EncodeOption(opts[numOpts:], OptMessageType, byte(MsgDiscover)) + numOpts += n + n, _ = EncodeOption(opts[numOpts:], OptParameterRequestList, defaultParamReqList...) + numOpts += n maxlen := len(dst) if maxlen > math.MaxUint16 { maxlen = math.MaxUint16 } - optBuf = AppendOption(optBuf, OptMaximumMessageSize, byte(maxlen>>8), byte(maxlen)) + n, _ = EncodeOption16(opts[numOpts:], OptMaximumMessageSize, uint16(maxlen)) + numOpts += n if c.reqIP != [4]byte{} { - optBuf = AppendOption(optBuf, OptRequestedIPaddress, c.reqIP[:]...) + n, _ = EncodeOption(opts[numOpts:], OptRequestedIPaddress, c.reqIP[:]...) + numOpts += n } nextState = StateSelecting @@ -134,27 +140,33 @@ func (c *Client) Encapsulate(carrierFrame []byte, frameOffset int) (int, error) return 0, nil // Offer not yet received. } // Send out request, we know we've received an offer by now. - optBuf = AppendOption(optBuf, OptMessageType, byte(MsgRequest)) - optBuf = AppendOption(optBuf, OptRequestedIPaddress, c.offer[:]...) - optBuf = AppendOption(optBuf, OptServerIdentification, c.svip[:]...) + n, _ := EncodeOption(opts[numOpts:], OptMessageType, byte(MsgRequest)) + numOpts += n + n, _ = EncodeOption(opts[numOpts:], OptRequestedIPaddress, c.offer[:]...) + numOpts += n + n, _ = EncodeOption(opts[numOpts:], OptServerIdentification, c.svip[:]...) + numOpts += n nextState = StateRequesting default: return 0, errors.New("unhandled state") } + n, _ := EncodeOption(opts[numOpts:], OptClientIdentifier, c.clientMAC[:]...) + numOpts += n if len(c.reqHostname) > 0 { - optBuf = AppendOptionString(optBuf, OptHostName, c.reqHostname) - } - optBuf = append(optBuf, 0xff) // End mark. - options := frm.OptionsPayload() - if len(optBuf) > len(options) { - return 0, errors.New("DHCPv4 short buffer for options") + n, err := EncodeOptionString(opts[numOpts:], OptHostName, c.reqHostname) + numOpts += n + if err != nil { + return 0, err + } } + + opts[numOpts] = byte(OptEnd) + numOpts++ c.setHeader(frm) - n := copy(options, optBuf) c.setIP(carrierFrame, frameOffset) c.state = nextState - return optionsOffset + n, nil + return optionsOffset + numOpts, nil } func (c *Client) Demux(carrierData []byte, frameOffset int) error { diff --git a/dhcpv4/definitions.go b/dhcpv4/definitions.go index c7c239d..e470a09 100644 --- a/dhcpv4/definitions.go +++ b/dhcpv4/definitions.go @@ -35,18 +35,19 @@ func (state ClientState) HasIP() bool { return state == StateBound || state == StateRenewing || state == StateRebinding } -func AppendOption(dst []byte, opt OptNum, data ...byte) []byte { - if len(data) > 255 { - panic("option data too long") - } - dst = append(dst, byte(opt), byte(len(data))) - dst = append(dst, data...) - return dst +func EncodeOptionString(dst []byte, opt OptNum, data string) (int, error) { + bdata := unsafe.Slice(unsafe.StringData(data), len(data)) + return EncodeOption(dst, opt, bdata...) } -func AppendOptionString(dst []byte, opt OptNum, data string) []byte { - bdata := unsafe.Slice(unsafe.StringData(data), len(data)) - return AppendOption(dst, opt, bdata...) +func EncodeOption16(dst []byte, opt OptNum, v uint16) (int, error) { + // See binary.BigEndian.PutUint16() + return EncodeOption(dst, opt, byte(v>>8), byte(v)) +} + +func EncodeOption32(dst []byte, opt OptNum, v uint32) (int, error) { + // See binary.BigEndian.PutUint32() + return EncodeOption(dst, opt, byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) } func EncodeOption(dst []byte, opt OptNum, data ...byte) (int, error) { @@ -66,6 +67,8 @@ type OptNum uint8 // DHCP options. Taken from https://help.sonicwall.com/help/sw/eng/6800/26/2/3/content/Network_DHCP_Server.042.12.htm. const ( + OptEnd OptNum = 255 // end options + OptWordAligned OptNum = 0 // word-aligned OptSubnetMask OptNum = 1 // subnet mask OptTimeOffset OptNum = 2 // Time offset in seconds from UTC @@ -128,6 +131,7 @@ const ( OptRebindingTimeValue OptNum = 59 // DHCP rebinding (T2) time OptClientIdentifier OptNum = 60 // Client identifier OptClientIdentifier1 OptNum = 61 // Client identifier(1) + ) type Op byte diff --git a/dhcpv4/dhcp_test.go b/dhcpv4/dhcp_test.go index d76f31e..fd900fd 100644 --- a/dhcpv4/dhcp_test.go +++ b/dhcpv4/dhcp_test.go @@ -78,3 +78,65 @@ func TestClientServer(t *testing.T) { } assertClState(StateBound) } + +func TestExample(t *testing.T) { + const ( + xid = 1 + offerLease = 9001 + ) + var cl Client + clientHwaddr := [6]byte{0, 0, 0, 0, 0, 1} + clientReqAddr := [4]byte{192, 168, 1, 2} + clientHostname := "client" + serverIP := [4]byte{192, 168, 1, 1} + subnetMask := [4]byte{255, 255, 255, 0} + routerAddr := [4]byte{192, 168, 1, 0} + dnsAddr := [4]byte{192, 168, 1, 255} + cl.BeginRequest(xid, RequestConfig{ + RequestedAddr: clientReqAddr, + ClientHardwareAddr: clientHwaddr, + Hostname: clientHostname, + }) + buf := make([]byte, 2048) + n, err := cl.Encapsulate(buf, 0) + if err != nil { + t.Fatal(err) + } else if n <= 0 { + t.Fatal("no data sent out by client after starting request") + } + + dfrm, _ := NewFrame(buf) + dfrm.ClearHeader() + dfrm.SetOp(OpReply) + dfrm.SetHardware(1, 6, 0) + dfrm.SetFlags(0) + dfrm.SetXID(xid) + dfrm.SetSecs(1) + *dfrm.YIAddr() = clientReqAddr + copy(dfrm.CHAddr()[:], clientHwaddr[:]) + dfrm.SetMagicCookie(MagicCookie) + ntot := 0 + nopt, _ := EncodeOption(buf[optionsOffset+ntot:], OptMessageType, byte(MsgOffer)) + ntot += nopt + nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptServerIdentification, serverIP[:]...) + ntot += nopt + nopt, _ = EncodeOption32(buf[optionsOffset+ntot:], OptServerIdentification, offerLease) + ntot += nopt + nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptSubnetMask, subnetMask[:]...) + ntot += nopt + nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptRouter, routerAddr[:]...) + ntot += nopt + nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptDNSServers, dnsAddr[:]...) + ntot += nopt + nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptEnd, dnsAddr[:]...) + ntot += nopt + + err = cl.Demux(buf[:optionsOffset+ntot], 0) + if err != nil { + t.Fatal(err) + } + // frame := buf[:optionsOffset] + // frame = AppendOption(frame, OptMessageType, byte(MsgOffer)) + // frame = AppendOption(frame, OptServerIdentification, serverIP[:]...) + // frame = AppendOption(frame, OptIPAddressLeaseTime, serverIP[:]...) +} diff --git a/dhcpv4/server.go b/dhcpv4/server.go index c8609fe..afd63b3 100644 --- a/dhcpv4/server.go +++ b/dhcpv4/server.go @@ -136,8 +136,14 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error { sv.pending++ case MsgRequest: - if client.state != StateSelecting && client.state != StateRequesting { + if !clientExists { + err = errors.New("request for non existing client?") + } else if dfrm.XID() != client.xid { + err = errors.New("unexpected XID for client") + } else if client.state != StateSelecting && client.state != StateRequesting { err = errors.New("DHCP request unexpected state") + } + if err != nil { break } client.state = StateRequesting @@ -151,16 +157,15 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error { } sv.hosts[clientIDRaw] = client return nil - // n := copy(dfrm.OptionsPayload(), optBuf) } func (sv *Server) Encapsulate(carrierData []byte, frameOffset int) (int, error) { carrierIsIP := frameOffset >= 28 dfrm, err := NewFrame(carrierData[frameOffset:]) - optBuf := dfrm.OptionsPayload()[:0] + optBuf := dfrm.OptionsPayload()[:] if err != nil { return 0, err - } else if cap(optBuf) < 255 { + } else if len(optBuf) < 255 { return 0, errOptionNotFit } if sv.pending == 0 { @@ -181,15 +186,27 @@ func (sv *Server) Encapsulate(carrierData []byte, frameOffset int) (int, error) return 0, nil // Nothing to do. } futureState := ClientState(0) + var nopt int switch client.state { case StateInit: futureState = StateSelecting - optBuf = AppendOption(optBuf, OptMessageType, byte(MsgOffer)) + nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgOffer)) case StateRequesting: futureState = StateBound - optBuf = AppendOption(optBuf, OptMessageType, byte(MsgAck)) + nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgAck)) *dfrm.CIAddr() = client.addr } + if err != nil { + return 0, err + } + n, _ := EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...) + nopt += n + if sv.gwaddr != [4]byte{} { + n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...) + nopt += n + } + optBuf[nopt] = byte(OptEnd) + nopt++ dfrm.ClearHeader() dfrm.SetOp(OpReply) @@ -208,12 +225,13 @@ func (sv *Server) Encapsulate(carrierData []byte, frameOffset int) (int, error) return 0, err } } + client.state = futureState // Set server state. sv.hosts[clientID] = client sv.pending-- - return optionsOffset + len(optBuf), nil + return optionsOffset + nopt, nil } func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) { From 489e9951fac7fc5611edb7ea7560fc1a747f04a3 Mon Sep 17 00:00:00 2001 From: soypat Date: Wed, 9 Jul 2025 16:54:48 -0300 Subject: [PATCH 2/2] dhcp working! --- dhcpv4/client.go | 47 ++++++------- dhcpv4/dhcp_test.go | 40 +++++++---- dhcpv4/frame.go | 18 +++-- dhcpv4/server.go | 4 +- examples/bridge/main.go | 44 ++++++++----- internet/pcap/capture.go | 128 +++++++++++++++++++++++++++++++++++- internet/stack-ethernet.go | 2 +- internet/stack-ip.go | 10 ++- internet/stack-udpport.go | 2 +- internet/stackbasic_test.go | 2 +- 10 files changed, 228 insertions(+), 69 deletions(-) diff --git a/dhcpv4/client.go b/dhcpv4/client.go index 26656be..2572657 100644 --- a/dhcpv4/client.go +++ b/dhcpv4/client.go @@ -19,6 +19,7 @@ type Client struct { hostname []byte dns [][4]byte + svIPtos ipv4.ToS tRenew uint32 tRebind uint32 tIPLease uint32 @@ -73,26 +74,16 @@ func (c *Client) setIP(b []byte, frameOffset int) { ifrm, _ := ipv4.NewFrame(b) ifrm.SetID((uint16(c.currentXID) ^ uint16(c.currentXID>>16)) + uint16(c.state)) if c.state > StateInit { - // TODO(soypat): Document why disabling ToS used by DHCP server may cause Request to fail. - // Apparently server sets ToS=192. Uncommenting this line causes DHCP to fail on my setup. - // If left fixed at 192, DHCP does not work. - // If left fixed at 0, DHCP does not work. - // Apparently ToS is a function of which state of DHCP one is in. Not sure why code below works. - // Note: Not exactly needed for all servers. - const ecnmask = 0b1100_0000 - ifrm.SetToS(ecnmask) + // Match server ToS since some routers drop DHCP requests if no ToS set apparently? + ifrm.SetToS(c.svIPtos) } src := ifrm.SourceAddr() for i := range src { src[i] = 0 } dst := ifrm.DestinationAddr()[:] - if c.svip == ([4]byte{}) { - for i := range dst { - dst[i] = 255 - } - } else { - copy(dst, c.svip[:]) + for i := range dst { + dst[i] = 255 } } @@ -103,6 +94,8 @@ func (c *Client) Encapsulate(carrierFrame []byte, frameOffset int) (int, error) return 0, nil // No offer received yet. } else if c.state == StateBound { return 0, nil // Done! + } else if c.state == StateRequesting { + return 0, nil // Currently awaiting ACK. } dst := carrierFrame[frameOffset:] frm, err := NewFrame(dst) @@ -149,7 +142,7 @@ func (c *Client) Encapsulate(carrierFrame []byte, frameOffset int) (int, error) nextState = StateRequesting default: - return 0, errors.New("unhandled state") + return 0, errors.New("unhandled state" + c.state.String()) } n, _ := EncodeOption(opts[numOpts:], OptClientIdentifier, c.clientMAC[:]...) numOpts += n @@ -166,10 +159,11 @@ func (c *Client) Encapsulate(carrierFrame []byte, frameOffset int) (int, error) c.setHeader(frm) c.setIP(carrierFrame, frameOffset) c.state = nextState - return optionsOffset + numOpts, nil + return OptionsOffset + numOpts, nil } func (c *Client) Demux(carrierData []byte, frameOffset int) error { + fmt.Println("DEMUX DHCP") if c.isClosed() { return net.ErrClosed } @@ -212,13 +206,17 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error { default: return fmt.Errorf("dcpv4 unexpected state in recv %s", c.state.String()) } + if frameOffset > 28 && c.svIPtos == 0 { + ifrm, _ := ipv4.NewFrame(carrierData) + c.svIPtos = ifrm.ToS() + } return nil } func (c *Client) getMessageType(frm Frame) MessageType { c.auxbuf[0] = 255 ptrMsgType := &c.auxbuf[0] - frm.ForEachOption(func(opt OptNum, data []byte) error { + frm.ForEachOption(func(_ int, opt OptNum, data []byte) error { if len(data) == 1 { *ptrMsgType = data[0] return io.EOF @@ -229,7 +227,7 @@ func (c *Client) getMessageType(frm Frame) MessageType { } func (c *Client) setOptions(frm Frame) error { - return frm.ForEachOption(func(opt OptNum, data []byte) error { + err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error { switch opt { case OptRenewTimeValue: c.tRenew = maybeU32(data) @@ -237,7 +235,6 @@ func (c *Client) setOptions(frm Frame) error { c.tIPLease = maybeU32(data) case OptRebindingTimeValue: c.tRebind = maybeU32(data) - case OptServerIdentification: c.svip = maybe4byte(data) case OptRouter: @@ -261,6 +258,7 @@ func (c *Client) setOptions(frm Frame) error { } return nil }) + return err } func (c *Client) isClosed() bool { return c.state == 0 || c.currentXID == 0 } @@ -272,16 +270,21 @@ func (c *Client) setHeader(frm Frame) { frm.SetHardware(1, 6, 0) frm.SetSecs(1) if c.state.HasIP() { - copy(frm.CIAddr()[:], c.offer[:]) + *frm.CIAddr() = c.offer } if c.state == StateInit { siaddr := frm.SIAddr()[:] for i := range siaddr { siaddr[i] = 255 } + } else { + if c.siip == [4]byte{} { + *frm.SIAddr() = c.svip + } else { + *frm.SIAddr() = c.siip + } } - - copy(frm.YIAddr()[:], c.offer[:]) + *frm.YIAddr() = c.offer copy(frm.CHAddrAs6()[:], c.clientMAC[:]) frm.SetMagicCookie(MagicCookie) } diff --git a/dhcpv4/dhcp_test.go b/dhcpv4/dhcp_test.go index fd900fd..66f0990 100644 --- a/dhcpv4/dhcp_test.go +++ b/dhcpv4/dhcp_test.go @@ -98,13 +98,18 @@ func TestExample(t *testing.T) { Hostname: clientHostname, }) buf := make([]byte, 2048) + buf2 := make([]byte, len(buf)) n, err := cl.Encapsulate(buf, 0) if err != nil { t.Fatal(err) } else if n <= 0 { t.Fatal("no data sent out by client after starting request") } - + n, err = cl.Encapsulate(buf2, 0) + if err != nil { + t.Error("client encaps double tap after discover:", err) + } + // Fabricate server OFFER response. dfrm, _ := NewFrame(buf) dfrm.ClearHeader() dfrm.SetOp(OpReply) @@ -116,27 +121,36 @@ func TestExample(t *testing.T) { copy(dfrm.CHAddr()[:], clientHwaddr[:]) dfrm.SetMagicCookie(MagicCookie) ntot := 0 - nopt, _ := EncodeOption(buf[optionsOffset+ntot:], OptMessageType, byte(MsgOffer)) + nopt, _ := EncodeOption(buf[OptionsOffset+ntot:], OptMessageType, byte(MsgOffer)) ntot += nopt - nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptServerIdentification, serverIP[:]...) + nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptServerIdentification, serverIP[:]...) ntot += nopt - nopt, _ = EncodeOption32(buf[optionsOffset+ntot:], OptServerIdentification, offerLease) + nopt, _ = EncodeOption32(buf[OptionsOffset+ntot:], OptServerIdentification, offerLease) ntot += nopt - nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptSubnetMask, subnetMask[:]...) + nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptSubnetMask, subnetMask[:]...) ntot += nopt - nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptRouter, routerAddr[:]...) + nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptRouter, routerAddr[:]...) ntot += nopt - nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptDNSServers, dnsAddr[:]...) + nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptDNSServers, dnsAddr[:]...) ntot += nopt - nopt, _ = EncodeOption(buf[optionsOffset+ntot:], OptEnd, dnsAddr[:]...) + nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptEnd, dnsAddr[:]...) ntot += nopt - err = cl.Demux(buf[:optionsOffset+ntot], 0) + err = cl.Demux(buf[:OptionsOffset+ntot], 0) if err != nil { t.Fatal(err) } - // frame := buf[:optionsOffset] - // frame = AppendOption(frame, OptMessageType, byte(MsgOffer)) - // frame = AppendOption(frame, OptServerIdentification, serverIP[:]...) - // frame = AppendOption(frame, OptIPAddressLeaseTime, serverIP[:]...) + + n, err = cl.Encapsulate(buf[:], 0) + if err != nil { + t.Fatal(err) + } else if n <= 0 { + t.Fatal("no data written from client in response to offer") + } + n, err = cl.Encapsulate(buf[:], 0) + if err != nil { + t.Error("encapsulate double tap after request:", err) + } else if n > 0 { + t.Error("encapsulate double tap got data!", n) + } } diff --git a/dhcpv4/frame.go b/dhcpv4/frame.go index cdecd4f..916ca81 100644 --- a/dhcpv4/frame.go +++ b/dhcpv4/frame.go @@ -17,7 +17,7 @@ const ( // Expected Magic Cookie value. MagicCookie uint32 = 0x63825363 // DHCP Options offset measured from the start of the UDP payload. - optionsOffset = magicCookieOffset + 4 + OptionsOffset = magicCookieOffset + 4 DefaultClientPort = 68 DefaultServerPort = 67 @@ -26,12 +26,16 @@ const ( // NewFrame returns a new DHCPv4 Frame with data set to buf. // An error is returned if the buffer size is smaller than 240. func NewFrame(buf []byte) (Frame, error) { - if len(buf) < optionsOffset { + if len(buf) < OptionsOffset { return Frame{}, errSmallFrame } return Frame{buf: buf}, nil } +func PayloadIsDHCPv4(payload []byte) bool { + return len(payload) >= OptionsOffset && binary.BigEndian.Uint32(payload[magicCookieOffset:]) == MagicCookie +} + // Frame encapsulates the raw data of a DHCP packet // and provides methods for manipulating, validating and // retrieving fields and payload data. See [RFC2131]. @@ -43,7 +47,7 @@ type Frame struct { // OptionsPayload returns the options portion of the DHCP frame. May be zero lengthed. func (frm Frame) OptionsPayload() []byte { - return frm.buf[optionsOffset:] + return frm.buf[OptionsOffset:] } func (frm Frame) Op() Op { return Op(frm.buf[0]) } @@ -111,16 +115,16 @@ func (frm Frame) SetMagicCookie(cookie uint32) { // ClearHeader zeros out the header contents. func (frm Frame) ClearHeader() { - for i := range frm.buf[:optionsOffset] { + for i := range frm.buf[:OptionsOffset] { frm.buf[i] = 0 } } // ForEachOption iterates over all DHCPv4 options returning an error on a malformed option or when user provided callback returns an error. // If the user provided callback is nil then only option buffer validation is performed. -func (frm Frame) ForEachOption(fn func(op OptNum, data []byte) error) error { +func (frm Frame) ForEachOption(fn func(off int, op OptNum, data []byte) error) error { // Parse DHCP options. - ptr := optionsOffset + ptr := OptionsOffset if ptr > len(frm.buf) { return errSmallFrame } else if len(frm.buf[ptr:]) == 0 { @@ -141,7 +145,7 @@ func (frm Frame) ForEachOption(fn func(op OptNum, data []byte) error) error { optlen := frm.buf[ptr+1] if callback { optionData := frm.buf[ptr+2 : ptr+2+int(optlen)] - if err := fn(optnum, optionData); err != nil { + if err := fn(ptr, optnum, optionData); err != nil { return err } } diff --git a/dhcpv4/server.go b/dhcpv4/server.go index afd63b3..df2a143 100644 --- a/dhcpv4/server.go +++ b/dhcpv4/server.go @@ -77,7 +77,7 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error { var reqlist []byte var reqAddr []byte var hostname []byte - err = dfrm.ForEachOption(func(op OptNum, data []byte) error { + err = dfrm.ForEachOption(func(off int, op OptNum, data []byte) error { switch op { case OptMessageType: if len(data) == 1 { @@ -231,7 +231,7 @@ func (sv *Server) Encapsulate(carrierData []byte, frameOffset int) (int, error) // Set server state. sv.hosts[clientID] = client sv.pending-- - return optionsOffset + nopt, nil + return OptionsOffset + nopt, nil } func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) { diff --git a/examples/bridge/main.go b/examples/bridge/main.go index 0814ad5..1274a6a 100644 --- a/examples/bridge/main.go +++ b/examples/bridge/main.go @@ -6,6 +6,7 @@ import ( "errors" "flag" "fmt" + "log/slog" "net" "net/netip" "os" @@ -89,7 +90,7 @@ func run() (err error) { } fmt.Println("NIC hardware address:", net.HardwareAddr(nicHW[:]).String(), "bridgeHW:", net.HardwareAddr(brHW[:]).String(), "mtu:", mtu, "addr:", nicAddr.String()) var stack Stack - err = stack.Reset(brHW, nicAddr.Addr().Next(), uint16(mtu)) + err = stack.Reset(brHW, netip.AddrFrom4([4]byte{}), uint16(mtu)) if err != nil { return err } @@ -97,9 +98,7 @@ func run() (err error) { if err != nil { return err } - var shark pcap.PacketBreakdown buf := make([]byte, mtu) - var iframes []pcap.Frame lastAction := time.Now() dnsOngoing := false for { @@ -126,12 +125,6 @@ func run() (err error) { if err != nil { fmt.Println("ERR:ENCAPSULATE", err) } else if nwrite > 0 { - iframes, err = shark.CaptureEthernet(iframes[:0], buf[:nwrite], 0) - if err != nil { - fmt.Println("OU", iframes, err.Error()) - } else { - fmt.Println("OU", iframes) - } n, err := iface.Write(buf[:nwrite]) if err != nil { return err @@ -145,12 +138,6 @@ func run() (err error) { if err != nil { return err } else if nread > 0 { - iframes, err = shark.CaptureEthernet(iframes[:0], buf[:nread], 0) - if err != nil { - fmt.Println("IN", iframes, err.Error()) - } else { - fmt.Println("IN", iframes) - } err = stack.Demux(buf[:nread], 0) if err != nil { fmt.Println("ERR:DEMUX", err) @@ -174,14 +161,36 @@ type Stack struct { dhcp dhcpv4.Client dns dns.Client lookup dns.Message + + // Packet capture and top level filtering. + shark pcap.PacketBreakdown + aux []pcap.Frame } -func (s *Stack) Demux(b []byte, _ int) error { +func (s *Stack) Demux(b []byte, _ int) (err error) { + s.aux, err = s.shark.CaptureEthernet(s.aux[:0], b, 0) + if s.aux[len(s.aux)-1].Protocol != "DHCPv4" { + return nil + } + if err != nil { + fmt.Println("IN", s.aux, err.Error()) + } else { + fmt.Println("IN", s.aux) + } return s.link.Demux(b, 0) } func (s *Stack) Encapsulate(b []byte, _ int) (int, error) { - return s.link.Encapsulate(b, 0) + n, err := s.link.Encapsulate(b, 0) + if n > 0 { + iframes, errpcap := s.shark.CaptureEthernet(s.aux[:0], b[:n], 0) + if errpcap != nil { + fmt.Println("OU", iframes, errpcap.Error()) + } else { + fmt.Println("OU", iframes) + } + } + return n, err } func (s *Stack) Reset(mac [6]byte, addr netip.Addr, mtu uint16) error { @@ -227,6 +236,7 @@ func (s *Stack) Reset(mac [6]byte, addr netip.Addr, mtu uint16) error { if err != nil { return err } + s.ip.SetLogger(slog.Default()) return nil } diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 6a20062..083c5d7 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -9,6 +9,7 @@ import ( "github.com/soypat/lneto" "github.com/soypat/lneto/arp" + "github.com/soypat/lneto/dhcpv4" "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/http/httpraw" "github.com/soypat/lneto/ipv4" @@ -238,7 +239,6 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( } } return pc.captureIPProto(proto, dst, pkt, end, protoErrs...) - } func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt []byte, bitOffset int, ipProtoErrs ...error) (_ []Frame, err error) { @@ -321,7 +321,49 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([ finfo.Fields = append(finfo.Fields, baseUDPFields[:]...) dst = append(dst, finfo) end := bitOffset + 8*octet - dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))) + payload := ufrm.Payload() + if dhcpv4.PayloadIsDHCPv4(payload) { + return pc.CaptureDHCPv4(dst, pkt, end) + } else { + dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))) + } + return dst, nil +} + +func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { + if bitOffset%8 != 0 { + return nil, errors.New("DHCP must be parsed at byte boundary") + } + dhcpData := pkt[bitOffset/8:] + dfrm, err := dhcpv4.NewFrame(dhcpData) + if err != nil { + return nil, err + } + finfo := Frame{ + Protocol: "DHCPv4", + PacketBitOffset: bitOffset, + } + magic := dfrm.MagicCookie() + if magic != dhcpv4.MagicCookie { + finfo.Errors = append(finfo.Errors, errors.New("incorrect DHCPv4 magic cookie")) + } + finfo.Fields = append(finfo.Fields, baseDHCPv4Fields[:]...) + options := dfrm.OptionsPayload() + if len(options) > 0 { + err = dfrm.ForEachOption(func(optoff int, op dhcpv4.OptNum, data []byte) error { + finfo.Fields = append(finfo.Fields, FrameField{ + Name: op.String(), + Class: FieldClassOptions, + FrameBitOffset: optoff * octet, + BitLength: (2 + len(data)) * octet, + }) + return nil + }) + if err != nil { + finfo.Errors = append(finfo.Errors, err) + } + } + dst = append(dst, finfo) return dst, nil } @@ -547,6 +589,7 @@ const ( FieldClassOptions // options FieldClassPayload // payload FieldClassText // text + FieldClassAddress // address ) const octet = 8 @@ -799,6 +842,87 @@ var baseUDPFields = [...]FrameField{ }, } +var baseDHCPv4Fields = [...]FrameField{ + { + Name: "Opcode", + Class: FieldClassType, + FrameBitOffset: 0, + BitLength: 1 * octet, + }, + { + Name: "Hardware Address Type", + Class: FieldClassProto, + FrameBitOffset: 1 * octet, + BitLength: 1 * octet, + }, + { + Name: "Hardware Address Length", + Class: FieldClassSize, + FrameBitOffset: 2 * octet, + BitLength: 1 * octet, + }, + { + Name: "Hops", + Class: fieldClassUndefined, + FrameBitOffset: 3 * octet, + BitLength: 1 * octet, + }, + { + Name: "Transaction ID", + Class: FieldClassID, + FrameBitOffset: 4 * octet, + BitLength: 4 * octet, + }, + { + Name: "Start Time", + Class: fieldClassUndefined, + FrameBitOffset: 8 * octet, + BitLength: 2 * octet, + }, + { + Name: "Flags", + Class: FieldClassFlags, + FrameBitOffset: 10 * octet, + BitLength: 2 * octet, + }, + { + Name: "Client Address", + Class: FieldClassAddress, + FrameBitOffset: 12 * octet, + BitLength: 4 * octet, + }, + { + Name: "Offered Address", + Class: FieldClassAddress, + FrameBitOffset: 16 * octet, + BitLength: 4 * octet, + }, + { + Name: "Server Next Address", + Class: FieldClassAddress, + FrameBitOffset: 20 * octet, + BitLength: 4 * octet, + }, + { + Name: "Relay Agent Address", + Class: FieldClassAddress, + FrameBitOffset: 24 * octet, + BitLength: 4 * octet, + }, + { + Name: "Client Hardware Address", + Class: FieldClassAddress, + FrameBitOffset: 28 * octet, + BitLength: 16 * octet, + }, + { + Name: "BOOTP", + Class: FieldClassAddress, + FrameBitOffset: (28 + 16) * octet, + BitLength: (dhcpv4.OptionsOffset - (28 + 16)) * octet, + }, +} + func remainingFrameInfo(proto any, class FieldClass, pktBitOffset, pktBitLen int) Frame { return Frame{ Protocol: proto, diff --git a/internet/stack-ethernet.go b/internet/stack-ethernet.go index bbe4445..b6f8f45 100644 --- a/internet/stack-ethernet.go +++ b/internet/stack-ethernet.go @@ -100,7 +100,7 @@ func (ls *StackEthernet) Demux(carrierData []byte, frameOffset int) (err error) } DROP: ls.info("LinkStack:drop-packet", slog.String("dsthw", net.HardwareAddr(dstaddr[:]).String()), slog.String("ethertype", efrm.EtherTypeOrSize().String())) - return nil + return errors.New("drop packet") } func (ls *StackEthernet) Encapsulate(carrierData []byte, frameOffset int) (n int, err error) { diff --git a/internet/stack-ip.go b/internet/stack-ip.go index 478391b..771fbde 100644 --- a/internet/stack-ip.go +++ b/internet/stack-ip.go @@ -64,6 +64,10 @@ func (sb *StackIP) Addr() netip.Addr { return netip.AddrFrom4(sb.ip) } +func (sb *StackIP) SetLogger(logger *slog.Logger) { + sb.logger.log = logger +} + func (sb *StackIP) Demux(carrierData []byte, offset int) error { sb.info("StackIP.Demux:start") frame := carrierData[offset:] // we don't care about carrier data in IP. @@ -72,8 +76,8 @@ func (sb *StackIP) Demux(carrierData []byte, offset int) error { return err } dst := ifrm.DestinationAddr() - if *dst != sb.ip { - return nil // Not meant for us. + if sb.ip != ([4]byte{}) && *dst != sb.ip { + return errors.New("not meant for us") // Not meant for us. } sb.validator.ResetErr() @@ -155,7 +159,7 @@ func (sb *StackIP) Encapsulate(carrierData []byte, frameOffset int) (int, error) println("NODE REMOVED", proto.String(), h.port) h.destroy() } - sb.error("StackIP:handle", slog.String("proto", proto.String()), slog.String("err", err.Error())) + sb.error("StackIP:encapsulate", slog.String("proto", proto.String()), slog.String("err", err.Error())) continue } else if n == 0 { continue diff --git a/internet/stack-udpport.go b/internet/stack-udpport.go index 150c714..ecf2730 100644 --- a/internet/stack-udpport.go +++ b/internet/stack-udpport.go @@ -74,7 +74,7 @@ func (sudp *StackUDPPort) Encapsulate(carrierData []byte, frameOffset int) (int, n, err := sudp.h.encapsulate(carrierData, frameOffset+8) if n == 0 { if err != nil { - slog.Error("stackudp:demux", slog.String("err", err.Error())) + slog.Error("stackudp:encapsulate", slog.String("err", err.Error())) } return 0, err } diff --git a/internet/stackbasic_test.go b/internet/stackbasic_test.go index 82f95de..19c6060 100644 --- a/internet/stackbasic_test.go +++ b/internet/stackbasic_test.go @@ -46,7 +46,7 @@ func expectExchange(t *testing.T, from, to *StackIP, buf []byte) { t.Helper() n, err := from.Encapsulate(buf, 0) if err != nil { - t.Error("expectExchange:Handle:", err) + t.Error("expectExchange:encapsulate:", err) } else if n == 0 { t.Error("expected data exchange") return