f-46: dhcpd: build dhcp replies over insomniacslk/dhcp #46

internal/dhcpd décide quelles options partent vers quelle MAC, sans I/O ni
état. L'encodage RFC 3442 de l'option 121 est délégué à la bibliothèque, qui le
teste elle-même.

L'option 3 est omise plutôt qu'envoyée vide : la dhcp-option=3 nue n'existait
que pour empêcher dnsmasq d'annoncer sa propre adresse. La route par défaut est
conditionnée au subnet (DefaultGateway) et à l'interface (Host.DefaultRoute),
ce qui remplace le mécanisme de tags.

24 tests plus un fuzz sur BuildReply. Les assertions d'encodage sont validées
par mutation.

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-08-31 15:51:36 +02:00
commit 8658acfe08
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
6 changed files with 570 additions and 22 deletions

27
go.mod
View file

@ -4,11 +4,21 @@ go 1.25.0
toolchain go1.25.14 toolchain go1.25.14
require (
github.com/coreos/go-systemd/v22 v22.6.0
github.com/dgraph-io/badger/v4 v4.8.0
github.com/insomniacslk/dhcp v0.0.0-20260831074340-8416b400a2b2
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/spf13/viper v1.21.0
github.com/vishvananda/netlink v1.3.1
github.com/vishvananda/netns v0.0.5
golang.org/x/sys v0.45.0
)
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/dgraph-io/badger/v4 v4.8.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
@ -17,11 +27,11 @@ require (
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
@ -29,18 +39,15 @@ require (
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.43.0 // indirect golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.37.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.8 // indirect
) )

54
go.sum
View file

@ -4,12 +4,18 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@ -23,12 +29,29 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/insomniacslk/dhcp v0.0.0-20260831074340-8416b400a2b2 h1:rhbGNh5bIA6JYahHdphjFiIbavqVspsvXFvTpBR7dQE=
github.com/insomniacslk/dhcp v0.0.0-20260831074340-8416b400a2b2/go.mod h1:tGfUTcnFYGYvVNCaZZhwlJySU/fQQxh9TmpsFzWXnnY=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
@ -37,6 +60,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
@ -49,8 +74,12 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
@ -63,24 +92,25 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

29
internal/dhcpd/dhcpd.go Normal file
View file

@ -0,0 +1,29 @@
package dhcpd
import (
"net"
"time"
)
const LeaseTime = 12 * time.Hour
func DNSServers() []net.IP {
return []net.IP{
net.IPv4(1, 1, 1, 1),
net.IPv4(8, 8, 8, 8),
}
}
type SubnetConfig struct {
Network *net.IPNet
InterfaceIP net.IP
VPCRoute *net.IPNet
DefaultGateway net.IP
}
type Host struct {
MAC net.HardwareAddr
IP net.IP
VM string
DefaultRoute bool
}

View file

@ -0,0 +1,59 @@
package dhcpd
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv4"
)
func FuzzBuildReply(f *testing.F) {
mac, err := net.ParseMAC("00:22:33:00:00:0a")
if err != nil {
f.Fatalf("ParseMAC: %v", err)
}
for _, kind := range []dhcpv4.MessageType{
dhcpv4.MessageTypeDiscover,
dhcpv4.MessageTypeRequest,
dhcpv4.MessageTypeRelease,
dhcpv4.MessageTypeDecline,
} {
req, err := dhcpv4.New(dhcpv4.WithMessageType(kind), dhcpv4.WithHwAddr(mac))
if err != nil {
f.Fatalf("New request: %v", err)
}
f.Add(req.ToBytes())
}
_, network, err := net.ParseCIDR("10.0.5.0/24")
if err != nil {
f.Fatalf("ParseCIDR: %v", err)
}
_, vpcRoute, err := net.ParseCIDR("10.0.0.0/16")
if err != nil {
f.Fatalf("ParseCIDR: %v", err)
}
c := SubnetConfig{
Network: network,
InterfaceIP: net.ParseIP("10.0.5.1"),
VPCRoute: vpcRoute,
DefaultGateway: net.ParseIP("10.0.5.254"),
}
h := Host{MAC: mac, IP: net.ParseIP("10.0.5.10"), VM: "vm-fuzz", DefaultRoute: true}
f.Fuzz(func(t *testing.T, raw []byte) {
req, err := dhcpv4.FromBytes(raw)
if err != nil {
return
}
reply, err := BuildReply(c, h, req)
if err != nil {
return
}
if reply == nil {
t.Fatal("nil reply without an error")
}
reply.ToBytes()
})
}

97
internal/dhcpd/reply.go Normal file
View file

@ -0,0 +1,97 @@
package dhcpd
import (
"errors"
"fmt"
"net"
"git.g3e.fr/syonad/two/internal/metadata"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var (
ErrNoInterfaceIP = errors.New("interface ip is required: guests would have no route to the metadata server")
ErrNoNetwork = errors.New("subnet network is required")
ErrNoHostIP = errors.New("host ip is required")
ErrNoRequest = errors.New("request is nil")
)
func metadataRoute() *net.IPNet {
return &net.IPNet{
IP: net.ParseIP(metadata.ServiceIP).To4(),
Mask: net.CIDRMask(32, 32),
}
}
func defaultRoute() *net.IPNet {
return &net.IPNet{
IP: net.IPv4zero.To4(),
Mask: net.CIDRMask(0, 32),
}
}
func Routes(c SubnetConfig, h Host) (dhcpv4.Routes, error) {
if c.InterfaceIP == nil {
return nil, ErrNoInterfaceIP
}
routes := dhcpv4.Routes{{Dest: metadataRoute(), Router: c.InterfaceIP}}
if c.VPCRoute != nil {
routes = append(routes, &dhcpv4.Route{Dest: c.VPCRoute, Router: c.InterfaceIP})
}
if h.DefaultRoute && c.DefaultGateway != nil {
routes = append(routes, &dhcpv4.Route{Dest: defaultRoute(), Router: c.DefaultGateway})
}
return routes, nil
}
func replyType(req *dhcpv4.DHCPv4) (dhcpv4.MessageType, error) {
switch req.MessageType() {
case dhcpv4.MessageTypeDiscover:
return dhcpv4.MessageTypeOffer, nil
case dhcpv4.MessageTypeRequest:
return dhcpv4.MessageTypeAck, nil
default:
return 0, fmt.Errorf("no reply built for message type %s", req.MessageType())
}
}
func BuildReply(c SubnetConfig, h Host, req *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, error) {
if req == nil {
return nil, ErrNoRequest
}
if c.Network == nil {
return nil, ErrNoNetwork
}
if h.IP == nil {
return nil, ErrNoHostIP
}
kind, err := replyType(req)
if err != nil {
return nil, err
}
routes, err := Routes(c, h)
if err != nil {
return nil, err
}
mods := []dhcpv4.Modifier{
dhcpv4.WithMessageType(kind),
dhcpv4.WithServerIP(c.InterfaceIP),
dhcpv4.WithYourIP(h.IP),
dhcpv4.WithNetmask(c.Network.Mask),
dhcpv4.WithLeaseTime(uint32(LeaseTime.Seconds())),
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(c.InterfaceIP)),
dhcpv4.WithOption(dhcpv4.OptDNS(DNSServers()...)),
dhcpv4.WithOption(dhcpv4.OptClasslessStaticRoute(routes...)),
}
if h.DefaultRoute && c.DefaultGateway != nil {
mods = append(mods, dhcpv4.WithOption(dhcpv4.OptRouter(c.DefaultGateway)))
}
return dhcpv4.NewReplyFromRequest(req, mods...)
}

View file

@ -0,0 +1,326 @@
package dhcpd
import (
"bytes"
"errors"
"net"
"testing"
"git.g3e.fr/syonad/two/internal/metadata"
"github.com/insomniacslk/dhcp/dhcpv4"
)
func cidr(t *testing.T, s string) *net.IPNet {
t.Helper()
_, n, err := net.ParseCIDR(s)
if err != nil {
t.Fatalf("ParseCIDR(%q): %v", s, err)
}
return n
}
func testConfig(t *testing.T) SubnetConfig {
t.Helper()
return SubnetConfig{
Network: cidr(t, "10.0.5.0/24"),
InterfaceIP: net.ParseIP("10.0.5.1"),
}
}
func testHost(t *testing.T) Host {
t.Helper()
mac, err := net.ParseMAC("00:22:33:00:00:0a")
if err != nil {
t.Fatalf("ParseMAC: %v", err)
}
return Host{MAC: mac, IP: net.ParseIP("10.0.5.10"), VM: "vm-test", DefaultRoute: true}
}
func request(t *testing.T, kind dhcpv4.MessageType, mac net.HardwareAddr) *dhcpv4.DHCPv4 {
t.Helper()
req, err := dhcpv4.New(dhcpv4.WithMessageType(kind), dhcpv4.WithHwAddr(mac))
if err != nil {
t.Fatalf("New request: %v", err)
}
return req
}
func encodedRoute(t *testing.T, routes dhcpv4.Routes, dest string) []byte {
t.Helper()
for _, r := range routes {
if r.Dest.String() == dest {
return dhcpv4.Routes{r}.ToBytes()
}
}
t.Fatalf("no route to %s in %s", dest, routes)
return nil
}
func TestRoutes_AlwaysCarriesTheMetadataRoute(t *testing.T) {
c := testConfig(t)
routes, err := Routes(c, Host{IP: net.ParseIP("10.0.5.10")})
if err != nil {
t.Fatalf("Routes: %v", err)
}
if len(routes) != 1 {
t.Fatalf("expected the metadata route alone, got %s", routes)
}
if got := routes[0].Dest.String(); got != metadata.ServiceIP+"/32" {
t.Errorf("destination = %s, want %s/32", got, metadata.ServiceIP)
}
if !routes[0].Router.Equal(c.InterfaceIP) {
t.Errorf("next-hop = %s, want the subnet interface ip %s", routes[0].Router, c.InterfaceIP)
}
}
func TestRoutes_WithoutInterfaceIPIsRejected(t *testing.T) {
_, err := Routes(SubnetConfig{Network: cidr(t, "10.0.5.0/24")}, testHost(t))
if !errors.Is(err, ErrNoInterfaceIP) {
t.Fatalf("error = %v, want ErrNoInterfaceIP", err)
}
}
func TestRoutes_VPCRouteUsesTheInterfaceIPAsNextHop(t *testing.T) {
c := testConfig(t)
c.VPCRoute = cidr(t, "10.0.0.0/16")
c.DefaultGateway = net.ParseIP("192.0.2.1")
routes, err := Routes(c, testHost(t))
if err != nil {
t.Fatalf("Routes: %v", err)
}
for _, r := range routes {
if r.Dest.String() != "10.0.0.0/16" {
continue
}
if !r.Router.Equal(c.InterfaceIP) {
t.Fatalf("vpc route next-hop = %s, want %s", r.Router, c.InterfaceIP)
}
return
}
t.Fatalf("no vpc route in %s", routes)
}
func TestRoutes_NoDefaultRouteWithoutDefaultGateway(t *testing.T) {
routes, err := Routes(testConfig(t), testHost(t))
if err != nil {
t.Fatalf("Routes: %v", err)
}
for _, r := range routes {
if ones, _ := r.Dest.Mask.Size(); ones == 0 {
t.Fatalf("unexpected default route in %s", routes)
}
}
}
func TestRoutes_NoDefaultRouteWhenTheInterfaceDoesNotReceiveIt(t *testing.T) {
c := testConfig(t)
c.DefaultGateway = net.ParseIP("10.0.5.254")
h := testHost(t)
h.DefaultRoute = false
routes, err := Routes(c, h)
if err != nil {
t.Fatalf("Routes: %v", err)
}
for _, r := range routes {
if ones, _ := r.Dest.Mask.Size(); ones == 0 {
t.Fatalf("a secondary interface must not receive the default route, got %s", routes)
}
}
}
func TestRoutes_DefaultRouteEncodesZeroDestinationOctets(t *testing.T) {
c := testConfig(t)
c.DefaultGateway = net.ParseIP("10.0.5.254")
routes, err := Routes(c, testHost(t))
if err != nil {
t.Fatalf("Routes: %v", err)
}
want := []byte{0x00, 10, 0, 5, 254}
if got := encodedRoute(t, routes, "0.0.0.0/0"); !bytes.Equal(got, want) {
t.Errorf("default route encoding = % x, want % x", got, want)
}
}
func TestRoutes_UnalignedPrefixEncodesOnlyItsSignificantOctets(t *testing.T) {
c := testConfig(t)
c.VPCRoute = cidr(t, "10.16.0.0/12")
routes, err := Routes(c, testHost(t))
if err != nil {
t.Fatalf("Routes: %v", err)
}
want := []byte{0x0c, 10, 16, 10, 0, 5, 1}
if got := encodedRoute(t, routes, "10.16.0.0/12"); !bytes.Equal(got, want) {
t.Errorf("/12 encoding = % x, want % x", got, want)
}
}
func TestRoutes_MetadataRouteEncodesOnFourDestinationOctets(t *testing.T) {
routes, err := Routes(testConfig(t), testHost(t))
if err != nil {
t.Fatalf("Routes: %v", err)
}
want := []byte{0x20, 169, 254, 169, 254, 10, 0, 5, 1}
if got := encodedRoute(t, routes, metadata.ServiceIP+"/32"); !bytes.Equal(got, want) {
t.Errorf("metadata route encoding = % x, want % x", got, want)
}
}
func TestBuildReply_DiscoverIsAnsweredWithAnOffer(t *testing.T) {
h := testHost(t)
reply, err := BuildReply(testConfig(t), h, request(t, dhcpv4.MessageTypeDiscover, h.MAC))
if err != nil {
t.Fatalf("BuildReply: %v", err)
}
if reply.MessageType() != dhcpv4.MessageTypeOffer {
t.Errorf("message type = %s, want OFFER", reply.MessageType())
}
}
func TestBuildReply_RequestIsAnsweredWithAnAck(t *testing.T) {
h := testHost(t)
reply, err := BuildReply(testConfig(t), h, request(t, dhcpv4.MessageTypeRequest, h.MAC))
if err != nil {
t.Fatalf("BuildReply: %v", err)
}
if reply.MessageType() != dhcpv4.MessageTypeAck {
t.Errorf("message type = %s, want ACK", reply.MessageType())
}
}
func TestBuildReply_ReleaseGetsNoReply(t *testing.T) {
h := testHost(t)
if _, err := BuildReply(testConfig(t), h, request(t, dhcpv4.MessageTypeRelease, h.MAC)); err == nil {
t.Fatal("a RELEASE must not produce a reply")
}
}
func TestBuildReply_DeclineGetsNoReply(t *testing.T) {
h := testHost(t)
if _, err := BuildReply(testConfig(t), h, request(t, dhcpv4.MessageTypeDecline, h.MAC)); err == nil {
t.Fatal("a DECLINE must not produce a reply")
}
}
func TestBuildReply_CarriesAddressMaskLeaseAndServerIdentifier(t *testing.T) {
c := testConfig(t)
h := testHost(t)
reply, err := BuildReply(c, h, request(t, dhcpv4.MessageTypeRequest, h.MAC))
if err != nil {
t.Fatalf("BuildReply: %v", err)
}
if !reply.YourIPAddr.Equal(h.IP) {
t.Errorf("yiaddr = %s, want %s", reply.YourIPAddr, h.IP)
}
if got := net.IP(reply.SubnetMask()).String(); got != net.IP(c.Network.Mask).String() {
t.Errorf("netmask = %s, want %s", got, net.IP(c.Network.Mask))
}
if got := reply.IPAddressLeaseTime(0); got != LeaseTime {
t.Errorf("lease time = %s, want %s", got, LeaseTime)
}
if got := reply.ServerIdentifier(); !got.Equal(c.InterfaceIP) {
t.Errorf("server identifier = %s, want %s", got, c.InterfaceIP)
}
if got := reply.DNS(); len(got) != 2 || !got[0].Equal(net.IPv4(1, 1, 1, 1)) || !got[1].Equal(net.IPv4(8, 8, 8, 8)) {
t.Errorf("dns = %v, want 1.1.1.1 and 8.8.8.8", got)
}
}
func TestBuildReply_NoRouterOptionWithoutDefaultRoute(t *testing.T) {
h := testHost(t)
reply, err := BuildReply(testConfig(t), h, request(t, dhcpv4.MessageTypeRequest, h.MAC))
if err != nil {
t.Fatalf("BuildReply: %v", err)
}
if got := reply.Router(); len(got) != 0 {
t.Errorf("router option = %v, want none: the guest would use the server as its gateway", got)
}
}
func TestBuildReply_RouterOptionCarriesTheDefaultGateway(t *testing.T) {
c := testConfig(t)
c.DefaultGateway = net.ParseIP("10.0.5.254")
h := testHost(t)
reply, err := BuildReply(c, h, request(t, dhcpv4.MessageTypeRequest, h.MAC))
if err != nil {
t.Fatalf("BuildReply: %v", err)
}
got := reply.Router()
if len(got) != 1 || !got[0].Equal(c.DefaultGateway) {
t.Errorf("router option = %v, want [%s]", got, c.DefaultGateway)
}
}
func TestBuildReply_SecondaryInterfaceGetsNoRouterOption(t *testing.T) {
c := testConfig(t)
c.DefaultGateway = net.ParseIP("10.0.5.254")
h := testHost(t)
h.DefaultRoute = false
reply, err := BuildReply(c, h, request(t, dhcpv4.MessageTypeRequest, h.MAC))
if err != nil {
t.Fatalf("BuildReply: %v", err)
}
if got := reply.Router(); len(got) != 0 {
t.Errorf("router option = %v, want none on a secondary interface", got)
}
}
func TestBuildReply_ClasslessStaticRouteIsPresent(t *testing.T) {
h := testHost(t)
reply, err := BuildReply(testConfig(t), h, request(t, dhcpv4.MessageTypeRequest, h.MAC))
if err != nil {
t.Fatalf("BuildReply: %v", err)
}
if got := reply.ClasslessStaticRoute(); len(got) == 0 {
t.Fatal("option 121 missing: cloud-init would have no route to the metadata server")
}
}
func TestBuildReply_WithoutInterfaceIPIsRejected(t *testing.T) {
c := testConfig(t)
c.InterfaceIP = nil
h := testHost(t)
if _, err := BuildReply(c, h, request(t, dhcpv4.MessageTypeRequest, h.MAC)); !errors.Is(err, ErrNoInterfaceIP) {
t.Fatalf("error = %v, want ErrNoInterfaceIP", err)
}
}
func TestBuildReply_WithoutNetworkIsRejected(t *testing.T) {
c := testConfig(t)
c.Network = nil
h := testHost(t)
if _, err := BuildReply(c, h, request(t, dhcpv4.MessageTypeRequest, h.MAC)); !errors.Is(err, ErrNoNetwork) {
t.Fatalf("error = %v, want ErrNoNetwork", err)
}
}
func TestBuildReply_WithoutHostIPIsRejected(t *testing.T) {
h := testHost(t)
h.IP = nil
if _, err := BuildReply(testConfig(t), h, request(t, dhcpv4.MessageTypeRequest, testHost(t).MAC)); !errors.Is(err, ErrNoHostIP) {
t.Fatalf("error = %v, want ErrNoHostIP", err)
}
}
func TestBuildReply_NilRequestIsRejected(t *testing.T) {
if _, err := BuildReply(testConfig(t), testHost(t), nil); !errors.Is(err, ErrNoRequest) {
t.Fatalf("error = %v, want ErrNoRequest", err)
}
}