Pull request 1771: AG-20352-dhcpd-lease-netip-addr

Merge in DNS/adguard-home from AG-20352-dhcpd-lease-netip-addr to master

Squashed commit of the following:

commit 4acd094e2d6ed972bac99cdb671670f6d8e61721
Merge: 51f61c19 df61741f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Mar 23 16:44:17 2023 +0300

    Merge branch 'master' into AG-20352-dhcpd-lease-netip-addr

commit 51f61c193fdd31ee675be5598fc361228e407eb3
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 22 18:25:30 2023 +0300

    dhcpd: fix typo

commit 2e64ad55475957925d2a3010c649e0adc5f18c4f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 22 17:20:31 2023 +0300

    dhcpd: add todo

commit 668d4f62fd
Merge: 0020006e 306c1983
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 22 15:09:38 2023 +0300

    Merge branch 'master' into AG-20352-dhcpd-lease-netip-addr

commit 0020006e89
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 22 15:08:58 2023 +0300

    all: imp code

commit 9a77f79869
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Mar 20 18:13:35 2023 +0300

    dhcpd: add todo

commit 638c4ce2af
Merge: c82b18f1 48431f8b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Mar 20 13:57:19 2023 +0300

    Merge branch 'master' into AG-20352-dhcpd-lease-netip-addr

commit c82b18f140
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Mar 20 13:56:31 2023 +0300

    all: imp code

commit 27e5181200
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Mar 17 12:14:02 2023 +0300

    dhcpd: imp tests

commit 8e919b0ceb
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Mar 17 11:02:50 2023 +0300

    dhcpd: add tests

commit 78ddefa73a
Merge: c68e85c4 9f7a582d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Mar 16 14:15:24 2023 +0300

    Merge branch 'master' into AG-20352-dhcpd-lease-netip-addr

commit c68e85c409
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Mar 16 14:14:43 2023 +0300

    all: add tests

commit f338086309
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 15 12:44:06 2023 +0300

    all: dhcpd lease netip addr
This commit is contained in:
Stanislav Chzhen 2023-03-23 16:52:01 +03:00
parent df61741f60
commit bea39934bd
16 changed files with 562 additions and 197 deletions

View File

@ -2,51 +2,13 @@ package aghnet
import ( import (
"fmt" "fmt"
"net" "net/netip"
"strconv"
"strings" "strings"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
) )
// The maximum lengths of generated hostnames for different IP versions.
const (
ipv4HostnameMaxLen = len("192-168-100-100")
ipv6HostnameMaxLen = len("ff80-f076-0000-0000-0000-0000-0000-0010")
)
// generateIPv4Hostname generates the hostname by IP address version 4.
func generateIPv4Hostname(ipv4 net.IP) (hostname string) {
hnData := make([]byte, 0, ipv4HostnameMaxLen)
for i, part := range ipv4 {
if i > 0 {
hnData = append(hnData, '-')
}
hnData = strconv.AppendUint(hnData, uint64(part), 10)
}
return string(hnData)
}
// generateIPv6Hostname generates the hostname by IP address version 6.
func generateIPv6Hostname(ipv6 net.IP) (hostname string) {
hnData := make([]byte, 0, ipv6HostnameMaxLen)
for i, partsNum := 0, net.IPv6len/2; i < partsNum; i++ {
if i > 0 {
hnData = append(hnData, '-')
}
for _, val := range ipv6[i*2 : i*2+2] {
if val < 10 {
hnData = append(hnData, '0')
}
hnData = strconv.AppendUint(hnData, uint64(val), 16)
}
}
return string(hnData)
}
// GenerateHostname generates the hostname from ip. In case of using IPv4 the // GenerateHostname generates the hostname from ip. In case of using IPv4 the
// result should be like: // result should be like:
// //
@ -57,12 +19,20 @@ func generateIPv6Hostname(ipv6 net.IP) (hostname string) {
// ff80-f076-0000-0000-0000-0000-0000-0010 // ff80-f076-0000-0000-0000-0000-0000-0010
// //
// ip must be either an IPv4 or an IPv6. // ip must be either an IPv4 or an IPv6.
func GenerateHostname(ip net.IP) (hostname string) { func GenerateHostname(ip netip.Addr) (hostname string) {
if ipv4 := ip.To4(); ipv4 != nil { if !ip.IsValid() {
return generateIPv4Hostname(ipv4) // TODO(s.chzhen): Get rid of it.
panic("aghnet generate hostname: invalid ip")
} }
return generateIPv6Hostname(ip) ip = ip.Unmap()
hostname = ip.StringExpanded()
if ip.Is4() {
return strings.Replace(hostname, ".", "-", -1)
}
return strings.Replace(hostname, ":", "-", -1)
} }
// NewDomainNameSet returns nil and error, if list has duplicate or empty // NewDomainNameSet returns nil and error, if list has duplicate or empty

View File

@ -1,7 +1,7 @@
package aghnet package aghnet
import ( import (
"net" "net/netip"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -12,19 +12,19 @@ func TestGenerateHostName(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
want string want string
ip net.IP ip netip.Addr
}{{ }{{
name: "good_ipv4", name: "good_ipv4",
want: "127-0-0-1", want: "127-0-0-1",
ip: net.IP{127, 0, 0, 1}, ip: netip.MustParseAddr("127.0.0.1"),
}, { }, {
name: "good_ipv6", name: "good_ipv6",
want: "fe00-0000-0000-0000-0000-0000-0000-0001", want: "fe00-0000-0000-0000-0000-0000-0000-0001",
ip: net.ParseIP("fe00::1"), ip: netip.MustParseAddr("fe00::1"),
}, { }, {
name: "4to6", name: "4to6",
want: "1-2-3-4", want: "1-2-3-4",
ip: net.ParseIP("::ffff:1.2.3.4"), ip: netip.MustParseAddr("::ffff:1.2.3.4"),
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@ -36,29 +36,6 @@ func TestGenerateHostName(t *testing.T) {
}) })
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
testCases := []struct { assert.Panics(t, func() { GenerateHostname(netip.Addr{}) })
name string
ip net.IP
}{{
name: "bad_ipv4",
ip: net.IP{127, 0, 0, 1, 0},
}, {
name: "bad_ipv6",
ip: net.IP{
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff,
},
}, {
name: "nil",
ip: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Panics(t, func() { GenerateHostname(tc.ip) })
})
}
}) })
} }

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"net/netip"
"os" "os"
"time" "time"
@ -32,6 +33,8 @@ func normalizeIP(ip net.IP) net.IP {
} }
// Load lease table from DB // Load lease table from DB
//
// TODO(s.chzhen): Decrease complexity.
func (s *server) dbLoad() (err error) { func (s *server) dbLoad() (err error) {
dynLeases := []*Lease{} dynLeases := []*Lease{}
staticLeases := []*Lease{} staticLeases := []*Lease{}
@ -57,14 +60,15 @@ func (s *server) dbLoad() (err error) {
for i := range obj { for i := range obj {
obj[i].IP = normalizeIP(obj[i].IP) obj[i].IP = normalizeIP(obj[i].IP)
if !(len(obj[i].IP) == 4 || len(obj[i].IP) == 16) { ip, ok := netip.AddrFromSlice(obj[i].IP)
if !ok {
log.Info("dhcp: invalid IP: %s", obj[i].IP) log.Info("dhcp: invalid IP: %s", obj[i].IP)
continue continue
} }
lease := Lease{ lease := Lease{
HWAddr: obj[i].HWAddr, HWAddr: obj[i].HWAddr,
IP: obj[i].IP, IP: ip,
Hostname: obj[i].Hostname, Hostname: obj[i].Hostname,
Expiry: time.Unix(obj[i].Expiry, 0), Expiry: time.Unix(obj[i].Expiry, 0),
} }
@ -145,7 +149,7 @@ func (s *server) dbStore() (err error) {
lease := leaseJSON{ lease := leaseJSON{
HWAddr: l.HWAddr, HWAddr: l.HWAddr,
IP: l.IP, IP: l.IP.AsSlice(),
Hostname: l.Hostname, Hostname: l.Hostname,
Expiry: l.Expiry.Unix(), Expiry: l.Expiry.Unix(),
} }
@ -162,7 +166,7 @@ func (s *server) dbStore() (err error) {
lease := leaseJSON{ lease := leaseJSON{
HWAddr: l.HWAddr, HWAddr: l.HWAddr,
IP: l.IP, IP: l.IP.AsSlice(),
Hostname: l.Hostname, Hostname: l.Hostname,
Expiry: l.Expiry.Unix(), Expiry: l.Expiry.Unix(),
} }

View File

@ -41,13 +41,16 @@ type Lease struct {
// of 1 means that this is a static lease. // of 1 means that this is a static lease.
Expiry time.Time `json:"expires"` Expiry time.Time `json:"expires"`
Hostname string `json:"hostname"` // Hostname of the client.
HWAddr net.HardwareAddr `json:"mac"` Hostname string `json:"hostname"`
// HWAddr is the physical hardware address (MAC address).
HWAddr net.HardwareAddr `json:"mac"`
// IP is the IP address leased to the client. // IP is the IP address leased to the client.
// //
// TODO(a.garipov): Migrate leases.db and use netip.Addr. // TODO(a.garipov): Migrate leases.db.
IP net.IP `json:"ip"` IP netip.Addr `json:"ip"`
} }
// Clone returns a deep copy of l. // Clone returns a deep copy of l.
@ -60,7 +63,7 @@ func (l *Lease) Clone() (clone *Lease) {
Expiry: l.Expiry, Expiry: l.Expiry,
Hostname: l.Hostname, Hostname: l.Hostname,
HWAddr: slices.Clone(l.HWAddr), HWAddr: slices.Clone(l.HWAddr),
IP: slices.Clone(l.IP), IP: l.IP,
} }
} }

View File

@ -48,11 +48,11 @@ func TestDB(t *testing.T) {
Expiry: time.Now().Add(time.Hour), Expiry: time.Now().Add(time.Hour),
Hostname: "static-1.local", Hostname: "static-1.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 100}, IP: netip.MustParseAddr("192.168.10.100"),
}, { }, {
Hostname: "static-2.local", Hostname: "static-2.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB},
IP: net.IP{192, 168, 10, 101}, IP: netip.MustParseAddr("192.168.10.101"),
}} }}
srv4, ok := s.srv4.(*v4Server) srv4, ok := s.srv4.(*v4Server)
@ -96,7 +96,7 @@ func TestNormalizeLeases(t *testing.T) {
staticLeases := []*Lease{{ staticLeases := []*Lease{{
HWAddr: net.HardwareAddr{1, 2, 3, 4}, HWAddr: net.HardwareAddr{1, 2, 3, 4},
IP: net.IP{0, 2, 3, 4}, IP: netip.MustParseAddr("0.2.3.4"),
}, { }, {
HWAddr: net.HardwareAddr{2, 2, 3, 4}, HWAddr: net.HardwareAddr{2, 2, 3, 4},
}} }}

View File

@ -496,18 +496,18 @@ func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
return return
} }
if l.IP == nil { if !l.IP.IsValid() {
aghhttp.Error(r, w, http.StatusBadRequest, "invalid IP") aghhttp.Error(r, w, http.StatusBadRequest, "invalid IP")
return return
} }
l.IP = l.IP.Unmap()
var srv DHCPServer var srv DHCPServer
if ip4 := l.IP.To4(); ip4 != nil { if l.IP.Is4() {
l.IP = ip4
srv = s.srv4 srv = s.srv4
} else { } else {
l.IP = l.IP.To16()
srv = s.srv6 srv = s.srv6
} }
@ -528,27 +528,22 @@ func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
return return
} }
if l.IP == nil { if !l.IP.IsValid() {
aghhttp.Error(r, w, http.StatusBadRequest, "invalid IP") aghhttp.Error(r, w, http.StatusBadRequest, "invalid IP")
return return
} }
ip4 := l.IP.To4() l.IP = l.IP.Unmap()
if ip4 == nil { var srv DHCPServer
l.IP = l.IP.To16() if l.IP.Is4() {
srv = s.srv4
err = s.srv6.RemoveStaticLease(l) } else {
if err != nil { srv = s.srv6
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
}
return
} }
l.IP = ip4 err = srv.RemoveStaticLease(l)
err = s.srv4.RemoveStaticLease(l)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)

View File

@ -0,0 +1,161 @@
//go:build darwin || freebsd || linux || openbsd
package dhcpd
import (
"bytes"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestServer_handleDHCPStatus(t *testing.T) {
const staticName = "static-client"
staticIP := netip.MustParseAddr("192.168.10.10")
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
staticLease := &Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName,
HWAddr: staticMAC,
IP: staticIP,
}
s, err := Create(&ServerConfig{
Enabled: true,
Conf4: *defaultV4ServerConf(),
WorkDir: t.TempDir(),
DBFilePath: dbFilename,
ConfigModified: func() {},
})
require.NoError(t, err)
// checkStatus is a helper that asserts the response of
// [*server.handleDHCPStatus].
checkStatus := func(t *testing.T, want *dhcpStatusResponse) {
w := httptest.NewRecorder()
var req *http.Request
req, err = http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
b := &bytes.Buffer{}
err = json.NewEncoder(b).Encode(&want)
require.NoError(t, err)
s.handleDHCPStatus(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.JSONEq(t, b.String(), w.Body.String())
}
// defaultResponse is a helper that returs the response with default
// configuration.
defaultResponse := func() *dhcpStatusResponse {
conf4 := defaultV4ServerConf()
conf4.LeaseDuration = 86400
resp := &dhcpStatusResponse{
V4: *conf4,
V6: V6ServerConf{},
Leases: []*Lease{},
StaticLeases: []*Lease{},
Enabled: true,
}
return resp
}
ok := t.Run("status", func(t *testing.T) {
resp := defaultResponse()
checkStatus(t, resp)
})
require.True(t, ok)
ok = t.Run("add_static_lease", func(t *testing.T) {
w := httptest.NewRecorder()
b := &bytes.Buffer{}
err = json.NewEncoder(b).Encode(staticLease)
require.NoError(t, err)
var r *http.Request
r, err = http.NewRequest(http.MethodPost, "", b)
require.NoError(t, err)
s.handleDHCPAddStaticLease(w, r)
assert.Equal(t, http.StatusOK, w.Code)
resp := defaultResponse()
resp.StaticLeases = []*Lease{staticLease}
checkStatus(t, resp)
})
require.True(t, ok)
ok = t.Run("add_invalid_lease", func(t *testing.T) {
w := httptest.NewRecorder()
b := &bytes.Buffer{}
err = json.NewEncoder(b).Encode(&Lease{})
require.NoError(t, err)
var r *http.Request
r, err = http.NewRequest(http.MethodPost, "", b)
require.NoError(t, err)
s.handleDHCPAddStaticLease(w, r)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
require.True(t, ok)
ok = t.Run("remove_static_lease", func(t *testing.T) {
w := httptest.NewRecorder()
b := &bytes.Buffer{}
err = json.NewEncoder(b).Encode(staticLease)
require.NoError(t, err)
var r *http.Request
r, err = http.NewRequest(http.MethodPost, "", b)
require.NoError(t, err)
s.handleDHCPRemoveStaticLease(w, r)
assert.Equal(t, http.StatusOK, w.Code)
resp := defaultResponse()
checkStatus(t, resp)
})
require.True(t, ok)
ok = t.Run("set_config", func(t *testing.T) {
w := httptest.NewRecorder()
resp := defaultResponse()
resp.Enabled = false
b := &bytes.Buffer{}
err = json.NewEncoder(b).Encode(&resp)
require.NoError(t, err)
var r *http.Request
r, err = http.NewRequest(http.MethodPost, "", b)
require.NoError(t, err)
s.handleDHCPSetConfig(w, r)
assert.Equal(t, http.StatusOK, w.Code)
checkStatus(t, resp)
})
require.True(t, ok)
}

View File

@ -16,6 +16,8 @@ import (
// //
// TODO(a.garipov): Perhaps create an optimized version with uint32 for IPv4 // TODO(a.garipov): Perhaps create an optimized version with uint32 for IPv4
// ranges? Or use one of uint128 packages? // ranges? Or use one of uint128 packages?
//
// TODO(e.burkov): Use netip.Addr.
type ipRange struct { type ipRange struct {
start *big.Int start *big.Int
end *big.Int end *big.Int
@ -27,8 +29,6 @@ const maxRangeLen = math.MaxUint32
// newIPRange creates a new IP address range. start must be less than end. The // newIPRange creates a new IP address range. start must be less than end. The
// resulting range must not be greater than maxRangeLen. // resulting range must not be greater than maxRangeLen.
//
// TODO(e.burkov): Use netip.Addr.
func newIPRange(start, end net.IP) (r *ipRange, err error) { func newIPRange(start, end net.IP) (r *ipRange, err error) {
defer func() { err = errors.Annotate(err, "invalid ip range: %w") }() defer func() { err = errors.Annotate(err, "invalid ip range: %w") }()

View File

@ -98,7 +98,7 @@ func normalizeHostname(hostname string) (norm string, err error) {
// validHostnameForClient accepts the hostname sent by the client and its IP and // validHostnameForClient accepts the hostname sent by the client and its IP and
// returns either a normalized version of that hostname, or a new hostname // returns either a normalized version of that hostname, or a new hostname
// generated from the IP address, or an empty string. // generated from the IP address, or an empty string.
func (s *v4Server) validHostnameForClient(cliHostname string, ip net.IP) (hostname string) { func (s *v4Server) validHostnameForClient(cliHostname string, ip netip.Addr) (hostname string) {
hostname, err := normalizeHostname(cliHostname) hostname, err := normalizeHostname(cliHostname)
if err != nil { if err != nil {
log.Info("dhcpv4: %s", err) log.Info("dhcpv4: %s", err)
@ -211,9 +211,8 @@ func (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
return nil return nil
} }
netIP := ip.AsSlice()
for _, l := range s.leases { for _, l := range s.leases {
if l.IP.Equal(netIP) { if l.IP == ip {
if l.Expiry.After(now) || l.IsStatic() { if l.Expiry.After(now) || l.IsStatic() {
return l.HWAddr return l.HWAddr
} }
@ -247,7 +246,8 @@ func (s *v4Server) rmLeaseByIndex(i int) {
s.leases = append(s.leases[:i], s.leases[i+1:]...) s.leases = append(s.leases[:i], s.leases[i+1:]...)
r := s.conf.ipRange r := s.conf.ipRange
offset, ok := r.offset(l.IP) leaseIP := net.IP(l.IP.AsSlice())
offset, ok := r.offset(leaseIP)
if ok { if ok {
s.leasedOffsets.set(offset, false) s.leasedOffsets.set(offset, false)
} }
@ -263,7 +263,7 @@ func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
for i, l := range s.leases { for i, l := range s.leases {
isStatic := l.IsStatic() isStatic := l.IsStatic()
if bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP.Equal(lease.IP) { if bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP == lease.IP {
if isStatic { if isStatic {
return errors.Error("static lease already exists") return errors.Error("static lease already exists")
} }
@ -291,13 +291,13 @@ const ErrDupHostname = errors.Error("hostname is not unique")
// addLease adds a dynamic or static lease. // addLease adds a dynamic or static lease.
func (s *v4Server) addLease(l *Lease) (err error) { func (s *v4Server) addLease(l *Lease) (err error) {
r := s.conf.ipRange r := s.conf.ipRange
offset, inOffset := r.offset(l.IP) leaseIP := net.IP(l.IP.AsSlice())
offset, inOffset := r.offset(leaseIP)
if l.IsStatic() { if l.IsStatic() {
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is // TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
// disabled. // disabled.
addr := netip.AddrFrom4(*(*[4]byte)(l.IP.To4())) if sn := s.conf.subnet; !sn.Contains(l.IP) {
if sn := s.conf.subnet; !sn.Contains(addr) {
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP) return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
} }
} else if !inOffset { } else if !inOffset {
@ -325,7 +325,7 @@ func (s *v4Server) rmLease(lease *Lease) (err error) {
} }
for i, l := range s.leases { for i, l := range s.leases {
if l.IP.Equal(lease.IP) { if l.IP == lease.IP {
if !bytes.Equal(l.HWAddr, lease.HWAddr) || l.Hostname != lease.Hostname { if !bytes.Equal(l.HWAddr, lease.HWAddr) || l.Hostname != lease.Hostname {
return fmt.Errorf("lease for ip %s is different: %+v", lease.IP, l) return fmt.Errorf("lease for ip %s is different: %+v", lease.IP, l)
} }
@ -352,10 +352,11 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
return ErrUnconfigured return ErrUnconfigured
} }
ip := l.IP.To4() l.IP = l.IP.Unmap()
if ip == nil {
if !l.IP.Is4() {
return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP) return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP)
} else if gwIP := s.conf.GatewayIP; gwIP == netip.AddrFrom4(*(*[4]byte)(ip)) { } else if gwIP := s.conf.GatewayIP; gwIP == l.IP {
return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP) return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP)
} }
@ -396,7 +397,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
if err != nil { if err != nil {
err = fmt.Errorf( err = fmt.Errorf(
"removing dynamic leases for %s (%s): %w", "removing dynamic leases for %s (%s): %w",
ip, l.IP,
l.HWAddr, l.HWAddr,
err, err,
) )
@ -406,7 +407,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
err = s.addLease(l) err = s.addLease(l)
if err != nil { if err != nil {
err = fmt.Errorf("adding static lease for %s (%s): %w", ip, l.HWAddr, err) err = fmt.Errorf("adding static lease for %s (%s): %w", l.IP, l.HWAddr, err)
return return
} }
@ -429,7 +430,7 @@ func (s *v4Server) RemoveStaticLease(l *Lease) (err error) {
return ErrUnconfigured return ErrUnconfigured
} }
if len(l.IP) != 4 { if !l.IP.Is4() {
return fmt.Errorf("invalid IP") return fmt.Errorf("invalid IP")
} }
@ -542,8 +543,8 @@ func (s *v4Server) findExpiredLease() int {
func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) { func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) {
l = &Lease{HWAddr: slices.Clone(mac)} l = &Lease{HWAddr: slices.Clone(mac)}
l.IP = s.nextIP() nextIP := s.nextIP()
if l.IP == nil { if nextIP == nil {
i := s.findExpiredLease() i := s.findExpiredLease()
if i < 0 { if i < 0 {
return nil, nil return nil, nil
@ -554,6 +555,13 @@ func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) {
return s.leases[i], nil return s.leases[i], nil
} }
netIP, ok := netip.AddrFromSlice(nextIP)
if !ok {
return nil, errors.Error("invalid ip")
}
l.IP = netIP
err = s.addLease(l) err = s.addLease(l)
if err != nil { if err != nil {
return nil, err return nil, err
@ -603,7 +611,8 @@ func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *Lease, err error) {
return nil, nil return nil, nil
} }
if s.addrAvailable(l.IP) { leaseIP := l.IP.AsSlice()
if s.addrAvailable(leaseIP) {
return l, nil return l, nil
} }
@ -623,8 +632,9 @@ func (s *v4Server) handleDiscover(req, resp *dhcpv4.DHCPv4) (l *Lease, err error
l = s.findLease(mac) l = s.findLease(mac)
if l != nil { if l != nil {
reqIP := req.RequestedIPAddress() reqIP := req.RequestedIPAddress()
if len(reqIP) != 0 && !reqIP.Equal(l.IP) { leaseIP := net.IP(l.IP.AsSlice())
log.Debug("dhcpv4: different RequestedIP: %s != %s", reqIP, l.IP) if len(reqIP) != 0 && !reqIP.Equal(leaseIP) {
log.Debug("dhcpv4: different RequestedIP: %s != %s", reqIP, leaseIP)
} }
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
@ -674,12 +684,19 @@ func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (lease *Lease, mi
s.leasesLock.Lock() s.leasesLock.Lock()
defer s.leasesLock.Unlock() defer s.leasesLock.Unlock()
netIP, ok := netip.AddrFromSlice(ip)
if !ok {
log.Info("check lease: invalid IP: %s", ip)
return nil, false
}
for _, l := range s.leases { for _, l := range s.leases {
if !bytes.Equal(l.HWAddr, mac) { if !bytes.Equal(l.HWAddr, mac) {
continue continue
} }
if l.IP.Equal(ip) { if l.IP == netIP {
return l, false return l, false
} }
@ -878,9 +895,16 @@ func (s *v4Server) handleDecline(req, resp *dhcpv4.DHCPv4) (err error) {
reqIP = req.ClientIPAddr reqIP = req.ClientIPAddr
} }
netIP, ok := netip.AddrFromSlice(reqIP)
if !ok {
log.Info("dhcpv4: invalid IP: %s", reqIP)
return nil
}
var oldLease *Lease var oldLease *Lease
for _, l := range s.leases { for _, l := range s.leases {
if bytes.Equal(l.HWAddr, mac) && l.IP.Equal(reqIP) { if bytes.Equal(l.HWAddr, mac) && l.IP == netIP {
oldLease = l oldLease = l
break break
@ -920,8 +944,7 @@ func (s *v4Server) handleDecline(req, resp *dhcpv4.DHCPv4) (err error) {
log.Info("dhcpv4: changed ip from %s to %s for %s", reqIP, newLease.IP, mac) log.Info("dhcpv4: changed ip from %s to %s for %s", reqIP, newLease.IP, mac)
resp.YourIPAddr = make([]byte, 4) resp.YourIPAddr = net.IP(newLease.IP.AsSlice())
copy(resp.YourIPAddr, newLease.IP)
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
@ -944,8 +967,15 @@ func (s *v4Server) handleRelease(req, resp *dhcpv4.DHCPv4) (err error) {
s.leasesLock.Lock() s.leasesLock.Lock()
defer s.leasesLock.Unlock() defer s.leasesLock.Unlock()
netIP, ok := netip.AddrFromSlice(reqIP)
if !ok {
log.Info("dhcpv4: invalid IP: %s", reqIP)
return nil
}
for _, l := range s.leases { for _, l := range s.leases {
if !bytes.Equal(l.HWAddr, mac) || !l.IP.Equal(reqIP) { if !bytes.Equal(l.HWAddr, mac) || l.IP != netIP {
continue continue
} }
@ -1018,7 +1048,7 @@ func (s *v4Server) handle(req, resp *dhcpv4.DHCPv4) int {
} }
if l != nil { if l != nil {
resp.YourIPAddr = slices.Clone(l.IP) resp.YourIPAddr = net.IP(l.IP.AsSlice())
} }
s.updateOptions(req, resp) s.updateOptions(req, resp)

View File

@ -62,7 +62,7 @@ func TestV4Server_leasing(t *testing.T) {
anotherName = "another-client" anotherName = "another-client"
) )
staticIP := net.IP{192, 168, 10, 10} staticIP := netip.MustParseAddr("192.168.10.10")
anotherIP := DefaultRangeStart anotherIP := DefaultRangeStart
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB} anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}
@ -83,7 +83,7 @@ func TestV4Server_leasing(t *testing.T) {
Expiry: time.Unix(leaseExpireStatic, 0), Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName, Hostname: staticName,
HWAddr: anotherMAC, HWAddr: anotherMAC,
IP: anotherIP.AsSlice(), IP: anotherIP,
}) })
assert.ErrorIs(t, err, ErrDupHostname) assert.ErrorIs(t, err, ErrDupHostname)
}) })
@ -97,7 +97,7 @@ func TestV4Server_leasing(t *testing.T) {
Expiry: time.Unix(leaseExpireStatic, 0), Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: anotherName, Hostname: anotherName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: anotherIP.AsSlice(), IP: anotherIP,
}) })
testutil.AssertErrorMsg(t, wantErrMsg, err) testutil.AssertErrorMsg(t, wantErrMsg, err)
}) })
@ -124,13 +124,14 @@ func TestV4Server_leasing(t *testing.T) {
discoverAnOffer := func( discoverAnOffer := func(
t *testing.T, t *testing.T,
name string, name string,
ip net.IP, netIP netip.Addr,
mac net.HardwareAddr, mac net.HardwareAddr,
) (resp *dhcpv4.DHCPv4) { ) (resp *dhcpv4.DHCPv4) {
testutil.CleanupAndRequireSuccess(t, func() (err error) { testutil.CleanupAndRequireSuccess(t, func() (err error) {
return s.ResetLeases(s.GetLeases(LeasesStatic)) return s.ResetLeases(s.GetLeases(LeasesStatic))
}) })
ip := net.IP(netIP.AsSlice())
req, err := dhcpv4.NewDiscovery( req, err := dhcpv4.NewDiscovery(
mac, mac,
dhcpv4.WithOption(dhcpv4.OptHostName(name)), dhcpv4.WithOption(dhcpv4.OptHostName(name)),
@ -151,7 +152,7 @@ func TestV4Server_leasing(t *testing.T) {
} }
t.Run("same_name", func(t *testing.T) { t.Run("same_name", func(t *testing.T) {
resp := discoverAnOffer(t, staticName, anotherIP.AsSlice(), anotherMAC) resp := discoverAnOffer(t, staticName, anotherIP, anotherMAC)
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption( req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
dhcpv4.OptHostName(staticName), dhcpv4.OptHostName(staticName),
@ -161,11 +162,15 @@ func TestV4Server_leasing(t *testing.T) {
res := s4.handle(req, resp) res := s4.handle(req, resp)
require.Positive(t, res) require.Positive(t, res)
assert.Equal(t, aghnet.GenerateHostname(resp.YourIPAddr), resp.HostName()) var netIP netip.Addr
netIP, ok = netip.AddrFromSlice(resp.YourIPAddr)
require.True(t, ok)
assert.Equal(t, aghnet.GenerateHostname(netIP), resp.HostName())
}) })
t.Run("same_mac", func(t *testing.T) { t.Run("same_mac", func(t *testing.T) {
resp := discoverAnOffer(t, anotherName, anotherIP.AsSlice(), staticMAC) resp := discoverAnOffer(t, anotherName, anotherIP, staticMAC)
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption( req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
dhcpv4.OptHostName(anotherName), dhcpv4.OptHostName(anotherName),
@ -179,7 +184,8 @@ func TestV4Server_leasing(t *testing.T) {
require.Len(t, fqdnOptData, 3+len(staticName)) require.Len(t, fqdnOptData, 3+len(staticName))
assert.Equal(t, []uint8(staticName), fqdnOptData[3:]) assert.Equal(t, []uint8(staticName), fqdnOptData[3:])
assert.Equal(t, staticIP, resp.YourIPAddr) ip := net.IP(staticIP.AsSlice())
assert.Equal(t, ip, resp.YourIPAddr)
}) })
t.Run("same_ip", func(t *testing.T) { t.Run("same_ip", func(t *testing.T) {
@ -212,7 +218,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
lease: &Lease{ lease: &Lease{
Hostname: "success.local", Hostname: "success.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150}, IP: netip.MustParseAddr("192.168.10.150"),
}, },
name: "success", name: "success",
wantErrMsg: "", wantErrMsg: "",
@ -220,7 +226,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
lease: &Lease{ lease: &Lease{
Hostname: "probably-router.local", Hostname: "probably-router.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: DefaultGatewayIP.AsSlice(), IP: DefaultGatewayIP,
}, },
name: "with_gateway_ip", name: "with_gateway_ip",
wantErrMsg: "dhcpv4: adding static lease: " + wantErrMsg: "dhcpv4: adding static lease: " +
@ -229,7 +235,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
lease: &Lease{ lease: &Lease{
Hostname: "ip6.local", Hostname: "ip6.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.ParseIP("ffff::1"), IP: netip.MustParseAddr("ffff::1"),
}, },
name: "ipv6", name: "ipv6",
wantErrMsg: `dhcpv4: adding static lease: ` + wantErrMsg: `dhcpv4: adding static lease: ` +
@ -238,7 +244,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
lease: &Lease{ lease: &Lease{
Hostname: "bad-mac.local", Hostname: "bad-mac.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150}, IP: netip.MustParseAddr("192.168.10.150"),
}, },
name: "bad_mac", name: "bad_mac",
wantErrMsg: `dhcpv4: adding static lease: bad mac address "aa:aa": ` + wantErrMsg: `dhcpv4: adding static lease: bad mac address "aa:aa": ` +
@ -247,7 +253,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
lease: &Lease{ lease: &Lease{
Hostname: "bad-lbl-.local", Hostname: "bad-lbl-.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150}, IP: netip.MustParseAddr("192.168.10.150"),
}, },
name: "bad_hostname", name: "bad_hostname",
wantErrMsg: `dhcpv4: adding static lease: validating hostname: ` + wantErrMsg: `dhcpv4: adding static lease: validating hostname: ` +
@ -289,11 +295,11 @@ func TestV4_AddReplace(t *testing.T) {
dynLeases := []Lease{{ dynLeases := []Lease{{
Hostname: "dynamic-1.local", Hostname: "dynamic-1.local",
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150}, IP: netip.MustParseAddr("192.168.10.150"),
}, { }, {
Hostname: "dynamic-2.local", Hostname: "dynamic-2.local",
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 151}, IP: netip.MustParseAddr("192.168.10.151"),
}} }}
for i := range dynLeases { for i := range dynLeases {
@ -304,11 +310,11 @@ func TestV4_AddReplace(t *testing.T) {
stLeases := []*Lease{{ stLeases := []*Lease{{
Hostname: "static-1.local", Hostname: "static-1.local",
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150}, IP: netip.MustParseAddr("192.168.10.150"),
}, { }, {
Hostname: "static-2.local", Hostname: "static-2.local",
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 152}, IP: netip.MustParseAddr("192.168.10.152"),
}} }}
for _, l := range stLeases { for _, l := range stLeases {
@ -320,7 +326,7 @@ func TestV4_AddReplace(t *testing.T) {
require.Len(t, ls, 2) require.Len(t, ls, 2)
for i, l := range ls { for i, l := range ls {
assert.True(t, stLeases[i].IP.Equal(l.IP)) assert.Equal(t, stLeases[i].IP, l.IP)
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr) assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
assert.True(t, l.IsStatic()) assert.True(t, l.IsStatic())
} }
@ -513,7 +519,7 @@ func TestV4StaticLease_Get(t *testing.T) {
l := &Lease{ l := &Lease{
Hostname: "static-1.local", Hostname: "static-1.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: net.IP{192, 168, 10, 150}, IP: netip.MustParseAddr("192.168.10.150"),
} }
err := s.AddStaticLease(l) err := s.AddStaticLease(l)
require.NoError(t, err) require.NoError(t, err)
@ -539,7 +545,9 @@ func TestV4StaticLease_Get(t *testing.T) {
t.Run("offer", func(t *testing.T) { t.Run("offer", func(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
assert.Equal(t, mac, resp.ClientHWAddr) assert.Equal(t, mac, resp.ClientHWAddr)
assert.True(t, l.IP.Equal(resp.YourIPAddr))
ip := net.IP(l.IP.AsSlice())
assert.True(t, ip.Equal(resp.YourIPAddr))
assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice())) assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice())) assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
@ -564,7 +572,9 @@ func TestV4StaticLease_Get(t *testing.T) {
t.Run("ack", func(t *testing.T) { t.Run("ack", func(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
assert.Equal(t, mac, resp.ClientHWAddr) assert.Equal(t, mac, resp.ClientHWAddr)
assert.True(t, l.IP.Equal(resp.YourIPAddr))
ip := net.IP(l.IP.AsSlice())
assert.True(t, ip.Equal(resp.YourIPAddr))
assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice())) assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice())) assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
@ -583,7 +593,7 @@ func TestV4StaticLease_Get(t *testing.T) {
ls := s.GetLeases(LeasesStatic) ls := s.GetLeases(LeasesStatic)
require.Len(t, ls, 1) require.Len(t, ls, 1)
assert.True(t, l.IP.Equal(ls[0].IP)) assert.Equal(t, l.IP, ls[0].IP)
assert.Equal(t, mac, ls[0].HWAddr) assert.Equal(t, mac, ls[0].HWAddr)
}) })
} }
@ -681,7 +691,8 @@ func TestV4DynamicLease_Get(t *testing.T) {
ls := s.GetLeases(LeasesDynamic) ls := s.GetLeases(LeasesDynamic)
require.Len(t, ls, 1) require.Len(t, ls, 1)
assert.True(t, net.IP{192, 168, 10, 100}.Equal(ls[0].IP)) ip := netip.MustParseAddr("192.168.10.100")
assert.Equal(t, ip, ls[0].IP)
assert.Equal(t, mac, ls[0].HWAddr) assert.Equal(t, mac, ls[0].HWAddr)
}) })
} }
@ -862,3 +873,143 @@ func TestV4Server_Send(t *testing.T) {
assert.True(t, resp.IsBroadcast()) assert.True(t, resp.IsBroadcast())
}) })
} }
func TestV4Server_FindMACbyIP(t *testing.T) {
const (
staticName = "static-client"
anotherName = "another-client"
)
staticIP := netip.MustParseAddr("192.168.10.10")
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
anotherIP := netip.MustParseAddr("192.168.100.100")
anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}
s := &v4Server{
leases: []*Lease{{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName,
HWAddr: staticMAC,
IP: staticIP,
}, {
Expiry: time.Unix(10, 0),
Hostname: anotherName,
HWAddr: anotherMAC,
IP: anotherIP,
}},
}
testCases := []struct {
want net.HardwareAddr
ip netip.Addr
name string
}{{
name: "basic",
ip: staticIP,
want: staticMAC,
}, {
name: "not_found",
ip: netip.MustParseAddr("1.2.3.4"),
want: nil,
}, {
name: "expired",
ip: anotherIP,
want: nil,
}, {
name: "v6",
ip: netip.MustParseAddr("ffff::1"),
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mac := s.FindMACbyIP(tc.ip)
require.Equal(t, tc.want, mac)
})
}
}
func TestV4Server_handleDecline(t *testing.T) {
const (
dynamicName = "dynamic-client"
anotherName = "another-client"
)
dynamicIP := netip.MustParseAddr("192.168.10.200")
dynamicMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
s := defaultSrv(t)
s4, ok := s.(*v4Server)
require.True(t, ok)
s4.leases = []*Lease{{
Hostname: dynamicName,
HWAddr: dynamicMAC,
IP: dynamicIP,
}}
req, err := dhcpv4.New(
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(net.IP(dynamicIP.AsSlice()))),
)
require.NoError(t, err)
req.ClientIPAddr = net.IP(dynamicIP.AsSlice())
req.ClientHWAddr = dynamicMAC
resp := &dhcpv4.DHCPv4{}
err = s4.handleDecline(req, resp)
require.NoError(t, err)
wantResp := &dhcpv4.DHCPv4{
YourIPAddr: net.IP(s4.conf.RangeStart.AsSlice()),
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeAck),
),
}
require.Equal(t, wantResp, resp)
}
func TestV4Server_handleRelease(t *testing.T) {
const (
dynamicName = "dymamic-client"
anotherName = "another-client"
)
dynamicIP := netip.MustParseAddr("192.168.10.200")
dynamicMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
s := defaultSrv(t)
s4, ok := s.(*v4Server)
require.True(t, ok)
s4.leases = []*Lease{{
Hostname: dynamicName,
HWAddr: dynamicMAC,
IP: dynamicIP,
}}
req, err := dhcpv4.New(
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(net.IP(dynamicIP.AsSlice()))),
)
require.NoError(t, err)
req.ClientIPAddr = net.IP(dynamicIP.AsSlice())
req.ClientHWAddr = dynamicMAC
resp := &dhcpv4.DHCPv4{}
err = s4.handleRelease(req, resp)
require.NoError(t, err)
wantResp := &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeAck),
),
}
require.Equal(t, wantResp, resp)
}

View File

@ -61,13 +61,13 @@ func ip6InRange(start, ip net.IP) bool {
// ResetLeases resets leases. // ResetLeases resets leases.
func (s *v6Server) ResetLeases(leases []*Lease) (err error) { func (s *v6Server) ResetLeases(leases []*Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
s.leases = nil s.leases = nil
for _, l := range leases { for _, l := range leases {
ip := net.IP(l.IP.AsSlice())
if l.Expiry.Unix() != leaseExpireStatic && if l.Expiry.Unix() != leaseExpireStatic &&
!ip6InRange(s.conf.ipStart, l.IP) { !ip6InRange(s.conf.ipStart, ip) {
log.Debug("dhcpv6: skipping a lease with IP %v: not within current IP range", l.IP) log.Debug("dhcpv6: skipping a lease with IP %v: not within current IP range", l.IP)
@ -119,9 +119,8 @@ func (s *v6Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
return nil return nil
} }
netIP := ip.AsSlice()
for _, l := range s.leases { for _, l := range s.leases {
if l.IP.Equal(netIP) { if l.IP == ip {
if l.Expiry.After(now) || l.IsStatic() { if l.Expiry.After(now) || l.IsStatic() {
return l.HWAddr return l.HWAddr
} }
@ -133,7 +132,8 @@ func (s *v6Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
// Remove (swap) lease by index // Remove (swap) lease by index
func (s *v6Server) leaseRemoveSwapByIndex(i int) { func (s *v6Server) leaseRemoveSwapByIndex(i int) {
s.ipAddrs[s.leases[i].IP[15]] = 0 leaseIP := s.leases[i].IP.As16()
s.ipAddrs[leaseIP[15]] = 0
log.Debug("dhcpv6: removed lease %s", s.leases[i].HWAddr) log.Debug("dhcpv6: removed lease %s", s.leases[i].HWAddr)
n := len(s.leases) n := len(s.leases)
@ -162,7 +162,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) {
l = s.leases[i] l = s.leases[i]
} }
if net.IP.Equal(l.IP, lease.IP) { if l.IP == lease.IP {
if l.Expiry.Unix() == leaseExpireStatic { if l.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease already exists") return fmt.Errorf("static lease already exists")
} }
@ -178,7 +178,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) {
func (s *v6Server) AddStaticLease(l *Lease) (err error) { func (s *v6Server) AddStaticLease(l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
if len(l.IP) != net.IPv6len { if !l.IP.Is6() {
return fmt.Errorf("invalid IP") return fmt.Errorf("invalid IP")
} }
@ -210,7 +210,7 @@ func (s *v6Server) AddStaticLease(l *Lease) (err error) {
func (s *v6Server) RemoveStaticLease(l *Lease) (err error) { func (s *v6Server) RemoveStaticLease(l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }() defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
if len(l.IP) != 16 { if !l.IP.Is6() {
return fmt.Errorf("invalid IP") return fmt.Errorf("invalid IP")
} }
@ -234,14 +234,15 @@ func (s *v6Server) RemoveStaticLease(l *Lease) (err error) {
// Add a lease // Add a lease
func (s *v6Server) addLease(l *Lease) { func (s *v6Server) addLease(l *Lease) {
s.leases = append(s.leases, l) s.leases = append(s.leases, l)
s.ipAddrs[l.IP[15]] = 1 ip := l.IP.As16()
s.ipAddrs[ip[15]] = 1
log.Debug("dhcpv6: added lease %s <-> %s", l.IP, l.HWAddr) log.Debug("dhcpv6: added lease %s <-> %s", l.IP, l.HWAddr)
} }
// Remove a lease with the same properties // Remove a lease with the same properties
func (s *v6Server) rmLease(lease *Lease) (err error) { func (s *v6Server) rmLease(lease *Lease) (err error) {
for i, l := range s.leases { for i, l := range s.leases {
if net.IP.Equal(l.IP, lease.IP) { if l.IP == lease.IP {
if !bytes.Equal(l.HWAddr, lease.HWAddr) || if !bytes.Equal(l.HWAddr, lease.HWAddr) ||
l.Hostname != lease.Hostname { l.Hostname != lease.Hostname {
return fmt.Errorf("lease not found") return fmt.Errorf("lease not found")
@ -308,18 +309,27 @@ func (s *v6Server) reserveLease(mac net.HardwareAddr) *Lease {
s.leasesLock.Lock() s.leasesLock.Lock()
defer s.leasesLock.Unlock() defer s.leasesLock.Unlock()
copy(l.IP, s.conf.ipStart) ip := s.findFreeIP()
l.IP = s.findFreeIP() if ip == nil {
if l.IP == nil {
i := s.findExpiredLease() i := s.findExpiredLease()
if i < 0 { if i < 0 {
return nil return nil
} }
copy(s.leases[i].HWAddr, mac) copy(s.leases[i].HWAddr, mac)
return s.leases[i] return s.leases[i]
} }
netIP, ok := netip.AddrFromSlice(ip)
if !ok {
return nil
}
l.IP = netIP
s.addLease(&l) s.addLease(&l)
return &l return &l
} }
@ -388,7 +398,8 @@ func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *Lease) error {
return fmt.Errorf("no IANA.Addr option in %s", msg.Type().String()) return fmt.Errorf("no IANA.Addr option in %s", msg.Type().String())
} }
if !oiaAddr.IPv6Addr.Equal(lease.IP) { leaseIP := net.IP(lease.IP.AsSlice())
if !oiaAddr.IPv6Addr.Equal(leaseIP) {
return fmt.Errorf("invalid IANA.Addr option in %s", msg.Type().String()) return fmt.Errorf("invalid IANA.Addr option in %s", msg.Type().String())
} }
} }
@ -475,7 +486,7 @@ func (s *v6Server) process(msg *dhcpv6.Message, req, resp dhcpv6.DHCPv6) bool {
copy(oia.IaId[:], []byte(valueIAID)) copy(oia.IaId[:], []byte(valueIAID))
} }
oiaAddr := &dhcpv6.OptIAAddress{ oiaAddr := &dhcpv6.OptIAAddress{
IPv6Addr: lease.IP, IPv6Addr: net.IP(lease.IP.AsSlice()),
PreferredLifetime: lifetime, PreferredLifetime: lifetime,
ValidLifetime: lifetime, ValidLifetime: lifetime,
} }

View File

@ -4,7 +4,9 @@ package dhcpd
import ( import (
"net" "net"
"net/netip"
"testing" "testing"
"time"
"github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/iana"
@ -27,7 +29,7 @@ func TestV6_AddRemove_static(t *testing.T) {
// Add static lease. // Add static lease.
l := &Lease{ l := &Lease{
IP: net.ParseIP("2001::1"), IP: netip.MustParseAddr("2001::1"),
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
} }
err = s.AddStaticLease(l) err = s.AddStaticLease(l)
@ -46,7 +48,7 @@ func TestV6_AddRemove_static(t *testing.T) {
// Try to remove non-existent static lease. // Try to remove non-existent static lease.
err = s.RemoveStaticLease(&Lease{ err = s.RemoveStaticLease(&Lease{
IP: net.ParseIP("2001::2"), IP: netip.MustParseAddr("2001::2"),
HWAddr: l.HWAddr, HWAddr: l.HWAddr,
}) })
require.Error(t, err) require.Error(t, err)
@ -71,10 +73,10 @@ func TestV6_AddReplace(t *testing.T) {
// Add dynamic leases. // Add dynamic leases.
dynLeases := []*Lease{{ dynLeases := []*Lease{{
IP: net.ParseIP("2001::1"), IP: netip.MustParseAddr("2001::1"),
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}, { }, {
IP: net.ParseIP("2001::2"), IP: netip.MustParseAddr("2001::2"),
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}} }}
@ -83,10 +85,10 @@ func TestV6_AddReplace(t *testing.T) {
} }
stLeases := []*Lease{{ stLeases := []*Lease{{
IP: net.ParseIP("2001::1"), IP: netip.MustParseAddr("2001::1"),
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}, { }, {
IP: net.ParseIP("2001::3"), IP: netip.MustParseAddr("2001::3"),
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
}} }}
@ -99,7 +101,7 @@ func TestV6_AddReplace(t *testing.T) {
require.Len(t, ls, 2) require.Len(t, ls, 2)
for i, l := range ls { for i, l := range ls {
assert.True(t, stLeases[i].IP.Equal(l.IP)) assert.Equal(t, stLeases[i].IP, l.IP)
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr) assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix()) assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix())
} }
@ -126,7 +128,7 @@ func TestV6GetLease(t *testing.T) {
} }
l := &Lease{ l := &Lease{
IP: net.ParseIP("2001::1"), IP: netip.MustParseAddr("2001::1"),
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
} }
err = s.AddStaticLease(l) err = s.AddStaticLease(l)
@ -158,7 +160,8 @@ func TestV6GetLease(t *testing.T) {
oia = resp.Options.OneIANA() oia = resp.Options.OneIANA()
oiaAddr = oia.Options.OneAddress() oiaAddr = oia.Options.OneAddress()
assert.Equal(t, l.IP, oiaAddr.IPv6Addr) ip := net.IP(l.IP.AsSlice())
assert.Equal(t, ip, oiaAddr.IPv6Addr)
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds()) assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
}) })
@ -182,7 +185,8 @@ func TestV6GetLease(t *testing.T) {
oia = resp.Options.OneIANA() oia = resp.Options.OneIANA()
oiaAddr = oia.Options.OneAddress() oiaAddr = oia.Options.OneAddress()
assert.Equal(t, l.IP, oiaAddr.IPv6Addr) ip := net.IP(l.IP.AsSlice())
assert.Equal(t, ip, oiaAddr.IPv6Addr)
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds()) assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
}) })
@ -308,3 +312,72 @@ func TestIP6InRange(t *testing.T) {
}) })
} }
} }
func TestV6_FindMACbyIP(t *testing.T) {
const (
staticName = "static-client"
anotherName = "another-client"
)
staticIP := netip.MustParseAddr("2001::1")
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
anotherIP := netip.MustParseAddr("2001::100")
anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}
s := &v6Server{
leases: []*Lease{{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName,
HWAddr: staticMAC,
IP: staticIP,
}, {
Expiry: time.Unix(10, 0),
Hostname: anotherName,
HWAddr: anotherMAC,
IP: anotherIP,
}},
}
s.leases = []*Lease{{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName,
HWAddr: staticMAC,
IP: staticIP,
}, {
Expiry: time.Unix(10, 0),
Hostname: anotherName,
HWAddr: anotherMAC,
IP: anotherIP,
}}
testCases := []struct {
want net.HardwareAddr
ip netip.Addr
name string
}{{
name: "basic",
ip: staticIP,
want: staticMAC,
}, {
name: "not_found",
ip: netip.MustParseAddr("ffff::1"),
want: nil,
}, {
name: "expired",
ip: anotherIP,
want: nil,
}, {
name: "v4",
ip: netip.MustParseAddr("1.2.3.4"),
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mac := s.FindMACbyIP(tc.ip)
require.Equal(t, tc.want, mac)
})
}
}

View File

@ -243,17 +243,16 @@ func (s *Server) onDHCPLeaseChanged(flags int) {
lowhost := strings.ToLower(l.Hostname + "." + s.localDomainSuffix) lowhost := strings.ToLower(l.Hostname + "." + s.localDomainSuffix)
// Assume that we only process IPv4 now. // Assume that we only process IPv4 now.
// if !l.IP.Is4() {
// TODO(a.garipov): Remove once we switch to netip.Addr more fully. log.Debug("dnsforward: skipping invalid ip from dhcp: bad ipv4 net.IP %v", l.IP)
ip, err := netutil.IPToAddr(l.IP, netutil.AddrFamilyIPv4)
if err != nil {
log.Debug("dnsforward: skipping invalid ip %v from dhcp: %s", l.IP, err)
continue continue
} }
ipToHost[ip] = lowhost leaseIP := l.IP
hostToIP[lowhost] = ip
ipToHost[leaseIP] = lowhost
hostToIP[lowhost] = leaseIP
} }
s.setTableHostToIP(hostToIP) s.setTableHostToIP(hostToIP)

View File

@ -1073,7 +1073,7 @@ var testDHCP = &dhcpd.MockInterface{
OnEnabled: func() (ok bool) { return true }, OnEnabled: func() (ok bool) { return true },
OnLeases: func(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) { OnLeases: func(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) {
return []*dhcpd.Lease{{ return []*dhcpd.Lease{{
IP: net.IP{192, 168, 12, 34}, IP: netip.MustParseAddr("192.168.12.34"),
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
Hostname: "myhost", Hostname: "myhost",
}} }}

View File

@ -19,7 +19,6 @@ import (
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@ -859,15 +858,7 @@ func (clients *clientsContainer) updateFromDHCP(add bool) {
continue continue
} }
// TODO(a.garipov): Remove once we switch to netip.Addr more fully. ok := clients.addHostLocked(l.IP, l.Hostname, ClientSourceDHCP)
ipAddr, err := netutil.IPToAddrNoMapped(l.IP)
if err != nil {
log.Error("clients: bad client ip %v from dhcp: %s", l.IP, err)
continue
}
ok := clients.addHostLocked(ipAddr, l.Hostname, ClientSourceDHCP)
if ok { if ok {
n++ n++
} }

View File

@ -275,7 +275,7 @@ func TestClientsAddExisting(t *testing.T) {
t.Skip("skipping dhcp test on windows") t.Skip("skipping dhcp test on windows")
} }
ip := net.IP{1, 2, 3, 4} ip := netip.MustParseAddr("1.2.3.4")
// First, init a DHCP server with a single static lease. // First, init a DHCP server with a single static lease.
config := &dhcpd.ServerConfig{ config := &dhcpd.ServerConfig{