mirror of https://github.com/jetkvm/kvm.git
feat: network configuration manager
This commit is contained in:
parent
4275d0957a
commit
7f6b937945
3
Makefile
3
Makefile
|
@ -22,6 +22,8 @@ BIN_DIR := $(shell pwd)/bin
|
|||
|
||||
TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u)
|
||||
|
||||
TEST_FORMAT := testdox
|
||||
|
||||
hash_resource:
|
||||
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
||||
|
||||
|
@ -59,6 +61,7 @@ build_dev_test: build_test2json build_gotestsum
|
|||
chmod +x $(BIN_DIR)/tests/run_all_tests; \
|
||||
cp $(BIN_DIR)/test2json $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/test2json; \
|
||||
cp $(BIN_DIR)/gotestsum $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/gotestsum; \
|
||||
echo $(TEST_FORMAT) > $(BIN_DIR)/tests/.test_format; \
|
||||
tar czfv device-tests.tar.gz -C $(BIN_DIR)/tests .
|
||||
|
||||
frontend:
|
||||
|
|
|
@ -45,6 +45,7 @@ LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
|||
RUN_GO_TESTS=false
|
||||
RUN_GO_TESTS_ONLY=false
|
||||
INSTALL_APP=false
|
||||
TEST_FORMAT=${TEST_FORMAT:-"testdox"}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
@ -105,7 +106,7 @@ fi
|
|||
|
||||
if [ "$RUN_GO_TESTS" = true ]; then
|
||||
msg_info "▶ Building go tests"
|
||||
make build_dev_test
|
||||
make TEST_FORMAT=${TEST_FORMAT} build_dev_test
|
||||
|
||||
msg_info "▶ Copying device-tests.tar.gz to remote host"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||
|
@ -116,7 +117,7 @@ set -e
|
|||
TMP_DIR=$(mktemp -d)
|
||||
cd ${TMP_DIR}
|
||||
tar zxf /tmp/device-tests.tar.gz
|
||||
./gotestsum --format=testdox \
|
||||
./gotestsum --format=$(cat .test_format) \
|
||||
--jsonfile=/tmp/device-tests.json \
|
||||
--post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \
|
||||
--raw-command -- ./run_all_tests -json
|
||||
|
|
17
go.mod
17
go.mod
|
@ -8,13 +8,14 @@ require (
|
|||
github.com/coder/websocket v1.8.13
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/logger v1.2.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-co-op/gocron/v2 v2.16.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/guregu/null/v6 v6.0.0
|
||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/mdns/v2 v2.0.7
|
||||
github.com/pion/webrtc/v4 v4.1.3
|
||||
|
@ -28,9 +29,9 @@ require (
|
|||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.bug.st/serial v1.6.4
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/sys v0.34.0
|
||||
)
|
||||
|
||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||
|
@ -50,15 +51,20 @@ require (
|
|||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/packet v1.1.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/pilebones/go-udev v0.9.1 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
|
@ -75,14 +81,17 @@ require (
|
|||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
42
go.sum
42
go.sum
|
@ -28,8 +28,6 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc=
|
||||
|
@ -38,6 +36,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
|
||||
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
|
@ -62,6 +62,15 @@ github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341 h1:zPrkLSKi7kKJoN
|
|||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f h1:dd33oobuIv9PcBVqvbEiCXEbNTomOHyj3WFuC5YiPRU=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f/go.mod h1:zhFlBeJssZ1YBCMZ5Lzu1pX4vhftDvU10WUVb1uXKtM=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
|
@ -88,6 +97,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
|
||||
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -99,6 +112,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||
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/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
|
@ -146,6 +161,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
|
|||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
|
@ -156,6 +173,8 @@ github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzr
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
@ -165,6 +184,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
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/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
|
@ -175,23 +196,28 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
|||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
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/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -379,6 +379,18 @@ func (f *FieldConfig) validateSingleValue(val string, index int) error {
|
|||
}
|
||||
|
||||
switch validateType {
|
||||
case "int":
|
||||
if _, err := strconv.Atoi(val); err != nil {
|
||||
return fmt.Errorf("field `%s` is not a valid integer: %s", f.Name, val)
|
||||
}
|
||||
case "ipv6_prefix_length":
|
||||
valInt, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field `%s` is not a valid IPv6 prefix length: %s", f.Name, val)
|
||||
}
|
||||
if valInt < 0 || valInt > 128 {
|
||||
return fmt.Errorf("field `%s` is not a valid IPv6 prefix length: %s", f.Name, val)
|
||||
}
|
||||
case "ipv4":
|
||||
if net.ParseIP(val).To4() == nil {
|
||||
return fmt.Errorf("%s is not a valid IPv4 address: %s", fieldRef, val)
|
||||
|
|
|
@ -25,7 +25,7 @@ type testIPv4StaticConfig struct {
|
|||
|
||||
type testIPv6StaticConfig struct {
|
||||
Address null.String `json:"address" validate_type:"ipv6" required:"true"`
|
||||
Prefix null.String `json:"prefix" validate_type:"ipv6" required:"true"`
|
||||
PrefixLength null.Int `json:"prefix_length" validate_type:"ipv6_prefix_length" required:"true"`
|
||||
Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"`
|
||||
DNS []string `json:"dns" validate_type:"ipv6" required:"true"`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,424 @@
|
|||
package dhclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
VendorIdentifier = "jetkvm"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = logging.GetSubsystemLogger("dhclient")
|
||||
|
||||
ErrIPv6LinkTimeout = errors.New("timeout after waiting for a non-tentative IPv6 address")
|
||||
ErrIPv6RouteTimeout = errors.New("timeout after waiting for an IPv6 route")
|
||||
ErrInterfaceUpTimeout = errors.New("timeout after waiting for an interface to come up")
|
||||
ErrInterfaceUpCanceled = errors.New("context canceled while waiting for an interface to come up")
|
||||
)
|
||||
|
||||
type LeaseChangeHandler func(lease *Lease)
|
||||
|
||||
// Config is a DHCP client configuration.
|
||||
type Config struct {
|
||||
LinkUpTimeout time.Duration
|
||||
|
||||
// Timeout is the timeout for one DHCP request attempt.
|
||||
Timeout time.Duration
|
||||
|
||||
// Retries is how many times to retry DHCP attempts.
|
||||
Retries int
|
||||
|
||||
// IPv4 is whether to request an IPv4 lease.
|
||||
IPv4 bool
|
||||
|
||||
// IPv6 is whether to request an IPv6 lease.
|
||||
IPv6 bool
|
||||
|
||||
// Modifiers4 allows modifications to the IPv4 DHCP request.
|
||||
Modifiers4 []dhcpv4.Modifier
|
||||
|
||||
// Modifiers6 allows modifications to the IPv6 DHCP request.
|
||||
Modifiers6 []dhcpv6.Modifier
|
||||
|
||||
// V6ServerAddr can be a unicast or broadcast destination for DHCPv6
|
||||
// messages.
|
||||
//
|
||||
// If not set, it will default to nclient6's default (all servers &
|
||||
// relay agents).
|
||||
V6ServerAddr *net.UDPAddr
|
||||
|
||||
// V6ClientPort is the port that is used to send and receive DHCPv6
|
||||
// messages.
|
||||
//
|
||||
// If not set, it will default to dhcpv6's default (546).
|
||||
V6ClientPort *int
|
||||
|
||||
// V4ServerAddr can be a unicast or broadcast destination for IPv4 DHCP
|
||||
// messages.
|
||||
//
|
||||
// If not set, it will default to nclient4's default (DHCP broadcast
|
||||
// address).
|
||||
V4ServerAddr *net.UDPAddr
|
||||
|
||||
// If true, add Client Identifier (61) option to the IPv4 request.
|
||||
V4ClientIdentifier bool
|
||||
|
||||
OnLease4Change LeaseChangeHandler
|
||||
OnLease6Change LeaseChangeHandler
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ifaces []netlink.Link
|
||||
cfg Config
|
||||
l *zerolog.Logger
|
||||
|
||||
ctx context.Context
|
||||
|
||||
// TODO: support multiple interfaces
|
||||
currentLease4 *Lease
|
||||
currentLease6 *Lease
|
||||
|
||||
mu sync.Mutex
|
||||
cfgMu sync.Mutex
|
||||
|
||||
lease4Mu sync.Mutex
|
||||
lease6Mu sync.Mutex
|
||||
|
||||
scheduler gocron.Scheduler
|
||||
}
|
||||
|
||||
// NewClient creates a new DHCP client for the given interface.
|
||||
func NewClient(ctx context.Context, ifaces []netlink.Link, c *Config, l *zerolog.Logger) (*Client, error) {
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create scheduler: %w", err)
|
||||
}
|
||||
|
||||
cfg := *c
|
||||
if cfg.LinkUpTimeout == 0 {
|
||||
cfg.LinkUpTimeout = 30 * time.Second
|
||||
}
|
||||
|
||||
if cfg.Timeout == 0 {
|
||||
cfg.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
if cfg.Retries == 0 {
|
||||
cfg.Retries = 3
|
||||
}
|
||||
|
||||
return &Client{
|
||||
ctx: ctx,
|
||||
ifaces: ifaces,
|
||||
cfg: cfg,
|
||||
l: l,
|
||||
scheduler: scheduler,
|
||||
|
||||
currentLease4: nil,
|
||||
currentLease6: nil,
|
||||
|
||||
lease4Mu: sync.Mutex{},
|
||||
lease6Mu: sync.Mutex{},
|
||||
|
||||
mu: sync.Mutex{},
|
||||
cfgMu: sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) ensureInterfaceUp(iface netlink.Link) (netlink.Link, error) {
|
||||
ifname := iface.Attrs().Name
|
||||
|
||||
l := c.l.With().Str("interface", ifname).Logger()
|
||||
|
||||
linkUpTimeout := time.After(c.cfg.LinkUpTimeout)
|
||||
|
||||
for {
|
||||
link, err := netlink.LinkByName(ifname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state := link.Attrs().OperState
|
||||
if state == netlink.OperUp || state == netlink.OperUnknown {
|
||||
return link, nil
|
||||
}
|
||||
|
||||
l.Info().Interface("state", state).Msg("bringing up interface")
|
||||
|
||||
if err = netlink.LinkSetUp(link); err != nil {
|
||||
l.Error().Err(err).Msg("interface can't make it up")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
continue
|
||||
case <-c.ctx.Done():
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, ErrInterfaceUpCanceled
|
||||
case <-linkUpTimeout:
|
||||
l.Error().Msg("interface is still down after timeout")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, ErrInterfaceUpTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) sendInitialRequests() chan interface{} {
|
||||
return c.sendRequests(c.cfg.IPv4, c.cfg.IPv6)
|
||||
}
|
||||
|
||||
func (c *Client) sendRequests(ipv4, ipv6 bool) chan interface{} {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Yeah, this is a hack, until we can cancel all leases in progress.
|
||||
r := make(chan interface{}, 3*len(c.ifaces))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, iface := range c.ifaces {
|
||||
wg.Add(1)
|
||||
go func(iface netlink.Link) {
|
||||
defer wg.Done()
|
||||
|
||||
ifname := iface.Attrs().Name
|
||||
l := c.l.With().Str("interface", ifname).Logger()
|
||||
|
||||
iface, err := c.ensureInterfaceUp(iface)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("Could not bring up interface")
|
||||
return
|
||||
}
|
||||
|
||||
if ipv4 {
|
||||
wg.Add(1)
|
||||
go func(iface netlink.Link) {
|
||||
defer wg.Done()
|
||||
lease, err := c.requestLease4(iface)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("Could not get IPv4 lease")
|
||||
return
|
||||
}
|
||||
r <- lease
|
||||
}(iface)
|
||||
}
|
||||
|
||||
if ipv6 {
|
||||
return // TODO: implement DHCP6
|
||||
wg.Add(1)
|
||||
go func(iface netlink.Link) {
|
||||
defer wg.Done()
|
||||
lease, err := c.requestLease6(iface)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("Could not get IPv6 lease")
|
||||
return
|
||||
}
|
||||
r <- lease
|
||||
}(iface)
|
||||
}
|
||||
}(iface)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(r)
|
||||
}()
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *Client) Lease4() *Lease {
|
||||
c.lease4Mu.Lock()
|
||||
defer c.lease4Mu.Unlock()
|
||||
|
||||
return c.currentLease4
|
||||
}
|
||||
|
||||
func (c *Client) Lease6() *Lease {
|
||||
c.lease6Mu.Lock()
|
||||
defer c.lease6Mu.Unlock()
|
||||
|
||||
return c.currentLease6
|
||||
}
|
||||
|
||||
func (c *Client) Domain() string {
|
||||
c.lease4Mu.Lock()
|
||||
defer c.lease4Mu.Unlock()
|
||||
|
||||
if c.currentLease4 != nil {
|
||||
return c.currentLease4.Domain
|
||||
}
|
||||
|
||||
c.lease6Mu.Lock()
|
||||
defer c.lease6Mu.Unlock()
|
||||
|
||||
if c.currentLease6 != nil {
|
||||
return c.currentLease6.Domain
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Client) handleLeaseChange(lease *Lease) {
|
||||
// do not use defer here, because we need to unlock the mutex before returning
|
||||
|
||||
ipv4 := lease.p4 != nil
|
||||
version := "ipv4"
|
||||
|
||||
if ipv4 {
|
||||
c.lease4Mu.Lock()
|
||||
c.currentLease4 = lease
|
||||
} else {
|
||||
version = "ipv6"
|
||||
c.lease6Mu.Lock()
|
||||
c.currentLease6 = lease
|
||||
}
|
||||
|
||||
// clear all current jobs with the same tags
|
||||
c.scheduler.RemoveByTags(version)
|
||||
|
||||
// add scheduler job to renew the lease
|
||||
if lease.RenewalTime > 0 {
|
||||
c.scheduler.NewJob(
|
||||
gocron.DurationJob(lease.RenewalTime),
|
||||
gocron.NewTask(func() {
|
||||
c.l.Info().Msg("renewing lease")
|
||||
for lease := range c.sendRequests(ipv4, !ipv4) {
|
||||
if lease, ok := lease.(*Lease); ok {
|
||||
c.handleLeaseChange(lease)
|
||||
}
|
||||
}
|
||||
}),
|
||||
gocron.WithName(fmt.Sprintf("renew-%s", version)),
|
||||
gocron.WithSingletonMode(gocron.LimitModeWait),
|
||||
gocron.WithTags(version),
|
||||
)
|
||||
}
|
||||
|
||||
c.apply()
|
||||
|
||||
if ipv4 {
|
||||
c.lease4Mu.Unlock()
|
||||
} else {
|
||||
c.lease6Mu.Unlock()
|
||||
}
|
||||
|
||||
// TODO: handle lease expiration
|
||||
if c.cfg.OnLease4Change != nil && ipv4 {
|
||||
c.cfg.OnLease4Change(lease)
|
||||
}
|
||||
|
||||
if c.cfg.OnLease6Change != nil && !ipv4 {
|
||||
c.cfg.OnLease6Change(lease)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) renew() {
|
||||
for lease := range c.sendRequests(c.cfg.IPv4, c.cfg.IPv6) {
|
||||
if lease, ok := lease.(*Lease); ok {
|
||||
c.handleLeaseChange(lease)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Renew() {
|
||||
go c.renew()
|
||||
}
|
||||
|
||||
func (c *Client) Release() {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
func (c *Client) SetIPv4(ipv4 bool) {
|
||||
c.cfgMu.Lock()
|
||||
defer c.cfgMu.Unlock()
|
||||
|
||||
currentIPv4 := c.cfg.IPv4
|
||||
c.cfg.IPv4 = ipv4
|
||||
|
||||
if !ipv4 {
|
||||
c.lease4Mu.Lock()
|
||||
c.currentLease4 = nil
|
||||
c.lease4Mu.Unlock()
|
||||
c.scheduler.RemoveByTags("ipv4")
|
||||
}
|
||||
|
||||
if currentIPv4 || ipv4 {
|
||||
// TODO: send initial requests
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetIPv6(ipv6 bool) {
|
||||
c.cfg.IPv6 = ipv6
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
if err := c.killUdhcpc(); err != nil {
|
||||
c.l.Warn().Err(err).Msg("failed to kill udhcpc processes, continuing anyway")
|
||||
}
|
||||
|
||||
c.scheduler.Start()
|
||||
|
||||
go func() {
|
||||
for lease := range c.sendInitialRequests() {
|
||||
if lease, ok := lease.(*Lease); ok {
|
||||
c.handleLeaseChange(lease)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) apply() {
|
||||
var (
|
||||
iface string
|
||||
nameservers []net.IP
|
||||
searchList []string
|
||||
domain string
|
||||
)
|
||||
|
||||
if c.currentLease4 != nil {
|
||||
iface = c.currentLease4.InterfaceName
|
||||
nameservers = c.currentLease4.DNS
|
||||
searchList = c.currentLease4.SearchList
|
||||
domain = c.currentLease4.Domain
|
||||
}
|
||||
|
||||
if c.currentLease6 != nil {
|
||||
iface = c.currentLease6.InterfaceName
|
||||
nameservers = append(nameservers, c.currentLease6.DNS...)
|
||||
searchList = append(searchList, c.currentLease6.SearchList...)
|
||||
domain = c.currentLease6.Domain
|
||||
}
|
||||
|
||||
// deduplicate searchList
|
||||
searchList = slices.Compact(searchList)
|
||||
|
||||
c.l.Info().
|
||||
Str("interface", iface).
|
||||
Interface("nameservers", nameservers).
|
||||
Interface("searchList", searchList).
|
||||
Str("domain", domain).
|
||||
Msg("updating resolv.conf")
|
||||
|
||||
if err := updateResolvConf(iface, nameservers, searchList, domain); err != nil {
|
||||
c.l.Error().Err(err).Msg("failed to update resolv.conf")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package dhclient
|
||||
|
||||
import (
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func (c *Client) requestLease4(iface netlink.Link) (*Lease, error) {
|
||||
ifname := iface.Attrs().Name
|
||||
l := c.l.With().Str("interface", ifname).Logger()
|
||||
|
||||
mods := []nclient4.ClientOpt{
|
||||
nclient4.WithTimeout(c.cfg.Timeout),
|
||||
nclient4.WithRetry(c.cfg.Retries),
|
||||
}
|
||||
mods = append(mods, c.getDHCP4Logger(ifname))
|
||||
if c.cfg.V4ServerAddr != nil {
|
||||
mods = append(mods, nclient4.WithServerAddr(c.cfg.V4ServerAddr))
|
||||
}
|
||||
|
||||
client, err := nclient4.New(ifname, mods...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Prepend modifiers with default options, so they can be overriden.
|
||||
reqmods := append(
|
||||
[]dhcpv4.Modifier{
|
||||
dhcpv4.WithOption(dhcpv4.OptClassIdentifier(VendorIdentifier)),
|
||||
dhcpv4.WithRequestedOptions(dhcpv4.OptionSubnetMask),
|
||||
},
|
||||
c.cfg.Modifiers4...)
|
||||
|
||||
if c.cfg.V4ClientIdentifier {
|
||||
// Client Id is hardware type + mac per RFC 2132 9.14.
|
||||
ident := []byte{0x01} // Type ethernet
|
||||
ident = append(ident, iface.Attrs().HardwareAddr...)
|
||||
reqmods = append(reqmods, dhcpv4.WithOption(dhcpv4.OptClientIdentifier(ident)))
|
||||
}
|
||||
|
||||
l.Info().Msg("attempting to get DHCPv4 lease")
|
||||
lease, err := client.Request(c.ctx, reqmods...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Info().Msgf("DHCPv4 lease acquired: %s", lease.ACK.Summary())
|
||||
|
||||
return fromNclient4Lease(lease, ifname), nil
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package dhclient
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// isIPv6LinkReady returns true if the interface has a link-local address
|
||||
// which is not tentative.
|
||||
func isIPv6LinkReady(l netlink.Link) (bool, error) {
|
||||
addrs, err := netlink.AddrList(l, netlink.FAMILY_V6)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.IsLinkLocalUnicast() && (addr.Flags&unix.IFA_F_TENTATIVE == 0) {
|
||||
if addr.Flags&unix.IFA_F_DADFAILED != 0 {
|
||||
log.Printf("DADFAILED for %v, continuing anyhow", addr.IP)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// isIPv6RouteReady returns true if serverAddr is reachable.
|
||||
func isIPv6RouteReady(serverAddr net.IP) waitForCondition {
|
||||
return func(l netlink.Link) (bool, error) {
|
||||
if serverAddr.IsMulticast() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
routes, err := netlink.RouteList(l, netlink.FAMILY_V6)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, route := range routes {
|
||||
if route.LinkIndex != l.Attrs().Index {
|
||||
continue
|
||||
}
|
||||
// Default route.
|
||||
if route.Dst == nil {
|
||||
return true, nil
|
||||
}
|
||||
if route.Dst.Contains(serverAddr) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) requestLease6(iface netlink.Link) (*Lease, error) {
|
||||
ifname := iface.Attrs().Name
|
||||
l := c.l.With().Str("interface", ifname).Logger()
|
||||
|
||||
clientPort := dhcpv6.DefaultClientPort
|
||||
if c.cfg.V6ClientPort != nil {
|
||||
clientPort = *c.cfg.V6ClientPort
|
||||
}
|
||||
|
||||
// For ipv6, we cannot bind to the port until Duplicate Address
|
||||
// Detection (DAD) is complete which is indicated by the link being no
|
||||
// longer marked as "tentative". This usually takes about a second.
|
||||
|
||||
// If the link is never going to be ready, don't wait forever.
|
||||
// (The user may not have configured a ctx with a timeout.)
|
||||
|
||||
linkUpTimeout := time.After(c.cfg.LinkUpTimeout)
|
||||
if err := c.waitFor(
|
||||
iface,
|
||||
linkUpTimeout,
|
||||
isIPv6LinkReady,
|
||||
ErrIPv6LinkTimeout,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If user specified a non-multicast address, make sure it's routable before we start.
|
||||
if c.cfg.V6ServerAddr != nil {
|
||||
if err := c.waitFor(
|
||||
iface,
|
||||
linkUpTimeout,
|
||||
isIPv6RouteReady(c.cfg.V6ServerAddr.IP),
|
||||
ErrIPv6RouteTimeout,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
mods := []nclient6.ClientOpt{
|
||||
nclient6.WithTimeout(c.cfg.Timeout),
|
||||
nclient6.WithRetry(c.cfg.Retries),
|
||||
c.getDHCP6Logger(),
|
||||
}
|
||||
if c.cfg.V6ServerAddr != nil {
|
||||
mods = append(mods, nclient6.WithBroadcastAddr(c.cfg.V6ServerAddr))
|
||||
}
|
||||
|
||||
conn, err := nclient6.NewIPv6UDPConn(iface.Attrs().Name, clientPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := nclient6.NewWithConn(conn, iface.Attrs().HardwareAddr, mods...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Prepend modifiers with default options, so they can be overriden.
|
||||
reqmods := append(
|
||||
[]dhcpv6.Modifier{
|
||||
dhcpv6.WithNetboot,
|
||||
},
|
||||
c.cfg.Modifiers6...)
|
||||
|
||||
l.Info().Msg("attempting to get DHCPv6 lease")
|
||||
p, err := client.RapidSolicit(c.ctx, reqmods...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Info().Msgf("DHCPv6 lease acquired: %s", p.Summary())
|
||||
return fromNclient6Lease(p, ifname), nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package udhcpc
|
||||
package dhclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
@ -10,8 +10,17 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLeaseTime = time.Duration(30 * time.Minute)
|
||||
defaultRenewalTime = time.Duration(15 * time.Minute)
|
||||
)
|
||||
|
||||
// Lease is a network configuration obtained by DHCP.
|
||||
type Lease struct {
|
||||
// from https://udhcp.busybox.net/README.udhcpc
|
||||
IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP
|
||||
|
@ -21,6 +30,7 @@ type Lease struct {
|
|||
MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network
|
||||
HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname
|
||||
Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network
|
||||
SearchList []string `env:"search" json:"search_list,omitempty"` // The search list for the network
|
||||
BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option
|
||||
BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option
|
||||
BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option
|
||||
|
@ -38,24 +48,100 @@ type Lease struct {
|
|||
BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile
|
||||
RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk
|
||||
LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds
|
||||
RenewalTime time.Duration `env:"renewal" json:"renewal,omitempty"` // The renewal time, in seconds
|
||||
RebindingTime time.Duration `env:"rebinding" json:"rebinding,omitempty"` // The rebinding time, in seconds
|
||||
DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored)
|
||||
ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server
|
||||
Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK
|
||||
TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name
|
||||
BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name
|
||||
Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds
|
||||
ClassIdentifier string `env:"classid" json:"class_identifier,omitempty"` // The class identifier
|
||||
LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
|
||||
|
||||
InterfaceName string `json:"interface_name,omitempty"` // The name of the interface
|
||||
|
||||
p4 *nclient4.Lease
|
||||
p6 *dhcpv6.Message
|
||||
|
||||
isEmpty map[string]bool
|
||||
}
|
||||
|
||||
// fromNclient4Lease creates a lease from a nclient4.Lease.
|
||||
func fromNclient4Lease(l *nclient4.Lease, iface string) *Lease {
|
||||
lease := &Lease{}
|
||||
|
||||
lease.p4 = l
|
||||
|
||||
// only the fields that we need are set
|
||||
lease.Routers = l.ACK.Router()
|
||||
lease.IPAddress = l.ACK.YourIPAddr
|
||||
lease.Netmask = net.IP(l.ACK.SubnetMask())
|
||||
lease.Broadcast = l.ACK.BroadcastAddress()
|
||||
// lease.MTU = int(resp.Options.Get(dhcpv4.OptionInterfaceMTU))
|
||||
|
||||
lease.NTPServers = l.ACK.NTPServers()
|
||||
|
||||
lease.HostName = l.ACK.HostName()
|
||||
lease.Domain = l.ACK.DomainName()
|
||||
|
||||
searchList := l.ACK.DomainSearch()
|
||||
if searchList != nil {
|
||||
lease.SearchList = searchList.Labels
|
||||
}
|
||||
|
||||
lease.DNS = l.ACK.DNS()
|
||||
|
||||
lease.ClassIdentifier = l.ACK.ClassIdentifier()
|
||||
lease.ServerID = l.ACK.ServerIdentifier().String()
|
||||
|
||||
lease.Message = l.ACK.Message()
|
||||
lease.LeaseTime = l.ACK.IPAddressLeaseTime(defaultLeaseTime)
|
||||
lease.RenewalTime = l.ACK.IPAddressRenewalTime(defaultRenewalTime)
|
||||
|
||||
lease.InterfaceName = iface
|
||||
|
||||
return lease
|
||||
}
|
||||
|
||||
// fromNclient6Lease creates a lease from a nclient6.Message.
|
||||
func fromNclient6Lease(l *dhcpv6.Message, iface string) *Lease {
|
||||
lease := &Lease{}
|
||||
|
||||
lease.p6 = l
|
||||
|
||||
iana := l.Options.OneIANA()
|
||||
if iana == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
address := iana.Options.OneAddress()
|
||||
if address == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lease.IPAddress = address.IPv6Addr
|
||||
lease.Netmask = net.IP(net.CIDRMask(128, 128))
|
||||
lease.DNS = l.Options.DNS()
|
||||
// lease.LeaseTime = iana.Options.OnePreferredLifetime()
|
||||
// lease.RenewalTime = iana.Options.OneValidLifetime()
|
||||
// lease.RebindingTime = iana.Options.OneRebindingTime()
|
||||
|
||||
lease.InterfaceName = iface
|
||||
|
||||
return lease
|
||||
}
|
||||
|
||||
func (l *Lease) setIsEmpty(m map[string]bool) {
|
||||
l.isEmpty = m
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the lease is empty for the given key.
|
||||
func (l *Lease) IsEmpty(key string) bool {
|
||||
return l.isEmpty[key]
|
||||
}
|
||||
|
||||
// ToJSON returns the lease as a JSON string.
|
||||
func (l *Lease) ToJSON() string {
|
||||
json, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
|
@ -64,13 +150,13 @@ func (l *Lease) ToJSON() string {
|
|||
return string(json)
|
||||
}
|
||||
|
||||
// SetLeaseExpiry sets the lease expiry time.
|
||||
func (l *Lease) SetLeaseExpiry() (time.Time, error) {
|
||||
if l.Uptime == 0 || l.LeaseTime == 0 {
|
||||
return time.Time{}, fmt.Errorf("uptime or lease time isn't set")
|
||||
}
|
||||
|
||||
// get the uptime of the device
|
||||
|
||||
file, err := os.Open("/proc/uptime")
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err)
|
||||
|
@ -98,6 +184,7 @@ func (l *Lease) SetLeaseExpiry() (time.Time, error) {
|
|||
return leaseExpiry, nil
|
||||
}
|
||||
|
||||
// UnmarshalDHCPCLease unmarshals a lease from a string.
|
||||
func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
||||
// parse the lease file as a map
|
||||
data := make(map[string]string)
|
||||
|
@ -184,3 +271,45 @@ func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalDHCPCLease marshals a lease to a string.
|
||||
func MarshalDHCPCLease(lease *Lease) (string, error) {
|
||||
leaseType := reflect.TypeOf(lease).Elem()
|
||||
leaseValue := reflect.ValueOf(lease).Elem()
|
||||
|
||||
leaseFile := ""
|
||||
|
||||
for i := 0; i < leaseType.NumField(); i++ {
|
||||
field := leaseValue.Field(i)
|
||||
key := leaseType.Field(i).Tag.Get("env")
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
outValue := ""
|
||||
|
||||
switch field.Interface().(type) {
|
||||
case string:
|
||||
outValue = field.String()
|
||||
case int:
|
||||
outValue = strconv.Itoa(int(field.Int()))
|
||||
case time.Duration:
|
||||
outValue = strconv.Itoa(int(field.Int()))
|
||||
case net.IP:
|
||||
outValue = field.String()
|
||||
case []net.IP:
|
||||
ips := field.Interface().([]net.IP)
|
||||
ipStrings := make([]string, len(ips))
|
||||
for i, ip := range ips {
|
||||
ipStrings[i] = ip.String()
|
||||
}
|
||||
outValue = strings.Join(ipStrings, " ")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
|
||||
}
|
||||
|
||||
leaseFile += fmt.Sprintf("%s=%s\n", key, outValue)
|
||||
}
|
||||
|
||||
return leaseFile, nil
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package dhclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func readFileNoStat(filename string) ([]byte, error) {
|
||||
const maxBufferSize = 1024 * 1024
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
reader := io.LimitReader(f, maxBufferSize)
|
||||
return io.ReadAll(reader)
|
||||
}
|
||||
|
||||
func toCmdline(path string) ([]string, error) {
|
||||
data, err := readFileNoStat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) < 1 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil
|
||||
}
|
||||
|
||||
func (c *Client) killUdhcpc() error {
|
||||
// read procfs for udhcpc processes
|
||||
// we do not use procfs.AllProcs() because we want to avoid the overhead of reading the entire procfs
|
||||
processes, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
matchedPids := make([]int, 0)
|
||||
|
||||
// iterate over the processes
|
||||
for _, d := range processes {
|
||||
// check if file is numeric
|
||||
pid, err := strconv.Atoi(d.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if it's a directory
|
||||
if !d.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
cmdline, err := toCmdline(filepath.Join("/proc", d.Name(), "cmdline"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(cmdline) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if cmdline[0] != "udhcpc" {
|
||||
continue
|
||||
}
|
||||
|
||||
matchedPids = append(matchedPids, pid)
|
||||
}
|
||||
|
||||
if len(matchedPids) == 0 {
|
||||
c.l.Info().Msg("no udhcpc processes found")
|
||||
return nil
|
||||
}
|
||||
|
||||
c.l.Info().Ints("pids", matchedPids).Msg("found udhcpc processes, terminating")
|
||||
|
||||
for _, pid := range matchedPids {
|
||||
err := syscall.Kill(pid, syscall.SIGTERM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.l.Info().Int("pid", pid).Msg("terminated udhcpc process")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package dhclient
|
||||
|
||||
import (
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type dhcpLogger struct {
|
||||
// Printfer is used for actual output of the logger
|
||||
nclient4.Printfer
|
||||
|
||||
l *zerolog.Logger
|
||||
}
|
||||
|
||||
// Printf prints a log message as-is via predefined Printfer
|
||||
func (s dhcpLogger) Printf(format string, v ...interface{}) {
|
||||
s.l.Info().Msgf(format, v...)
|
||||
}
|
||||
|
||||
// PrintMessage prints a DHCP message in the short format via predefined Printfer
|
||||
func (s dhcpLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) {
|
||||
s.l.Info().Str("prefix", prefix).Str("message", message.String()).Msg("DHCP message")
|
||||
}
|
||||
|
||||
func (c *Client) getDHCP4Logger(ifname string) nclient4.ClientOpt {
|
||||
logger := c.l.With().Str("interface", ifname).Logger()
|
||||
|
||||
return nclient4.WithLogger(dhcpLogger{
|
||||
l: &logger,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: nclient6 doesn't implement the WithLogger option,
|
||||
// we might need to open a PR to add it
|
||||
|
||||
func (c *Client) getDHCP6Logger() nclient6.ClientOpt {
|
||||
return nclient6.WithSummaryLogger()
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package dhclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
resolvConfPath = "/etc/resolv.conf"
|
||||
resolvConfFileMode = 0644
|
||||
resolvConfTemplate = `# the resolv.conf file is managed by the jetkvm-dhclient service
|
||||
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
|
||||
|
||||
{{ if .searchList }}
|
||||
search {{ join .searchList " " }} # {{ .iface }}
|
||||
{{- end -}}
|
||||
{{ if .domain }}
|
||||
domain {{ .domain }} # {{ .iface }}
|
||||
{{- end -}}
|
||||
{{ range .nameservers }}
|
||||
nameserver {{ printf "%s" . }} # {{ $.iface }}
|
||||
{{- end }}
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
tplFuncMap = template.FuncMap{
|
||||
"join": strings.Join,
|
||||
}
|
||||
)
|
||||
|
||||
func toResolvConf(iface string, nameservers []net.IP, searchList []string, domain string) (bytes.Buffer, error) {
|
||||
rc := bytes.Buffer{}
|
||||
tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate)
|
||||
if err != nil {
|
||||
return rc, fmt.Errorf("failed to parse template: %w", err)
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(&rc, map[string]interface{}{
|
||||
"iface": iface,
|
||||
"nameservers": nameservers,
|
||||
"searchList": searchList,
|
||||
"domain": domain,
|
||||
}); err != nil {
|
||||
return rc, fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func updateResolvConf(iface string, nameservers []net.IP, searchList []string, domain string) error {
|
||||
rc, err := toResolvConf(iface, nameservers, searchList, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(resolvConfPath, rc.Bytes(), resolvConfFileMode)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package dhclient
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToResolvConf(t *testing.T) {
|
||||
rc, err := toResolvConf(
|
||||
"eth0",
|
||||
[]net.IP{
|
||||
net.ParseIP("198.51.100.53"),
|
||||
net.ParseIP("203.0.113.53"),
|
||||
},
|
||||
[]string{"example.com"},
|
||||
"example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := `# the resolv.conf file is managed by the jetkvm-dhclient service
|
||||
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
|
||||
|
||||
|
||||
search example.com # eth0
|
||||
domain example.com # eth0
|
||||
nameserver 198.51.100.53 # eth0
|
||||
nameserver 203.0.113.53 # eth0
|
||||
`
|
||||
|
||||
assert.Equal(t, want, rc.String())
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package dhclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
type waitForCondition func(l netlink.Link) (ready bool, err error)
|
||||
|
||||
func (c *Client) waitFor(
|
||||
link netlink.Link,
|
||||
timeout <-chan time.Time,
|
||||
condition waitForCondition,
|
||||
timeoutError error,
|
||||
) error {
|
||||
return waitFor(c.ctx, link, timeout, condition, timeoutError)
|
||||
}
|
||||
|
||||
func waitFor(
|
||||
ctx context.Context,
|
||||
link netlink.Link,
|
||||
timeout <-chan time.Time,
|
||||
condition waitForCondition,
|
||||
timeoutError error,
|
||||
) error {
|
||||
for {
|
||||
if ready, err := condition(link); err != nil {
|
||||
return err
|
||||
} else if ready {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
continue
|
||||
case <-timeout:
|
||||
return timeoutError
|
||||
case <-ctx.Done():
|
||||
return timeoutError
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -28,7 +28,8 @@ type IPv4StaticConfig struct {
|
|||
}
|
||||
|
||||
type IPv6StaticConfig struct {
|
||||
Address null.String `json:"address,omitempty" validate_type:"cidr" required:"true"`
|
||||
Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"`
|
||||
PrefixLength null.Int `json:"prefix_length" validate_type:"ipv6_prefix_length" required:"true"`
|
||||
Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"`
|
||||
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
||||
}
|
||||
|
@ -108,7 +109,7 @@ func (s *NetworkInterfaceState) GetDomain() string {
|
|||
domain := ToValidDomain(s.config.Domain.String)
|
||||
|
||||
if domain == "" {
|
||||
lease := s.dhcpClient.GetLease()
|
||||
lease := s.dhcpClient.Lease4()
|
||||
if lease != nil && lease.Domain != "" {
|
||||
domain = ToValidDomain(lease.Domain)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/confparser"
|
||||
"github.com/jetkvm/kvm/internal/dhclient"
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
@ -28,7 +29,8 @@ type NetworkInterfaceState struct {
|
|||
stateLock sync.Mutex
|
||||
|
||||
config *NetworkConfig
|
||||
dhcpClient *udhcpc.DHCPClient
|
||||
ifConfig *NetworkInterfaceConfig
|
||||
dhcpClient *dhclient.Client
|
||||
|
||||
defaultHostname string
|
||||
currentHostname string
|
||||
|
@ -48,7 +50,7 @@ type NetworkInterfaceOptions struct {
|
|||
DefaultHostname string
|
||||
OnStateChange func(state *NetworkInterfaceState)
|
||||
OnInitialCheck func(state *NetworkInterfaceState)
|
||||
OnDhcpLeaseChange func(lease *udhcpc.Lease)
|
||||
OnDhcpLeaseChange func(lease *dhclient.Lease)
|
||||
OnConfigChange func(config *NetworkConfig)
|
||||
NetworkConfig *NetworkConfig
|
||||
}
|
||||
|
@ -80,12 +82,23 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
ntpAddresses: make([]*net.IP, 0),
|
||||
}
|
||||
|
||||
ifConfig, err := NewNetworkInterfaceConfig(opts.InterfaceName, opts.NetworkConfig, opts.Logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByName(opts.InterfaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// create the dhcp client
|
||||
dhcpClient := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{
|
||||
InterfaceName: opts.InterfaceName,
|
||||
PidFile: opts.DhcpPidFile,
|
||||
Logger: l,
|
||||
OnLeaseChange: func(lease *udhcpc.Lease) {
|
||||
dhcpClient, err := dhclient.NewClient(ctx, []netlink.Link{link}, &dhclient.Config{
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
OnLease4Change: func(lease *dhclient.Lease) {
|
||||
_, err := s.update()
|
||||
if err != nil {
|
||||
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
||||
|
@ -96,9 +109,16 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
|
||||
opts.OnDhcpLeaseChange(lease)
|
||||
},
|
||||
})
|
||||
OnLease6Change: func(lease *dhclient.Lease) {
|
||||
// NOT IMPLEMENTED
|
||||
},
|
||||
}, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.dhcpClient = dhcpClient
|
||||
s.ifConfig = ifConfig
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
@ -341,7 +361,7 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
return dhcpTargetState, nil
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error {
|
||||
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *dhclient.Lease) error {
|
||||
if lease != nil && len(lease.NTPServers) > 0 {
|
||||
s.l.Info().Msg("lease found, updating DHCP NTP addresses")
|
||||
s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers))
|
||||
|
@ -369,10 +389,10 @@ func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
|||
switch dhcpTargetState {
|
||||
case DhcpTargetStateRenew:
|
||||
s.l.Info().Msg("renewing DHCP lease")
|
||||
_ = s.dhcpClient.Renew()
|
||||
s.dhcpClient.Renew()
|
||||
case DhcpTargetStateRelease:
|
||||
s.l.Info().Msg("releasing DHCP lease")
|
||||
_ = s.dhcpClient.Release()
|
||||
s.dhcpClient.Release()
|
||||
case DhcpTargetStateStart:
|
||||
s.l.Warn().Msg("dhcpTargetStateStart not implemented")
|
||||
case DhcpTargetStateStop:
|
||||
|
@ -384,5 +404,10 @@ func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
|||
|
||||
func (s *NetworkInterfaceState) onConfigChange(config *NetworkConfig) {
|
||||
_ = s.setHostnameIfNotSame()
|
||||
|
||||
if err := s.ifConfig.Apply(s); err != nil {
|
||||
s.l.Error().Err(err).Msg("failed to apply network interface config")
|
||||
}
|
||||
|
||||
s.cbConfigChange(config)
|
||||
}
|
||||
|
|
|
@ -28,12 +28,18 @@ func (s *NetworkInterfaceState) Run() error {
|
|||
_ = s.setHostnameIfNotSame()
|
||||
|
||||
// run the dhcp client
|
||||
go s.dhcpClient.Run() // nolint:errcheck
|
||||
if err := s.dhcpClient.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.CheckAndUpdateDhcp(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.ifConfig.Apply(s); err != nil {
|
||||
s.l.Error().Err(err).Msg("failed to apply network interface config")
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/confparser"
|
||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
||||
"github.com/jetkvm/kvm/internal/dhclient"
|
||||
)
|
||||
|
||||
type RpcIPv6Address struct {
|
||||
|
@ -23,7 +23,8 @@ type RpcNetworkState struct {
|
|||
IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
|
||||
IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
|
||||
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"`
|
||||
DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"`
|
||||
DHCPLease4 *dhclient.Lease `json:"dhcp_lease,omitempty"` // name kept for backwards compatibility
|
||||
DHCPLease6 *dhclient.Lease `json:"dhcp_lease6,omitempty"`
|
||||
}
|
||||
|
||||
type RpcNetworkSettings struct {
|
||||
|
@ -84,7 +85,8 @@ func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
|
|||
IPv6LinkLocal: s.IPv6LinkLocalAddress(),
|
||||
IPv4Addresses: s.ipv4Addresses,
|
||||
IPv6Addresses: ipv6Addresses,
|
||||
DHCPLease: s.dhcpClient.GetLease(),
|
||||
DHCPLease4: s.dhcpClient.Lease4(),
|
||||
DHCPLease6: s.dhcpClient.Lease6(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,6 +113,14 @@ func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSetting
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := validateIPv4Config(&settings.NetworkConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateIPv6Config(&settings.NetworkConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.config = &settings.NetworkConfig
|
||||
s.onConfigChange(s.config)
|
||||
|
||||
|
@ -122,5 +132,7 @@ func (s *NetworkInterfaceState) RpcRenewDHCPLease() error {
|
|||
return fmt.Errorf("dhcp client not initialized")
|
||||
}
|
||||
|
||||
return s.dhcpClient.Renew()
|
||||
s.dhcpClient.Renew()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,367 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/vishvananda/netlink"
|
||||
"github.com/vishvananda/netlink/nl"
|
||||
)
|
||||
|
||||
const defaultInterface = "eth0"
|
||||
|
||||
const (
|
||||
AF_INET = nl.FAMILY_V4
|
||||
AF_INET6 = nl.FAMILY_V6
|
||||
|
||||
sysctlBase = "/proc/sys"
|
||||
sysctlFileMode = 0640
|
||||
)
|
||||
|
||||
type NetworkInterfaceConfig struct {
|
||||
config *NetworkConfig
|
||||
l *zerolog.Logger
|
||||
|
||||
ifaceName string
|
||||
iface *netlink.Link
|
||||
|
||||
lock sync.Mutex
|
||||
ipv4Lock sync.Mutex
|
||||
ipv6Lock sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
ipv4DefaultRoute = net.IPNet{
|
||||
IP: net.IPv4zero,
|
||||
Mask: net.CIDRMask(0, 0),
|
||||
}
|
||||
|
||||
ipv6DefaultRoute = net.IPNet{
|
||||
IP: net.IPv6zero,
|
||||
Mask: net.CIDRMask(0, 0),
|
||||
}
|
||||
)
|
||||
|
||||
// NewNetworkInterfaceConfig ...
|
||||
func NewNetworkInterfaceConfig(ifaceName string, config *NetworkConfig, logger *zerolog.Logger) (*NetworkInterfaceConfig, error) {
|
||||
if ifaceName == "" {
|
||||
ifaceName = defaultInterface
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByName(ifaceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get link by name: %w", err)
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
logger = logging.GetSubsystemLogger("network")
|
||||
}
|
||||
|
||||
scopedLogger := logger.With().Str("iface", ifaceName).Logger()
|
||||
|
||||
return &NetworkInterfaceConfig{
|
||||
config: config,
|
||||
l: &scopedLogger,
|
||||
ifaceName: ifaceName,
|
||||
iface: &link,
|
||||
lock: sync.Mutex{},
|
||||
ipv4Lock: sync.Mutex{},
|
||||
ipv6Lock: sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) Apply(s *NetworkInterfaceState) error {
|
||||
if err := c.applyIPv4(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.applyIPv6(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) applyIPv4(s *NetworkInterfaceState) error {
|
||||
switch c.config.IPv4Mode.String {
|
||||
case "static":
|
||||
return c.applyIPv4Static(s)
|
||||
case "dhcp":
|
||||
s.dhcpClient.SetIPv4(true)
|
||||
return nil
|
||||
case "disabled":
|
||||
s.dhcpClient.SetIPv4(false)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid IPv4 mode: %s", c.config.IPv4Mode.String)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) applyIPv6(s *NetworkInterfaceState) error {
|
||||
switch c.config.IPv6Mode.String {
|
||||
case "static":
|
||||
return c.applyIPv6Static(s)
|
||||
case "dhcpv6":
|
||||
return fmt.Errorf("not implemented")
|
||||
case "slaac":
|
||||
return c.applyIPv6Slaac()
|
||||
case "slaac_and_dhcpv6":
|
||||
return fmt.Errorf("not implemented")
|
||||
case "link_local":
|
||||
return c.applyIPv6LinkLocalOnly()
|
||||
case "disabled":
|
||||
return c.disableIPv6()
|
||||
default:
|
||||
return fmt.Errorf("invalid IPv6 mode: %s", c.config.IPv6Mode.String)
|
||||
}
|
||||
}
|
||||
|
||||
func checkIfAddressIsSetOrReturnCurrent(iface *netlink.Link, address net.IP, family int) (bool, []*netlink.Addr) {
|
||||
addr, err := netlink.AddrList(*iface, family)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
hit := false
|
||||
linkAddrs := make([]*netlink.Addr, 0)
|
||||
|
||||
for _, a := range addr {
|
||||
if a.IP.Equal(address) {
|
||||
hit = true
|
||||
continue
|
||||
}
|
||||
|
||||
// we don't want to delete link-local addresses
|
||||
if family == AF_INET6 && a.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
|
||||
linkAddrs = append(linkAddrs, &a)
|
||||
}
|
||||
|
||||
return hit, linkAddrs
|
||||
}
|
||||
|
||||
func reconcileLinkAddrs(iface *netlink.Link, family int, expectedAddr *net.IPNet, logger *zerolog.Logger) error {
|
||||
// TODO: we need to check if the netmask is the same
|
||||
hit, currentLinkAddrs := checkIfAddressIsSetOrReturnCurrent(iface, expectedAddr.IP, family)
|
||||
if !hit {
|
||||
logger.Info().Interface("ip", expectedAddr).Msg("adding address")
|
||||
if err := netlink.AddrAdd(*iface, &netlink.Addr{
|
||||
IPNet: expectedAddr,
|
||||
}); err != nil {
|
||||
logger.Info().Interface("ip", expectedAddr).Msg("failed to set address")
|
||||
return fmt.Errorf("failed to add address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range currentLinkAddrs {
|
||||
logger.Info().Interface("ip", addr.IP).Msg("deleting address")
|
||||
if err := netlink.AddrDel(*iface, addr); err != nil {
|
||||
return fmt.Errorf("failed to delete address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIfDefaultRouteIsSet(gateway net.IP, family int) bool {
|
||||
defaultRoute := ipv4DefaultRoute
|
||||
if family == AF_INET6 {
|
||||
defaultRoute = ipv6DefaultRoute
|
||||
}
|
||||
|
||||
routes, err := netlink.RouteListFiltered(family, &netlink.Route{Dst: &defaultRoute}, netlink.RT_FILTER_DST)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range routes {
|
||||
if r.Dst.IP.Equal(defaultRoute.IP) && r.Gw.Equal(gateway) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureInterfaceIsUp(iface *netlink.Link) error {
|
||||
if (*iface).Attrs().OperState == netlink.OperUp {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(*iface); err != nil {
|
||||
return fmt.Errorf("failed to set interface up: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) applyIPv4Static(s *NetworkInterfaceState) error {
|
||||
c.ipv4Lock.Lock()
|
||||
defer c.ipv4Lock.Unlock()
|
||||
|
||||
config, err := parseAndValidateStaticIPv4Config(c.config.IPv4Static)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.iface == nil {
|
||||
return fmt.Errorf("interface handle is nil")
|
||||
}
|
||||
|
||||
// disable DHCPv4
|
||||
s.dhcpClient.SetIPv4(false)
|
||||
|
||||
if err := ensureInterfaceIsUp(c.iface); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := reconcileLinkAddrs(c.iface, AF_INET, &config.network, c.l); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !checkIfDefaultRouteIsSet(config.gateway, AF_INET) {
|
||||
c.l.Info().Str("iface", c.ifaceName).Interface("gateway", config.gateway).Msg("adding default route")
|
||||
// TODO: support point-to-point
|
||||
if err := netlink.RouteReplace(&netlink.Route{
|
||||
Dst: &ipv4DefaultRoute,
|
||||
Gw: config.gateway,
|
||||
LinkIndex: (*c.iface).Attrs().Index,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to add default route: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ensureInterfaceIsUp(c.iface)
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) setSysctlValues(values map[string]int) error {
|
||||
for name, value := range values {
|
||||
name = fmt.Sprintf(name, c.ifaceName)
|
||||
name = strings.ReplaceAll(name, ".", "/")
|
||||
|
||||
if err := os.WriteFile(path.Join(sysctlBase, name), []byte(strconv.Itoa(value)), sysctlFileMode); err != nil {
|
||||
return fmt.Errorf("failed to set sysctl %s=%d: %w", name, value, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) applyIPv4Dhcp() error {
|
||||
c.ipv4Lock.Lock()
|
||||
defer c.ipv4Lock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) applyIPv4Disabled() error {
|
||||
c.ipv4Lock.Lock()
|
||||
defer c.ipv4Lock.Unlock()
|
||||
|
||||
addr, err := netlink.AddrList(*c.iface, AF_INET)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get address list: %w", err)
|
||||
}
|
||||
|
||||
for _, a := range addr {
|
||||
netlink.AddrDel(*c.iface, &a)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) applyIPv6Slaac() error {
|
||||
c.ipv6Lock.Lock()
|
||||
defer c.ipv6Lock.Unlock()
|
||||
|
||||
if err := c.setSysctlValues(map[string]int{
|
||||
"net.ipv6.conf.%s.disable_ipv6": 0, // enable IPv6
|
||||
"net.ipv6.conf.%s.accept_ra": 2, // accept even if forwarding is disabled
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) applyIPv6LinkLocalOnly() error {
|
||||
c.ipv6Lock.Lock()
|
||||
defer c.ipv6Lock.Unlock()
|
||||
|
||||
if err := c.setSysctlValues(map[string]int{
|
||||
"net.ipv6.conf.%s.disable_ipv6": 0, // enable IPv6
|
||||
"net.ipv6.conf.%s.accept_ra": 0, // disable RA
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr, err := netlink.AddrList(*c.iface, AF_INET6)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get address list: %w", err)
|
||||
}
|
||||
|
||||
for _, a := range addr {
|
||||
if !a.IP.IsLinkLocalUnicast() {
|
||||
netlink.AddrDel(*c.iface, &a)
|
||||
}
|
||||
}
|
||||
|
||||
return ensureInterfaceIsUp(c.iface)
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) applyIPv6Static(s *NetworkInterfaceState) error {
|
||||
c.ipv6Lock.Lock()
|
||||
defer c.ipv6Lock.Unlock()
|
||||
|
||||
config, err := parseAndValidateStaticIPv6Config(c.config.IPv6Static)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.iface == nil {
|
||||
return fmt.Errorf("interface handle is nil")
|
||||
}
|
||||
|
||||
if err := c.setSysctlValues(map[string]int{
|
||||
"net.ipv6.conf.%s.disable_ipv6": 0, // enable IPv6
|
||||
"net.ipv6.conf.%s.accept_ra": 2, // accept even if forwarding is disabled
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// disable DHCPv6
|
||||
s.dhcpClient.SetIPv6(false)
|
||||
|
||||
if err := reconcileLinkAddrs(c.iface, AF_INET6, &config.prefix, c.l); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !checkIfDefaultRouteIsSet(config.gateway, AF_INET6) {
|
||||
if err := netlink.RouteReplace(&netlink.Route{
|
||||
Dst: &ipv6DefaultRoute,
|
||||
Gw: config.gateway,
|
||||
LinkIndex: (*c.iface).Attrs().Index,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to add default route: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ensureInterfaceIsUp(c.iface)
|
||||
}
|
||||
|
||||
func (c *NetworkInterfaceConfig) disableIPv6() error {
|
||||
return c.setSysctlValues(map[string]int{
|
||||
"net.ipv6.conf.%s.disable_ipv6": 1, // disable IPv6
|
||||
})
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplyIPv4Static(t *testing.T) {
|
||||
assert.NoError(t, os.Setenv("JETKVM_DEBUG", "1"))
|
||||
|
||||
// conf := &NetworkConfig{
|
||||
// IPv4Mode: null.StringFrom("dhcp"),
|
||||
// // IPv4Static: &IPv4StaticConfig{
|
||||
// // Address: null.StringFrom("203.0.113.100"),
|
||||
// // Netmask: null.StringFrom("255.255.255.0"),
|
||||
// // Gateway: null.StringFrom("203.0.113.1"),
|
||||
// // },
|
||||
// IPv6Mode: null.StringFrom("disabled"),
|
||||
// }
|
||||
// ifc, err := NewNetworkInterfaceConfig("eth0", conf, nil)
|
||||
// assert.NoError(t, err)
|
||||
|
||||
// assert.NoError(t, ifc.Apply())
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
netMask32 = net.IPv4Mask(255, 255, 255, 255)
|
||||
)
|
||||
|
||||
func parseIP(ip string, family int) (net.IP, error) {
|
||||
addr := net.ParseIP(ip)
|
||||
if addr == nil {
|
||||
return nil, fmt.Errorf("failed to parse IP: %s", ip)
|
||||
}
|
||||
|
||||
switch family {
|
||||
case AF_INET:
|
||||
ia := addr.To4()
|
||||
if ia == nil {
|
||||
return nil, fmt.Errorf("address is not a valid IPv4 address")
|
||||
}
|
||||
return ia, nil
|
||||
case AF_INET6:
|
||||
ia := addr.To16()
|
||||
if ia == nil {
|
||||
return nil, fmt.Errorf("address is not a valid IPv6 address")
|
||||
}
|
||||
return ia, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid family: %d", family)
|
||||
}
|
||||
}
|
||||
|
||||
func parseAndValidateUnicastIP(ip string, family int) (net.IP, error) {
|
||||
addr, err := parseIP(ip, family)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !addr.IsGlobalUnicast() {
|
||||
return nil, fmt.Errorf("address is not a global unicast address")
|
||||
}
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func isValidNetMask(s string) bool {
|
||||
ip := net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
var m net.IPMask
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
m = net.IPMask(v4) // 4 bytes
|
||||
} else {
|
||||
v6 := ip.To16()
|
||||
if v6 == nil {
|
||||
return false
|
||||
}
|
||||
m = net.IPMask(v6) // 16 bytes
|
||||
}
|
||||
ones, bits := m.Size()
|
||||
// Non-contiguous masks return (0,0). /0 is valid and returns (0, 32|128).
|
||||
return !(ones == 0 && bits == 0)
|
||||
}
|
||||
|
||||
type parsedIPv4Config struct {
|
||||
address net.IP
|
||||
netmask net.IPMask
|
||||
network net.IPNet
|
||||
gateway net.IP
|
||||
}
|
||||
|
||||
type parsedIPv6Config struct {
|
||||
address net.IP
|
||||
prefix net.IPNet
|
||||
gateway net.IP
|
||||
}
|
||||
|
||||
func parseAndValidateStaticIPv4Config(config *IPv4StaticConfig) (*parsedIPv4Config, error) {
|
||||
addr, err := parseAndValidateUnicastIP(config.Address.String, AF_INET)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse address: %w", err)
|
||||
}
|
||||
|
||||
netmask, err := parseIP(config.Netmask.String, AF_INET)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse netmask: %w", err)
|
||||
}
|
||||
if !isValidNetMask(config.Netmask.String) {
|
||||
return nil, fmt.Errorf("netmask is not a valid netmask")
|
||||
}
|
||||
|
||||
gateway, err := parseAndValidateUnicastIP(config.Gateway.String, AF_INET)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse gateway: %w", err)
|
||||
}
|
||||
|
||||
if addr.Equal(gateway) {
|
||||
return nil, fmt.Errorf("address and gateway cannot be the same")
|
||||
}
|
||||
|
||||
netMask := net.IPMask(netmask)
|
||||
|
||||
ipNet := net.IPNet{
|
||||
IP: addr,
|
||||
Mask: netMask,
|
||||
}
|
||||
|
||||
if !ipNet.Contains(gateway) && !bytes.Equal(ipNet.Mask, netMask32) {
|
||||
return nil, fmt.Errorf("address is not in the same subnet as the gateway")
|
||||
}
|
||||
|
||||
return &parsedIPv4Config{
|
||||
address: addr,
|
||||
netmask: netMask,
|
||||
network: ipNet,
|
||||
gateway: gateway,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseAndValidateStaticIPv6Config(config *IPv6StaticConfig) (*parsedIPv6Config, error) {
|
||||
addr, prefix, err := net.ParseCIDR(fmt.Sprintf("%s/%d", config.Address.String, config.PrefixLength.Int64))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse prefix: %w", err)
|
||||
}
|
||||
|
||||
if !addr.IsGlobalUnicast() {
|
||||
return nil, fmt.Errorf("address is not a global unicast address")
|
||||
}
|
||||
|
||||
gateway, err := parseIP(config.Gateway.String, AF_INET6)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse gateway: %w", err)
|
||||
}
|
||||
|
||||
if addr.Equal(gateway) {
|
||||
return nil, fmt.Errorf("address and gateway cannot be the same")
|
||||
}
|
||||
|
||||
if !prefix.Contains(gateway) && !gateway.IsLinkLocalUnicast() {
|
||||
return nil, fmt.Errorf("gateway is not in the same subnet as the address or is a link-local address")
|
||||
}
|
||||
|
||||
return &parsedIPv6Config{
|
||||
address: addr,
|
||||
prefix: *prefix,
|
||||
gateway: gateway,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateIPv4Config(config *NetworkConfig) error {
|
||||
switch config.IPv4Mode.String {
|
||||
case "static":
|
||||
_, err := parseAndValidateStaticIPv4Config(config.IPv4Static)
|
||||
return err
|
||||
case "dhcp":
|
||||
return nil
|
||||
case "disabled":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid IPv4 mode: %s", config.IPv4Mode.String)
|
||||
}
|
||||
}
|
||||
|
||||
func validateIPv6Config(config *NetworkConfig) error {
|
||||
switch config.IPv6Mode.String {
|
||||
case "static":
|
||||
_, err := parseAndValidateStaticIPv6Config(config.IPv6Static)
|
||||
return err
|
||||
case "slaac":
|
||||
return nil
|
||||
case "dhcpv6":
|
||||
return fmt.Errorf("not implemented")
|
||||
case "slaac_and_dhcpv6":
|
||||
return fmt.Errorf("not implemented")
|
||||
case "link_local":
|
||||
return nil
|
||||
case "disabled":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid IPv6 mode: %s", config.IPv6Mode.String)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateInvalidIpv4Netmask(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv4Config(
|
||||
&IPv4StaticConfig{
|
||||
Address: null.StringFrom("192.168.1.100"),
|
||||
Netmask: null.StringFrom("123.45.67.89"),
|
||||
Gateway: null.StringFrom("192.168.1.1"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateInvalidIpv4Address(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv4Config(
|
||||
&IPv4StaticConfig{
|
||||
Address: null.StringFrom("192.168.1.666"),
|
||||
Netmask: null.StringFrom("255.255.255.0"),
|
||||
Gateway: null.StringFrom("192.168.1.1"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateIpv4GatewaySameAsAddress(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv4Config(
|
||||
&IPv4StaticConfig{
|
||||
Address: null.StringFrom("192.168.1.1"),
|
||||
Netmask: null.StringFrom("255.255.255.0"),
|
||||
Gateway: null.StringFrom("192.168.1.1"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateIpv4GatewayNotInNetmask(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv4Config(
|
||||
&IPv4StaticConfig{
|
||||
Address: null.StringFrom("192.168.1.100"),
|
||||
Netmask: null.StringFrom("255.255.255.0"),
|
||||
Gateway: null.StringFrom("192.168.2.1"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateIpv4GatewayNotGlobalUnicast(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv4Config(
|
||||
&IPv4StaticConfig{
|
||||
Address: null.StringFrom("127.0.0.1"),
|
||||
Netmask: null.StringFrom("255.255.255.0"),
|
||||
Gateway: null.StringFrom("127.0.0.1"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateIpv4GatewayPointToPoint(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv4Config(
|
||||
&IPv4StaticConfig{
|
||||
Address: null.StringFrom("192.168.1.100"),
|
||||
Netmask: null.StringFrom("255.255.255.255"),
|
||||
Gateway: null.StringFrom("203.0.113.1"),
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err, "expected no error, got %v", err)
|
||||
}
|
||||
|
||||
func TestValidateIpv4Config(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv4Config(
|
||||
&IPv4StaticConfig{
|
||||
Address: null.StringFrom("192.168.1.100"),
|
||||
Netmask: null.StringFrom("255.255.255.0"),
|
||||
Gateway: null.StringFrom("192.168.1.1"),
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err, "expected no error, got %v", err)
|
||||
}
|
||||
|
||||
func TestValidateIpv6LinkLocalAddress(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv6Config(
|
||||
&IPv6StaticConfig{
|
||||
Address: null.StringFrom("fe80::114"),
|
||||
PrefixLength: null.IntFrom(64),
|
||||
Gateway: null.StringFrom("fe80::1"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateIpv6GatewaySameAsAddress(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv6Config(
|
||||
&IPv6StaticConfig{
|
||||
Address: null.StringFrom("2001:0db8::2"),
|
||||
PrefixLength: null.IntFrom(64),
|
||||
Gateway: null.StringFrom("2001:0db8::2"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateIpv6GatewayNotInPrefix(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv6Config(
|
||||
&IPv6StaticConfig{
|
||||
Address: null.StringFrom("2001:0db8::2"),
|
||||
PrefixLength: null.IntFrom(64),
|
||||
Gateway: null.StringFrom("2001:0db8:1::1"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateIpv6AddressInvalidPrefixLength(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv6Config(
|
||||
&IPv6StaticConfig{
|
||||
Address: null.StringFrom("2001:0db8::2"),
|
||||
PrefixLength: null.IntFrom(1919),
|
||||
Gateway: null.StringFrom("2001:0db8::1"),
|
||||
},
|
||||
)
|
||||
assert.Error(t, err, "expected error, got nil")
|
||||
}
|
||||
|
||||
func TestValidateIpv6GatewayLinkLocal(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv6Config(
|
||||
&IPv6StaticConfig{
|
||||
Address: null.StringFrom("2001:0db8::2"),
|
||||
PrefixLength: null.IntFrom(64),
|
||||
Gateway: null.StringFrom("fe80::1"),
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err, "expected no error, got %v", err)
|
||||
}
|
||||
|
||||
func TestValidateIpv6Config(t *testing.T) {
|
||||
_, err := parseAndValidateStaticIPv6Config(
|
||||
&IPv6StaticConfig{
|
||||
Address: null.StringFrom("2001:0db8::2"),
|
||||
PrefixLength: null.IntFrom(64),
|
||||
Gateway: null.StringFrom("2001:0db8::1"),
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err, "expected no error, got %v", err)
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package udhcpc
|
||||
|
||||
func (u *DHCPClient) GetNtpServers() []string {
|
||||
if u.lease == nil {
|
||||
return nil
|
||||
}
|
||||
servers := make([]string, len(u.lease.NTPServers))
|
||||
for i, server := range u.lease.NTPServers {
|
||||
servers[i] = server.String()
|
||||
}
|
||||
return servers
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package udhcpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUnmarshalDHCPCLease(t *testing.T) {
|
||||
lease := &Lease{}
|
||||
err := UnmarshalDHCPCLease(lease, `
|
||||
# generated @ Mon Jan 4 19:31:53 UTC 2021
|
||||
# 19:31:53 up 0 min, 0 users, load average: 0.72, 0.14, 0.04
|
||||
# the date might be inaccurate if the clock is not set
|
||||
ip=192.168.0.240
|
||||
siaddr=192.168.0.1
|
||||
sname=
|
||||
boot_file=
|
||||
subnet=255.255.255.0
|
||||
timezone=
|
||||
router=192.168.0.1
|
||||
timesvr=
|
||||
namesvr=
|
||||
dns=172.19.53.2
|
||||
logsvr=
|
||||
cookiesvr=
|
||||
lprsvr=
|
||||
hostname=
|
||||
bootsize=
|
||||
domain=
|
||||
swapsvr=
|
||||
rootpath=
|
||||
ipttl=
|
||||
mtu=
|
||||
broadcast=
|
||||
ntpsrv=162.159.200.123
|
||||
wins=
|
||||
lease=172800
|
||||
dhcptype=
|
||||
serverid=192.168.0.1
|
||||
message=
|
||||
tftp=
|
||||
bootfile=
|
||||
`)
|
||||
if lease.IPAddress.String() != "192.168.0.240" {
|
||||
t.Fatalf("expected ip to be 192.168.0.240, got %s", lease.IPAddress.String())
|
||||
}
|
||||
if lease.Netmask.String() != "255.255.255.0" {
|
||||
t.Fatalf("expected netmask to be 255.255.255.0, got %s", lease.Netmask.String())
|
||||
}
|
||||
if len(lease.Routers) != 1 {
|
||||
t.Fatalf("expected 1 router, got %d", len(lease.Routers))
|
||||
}
|
||||
if lease.Routers[0].String() != "192.168.0.1" {
|
||||
t.Fatalf("expected router to be 192.168.0.1, got %s", lease.Routers[0].String())
|
||||
}
|
||||
if len(lease.NTPServers) != 1 {
|
||||
t.Fatalf("expected 1 timeserver, got %d", len(lease.NTPServers))
|
||||
}
|
||||
if lease.NTPServers[0].String() != "162.159.200.123" {
|
||||
t.Fatalf("expected timeserver to be 162.159.200.123, got %s", lease.NTPServers[0].String())
|
||||
}
|
||||
if len(lease.DNS) != 1 {
|
||||
t.Fatalf("expected 1 dns, got %d", len(lease.DNS))
|
||||
}
|
||||
if lease.DNS[0].String() != "172.19.53.2" {
|
||||
t.Fatalf("expected dns to be 172.19.53.2, got %s", lease.DNS[0].String())
|
||||
}
|
||||
if lease.LeaseTime != 172800*time.Second {
|
||||
t.Fatalf("expected lease time to be 172800 seconds, got %d", lease.LeaseTime)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
package udhcpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func readFileNoStat(filename string) ([]byte, error) {
|
||||
const maxBufferSize = 1024 * 1024
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
reader := io.LimitReader(f, maxBufferSize)
|
||||
return io.ReadAll(reader)
|
||||
}
|
||||
|
||||
func toCmdline(path string) ([]string, error) {
|
||||
data, err := readFileNoStat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) < 1 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil
|
||||
}
|
||||
|
||||
func (p *DHCPClient) findUdhcpcProcess() (int, error) {
|
||||
// read procfs for udhcpc processes
|
||||
// we do not use procfs.AllProcs() because we want to avoid the overhead of reading the entire procfs
|
||||
processes, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// iterate over the processes
|
||||
for _, d := range processes {
|
||||
// check if file is numeric
|
||||
pid, err := strconv.Atoi(d.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if it's a directory
|
||||
if !d.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
cmdline, err := toCmdline(filepath.Join("/proc", d.Name(), "cmdline"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(cmdline) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if cmdline[0] != "udhcpc" {
|
||||
continue
|
||||
}
|
||||
|
||||
cmdlineText := strings.Join(cmdline, " ")
|
||||
|
||||
// check if it's a udhcpc process
|
||||
if strings.Contains(cmdlineText, fmt.Sprintf("-i %s", p.InterfaceName)) {
|
||||
p.logger.Debug().
|
||||
Str("pid", d.Name()).
|
||||
Interface("cmdline", cmdline).
|
||||
Msg("found udhcpc process")
|
||||
return pid, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New("udhcpc process not found")
|
||||
}
|
||||
|
||||
func (c *DHCPClient) getProcessPid() (int, error) {
|
||||
var pid int
|
||||
if c.pidFile != "" {
|
||||
// try to read the pid file
|
||||
pidHandle, err := os.ReadFile(c.pidFile)
|
||||
if err != nil {
|
||||
c.logger.Warn().Err(err).
|
||||
Str("pidFile", c.pidFile).Msg("failed to read udhcpc pid file")
|
||||
}
|
||||
|
||||
// if it exists, try to read the pid
|
||||
if pidHandle != nil {
|
||||
pidFromFile, err := strconv.Atoi(string(pidHandle))
|
||||
if err != nil {
|
||||
c.logger.Warn().Err(err).
|
||||
Str("pidFile", c.pidFile).Msg("failed to convert pid file to int")
|
||||
}
|
||||
pid = pidFromFile
|
||||
}
|
||||
}
|
||||
|
||||
// if the pid is 0, try to find the pid using procfs
|
||||
if pid == 0 {
|
||||
newPid, err := c.findUdhcpcProcess()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
pid = newPid
|
||||
}
|
||||
|
||||
return pid, nil
|
||||
}
|
||||
|
||||
func (c *DHCPClient) getProcess() *os.Process {
|
||||
pid, err := c.getProcessPid()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
c.logger.Warn().Err(err).
|
||||
Int("pid", pid).Msg("failed to find process")
|
||||
return nil
|
||||
}
|
||||
|
||||
return process
|
||||
}
|
||||
|
||||
func (c *DHCPClient) GetProcess() *os.Process {
|
||||
if c.process == nil {
|
||||
process := c.getProcess()
|
||||
if process == nil {
|
||||
return nil
|
||||
}
|
||||
c.process = process
|
||||
}
|
||||
|
||||
err := c.process.Signal(syscall.Signal(0))
|
||||
if err != nil && errors.Is(err, os.ErrProcessDone) {
|
||||
oldPid := c.process.Pid
|
||||
|
||||
c.process = nil
|
||||
c.process = c.getProcess()
|
||||
if c.process == nil {
|
||||
c.logger.Error().Msg("failed to find new udhcpc process")
|
||||
return nil
|
||||
}
|
||||
c.logger.Warn().
|
||||
Int("oldPid", oldPid).
|
||||
Int("newPid", c.process.Pid).
|
||||
Msg("udhcpc process pid changed")
|
||||
} else if err != nil {
|
||||
c.logger.Warn().Err(err).
|
||||
Int("pid", c.process.Pid).Msg("udhcpc process is not running")
|
||||
}
|
||||
|
||||
return c.process
|
||||
}
|
||||
|
||||
func (c *DHCPClient) KillProcess() error {
|
||||
process := c.GetProcess()
|
||||
if process == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return process.Kill()
|
||||
}
|
||||
|
||||
func (c *DHCPClient) ReleaseProcess() error {
|
||||
process := c.GetProcess()
|
||||
if process == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return process.Release()
|
||||
}
|
||||
|
||||
func (c *DHCPClient) signalProcess(sig syscall.Signal) error {
|
||||
process := c.GetProcess()
|
||||
if process == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := process.Signal(sig)
|
||||
if s != nil {
|
||||
c.logger.Warn().Err(s).
|
||||
Int("pid", process.Pid).
|
||||
Str("signal", sig.String()).
|
||||
Msg("failed to signal udhcpc process")
|
||||
return s
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DHCPClient) Renew() error {
|
||||
return c.signalProcess(syscall.SIGUSR1)
|
||||
}
|
||||
|
||||
func (c *DHCPClient) Release() error {
|
||||
return c.signalProcess(syscall.SIGUSR2)
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
package udhcpc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
DHCPLeaseFile = "/run/udhcpc.%s.info"
|
||||
DHCPPidFile = "/run/udhcpc.%s.pid"
|
||||
)
|
||||
|
||||
type DHCPClient struct {
|
||||
InterfaceName string
|
||||
leaseFile string
|
||||
pidFile string
|
||||
lease *Lease
|
||||
logger *zerolog.Logger
|
||||
process *os.Process
|
||||
onLeaseChange func(lease *Lease)
|
||||
}
|
||||
|
||||
type DHCPClientOptions struct {
|
||||
InterfaceName string
|
||||
PidFile string
|
||||
Logger *zerolog.Logger
|
||||
OnLeaseChange func(lease *Lease)
|
||||
}
|
||||
|
||||
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
|
||||
|
||||
func NewDHCPClient(options *DHCPClientOptions) *DHCPClient {
|
||||
if options.Logger == nil {
|
||||
options.Logger = &defaultLogger
|
||||
}
|
||||
|
||||
l := options.Logger.With().Str("interface", options.InterfaceName).Logger()
|
||||
return &DHCPClient{
|
||||
InterfaceName: options.InterfaceName,
|
||||
logger: &l,
|
||||
leaseFile: fmt.Sprintf(DHCPLeaseFile, options.InterfaceName),
|
||||
pidFile: options.PidFile,
|
||||
onLeaseChange: options.OnLeaseChange,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DHCPClient) getWatchPaths() []string {
|
||||
watchPaths := make(map[string]interface{})
|
||||
watchPaths[filepath.Dir(c.leaseFile)] = nil
|
||||
|
||||
if c.pidFile != "" {
|
||||
watchPaths[filepath.Dir(c.pidFile)] = nil
|
||||
}
|
||||
|
||||
paths := make([]string, 0)
|
||||
for path := range watchPaths {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// Run starts the DHCP client and watches the lease file for changes.
|
||||
// this isn't a blocking call, and the lease file is reloaded when a change is detected.
|
||||
func (c *DHCPClient) Run() error {
|
||||
err := c.loadLeaseFile()
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
|
||||
continue
|
||||
}
|
||||
|
||||
if event.Name == c.leaseFile {
|
||||
c.logger.Debug().
|
||||
Str("event", event.Op.String()).
|
||||
Str("path", event.Name).
|
||||
Msg("udhcpc lease file updated, reloading lease")
|
||||
_ = c.loadLeaseFile()
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.logger.Error().Err(err).Msg("error watching lease file")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, path := range c.getWatchPaths() {
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
c.logger.Error().
|
||||
Err(err).
|
||||
Str("path", path).
|
||||
Msg("failed to watch directory")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: update udhcpc pid file
|
||||
// we'll comment this out for now because the pid might change
|
||||
// process := c.GetProcess()
|
||||
// if process == nil {
|
||||
// c.logger.Error().Msg("udhcpc process not found")
|
||||
// }
|
||||
|
||||
// block the goroutine until the lease file is updated
|
||||
<-make(chan struct{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DHCPClient) loadLeaseFile() error {
|
||||
file, err := os.ReadFile(c.leaseFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := string(file)
|
||||
if data == "" {
|
||||
c.logger.Debug().Msg("udhcpc lease file is empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
lease := &Lease{}
|
||||
err = UnmarshalDHCPCLease(lease, string(file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isFirstLoad := c.lease == nil
|
||||
|
||||
// Skip processing if lease hasn't changed to avoid unnecessary wake-ups.
|
||||
if reflect.DeepEqual(c.lease, lease) {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.lease = lease
|
||||
|
||||
if lease.IPAddress == nil {
|
||||
c.logger.Info().
|
||||
Interface("lease", lease).
|
||||
Str("data", string(file)).
|
||||
Msg("udhcpc lease cleared")
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := "udhcpc lease updated"
|
||||
if isFirstLoad {
|
||||
msg = "udhcpc lease loaded"
|
||||
}
|
||||
|
||||
leaseExpiry, err := lease.SetLeaseExpiry()
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to get dhcp lease expiry")
|
||||
} else {
|
||||
expiresIn := time.Until(leaseExpiry)
|
||||
c.logger.Info().
|
||||
Interface("expiry", leaseExpiry).
|
||||
Str("expiresIn", expiresIn.String()).
|
||||
Msg("current dhcp lease expiry time calculated")
|
||||
}
|
||||
|
||||
c.onLeaseChange(lease)
|
||||
|
||||
c.logger.Info().
|
||||
Str("ip", lease.IPAddress.String()).
|
||||
Str("leaseTime", lease.LeaseTime.String()).
|
||||
Interface("data", lease).
|
||||
Msg(msg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DHCPClient) GetLease() *Lease {
|
||||
return c.lease
|
||||
}
|
|
@ -3,8 +3,8 @@ package kvm
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/dhclient"
|
||||
"github.com/jetkvm/kvm/internal/network"
|
||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -53,7 +53,7 @@ func initNetwork() error {
|
|||
OnInitialCheck: func(state *network.NetworkInterfaceState) {
|
||||
networkStateChanged()
|
||||
},
|
||||
OnDhcpLeaseChange: func(lease *udhcpc.Lease) {
|
||||
OnDhcpLeaseChange: func(lease *dhclient.Lease) {
|
||||
networkStateChanged()
|
||||
|
||||
if currentSession == nil {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"react": "^19.1.1",
|
||||
"react-animate-height": "^3.2.3",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
|
@ -5786,6 +5787,21 @@
|
|||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.62.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
|
||||
|
|
|
@ -267,7 +267,7 @@ export default function SettingsNetworkRoute() {
|
|||
placeholder="http://proxy.example.com:8080"
|
||||
{...register("http_proxy", {
|
||||
validate: (value: string | null) => {
|
||||
if (value === "") return true;
|
||||
if (value === "" || value === null) return true;
|
||||
if (!validator.isURL(value || "", { protocols: ["http", "https"] })) {
|
||||
return "Invalid HTTP proxy URL";
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue