From 86e772021e25db78b762b6481c560c86f4bab256 Mon Sep 17 00:00:00 2001 From: vista Date: Tue, 12 Mar 2024 19:28:41 +0100 Subject: [PATCH 1/9] Add true *BSD support for nclient4 Signed-off-by: vista --- dhcpv4/nclient4/conn.go | 68 +++++++++++ dhcpv4/nclient4/conn_linux.go | 100 +++++++++++++++ dhcpv4/nclient4/conn_unix.go | 224 +++++++++++++++++++++++----------- go.mod | 1 + go.sum | 2 + 5 files changed, 324 insertions(+), 71 deletions(-) create mode 100644 dhcpv4/nclient4/conn.go create mode 100644 dhcpv4/nclient4/conn_linux.go diff --git a/dhcpv4/nclient4/conn.go b/dhcpv4/nclient4/conn.go new file mode 100644 index 00000000..eb1a6a00 --- /dev/null +++ b/dhcpv4/nclient4/conn.go @@ -0,0 +1,68 @@ +package nclient4 + +import ( + "errors" + "net" + + "github.com/u-root/uio/uio" +) + +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}) +) + +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/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..c7ebe732 100644 --- a/dhcpv4/nclient4/conn_unix.go +++ b/dhcpv4/nclient4/conn_unix.go @@ -1,94 +1,153 @@ -// 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" + "encoding/binary" "io" "net" - "github.com/mdlayher/packet" + "github.com/mdlayher/raw" "github.com/u-root/uio/uio" - "golang.org/x/sys/unix" ) -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 + + etherIPv4Proto uint16 = 0x0800 + ethHdrMinimum int = 14 -var ( - // ErrUDPAddrIsRequired is an error used when a passed argument is not of type "*net.UDPAddr". - ErrUDPAddrIsRequired = errors.New("must supply UDPAddr") + vlanTagLen int = 4 + vlanMax uint16 = 0x0FFF + vlanTPID uint16 = 0x8100 ) +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 + } + + 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 +// 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 + 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 bytes + 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 correctly consumed + vlanStackIsCorrect = true + } else { + // VLAN tags remaining in configured stack -> not a match + vlanStackIsCorrect = false + } + return vlanStackIsCorrect + default: + vlanStackIsCorrect = false + return vlanStackIsCorrect + } } - if bound.IP != nil && !bound.IP.Equal(addr.IP) { - return false +} + +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 bound.Port == addr.Port + + return buf.Data() } -// 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) { + ethHdrLen := ethHdrMinimum + 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 +155,70 @@ 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) { + // We're only interested in properly tagged packets + pkt = getEthernetPayload(pkt[:n], upc.VLANs) + if pkt == nil { continue } - - ipHdr = ipv4(buf.Consume(int(ipHdr.headerLength()))) - - if ipHdr.transportProtocol() != udpProtocolNumber { + dhcpPkt, srcAddr := getUDP4pkt(pkt[:n], upc.boundAddr) + if dhcpPkt == nil { continue } - if !buf.Has(udpHdrLen) { - continue - } + return copy(b, dhcpPkt), srcAddr, nil + } +} - udpHdr := udp(buf.Consume(udpHdrLen)) +// createVLANTag returns the bytes of a 4-byte long 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 + tci |= dei << 12 + tci |= vlan + binary.BigEndian.PutUint16(vlanTag[2:], tci) + + return vlanTag +} - 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 +// 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 := ethHdrMinimum + 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 } // 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, this implementation does not work well with VLAN +// tagging and such. func (upc *BroadcastRawUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { udpAddr, ok := addr.(*net.UDPAddr) if !ok { @@ -149,6 +228,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/go.mod b/go.mod index f93fee90..9fcd8e23 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 diff --git a/go.sum b/go.sum index 6b08a535..b0584f77 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,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= From 1f863846aea7a5013399027aad5f49135861aef5 Mon Sep 17 00:00:00 2001 From: vista Date: Tue, 12 Mar 2024 19:47:27 +0100 Subject: [PATCH 2/9] Improve comments and formatting Signed-off-by: vista --- dhcpv4/nclient4/conn_unix.go | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/dhcpv4/nclient4/conn_unix.go b/dhcpv4/nclient4/conn_unix.go index c7ebe732..4d675635 100644 --- a/dhcpv4/nclient4/conn_unix.go +++ b/dhcpv4/nclient4/conn_unix.go @@ -90,7 +90,7 @@ func processVLANStack(buf *uio.Lexer, vlans []uint16) bool { switch etherType := binary.BigEndian.Uint16(buf.Consume(2)); etherType { case vlanTPID: tci := binary.BigEndian.Uint16(buf.Consume(2)) - vlanID := tci & vlanMax // Mask first 4 bytes + vlanID := tci & vlanMax // Mask first 4 bits if len(configuredVLANs) != 0 { currentVLAN, configuredVLANs = configuredVLANs[0], configuredVLANs[1:] if vlanID != currentVLAN { @@ -117,6 +117,13 @@ func processVLANStack(buf *uio.Lexer, vlans []uint16) bool { } } +// 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) @@ -138,6 +145,10 @@ func getEthernetPayload(pkt []byte, vlans []uint16) []byte { return buf.Data() } +// 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) { ethHdrLen := ethHdrMinimum if len(upc.VLANs) > 0 { @@ -156,9 +167,9 @@ func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF } - // We're only interested in properly tagged packets pkt = getEthernetPayload(pkt[:n], upc.VLANs) if pkt == nil { + // VLAN stack does not match our configuration continue } dhcpPkt, srcAddr := getUDP4pkt(pkt[:n], upc.boundAddr) @@ -170,8 +181,8 @@ func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { } } -// createVLANTag returns the bytes of a 4-byte long VLAN tag, which can be inserted -// in an Ethernet frame header. +// 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) @@ -179,11 +190,11 @@ func createVLANTag(vlan uint16) []byte { 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 - tci |= dei << 12 - tci |= vlan + 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 @@ -202,6 +213,7 @@ func addEthernetHdr(b []byte, dstMac, srcMac net.HardwareAddr, etherProto uint16 offset += len(dstMac) copy(b[offset:], srcMac) offset += len(srcMac) + for _, vlan := range vlans { copy(b[offset:], createVLANTag(vlan)) offset += vlanTagLen @@ -217,8 +229,7 @@ func addEthernetHdr(b []byte, dstMac, srcMac net.HardwareAddr, etherProto uint16 // // 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, this implementation does not work well with VLAN -// tagging and such. +// 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 { From 63e1d3707c96ae1c05d5c8f42b04630f04c229f8 Mon Sep 17 00:00:00 2001 From: vista Date: Tue, 12 Mar 2024 20:28:25 +0100 Subject: [PATCH 3/9] Move Ethernet and VLAN-related nclient4 functions into separate file Signed-off-by: vista --- dhcpv4/nclient4/conn_unix.go | 122 ---------------------------- dhcpv4/nclient4/ethernet_unix.go | 134 +++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 122 deletions(-) create mode 100644 dhcpv4/nclient4/ethernet_unix.go diff --git a/dhcpv4/nclient4/conn_unix.go b/dhcpv4/nclient4/conn_unix.go index 4d675635..7f422ad6 100644 --- a/dhcpv4/nclient4/conn_unix.go +++ b/dhcpv4/nclient4/conn_unix.go @@ -5,23 +5,14 @@ package nclient4 import ( - "encoding/binary" "io" "net" "github.com/mdlayher/raw" - "github.com/u-root/uio/uio" ) const ( bpfFilterBidirectional int = 1 - - etherIPv4Proto uint16 = 0x0800 - ethHdrMinimum int = 14 - - vlanTagLen int = 4 - vlanMax uint16 = 0x0FFF - vlanTPID uint16 = 0x8100 ) var rawConnectionConfig = &raw.Config{ @@ -75,76 +66,6 @@ func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr, v } } -// 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 - 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 correctly consumed - vlanStackIsCorrect = true - } 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() -} - // ReadFrom implements net.PacketConn.ReadFrom. // // ReadFrom reads raw IP packets and will try to match them against @@ -181,49 +102,6 @@ func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { } } -// 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 := ethHdrMinimum - 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 -} - // WriteTo implements net.PacketConn.WriteTo and broadcasts all packets at the // raw socket level. // diff --git a/dhcpv4/nclient4/ethernet_unix.go b/dhcpv4/nclient4/ethernet_unix.go new file mode 100644 index 00000000..3d7b2acf --- /dev/null +++ b/dhcpv4/nclient4/ethernet_unix.go @@ -0,0 +1,134 @@ +//go:build go1.12 && (darwin || freebsd || netbsd || openbsd || dragonfly) +// +build go1.12 +// +build darwin freebsd netbsd openbsd dragonfly + +package nclient4 + +import ( + "encoding/binary" + "net" + + "github.com/u-root/uio/uio" +) + +const ( + etherIPv4Proto uint16 = 0x0800 + ethHdrMinimum int = 14 + + vlanTagLen int = 4 + vlanMax uint16 = 0x0FFF + vlanTPID uint16 = 0x8100 +) + +// 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 + 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 correctly consumed + vlanStackIsCorrect = true + } 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 := ethHdrMinimum + 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 +} From 6b937331216c8cfdeba7fe5d8c5b404605a23a4e Mon Sep 17 00:00:00 2001 From: vista Date: Wed, 13 Mar 2024 14:10:47 +0100 Subject: [PATCH 4/9] Fix logic bug in VLAN stack parsing, remove build scoping from ethernet.go Signed-off-by: vista --- dhcpv4/nclient4/{ethernet_unix.go => ethernet.go} | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) rename dhcpv4/nclient4/{ethernet_unix.go => ethernet.go} (94%) diff --git a/dhcpv4/nclient4/ethernet_unix.go b/dhcpv4/nclient4/ethernet.go similarity index 94% rename from dhcpv4/nclient4/ethernet_unix.go rename to dhcpv4/nclient4/ethernet.go index 3d7b2acf..a03f8a19 100644 --- a/dhcpv4/nclient4/ethernet_unix.go +++ b/dhcpv4/nclient4/ethernet.go @@ -1,6 +1,5 @@ -//go:build go1.12 && (darwin || freebsd || netbsd || openbsd || dragonfly) +//go:build go1.12 // +build go1.12 -// +build darwin freebsd netbsd openbsd dragonfly package nclient4 @@ -27,7 +26,7 @@ const ( // 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 + var vlanStackIsCorrect bool = true configuredVLANs := make([]uint16, len(vlans)) copy(configuredVLANs, vlans) @@ -48,8 +47,8 @@ func processVLANStack(buf *uio.Lexer, vlans []uint16) bool { } case etherIPv4Proto: if len(configuredVLANs) == 0 { - // Packet VLAN stack has been correctly consumed - vlanStackIsCorrect = true + // Packet VLAN stack has been consumed, return result + return vlanStackIsCorrect } else { // VLAN tags remaining in configured stack -> not a match vlanStackIsCorrect = false From 445357b43fd71245ce5d47bba563c2d3a668c934 Mon Sep 17 00:00:00 2001 From: vista Date: Wed, 13 Mar 2024 14:11:26 +0100 Subject: [PATCH 5/9] Add build scoping to client_ and lease_test.go (socketpair only supports Linux platforms) Signed-off-by: vista --- dhcpv4/nclient4/client_test.go | 3 ++- dhcpv4/nclient4/lease_test.go | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) 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/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 { From 4757488724a845df0cc8914c0c2ddf30477f5fbb Mon Sep 17 00:00:00 2001 From: vista Date: Wed, 13 Mar 2024 14:12:02 +0100 Subject: [PATCH 6/9] Add unit tests for Ethernet and VLAN manipulation functions Signed-off-by: vista --- dhcpv4/nclient4/ethernet_test.go | 176 +++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 dhcpv4/nclient4/ethernet_test.go diff --git a/dhcpv4/nclient4/ethernet_test.go b/dhcpv4/nclient4/ethernet_test.go new file mode 100644 index 00000000..6ed586e1 --- /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 TestAddEthernetHdrTwo(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) + } + }) + } +} From 20b1aea67e6d0bb1b688298d38d35eac30c95422 Mon Sep 17 00:00:00 2001 From: vista Date: Sat, 23 Mar 2024 12:53:20 +0100 Subject: [PATCH 7/9] Add gopacket dependency for Ethernet functionality testing Signed-off-by: vista --- go.mod | 1 + go.sum | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/go.mod b/go.mod index 9fcd8e23..c0ca1d8f 100644 --- a/go.mod +++ b/go.mod @@ -17,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 b0584f77..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= @@ -28,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= From 2b6b08dd9558b52a4f41ec99e2f69a5af903ea23 Mon Sep 17 00:00:00 2001 From: vista Date: Sat, 23 Mar 2024 13:05:53 +0100 Subject: [PATCH 8/9] Move UDP-related functions and variables into separate file, add/fix some comments Signed-off-by: vista --- dhcpv4/nclient4/conn_unix.go | 8 ++++---- dhcpv4/nclient4/ethernet.go | 11 +++++++++-- dhcpv4/nclient4/{conn.go => udp.go} | 10 +++------- 3 files changed, 16 insertions(+), 13 deletions(-) rename dhcpv4/nclient4/{conn.go => udp.go} (86%) diff --git a/dhcpv4/nclient4/conn_unix.go b/dhcpv4/nclient4/conn_unix.go index 7f422ad6..1f648324 100644 --- a/dhcpv4/nclient4/conn_unix.go +++ b/dhcpv4/nclient4/conn_unix.go @@ -21,8 +21,6 @@ var rawConnectionConfig = &raw.Config{ // 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, vlans ...uint16) (net.PacketConn, error) { ifc, err := net.InterfaceByName(iface) if err != nil { @@ -68,8 +66,10 @@ func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr, v // 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 packets, parses the VLAN stack (if configured) +// and will try to match the IP+UDP destinations against upc.boundAddr. +// +// Any matching packets are returned via the given buffer. func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { ethHdrLen := ethHdrMinimum if len(upc.VLANs) > 0 { diff --git a/dhcpv4/nclient4/ethernet.go b/dhcpv4/nclient4/ethernet.go index a03f8a19..aa73a4f9 100644 --- a/dhcpv4/nclient4/ethernet.go +++ b/dhcpv4/nclient4/ethernet.go @@ -19,11 +19,18 @@ const ( 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 +// 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 +// 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 diff --git a/dhcpv4/nclient4/conn.go b/dhcpv4/nclient4/udp.go similarity index 86% rename from dhcpv4/nclient4/conn.go rename to dhcpv4/nclient4/udp.go index eb1a6a00..5dcddde4 100644 --- a/dhcpv4/nclient4/conn.go +++ b/dhcpv4/nclient4/udp.go @@ -1,3 +1,6 @@ +//go:build go1.12 +// +build go1.12 + package nclient4 import ( @@ -7,13 +10,6 @@ import ( "github.com/u-root/uio/uio" ) -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}) -) - var ( // ErrUDPAddrIsRequired is an error used when a passed argument is not of type "*net.UDPAddr". ErrUDPAddrIsRequired = errors.New("must supply UDPAddr") From 3191f87498c204b26e12e1c48291ae0a71e755bc Mon Sep 17 00:00:00 2001 From: vista Date: Sat, 23 Mar 2024 13:07:14 +0100 Subject: [PATCH 9/9] Further comment fixes, better variable/function naming Signed-off-by: vista --- dhcpv4/nclient4/conn_unix.go | 9 ++++++--- dhcpv4/nclient4/ethernet.go | 4 ++-- dhcpv4/nclient4/ethernet_test.go | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/dhcpv4/nclient4/conn_unix.go b/dhcpv4/nclient4/conn_unix.go index 1f648324..c35bfe49 100644 --- a/dhcpv4/nclient4/conn_unix.go +++ b/dhcpv4/nclient4/conn_unix.go @@ -21,6 +21,8 @@ var rawConnectionConfig = &raw.Config{ // 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, vlans ...uint16) (net.PacketConn, error) { ifc, err := net.InterfaceByName(iface) if err != nil { @@ -34,6 +36,7 @@ func NewRawUDPConn(iface string, port int, vlans ...uint16) (net.PacketConn, err 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 @@ -66,12 +69,12 @@ func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr, v // ReadFrom implements net.PacketConn.ReadFrom. // -// ReadFrom reads raw Ethernet packets, parses the VLAN stack (if configured) -// and will try to match the IP+UDP destinations against upc.boundAddr. +// 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 := ethHdrMinimum + ethHdrLen := ethHdrBaseLen if len(upc.VLANs) > 0 { ethHdrLen += len(upc.VLANs) * vlanTagLen } diff --git a/dhcpv4/nclient4/ethernet.go b/dhcpv4/nclient4/ethernet.go index aa73a4f9..b21cbfd1 100644 --- a/dhcpv4/nclient4/ethernet.go +++ b/dhcpv4/nclient4/ethernet.go @@ -12,7 +12,7 @@ import ( const ( etherIPv4Proto uint16 = 0x0800 - ethHdrMinimum int = 14 + ethHdrBaseLen int = 14 vlanTagLen int = 4 vlanMax uint16 = 0x0FFF @@ -118,7 +118,7 @@ func createVLANTag(vlan uint16) []byte { // 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 := ethHdrMinimum + ethHdrLen := ethHdrBaseLen if len(vlans) > 0 { ethHdrLen += len(vlans) * vlanTagLen } diff --git a/dhcpv4/nclient4/ethernet_test.go b/dhcpv4/nclient4/ethernet_test.go index 6ed586e1..322f30db 100644 --- a/dhcpv4/nclient4/ethernet_test.go +++ b/dhcpv4/nclient4/ethernet_test.go @@ -82,7 +82,7 @@ func TestProcessVLANStack(t *testing.T) { } 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 + // 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} @@ -135,7 +135,7 @@ func TestGetEthernetPayload(t *testing.T) { } } -func TestAddEthernetHdrTwo(t *testing.T) { +func TestAddEthernetHdr(t *testing.T) { for _, tt := range []struct { name string testLayers []gopacket.SerializableLayer