| Tim Windelschmidt | 6d33a43 | 2025-02-04 14:34:25 +0100 | [diff] [blame^] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 4 | package netdump |
| 5 | |
| 6 | import ( |
| 7 | "bytes" |
| 8 | "fmt" |
| 9 | "math" |
| Lorenz Brun | 227c5cb | 2025-01-09 21:39:55 +0100 | [diff] [blame] | 10 | "net/netip" |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 11 | "os" |
| 12 | "sort" |
| 13 | "strconv" |
| 14 | "strings" |
| 15 | |
| 16 | "github.com/vishvananda/netlink" |
| Lorenz Brun | 227c5cb | 2025-01-09 21:39:55 +0100 | [diff] [blame] | 17 | "go4.org/netipx" |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 18 | "golang.org/x/sys/unix" |
| 19 | |
| Tim Windelschmidt | 10ef8f9 | 2024-08-13 15:35:10 +0200 | [diff] [blame] | 20 | netapi "source.monogon.dev/osbase/net/proto" |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 21 | ) |
| 22 | |
| 23 | var vlanProtoMap = map[netlink.VlanProtocol]netapi.VLAN_Protocol{ |
| Tim Windelschmidt | a10d0cb | 2025-01-13 14:44:15 +0100 | [diff] [blame] | 24 | netlink.VLAN_PROTOCOL_8021Q: netapi.VLAN_PROTOCOL_CVLAN, |
| 25 | netlink.VLAN_PROTOCOL_8021AD: netapi.VLAN_PROTOCOL_SVLAN, |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 26 | } |
| 27 | |
| 28 | // From iproute2's rt_protos |
| 29 | const ( |
| 30 | protoUnspec = 0 |
| 31 | protoKernel = 2 |
| 32 | protoBoot = 3 |
| 33 | protoStatic = 4 |
| 34 | ) |
| 35 | |
| Lorenz Brun | 227c5cb | 2025-01-09 21:39:55 +0100 | [diff] [blame] | 36 | type ifaceAddrRef struct { |
| 37 | iface *netapi.Interface |
| 38 | addrIdx int |
| 39 | } |
| 40 | |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 41 | // Dump dumps the network configuration of the current network namespace into |
| Tim Windelschmidt | 10ef8f9 | 2024-08-13 15:35:10 +0200 | [diff] [blame] | 42 | // a osbase.net.proto.Net structure. This is currently only expected to work for |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 43 | // systems which do not use a dynamic routing protocol to establish basic |
| 44 | // internet connectivity. |
| 45 | // The second return value is a list of warnings, i.e. things which might be |
| 46 | // problematic, but might still result in a working (though less complete) |
| 47 | // configuration. The Net in the first return value is set to a non-nil value |
| 48 | // even if there are warnings. The third return value is for a hard error, |
| 49 | // the Net value will be nil in that case. |
| 50 | func Dump() (*netapi.Net, []error, error) { |
| 51 | var n netapi.Net |
| 52 | var warnings []error |
| 53 | links, err := netlink.LinkList() |
| 54 | if err != nil { |
| 55 | return nil, nil, fmt.Errorf("failed to list network links: %w", err) |
| 56 | } |
| 57 | // Map interface index -> interface pointer |
| 58 | ifIdxMap := make(map[int]*netapi.Interface) |
| 59 | // Map interface index -> names of children |
| 60 | ifChildren := make(map[int][]string) |
| Lorenz Brun | 227c5cb | 2025-01-09 21:39:55 +0100 | [diff] [blame] | 61 | // Interface address implied on-link routes |
| 62 | impliedOnLinkRoutes := make(map[netip.Prefix]ifaceAddrRef) |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 63 | // Map interface index -> number of reverse dependencies |
| 64 | ifNRevDeps := make(map[int]int) |
| 65 | for _, link := range links { |
| 66 | linkAttrs := link.Attrs() |
| 67 | // Ignore loopback interfaces. The default one will always be |
| 68 | // created, and we don't have support for additional loopbacks. |
| 69 | if linkAttrs.EncapType == "loopback" { |
| 70 | continue |
| 71 | } |
| 72 | // Gather interface-type-specific data into a netapi interface. |
| 73 | var iface netapi.Interface |
| 74 | switch l := link.(type) { |
| 75 | case *netlink.Device: |
| Lorenz Brun | 153c9c1 | 2025-01-07 17:44:45 +0100 | [diff] [blame] | 76 | mac := link.Attrs().PermHWAddr |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 77 | if len(mac) == 0 { |
| 78 | // Try legacy method for old kernels |
| 79 | mac, err = getPermanentHWAddrLegacy(l.Name) |
| 80 | // Errors are expected, not all interfaces support this. |
| 81 | // If a permanent hardware address could not be obtained, fall |
| 82 | // back to the configured hardware address. |
| 83 | if err != nil { |
| 84 | mac = link.Attrs().HardwareAddr |
| 85 | } |
| 86 | } |
| 87 | iface.Type = &netapi.Interface_Device{Device: &netapi.Device{ |
| 88 | HardwareAddress: mac.String(), |
| 89 | }} |
| 90 | case *netlink.Bond: |
| 91 | bond := netapi.Bond{ |
| 92 | MinLinks: int32(l.MinLinks), |
| 93 | TransmitHashPolicy: netapi.Bond_TransmitHashPolicy(l.XmitHashPolicy), |
| 94 | } |
| 95 | switch l.Mode { |
| 96 | case netlink.BOND_MODE_802_3AD: |
| 97 | lacp := netapi.Bond_LACP{ |
| 98 | Rate: netapi.Bond_LACP_Rate(l.LacpRate), |
| 99 | ActorSystemPriority: int32(l.AdActorSysPrio), |
| 100 | UserPortKey: int32(l.AdUserPortKey), |
| 101 | SelectionLogic: netapi.Bond_LACP_SelectionLogic(l.AdSelect), |
| 102 | } |
| 103 | if len(bytes.TrimLeft(l.AdActorSystem, "\x00")) != 0 { |
| 104 | lacp.ActorSystemMac = l.AdActorSystem.String() |
| 105 | } |
| 106 | bond.Mode = &netapi.Bond_Lacp{Lacp: &lacp} |
| 107 | default: |
| 108 | } |
| 109 | iface.Type = &netapi.Interface_Bond{Bond: &bond} |
| 110 | case *netlink.Vlan: |
| 111 | parentLink, err := netlink.LinkByIndex(l.ParentIndex) |
| 112 | if err != nil { |
| 113 | warnings = append(warnings, fmt.Errorf("unable to get parent for VLAN interface %q, interface ignored: %w", iface.Name, err)) |
| 114 | continue |
| 115 | } |
| 116 | iface.Type = &netapi.Interface_Vlan{Vlan: &netapi.VLAN{ |
| 117 | Id: int32(l.VlanId), |
| 118 | Protocol: vlanProtoMap[l.VlanProtocol], |
| 119 | Parent: parentLink.Attrs().Name, |
| 120 | }} |
| 121 | default: |
| 122 | continue |
| 123 | } |
| 124 | // Append common interface data to netapi interface. |
| 125 | iface.Name = linkAttrs.Name |
| 126 | iface.Mtu = int32(linkAttrs.MTU) |
| 127 | // Collect addresses into interface. |
| 128 | addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL) |
| 129 | if err != nil { |
| 130 | warnings = append(warnings, fmt.Errorf("unable to get addresses for interface %q, interface ignored: %w", iface.Name, err)) |
| 131 | continue |
| 132 | } |
| 133 | for _, a := range addrs { |
| 134 | // Ignore IPv6 link-local addresses |
| 135 | if a.IP.IsLinkLocalUnicast() && a.IP.To4() == nil { |
| 136 | continue |
| 137 | } |
| 138 | // Sadly it's not possible to reliably determine if a DHCP client is |
| 139 | // running. Good clients usually either don't set the permanent flag |
| 140 | // and/or a lifetime. |
| 141 | if a.Flags&unix.IFA_F_PERMANENT == 0 || (a.ValidLft > 0 && a.ValidLft < math.MaxUint32) { |
| 142 | if a.IP.To4() == nil { |
| 143 | // Enable IPv6 Autoconfig |
| 144 | if iface.Ipv6Autoconfig == nil { |
| 145 | iface.Ipv6Autoconfig = &netapi.IPv6Autoconfig{} |
| 146 | iface.Ipv6Autoconfig.Privacy, err = getIPv6IfaceAutoconfigPrivacy(linkAttrs.Name) |
| 147 | if err != nil { |
| 148 | warnings = append(warnings, err) |
| 149 | } |
| 150 | } |
| 151 | } else { |
| 152 | if iface.Ipv4Autoconfig == nil { |
| 153 | iface.Ipv4Autoconfig = &netapi.IPv4Autoconfig{} |
| 154 | } |
| 155 | } |
| 156 | // Dynamic address, ignore |
| 157 | continue |
| 158 | } |
| 159 | if a.Peer != nil { |
| 160 | // Add an interface route for the peer |
| 161 | iface.Route = append(iface.Route, &netapi.Interface_Route{ |
| 162 | Destination: a.Peer.String(), |
| 163 | SourceIp: a.IP.String(), |
| 164 | }) |
| 165 | } |
| Lorenz Brun | 227c5cb | 2025-01-09 21:39:55 +0100 | [diff] [blame] | 166 | ones, bits := a.Mask.Size() |
| 167 | baseAddr, ok := netip.AddrFromSlice(a.IP.Mask(a.Mask)) |
| 168 | prefix := netip.PrefixFrom(baseAddr, ones) |
| 169 | if ok && bits != 0 { |
| 170 | if !prefix.IsSingleIP() { |
| 171 | impliedOnLinkRoutes[prefix] = ifaceAddrRef{iface: &iface, addrIdx: len(iface.Address)} |
| 172 | } |
| 173 | iface.Address = append(iface.Address, a.IPNet.String()) |
| 174 | } else { |
| 175 | warnings = append(warnings, fmt.Errorf("address %v on %q is invalid, ignoring", a.IPNet, iface.Name)) |
| 176 | } |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 177 | } |
| 178 | if linkAttrs.MasterIndex != 0 { |
| 179 | ifChildren[linkAttrs.MasterIndex] = append(ifChildren[linkAttrs.MasterIndex], iface.Name) |
| 180 | ifNRevDeps[linkAttrs.Index]++ |
| 181 | } |
| 182 | if linkAttrs.ParentIndex != 0 { |
| 183 | ifNRevDeps[linkAttrs.ParentIndex]++ |
| 184 | } |
| 185 | ifIdxMap[link.Attrs().Index] = &iface |
| 186 | } |
| 187 | routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL) |
| 188 | if err != nil { |
| 189 | return nil, nil, fmt.Errorf("failed to list routes: %w", err) |
| 190 | } |
| 191 | // Collect all routes into routes assigned to exact netapi interfaces. |
| 192 | for _, r := range routes { |
| 193 | if r.Family != netlink.FAMILY_V4 && r.Family != netlink.FAMILY_V6 { |
| 194 | continue |
| 195 | } |
| 196 | var route netapi.Interface_Route |
| 197 | // Ignore all dynamic routes |
| 198 | if r.Protocol != protoUnspec && r.Protocol != protoBoot && |
| 199 | r.Protocol != protoStatic { |
| 200 | continue |
| 201 | } |
| 202 | if r.LinkIndex == 0 { |
| 203 | // Only for "exotic" routes like "unreachable" which are not |
| 204 | // necessary for connectivity, skip for now |
| 205 | continue |
| 206 | } |
| 207 | if r.Dst == nil { |
| 208 | switch r.Family { |
| 209 | case netlink.FAMILY_V4: |
| 210 | route.Destination = "0.0.0.0/0" |
| 211 | case netlink.FAMILY_V6: |
| 212 | route.Destination = "::/0" |
| 213 | default: |
| 214 | // Switch is complete, all other families get ignored at the start |
| 215 | // of the loop. |
| 216 | panic("route family changed under us") |
| 217 | } |
| 218 | } else { |
| Lorenz Brun | 227c5cb | 2025-01-09 21:39:55 +0100 | [diff] [blame] | 219 | dst, ok := netipx.FromStdIPNet(r.Dst) |
| 220 | if !ok { |
| 221 | warnings = append(warnings, fmt.Errorf("route %v invalid, ignoring", r.Dst)) |
| 222 | } |
| 223 | if ref, ok := impliedOnLinkRoutes[dst]; ok && !r.Gw.IsUnspecified() && len(r.Gw) != 0 { |
| 224 | // Address is not on-link, remove prefix from address to not |
| 225 | // get an improper on-link route. |
| 226 | prefix := netip.MustParsePrefix(ref.iface.Address[ref.addrIdx]) |
| 227 | ref.iface.Address[ref.addrIdx] = netip.PrefixFrom(prefix.Addr(), prefix.Addr().BitLen()).String() |
| 228 | } |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 229 | route.Destination = r.Dst.String() |
| 230 | } |
| 231 | if !r.Gw.IsUnspecified() && len(r.Gw) != 0 { |
| 232 | route.GatewayIp = r.Gw.String() |
| 233 | } |
| 234 | if !r.Src.IsUnspecified() && len(r.Src) != 0 { |
| 235 | route.SourceIp = r.Src.String() |
| 236 | } |
| 237 | // Linux calls the metric RTA_PRIORITY even though it behaves as lower- |
| 238 | // is-better. Note that RTA_METRICS is NOT the metric. |
| 239 | route.Metric = int32(r.Priority) |
| 240 | iface, ok := ifIdxMap[r.LinkIndex] |
| 241 | if !ok { |
| 242 | continue |
| 243 | } |
| 244 | |
| 245 | iface.Route = append(iface.Route, &route) |
| 246 | } |
| 247 | // Finally, gather all interface into a list, filtering out unused ones. |
| 248 | for ifIdx, iface := range ifIdxMap { |
| 249 | switch i := iface.Type.(type) { |
| 250 | case *netapi.Interface_Bond: |
| 251 | // Add children here, as now they are all known |
| 252 | i.Bond.MemberInterface = ifChildren[ifIdx] |
| 253 | case *netapi.Interface_Device: |
| 254 | // Drop physical interfaces from the config if they have no IPs and |
| 255 | // no reverse dependencies. |
| 256 | if len(iface.Address) == 0 && iface.Ipv4Autoconfig == nil && |
| 257 | iface.Ipv6Autoconfig == nil && ifNRevDeps[ifIdx] == 0 { |
| 258 | continue |
| 259 | } |
| 260 | } |
| 261 | n.Interface = append(n.Interface, iface) |
| 262 | } |
| 263 | // Make the output stable |
| 264 | sort.Slice(n.Interface, func(i, j int) bool { return n.Interface[i].Name < n.Interface[j].Name }) |
| 265 | return &n, warnings, nil |
| 266 | } |
| 267 | |
| 268 | func getIPv6IfaceAutoconfigPrivacy(name string) (netapi.IPv6Autoconfig_Privacy, error) { |
| 269 | useTempaddrRaw, err := os.ReadFile(fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/use_tempaddr", name)) |
| 270 | if err != nil { |
| Tim Windelschmidt | a10d0cb | 2025-01-13 14:44:15 +0100 | [diff] [blame] | 271 | return netapi.IPv6Autoconfig_PRIVACY_DISABLE, fmt.Errorf("failed to read use_tempaddr sysctl for interface %q: %w", name, err) |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 272 | } |
| 273 | useTempaddr, err := strconv.ParseInt(strings.TrimSpace(string(useTempaddrRaw)), 10, 64) |
| 274 | if err != nil { |
| Tim Windelschmidt | a10d0cb | 2025-01-13 14:44:15 +0100 | [diff] [blame] | 275 | return netapi.IPv6Autoconfig_PRIVACY_DISABLE, fmt.Errorf("failed to parse use_tempaddr sysctl for interface %q: %w", name, err) |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 276 | } |
| 277 | switch { |
| 278 | case useTempaddr <= 0: |
| Tim Windelschmidt | a10d0cb | 2025-01-13 14:44:15 +0100 | [diff] [blame] | 279 | return netapi.IPv6Autoconfig_PRIVACY_DISABLE, nil |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 280 | case useTempaddr == 1: |
| Tim Windelschmidt | a10d0cb | 2025-01-13 14:44:15 +0100 | [diff] [blame] | 281 | return netapi.IPv6Autoconfig_PRIVACY_AVOID, nil |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 282 | case useTempaddr > 1: |
| Tim Windelschmidt | a10d0cb | 2025-01-13 14:44:15 +0100 | [diff] [blame] | 283 | return netapi.IPv6Autoconfig_PRIVACY_PREFER, nil |
| Lorenz Brun | 8733950 | 2023-03-07 15:49:42 +0100 | [diff] [blame] | 284 | default: |
| 285 | panic("switch is complete but hit default case") |
| 286 | } |
| 287 | } |