mirror of https://github.com/jetkvm/kvm.git
Compare commits
17 Commits
a9cd36c5fb
...
459dc5c9fa
| Author | SHA1 | Date |
|---|---|---|
|
|
459dc5c9fa | |
|
|
110790a664 | |
|
|
403c1f8fa1 | |
|
|
ece467eba8 | |
|
|
22f5ed2a8b | |
|
|
9cd29a3b1b | |
|
|
1ad44ed461 | |
|
|
bb45be1d6d | |
|
|
1f568c96dd | |
|
|
ed90e42324 | |
|
|
feec19ab13 | |
|
|
59b7141d84 | |
|
|
e47442d701 | |
|
|
b3ce961b79 | |
|
|
fe074b2253 | |
|
|
579345e5b4 | |
|
|
6ff4f37a36 |
|
|
@ -9,5 +9,6 @@
|
||||||
"synctrace"
|
"synctrace"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"git.ignoreLimitWarning": true
|
"git.ignoreLimitWarning": true,
|
||||||
|
"cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo"
|
||||||
}
|
}
|
||||||
25
cmd/main.go
25
cmd/main.go
|
|
@ -117,6 +117,29 @@ func supervise() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isSymlinkTo(dst, src string) bool {
|
||||||
|
file, err := os.Stat(dst)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if file.Mode()&os.ModeSymlink != os.ModeSymlink {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
target, err := os.Readlink(dst)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return target == src
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureSymlink(dst, src string) error {
|
||||||
|
if isSymlinkTo(dst, src) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = os.Remove(dst)
|
||||||
|
return os.Symlink(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
func createErrorDump(logFile *os.File) {
|
func createErrorDump(logFile *os.File) {
|
||||||
logFile.Close()
|
logFile.Close()
|
||||||
|
|
||||||
|
|
@ -160,6 +183,8 @@ func createErrorDump(logFile *os.File) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("error dump created: %s\n", filePath)
|
fmt.Printf("error dump created: %s\n", filePath)
|
||||||
|
|
||||||
|
_ = ensureSymlink(filePath, filepath.Join(errorDumpDir, "last-crash.log"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func doSupervise() {
|
func doSupervise() {
|
||||||
|
|
|
||||||
25
display.go
25
display.go
|
|
@ -27,6 +27,11 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func switchToMainScreen() {
|
func switchToMainScreen() {
|
||||||
|
if networkManager == nil {
|
||||||
|
nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if networkManager.IsUp() {
|
if networkManager.IsUp() {
|
||||||
nativeInstance.SwitchToScreenIfDifferent("home_screen")
|
nativeInstance.SwitchToScreenIfDifferent("home_screen")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -35,13 +40,21 @@ func switchToMainScreen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDisplay() {
|
func updateDisplay() {
|
||||||
nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkManager.IPv4String())
|
if networkManager != nil {
|
||||||
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkManager.IPv6String())
|
nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkManager.IPv4String())
|
||||||
|
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkManager.IPv6String())
|
||||||
|
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
|
||||||
|
}
|
||||||
|
|
||||||
_, _ = 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", networkManager.MACString())
|
switch config.NetworkConfig.DHCPClient.String {
|
||||||
|
case "jetdhcpc":
|
||||||
|
nativeInstance.UpdateLabelIfChanged("dhcp_client_change_label", "Change to udhcpc")
|
||||||
|
case "udhcpc":
|
||||||
|
nativeInstance.UpdateLabelIfChanged("dhcp_client_change_label", "Change to JetKVM")
|
||||||
|
}
|
||||||
|
|
||||||
if usbState == "configured" {
|
if usbState == "configured" {
|
||||||
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected")
|
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected")
|
||||||
|
|
@ -59,7 +72,7 @@ func updateDisplay() {
|
||||||
}
|
}
|
||||||
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
|
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
|
||||||
|
|
||||||
if networkManager.IsUp() {
|
if networkManager != nil && 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 {
|
||||||
|
|
@ -189,7 +202,9 @@ func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool, reason string) {
|
||||||
|
|
||||||
func updateStaticContents() {
|
func updateStaticContents() {
|
||||||
//contents that never change
|
//contents that never change
|
||||||
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
|
if networkManager != nil {
|
||||||
|
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 {
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -75,15 +75,19 @@ void action_about_screen_gesture(lv_event_t * e) {
|
||||||
// user_data doesn't seem to be working, so we use a global variable here
|
// user_data doesn't seem to be working, so we use a global variable here
|
||||||
static uint32_t t_reset_config;
|
static uint32_t t_reset_config;
|
||||||
static uint32_t t_reboot;
|
static uint32_t t_reboot;
|
||||||
|
static uint32_t t_dhcpc;
|
||||||
|
|
||||||
static bool b_reboot = false;
|
static bool b_reboot = false;
|
||||||
static bool b_reset_config = false;
|
static bool b_reset_config = false;
|
||||||
|
static bool b_dhcpc = false;
|
||||||
|
|
||||||
static bool b_reboot_lock = false;
|
static bool b_reboot_lock = false;
|
||||||
static bool b_reset_config_lock = false;
|
static bool b_reset_config_lock = false;
|
||||||
|
static bool b_dhcpc_lock = false;
|
||||||
|
|
||||||
const int RESET_CONFIG_HOLD_TIME = 10;
|
const int RESET_CONFIG_HOLD_TIME = 10;
|
||||||
const int REBOOT_HOLD_TIME = 5;
|
const int REBOOT_HOLD_TIME = 5;
|
||||||
|
const int DHCPC_HOLD_TIME = 5;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t *start_time;
|
uint32_t *start_time;
|
||||||
|
|
@ -153,6 +157,22 @@ void action_reset_config(lv_event_t * e) {
|
||||||
handle_hold_action(e, &config);
|
handle_hold_action(e, &config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void action_dhcpc(lv_event_t * e) {
|
||||||
|
hold_action_config_t config = {
|
||||||
|
.start_time = &t_dhcpc,
|
||||||
|
.completed = &b_dhcpc,
|
||||||
|
.lock = &b_dhcpc_lock,
|
||||||
|
.hold_time_seconds = DHCPC_HOLD_TIME,
|
||||||
|
.rpc_method = "toggleDHCPClient",
|
||||||
|
.button_obj = NULL, // No button/spinner for reboot
|
||||||
|
.spinner_obj = NULL,
|
||||||
|
.label_obj = objects.dhcpc_label,
|
||||||
|
.default_text = "Press and hold for\n5 seconds"
|
||||||
|
};
|
||||||
|
|
||||||
|
handle_hold_action(e, &config);
|
||||||
|
}
|
||||||
|
|
||||||
void action_reboot(lv_event_t * e) {
|
void action_reboot(lv_event_t * e) {
|
||||||
hold_action_config_t config = {
|
hold_action_config_t config = {
|
||||||
.start_time = &t_reboot,
|
.start_time = &t_reboot,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ extern void action_handle_common_press_event(lv_event_t * e);
|
||||||
extern void action_reset_config(lv_event_t * e);
|
extern void action_reset_config(lv_event_t * e);
|
||||||
extern void action_reboot(lv_event_t * e);
|
extern void action_reboot(lv_event_t * e);
|
||||||
extern void action_switch_to_reboot(lv_event_t * e);
|
extern void action_switch_to_reboot(lv_event_t * e);
|
||||||
|
extern void action_dhcpc(lv_event_t * e);
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
|
||||||
|
|
@ -887,6 +887,26 @@ void create_screen_menu_advanced_screen() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
// MenuBtnDHCPClient
|
||||||
|
lv_obj_t *obj = lv_button_create(parent_obj);
|
||||||
|
objects.menu_btn_dhcp_client = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), 50);
|
||||||
|
lv_obj_add_event_cb(obj, action_switch_to_reboot, LV_EVENT_PRESSED, (void *)0);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SNAPPABLE);
|
||||||
|
add_style_menu_button(obj);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
lv_obj_t *obj = lv_label_create(parent_obj);
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
add_style_menu_button_label(obj);
|
||||||
|
lv_label_set_text(obj, "DHCP Client");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
// MenuBtnAdvancedResetConfig
|
// MenuBtnAdvancedResetConfig
|
||||||
lv_obj_t *obj = lv_button_create(parent_obj);
|
lv_obj_t *obj = lv_button_create(parent_obj);
|
||||||
|
|
@ -2197,6 +2217,221 @@ void create_screen_rebooting_screen() {
|
||||||
void tick_screen_rebooting_screen() {
|
void tick_screen_rebooting_screen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void create_screen_switch_dhcp_client_screen() {
|
||||||
|
lv_obj_t *obj = lv_obj_create(0);
|
||||||
|
objects.switch_dhcp_client_screen = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, 300, 240);
|
||||||
|
lv_obj_add_event_cb(obj, action_about_screen_gesture, LV_EVENT_GESTURE, (void *)0);
|
||||||
|
add_style_flex_screen_menu(obj);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent_obj);
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100));
|
||||||
|
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_right(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
add_style_flex_start(obj);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
// DHCPClientHeader
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent_obj);
|
||||||
|
objects.dhcp_client_header = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
add_style_flow_row_space_between(obj);
|
||||||
|
lv_obj_set_style_pad_right(obj, 4, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
lv_obj_t *obj = lv_button_create(parent_obj);
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, 32, 32);
|
||||||
|
lv_obj_add_event_cb(obj, action_switch_to_menu, LV_EVENT_CLICKED, (void *)0);
|
||||||
|
add_style_back_button(obj);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
lv_obj_t *obj = lv_image_create(parent_obj);
|
||||||
|
lv_obj_set_pos(obj, -1, 2);
|
||||||
|
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_image_set_src(obj, &img_back_caret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
lv_obj_t *obj = lv_label_create(parent_obj);
|
||||||
|
lv_obj_set_pos(obj, LV_PCT(0), LV_PCT(0));
|
||||||
|
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
add_style_header_link(obj);
|
||||||
|
lv_label_set_text(obj, "Reset Config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// DHCPClientContainer
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent_obj);
|
||||||
|
objects.dhcp_client_container = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), LV_PCT(80));
|
||||||
|
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_AUTO);
|
||||||
|
lv_obj_set_scroll_dir(obj, LV_DIR_VER);
|
||||||
|
lv_obj_set_scroll_snap_x(obj, LV_SCROLL_SNAP_START);
|
||||||
|
add_style_flex_column_start(obj);
|
||||||
|
lv_obj_set_style_pad_right(obj, 4, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent_obj);
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
add_style_flex_column_start(obj);
|
||||||
|
lv_obj_set_style_pad_right(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
// DHCPClientLabelContainer
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent_obj);
|
||||||
|
objects.dhcp_client_label_container = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
add_style_flex_column_start(obj);
|
||||||
|
lv_obj_set_style_pad_right(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_left(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_top(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_bottom(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
// DHCPC_Label
|
||||||
|
lv_obj_t *obj = lv_label_create(parent_obj);
|
||||||
|
objects.dhcpc_label = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
add_style_info_content_label(obj);
|
||||||
|
lv_obj_set_style_text_font(obj, &ui_font_font_book20, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_label_set_text(obj, "Press and hold for\n10 seconds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// DHCPClientSpinner
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent_obj);
|
||||||
|
objects.dhcp_client_spinner = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_right(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_CLICKABLE|LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
add_style_flex_column_start(obj);
|
||||||
|
lv_obj_set_style_flex_main_place(obj, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_flex_cross_place(obj, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_flex_track_place(obj, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
lv_obj_t *obj = lv_spinner_create(parent_obj);
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, 80, 80);
|
||||||
|
lv_spinner_set_anim_params(obj, 1000, 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// DHCPClientButton
|
||||||
|
lv_obj_t *obj = lv_obj_create(parent_obj);
|
||||||
|
objects.dhcp_client_button = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_right(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
add_style_flex_column_start(obj);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
lv_obj_t *obj = lv_button_create(parent_obj);
|
||||||
|
objects.obj2 = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_PCT(100), 50);
|
||||||
|
lv_obj_add_event_cb(obj, action_dhcpc, LV_EVENT_PRESSED, (void *)0);
|
||||||
|
lv_obj_add_event_cb(obj, action_dhcpc, LV_EVENT_PRESSING, (void *)0);
|
||||||
|
lv_obj_add_event_cb(obj, action_dhcpc, LV_EVENT_RELEASED, (void *)0);
|
||||||
|
lv_obj_set_style_bg_color(obj, lv_color_hex(0xffdc2626), LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_right(obj, 13, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
{
|
||||||
|
lv_obj_t *parent_obj = obj;
|
||||||
|
{
|
||||||
|
// DHCPClientChangeLabel
|
||||||
|
lv_obj_t *obj = lv_label_create(parent_obj);
|
||||||
|
objects.dhcp_client_change_label = obj;
|
||||||
|
lv_obj_set_pos(obj, 0, 0);
|
||||||
|
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||||
|
lv_label_set_text(obj, "Switch to udhcpc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tick_screen_switch_dhcp_client_screen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick_screen_switch_dhcp_client_screen() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
typedef void (*tick_screen_func_t)();
|
typedef void (*tick_screen_func_t)();
|
||||||
|
|
@ -2212,6 +2447,7 @@ tick_screen_func_t tick_screen_funcs[] = {
|
||||||
tick_screen_reset_config_screen,
|
tick_screen_reset_config_screen,
|
||||||
tick_screen_reboot_screen,
|
tick_screen_reboot_screen,
|
||||||
tick_screen_rebooting_screen,
|
tick_screen_rebooting_screen,
|
||||||
|
tick_screen_switch_dhcp_client_screen,
|
||||||
};
|
};
|
||||||
void tick_screen(int screen_index) {
|
void tick_screen(int screen_index) {
|
||||||
tick_screen_funcs[screen_index]();
|
tick_screen_funcs[screen_index]();
|
||||||
|
|
@ -2236,4 +2472,5 @@ void create_screens() {
|
||||||
create_screen_reset_config_screen();
|
create_screen_reset_config_screen();
|
||||||
create_screen_reboot_screen();
|
create_screen_reboot_screen();
|
||||||
create_screen_rebooting_screen();
|
create_screen_rebooting_screen();
|
||||||
|
create_screen_switch_dhcp_client_screen();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ typedef struct _objects_t {
|
||||||
lv_obj_t *reset_config_screen;
|
lv_obj_t *reset_config_screen;
|
||||||
lv_obj_t *reboot_screen;
|
lv_obj_t *reboot_screen;
|
||||||
lv_obj_t *rebooting_screen;
|
lv_obj_t *rebooting_screen;
|
||||||
|
lv_obj_t *switch_dhcp_client_screen;
|
||||||
lv_obj_t *boot_logo;
|
lv_obj_t *boot_logo;
|
||||||
lv_obj_t *boot_screen_version;
|
lv_obj_t *boot_screen_version;
|
||||||
lv_obj_t *no_network_header_container;
|
lv_obj_t *no_network_header_container;
|
||||||
|
|
@ -54,6 +55,7 @@ typedef struct _objects_t {
|
||||||
lv_obj_t *menu_btn_advanced_developer_mode;
|
lv_obj_t *menu_btn_advanced_developer_mode;
|
||||||
lv_obj_t *menu_btn_advanced_usb_emulation;
|
lv_obj_t *menu_btn_advanced_usb_emulation;
|
||||||
lv_obj_t *menu_btn_advanced_reboot;
|
lv_obj_t *menu_btn_advanced_reboot;
|
||||||
|
lv_obj_t *menu_btn_dhcp_client;
|
||||||
lv_obj_t *menu_btn_advanced_reset_config;
|
lv_obj_t *menu_btn_advanced_reset_config;
|
||||||
lv_obj_t *menu_header_container_2;
|
lv_obj_t *menu_header_container_2;
|
||||||
lv_obj_t *menu_items_container_2;
|
lv_obj_t *menu_items_container_2;
|
||||||
|
|
@ -101,6 +103,14 @@ typedef struct _objects_t {
|
||||||
lv_obj_t *obj1;
|
lv_obj_t *obj1;
|
||||||
lv_obj_t *reboot_in_progress_logo;
|
lv_obj_t *reboot_in_progress_logo;
|
||||||
lv_obj_t *reboot_in_progress_label;
|
lv_obj_t *reboot_in_progress_label;
|
||||||
|
lv_obj_t *dhcp_client_header;
|
||||||
|
lv_obj_t *dhcp_client_container;
|
||||||
|
lv_obj_t *dhcp_client_label_container;
|
||||||
|
lv_obj_t *dhcpc_label;
|
||||||
|
lv_obj_t *dhcp_client_spinner;
|
||||||
|
lv_obj_t *dhcp_client_button;
|
||||||
|
lv_obj_t *obj2;
|
||||||
|
lv_obj_t *dhcp_client_change_label;
|
||||||
} objects_t;
|
} objects_t;
|
||||||
|
|
||||||
extern objects_t objects;
|
extern objects_t objects;
|
||||||
|
|
@ -117,6 +127,7 @@ enum ScreensEnum {
|
||||||
SCREEN_ID_RESET_CONFIG_SCREEN = 9,
|
SCREEN_ID_RESET_CONFIG_SCREEN = 9,
|
||||||
SCREEN_ID_REBOOT_SCREEN = 10,
|
SCREEN_ID_REBOOT_SCREEN = 10,
|
||||||
SCREEN_ID_REBOOTING_SCREEN = 11,
|
SCREEN_ID_REBOOTING_SCREEN = 11,
|
||||||
|
SCREEN_ID_SWITCH_DHCP_CLIENT_SCREEN = 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
void create_screen_boot_screen();
|
void create_screen_boot_screen();
|
||||||
|
|
@ -152,6 +163,9 @@ void tick_screen_reboot_screen();
|
||||||
void create_screen_rebooting_screen();
|
void create_screen_rebooting_screen();
|
||||||
void tick_screen_rebooting_screen();
|
void tick_screen_rebooting_screen();
|
||||||
|
|
||||||
|
void create_screen_switch_dhcp_client_screen();
|
||||||
|
void tick_screen_switch_dhcp_client_screen();
|
||||||
|
|
||||||
void tick_screen_by_id(enum ScreensEnum screenId);
|
void tick_screen_by_id(enum ScreensEnum screenId);
|
||||||
void tick_screen(int screen_index);
|
void tick_screen(int screen_index);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,12 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/guregu/null/v6"
|
"github.com/guregu/null/v6"
|
||||||
"github.com/vishvananda/netlink"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IPAddress represents a network interface address
|
|
||||||
type IPAddress struct {
|
|
||||||
Family int
|
|
||||||
Address net.IPNet
|
|
||||||
Gateway net.IP
|
|
||||||
MTU int
|
|
||||||
Secondary bool
|
|
||||||
Permanent bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *IPAddress) String() string {
|
|
||||||
return a.Address.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *IPAddress) Compare(n netlink.Addr) bool {
|
|
||||||
if !a.Address.IP.Equal(n.IP) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if slices.Compare(a.Address.Mask, n.IPNet.Mask) != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *IPAddress) NetlinkAddr() netlink.Addr {
|
|
||||||
return netlink.Addr{
|
|
||||||
IPNet: &a.Address,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *IPAddress) DefaultRoute(linkIndex int) netlink.Route {
|
|
||||||
return netlink.Route{
|
|
||||||
Dst: nil,
|
|
||||||
Gw: a.Gateway,
|
|
||||||
LinkIndex: linkIndex,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParsedIPConfig represents the parsed IP configuration
|
|
||||||
type ParsedIPConfig struct {
|
|
||||||
Addresses []IPAddress
|
|
||||||
Nameservers []net.IP
|
|
||||||
SearchList []string
|
|
||||||
Domain string
|
|
||||||
MTU int
|
|
||||||
Interface string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPv6Address represents an IPv6 address with lifetime information
|
|
||||||
type IPv6Address struct {
|
|
||||||
Address net.IP `json:"address"`
|
|
||||||
Prefix net.IPNet `json:"prefix"`
|
|
||||||
ValidLifetime *time.Time `json:"valid_lifetime"`
|
|
||||||
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
|
||||||
Scope int `json:"scope"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPv4StaticConfig represents static IPv4 configuration
|
// 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"`
|
||||||
|
|
@ -83,6 +22,12 @@ type IPv6StaticConfig struct {
|
||||||
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MDNSListenOptions represents MDNS listening options
|
||||||
|
type MDNSListenOptions struct {
|
||||||
|
IPv4 bool
|
||||||
|
IPv6 bool
|
||||||
|
}
|
||||||
|
|
||||||
// NetworkConfig represents the complete network configuration for an interface
|
// NetworkConfig represents the complete network configuration for an interface
|
||||||
type NetworkConfig struct {
|
type NetworkConfig struct {
|
||||||
DHCPClient null.String `json:"dhcp_client,omitempty" one_of:"jetdhcpc,udhcpc" default:"jetdhcpc"`
|
DHCPClient null.String `json:"dhcp_client,omitempty" one_of:"jetdhcpc,udhcpc" default:"jetdhcpc"`
|
||||||
|
|
@ -129,44 +74,18 @@ func (c *NetworkConfig) GetMDNSMode() *MDNSListenOptions {
|
||||||
return listenOptions
|
return listenOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// MDNSListenOptions represents MDNS listening options
|
|
||||||
type MDNSListenOptions struct {
|
|
||||||
IPv4 bool
|
|
||||||
IPv6 bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransportProxyFunc returns a function for HTTP proxy configuration
|
// GetTransportProxyFunc returns a function for HTTP proxy configuration
|
||||||
func (c *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
|
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 c.HTTPProxy.String == "" {
|
if c.HTTPProxy.String == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else {
|
} else {
|
||||||
proxyUrl, _ := url.Parse(c.HTTPProxy.String)
|
proxyURL, _ := url.Parse(c.HTTPProxy.String)
|
||||||
return proxyUrl, nil
|
return proxyURL, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"`
|
|
||||||
IPv6Gateway string `json:"ipv6_gateway,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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkConfig interface for backward compatibility
|
// NetworkConfig interface for backward compatibility
|
||||||
type NetworkConfigInterface interface {
|
type NetworkConfigInterface interface {
|
||||||
InterfaceName() string
|
InterfaceName() string
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InterfaceState represents the current state of a network interface
|
||||||
|
type InterfaceState struct {
|
||||||
|
InterfaceName string `json:"interface_name"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
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"`
|
||||||
|
IPv6Gateway string `json:"ipv6_gateway,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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RpcInterfaceState is the RPC representation of an interface state
|
||||||
|
type RpcInterfaceState struct {
|
||||||
|
InterfaceState
|
||||||
|
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRpcInterfaceState converts an InterfaceState to a RpcInterfaceState
|
||||||
|
func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState {
|
||||||
|
addrs := make([]RpcIPv6Address, len(s.IPv6Addresses))
|
||||||
|
for i, addr := range s.IPv6Addresses {
|
||||||
|
addrs[i] = RpcIPv6Address{
|
||||||
|
Address: addr.Address.String(),
|
||||||
|
Prefix: addr.Prefix.String(),
|
||||||
|
ValidLifetime: addr.ValidLifetime,
|
||||||
|
PreferredLifetime: addr.PreferredLifetime,
|
||||||
|
Scope: addr.Scope,
|
||||||
|
Flags: addr.Flags,
|
||||||
|
FlagSecondary: addr.Flags&unix.IFA_F_SECONDARY != 0,
|
||||||
|
FlagPermanent: addr.Flags&unix.IFA_F_PERMANENT != 0,
|
||||||
|
FlagTemporary: addr.Flags&unix.IFA_F_TEMPORARY != 0,
|
||||||
|
FlagStablePrivacy: addr.Flags&unix.IFA_F_STABLE_PRIVACY != 0,
|
||||||
|
FlagDeprecated: addr.Flags&unix.IFA_F_DEPRECATED != 0,
|
||||||
|
FlagOptimistic: addr.Flags&unix.IFA_F_OPTIMISTIC != 0,
|
||||||
|
FlagDADFailed: addr.Flags&unix.IFA_F_DADFAILED != 0,
|
||||||
|
FlagTentative: addr.Flags&unix.IFA_F_TENTATIVE != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &RpcInterfaceState{
|
||||||
|
InterfaceState: *s,
|
||||||
|
IPv6Addresses: addrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IPAddress represents a network interface address
|
||||||
|
type IPAddress struct {
|
||||||
|
Family int
|
||||||
|
Address net.IPNet
|
||||||
|
Gateway net.IP
|
||||||
|
MTU int
|
||||||
|
Secondary bool
|
||||||
|
Permanent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *IPAddress) String() string {
|
||||||
|
return a.Address.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *IPAddress) Compare(n netlink.Addr) bool {
|
||||||
|
if !a.Address.IP.Equal(n.IP) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if slices.Compare(a.Address.Mask, n.Mask) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *IPAddress) NetlinkAddr() netlink.Addr {
|
||||||
|
return netlink.Addr{
|
||||||
|
IPNet: &a.Address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *IPAddress) DefaultRoute(linkIndex int) netlink.Route {
|
||||||
|
return netlink.Route{
|
||||||
|
Dst: nil,
|
||||||
|
Gw: a.Gateway,
|
||||||
|
LinkIndex: linkIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsedIPConfig represents the parsed IP configuration
|
||||||
|
type ParsedIPConfig struct {
|
||||||
|
Addresses []IPAddress
|
||||||
|
Nameservers []net.IP
|
||||||
|
SearchList []string
|
||||||
|
Domain string
|
||||||
|
MTU int
|
||||||
|
Interface string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6Address represents an IPv6 address with lifetime information
|
||||||
|
type IPv6Address struct {
|
||||||
|
Address net.IP `json:"address"`
|
||||||
|
Prefix net.IPNet `json:"prefix"`
|
||||||
|
ValidLifetime *time.Time `json:"valid_lifetime"`
|
||||||
|
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
||||||
|
Flags int `json:"flags"`
|
||||||
|
Scope int `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RpcIPv6Address is the RPC representation of an IPv6 address
|
||||||
|
type RpcIPv6Address struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
ValidLifetime *time.Time `json:"valid_lifetime"`
|
||||||
|
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
||||||
|
Scope int `json:"scope"`
|
||||||
|
Flags int `json:"flags"`
|
||||||
|
FlagSecondary bool `json:"flag_secondary"`
|
||||||
|
FlagPermanent bool `json:"flag_permanent"`
|
||||||
|
FlagTemporary bool `json:"flag_temporary"`
|
||||||
|
FlagStablePrivacy bool `json:"flag_stable_privacy"`
|
||||||
|
FlagDeprecated bool `json:"flag_deprecated"`
|
||||||
|
FlagOptimistic bool `json:"flag_optimistic"`
|
||||||
|
FlagDADFailed bool `json:"flag_dad_failed"`
|
||||||
|
FlagTentative bool `json:"flag_tentative"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
// InterfaceResolvConf represents the DNS configuration for a network interface
|
||||||
|
type InterfaceResolvConf struct {
|
||||||
|
NameServers []net.IP `json:"nameservers"`
|
||||||
|
SearchList []string `json:"search_list"`
|
||||||
|
Domain string `json:"domain,omitempty"` // TODO: remove this once we have a better way to handle the domain
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceResolvConfMap ..
|
||||||
|
type InterfaceResolvConfMap map[string]InterfaceResolvConf
|
||||||
|
|
||||||
|
// ResolvConf represents the DNS configuration for the system
|
||||||
|
type ResolvConf struct {
|
||||||
|
ConfigIPv4 InterfaceResolvConfMap `json:"config_ipv4"`
|
||||||
|
ConfigIPv6 InterfaceResolvConfMap `json:"config_ipv6"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
HostName string `json:"host_name"`
|
||||||
|
}
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package types
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type RpcIPv6Address struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
Prefix string `json:"prefix"`
|
|
||||||
ValidLifetime *time.Time `json:"valid_lifetime"`
|
|
||||||
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
|
||||||
Scope int `json:"scope"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RpcInterfaceState struct {
|
|
||||||
InterfaceState
|
|
||||||
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState {
|
|
||||||
addrs := make([]RpcIPv6Address, len(s.IPv6Addresses))
|
|
||||||
for i, addr := range s.IPv6Addresses {
|
|
||||||
addrs[i] = RpcIPv6Address{
|
|
||||||
Address: addr.Address.String(),
|
|
||||||
Prefix: addr.Prefix.String(),
|
|
||||||
ValidLifetime: addr.ValidLifetime,
|
|
||||||
PreferredLifetime: addr.PreferredLifetime,
|
|
||||||
Scope: addr.Scope,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &RpcInterfaceState{
|
|
||||||
InterfaceState: *s,
|
|
||||||
IPv6Addresses: addrs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -43,6 +43,8 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
||||||
_ = rpcReboot(true)
|
_ = rpcReboot(true)
|
||||||
case "reboot":
|
case "reboot":
|
||||||
_ = rpcReboot(true)
|
_ = rpcReboot(true)
|
||||||
|
case "toggleDHCPClient":
|
||||||
|
_ = rpcToggleDHCPClient()
|
||||||
default:
|
default:
|
||||||
nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received")
|
nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
network.go
66
network.go
|
|
@ -42,8 +42,8 @@ func restartMdns() {
|
||||||
IPv6: config.NetworkConfig.MDNSMode.String != "disabled",
|
IPv6: config.NetworkConfig.MDNSMode.String != "disabled",
|
||||||
})
|
})
|
||||||
_ = mDNS.SetLocalNames([]string{
|
_ = mDNS.SetLocalNames([]string{
|
||||||
networkManager.GetHostname(),
|
networkManager.Hostname(),
|
||||||
networkManager.GetFQDN(),
|
networkManager.FQDN(),
|
||||||
}, true)
|
}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +54,12 @@ func triggerTimeSyncOnNetworkStateChange() {
|
||||||
|
|
||||||
// set the NTP servers from the network manager
|
// set the NTP servers from the network manager
|
||||||
if networkManager != nil {
|
if networkManager != nil {
|
||||||
timeSync.SetDhcpNtpAddresses(networkManager.NTPServerStrings())
|
ntpServers := make([]string, len(networkManager.NTPServers()))
|
||||||
|
for i, server := range networkManager.NTPServers() {
|
||||||
|
ntpServers[i] = server.String()
|
||||||
|
}
|
||||||
|
networkLogger.Info().Strs("ntpServers", ntpServers).Msg("setting NTP servers from network manager")
|
||||||
|
timeSync.SetDhcpNtpAddresses(ntpServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sync time
|
// sync time
|
||||||
|
|
@ -89,12 +94,16 @@ func validateNetworkConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
networkLogger.Error().Err(err).Msg("failed to validate config, reverting to default config")
|
networkLogger.Error().Err(err).Msg("failed to validate config, reverting to default config")
|
||||||
SaveBackupConfig()
|
if err := SaveBackupConfig(); err != nil {
|
||||||
|
networkLogger.Error().Err(err).Msg("failed to save backup config")
|
||||||
|
}
|
||||||
|
|
||||||
// do not use a pointer to the default config
|
// do not use a pointer to the default config
|
||||||
// it has been already changed during LoadConfig
|
// it has been already changed during LoadConfig
|
||||||
config.NetworkConfig = &(types.NetworkConfig{})
|
config.NetworkConfig = &(types.NetworkConfig{})
|
||||||
SaveConfig()
|
if err := SaveConfig(); err != nil {
|
||||||
|
networkLogger.Error().Err(err).Msg("failed to save config")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initNetwork() error {
|
func initNetwork() error {
|
||||||
|
|
@ -103,15 +112,32 @@ func initNetwork() error {
|
||||||
// validate the config, if it's invalid, revert to the default config and save the backup
|
// validate the config, if it's invalid, revert to the default config and save the backup
|
||||||
validateNetworkConfig()
|
validateNetworkConfig()
|
||||||
|
|
||||||
networkManager = nmlite.NewNetworkManager(context.Background(), networkLogger)
|
nc := config.NetworkConfig
|
||||||
networkManager.SetOnInterfaceStateChange(networkStateChanged)
|
|
||||||
if err := networkManager.AddInterface(NetIfName, config.NetworkConfig); err != nil {
|
nm := nmlite.NewNetworkManager(context.Background(), networkLogger)
|
||||||
|
_ = setHostname(nm, nc.Hostname.String, nc.Domain.String)
|
||||||
|
nm.SetOnInterfaceStateChange(networkStateChanged)
|
||||||
|
if err := nm.AddInterface(NetIfName, nc); err != nil {
|
||||||
return fmt.Errorf("failed to add interface: %w", err)
|
return fmt.Errorf("failed to add interface: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
networkManager = nm
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error {
|
||||||
|
if nm == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostname == "" {
|
||||||
|
hostname = GetDefaultHostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nm.SetHostname(hostname, domain)
|
||||||
|
}
|
||||||
|
|
||||||
func rpcGetNetworkState() *types.RpcInterfaceState {
|
func rpcGetNetworkState() *types.RpcInterfaceState {
|
||||||
state, _ := networkManager.GetInterfaceState(NetIfName)
|
state, _ := networkManager.GetInterfaceState(NetIfName)
|
||||||
return state.ToRpcInterfaceState()
|
return state.ToRpcInterfaceState()
|
||||||
|
|
@ -131,6 +157,13 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
|
||||||
|
|
||||||
l.Debug().Msg("setting new config")
|
l.Debug().Msg("setting new config")
|
||||||
|
|
||||||
|
var rebootRequired bool
|
||||||
|
if netConfig.DHCPClient.String != config.NetworkConfig.DHCPClient.String {
|
||||||
|
rebootRequired = true
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = setHostname(networkManager, netConfig.Hostname.String, netConfig.Domain.String)
|
||||||
|
|
||||||
s := networkManager.SetInterfaceConfig(NetIfName, netConfig)
|
s := networkManager.SetInterfaceConfig(NetIfName, netConfig)
|
||||||
if s != nil {
|
if s != nil {
|
||||||
return nil, s
|
return nil, s
|
||||||
|
|
@ -148,9 +181,26 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rebootRequired {
|
||||||
|
if err := rpcReboot(false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return toRpcNetworkSettings(newConfig), nil
|
return toRpcNetworkSettings(newConfig), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcRenewDHCPLease() error {
|
func rpcRenewDHCPLease() error {
|
||||||
return networkManager.RenewDHCPLease(NetIfName)
|
return networkManager.RenewDHCPLease(NetIfName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcToggleDHCPClient() error {
|
||||||
|
switch config.NetworkConfig.DHCPClient.String {
|
||||||
|
case "jetdhcpc":
|
||||||
|
config.NetworkConfig.DHCPClient.String = "udhcpc"
|
||||||
|
case "udhcpc":
|
||||||
|
config.NetworkConfig.DHCPClient.String = "jetdhcpc"
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpcReboot(false)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/jetkvm/kvm/pkg/nmlite/jetdhcpc"
|
"github.com/jetkvm/kvm/pkg/nmlite/jetdhcpc"
|
||||||
"github.com/jetkvm/kvm/pkg/nmlite/udhcpc"
|
"github.com/jetkvm/kvm/pkg/nmlite/udhcpc"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/vishvananda/netlink"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DHCPClient wraps the dhclient package for use in the network manager
|
// DHCPClient wraps the dhclient package for use in the network manager
|
||||||
|
|
@ -19,7 +18,6 @@ type DHCPClient struct {
|
||||||
logger *zerolog.Logger
|
logger *zerolog.Logger
|
||||||
client types.DHCPClient
|
client types.DHCPClient
|
||||||
clientType string
|
clientType string
|
||||||
link netlink.Link
|
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
ipv4Enabled bool
|
ipv4Enabled bool
|
||||||
|
|
@ -81,8 +79,9 @@ func (dc *DHCPClient) initClient() (types.DHCPClient, error) {
|
||||||
|
|
||||||
func (dc *DHCPClient) initJetDHCPC() (types.DHCPClient, error) {
|
func (dc *DHCPClient) initJetDHCPC() (types.DHCPClient, error) {
|
||||||
return jetdhcpc.NewClient(dc.ctx, []string{dc.ifaceName}, &jetdhcpc.Config{
|
return jetdhcpc.NewClient(dc.ctx, []string{dc.ifaceName}, &jetdhcpc.Config{
|
||||||
IPv4: dc.ipv4Enabled,
|
IPv4: dc.ipv4Enabled,
|
||||||
IPv6: dc.ipv6Enabled,
|
IPv6: dc.ipv6Enabled,
|
||||||
|
V4ClientIdentifier: true,
|
||||||
OnLease4Change: func(lease *types.DHCPLease) {
|
OnLease4Change: func(lease *types.DHCPLease) {
|
||||||
dc.handleLeaseChange(lease, false)
|
dc.handleLeaseChange(lease, false)
|
||||||
},
|
},
|
||||||
|
|
@ -180,7 +179,9 @@ func (dc *DHCPClient) Renew() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
dc.logger.Info().Msg("renewing DHCP lease")
|
dc.logger.Info().Msg("renewing DHCP lease")
|
||||||
dc.client.Renew()
|
if err := dc.client.Renew(); err != nil {
|
||||||
|
return fmt.Errorf("failed to renew DHCP lease: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +192,9 @@ func (dc *DHCPClient) Release() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
dc.logger.Info().Msg("releasing DHCP lease")
|
dc.logger.Info().Msg("releasing DHCP lease")
|
||||||
dc.client.Release()
|
if err := dc.client.Release(); err != nil {
|
||||||
|
return fmt.Errorf("failed to release DHCP lease: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,6 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/sync"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -18,38 +15,89 @@ const (
|
||||||
hostsPath = "/etc/hosts"
|
hostsPath = "/etc/hosts"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HostnameManager manages system hostname and /etc/hosts
|
|
||||||
type HostnameManager struct {
|
|
||||||
logger *zerolog.Logger
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// SetHostname sets the system hostname and updates /etc/hosts
|
||||||
func (hm *HostnameManager) SetHostname(hostname, fqdn string) error {
|
func (hm *ResolvConfManager) SetHostname(hostname, domain string) error {
|
||||||
hm.mu.Lock()
|
|
||||||
defer hm.mu.Unlock()
|
|
||||||
|
|
||||||
hostname = ToValidHostname(strings.TrimSpace(hostname))
|
hostname = ToValidHostname(strings.TrimSpace(hostname))
|
||||||
fqdn = ToValidHostname(strings.TrimSpace(fqdn))
|
domain = ToValidHostname(strings.TrimSpace(domain))
|
||||||
|
|
||||||
if hostname == "" {
|
if hostname == "" {
|
||||||
return fmt.Errorf("invalid hostname: %s", hostname)
|
return fmt.Errorf("invalid hostname: %s", hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fqdn == "" {
|
hm.hostname = hostname
|
||||||
fqdn = hostname
|
hm.domain = domain
|
||||||
|
|
||||||
|
return hm.reconcileHostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm *ResolvConfManager) Domain() string {
|
||||||
|
hm.mu.Lock()
|
||||||
|
defer hm.mu.Unlock()
|
||||||
|
return hm.getDomain()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm *ResolvConfManager) Hostname() string {
|
||||||
|
hm.mu.Lock()
|
||||||
|
defer hm.mu.Unlock()
|
||||||
|
return hm.getHostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm *ResolvConfManager) FQDN() string {
|
||||||
|
hm.mu.Lock()
|
||||||
|
defer hm.mu.Unlock()
|
||||||
|
return hm.getFQDN()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm *ResolvConfManager) getFQDN() string {
|
||||||
|
hostname := hm.getHostname()
|
||||||
|
domain := hm.getDomain()
|
||||||
|
|
||||||
|
if domain == "" {
|
||||||
|
return hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s.%s", hostname, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm *ResolvConfManager) getHostname() string {
|
||||||
|
if hm.hostname != "" {
|
||||||
|
return hm.hostname
|
||||||
|
}
|
||||||
|
return "jetkvm"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm *ResolvConfManager) getDomain() string {
|
||||||
|
if hm.domain != "" {
|
||||||
|
return hm.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range hm.conf.ConfigIPv4 {
|
||||||
|
if iface.Domain != "" {
|
||||||
|
return iface.Domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range hm.conf.ConfigIPv6 {
|
||||||
|
if iface.Domain != "" {
|
||||||
|
return iface.Domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hm *ResolvConfManager) reconcileHostname() error {
|
||||||
|
hm.mu.Lock()
|
||||||
|
domain := hm.getDomain()
|
||||||
|
hostname := hm.hostname
|
||||||
|
if hostname == "" {
|
||||||
|
hostname = "jetkvm"
|
||||||
|
}
|
||||||
|
hm.mu.Unlock()
|
||||||
|
|
||||||
|
fqdn := hostname
|
||||||
|
if fqdn != "" {
|
||||||
|
fqdn = fmt.Sprintf("%s.%s", hostname, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
hm.logger.Info().
|
hm.logger.Info().
|
||||||
|
|
@ -81,12 +129,12 @@ func (hm *HostnameManager) SetHostname(hostname, fqdn string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentHostname returns the current system hostname
|
// GetCurrentHostname returns the current system hostname
|
||||||
func (hm *HostnameManager) GetCurrentHostname() (string, error) {
|
func (hm *ResolvConfManager) GetCurrentHostname() (string, error) {
|
||||||
return os.Hostname()
|
return os.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentFQDN returns the current FQDN
|
// GetCurrentFQDN returns the current FQDN
|
||||||
func (hm *HostnameManager) GetCurrentFQDN() (string, error) {
|
func (hm *ResolvConfManager) GetCurrentFQDN() (string, error) {
|
||||||
hostname, err := hm.GetCurrentHostname()
|
hostname, err := hm.GetCurrentHostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -97,7 +145,7 @@ func (hm *HostnameManager) GetCurrentFQDN() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateEtcHostname updates the /etc/hostname file
|
// updateEtcHostname updates the /etc/hostname file
|
||||||
func (hm *HostnameManager) updateEtcHostname(hostname string) error {
|
func (hm *ResolvConfManager) updateEtcHostname(hostname string) error {
|
||||||
if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil {
|
if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", hostnamePath, err)
|
return fmt.Errorf("failed to write %s: %w", hostnamePath, err)
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +155,7 @@ func (hm *HostnameManager) updateEtcHostname(hostname string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateEtcHosts updates the /etc/hosts file
|
// updateEtcHosts updates the /etc/hosts file
|
||||||
func (hm *HostnameManager) updateEtcHosts(hostname, fqdn string) error {
|
func (hm *ResolvConfManager) updateEtcHosts(hostname, fqdn string) error {
|
||||||
// Open /etc/hosts for reading and writing
|
// Open /etc/hosts for reading and writing
|
||||||
hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive)
|
hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -166,7 +214,7 @@ func (hm *HostnameManager) updateEtcHosts(hostname, fqdn string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// setSystemHostname sets the system hostname using the hostname command
|
// setSystemHostname sets the system hostname using the hostname command
|
||||||
func (hm *HostnameManager) setSystemHostname(hostname string) error {
|
func (hm *ResolvConfManager) setSystemHostname(hostname string) error {
|
||||||
cmd := exec.Command("hostname", "-F", hostnamePath)
|
cmd := exec.Command("hostname", "-F", hostnamePath)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to run hostname command: %w", err)
|
return fmt.Errorf("failed to run hostname command: %w", err)
|
||||||
|
|
@ -177,7 +225,7 @@ func (hm *HostnameManager) setSystemHostname(hostname string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFQDNFromHosts tries to get the FQDN from /etc/hosts
|
// getFQDNFromHosts tries to get the FQDN from /etc/hosts
|
||||||
func (hm *HostnameManager) getFQDNFromHosts(hostname string) (string, error) {
|
func (hm *ResolvConfManager) getFQDNFromHosts(hostname string) (string, error) {
|
||||||
content, err := os.ReadFile(hostsPath)
|
content, err := os.ReadFile(hostsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return hostname, nil // Return hostname as fallback
|
return hostname, nil // Return hostname as fallback
|
||||||
|
|
@ -208,38 +256,6 @@ func ToValidHostname(hostname string) string {
|
||||||
|
|
||||||
// ValidateHostname validates a hostname
|
// ValidateHostname validates a hostname
|
||||||
func ValidateHostname(hostname string) error {
|
func ValidateHostname(hostname string) error {
|
||||||
if hostname == "" {
|
_, err := idna.Lookup.ToASCII(hostname)
|
||||||
return fmt.Errorf("hostname cannot be empty")
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import (
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ResolvConfChangeCallback func(family int, resolvConf *types.InterfaceResolvConf) error
|
||||||
|
|
||||||
// InterfaceManager manages a single network interface
|
// InterfaceManager manages a single network interface
|
||||||
type InterfaceManager struct {
|
type InterfaceManager struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
@ -32,13 +34,12 @@ type InterfaceManager struct {
|
||||||
// Network components
|
// Network components
|
||||||
staticConfig *StaticConfigManager
|
staticConfig *StaticConfigManager
|
||||||
dhcpClient *DHCPClient
|
dhcpClient *DHCPClient
|
||||||
resolvConf *ResolvConfManager
|
|
||||||
hostname *HostnameManager
|
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
onStateChange func(state types.InterfaceState)
|
onStateChange func(state types.InterfaceState)
|
||||||
onConfigChange func(config *types.NetworkConfig)
|
onConfigChange func(config *types.NetworkConfig)
|
||||||
onDHCPLeaseChange func(lease *types.DHCPLease)
|
onDHCPLeaseChange func(lease *types.DHCPLease)
|
||||||
|
onResolvConfChange ResolvConfChangeCallback
|
||||||
|
|
||||||
// Control
|
// Control
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
|
|
@ -87,9 +88,6 @@ func NewInterfaceManager(ctx context.Context, ifaceName string, config *types.Ne
|
||||||
return nil, fmt.Errorf("failed to create DHCP client: %w", err)
|
return nil, fmt.Errorf("failed to create DHCP client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
im.resolvConf = NewResolvConfManager(&scopedLogger)
|
|
||||||
im.hostname = NewHostnameManager(&scopedLogger)
|
|
||||||
|
|
||||||
// Set up DHCP client callbacks
|
// Set up DHCP client callbacks
|
||||||
im.dhcpClient.SetOnLeaseChange(func(lease *types.DHCPLease) {
|
im.dhcpClient.SetOnLeaseChange(func(lease *types.DHCPLease) {
|
||||||
if err := im.applyDHCPLease(lease); err != nil {
|
if err := im.applyDHCPLease(lease); err != nil {
|
||||||
|
|
@ -162,7 +160,9 @@ func (im *InterfaceManager) Stop() error {
|
||||||
|
|
||||||
// Stop DHCP client
|
// Stop DHCP client
|
||||||
if im.dhcpClient != nil {
|
if im.dhcpClient != nil {
|
||||||
im.dhcpClient.Stop()
|
if err := im.dhcpClient.Stop(); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop DHCP client: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
im.logger.Info().Msg("interface manager stopped")
|
im.logger.Info().Msg("interface manager stopped")
|
||||||
|
|
@ -182,6 +182,10 @@ func (im *InterfaceManager) IsUp() bool {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.Up
|
return im.state.Up
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,6 +194,10 @@ func (im *InterfaceManager) IsOnline() bool {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.Online
|
return im.state.Online
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,6 +206,10 @@ func (im *InterfaceManager) IPv4Ready() bool {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.IPv4Ready
|
return im.state.IPv4Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,6 +218,10 @@ func (im *InterfaceManager) IPv6Ready() bool {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.IPv6Ready
|
return im.state.IPv6Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,6 +230,10 @@ func (im *InterfaceManager) GetIPv4Addresses() []string {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.IPv4Addresses
|
return im.state.IPv4Addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +242,10 @@ func (im *InterfaceManager) GetIPv4Address() string {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.IPv4Address
|
return im.state.IPv4Address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,6 +254,10 @@ func (im *InterfaceManager) GetIPv6Address() string {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.IPv6Address
|
return im.state.IPv6Address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,15 +267,27 @@ func (im *InterfaceManager) GetIPv6Addresses() []string {
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
addresses := []string{}
|
addresses := []string{}
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
|
||||||
for _, addr := range im.state.IPv6Addresses {
|
for _, addr := range im.state.IPv6Addresses {
|
||||||
addresses = append(addresses, addr.Address.String())
|
addresses = append(addresses, addr.Address.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{}
|
return addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMACAddress returns the MAC address of the interface
|
// GetMACAddress returns the MAC address of the interface
|
||||||
func (im *InterfaceManager) GetMACAddress() string {
|
func (im *InterfaceManager) GetMACAddress() string {
|
||||||
|
im.stateMu.RLock()
|
||||||
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.MACAddress
|
return im.state.MACAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,9 +308,32 @@ func (im *InterfaceManager) NTPServers() []net.IP {
|
||||||
im.stateMu.RLock()
|
im.stateMu.RLock()
|
||||||
defer im.stateMu.RUnlock()
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return []net.IP{}
|
||||||
|
}
|
||||||
|
|
||||||
return im.state.NTPServers
|
return im.state.NTPServers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (im *InterfaceManager) Domain() string {
|
||||||
|
im.stateMu.RLock()
|
||||||
|
defer im.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if im.state == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.state.DHCPLease4 != nil {
|
||||||
|
return im.state.DHCPLease4.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.state.DHCPLease6 != nil {
|
||||||
|
return im.state.DHCPLease6.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// GetConfig returns the current interface configuration
|
// GetConfig returns the current interface configuration
|
||||||
func (im *InterfaceManager) GetConfig() *types.NetworkConfig {
|
func (im *InterfaceManager) GetConfig() *types.NetworkConfig {
|
||||||
// Return a copy to avoid race conditions
|
// Return a copy to avoid race conditions
|
||||||
|
|
@ -335,6 +398,11 @@ func (im *InterfaceManager) SetOnDHCPLeaseChange(callback func(lease *types.DHCP
|
||||||
im.onDHCPLeaseChange = callback
|
im.onDHCPLeaseChange = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOnResolvConfChange sets the callback for resolv.conf changes
|
||||||
|
func (im *InterfaceManager) SetOnResolvConfChange(callback ResolvConfChangeCallback) {
|
||||||
|
im.onResolvConfChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
// applyConfiguration applies the current configuration to the interface
|
// applyConfiguration applies the current configuration to the interface
|
||||||
func (im *InterfaceManager) applyConfiguration() error {
|
func (im *InterfaceManager) applyConfiguration() error {
|
||||||
im.logger.Info().Msg("applying configuration")
|
im.logger.Info().Msg("applying configuration")
|
||||||
|
|
@ -349,11 +417,6 @@ func (im *InterfaceManager) applyConfiguration() error {
|
||||||
return fmt.Errorf("failed to apply IPv6 config: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,6 +482,13 @@ func (im *InterfaceManager) applyIPv4Static() error {
|
||||||
|
|
||||||
im.logger.Info().Interface("config", config).Msg("converted IPv4 static configuration")
|
im.logger.Info().Interface("config", config).Msg("converted IPv4 static configuration")
|
||||||
|
|
||||||
|
if err := im.onResolvConfChange(link.AfInet, &types.InterfaceResolvConf{
|
||||||
|
NameServers: config.Nameservers,
|
||||||
|
Source: "static",
|
||||||
|
}); err != nil {
|
||||||
|
im.logger.Warn().Err(err).Msg("failed to update resolv.conf")
|
||||||
|
}
|
||||||
|
|
||||||
return im.ReconcileLinkAddrs(config.Addresses, link.AfInet)
|
return im.ReconcileLinkAddrs(config.Addresses, link.AfInet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -463,6 +533,13 @@ func (im *InterfaceManager) applyIPv6Static() error {
|
||||||
}
|
}
|
||||||
im.logger.Info().Interface("config", config).Msg("converted IPv6 static configuration")
|
im.logger.Info().Interface("config", config).Msg("converted IPv6 static configuration")
|
||||||
|
|
||||||
|
if err := im.onResolvConfChange(link.AfInet6, &types.InterfaceResolvConf{
|
||||||
|
NameServers: config.Nameservers,
|
||||||
|
Source: "static",
|
||||||
|
}); err != nil {
|
||||||
|
im.logger.Warn().Err(err).Msg("failed to update resolv.conf")
|
||||||
|
}
|
||||||
|
|
||||||
return im.ReconcileLinkAddrs(config.Addresses, link.AfInet6)
|
return im.ReconcileLinkAddrs(config.Addresses, link.AfInet6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -514,7 +591,9 @@ func (im *InterfaceManager) applyIPv6SLAACAndDHCP() error {
|
||||||
// Enable both SLAAC and DHCPv6
|
// Enable both SLAAC and DHCPv6
|
||||||
if im.dhcpClient != nil {
|
if im.dhcpClient != nil {
|
||||||
im.dhcpClient.SetIPv6(true)
|
im.dhcpClient.SetIPv6(true)
|
||||||
im.dhcpClient.Start()
|
if err := im.dhcpClient.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start DHCP client: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return im.staticConfig.EnableIPv6SLAAC()
|
return im.staticConfig.EnableIPv6SLAAC()
|
||||||
|
|
@ -542,39 +621,6 @@ func (im *InterfaceManager) disableIPv6() error {
|
||||||
return im.staticConfig.DisableIPv6()
|
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.Lease4(); lease != nil && lease.Domain != "" {
|
|
||||||
return lease.Domain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "local"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (im *InterfaceManager) handleLinkStateChange(link *link.Link) {
|
func (im *InterfaceManager) handleLinkStateChange(link *link.Link) {
|
||||||
{
|
{
|
||||||
im.stateMu.Lock()
|
im.stateMu.Lock()
|
||||||
|
|
@ -647,15 +693,23 @@ func (im *InterfaceManager) SendRouterSolicitation() error {
|
||||||
func (im *InterfaceManager) handleLinkUp() {
|
func (im *InterfaceManager) handleLinkUp() {
|
||||||
im.logger.Info().Msg("link up")
|
im.logger.Info().Msg("link up")
|
||||||
|
|
||||||
im.applyConfiguration()
|
if err := im.applyConfiguration(); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to apply configuration")
|
||||||
|
}
|
||||||
|
|
||||||
if im.config.IPv4Mode.String == "dhcp" {
|
if im.config.IPv4Mode.String == "dhcp" {
|
||||||
im.dhcpClient.Renew()
|
if err := im.dhcpClient.Renew(); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to renew DHCP lease")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if im.config.IPv6Mode.String == "slaac" {
|
if im.config.IPv6Mode.String == "slaac" {
|
||||||
im.staticConfig.EnableIPv6SLAAC()
|
if err := im.staticConfig.EnableIPv6SLAAC(); err != nil {
|
||||||
im.SendRouterSolicitation()
|
im.logger.Error().Err(err).Msg("failed to enable IPv6 SLAAC")
|
||||||
|
}
|
||||||
|
if err := im.SendRouterSolicitation(); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to send router solicitation")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -663,7 +717,9 @@ func (im *InterfaceManager) handleLinkDown() {
|
||||||
im.logger.Info().Msg("link down")
|
im.logger.Info().Msg("link down")
|
||||||
|
|
||||||
if im.config.IPv4Mode.String == "dhcp" {
|
if im.config.IPv4Mode.String == "dhcp" {
|
||||||
im.dhcpClient.Stop()
|
if err := im.dhcpClient.Stop(); err != nil {
|
||||||
|
im.logger.Error().Err(err).Msg("failed to stop DHCP client")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
netlinkMgr := getNetlinkManager()
|
netlinkMgr := getNetlinkManager()
|
||||||
|
|
@ -697,18 +753,37 @@ func (im *InterfaceManager) monitorInterfaceState() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateStateFromDHCPLease updates the state from a DHCP lease
|
// updateStateFromDHCPLease updates the state from a DHCP lease
|
||||||
func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) {
|
func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) {
|
||||||
|
family := link.AfInet
|
||||||
|
|
||||||
im.stateMu.Lock()
|
im.stateMu.Lock()
|
||||||
im.state.DHCPLease4 = lease
|
if lease.IsIPv6() {
|
||||||
|
im.state.DHCPLease6 = lease
|
||||||
|
family = link.AfInet6
|
||||||
|
} else {
|
||||||
|
im.state.DHCPLease4 = lease
|
||||||
|
}
|
||||||
im.stateMu.Unlock()
|
im.stateMu.Unlock()
|
||||||
|
|
||||||
// Update resolv.conf with DNS information
|
// Update resolv.conf with DNS information
|
||||||
if im.resolvConf != nil {
|
if im.onResolvConfChange == nil {
|
||||||
im.resolvConf.UpdateFromLease(lease)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.ifaceName == "" {
|
||||||
|
im.logger.Warn().Msg("interface name is empty, skipping resolv.conf update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := im.onResolvConfChange(family, &types.InterfaceResolvConf{
|
||||||
|
NameServers: lease.DNS,
|
||||||
|
SearchList: lease.SearchList,
|
||||||
|
Source: "dhcp",
|
||||||
|
}); err != nil {
|
||||||
|
im.logger.Warn().Err(err).Msg("failed to update resolv.conf")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool,
|
||||||
Address: addr.IP,
|
Address: addr.IP,
|
||||||
Prefix: *addr.IPNet,
|
Prefix: *addr.IPNet,
|
||||||
Scope: addr.Scope,
|
Scope: addr.Scope,
|
||||||
|
Flags: addr.Flags,
|
||||||
ValidLifetime: lifetimeToTime(addr.ValidLft),
|
ValidLifetime: lifetimeToTime(addr.ValidLft),
|
||||||
PreferredLifetime: lifetimeToTime(addr.PreferedLft),
|
PreferredLifetime: lifetimeToTime(addr.PreferedLft),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ type Config struct {
|
||||||
// If true, add Client Identifier (61) option to the IPv4 request.
|
// If true, add Client Identifier (61) option to the IPv4 request.
|
||||||
V4ClientIdentifier bool
|
V4ClientIdentifier bool
|
||||||
|
|
||||||
|
Hostname string
|
||||||
|
|
||||||
OnLease4Change LeaseChangeHandler
|
OnLease4Change LeaseChangeHandler
|
||||||
OnLease6Change LeaseChangeHandler
|
OnLease6Change LeaseChangeHandler
|
||||||
|
|
||||||
|
|
@ -107,8 +109,9 @@ type Client struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultTimerDuration = 1 * time.Second
|
defaultTimerDuration = 1 * time.Second
|
||||||
defaultLinkUpTimeout = 30 * time.Second
|
defaultLinkUpTimeout = 30 * time.Second
|
||||||
|
maxRenewalAttemptDuration = 2 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewClient creates a new DHCP client for the given interface.
|
// NewClient creates a new DHCP client for the given interface.
|
||||||
|
|
@ -155,10 +158,21 @@ func resetTimer(t *time.Timer, l *zerolog.Logger) {
|
||||||
t.Reset(defaultTimerDuration)
|
t.Reset(defaultTimerDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRenewalTime(lease *Lease) time.Duration {
|
||||||
|
if lease.RenewalTime <= 0 || lease.LeaseTime > maxRenewalAttemptDuration/2 {
|
||||||
|
return maxRenewalAttemptDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
return lease.RenewalTime
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
|
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
|
||||||
|
l := c.l.With().Str("interface", ifname).Int("family", family).Logger()
|
||||||
for range t.C {
|
for range t.C {
|
||||||
|
l.Info().Msg("requesting lease")
|
||||||
|
|
||||||
if _, err := c.ensureInterfaceUp(ifname); err != nil {
|
if _, err := c.ensureInterfaceUp(ifname); err != nil {
|
||||||
c.l.Error().Err(err).Msg("failed to ensure interface up")
|
l.Error().Err(err).Msg("failed to ensure interface up")
|
||||||
resetTimer(t, c.l)
|
resetTimer(t, c.l)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -174,12 +188,22 @@ func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
|
||||||
lease, err = c.requestLease6(ifname)
|
lease, err = c.requestLease6(ifname)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.l.Error().Err(err).Msg("failed to request lease")
|
l.Error().Err(err).Msg("failed to request lease")
|
||||||
resetTimer(t, c.l)
|
resetTimer(t, c.l)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c.handleLeaseChange(lease)
|
c.handleLeaseChange(lease)
|
||||||
|
|
||||||
|
nextRenewal := getRenewalTime(lease)
|
||||||
|
|
||||||
|
l.Info().
|
||||||
|
Dur("nextRenewal", nextRenewal).
|
||||||
|
Dur("leaseTime", lease.LeaseTime).
|
||||||
|
Dur("rebindingTime", lease.RebindingTime).
|
||||||
|
Msg("sleeping until next renewal")
|
||||||
|
|
||||||
|
t.Reset(nextRenewal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,25 +286,9 @@ func (c *Client) handleLeaseChange(lease *Lease) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) doRenewLoop() {
|
|
||||||
timer := time.NewTimer(time.Duration(c.currentLease4.RenewalTime) * time.Second)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
for range timer.C {
|
|
||||||
c.renew()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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() error {
|
func (c *Client) Renew() error {
|
||||||
go c.renew()
|
c.timer4.Reset(defaultTimerDuration)
|
||||||
|
c.timer6.Reset(defaultTimerDuration)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,9 +312,11 @@ func (c *Client) SetIPv4(ipv4 bool) {
|
||||||
c.lease4Mu.Lock()
|
c.lease4Mu.Lock()
|
||||||
c.currentLease4 = nil
|
c.currentLease4 = nil
|
||||||
c.lease4Mu.Unlock()
|
c.lease4Mu.Unlock()
|
||||||
|
|
||||||
|
c.timer4.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.timer4.Stop()
|
c.timer4.Reset(defaultTimerDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetIPv6(ipv6 bool) {
|
func (c *Client) SetIPv6(ipv6 bool) {
|
||||||
|
|
@ -323,10 +333,12 @@ func (c *Client) SetIPv6(ipv6 bool) {
|
||||||
if !ipv6 {
|
if !ipv6 {
|
||||||
c.lease6Mu.Lock()
|
c.lease6Mu.Lock()
|
||||||
c.currentLease6 = nil
|
c.currentLease6 = nil
|
||||||
c.lease4Mu.Unlock()
|
c.lease6Mu.Unlock()
|
||||||
|
|
||||||
|
c.timer6.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.timer6.Stop()
|
c.timer6.Reset(defaultTimerDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Start() error {
|
func (c *Client) Start() error {
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,18 @@ func (c *Client) requestLease4(ifname string) (*Lease, error) {
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
// Prepend modifiers with default options, so they can be overriden.
|
// Prepend modifiers with default options, so they can be overridden.
|
||||||
reqmods := append(
|
reqmods := append(
|
||||||
[]dhcpv4.Modifier{
|
[]dhcpv4.Modifier{
|
||||||
dhcpv4.WithOption(dhcpv4.OptClassIdentifier(VendorIdentifier)),
|
dhcpv4.WithOption(dhcpv4.OptClassIdentifier(VendorIdentifier)),
|
||||||
dhcpv4.WithRequestedOptions(dhcpv4.OptionSubnetMask),
|
dhcpv4.WithRequestedOptions(
|
||||||
|
dhcpv4.OptionSubnetMask,
|
||||||
|
dhcpv4.OptionInterfaceMTU,
|
||||||
|
dhcpv4.OptionNTPServers,
|
||||||
|
dhcpv4.OptionDomainName,
|
||||||
|
dhcpv4.OptionDomainNameServer,
|
||||||
|
dhcpv4.OptionDNSDomainSearchList,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
c.cfg.Modifiers4...)
|
c.cfg.Modifiers4...)
|
||||||
|
|
||||||
|
|
@ -46,10 +53,25 @@ func (c *Client) requestLease4(ifname string) (*Lease, error) {
|
||||||
reqmods = append(reqmods, dhcpv4.WithOption(dhcpv4.OptClientIdentifier(ident)))
|
reqmods = append(reqmods, dhcpv4.WithOption(dhcpv4.OptClientIdentifier(ident)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.cfg.Hostname != "" {
|
||||||
|
reqmods = append(reqmods, dhcpv4.WithOption(dhcpv4.OptHostName(c.cfg.Hostname)))
|
||||||
|
}
|
||||||
|
|
||||||
l.Info().Msg("attempting to get DHCPv4 lease")
|
l.Info().Msg("attempting to get DHCPv4 lease")
|
||||||
lease, err := client.Request(c.ctx, reqmods...)
|
var (
|
||||||
if err != nil {
|
lease *nclient4.Lease
|
||||||
return nil, err
|
reqErr error
|
||||||
|
)
|
||||||
|
if c.currentLease4 != nil {
|
||||||
|
l.Info().Msg("current lease is not nil, renewing")
|
||||||
|
lease, reqErr = client.Renew(c.ctx, c.currentLease4.p4, reqmods...)
|
||||||
|
} else {
|
||||||
|
l.Info().Msg("current lease is nil, requesting new lease")
|
||||||
|
lease, reqErr = client.Request(c.ctx, reqmods...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqErr != nil {
|
||||||
|
return nil, reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if lease == nil || lease.ACK == nil {
|
if lease == nil || lease.ACK == nil {
|
||||||
|
|
@ -57,6 +79,7 @@ func (c *Client) requestLease4(ifname string) (*Lease, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryStructured(lease.ACK, &l).Info().Msgf("DHCPv4 lease acquired: %s", lease.ACK.String())
|
summaryStructured(lease.ACK, &l).Info().Msgf("DHCPv4 lease acquired: %s", lease.ACK.String())
|
||||||
|
l.Trace().Interface("options", lease.ACK.Options.String()).Msg("DHCPv4 lease options")
|
||||||
|
|
||||||
return fromNclient4Lease(lease, ifname), nil
|
return fromNclient4Lease(lease, ifname), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ func (c *Client) requestLease6(ifname string) (*Lease, error) {
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
// Prepend modifiers with default options, so they can be overriden.
|
// Prepend modifiers with default options, so they can be overridden.
|
||||||
reqmods := append(
|
reqmods := append(
|
||||||
[]dhcpv6.Modifier{
|
[]dhcpv6.Modifier{
|
||||||
dhcpv6.WithNetboot,
|
dhcpv6.WithNetboot,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package jetdhcpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
@ -11,6 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||||
"github.com/jetkvm/kvm/internal/network/types"
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
|
@ -66,6 +68,11 @@ func fromNclient4Lease(l *nclient4.Lease, iface string) *Lease {
|
||||||
lease.ClassIdentifier = l.ACK.ClassIdentifier()
|
lease.ClassIdentifier = l.ACK.ClassIdentifier()
|
||||||
lease.ServerID = l.ACK.ServerIdentifier().String()
|
lease.ServerID = l.ACK.ServerIdentifier().String()
|
||||||
|
|
||||||
|
mtu := l.ACK.Options.Get(dhcpv4.OptionInterfaceMTU)
|
||||||
|
if mtu != nil {
|
||||||
|
lease.MTU = int(binary.BigEndian.Uint16(mtu))
|
||||||
|
}
|
||||||
|
|
||||||
lease.Message = l.ACK.Message()
|
lease.Message = l.ACK.Message()
|
||||||
lease.LeaseTime = l.ACK.IPAddressLeaseTime(defaultLeaseTime)
|
lease.LeaseTime = l.ACK.IPAddressLeaseTime(defaultLeaseTime)
|
||||||
lease.RenewalTime = l.ACK.IPAddressRenewalTime(defaultRenewalTime)
|
lease.RenewalTime = l.ACK.IPAddressRenewalTime(defaultRenewalTime)
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,9 @@ func (nm *NetlinkManager) monitorStateChange() {
|
||||||
updateCh := make(chan netlink.LinkUpdate)
|
updateCh := make(chan netlink.LinkUpdate)
|
||||||
// we don't need to stop the subscription, as it will be closed when the program exits
|
// we don't need to stop the subscription, as it will be closed when the program exits
|
||||||
stopCh := make(chan struct{}) //nolint:unused
|
stopCh := make(chan struct{}) //nolint:unused
|
||||||
netlink.LinkSubscribe(updateCh, stopCh)
|
if err := netlink.LinkSubscribe(updateCh, stopCh); err != nil {
|
||||||
|
nm.logger.Error().Err(err).Msg("failed to subscribe to link state changes")
|
||||||
|
}
|
||||||
|
|
||||||
nm.logger.Info().Msg("state change monitoring started")
|
nm.logger.Info().Msg("state change monitoring started")
|
||||||
|
|
||||||
|
|
@ -153,7 +155,7 @@ func (nm *NetlinkManager) EnsureInterfaceUpWithTimeout(ctx context.Context, ifac
|
||||||
|
|
||||||
linkUpTimeout := time.After(timeout)
|
linkUpTimeout := time.After(timeout)
|
||||||
|
|
||||||
attempt := 0
|
var attempt int
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|
@ -208,7 +210,8 @@ func (nm *NetlinkManager) EnsureInterfaceUpWithTimeout(ctx context.Context, ifac
|
||||||
return nil, ErrInterfaceUpCanceled
|
return nil, ErrInterfaceUpCanceled
|
||||||
case <-linkUpTimeout:
|
case <-linkUpTimeout:
|
||||||
attempt++
|
attempt++
|
||||||
l.Error().Msg("interface is still down after timeout")
|
l.Error().
|
||||||
|
Int("attempt", attempt).Msg("interface is still down after timeout")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -444,12 +447,22 @@ func (nm *NetlinkManager) ReconcileLink(link *Link, expected []types.IPAddress,
|
||||||
|
|
||||||
expectedGateways := make(map[string]net.IP)
|
expectedGateways := make(map[string]net.IP)
|
||||||
|
|
||||||
|
mtu := link.Attrs().MTU
|
||||||
|
expectedMTU := mtu
|
||||||
// add all expected addresses to the map
|
// add all expected addresses to the map
|
||||||
for _, addr := range expected {
|
for _, addr := range expected {
|
||||||
expectedAddrs[addr.String()] = &addr
|
expectedAddrs[addr.String()] = &addr
|
||||||
if addr.Gateway != nil {
|
if addr.Gateway != nil {
|
||||||
expectedGateways[addr.String()] = addr.Gateway
|
expectedGateways[addr.String()] = addr.Gateway
|
||||||
}
|
}
|
||||||
|
if addr.MTU != 0 {
|
||||||
|
mtu = addr.MTU
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if expectedMTU != mtu {
|
||||||
|
if err := link.SetMTU(expectedMTU); err != nil {
|
||||||
|
nm.logger.Warn().Err(err).Int("expected_mtu", expectedMTU).Int("mtu", mtu).Msg("failed to set MTU")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs, err := nm.AddrList(link, family)
|
addrs, err := nm.AddrList(link, family)
|
||||||
|
|
@ -507,6 +520,13 @@ func (nm *NetlinkManager) ReconcileLink(link *Link, expected []types.IPAddress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, addr := range toAdd {
|
||||||
|
netlinkAddr := addr.NetlinkAddr()
|
||||||
|
if err := nm.AddrAdd(link, &netlinkAddr); err != nil {
|
||||||
|
nm.logger.Warn().Err(err).Str("address", addr.Address.String()).Msg("failed to add address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actualToAdd := len(toAdd) - len(toUpdate)
|
actualToAdd := len(toAdd) - len(toUpdate)
|
||||||
if len(toAdd) > 0 || len(toUpdate) > 0 || len(toRemove) > 0 {
|
if len(toAdd) > 0 || len(toUpdate) > 0 || len(toRemove) > 0 {
|
||||||
nm.logger.Info().
|
nm.logger.Info().
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,13 @@ func (l *Link) AddrList(family int) ([]netlink.Addr, error) {
|
||||||
return netlink.AddrList(l.Link, family)
|
return netlink.AddrList(l.Link, family)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Link) SetMTU(mtu int) error {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
return netlink.LinkSetMTU(l.Link, mtu)
|
||||||
|
}
|
||||||
|
|
||||||
// HasGlobalUnicastAddress returns true if the link has a global unicast address
|
// HasGlobalUnicastAddress returns true if the link has a global unicast address
|
||||||
func (l *Link) HasGlobalUnicastAddress() bool {
|
func (l *Link) HasGlobalUnicastAddress() bool {
|
||||||
addrs, err := l.AddrList(AfUnspec)
|
addrs, err := l.AddrList(AfUnspec)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ type NetworkManager struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
resolvConf *ResolvConfManager
|
||||||
|
|
||||||
// Callback functions for state changes
|
// Callback functions for state changes
|
||||||
onInterfaceStateChange func(iface string, state types.InterfaceState)
|
onInterfaceStateChange func(iface string, state types.InterfaceState)
|
||||||
onConfigChange func(iface string, config *types.NetworkConfig)
|
onConfigChange func(iface string, config *types.NetworkConfig)
|
||||||
|
|
@ -45,9 +47,20 @@ func NewNetworkManager(ctx context.Context, logger *zerolog.Logger) *NetworkMana
|
||||||
logger: logger,
|
logger: logger,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
resolvConf: NewResolvConfManager(logger),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetHostname sets the hostname and domain for the network manager
|
||||||
|
func (nm *NetworkManager) SetHostname(hostname string, domain string) error {
|
||||||
|
return nm.resolvConf.SetHostname(hostname, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain returns the effective domain for the network manager
|
||||||
|
func (nm *NetworkManager) Domain() string {
|
||||||
|
return nm.resolvConf.Domain()
|
||||||
|
}
|
||||||
|
|
||||||
// AddInterface adds a new network interface to be managed
|
// AddInterface adds a new network interface to be managed
|
||||||
func (nm *NetworkManager) AddInterface(iface string, config *types.NetworkConfig) error {
|
func (nm *NetworkManager) AddInterface(iface string, config *types.NetworkConfig) error {
|
||||||
nm.mu.Lock()
|
nm.mu.Lock()
|
||||||
|
|
@ -65,6 +78,7 @@ func (nm *NetworkManager) AddInterface(iface string, config *types.NetworkConfig
|
||||||
// Set up callbacks
|
// Set up callbacks
|
||||||
im.SetOnStateChange(func(state types.InterfaceState) {
|
im.SetOnStateChange(func(state types.InterfaceState) {
|
||||||
if nm.onInterfaceStateChange != nil {
|
if nm.onInterfaceStateChange != nil {
|
||||||
|
state.Hostname = nm.Hostname()
|
||||||
nm.onInterfaceStateChange(iface, state)
|
nm.onInterfaceStateChange(iface, state)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -81,6 +95,10 @@ func (nm *NetworkManager) AddInterface(iface string, config *types.NetworkConfig
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
im.SetOnResolvConfChange(func(family int, resolvConf *types.InterfaceResolvConf) error {
|
||||||
|
return nm.resolvConf.SetInterfaceConfig(iface, family, *resolvConf)
|
||||||
|
})
|
||||||
|
|
||||||
nm.interfaces[iface] = im
|
nm.interfaces[iface] = im
|
||||||
|
|
||||||
// Start monitoring the interface
|
// Start monitoring the interface
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,29 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/network/types"
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
|
"github.com/jetkvm/kvm/internal/sync"
|
||||||
|
"github.com/jetkvm/kvm/pkg/nmlite/link"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
resolvConfPath = "/etc/resolv.conf"
|
resolvConfPath = "/etc/resolv.conf"
|
||||||
resolvConfFileMode = 0644
|
resolvConfFileMode = 0644
|
||||||
resolvConfTemplate = `# the resolv.conf file is managed by the jetkvm network manager
|
resolvConfTemplate = `# the resolv.conf file is managed by JetKVM
|
||||||
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
|
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
|
||||||
|
|
||||||
{{ if .searchList }}
|
{{ if .searchList }}
|
||||||
search {{ join .searchList " " }} # {{ .iface }}
|
search {{ join .searchList " " }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{ if .domain }}
|
{{ if .domain }}
|
||||||
domain {{ .domain }} # {{ .iface }}
|
domain {{ .domain }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{ range .nameservers }}
|
{{ range $ns, $comment := .nameservers }}
|
||||||
nameserver {{ printf "%s" . }} # {{ $.iface }}
|
nameserver {{ printf "%s" $ns }} # {{ join $comment ", " }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
@ -39,6 +40,11 @@ var (
|
||||||
// ResolvConfManager manages the resolv.conf file
|
// ResolvConfManager manages the resolv.conf file
|
||||||
type ResolvConfManager struct {
|
type ResolvConfManager struct {
|
||||||
logger *zerolog.Logger
|
logger *zerolog.Logger
|
||||||
|
mu sync.Mutex
|
||||||
|
conf *types.ResolvConf
|
||||||
|
|
||||||
|
hostname string
|
||||||
|
domain string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewResolvConfManager creates a new resolv.conf manager
|
// NewResolvConfManager creates a new resolv.conf manager
|
||||||
|
|
@ -50,148 +56,154 @@ func NewResolvConfManager(logger *zerolog.Logger) *ResolvConfManager {
|
||||||
|
|
||||||
return &ResolvConfManager{
|
return &ResolvConfManager{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
conf: &types.ResolvConf{
|
||||||
|
ConfigIPv4: make(map[string]types.InterfaceResolvConf),
|
||||||
|
ConfigIPv6: make(map[string]types.InterfaceResolvConf),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFromLease updates resolv.conf from a DHCP lease
|
// SetInterfaceConfig sets the resolv.conf configuration for a specific interface
|
||||||
func (rcm *ResolvConfManager) UpdateFromLease(lease *types.DHCPLease) error {
|
func (rcm *ResolvConfManager) SetInterfaceConfig(iface string, family int, config types.InterfaceResolvConf) error {
|
||||||
if lease == nil {
|
// DO NOT USE defer HERE, rcm.update() also locks the mutex
|
||||||
return fmt.Errorf("lease cannot be nil")
|
rcm.mu.Lock()
|
||||||
|
switch family {
|
||||||
|
case link.AfInet:
|
||||||
|
rcm.conf.ConfigIPv4[iface] = config
|
||||||
|
case link.AfInet6:
|
||||||
|
rcm.conf.ConfigIPv6[iface] = config
|
||||||
|
default:
|
||||||
|
rcm.mu.Unlock()
|
||||||
|
return fmt.Errorf("invalid family: %d", family)
|
||||||
|
}
|
||||||
|
rcm.mu.Unlock()
|
||||||
|
|
||||||
|
if err := rcm.reconcileHostname(); err != nil {
|
||||||
|
return fmt.Errorf("failed to reconcile hostname: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rcm.logger.Info().
|
return rcm.update()
|
||||||
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
|
// SetConfig sets the resolv.conf configuration
|
||||||
func (rcm *ResolvConfManager) UpdateFromStaticConfig(iface string, dns []string) error {
|
func (rcm *ResolvConfManager) SetConfig(resolvConf *types.ResolvConf) error {
|
||||||
if len(dns) == 0 {
|
if resolvConf == nil {
|
||||||
rcm.logger.Debug().Str("interface", iface).Msg("no DNS servers in static config")
|
return fmt.Errorf("resolvConf cannot be nil")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse DNS servers
|
rcm.mu.Lock()
|
||||||
var dnsIPs []net.IP
|
rcm.conf = resolvConf
|
||||||
for _, dnsStr := range dns {
|
defer rcm.mu.Unlock()
|
||||||
dnsIP := net.ParseIP(dnsStr)
|
|
||||||
if dnsIP == nil {
|
return rcm.update()
|
||||||
rcm.logger.Warn().Str("dns", dnsStr).Msg("invalid DNS server, skipping")
|
}
|
||||||
continue
|
|
||||||
}
|
// Reconcile reconciles the resolv.conf configuration
|
||||||
dnsIPs = append(dnsIPs, dnsIP)
|
func (rcm *ResolvConfManager) Reconcile() error {
|
||||||
|
if err := rcm.reconcileHostname(); err != nil {
|
||||||
|
return fmt.Errorf("failed to reconcile hostname: %w", err)
|
||||||
}
|
}
|
||||||
|
return rcm.update()
|
||||||
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
|
// Update updates the resolv.conf file
|
||||||
func (rcm *ResolvConfManager) Update(iface string, nameservers []net.IP, searchList []string, domain string) error {
|
func (rcm *ResolvConfManager) update() error {
|
||||||
rcm.logger.Debug().
|
rcm.mu.Lock()
|
||||||
Str("interface", iface).
|
defer rcm.mu.Unlock()
|
||||||
Interface("nameservers", nameservers).
|
|
||||||
Interface("searchList", searchList).
|
rcm.logger.Debug().Msg("updating resolv.conf")
|
||||||
Str("domain", domain).
|
|
||||||
Msg("updating resolv.conf")
|
|
||||||
|
|
||||||
// Generate resolv.conf content
|
// Generate resolv.conf content
|
||||||
content, err := rcm.generateResolvConf(iface, nameservers, searchList, domain)
|
content, err := rcm.generateResolvConf(rcm.conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to generate resolv.conf: %w", err)
|
return fmt.Errorf("failed to generate resolv.conf: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the file is the same
|
||||||
|
if _, err := os.Stat(resolvConfPath); err == nil {
|
||||||
|
existingContent, err := os.ReadFile(resolvConfPath)
|
||||||
|
if err != nil {
|
||||||
|
rcm.logger.Warn().Err(err).Msg("failed to read existing resolv.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(existingContent, content) {
|
||||||
|
rcm.logger.Debug().Msg("resolv.conf is the same, skipping write")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write to file
|
// Write to file
|
||||||
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
|
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
|
||||||
return fmt.Errorf("failed to write resolv.conf: %w", err)
|
return fmt.Errorf("failed to write resolv.conf: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rcm.logger.Info().
|
rcm.logger.Info().
|
||||||
Str("interface", iface).
|
Interface("config", rcm.conf).
|
||||||
Int("nameservers", len(nameservers)).
|
|
||||||
Msg("resolv.conf updated successfully")
|
Msg("resolv.conf updated successfully")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type configMap map[string][]string
|
||||||
|
|
||||||
|
func mergeConfig(nameservers *configMap, searchList *configMap, config *types.InterfaceResolvConfMap) {
|
||||||
|
localNameservers := *nameservers
|
||||||
|
localSearchList := *searchList
|
||||||
|
|
||||||
|
for ifname, iface := range *config {
|
||||||
|
comment := ifname
|
||||||
|
if iface.Source != "" {
|
||||||
|
comment += fmt.Sprintf(" (%s)", iface.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range iface.NameServers {
|
||||||
|
ns := ip.String()
|
||||||
|
if _, ok := localNameservers[ns]; !ok {
|
||||||
|
localNameservers[ns] = []string{}
|
||||||
|
}
|
||||||
|
localNameservers[ns] = append(localNameservers[ns], comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, search := range iface.SearchList {
|
||||||
|
search = strings.Trim(search, ".")
|
||||||
|
if _, ok := localSearchList[search]; !ok {
|
||||||
|
localSearchList[search] = []string{}
|
||||||
|
}
|
||||||
|
localSearchList[search] = append(localSearchList[search], comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*nameservers = localNameservers
|
||||||
|
*searchList = localSearchList
|
||||||
|
}
|
||||||
|
|
||||||
// generateResolvConf generates resolv.conf content
|
// generateResolvConf generates resolv.conf content
|
||||||
func (rcm *ResolvConfManager) generateResolvConf(iface string, nameservers []net.IP, searchList []string, domain string) ([]byte, error) {
|
func (rcm *ResolvConfManager) generateResolvConf(conf *types.ResolvConf) ([]byte, error) {
|
||||||
tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate)
|
tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse template: %w", err)
|
return nil, fmt.Errorf("failed to parse template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// merge the nameservers and searchList
|
||||||
|
nameservers := configMap{}
|
||||||
|
searchList := configMap{}
|
||||||
|
|
||||||
|
mergeConfig(&nameservers, &searchList, &conf.ConfigIPv4)
|
||||||
|
mergeConfig(&nameservers, &searchList, &conf.ConfigIPv6)
|
||||||
|
|
||||||
|
flattenedSearchList := []string{}
|
||||||
|
for search := range searchList {
|
||||||
|
flattenedSearchList = append(flattenedSearchList, search)
|
||||||
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := tmpl.Execute(&buf, map[string]any{
|
if err := tmpl.Execute(&buf, map[string]any{
|
||||||
"iface": iface,
|
|
||||||
"nameservers": nameservers,
|
"nameservers": nameservers,
|
||||||
"searchList": searchList,
|
"searchList": flattenedSearchList,
|
||||||
"domain": domain,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, fmt.Errorf("failed to execute template: %w", err)
|
return nil, fmt.Errorf("failed to execute template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package nmlite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestToResolvConf(t *testing.T) {
|
|
||||||
rc, err := ResolvConfManager{}.generateResolvConf(
|
|
||||||
"eth0",
|
|
||||||
[]net.IP{
|
|
||||||
net.ParseIP("198.51.100.53"),
|
|
||||||
net.ParseIP("203.0.113.53"),
|
|
||||||
},
|
|
||||||
[]string{"example.com"},
|
|
||||||
"example.com",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
want := `# the resolv.conf file is managed by the jetkvm network manager
|
|
||||||
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
|
|
||||||
|
|
||||||
|
|
||||||
search example.com # eth0
|
|
||||||
domain example.com # eth0
|
|
||||||
nameserver 198.51.100.53 # eth0
|
|
||||||
nameserver 203.0.113.53 # eth0
|
|
||||||
`
|
|
||||||
|
|
||||||
assert.Equal(t, want, rc.String())
|
|
||||||
}
|
|
||||||
|
|
@ -20,12 +20,12 @@ func (nm *NetworkManager) IsUp() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nm *NetworkManager) GetHostname() string {
|
func (nm *NetworkManager) Hostname() string {
|
||||||
return "jetkvm"
|
return nm.resolvConf.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nm *NetworkManager) GetFQDN() string {
|
func (nm *NetworkManager) FQDN() string {
|
||||||
return "jetkvm.local"
|
return nm.resolvConf.FQDN()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nm *NetworkManager) NTPServers() []net.IP {
|
func (nm *NetworkManager) NTPServers() []net.IP {
|
||||||
|
|
|
||||||
|
|
@ -182,9 +182,3 @@ func (scm *StaticConfigManager) removeIPv4DefaultRoute() error {
|
||||||
netlinkMgr := getNetlinkManager()
|
netlinkMgr := getNetlinkManager()
|
||||||
return netlinkMgr.RemoveDefaultRoute(link.AfInet)
|
return netlinkMgr.RemoveDefaultRoute(link.AfInet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// enableIPv6 enables IPv6 on the interface
|
|
||||||
func (scm *StaticConfigManager) enableIPv6() error {
|
|
||||||
netlinkMgr := getNetlinkManager()
|
|
||||||
return netlinkMgr.EnableIPv6(scm.ifaceName)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,15 @@ func compareIPv6AddressSlices(a, b []types.IPv6Address) bool {
|
||||||
if a[i].Address.String() != b[i].Address.String() {
|
if a[i].Address.String() != b[i].Address.String() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if a[i].Prefix.String() != b[i].Prefix.String() {
|
if a[i].Prefix.String() != b[i].Prefix.String() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a[i].Flags != b[i].Flags {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// we don't compare the lifetimes because they are not always same
|
// we don't compare the lifetimes because they are not always same
|
||||||
if a[i].Scope != b[i].Scope {
|
if a[i].Scope != b[i].Scope {
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,22 @@
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
import { NetworkState } from "../hooks/stores";
|
import { NetworkState } from "../hooks/stores";
|
||||||
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
|
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
|
||||||
|
|
||||||
import { GridCard } from "./Card";
|
import { GridCard } from "./Card";
|
||||||
|
|
||||||
|
export function FlagLabel({ flag, className }: { flag: string, className?: string }) {
|
||||||
|
const classes = cx(
|
||||||
|
"ml-2 rounded-sm bg-red-500 px-2 py-1 text-[10px] font-medium leading-none text-white dark:border",
|
||||||
|
"bg-red-500 text-white dark:border-red-700 dark:bg-red-800 dark:text-red-50",
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
return <span className={classes}>
|
||||||
|
{flag}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
export default function Ipv6NetworkCard({
|
export default function Ipv6NetworkCard({
|
||||||
networkState,
|
networkState,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -49,7 +63,13 @@ export default function Ipv6NetworkCard({
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
Address
|
Address
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">{addr.address}</span>
|
<span className="text-sm font-medium flex">
|
||||||
|
<span className="flex-1">{addr.address}</span>
|
||||||
|
<span className="text-sm font-medium flex gap-x-1">
|
||||||
|
{addr.flag_deprecated ? <FlagLabel flag="Deprecated" /> : null}
|
||||||
|
{addr.flag_dad_failed ? <FlagLabel flag="DAD Failed" /> : null}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{addr.valid_lifetime && (
|
{addr.valid_lifetime && (
|
||||||
|
|
|
||||||
|
|
@ -699,6 +699,15 @@ export interface IPv6Address {
|
||||||
valid_lifetime: string;
|
valid_lifetime: string;
|
||||||
preferred_lifetime: string;
|
preferred_lifetime: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
|
flags: number;
|
||||||
|
flag_secondary?: boolean;
|
||||||
|
flag_permanent?: boolean;
|
||||||
|
flag_temporary?: boolean;
|
||||||
|
flag_stable_privacy?: boolean;
|
||||||
|
flag_deprecated?: boolean;
|
||||||
|
flag_optimistic?: boolean;
|
||||||
|
flag_dad_failed?: boolean;
|
||||||
|
flag_tentative?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkState {
|
export interface NetworkState {
|
||||||
|
|
@ -711,6 +720,7 @@ export interface NetworkState {
|
||||||
ipv6_link_local?: string;
|
ipv6_link_local?: string;
|
||||||
ipv6_gateway?: string;
|
ipv6_gateway?: string;
|
||||||
dhcp_lease?: DhcpLease;
|
dhcp_lease?: DhcpLease;
|
||||||
|
hostname?: string;
|
||||||
|
|
||||||
setNetworkState: (state: NetworkState) => void;
|
setNetworkState: (state: NetworkState) => void;
|
||||||
setDhcpLease: (lease: NetworkState["dhcp_lease"]) => void;
|
setDhcpLease: (lease: NetworkState["dhcp_lease"]) => void;
|
||||||
|
|
@ -749,6 +759,7 @@ export interface IPv6StaticConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkSettings {
|
export interface NetworkSettings {
|
||||||
|
dhcp_client: string;
|
||||||
hostname: string | null;
|
hostname: string | null;
|
||||||
domain: string | null;
|
domain: string | null;
|
||||||
http_proxy: string | null;
|
http_proxy: string | null;
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ export default function SettingsNetworkRoute() {
|
||||||
notifications.error(err instanceof Error ? err.message : "Unknown error");
|
notifications.error(err instanceof Error ? err.message : "Unknown error");
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}, []);
|
}, [setNetworkState]);
|
||||||
|
|
||||||
const formMethods = useForm<NetworkSettings>({
|
const formMethods = useForm<NetworkSettings>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
|
|
@ -155,7 +155,7 @@ export default function SettingsNetworkRoute() {
|
||||||
const { register, handleSubmit, watch, formState, reset } = formMethods;
|
const { register, handleSubmit, watch, formState, reset } = formMethods;
|
||||||
|
|
||||||
const onSubmit = async (settings: NetworkSettings) => {
|
const onSubmit = async (settings: NetworkSettings) => {
|
||||||
send("setNetworkSettings", { settings }, async (resp: any) => {
|
send("setNetworkSettings", { settings }, async (resp) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
return notifications.error(
|
return notifications.error(
|
||||||
resp.error.data ? resp.error.data : resp.error.message,
|
resp.error.data ? resp.error.data : resp.error.message,
|
||||||
|
|
@ -205,7 +205,7 @@ export default function SettingsNetworkRoute() {
|
||||||
const ipv6mode = watch("ipv6_mode");
|
const ipv6mode = watch("ipv6_mode");
|
||||||
|
|
||||||
const onDhcpLeaseRenew = () => {
|
const onDhcpLeaseRenew = () => {
|
||||||
send("renewDHCPLease", {}, (resp: any) => {
|
send("renewDHCPLease", {}, (resp) => {
|
||||||
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 {
|
||||||
|
|
@ -253,7 +253,7 @@ export default function SettingsNetworkRoute() {
|
||||||
<SettingsItem title="Hostname" description="Set the device hostname">
|
<SettingsItem title="Hostname" description="Set the device hostname">
|
||||||
<InputField
|
<InputField
|
||||||
size="SM"
|
size="SM"
|
||||||
placeholder="jetkvm"
|
placeholder={networkState?.hostname || "jetkvm"}
|
||||||
{...register("hostname")}
|
{...register("hostname")}
|
||||||
error={formState.errors.hostname?.message}
|
error={formState.errors.hostname?.message}
|
||||||
/>
|
/>
|
||||||
|
|
@ -334,6 +334,17 @@ export default function SettingsNetworkRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem title="DHCP client" description="Configure which DHCP client to use (reboot required)">
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
options={[
|
||||||
|
{ value: "jetdhcpc", label: "JetKVM" },
|
||||||
|
{ value: "udhcpc", label: "udhcpc" },
|
||||||
|
]}
|
||||||
|
{...register("dhcp_client")}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
||||||
<SelectMenuBasic
|
<SelectMenuBasic
|
||||||
size="SM"
|
size="SM"
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export function callJsonRpc(options: JsonRpcCallOptions): Promise<JsonRpcCallRes
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeout = options.timeout || 5000;
|
const timeout = options.timeout || 5000;
|
||||||
let timeoutId: number | undefined;
|
let timeoutId: number | undefined; // eslint-disable-line prefer-const
|
||||||
|
|
||||||
const messageHandler = (event: MessageEvent) => {
|
const messageHandler = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue