diff --git a/dhcpv4/nclient4/client_test.go b/dhcpv4/nclient4/client_test.go index df851abb..ed8433f7 100644 --- a/dhcpv4/nclient4/client_test.go +++ b/dhcpv4/nclient4/client_test.go @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.12 +//go:build go1.12 && linux +// +build go1.12,linux package nclient4 diff --git a/dhcpv4/nclient4/conn_linux.go b/dhcpv4/nclient4/conn_linux.go new file mode 100644 index 00000000..581e8aa4 --- /dev/null +++ b/dhcpv4/nclient4/conn_linux.go @@ -0,0 +1,100 @@ +// Copyright 2018 the u-root Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.12 && linux +// +build go1.12,linux + +package nclient4 + +import ( + "io" + "net" + + "github.com/mdlayher/packet" + "golang.org/x/sys/unix" +) + +// NewRawUDPConn returns a UDP connection bound to the interface and port +// given based on a raw packet socket. All packets are broadcasted. +// +// The interface can be completely unconfigured. +func NewRawUDPConn(iface string, port int) (net.PacketConn, error) { + ifc, err := net.InterfaceByName(iface) + if err != nil { + return nil, err + } + rawConn, err := packet.Listen(ifc, packet.Datagram, unix.ETH_P_IP, nil) + if err != nil { + return nil, err + } + return NewBroadcastUDPConn(rawConn, &net.UDPAddr{Port: port}), nil +} + +// BroadcastRawUDPConn uses a raw socket to send UDP packets to the broadcast +// MAC address. +type BroadcastRawUDPConn struct { + // PacketConn is a raw DGRAM socket. + net.PacketConn + + // boundAddr is the address this RawUDPConn is "bound" to. + // + // Calls to ReadFrom will only return packets destined to this address. + boundAddr *net.UDPAddr +} + +// NewBroadcastUDPConn returns a PacketConn that marshals and unmarshals UDP +// packets, sending them to the broadcast MAC at on rawPacketConn. +// +// Calls to ReadFrom will only return packets destined to boundAddr. +func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr) net.PacketConn { + return &BroadcastRawUDPConn{ + PacketConn: rawPacketConn, + boundAddr: boundAddr, + } +} + +// ReadFrom implements net.PacketConn.ReadFrom. +// +// ReadFrom reads raw IP packets and will try to match them against +// upc.boundAddr. Any matching packets are returned via the given buffer. +func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { + ipHdrMaxLen := ipv4MaximumHeaderSize + udpHdrLen := udpMinimumSize + + for { + pkt := make([]byte, ipHdrMaxLen+udpHdrLen+len(b)) + n, _, err := upc.PacketConn.ReadFrom(pkt) + if err != nil { + return 0, nil, err + } + if n == 0 { + return 0, nil, io.EOF + } + pkt = pkt[:n] + dhcpPkt, srcAddr := getUDP4pkt(pkt, upc.boundAddr) + if dhcpPkt == nil { + continue + } + + return copy(b, dhcpPkt), srcAddr, nil + } +} + +// WriteTo implements net.PacketConn.WriteTo and broadcasts all packets at the +// raw socket level. +// +// WriteTo wraps the given packet in the appropriate UDP and IP header before +// sending it on the packet conn. +func (upc *BroadcastRawUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { + udpAddr, ok := addr.(*net.UDPAddr) + if !ok { + return 0, ErrUDPAddrIsRequired + } + + // Using the boundAddr is not quite right here, but it works. + pkt := udp4pkt(b, udpAddr, upc.boundAddr) + + // Broadcasting is not always right, but hell, what the ARP do I know. + return upc.PacketConn.WriteTo(pkt, &packet.Addr{HardwareAddr: BroadcastMac}) +} diff --git a/dhcpv4/nclient4/conn_unix.go b/dhcpv4/nclient4/conn_unix.go index f3e48c66..c35bfe49 100644 --- a/dhcpv4/nclient4/conn_unix.go +++ b/dhcpv4/nclient4/conn_unix.go @@ -1,94 +1,88 @@ -// Copyright 2018 the u-root Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.12 && (darwin || freebsd || linux || netbsd || openbsd || dragonfly) +//go:build go1.12 && (darwin || freebsd || netbsd || openbsd || dragonfly) // +build go1.12 -// +build darwin freebsd linux netbsd openbsd dragonfly +// +build darwin freebsd netbsd openbsd dragonfly package nclient4 import ( - "errors" "io" "net" - "github.com/mdlayher/packet" - "github.com/u-root/uio/uio" - "golang.org/x/sys/unix" + "github.com/mdlayher/raw" ) -var ( - // BroadcastMac is the broadcast MAC address. - // - // Any UDP packet sent to this address is broadcast on the subnet. - BroadcastMac = net.HardwareAddr([]byte{255, 255, 255, 255, 255, 255}) +const ( + bpfFilterBidirectional int = 1 ) -var ( - // ErrUDPAddrIsRequired is an error used when a passed argument is not of type "*net.UDPAddr". - ErrUDPAddrIsRequired = errors.New("must supply UDPAddr") -) +var rawConnectionConfig = &raw.Config{ + BPFDirection: bpfFilterBidirectional, +} // NewRawUDPConn returns a UDP connection bound to the interface and port // given based on a raw packet socket. All packets are broadcasted. // // The interface can be completely unconfigured. -func NewRawUDPConn(iface string, port int) (net.PacketConn, error) { +func NewRawUDPConn(iface string, port int, vlans ...uint16) (net.PacketConn, error) { ifc, err := net.InterfaceByName(iface) if err != nil { return nil, err } - rawConn, err := packet.Listen(ifc, packet.Datagram, unix.ETH_P_IP, nil) + + var etherType uint16 + if len(vlans) > 0 { + etherType = vlanTPID // The VLAN TPID field is located in the same offset as EtherType + } else { + etherType = etherIPv4Proto + } + + // Create a bidirectional raw socket on ifc with etherType as the filter + rawConn, err := raw.ListenPacket(ifc, etherType, rawConnectionConfig) if err != nil { return nil, err } - return NewBroadcastUDPConn(rawConn, &net.UDPAddr{Port: port}), nil + + return NewBroadcastUDPConn(net.PacketConn(rawConn), &net.UDPAddr{Port: port}, vlans...), nil } -// BroadcastRawUDPConn uses a raw socket to send UDP packets to the broadcast -// MAC address. type BroadcastRawUDPConn struct { - // PacketConn is a raw DGRAM socket. + // PacketConn is a raw network socket net.PacketConn - // boundAddr is the address this RawUDPConn is "bound" to. - // - // Calls to ReadFrom will only return packets destined to this address. boundAddr *net.UDPAddr + // VLAN tags can be configured to make up for the shortcoming of the BSD implementation + VLANs []uint16 } // NewBroadcastUDPConn returns a PacketConn that marshals and unmarshals UDP // packets, sending them to the broadcast MAC at on rawPacketConn. +// Supplied VLAN tags are inserted into the Ethernet frame before sending. // // Calls to ReadFrom will only return packets destined to boundAddr. -func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr) net.PacketConn { +func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr, vlans ...uint16) net.PacketConn { return &BroadcastRawUDPConn{ PacketConn: rawPacketConn, boundAddr: boundAddr, + VLANs: vlans, } } -func udpMatch(addr *net.UDPAddr, bound *net.UDPAddr) bool { - if bound == nil { - return true - } - if bound.IP != nil && !bound.IP.Equal(addr.IP) { - return false - } - return bound.Port == addr.Port -} - // ReadFrom implements net.PacketConn.ReadFrom. // -// ReadFrom reads raw IP packets and will try to match them against -// upc.boundAddr. Any matching packets are returned via the given buffer. +// ReadFrom reads raw Ethernet frames, parses and matches the VLAN stack (if configured), +// and will try to match the remaining IP packet against upc.boundAddr. +// +// Any matching packets are returned via the given buffer. func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { + ethHdrLen := ethHdrBaseLen + if len(upc.VLANs) > 0 { + ethHdrLen += len(upc.VLANs) * vlanTagLen + } ipHdrMaxLen := ipv4MaximumHeaderSize udpHdrLen := udpMinimumSize for { - pkt := make([]byte, ipHdrMaxLen+udpHdrLen+len(b)) + pkt := make([]byte, ethHdrLen+ipHdrMaxLen+udpHdrLen+len(b)) n, _, err := upc.PacketConn.ReadFrom(pkt) if err != nil { return 0, nil, err @@ -96,50 +90,27 @@ func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { if n == 0 { return 0, nil, io.EOF } - pkt = pkt[:n] - buf := uio.NewBigEndianBuffer(pkt) - ipHdr := ipv4(buf.Data()) - - if !ipHdr.isValid(n) { - continue - } - - ipHdr = ipv4(buf.Consume(int(ipHdr.headerLength()))) - - if ipHdr.transportProtocol() != udpProtocolNumber { + pkt = getEthernetPayload(pkt[:n], upc.VLANs) + if pkt == nil { + // VLAN stack does not match our configuration continue } - - if !buf.Has(udpHdrLen) { + dhcpPkt, srcAddr := getUDP4pkt(pkt[:n], upc.boundAddr) + if dhcpPkt == nil { continue } - udpHdr := udp(buf.Consume(udpHdrLen)) - - addr := &net.UDPAddr{ - IP: ipHdr.destinationAddress(), - Port: int(udpHdr.destinationPort()), - } - if !udpMatch(addr, upc.boundAddr) { - continue - } - srcAddr := &net.UDPAddr{ - IP: ipHdr.sourceAddress(), - Port: int(udpHdr.sourcePort()), - } - // Extra padding after end of IP packet should be ignored, - // if not dhcp option parsing will fail. - dhcpLen := int(ipHdr.payloadLength()) - udpHdrLen - return copy(b, buf.Consume(dhcpLen)), srcAddr, nil + return copy(b, dhcpPkt), srcAddr, nil } } // WriteTo implements net.PacketConn.WriteTo and broadcasts all packets at the // raw socket level. // -// WriteTo wraps the given packet in the appropriate UDP and IP header before -// sending it on the packet conn. +// WriteTo wraps the given packet in the appropriate UDP, IP and Ethernet header +// before sending it on the packet conn. Since the Ethernet encapsulation is done +// on the application's side, VLAN tagging also has to be handled in the application. func (upc *BroadcastRawUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { udpAddr, ok := addr.(*net.UDPAddr) if !ok { @@ -149,6 +120,9 @@ func (upc *BroadcastRawUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { // Using the boundAddr is not quite right here, but it works. pkt := udp4pkt(b, udpAddr, upc.boundAddr) - // Broadcasting is not always right, but hell, what the ARP do I know. - return upc.PacketConn.WriteTo(pkt, &packet.Addr{HardwareAddr: BroadcastMac}) + srcMac := upc.PacketConn.LocalAddr().(*raw.Addr).HardwareAddr + pkt = addEthernetHdr(pkt, BroadcastMac, srcMac, etherIPv4Proto, upc.VLANs) + + // The `raw` packet connection does not take any address as an argument. + return upc.PacketConn.WriteTo(pkt, nil) } diff --git a/dhcpv4/nclient4/ethernet.go b/dhcpv4/nclient4/ethernet.go new file mode 100644 index 00000000..b21cbfd1 --- /dev/null +++ b/dhcpv4/nclient4/ethernet.go @@ -0,0 +1,140 @@ +//go:build go1.12 +// +build go1.12 + +package nclient4 + +import ( + "encoding/binary" + "net" + + "github.com/u-root/uio/uio" +) + +const ( + etherIPv4Proto uint16 = 0x0800 + ethHdrBaseLen int = 14 + + vlanTagLen int = 4 + vlanMax uint16 = 0x0FFF + vlanTPID uint16 = 0x8100 +) + +var ( + // BroadcastMac is the broadcast MAC address. + // + // Any UDP packet sent to this address is broadcast on the subnet. + BroadcastMac = net.HardwareAddr([]byte{255, 255, 255, 255, 255, 255}) +) + +// processVLANStack receives a buffer starting at the first TPID/EtherType field, and walks through +// the VLAN stack until either an unexpected VLAN is found, or if an IPv4 EtherType is found. +// The data from the provided buffer is consumed until the end of the Ethernet header. +// +// processVLANStack returns true if the VLAN stack in the packet corresponds to the VLAN configuration, false otherwise. +func processVLANStack(buf *uio.Lexer, vlans []uint16) bool { + var currentVLAN uint16 + var vlanStackIsCorrect bool = true + configuredVLANs := make([]uint16, len(vlans)) + copy(configuredVLANs, vlans) + + for { + switch etherType := binary.BigEndian.Uint16(buf.Consume(2)); etherType { + case vlanTPID: + tci := binary.BigEndian.Uint16(buf.Consume(2)) + vlanID := tci & vlanMax // Mask first 4 bits + if len(configuredVLANs) != 0 { + currentVLAN, configuredVLANs = configuredVLANs[0], configuredVLANs[1:] + if vlanID != currentVLAN { + // Packet VLAN tag does not match configured VLAN stack + vlanStackIsCorrect = false + } + } else { + // Packet VLAN stack is too long + vlanStackIsCorrect = false + } + case etherIPv4Proto: + if len(configuredVLANs) == 0 { + // Packet VLAN stack has been consumed, return result + return vlanStackIsCorrect + } else { + // VLAN tags remaining in configured stack -> not a match + vlanStackIsCorrect = false + } + return vlanStackIsCorrect + default: + vlanStackIsCorrect = false + return vlanStackIsCorrect + } + } +} + +// getEthernetPayload processes an Ethernet header, verifies the +// VLAN tags contained in it and returns the payload as a byte slice. +// +// If the VLAN tag stack does not match the VLAN configuration, +// nil is returned (since the packet is not meant for us). +// In case the EtherType does not match the IPv4 proto value, +// nil is returned too (since the packet could not be DHCPv4). +func getEthernetPayload(pkt []byte, vlans []uint16) []byte { + buf := uio.NewBigEndianBuffer(pkt) + dstMac := buf.Consume(6) + srcMac := buf.Consume(6) + _, _ = dstMac, srcMac + + if len(vlans) > 0 { + success := processVLANStack(buf, vlans) + if !success { + return nil + } + } else { + etherType := binary.BigEndian.Uint16(buf.Consume(2)) + if etherType != etherIPv4Proto { + return nil + } + } + + return buf.Data() +} + +// createVLANTag returns the bytes of a 4-byte long 802.1Q VLAN tag, +// which can be inserted in an Ethernet frame header. +func createVLANTag(vlan uint16) []byte { + vlanTag := make([]byte, vlanTagLen) + // First 2 bytes are the TPID. Only support 802.1Q for now (even for QinQ, 802.1ad is rarely used) + binary.BigEndian.PutUint16(vlanTag, vlanTPID) + + var pcp, dei, tci uint16 + // TCI - tag control information, 2 bytes. Format: | PCP (3 bits) | DEI (1 bit) | VLAN ID (12 bits) | + pcp = 0x0 // 802.1p priority level - 3 bits, valid values range from 0x0 to 0x7. 0x0 - best effort + dei = 0x0 // drop eligible indicator - 1 bit, valid values are 0x0 or 0x1. 0x0 - not drop eligible + tci |= pcp << 13 // 16-3 = 13 offset + tci |= dei << 12 // 13-1 = 12 offset + tci |= vlan // VLAN ID (VID) is 12 bits + binary.BigEndian.PutUint16(vlanTag[2:], tci) + + return vlanTag +} + +// addEthernetHdr returns the supplied packet (in bytes) with an +// added Ethernet header with the specified EtherType. +func addEthernetHdr(b []byte, dstMac, srcMac net.HardwareAddr, etherProto uint16, vlans []uint16) []byte { + ethHdrLen := ethHdrBaseLen + if len(vlans) > 0 { + ethHdrLen += len(vlans) * vlanTagLen + } + b = append(make([]byte, ethHdrLen), b...) + offset := 0 + copy(b, dstMac) + offset += len(dstMac) + copy(b[offset:], srcMac) + offset += len(srcMac) + + for _, vlan := range vlans { + copy(b[offset:], createVLANTag(vlan)) + offset += vlanTagLen + } + + binary.BigEndian.PutUint16(b[offset:], etherProto) + + return b +} diff --git a/dhcpv4/nclient4/ethernet_test.go b/dhcpv4/nclient4/ethernet_test.go new file mode 100644 index 00000000..322f30db --- /dev/null +++ b/dhcpv4/nclient4/ethernet_test.go @@ -0,0 +1,176 @@ +//go:build go1.12 +// +build go1.12 + +package nclient4 + +import ( + "bytes" + "net" + "testing" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/u-root/uio/uio" +) + +var ( + testMac = net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06} + // Test payload 123456789abcdefghijklmnopqrstvwxyz + // This length is required to avoid zero-padding the Ethernet frame from the right side + testPayload = gopacket.Payload{0x54, 0x65, 0x73, 0x74, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x20, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7a} + + ethHdrIPv4 = &layers.Ethernet{ + DstMAC: BroadcastMac, + SrcMAC: testMac, + EthernetType: layers.EthernetTypeIPv4, + } + ethHdrVLAN = &layers.Ethernet{ + DstMAC: BroadcastMac, + SrcMAC: testMac, + EthernetType: layers.EthernetTypeDot1Q, + } + ethHdrARP = &layers.Ethernet{ + DstMAC: BroadcastMac, + SrcMAC: testMac, + EthernetType: layers.EthernetTypeARP, + } + vlanTagOuter = &layers.Dot1Q{ + Priority: 0, + DropEligible: false, + VLANIdentifier: 100, + Type: layers.EthernetTypeDot1Q, + } + vlanTagInner = &layers.Dot1Q{ + Priority: 0, + DropEligible: false, + VLANIdentifier: 200, + Type: layers.EthernetTypeIPv4, + } + ipv4Hdr = &layers.IPv4{ + SrcIP: net.IP{1, 2, 3, 4}, + DstIP: net.IP{5, 6, 7, 8}, + } + opts = gopacket.SerializeOptions{} +) + +func TestProcessVLANStack(t *testing.T) { + for _, tt := range []struct { + name string + inputBytes []byte + vlanConfig []uint16 + wantSuccess bool + }{ + {"no VLANs + no VLANs configured", []byte{0x08, 0x00}, []uint16{}, true}, + {"no VLANs + VLAN configured", []byte{0x08, 0x00}, []uint16{100}, false}, + {"valid VLAN stack (single)", []byte{0x81, 0x00, 0x01, 0x00, 0x08, 0x00}, []uint16{0x100}, true}, + {"invalid VLAN stack (single)", []byte{0x81, 0x00, 0x01, 0xFF, 0x08, 0x00}, []uint16{0x100}, false}, + {"valid VLAN stack (double)", []byte{0x81, 0x00, 0x01, 0x00, 0x81, 0x00, 0x02, 0x00, 0x08, 0x00}, []uint16{0x100, 0x200}, true}, + {"invalid VLAN stack (double)", []byte{0x81, 0x00, 0x01, 0x00, 0x81, 0x00, 0x02, 0xFF, 0x08, 0x00}, []uint16{0x100, 0x200}, false}, + {"invalid VLAN stack (too short)", []byte{0x81, 0x00, 0x01, 0x00, 0x08, 0x00}, []uint16{0x100, 0x200}, false}, + {"invalid VLAN stack (too long)", []byte{0x81, 0x00, 0x01, 0x00, 0x81, 0x00, 0x02, 0x00, 0x08, 0x00}, []uint16{0x100}, false}, + {"invalid packet (ARP)", []byte{0x08, 0x06}, []uint16{}, false}, + } { + t.Run(tt.name, func(t *testing.T) { + testBuf := uio.NewBigEndianBuffer(tt.inputBytes) + testSuccess := processVLANStack(testBuf, tt.vlanConfig) + + if testSuccess != tt.wantSuccess { + t.Errorf("got %v, want %v", testSuccess, tt.wantSuccess) + } + }) + } +} + +func TestCreateVLANTag(t *testing.T) { + // gopacket builds VLAN tags the other way around: first VLAN ID/TCI, then TPID, due to their different layered approach + // Since a VLAN tag is only 4 bytes, and the value is well-known, it makes sense to just construct the packet by hand. + want := []byte{0x81, 0x00, 0x01, 0x23} + + test := createVLANTag(0x0123) + + if !bytes.Equal(test, want) { + t.Errorf("got %v, want %v", test, want) + } +} + +func TestGetEthernetPayload(t *testing.T) { + for _, tt := range []struct { + name string + testLayers []gopacket.SerializableLayer + wantLayers []gopacket.SerializableLayer + vlanConfig []uint16 + }{ + {"no VLAN", []gopacket.SerializableLayer{ethHdrIPv4, ipv4Hdr, testPayload}, []gopacket.SerializableLayer{ipv4Hdr, testPayload}, []uint16{}}, + {"single VLAN", []gopacket.SerializableLayer{ethHdrVLAN, vlanTagInner, ipv4Hdr, testPayload}, []gopacket.SerializableLayer{ipv4Hdr, testPayload}, []uint16{200}}, + {"QinQ", []gopacket.SerializableLayer{ethHdrVLAN, vlanTagOuter, vlanTagInner, ipv4Hdr, testPayload}, []gopacket.SerializableLayer{ipv4Hdr, testPayload}, []uint16{100, 200}}, + {"invalid VLAN", []gopacket.SerializableLayer{ethHdrVLAN, vlanTagInner, ipv4Hdr, testPayload}, nil, []uint16{300}}, + {"invalid packet (ARP)", []gopacket.SerializableLayer{ethHdrARP}, nil, []uint16{}}, + } { + t.Run(tt.name, func(t *testing.T) { + testBuf := gopacket.NewSerializeBuffer() + err := gopacket.SerializeLayers(testBuf, opts, tt.testLayers...) + if err != nil { + t.Errorf("error serializing in gopacket (not our bug!)") + } + + var want []byte + if tt.wantLayers == nil { + want = nil + } else { + wantBuf := gopacket.NewSerializeBuffer() + err := gopacket.SerializeLayers(wantBuf, opts, tt.wantLayers...) + if err != nil { + t.Errorf("error serializing in gopacket (not our bug!)") + } + want = wantBuf.Bytes() + } + + testBytes := testBuf.Bytes() + test := getEthernetPayload(testBytes, tt.vlanConfig) + + if !bytes.Equal(test, want) { + t.Errorf("got %v, want %v", test, want) + } + }) + } +} + +func TestAddEthernetHdr(t *testing.T) { + for _, tt := range []struct { + name string + testLayers []gopacket.SerializableLayer + wantLayers []gopacket.SerializableLayer + vlanConfig []uint16 + }{ + {"no VLAN", []gopacket.SerializableLayer{ipv4Hdr, testPayload}, []gopacket.SerializableLayer{ethHdrIPv4, ipv4Hdr, testPayload}, []uint16{}}, + {"single VLAN", []gopacket.SerializableLayer{ipv4Hdr, testPayload}, []gopacket.SerializableLayer{ethHdrVLAN, vlanTagInner, ipv4Hdr, testPayload}, []uint16{200}}, + {"QinQ", []gopacket.SerializableLayer{ipv4Hdr, testPayload}, []gopacket.SerializableLayer{ethHdrVLAN, vlanTagOuter, vlanTagInner, ipv4Hdr, testPayload}, []uint16{100, 200}}, + } { + t.Run(tt.name, func(t *testing.T) { + testBuf := gopacket.NewSerializeBuffer() + err := gopacket.SerializeLayers(testBuf, opts, tt.testLayers...) + if err != nil { + t.Errorf("error serializing in gopacket (not our bug!)") + } + + var want []byte + if tt.wantLayers == nil { + want = nil + } else { + wantBuf := gopacket.NewSerializeBuffer() + err := gopacket.SerializeLayers(wantBuf, opts, tt.wantLayers...) + if err != nil { + t.Errorf("error serializing in gopacket (not our bug!)") + } + want = wantBuf.Bytes() + } + + testBytes := testBuf.Bytes() + test := addEthernetHdr(testBytes, BroadcastMac, testMac, etherIPv4Proto, tt.vlanConfig) + + if !bytes.Equal(test, want) { + t.Errorf("got %v, want %v", test, want) + } + }) + } +} diff --git a/dhcpv4/nclient4/lease_test.go b/dhcpv4/nclient4/lease_test.go index d9377e7a..c7fdbcbd 100644 --- a/dhcpv4/nclient4/lease_test.go +++ b/dhcpv4/nclient4/lease_test.go @@ -1,4 +1,6 @@ // this tests nclient4 with lease, renew and release +//go:build go1.12 && linux +// +build go1.12,linux package nclient4 @@ -29,7 +31,7 @@ func (lk testLeaseKey) compare(b testLeaseKey) bool { return true } -//this represents one test case +// this represents one test case type testServerLease struct { key *testLeaseKey assignedAddr net.IP @@ -64,10 +66,10 @@ func (sll *testServerLeaseList) getKey(m *dhcpv4.DHCPv4) *testLeaseKey { } -//use following setting to handle DORA -//server-id: 1.2.3.4 -//subnet-mask: /24 -//return address from sll.list +// use following setting to handle DORA +// server-id: 1.2.3.4 +// subnet-mask: /24 +// return address from sll.list func (sll *testServerLeaseList) testLeaseDORAHandle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) error { reply, err := dhcpv4.NewReplyFromRequest(m) if err != nil { @@ -99,7 +101,7 @@ func (sll *testServerLeaseList) testLeaseDORAHandle(conn net.PacketConn, peer ne return nil } -//return check list for options must and may in the release msg according to RFC2131,section 4.4.1 +// return check list for options must and may in the release msg according to RFC2131,section 4.4.1 func (sll *testServerLeaseList) getCheckList() (mustHaveOpts, mayHaveOpts map[uint8]bool) { mustHaveOpts = make(map[uint8]bool) mayHaveOpts = make(map[uint8]bool) @@ -112,7 +114,7 @@ func (sll *testServerLeaseList) getCheckList() (mustHaveOpts, mayHaveOpts map[ui } -//check request message according to RFC2131, section 4.4.1 +// check request message according to RFC2131, section 4.4.1 func (sll *testServerLeaseList) testLeaseReleaseHandle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) error { if m.HopCount != 0 { diff --git a/dhcpv4/nclient4/udp.go b/dhcpv4/nclient4/udp.go new file mode 100644 index 00000000..5dcddde4 --- /dev/null +++ b/dhcpv4/nclient4/udp.go @@ -0,0 +1,64 @@ +//go:build go1.12 +// +build go1.12 + +package nclient4 + +import ( + "errors" + "net" + + "github.com/u-root/uio/uio" +) + +var ( + // ErrUDPAddrIsRequired is an error used when a passed argument is not of type "*net.UDPAddr". + ErrUDPAddrIsRequired = errors.New("must supply UDPAddr") +) + +func udpMatch(addr *net.UDPAddr, bound *net.UDPAddr) bool { + if bound == nil { + return true + } + if bound.IP != nil && !bound.IP.Equal(addr.IP) { + return false + } + return bound.Port == addr.Port +} + +func getUDP4pkt(pkt []byte, boundAddr *net.UDPAddr) ([]byte, *net.UDPAddr) { + buf := uio.NewBigEndianBuffer(pkt) + + ipHdr := ipv4(buf.Data()) + + if !ipHdr.isValid(len(pkt)) { + return nil, nil + } + + ipHdr = ipv4(buf.Consume(int(ipHdr.headerLength()))) + + if ipHdr.transportProtocol() != udpProtocolNumber { + return nil, nil + } + + if !buf.Has(udpMinimumSize) { + return nil, nil + } + + udpHdr := udp(buf.Consume(udpMinimumSize)) + + addr := &net.UDPAddr{ + IP: ipHdr.destinationAddress(), + Port: int(udpHdr.destinationPort()), + } + if !udpMatch(addr, boundAddr) { + return nil, nil + } + srcAddr := &net.UDPAddr{ + IP: ipHdr.sourceAddress(), + Port: int(udpHdr.sourcePort()), + } + // Extra padding after end of IP packet should be ignored, + // if not dhcp option parsing will fail. + dhcpLen := int(ipHdr.payloadLength()) - udpMinimumSize + return buf.Consume(dhcpLen), srcAddr +} diff --git a/go.mod b/go.mod index f93fee90..c0ca1d8f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/jsimonetti/rtnetlink v1.3.5 github.com/mdlayher/netlink v1.7.2 github.com/mdlayher/packet v1.1.2 + github.com/mdlayher/raw v0.1.0 github.com/stretchr/testify v1.6.1 github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 golang.org/x/net v0.23.0 @@ -16,6 +17,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.0 // indirect + github.com/google/gopacket v1.1.19 github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect diff --git a/go.sum b/go.sum index 6b08a535..4f7fec63 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= @@ -14,6 +16,8 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= +github.com/mdlayher/raw v0.1.0 h1:K4PFMVy+AFsp0Zdlrts7yNhxc/uXoPVHi9RzRvtZF2Y= +github.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5s9Sg= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= @@ -26,14 +30,26 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=