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