Skip to content

Commit c6cb16d

Browse files
committed
Add support for DHCP relays + fix bugs
- Fix header.Op to always be BOOT_REPLY, rather than the code of the dhcp response type - Add support for DHCP relays, tested against isc-dhcp-relay
1 parent 110f5c8 commit c6cb16d

10 files changed

+165
-42
lines changed

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ I wanted to make a small and safe Go solution, as well as to learn the DHCP prot
1515
2. go build
1616
3. Configure conf.yaml
1717
3. Run with permissions needed to listen on port 67 (eg run as root or use linux capabilities), as follows
18-
4. Run a separate VM on the same bridge/vlan as a dhcp client. This is tested against Alpine's udhcpc and ubuntu's dhclient
18+
4. Run a separate VM on the same bridge/vlan as a dhcp client
1919

2020
### Configuration
2121

@@ -72,16 +72,18 @@ root@ubuntu2:~#
7272

7373
## Status
7474

75-
Verified to work with Alpine's `udhcpc` client, Ubuntu's `dhclient` client, and Windows 10.
75+
- Verified to work with Alpine's `udhcpc` client, Ubuntu's `dhclient` client, and Windows 10.
76+
- Relay requests verified to work with isc-dhcp-relay.
7677

7778
## Implemented
7879

7980
- Bare minimum wire protocol for DHCPDISCOVER, DHCPOFFER, DHCPREQUEST, DHCPNAK, DHCPACK, and DHCPRELEASE to work
80-
- Multiple IP Pools, sourced from configuration
81+
- Supports relayed requests
82+
- Supports multiple IP Pools, sourced from configuration
8183

8284
## TODO
8385

84-
- Support relay requests, and acting as a relay
86+
- Support acting as a relay
8587
- Support arbitrary options
8688
- Support hosts in config with hardcoded IPs, based on mac address
8789
- More Tests

app.go

+51-13
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ func (a *App) oObToInterface(oob []byte) (*net.Interface, error) {
9090
return iface, nil
9191
}
9292

93+
// For non-relayed requests: find a pool by comparing nets to local nic
94+
// IPs
9395
func (a *App) findPoolByInterface(iface *net.Interface) (*Pool, error) {
9496
addrs, err := iface.Addrs()
9597

@@ -117,7 +119,32 @@ func (a *App) findPoolByInterface(iface *net.Interface) (*Pool, error) {
117119
return nil, errors.New("Not found")
118120
}
119121

122+
// For relayed requests: find a pool by comparing giaddr to configured
123+
// pool nets
124+
func (a *App) findPoolbyGiaddr(giaddr FixedV4) (*Pool, error) {
125+
for _, pool := range a.ipnet2pool {
126+
ipnet := &net.IPNet{
127+
IP: pool.Network,
128+
Mask: net.IPMask([]byte(pool.Netmask)),
129+
}
130+
if ipnet.Contains(giaddr.NetIp()) {
131+
return pool, nil
132+
}
133+
}
134+
135+
return nil, errors.New("Not found")
136+
}
137+
120138
func (a *App) DispatchMessage(myBuf, myOob []byte, remote *net.UDPAddr, localSocket *net.UDPConn) {
139+
// Sanity remote port check
140+
if remote.Port != 67 && remote.Port != 68 {
141+
log.Printf("Ignoring DHCP packet with source port %d rather than 67 or 68", remote.Port)
142+
return
143+
}
144+
145+
var err error
146+
147+
// Grab iface and verify we're configured to work on it
121148
iface, err := a.oObToInterface(myOob)
122149
if err != nil {
123150
log.Printf("Failed parsing interface out of OOB: %v", err)
@@ -129,30 +156,41 @@ func (a *App) DispatchMessage(myBuf, myOob []byte, remote *net.UDPAddr, localSoc
129156
return
130157
}
131158

132-
pool, err := a.findPoolByInterface(iface)
159+
// Parse entire dhcp message
160+
message, err := ParseDhcpMessage(myBuf)
133161
if err != nil {
134-
log.Printf("Can't find pool based on IPs bound to %v", iface.Name)
162+
log.Printf("Failed parsing dhcp packet: %v", err)
135163
return
136164
}
137165

138-
if remote.Port != 68 {
139-
log.Printf("Ignoring DHCP packet with source port %d rather than 68", remote.Port)
140-
return
141-
}
166+
var pool *Pool
142167

143-
message, err := ParseDhcpMessage(myBuf)
144-
if err != nil {
145-
log.Printf("Failed parsing dhcp packet: %v", err)
146-
return
168+
// Relayed request. Find pool based on giaddr
169+
if !message.Header.GatewayAddr.Empty() {
170+
pool, err = a.findPoolbyGiaddr(message.Header.GatewayAddr)
171+
if err != nil {
172+
log.Printf("Can't find pool based on IPs bound to %v", iface.Name)
173+
return
174+
}
175+
176+
} else {
177+
pool, err = a.findPoolByInterface(iface)
178+
if err != nil {
179+
log.Printf("Can't find pool based on IPs bound to %v", iface.Name)
180+
return
181+
}
147182
}
148183

149184
handler := NewRequestHandler(message, pool)
150185

151186
response := handler.Handle()
152187

153188
if response != nil {
154-
// FIXME: options to sending to unicast, sending to relay, etc. Move these send functions
155-
// somewhere else.
156-
handler.sendMessageBroadcast(response, localSocket)
189+
// In the case of a relayed request, send the response unicast to the relaying server
190+
if !message.Header.GatewayAddr.Empty() {
191+
handler.sendMessageRelayed(response, message.Header.GatewayAddr, localSocket)
192+
} else {
193+
handler.sendMessageBroadcast(response, localSocket)
194+
}
157195
}
158196
}

conf.yaml

+13-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,20 @@ pools:
99
routers: [ 172.17.0.1 ]
1010
dns: [ 1.1.1.1, 8.8.8.8 ]
1111

12+
# Example pool acting as a relay, using a separate
13+
# net on eth2
14+
- name: other network
15+
network: 192.168.0.0
16+
mask: 255.255.255.0
17+
start: 192.168.0.200
18+
end: 192.168.0.200
19+
leasetime: 60
20+
myip: 192.168.0.1
21+
routers: [ 192.168.0.1 ]
22+
dns: [ 1.1.1.1, 8.8.8.8 ]
23+
1224
leasedir: .
13-
interfaces: [ eth1 ]
25+
interfaces: [ eth1, eth2 ]
1426

1527
# The following makes more sense
1628
#leasedir: /var/lib/godhcpd

constants.go

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package main
22

3+
//
4+
// DHCP Op types
5+
//
6+
const (
7+
BOOT_REQUEST = 1
8+
BOOT_REPLY = 2
9+
)
10+
311
//
412
// DHCP Message types
513
//

options.go

+16
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,27 @@ func (o *Options) GetAll() map[byte]Option {
4949
return o.data
5050
}
5151

52+
func (o *Options) Dump() {
53+
for _, key := range o.order {
54+
option := o.data[key]
55+
log.Printf("%v = %v (%+v)", key, string(""), option.Data)
56+
}
57+
}
58+
5259
func (o *Options) Get(code byte) (Option, bool) {
5360
option, ok := o.data[code]
5461
return option, ok
5562
}
5663

64+
func (o *Options) GetByte(code byte) byte {
65+
if option, ok := o.data[code]; ok {
66+
if len(option.Data) == 1 {
67+
return option.Data[0]
68+
}
69+
}
70+
return 0
71+
}
72+
5773
func (o *Options) Set(code byte, data []byte) {
5874
option := Option{
5975
Data: data,

payload.go

+2-8
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,8 @@ func ParseDhcpMessage(buf []byte) (*DHCPMessage, error) {
4747
// Parse arbitrary options
4848
options := ParseOptions(reader)
4949

50-
// Confusingly, the Op type can be overridden using an option
51-
if option, ok := options.Get(OPTION_MESSAGE_TYPE); ok {
52-
if option.Header.Length == 1 {
53-
header.Op = option.Data[0]
54-
}
55-
}
56-
57-
// Similarly, so can the ClientAddr
50+
// ClientAddr overriden by option?
51+
// FIXME: verify if this logic is actually needed
5852
if option, ok := options.Get(OPTION_REQUESTED_IP); ok {
5953
if option.Header.Length == 4 {
6054
ip, err := BytesToFixedV4(option.Data)

payload_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func TestParseDhcpMessage(t *testing.T) {
1616
require.Nil(t, err)
1717

1818
require.Equal(t, "0:1c:42:b4:6e:1d", message.Header.Mac.String())
19-
require.Equal(t, byte(DHCPREQUEST), message.Header.Op)
19+
require.Equal(t, byte(DHCPREQUEST), message.Options.GetByte(OPTION_MESSAGE_TYPE))
2020
require.Equal(t, uint32(0x6effc930), message.Header.Identifier)
2121
require.Equal(t, IpToFixedV4(net.ParseIP("172.17.0.100")), message.Header.ClientAddr)
2222

request.go

+52-7
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func NewRequestHandler(message *DHCPMessage, pool *Pool) *RequestHandler {
2222
}
2323

2424
func (r *RequestHandler) Handle() *DHCPMessage {
25-
switch r.header.Op {
25+
switch r.options.GetByte(OPTION_MESSAGE_TYPE) {
2626
case DHCPDISCOVER:
2727
return r.HandleDiscover()
2828
case DHCPREQUEST:
@@ -66,7 +66,6 @@ func (r *RequestHandler) HandleRequest() *DHCPMessage {
6666
if lease, ok = r.pool.TouchLeaseByMac(mac); !ok {
6767
log.Printf("Unrecognized lease for %v", mac.String())
6868
return r.SendNAK()
69-
7069
}
7170

7271
// Verify IP matches what is in our lease
@@ -103,7 +102,7 @@ func (r *RequestHandler) HandleRelease() *DHCPMessage {
103102
// Share code for DHCPOFFER and DHCPACK
104103
func (r *RequestHandler) SendLeaseInfo(lease *Lease, op byte) *DHCPMessage {
105104
header := &MessageHeader{
106-
Op: op,
105+
Op: BOOT_REPLY,
107106
Hops: 0,
108107
Identifier: r.header.Identifier,
109108
YourAddr: lease.IP,
@@ -150,23 +149,27 @@ func (r *RequestHandler) SendLeaseInfo(lease *Lease, op byte) *DHCPMessage {
150149

151150
func (r *RequestHandler) SendNAK() *DHCPMessage {
152151
header := &MessageHeader{
153-
Op: DHCPNAK,
152+
Op: BOOT_REPLY,
154153
Hops: 0,
155154
Identifier: r.header.Identifier,
156155
ServerAddr: r.pool.MyIp,
157156
Mac: r.header.Mac,
158157
}
159158

160-
log.Printf("Sending %s to %v", opNames[header.Op], r.header.Mac.String())
159+
log.Printf("Sending %s to %v", opNames[DHCPNAK], r.header.Mac.String())
161160

162161
options := NewOptions()
163-
options.Set(OPTION_MESSAGE_TYPE, []byte{header.Op})
162+
options.Set(OPTION_MESSAGE_TYPE, []byte{DHCPNAK})
164163

165164
// FIXME: we likely need more options
166165

167166
return &DHCPMessage{header, options}
168167
}
169168

169+
//
170+
// Send a dhcp response message to broadcast address
171+
//
172+
170173
func (r *RequestHandler) sendMessageBroadcast(message *DHCPMessage, localSocket *net.UDPConn) {
171174
buf := new(bytes.Buffer)
172175

@@ -178,7 +181,7 @@ func (r *RequestHandler) sendMessageBroadcast(message *DHCPMessage, localSocket
178181

179182
err = r.sendBroadcast(buf.Bytes(), localSocket)
180183
if err != nil {
181-
log.Printf("Failed sending %s payload: %v", opNames[message.Header.Op], err)
184+
log.Printf("Failed sending %s payload: %v", opNames[message.Options.GetByte(OPTION_MESSAGE_TYPE)], err)
182185
}
183186
}
184187

@@ -197,3 +200,45 @@ func (r *RequestHandler) sendBroadcast(data []byte, localSocket *net.UDPConn) er
197200
}
198201
return nil
199202
}
203+
204+
//
205+
// Send a dhcp response message to a unicast address
206+
//
207+
208+
func (r *RequestHandler) sendMessageRelayed(message *DHCPMessage, dest FixedV4, localSocket *net.UDPConn) {
209+
// FIXME: maybe more/fixed header mangling?
210+
message.Header.GatewayAddr = r.header.GatewayAddr
211+
message.Header.Flags = r.header.Flags
212+
r.sendMessageUnicast(message, dest, localSocket)
213+
}
214+
215+
func (r *RequestHandler) sendMessageUnicast(message *DHCPMessage, dest FixedV4, localSocket *net.UDPConn) {
216+
buf := new(bytes.Buffer)
217+
218+
err := message.Encode(buf)
219+
if err != nil {
220+
log.Printf("Failed encoding payload: %v", err)
221+
return
222+
}
223+
224+
err = r.sendUnicast(buf.Bytes(), dest, localSocket)
225+
if err != nil {
226+
log.Printf("Failed sending %s unicast payload: %v", opNames[message.Options.GetByte(OPTION_MESSAGE_TYPE)], err)
227+
}
228+
}
229+
230+
func (r *RequestHandler) sendUnicast(data []byte, dest FixedV4, localSocket *net.UDPConn) error {
231+
// Quickly ripped from https://github.com/aler9/howto-udp-broadcast-golang
232+
addr, err := net.ResolveUDPAddr("udp4", dest.String()+":67")
233+
if err != nil {
234+
return fmt.Errorf("Failed resolving remote: %v", err)
235+
}
236+
237+
// Need to use our original listening socket to maintain source port 67,
238+
// otherwise windows dhcp will not see our responses
239+
_, err = localSocket.WriteTo(data, addr)
240+
if err != nil {
241+
return fmt.Errorf("Failed writing: %v", err)
242+
}
243+
return nil
244+
}

request_test.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func TestDhcpDiscover(t *testing.T) {
2828
handler := NewRequestHandler(message, pool)
2929
response := handler.Handle()
3030

31-
require.Equal(t, byte(DHCPNAK), response.Header.Op)
31+
require.Equal(t, byte(DHCPNAK), response.Options.GetByte(OPTION_MESSAGE_TYPE))
3232

3333
//
3434
// DISCOVER. Should get back a lease.
@@ -39,12 +39,12 @@ func TestDhcpDiscover(t *testing.T) {
3939

4040
message, err = ParseDhcpMessage(b)
4141
require.Nil(t, err)
42-
require.Equal(t, byte(DHCPDISCOVER), message.Header.Op)
42+
require.Equal(t, byte(DHCPDISCOVER), message.Options.GetByte(OPTION_MESSAGE_TYPE))
4343

4444
handler = NewRequestHandler(message, pool)
4545
response = handler.Handle()
4646

47-
require.Equal(t, byte(DHCPOFFER), response.Header.Op)
47+
require.Equal(t, byte(DHCPOFFER), response.Options.GetByte(OPTION_MESSAGE_TYPE))
4848
require.Equal(t, IpToFixedV4(net.ParseIP("10.0.0.10")), response.Header.YourAddr)
4949

5050
// Pool should have a lease for this mac
@@ -61,12 +61,12 @@ func TestDhcpDiscover(t *testing.T) {
6161

6262
message, err = ParseDhcpMessage(b)
6363
require.Nil(t, err)
64-
require.Equal(t, byte(DHCPREQUEST), message.Header.Op)
64+
require.Equal(t, byte(DHCPREQUEST), message.Options.GetByte(OPTION_MESSAGE_TYPE))
6565

6666
handler = NewRequestHandler(message, pool)
6767
response = handler.Handle()
6868

69-
require.Equal(t, byte(DHCPNAK), response.Header.Op)
69+
require.Equal(t, byte(DHCPNAK), response.Options.GetByte(OPTION_MESSAGE_TYPE))
7070

7171
//
7272
// A request should now get back an ACK
@@ -77,12 +77,12 @@ func TestDhcpDiscover(t *testing.T) {
7777

7878
message, err = ParseDhcpMessage(b)
7979
require.Nil(t, err)
80-
require.Equal(t, byte(DHCPREQUEST), message.Header.Op)
80+
require.Equal(t, byte(DHCPREQUEST), message.Options.GetByte(OPTION_MESSAGE_TYPE))
8181

8282
handler = NewRequestHandler(message, pool)
8383
response = handler.Handle()
8484

85-
require.Equal(t, byte(DHCPACK), response.Header.Op)
85+
require.Equal(t, byte(DHCPACK), response.Options.GetByte(OPTION_MESSAGE_TYPE))
8686

8787
//
8888
// Do a DHCPRELEASE
@@ -93,7 +93,7 @@ func TestDhcpDiscover(t *testing.T) {
9393

9494
message, err = ParseDhcpMessage(b)
9595
require.Nil(t, err)
96-
require.Equal(t, byte(DHCPRELEASE), message.Header.Op)
96+
require.Equal(t, byte(DHCPRELEASE), message.Options.GetByte(OPTION_MESSAGE_TYPE))
9797

9898
handler = NewRequestHandler(message, pool)
9999
response = handler.Handle()

0 commit comments

Comments
 (0)