Skip to content

Commit

Permalink
Pull request: dhcpd: fix ip ranges
Browse files Browse the repository at this point in the history
Updates AdguardTeam#2541.

Squashed commit of the following:

commit c812999
Author: Ainar Garipov <[email protected]>
Date:   Tue Mar 16 18:10:07 2021 +0300

    agherr: imp docs

commit f43a5f5
Author: Ainar Garipov <[email protected]>
Date:   Tue Mar 16 17:35:59 2021 +0300

    all: imp err handling, fix code

commit ed26ad0
Author: Ainar Garipov <[email protected]>
Date:   Tue Mar 16 12:24:17 2021 +0300

    dhcpd: fix ip ranges
  • Loading branch information
ainar-g committed Mar 16, 2021
1 parent e6a8fe4 commit 9736123
Show file tree
Hide file tree
Showing 13 changed files with 463 additions and 137 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/stretchr/testify v1.6.1
github.com/ti-mo/netfilter v0.4.0
github.com/u-root/u-root v7.0.0+incompatible
github.com/willf/bitset v1.1.11
go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
Expand Down
48 changes: 42 additions & 6 deletions internal/agherr/agherr.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Package agherr contains the extended error type, and the function for
// wrapping several errors.
// Package agherr contains AdGuard Home's error handling helpers.
package agherr

import (
Expand All @@ -23,8 +22,10 @@ type manyError struct {
}

// Many wraps several errors and returns a single error.
func Many(message string, underlying ...error) error {
err := &manyError{
//
// TODO(a.garipov): Add formatting to message.
func Many(message string, underlying ...error) (err error) {
err = &manyError{
message: message,
underlying: underlying,
}
Expand All @@ -33,7 +34,7 @@ func Many(message string, underlying ...error) error {
}

// Error implements the error interface for *manyError.
func (e *manyError) Error() string {
func (e *manyError) Error() (msg string) {
switch len(e.underlying) {
case 0:
return e.message
Expand All @@ -58,7 +59,7 @@ func (e *manyError) Error() string {
}

// Unwrap implements the hidden errors.wrapper interface for *manyError.
func (e *manyError) Unwrap() error {
func (e *manyError) Unwrap() (err error) {
if len(e.underlying) == 0 {
return nil
}
Expand All @@ -71,3 +72,38 @@ func (e *manyError) Unwrap() error {
type wrapper interface {
Unwrap() error
}

// Annotate annotates the error with the message, unless the error is nil. This
// is a helper function to simplify code like this:
//
// func (f *foo) doStuff(s string) (err error) {
// defer func() {
// if err != nil {
// err = fmt.Errorf("bad foo string %q: %w", s, err)
// }
// }()
//
// // …
// }
//
// Instead, write:
//
// func (f *foo) doStuff(s string) (err error) {
// defer agherr.Annotate("bad foo string %q: %w", &err, s)
//
// // …
// }
//
// msg must contain the final ": %w" verb.
func Annotate(msg string, errPtr *error, args ...interface{}) {
if errPtr == nil {
return
}

err := *errPtr
if err != nil {
args = append(args, err)

*errPtr = fmt.Errorf(msg, args...)
}
}
67 changes: 57 additions & 10 deletions internal/agherr/agherr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,32 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestError_Error(t *testing.T) {
testCases := []struct {
err error
name string
want string
err error
}{{
err: Many("a"),
name: "simple",
want: "a",
err: Many("a"),
}, {
err: Many("a", errors.New("b")),
name: "wrapping",
want: "a: b",
err: Many("a", errors.New("b")),
}, {
err: Many("a", errors.New("b"), errors.New("c"), errors.New("d")),
name: "wrapping several",
want: "a: b (hidden: c, d)",
err: Many("a", errors.New("b"), errors.New("c"), errors.New("d")),
}, {
err: Many("a", Many("b", errors.New("c"), errors.New("d"))),
name: "wrapping wrapper",
want: "a: b: c (hidden: d)",
err: Many("a", Many("b", errors.New("c"), errors.New("d"))),
}}

for _, tc := range testCases {
assert.Equal(t, tc.want, tc.err.Error(), tc.name)
}
Expand All @@ -43,33 +45,78 @@ func TestError_Unwrap(t *testing.T) {
errWrapped
errNil
)

errs := []error{
errSimple: errors.New("a"),
errWrapped: fmt.Errorf("err: %w", errors.New("nested")),
errNil: nil,
}

testCases := []struct {
name string
want error
wrapped error
name string
}{{
name: "simple",
want: errs[errSimple],
wrapped: Many("a", errs[errSimple]),
name: "simple",
}, {
name: "nested",
want: errs[errWrapped],
wrapped: Many("b", errs[errWrapped]),
name: "nested",
}, {
name: "nil passed",
want: errs[errNil],
wrapped: Many("c", errs[errNil]),
name: "nil passed",
}, {
name: "nil not passed",
want: nil,
wrapped: Many("d"),
name: "nil not passed",
}}

for _, tc := range testCases {
assert.Equal(t, tc.want, errors.Unwrap(tc.wrapped), tc.name)
}
}

func TestAnnotate(t *testing.T) {
const s = "1234"
const wantMsg = `bad string "1234": test`

// Don't use const, because we can't take a pointer of a constant.
var errTest error = Error("test")

t.Run("nil", func(t *testing.T) {
var errPtr *error
assert.NotPanics(t, func() {
Annotate("bad string %q: %w", errPtr, s)
})
})

t.Run("non_nil", func(t *testing.T) {
errPtr := &errTest
assert.NotPanics(t, func() {
Annotate("bad string %q: %w", errPtr, s)
})

require.NotNil(t, errPtr)

err := *errPtr
require.NotNil(t, err)

assert.Equal(t, wantMsg, err.Error())
})

t.Run("defer", func(t *testing.T) {
f := func() (err error) {
defer Annotate("bad string %q: %w", &errTest, s)

return errTest
}

err := f()
require.NotNil(t, err)

assert.Equal(t, wantMsg, err.Error())
})
}
99 changes: 99 additions & 0 deletions internal/dhcpd/iprange.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package dhcpd

import (
"fmt"
"math"
"math/big"
"net"

"github.com/AdguardTeam/AdGuardHome/internal/agherr"
)

// ipRange is an inclusive range of IP addresses.
//
// It is safe for concurrent use.
//
// TODO(a.garipov): Perhaps create an optimised version with uint32 for
// IPv4 ranges? Or use one of uint128 packages?
type ipRange struct {
start *big.Int
end *big.Int
}

// maxRangeLen is the maximum IP range length. The bitsets used in servers only
// accept uints, which can have the size of 32 bit.
const maxRangeLen = math.MaxUint32

// newIPRange creates a new IP address range. start must be less than end. The
// resulting range must not be greater than maxRangeLen.
func newIPRange(start, end net.IP) (r *ipRange, err error) {
defer agherr.Annotate("invalid ip range: %w", &err)

// Make sure that both are 16 bytes long to simplify handling in
// methods.
start, end = start.To16(), end.To16()

startInt := (&big.Int{}).SetBytes(start)
endInt := (&big.Int{}).SetBytes(end)
diff := (&big.Int{}).Sub(endInt, startInt)

if diff.Sign() <= 0 {
return nil, fmt.Errorf("start is greater than or equal to end")
} else if !diff.IsUint64() || diff.Uint64() > maxRangeLen {
return nil, fmt.Errorf("range is too large")
}

r = &ipRange{
start: startInt,
end: endInt,
}

return r, nil
}

// contains returns true if r contains ip.
func (r *ipRange) contains(ip net.IP) (ok bool) {
ipInt := (&big.Int{}).SetBytes(ip.To16())

return r.containsInt(ipInt)
}

// containsInt returns true if r contains ipInt.
func (r *ipRange) containsInt(ipInt *big.Int) (ok bool) {
return ipInt.Cmp(r.start) >= 0 && ipInt.Cmp(r.end) <= 0
}

// ipPredicate is a function that is called on every IP address in
// (*ipRange).find. ip is given in the 16-byte form.
type ipPredicate func(ip net.IP) (ok bool)

// find finds the first IP address in r for which p returns true. ip is in the
// 16-byte form.
func (r *ipRange) find(p ipPredicate) (ip net.IP) {
ip = make(net.IP, net.IPv6len)
_1 := big.NewInt(1)
for i := (&big.Int{}).Set(r.start); i.Cmp(r.end) <= 0; i.Add(i, _1) {
i.FillBytes(ip)
if p(ip) {
return ip
}
}

return nil
}

// offset returns the offset of ip from the beginning of r. It returns 0 and
// false if ip is not in r.
func (r *ipRange) offset(ip net.IP) (offset uint, ok bool) {
ip = ip.To16()
ipInt := (&big.Int{}).SetBytes(ip)
if !r.containsInt(ipInt) {
return 0, false
}

offsetInt := (&big.Int{}).Sub(ipInt, r.start)

// Assume that the range was checked against maxRangeLen during
// construction.
return uint(offsetInt.Uint64()), true
}
Loading

0 comments on commit 9736123

Please sign in to comment.