mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
The journal records the network state of the host and each change of that state. The support team reads one file to see the interfaces, the routes, the resolvers, and the recoveries around an incident. The same file shows the health of the query path. A change that touches only AirDrop or virtual interfaces runs no DNS work.
159 lines
5.3 KiB
Go
159 lines
5.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"net"
|
|
"net/netip"
|
|
"syscall"
|
|
|
|
"golang.org/x/net/route"
|
|
)
|
|
|
|
// routeInterfaceNameFn names the interface of one route table index. A test
|
|
// replaces it, because the interfaces of the host are not a fixture.
|
|
var routeInterfaceNameFn = interfaceNameByIndex
|
|
|
|
// defaultRoutes reads the default route of each family. It reads the kernel
|
|
// route table, because a snapshot follows every network change and a
|
|
// subprocess per change costs too much on a busy host. A table that the kernel
|
|
// does not give leaves both routes empty.
|
|
func defaultRoutes() (v4, v6 defaultRoute) {
|
|
rib, err := route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0)
|
|
if err != nil {
|
|
return defaultRoute{}, defaultRoute{}
|
|
}
|
|
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
|
|
if err != nil {
|
|
return defaultRoute{}, defaultRoute{}
|
|
}
|
|
return defaultRoutesFromMessages(messages)
|
|
}
|
|
|
|
// defaultRoutesFromMessages picks the first default route of each family. The
|
|
// next hop and the interface come from one message, so a reader sees the pair
|
|
// that the kernel holds. The kernel keeps one unscoped default per family, so
|
|
// the first match is the only one.
|
|
func defaultRoutesFromMessages(messages []route.Message) (v4, v6 defaultRoute) {
|
|
foundV4, foundV6 := false, false
|
|
for _, message := range messages {
|
|
match, ok := matchDefaultRoute(message)
|
|
if !ok {
|
|
continue
|
|
}
|
|
found := defaultRoute{Interface: routeInterfaceNameFn(match.index)}
|
|
if match.gateway.IsValid() {
|
|
found.Gateway = match.gateway.String()
|
|
}
|
|
if match.v4 && !foundV4 {
|
|
v4, foundV4 = found, true
|
|
}
|
|
if !match.v4 && !foundV6 {
|
|
v6, foundV6 = found, true
|
|
}
|
|
if foundV4 && foundV6 {
|
|
return v4, v6
|
|
}
|
|
}
|
|
return v4, v6
|
|
}
|
|
|
|
// defaultRouteMatch is one default route of the table. A tunnel default has
|
|
// no next hop, so gateway can be invalid while the interface is set.
|
|
type defaultRouteMatch struct {
|
|
v4 bool
|
|
gateway netip.Addr
|
|
index int
|
|
}
|
|
|
|
// defaultRouteGateway reports the next hop and the interface index of one
|
|
// default route, for the tests of the selector. See matchDefaultRoute.
|
|
func defaultRouteGateway(message route.Message) (netip.Addr, int, bool) {
|
|
match, ok := matchDefaultRoute(message)
|
|
return match.gateway, match.index, ok
|
|
}
|
|
|
|
// matchDefaultRoute reports whether one message is the default route of the
|
|
// host for its family. A route to another destination or to a part of the
|
|
// address space gives nothing: a VPN splits the default into two /1 routes,
|
|
// and the zero address alone does not make a route the default. A route bound
|
|
// to one interface serves that interface only, a host route is no default, and
|
|
// a reject or blackhole route delivers nothing. With the gateway flag, the
|
|
// next hop must be a host of the same family. Without it, the link itself
|
|
// delivers, which is how a tunnel owns the default; the match then has no
|
|
// gateway and names the interface.
|
|
func matchDefaultRoute(message route.Message) (defaultRouteMatch, bool) {
|
|
routeMessage, ok := message.(*route.RouteMessage)
|
|
if !ok || len(routeMessage.Addrs) <= syscall.RTAX_NETMASK {
|
|
return defaultRouteMatch{}, false
|
|
}
|
|
if routeMessage.Flags&(syscall.RTF_IFSCOPE|syscall.RTF_HOST|syscall.RTF_REJECT|syscall.RTF_BLACKHOLE) != 0 {
|
|
return defaultRouteMatch{}, false
|
|
}
|
|
destination, ok := routeMessageAddr(routeMessage.Addrs[syscall.RTAX_DST])
|
|
if !ok || !destination.IsUnspecified() || !zeroNetmask(routeMessage.Addrs[syscall.RTAX_NETMASK], destination.Is4()) {
|
|
return defaultRouteMatch{}, false
|
|
}
|
|
match := defaultRouteMatch{v4: destination.Is4(), index: routeMessage.Index}
|
|
if routeMessage.Flags&syscall.RTF_GATEWAY == 0 {
|
|
if _, link := routeMessage.Addrs[syscall.RTAX_GATEWAY].(*route.LinkAddr); !link {
|
|
return defaultRouteMatch{}, false
|
|
}
|
|
return match, true
|
|
}
|
|
gateway, ok := routeMessageAddr(routeMessage.Addrs[syscall.RTAX_GATEWAY])
|
|
if !ok || gateway.Is4() != destination.Is4() {
|
|
return defaultRouteMatch{}, false
|
|
}
|
|
match.gateway = gateway
|
|
return match, true
|
|
}
|
|
|
|
// zeroNetmask reports whether the netmask of a route covers the whole address
|
|
// space of its family, which only the default route does.
|
|
func zeroNetmask(addr route.Addr, v4 bool) bool {
|
|
switch mask := addr.(type) {
|
|
case *route.Inet4Addr:
|
|
return v4 && mask.IP == [4]byte{}
|
|
case *route.Inet6Addr:
|
|
return !v4 && mask.IP == [16]byte{}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func routeMessageAddr(addr route.Addr) (netip.Addr, bool) {
|
|
switch address := addr.(type) {
|
|
case *route.Inet4Addr:
|
|
return netip.AddrFrom4(address.IP), true
|
|
case *route.Inet6Addr:
|
|
return withInterfaceZone(netip.AddrFrom16(address.IP), address.ZoneID), true
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// withInterfaceZone names the interface of a link-local next hop. Every link
|
|
// repeats the same range, so the address alone names no host. A tunnel gives
|
|
// the bare address fe80:: and the interface is the only part that tells the
|
|
// links apart.
|
|
func withInterfaceZone(address netip.Addr, zoneID int) netip.Addr {
|
|
if zoneID == 0 || !address.IsLinkLocalUnicast() {
|
|
return address
|
|
}
|
|
name := routeInterfaceNameFn(zoneID)
|
|
if name == "" {
|
|
return address
|
|
}
|
|
return address.WithZone(name)
|
|
}
|
|
|
|
// interfaceNameByIndex names one interface of the route table. An index that
|
|
// no interface holds gives an empty name.
|
|
func interfaceNameByIndex(index int) string {
|
|
if index == 0 {
|
|
return ""
|
|
}
|
|
iface, err := net.InterfaceByIndex(index)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return iface.Name
|
|
}
|