blob: b61af8007e5bbf1d790dc2ce4a339d9851d969af [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun56a7ae62020-10-29 11:03:30 +01002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun56a7ae62020-10-29 11:03:30 +01003
4package transport
5
6import (
7 "errors"
8 "fmt"
9 "math"
10 "net"
11 "time"
12
13 "github.com/google/gopacket"
14 "github.com/google/gopacket/layers"
15 "github.com/insomniacslk/dhcp/dhcpv4"
Tim Windelschmidt786134d2023-11-13 16:55:14 +010016 "github.com/mdlayher/packet"
Lorenz Brun56a7ae62020-10-29 11:03:30 +010017 "golang.org/x/net/bpf"
18)
19
20const (
Serge Bazanski216fe7b2021-05-21 18:36:16 +020021 // RFC2474 Section 4.2.2.1 with reference to RFC791 Section 3.1 (Network
22 // Control Precedence)
Lorenz Brun56a7ae62020-10-29 11:03:30 +010023 dscpCS7 = 0x7 << 3
24
25 // IPv4 MTU
26 maxIPv4MTU = math.MaxUint16 // IPv4 "Total Length" field is an unsigned 16 bit integer
27)
28
29// mustAssemble calls bpf.Assemble and panics if it retuns an error.
30func mustAssemble(insns []bpf.Instruction) []bpf.RawInstruction {
31 rawInsns, err := bpf.Assemble(insns)
32 if err != nil {
33 panic("mustAssemble failed to assemble BPF: " + err.Error())
34 }
35 return rawInsns
36}
37
38// BPF filter for UDP in IPv4 with destination port 68 (DHCP Client)
39//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020040// This is used to make the kernel drop non-DHCP traffic for us so that we
41// don't have to handle excessive unrelated traffic flowing on a given link
42// which might overwhelm the single-threaded receiver.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010043var bpfFilterInstructions = []bpf.Instruction{
44 // Check IP protocol version equals 4 (first 4 bits of the first byte)
Serge Bazanski216fe7b2021-05-21 18:36:16 +020045 // With Ethernet II framing, this is more of a sanity check. We already
46 // request the kernel to only return EtherType 0x0800 (IPv4) frames.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010047 bpf.LoadAbsolute{Off: 0, Size: 1},
48 bpf.ALUOpConstant{Op: bpf.ALUOpAnd, Val: 0xf0}, // SubnetMask second 4 bits
49 bpf.JumpIf{Cond: bpf.JumpEqual, Val: 4 << 4, SkipTrue: 1},
50 bpf.RetConstant{Val: 0}, // Discard
51
52 // Check IPv4 Protocol byte (offset 9) equals UDP
53 bpf.LoadAbsolute{Off: 9, Size: 1},
54 bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(layers.IPProtocolUDP), SkipTrue: 1},
55 bpf.RetConstant{Val: 0}, // Discard
56
57 // Check if IPv4 fragment offset is all-zero (this is not a fragment)
58 bpf.LoadAbsolute{Off: 6, Size: 2},
59 bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: 0x1fff, SkipFalse: 1},
60 bpf.RetConstant{Val: 0}, // Discard
61
62 // Load IPv4 header size from offset zero and store it into X
63 bpf.LoadMemShift{Off: 0},
64
65 // Check if UDP header destination port equals 68
66 bpf.LoadIndirect{Off: 2, Size: 2}, // Offset relative to header size in register X
67 bpf.JumpIf{Cond: bpf.JumpEqual, Val: 68, SkipTrue: 1},
68 bpf.RetConstant{Val: 0}, // Discard
69
70 // Accept packet and pass through up maximum IP packet size
71 bpf.RetConstant{Val: maxIPv4MTU},
72}
73
74var bpfFilter = mustAssemble(bpfFilterInstructions)
75
Serge Bazanski216fe7b2021-05-21 18:36:16 +020076// BroadcastTransport implements a DHCP transport based on a custom IP/UDP
77// stack fulfilling the specific requirements for broadcasting DHCP packets
78// (like all-zero source address, no ARP, ...)
Lorenz Brun56a7ae62020-10-29 11:03:30 +010079type BroadcastTransport struct {
Tim Windelschmidt786134d2023-11-13 16:55:14 +010080 rawConn *packet.Conn
Lorenz Brun56a7ae62020-10-29 11:03:30 +010081 iface *net.Interface
82}
83
84func NewBroadcastTransport(iface *net.Interface) *BroadcastTransport {
85 return &BroadcastTransport{iface: iface}
86}
87
88func (t *BroadcastTransport) Open() error {
89 if t.rawConn != nil {
90 return errors.New("broadcast transport already open")
91 }
Tim Windelschmidt786134d2023-11-13 16:55:14 +010092 rawConn, err := packet.Listen(t.iface, packet.Datagram, int(layers.EthernetTypeIPv4), &packet.Config{
93 Filter: bpfFilter,
Lorenz Brun56a7ae62020-10-29 11:03:30 +010094 })
95 if err != nil {
96 return fmt.Errorf("failed to create raw listener: %w", err)
97 }
98 t.rawConn = rawConn
99 return nil
100}
101
102func (t *BroadcastTransport) Send(payload *dhcpv4.DHCPv4) error {
103 if t.rawConn == nil {
104 return errors.New("broadcast transport closed")
105 }
Tim Windelschmidt786134d2023-11-13 16:55:14 +0100106 pkt := gopacket.NewSerializeBuffer()
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100107 opts := gopacket.SerializeOptions{
108 ComputeChecksums: true,
109 FixLengths: true,
110 }
111
112 ipLayer := &layers.IPv4{
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200113 Version: 4,
114 // Shift left of ECN field
115 TOS: dscpCS7 << 2,
116 // These packets should never be routed (their IP headers contain
117 // garbage)
118 TTL: 1,
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100119 Protocol: layers.IPProtocolUDP,
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200120 // Most DHCP servers don't support fragmented packets.
121 Flags: layers.IPv4DontFragment,
122 DstIP: net.IPv4bcast,
123 SrcIP: net.IPv4zero,
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100124 }
125 udpLayer := &layers.UDP{
126 DstPort: 67,
127 SrcPort: 68,
128 }
129 if err := udpLayer.SetNetworkLayerForChecksum(ipLayer); err != nil {
130 panic("Invalid layer stackup encountered")
131 }
132
Tim Windelschmidt786134d2023-11-13 16:55:14 +0100133 err := gopacket.SerializeLayers(pkt, opts,
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100134 ipLayer,
135 udpLayer,
136 gopacket.Payload(payload.ToBytes()))
137
138 if err != nil {
139 return fmt.Errorf("failed to assemble packet: %w", err)
140 }
141
Tim Windelschmidt786134d2023-11-13 16:55:14 +0100142 _, err = t.rawConn.WriteTo(pkt.Bytes(), &packet.Addr{HardwareAddr: layers.EthernetBroadcast})
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100143 if err != nil {
144 return fmt.Errorf("failed to transmit broadcast packet: %w", err)
145 }
146 return nil
147}
148
149func (t *BroadcastTransport) Receive() (*dhcpv4.DHCPv4, error) {
150 if t.rawConn == nil {
151 return nil, errors.New("broadcast transport closed")
152 }
153 buf := make([]byte, math.MaxUint16) // Maximum IP packet size
154 n, _, err := t.rawConn.ReadFrom(buf)
155 if err != nil {
156 return nil, deadlineFromTimeout(err)
157 }
158 respPacket := gopacket.NewPacket(buf[:n], layers.LayerTypeIPv4, gopacket.Default)
159 ipLayer := respPacket.Layer(layers.LayerTypeIPv4)
160 if ipLayer == nil {
161 return nil, NewInvalidMessageError(errors.New("got invalid IP packet"))
162 }
163 ip := ipLayer.(*layers.IPv4)
164 if ip.Flags&layers.IPv4MoreFragments != 0 {
165 return nil, NewInvalidMessageError(errors.New("got fragmented message"))
166 }
167
168 udpLayer := respPacket.Layer(layers.LayerTypeUDP)
169 if udpLayer == nil {
170 return nil, NewInvalidMessageError(errors.New("got non-UDP packet"))
171 }
172 udp := udpLayer.(*layers.UDP)
173 if udp.DstPort != 68 {
174 return nil, NewInvalidMessageError(errors.New("message not for DHCP client port"))
175 }
176 msg, err := dhcpv4.FromBytes(udp.Payload)
177 if err != nil {
178 return nil, NewInvalidMessageError(fmt.Errorf("failed to decode DHCPv4 message: %w", err))
179 }
180 return msg, nil
181}
182
183func (t *BroadcastTransport) Close() error {
184 if t.rawConn == nil {
185 return nil
186 }
187 if err := t.rawConn.Close(); err != nil {
188 return err
189 }
190 t.rawConn = nil
191 return nil
192}
193
194func (t *BroadcastTransport) SetReceiveDeadline(deadline time.Time) error {
195 if t.rawConn == nil {
196 return errors.New("broadcast transport closed")
197 }
198 return t.rawConn.SetReadDeadline(deadline)
199}