diff --git a/internal/lldp/lldp.go b/internal/lldp/lldp.go index 11f17dd9..450e06d2 100644 --- a/internal/lldp/lldp.go +++ b/internal/lldp/lldp.go @@ -116,16 +116,6 @@ func (l *LLDP) startRx() error { return l.startCapture() } -// StopRx stops the LLDP receiver if running -func (l *LLDP) StopRx() error { - return l.stopCapture() -} - -// StopTx stops the LLDP transmitter if running -func (l *LLDP) StopTx() error { - return l.stopTx() -} - // SetAdvertiseOptions updates the advertise options and resends LLDP packets if TX is running func (l *LLDP) SetAdvertiseOptions(opts *AdvertiseOptions) error { l.mu.Lock() @@ -143,3 +133,34 @@ func (l *LLDP) SetAdvertiseOptions(opts *AdvertiseOptions) error { return nil } + +func (l *LLDP) SetRxAndTx(rx, tx bool) error { + l.mu.Lock() + l.enableRx = rx + l.enableTx = tx + l.mu.Unlock() + + // if rx is enabled, start the RX + if rx { + if err := l.startRx(); err != nil { + return fmt.Errorf("failed to start RX: %w", err) + } + } else { + if err := l.stopRx(); err != nil { + return fmt.Errorf("failed to stop RX: %w", err) + } + } + + // if tx is enabled, start the TX + if tx { + if err := l.startTx(); err != nil { + return fmt.Errorf("failed to start TX: %w", err) + } + } else { + if err := l.stopTx(); err != nil { + return fmt.Errorf("failed to stop TX: %w", err) + } + } + + return nil +} diff --git a/internal/lldp/neigh.go b/internal/lldp/neigh.go index 5291e1fa..b3460738 100644 --- a/internal/lldp/neigh.go +++ b/internal/lldp/neigh.go @@ -2,8 +2,6 @@ package lldp import ( "fmt" - "sort" - "strings" "time" ) @@ -50,8 +48,10 @@ func (l *LLDP) addNeighbor(neighbor *Neighbor, ttl time.Duration) { } } - logger.Info().Msg("adding neighbor") + logger.Trace().Msg("adding neighbor") l.neighbors.Set(key, *neighbor, ttl) + + l.onChange(l.GetNeighbors()) } func (l *LLDP) deleteNeighbor(neighbor *Neighbor) { @@ -61,6 +61,8 @@ func (l *LLDP) deleteNeighbor(neighbor *Neighbor) { logger.Info().Msg("deleting neighbor") l.neighbors.Delete(neighbor.cacheKey()) + + l.onChange(l.GetNeighbors()) } func (l *LLDP) GetNeighbors() []Neighbor { @@ -71,10 +73,5 @@ func (l *LLDP) GetNeighbors() []Neighbor { neighbors = append(neighbors, item.Value()) } - // sort based on MAC address - sort.Slice(neighbors, func(i, j int) bool { - return strings.Compare(neighbors[i].Mac, neighbors[j].Mac) > 0 - }) - return neighbors } diff --git a/internal/lldp/rx.go b/internal/lldp/rx.go index b822e832..1ea35156 100644 --- a/internal/lldp/rx.go +++ b/internal/lldp/rx.go @@ -166,8 +166,7 @@ func (l *LLDP) handlePacket(packet gopacket.Packet, logger *zerolog.Logger) erro lldpRaw := packet.Layer(layers.LayerTypeLinkLayerDiscovery) if lldpRaw != nil { - logger.Trace().Msg("Found LLDP Frame") - l.l.Info().Hex("packet", packet.Data()).Msg("received packet") + l.l.Trace().Hex("packet", packet.Data()).Msg("received LLDP frame") lldpInfo := packet.Layer(layers.LayerTypeLinkLayerDiscoveryInfo) if lldpInfo == nil { @@ -183,7 +182,7 @@ func (l *LLDP) handlePacket(packet gopacket.Packet, logger *zerolog.Logger) erro cdpRaw := packet.Layer(layers.LayerTypeCiscoDiscovery) if cdpRaw != nil { - logger.Trace().Msg("Found CDP Frame") + l.l.Trace().Hex("packet", packet.Data()).Msg("received CDP frame") cdpInfo := packet.Layer(layers.LayerTypeCiscoDiscoveryInfo) if cdpInfo == nil { @@ -351,6 +350,9 @@ func (l *LLDP) stopCapture() error { l.rxCancel = nil } + // Wait a bit for goroutine to finish + time.Sleep(1000 * time.Millisecond) + if l.tPacketRx != nil { l.tPacketRx.Close() l.tPacketRx = nil @@ -360,7 +362,17 @@ func (l *LLDP) stopCapture() error { l.pktSourceRx = nil } - time.Sleep(100 * time.Millisecond) + return nil +} + +func (l *LLDP) stopRx() error { + if err := l.stopCapture(); err != nil { + return err + } + + // clean up the neighbors table + l.neighbors.DeleteAll() + l.onChange([]Neighbor{}) return nil } diff --git a/internal/network/types/config.go b/internal/network/types/config.go index 364f8609..bc6a6900 100644 --- a/internal/network/types/config.go +++ b/internal/network/types/config.go @@ -42,7 +42,7 @@ type NetworkConfig struct { IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` - LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` + LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,rx_and_tx,basic,all" default:"rx_and_tx"` LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` @@ -53,6 +53,14 @@ type NetworkConfig struct { TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"` } +func (c *NetworkConfig) ShouldEnableLLDPTransmit() bool { + return c.LLDPMode.String != "rx_only" && c.LLDPMode.String != "disabled" +} + +func (c *NetworkConfig) ShouldEnableLLDPReceive() bool { + return c.LLDPMode.String != "tx_only" && c.LLDPMode.String != "disabled" +} + // GetMDNSMode returns the MDNS mode configuration func (c *NetworkConfig) GetMDNSMode() *MDNSListenOptions { mode := c.MDNSMode.String diff --git a/network.go b/network.go index ed2a555f..30c76f53 100644 --- a/network.go +++ b/network.go @@ -144,6 +144,15 @@ func validateNetworkConfig() { } } +func getLLDPAdvertiseOptions(nm *nmlite.NetworkManager) *lldp.AdvertiseOptions { + return &lldp.AdvertiseOptions{ + SysName: nm.Hostname(), + SysDescription: toLLDPSysDescription(), + SysCapabilities: []string{"other", "router", "wlanap"}, + EnabledCapabilities: []string{"other"}, + } +} + func initNetwork() error { ensureConfigLoaded() @@ -163,17 +172,11 @@ func initNetwork() error { networkManager = nm - advertiseOptions := &lldp.AdvertiseOptions{ - SysName: networkManager.Hostname(), - SysDescription: toLLDPSysDescription(nc), - SysCapabilities: []string{"other", "router", "wlanap"}, - EnabledCapabilities: []string{"other"}, - } - + advertiseOptions := getLLDPAdvertiseOptions(nm) lldpService = lldp.NewLLDP(&lldp.Options{ InterfaceName: NetIfName, - EnableRx: nc.LLDPMode.String != "disabled", - EnableTx: nc.LLDPMode.String != "disabled", + EnableRx: nc.ShouldEnableLLDPReceive(), + EnableTx: nc.ShouldEnableLLDPTransmit(), AdvertiseOptions: advertiseOptions, OnChange: func(neighbors []lldp.Neighbor) { writeJSONRPCEvent("lldpNeighbors", neighbors, currentSession) @@ -187,7 +190,7 @@ func initNetwork() error { return nil } -func toLLDPSysDescription(nc *types.NetworkConfig) string { +func toLLDPSysDescription() string { systemVersion, appVersion, err := GetLocalVersion() if err == nil { return fmt.Sprintf("JetKVM (app: %s)", GetBuiltAppVersion()) @@ -196,6 +199,21 @@ func toLLDPSysDescription(nc *types.NetworkConfig) string { return fmt.Sprintf("JetKVM (app: %s, system: %s)", appVersion.String(), systemVersion.String()) } +func updateLLDPOptions(nc *types.NetworkConfig) { + if lldpService == nil { + return + } + + if err := lldpService.SetRxAndTx(nc.ShouldEnableLLDPReceive(), nc.ShouldEnableLLDPTransmit()); err != nil { + networkLogger.Error().Err(err).Msg("failed to set LLDP RX and TX") + } + + advertiseOptions := getLLDPAdvertiseOptions(networkManager) + if err := lldpService.SetAdvertiseOptions(advertiseOptions); err != nil { + networkLogger.Error().Err(err).Msg("failed to set LLDP advertise options") + } +} + func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error { if nm == nil { return nil @@ -209,6 +227,12 @@ func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error { } func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (rebootRequired bool, postRebootAction *PostRebootAction) { + rebootReasons := []string{} + defer func() { + if len(rebootReasons) > 0 { + networkLogger.Info().Strs("reasons", rebootReasons).Msg("reboot required") + } + }() oldDhcpClient := oldConfig.DHCPClient.String l := networkLogger.With(). @@ -217,9 +241,10 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re Logger() // DHCP client change always requires reboot - if newConfig.DHCPClient.String != oldDhcpClient { + newDhcpClient := newConfig.DHCPClient.String + if newDhcpClient != oldDhcpClient { rebootRequired = true - l.Info().Msg("DHCP client changed, reboot required") + rebootReasons = append(rebootReasons, fmt.Sprintf("DHCP client changed from %s to %s", oldDhcpClient, newDhcpClient)) return rebootRequired, postRebootAction } @@ -229,7 +254,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re // IPv4 mode change requires reboot if newIPv4Mode != oldIPv4Mode { rebootRequired = true - l.Info().Msg("IPv4 mode changed with udhcpc, reboot required") + rebootReasons = append(rebootReasons, fmt.Sprintf("IPv4 mode changed from %s to %s", oldIPv4Mode, newIPv4Mode)) if newIPv4Mode == "static" && oldIPv4Mode != "static" { postRebootAction = &PostRebootAction{ @@ -243,8 +268,11 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re } // IPv4 static config changes require reboot - if !reflect.DeepEqual(oldConfig.IPv4Static, newConfig.IPv4Static) { + // but if it's not activated, don't care about the changes + if !reflect.DeepEqual(oldConfig.IPv4Static, newConfig.IPv4Static) && newIPv4Mode == "static" { rebootRequired = true + // TODO: do not restart if it's just the DNS servers that changed + rebootReasons = append(rebootReasons, "IPv4 static config changed") // Handle IP change for redirect (only if both are not nil and IP changed) if newConfig.IPv4Static != nil && oldConfig.IPv4Static != nil && @@ -253,17 +281,17 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String), RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String), } - - l.Info().Interface("postRebootAction", postRebootAction).Msg("IPv4 static config changed, reboot required") } return rebootRequired, postRebootAction } // IPv6 mode change requires reboot when using udhcpc + oldIPv6Mode := oldConfig.IPv6Mode.String + newIPv6Mode := newConfig.IPv6Mode.String if newConfig.IPv6Mode.String != oldConfig.IPv6Mode.String && oldDhcpClient == "udhcpc" { rebootRequired = true - l.Info().Msg("IPv6 mode changed with udhcpc, reboot required") + rebootReasons = append(rebootReasons, fmt.Sprintf("IPv6 mode changed from %s to %s when using udhcpc", oldIPv6Mode, newIPv6Mode)) } return rebootRequired, postRebootAction @@ -288,6 +316,8 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er l.Debug().Msg("setting new config") + // TODO: do not restart everything if it's just the LLDP mode that changed + // Check if reboot is needed rebootRequired, postRebootAction := shouldRebootForNetworkChange(config.NetworkConfig, netConfig) @@ -311,6 +341,9 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er } config.NetworkConfig = newConfig + // update the LLDP advertise options + updateLLDPOptions(newConfig) + l.Debug().Msg("saving new config") if err := SaveConfig(); err != nil { return nil, err diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 0356e8e5..61f4136f 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -634,10 +634,11 @@ "network_ipv6_mode_title": "IPv6 Mode", "network_ipv6_prefix": "IP Prefix", "network_ipv6_prefix_invalid": "Prefix must be between 0 and 128", - "network_ll_dp_all": "All", - "network_ll_dp_basic": "Basic", "network_ll_dp_description": "Control which TLVs will be sent over Link Layer Discovery Protocol", "network_ll_dp_disabled": "Disabled", + "network_ll_dp_rx_only": "Receive only", + "network_ll_dp_tx_only": "Transmit only", + "network_ll_dp_rx_and_tx": "Receive and transmit", "network_ll_dp_title": "LLDP", "network_mac_address_copy_error": "Failed to copy MAC address", "network_mac_address_copy_success": "MAC address { mac } copied to clipboard", diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index b87d2f4e..3198b407 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -761,7 +761,7 @@ export type IPv6Mode = | "link_local" | "unknown"; export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown"; -export type LLDPMode = "disabled" | "basic" | "all" | "tx_only" | "rx_only" | "unknown"; +export type LLDPMode = "disabled" | "rx_only" | "tx_only" | "rx_and_tx" | "unknown"; export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown"; export type TimeSyncMode = | "ntp_only" @@ -835,6 +835,18 @@ export const useNetworkStateStore = create((set, get) => ({ }, })); + + +export interface LLDPNeighborsState { + neighbors: LLDPNeighbor[]; + setNeighbors: (neighbors: LLDPNeighbor[]) => void; +} + +export const useLLDPNeighborsStore = create((set) => ({ + neighbors: [], + setNeighbors: (neighbors: LLDPNeighbor[]) => set({ neighbors }), +})); + export interface KeySequenceStep { keys: string[]; modifiers: string[]; diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index aea5fbbe..d1b4a541 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -564,8 +564,9 @@ export default function SettingsNetworkRoute() { size="SM" options={[ { value: "disabled", label: m.network_ll_dp_disabled() }, - { value: "basic", label: m.network_ll_dp_basic() }, - { value: "all", label: m.network_ll_dp_all() }, + { value: "rx_only", label: m.network_ll_dp_rx_only() }, + { value: "tx_only", label: m.network_ll_dp_tx_only() }, + { value: "rx_and_tx", label: m.network_ll_dp_rx_and_tx() }, ]} {...register("lldp_mode")} /> diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index bae8faa6..e7ed6b27 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -21,11 +21,13 @@ import { checkAuth, isInCloud, isOnDevice } from "@/main"; import { KeyboardLedState, KeysDownState, + LLDPNeighbor, NetworkState, OtaState, PostRebootAction, USBStates, useHidStore, + useLLDPNeighborsStore, useNetworkStateStore, User, useRTCStore, @@ -612,6 +614,7 @@ export default function KvmIdRoute() { }, 10000); const { setNetworkState } = useNetworkStateStore(); + const { setNeighbors } = useLLDPNeighborsStore(); const { setHdmiState } = useVideoStore(); const { keyboardLedState, setKeyboardLedState, @@ -634,6 +637,12 @@ export default function KvmIdRoute() { setUsbState(usbState); } + if (resp.method === "lldpNeighbors") { + const neighbors = resp.params as LLDPNeighbor[]; + console.debug("Setting LLDP neighbors", neighbors); + setNeighbors(neighbors); + } + if (resp.method === "videoInputState") { const hdmiState = resp.params as Parameters[0]; console.debug("Setting HDMI state", hdmiState);