mirror of https://github.com/jetkvm/kvm.git
rewrite network manager
This commit is contained in:
parent
317218a682
commit
ef0bdc0f65
2
cloud.go
2
cloud.go
|
|
@ -494,7 +494,7 @@ func RunWebsocketClient() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the network is not up, well, we can't connect to the cloud.
|
// If the network is not up, well, we can't connect to the cloud.
|
||||||
if !networkState.IsOnline() {
|
if !networkManager.IsOnline() {
|
||||||
cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds")
|
cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds")
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
"github.com/jetkvm/kvm/internal/network"
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
@ -102,7 +102,7 @@ type Config struct {
|
||||||
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
|
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
|
||||||
UsbConfig *usbgadget.Config `json:"usb_config"`
|
UsbConfig *usbgadget.Config `json:"usb_config"`
|
||||||
UsbDevices *usbgadget.Devices `json:"usb_devices"`
|
UsbDevices *usbgadget.Devices `json:"usb_devices"`
|
||||||
NetworkConfig *network.NetworkConfig `json:"network_config"`
|
NetworkConfig *types.NetworkConfig `json:"network_config"`
|
||||||
DefaultLogLevel string `json:"default_log_level"`
|
DefaultLogLevel string `json:"default_log_level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,7 +160,7 @@ var defaultConfig = &Config{
|
||||||
Keyboard: true,
|
Keyboard: true,
|
||||||
MassStorage: true,
|
MassStorage: true,
|
||||||
},
|
},
|
||||||
NetworkConfig: &network.NetworkConfig{},
|
NetworkConfig: &types.NetworkConfig{},
|
||||||
DefaultLogLevel: "INFO",
|
DefaultLogLevel: "INFO",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
12
display.go
12
display.go
|
|
@ -27,7 +27,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func switchToMainScreen() {
|
func switchToMainScreen() {
|
||||||
if networkState.IsUp() {
|
if networkManager.IsUp() {
|
||||||
nativeInstance.SwitchToScreenIfDifferent("home_screen")
|
nativeInstance.SwitchToScreenIfDifferent("home_screen")
|
||||||
} else {
|
} else {
|
||||||
nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
|
nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
|
||||||
|
|
@ -35,13 +35,13 @@ func switchToMainScreen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDisplay() {
|
func updateDisplay() {
|
||||||
nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkState.IPv4String())
|
nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkManager.IPv4String())
|
||||||
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkState.IPv6String())
|
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkManager.IPv6String())
|
||||||
|
|
||||||
_, _ = nativeInstance.UIObjHide("menu_btn_network")
|
_, _ = nativeInstance.UIObjHide("menu_btn_network")
|
||||||
_, _ = nativeInstance.UIObjHide("menu_btn_access")
|
_, _ = nativeInstance.UIObjHide("menu_btn_access")
|
||||||
|
|
||||||
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkState.MACString())
|
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
|
||||||
|
|
||||||
if usbState == "configured" {
|
if usbState == "configured" {
|
||||||
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected")
|
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected")
|
||||||
|
|
@ -59,7 +59,7 @@ func updateDisplay() {
|
||||||
}
|
}
|
||||||
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
|
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
|
||||||
|
|
||||||
if networkState.IsUp() {
|
if networkManager.IsUp() {
|
||||||
nativeInstance.UISetVar("main_screen", "home_screen")
|
nativeInstance.UISetVar("main_screen", "home_screen")
|
||||||
nativeInstance.SwitchToScreenIf("home_screen", []string{"no_network_screen", "boot_screen"})
|
nativeInstance.SwitchToScreenIf("home_screen", []string{"no_network_screen", "boot_screen"})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -190,7 +190,7 @@ func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool, reason string) {
|
||||||
|
|
||||||
func updateStaticContents() {
|
func updateStaticContents() {
|
||||||
//contents that never change
|
//contents that never change
|
||||||
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkState.MACString())
|
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
|
||||||
|
|
||||||
// get cpu info
|
// get cpu info
|
||||||
if cpuInfo, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
if cpuInfo, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
||||||
|
|
|
||||||
7
go.mod
7
go.mod
|
|
@ -16,6 +16,7 @@ require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/guregu/null/v6 v6.0.0
|
github.com/guregu/null/v6 v6.0.0
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
|
||||||
|
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e
|
||||||
github.com/pion/logging v0.2.4
|
github.com/pion/logging v0.2.4
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
github.com/pion/webrtc/v4 v4.1.4
|
github.com/pion/webrtc/v4 v4.1.4
|
||||||
|
|
@ -54,15 +55,19 @@ require (
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // 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/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||||
github.com/pilebones/go-udev v0.9.1 // indirect
|
github.com/pilebones/go-udev v0.9.1 // indirect
|
||||||
github.com/pion/datachannel v1.5.10 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.7 // indirect
|
github.com/pion/dtls/v3 v3.0.7 // indirect
|
||||||
|
|
@ -82,12 +87,14 @@ require (
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.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/ugorji/go/codec v1.3.0 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/text v0.29.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|
|
||||||
20
go.sum
20
go.sum
|
|
@ -66,8 +66,15 @@ github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||||
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||||
|
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-20250919081422-f80a1952f48e h1:nu5z6Kg+gMNW6tdqnVjg/QEJ8Nw71IJQqOtWj00XHEU=
|
||||||
|
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
|
||||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
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=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
|
@ -92,6 +99,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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|
@ -101,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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
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/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
|
|
@ -161,6 +174,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.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.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.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
|
@ -169,6 +184,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
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/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 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/vearutop/statigz v1.5.0 h1:FuWwZiT82yBw4xbWdWIawiP2XFTyEPhIo8upRxiKLqk=
|
github.com/vearutop/statigz v1.5.0 h1:FuWwZiT82yBw4xbWdWIawiP2XFTyEPhIo8upRxiKLqk=
|
||||||
|
|
@ -193,6 +210,9 @@ golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
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/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
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.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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,22 @@ import (
|
||||||
type FieldConfig struct {
|
type FieldConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Required bool
|
Required bool
|
||||||
RequiredIf map[string]any
|
RequiredIf map[string]interface{}
|
||||||
OneOf []string
|
OneOf []string
|
||||||
ValidateTypes []string
|
ValidateTypes []string
|
||||||
Defaults any
|
Defaults interface{}
|
||||||
IsEmpty bool
|
IsEmpty bool
|
||||||
CurrentValue any
|
CurrentValue interface{}
|
||||||
TypeString string
|
TypeString string
|
||||||
Delegated bool
|
Delegated bool
|
||||||
shouldUpdateValue bool
|
shouldUpdateValue bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetDefaultsAndValidate(config any) error {
|
func SetDefaultsAndValidate(config interface{}) error {
|
||||||
return setDefaultsAndValidate(config, true)
|
return setDefaultsAndValidate(config, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDefaultsAndValidate(config any, isRoot bool) error {
|
func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
||||||
// first we need to check if the config is a pointer
|
// first we need to check if the config is a pointer
|
||||||
if reflect.TypeOf(config).Kind() != reflect.Ptr {
|
if reflect.TypeOf(config).Kind() != reflect.Ptr {
|
||||||
return fmt.Errorf("config is not a pointer")
|
return fmt.Errorf("config is not a pointer")
|
||||||
|
|
@ -55,7 +55,7 @@ func setDefaultsAndValidate(config any, isRoot bool) error {
|
||||||
Name: field.Name,
|
Name: field.Name,
|
||||||
OneOf: splitString(field.Tag.Get("one_of")),
|
OneOf: splitString(field.Tag.Get("one_of")),
|
||||||
ValidateTypes: splitString(field.Tag.Get("validate_type")),
|
ValidateTypes: splitString(field.Tag.Get("validate_type")),
|
||||||
RequiredIf: make(map[string]any),
|
RequiredIf: make(map[string]interface{}),
|
||||||
CurrentValue: fieldValue.Interface(),
|
CurrentValue: fieldValue.Interface(),
|
||||||
IsEmpty: false,
|
IsEmpty: false,
|
||||||
TypeString: fieldType,
|
TypeString: fieldType,
|
||||||
|
|
@ -142,8 +142,8 @@ func setDefaultsAndValidate(config any, isRoot bool) error {
|
||||||
// now check if the field has required_if
|
// now check if the field has required_if
|
||||||
requiredIf := field.Tag.Get("required_if")
|
requiredIf := field.Tag.Get("required_if")
|
||||||
if requiredIf != "" {
|
if requiredIf != "" {
|
||||||
requiredIfParts := strings.SplitSeq(requiredIf, ",")
|
requiredIfParts := strings.Split(requiredIf, ",")
|
||||||
for part := range requiredIfParts {
|
for _, part := range requiredIfParts {
|
||||||
partVal := strings.SplitN(part, "=", 2)
|
partVal := strings.SplitN(part, "=", 2)
|
||||||
if len(partVal) != 2 {
|
if len(partVal) != 2 {
|
||||||
return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf)
|
return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf)
|
||||||
|
|
@ -168,7 +168,7 @@ func setDefaultsAndValidate(config any, isRoot bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateFields(config any, fields map[string]FieldConfig) error {
|
func validateFields(config interface{}, fields map[string]FieldConfig) error {
|
||||||
// now we can start to validate the fields
|
// now we can start to validate the fields
|
||||||
for _, fieldConfig := range fields {
|
for _, fieldConfig := range fields {
|
||||||
if err := fieldConfig.validate(fields); err != nil {
|
if err := fieldConfig.validate(fields); err != nil {
|
||||||
|
|
@ -215,7 +215,7 @@ func (f *FieldConfig) validate(fields map[string]FieldConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FieldConfig) populate(config any) {
|
func (f *FieldConfig) populate(config interface{}) {
|
||||||
// update the field if it's not empty
|
// update the field if it's not empty
|
||||||
if !f.shouldUpdateValue {
|
if !f.shouldUpdateValue {
|
||||||
return
|
return
|
||||||
|
|
@ -346,6 +346,17 @@ func (f *FieldConfig) validateField() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle []string types, like dns servers, time sync ntp servers, etc.
|
||||||
|
if slice, ok := f.CurrentValue.([]string); ok {
|
||||||
|
for i, item := range slice {
|
||||||
|
if err := f.validateSingleValue(item, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle single string types
|
||||||
val, err := toString(f.CurrentValue)
|
val, err := toString(f.CurrentValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err)
|
return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err)
|
||||||
|
|
@ -355,27 +366,68 @@ func (f *FieldConfig) validateField() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return f.validateSingleValue(val, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldConfig) validateSingleValue(val string, index int) error {
|
||||||
for _, validateType := range f.ValidateTypes {
|
for _, validateType := range f.ValidateTypes {
|
||||||
|
var fieldRef string
|
||||||
|
if index >= 0 {
|
||||||
|
fieldRef = fmt.Sprintf("field `%s[%d]`", f.Name, index)
|
||||||
|
} else {
|
||||||
|
fieldRef = fmt.Sprintf("field `%s`", f.Name)
|
||||||
|
}
|
||||||
|
|
||||||
switch validateType {
|
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":
|
case "ipv4":
|
||||||
if net.ParseIP(val).To4() == nil {
|
if net.ParseIP(val).To4() == nil {
|
||||||
return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", f.Name, val)
|
return fmt.Errorf("%s is not a valid IPv4 address: %s", fieldRef, val)
|
||||||
}
|
}
|
||||||
case "ipv6":
|
case "ipv6":
|
||||||
if net.ParseIP(val).To16() == nil {
|
if net.ParseIP(val).To16() == nil {
|
||||||
return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", f.Name, val)
|
return fmt.Errorf("%s is not a valid IPv6 address: %s", fieldRef, val)
|
||||||
|
}
|
||||||
|
case "ipv6_prefix":
|
||||||
|
if i, _, err := net.ParseCIDR(val); err != nil {
|
||||||
|
if i.To16() == nil {
|
||||||
|
return fmt.Errorf("%s is not a valid IPv6 prefix: %s", fieldRef, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ipv4_or_ipv6":
|
||||||
|
if net.ParseIP(val) == nil {
|
||||||
|
return fmt.Errorf("%s is not a valid IPv4 or IPv6 address: %s", fieldRef, val)
|
||||||
}
|
}
|
||||||
case "hwaddr":
|
case "hwaddr":
|
||||||
if _, err := net.ParseMAC(val); err != nil {
|
if _, err := net.ParseMAC(val); err != nil {
|
||||||
return fmt.Errorf("field `%s` is not a valid MAC address: %s", f.Name, val)
|
return fmt.Errorf("%s is not a valid MAC address: %s", fieldRef, val)
|
||||||
}
|
}
|
||||||
case "hostname":
|
case "hostname":
|
||||||
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
||||||
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
|
return fmt.Errorf("%s is not a valid hostname: %s", fieldRef, val)
|
||||||
}
|
}
|
||||||
case "proxy":
|
case "proxy":
|
||||||
if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" {
|
if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" {
|
||||||
return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val)
|
return fmt.Errorf("%s is not a valid HTTP proxy URL: %s", fieldRef, val)
|
||||||
|
}
|
||||||
|
case "url":
|
||||||
|
if _, err := url.Parse(val); err != nil {
|
||||||
|
return fmt.Errorf("%s is not a valid URL: %s", fieldRef, val)
|
||||||
|
}
|
||||||
|
case "cidr":
|
||||||
|
if _, _, err := net.ParseCIDR(val); err != nil {
|
||||||
|
return fmt.Errorf("%s is not a valid CIDR notation: %s", fieldRef, val)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ type testIPv4StaticConfig struct {
|
||||||
|
|
||||||
type testIPv6StaticConfig struct {
|
type testIPv6StaticConfig struct {
|
||||||
Address null.String `json:"address" validate_type:"ipv6" required:"true"`
|
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"`
|
Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"`
|
||||||
DNS []string `json:"dns" validate_type:"ipv6" required:"true"`
|
DNS []string `json:"dns" validate_type:"ipv6" required:"true"`
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ type testNetworkConfig struct {
|
||||||
IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||||
IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
||||||
|
|
||||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,enabled" default:"enabled"`
|
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
||||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ func splitString(s string) []string {
|
||||||
return strings.Split(s, ",")
|
return strings.Split(s, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func toString(v any) (string, error) {
|
func toString(v interface{}) (string, error) {
|
||||||
switch v := v.(type) {
|
switch v := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
return v, nil
|
return v, nil
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package netif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
type DhcpTargetState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
DhcpTargetStateDoNothing DhcpTargetState = iota
|
|
||||||
DhcpTargetStateStart
|
|
||||||
DhcpTargetStateStop
|
|
||||||
DhcpTargetStateRenew
|
|
||||||
DhcpTargetStateRelease
|
|
||||||
)
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"golang.org/x/net/idna"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hostnamePath = "/etc/hostname"
|
|
||||||
hostsPath = "/etc/hosts"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
hostnameLock sync.Mutex = sync.Mutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func updateEtcHosts(hostname string, fqdn string) error {
|
|
||||||
// update /etc/hosts
|
|
||||||
hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
defer hostsFile.Close()
|
|
||||||
|
|
||||||
// read all lines
|
|
||||||
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
lines, err := io.ReadAll(hostsFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newLines := []string{}
|
|
||||||
hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn)
|
|
||||||
hostLineExists := false
|
|
||||||
|
|
||||||
for line := range strings.SplitSeq(string(lines), "\n") {
|
|
||||||
if strings.HasPrefix(line, "127.0.1.1") {
|
|
||||||
hostLineExists = true
|
|
||||||
line = hostLine
|
|
||||||
}
|
|
||||||
newLines = append(newLines, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hostLineExists {
|
|
||||||
newLines = append(newLines, hostLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := hostsFile.Truncate(0); err != nil {
|
|
||||||
return fmt.Errorf("failed to truncate %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := hostsFile.Write([]byte(strings.Join(newLines, "\n"))); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToValidHostname(hostname string) string {
|
|
||||||
ascii, err := idna.Lookup.ToASCII(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return ascii
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetHostname(hostname string, fqdn string) error {
|
|
||||||
hostnameLock.Lock()
|
|
||||||
defer hostnameLock.Unlock()
|
|
||||||
|
|
||||||
hostname = ToValidHostname(strings.TrimSpace(hostname))
|
|
||||||
fqdn = ToValidHostname(strings.TrimSpace(fqdn))
|
|
||||||
|
|
||||||
if hostname == "" {
|
|
||||||
return fmt.Errorf("invalid hostname: %s", hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fqdn == "" {
|
|
||||||
fqdn = hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
// update /etc/hostname
|
|
||||||
if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", hostnamePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update /etc/hosts
|
|
||||||
if err := updateEtcHosts(hostname, fqdn); err != nil {
|
|
||||||
return fmt.Errorf("failed to update /etc/hosts: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// run hostname
|
|
||||||
if err := exec.Command("hostname", "-F", hostnamePath).Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to run hostname: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) setHostnameIfNotSame() error {
|
|
||||||
hostname := s.GetHostname()
|
|
||||||
currentHostname, _ := os.Hostname()
|
|
||||||
|
|
||||||
fqdn := fmt.Sprintf("%s.%s", hostname, s.GetDomain())
|
|
||||||
|
|
||||||
if currentHostname == hostname && s.currentFqdn == fqdn && s.currentHostname == hostname {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
scopedLogger := s.l.With().Str("hostname", hostname).Str("fqdn", fqdn).Logger()
|
|
||||||
|
|
||||||
err := SetHostname(hostname, fqdn)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Error().Err(err).Msg("failed to set hostname")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.currentHostname = hostname
|
|
||||||
s.currentFqdn = fqdn
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("hostname set")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,403 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/confparser"
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
|
||||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NetworkInterfaceState struct {
|
|
||||||
interfaceName string
|
|
||||||
interfaceUp bool
|
|
||||||
ipv4Addr *net.IP
|
|
||||||
ipv4Addresses []string
|
|
||||||
ipv6Addr *net.IP
|
|
||||||
ipv6Addresses []IPv6Address
|
|
||||||
ipv6LinkLocal *net.IP
|
|
||||||
ntpAddresses []*net.IP
|
|
||||||
macAddr *net.HardwareAddr
|
|
||||||
|
|
||||||
l *zerolog.Logger
|
|
||||||
stateLock sync.Mutex
|
|
||||||
|
|
||||||
config *NetworkConfig
|
|
||||||
dhcpClient *udhcpc.DHCPClient
|
|
||||||
|
|
||||||
defaultHostname string
|
|
||||||
currentHostname string
|
|
||||||
currentFqdn string
|
|
||||||
|
|
||||||
onStateChange func(state *NetworkInterfaceState)
|
|
||||||
onInitialCheck func(state *NetworkInterfaceState)
|
|
||||||
cbConfigChange func(config *NetworkConfig)
|
|
||||||
|
|
||||||
checked bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type NetworkInterfaceOptions struct {
|
|
||||||
InterfaceName string
|
|
||||||
DhcpPidFile string
|
|
||||||
Logger *zerolog.Logger
|
|
||||||
DefaultHostname string
|
|
||||||
OnStateChange func(state *NetworkInterfaceState)
|
|
||||||
OnInitialCheck func(state *NetworkInterfaceState)
|
|
||||||
OnDhcpLeaseChange func(lease *udhcpc.Lease, state *NetworkInterfaceState)
|
|
||||||
OnConfigChange func(config *NetworkConfig)
|
|
||||||
NetworkConfig *NetworkConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceState, error) {
|
|
||||||
if opts.NetworkConfig == nil {
|
|
||||||
return nil, fmt.Errorf("NetworkConfig can not be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.DefaultHostname == "" {
|
|
||||||
opts.DefaultHostname = "jetkvm"
|
|
||||||
}
|
|
||||||
|
|
||||||
err := confparser.SetDefaultsAndValidate(opts.NetworkConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l := opts.Logger
|
|
||||||
s := &NetworkInterfaceState{
|
|
||||||
interfaceName: opts.InterfaceName,
|
|
||||||
defaultHostname: opts.DefaultHostname,
|
|
||||||
stateLock: sync.Mutex{},
|
|
||||||
l: l,
|
|
||||||
onStateChange: opts.OnStateChange,
|
|
||||||
onInitialCheck: opts.OnInitialCheck,
|
|
||||||
cbConfigChange: opts.OnConfigChange,
|
|
||||||
config: opts.NetworkConfig,
|
|
||||||
ntpAddresses: make([]*net.IP, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the dhcp client
|
|
||||||
dhcpClient := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{
|
|
||||||
InterfaceName: opts.InterfaceName,
|
|
||||||
PidFile: opts.DhcpPidFile,
|
|
||||||
Logger: l,
|
|
||||||
OnLeaseChange: func(lease *udhcpc.Lease) {
|
|
||||||
_, err := s.update()
|
|
||||||
if err != nil {
|
|
||||||
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = s.updateNtpServersFromLease(lease)
|
|
||||||
_ = s.setHostnameIfNotSame()
|
|
||||||
|
|
||||||
opts.OnDhcpLeaseChange(lease, s)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
s.dhcpClient = dhcpClient
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IsUp() bool {
|
|
||||||
return s.interfaceUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) HasIPAssigned() bool {
|
|
||||||
return s.ipv4Addr != nil || s.ipv6Addr != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IsOnline() bool {
|
|
||||||
return s.IsUp() && s.HasIPAssigned()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv4() *net.IP {
|
|
||||||
return s.ipv4Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv4String() string {
|
|
||||||
if s.ipv4Addr == nil {
|
|
||||||
return "..."
|
|
||||||
}
|
|
||||||
return s.ipv4Addr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv6() *net.IP {
|
|
||||||
return s.ipv6Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv6String() string {
|
|
||||||
if s.ipv6Addr == nil {
|
|
||||||
return "..."
|
|
||||||
}
|
|
||||||
return s.ipv6Addr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) NtpAddresses() []*net.IP {
|
|
||||||
return s.ntpAddresses
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) NtpAddressesString() []string {
|
|
||||||
ntpServers := []string{}
|
|
||||||
|
|
||||||
if s != nil {
|
|
||||||
s.l.Debug().Any("s", s).Msg("getting NTP address strings")
|
|
||||||
|
|
||||||
if len(s.ntpAddresses) > 0 {
|
|
||||||
for _, server := range s.ntpAddresses {
|
|
||||||
s.l.Debug().IPAddr("server", *server).Msg("converting NTP address")
|
|
||||||
ntpServers = append(ntpServers, server.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ntpServers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
|
|
||||||
return s.macAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) MACString() string {
|
|
||||||
if s.macAddr == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return s.macAddr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|
||||||
s.stateLock.Lock()
|
|
||||||
defer s.stateLock.Unlock()
|
|
||||||
|
|
||||||
dhcpTargetState := DhcpTargetStateDoNothing
|
|
||||||
|
|
||||||
iface, err := netlink.LinkByName(s.interfaceName)
|
|
||||||
if err != nil {
|
|
||||||
s.l.Error().Err(err).Msg("failed to get interface")
|
|
||||||
return dhcpTargetState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// detect if the interface status changed
|
|
||||||
var changed bool
|
|
||||||
attrs := iface.Attrs()
|
|
||||||
state := attrs.OperState
|
|
||||||
newInterfaceUp := state == netlink.OperUp
|
|
||||||
|
|
||||||
// check if the interface is coming up
|
|
||||||
interfaceGoingUp := !s.interfaceUp && newInterfaceUp
|
|
||||||
interfaceGoingDown := s.interfaceUp && !newInterfaceUp
|
|
||||||
|
|
||||||
if s.interfaceUp != newInterfaceUp {
|
|
||||||
s.interfaceUp = newInterfaceUp
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
if interfaceGoingUp {
|
|
||||||
s.l.Info().Msg("interface state transitioned to up")
|
|
||||||
dhcpTargetState = DhcpTargetStateRenew
|
|
||||||
} else if interfaceGoingDown {
|
|
||||||
s.l.Info().Msg("interface state transitioned to down")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the mac address
|
|
||||||
s.macAddr = &attrs.HardwareAddr
|
|
||||||
|
|
||||||
// get the ip addresses
|
|
||||||
addrs, err := netlinkAddrs(iface)
|
|
||||||
if err != nil {
|
|
||||||
return dhcpTargetState, logging.ErrorfL(s.l, "failed to get ip addresses", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ipv4Addresses = make([]net.IP, 0)
|
|
||||||
ipv4AddressesString = make([]string, 0)
|
|
||||||
ipv6Addresses = make([]IPv6Address, 0)
|
|
||||||
// ipv6AddressesString = make([]string, 0)
|
|
||||||
ipv6LinkLocal *net.IP
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if addr.IP.To4() != nil {
|
|
||||||
scopedLogger := s.l.With().Str("ipv4", addr.IP.String()).Logger()
|
|
||||||
if interfaceGoingDown {
|
|
||||||
// remove all IPv4 addresses from the interface.
|
|
||||||
scopedLogger.Info().Msg("state transitioned to down, removing IPv4 address")
|
|
||||||
err := netlink.AddrDel(iface, &addr)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to delete address")
|
|
||||||
}
|
|
||||||
// notify the DHCP client to release the lease
|
|
||||||
dhcpTargetState = DhcpTargetStateRelease
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ipv4Addresses = append(ipv4Addresses, addr.IP)
|
|
||||||
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
|
|
||||||
} else if addr.IP.To16() != nil {
|
|
||||||
if s.config.IPv6Mode.String == "disabled" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
|
|
||||||
// check if it's a link local address
|
|
||||||
if addr.IP.IsLinkLocalUnicast() {
|
|
||||||
ipv6LinkLocal = &addr.IP
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !addr.IP.IsGlobalUnicast() {
|
|
||||||
scopedLogger.Trace().Msg("not a global unicast address, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if interfaceGoingDown {
|
|
||||||
scopedLogger.Info().Msg("state transitioned to down, removing IPv6 address")
|
|
||||||
err := netlink.AddrDel(iface, &addr)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to delete address")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ipv6Addresses = append(ipv6Addresses, IPv6Address{
|
|
||||||
Address: addr.IP,
|
|
||||||
Prefix: *addr.IPNet,
|
|
||||||
ValidLifetime: lifetimeToTime(addr.ValidLft),
|
|
||||||
PreferredLifetime: lifetimeToTime(addr.PreferedLft),
|
|
||||||
Scope: addr.Scope,
|
|
||||||
})
|
|
||||||
// ipv6AddressesString = append(ipv6AddressesString, addr.IPNet.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ipv4Addresses) > 0 {
|
|
||||||
// compare the addresses to see if there's a change
|
|
||||||
if s.ipv4Addr == nil || s.ipv4Addr.String() != ipv4Addresses[0].String() {
|
|
||||||
scopedLogger := s.l.With().Str("ipv4", ipv4Addresses[0].String()).Logger()
|
|
||||||
if s.ipv4Addr != nil {
|
|
||||||
scopedLogger.Info().
|
|
||||||
Str("old_ipv4", s.ipv4Addr.String()).
|
|
||||||
Msg("IPv4 address changed")
|
|
||||||
} else {
|
|
||||||
scopedLogger.Info().Msg("IPv4 address found")
|
|
||||||
}
|
|
||||||
s.ipv4Addr = &ipv4Addresses[0]
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.ipv4Addresses = ipv4AddressesString
|
|
||||||
|
|
||||||
if s.config.IPv6Mode.String != "disabled" {
|
|
||||||
if ipv6LinkLocal != nil {
|
|
||||||
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
|
|
||||||
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
|
|
||||||
if s.ipv6LinkLocal != nil {
|
|
||||||
scopedLogger.Info().
|
|
||||||
Str("old_ipv6", s.ipv6LinkLocal.String()).
|
|
||||||
Msg("IPv6 link local address changed")
|
|
||||||
} else {
|
|
||||||
scopedLogger.Info().Msg("IPv6 link local address found")
|
|
||||||
}
|
|
||||||
s.ipv6LinkLocal = ipv6LinkLocal
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.ipv6Addresses = ipv6Addresses
|
|
||||||
|
|
||||||
if len(ipv6Addresses) > 0 {
|
|
||||||
// compare the addresses to see if there's a change
|
|
||||||
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() {
|
|
||||||
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger()
|
|
||||||
if s.ipv6Addr != nil {
|
|
||||||
scopedLogger.Info().
|
|
||||||
Str("old_ipv6", s.ipv6Addr.String()).
|
|
||||||
Msg("IPv6 address changed")
|
|
||||||
} else {
|
|
||||||
scopedLogger.Info().Msg("IPv6 address found")
|
|
||||||
}
|
|
||||||
s.ipv6Addr = &ipv6Addresses[0].Address
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it's the initial check, we'll set changed to false
|
|
||||||
initialCheck := !s.checked
|
|
||||||
if initialCheck {
|
|
||||||
s.checked = true
|
|
||||||
changed = false
|
|
||||||
if dhcpTargetState == DhcpTargetStateRenew {
|
|
||||||
// it's the initial check, we'll start the DHCP client
|
|
||||||
// dhcpTargetState = DhcpTargetStateStart
|
|
||||||
// TODO: manage DHCP client start/stop
|
|
||||||
dhcpTargetState = DhcpTargetStateDoNothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if initialCheck {
|
|
||||||
s.handleInitialCheck()
|
|
||||||
} else if changed {
|
|
||||||
s.handleStateChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
return dhcpTargetState, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.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))
|
|
||||||
|
|
||||||
for _, ntpServer := range lease.NTPServers {
|
|
||||||
if ntpServer != nil {
|
|
||||||
s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease")
|
|
||||||
s.ntpAddresses = append(s.ntpAddresses, &ntpServer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s.l.Info().Msg("no NTP servers found in lease")
|
|
||||||
s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) handleInitialCheck() {
|
|
||||||
// if s.IsUp() {}
|
|
||||||
s.onInitialCheck(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) handleStateChange() {
|
|
||||||
// if s.IsUp() {} else {}
|
|
||||||
s.onStateChange(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
|
||||||
dhcpTargetState, err := s.update()
|
|
||||||
if err != nil {
|
|
||||||
return logging.ErrorfL(s.l, "failed to update network state", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch dhcpTargetState {
|
|
||||||
case DhcpTargetStateRenew:
|
|
||||||
s.l.Info().Msg("renewing DHCP lease")
|
|
||||||
_ = s.dhcpClient.Renew()
|
|
||||||
case DhcpTargetStateRelease:
|
|
||||||
s.l.Info().Msg("releasing DHCP lease")
|
|
||||||
_ = s.dhcpClient.Release()
|
|
||||||
case DhcpTargetStateStart:
|
|
||||||
s.l.Warn().Msg("dhcpTargetStateStart not implemented")
|
|
||||||
case DhcpTargetStateStop:
|
|
||||||
s.l.Warn().Msg("dhcpTargetStateStop not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) onConfigChange(config *NetworkConfig) {
|
|
||||||
_ = s.setHostnameIfNotSame()
|
|
||||||
s.cbConfigChange(config)
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
|
||||||
"github.com/vishvananda/netlink/nl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) {
|
|
||||||
if update.Link.Attrs().Name == s.interfaceName {
|
|
||||||
s.l.Info().Interface("update", update).Msg("interface link update received")
|
|
||||||
_ = s.CheckAndUpdateDhcp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) Run() error {
|
|
||||||
updates := make(chan netlink.LinkUpdate)
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
if err := netlink.LinkSubscribe(updates, done); err != nil {
|
|
||||||
s.l.Warn().Err(err).Msg("failed to subscribe to link updates")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = s.setHostnameIfNotSame()
|
|
||||||
|
|
||||||
// run the dhcp client
|
|
||||||
go s.dhcpClient.Run() // nolint:errcheck
|
|
||||||
|
|
||||||
if err := s.CheckAndUpdateDhcp(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case update := <-updates:
|
|
||||||
s.HandleLinkUpdate(update)
|
|
||||||
case <-ticker.C:
|
|
||||||
_ = s.CheckAndUpdateDhcp()
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) {
|
|
||||||
return netlink.AddrList(iface, nl.FAMILY_ALL)
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
//go:build !linux
|
|
||||||
|
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) HandleLinkUpdate() error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) Run() error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) {
|
|
||||||
return nil, fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +1,127 @@
|
||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
// import (
|
||||||
"fmt"
|
// "fmt"
|
||||||
"time"
|
// "time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/confparser"
|
// "github.com/jetkvm/kvm/internal/confparser"
|
||||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
// "github.com/jetkvm/kvm/internal/network/types"
|
||||||
)
|
// "github.com/jetkvm/kvm/internal/udhcpc"
|
||||||
|
// )
|
||||||
|
|
||||||
type RpcIPv6Address struct {
|
// type RpcIPv6Address struct {
|
||||||
Address string `json:"address"`
|
// Address string `json:"address"`
|
||||||
ValidLifetime *time.Time `json:"valid_lifetime,omitempty"`
|
// ValidLifetime *time.Time `json:"valid_lifetime,omitempty"`
|
||||||
PreferredLifetime *time.Time `json:"preferred_lifetime,omitempty"`
|
// PreferredLifetime *time.Time `json:"preferred_lifetime,omitempty"`
|
||||||
Scope int `json:"scope"`
|
// Scope int `json:"scope"`
|
||||||
}
|
// }
|
||||||
|
|
||||||
type RpcNetworkState struct {
|
// type RpcNetworkState struct {
|
||||||
InterfaceName string `json:"interface_name"`
|
// InterfaceName string `json:"interface_name"`
|
||||||
MacAddress string `json:"mac_address"`
|
// MacAddress string `json:"mac_address"`
|
||||||
IPv4 string `json:"ipv4,omitempty"`
|
// IPv4 string `json:"ipv4,omitempty"`
|
||||||
IPv6 string `json:"ipv6,omitempty"`
|
// IPv6 string `json:"ipv6,omitempty"`
|
||||||
IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
|
// IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
|
||||||
IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
|
// IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
|
||||||
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"`
|
// IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"`
|
||||||
DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"`
|
// DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"`
|
||||||
}
|
// }
|
||||||
|
|
||||||
type RpcNetworkSettings struct {
|
// type RpcNetworkSettings struct {
|
||||||
NetworkConfig
|
// NetworkConfig types.NetworkConfig
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) MacAddress() string {
|
// // func (s *NetworkInterfaceState) MacAddress() string {
|
||||||
if s.macAddr == nil {
|
// // if s.macAddr == nil {
|
||||||
return ""
|
// // return ""
|
||||||
}
|
// // }
|
||||||
|
|
||||||
return s.macAddr.String()
|
// // return s.macAddr.String()
|
||||||
}
|
// // }
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv4Address() string {
|
// // func (s *NetworkInterfaceState) IPv4Address() string {
|
||||||
if s.ipv4Addr == nil {
|
// // if s.ipv4Addr == nil {
|
||||||
return ""
|
// // return ""
|
||||||
}
|
// // }
|
||||||
|
|
||||||
return s.ipv4Addr.String()
|
// // return s.ipv4Addr.String()
|
||||||
}
|
// // }
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv6Address() string {
|
// // func (s *NetworkInterfaceState) IPv6Address() string {
|
||||||
if s.ipv6Addr == nil {
|
// // if s.ipv6Addr == nil {
|
||||||
return ""
|
// // return ""
|
||||||
}
|
// // }
|
||||||
|
|
||||||
return s.ipv6Addr.String()
|
// // return s.ipv6Addr.String()
|
||||||
}
|
// // }
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
|
// // func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
|
||||||
if s.ipv6LinkLocal == nil {
|
// // if s.ipv6LinkLocal == nil {
|
||||||
return ""
|
// // return ""
|
||||||
}
|
// // }
|
||||||
|
|
||||||
return s.ipv6LinkLocal.String()
|
// // return s.ipv6LinkLocal.String()
|
||||||
}
|
// // }
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
|
// func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
|
||||||
ipv6Addresses := make([]RpcIPv6Address, 0)
|
// ipv6Addresses := make([]RpcIPv6Address, 0)
|
||||||
|
|
||||||
if s.ipv6Addresses != nil && s.config.IPv6Mode.String != "disabled" {
|
// if s.ipv6Addresses != nil && s.config.IPv6Mode.String != "disabled" {
|
||||||
for _, addr := range s.ipv6Addresses {
|
// for _, addr := range s.ipv6Addresses {
|
||||||
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
|
// ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
|
||||||
Address: addr.Prefix.String(),
|
// Address: addr.Prefix.String(),
|
||||||
ValidLifetime: addr.ValidLifetime,
|
// ValidLifetime: addr.ValidLifetime,
|
||||||
PreferredLifetime: addr.PreferredLifetime,
|
// PreferredLifetime: addr.PreferredLifetime,
|
||||||
Scope: addr.Scope,
|
// Scope: addr.Scope,
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return RpcNetworkState{
|
// return RpcNetworkState{
|
||||||
InterfaceName: s.interfaceName,
|
// InterfaceName: s.interfaceName,
|
||||||
MacAddress: s.MacAddress(),
|
// MacAddress: s.MacAddress(),
|
||||||
IPv4: s.IPv4Address(),
|
// IPv4: s.IPv4Address(),
|
||||||
IPv6: s.IPv6Address(),
|
// IPv6: s.IPv6Address(),
|
||||||
IPv6LinkLocal: s.IPv6LinkLocalAddress(),
|
// IPv6LinkLocal: s.IPv6LinkLocalAddress(),
|
||||||
IPv4Addresses: s.ipv4Addresses,
|
// IPv4Addresses: s.ipv4Addresses,
|
||||||
IPv6Addresses: ipv6Addresses,
|
// IPv6Addresses: ipv6Addresses,
|
||||||
DHCPLease: s.dhcpClient.GetLease(),
|
// DHCPLease: s.dhcpClient.GetLease(),
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings {
|
// func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings {
|
||||||
if s.config == nil {
|
// if s.config == nil {
|
||||||
return RpcNetworkSettings{}
|
// return RpcNetworkSettings{}
|
||||||
}
|
// }
|
||||||
|
|
||||||
return RpcNetworkSettings{
|
// return RpcNetworkSettings{
|
||||||
NetworkConfig: *s.config,
|
// NetworkConfig: *s.config,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error {
|
// func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error {
|
||||||
currentSettings := s.config
|
// currentSettings := s.config
|
||||||
|
|
||||||
err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig)
|
// err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
if IsSame(currentSettings, settings.NetworkConfig) {
|
// if IsSame(currentSettings, settings.NetworkConfig) {
|
||||||
// no changes, do nothing
|
// // no changes, do nothing
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
s.config = &settings.NetworkConfig
|
// s.config = &settings.NetworkConfig
|
||||||
s.onConfigChange(s.config)
|
// s.onConfigChange(s.config)
|
||||||
|
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) RpcRenewDHCPLease() error {
|
// func (s *NetworkInterfaceState) RpcRenewDHCPLease() error {
|
||||||
if s.dhcpClient == nil {
|
// if s.dhcpClient == nil {
|
||||||
return fmt.Errorf("dhcp client not initialized")
|
// return fmt.Errorf("dhcp client not initialized")
|
||||||
}
|
// }
|
||||||
|
|
||||||
return s.dhcpClient.Renew()
|
// return s.dhcpClient.Renew()
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
package network
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/guregu/null/v6"
|
"github.com/guregu/null/v6"
|
||||||
"github.com/jetkvm/kvm/internal/mdns"
|
|
||||||
"golang.org/x/net/idna"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IPAddress represents a network interface address
|
||||||
|
type IPAddress struct {
|
||||||
|
Family int
|
||||||
|
Address net.IPNet
|
||||||
|
Gateway net.IP
|
||||||
|
MTU int
|
||||||
|
Secondary bool
|
||||||
|
Permanent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6Address represents an IPv6 address with lifetime information
|
||||||
type IPv6Address struct {
|
type IPv6Address struct {
|
||||||
Address net.IP `json:"address"`
|
Address net.IP `json:"address"`
|
||||||
Prefix net.IPNet `json:"prefix"`
|
Prefix net.IPNet `json:"prefix"`
|
||||||
|
|
@ -20,6 +28,7 @@ type IPv6Address struct {
|
||||||
Scope int `json:"scope"`
|
Scope int `json:"scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPv4StaticConfig represents static IPv4 configuration
|
||||||
type IPv4StaticConfig struct {
|
type IPv4StaticConfig struct {
|
||||||
Address null.String `json:"address,omitempty" validate_type:"ipv4" required:"true"`
|
Address null.String `json:"address,omitempty" validate_type:"ipv4" required:"true"`
|
||||||
Netmask null.String `json:"netmask,omitempty" validate_type:"ipv4" required:"true"`
|
Netmask null.String `json:"netmask,omitempty" validate_type:"ipv4" required:"true"`
|
||||||
|
|
@ -27,12 +36,14 @@ type IPv4StaticConfig struct {
|
||||||
DNS []string `json:"dns,omitempty" validate_type:"ipv4" required:"true"`
|
DNS []string `json:"dns,omitempty" validate_type:"ipv4" required:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPv6StaticConfig represents static IPv6 configuration
|
||||||
type IPv6StaticConfig struct {
|
type IPv6StaticConfig struct {
|
||||||
Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"`
|
Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6_prefix" required:"true"`
|
||||||
Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6" required:"true"`
|
|
||||||
Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"`
|
Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"`
|
||||||
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NetworkConfig represents the complete network configuration for an interface
|
||||||
type NetworkConfig struct {
|
type NetworkConfig struct {
|
||||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||||
HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"`
|
HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"`
|
||||||
|
|
@ -44,7 +55,7 @@ type NetworkConfig struct {
|
||||||
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||||
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
||||||
|
|
||||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,basic,all,enabled" default:"enabled"`
|
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
||||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||||
|
|
@ -55,13 +66,15 @@ type NetworkConfig struct {
|
||||||
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
// GetMDNSMode returns the MDNS mode configuration
|
||||||
listenOptions := &mdns.MDNSListenOptions{
|
func (c *NetworkConfig) GetMDNSMode() *MDNSListenOptions {
|
||||||
IPv4: c.IPv4Mode.String != "disabled",
|
mode := c.MDNSMode.String
|
||||||
IPv6: c.IPv6Mode.String != "disabled",
|
listenOptions := &MDNSListenOptions{
|
||||||
|
IPv4: true,
|
||||||
|
IPv6: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch c.MDNSMode.String {
|
switch mode {
|
||||||
case "ipv4_only":
|
case "ipv4_only":
|
||||||
listenOptions.IPv6 = false
|
listenOptions.IPv6 = false
|
||||||
case "ipv6_only":
|
case "ipv6_only":
|
||||||
|
|
@ -74,53 +87,66 @@ func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
||||||
return listenOptions
|
return listenOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
|
// MDNSListenOptions represents MDNS listening options
|
||||||
|
type MDNSListenOptions struct {
|
||||||
|
IPv4 bool
|
||||||
|
IPv6 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransportProxyFunc returns a function for HTTP proxy configuration
|
||||||
|
func (c *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||||
return func(*http.Request) (*url.URL, error) {
|
return func(*http.Request) (*url.URL, error) {
|
||||||
if s.HTTPProxy.String == "" {
|
if c.HTTPProxy.String == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else {
|
} else {
|
||||||
proxyUrl, _ := url.Parse(s.HTTPProxy.String)
|
proxyUrl, _ := url.Parse(c.HTTPProxy.String)
|
||||||
return proxyUrl, nil
|
return proxyUrl, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) GetHostname() string {
|
// DHCPLease represents a DHCP lease
|
||||||
hostname := ToValidHostname(s.config.Hostname.String)
|
type DHCPLease struct {
|
||||||
|
InterfaceName string `json:"interface_name"`
|
||||||
if hostname == "" {
|
IPAddress net.IP `json:"ip_address"`
|
||||||
return s.defaultHostname
|
Netmask net.IP `json:"netmask"`
|
||||||
|
Gateway net.IP `json:"gateway"`
|
||||||
|
DNS []net.IP `json:"dns"`
|
||||||
|
SearchList []string `json:"search_list"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
NTPServers []net.IP `json:"ntp_servers"`
|
||||||
|
LeaseTime time.Time `json:"lease_time"`
|
||||||
|
RenewalTime time.Time `json:"renewal_time"`
|
||||||
|
RebindingTime time.Time `json:"rebinding_time"`
|
||||||
|
ExpiryTime time.Time `json:"expiry_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
return hostname
|
// InterfaceState represents the current state of a network interface
|
||||||
|
type InterfaceState struct {
|
||||||
|
InterfaceName string `json:"interface_name"`
|
||||||
|
MACAddress string `json:"mac_address"`
|
||||||
|
Up bool `json:"up"`
|
||||||
|
Online bool `json:"online"`
|
||||||
|
IPv4Ready bool `json:"ipv4_ready"`
|
||||||
|
IPv6Ready bool `json:"ipv6_ready"`
|
||||||
|
IPv4Address string `json:"ipv4_address,omitempty"`
|
||||||
|
IPv6Address string `json:"ipv6_address,omitempty"`
|
||||||
|
IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
|
||||||
|
IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
|
||||||
|
IPv6Addresses []IPv6Address `json:"ipv6_addresses,omitempty"`
|
||||||
|
NTPServers []net.IP `json:"ntp_servers,omitempty"`
|
||||||
|
DHCPLease4 *DHCPLease `json:"dhcp_lease,omitempty"`
|
||||||
|
DHCPLease6 *DHCPLease `json:"dhcp_lease6,omitempty"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToValidDomain(domain string) string {
|
// NetworkConfig interface for backward compatibility
|
||||||
ascii, err := idna.Lookup.ToASCII(domain)
|
type NetworkConfigInterface interface {
|
||||||
if err != nil {
|
InterfaceName() string
|
||||||
return ""
|
IPv4Addresses() []IPAddress
|
||||||
|
IPv6Addresses() []IPAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
return ascii
|
func (d *DHCPLease) IsIPv6() bool {
|
||||||
}
|
return d.IPAddress.To4() == nil
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) GetDomain() string {
|
|
||||||
domain := ToValidDomain(s.config.Domain.String)
|
|
||||||
|
|
||||||
if domain == "" {
|
|
||||||
lease := s.dhcpClient.GetLease()
|
|
||||||
if lease != nil && lease.Domain != "" {
|
|
||||||
domain = ToValidDomain(lease.Domain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if domain == "" {
|
|
||||||
return "local"
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) GetFQDN() string {
|
|
||||||
return fmt.Sprintf("%s.%s", s.GetHostname(), s.GetDomain())
|
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/beevik/ntp"
|
"github.com/beevik/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultNTPServerIPs = []string{
|
var DefaultNTPServerIPs = []string{
|
||||||
// These servers are known by static IP and as such don't need DNS lookups
|
// These servers are known by static IP and as such don't need DNS lookups
|
||||||
// These are from Google and Cloudflare since if they're down, the internet
|
// These are from Google and Cloudflare since if they're down, the internet
|
||||||
// is broken anyway
|
// is broken anyway
|
||||||
|
|
@ -27,7 +27,7 @@ var defaultNTPServerIPs = []string{
|
||||||
"2001:4860:4806:c::", // time.google.com IPv6
|
"2001:4860:4806:c::", // time.google.com IPv6
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultNTPServerHostnames = []string{
|
var DefaultNTPServerHostnames = []string{
|
||||||
// should use something from https://github.com/jauderho/public-ntp-servers
|
// should use something from https://github.com/jauderho/public-ntp-servers
|
||||||
"time.apple.com",
|
"time.apple.com",
|
||||||
"time.aws.com",
|
"time.aws.com",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/network"
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ type TimeSync struct {
|
||||||
syncLock *sync.Mutex
|
syncLock *sync.Mutex
|
||||||
l *zerolog.Logger
|
l *zerolog.Logger
|
||||||
|
|
||||||
networkConfig *network.NetworkConfig
|
networkConfig *types.NetworkConfig
|
||||||
dhcpNtpAddresses []string
|
dhcpNtpAddresses []string
|
||||||
|
|
||||||
rtcDevicePath string
|
rtcDevicePath string
|
||||||
|
|
@ -43,7 +43,7 @@ type TimeSync struct {
|
||||||
type TimeSyncOptions struct {
|
type TimeSyncOptions struct {
|
||||||
PreCheckFunc func() (bool, error)
|
PreCheckFunc func() (bool, error)
|
||||||
Logger *zerolog.Logger
|
Logger *zerolog.Logger
|
||||||
NetworkConfig *network.NetworkConfig
|
NetworkConfig *types.NetworkConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncMode struct {
|
type SyncMode struct {
|
||||||
|
|
@ -188,10 +188,10 @@ Orders:
|
||||||
case "ntp":
|
case "ntp":
|
||||||
if syncMode.Ntp && syncMode.NtpUseFallback {
|
if syncMode.Ntp && syncMode.NtpUseFallback {
|
||||||
log.Info().Msg("using NTP fallback IPs")
|
log.Info().Msg("using NTP fallback IPs")
|
||||||
now, offset = t.queryNetworkTime(defaultNTPServerIPs)
|
now, offset = t.queryNetworkTime(DefaultNTPServerIPs)
|
||||||
if now == nil {
|
if now == nil {
|
||||||
log.Info().Msg("using NTP fallback hostnames")
|
log.Info().Msg("using NTP fallback hostnames")
|
||||||
now, offset = t.queryNetworkTime(defaultNTPServerHostnames)
|
now, offset = t.queryNetworkTime(DefaultNTPServerHostnames)
|
||||||
}
|
}
|
||||||
if now != nil {
|
if now != nil {
|
||||||
break Orders
|
break Orders
|
||||||
|
|
|
||||||
10
mdns.go
10
mdns.go
|
|
@ -10,10 +10,14 @@ func initMdns() error {
|
||||||
m, err := mdns.NewMDNS(&mdns.MDNSOptions{
|
m, err := mdns.NewMDNS(&mdns.MDNSOptions{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
LocalNames: []string{
|
LocalNames: []string{
|
||||||
networkState.GetHostname(),
|
"jetkvm", "jetkvm.local",
|
||||||
networkState.GetFQDN(),
|
// networkManager.GetHostname(),
|
||||||
|
// networkManager.GetFQDN(),
|
||||||
|
},
|
||||||
|
ListenOptions: &mdns.MDNSListenOptions{
|
||||||
|
IPv4: config.NetworkConfig.MDNSMode.String != "disabled",
|
||||||
|
IPv6: config.NetworkConfig.MDNSMode.String != "disabled",
|
||||||
},
|
},
|
||||||
ListenOptions: config.NetworkConfig.GetMDNSMode(),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
126
network.go
126
network.go
|
|
@ -1,10 +1,11 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/network"
|
"github.com/jetkvm/kvm/internal/mdns"
|
||||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
"github.com/jetkvm/kvm/pkg/nmlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -12,16 +13,45 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
networkState *network.NetworkInterfaceState
|
networkManager *nmlite.NetworkManager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RpcNetworkSettings struct {
|
||||||
|
types.NetworkConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RpcNetworkSettings) ToNetworkConfig() *types.NetworkConfig {
|
||||||
|
return &s.NetworkConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRpcNetworkSettings(config *types.NetworkConfig) *RpcNetworkSettings {
|
||||||
|
return &RpcNetworkSettings{
|
||||||
|
NetworkConfig: *config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartMdns() {
|
||||||
|
if mDNS == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = mDNS.SetListenOptions(&mdns.MDNSListenOptions{
|
||||||
|
IPv4: config.NetworkConfig.MDNSMode.String != "disabled",
|
||||||
|
IPv6: config.NetworkConfig.MDNSMode.String != "disabled",
|
||||||
|
})
|
||||||
|
_ = mDNS.SetLocalNames([]string{
|
||||||
|
networkManager.GetHostname(),
|
||||||
|
networkManager.GetFQDN(),
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
func networkStateChanged(isOnline bool) {
|
func networkStateChanged(isOnline bool) {
|
||||||
// do not block the main thread
|
// do not block the main thread
|
||||||
go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed")
|
go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed")
|
||||||
|
|
||||||
if timeSync != nil {
|
if timeSync != nil {
|
||||||
if networkState != nil {
|
if networkManager != nil {
|
||||||
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
|
timeSync.SetDhcpNtpAddresses(networkManager.NTPServerStrings())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := timeSync.Sync(); err != nil {
|
if err := timeSync.Sync(); err != nil {
|
||||||
|
|
@ -31,11 +61,7 @@ func networkStateChanged(isOnline bool) {
|
||||||
|
|
||||||
// always restart mDNS when the network state changes
|
// always restart mDNS when the network state changes
|
||||||
if mDNS != nil {
|
if mDNS != nil {
|
||||||
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
|
restartMdns()
|
||||||
_ = mDNS.SetLocalNames([]string{
|
|
||||||
networkState.GetHostname(),
|
|
||||||
networkState.GetFQDN(),
|
|
||||||
}, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the network is now online, trigger an NTP sync if still needed
|
// if the network is now online, trigger an NTP sync if still needed
|
||||||
|
|
@ -49,77 +75,47 @@ func networkStateChanged(isOnline bool) {
|
||||||
func initNetwork() error {
|
func initNetwork() error {
|
||||||
ensureConfigLoaded()
|
ensureConfigLoaded()
|
||||||
|
|
||||||
state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{
|
networkManager = nmlite.NewNetworkManager(context.Background(), networkLogger)
|
||||||
DefaultHostname: GetDefaultHostname(),
|
networkManager.AddInterface(NetIfName, config.NetworkConfig)
|
||||||
InterfaceName: NetIfName,
|
|
||||||
NetworkConfig: config.NetworkConfig,
|
|
||||||
Logger: networkLogger,
|
|
||||||
OnStateChange: func(state *network.NetworkInterfaceState) {
|
|
||||||
networkStateChanged(state.IsOnline())
|
|
||||||
},
|
|
||||||
OnInitialCheck: func(state *network.NetworkInterfaceState) {
|
|
||||||
networkStateChanged(state.IsOnline())
|
|
||||||
},
|
|
||||||
OnDhcpLeaseChange: func(lease *udhcpc.Lease, state *network.NetworkInterfaceState) {
|
|
||||||
networkStateChanged(state.IsOnline())
|
|
||||||
|
|
||||||
if currentSession == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSONRPCEvent("networkState", networkState.RpcGetNetworkState(), currentSession)
|
|
||||||
},
|
|
||||||
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
|
||||||
config.NetworkConfig = networkConfig
|
|
||||||
networkStateChanged(false)
|
|
||||||
|
|
||||||
if mDNS != nil {
|
|
||||||
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
|
|
||||||
_ = mDNS.SetLocalNames([]string{
|
|
||||||
networkState.GetHostname(),
|
|
||||||
networkState.GetFQDN(),
|
|
||||||
}, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if state == nil {
|
|
||||||
if err == nil {
|
|
||||||
return fmt.Errorf("failed to create NetworkInterfaceState")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := state.Run(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
networkState = state
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetNetworkState() network.RpcNetworkState {
|
func rpcGetNetworkState() *types.InterfaceState {
|
||||||
return networkState.RpcGetNetworkState()
|
state, _ := networkManager.GetInterfaceState(NetIfName)
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetNetworkSettings() network.RpcNetworkSettings {
|
func rpcGetNetworkSettings() *RpcNetworkSettings {
|
||||||
return networkState.RpcGetNetworkSettings()
|
return toRpcNetworkSettings(config.NetworkConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNetworkSettings, error) {
|
func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, error) {
|
||||||
s := networkState.RpcSetNetworkSettings(settings)
|
netConfig := settings.ToNetworkConfig()
|
||||||
|
|
||||||
|
networkLogger.Debug().Interface("newConfig", netConfig).Interface("config", settings).Msg("setting new config")
|
||||||
|
|
||||||
|
s := networkManager.SetInterfaceConfig(NetIfName, netConfig)
|
||||||
if s != nil {
|
if s != nil {
|
||||||
return nil, s
|
return nil, s
|
||||||
}
|
}
|
||||||
|
networkLogger.Debug().Interface("newConfig", netConfig).Interface("config", settings).Msg("new config")
|
||||||
|
|
||||||
|
newConfig, err := networkManager.GetInterfaceConfig(NetIfName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.NetworkConfig = newConfig
|
||||||
|
|
||||||
|
networkLogger.Debug().Interface("newConfig", newConfig).Interface("config", settings).Msg("saving config")
|
||||||
|
|
||||||
if err := SaveConfig(); err != nil {
|
if err := SaveConfig(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &network.RpcNetworkSettings{NetworkConfig: *config.NetworkConfig}, nil
|
return toRpcNetworkSettings(newConfig), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcRenewDHCPLease() error {
|
func rpcRenewDHCPLease() error {
|
||||||
return networkState.RpcRenewDHCPLease()
|
return networkManager.RenewDHCPLease(NetIfName)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
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/pkg/nmlite/link"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
VendorIdentifier = "jetkvm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
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
|
||||||
|
|
||||||
|
UpdateResolvConf func([]string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
ifaces []string
|
||||||
|
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
|
||||||
|
stateDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new DHCP client for the given interface.
|
||||||
|
func NewClient(ctx context.Context, ifaces []string, 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,
|
||||||
|
stateDir: "/run/jetkvm-dhcp",
|
||||||
|
|
||||||
|
currentLease4: nil,
|
||||||
|
currentLease6: nil,
|
||||||
|
|
||||||
|
lease4Mu: sync.Mutex{},
|
||||||
|
lease6Mu: sync.Mutex{},
|
||||||
|
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
cfgMu: sync.Mutex{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ensureInterfaceUp(ifname string) (*link.Link, error) {
|
||||||
|
nlm := link.GetNetlinkManager()
|
||||||
|
iface, err := nlm.GetLinkByName(ifname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nlm.EnsureInterfaceUpWithTimeout(c.ctx, iface, c.cfg.LinkUpTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(ifname string) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
l := c.l.With().Str("interface", ifname).Logger()
|
||||||
|
|
||||||
|
iface, err := c.ensureInterfaceUp(ifname)
|
||||||
|
if err != nil {
|
||||||
|
l.Error().Err(err).Msg("Could not bring up interface")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipv4 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(iface *link.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 *link.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(time.Duration(lease.RenewalTime)*time.Second),
|
||||||
|
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 currentIPv4 == ipv4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ipv4 {
|
||||||
|
c.lease4Mu.Lock()
|
||||||
|
c.currentLease4 = nil
|
||||||
|
c.lease4Mu.Unlock()
|
||||||
|
c.scheduler.RemoveByTags("ipv4")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sendRequests(ipv4, c.cfg.IPv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if c.cfg.UpdateResolvConf == nil {
|
||||||
|
c.l.Warn().Msg("no UpdateResolvConf function set, skipping resolv.conf update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.l.Info().
|
||||||
|
Str("interface", iface).
|
||||||
|
Interface("nameservers", nameservers).
|
||||||
|
Interface("searchList", searchList).
|
||||||
|
Str("domain", domain).
|
||||||
|
Msg("updating resolv.conf")
|
||||||
|
|
||||||
|
// Convert net.IP to string slice
|
||||||
|
var nameserverStrings []string
|
||||||
|
for _, ns := range nameservers {
|
||||||
|
nameserverStrings = append(nameserverStrings, ns.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.cfg.UpdateResolvConf(nameserverStrings); 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,131 @@
|
||||||
|
package dhclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, 10) // AF_INET6
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if addr.IP.IsLinkLocalUnicast() && (addr.Flags&0x40 == 0) { // IFA_F_TENTATIVE
|
||||||
|
if addr.Flags&0x80 != 0 { // IFA_F_DADFAILED
|
||||||
|
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, 10) // AF_INET6
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
package dhclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"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
|
||||||
|
Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask
|
||||||
|
Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network
|
||||||
|
TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network
|
||||||
|
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
|
||||||
|
Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC
|
||||||
|
Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers
|
||||||
|
DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers
|
||||||
|
NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers
|
||||||
|
LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers
|
||||||
|
TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete)
|
||||||
|
IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete)
|
||||||
|
LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete)
|
||||||
|
CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete)
|
||||||
|
WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers
|
||||||
|
SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server
|
||||||
|
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 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var uptime time.Duration
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
parts := strings.Split(text, " ")
|
||||||
|
uptime, err = time.ParseDuration(parts[0] + "s")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime
|
||||||
|
leaseExpiry := time.Now().Add(relativeLeaseRemaining)
|
||||||
|
|
||||||
|
l.LeaseExpiry = &leaseExpiry
|
||||||
|
|
||||||
|
return leaseExpiry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lease) Apply() error {
|
||||||
|
if l.p4 != nil {
|
||||||
|
return l.applyIPv4()
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.p6 != nil {
|
||||||
|
return l.applyIPv6()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lease) applyIPv4() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lease) applyIPv6() error {
|
||||||
|
return 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)
|
||||||
|
for _, line := range strings.Split(str, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
// skip empty lines and comments
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
data[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// now iterate over the lease struct and set the values
|
||||||
|
leaseType := reflect.TypeOf(lease).Elem()
|
||||||
|
leaseValue := reflect.ValueOf(lease).Elem()
|
||||||
|
|
||||||
|
valuesParsed := make(map[string]bool)
|
||||||
|
|
||||||
|
for i := 0; i < leaseType.NumField(); i++ {
|
||||||
|
field := leaseValue.Field(i)
|
||||||
|
|
||||||
|
// get the env tag
|
||||||
|
key := leaseType.Field(i).Tag.Get("env")
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesParsed[key] = false
|
||||||
|
|
||||||
|
// get the value from the data map
|
||||||
|
value, ok := data[key]
|
||||||
|
if !ok || value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Interface().(type) {
|
||||||
|
case string:
|
||||||
|
field.SetString(value)
|
||||||
|
case int:
|
||||||
|
val, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
field.SetInt(int64(val))
|
||||||
|
case time.Duration:
|
||||||
|
val, err := time.ParseDuration(value + "s")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
field.Set(reflect.ValueOf(val))
|
||||||
|
case net.IP:
|
||||||
|
ip := net.ParseIP(value)
|
||||||
|
if ip == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
field.Set(reflect.ValueOf(ip))
|
||||||
|
case []net.IP:
|
||||||
|
val := make([]net.IP, 0)
|
||||||
|
for _, ipStr := range strings.Fields(value) {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val = append(val, ip)
|
||||||
|
}
|
||||||
|
field.Set(reflect.ValueOf(val))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesParsed[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
lease.setIsEmpty(valuesParsed)
|
||||||
|
|
||||||
|
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,248 @@
|
||||||
|
// Package nmlite provides DHCP state persistence for the network manager.
|
||||||
|
package dhclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultStateDir is the default state directory
|
||||||
|
DefaultStateDir = "/var/run/"
|
||||||
|
// DHCPStateFile is the name of the DHCP state file
|
||||||
|
DHCPStateFile = "jetkvm_dhcp_state.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DHCPState represents the persistent state of DHCP clients
|
||||||
|
type DHCPState struct {
|
||||||
|
InterfaceStates map[string]*InterfaceDHCPState `json:"interface_states"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceDHCPState represents the DHCP state for a specific interface
|
||||||
|
type InterfaceDHCPState struct {
|
||||||
|
InterfaceName string `json:"interface_name"`
|
||||||
|
IPv4Enabled bool `json:"ipv4_enabled"`
|
||||||
|
IPv6Enabled bool `json:"ipv6_enabled"`
|
||||||
|
IPv4Lease *Lease `json:"ipv4_lease,omitempty"`
|
||||||
|
IPv6Lease *Lease `json:"ipv6_lease,omitempty"`
|
||||||
|
LastRenewal time.Time `json:"last_renewal"`
|
||||||
|
Config *types.NetworkConfig `json:"config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveState saves the current DHCP state to disk
|
||||||
|
func (c *Client) SaveState(state *DHCPState) error {
|
||||||
|
if state == nil {
|
||||||
|
return fmt.Errorf("state cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return error if state directory doesn't exist
|
||||||
|
if _, err := os.Stat(c.stateDir); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("state directory does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update timestamp
|
||||||
|
state.LastUpdated = time.Now()
|
||||||
|
state.Version = "1.0"
|
||||||
|
|
||||||
|
// Serialize state
|
||||||
|
data, err := json.MarshalIndent(state, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to temporary file first, then rename to ensure atomic operation
|
||||||
|
tmpFile, err := os.CreateTemp(c.stateDir, DHCPStateFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temporary file: %w", err)
|
||||||
|
}
|
||||||
|
defer tmpFile.Close()
|
||||||
|
|
||||||
|
if err := os.WriteFile(tmpFile.Name(), data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write state file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateFile := filepath.Join(c.stateDir, DHCPStateFile)
|
||||||
|
if err := os.Rename(tmpFile.Name(), stateFile); err != nil {
|
||||||
|
os.Remove(tmpFile.Name())
|
||||||
|
return fmt.Errorf("failed to rename state file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.l.Debug().Str("file", stateFile).Msg("DHCP state saved")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadState loads the DHCP state from disk
|
||||||
|
func (c *Client) LoadState() (*DHCPState, error) {
|
||||||
|
stateFile := filepath.Join(c.stateDir, DHCPStateFile)
|
||||||
|
|
||||||
|
// Check if state file exists
|
||||||
|
if _, err := os.Stat(stateFile); os.IsNotExist(err) {
|
||||||
|
c.l.Debug().Msg("No existing DHCP state file found")
|
||||||
|
return &DHCPState{
|
||||||
|
InterfaceStates: make(map[string]*InterfaceDHCPState),
|
||||||
|
LastUpdated: time.Now(),
|
||||||
|
Version: "1.0",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read state file
|
||||||
|
data, err := os.ReadFile(stateFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read state file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize state
|
||||||
|
var state DHCPState
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize interface states map if nil
|
||||||
|
if state.InterfaceStates == nil {
|
||||||
|
state.InterfaceStates = make(map[string]*InterfaceDHCPState)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.l.Debug().Str("file", stateFile).Msg("DHCP state loaded")
|
||||||
|
return &state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInterfaceState updates the state for a specific interface
|
||||||
|
func (c *Client) UpdateInterfaceState(ifaceName string, state *InterfaceDHCPState) error {
|
||||||
|
// Load current state
|
||||||
|
currentState, err := c.LoadState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load current state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update interface state
|
||||||
|
currentState.InterfaceStates[ifaceName] = state
|
||||||
|
|
||||||
|
// Save updated state
|
||||||
|
return c.SaveState(currentState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterfaceState gets the state for a specific interface
|
||||||
|
func (c *Client) GetInterfaceState(ifaceName string) (*InterfaceDHCPState, error) {
|
||||||
|
state, err := c.LoadState()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.InterfaceStates[ifaceName], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveInterfaceState removes the state for a specific interface
|
||||||
|
func (c *Client) RemoveInterfaceState(ifaceName string) error {
|
||||||
|
// Load current state
|
||||||
|
currentState, err := c.LoadState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load current state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove interface state
|
||||||
|
delete(currentState.InterfaceStates, ifaceName)
|
||||||
|
|
||||||
|
// Save updated state
|
||||||
|
return c.SaveState(currentState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLeaseValid checks if a DHCP lease is still valid
|
||||||
|
func (c *Client) IsLeaseValid(lease *Lease) bool {
|
||||||
|
if lease == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if lease has expired
|
||||||
|
if lease.LeaseExpiry == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now().Before(*lease.LeaseExpiry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldRenewLease checks if a lease should be renewed
|
||||||
|
func (c *Client) ShouldRenewLease(lease *Lease) bool {
|
||||||
|
if !c.IsLeaseValid(lease) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
expiry := *lease.LeaseExpiry
|
||||||
|
leaseTime := time.Now().Add(time.Duration(lease.LeaseTime) * time.Second)
|
||||||
|
|
||||||
|
// Renew if lease expires within 50% of its lifetime
|
||||||
|
leaseDuration := expiry.Sub(leaseTime)
|
||||||
|
renewalTime := leaseTime.Add(leaseDuration / 2)
|
||||||
|
|
||||||
|
return time.Now().After(renewalTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupExpiredStates removes expired states from the state file
|
||||||
|
func (c *Client) CleanupExpiredStates() error {
|
||||||
|
state, err := c.LoadState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned := false
|
||||||
|
for ifaceName, ifaceState := range state.InterfaceStates {
|
||||||
|
// Remove interface state if both leases are expired
|
||||||
|
ipv4Valid := c.IsLeaseValid(ifaceState.IPv4Lease)
|
||||||
|
ipv6Valid := c.IsLeaseValid(ifaceState.IPv6Lease)
|
||||||
|
|
||||||
|
if !ipv4Valid && !ipv6Valid {
|
||||||
|
delete(state.InterfaceStates, ifaceName)
|
||||||
|
cleaned = true
|
||||||
|
c.l.Debug().Str("interface", ifaceName).Msg("Removed expired DHCP state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cleaned {
|
||||||
|
return c.SaveState(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStateSummary returns a summary of the current state
|
||||||
|
func (c *Client) GetStateSummary() (map[string]interface{}, error) {
|
||||||
|
state, err := c.LoadState()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := map[string]interface{}{
|
||||||
|
"last_updated": state.LastUpdated,
|
||||||
|
"version": state.Version,
|
||||||
|
"interface_count": len(state.InterfaceStates),
|
||||||
|
"interfaces": make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces := summary["interfaces"].(map[string]interface{})
|
||||||
|
for ifaceName, ifaceState := range state.InterfaceStates {
|
||||||
|
interfaceInfo := map[string]interface{}{
|
||||||
|
"ipv4_enabled": ifaceState.IPv4Enabled,
|
||||||
|
"ipv6_enabled": ifaceState.IPv6Enabled,
|
||||||
|
"last_renewal": ifaceState.LastRenewal,
|
||||||
|
// "ipv4_lease_valid": c.IsLeaseValid(ifaceState.IPv4Lease.(*Lease)),
|
||||||
|
// "ipv6_lease_valid": c.IsLeaseValid(ifaceState.IPv6Lease),
|
||||||
|
}
|
||||||
|
|
||||||
|
if ifaceState.IPv4Lease != nil {
|
||||||
|
interfaceInfo["ipv4_lease_expiry"] = ifaceState.IPv4Lease.LeaseExpiry
|
||||||
|
}
|
||||||
|
if ifaceState.IPv6Lease != nil {
|
||||||
|
interfaceInfo["ipv6_lease_expiry"] = ifaceState.IPv6Lease.LeaseExpiry
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces[ifaceName] = interfaceInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary, nil
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
// Package nmlite provides DHCP client functionality for the network manager.
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
"github.com/jetkvm/kvm/pkg/nmlite/dhclient"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DHCPClient wraps the dhclient package for use in the network manager
|
||||||
|
type DHCPClient struct {
|
||||||
|
ctx context.Context
|
||||||
|
ifaceName string
|
||||||
|
logger *zerolog.Logger
|
||||||
|
client *dhclient.Client
|
||||||
|
link netlink.Link
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
ipv4Enabled bool
|
||||||
|
ipv6Enabled bool
|
||||||
|
|
||||||
|
// State management
|
||||||
|
// stateManager *DHCPStateManager
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
onLeaseChange func(lease *types.DHCPLease)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDHCPClient creates a new DHCP client
|
||||||
|
func NewDHCPClient(ctx context.Context, ifaceName string, logger *zerolog.Logger) (*DHCPClient, error) {
|
||||||
|
if ifaceName == "" {
|
||||||
|
return nil, fmt.Errorf("interface name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger == nil {
|
||||||
|
return nil, fmt.Errorf("logger cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create state manager
|
||||||
|
// stateManager := NewDHCPStateManager("", logger)
|
||||||
|
|
||||||
|
return &DHCPClient{
|
||||||
|
ctx: ctx,
|
||||||
|
ifaceName: ifaceName,
|
||||||
|
logger: logger,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIPv4 enables or disables IPv4 DHCP
|
||||||
|
func (dc *DHCPClient) SetIPv4(enabled bool) {
|
||||||
|
dc.ipv4Enabled = enabled
|
||||||
|
if dc.client != nil {
|
||||||
|
dc.client.SetIPv4(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIPv6 enables or disables IPv6 DHCP
|
||||||
|
func (dc *DHCPClient) SetIPv6(enabled bool) {
|
||||||
|
dc.ipv6Enabled = enabled
|
||||||
|
if dc.client != nil {
|
||||||
|
dc.client.SetIPv6(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnLeaseChange sets the callback for lease changes
|
||||||
|
func (dc *DHCPClient) SetOnLeaseChange(callback func(lease *types.DHCPLease)) {
|
||||||
|
dc.onLeaseChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the DHCP client
|
||||||
|
func (dc *DHCPClient) Start() error {
|
||||||
|
if dc.client != nil {
|
||||||
|
dc.logger.Warn().Msg("DHCP client already started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.logger.Info().Msg("starting DHCP client")
|
||||||
|
|
||||||
|
// Create the underlying DHCP client
|
||||||
|
client, err := dhclient.NewClient(dc.ctx, []string{dc.ifaceName}, &dhclient.Config{
|
||||||
|
IPv4: dc.ipv4Enabled,
|
||||||
|
IPv6: dc.ipv6Enabled,
|
||||||
|
OnLease4Change: func(lease *dhclient.Lease) {
|
||||||
|
dc.handleLeaseChange(lease, false)
|
||||||
|
},
|
||||||
|
OnLease6Change: func(lease *dhclient.Lease) {
|
||||||
|
dc.handleLeaseChange(lease, true)
|
||||||
|
},
|
||||||
|
UpdateResolvConf: func(nameservers []string) error {
|
||||||
|
// This will be handled by the resolv.conf manager
|
||||||
|
dc.logger.Debug().
|
||||||
|
Interface("nameservers", nameservers).
|
||||||
|
Msg("DHCP client requested resolv.conf update")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}, dc.logger)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create DHCP client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.client = client
|
||||||
|
|
||||||
|
// Start the client
|
||||||
|
if err := dc.client.Start(); err != nil {
|
||||||
|
dc.client = nil
|
||||||
|
return fmt.Errorf("failed to start DHCP client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.logger.Info().Msg("DHCP client started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the DHCP client
|
||||||
|
func (dc *DHCPClient) Stop() error {
|
||||||
|
if dc.client == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.logger.Info().Msg("stopping DHCP client")
|
||||||
|
|
||||||
|
dc.client = nil
|
||||||
|
dc.logger.Info().Msg("DHCP client stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renew renews the DHCP lease
|
||||||
|
func (dc *DHCPClient) Renew() error {
|
||||||
|
if dc.client == nil {
|
||||||
|
return fmt.Errorf("DHCP client not started")
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.logger.Info().Msg("renewing DHCP lease")
|
||||||
|
dc.client.Renew()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release releases the DHCP lease
|
||||||
|
func (dc *DHCPClient) Release() error {
|
||||||
|
if dc.client == nil {
|
||||||
|
return fmt.Errorf("DHCP client not started")
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.logger.Info().Msg("releasing DHCP lease")
|
||||||
|
dc.client.Release()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLease4 returns the current IPv4 lease
|
||||||
|
func (dc *DHCPClient) GetLease4() *types.DHCPLease {
|
||||||
|
if dc.client == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lease := dc.client.Lease4()
|
||||||
|
if lease == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return dc.convertLease(lease, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLease6 returns the current IPv6 lease
|
||||||
|
func (dc *DHCPClient) GetLease6() *types.DHCPLease {
|
||||||
|
if dc.client == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lease := dc.client.Lease6()
|
||||||
|
if lease == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return dc.convertLease(lease, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLeaseChange handles lease changes from the underlying DHCP client
|
||||||
|
func (dc *DHCPClient) handleLeaseChange(lease *dhclient.Lease, isIPv6 bool) {
|
||||||
|
if lease == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedLease := dc.convertLease(lease, isIPv6)
|
||||||
|
if convertedLease == nil {
|
||||||
|
dc.logger.Error().Msg("failed to convert lease")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.logger.Info().
|
||||||
|
Bool("ipv6", isIPv6).
|
||||||
|
Str("ip", convertedLease.IPAddress.String()).
|
||||||
|
Msg("DHCP lease changed")
|
||||||
|
|
||||||
|
// Notify callback
|
||||||
|
if dc.onLeaseChange != nil {
|
||||||
|
dc.onLeaseChange(convertedLease)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertLease converts a dhclient.Lease to types.DHCPLease
|
||||||
|
func (dc *DHCPClient) convertLease(lease *dhclient.Lease, isIPv6 bool) *types.DHCPLease {
|
||||||
|
if lease == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the lease
|
||||||
|
convertedLease := &types.DHCPLease{
|
||||||
|
InterfaceName: dc.ifaceName,
|
||||||
|
Domain: lease.Domain,
|
||||||
|
SearchList: lease.SearchList,
|
||||||
|
NTPServers: lease.NTPServers,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set IP address and related information
|
||||||
|
convertedLease.IPAddress = lease.IPAddress
|
||||||
|
convertedLease.Netmask = lease.Netmask
|
||||||
|
if len(lease.Routers) > 0 {
|
||||||
|
convertedLease.Gateway = lease.Routers[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set DNS servers
|
||||||
|
convertedLease.DNS = lease.DNS
|
||||||
|
// convertedLease.LeaseTime = lease.LeaseTime
|
||||||
|
// convertedLease.RenewalTime = lease.RenewalTime
|
||||||
|
// convertedLease.RebindingTime = lease.RebindingTime
|
||||||
|
|
||||||
|
return convertedLease
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hostnamePath = "/etc/hostname"
|
||||||
|
hostsPath = "/etc/hosts"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hostnameLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// HostnameManager manages system hostname and /etc/hosts
|
||||||
|
type HostnameManager struct {
|
||||||
|
logger *zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHostnameManager creates a new hostname manager
|
||||||
|
func NewHostnameManager(logger *zerolog.Logger) *HostnameManager {
|
||||||
|
if logger == nil {
|
||||||
|
// Create a no-op logger if none provided
|
||||||
|
logger = &zerolog.Logger{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HostnameManager{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHostname sets the system hostname and updates /etc/hosts
|
||||||
|
func (hm *HostnameManager) SetHostname(hostname, fqdn string) error {
|
||||||
|
hostnameLock.Lock()
|
||||||
|
defer hostnameLock.Unlock()
|
||||||
|
|
||||||
|
hostname = ToValidHostname(strings.TrimSpace(hostname))
|
||||||
|
fqdn = ToValidHostname(strings.TrimSpace(fqdn))
|
||||||
|
|
||||||
|
if hostname == "" {
|
||||||
|
return fmt.Errorf("invalid hostname: %s", hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fqdn == "" {
|
||||||
|
fqdn = hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
hm.logger.Info().
|
||||||
|
Str("hostname", hostname).
|
||||||
|
Str("fqdn", fqdn).
|
||||||
|
Msg("setting hostname")
|
||||||
|
|
||||||
|
// Update /etc/hostname
|
||||||
|
if err := hm.updateEtcHostname(hostname); err != nil {
|
||||||
|
return fmt.Errorf("failed to update /etc/hostname: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update /etc/hosts
|
||||||
|
if err := hm.updateEtcHosts(hostname, fqdn); err != nil {
|
||||||
|
return fmt.Errorf("failed to update /etc/hosts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the hostname using hostname command
|
||||||
|
if err := hm.setSystemHostname(hostname); err != nil {
|
||||||
|
return fmt.Errorf("failed to set system hostname: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hm.logger.Info().
|
||||||
|
Str("hostname", hostname).
|
||||||
|
Str("fqdn", fqdn).
|
||||||
|
Msg("hostname set successfully")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentHostname returns the current system hostname
|
||||||
|
func (hm *HostnameManager) GetCurrentHostname() (string, error) {
|
||||||
|
return os.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentFQDN returns the current FQDN
|
||||||
|
func (hm *HostnameManager) GetCurrentFQDN() (string, error) {
|
||||||
|
hostname, err := hm.GetCurrentHostname()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the FQDN from /etc/hosts
|
||||||
|
return hm.getFQDNFromHosts(hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateEtcHostname updates the /etc/hostname file
|
||||||
|
func (hm *HostnameManager) updateEtcHostname(hostname string) error {
|
||||||
|
if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", hostnamePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hm.logger.Debug().Str("file", hostnamePath).Str("hostname", hostname).Msg("updated /etc/hostname")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateEtcHosts updates the /etc/hosts file
|
||||||
|
func (hm *HostnameManager) updateEtcHosts(hostname, fqdn string) error {
|
||||||
|
// Open /etc/hosts for reading and writing
|
||||||
|
hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open %s: %w", hostsPath, err)
|
||||||
|
}
|
||||||
|
defer hostsFile.Close()
|
||||||
|
|
||||||
|
// Read all lines
|
||||||
|
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, err := io.ReadAll(hostsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read %s: %w", hostsPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process lines
|
||||||
|
newLines := []string{}
|
||||||
|
hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn)
|
||||||
|
hostLineExists := false
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(lines), "\n") {
|
||||||
|
if strings.HasPrefix(line, "127.0.1.1") {
|
||||||
|
hostLineExists = true
|
||||||
|
line = hostLine
|
||||||
|
}
|
||||||
|
newLines = append(newLines, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add host line if it doesn't exist
|
||||||
|
if !hostLineExists {
|
||||||
|
newLines = append(newLines, hostLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
if err := hostsFile.Truncate(0); err != nil {
|
||||||
|
return fmt.Errorf("failed to truncate %s: %w", hostsPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := hostsFile.Write([]byte(strings.Join(newLines, "\n"))); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", hostsPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hm.logger.Debug().
|
||||||
|
Str("file", hostsPath).
|
||||||
|
Str("hostname", hostname).
|
||||||
|
Str("fqdn", fqdn).
|
||||||
|
Msg("updated /etc/hosts")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSystemHostname sets the system hostname using the hostname command
|
||||||
|
func (hm *HostnameManager) setSystemHostname(hostname string) error {
|
||||||
|
cmd := exec.Command("hostname", "-F", hostnamePath)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to run hostname command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hm.logger.Debug().Str("hostname", hostname).Msg("set system hostname")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFQDNFromHosts tries to get the FQDN from /etc/hosts
|
||||||
|
func (hm *HostnameManager) getFQDNFromHosts(hostname string) (string, error) {
|
||||||
|
content, err := os.ReadFile(hostsPath)
|
||||||
|
if err != nil {
|
||||||
|
return hostname, nil // Return hostname as fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "127.0.1.1") {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
// The second part should be the FQDN
|
||||||
|
return parts[1], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostname, nil // Return hostname as fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToValidHostname converts a hostname to a valid format
|
||||||
|
func ToValidHostname(hostname string) string {
|
||||||
|
ascii, err := idna.Lookup.ToASCII(hostname)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ascii
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateHostname validates a hostname
|
||||||
|
func ValidateHostname(hostname string) error {
|
||||||
|
if hostname == "" {
|
||||||
|
return fmt.Errorf("hostname cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
validHostname := ToValidHostname(hostname)
|
||||||
|
if validHostname != hostname {
|
||||||
|
return fmt.Errorf("hostname contains invalid characters: %s", hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hostname) > 253 {
|
||||||
|
return fmt.Errorf("hostname too long: %d characters (max 253)", len(hostname))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid characters (alphanumeric, hyphens, dots)
|
||||||
|
for _, char := range hostname {
|
||||||
|
if !((char >= 'a' && char <= 'z') ||
|
||||||
|
(char >= 'A' && char <= 'Z') ||
|
||||||
|
(char >= '0' && char <= '9') ||
|
||||||
|
char == '-' || char == '.') {
|
||||||
|
return fmt.Errorf("hostname contains invalid character: %c", char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that it doesn't start or end with hyphen
|
||||||
|
if strings.HasPrefix(hostname, "-") || strings.HasSuffix(hostname, "-") {
|
||||||
|
return fmt.Errorf("hostname cannot start or end with hyphen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that it doesn't start or end with dot
|
||||||
|
if strings.HasPrefix(hostname, ".") || strings.HasSuffix(hostname, ".") {
|
||||||
|
return fmt.Errorf("hostname cannot start or end with dot")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,624 @@
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/confparser"
|
||||||
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InterfaceManager manages a single network interface
|
||||||
|
type InterfaceManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
ifaceName string
|
||||||
|
config *types.NetworkConfig
|
||||||
|
logger *zerolog.Logger
|
||||||
|
state *types.InterfaceState
|
||||||
|
stateMu sync.RWMutex
|
||||||
|
|
||||||
|
// Network components
|
||||||
|
staticConfig *StaticConfigManager
|
||||||
|
dhcpClient *DHCPClient
|
||||||
|
resolvConf *ResolvConfManager
|
||||||
|
hostname *HostnameManager
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
onStateChange func(state *types.InterfaceState)
|
||||||
|
onConfigChange func(config *types.NetworkConfig)
|
||||||
|
onDHCPLeaseChange func(lease *types.DHCPLease)
|
||||||
|
|
||||||
|
// Control
|
||||||
|
stopCh chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInterfaceManager creates a new interface manager
|
||||||
|
func NewInterfaceManager(ctx context.Context, ifaceName string, config *types.NetworkConfig, logger *zerolog.Logger) (*InterfaceManager, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, fmt.Errorf("config cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger == nil {
|
||||||
|
logger = logging.GetSubsystemLogger("interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedLogger := logger.With().Str("interface", ifaceName).Logger()
|
||||||
|
|
||||||
|
// Validate and set defaults
|
||||||
|
if err := confparser.SetDefaultsAndValidate(config); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
im := &InterfaceManager{
|
||||||
|
ctx: ctx,
|
||||||
|
ifaceName: ifaceName,
|
||||||
|
config: config,
|
||||||
|
logger: &scopedLogger,
|
||||||
|
state: &types.InterfaceState{
|
||||||
|
InterfaceName: ifaceName,
|
||||||
|
// LastUpdated: time.Now(),
|
||||||
|
},
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize components
|
||||||
|
var err error
|
||||||
|
im.staticConfig, err = NewStaticConfigManager(ifaceName, &scopedLogger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create static config manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the dhcp client
|
||||||
|
im.dhcpClient, err = NewDHCPClient(ctx, ifaceName, &scopedLogger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create DHCP client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
im.resolvConf = NewResolvConfManager(&scopedLogger)
|
||||||
|
im.hostname = NewHostnameManager(&scopedLogger)
|
||||||
|
|
||||||
|
// Set up DHCP client callbacks
|
||||||
|
im.dhcpClient.SetOnLeaseChange(func(lease *types.DHCPLease) {
|
||||||
|
im.updateStateFromDHCPLease(lease)
|
||||||
|
if im.onDHCPLeaseChange != nil {
|
||||||
|
im.onDHCPLeaseChange(lease)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return im, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts managing the interface
|
||||||
|
func (im *InterfaceManager) Start() error {
|
||||||
|
im.logger.Info().Msg("starting interface manager")
|
||||||
|
|
||||||
|
// Start monitoring interface state
|
||||||
|
im.wg.Add(1)
|
||||||
|
go im.monitorInterfaceState()
|
||||||
|
|
||||||
|
// Apply initial configuration
|
||||||
|
if err := im.applyConfiguration(); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to apply initial configuration")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
im.logger.Info().Msg("interface manager started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops managing the interface
|
||||||
|
func (im *InterfaceManager) Stop() error {
|
||||||
|
im.logger.Info().Msg("stopping interface manager")
|
||||||
|
|
||||||
|
close(im.stopCh)
|
||||||
|
im.wg.Wait()
|
||||||
|
|
||||||
|
// Stop DHCP client
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
im.dhcpClient.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
im.logger.Info().Msg("interface manager stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *InterfaceManager) link() (*link.Link, error) {
|
||||||
|
nl := getNetlinkManager()
|
||||||
|
if nl == nil {
|
||||||
|
return nil, fmt.Errorf("netlink manager not initialized")
|
||||||
|
}
|
||||||
|
return nl.GetLinkByName(im.ifaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUp returns true if the interface is up
|
||||||
|
func (im *InterfaceManager) IsUp() bool {
|
||||||
|
return im.state.Up
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *InterfaceManager) IsOnline() bool {
|
||||||
|
return im.IsUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *InterfaceManager) GetIPv4Addresses() []string {
|
||||||
|
return im.state.IPv4Addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *InterfaceManager) GetIPv6Addresses() []string {
|
||||||
|
addresses := []string{}
|
||||||
|
for _, addr := range im.state.IPv6Addresses {
|
||||||
|
addresses = append(addresses, addr.Address.String())
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *InterfaceManager) GetMACAddress() string {
|
||||||
|
return im.state.MACAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current interface state
|
||||||
|
func (im *InterfaceManager) GetState() *types.InterfaceState {
|
||||||
|
im.stateMu.RLock()
|
||||||
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
// Return a copy to avoid race conditions
|
||||||
|
state := *im.state
|
||||||
|
return &state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *InterfaceManager) NTPServers() []net.IP {
|
||||||
|
return im.state.NTPServers
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig returns the current interface configuration
|
||||||
|
func (im *InterfaceManager) GetConfig() *types.NetworkConfig {
|
||||||
|
// Return a copy to avoid race conditions
|
||||||
|
config := *im.config
|
||||||
|
return &config
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig updates the interface configuration
|
||||||
|
func (im *InterfaceManager) SetConfig(config *types.NetworkConfig) error {
|
||||||
|
if config == nil {
|
||||||
|
return fmt.Errorf("config cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and set defaults
|
||||||
|
if err := confparser.SetDefaultsAndValidate(config); err != nil {
|
||||||
|
return fmt.Errorf("invalid config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
im.config = config
|
||||||
|
|
||||||
|
// Apply the new configuration
|
||||||
|
if err := im.applyConfiguration(); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to apply new configuration")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify callback
|
||||||
|
if im.onConfigChange != nil {
|
||||||
|
im.onConfigChange(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
im.logger.Info().Msg("configuration updated")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenewDHCPLease renews the DHCP lease
|
||||||
|
func (im *InterfaceManager) RenewDHCPLease() error {
|
||||||
|
if im.dhcpClient == nil {
|
||||||
|
return fmt.Errorf("DHCP client not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.dhcpClient.Renew()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnStateChange sets the callback for state changes
|
||||||
|
func (im *InterfaceManager) SetOnStateChange(callback func(state *types.InterfaceState)) {
|
||||||
|
im.onStateChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnConfigChange sets the callback for configuration changes
|
||||||
|
func (im *InterfaceManager) SetOnConfigChange(callback func(config *types.NetworkConfig)) {
|
||||||
|
im.onConfigChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnDHCPLeaseChange sets the callback for DHCP lease changes
|
||||||
|
func (im *InterfaceManager) SetOnDHCPLeaseChange(callback func(lease *types.DHCPLease)) {
|
||||||
|
im.onDHCPLeaseChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyConfiguration applies the current configuration to the interface
|
||||||
|
func (im *InterfaceManager) applyConfiguration() error {
|
||||||
|
im.logger.Info().Msg("applying configuration")
|
||||||
|
|
||||||
|
// Apply IPv4 configuration
|
||||||
|
if err := im.applyIPv4Config(); err != nil {
|
||||||
|
return fmt.Errorf("failed to apply IPv4 config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply IPv6 configuration
|
||||||
|
if err := im.applyIPv6Config(); err != nil {
|
||||||
|
return fmt.Errorf("failed to apply IPv6 config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update hostname
|
||||||
|
if err := im.updateHostname(); err != nil {
|
||||||
|
im.logger.Warn().Err(err).Msg("failed to update hostname")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv4Config applies IPv4 configuration
|
||||||
|
func (im *InterfaceManager) applyIPv4Config() error {
|
||||||
|
mode := im.config.IPv4Mode.String
|
||||||
|
im.logger.Info().Str("mode", mode).Msg("applying IPv4 configuration")
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case "static":
|
||||||
|
return im.applyIPv4Static()
|
||||||
|
case "dhcp":
|
||||||
|
return im.applyIPv4DHCP()
|
||||||
|
case "disabled":
|
||||||
|
return im.disableIPv4()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid IPv4 mode: %s", mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv6Config applies IPv6 configuration
|
||||||
|
func (im *InterfaceManager) applyIPv6Config() error {
|
||||||
|
mode := im.config.IPv6Mode.String
|
||||||
|
im.logger.Info().Str("mode", mode).Msg("applying IPv6 configuration")
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case "static":
|
||||||
|
return im.applyIPv6Static()
|
||||||
|
case "dhcpv6":
|
||||||
|
return im.applyIPv6DHCP()
|
||||||
|
case "slaac":
|
||||||
|
return im.applyIPv6SLAAC()
|
||||||
|
case "slaac_and_dhcpv6":
|
||||||
|
return im.applyIPv6SLAACAndDHCP()
|
||||||
|
case "link_local":
|
||||||
|
return im.applyIPv6LinkLocal()
|
||||||
|
case "disabled":
|
||||||
|
return im.disableIPv6()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid IPv6 mode: %s", mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv4Static applies static IPv4 configuration
|
||||||
|
func (im *InterfaceManager) applyIPv4Static() error {
|
||||||
|
if im.config.IPv4Static == nil {
|
||||||
|
return fmt.Errorf("IPv4 static configuration is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable DHCP
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
im.dhcpClient.SetIPv4(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply static configuration
|
||||||
|
return im.staticConfig.ApplyIPv4Static(im.config.IPv4Static)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv4DHCP applies DHCP IPv4 configuration
|
||||||
|
func (im *InterfaceManager) applyIPv4DHCP() error {
|
||||||
|
if im.dhcpClient == nil {
|
||||||
|
return fmt.Errorf("DHCP client not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable DHCP
|
||||||
|
im.dhcpClient.SetIPv4(true)
|
||||||
|
return im.dhcpClient.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableIPv4 disables IPv4
|
||||||
|
func (im *InterfaceManager) disableIPv4() error {
|
||||||
|
// Disable DHCP
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
im.dhcpClient.SetIPv4(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all IPv4 addresses
|
||||||
|
return im.staticConfig.DisableIPv4()
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv6Static applies static IPv6 configuration
|
||||||
|
func (im *InterfaceManager) applyIPv6Static() error {
|
||||||
|
if im.config.IPv6Static == nil {
|
||||||
|
return fmt.Errorf("IPv6 static configuration is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable DHCPv6
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
im.dhcpClient.SetIPv6(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply static configuration
|
||||||
|
return im.staticConfig.ApplyIPv6Static(im.config.IPv6Static)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv6DHCP applies DHCPv6 configuration
|
||||||
|
func (im *InterfaceManager) applyIPv6DHCP() error {
|
||||||
|
if im.dhcpClient == nil {
|
||||||
|
return fmt.Errorf("DHCP client not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable DHCPv6
|
||||||
|
im.dhcpClient.SetIPv6(true)
|
||||||
|
return im.dhcpClient.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv6SLAAC applies SLAAC configuration
|
||||||
|
func (im *InterfaceManager) applyIPv6SLAAC() error {
|
||||||
|
// Disable DHCPv6
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
im.dhcpClient.SetIPv6(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove static IPv6 configuration
|
||||||
|
l, err := im.link()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(l); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove non-link-local IPv6 addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable SLAAC
|
||||||
|
return im.staticConfig.EnableIPv6SLAAC()
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv6SLAACAndDHCP applies SLAAC + DHCPv6 configuration
|
||||||
|
func (im *InterfaceManager) applyIPv6SLAACAndDHCP() error {
|
||||||
|
// Enable both SLAAC and DHCPv6
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
im.dhcpClient.SetIPv6(true)
|
||||||
|
im.dhcpClient.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.staticConfig.EnableIPv6SLAAC()
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv6LinkLocal applies link-local only IPv6 configuration
|
||||||
|
func (im *InterfaceManager) applyIPv6LinkLocal() error {
|
||||||
|
// Disable DHCPv6
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
im.dhcpClient.SetIPv6(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable link-local only
|
||||||
|
return im.staticConfig.EnableIPv6LinkLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableIPv6 disables IPv6
|
||||||
|
func (im *InterfaceManager) disableIPv6() error {
|
||||||
|
// Disable DHCPv6
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
im.dhcpClient.SetIPv6(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable IPv6
|
||||||
|
return im.staticConfig.DisableIPv6()
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateHostname updates the system hostname
|
||||||
|
func (im *InterfaceManager) updateHostname() error {
|
||||||
|
hostname := im.getHostname()
|
||||||
|
domain := im.getDomain()
|
||||||
|
fqdn := fmt.Sprintf("%s.%s", hostname, domain)
|
||||||
|
|
||||||
|
return im.hostname.SetHostname(hostname, fqdn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHostname returns the configured hostname or default
|
||||||
|
func (im *InterfaceManager) getHostname() string {
|
||||||
|
if im.config.Hostname.String != "" {
|
||||||
|
return im.config.Hostname.String
|
||||||
|
}
|
||||||
|
return "jetkvm"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDomain returns the configured domain or default
|
||||||
|
func (im *InterfaceManager) getDomain() string {
|
||||||
|
if im.config.Domain.String != "" {
|
||||||
|
return im.config.Domain.String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get domain from DHCP lease
|
||||||
|
if im.dhcpClient != nil {
|
||||||
|
if lease := im.dhcpClient.GetLease4(); lease != nil && lease.Domain != "" {
|
||||||
|
return lease.Domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "local"
|
||||||
|
}
|
||||||
|
|
||||||
|
// monitorInterfaceState monitors the interface state and updates accordingly
|
||||||
|
func (im *InterfaceManager) monitorInterfaceState() {
|
||||||
|
defer im.wg.Done()
|
||||||
|
|
||||||
|
// TODO: use netlink subscription instead of polling
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-im.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-im.stopCh:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := im.updateInterfaceState(); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to update interface state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateInterfaceState updates the current interface state
|
||||||
|
func (im *InterfaceManager) updateInterfaceState() error {
|
||||||
|
nl, err := im.link()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := nl.Attrs()
|
||||||
|
isUp := attrs.OperState == netlink.OperUp
|
||||||
|
|
||||||
|
hasAddrs := false
|
||||||
|
addrs, err := nl.AddrList(link.AfInet)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get addresses: %w", err)
|
||||||
|
}
|
||||||
|
if len(addrs) > 0 {
|
||||||
|
hasAddrs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
im.stateMu.Lock()
|
||||||
|
defer im.stateMu.Unlock()
|
||||||
|
|
||||||
|
// Check if state changed
|
||||||
|
stateChanged := false
|
||||||
|
if im.state.Up != isUp {
|
||||||
|
im.state.Up = isUp
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
if im.state.Online != hasAddrs {
|
||||||
|
im.state.Online = hasAddrs
|
||||||
|
stateChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if im.state.MACAddr != attrs.HardwareAddr {
|
||||||
|
// im.state.MACAddr = attrs.HardwareAddr
|
||||||
|
// stateChanged = true
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Update IP addresses
|
||||||
|
if err := im.updateIPAddresses(nl); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to update IP addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
// im.state.LastUpdated = time.Now() // TODO: remove this
|
||||||
|
|
||||||
|
// Notify callback if state changed
|
||||||
|
if stateChanged && im.onStateChange != nil {
|
||||||
|
state := *im.state
|
||||||
|
im.onStateChange(&state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateIPAddresses updates the IP addresses in the state
|
||||||
|
func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error {
|
||||||
|
addrs, err := nl.AddrList(link.AfUnspec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipv4Addresses []string
|
||||||
|
var ipv6Addresses []types.IPv6Address
|
||||||
|
var ipv4Addr, ipv6Addr string
|
||||||
|
var ipv6LinkLocal string
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if addr.IP.To4() != nil {
|
||||||
|
// IPv4 address
|
||||||
|
ipv4Addresses = append(ipv4Addresses, addr.IPNet.String())
|
||||||
|
if ipv4Addr == "" {
|
||||||
|
ipv4Addr = addr.IP.String()
|
||||||
|
}
|
||||||
|
} else if addr.IP.To16() != nil {
|
||||||
|
// IPv6 address
|
||||||
|
if addr.IP.IsLinkLocalUnicast() {
|
||||||
|
ipv6LinkLocal = addr.IP.String()
|
||||||
|
} else if addr.IP.IsGlobalUnicast() {
|
||||||
|
ipv6Addresses = append(ipv6Addresses, types.IPv6Address{
|
||||||
|
Address: addr.IP,
|
||||||
|
Prefix: *addr.IPNet,
|
||||||
|
Scope: addr.Scope,
|
||||||
|
})
|
||||||
|
if ipv6Addr == "" {
|
||||||
|
ipv6Addr = addr.IP.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
im.state.IPv4Addresses = ipv4Addresses
|
||||||
|
im.state.IPv6Addresses = ipv6Addresses
|
||||||
|
im.state.IPv6LinkLocal = ipv6LinkLocal
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateStateFromDHCPLease updates the state from a DHCP lease
|
||||||
|
func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) {
|
||||||
|
im.stateMu.Lock()
|
||||||
|
defer im.stateMu.Unlock()
|
||||||
|
|
||||||
|
im.state.DHCPLease4 = lease
|
||||||
|
|
||||||
|
// Update resolv.conf with DNS information
|
||||||
|
if im.resolvConf != nil {
|
||||||
|
im.resolvConf.UpdateFromLease(lease)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (im *InterfaceManager) ReconcileLinkAddrs(ipv4Config *types.IPv4StaticConfig) error {
|
||||||
|
// nl := getNetlinkManager()
|
||||||
|
// return nl.ReconcileLinkAddrs(ipv4Config)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDHCPLease applies DHCP lease configuration using ReconcileLinkAddrs
|
||||||
|
func (im *InterfaceManager) applyDHCPLease(lease *types.DHCPLease) error {
|
||||||
|
// Convert DHCP lease to IPv4Config
|
||||||
|
// ipv4Config := im.convertDHCPLeaseToIPv4Config(lease)
|
||||||
|
|
||||||
|
// Apply the configuration using ReconcileLinkAddrs
|
||||||
|
// return im.ReconcileLinkAddrs(ipv4Config)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertDHCPLeaseToIPv4Config converts a DHCP lease to IPv4Config
|
||||||
|
// func (im *InterfaceManager) convertDHCPLeaseToIPv4Config(lease *types.DHCPLease) *netif.IPv4Config {
|
||||||
|
// // Create IPNet from IP and netmask
|
||||||
|
// ipNet := &net.IPNet{
|
||||||
|
// IP: lease.IPAddress,
|
||||||
|
// Mask: net.IPMask(lease.Netmask),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Create IPv4Address
|
||||||
|
// ipv4Addr := netif.IPv4Address{
|
||||||
|
// Address: *ipNet,
|
||||||
|
// Gateway: lease.Gateway,
|
||||||
|
// Secondary: false,
|
||||||
|
// Permanent: false,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Create IPv4Config
|
||||||
|
// return &netif.IPv4Config{
|
||||||
|
// Addresses: []netif.IPv4Address{ipv4Addr},
|
||||||
|
// Nameservers: lease.DNS,
|
||||||
|
// SearchList: lease.SearchList,
|
||||||
|
// Domain: lease.Domain,
|
||||||
|
// Interface: im.ifaceName,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
@ -0,0 +1,465 @@
|
||||||
|
// Package link provides a wrapper around netlink.Link and provides a singleton netlink manager.
|
||||||
|
package link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AfUnspec is the unspecified address family constant
|
||||||
|
AfUnspec = 0
|
||||||
|
// AfInet is the IPv4 address family constant
|
||||||
|
AfInet = 2
|
||||||
|
// AfInet6 is the IPv6 address family constant
|
||||||
|
AfInet6 = 10
|
||||||
|
|
||||||
|
sysctlBase = "/proc/sys"
|
||||||
|
sysctlFileMode = 0640
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ipv4DefaultRoute = net.IPNet{
|
||||||
|
IP: net.IPv4zero,
|
||||||
|
Mask: net.CIDRMask(0, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv6DefaultRoute = net.IPNet{
|
||||||
|
IP: net.IPv6zero,
|
||||||
|
Mask: net.CIDRMask(0, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
netlinkManagerInstance *NetlinkManager
|
||||||
|
netlinkManagerOnce sync.Once
|
||||||
|
|
||||||
|
// Error definitions
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
// NetlinkManager provides centralized netlink operations
|
||||||
|
type NetlinkManager struct {
|
||||||
|
logger *zerolog.Logger
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link is a wrapper around netlink.Link
|
||||||
|
type Link struct {
|
||||||
|
netlink.Link
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attrs returns the attributes of the link
|
||||||
|
func (l *Link) Attrs() *netlink.LinkAttrs {
|
||||||
|
return l.Link.Attrs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Link) AddrList(family int) ([]netlink.Addr, error) {
|
||||||
|
return netlink.AddrList(l, family)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNetlinkManager returns the singleton NetlinkManager instance
|
||||||
|
func GetNetlinkManager() *NetlinkManager {
|
||||||
|
netlinkManagerOnce.Do(func() {
|
||||||
|
netlinkManagerInstance = &NetlinkManager{
|
||||||
|
logger: &zerolog.Logger{}, // Default no-op logger
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return netlinkManagerInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeNetlinkManager initializes the singleton NetlinkManager with a logger
|
||||||
|
func InitializeNetlinkManager(logger *zerolog.Logger) *NetlinkManager {
|
||||||
|
netlinkManagerOnce.Do(func() {
|
||||||
|
if logger == nil {
|
||||||
|
// Create a no-op logger if none provided
|
||||||
|
logger = &zerolog.Logger{}
|
||||||
|
}
|
||||||
|
netlinkManagerInstance = &NetlinkManager{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return netlinkManagerInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface operations
|
||||||
|
|
||||||
|
// GetLinkByName gets a network link by name
|
||||||
|
func (nm *NetlinkManager) GetLinkByName(name string) (*Link, error) {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
link, err := netlink.LinkByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Link{Link: link}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkSetUp brings a network interface up
|
||||||
|
func (nm *NetlinkManager) LinkSetUp(link *Link) error {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.LinkSetUp(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkSetDown brings a network interface down
|
||||||
|
func (nm *NetlinkManager) LinkSetDown(link *Link) error {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.LinkSetDown(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureInterfaceUp ensures the interface is up
|
||||||
|
func (nm *NetlinkManager) EnsureInterfaceUp(link *Link) error {
|
||||||
|
if link.Attrs().OperState == netlink.OperUp {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nm.LinkSetUp(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureInterfaceUpWithTimeout ensures the interface is up with timeout and retry logic
|
||||||
|
func (nm *NetlinkManager) EnsureInterfaceUpWithTimeout(ctx context.Context, iface *Link, timeout time.Duration) (*Link, error) {
|
||||||
|
ifname := iface.Attrs().Name
|
||||||
|
|
||||||
|
l := nm.logger.With().Str("interface", ifname).Logger()
|
||||||
|
|
||||||
|
linkUpTimeout := time.After(timeout)
|
||||||
|
|
||||||
|
attempt := 0
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
for {
|
||||||
|
link, err := nm.GetLinkByName(ifname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
state := link.Attrs().OperState
|
||||||
|
if state == netlink.OperUp || state == netlink.OperUnknown {
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info().Str("state", state.String()).Msg("bringing up interface")
|
||||||
|
|
||||||
|
if err = nm.LinkSetUp(link); err != nil {
|
||||||
|
l.Error().Err(err).Msg("interface can't make it up")
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l.With().Int("attempt", attempt).Dur("duration", time.Since(start)).Logger()
|
||||||
|
|
||||||
|
if attempt > 0 {
|
||||||
|
l.Info().Msg("interface up")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
attempt++
|
||||||
|
continue
|
||||||
|
case <-ctx.Done():
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, ErrInterfaceUpCanceled
|
||||||
|
case <-linkUpTimeout:
|
||||||
|
attempt++
|
||||||
|
l.Error().Msg("interface is still down after timeout")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, ErrInterfaceUpTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address operations
|
||||||
|
|
||||||
|
// AddrList gets all addresses for a link
|
||||||
|
func (nm *NetlinkManager) AddrList(link *Link, family int) ([]netlink.Addr, error) {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.AddrList(link, family)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddrAdd adds an address to a link
|
||||||
|
func (nm *NetlinkManager) AddrAdd(link *Link, addr *netlink.Addr) error {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.AddrAdd(link, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddrDel removes an address from a link
|
||||||
|
func (nm *NetlinkManager) AddrDel(link *Link, addr *netlink.Addr) error {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.AddrDel(link, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAllAddresses removes all addresses of a specific family from a link
|
||||||
|
func (nm *NetlinkManager) RemoveAllAddresses(link *Link, family int) error {
|
||||||
|
addrs, err := nm.AddrList(link, family)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if err := nm.AddrDel(link, &addr); err != nil {
|
||||||
|
nm.logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("failed to remove address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveNonLinkLocalIPv6Addresses removes all non-link-local IPv6 addresses
|
||||||
|
func (nm *NetlinkManager) RemoveNonLinkLocalIPv6Addresses(link *Link) error {
|
||||||
|
addrs, err := nm.AddrList(link, AfInet6)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get IPv6 addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if !addr.IP.IsLinkLocalUnicast() {
|
||||||
|
if err := nm.AddrDel(link, &addr); err != nil {
|
||||||
|
nm.logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("failed to remove IPv6 address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route operations
|
||||||
|
|
||||||
|
// RouteList gets all routes
|
||||||
|
func (nm *NetlinkManager) RouteList(link *Link, family int) ([]netlink.Route, error) {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.RouteList(link, family)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteAdd adds a route
|
||||||
|
func (nm *NetlinkManager) RouteAdd(route *netlink.Route) error {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.RouteAdd(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteDel removes a route
|
||||||
|
func (nm *NetlinkManager) RouteDel(route *netlink.Route) error {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.RouteDel(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteReplace replaces a route
|
||||||
|
func (nm *NetlinkManager) RouteReplace(route *netlink.Route) error {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
return netlink.RouteReplace(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasDefaultRoute checks if a default route exists for the given family
|
||||||
|
func (nm *NetlinkManager) HasDefaultRoute(family int) bool {
|
||||||
|
routes, err := netlink.RouteList(nil, family)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
if route.Dst == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if family == AfInet && route.Dst.IP.Equal(net.IPv4zero) && route.Dst.Mask.String() == "0.0.0.0/0" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if family == AfInet6 && route.Dst.IP.Equal(net.IPv6zero) && route.Dst.Mask.String() == "::/0" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDefaultRoute adds a default route
|
||||||
|
func (nm *NetlinkManager) AddDefaultRoute(link *Link, gateway net.IP, family int) error {
|
||||||
|
var dst *net.IPNet
|
||||||
|
if family == AfInet {
|
||||||
|
dst = &ipv4DefaultRoute
|
||||||
|
} else if family == AfInet6 {
|
||||||
|
dst = &ipv6DefaultRoute
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unsupported address family: %d", family)
|
||||||
|
}
|
||||||
|
|
||||||
|
route := &netlink.Route{
|
||||||
|
Dst: dst,
|
||||||
|
Gw: gateway,
|
||||||
|
LinkIndex: link.Attrs().Index,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nm.RouteReplace(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveDefaultRoute removes the default route for the given family
|
||||||
|
func (nm *NetlinkManager) RemoveDefaultRoute(family int) error {
|
||||||
|
routes, err := nm.RouteList(nil, family)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get routes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
if route.Dst != nil {
|
||||||
|
if family == AfInet && route.Dst.IP.Equal(net.IPv4zero) && route.Dst.Mask.String() == "0.0.0.0/0" {
|
||||||
|
if err := nm.RouteDel(&route); err != nil {
|
||||||
|
nm.logger.Warn().Err(err).Msg("failed to remove IPv4 default route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if family == AfInet6 && route.Dst.IP.Equal(net.IPv6zero) && route.Dst.Mask.String() == "::/0" {
|
||||||
|
if err := nm.RouteDel(&route); err != nil {
|
||||||
|
nm.logger.Warn().Err(err).Msg("failed to remove IPv6 default route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sysctl operations
|
||||||
|
|
||||||
|
// SetSysctlValues sets sysctl values for the interface
|
||||||
|
func (nm *NetlinkManager) SetSysctlValues(ifaceName string, values map[string]int) error {
|
||||||
|
for name, value := range values {
|
||||||
|
name = fmt.Sprintf(name, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableIPv6 enables IPv6 on the interface
|
||||||
|
func (nm *NetlinkManager) EnableIPv6(ifaceName string) error {
|
||||||
|
return nm.SetSysctlValues(ifaceName, map[string]int{
|
||||||
|
"net.ipv6.conf.%s.disable_ipv6": 0,
|
||||||
|
"net.ipv6.conf.%s.accept_ra": 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableIPv6 disables IPv6 on the interface
|
||||||
|
func (nm *NetlinkManager) DisableIPv6(ifaceName string) error {
|
||||||
|
return nm.SetSysctlValues(ifaceName, map[string]int{
|
||||||
|
"net.ipv6.conf.%s.disable_ipv6": 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableIPv6SLAAC enables IPv6 SLAAC on the interface
|
||||||
|
func (nm *NetlinkManager) EnableIPv6SLAAC(ifaceName string) error {
|
||||||
|
return nm.SetSysctlValues(ifaceName, map[string]int{
|
||||||
|
"net.ipv6.conf.%s.disable_ipv6": 0,
|
||||||
|
"net.ipv6.conf.%s.accept_ra": 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableIPv6LinkLocal enables IPv6 link-local only on the interface
|
||||||
|
func (nm *NetlinkManager) EnableIPv6LinkLocal(ifaceName string) error {
|
||||||
|
return nm.SetSysctlValues(ifaceName, map[string]int{
|
||||||
|
"net.ipv6.conf.%s.disable_ipv6": 0,
|
||||||
|
"net.ipv6.conf.%s.accept_ra": 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
|
||||||
|
// ParseIPv4Netmask parses an IPv4 netmask string and returns the IPNet
|
||||||
|
func (nm *NetlinkManager) ParseIPv4Netmask(address, netmask string) (*net.IPNet, error) {
|
||||||
|
if strings.Contains(address, "/") {
|
||||||
|
_, ipNet, err := net.ParseCIDR(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid IPv4 address: %s", address)
|
||||||
|
}
|
||||||
|
return ipNet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(address)
|
||||||
|
if ip == nil {
|
||||||
|
return nil, fmt.Errorf("invalid IPv4 address: %s", address)
|
||||||
|
}
|
||||||
|
if ip.To4() == nil {
|
||||||
|
return nil, fmt.Errorf("not an IPv4 address: %s", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
mask := net.ParseIP(netmask)
|
||||||
|
if mask == nil {
|
||||||
|
return nil, fmt.Errorf("invalid IPv4 netmask: %s", netmask)
|
||||||
|
}
|
||||||
|
if mask.To4() == nil {
|
||||||
|
return nil, fmt.Errorf("not an IPv4 netmask: %s", netmask)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &net.IPNet{
|
||||||
|
IP: ip,
|
||||||
|
Mask: net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseIPv6Prefix parses an IPv6 address and prefix length
|
||||||
|
func (nm *NetlinkManager) ParseIPv6Prefix(address string, prefixLength int) (*net.IPNet, error) {
|
||||||
|
if strings.Contains(address, "/") {
|
||||||
|
_, ipNet, err := net.ParseCIDR(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid IPv6 address: %s", address)
|
||||||
|
}
|
||||||
|
return ipNet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(address)
|
||||||
|
if ip == nil {
|
||||||
|
return nil, fmt.Errorf("invalid IPv6 address: %s", address)
|
||||||
|
}
|
||||||
|
if ip.To16() == nil || ip.To4() != nil {
|
||||||
|
return nil, fmt.Errorf("not an IPv6 address: %s", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefixLength < 0 || prefixLength > 128 {
|
||||||
|
return nil, fmt.Errorf("invalid IPv6 prefix length: %d (must be 0-128)", prefixLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &net.IPNet{
|
||||||
|
IP: ip,
|
||||||
|
Mask: net.CIDRMask(prefixLength, 128),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIPAddress validates an IP address
|
||||||
|
func (nm *NetlinkManager) ValidateIPAddress(address string, isIPv6 bool) error {
|
||||||
|
ip := net.ParseIP(address)
|
||||||
|
if ip == nil {
|
||||||
|
return fmt.Errorf("invalid IP address: %s", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isIPv6 {
|
||||||
|
if ip.To16() == nil || ip.To4() != nil {
|
||||||
|
return fmt.Errorf("not an IPv6 address: %s", address)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ip.To4() == nil {
|
||||||
|
return fmt.Errorf("not an IPv4 address: %s", address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IPv4Address represents an IPv4 address and its gateway
|
||||||
|
type IPv4Address struct {
|
||||||
|
Address net.IPNet
|
||||||
|
Gateway net.IP
|
||||||
|
Secondary bool
|
||||||
|
Permanent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4Config represents the configuration for an IPv4 interface
|
||||||
|
type IPv4Config struct {
|
||||||
|
Addresses []IPv4Address
|
||||||
|
Nameservers []net.IP
|
||||||
|
SearchList []string
|
||||||
|
Domain string
|
||||||
|
MTU int
|
||||||
|
Interface string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
// Package nmlite provides a lightweight network management system.
|
||||||
|
// It supports multiple network interfaces with static and DHCP configuration,
|
||||||
|
// IPv4/IPv6 support, and proper separation of concerns.
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NetworkManager manages multiple network interfaces
|
||||||
|
type NetworkManager struct {
|
||||||
|
interfaces map[string]*InterfaceManager
|
||||||
|
mu sync.RWMutex
|
||||||
|
logger *zerolog.Logger
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
// Callback functions for state changes
|
||||||
|
onInterfaceStateChange func(iface string, state *types.InterfaceState)
|
||||||
|
onConfigChange func(iface string, config *types.NetworkConfig)
|
||||||
|
onDHCPLeaseChange func(iface string, lease *types.DHCPLease)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNetworkManager creates a new network manager
|
||||||
|
func NewNetworkManager(ctx context.Context, logger *zerolog.Logger) *NetworkManager {
|
||||||
|
if logger == nil {
|
||||||
|
logger = logging.GetSubsystemLogger("networkmgr")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the NetlinkManager singleton
|
||||||
|
link.InitializeNetlinkManager(logger)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
return &NetworkManager{
|
||||||
|
interfaces: make(map[string]*InterfaceManager),
|
||||||
|
logger: logger,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddInterface adds a new network interface to be managed
|
||||||
|
func (nm *NetworkManager) AddInterface(iface string, config *types.NetworkConfig) error {
|
||||||
|
nm.mu.Lock()
|
||||||
|
defer nm.mu.Unlock()
|
||||||
|
|
||||||
|
if _, exists := nm.interfaces[iface]; exists {
|
||||||
|
return fmt.Errorf("interface %s already managed", iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
im, err := NewInterfaceManager(nm.ctx, iface, config, nm.logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create interface manager for %s: %w", iface, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up callbacks
|
||||||
|
im.SetOnStateChange(func(state *types.InterfaceState) {
|
||||||
|
if nm.onInterfaceStateChange != nil {
|
||||||
|
nm.onInterfaceStateChange(iface, state)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
im.SetOnConfigChange(func(config *types.NetworkConfig) {
|
||||||
|
if nm.onConfigChange != nil {
|
||||||
|
nm.onConfigChange(iface, config)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
im.SetOnDHCPLeaseChange(func(lease *types.DHCPLease) {
|
||||||
|
if nm.onDHCPLeaseChange != nil {
|
||||||
|
nm.onDHCPLeaseChange(iface, lease)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
nm.interfaces[iface] = im
|
||||||
|
|
||||||
|
// Start monitoring the interface
|
||||||
|
if err := im.Start(); err != nil {
|
||||||
|
delete(nm.interfaces, iface)
|
||||||
|
return fmt.Errorf("failed to start interface manager for %s: %w", iface, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nm.logger.Info().Str("interface", iface).Msg("added interface to network manager")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveInterface removes a network interface from management
|
||||||
|
func (nm *NetworkManager) RemoveInterface(iface string) error {
|
||||||
|
nm.mu.Lock()
|
||||||
|
defer nm.mu.Unlock()
|
||||||
|
|
||||||
|
im, exists := nm.interfaces[iface]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("interface %s not managed", iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := im.Stop(); err != nil {
|
||||||
|
nm.logger.Error().Err(err).Str("interface", iface).Msg("failed to stop interface manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(nm.interfaces, iface)
|
||||||
|
nm.logger.Info().Str("interface", iface).Msg("removed interface from network manager")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterface returns the interface manager for a specific interface
|
||||||
|
func (nm *NetworkManager) GetInterface(iface string) (*InterfaceManager, error) {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
|
||||||
|
im, exists := nm.interfaces[iface]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("interface %s not managed", iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
return im, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListInterfaces returns a list of all managed interfaces
|
||||||
|
func (nm *NetworkManager) ListInterfaces() []string {
|
||||||
|
nm.mu.RLock()
|
||||||
|
defer nm.mu.RUnlock()
|
||||||
|
|
||||||
|
interfaces := make([]string, 0, len(nm.interfaces))
|
||||||
|
for iface := range nm.interfaces {
|
||||||
|
interfaces = append(interfaces, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterfaceState returns the current state of a specific interface
|
||||||
|
func (nm *NetworkManager) GetInterfaceState(iface string) (*types.InterfaceState, error) {
|
||||||
|
im, err := nm.GetInterface(iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.GetState(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterfaceConfig returns the current configuration of a specific interface
|
||||||
|
func (nm *NetworkManager) GetInterfaceConfig(iface string) (*types.NetworkConfig, error) {
|
||||||
|
im, err := nm.GetInterface(iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.GetConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInterfaceConfig updates the configuration of a specific interface
|
||||||
|
func (nm *NetworkManager) SetInterfaceConfig(iface string, config *types.NetworkConfig) error {
|
||||||
|
im, err := nm.GetInterface(iface)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.SetConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenewDHCPLease renews the DHCP lease for a specific interface
|
||||||
|
func (nm *NetworkManager) RenewDHCPLease(iface string) error {
|
||||||
|
im, err := nm.GetInterface(iface)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return im.RenewDHCPLease()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnInterfaceStateChange sets the callback for interface state changes
|
||||||
|
func (nm *NetworkManager) SetOnInterfaceStateChange(callback func(iface string, state *types.InterfaceState)) {
|
||||||
|
nm.onInterfaceStateChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnConfigChange sets the callback for configuration changes
|
||||||
|
func (nm *NetworkManager) SetOnConfigChange(callback func(iface string, config *types.NetworkConfig)) {
|
||||||
|
nm.onConfigChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOnDHCPLeaseChange sets the callback for DHCP lease changes
|
||||||
|
func (nm *NetworkManager) SetOnDHCPLeaseChange(callback func(iface string, lease *types.DHCPLease)) {
|
||||||
|
nm.onDHCPLeaseChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the network manager and all managed interfaces
|
||||||
|
func (nm *NetworkManager) Stop() error {
|
||||||
|
nm.mu.Lock()
|
||||||
|
defer nm.mu.Unlock()
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for iface, im := range nm.interfaces {
|
||||||
|
if err := im.Stop(); err != nil {
|
||||||
|
nm.logger.Error().Err(err).Str("interface", iface).Msg("failed to stop interface manager")
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nm.cancel()
|
||||||
|
nm.logger.Info().Msg("network manager stopped")
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import "github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||||
|
|
||||||
|
func getNetlinkManager() *link.NetlinkManager {
|
||||||
|
return link.GetNetlinkManager()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
resolvConfPath = "/etc/resolv.conf"
|
||||||
|
resolvConfFileMode = 0644
|
||||||
|
resolvConfTemplate = `# the resolv.conf file is managed by the jetkvm network manager
|
||||||
|
# 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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolvConfManager manages the resolv.conf file
|
||||||
|
type ResolvConfManager struct {
|
||||||
|
logger *zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResolvConfManager creates a new resolv.conf manager
|
||||||
|
func NewResolvConfManager(logger *zerolog.Logger) *ResolvConfManager {
|
||||||
|
if logger == nil {
|
||||||
|
// Create a no-op logger if none provided
|
||||||
|
logger = &zerolog.Logger{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ResolvConfManager{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFromLease updates resolv.conf from a DHCP lease
|
||||||
|
func (rcm *ResolvConfManager) UpdateFromLease(lease *types.DHCPLease) error {
|
||||||
|
if lease == nil {
|
||||||
|
return fmt.Errorf("lease cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
rcm.logger.Info().
|
||||||
|
Str("interface", lease.InterfaceName).
|
||||||
|
Msg("updating resolv.conf from DHCP lease")
|
||||||
|
|
||||||
|
return rcm.Update(lease.InterfaceName, lease.DNS, lease.SearchList, lease.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFromStaticConfig updates resolv.conf from static configuration
|
||||||
|
func (rcm *ResolvConfManager) UpdateFromStaticConfig(iface string, dns []string) error {
|
||||||
|
if len(dns) == 0 {
|
||||||
|
rcm.logger.Debug().Str("interface", iface).Msg("no DNS servers in static config")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse DNS servers
|
||||||
|
var dnsIPs []net.IP
|
||||||
|
for _, dnsStr := range dns {
|
||||||
|
dnsIP := net.ParseIP(dnsStr)
|
||||||
|
if dnsIP == nil {
|
||||||
|
rcm.logger.Warn().Str("dns", dnsStr).Msg("invalid DNS server, skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dnsIPs = append(dnsIPs, dnsIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dnsIPs) == 0 {
|
||||||
|
rcm.logger.Debug().Str("interface", iface).Msg("no valid DNS servers in static config")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rcm.logger.Info().
|
||||||
|
Str("interface", iface).
|
||||||
|
Interface("dns", dnsIPs).
|
||||||
|
Msg("updating resolv.conf from static config")
|
||||||
|
|
||||||
|
return rcm.Update(iface, dnsIPs, nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the resolv.conf file
|
||||||
|
func (rcm *ResolvConfManager) Update(iface string, nameservers []net.IP, searchList []string, domain string) error {
|
||||||
|
rcm.logger.Debug().
|
||||||
|
Str("interface", iface).
|
||||||
|
Interface("nameservers", nameservers).
|
||||||
|
Interface("searchList", searchList).
|
||||||
|
Str("domain", domain).
|
||||||
|
Msg("updating resolv.conf")
|
||||||
|
|
||||||
|
// Generate resolv.conf content
|
||||||
|
content, err := rcm.generateResolvConf(iface, nameservers, searchList, domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate resolv.conf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
|
||||||
|
return fmt.Errorf("failed to write resolv.conf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rcm.logger.Info().
|
||||||
|
Str("interface", iface).
|
||||||
|
Int("nameservers", len(nameservers)).
|
||||||
|
Msg("resolv.conf updated successfully")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateResolvConf generates resolv.conf content
|
||||||
|
func (rcm *ResolvConfManager) generateResolvConf(iface string, nameservers []net.IP, searchList []string, domain string) ([]byte, error) {
|
||||||
|
tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buf, map[string]interface{}{
|
||||||
|
"iface": iface,
|
||||||
|
"nameservers": nameservers,
|
||||||
|
"searchList": searchList,
|
||||||
|
"domain": domain,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to execute template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear clears the resolv.conf file (removes all entries)
|
||||||
|
func (rcm *ResolvConfManager) Clear() error {
|
||||||
|
rcm.logger.Info().Msg("clearing resolv.conf")
|
||||||
|
|
||||||
|
content := []byte("# the resolv.conf file is managed by the jetkvm network manager\n# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN\n")
|
||||||
|
|
||||||
|
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
|
||||||
|
return fmt.Errorf("failed to clear resolv.conf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rcm.logger.Info().Msg("resolv.conf cleared")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentContent returns the current content of resolv.conf
|
||||||
|
func (rcm *ResolvConfManager) GetCurrentContent() ([]byte, error) {
|
||||||
|
return os.ReadFile(resolvConfPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup creates a backup of the current resolv.conf
|
||||||
|
func (rcm *ResolvConfManager) Backup() error {
|
||||||
|
content, err := rcm.GetCurrentContent()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read current resolv.conf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupPath := resolvConfPath + ".backup"
|
||||||
|
if err := os.WriteFile(backupPath, content, resolvConfFileMode); err != nil {
|
||||||
|
return fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rcm.logger.Info().Str("backup", backupPath).Msg("resolv.conf backed up")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores resolv.conf from backup
|
||||||
|
func (rcm *ResolvConfManager) Restore() error {
|
||||||
|
backupPath := resolvConfPath + ".backup"
|
||||||
|
content, err := os.ReadFile(backupPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read backup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
|
||||||
|
return fmt.Errorf("failed to restore resolv.conf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rcm.logger.Info().Str("backup", backupPath).Msg("resolv.conf restored from backup")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
func (nm *NetworkManager) IsOnline() bool {
|
||||||
|
for _, iface := range nm.interfaces {
|
||||||
|
if iface.IsOnline() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) IsUp() bool {
|
||||||
|
return nm.IsOnline()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) GetHostname() string {
|
||||||
|
return "jetkvm"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) GetFQDN() string {
|
||||||
|
return "jetkvm.local"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) NTPServers() []net.IP {
|
||||||
|
servers := []net.IP{}
|
||||||
|
for _, iface := range nm.interfaces {
|
||||||
|
servers = append(servers, iface.NTPServers()...)
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) NTPServerStrings() []string {
|
||||||
|
servers := []string{}
|
||||||
|
for _, server := range nm.NTPServers() {
|
||||||
|
servers = append(servers, server.String())
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) GetIPv4Addresses() []string {
|
||||||
|
for _, iface := range nm.interfaces {
|
||||||
|
return iface.GetIPv4Addresses()
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) GetIPv6Addresses() []string {
|
||||||
|
for _, iface := range nm.interfaces {
|
||||||
|
return iface.GetIPv6Addresses()
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) GetMACAddress() string {
|
||||||
|
for _, iface := range nm.interfaces {
|
||||||
|
return iface.GetMACAddress()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) IPv4String() string {
|
||||||
|
l := nm.GetIPv4Addresses()
|
||||||
|
if len(l) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return l[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) IPv6String() string {
|
||||||
|
l := nm.GetIPv6Addresses()
|
||||||
|
if len(l) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return l[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkManager) MACString() string {
|
||||||
|
return nm.GetMACAddress()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,348 @@
|
||||||
|
package nmlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StaticConfigManager manages static network configuration
|
||||||
|
type StaticConfigManager struct {
|
||||||
|
ifaceName string
|
||||||
|
logger *zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStaticConfigManager creates a new static configuration manager
|
||||||
|
func NewStaticConfigManager(ifaceName string, logger *zerolog.Logger) (*StaticConfigManager, error) {
|
||||||
|
if ifaceName == "" {
|
||||||
|
return nil, fmt.Errorf("interface name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger == nil {
|
||||||
|
return nil, fmt.Errorf("logger cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StaticConfigManager{
|
||||||
|
ifaceName: ifaceName,
|
||||||
|
logger: logger,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyIPv4Static applies static IPv4 configuration
|
||||||
|
func (scm *StaticConfigManager) ApplyIPv4Static(config *types.IPv4StaticConfig) error {
|
||||||
|
scm.logger.Info().Msg("applying static IPv4 configuration")
|
||||||
|
|
||||||
|
// Parse and validate configuration
|
||||||
|
ipv4Config, err := scm.parseIPv4Config(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse IPv4 config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get interface
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
link, err := netlinkMgr.GetLinkByName(scm.ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure interface is up
|
||||||
|
if err := netlinkMgr.EnsureInterfaceUp(link); err != nil {
|
||||||
|
return fmt.Errorf("failed to bring interface up: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply IP address
|
||||||
|
if err := scm.applyIPv4Address(link, ipv4Config); err != nil {
|
||||||
|
return fmt.Errorf("failed to apply IPv4 address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply default route
|
||||||
|
if err := scm.applyIPv4Route(link, ipv4Config); err != nil {
|
||||||
|
return fmt.Errorf("failed to apply IPv4 route: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scm.logger.Info().Msg("static IPv4 configuration applied successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyIPv6Static applies static IPv6 configuration
|
||||||
|
func (scm *StaticConfigManager) ApplyIPv6Static(config *types.IPv6StaticConfig) error {
|
||||||
|
scm.logger.Info().Msg("applying static IPv6 configuration")
|
||||||
|
|
||||||
|
// Parse and validate configuration
|
||||||
|
ipv6Config, err := scm.parseIPv6Config(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse IPv6 config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get interface
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
link, err := netlinkMgr.GetLinkByName(scm.ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable IPv6
|
||||||
|
if err := scm.enableIPv6(); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable IPv6: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure interface is up
|
||||||
|
if err := netlinkMgr.EnsureInterfaceUp(link); err != nil {
|
||||||
|
return fmt.Errorf("failed to bring interface up: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply IP address
|
||||||
|
if err := scm.applyIPv6Address(link, ipv6Config); err != nil {
|
||||||
|
return fmt.Errorf("failed to apply IPv6 address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply default route
|
||||||
|
if err := scm.applyIPv6Route(link, ipv6Config); err != nil {
|
||||||
|
return fmt.Errorf("failed to apply IPv6 route: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scm.logger.Info().Msg("static IPv6 configuration applied successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableIPv4 disables IPv4 on the interface
|
||||||
|
func (scm *StaticConfigManager) DisableIPv4() error {
|
||||||
|
scm.logger.Info().Msg("disabling IPv4")
|
||||||
|
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
iface, err := netlinkMgr.GetLinkByName(scm.ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all IPv4 addresses
|
||||||
|
if err := netlinkMgr.RemoveAllAddresses(iface, link.AfInet); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove IPv4 addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove default route
|
||||||
|
if err := scm.removeIPv4DefaultRoute(); err != nil {
|
||||||
|
scm.logger.Warn().Err(err).Msg("failed to remove IPv4 default route")
|
||||||
|
}
|
||||||
|
|
||||||
|
scm.logger.Info().Msg("IPv4 disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableIPv6 disables IPv6 on the interface
|
||||||
|
func (scm *StaticConfigManager) DisableIPv6() error {
|
||||||
|
scm.logger.Info().Msg("disabling IPv6")
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
return netlinkMgr.DisableIPv6(scm.ifaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableIPv6SLAAC enables IPv6 SLAAC
|
||||||
|
func (scm *StaticConfigManager) EnableIPv6SLAAC() error {
|
||||||
|
scm.logger.Info().Msg("enabling IPv6 SLAAC")
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
return netlinkMgr.EnableIPv6SLAAC(scm.ifaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableIPv6LinkLocal enables IPv6 link-local only
|
||||||
|
func (scm *StaticConfigManager) EnableIPv6LinkLocal() error {
|
||||||
|
scm.logger.Info().Msg("enabling IPv6 link-local only")
|
||||||
|
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
if err := netlinkMgr.EnableIPv6LinkLocal(scm.ifaceName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all non-link-local IPv6 addresses
|
||||||
|
link, err := netlinkMgr.GetLinkByName(scm.ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(link); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove non-link-local IPv6 addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return netlinkMgr.EnsureInterfaceUp(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIPv4Config parses and validates IPv4 static configuration
|
||||||
|
func (scm *StaticConfigManager) parseIPv4Config(config *types.IPv4StaticConfig) (*parsedIPv4Config, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, fmt.Errorf("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse IP address and netmask
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
ipNet, err := netlinkMgr.ParseIPv4Netmask(config.Address.String, config.Netmask.String)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scm.logger.Info().Str("ipNet", ipNet.String()).Interface("ipc", config).Msg("parsed IPv4 address and netmask")
|
||||||
|
|
||||||
|
// Parse gateway
|
||||||
|
gateway := net.ParseIP(config.Gateway.String)
|
||||||
|
if gateway == nil {
|
||||||
|
return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse DNS servers
|
||||||
|
var dns []net.IP
|
||||||
|
for _, dnsStr := range config.DNS {
|
||||||
|
if err := netlinkMgr.ValidateIPAddress(dnsStr, false); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid DNS server: %w", err)
|
||||||
|
}
|
||||||
|
dns = append(dns, net.ParseIP(dnsStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &parsedIPv4Config{
|
||||||
|
network: *ipNet,
|
||||||
|
gateway: gateway,
|
||||||
|
dns: dns,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIPv6Config parses and validates IPv6 static configuration
|
||||||
|
func (scm *StaticConfigManager) parseIPv6Config(config *types.IPv6StaticConfig) (*parsedIPv6Config, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, fmt.Errorf("config is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse IP address and prefix
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
ipNet, err := netlinkMgr.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse gateway
|
||||||
|
gateway := net.ParseIP(config.Gateway.String)
|
||||||
|
if gateway == nil {
|
||||||
|
return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse DNS servers
|
||||||
|
var dns []net.IP
|
||||||
|
for _, dnsStr := range config.DNS {
|
||||||
|
dnsIP := net.ParseIP(dnsStr)
|
||||||
|
if dnsIP == nil {
|
||||||
|
return nil, fmt.Errorf("invalid DNS server: %s", dnsStr)
|
||||||
|
}
|
||||||
|
dns = append(dns, dnsIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &parsedIPv6Config{
|
||||||
|
prefix: *ipNet,
|
||||||
|
gateway: gateway,
|
||||||
|
dns: dns,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv4Address applies IPv4 address to interface
|
||||||
|
func (scm *StaticConfigManager) applyIPv4Address(iface *link.Link, config *parsedIPv4Config) error {
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
|
||||||
|
// Remove existing IPv4 addresses
|
||||||
|
if err := netlinkMgr.RemoveAllAddresses(iface, link.AfInet); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove existing IPv4 addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new address
|
||||||
|
addr := &netlink.Addr{
|
||||||
|
IPNet: &config.network,
|
||||||
|
}
|
||||||
|
if err := netlinkMgr.AddrAdd(iface, addr); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IPv4 address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scm.logger.Info().Str("address", config.network.String()).Msg("IPv4 address applied")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv6Address applies IPv6 address to interface
|
||||||
|
func (scm *StaticConfigManager) applyIPv6Address(iface *link.Link, config *parsedIPv6Config) error {
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
|
||||||
|
// Remove existing global IPv6 addresses
|
||||||
|
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(iface); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove existing IPv6 addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new address
|
||||||
|
addr := &netlink.Addr{
|
||||||
|
IPNet: &config.prefix,
|
||||||
|
}
|
||||||
|
if err := netlinkMgr.AddrAdd(iface, addr); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IPv6 address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scm.logger.Info().Str("address", config.prefix.String()).Msg("IPv6 address applied")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv4Route applies IPv4 default route
|
||||||
|
func (scm *StaticConfigManager) applyIPv4Route(iface *link.Link, config *parsedIPv4Config) error {
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
|
||||||
|
// Check if default route already exists
|
||||||
|
if netlinkMgr.HasDefaultRoute(link.AfInet) {
|
||||||
|
scm.logger.Info().Msg("IPv4 default route already exists")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default route
|
||||||
|
if err := netlinkMgr.AddDefaultRoute(iface, config.gateway, link.AfInet); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IPv4 default route: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scm.logger.Info().Str("gateway", config.gateway.String()).Msg("IPv4 default route applied")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyIPv6Route applies IPv6 default route
|
||||||
|
func (scm *StaticConfigManager) applyIPv6Route(iface *link.Link, config *parsedIPv6Config) error {
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
|
||||||
|
// Check if default route already exists
|
||||||
|
if netlinkMgr.HasDefaultRoute(link.AfInet6) {
|
||||||
|
scm.logger.Info().Msg("IPv6 default route already exists")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default route
|
||||||
|
if err := netlinkMgr.AddDefaultRoute(iface, config.gateway, link.AfInet6); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IPv6 default route: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scm.logger.Info().Str("gateway", config.gateway.String()).Msg("IPv6 default route applied")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeIPv4DefaultRoute removes IPv4 default route
|
||||||
|
func (scm *StaticConfigManager) removeIPv4DefaultRoute() error {
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
return netlinkMgr.RemoveDefaultRoute(link.AfInet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableIPv6 enables IPv6 on the interface
|
||||||
|
func (scm *StaticConfigManager) enableIPv6() error {
|
||||||
|
netlinkMgr := getNetlinkManager()
|
||||||
|
return netlinkMgr.EnableIPv6(scm.ifaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsedIPv4Config represents parsed IPv4 configuration
|
||||||
|
type parsedIPv4Config struct {
|
||||||
|
network net.IPNet
|
||||||
|
gateway net.IP
|
||||||
|
dns []net.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsedIPv6Config represents parsed IPv6 configuration
|
||||||
|
type parsedIPv6Config struct {
|
||||||
|
prefix net.IPNet
|
||||||
|
gateway net.IP
|
||||||
|
dns []net.IP
|
||||||
|
}
|
||||||
|
|
@ -44,7 +44,7 @@ func initTimeSync() {
|
||||||
Logger: timesyncLogger,
|
Logger: timesyncLogger,
|
||||||
NetworkConfig: config.NetworkConfig,
|
NetworkConfig: config.NetworkConfig,
|
||||||
PreCheckFunc: func() (bool, error) {
|
PreCheckFunc: func() (bool, error) {
|
||||||
if !networkState.IsOnline() {
|
if !networkManager.IsOnline() {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
|
||||||
|
|
@ -5856,6 +5856,25 @@
|
||||||
"react": "^19.1.1"
|
"react": "^19.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
<<<<<<< Updated upstream
|
||||||
|
=======
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
>>>>>>> Stashed changes
|
||||||
"node_modules/react-hot-toast": {
|
"node_modules/react-hot-toast": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.3",
|
||||||
"react-simple-keyboard": "^3.8.125",
|
"react-simple-keyboard": "^3.8.125",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import {
|
import { CheckCircleIcon } from "@heroicons/react/24/outline";
|
||||||
CheckCircleIcon,
|
import { CloseButton } from "@headlessui/react";
|
||||||
ExclamationTriangleIcon,
|
import { LuInfo, LuOctagonAlert, LuTriangleAlert } from "react-icons/lu";
|
||||||
InformationCircleIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
type Variant = "danger" | "success" | "warning" | "info";
|
type Variant = "danger" | "success" | "warning" | "info";
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
|
|
@ -24,27 +21,27 @@ interface ConfirmDialogProps {
|
||||||
|
|
||||||
const variantConfig = {
|
const variantConfig = {
|
||||||
danger: {
|
danger: {
|
||||||
icon: ExclamationTriangleIcon,
|
icon: LuOctagonAlert,
|
||||||
iconClass: "text-red-600",
|
iconClass: "text-red-600",
|
||||||
iconBgClass: "bg-red-100",
|
iconBgClass: "bg-red-100 border border-red-500/90",
|
||||||
buttonTheme: "danger",
|
buttonTheme: "danger",
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
icon: CheckCircleIcon,
|
icon: CheckCircleIcon,
|
||||||
iconClass: "text-green-600",
|
iconClass: "text-green-600",
|
||||||
iconBgClass: "bg-green-100",
|
iconBgClass: "bg-green-100 border border-green-500/90",
|
||||||
buttonTheme: "primary",
|
buttonTheme: "primary",
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
icon: ExclamationTriangleIcon,
|
icon: LuTriangleAlert,
|
||||||
iconClass: "text-yellow-600",
|
iconClass: "text-yellow-600",
|
||||||
iconBgClass: "bg-yellow-100",
|
iconBgClass: "bg-yellow-100 border border-yellow-500/90",
|
||||||
buttonTheme: "lightDanger",
|
buttonTheme: "primary",
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
icon: InformationCircleIcon,
|
icon: LuInfo,
|
||||||
iconClass: "text-blue-600",
|
iconClass: "text-blue-600",
|
||||||
iconBgClass: "bg-blue-100",
|
iconBgClass: "bg-blue-100 border border-blue-500/90",
|
||||||
buttonTheme: "primary",
|
buttonTheme: "primary",
|
||||||
},
|
},
|
||||||
} as Record<
|
} as Record<
|
||||||
|
|
@ -94,12 +91,13 @@ export function ConfirmDialog({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-x-2">
|
<div className="flex justify-end gap-x-2" autoFocus>
|
||||||
{cancelText && (
|
{cancelText && (
|
||||||
<Button size="SM" theme="blank" text={cancelText} onClick={onClose} />
|
<CloseButton as={Button} size="SM" theme="blank" text={cancelText} />
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
|
type="button"
|
||||||
theme={buttonTheme}
|
theme={buttonTheme}
|
||||||
text={isConfirming ? `${confirmText}...` : confirmText}
|
text={isConfirming ? `${confirmText}...` : confirmText}
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,48 @@ import { GridCard } from "@/components/Card";
|
||||||
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
||||||
import { NetworkState } from "@/hooks/stores";
|
import { NetworkState } from "@/hooks/stores";
|
||||||
|
|
||||||
|
import EmptyCard from "./EmptyCard";
|
||||||
|
|
||||||
export default function DhcpLeaseCard({
|
export default function DhcpLeaseCard({
|
||||||
networkState,
|
networkState,
|
||||||
setShowRenewLeaseConfirm,
|
setShowRenewLeaseConfirm,
|
||||||
}: {
|
}: {
|
||||||
networkState: NetworkState;
|
networkState: NetworkState | null;
|
||||||
setShowRenewLeaseConfirm: (show: boolean) => void;
|
setShowRenewLeaseConfirm: (show: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const isDhcpLeaseEmpty = Object.keys(networkState?.dhcp_lease || {}).length === 0;
|
||||||
|
|
||||||
|
if (isDhcpLeaseEmpty) {
|
||||||
|
return (
|
||||||
|
<EmptyCard
|
||||||
|
headline="No DHCP Lease information"
|
||||||
|
description="We haven't received any DHCP lease information from the device yet."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black dark:text-white">
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
DHCP Lease Information
|
DHCP Lease Information
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
type="button"
|
||||||
|
className="text-red-500"
|
||||||
|
text="Renew DHCP Lease"
|
||||||
|
LeadingIcon={LuRefreshCcw}
|
||||||
|
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-x-6 gap-y-2">
|
<div className="flex gap-x-6 gap-y-2">
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
{networkState?.dhcp_lease?.ip && (
|
{networkState?.dhcp_lease?.ip && (
|
||||||
|
|
@ -44,24 +71,15 @@ export default function DhcpLeaseCard({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.dns && (
|
{networkState?.dhcp_lease?.dns_servers && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
DNS Servers
|
DNS Servers
|
||||||
</span>
|
</span>
|
||||||
<span className="text-right text-sm font-medium">
|
<span className="text-right text-sm font-medium">
|
||||||
{networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
|
{networkState?.dhcp_lease?.dns_servers.map(dns => (
|
||||||
</span>
|
<div key={dns}>{dns}</div>
|
||||||
</div>
|
))}
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.broadcast && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Broadcast
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.broadcast}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -142,6 +160,17 @@ export default function DhcpLeaseCard({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.broadcast && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Broadcast
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.broadcast}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.mtu && (
|
{networkState?.dhcp_lease?.mtu && (
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
||||||
|
|
@ -194,17 +223,6 @@ export default function DhcpLeaseCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="light"
|
|
||||||
className="text-red-500"
|
|
||||||
text="Renew DHCP Lease"
|
|
||||||
LeadingIcon={LuRefreshCcw}
|
|
||||||
onClick={() => setShowRenewLeaseConfirm(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { GridCard } from "./Card";
|
||||||
export default function Ipv6NetworkCard({
|
export default function Ipv6NetworkCard({
|
||||||
networkState,
|
networkState,
|
||||||
}: {
|
}: {
|
||||||
networkState: NetworkState;
|
networkState: NetworkState | undefined;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
|
|
@ -17,7 +17,7 @@ export default function Ipv6NetworkCard({
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||||
{networkState?.ipv6_link_local && (
|
{networkState?.dhcp_lease?.ip && (
|
||||||
<div className="flex flex-col justify-between">
|
<div className="flex flex-col justify-between">
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Link-local
|
Link-local
|
||||||
|
|
@ -33,8 +33,7 @@ export default function Ipv6NetworkCard({
|
||||||
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (
|
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">IPv6 Addresses</h4>
|
<h4 className="text-sm font-semibold">IPv6 Addresses</h4>
|
||||||
{networkState.ipv6_addresses.map(
|
{networkState.ipv6_addresses.map(addr => (
|
||||||
addr => (
|
|
||||||
<div
|
<div
|
||||||
key={addr.address}
|
key={addr.address}
|
||||||
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent"
|
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent"
|
||||||
|
|
@ -81,8 +80,7 @@ export default function Ipv6NetworkCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,19 @@ import { ReactNode } from "react";
|
||||||
export function SettingsPageHeader({
|
export function SettingsPageHeader({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
action,
|
||||||
}: {
|
}: {
|
||||||
title: string | ReactNode;
|
title: string | ReactNode;
|
||||||
description: string | ReactNode;
|
description: string | ReactNode;
|
||||||
|
action?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="select-none">
|
<div className="flex items-center justify-between gap-x-2 select-none">
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
<h2 className="text-xl font-extrabold text-black dark:text-white">{title}</h2>
|
<h2 className="text-xl font-extrabold text-black dark:text-white">{title}</h2>
|
||||||
<div className="text-sm text-black dark:text-slate-300">{description}</div>
|
<div className="text-sm text-black dark:text-slate-300">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{action && <div className="">{action}</div>}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { LuPlus, LuX } from "react-icons/lu";
|
||||||
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import validator from "validator";
|
||||||
|
|
||||||
|
import { GridCard } from "@/components/Card";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
|
import { NetworkSettings } from "@/hooks/stores";
|
||||||
|
|
||||||
|
export default function StaticIpv4Card() {
|
||||||
|
const formMethods = useFormContext<NetworkSettings>();
|
||||||
|
const { register, formState, watch } = formMethods;
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" });
|
||||||
|
|
||||||
|
// TODO: set subnet mask if IP address is in CIDR notation
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fields.length === 0) append("");
|
||||||
|
}, [append, fields.length]);
|
||||||
|
|
||||||
|
const dns = watch("ipv4_static.dns");
|
||||||
|
|
||||||
|
const validate = (value: string) => {
|
||||||
|
if (!validator.isIP(value)) return "Invalid IP address";
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCard>
|
||||||
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
Static IPv4 Configuration
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="IP Address"
|
||||||
|
type="text"
|
||||||
|
size="SM"
|
||||||
|
placeholder="192.168.1.100"
|
||||||
|
{...register("ipv4_static.address", { validate })}
|
||||||
|
error={formState.errors.ipv4_static?.address?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="Subnet Mask"
|
||||||
|
type="text"
|
||||||
|
size="SM"
|
||||||
|
placeholder="255.255.255.0"
|
||||||
|
{...register("ipv4_static.netmask", { validate })}
|
||||||
|
error={formState.errors.ipv4_static?.netmask?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="Gateway"
|
||||||
|
type="text"
|
||||||
|
size="SM"
|
||||||
|
placeholder="192.168.1.1"
|
||||||
|
{...register("ipv4_static.gateway", { validate })}
|
||||||
|
error={formState.errors.ipv4_static?.gateway?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* DNS server fields */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((dns, index) => {
|
||||||
|
return (
|
||||||
|
<div key={dns.id}>
|
||||||
|
<div className="flex items-start gap-x-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label={index === 0 ? "DNS Server" : null}
|
||||||
|
type="text"
|
||||||
|
size="SM"
|
||||||
|
placeholder="1.1.1.1"
|
||||||
|
{...register(`ipv4_static.dns.${index}`, { validate })}
|
||||||
|
error={formState.errors.ipv4_static?.dns?.[index]?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{index > 0 && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
type="button"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
LeadingIcon={LuX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
onClick={() => append("", { shouldFocus: true })}
|
||||||
|
LeadingIcon={LuPlus}
|
||||||
|
type="button"
|
||||||
|
text="Add DNS Server"
|
||||||
|
disabled={dns[0] === ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { LuPlus, LuX } from "react-icons/lu";
|
||||||
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import validator from "validator";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { GridCard } from "@/components/Card";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
|
import { NetworkSettings } from "@/hooks/stores";
|
||||||
|
|
||||||
|
export default function StaticIpv6Card() {
|
||||||
|
const formMethods = useFormContext<NetworkSettings>();
|
||||||
|
const { register, formState, watch } = formMethods;
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({ name: "ipv6_static.dns" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fields.length === 0) append("");
|
||||||
|
}, [append, fields.length]);
|
||||||
|
|
||||||
|
const dns = watch("ipv6_static.dns");
|
||||||
|
|
||||||
|
const cidrValidation = (value: string) => {
|
||||||
|
if (value === "") return true;
|
||||||
|
|
||||||
|
// Check if it's a valid IPv6 address with CIDR notation
|
||||||
|
const parts = value.split("/");
|
||||||
|
if (parts.length !== 2) return "Please use CIDR notation (e.g., 2001:db8::1/64)";
|
||||||
|
|
||||||
|
const [address, prefix] = parts;
|
||||||
|
if (!validator.isIP(address, 6)) return "Invalid IPv6 address";
|
||||||
|
const prefixNum = parseInt(prefix);
|
||||||
|
if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) {
|
||||||
|
return "Prefix must be between 0 and 128";
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ipv6Validation = (value: string) => {
|
||||||
|
if (!validator.isIP(value, 6)) return "Invalid IPv6 address";
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCard>
|
||||||
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
Static IPv6 Configuration
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="IP Prefix"
|
||||||
|
type="text"
|
||||||
|
size="SM"
|
||||||
|
placeholder="2001:db8::1/64"
|
||||||
|
{...register("ipv6_static.prefix", { validate: cidrValidation })}
|
||||||
|
error={formState.errors.ipv6_static?.prefix?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label="Gateway"
|
||||||
|
type="text"
|
||||||
|
size="SM"
|
||||||
|
placeholder="2001:db8::1"
|
||||||
|
{...register("ipv6_static.gateway", { validate: ipv6Validation })}
|
||||||
|
error={formState.errors.ipv6_static?.gateway?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* DNS server fields */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((dns, index) => {
|
||||||
|
return (
|
||||||
|
<div key={dns.id}>
|
||||||
|
<div className="flex items-start gap-x-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<InputFieldWithLabel
|
||||||
|
label={index === 0 ? "DNS Server" : null}
|
||||||
|
type="text"
|
||||||
|
size="SM"
|
||||||
|
placeholder="2001:4860:4860::8888"
|
||||||
|
{...register(`ipv6_static.dns.${index}`, {
|
||||||
|
validate: ipv6Validation,
|
||||||
|
})}
|
||||||
|
error={formState.errors.ipv6_static?.dns?.[index]?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{index > 0 && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
type="button"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
LeadingIcon={LuX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
onClick={() => append("", { shouldFocus: true })}
|
||||||
|
LeadingIcon={LuPlus}
|
||||||
|
type="button"
|
||||||
|
text="Add DNS Server"
|
||||||
|
disabled={dns[0] === ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -672,6 +672,7 @@ export interface DhcpLease {
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
routers?: string[];
|
routers?: string[];
|
||||||
dns?: string[];
|
dns?: string[];
|
||||||
|
dns_servers?: string[];
|
||||||
ntp_servers?: string[];
|
ntp_servers?: string[];
|
||||||
lpr_servers?: string[];
|
lpr_servers?: string[];
|
||||||
_time_servers?: string[];
|
_time_servers?: string[];
|
||||||
|
|
@ -732,12 +733,27 @@ export type TimeSyncMode =
|
||||||
| "custom"
|
| "custom"
|
||||||
| "unknown";
|
| "unknown";
|
||||||
|
|
||||||
|
export interface IPv4StaticConfig {
|
||||||
|
address: string;
|
||||||
|
netmask: string;
|
||||||
|
gateway: string;
|
||||||
|
dns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPv6StaticConfig {
|
||||||
|
prefix: string;
|
||||||
|
gateway: string;
|
||||||
|
dns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface NetworkSettings {
|
export interface NetworkSettings {
|
||||||
hostname: string;
|
hostname: string | null;
|
||||||
domain: string;
|
domain: string | null;
|
||||||
http_proxy: string;
|
http_proxy: string | null;
|
||||||
ipv4_mode: IPv4Mode;
|
ipv4_mode: IPv4Mode;
|
||||||
|
ipv4_static?: IPv4StaticConfig;
|
||||||
ipv6_mode: IPv6Mode;
|
ipv6_mode: IPv6Mode;
|
||||||
|
ipv6_static?: IPv6StaticConfig;
|
||||||
lldp_mode: LLDPMode;
|
lldp_mode: LLDPMode;
|
||||||
lldp_tx_tlvs: string[];
|
lldp_tx_tlvs: string[];
|
||||||
mdns_mode: mDNSMode;
|
mdns_mode: mDNSMode;
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInStill {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideUpFade {
|
@keyframes slideUpFade {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,48 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { FieldValues, FormProvider, useForm } from "react-hook-form";
|
||||||
import { LuEthernetPort } from "react-icons/lu";
|
import { LuEthernetPort } from "react-icons/lu";
|
||||||
|
import validator from "validator";
|
||||||
|
|
||||||
import {
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
IPv4Mode,
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
IPv6Mode,
|
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||||
LLDPMode,
|
import { NetworkSettings, NetworkState, useRTCStore } from "@/hooks/stores";
|
||||||
mDNSMode,
|
import notifications from "@/notifications";
|
||||||
NetworkSettings,
|
import { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
||||||
NetworkState,
|
|
||||||
TimeSyncMode,
|
|
||||||
useNetworkStateStore,
|
|
||||||
} from "@/hooks/stores";
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
||||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
|
||||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
|
||||||
import Fieldset from "@/components/Fieldset";
|
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
||||||
import { SettingsItem } from "@components/SettingsItem";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
|
|
||||||
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
|
||||||
import EmptyCard from "../components/EmptyCard";
|
|
||||||
import AutoHeight from "../components/AutoHeight";
|
import AutoHeight from "../components/AutoHeight";
|
||||||
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
||||||
|
import EmptyCard from "../components/EmptyCard";
|
||||||
|
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
||||||
|
import StaticIpv4Card from "../components/StaticIpv4Card";
|
||||||
|
import StaticIpv6Card from "../components/StaticIpv6Card";
|
||||||
|
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||||
|
import { SettingsItem } from "../components/SettingsItem";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const defaultNetworkSettings: NetworkSettings = {
|
const resolveOnRtcReady = () => {
|
||||||
hostname: "",
|
return new Promise(resolve => {
|
||||||
http_proxy: "",
|
// Check if RTC is already connected
|
||||||
domain: "",
|
const currentState = useRTCStore.getState();
|
||||||
ipv4_mode: "unknown",
|
if (currentState.rpcDataChannel?.readyState === "open") {
|
||||||
ipv6_mode: "unknown",
|
// Already connected, fetch data immediately
|
||||||
lldp_mode: "unknown",
|
return resolve(void 0);
|
||||||
lldp_tx_tlvs: [],
|
}
|
||||||
mdns_mode: "unknown",
|
|
||||||
time_sync_mode: "unknown",
|
// Not connected yet, subscribe to state changes
|
||||||
|
const unsubscribe = useRTCStore.subscribe(state => {
|
||||||
|
if (state.rpcDataChannel?.readyState === "open") {
|
||||||
|
unsubscribe(); // Clean up subscription
|
||||||
|
return resolve(void 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||||
|
|
@ -72,158 +74,170 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||||
|
|
||||||
export default function SettingsNetworkRoute() {
|
export default function SettingsNetworkRoute() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [
|
|
||||||
state,
|
|
||||||
state.setNetworkState,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [networkSettings, setNetworkSettings] =
|
const [networkState, setNetworkState] = useState<NetworkState | null>(null);
|
||||||
useState<NetworkSettings>(defaultNetworkSettings);
|
|
||||||
|
|
||||||
// We use this to determine whether the settings have changed
|
|
||||||
const firstNetworkSettings = useRef<NetworkSettings | undefined>(undefined);
|
|
||||||
|
|
||||||
const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false);
|
|
||||||
|
|
||||||
|
// Some input needs direct state management. Mostly options that open more details
|
||||||
const [customDomain, setCustomDomain] = useState<string>("");
|
const [customDomain, setCustomDomain] = useState<string>("");
|
||||||
const [selectedDomainOption, setSelectedDomainOption] = useState<string>("dhcp");
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Confirm dialog
|
||||||
if (networkSettings.domain && networkSettingsLoaded) {
|
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
|
||||||
// Check if the domain is one of the predefined options
|
const initialSettingsRef = useRef<NetworkSettings | null>(null);
|
||||||
const predefinedOptions = ["dhcp", "local"];
|
|
||||||
if (predefinedOptions.includes(networkSettings.domain)) {
|
|
||||||
setSelectedDomainOption(networkSettings.domain);
|
|
||||||
} else {
|
|
||||||
setSelectedDomainOption("custom");
|
|
||||||
setCustomDomain(networkSettings.domain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [networkSettings.domain, networkSettingsLoaded]);
|
|
||||||
|
|
||||||
const getNetworkSettings = useCallback(() => {
|
const [showCriticalSettingsConfirm, setShowCriticalSettingsConfirm] = useState(false);
|
||||||
setNetworkSettingsLoaded(false);
|
const [stagedSettings, setStagedSettings] = useState<NetworkSettings | null>(null);
|
||||||
send("getNetworkSettings", {}, (resp: JsonRpcResponse) => {
|
const [criticalChanges, setCriticalChanges] = useState<
|
||||||
if ("error" in resp) return;
|
{ label: string; from: string; to: string }[]
|
||||||
const networkSettings = resp.result as NetworkSettings;
|
>([]);
|
||||||
console.debug("Network settings: ", networkSettings);
|
|
||||||
setNetworkSettings(networkSettings);
|
|
||||||
|
|
||||||
if (!firstNetworkSettings.current) {
|
const fetchNetworkData = useCallback(async () => {
|
||||||
firstNetworkSettings.current = networkSettings;
|
try {
|
||||||
}
|
console.log("Fetching network data...");
|
||||||
setNetworkSettingsLoaded(true);
|
|
||||||
});
|
|
||||||
}, [send]);
|
|
||||||
|
|
||||||
const getNetworkState = useCallback(() => {
|
const [settings, state] = (await Promise.all([
|
||||||
send("getNetworkState", {}, (resp: JsonRpcResponse) => {
|
getNetworkSettings(),
|
||||||
if ("error" in resp) return;
|
getNetworkState(),
|
||||||
const networkState = resp.result as NetworkState;
|
])) as [NetworkSettings, NetworkState];
|
||||||
console.debug("Network state:", networkState);
|
|
||||||
setNetworkState(networkState);
|
|
||||||
});
|
|
||||||
}, [send, setNetworkState]);
|
|
||||||
|
|
||||||
const setNetworkSettingsRemote = useCallback(
|
setNetworkState(state as NetworkState);
|
||||||
(settings: NetworkSettings) => {
|
|
||||||
setNetworkSettingsLoaded(false);
|
const settingsWithDefaults = {
|
||||||
send("setNetworkSettings", { settings }, (resp: JsonRpcResponse) => {
|
...settings,
|
||||||
if ("error" in resp) {
|
|
||||||
notifications.error(
|
domain: settings.domain || "local", // TODO: null means local domain TRUE?????
|
||||||
"Failed to save network settings: " +
|
mdns_mode: settings.mdns_mode || "disabled",
|
||||||
(resp.error.data ? resp.error.data : resp.error.message),
|
time_sync_mode: settings.time_sync_mode || "ntp_only",
|
||||||
);
|
ipv4_static: {
|
||||||
setNetworkSettingsLoaded(true);
|
address: settings.ipv4_static?.address || state.dhcp_lease?.ip || "",
|
||||||
return;
|
netmask: settings.ipv4_static?.netmask || state.dhcp_lease?.netmask || "",
|
||||||
}
|
gateway: settings.ipv4_static?.gateway || state.dhcp_lease?.routers?.[0] || "",
|
||||||
const networkSettings = resp.result as NetworkSettings;
|
dns: settings.ipv4_static?.dns || state.dhcp_lease?.dns_servers || [],
|
||||||
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
|
|
||||||
firstNetworkSettings.current = networkSettings;
|
|
||||||
setNetworkSettings(networkSettings);
|
|
||||||
getNetworkState();
|
|
||||||
setNetworkSettingsLoaded(true);
|
|
||||||
notifications.success("Network settings saved");
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[getNetworkState, send],
|
ipv6_static: {
|
||||||
);
|
prefix: settings.ipv6_static?.prefix || state.ipv6_addresses?.[0]?.prefix || "",
|
||||||
|
gateway: settings.ipv6_static?.gateway || "",
|
||||||
|
dns: settings.ipv6_static?.dns || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const handleRenewLease = useCallback(() => {
|
initialSettingsRef.current = settingsWithDefaults;
|
||||||
send("renewDHCPLease", {}, (resp: JsonRpcResponse) => {
|
return { settings: settingsWithDefaults, state };
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(err instanceof Error ? err.message : "Unknown error");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formMethods = useForm<NetworkSettings>({
|
||||||
|
mode: "onBlur",
|
||||||
|
|
||||||
|
defaultValues: async () => {
|
||||||
|
console.log("Preparing form default values...");
|
||||||
|
|
||||||
|
// Ensure data channel is ready, before fetching network data from the device
|
||||||
|
await resolveOnRtcReady();
|
||||||
|
|
||||||
|
const { settings } = await fetchNetworkData();
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prepareSettings = (data: FieldValues) => {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
|
||||||
|
// If custom domain option is selected, use the custom domain as value
|
||||||
|
domain: data.domain === "custom" ? customDomain : data.domain,
|
||||||
|
} as NetworkSettings;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { register, handleSubmit, watch, formState, reset } = formMethods;
|
||||||
|
|
||||||
|
const onSubmit = async (settings: NetworkSettings) => {
|
||||||
|
send("setNetworkSettings", { settings }, async (resp: any) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
return notifications.error(
|
||||||
|
resp.error.data ? resp.error.data : resp.error.message,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If the settings are saved successfully, fetch the latest network data and reset the form
|
||||||
|
// We do this so we get all the form state values, for stuff like is the form dirty, etc...
|
||||||
|
const networkData = await fetchNetworkData();
|
||||||
|
reset(networkData.settings);
|
||||||
|
notifications.success("Network settings saved");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitGate = async (data: FieldValues) => {
|
||||||
|
const settings = prepareSettings(data);
|
||||||
|
const dirty = formState.dirtyFields;
|
||||||
|
|
||||||
|
// These fields will prompt a confirm dialog, all else save immediately
|
||||||
|
const criticalFields = [
|
||||||
|
// Label is for the UI, key is the internal key of the field
|
||||||
|
{ label: "IPv4 mode", key: "ipv4_mode" },
|
||||||
|
{ label: "IPv6 mode", key: "ipv6_mode" },
|
||||||
|
] as { label: string; key: keyof NetworkSettings }[];
|
||||||
|
|
||||||
|
const criticalChanged = criticalFields.some(field => dirty[field.key]);
|
||||||
|
|
||||||
|
// If no critical fields are changed, save immediately
|
||||||
|
if (!criticalChanged) return onSubmit(settings);
|
||||||
|
|
||||||
|
const changes = new Set<{ label: string; from: string; to: string }>();
|
||||||
|
criticalFields.forEach(field => {
|
||||||
|
const { key, label } = field;
|
||||||
|
if (dirty[key]) {
|
||||||
|
const from = initialSettingsRef?.current?.[key] as string;
|
||||||
|
const to = data[key] as string;
|
||||||
|
changes.add({ label, from, to });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setStagedSettings(settings);
|
||||||
|
setCriticalChanges(Array.from(changes));
|
||||||
|
setShowCriticalSettingsConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ipv4mode = watch("ipv4_mode");
|
||||||
|
const ipv6mode = watch("ipv6_mode");
|
||||||
|
|
||||||
|
const onDhcpLeaseRenew = () => {
|
||||||
|
send("renewDHCPLease", {}, (resp: any) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error("Failed to renew lease: " + resp.error.message);
|
notifications.error("Failed to renew lease: " + resp.error.message);
|
||||||
} else {
|
} else {
|
||||||
notifications.success("DHCP lease renewed");
|
notifications.success("DHCP lease renewed");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [send]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getNetworkState();
|
|
||||||
getNetworkSettings();
|
|
||||||
}, [getNetworkState, getNetworkSettings]);
|
|
||||||
|
|
||||||
const handleIpv4ModeChange = (value: IPv4Mode | string) => {
|
|
||||||
setNetworkSettingsRemote({ ...networkSettings, ipv4_mode: value as IPv4Mode });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIpv6ModeChange = (value: IPv6Mode | string) => {
|
|
||||||
setNetworkSettingsRemote({ ...networkSettings, ipv6_mode: value as IPv6Mode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLldpModeChange = (value: LLDPMode | string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMdnsModeChange = (value: mDNSMode | string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeSyncModeChange = (value: TimeSyncMode | string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHostnameChange = (value: string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, hostname: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProxyChange = (value: string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, http_proxy: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDomainChange = (value: string) => {
|
|
||||||
setNetworkSettings({ ...networkSettings, domain: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDomainOptionChange = (value: string) => {
|
|
||||||
setSelectedDomainOption(value);
|
|
||||||
if (value !== "custom") {
|
|
||||||
handleDomainChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCustomDomainChange = (value: string) => {
|
|
||||||
setCustomDomain(value);
|
|
||||||
handleDomainChange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterUnknown = useCallback(
|
|
||||||
(options: { value: string; label: string }[]) => {
|
|
||||||
if (!networkSettingsLoaded) return options;
|
|
||||||
return options.filter(option => option.value !== "unknown");
|
|
||||||
},
|
|
||||||
[networkSettingsLoaded],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Fieldset disabled={!networkSettingsLoaded} className="space-y-4">
|
<FormProvider {...formMethods}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmitGate)} className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Network"
|
title="Network"
|
||||||
description="Configure your network settings"
|
description="Configure the network settings for the device"
|
||||||
|
action={
|
||||||
|
<>
|
||||||
|
|
||||||
|
{(formState.isDirty || formState.isSubmitting) && (
|
||||||
|
// <div className="animate-fadeInStill opacity-1 animation-duration-300">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="primary"
|
||||||
|
disabled={formState.isSubmitting}
|
||||||
|
loading={formState.isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
|
|
@ -239,49 +253,29 @@ export default function SettingsNetworkRoute() {
|
||||||
className="dark:!text-opacity-60"
|
className="dark:!text-opacity-60"
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
<SettingsItem title="Hostname" description="Set the device hostname">
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem
|
|
||||||
title="Hostname"
|
|
||||||
description="Device identifier on the network. Blank for system default"
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<div>
|
|
||||||
<InputField
|
<InputField
|
||||||
size="SM"
|
size="SM"
|
||||||
type="text"
|
{...register("hostname")}
|
||||||
placeholder="jetkvm"
|
error={formState.errors.hostname?.message}
|
||||||
defaultValue={networkSettings.hostname}
|
|
||||||
onChange={e => {
|
|
||||||
handleHostnameChange(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
<SettingsItem title="HTTP Proxy" description="Configure HTTP proxy settings">
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem
|
|
||||||
title="HTTP Proxy"
|
|
||||||
description="Proxy server for outgoing HTTP(S) requests from the device. Blank for none."
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<div>
|
|
||||||
<InputField
|
<InputField
|
||||||
size="SM"
|
size="SM"
|
||||||
type="text"
|
placeholder="http://proxy.example.com:8080"
|
||||||
placeholder="http://proxy.example.com:8080/"
|
{...register("http_proxy", {
|
||||||
defaultValue={networkSettings.http_proxy}
|
validate: (value: string | null) => {
|
||||||
onChange={e => {
|
if (value === "" || value === null) return true;
|
||||||
handleProxyChange(e.target.value);
|
if (!validator.isURL(value || "", { protocols: ["http", "https"] })) {
|
||||||
}}
|
return "Invalid HTTP proxy URL";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
error={formState.errors.http_proxy?.message}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Domain"
|
title="Domain"
|
||||||
|
|
@ -290,114 +284,94 @@ export default function SettingsNetworkRoute() {
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
value={selectedDomainOption}
|
|
||||||
onChange={e => handleDomainOptionChange(e.target.value)}
|
|
||||||
options={[
|
options={[
|
||||||
{ value: "dhcp", label: "DHCP provided" },
|
{ value: "dhcp", label: "DHCP provided" },
|
||||||
{ value: "local", label: ".local" },
|
{ value: "local", label: ".local" },
|
||||||
{ value: "custom", label: "Custom" },
|
{ value: "custom", label: "Custom" },
|
||||||
]}
|
]}
|
||||||
|
{...register("domain")}
|
||||||
|
error={formState.errors.domain?.message}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{selectedDomainOption === "custom" && (
|
{watch("domain") === "custom" && (
|
||||||
<div className="mt-2 w-1/3 border-l border-slate-800/10 pl-4 dark:border-slate-300/20">
|
<div className="mt-2 w-1/3 border-l border-slate-800/10 pl-4 dark:border-slate-300/20">
|
||||||
<InputFieldWithLabel
|
<InputFieldWithLabel
|
||||||
size="SM"
|
size="SM"
|
||||||
type="text"
|
type="text"
|
||||||
label="Custom Domain"
|
label="Custom Domain"
|
||||||
placeholder="home"
|
placeholder="home"
|
||||||
value={customDomain}
|
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setCustomDomain(e.target.value);
|
setCustomDomain(e.target.value);
|
||||||
handleCustomDomainChange(e.target.value);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem
|
<SettingsItem title="mDNS Mode" description="Configure mDNS settings">
|
||||||
title="mDNS"
|
|
||||||
description="Control mDNS (multicast DNS) operational mode"
|
|
||||||
>
|
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkSettings.mdns_mode}
|
options={[
|
||||||
onChange={e => handleMdnsModeChange(e.target.value)}
|
|
||||||
options={filterUnknown([
|
|
||||||
{ value: "disabled", label: "Disabled" },
|
{ value: "disabled", label: "Disabled" },
|
||||||
{ value: "auto", label: "Auto" },
|
{ value: "auto", label: "Auto" },
|
||||||
{ value: "ipv4_only", label: "IPv4 only" },
|
{ value: "ipv4_only", label: "IPv4 only" },
|
||||||
{ value: "ipv6_only", label: "IPv6 only" },
|
{ value: "ipv6_only", label: "IPv6 only" },
|
||||||
])}
|
]}
|
||||||
|
{...register("mdns_mode")}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Time synchronization"
|
title="Time synchronization"
|
||||||
description="Configure time synchronization settings"
|
description="Configure time synchronization settings"
|
||||||
>
|
>
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkSettings.time_sync_mode}
|
options={[
|
||||||
onChange={e => {
|
|
||||||
handleTimeSyncModeChange(e.target.value);
|
|
||||||
}}
|
|
||||||
options={filterUnknown([
|
|
||||||
{ value: "unknown", label: "..." },
|
|
||||||
// { value: "auto", label: "Auto" },
|
|
||||||
{ value: "ntp_only", label: "NTP only" },
|
{ value: "ntp_only", label: "NTP only" },
|
||||||
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
||||||
{ value: "http_only", label: "HTTP only" },
|
{ value: "http_only", label: "HTTP only" },
|
||||||
// { value: "custom", label: "Custom" },
|
]}
|
||||||
])}
|
{...register("time_sync_mode")}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="primary"
|
|
||||||
disabled={firstNetworkSettings.current === networkSettings}
|
|
||||||
text="Save Settings"
|
|
||||||
onClick={() => setNetworkSettingsRemote(networkSettings)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkSettings.ipv4_mode}
|
options={[
|
||||||
onChange={e => handleIpv4ModeChange(e.target.value)}
|
|
||||||
options={filterUnknown([
|
|
||||||
{ value: "dhcp", label: "DHCP" },
|
{ value: "dhcp", label: "DHCP" },
|
||||||
// { value: "static", label: "Static" },
|
{ value: "static", label: "Static" },
|
||||||
])}
|
]}
|
||||||
|
{...register("ipv4_mode")}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
<div>
|
||||||
<AutoHeight>
|
<AutoHeight>
|
||||||
{!networkSettingsLoaded && !networkState?.dhcp_lease ? (
|
{formState.isLoading ? (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<div className="h-6 w-1/3 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
DHCP Lease Information
|
<div className="animate-pulse space-y-2">
|
||||||
</h3>
|
<div className="h-4 w-1/4 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
<div className="animate-pulse space-y-3">
|
|
||||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
|
||||||
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
|
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
|
<div className="h-4 w-1/4 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
) : networkState?.dhcp_lease && networkState.dhcp_lease.ip ? (
|
) : ipv4mode === "static" ? (
|
||||||
|
<StaticIpv4Card />
|
||||||
|
) : ipv4mode === "dhcp" && !!formState.dirtyFields.ipv4_mode ? (
|
||||||
|
<EmptyCard
|
||||||
|
IconElm={LuEthernetPort}
|
||||||
|
headline="Pending DHCP IPv4 mode change"
|
||||||
|
description="Save settings to enable DHCP mode and view lease information"
|
||||||
|
/>
|
||||||
|
) : ipv4mode === "dhcp" ? (
|
||||||
<DhcpLeaseCard
|
<DhcpLeaseCard
|
||||||
networkState={networkState}
|
networkState={networkState}
|
||||||
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
||||||
|
|
@ -405,36 +379,31 @@ export default function SettingsNetworkRoute() {
|
||||||
) : (
|
) : (
|
||||||
<EmptyCard
|
<EmptyCard
|
||||||
IconElm={LuEthernetPort}
|
IconElm={LuEthernetPort}
|
||||||
headline="DHCP Information"
|
headline="Network Information"
|
||||||
description="No DHCP lease information available"
|
description="No network configuration available"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoHeight>
|
</AutoHeight>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkSettings.ipv6_mode}
|
options={[
|
||||||
onChange={e => handleIpv6ModeChange(e.target.value)}
|
|
||||||
options={filterUnknown([
|
|
||||||
{ value: "disabled", label: "Disabled" },
|
|
||||||
{ value: "slaac", label: "SLAAC" },
|
{ value: "slaac", label: "SLAAC" },
|
||||||
// { value: "dhcpv6", label: "DHCPv6" },
|
{ value: "static", label: "Static" },
|
||||||
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
]}
|
||||||
// { value: "static", label: "Static" },
|
{...register("ipv6_mode")}
|
||||||
// { value: "link_local", label: "Link-local only" },
|
|
||||||
])}
|
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
<div className="space-y-4">
|
||||||
<AutoHeight>
|
<AutoHeight>
|
||||||
{!networkSettingsLoaded &&
|
{!networkState ? (
|
||||||
!(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? (
|
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
IPv6 Information
|
IPv6 Network Information
|
||||||
</h3>
|
</h3>
|
||||||
<div className="animate-pulse space-y-3">
|
<div className="animate-pulse space-y-3">
|
||||||
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
|
|
@ -444,46 +413,118 @@ export default function SettingsNetworkRoute() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
) : networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0 ? (
|
) : ipv6mode === "static" ? (
|
||||||
<Ipv6NetworkCard networkState={networkState} />
|
<StaticIpv6Card />
|
||||||
) : (
|
) : (
|
||||||
<EmptyCard
|
<Ipv6NetworkCard networkState={networkState || undefined} />
|
||||||
IconElm={LuEthernetPort}
|
|
||||||
headline="IPv6 Information"
|
|
||||||
description="No IPv6 addresses configured"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</AutoHeight>
|
</AutoHeight>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden space-y-4">
|
{(formState.isDirty || formState.isSubmitting) && (
|
||||||
<SettingsItem
|
<>
|
||||||
title="LLDP"
|
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
description="Control which TLVs will be sent over Link Layer Discovery Protocol"
|
<div className="animate-fadeInStill opacity-0 animation-duration-300">
|
||||||
>
|
<Button
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkSettings.lldp_mode}
|
theme="primary"
|
||||||
onChange={e => handleLldpModeChange(e.target.value)}
|
disabled={formState.isSubmitting}
|
||||||
options={filterUnknown([
|
loading={formState.isSubmitting}
|
||||||
{ value: "disabled", label: "Disabled" },
|
type="submit"
|
||||||
{ value: "basic", label: "Basic" },
|
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||||
{ value: "all", label: "All" },
|
|
||||||
])}
|
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
|
||||||
</div>
|
</div>
|
||||||
</Fieldset>
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
|
{/* Critical change confirm */}
|
||||||
|
<ConfirmDialog
|
||||||
|
open={showCriticalSettingsConfirm}
|
||||||
|
title="Apply network settings"
|
||||||
|
variant="warning"
|
||||||
|
confirmText="Apply changes"
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowCriticalSettingsConfirm(false);
|
||||||
|
if (stagedSettings) onSubmit(stagedSettings);
|
||||||
|
|
||||||
|
// Wait for the close animation to finish before resetting the staged settings
|
||||||
|
setTimeout(() => {
|
||||||
|
setStagedSettings(null);
|
||||||
|
setCriticalChanges([]);
|
||||||
|
}, 500);
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
// close();
|
||||||
|
setShowCriticalSettingsConfirm(false);
|
||||||
|
}}
|
||||||
|
isConfirming={formState.isSubmitting}
|
||||||
|
description={
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p>
|
||||||
|
This will update the device's network configuration and may briefly
|
||||||
|
disconnect your session.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="rounded-md border border-slate-200 bg-slate-50 p-3 dark:border-slate-700 dark:bg-slate-900/40">
|
||||||
|
<div className="mb-2 text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400">
|
||||||
|
Pending changes
|
||||||
|
</div>
|
||||||
|
<dl className="grid grid-cols-1 gap-y-2">
|
||||||
|
{criticalChanges.map((c, idx) => (
|
||||||
|
<div key={idx} className="w-full not-last:pb-2">
|
||||||
|
<div className="flex items-center gap-2 gap-x-8">
|
||||||
|
<dt className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{c.label}
|
||||||
|
</dt>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="rounded-sm bg-slate-200 px-1.5 py-0.5 text-sm font-medium text-slate-900 dark:bg-slate-700 dark:text-slate-100">
|
||||||
|
{c.from || "—"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="rounded-sm bg-slate-200 px-1.5 py-0.5 text-sm font-medium text-slate-900 dark:bg-slate-700 dark:text-slate-100">
|
||||||
|
{c.to}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm">
|
||||||
|
If the network settings are invalid,{" "}
|
||||||
|
<strong>the device may become unreachable</strong> and require a factory
|
||||||
|
reset to restore connectivity.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={showRenewLeaseConfirm}
|
open={showRenewLeaseConfirm}
|
||||||
onClose={() => setShowRenewLeaseConfirm(false)}
|
|
||||||
title="Renew DHCP Lease"
|
title="Renew DHCP Lease"
|
||||||
description="This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process."
|
variant="warning"
|
||||||
variant="danger"
|
|
||||||
confirmText="Renew Lease"
|
confirmText="Renew Lease"
|
||||||
|
description={
|
||||||
|
<p>
|
||||||
|
This will request a new IP address from your router. The device may briefly
|
||||||
|
disconnect during the renewal process.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
If you receive a new IP address,{" "}
|
||||||
|
<strong>you may need to reconnect using the new address</strong>.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
handleRenewLease();
|
|
||||||
setShowRenewLeaseConfirm(false);
|
setShowRenewLeaseConfirm(false);
|
||||||
|
onDhcpLeaseRenew();
|
||||||
}}
|
}}
|
||||||
|
onClose={() => setShowRenewLeaseConfirm(false)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { useRTCStore } from "@/hooks/stores";
|
||||||
|
|
||||||
|
// JSON-RPC utility for use outside of React components
|
||||||
|
export interface JsonRpcCallOptions {
|
||||||
|
method: string;
|
||||||
|
params?: unknown;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JsonRpcCallResponse {
|
||||||
|
jsonrpc: string;
|
||||||
|
result?: unknown;
|
||||||
|
error?: {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
|
id: number | string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rpcCallCounter = 0;
|
||||||
|
|
||||||
|
export function callJsonRpc(options: JsonRpcCallOptions): Promise<JsonRpcCallResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Access the RTC store directly outside of React context
|
||||||
|
const rpcDataChannel = useRTCStore.getState().rpcDataChannel;
|
||||||
|
|
||||||
|
if (!rpcDataChannel || rpcDataChannel.readyState !== "open") {
|
||||||
|
reject(new Error("RPC data channel not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcCallCounter++;
|
||||||
|
const requestId = `rpc_${Date.now()}_${rpcCallCounter}`;
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: options.method,
|
||||||
|
params: options.params || {},
|
||||||
|
id: requestId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = options.timeout || 5000;
|
||||||
|
let timeoutId: number | undefined;
|
||||||
|
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(event.data) as JsonRpcCallResponse;
|
||||||
|
if (response.id === requestId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
rpcDataChannel.removeEventListener("message", messageHandler);
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore parse errors from other messages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
rpcDataChannel.removeEventListener("message", messageHandler);
|
||||||
|
reject(new Error(`JSON-RPC call timed out after ${timeout}ms`));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
rpcDataChannel.addEventListener("message", messageHandler);
|
||||||
|
rpcDataChannel.send(JSON.stringify(request));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific network settings API calls
|
||||||
|
export async function getNetworkSettings() {
|
||||||
|
const response = await callJsonRpc({ method: "getNetworkSettings" });
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setNetworkSettings(settings: unknown) {
|
||||||
|
const response = await callJsonRpc({
|
||||||
|
method: "setNetworkSettings",
|
||||||
|
params: { settings },
|
||||||
|
});
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNetworkState() {
|
||||||
|
const response = await callJsonRpc({ method: "getNetworkState" });
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renewDHCPLease() {
|
||||||
|
const response = await callJsonRpc({ method: "renewDHCPLease" });
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue