Pull request: 3443 dhcp broadcast vol.2

Merge in DNS/adguard-home from 3443-dhcp-broadcast-vol.2 to master

Closes #3443.

Squashed commit of the following:

commit a85af89cb43f2489126fe3c12366fc034e89f59d
Merge: 72eb3a88 a4e07827
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 30 18:08:19 2021 +0300

    Merge branch 'master' into 3443-dhcp-broadcast-vol.2

commit 72eb3a8853540b06ee1096decf50e836b539fe45
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 30 18:03:19 2021 +0300

    dhcpd: imp code readability

commit 2d1fbc40d04a4125855d6be9f02e09d15430150d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 30 14:16:59 2021 +0300

    dhcpd: imp tests

commit 889fad3084ad2b81edfc12100e2ce29d323227ba
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Sep 29 20:09:25 2021 +0300

    dhcpd: imp code, docs

commit 1fd6b2346ff66e033bceaa169aed751be5822ca8
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 23 16:08:18 2021 +0300

    dhcpd: unicast to mac address
This commit is contained in:
Eugene Burkov 2021-09-30 18:28:19 +03:00
parent a4e078271c
commit da86620288
12 changed files with 531 additions and 231 deletions

4
go.mod
View File

@ -12,12 +12,14 @@ require (
github.com/fsnotify/fsnotify v1.4.9 github.com/fsnotify/fsnotify v1.4.9
github.com/go-ping/ping v0.0.0-20210506233800-ff8be3320020 github.com/go-ping/ping v0.0.0-20210506233800-ff8be3320020
github.com/google/go-cmp v0.5.5 github.com/google/go-cmp v0.5.5
github.com/google/gopacket v1.1.19
github.com/google/renameio v1.0.1 github.com/google/renameio v1.0.1
github.com/insomniacslk/dhcp v0.0.0-20210310193751-cfd4d47082c2 github.com/insomniacslk/dhcp v0.0.0-20210310193751-cfd4d47082c2
github.com/kardianos/service v1.2.0 github.com/kardianos/service v1.2.0
github.com/lucas-clemente/quic-go v0.21.1 github.com/lucas-clemente/quic-go v0.21.1
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7
github.com/mdlayher/netlink v1.4.0 github.com/mdlayher/netlink v1.4.0
github.com/mdlayher/raw v0.0.0-20210412142147-51b895745faf // indirect github.com/mdlayher/raw v0.0.0-20210412142147-51b895745faf
github.com/miekg/dns v1.1.43 github.com/miekg/dns v1.1.43
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/objx v0.1.1 // indirect

4
go.sum
View File

@ -93,6 +93,8 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
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/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
@ -268,6 +270,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -377,6 +380,7 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -5,33 +5,17 @@ package dhcpd
import ( import (
"net" "net"
"github.com/AdguardTeam/golibs/log"
"github.com/insomniacslk/dhcp/dhcpv4"
) )
// broadcast sends resp to the broadcast address specific for network interface. // broadcast sends resp to the broadcast address specific for network interface.
func (s *v4Server) broadcast(peer net.Addr, conn net.PacketConn, resp *dhcpv4.DHCPv4) { func (c *dhcpConn) broadcast(respData []byte, peer *net.UDPAddr) (n int, err error) {
// peer is expected to be of type *net.UDPConn as the server4.NewServer
// initializes it.
udpPeer, ok := peer.(*net.UDPAddr)
if !ok {
log.Error("dhcpv4: peer is of unexpected type %T", peer)
return
}
// Despite the fact that server4.NewIPv4UDPConn explicitly sets socket // Despite the fact that server4.NewIPv4UDPConn explicitly sets socket
// options to allow broadcasting, it also binds the connection to a // options to allow broadcasting, it also binds the connection to a
// specific interface. On FreeBSD and OpenBSD conn.WriteTo causes // specific interface. On FreeBSD and OpenBSD net.UDPConn.WriteTo
// errors while writing to the addresses that belong to another // causes errors while writing to the addresses that belong to another
// interface. So, use the broadcast address specific for the binded // interface. So, use the broadcast address specific for the interface
// interface. // bound.
udpPeer.IP = s.conf.broadcastIP peer.IP = c.bcastIP
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary()) return c.udpConn.WriteTo(respData, peer)
if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
} }

View File

@ -8,17 +8,16 @@ import (
"net" "net"
"testing" "testing"
"github.com/AdguardTeam/golibs/netutil"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestV4Server_Send_broadcast(t *testing.T) { func TestDHCPConn_Broadcast(t *testing.T) {
b := &bytes.Buffer{} b := &bytes.Buffer{}
var peer *net.UDPAddr var peer *net.UDPAddr
conn := &fakePacketConn{ udpConn := &fakePacketConn{
writeTo: func(p []byte, addr net.Addr) (n int, err error) { writeTo: func(p []byte, addr net.Addr) (n int, err error) {
udpPeer, ok := addr.(*net.UDPAddr) udpPeer, ok := addr.(*net.UDPAddr)
require.True(t, ok) require.True(t, ok)
@ -31,57 +30,22 @@ func TestV4Server_Send_broadcast(t *testing.T) {
return n, nil return n, nil
}, },
} }
conn := &dhcpConn{
udpConn: udpConn,
bcastIP: net.IP{1, 2, 3, 255},
}
defaultPeer := &net.UDPAddr{ defaultPeer := &net.UDPAddr{
IP: net.IP{1, 2, 3, 4}, IP: net.IP{1, 2, 3, 4},
// Use neither client nor server port. // Use neither client nor server port.
Port: 1234, Port: 1234,
} }
s := &v4Server{ respData := (&dhcpv4.DHCPv4{}).ToBytes()
conf: V4ServerConf{
broadcastIP: net.IP{1, 2, 3, 255},
},
}
testCases := []struct { _, _ = conn.broadcast(respData, cloneUDPAddr(defaultPeer))
name string
req *dhcpv4.DHCPv4
resp *dhcpv4.DHCPv4
}{{
name: "nak",
req: &dhcpv4.DHCPv4{
GatewayIPAddr: netutil.IPv4Zero(),
},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
},
}, {
name: "fully_unspecified",
req: &dhcpv4.DHCPv4{
GatewayIPAddr: netutil.IPv4Zero(),
ClientIPAddr: netutil.IPv4Zero(),
},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer),
),
},
}}
for _, tc := range testCases { assert.EqualValues(t, respData, b.Bytes())
t.Run(tc.name, func(t *testing.T) { assert.Equal(t, &net.UDPAddr{
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp) IP: conn.bcastIP,
assert.EqualValues(t, tc.resp.ToBytes(), b.Bytes()) Port: defaultPeer.Port,
assert.Equal(t, &net.UDPAddr{ }, peer)
IP: s.conf.broadcastIP,
Port: defaultPeer.Port,
Zone: defaultPeer.Zone,
}, peer)
})
b.Reset()
peer = nil
}
} }

View File

@ -5,17 +5,10 @@ package dhcpd
import ( import (
"net" "net"
"github.com/AdguardTeam/golibs/log"
"github.com/insomniacslk/dhcp/dhcpv4"
) )
// broadcast sends resp to the broadcast address specific for network interface. // broadcast sends resp to the broadcast address specific for network interface.
func (s *v4Server) broadcast(peer net.Addr, conn net.PacketConn, resp *dhcpv4.DHCPv4) { func (c *dhcpConn) broadcast(respData []byte, peer *net.UDPAddr) (n int, err error) {
respData := resp.ToBytes()
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary())
// This write to 0xffffffff reverts some behavior changes made in // This write to 0xffffffff reverts some behavior changes made in
// https://github.com/AdguardTeam/AdGuardHome/issues/3289. The DHCP // https://github.com/AdguardTeam/AdGuardHome/issues/3289. The DHCP
// server should broadcast the message to 0xffffffff but it's // server should broadcast the message to 0xffffffff but it's
@ -26,26 +19,13 @@ func (s *v4Server) broadcast(peer net.Addr, conn net.PacketConn, resp *dhcpv4.DH
// https://github.com/AdguardTeam/AdGuardHome/issues/3366. // https://github.com/AdguardTeam/AdGuardHome/issues/3366.
// //
// See also https://github.com/AdguardTeam/AdGuardHome/issues/3539. // See also https://github.com/AdguardTeam/AdGuardHome/issues/3539.
if _, err := conn.WriteTo(respData, peer); err != nil { if n, err = c.udpConn.WriteTo(respData, peer); err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err) return n, err
}
// peer is expected to be of type *net.UDPConn as the server4.NewServer
// initializes it.
udpPeer, ok := peer.(*net.UDPAddr)
if !ok {
log.Error("dhcpv4: peer is of unexpected type %T", peer)
return
} }
// Broadcast the message one more time using the interface-specific // Broadcast the message one more time using the interface-specific
// broadcast address. // broadcast address.
udpPeer.IP = s.conf.broadcastIP peer.IP = c.bcastIP
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary()) return c.udpConn.WriteTo(respData, peer)
if _, err := conn.WriteTo(respData, peer); err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
} }

View File

@ -8,17 +8,16 @@ import (
"net" "net"
"testing" "testing"
"github.com/AdguardTeam/golibs/netutil"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestV4Server_Send_broadcast(t *testing.T) { func TestDHCPConn_Broadcast(t *testing.T) {
b := &bytes.Buffer{} b := &bytes.Buffer{}
var peers []*net.UDPAddr var peers []*net.UDPAddr
conn := &fakePacketConn{ udpConn := &fakePacketConn{
writeTo: func(p []byte, addr net.Addr) (n int, err error) { writeTo: func(p []byte, addr net.Addr) (n int, err error) {
udpPeer, ok := addr.(*net.UDPAddr) udpPeer, ok := addr.(*net.UDPAddr)
require.True(t, ok) require.True(t, ok)
@ -31,66 +30,27 @@ func TestV4Server_Send_broadcast(t *testing.T) {
return n, nil return n, nil
}, },
} }
conn := &dhcpConn{
udpConn: udpConn,
bcastIP: net.IP{1, 2, 3, 255},
}
defaultPeer := &net.UDPAddr{ defaultPeer := &net.UDPAddr{
IP: net.IP{1, 2, 3, 4}, IP: net.IP{1, 2, 3, 4},
// Use neither client nor server port. // Use neither client nor server port.
Port: 1234, Port: 1234,
} }
s := &v4Server{ respData := (&dhcpv4.DHCPv4{}).ToBytes()
conf: V4ServerConf{
broadcastIP: net.IP{1, 2, 3, 255},
},
}
testCases := []struct { _, _ = conn.broadcast(respData, cloneUDPAddr(defaultPeer))
name string
req *dhcpv4.DHCPv4
resp *dhcpv4.DHCPv4
}{{
name: "nak",
req: &dhcpv4.DHCPv4{
GatewayIPAddr: netutil.IPv4Zero(),
},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
},
}, {
name: "fully_unspecified",
req: &dhcpv4.DHCPv4{
GatewayIPAddr: netutil.IPv4Zero(),
ClientIPAddr: netutil.IPv4Zero(),
},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer),
),
},
}}
for _, tc := range testCases { // The same response is written twice but for different peers.
t.Run(tc.name, func(t *testing.T) { assert.EqualValues(t, append(respData, respData...), b.Bytes())
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
// The same response is written twice. require.Len(t, peers, 2)
respData := tc.resp.ToBytes()
assert.EqualValues(t, append(respData, respData...), b.Bytes())
require.Len(t, peers, 2) assert.Equal(t, cloneUDPAddr(defaultPeer), peers[0])
assert.Equal(t, &net.UDPAddr{
assert.Equal(t, &net.UDPAddr{ IP: conn.bcastIP,
IP: defaultPeer.IP, Port: defaultPeer.Port,
Port: defaultPeer.Port, }, peers[1])
}, peers[0])
assert.Equal(t, &net.UDPAddr{
IP: s.conf.broadcastIP,
Port: defaultPeer.Port,
}, peers[1])
})
b.Reset()
peers = nil
}
} }

244
internal/dhcpd/conn_unix.go Normal file
View File

@ -0,0 +1,244 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd
import (
"fmt"
"net"
"os"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/ethernet"
"github.com/mdlayher/raw"
)
// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to
// the unconfigured host.
type dhcpUnicastAddr struct {
// raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
// actually implementing all methods. It also contains the client's
// hardware address.
raw.Addr
// yiaddr is an IP address just allocated by server for the host.
yiaddr net.IP
}
// dhcpConn is the net.PacketConn capable of handling both net.UDPAddr and
// net.HardwareAddr.
type dhcpConn struct {
// udpConn is the connection for UDP addresses.
udpConn net.PacketConn
// bcastIP is the broadcast address specific for the configured
// interface's subnet.
bcastIP net.IP
// rawConn is the connection for MAC addresses.
rawConn net.PacketConn
// srcMAC is the hardware address of the configured network interface.
srcMAC net.HardwareAddr
// srcIP is the IP address of the configured network interface.
srcIP net.IP
}
// newDHCPConn creates the special connection for DHCP server.
func (s *v4Server) newDHCPConn(ifi *net.Interface) (c net.PacketConn, err error) {
// Create the raw connection.
var ucast net.PacketConn
if ucast, err = raw.ListenPacket(ifi, uint16(ethernet.EtherTypeIPv4), nil); err != nil {
return nil, fmt.Errorf("creating raw udp connection: %w", err)
}
// Create the UDP connection.
var bcast net.PacketConn
bcast, err = server4.NewIPv4UDPConn(ifi.Name, &net.UDPAddr{
// TODO(e.burkov): Listening on zeroes makes the server handle
// requests from all the interfaces. Inspect the ways to
// specify the interface-specific listening addresses.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
IP: net.IP{0, 0, 0, 0},
Port: dhcpv4.ServerPort,
})
if err != nil {
return nil, fmt.Errorf("creating ipv4 udp connection: %w", err)
}
return &dhcpConn{
udpConn: bcast,
bcastIP: s.conf.broadcastIP,
rawConn: ucast,
srcMAC: ifi.HardwareAddr,
srcIP: s.conf.dnsIPAddrs[0],
}, nil
}
// wrapErrs is a helper to wrap the errors from two independent underlying
// connections.
func (c *dhcpConn) wrapErrs(action string, udpConnErr, rawConnErr error) (err error) {
switch {
case udpConnErr != nil && rawConnErr != nil:
return errors.List(fmt.Sprintf("%s both connections", action), udpConnErr, rawConnErr)
case udpConnErr != nil:
return fmt.Errorf("%s udp connection: %w", action, udpConnErr)
case rawConnErr != nil:
return fmt.Errorf("%s raw connection: %w", action, rawConnErr)
default:
return nil
}
}
// WriteTo implements net.PacketConn for *dhcpConn. It selects the underlying
// connection to write to based on the type of addr.
func (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
switch addr := addr.(type) {
case *dhcpUnicastAddr:
// Unicast the message to the client's MAC address. Use the raw
// connection.
//
// Note: unicasting is performed on the only network interface
// that is configured. For now it may be not what users expect
// so additionally broadcast the message via UDP connection.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
var rerr error
n, rerr = c.unicast(p, addr)
_, uerr := c.broadcast(p, &net.UDPAddr{
IP: netutil.IPv4bcast(),
Port: dhcpv4.ClientPort,
})
return n, c.wrapErrs("writing to", uerr, rerr)
case *net.UDPAddr:
if addr.IP.Equal(net.IPv4bcast) {
// Broadcast the message for the client which supports
// it. Use the UDP connection.
return c.broadcast(p, addr)
}
// Unicast the message to the client's IP address. Use the UDP
// connection.
return c.udpConn.WriteTo(p, addr)
default:
return 0, fmt.Errorf("peer is of unexpected type %T", addr)
}
}
// ReadFrom implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
return c.udpConn.ReadFrom(p)
}
// unicast wraps respData with required frames and writes it to the peer.
func (c *dhcpConn) unicast(respData []byte, peer *dhcpUnicastAddr) (n int, err error) {
var data []byte
data, err = c.buildEtherPkt(respData, peer)
if err != nil {
return 0, err
}
return c.rawConn.WriteTo(data, &peer.Addr)
}
// Close implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) Close() (err error) {
rerr := c.rawConn.Close()
if errors.Is(rerr, os.ErrClosed) {
// Ignore the error since the actual file is closed already.
rerr = nil
}
return c.wrapErrs("closing", c.udpConn.Close(), rerr)
}
// LocalAddr implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) LocalAddr() (a net.Addr) {
return c.udpConn.LocalAddr()
}
// SetDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetDeadline(t time.Time) (err error) {
return c.wrapErrs("setting deadline on", c.udpConn.SetDeadline(t), c.rawConn.SetDeadline(t))
}
// SetReadDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetReadDeadline(t time.Time) error {
return c.wrapErrs(
"setting reading deadline on",
c.udpConn.SetReadDeadline(t),
c.rawConn.SetReadDeadline(t),
)
}
// SetWriteDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetWriteDeadline(t time.Time) error {
return c.wrapErrs(
"setting writing deadline on",
c.udpConn.SetWriteDeadline(t),
c.rawConn.SetWriteDeadline(t),
)
}
// ipv4DefaultTTL is the default Time to Live value as recommended by
// RFC-1700 (https://datatracker.ietf.org/doc/html/rfc1700) in seconds.
const ipv4DefaultTTL = 64
// errInvalidPktDHCP is returned when the provided payload is not a valid DHCP
// packet.
const errInvalidPktDHCP errors.Error = "packet is not a valid dhcp packet"
// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames. The
// payload is expected to be an encoded DHCP packet.
func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) {
dhcpLayer := gopacket.NewPacket(payload, layers.LayerTypeDHCPv4, gopacket.DecodeOptions{
NoCopy: true,
}).Layer(layers.LayerTypeDHCPv4)
// Check if the decoding succeeded and the resulting layer doesn't
// contain any errors. It should guarantee panic-safe converting of the
// layer into gopacket.SerializableLayer.
if dhcpLayer == nil || dhcpLayer.LayerType() != layers.LayerTypeDHCPv4 {
return nil, errInvalidPktDHCP
}
udpLayer := &layers.UDP{
SrcPort: dhcpv4.ServerPort,
DstPort: dhcpv4.ClientPort,
}
ipv4Layer := &layers.IPv4{
Version: uint8(layers.IPProtocolIPv4),
Flags: layers.IPv4DontFragment,
TTL: ipv4DefaultTTL,
Protocol: layers.IPProtocolUDP,
SrcIP: c.srcIP,
DstIP: peer.yiaddr,
}
// Ignore the error since it's only returned for invalid network layer's
// type.
_ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)
ethLayer := &layers.Ethernet{
SrcMAC: c.srcMAC,
DstMAC: peer.HardwareAddr,
EthernetType: layers.EthernetTypeIPv4,
}
buf := gopacket.NewSerializeBuffer()
err = gopacket.SerializeLayers(buf, gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}, ethLayer, ipv4Layer, udpLayer, dhcpLayer.(gopacket.SerializableLayer))
if err != nil {
return nil, fmt.Errorf("serializing layers: %w", err)
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,114 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd
import (
"net"
"strings"
"testing"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/mdlayher/raw"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDHCPConn_WriteTo_common(t *testing.T) {
respData := (&dhcpv4.DHCPv4{}).ToBytes()
udpAddr := &net.UDPAddr{
IP: net.IP{1, 2, 3, 4},
Port: dhcpv4.ClientPort,
}
t.Run("unicast_ip", func(t *testing.T) {
writeTo := func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, udpAddr, addr)
return 0, nil
}
conn := &dhcpConn{udpConn: &fakePacketConn{writeTo: writeTo}}
_, err := conn.WriteTo(respData, udpAddr)
assert.NoError(t, err)
})
t.Run("unexpected_addr_type", func(t *testing.T) {
type unexpectedAddrType struct {
net.Addr
}
conn := &dhcpConn{}
n, err := conn.WriteTo(nil, &unexpectedAddrType{})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "peer is of unexpected type"))
assert.Zero(t, n)
})
}
func TestBuildEtherPkt(t *testing.T) {
conn := &dhcpConn{
srcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6},
srcIP: net.IP{1, 2, 3, 4},
}
peer := &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
yiaddr: net.IP{4, 3, 2, 1},
}
payload := (&dhcpv4.DHCPv4{}).ToBytes()
t.Run("success", func(t *testing.T) {
pkt, err := conn.buildEtherPkt(payload, peer)
require.NoError(t, err)
assert.NotEmpty(t, pkt)
actualPkt := gopacket.NewPacket(pkt, layers.LayerTypeEthernet, gopacket.DecodeOptions{
NoCopy: true,
})
require.NotNil(t, actualPkt)
wantTypes := []gopacket.LayerType{
layers.LayerTypeEthernet,
layers.LayerTypeIPv4,
layers.LayerTypeUDP,
layers.LayerTypeDHCPv4,
}
actualLayers := actualPkt.Layers()
require.Len(t, actualLayers, len(wantTypes))
for i, wantType := range wantTypes {
layer := actualLayers[i]
require.NotNil(t, layer)
assert.Equal(t, wantType, layer.LayerType())
}
})
t.Run("non-serializable", func(t *testing.T) {
// Create an invalid DHCP packet.
invalidPayload := []byte{1, 2, 3, 4}
pkt, err := conn.buildEtherPkt(invalidPayload, nil)
require.Error(t, err)
assert.ErrorIs(t, err, errInvalidPktDHCP)
assert.Empty(t, pkt)
})
t.Run("serializing_error", func(t *testing.T) {
// Create a peer with invalid MAC.
badPeer := &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
yiaddr: net.IP{4, 3, 2, 1},
}
pkt, err := conn.buildEtherPkt(payload, badPeer)
require.Error(t, err)
assert.Empty(t, pkt)
})
}

View File

@ -139,7 +139,7 @@ func TestNormalizeLeases(t *testing.T) {
} }
// cloneUDPAddr returns a deep copy of a. // cloneUDPAddr returns a deep copy of a.
func cloneUDPAddr(a *net.UDPAddr) (copy *net.UDPAddr) { func cloneUDPAddr(a *net.UDPAddr) (clone *net.UDPAddr) {
return &net.UDPAddr{ return &net.UDPAddr{
IP: netutil.CloneIP(a.IP), IP: netutil.CloneIP(a.IP),
Port: a.Port, Port: a.Port,

View File

@ -157,8 +157,8 @@ func prepareOptions(conf V4ServerConf) (opts dhcpv4.Options) {
dhcpv4.OptionPerformRouterDiscovery.Code(): []byte{1}, dhcpv4.OptionPerformRouterDiscovery.Code(): []byte{1},
// The all-routers address is preferred wherever possible, see // The all-routers address is preferred wherever possible, see
// https://datatracker.ietf.org/doc/html/rfc1256#section-5.1. // https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.
dhcpv4.OptionRouterSolicitationAddress.Code(): net.IPv4allrouter.To4(), dhcpv4.OptionRouterSolicitationAddress.Code(): netutil.IPv4allrouter(),
dhcpv4.OptionBroadcastAddress.Code(): net.IPv4bcast.To4(), dhcpv4.OptionBroadcastAddress.Code(): netutil.IPv4bcast(),
// Link-Layer Per Interface // Link-Layer Per Interface

View File

@ -19,6 +19,7 @@ import (
"github.com/go-ping/ping" "github.com/go-ping/ping"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4" "github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/raw"
) )
// v4Server is a DHCPv4 server. // v4Server is a DHCPv4 server.
@ -955,43 +956,44 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
// //
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1. // See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) { func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
var unicast bool switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
if giaddr := req.GatewayIPAddr; giaddr != nil && !giaddr.IsUnspecified() { case giaddr != nil && !giaddr.IsUnspecified():
// Send any return messages to the server port on the BOOTP // Send any return messages to the server port on the BOOTP
// relay agent whose address appears in giaddr. // relay agent whose address appears in giaddr.
peer = &net.UDPAddr{ peer = &net.UDPAddr{
IP: giaddr, IP: giaddr,
Port: dhcpv4.ServerPort, Port: dhcpv4.ServerPort,
} }
unicast = true if mtype == dhcpv4.MessageTypeNak {
} else if mtype := resp.MessageType(); mtype == dhcpv4.MessageTypeNak { // Set the broadcast bit in the DHCPNAK, so that the
// relay agent broadcasted it to the client, because the
// client may not have a correct network address or
// subnet mask, and the client may not be answering ARP
// requests.
resp.SetBroadcast()
}
case mtype == dhcpv4.MessageTypeNak:
// Broadcast any DHCPNAK messages to 0xffffffff. // Broadcast any DHCPNAK messages to 0xffffffff.
} else if ciaddr := req.ClientIPAddr; ciaddr != nil && !ciaddr.IsUnspecified() { case ciaddr != nil && !ciaddr.IsUnspecified():
// Unicast DHCPOFFER and DHCPACK messages to the address in // Unicast DHCPOFFER and DHCPACK messages to the address in
// ciaddr. // ciaddr.
peer = &net.UDPAddr{ peer = &net.UDPAddr{
IP: ciaddr, IP: ciaddr,
Port: dhcpv4.ClientPort, Port: dhcpv4.ClientPort,
} }
unicast = true case !req.IsBroadcast() && req.ClientHWAddr != nil:
} // Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
// TODO(e.burkov): Unicast the message to the actual link-layer address peer = &dhcpUnicastAddr{
// of the client if broadcast bit is not set. Perhaps, use custom Addr: raw.Addr{HardwareAddr: req.ClientHWAddr},
// connection when creating the server. yiaddr: resp.YourIPAddr,
// }
// See https://github.com/AdguardTeam/AdGuardHome/issues/3443. default:
// Go on since peer is already set to broadcast.
if !unicast {
s.broadcast(peer, conn, resp)
return
} }
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary()) log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary())
if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil {
_, err := conn.WriteTo(resp.ToBytes(), peer)
if err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err) log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
} }
} }
@ -1029,11 +1031,18 @@ func (s *v4Server) Start() (err error) {
s.conf.dnsIPAddrs = dnsIPAddrs s.conf.dnsIPAddrs = dnsIPAddrs
laddr := &net.UDPAddr{ var c net.PacketConn
IP: net.IP{0, 0, 0, 0}, if c, err = s.newDHCPConn(iface); err != nil {
Port: dhcpv4.ServerPort, return err
} }
s.srv, err = server4.NewServer(iface.Name, laddr, s.packetHandler, server4.WithDebugLogger())
s.srv, err = server4.NewServer(
iface.Name,
nil,
s.packetHandler,
server4.WithConn(c),
server4.WithDebugLogger(),
)
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,12 +4,11 @@
package dhcpd package dhcpd
import ( import (
"bytes"
"net" "net"
"testing" "testing"
"github.com/AdguardTeam/golibs/netutil"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/mdlayher/raw"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -390,67 +389,107 @@ func (fc *fakePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return fc.writeTo(p, addr) return fc.writeTo(p, addr)
} }
func TestV4Server_Send_unicast(t *testing.T) { func TestV4Server_Send(t *testing.T) {
b := &bytes.Buffer{}
var peer *net.UDPAddr
conn := &fakePacketConn{
writeTo: func(p []byte, addr net.Addr) (n int, err error) {
udpPeer, ok := addr.(*net.UDPAddr)
require.True(t, ok)
peer = cloneUDPAddr(udpPeer)
n, err = b.Write(p)
require.NoError(t, err)
return n, nil
},
}
defaultPeer := &net.UDPAddr{
IP: net.IP{1, 2, 3, 4},
// Use neither client nor server port.
Port: 1234,
}
defaultResp := &dhcpv4.DHCPv4{
OpCode: dhcpv4.OpcodeBootReply,
}
s := &v4Server{} s := &v4Server{}
var (
defaultIP = net.IP{99, 99, 99, 99}
knownIP = net.IP{4, 2, 4, 2}
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
)
defaultPeer := &net.UDPAddr{
IP: defaultIP,
// Use neither client nor server port to check it actually
// changed.
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
}
defaultResp := &dhcpv4.DHCPv4{}
testCases := []struct { testCases := []struct {
name string want net.Addr
req *dhcpv4.DHCPv4 req *dhcpv4.DHCPv4
wantPeer net.Addr resp *dhcpv4.DHCPv4
name string
}{{ }{{
name: "relay_agent", name: "giaddr",
req: &dhcpv4.DHCPv4{ req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
GatewayIPAddr: defaultPeer.IP, resp: defaultResp,
}, want: &net.UDPAddr{
wantPeer: &net.UDPAddr{ IP: knownIP,
IP: defaultPeer.IP,
Port: dhcpv4.ServerPort, Port: dhcpv4.ServerPort,
}, },
}, { }, {
name: "known_client", name: "nak",
req: &dhcpv4.DHCPv4{ req: &dhcpv4.DHCPv4{},
GatewayIPAddr: netutil.IPv4Zero(), resp: &dhcpv4.DHCPv4{
ClientIPAddr: net.IP{2, 3, 4, 5}, Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
}, },
wantPeer: &net.UDPAddr{ want: defaultPeer,
IP: net.IP{2, 3, 4, 5}, }, {
name: "ciaddr",
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
resp: &dhcpv4.DHCPv4{},
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ClientPort, Port: dhcpv4.ClientPort,
}, },
}, {
name: "chaddr",
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
want: &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: knownMAC},
yiaddr: knownIP,
},
}, {
name: "who_are_you",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{},
want: defaultPeer,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
s.send(defaultPeer, conn, tc.req, defaultResp) conn := &fakePacketConn{
assert.EqualValues(t, defaultResp.ToBytes(), b.Bytes()) writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, tc.wantPeer, peer) assert.Equal(t, tc.want, addr)
})
b.Reset() return 0, nil
peer = nil },
}
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
})
} }
t.Run("giaddr_nak", func(t *testing.T) {
req := &dhcpv4.DHCPv4{
GatewayIPAddr: knownIP,
}
// Ensure the request is for unicast.
req.SetUnicast()
resp := &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
}
want := &net.UDPAddr{
IP: req.GatewayIPAddr,
Port: dhcpv4.ServerPort,
}
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
assert.Equal(t, want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
assert.True(t, resp.IsBroadcast())
})
} }