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