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:
parent
4f591f6ab5
commit
8658acfe08
6 changed files with 570 additions and 22 deletions
27
go.mod
27
go.mod
|
|
@ -4,11 +4,21 @@ go 1.25.0
|
|||
|
||||
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 (
|
||||
github.com/beorn7/perks v1.0.1 // 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/dustin/go-humanize v1.0.1 // 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/godbus/dbus/v5 v5.1.0 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // 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/cast v1.10.0 // 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/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric 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/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
)
|
||||
|
|
|
|||
54
go.sum
54
go.sum
|
|
@ -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/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/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/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/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/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/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
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/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/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/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/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/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/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
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/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
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/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
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/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
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/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/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
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/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
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/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/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
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 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
29
internal/dhcpd/dhcpd.go
Normal 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
|
||||
}
|
||||
59
internal/dhcpd/fuzz_test.go
Normal file
59
internal/dhcpd/fuzz_test.go
Normal 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
97
internal/dhcpd/reply.go
Normal 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...)
|
||||
}
|
||||
326
internal/dhcpd/reply_test.go
Normal file
326
internal/dhcpd/reply_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue