From c5249690fa8aa3a562ce987b1f74586773969fb3 Mon Sep 17 00:00:00 2001 From: Milan Lenco Date: Tue, 10 Sep 2024 12:25:24 +0200 Subject: [PATCH] Enhance LinuxCollector to support detecting multiple app VIF IPs Application VIF may have multiple IP address assigned. They can be either assigned directly on the same interface, or used on separate VLAN sub-interfaces which share the parent interface MAC address. LinuxCollector should detect and publish all of them, instead of flapping between them and generating many IP address change notifications, which trigger a flood of NI and App info messages published to the controller. For DHCP assigned IP addresses, we use lease time to determine if a previously detected IP address is still valid. For statically assigned IP, we expect to see at least one associated ARP packet every 10 minutes. Otherwise, we consider the IP address to be removed or simply not used anymore. This commit also enhances LinuxCollector to support enabling or disabling ARP snooping in runtime, without requiring to recreate all switch network instances or rebooting device. Signed-off-by: Milan Lenco (cherry picked from commit 5b2acf4) --- docs/APP-CONNECTIVITY.md | 65 +++++ docs/CONFIG-PROPERTIES.md | 2 +- pkg/pillar/cmd/zedagent/handleconfig.go | 18 +- pkg/pillar/cmd/zedagent/handlemetrics.go | 31 ++- .../cmd/zedagent/handlenetworkinstance.go | 11 +- pkg/pillar/cmd/zedrouter/appnetwork.go | 8 +- pkg/pillar/cmd/zedrouter/ipam.go | 50 ++-- pkg/pillar/cmd/zedrouter/networkinstance.go | 2 +- pkg/pillar/cmd/zedrouter/pubsubhandlers.go | 46 +++- pkg/pillar/cmd/zedrouter/zedrouter.go | 26 +- pkg/pillar/nistate/linux.go | 250 +++++++++++++++--- pkg/pillar/nistate/linux_dnsmasq.go | 33 +-- pkg/pillar/nistate/linux_flow.go | 153 +++++------ pkg/pillar/nistate/linux_iptables.go | 2 +- pkg/pillar/nistate/statecollector.go | 37 +-- pkg/pillar/types/zedroutertypes.go | 61 ++++- 16 files changed, 562 insertions(+), 233 deletions(-) diff --git a/docs/APP-CONNECTIVITY.md b/docs/APP-CONNECTIVITY.md index c5a908b737..c064a4d6a7 100644 --- a/docs/APP-CONNECTIVITY.md +++ b/docs/APP-CONNECTIVITY.md @@ -207,6 +207,25 @@ to retrieve cloud-init configuration, obtain information from EVE (e.g. device U hostname, uplink IP address) or to download [patch envelopes](PATCH-ENVELOPES.md). More information about metadata server can be found in [ECO-METADATA.md](ECO-METADATA.md). +#### IPAM + +Every Local Network Instance must be configured with an IPv4 network subnet and an IP +range within this subnet for automatic IP allocations. Host IP addresses from this subnet +that do not fall within the IP range are available for manual assignment. + +Whether an IP address is selected manually or dynamically assigned by EVE from the configured +IP range, an internal DHCP server is used to distribute these IP addresses to applications. +Container applications are deployed inside a "shim VM", which EVE prepares, ensuring that +a DHCP client is running for every virtual interface connected to a network instance +This guarantees that the IP address is received and applied before the application starts. +In contrast, VM applications are responsible for starting their own DHCP client and applying +the received IP addresses. + +Regardless of the application type, EVE does not automatically assume that the allocated +IP address is actually in use. Instead, it monitors the set of IP leases granted by the internal +DHCP server and updates the set of application IP addresses in the published info messages +accordingly. + ### Switch Network Instance Switch Network Instance is a simple L2-only bridge between connected applications and @@ -232,6 +251,52 @@ inbound ACL rules are that much more important. Metadata HTTP server is run for switch network instance only if it has uplink port with an IP address. +#### IP address detection + +Unlike a Local Network Instance, a switch network instance is configured without any IP +configuration, and EVE does not run an internal DHCP server. Instead, if IP connectivity +is required, IP addresses must be assigned statically within the connected applications +or provided by an external DHCP server or another application offering DHCP services. + +Since EVE is not in control of IP address allocations and leases, it must monitor application +traffic to learn which IP addresses are being used and report this information to the controller. + +In the case of an external DHCP server (IPv4), EVE captures the DHCPACK packet from the server, +which confirms the leased IP address. Because EVE manages MAC address allocations, it knows +the MAC address of every application's virtual interface (VIF). It can then map the CHADDR +(Client Hardware Address) attribute to the corresponding application VIF and learn the assigned +IP address from the YIADDR (your, i.e. client, IP Address) attribute. Additionally, EVE reads +the DHCP option 51 (Lease Time), if available, to determine how long the leased IP address +is valid. If EVE does not observe an IP renewal within this period, it assumes that the IP address +is no longer in use and reports this change to the controller. + +For statically assigned IPv4 addresses, EVE captures both ARP reply and request packets to learn +the application VIF IP assignment from either Sender IP + MAC or Target IP + MAC attribute +pairs. Since ARP cache entries have a limited lifetime — typically around 2 minutes — EVE expects +to see at least one ARP packet for every assigned IP within a 10-minute window (this is not +configurable). If no ARP packet is observed within this period for a previously detected IP +assignment, EVE assumes that the IP address has been removed and reports this change to +the controller. EVE also captures ARP packets for IP addresses configured via DHCP, but these +are ignored as the information from the previously captured DHCPACK takes precedence. +Note that ARP-based IP detection is enabled by default but can be disabled by setting +the configuration item `network.switch.enable.arpsnoop` to `false`. Change in this config +options will apply to already deployed switch network instances. + +For an external DHCPv6 server, EVE captures DHCPv6 REPLY messages. It learns the target MAC +address from the DUID option (Client Identifier, option code 1), while the IPv6 address +and its valid lifetime are provided by the IA Address (option code 5). + +To learn IPv6 addresses assigned using SLAAC (Stateless Address Auto Configuration), +EVE captures unicast ICMPv6 Neighbor Solicitation messages. These are sent from the interface +with the assigned IPv6 address to check if the address is free or already in use by another +host — a process known as Duplicate Address Detection (DAD). The ICMPv6 packet sent to detect +IP duplicates for a particular VIF IP will have the VIF MAC address as the source address +in the Ethernet header. EVE uses this, along with the "Target Address" field from the ICMPv6 +header, to identify the assigned IPv6 address. + +EVE is capable of detecting multiple IPs assigned to the same VIF MAC address. This is commonly +seen when applications use VLAN sub-interfaces, which share the parent interface's MAC address. + ### Uplink Port Network instances can be configured with an "uplink" network adapter, which will be used to provide diff --git a/docs/CONFIG-PROPERTIES.md b/docs/CONFIG-PROPERTIES.md index cb452c9a2e..64b4648763 100644 --- a/docs/CONFIG-PROPERTIES.md +++ b/docs/CONFIG-PROPERTIES.md @@ -53,7 +53,7 @@ | netdump.topic.maxcount | integer | 10 | maximum number of netdumps that can be published for each topic. The oldest netdump is unpublished should a new netdump exceed the limit. | netdump.downloader.with.pcap | boolean | false | include packet captures inside netdumps for download requests. However, even if enabled, TCP segments carrying non-empty payload (i.e. content which is being downloaded) are excluded and the overall PCAP size is limited to 64MB. | | netdump.downloader.http.with.fieldvalue | boolean | false | include HTTP header field values in captured network traces for download requests (beware: may contain secrets, such as datastore credentials). | -| network.switch.enable.arpsnoop | boolean | true | enable ARP Snooping on switch Network Instance, may need a device reboot to take effect | +| network.switch.enable.arpsnoop | boolean | true | enable ARP Snooping on switch Network Instances | | wwan.query.visible.providers | bool | false | enable to periodically (once per hour) query the set of visible cellular service providers and publish them under WirelessStatus (for every modem) | | network.local.legacy.mac.address | bool | false | enables legacy MAC address generation for local network instances for those EVE nodes where changing MAC addresses in applications will lead to incorrect network configuration | diff --git a/pkg/pillar/cmd/zedagent/handleconfig.go b/pkg/pillar/cmd/zedagent/handleconfig.go index 29f85724b4..26766c8510 100644 --- a/pkg/pillar/cmd/zedagent/handleconfig.go +++ b/pkg/pillar/cmd/zedagent/handleconfig.go @@ -1200,8 +1200,22 @@ func updateLocalServerMap(getconfigCtx *getconfigContext, localServerURL string) continue } if localServerIP != nil { - // check if the defined IP of localServer equals the allocated IP of the app - if adapterStatus.AllocatedIPv4Addr.Equal(localServerIP) { + // Check if the defined IP of localServer equals one of the IPs + // allocated to the app. + var matchesApp bool + for _, ip := range adapterStatus.AssignedAddresses.IPv4Addrs { + if ip.Address.Equal(localServerIP) { + matchesApp = true + break + } + } + for _, ip := range adapterStatus.AssignedAddresses.IPv6Addrs { + if ip.Address.Equal(localServerIP) { + matchesApp = true + break + } + } + if matchesApp { srvAddr := localServerAddr{ localServerAddr: localServerURL, bridgeIP: adapterStatus.BridgeIPAddr, diff --git a/pkg/pillar/cmd/zedagent/handlemetrics.go b/pkg/pillar/cmd/zedagent/handlemetrics.go index 43c601f806..50effcf8c0 100644 --- a/pkg/pillar/cmd/zedagent/handlemetrics.go +++ b/pkg/pillar/cmd/zedagent/handlemetrics.go @@ -1108,16 +1108,18 @@ func PublishAppInfoToZedCloud(ctx *zedagentContext, uuid string, for _, ifname := range ifNames { networkInfo := new(info.ZInfoNetwork) networkInfo.LocalName = *proto.String(ifname) - ipv4Addr, ipv6Addrs, allocated, macAddr, ipAddrMismatch := - getAppIP(ctx, aiStatus, ifname) - if ipv4Addr != nil { - networkInfo.IPAddrs = append(networkInfo.IPAddrs, ipv4Addr.String()) + addrs, hasIPv4Addr, macAddr, ipAddrMismatch := + getAppIPs(ctx, aiStatus, ifname) + for _, ipv4Addr := range addrs.IPv4Addrs { + networkInfo.IPAddrs = append(networkInfo.IPAddrs, + ipv4Addr.Address.String()) } - for _, ipv6Addr := range ipv6Addrs { - networkInfo.IPAddrs = append(networkInfo.IPAddrs, ipv6Addr.String()) + for _, ipv6Addr := range addrs.IPv6Addrs { + networkInfo.IPAddrs = append(networkInfo.IPAddrs, + ipv6Addr.Address.String()) } networkInfo.MacAddr = *proto.String(macAddr.String()) - networkInfo.Ipv4Up = allocated + networkInfo.Ipv4Up = hasIPv4Addr networkInfo.IpAddrMisMatch = ipAddrMismatch name := appIfnameToName(aiStatus, ifname) log.Tracef("app %s/%s localName %s devName %s", @@ -1563,21 +1565,22 @@ func sendMetricsProtobuf(ctx *getconfigContext, // Use the ifname/vifname to find the AppNetAdapter status // and from there the (ip, allocated, mac) addresses for the app -func getAppIP(ctx *zedagentContext, aiStatus *types.AppInstanceStatus, - vifname string) (net.IP, []net.IP, bool, net.HardwareAddr, bool) { +func getAppIPs(ctx *zedagentContext, aiStatus *types.AppInstanceStatus, + vifname string) (types.AssignedAddrs, bool, net.HardwareAddr, bool) { log.Tracef("getAppIP(%s, %s)", aiStatus.Key(), vifname) for _, adapterStatus := range aiStatus.AppNetAdapters { if adapterStatus.VifUsed != vifname { continue } - log.Tracef("getAppIP(%s, %s) found AppIP v4: %s, v6: %s, ipv4 assigned %v mac %s", - aiStatus.Key(), vifname, adapterStatus.AllocatedIPv4Addr, - adapterStatus.AllocatedIPv6List, adapterStatus.IPv4Assigned, adapterStatus.Mac) - return adapterStatus.AllocatedIPv4Addr, adapterStatus.AllocatedIPv6List, adapterStatus.IPv4Assigned, + log.Tracef("getAppIP(%s, %s) found AppIPs v4: %v, v6: %v, ipv4 assigned %v mac %s", + aiStatus.Key(), vifname, adapterStatus.AssignedAddresses.IPv4Addrs, + adapterStatus.AssignedAddresses.IPv6Addrs, adapterStatus.IPv4Assigned, + adapterStatus.Mac) + return adapterStatus.AssignedAddresses, adapterStatus.IPv4Assigned, adapterStatus.Mac, adapterStatus.IPAddrMisMatch } - return nil, nil, false, nil, false + return types.AssignedAddrs{}, false, nil, false } func createVolumeInstanceMetrics(ctx *zedagentContext, reportMetrics *metrics.ZMetricMsg) { diff --git a/pkg/pillar/cmd/zedagent/handlenetworkinstance.go b/pkg/pillar/cmd/zedagent/handlenetworkinstance.go index 5c4e952ec1..945d889623 100644 --- a/pkg/pillar/cmd/zedagent/handlenetworkinstance.go +++ b/pkg/pillar/cmd/zedagent/handlenetworkinstance.go @@ -8,7 +8,6 @@ package zedagent import ( "bytes" "fmt" - "net" "time" "github.com/golang/protobuf/ptypes/timestamp" @@ -109,11 +108,13 @@ func prepareAndPublishNetworkInstanceInfoMsg(ctx *zedagentContext, for mac, addrs := range status.IPAssignments { assignment := new(zinfo.ZmetIPAssignmentEntry) assignment.MacAddress = mac - if !addrs.IPv4Addr.Equal(net.IP{}) { - assignment.IpAddress = append(assignment.IpAddress, addrs.IPv4Addr.String()) + for _, assignedIP := range addrs.IPv4Addrs { + assignment.IpAddress = append(assignment.IpAddress, + assignedIP.Address.String()) } - for _, ip := range addrs.IPv6Addrs { - assignment.IpAddress = append(assignment.IpAddress, ip.String()) + for _, assignedIP := range addrs.IPv6Addrs { + assignment.IpAddress = append(assignment.IpAddress, + assignedIP.Address.String()) } info.IpAssignments = append(info.IpAssignments, assignment) } diff --git a/pkg/pillar/cmd/zedrouter/appnetwork.go b/pkg/pillar/cmd/zedrouter/appnetwork.go index 353d11370b..40a0609933 100644 --- a/pkg/pillar/cmd/zedrouter/appnetwork.go +++ b/pkg/pillar/cmd/zedrouter/appnetwork.go @@ -62,7 +62,8 @@ func (z *zedrouter) updateVIFsForStateCollecting( } _, vifs, err := z.getArgsForNIStateCollecting(network) if err == nil { - err = z.niStateCollector.UpdateCollectingForNI(*netConfig, vifs) + err = z.niStateCollector.UpdateCollectingForNI(*netConfig, vifs, + z.enableArpSnooping) } if err != nil { z.log.Error(err) @@ -168,10 +169,7 @@ func (z *zedrouter) updateNIStatusAfterAppNetworkActivate(status *types.AppNetwo netInstStatus.AddVif(z.log, adapterStatus.Vif, adapterStatus.Mac, status.UUIDandVersion.UUID) netInstStatus.IPAssignments[adapterStatus.Mac.String()] = - types.AssignedAddrs{ - IPv4Addr: adapterStatus.AllocatedIPv4Addr, - IPv6Addrs: adapterStatus.AllocatedIPv6List, - } + adapterStatus.AssignedAddresses z.publishNetworkInstanceStatus(netInstStatus) } } diff --git a/pkg/pillar/cmd/zedrouter/ipam.go b/pkg/pillar/cmd/zedrouter/ipam.go index b1afc6087f..31aa4fff2b 100644 --- a/pkg/pillar/cmd/zedrouter/ipam.go +++ b/pkg/pillar/cmd/zedrouter/ipam.go @@ -120,13 +120,15 @@ func (z *zedrouter) lookupOrAllocateIPv4ForVIF(niStatus *types.NetworkInstanceSt // Lookup to see if it is already allocated. if ipAddr == nil { addrs := niStatus.IPAssignments[adapterStatus.Mac.String()] - if !isEmptyIP(addrs.IPv4Addr) { - z.log.Functionf("lookupOrAllocateIPv4(NI:%v, app:%v): found IP %v for MAC %v", - networkID, appID, addrs.IPv4Addr, adapterStatus.Mac) - ipAddr = addrs.IPv4Addr + ipAddr = addrs.GetInternallyLeasedIPv4Addr() + if ipAddr != nil { + z.log.Functionf("lookupOrAllocateIPv4(NI:%v, app:%v): "+ + "found EVE-allocated IP %v for MAC %v", networkID, appID, ipAddr, + adapterStatus.Mac) } } + var newlyAllocated bool if ipAddr == nil { // Allocate IP address dynamically. // Get the app number for the AppNetAdapter entry. @@ -140,6 +142,7 @@ func (z *zedrouter) lookupOrAllocateIPv4ForVIF(niStatus *types.NetworkInstanceSt } // Pick an IP address from the subnet. ipAddr = netutils.AddToIP(niStatus.DhcpRange.Start, appNum) + newlyAllocated = true // Check if the address falls into the Dhcp Range. if !niStatus.DhcpRange.Contains(ipAddr) { err := fmt.Errorf("no free IP addresses in DHCP range(%v, %v)", @@ -162,12 +165,19 @@ func (z *zedrouter) lookupOrAllocateIPv4ForVIF(niStatus *types.NetworkInstanceSt } } // Later will be overwritten with addresses received from nistate.Collector, - // which snoops DHCP traffic and watches DNS server leases to learn the *actual* + // which snoops DHCP traffic and watches DHCP server leases to learn the *actual* // IP address assignments. - addrs := niStatus.IPAssignments[adapterStatus.Mac.String()] // preserve IPv6 addresses - addrs.IPv4Addr = ipAddr - niStatus.IPAssignments[adapterStatus.Mac.String()] = addrs - z.publishNetworkInstanceStatus(niStatus) + if newlyAllocated { + // Preserve other IPv4 and IPv6 addresses. + addrs := niStatus.IPAssignments[adapterStatus.Mac.String()] + addrs.IPv4Addrs = append(addrs.IPv4Addrs, + types.AssignedAddr{ + Address: ipAddr, + AssignedBy: types.AddressSourceInternalDHCP, + }) + niStatus.IPAssignments[adapterStatus.Mac.String()] = addrs + z.publishNetworkInstanceStatus(niStatus) + } z.log.Functionf("lookupOrAllocateIPv4(NI:%v, app:%v): allocated IP %v for MAC %v", networkID, appID, ipAddr, adapterStatus.Mac) return ipAddr, nil @@ -188,16 +198,16 @@ func (z *zedrouter) recordAssignedIPsToAdapterStatus(adapter *types.AppNetAdapte z.removeAssignedIPsFromAdapterStatus(adapter) return } - adapter.AllocatedIPv4Addr = vifAddrs.IPv4Addr - if !isEmptyIP(adapter.AppIPAddr) && - !adapter.AppIPAddr.Equal(adapter.AllocatedIPv4Addr) { - // Config and status do not match. - adapter.IPAddrMisMatch = true - } else { - adapter.IPAddrMisMatch = false + adapter.AssignedAddresses = vifAddrs.AssignedAddrs + adapter.IPAddrMisMatch = false + if !isEmptyIP(adapter.AppIPAddr) { + leasedIP := adapter.AssignedAddresses.GetInternallyLeasedIPv4Addr() + if !adapter.AppIPAddr.Equal(leasedIP) { + // Config and status do not match. + adapter.IPAddrMisMatch = true + } } - adapter.AllocatedIPv6List = vifAddrs.IPv6Addrs - adapter.IPv4Assigned = !isEmptyIP(vifAddrs.IPv4Addr) + adapter.IPv4Assigned = len(vifAddrs.IPv4Addrs) > 0 } func (z *zedrouter) removeAssignedIPsFromAppNetStatus(status *types.AppNetworkStatus) { @@ -208,8 +218,8 @@ func (z *zedrouter) removeAssignedIPsFromAppNetStatus(status *types.AppNetworkSt } func (z *zedrouter) removeAssignedIPsFromAdapterStatus(adapterStatus *types.AppNetAdapterStatus) { - adapterStatus.AllocatedIPv6List = nil - adapterStatus.AllocatedIPv4Addr = nil + adapterStatus.AssignedAddresses.IPv4Addrs = nil + adapterStatus.AssignedAddresses.IPv6Addrs = nil adapterStatus.IPAddrMisMatch = false adapterStatus.IPv4Assigned = false } diff --git a/pkg/pillar/cmd/zedrouter/networkinstance.go b/pkg/pillar/cmd/zedrouter/networkinstance.go index eb47848fa3..3926ebb038 100644 --- a/pkg/pillar/cmd/zedrouter/networkinstance.go +++ b/pkg/pillar/cmd/zedrouter/networkinstance.go @@ -230,7 +230,7 @@ func (z *zedrouter) doUpdateActivatedNetworkInstance(config types.NetworkInstanc z.processNIReconcileStatus(niRecStatus, status) _, vifs, err := z.getArgsForNIStateCollecting(config.UUID) if err == nil { - err = z.niStateCollector.UpdateCollectingForNI(config, vifs) + err = z.niStateCollector.UpdateCollectingForNI(config, vifs, z.enableArpSnooping) } if err != nil { z.log.Error(err) diff --git a/pkg/pillar/cmd/zedrouter/pubsubhandlers.go b/pkg/pillar/cmd/zedrouter/pubsubhandlers.go index e0e25ac2a7..4305111c59 100644 --- a/pkg/pillar/cmd/zedrouter/pubsubhandlers.go +++ b/pkg/pillar/cmd/zedrouter/pubsubhandlers.go @@ -51,7 +51,33 @@ func (z *zedrouter) handleGlobalConfigImpl(ctxArg interface{}, key string, } z.metricInterval = metricInterval } - z.enableArpSnooping = gcp.GlobalValueBool(types.EnableARPSnoop) + enableArpSnooping := gcp.GlobalValueBool(types.EnableARPSnoop) + if z.enableArpSnooping != enableArpSnooping { + z.enableArpSnooping = enableArpSnooping + // Start/Stop ARP snooping in every activated Switch NI. + for _, item := range z.pubNetworkInstanceStatus.GetAll() { + niStatus := item.(types.NetworkInstanceStatus) + if !niStatus.Activated { + continue + } + if niStatus.Type != types.NetworkInstanceTypeSwitch { + // ARP snooping is only used in Switch NIs. + continue + } + niConfig := z.lookupNetworkInstanceConfig(niStatus.Key()) + if niConfig == nil { + continue + } + _, vifs, err := z.getArgsForNIStateCollecting(niConfig.UUID) + if err == nil { + err = z.niStateCollector.UpdateCollectingForNI( + *niConfig, vifs, z.enableArpSnooping) + } + if err != nil { + z.log.Error(err) + } + } + } z.localLegacyMACAddr = gcp.GlobalValueBool(types.NetworkLocalLegacyMACAddress) z.niReconciler.ApplyUpdatedGCP(z.runCtx, *gcp) } @@ -223,7 +249,13 @@ func (z *zedrouter) handleNetworkInstanceCreate(ctxArg interface{}, key string, // Set bridge IP address. if status.Gateway != nil { - addrs := types.AssignedAddrs{IPv4Addr: status.Gateway} + addrs := types.AssignedAddrs{ + IPv4Addrs: []types.AssignedAddr{ + { + Address: status.Gateway, + AssignedBy: types.AddressSourceEVEInternal, + }}, + } status.IPAssignments[status.BridgeMac.String()] = addrs status.BridgeIPAddr = status.Gateway } @@ -359,8 +391,14 @@ func (z *zedrouter) handleNetworkInstanceModify(ctxArg interface{}, key string, if status.BridgeMac != nil { delete(status.IPAssignments, status.BridgeMac.String()) } - if status.Gateway != nil { - addrs := types.AssignedAddrs{IPv4Addr: status.Gateway} + if status.Gateway != nil && status.BridgeMac != nil { + addrs := types.AssignedAddrs{ + IPv4Addrs: []types.AssignedAddr{ + { + Address: status.Gateway, + AssignedBy: types.AddressSourceEVEInternal, + }}, + } status.IPAssignments[status.BridgeMac.String()] = addrs status.BridgeIPAddr = status.Gateway } diff --git a/pkg/pillar/cmd/zedrouter/zedrouter.go b/pkg/pillar/cmd/zedrouter/zedrouter.go index f2a65249b7..91481ffbae 100644 --- a/pkg/pillar/cmd/zedrouter/zedrouter.go +++ b/pkg/pillar/cmd/zedrouter/zedrouter.go @@ -520,10 +520,7 @@ func (z *zedrouter) run(ctx context.Context) (err error) { niKey, vif.NetAdapterName) continue } - netStatus.IPAssignments[mac] = types.AssignedAddrs{ - IPv4Addr: newAddrs.IPv4Addr, - IPv6Addrs: newAddrs.IPv6Addrs, - } + netStatus.IPAssignments[mac] = newAddrs.AssignedAddrs z.publishNetworkInstanceStatus(netStatus) appKey := vif.App.String() appStatus := z.lookupAppNetworkStatus(appKey) @@ -1099,11 +1096,13 @@ func (z *zedrouter) lookupNetworkInstanceStatusByAppIP( for _, st := range items { status := st.(types.NetworkInstanceStatus) for _, addrs := range status.IPAssignments { - if ip.Equal(addrs.IPv4Addr) { - return &status + for _, assignedIP := range addrs.IPv4Addrs { + if ip.Equal(assignedIP.Address) { + return &status + } } - for _, nip := range addrs.IPv6Addrs { - if ip.Equal(nip) { + for _, assignedIP := range addrs.IPv6Addrs { + if ip.Equal(assignedIP.Address) { return &status } } @@ -1286,8 +1285,15 @@ func (z *zedrouter) lookupAppNetworkStatusByAppIP(ip net.IP) *types.AppNetworkSt for _, st := range items { status := st.(types.AppNetworkStatus) for _, adapterStatus := range status.AppNetAdapterList { - if adapterStatus.AllocatedIPv4Addr.Equal(ip) { - return &status + for _, adapterIP := range adapterStatus.AssignedAddresses.IPv4Addrs { + if adapterIP.Address.Equal(ip) { + return &status + } + } + for _, adapterIP := range adapterStatus.AssignedAddresses.IPv6Addrs { + if adapterIP.Address.Equal(ip) { + return &status + } } } } diff --git a/pkg/pillar/nistate/linux.go b/pkg/pillar/nistate/linux.go index fdcea9a655..ef17a9f539 100644 --- a/pkg/pillar/nistate/linux.go +++ b/pkg/pillar/nistate/linux.go @@ -4,7 +4,9 @@ package nistate import ( + "bytes" "fmt" + "net" "path/filepath" "strings" "sync" @@ -40,13 +42,169 @@ type LinuxCollector struct { } type niInfo struct { - config types.NetworkInstanceConfig - bridge NIBridge - vifs VIFAddrsList - ipLeases dnsmasqIPLeases - cancelPCAP context.CancelFunc - ipv4DNSReqs []dnsReq - ipv6DNSReqs []dnsReq + config types.NetworkInstanceConfig + bridge NIBridge + vifs []*vifInfo + ipLeases dnsmasqIPLeases + cancelPCAP context.CancelFunc + pcapWG sync.WaitGroup + ipv4DNSReqs []dnsReq + ipv6DNSReqs []dnsReq + arpSnoopEnabled bool +} + +// lookupVIFByGuestMAC : Lookup VIF by the MAC address of the guest interface. +func (ni *niInfo) lookupVIFByGuestMAC(mac net.HardwareAddr) *vifInfo { + for _, vif := range ni.vifs { + if bytes.Equal(vif.GuestIfMAC, mac) { + return vif + } + } + return nil +} + +type detectedAddr struct { + types.AssignedAddr + validUntil time.Time // zero timestamp if validity is not known/limited +} + +func (addr detectedAddr) hasExpired() bool { + return !addr.validUntil.IsZero() && time.Now().After(addr.validUntil) +} + +func (addr detectedAddr) fromDHCP() bool { + return addr.AssignedBy == types.AddressSourceInternalDHCP || + addr.AssignedBy == types.AddressSourceExternalDHCP +} + +type vifInfo struct { + AppVIF + ipv4Addrs []detectedAddr + ipv6Addrs []detectedAddr +} + +func (vif *vifInfo) exportVIFAddrs() VIFAddrs { + var assignedAddrs types.AssignedAddrs + for _, addr := range vif.ipv4Addrs { + assignedAddrs.IPv4Addrs = append(assignedAddrs.IPv4Addrs, + types.AssignedAddr{ + Address: addr.Address, + AssignedBy: addr.AssignedBy, + }) + } + for _, addr := range vif.ipv6Addrs { + assignedAddrs.IPv6Addrs = append(assignedAddrs.IPv6Addrs, + types.AssignedAddr{ + Address: addr.Address, + AssignedBy: addr.AssignedBy, + }) + } + return VIFAddrs{ + AssignedAddrs: assignedAddrs, + VIF: vif.AppVIF, + } +} + +func (vif *vifInfo) hasIP(ip net.IP) bool { + for _, ipv4Addr := range vif.ipv4Addrs { + if ip.Equal(ipv4Addr.Address) { + return true + } + } + for _, ipv6Addr := range vif.ipv6Addrs { + if ip.Equal(ipv6Addr.Address) { + return true + } + } + return false +} + +// addIP adds or updates detected/leased IP address into the list of assigned addresses. +func (vif *vifInfo) addIP(ip net.IP, source types.AddressSource, + validUntil time.Time) (update *VIFAddrsUpdate) { + ipList := vif.ipv4Addrs + if ip.To4() == nil { + ipList = vif.ipv6Addrs + } + var alreadyExists, changed bool + prevAddrs := vif.exportVIFAddrs() + for i := range ipList { + if ipList[i].Address.Equal(ip) { + // IP address is already known for this VIF. + // Just update the source and the expiration time. + alreadyExists = true + if source == types.AddressSourceStatic && ipList[i].fromDHCP() { + // Prefer info from DHCP snooping over ARP snooping. + // Ignore this update. + return nil + } + ipList[i].validUntil = validUntil + if ipList[i].AssignedBy != source { + // LinuxCollector will publish VIFAddrsUpdate only to update + // the IP address source. + changed = true + ipList[i].AssignedBy = source + } + break + } + } + if !alreadyExists { + // Newly detected IP address. + changed = true + ipList = append(ipList, detectedAddr{ + AssignedAddr: types.AssignedAddr{ + Address: ip, + AssignedBy: source, + }, + validUntil: validUntil, + }) + } + if ip.To4() == nil { + vif.ipv6Addrs = ipList + } else { + vif.ipv4Addrs = ipList + } + if !changed { + return nil + } + newAddrs := vif.exportVIFAddrs() + return &VIFAddrsUpdate{ + Prev: prevAddrs, + New: newAddrs, + } +} + +// delIPs removes all or only some IPs based on the source and the expiration. +func (vif *vifInfo) delIPs(sourceMask int, onlyExpired bool) *VIFAddrsUpdate { + var changed bool + var filteredV4Addrs, filteredV6Addrs []detectedAddr + for _, addr := range vif.ipv4Addrs { + if (sourceMask&int(addr.AssignedBy) > 0) && + (!onlyExpired || addr.hasExpired()) { + changed = true + continue + } + filteredV4Addrs = append(filteredV4Addrs, addr) + } + for _, addr := range vif.ipv6Addrs { + if (sourceMask&int(addr.AssignedBy) > 0) && + (!onlyExpired || addr.hasExpired()) { + changed = true + continue + } + filteredV6Addrs = append(filteredV6Addrs, addr) + } + if !changed { + return nil + } + prevAddrs := vif.exportVIFAddrs() + vif.ipv4Addrs = filteredV4Addrs + vif.ipv6Addrs = filteredV6Addrs + newAddrs := vif.exportVIFAddrs() + return &VIFAddrsUpdate{ + Prev: prevAddrs, + New: newAddrs, + } } // NewLinuxCollector is a constructor for LinuxCollector. @@ -83,15 +241,17 @@ func (lc *LinuxCollector) StartCollectingForNI( } pcapCtx, cancelPCAP := context.WithCancel(context.Background()) ni := &niInfo{ - config: niConfig, - bridge: br, - cancelPCAP: cancelPCAP, + config: niConfig, + bridge: br, + cancelPCAP: cancelPCAP, + arpSnoopEnabled: enableARPSnoop, } for _, vif := range vifs { - ni.vifs = append(ni.vifs, VIFAddrs{VIF: vif}) + ni.vifs = append(ni.vifs, &vifInfo{AppVIF: vif}) } lc.nis[niConfig.UUID] = ni - go lc.sniffDNSandDHCP(pcapCtx, br, niConfig.Type, enableARPSnoop) + ni.pcapWG.Add(1) + go lc.sniffDNSandDHCP(pcapCtx, &ni.pcapWG, br, niConfig.Type, enableARPSnoop) lc.log.Noticef("%s: Started collecting state data for NI %v "+ "(br: %+v, vifs: %+v)", LogAndErrPrefix, niConfig.UUID, br, vifs) return nil @@ -104,7 +264,7 @@ func (lc *LinuxCollector) StartCollectingForNI( // Note that not every change in network instance config is supported. For example, // network instance type (switch / local) cannot change. func (lc *LinuxCollector) UpdateCollectingForNI( - niConfig types.NetworkInstanceConfig, vifs []AppVIF) error { + niConfig types.NetworkInstanceConfig, vifs []AppVIF, enableARPSnoop bool) error { lc.mu.Lock() defer lc.mu.Unlock() if _, exists := lc.nis[niConfig.UUID]; !exists { @@ -112,17 +272,27 @@ func (lc *LinuxCollector) UpdateCollectingForNI( } ni := lc.nis[niConfig.UUID] ni.config = niConfig - // Preserve already known IP assignments. - prevVIFs := ni.vifs - ni.vifs = nil + var newVifs []*vifInfo for _, vif := range vifs { - vifWithAddrs := VIFAddrs{VIF: vif} - prevVIF := prevVIFs.LookupByGuestMAC(vif.GuestIfMAC) - if prevVIF != nil && prevVIF.VIF.App == vif.App && prevVIF.VIF.NI == vif.NI { - vifWithAddrs.IPv4Addr = prevVIF.IPv4Addr - vifWithAddrs.IPv6Addrs = prevVIF.IPv6Addrs + newVif := &vifInfo{AppVIF: vif} + // Preserve already known IP assignments. + prevVIF := ni.lookupVIFByGuestMAC(vif.GuestIfMAC) + if prevVIF != nil && prevVIF.App == vif.App && prevVIF.NI == vif.NI { + newVif.ipv4Addrs = prevVIF.ipv4Addrs + newVif.ipv6Addrs = prevVIF.ipv6Addrs } - ni.vifs = append(ni.vifs, vifWithAddrs) + newVifs = append(newVifs, newVif) + } + ni.vifs = newVifs + if ni.arpSnoopEnabled != enableARPSnoop { + // Restart packet capture with changed BPF filter. + ni.cancelPCAP() + ni.pcapWG.Wait() + pcapCtx, cancelPCAP := context.WithCancel(context.Background()) + ni.cancelPCAP = cancelPCAP + ni.pcapWG.Add(1) + go lc.sniffDNSandDHCP(pcapCtx, &ni.pcapWG, ni.bridge, niConfig.Type, enableARPSnoop) + ni.arpSnoopEnabled = enableARPSnoop } lc.log.Noticef("%s: Updated state collecting for NI %v "+ "(br: %+v, vifs: %+v)", LogAndErrPrefix, niConfig.UUID, ni.bridge, ni.vifs) @@ -137,7 +307,9 @@ func (lc *LinuxCollector) StopCollectingForNI(niID uuid.UUID) error { if _, exists := lc.nis[niID]; !exists { return ErrUnknownNI{NI: niID} } - lc.nis[niID].cancelPCAP() + ni := lc.nis[niID] + ni.cancelPCAP() + ni.pcapWG.Wait() delete(lc.nis, niID) lc.log.Noticef("%s: Stopped collecting state data for NI %v", LogAndErrPrefix, niID) return nil @@ -152,7 +324,11 @@ func (lc *LinuxCollector) GetIPAssignments(niID uuid.UUID) (VIFAddrsList, error) if !exists { return nil, ErrUnknownNI{NI: niID} } - return niInfo.vifs, nil + var addrList VIFAddrsList + for _, vif := range niInfo.vifs { + addrList = append(addrList, vif.exportVIFAddrs()) + } + return addrList, nil } // WatchIPAssignments : watch for changes in IP assignments to VIFs across @@ -263,7 +439,7 @@ func (lc *LinuxCollector) WatchFlows() <-chan types.IPFlow { // Run periodic and on-change state data collecting for network instances // from a separate Go routine. func (lc *LinuxCollector) runStateCollecting() { - gcIPLeases := time.NewTicker(time.Minute) + gcIPAssignments := time.NewTicker(time.Minute) fmax := float64(flowCollectInterval) fmin := fmax * 0.9 flowCollectTimer := flextimer.NewRangeTicker(time.Duration(fmin), time.Duration(fmax)) @@ -297,19 +473,31 @@ func (lc *LinuxCollector) runStateCollecting() { } } } - case <-gcIPLeases.C: + case <-gcIPAssignments.C: lc.mu.Lock() var addrChanges []VIFAddrsUpdate for _, ni := range lc.nis { + // First remove IP leases which are no longer reported by the internal + // DHCP server. removedAny := lc.gcIPLeases(ni) if removedAny { addrChanges = append(addrChanges, lc.processIPLeases(ni)...) } + // Next remove expired IP leases granted from external DHCP servers + // or statically configured IPs with ARP not seen for more than 10 minutes. + for _, vif := range ni.vifs { + sourceMask := int(types.AddressSourceExternalDHCP) | + int(types.AddressSourceStatic) + update := vif.delIPs(sourceMask, true) + if update != nil { + addrChanges = append(addrChanges, *update) + } + } } watchers := lc.ipAssignWatchers lc.mu.Unlock() if len(addrChanges) != 0 { - lc.logAddrChanges("IP Lease GC event", addrChanges) + lc.logAddrChanges("IP Assignment GC event", addrChanges) for _, watcherCh := range watchers { watcherCh <- addrChanges } @@ -358,18 +546,18 @@ func (lc *LinuxCollector) logAddrChanges(event string, changes []VIFAddrsUpdate) func (lc *LinuxCollector) getVIFByIfName(ifName string) (vif AppVIF, found bool) { for _, niState := range lc.nis { for _, niVIF := range niState.vifs { - if niVIF.VIF.HostIfName == ifName { - return niVIF.VIF, true + if niVIF.HostIfName == ifName { + return niVIF.AppVIF, true } } } return vif, false } -func (lc *LinuxCollector) getVIFsByAppNum(appNum int) (vifs VIFAddrsList) { +func (lc *LinuxCollector) getVIFsByAppNum(appNum int) (vifs []*vifInfo) { for _, niState := range lc.nis { for _, niVIF := range niState.vifs { - if niVIF.VIF.AppNum != appNum { + if niVIF.AppNum != appNum { continue } vifs = append(vifs, niVIF) diff --git a/pkg/pillar/nistate/linux_dnsmasq.go b/pkg/pillar/nistate/linux_dnsmasq.go index 5cc7b84c37..d8d95080bf 100644 --- a/pkg/pillar/nistate/linux_dnsmasq.go +++ b/pkg/pillar/nistate/linux_dnsmasq.go @@ -123,26 +123,21 @@ func (lc *LinuxCollector) processIPLeases(niInfo *niInfo) ( LogAndErrPrefix, niInfo) return nil } - for i := range niInfo.vifs { - vifAddrs := &niInfo.vifs[i] - vif := vifAddrs.VIF + for _, vif := range niInfo.vifs { ipLease := niInfo.ipLeases.findLease(vif.App.String(), vif.GuestIfMAC, true) - if ipLease == nil && vifAddrs.IPv4Addr != nil { - prevAddrs := *vifAddrs - vifAddrs.IPv4Addr = nil - newAddrs := *vifAddrs - addrUpdates = append(addrUpdates, VIFAddrsUpdate{ - Prev: prevAddrs, - New: newAddrs, - }) - } else if ipLease != nil && !ipLease.ipAddr.Equal(vifAddrs.IPv4Addr) { - prevAddrs := *vifAddrs - vifAddrs.IPv4Addr = ipLease.ipAddr - newAddrs := *vifAddrs - addrUpdates = append(addrUpdates, VIFAddrsUpdate{ - Prev: prevAddrs, - New: newAddrs, - }) + if ipLease == nil { + // Remove all recorded internally-granted IP leases for this VIF. + sourceMask := int(types.AddressSourceInternalDHCP) + update := vif.delIPs(sourceMask, false) + if update != nil { + addrUpdates = append(addrUpdates, *update) + } + } else if ipLease != nil { + update := vif.addIP(ipLease.ipAddr, types.AddressSourceInternalDHCP, + ipLease.leaseTime) + if update != nil { + addrUpdates = append(addrUpdates, *update) + } } } return addrUpdates diff --git a/pkg/pillar/nistate/linux_flow.go b/pkg/pillar/nistate/linux_flow.go index 8cb2c49535..81f354fbc7 100644 --- a/pkg/pillar/nistate/linux_flow.go +++ b/pkg/pillar/nistate/linux_flow.go @@ -8,8 +8,10 @@ package nistate import ( "bytes" "context" + "encoding/binary" "net" "strconv" + "sync" "syscall" "time" @@ -41,6 +43,10 @@ const ( // flowLogPrefix allows to filter logs specific to flow collecting // and packet sniffing. flowLogPrefix = LogAndErrPrefix + " (FlowStats)" + + // Statically configured IP address, detected using ARP snooping, is considered + // valid until we do not see any more ARPs for this IP for more than 10 minutes. + staticIPValidDuration = 10 * time.Minute ) type capturedPacket struct { @@ -98,8 +104,8 @@ func (lc *LinuxCollector) collectFlows() (flows []types.IPFlow) { var sequence int ipFlow := types.IPFlow{ Scope: types.FlowScope{ - AppUUID: vif.VIF.App, - NetAdapterName: vif.VIF.NetAdapterName, + AppUUID: vif.App, + NetAdapterName: vif.NetAdapterName, BrIfName: niInfo.bridge.BrIfName, NetUUID: niInfo.config.UUID, }, @@ -117,8 +123,8 @@ func (lc *LinuxCollector) collectFlows() (flows []types.IPFlow) { sequence++ } for _, flowrec := range timeoutedFlows { - if flowrec.vif.App != vif.VIF.App || - flowrec.vif.NetAdapterName != vif.VIF.NetAdapterName { + if flowrec.vif.App != vif.App || + flowrec.vif.NetAdapterName != vif.NetAdapterName { continue } ipFlow.Flows = append(ipFlow.Flows, flowrec.FlowRec) @@ -129,7 +135,7 @@ func (lc *LinuxCollector) collectFlows() (flows []types.IPFlow) { // Append DNS flows corresponding to this app. for _, dnsReq := range dnsReqs { - if !vif.HasIP(dnsReq.appIP) { + if !vif.hasIP(dnsReq.appIP) { continue } ipFlow.DNSReqs = append(ipFlow.DNSReqs, dnsReq.DNSReq) @@ -190,16 +196,16 @@ func (lc *LinuxCollector) convertConntrackToFlow( // refers to a remote endpoint, even for outside initiated flows, meaning // that "src" and "dst" is indeed a very confusing naming (already used // in EVE API therefore not easy to change). - vif := vifs.LookupByIP(entry.Forward.SrcIP) + vif := lookupVIFByIP(entry.Forward.SrcIP, vifs) forwSrcApp = vif != nil if !forwSrcApp { - vif = vifs.LookupByIP(entry.Forward.DstIP) + vif = lookupVIFByIP(entry.Forward.DstIP, vifs) forwDstApp = vif != nil if !forwDstApp { - vif = vifs.LookupByIP(entry.Reverse.SrcIP) + vif = lookupVIFByIP(entry.Reverse.SrcIP, vifs) backSrcApp = vif != nil if !backSrcApp { - vif = vifs.LookupByIP(entry.Reverse.DstIP) + vif = lookupVIFByIP(entry.Reverse.DstIP, vifs) backDstApp = vif != nil } } @@ -222,7 +228,7 @@ func (lc *LinuxCollector) convertConntrackToFlow( // similar to RFC5130 to merge two bidirectional flow using the method of "Perimeter", // here we define the flow src is always the local App endpoint, the flow dst will // be the opposite endpoint. - ipFlow.vif = vif.VIF + ipFlow.vif = vif.AppVIF ipFlow.Flow.Proto = int32(entry.Forward.Protocol) if forwSrcApp { // Src initiated flow, forward-src is the src, reverse-src is the flow dst @@ -277,8 +283,9 @@ func (lc *LinuxCollector) convertConntrackToFlow( // This function is merely capturing packets and then sending them to runStateCollecting, // so that all state collecting and processing happens from the main event loop // (to simplify and avoid race conditions...). -func (lc *LinuxCollector) sniffDNSandDHCP(ctx context.Context, +func (lc *LinuxCollector) sniffDNSandDHCP(ctx context.Context, wg *sync.WaitGroup, br NIBridge, niType types.NetworkInstanceType, enableArpSnoop bool) { + defer wg.Done() var ( err error snapshotLen int32 = 1280 // draft-madi-dnsop-udp4dns-00 @@ -502,13 +509,13 @@ func (lc *LinuxCollector) processARPPacket( return nil } - var vif *VIFAddrs + var vif *vifInfo var weAreSource bool var gotAddress []byte if arp.Operation == layers.ARPReply || arp.Operation == layers.ARPRequest { - vif = niInfo.vifs.LookupByGuestMAC(arp.DstHwAddress) + vif = niInfo.lookupVIFByGuestMAC(arp.DstHwAddress) if vif == nil { - vif = niInfo.vifs.LookupByGuestMAC(arp.SourceHwAddress) + vif = niInfo.lookupVIFByGuestMAC(arp.SourceHwAddress) if vif != nil { weAreSource = true } @@ -520,21 +527,16 @@ func (lc *LinuxCollector) processARPPacket( return nil } - prevAddrs := *vif if weAreSource { gotAddress = arp.SourceProtAddress } else { gotAddress = arp.DstProtAddress } - if vif.IPv4Addr.Equal(gotAddress) { - return nil + validUntil := time.Now().Add(staticIPValidDuration) + update := vif.addIP(gotAddress, types.AddressSourceStatic, validUntil) + if update != nil { + addrUpdates = append(addrUpdates, *update) } - vif.IPv4Addr = gotAddress - newAddrs := *vif - addrUpdates = append(addrUpdates, VIFAddrsUpdate{ - Prev: prevAddrs, - New: newAddrs, - }) return addrUpdates } @@ -559,7 +561,7 @@ func (lc *LinuxCollector) processDHCPPacket( // need to check those in payload to see if it's for an app. isBroadcast = true } else { - foundDstMac = niInfo.vifs.LookupByGuestMAC(etherPkt.DstMAC) != nil + foundDstMac = niInfo.lookupVIFByGuestMAC(etherPkt.DstMAC) != nil } } if !foundDstMac && !isBroadcast { @@ -579,16 +581,22 @@ func (lc *LinuxCollector) processDHCPPacket( } dhcpv4 := dhcpLayer.(*layers.DHCPv4) var isReplyAck bool + var validUntil time.Time if dhcpv4.Operation == layers.DHCPOpReply { opts := dhcpv4.Options for _, opt := range opts { - if opt.Type == layers.DHCPOptMessageType && - int(opt.Data[0]) == int(layers.DHCPMsgTypeAck) { - isReplyAck = true - break + switch opt.Type { + case layers.DHCPOptMessageType: + if int(opt.Data[0]) == int(layers.DHCPMsgTypeAck) { + isReplyAck = true + } + case layers.DHCPOptLeaseTime: + leaseTimeSecs := binary.BigEndian.Uint32(opt.Data) + validUntil = time.Now().Add(time.Duration(leaseTimeSecs) * time.Second) } } } + if !isReplyAck { // This is indeed a DHCP packet but not the DHCP Reply type. return nil, true @@ -607,20 +615,15 @@ func (lc *LinuxCollector) processDHCPPacket( return nil, true } - vif := niInfo.vifs.LookupByGuestMAC(dhcpv4.ClientHWAddr) + vif := niInfo.lookupVIFByGuestMAC(dhcpv4.ClientHWAddr) if vif == nil { return nil, true } - if vif.IPv4Addr.Equal(dhcpv4.YourClientIP) { - return nil, true + update := vif.addIP(dhcpv4.YourClientIP, types.AddressSourceExternalDHCP, + validUntil) + if update != nil { + addrUpdates = append(addrUpdates, *update) } - prevAddrs := *vif - vif.IPv4Addr = dhcpv4.YourClientIP - newAddrs := *vif - addrUpdates = append(addrUpdates, VIFAddrsUpdate{ - Prev: prevAddrs, - New: newAddrs, - }) return addrUpdates, true } @@ -640,29 +643,32 @@ func (lc *LinuxCollector) processDHCPPacket( // This is indeed a DHCP packet but not the DHCP Reply type. return nil, true } + var vif *vifInfo + var validUntil time.Time for _, opt := range dhcpv6.Options { - if opt.Code != layers.DHCPv6OptClientID { - continue - } - clientOption := &layers.DHCPv6DUID{} - clientOption.DecodeFromBytes(opt.Data) - vif := niInfo.vifs.LookupByGuestMAC(clientOption.LinkLayerAddress) - if vif == nil { - return nil, true - } - if isAddrPresent(vif.IPv6Addrs, dhcpv6.LinkAddr) { - return nil, true + switch opt.Code { + case layers.DHCPv6OptClientID: + clientOption := &layers.DHCPv6DUID{} + clientOption.DecodeFromBytes(opt.Data) + vif = niInfo.lookupVIFByGuestMAC(clientOption.LinkLayerAddress) + case layers.DHCPv6OptIAAddr: + // Parse IA Address option to get valid-lifetime. + if len(opt.Data) >= 24 { + // Valid-lifetime is at offset 20-23 (4 bytes). + validLifetimeSecs := binary.BigEndian.Uint32(opt.Data[20:24]) + validUntil = time.Now().Add(time.Duration(validLifetimeSecs) * time.Second) + } } - prevAddrs := *vif - vif.IPv6Addrs = append(vif.IPv6Addrs, dhcpv6.LinkAddr) - newAddrs := *vif - addrUpdates = append(addrUpdates, VIFAddrsUpdate{ - Prev: prevAddrs, - New: newAddrs, - }) - return addrUpdates, true } - return nil, true + if vif == nil { + return nil, true + } + update := vif.addIP(dhcpv6.LinkAddr, types.AddressSourceExternalDHCP, + validUntil) + if update != nil { + addrUpdates = append(addrUpdates, *update) + } + return addrUpdates, true } // Process captured ICMPv6 NS packet for a switched network instance to learn @@ -671,10 +677,10 @@ func (lc *LinuxCollector) processDHCPPacket( // by processing this packet. func (lc *LinuxCollector) processDADProbe( niInfo *niInfo, packet gopacket.Packet) (addrUpdates []VIFAddrsUpdate) { - var vif *VIFAddrs + var vif *vifInfo if etherLayer := packet.Layer(layers.LayerTypeEthernet); etherLayer != nil { etherPkt := etherLayer.(*layers.Ethernet) - vif = niInfo.vifs.LookupByGuestMAC(etherPkt.SrcMAC) + vif = niInfo.lookupVIFByGuestMAC(etherPkt.SrcMAC) } if vif == nil { return @@ -695,16 +701,15 @@ func (lc *LinuxCollector) processDADProbe( return } icmp6 := icmp6Layer.(*layers.ICMPv6NeighborSolicitation) - if isAddrPresent(vif.IPv6Addrs, icmp6.TargetAddress) { - return nil + // DAD is not performed periodically, therefore we should not remove the IP address + // from the list after some time duration just because we have not seen another + // ICMPv6 NS packet. + undefinedValidity := time.Time{} + update := vif.addIP(icmp6.TargetAddress, types.AddressSourceSLAAC, + undefinedValidity) + if update != nil { + addrUpdates = append(addrUpdates, *update) } - prevAddrs := *vif - vif.IPv6Addrs = append(vif.IPv6Addrs, icmp6.TargetAddress) - newAddrs := *vif - addrUpdates = append(addrUpdates, VIFAddrsUpdate{ - Prev: prevAddrs, - New: newAddrs, - }) return addrUpdates } @@ -765,11 +770,11 @@ func (lc *LinuxCollector) processDNSPacketInfo( } } -func isAddrPresent(list []net.IP, addr net.IP) bool { - for i := 0; i < len(list); i++ { - if addr.Equal(list[i]) { - return true +func lookupVIFByIP(ip net.IP, vifs []*vifInfo) *vifInfo { + for _, vif := range vifs { + if vif.hasIP(ip) { + return vif } } - return false + return nil } diff --git a/pkg/pillar/nistate/linux_iptables.go b/pkg/pillar/nistate/linux_iptables.go index 632e02698c..868e782c7f 100644 --- a/pkg/pillar/nistate/linux_iptables.go +++ b/pkg/pillar/nistate/linux_iptables.go @@ -56,7 +56,7 @@ func (lc *LinuxCollector) fetchIptablesCounters() []aclCounters { for _, niState := range lc.nis { for _, niVif := range niState.vifs { vifs = append(vifs, vif{ - ifName: niVif.VIF.HostIfName, + ifName: niVif.HostIfName, bridge: niState.bridge.BrIfName, }) } diff --git a/pkg/pillar/nistate/statecollector.go b/pkg/pillar/nistate/statecollector.go index 3a11066fce..2c521a074d 100644 --- a/pkg/pillar/nistate/statecollector.go +++ b/pkg/pillar/nistate/statecollector.go @@ -10,7 +10,6 @@ package nistate import ( - "bytes" "fmt" "net" @@ -40,7 +39,7 @@ type Collector interface { // Note that not every change in network instance config is supported. For example, // network instance type (switch / local) cannot change. UpdateCollectingForNI( - niConfig types.NetworkInstanceConfig, vifs []AppVIF) error + niConfig types.NetworkInstanceConfig, vifs []AppVIF, enableArpSnoop bool) error // StopCollectingForNI : stop collecting state data for network instance. // It is called by zedrouter whenever a network instance is about to be deleted. @@ -135,27 +134,6 @@ func (vifs VIFAddrsList) LookupByAdapterName( return nil } -// LookupByGuestMAC : Lookup VIF by the MAC address of the guest interface. -func (vifs VIFAddrsList) LookupByGuestMAC(mac net.HardwareAddr) *VIFAddrs { - for i := range vifs { - if bytes.Equal(vifs[i].VIF.GuestIfMAC, mac) { - return &vifs[i] - } - } - return nil -} - -// LookupByIP : Lookup VIF by the IP address assigned to the guest interface. -// Returns first match. -func (vifs VIFAddrsList) LookupByIP(ip net.IP) *VIFAddrs { - for i := range vifs { - if vifs[i].HasIP(ip) { - return &vifs[i] - } - } - return nil -} - // VIFAddrs lists IP addresses assigned to a VIF on the guest side // (inside the app). This is provided to zedrouter by Collector. type VIFAddrs struct { @@ -163,19 +141,6 @@ type VIFAddrs struct { VIF AppVIF } -// HasIP returns true if the given IP address is assigned to this VIF. -func (vif VIFAddrs) HasIP(ip net.IP) bool { - if ip.Equal(vif.IPv4Addr) { - return true - } - for _, ipv6Addr := range vif.IPv6Addrs { - if ip.Equal(ipv6Addr) { - return true - } - } - return false -} - // VIFAddrsUpdate describes a change in the address assignment for a single VIF. // Prev.VIF and New.VIF are always the same. // This is provided to zedrouter by Collector. diff --git a/pkg/pillar/types/zedroutertypes.go b/pkg/pillar/types/zedroutertypes.go index 87c322e356..66b60b1c81 100644 --- a/pkg/pillar/types/zedroutertypes.go +++ b/pkg/pillar/types/zedroutertypes.go @@ -349,10 +349,9 @@ type AppNetAdapterStatus struct { AppNetAdapterConfig VifInfo BridgeMac net.HardwareAddr - BridgeIPAddr net.IP // The address for DNS/DHCP service in zedrouter - AllocatedIPv4Addr net.IP // Assigned to domU - AllocatedIPv6List []net.IP // IPv6 addresses assigned to domU - IPv4Assigned bool // Set to true once DHCP has assigned it to domU + BridgeIPAddr net.IP // The address for DNS/DHCP service in zedrouter + AssignedAddresses AssignedAddrs // IPv4 and IPv6 addresses assigned to domU + IPv4Assigned bool // Set to true once DHCP has assigned it to domU IPAddrMisMatch bool HostName string } @@ -380,8 +379,48 @@ type NetworkInstanceInfo struct { // AssignedAddrs : IP addresses assigned to application network adapter. type AssignedAddrs struct { - IPv4Addr net.IP - IPv6Addrs []net.IP + IPv4Addrs []AssignedAddr + IPv6Addrs []AssignedAddr +} + +// GetInternallyLeasedIPv4Addr returns IPv4 address leased by EVE using +// an internally run DHCP server. +func (aa AssignedAddrs) GetInternallyLeasedIPv4Addr() net.IP { + for _, addr := range aa.IPv4Addrs { + if addr.AssignedBy == AddressSourceInternalDHCP { + return addr.Address + } + } + return nil +} + +// AddressSource determines the source of an IP address assigned to an app VIF. +// Values are power of two and therefore can be used with a bit mask. +type AddressSource uint8 + +const ( + // AddressSourceUndefined : IP address source is not defined + AddressSourceUndefined AddressSource = 0 + // AddressSourceEVEInternal : IP address is used only internally by EVE + // (i.e. inside dom0). + AddressSourceEVEInternal AddressSource = 1 << iota + // AddressSourceInternalDHCP : IP address is leased to an app by an internal DHCP server + // run by EVE. + AddressSourceInternalDHCP + // AddressSourceExternalDHCP : IP address is leased to an app by an external DHCP server. + AddressSourceExternalDHCP + // AddressSourceSLAAC : Stateless Address Autoconfiguration (SLAAC) was used by the client + // to generate a unique IPv6 address. + AddressSourceSLAAC + // AddressSourceStatic : IP address is assigned to an app statically + // (using e.g. cloud-init). + AddressSourceStatic +) + +// AssignedAddr : IP address assigned to an application interface (on the guest side). +type AssignedAddr struct { + Address net.IP + AssignedBy AddressSource } // VifNameMac : name and MAC address assigned to app VIF. @@ -874,11 +913,13 @@ func (status NetworkInstanceStatus) LogKey() string { // IsIpAssigned returns true if the given IP address is assigned to any app VIF. func (status *NetworkInstanceStatus) IsIpAssigned(ip net.IP) bool { for _, assignments := range status.IPAssignments { - if ip.Equal(assignments.IPv4Addr) { - return true + for _, assignedIP := range assignments.IPv4Addrs { + if ip.Equal(assignedIP.Address) { + return true + } } - for _, nip := range assignments.IPv6Addrs { - if ip.Equal(nip) { + for _, assignedIP := range assignments.IPv6Addrs { + if ip.Equal(assignedIP.Address) { return true } }