diff --git a/go.mod b/go.mod index cc743cb..0e59eb9 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 402452d..271450a 100644 --- a/go.sum +++ b/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= diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go new file mode 100644 index 0000000..3d1f95b --- /dev/null +++ b/internal/dhcpd/dhcpd.go @@ -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 +} diff --git a/internal/dhcpd/fuzz_test.go b/internal/dhcpd/fuzz_test.go new file mode 100644 index 0000000..b152bb9 --- /dev/null +++ b/internal/dhcpd/fuzz_test.go @@ -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() + }) +} diff --git a/internal/dhcpd/reply.go b/internal/dhcpd/reply.go new file mode 100644 index 0000000..a603b41 --- /dev/null +++ b/internal/dhcpd/reply.go @@ -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...) +} diff --git a/internal/dhcpd/reply_test.go b/internal/dhcpd/reply_test.go new file mode 100644 index 0000000..f96a126 --- /dev/null +++ b/internal/dhcpd/reply_test.go @@ -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) + } +}