wgengine/router: make the Windows ifconfig implementation reuse existing MibIPforwardRow2 when possible

Looking at profiles, we spend a lot of time in winipcfg.LUID.DeleteRoute
looking up the routing table entry for the provided RouteData.

But we already have the row! We previously obtained that data via the full
table dump we did in getInterfaceRoutes. We can make this a lot faster by
hanging onto a reference to the wipipcfg.MibIPforwardRow2 and executing
the delete operation directly on that.

Fixes #11123

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
This commit is contained in:
Aaron Klotz 2024-02-13 10:32:48 -07:00
parent 30c9189ed3
commit f7acbefbbb
2 changed files with 125 additions and 95 deletions

View File

@ -331,7 +331,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
} }
} }
var routes []*winipcfg.RouteData var routes []*routeData
foundDefault4 := false foundDefault4 := false
foundDefault6 := false foundDefault6 := false
for _, route := range cfg.Routes { for _, route := range cfg.Routes {
@ -361,10 +361,12 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
} else if route.Addr().Is6() { } else if route.Addr().Is6() {
gateway = firstGateway6 gateway = firstGateway6
} }
r := &winipcfg.RouteData{ r := &routeData{
Destination: route, RouteData: winipcfg.RouteData{
NextHop: gateway, Destination: route,
Metric: 0, NextHop: gateway,
Metric: 0,
},
} }
if r.Destination.Addr().Unmap() == gateway { if r.Destination.Addr().Unmap() == gateway {
// no need to add a route for the interface's // no need to add a route for the interface's
@ -393,9 +395,9 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
return fmt.Errorf("syncAddresses: %w", err) return fmt.Errorf("syncAddresses: %w", err)
} }
slices.SortFunc(routes, routeDataCompare) slices.SortFunc(routes, (*routeData).Compare)
deduplicatedRoutes := []*winipcfg.RouteData{} deduplicatedRoutes := []*routeData{}
for i := 0; i < len(routes); i++ { for i := 0; i < len(routes); i++ {
// There's only one way to get to a given IP+Mask, so delete // There's only one way to get to a given IP+Mask, so delete
// all matches after the first. // all matches after the first.
@ -596,18 +598,26 @@ func syncAddresses(ifc *winipcfg.IPAdapterAddresses, want []netip.Prefix) error
return erracc return erracc
} }
func routeDataLess(a, b *winipcfg.RouteData) bool { // routeData wraps winipcfg.RouteData with an additional field that permits
return routeDataCompare(a, b) < 0 // caching of the associated MibIPForwardRow2; by keeping it around, we can
// avoid unnecessary (and slow) lookups of information that we already have.
type routeData struct {
winipcfg.RouteData
Row *winipcfg.MibIPforwardRow2
} }
func routeDataCompare(a, b *winipcfg.RouteData) int { func (rd *routeData) Less(other *routeData) bool {
v := a.Destination.Addr().Compare(b.Destination.Addr()) return rd.Compare(other) < 0
}
func (rd *routeData) Compare(other *routeData) int {
v := rd.Destination.Addr().Compare(other.Destination.Addr())
if v != 0 { if v != 0 {
return v return v
} }
// Narrower masks first // Narrower masks first
b1, b2 := a.Destination.Bits(), b.Destination.Bits() b1, b2 := rd.Destination.Bits(), other.Destination.Bits()
if b1 != b2 { if b1 != b2 {
if b1 > b2 { if b1 > b2 {
return -1 return -1
@ -616,31 +626,31 @@ func routeDataCompare(a, b *winipcfg.RouteData) int {
} }
// No nexthop before non-empty nexthop // No nexthop before non-empty nexthop
v = a.NextHop.Compare(b.NextHop) v = rd.NextHop.Compare(other.NextHop)
if v != 0 { if v != 0 {
return v return v
} }
// Lower metrics first // Lower metrics first
if a.Metric < b.Metric { if rd.Metric < other.Metric {
return -1 return -1
} else if a.Metric > b.Metric { } else if rd.Metric > other.Metric {
return 1 return 1
} }
return 0 return 0
} }
func deltaRouteData(a, b []*winipcfg.RouteData) (add, del []*winipcfg.RouteData) { func deltaRouteData(a, b []*routeData) (add, del []*routeData) {
add = make([]*winipcfg.RouteData, 0, len(b)) add = make([]*routeData, 0, len(b))
del = make([]*winipcfg.RouteData, 0, len(a)) del = make([]*routeData, 0, len(a))
slices.SortFunc(a, routeDataCompare) slices.SortFunc(a, (*routeData).Compare)
slices.SortFunc(b, routeDataCompare) slices.SortFunc(b, (*routeData).Compare)
i := 0 i := 0
j := 0 j := 0
for i < len(a) && j < len(b) { for i < len(a) && j < len(b) {
switch routeDataCompare(a[i], b[j]) { switch a[i].Compare(b[j]) {
case -1: case -1:
// a < b, delete // a < b, delete
del = append(del, a[i]) del = append(del, a[i])
@ -677,7 +687,7 @@ func getInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses, family winipcfg.Addres
return return
} }
func getAllInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses) ([]*winipcfg.RouteData, error) { func getAllInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses) ([]*routeData, error) {
routes4, err := getInterfaceRoutes(ifc, windows.AF_INET) routes4, err := getInterfaceRoutes(ifc, windows.AF_INET)
if err != nil { if err != nil {
return nil, err return nil, err
@ -688,19 +698,27 @@ func getAllInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses) ([]*winipcfg.RouteD
// TODO: what if v6 unavailable? // TODO: what if v6 unavailable?
return nil, err return nil, err
} }
rd := make([]*winipcfg.RouteData, 0, len(routes4)+len(routes6))
rd := make([]*routeData, 0, len(routes4)+len(routes6))
for _, r := range routes4 { for _, r := range routes4 {
rd = append(rd, &winipcfg.RouteData{ rd = append(rd, &routeData{
Destination: r.DestinationPrefix.Prefix(), RouteData: winipcfg.RouteData{
NextHop: r.NextHop.Addr(), Destination: r.DestinationPrefix.Prefix(),
Metric: r.Metric, NextHop: r.NextHop.Addr(),
Metric: r.Metric,
},
Row: r,
}) })
} }
for _, r := range routes6 { for _, r := range routes6 {
rd = append(rd, &winipcfg.RouteData{ rd = append(rd, &routeData{
Destination: r.DestinationPrefix.Prefix(), RouteData: winipcfg.RouteData{
NextHop: r.NextHop.Addr(), Destination: r.DestinationPrefix.Prefix(),
Metric: r.Metric, NextHop: r.NextHop.Addr(),
Metric: r.Metric,
},
Row: r,
}) })
} }
return rd, nil return rd, nil
@ -708,7 +726,7 @@ func getAllInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses) ([]*winipcfg.RouteD
// filterRoutes removes routes that have been added by Windows and should not // filterRoutes removes routes that have been added by Windows and should not
// be managed by us. // be managed by us.
func filterRoutes(routes []*winipcfg.RouteData, dontDelete []netip.Prefix) []*winipcfg.RouteData { func filterRoutes(routes []*routeData, dontDelete []netip.Prefix) []*routeData {
ddm := make(map[netip.Prefix]bool) ddm := make(map[netip.Prefix]bool)
for _, dd := range dontDelete { for _, dd := range dontDelete {
// See issue 1448: we don't want to touch the routes added // See issue 1448: we don't want to touch the routes added
@ -727,7 +745,7 @@ func filterRoutes(routes []*winipcfg.RouteData, dontDelete []netip.Prefix) []*wi
lastIP := netipx.RangeOfPrefix(nr).To() lastIP := netipx.RangeOfPrefix(nr).To()
ddm[netip.PrefixFrom(lastIP, lastIP.BitLen())] = true ddm[netip.PrefixFrom(lastIP, lastIP.BitLen())] = true
} }
filtered := make([]*winipcfg.RouteData, 0, len(routes)) filtered := make([]*routeData, 0, len(routes))
for _, r := range routes { for _, r := range routes {
rr := r.Destination rr := r.Destination
if rr.IsValid() && ddm[rr] { if rr.IsValid() && ddm[rr] {
@ -742,7 +760,7 @@ func filterRoutes(routes []*winipcfg.RouteData, dontDelete []netip.Prefix) []*wi
// This avoids a full ifc.FlushRoutes call. // This avoids a full ifc.FlushRoutes call.
// dontDelete is a list of interface address routes that the // dontDelete is a list of interface address routes that the
// synchronization logic should never delete. // synchronization logic should never delete.
func syncRoutes(ifc *winipcfg.IPAdapterAddresses, want []*winipcfg.RouteData, dontDelete []netip.Prefix) error { func syncRoutes(ifc *winipcfg.IPAdapterAddresses, want []*routeData, dontDelete []netip.Prefix) error {
existingRoutes, err := getAllInterfaceRoutes(ifc) existingRoutes, err := getAllInterfaceRoutes(ifc)
if err != nil { if err != nil {
return err return err
@ -753,7 +771,15 @@ func syncRoutes(ifc *winipcfg.IPAdapterAddresses, want []*winipcfg.RouteData, do
var errs []error var errs []error
for _, a := range del { for _, a := range del {
err := ifc.LUID.DeleteRoute(a.Destination, a.NextHop) var err error
if a.Row == nil {
// DeleteRoute requires a routing table lookup, so only do that if
// a does not already have the row.
err = ifc.LUID.DeleteRoute(a.Destination, a.NextHop)
} else {
// Otherwise, delete the row directly.
err = a.Row.Delete()
}
if err != nil { if err != nil {
dstStr := a.Destination.String() dstStr := a.Destination.String()
if dstStr == "169.254.255.255/32" { if dstStr == "169.254.255.255/32" {

View File

@ -19,58 +19,62 @@ func randIP() netip.Addr {
return netip.AddrFrom4([4]byte{b, b, b, b}) return netip.AddrFrom4([4]byte{b, b, b, b})
} }
func randRouteData() *winipcfg.RouteData { func randRouteData() *routeData {
return &winipcfg.RouteData{ return &routeData{
Destination: netip.PrefixFrom(randIP(), rand.Intn(30)+1), RouteData: winipcfg.RouteData{
NextHop: randIP(), Destination: netip.PrefixFrom(randIP(), rand.Intn(30)+1),
Metric: uint32(rand.Intn(3)), NextHop: randIP(),
Metric: uint32(rand.Intn(3)),
},
} }
} }
type W = winipcfg.RouteData
func TestRouteLess(t *testing.T) { func TestRouteLess(t *testing.T) {
type D = winipcfg.RouteData type D = routeData
ipnet := netip.MustParsePrefix ipnet := netip.MustParsePrefix
tests := []struct { tests := []struct {
ri, rj *winipcfg.RouteData ri, rj *routeData
want bool want bool
}{ }{
{ {
ri: &D{Metric: 1}, ri: &D{RouteData: W{Metric: 1}},
rj: &D{Metric: 2}, rj: &D{RouteData: W{Metric: 2}},
want: true, want: true,
}, },
{ {
ri: &D{Destination: ipnet("1.1.0.0/16"), Metric: 2}, ri: &D{RouteData: W{Destination: ipnet("1.1.0.0/16"), Metric: 2}},
rj: &D{Destination: ipnet("2.2.0.0/16"), Metric: 1}, rj: &D{RouteData: W{Destination: ipnet("2.2.0.0/16"), Metric: 1}},
want: true, want: true,
}, },
{ {
ri: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1}, ri: &D{RouteData: W{Destination: ipnet("1.1.0.0/16"), Metric: 1}},
rj: &D{Destination: ipnet("2.2.0.0/16"), Metric: 1}, rj: &D{RouteData: W{Destination: ipnet("2.2.0.0/16"), Metric: 1}},
want: true, want: true,
}, },
{ {
ri: &D{Destination: ipnet("1.1.0.0/32"), Metric: 2}, ri: &D{RouteData: W{Destination: ipnet("1.1.0.0/32"), Metric: 2}},
rj: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1}, rj: &D{RouteData: W{Destination: ipnet("1.1.0.0/16"), Metric: 1}},
want: true, want: true,
}, },
{ {
ri: &D{Destination: ipnet("1.1.0.0/32"), Metric: 1}, ri: &D{RouteData: W{Destination: ipnet("1.1.0.0/32"), Metric: 1}},
rj: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1}, rj: &D{RouteData: W{Destination: ipnet("1.1.0.0/16"), Metric: 1}},
want: true, want: true,
}, },
{ {
ri: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1, NextHop: netip.MustParseAddr("3.3.3.3")}, ri: &D{RouteData: W{Destination: ipnet("1.1.0.0/16"), Metric: 1, NextHop: netip.MustParseAddr("3.3.3.3")}},
rj: &D{Destination: ipnet("1.1.0.0/16"), Metric: 1, NextHop: netip.MustParseAddr("4.4.4.4")}, rj: &D{RouteData: W{Destination: ipnet("1.1.0.0/16"), Metric: 1, NextHop: netip.MustParseAddr("4.4.4.4")}},
want: true, want: true,
}, },
} }
for i, tt := range tests { for i, tt := range tests {
got := routeDataLess(tt.ri, tt.rj) got := tt.ri.Less(tt.rj)
if got != tt.want { if got != tt.want {
t.Errorf("%v. less = %v; want %v", i, got, tt.want) t.Errorf("%v. less = %v; want %v", i, got, tt.want)
} }
back := routeDataLess(tt.rj, tt.ri) back := tt.rj.Less(tt.ri)
if back && got { if back && got {
t.Errorf("%v. less both ways", i) t.Errorf("%v. less both ways", i)
} }
@ -81,7 +85,7 @@ func TestRouteDataLessConsistent(t *testing.T) {
for i := 0; i < 10000; i++ { for i := 0; i < 10000; i++ {
ri := randRouteData() ri := randRouteData()
rj := randRouteData() rj := randRouteData()
if routeDataLess(ri, rj) && routeDataLess(rj, ri) { if ri.Less(rj) && rj.Less(ri) {
t.Fatalf("both compare less to each other:\n\t%#v\nand\n\t%#v", ri, rj) t.Fatalf("both compare less to each other:\n\t%#v\nand\n\t%#v", ri, rj)
} }
} }
@ -139,7 +143,7 @@ func TestDeltaNets(t *testing.T) {
} }
} }
func formatRouteData(rds []*winipcfg.RouteData) string { func formatRouteData(rds []*routeData) string {
var b strings.Builder var b strings.Builder
for _, rd := range rds { for _, rd := range rds {
b.WriteString(fmt.Sprintf("%+v", rd)) b.WriteString(fmt.Sprintf("%+v", rd))
@ -147,12 +151,12 @@ func formatRouteData(rds []*winipcfg.RouteData) string {
return b.String() return b.String()
} }
func equalRouteDatas(a, b []*winipcfg.RouteData) bool { func equalRouteDatas(a, b []*routeData) bool {
if len(a) != len(b) { if len(a) != len(b) {
return false return false
} }
for i := range a { for i := range a {
if routeDataCompare(a[i], b[i]) != 0 { if a[i].Compare(b[i]) != 0 {
return false return false
} }
} }
@ -166,33 +170,33 @@ func ipnet4(ip string, bits int) netip.Prefix {
func TestFilterRoutes(t *testing.T) { func TestFilterRoutes(t *testing.T) {
var h0 netip.Addr var h0 netip.Addr
in := []*winipcfg.RouteData{ in := []*routeData{
// LinkLocal and Loopback routes. // LinkLocal and Loopback routes.
{ipnet4("169.254.0.0", 16), h0, 1}, {RouteData: W{ipnet4("169.254.0.0", 16), h0, 1}},
{ipnet4("169.254.255.255", 32), h0, 1}, {RouteData: W{ipnet4("169.254.255.255", 32), h0, 1}},
{ipnet4("127.0.0.0", 8), h0, 1}, {RouteData: W{ipnet4("127.0.0.0", 8), h0, 1}},
{ipnet4("127.255.255.255", 32), h0, 1}, {RouteData: W{ipnet4("127.255.255.255", 32), h0, 1}},
// Local LAN routes. // Local LAN routes.
{ipnet4("192.168.0.0", 24), h0, 1}, {RouteData: W{ipnet4("192.168.0.0", 24), h0, 1}},
{ipnet4("192.168.0.255", 32), h0, 1}, {RouteData: W{ipnet4("192.168.0.255", 32), h0, 1}},
{ipnet4("192.168.1.0", 25), h0, 1}, {RouteData: W{ipnet4("192.168.1.0", 25), h0, 1}},
{ipnet4("192.168.1.127", 32), h0, 1}, {RouteData: W{ipnet4("192.168.1.127", 32), h0, 1}},
// Some random other route. // Some random other route.
{ipnet4("192.168.2.23", 32), h0, 1}, {RouteData: W{ipnet4("192.168.2.23", 32), h0, 1}},
// Our own tailscale address. // Our own tailscale address.
{ipnet4("100.100.100.100", 32), h0, 1}, {RouteData: W{ipnet4("100.100.100.100", 32), h0, 1}},
// Other tailscale addresses. // Other tailscale addresses.
{ipnet4("100.100.100.101", 32), h0, 1}, {RouteData: W{ipnet4("100.100.100.101", 32), h0, 1}},
{ipnet4("100.100.100.102", 32), h0, 1}, {RouteData: W{ipnet4("100.100.100.102", 32), h0, 1}},
} }
want := []*winipcfg.RouteData{ want := []*routeData{
{ipnet4("169.254.0.0", 16), h0, 1}, {RouteData: W{ipnet4("169.254.0.0", 16), h0, 1}},
{ipnet4("127.0.0.0", 8), h0, 1}, {RouteData: W{ipnet4("127.0.0.0", 8), h0, 1}},
{ipnet4("192.168.0.0", 24), h0, 1}, {RouteData: W{ipnet4("192.168.0.0", 24), h0, 1}},
{ipnet4("192.168.1.0", 25), h0, 1}, {RouteData: W{ipnet4("192.168.1.0", 25), h0, 1}},
{ipnet4("192.168.2.23", 32), h0, 1}, {RouteData: W{ipnet4("192.168.2.23", 32), h0, 1}},
{ipnet4("100.100.100.101", 32), h0, 1}, {RouteData: W{ipnet4("100.100.100.101", 32), h0, 1}},
{ipnet4("100.100.100.102", 32), h0, 1}, {RouteData: W{ipnet4("100.100.100.102", 32), h0, 1}},
} }
got := filterRoutes(in, mustCIDRs("100.100.100.100/32")) got := filterRoutes(in, mustCIDRs("100.100.100.100/32"))
@ -206,25 +210,25 @@ func TestDeltaRouteData(t *testing.T) {
h1 := netip.MustParseAddr("99.99.99.99") h1 := netip.MustParseAddr("99.99.99.99")
h2 := netip.MustParseAddr("99.99.9.99") h2 := netip.MustParseAddr("99.99.9.99")
a := []*winipcfg.RouteData{ a := []*routeData{
{ipnet4("1.2.3.4", 32), h0, 1}, {RouteData: W{ipnet4("1.2.3.4", 32), h0, 1}},
{ipnet4("1.2.3.4", 24), h1, 2}, {RouteData: W{ipnet4("1.2.3.4", 24), h1, 2}},
{ipnet4("1.2.3.4", 24), h2, 1}, {RouteData: W{ipnet4("1.2.3.4", 24), h2, 1}},
{ipnet4("1.2.3.5", 32), h0, 1}, {RouteData: W{ipnet4("1.2.3.5", 32), h0, 1}},
} }
b := []*winipcfg.RouteData{ b := []*routeData{
{ipnet4("1.2.3.5", 32), h0, 1}, {RouteData: W{ipnet4("1.2.3.5", 32), h0, 1}},
{ipnet4("1.2.3.4", 24), h1, 2}, {RouteData: W{ipnet4("1.2.3.4", 24), h1, 2}},
{ipnet4("1.2.3.4", 24), h2, 2}, {RouteData: W{ipnet4("1.2.3.4", 24), h2, 2}},
} }
add, del := deltaRouteData(a, b) add, del := deltaRouteData(a, b)
wantAdd := []*winipcfg.RouteData{ wantAdd := []*routeData{
{ipnet4("1.2.3.4", 24), h2, 2}, {RouteData: W{ipnet4("1.2.3.4", 24), h2, 2}},
} }
wantDel := []*winipcfg.RouteData{ wantDel := []*routeData{
{ipnet4("1.2.3.4", 32), h0, 1}, {RouteData: W{ipnet4("1.2.3.4", 32), h0, 1}},
{ipnet4("1.2.3.4", 24), h2, 1}, {RouteData: W{ipnet4("1.2.3.4", 24), h2, 1}},
} }
if !equalRouteDatas(add, wantAdd) { if !equalRouteDatas(add, wantAdd) {