feat: add LLDP neighbors store and update network settings

This commit is contained in:
Siyuan 2025-11-06 14:46:59 +00:00
parent fd44ff49fd
commit 3e39361aa7
9 changed files with 139 additions and 45 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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<NetworkState>((set, get) => ({
},
}));
export interface LLDPNeighborsState {
neighbors: LLDPNeighbor[];
setNeighbors: (neighbors: LLDPNeighbor[]) => void;
}
export const useLLDPNeighborsStore = create<LLDPNeighborsState>((set) => ({
neighbors: [],
setNeighbors: (neighbors: LLDPNeighbor[]) => set({ neighbors }),
}));
export interface KeySequenceStep {
keys: string[];
modifiers: string[];

View File

@ -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")}
/>

View File

@ -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<VideoState["setHdmiState"]>[0];
console.debug("Setting HDMI state", hdmiState);