When an exit node has been set and a new default route is added, create a new rtable in the default rdomain and add the current default route via its physical interface. When control() is requesting a connection not go through the exit-node default route, we can use the SO_RTABLE socket option to force it through the new rtable we created. Updates #17321 Signed-off-by: joshua stein <jcs@jcs.org>main
parent
7370c24eb4
commit
518d241700
@ -1,9 +1,9 @@ |
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
// FreeBSD and OpenBSD routing table functions.
|
||||
|
||||
//go:build freebsd
|
||||
//go:build freebsd || openbsd
|
||||
|
||||
package netmon |
||||
|
||||
@ -0,0 +1,178 @@ |
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build openbsd
|
||||
|
||||
package netns |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os/exec" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"syscall" |
||||
|
||||
"golang.org/x/sys/unix" |
||||
"tailscale.com/net/netmon" |
||||
"tailscale.com/types/logger" |
||||
) |
||||
|
||||
var ( |
||||
bypassMu sync.Mutex |
||||
bypassRtable int |
||||
) |
||||
|
||||
// Called by the router when exit node routes are configured.
|
||||
func SetBypassRtable(rtable int) { |
||||
bypassMu.Lock() |
||||
defer bypassMu.Unlock() |
||||
bypassRtable = rtable |
||||
} |
||||
|
||||
func GetBypassRtable() int { |
||||
bypassMu.Lock() |
||||
defer bypassMu.Unlock() |
||||
return bypassRtable |
||||
} |
||||
|
||||
func control(logf logger.Logf, _ *netmon.Monitor) func(network, address string, c syscall.RawConn) error { |
||||
return func(network, address string, c syscall.RawConn) error { |
||||
return controlC(logf, network, address, c) |
||||
} |
||||
} |
||||
|
||||
func controlC(logf logger.Logf, _, address string, c syscall.RawConn) error { |
||||
if isLocalhost(address) { |
||||
return nil |
||||
} |
||||
|
||||
rtable := GetBypassRtable() |
||||
if rtable == 0 { |
||||
return nil |
||||
} |
||||
|
||||
return bindToRtable(c, rtable, logf) |
||||
} |
||||
|
||||
func bindToRtable(c syscall.RawConn, rtable int, logf logger.Logf) error { |
||||
var sockErr error |
||||
err := c.Control(func(fd uintptr) { |
||||
sockErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RTABLE, rtable) |
||||
}) |
||||
if sockErr != nil { |
||||
logf("netns: SO_RTABLE(%d): %v", rtable, sockErr) |
||||
} |
||||
if err != nil { |
||||
return fmt.Errorf("RawConn.Control: %w", err) |
||||
} |
||||
return sockErr |
||||
} |
||||
|
||||
// SetupBypassRtable creates a bypass rtable with the existing default route
|
||||
// in it routing through its existing physical interface. It should be called
|
||||
// by the router when exit node routes are being added.
|
||||
// Returns the rtable number.
|
||||
func SetupBypassRtable(logf logger.Logf) (int, error) { |
||||
bypassMu.Lock() |
||||
defer bypassMu.Unlock() |
||||
|
||||
if bypassRtable != 0 { |
||||
return bypassRtable, nil |
||||
} |
||||
|
||||
gw, err := getPhysicalGateway() |
||||
if err != nil { |
||||
return 0, fmt.Errorf("getPhysicalGateway: %w", err) |
||||
} |
||||
|
||||
rtable, err := findAvailableRtable() |
||||
if err != nil { |
||||
return 0, fmt.Errorf("findAvailableRtable: %w", err) |
||||
} |
||||
|
||||
// Add the existing default route interface to the new bypass rtable
|
||||
out, err := exec.Command("route", "-T", strconv.Itoa(rtable), "-qn", "add", "default", gw).CombinedOutput() |
||||
if err != nil { |
||||
return 0, fmt.Errorf("route -T%d add default %s: %w\n%s", rtable, gw, err, out) |
||||
} |
||||
|
||||
bypassRtable = rtable |
||||
logf("netns: created bypass rtable %d with default route via %s", rtable, gw) |
||||
return rtable, nil |
||||
} |
||||
|
||||
func CleanupBypassRtable(logf logger.Logf) { |
||||
bypassMu.Lock() |
||||
defer bypassMu.Unlock() |
||||
|
||||
if bypassRtable == 0 { |
||||
return |
||||
} |
||||
|
||||
// Delete the default route from the bypass rtable which should clear it
|
||||
out, err := exec.Command("route", "-T", strconv.Itoa(bypassRtable), "-qn", "delete", "default").CombinedOutput() |
||||
if err != nil { |
||||
logf("netns: failed to clear bypass route: %v\n%s", err, out) |
||||
} else { |
||||
logf("netns: cleared bypass rtable %d", bypassRtable) |
||||
} |
||||
|
||||
bypassRtable = 0 |
||||
} |
||||
|
||||
// getPhysicalGateway returns the default gateway IP that goes through a
|
||||
// physical interface (not tun).
|
||||
func getPhysicalGateway() (string, error) { |
||||
out, err := exec.Command("route", "-n", "show", "-inet").CombinedOutput() |
||||
if err != nil { |
||||
return "", fmt.Errorf("route show: %w", err) |
||||
} |
||||
|
||||
// Parse the routing table looking for default routes not via tun
|
||||
for _, line := range strings.Split(string(out), "\n") { |
||||
fields := strings.Fields(line) |
||||
if len(fields) < 8 { |
||||
continue |
||||
} |
||||
// Format: Destination Gateway Flags Refs Use Mtu Prio Iface
|
||||
dest := fields[0] |
||||
gateway := fields[1] |
||||
iface := fields[7] |
||||
|
||||
if dest == "default" && !strings.HasPrefix(iface, "tun") { |
||||
return gateway, nil |
||||
} |
||||
} |
||||
|
||||
return "", fmt.Errorf("no physical default gateway found") |
||||
} |
||||
|
||||
func findAvailableRtable() (int, error) { |
||||
for i := 1; i <= 255; i++ { |
||||
out, err := exec.Command("route", "-T", strconv.Itoa(i), "-n", "show", "-inet").CombinedOutput() |
||||
if err != nil { |
||||
// rtable doesn't exist, consider it available
|
||||
return i, nil |
||||
} |
||||
// Check if the output only contains the header (no actual routes)
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n") |
||||
hasRoutes := false |
||||
for _, line := range lines { |
||||
line = strings.TrimSpace(line) |
||||
if line == "" || strings.HasPrefix(line, "Routing") || strings.HasPrefix(line, "Destination") { |
||||
continue |
||||
} |
||||
hasRoutes = true |
||||
break |
||||
} |
||||
if !hasRoutes { |
||||
return i, nil |
||||
} |
||||
} |
||||
return 0, fmt.Errorf("no available rtable") |
||||
} |
||||
|
||||
func UseSocketMark() bool { |
||||
return false |
||||
} |
||||
Loading…
Reference in new issue