mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
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
|
|
}
|