| package netdump |
| |
| import ( |
| "bytes" |
| "fmt" |
| "math" |
| "os" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "github.com/vishvananda/netlink" |
| "golang.org/x/sys/unix" |
| |
| netapi "source.monogon.dev/net/proto" |
| ) |
| |
| var vlanProtoMap = map[netlink.VlanProtocol]netapi.VLAN_Protocol{ |
| netlink.VLAN_PROTOCOL_8021Q: netapi.VLAN_CVLAN, |
| netlink.VLAN_PROTOCOL_8021AD: netapi.VLAN_SVLAN, |
| } |
| |
| // From iproute2's rt_protos |
| const ( |
| protoUnspec = 0 |
| protoKernel = 2 |
| protoBoot = 3 |
| protoStatic = 4 |
| ) |
| |
| // Dump dumps the network configuration of the current network namespace into |
| // a net.proto.Net structure. This is currently only expected to work for |
| // systems which do not use a dynamic routing protocol to establish basic |
| // internet connectivity. |
| // The second return value is a list of warnings, i.e. things which might be |
| // problematic, but might still result in a working (though less complete) |
| // configuration. The Net in the first return value is set to a non-nil value |
| // even if there are warnings. The third return value is for a hard error, |
| // the Net value will be nil in that case. |
| func Dump() (*netapi.Net, []error, error) { |
| var n netapi.Net |
| var warnings []error |
| links, err := netlink.LinkList() |
| if err != nil { |
| return nil, nil, fmt.Errorf("failed to list network links: %w", err) |
| } |
| // Map interface index -> interface pointer |
| ifIdxMap := make(map[int]*netapi.Interface) |
| // Map interface index -> names of children |
| ifChildren := make(map[int][]string) |
| // Map interface index -> number of reverse dependencies |
| ifNRevDeps := make(map[int]int) |
| for _, link := range links { |
| linkAttrs := link.Attrs() |
| // Ignore loopback interfaces. The default one will always be |
| // created, and we don't have support for additional loopbacks. |
| if linkAttrs.EncapType == "loopback" { |
| continue |
| } |
| // Gather interface-type-specific data into a netapi interface. |
| var iface netapi.Interface |
| switch l := link.(type) { |
| case *netlink.Device: |
| mac := link.Attrs().PermHardwareAddr |
| if len(mac) == 0 { |
| // Try legacy method for old kernels |
| mac, err = getPermanentHWAddrLegacy(l.Name) |
| // Errors are expected, not all interfaces support this. |
| // If a permanent hardware address could not be obtained, fall |
| // back to the configured hardware address. |
| if err != nil { |
| mac = link.Attrs().HardwareAddr |
| } |
| } |
| iface.Type = &netapi.Interface_Device{Device: &netapi.Device{ |
| HardwareAddress: mac.String(), |
| }} |
| case *netlink.Bond: |
| bond := netapi.Bond{ |
| MinLinks: int32(l.MinLinks), |
| TransmitHashPolicy: netapi.Bond_TransmitHashPolicy(l.XmitHashPolicy), |
| } |
| switch l.Mode { |
| case netlink.BOND_MODE_802_3AD: |
| lacp := netapi.Bond_LACP{ |
| Rate: netapi.Bond_LACP_Rate(l.LacpRate), |
| ActorSystemPriority: int32(l.AdActorSysPrio), |
| UserPortKey: int32(l.AdUserPortKey), |
| SelectionLogic: netapi.Bond_LACP_SelectionLogic(l.AdSelect), |
| } |
| if len(bytes.TrimLeft(l.AdActorSystem, "\x00")) != 0 { |
| lacp.ActorSystemMac = l.AdActorSystem.String() |
| } |
| bond.Mode = &netapi.Bond_Lacp{Lacp: &lacp} |
| default: |
| } |
| iface.Type = &netapi.Interface_Bond{Bond: &bond} |
| case *netlink.Vlan: |
| parentLink, err := netlink.LinkByIndex(l.ParentIndex) |
| if err != nil { |
| warnings = append(warnings, fmt.Errorf("unable to get parent for VLAN interface %q, interface ignored: %w", iface.Name, err)) |
| continue |
| } |
| iface.Type = &netapi.Interface_Vlan{Vlan: &netapi.VLAN{ |
| Id: int32(l.VlanId), |
| Protocol: vlanProtoMap[l.VlanProtocol], |
| Parent: parentLink.Attrs().Name, |
| }} |
| default: |
| continue |
| } |
| // Append common interface data to netapi interface. |
| iface.Name = linkAttrs.Name |
| iface.Mtu = int32(linkAttrs.MTU) |
| // Collect addresses into interface. |
| addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL) |
| if err != nil { |
| warnings = append(warnings, fmt.Errorf("unable to get addresses for interface %q, interface ignored: %w", iface.Name, err)) |
| continue |
| } |
| for _, a := range addrs { |
| // Ignore IPv6 link-local addresses |
| if a.IP.IsLinkLocalUnicast() && a.IP.To4() == nil { |
| continue |
| } |
| // Sadly it's not possible to reliably determine if a DHCP client is |
| // running. Good clients usually either don't set the permanent flag |
| // and/or a lifetime. |
| if a.Flags&unix.IFA_F_PERMANENT == 0 || (a.ValidLft > 0 && a.ValidLft < math.MaxUint32) { |
| if a.IP.To4() == nil { |
| // Enable IPv6 Autoconfig |
| if iface.Ipv6Autoconfig == nil { |
| iface.Ipv6Autoconfig = &netapi.IPv6Autoconfig{} |
| iface.Ipv6Autoconfig.Privacy, err = getIPv6IfaceAutoconfigPrivacy(linkAttrs.Name) |
| if err != nil { |
| warnings = append(warnings, err) |
| } |
| } |
| } else { |
| if iface.Ipv4Autoconfig == nil { |
| iface.Ipv4Autoconfig = &netapi.IPv4Autoconfig{} |
| } |
| } |
| // Dynamic address, ignore |
| continue |
| } |
| if a.Peer != nil { |
| // Add an interface route for the peer |
| iface.Route = append(iface.Route, &netapi.Interface_Route{ |
| Destination: a.Peer.String(), |
| SourceIp: a.IP.String(), |
| }) |
| } |
| iface.Address = append(iface.Address, a.IPNet.String()) |
| } |
| if linkAttrs.MasterIndex != 0 { |
| ifChildren[linkAttrs.MasterIndex] = append(ifChildren[linkAttrs.MasterIndex], iface.Name) |
| ifNRevDeps[linkAttrs.Index]++ |
| } |
| if linkAttrs.ParentIndex != 0 { |
| ifNRevDeps[linkAttrs.ParentIndex]++ |
| } |
| ifIdxMap[link.Attrs().Index] = &iface |
| } |
| routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL) |
| if err != nil { |
| return nil, nil, fmt.Errorf("failed to list routes: %w", err) |
| } |
| // Collect all routes into routes assigned to exact netapi interfaces. |
| for _, r := range routes { |
| if r.Family != netlink.FAMILY_V4 && r.Family != netlink.FAMILY_V6 { |
| continue |
| } |
| var route netapi.Interface_Route |
| // Ignore all dynamic routes |
| if r.Protocol != protoUnspec && r.Protocol != protoBoot && |
| r.Protocol != protoStatic { |
| continue |
| } |
| if r.LinkIndex == 0 { |
| // Only for "exotic" routes like "unreachable" which are not |
| // necessary for connectivity, skip for now |
| continue |
| } |
| if r.Dst == nil { |
| switch r.Family { |
| case netlink.FAMILY_V4: |
| route.Destination = "0.0.0.0/0" |
| case netlink.FAMILY_V6: |
| route.Destination = "::/0" |
| default: |
| // Switch is complete, all other families get ignored at the start |
| // of the loop. |
| panic("route family changed under us") |
| } |
| } else { |
| route.Destination = r.Dst.String() |
| } |
| if !r.Gw.IsUnspecified() && len(r.Gw) != 0 { |
| route.GatewayIp = r.Gw.String() |
| } |
| if !r.Src.IsUnspecified() && len(r.Src) != 0 { |
| route.SourceIp = r.Src.String() |
| } |
| // Linux calls the metric RTA_PRIORITY even though it behaves as lower- |
| // is-better. Note that RTA_METRICS is NOT the metric. |
| route.Metric = int32(r.Priority) |
| iface, ok := ifIdxMap[r.LinkIndex] |
| if !ok { |
| continue |
| } |
| |
| iface.Route = append(iface.Route, &route) |
| } |
| // Finally, gather all interface into a list, filtering out unused ones. |
| for ifIdx, iface := range ifIdxMap { |
| switch i := iface.Type.(type) { |
| case *netapi.Interface_Bond: |
| // Add children here, as now they are all known |
| i.Bond.MemberInterface = ifChildren[ifIdx] |
| case *netapi.Interface_Device: |
| // Drop physical interfaces from the config if they have no IPs and |
| // no reverse dependencies. |
| if len(iface.Address) == 0 && iface.Ipv4Autoconfig == nil && |
| iface.Ipv6Autoconfig == nil && ifNRevDeps[ifIdx] == 0 { |
| continue |
| } |
| } |
| n.Interface = append(n.Interface, iface) |
| } |
| // Make the output stable |
| sort.Slice(n.Interface, func(i, j int) bool { return n.Interface[i].Name < n.Interface[j].Name }) |
| return &n, warnings, nil |
| } |
| |
| func getIPv6IfaceAutoconfigPrivacy(name string) (netapi.IPv6Autoconfig_Privacy, error) { |
| useTempaddrRaw, err := os.ReadFile(fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/use_tempaddr", name)) |
| if err != nil { |
| return netapi.IPv6Autoconfig_DISABLE, fmt.Errorf("failed to read use_tempaddr sysctl for interface %q: %w", name, err) |
| } |
| useTempaddr, err := strconv.ParseInt(strings.TrimSpace(string(useTempaddrRaw)), 10, 64) |
| if err != nil { |
| return netapi.IPv6Autoconfig_DISABLE, fmt.Errorf("failed to parse use_tempaddr sysctl for interface %q: %w", name, err) |
| } |
| switch { |
| case useTempaddr <= 0: |
| return netapi.IPv6Autoconfig_DISABLE, nil |
| case useTempaddr == 1: |
| return netapi.IPv6Autoconfig_AVOID, nil |
| case useTempaddr > 1: |
| return netapi.IPv6Autoconfig_PREFER, nil |
| default: |
| panic("switch is complete but hit default case") |
| } |
| } |