blob: 318343e8646a06e8374dcbd407e00e0d3b0c46a8 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brun87339502023-03-07 15:49:42 +01004package netdump
5
6import (
7 "bytes"
8 "fmt"
9 "math"
Lorenz Brun227c5cb2025-01-09 21:39:55 +010010 "net/netip"
Lorenz Brun87339502023-03-07 15:49:42 +010011 "os"
12 "sort"
13 "strconv"
14 "strings"
15
16 "github.com/vishvananda/netlink"
Lorenz Brun227c5cb2025-01-09 21:39:55 +010017 "go4.org/netipx"
Lorenz Brun87339502023-03-07 15:49:42 +010018 "golang.org/x/sys/unix"
19
Tim Windelschmidt10ef8f92024-08-13 15:35:10 +020020 netapi "source.monogon.dev/osbase/net/proto"
Lorenz Brun87339502023-03-07 15:49:42 +010021)
22
23var vlanProtoMap = map[netlink.VlanProtocol]netapi.VLAN_Protocol{
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +010024 netlink.VLAN_PROTOCOL_8021Q: netapi.VLAN_PROTOCOL_CVLAN,
25 netlink.VLAN_PROTOCOL_8021AD: netapi.VLAN_PROTOCOL_SVLAN,
Lorenz Brun87339502023-03-07 15:49:42 +010026}
27
28// From iproute2's rt_protos
29const (
30 protoUnspec = 0
31 protoKernel = 2
32 protoBoot = 3
33 protoStatic = 4
34)
35
Lorenz Brun227c5cb2025-01-09 21:39:55 +010036type ifaceAddrRef struct {
37 iface *netapi.Interface
38 addrIdx int
39}
40
Lorenz Brun87339502023-03-07 15:49:42 +010041// Dump dumps the network configuration of the current network namespace into
Tim Windelschmidt10ef8f92024-08-13 15:35:10 +020042// a osbase.net.proto.Net structure. This is currently only expected to work for
Lorenz Brun87339502023-03-07 15:49:42 +010043// 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.
50func 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 Brun227c5cb2025-01-09 21:39:55 +010061 // Interface address implied on-link routes
62 impliedOnLinkRoutes := make(map[netip.Prefix]ifaceAddrRef)
Lorenz Brun87339502023-03-07 15:49:42 +010063 // 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 Brun153c9c12025-01-07 17:44:45 +010076 mac := link.Attrs().PermHWAddr
Lorenz Brun87339502023-03-07 15:49:42 +010077 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 Brun227c5cb2025-01-09 21:39:55 +0100166 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 Brun87339502023-03-07 15:49:42 +0100177 }
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 Brun227c5cb2025-01-09 21:39:55 +0100219 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 Brun87339502023-03-07 15:49:42 +0100229 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
268func 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 Windelschmidta10d0cb2025-01-13 14:44:15 +0100271 return netapi.IPv6Autoconfig_PRIVACY_DISABLE, fmt.Errorf("failed to read use_tempaddr sysctl for interface %q: %w", name, err)
Lorenz Brun87339502023-03-07 15:49:42 +0100272 }
273 useTempaddr, err := strconv.ParseInt(strings.TrimSpace(string(useTempaddrRaw)), 10, 64)
274 if err != nil {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100275 return netapi.IPv6Autoconfig_PRIVACY_DISABLE, fmt.Errorf("failed to parse use_tempaddr sysctl for interface %q: %w", name, err)
Lorenz Brun87339502023-03-07 15:49:42 +0100276 }
277 switch {
278 case useTempaddr <= 0:
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100279 return netapi.IPv6Autoconfig_PRIVACY_DISABLE, nil
Lorenz Brun87339502023-03-07 15:49:42 +0100280 case useTempaddr == 1:
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100281 return netapi.IPv6Autoconfig_PRIVACY_AVOID, nil
Lorenz Brun87339502023-03-07 15:49:42 +0100282 case useTempaddr > 1:
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100283 return netapi.IPv6Autoconfig_PRIVACY_PREFER, nil
Lorenz Brun87339502023-03-07 15:49:42 +0100284 default:
285 panic("switch is complete but hit default case")
286 }
287}