mirror of
https://github.com/tailscale/tailscale.git
synced 2026-03-29 03:31:22 -04:00
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>
179 lines
4.4 KiB
Go
179 lines
4.4 KiB
Go
// 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
|
|
}
|