diff --git a/internal/lldp/lldp.go b/internal/lldp/lldp.go index 3c693707..e1d61233 100644 --- a/internal/lldp/lldp.go +++ b/internal/lldp/lldp.go @@ -3,12 +3,11 @@ package lldp import ( "context" "fmt" + "net" "sync" - "time" "github.com/google/gopacket" "github.com/google/gopacket/afpacket" - "github.com/jellydator/ttlcache/v3" "github.com/jetkvm/kvm/internal/logging" "github.com/rs/zerolog" ) @@ -30,7 +29,8 @@ type LLDP struct { advertiseOptions *AdvertiseOptions onChange func(neighbors []Neighbor) - neighbors *ttlcache.Cache[neighborCacheKey, Neighbor] + neighbors map[neighborCacheKey]Neighbor + neighborsMu sync.RWMutex // State tracking txRunning bool @@ -47,6 +47,8 @@ type AdvertiseOptions struct { SysName string SysDescription string PortDescription string + IPv4Address *net.IP + IPv6Address *net.IP SysCapabilities []string EnabledCapabilities []string } @@ -76,14 +78,12 @@ func NewLLDP(opts *Options) *LLDP { enableTx: opts.EnableTx, rxWaitGroup: &sync.WaitGroup{}, l: opts.Logger, - neighbors: ttlcache.New(ttlcache.WithTTL[neighborCacheKey, Neighbor](1 * time.Hour)), + neighbors: make(map[neighborCacheKey]Neighbor), onChange: opts.OnChange, } } func (l *LLDP) Start() error { - go l.neighbors.Start() - if l.enableRx { if err := l.startRx(); err != nil { return fmt.Errorf("failed to start RX: %w", err) diff --git a/internal/lldp/neigh.go b/internal/lldp/neigh.go index 3a951c64..7e2af084 100644 --- a/internal/lldp/neigh.go +++ b/internal/lldp/neigh.go @@ -13,26 +13,48 @@ type ManagementAddress struct { } type Neighbor struct { - Mac string `json:"mac"` - Source string `json:"source"` - ChassisID string `json:"chassis_id"` - PortID string `json:"port_id"` - PortDescription string `json:"port_description"` - SystemName string `json:"system_name"` - SystemDescription string `json:"system_description"` - TTL uint16 `json:"ttl"` - ManagementAddress *ManagementAddress `json:"management_address,omitempty"` - Capabilities []string `json:"capabilities"` - Values map[string]string `json:"values"` + Mac string `json:"mac"` + Source string `json:"source"` + ChassisID string `json:"chassis_id"` + PortID string `json:"port_id"` + PortDescription string `json:"port_description"` + SystemName string `json:"system_name"` + SystemDescription string `json:"system_description"` + TTL uint16 `json:"ttl"` + ManagementAddresses []ManagementAddress `json:"management_addresses"` + Capabilities []string `json:"capabilities"` + Values map[string]string `json:"values"` + cacheTTL time.Time + cacheKey neighborCacheKey } +const ( + NeighborSourceLLDP uint8 = 0x1 + NeighborSourceCDP = 0x2 +) + +var ( + NeighborSourceMap = map[uint8]string{ + NeighborSourceLLDP: "lldp", + NeighborSourceCDP: "cdp", + } +) + type neighborCacheKey struct { - mac string - source string + Mac string + Source uint8 } -func (n *Neighbor) cacheKey() neighborCacheKey { - return neighborCacheKey{mac: n.Mac, source: n.Source} +func newNeighbor(mac string, source uint8) *Neighbor { + return &Neighbor{ + Mac: mac, + Source: NeighborSourceMap[source], + Values: make(map[string]string), + cacheKey: neighborCacheKey{ + Mac: mac, + Source: source, + }, + } } func (l *LLDP) addNeighbor(neighbor *Neighbor, ttl time.Duration) { @@ -42,19 +64,18 @@ func (l *LLDP) addNeighbor(neighbor *Neighbor, ttl time.Duration) { Interface("neighbor", neighbor). Logger() - key := neighbor.cacheKey() + l.neighborsMu.RLock() - currentNeighbor := l.neighbors.Get(key) - if currentNeighbor != nil { - currentSource := currentNeighbor.Value().Source - if currentSource == "lldp" && neighbor.Source != "lldp" { - logger.Info().Msg("skip updating neighbor, as LLDP has higher priority") - return - } + _, ok := l.neighbors[neighbor.cacheKey] + if ok { + logger.Trace().Msg("neighbor already exists, updating it") } logger.Trace().Msg("adding neighbor") - l.neighbors.Set(key, *neighbor, ttl) + neighbor.cacheTTL = time.Now().Add(ttl) + l.neighbors[neighbor.cacheKey] = *neighbor + + l.neighborsMu.RUnlock() l.onChange(l.GetNeighbors()) } @@ -66,17 +87,33 @@ func (l *LLDP) deleteNeighbor(neighbor *Neighbor) { Logger() logger.Info().Msg("deleting neighbor") - l.neighbors.Delete(neighbor.cacheKey()) + + l.neighborsMu.Lock() + delete(l.neighbors, neighbor.cacheKey) + l.neighborsMu.Unlock() l.onChange(l.GetNeighbors()) } -func (l *LLDP) GetNeighbors() []Neighbor { - items := l.neighbors.Items() - neighbors := make([]Neighbor, 0, len(items)) +func (l *LLDP) flushNeighbors() { + l.neighborsMu.Lock() + defer l.neighborsMu.Unlock() - for _, item := range items { - neighbors = append(neighbors, item.Value()) + l.neighbors = make(map[neighborCacheKey]Neighbor) +} + +func (l *LLDP) GetNeighbors() []Neighbor { + l.neighborsMu.Lock() + defer l.neighborsMu.Unlock() + + neighbors := make([]Neighbor, 0) + + for key, neighbor := range l.neighbors { + if time.Now().After(neighbor.cacheTTL) { + delete(l.neighbors, key) + continue + } + neighbors = append(neighbors, neighbor) } return neighbors diff --git a/internal/lldp/rx.go b/internal/lldp/rx.go index cf8fb543..d6523ba7 100644 --- a/internal/lldp/rx.go +++ b/internal/lldp/rx.go @@ -237,11 +237,7 @@ func capabilitiesToString(capabilities layers.LLDPCapabilities) []string { } func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info *layers.LinkLayerDiscoveryInfo) error { - n := &Neighbor{ - Values: make(map[string]string), - Source: "lldp", - Mac: mac, - } + n := newNeighbor(mac, NeighborSourceLLDP) ttl := lldpDefaultTTL @@ -263,12 +259,12 @@ func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info n.SystemDescription = info.SysDescription n.Values["system_description"] = n.SystemDescription case layers.LLDPTLVMgmtAddress: - n.ManagementAddress = &ManagementAddress{ - AddressFamily: info.MgmtAddress.Subtype.String(), - Address: net.IP(info.MgmtAddress.Address).String(), - InterfaceSubtype: info.MgmtAddress.InterfaceSubtype.String(), - InterfaceNumber: info.MgmtAddress.InterfaceNumber, - OID: info.MgmtAddress.OID, + mgmtAddress := parseTlvMgmtAddress(v) + if mgmtAddress != nil { + n.ManagementAddresses = append( + n.ManagementAddresses, + lldpMgmtAddressToSerializable(mgmtAddress), + ) } case layers.LLDPTLVSysCapabilities: n.Capabilities = capabilitiesToString(info.SysCapabilities.EnabledCap) @@ -297,11 +293,7 @@ func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info func (l *LLDP) handlePacketCDP(mac string, raw *layers.CiscoDiscovery, info *layers.CiscoDiscoveryInfo) error { // TODO: implement full CDP parsing - n := &Neighbor{ - Values: make(map[string]string), - Source: "cdp", - Mac: mac, - } + n := newNeighbor(mac, NeighborSourceCDP) ttl := cdpDefaultTTL @@ -315,27 +307,18 @@ func (l *LLDP) handlePacketCDP(mac string, raw *layers.CiscoDiscovery, info *lay ttl = time.Duration(n.TTL) * time.Second } - if len(info.MgmtAddresses) > 0 { - ip := info.MgmtAddresses[0] - ipFamily := "ipv4" - if ip.To4() == nil { - ipFamily = "ipv6" + for _, addr := range info.MgmtAddresses { + addrFamily := "ipv4" + if addr.To4() == nil { + addrFamily = "ipv6" } - - l.l.Info(). - Str("ip", ip.String()). - Str("ip_family", ipFamily). - Interface("ip", ip). - Interface("info", info). - Msg("parsed IP address") - - n.ManagementAddress = &ManagementAddress{ - AddressFamily: ipFamily, - Address: ip.String(), + n.ManagementAddresses = append(n.ManagementAddresses, ManagementAddress{ + AddressFamily: addrFamily, + Address: addr.String(), InterfaceSubtype: "if_name", InterfaceNumber: 0, OID: "", - } + }) } l.addNeighbor(n, ttl) @@ -402,7 +385,7 @@ func (l *LLDP) stopRx() error { } // clean up the neighbors table - l.neighbors.DeleteAll() + l.flushNeighbors() l.onChange([]Neighbor{}) return nil diff --git a/internal/lldp/tx.go b/internal/lldp/tx.go index 658c9d4c..0fd66b1e 100644 --- a/internal/lldp/tx.go +++ b/internal/lldp/tx.go @@ -17,57 +17,6 @@ var ( lldpEtherType = layers.EthernetTypeLinkLayerDiscovery ) -// func encodeMandatoryTLV(subType byte, id []byte) []byte { -// // 1 byte: subtype -// // N bytes: ID -// b := make([]byte, 1+len(id)) -// b[0] = byte(subtype) -// copy(b[1:], id) - -// return b -// } - -// func (l *LLDP) createLLDPPayload() ([]byte, error) { -// tlv := &layers.LinkLayerDiscoveryValue{ -// Type: layers.LLDPTLVChassisID, - -// } - -func tlvStringValue(tlvType layers.LLDPTLVType, value string) layers.LinkLayerDiscoveryValue { - return layers.LinkLayerDiscoveryValue{ - Type: tlvType, - Value: []byte(value), - Length: uint16(len(value)), - } -} - -var ( - capabilityMap = map[string]uint16{ - "other": layers.LLDPCapsOther, - "repeater": layers.LLDPCapsRepeater, - "bridge": layers.LLDPCapsBridge, - "wlanap": layers.LLDPCapsWLANAP, - "router": layers.LLDPCapsRouter, - "phone": layers.LLDPCapsPhone, - "docsis": layers.LLDPCapsDocSis, - "station_only": layers.LLDPCapsStationOnly, - "cvlan": layers.LLDPCapsCVLAN, - "svlan": layers.LLDPCapsSVLAN, - "tmpr": layers.LLDPCapsTmpr, - } -) - -func toLLDPCapabilitiesBytes(capabilities []string) uint16 { - r := uint16(0) - for _, capability := range capabilities { - mask, ok := capabilityMap[capability] - if ok { - r |= mask - } - } - return r -} - func (l *LLDP) toPayloadValues() []layers.LinkLayerDiscoveryValue { // See also: layers.LinkLayerDiscovery.SerializeTo() r := []layers.LinkLayerDiscoveryValue{} @@ -88,6 +37,24 @@ func (l *LLDP) toPayloadValues() []layers.LinkLayerDiscoveryValue { r = append(r, tlvStringValue(layers.LLDPTLVSysDescription, opts.SysDescription)) } + if opts.IPv4Address != nil { + r = append(r, tlvMgmtAddress(&layers.LLDPMgmtAddress{ + Subtype: layers.IANAAddressFamilyIPV4, + Address: opts.IPv4Address.To4(), + InterfaceSubtype: layers.LLDPInterfaceSubtypeifIndex, + InterfaceNumber: 0, + })) + } + + if opts.IPv6Address != nil { + r = append(r, tlvMgmtAddress(&layers.LLDPMgmtAddress{ + Subtype: layers.IANAAddressFamilyIPV6, + Address: opts.IPv6Address.To16(), + InterfaceSubtype: layers.LLDPInterfaceSubtypeifIndex, + InterfaceNumber: 0, + })) + } + if len(opts.SysCapabilities) > 0 { value := make([]byte, 4) binary.BigEndian.PutUint16(value[0:2], toLLDPCapabilitiesBytes(opts.SysCapabilities)) diff --git a/network.go b/network.go index 4213b486..c06e772b 100644 --- a/network.go +++ b/network.go @@ -3,6 +3,7 @@ package kvm import ( "context" "fmt" + "net" "reflect" "github.com/jetkvm/kvm/internal/confparser" @@ -119,6 +120,11 @@ func networkStateChanged(_ string, state types.InterfaceState) { triggerTimeSyncOnNetworkStateChange() } + // update the LLDP advertise options + if lldpService != nil { + _ = lldpService.SetAdvertiseOptions(getLLDPAdvertiseOptions(&state)) + } + // always restart mDNS when the network state changes if mDNS != nil { restartMdns() @@ -144,13 +150,29 @@ func validateNetworkConfig() { } } -func getLLDPAdvertiseOptions(nm *nmlite.NetworkManager) *lldp.AdvertiseOptions { - return &lldp.AdvertiseOptions{ - SysName: nm.Hostname(), +func getLLDPAdvertiseOptions(state *types.InterfaceState) *lldp.AdvertiseOptions { + a := &lldp.AdvertiseOptions{ SysDescription: toLLDPSysDescription(), SysCapabilities: []string{"other", "router", "wlanap"}, EnabledCapabilities: []string{"other"}, } + if state == nil { + return a + } + + a.SysName = state.Hostname + ip4String := state.IPv4Address + if ip4String != "" { + ip4 := net.ParseIP(ip4String) + a.IPv4Address = &ip4 + } + ip6String := state.IPv6Address + if ip6String != "" { + ip6 := net.ParseIP(ip6String) + a.IPv6Address = &ip6 + } + networkLogger.Info().Interface("advertiseOptions", a).Msg("LLDP advertise options") + return a } func initNetwork() error { @@ -172,7 +194,12 @@ func initNetwork() error { networkManager = nm - advertiseOptions := getLLDPAdvertiseOptions(nm) + ifState, err := nm.GetInterfaceState(NetIfName) + if err != nil { + networkLogger.Warn().Err(err).Msg("failed to get interface state, LLDP will use the default options") + } + + advertiseOptions := getLLDPAdvertiseOptions(ifState) lldpService = lldp.NewLLDP(&lldp.Options{ InterfaceName: NetIfName, EnableRx: nc.ShouldEnableLLDPReceive(), @@ -200,7 +227,7 @@ func toLLDPSysDescription() string { return fmt.Sprintf("JetKVM (app: %s, system: %s)", appVersion.String(), systemVersion.String()) } -func updateLLDPOptions(nc *types.NetworkConfig) { +func updateLLDPOptions(nc *types.NetworkConfig, ifState *types.InterfaceState) { if lldpService == nil { return } @@ -209,7 +236,16 @@ func updateLLDPOptions(nc *types.NetworkConfig) { networkLogger.Error().Err(err).Msg("failed to set LLDP RX and TX") } - advertiseOptions := getLLDPAdvertiseOptions(networkManager) + if ifState == nil { + newIfState, err := networkManager.GetInterfaceState(NetIfName) + if err != nil { + networkLogger.Warn().Err(err).Msg("failed to get interface state, LLDP will use the default options") + return + } + ifState = newIfState + } + + advertiseOptions := getLLDPAdvertiseOptions(ifState) if err := lldpService.SetAdvertiseOptions(advertiseOptions); err != nil { networkLogger.Error().Err(err).Msg("failed to set LLDP advertise options") } @@ -343,7 +379,7 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er config.NetworkConfig = newConfig // update the LLDP advertise options - updateLLDPOptions(newConfig) + updateLLDPOptions(newConfig, nil) l.Debug().Msg("saving new config") if err := SaveConfig(); err != nil { diff --git a/ui/src/components/LLDPNeighborsCard.tsx b/ui/src/components/LLDPNeighborsCard.tsx index 762d1e88..fe845d2c 100644 --- a/ui/src/components/LLDPNeighborsCard.tsx +++ b/ui/src/components/LLDPNeighborsCard.tsx @@ -64,8 +64,10 @@ export default function LLDPNeighborsCard({ )} - {neighbor.management_address && ( - + {neighbor.management_addresses && neighbor.management_addresses.length > 0 && ( + neighbor.management_addresses.map((address, index) => ( + + )) )} {neighbor.mac && ( diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 3198b407..7b27e9d7 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -801,7 +801,7 @@ export interface LLDPNeighbor { system_description: string; capabilities: string[]; ttl: number | null; - management_address: LLDPManagementAddress | null; + management_addresses: LLDPManagementAddress[]; values: Record; }