Compare commits

...

3 Commits

Author SHA1 Message Date
James Tucker 919ad7df82
ipn,ipn/ipnlocal,cmd/tailscale/cli: add support for passing a route filter via --accept-routes
Signed-off-by: James Tucker <james@tailscale.com>
2022-09-27 18:45:02 -07:00
James Tucker 627c60b0d1
ipn/ipnlocal: apply the new AcceptRoutesFilter to all peer routes
Signed-off-by: James Tucker <james@tailscale.com>
2022-09-27 18:10:56 -07:00
James Tucker 2d0ecbb883
ipn: add AcceptRoutesFilter preference for filtering peer routes
The preference is stored in string form as the internal representation
netipx.IPSetBuilder does not serialize. The string form is reasonably
compact and readable.

Signed-off-by: James Tucker <james@tailscale.com>
2022-09-27 17:46:16 -07:00
5 changed files with 101 additions and 15 deletions

View File

@ -69,14 +69,17 @@ func effectiveGOOS() string {
// acceptRouteDefault returns the CLI's default value of --accept-routes as
// a function of the platform it's running on.
func acceptRouteDefault(goos string) bool {
func acceptRouteDefault(goos string) string {
switch goos {
case "windows":
return true
return "true"
case "darwin":
return version.IsSandboxedMacOS()
if version.IsSandboxedMacOS() {
return "true"
}
return "false"
default:
return false
return "false"
}
}
@ -93,7 +96,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
upf.StringVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
@ -131,7 +134,7 @@ type upArgsT struct {
qr bool
reset bool
server string
acceptRoutes bool
acceptRoutes string
acceptDNS bool
singleRoutes bool
exitNodeIP string
@ -307,7 +310,25 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
prefs := ipn.NewPrefs()
prefs.ControlURL = upArgs.server
prefs.WantRunning = true
prefs.RouteAll = upArgs.acceptRoutes
switch upArgs.acceptRoutes {
case "0", "f", "false":
prefs.RouteAll = false
case "1", "t", "true":
prefs.RouteAll = true
default:
prefs.RouteAll = true
prefs.AcceptRoutesFilter = upArgs.acceptRoutes
// accept-routes accepts an include/exclude ip range of the form:
// 0.0.0.0/0,-192.168.20.0/24
// Ensure that the provided values parse correctly, as the backend can only
// bury errors in the logs.
_, err := ipn.ParseAcceptRoutesFilter(prefs.AcceptRoutesFilter)
if err != nil {
return nil, fmt.Errorf("accept-routes filter %q did not parse: %w", prefs.AcceptRoutesFilter, err)
}
}
if upArgs.exitNodeIP != "" {
if err := prefs.SetExitNodeIP(upArgs.exitNodeIP, st); err != nil {
@ -453,7 +474,7 @@ func runUp(ctx context.Context, args []string) (retErr error) {
if distro.Get() == distro.Synology {
notSupported := "not supported on Synology; see https://github.com/tailscale/tailscale/issues/1995"
if upArgs.acceptRoutes {
if upArgs.acceptRoutes != "" && upArgs.acceptRoutes != "f" && upArgs.acceptRoutes != "false" {
return errors.New("--accept-routes is " + notSupported)
}
if upArgs.exitNodeIP != "" {
@ -735,10 +756,10 @@ func init() {
// And this flag has two ipn.Prefs:
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeID")
addPrefFlagMapping("accept-routes", "RouteAll", "AcceptRoutesFilter")
// The rest are 1:1:
addPrefFlagMapping("accept-dns", "CorpDNS")
addPrefFlagMapping("accept-routes", "RouteAll")
addPrefFlagMapping("advertise-tags", "AdvertiseTags")
addPrefFlagMapping("host-routes", "AllowSingleHosts")
addPrefFlagMapping("hostname", "Hostname")

View File

@ -35,6 +35,7 @@ func (src *Prefs) Clone() *Prefs {
var _PrefsCloneNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
AcceptRoutesFilter string
AllowSingleHosts bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netip.Addr

View File

@ -2775,18 +2775,42 @@ func ipPrefixLess(ri, rj netip.Prefix) bool {
return ri.Addr().Less(rj.Addr())
}
func (b *LocalBackend) filterRoutes(routes []netip.Prefix, acceptFilter *netipx.IPSet) []netip.Prefix {
if acceptFilter == nil {
return routes
}
var builder netipx.IPSetBuilder
for _, r := range routes {
builder.AddPrefix(r)
}
builder.Intersect(acceptFilter)
set, err := builder.IPSet()
if err != nil {
b.logf("accept routes filter: failed to build filtered set, all routes will be accepted: %v (check accept-routes flag)", err)
return routes
}
b.logf("accept routes filter: accepting routes: %v", set.Ranges())
return set.Prefixes()
}
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs, oneCGNATRoute bool) *router.Config {
singleRouteThreshold := 10_000
if oneCGNATRoute {
singleRouteThreshold = 1
}
acceptRoutesFilterSet, err := ipn.ParseAcceptRoutesFilter(prefs.AcceptRoutesFilter)
if err != nil {
b.logf("accept routes filter: failed to build filter set from %q: %v", prefs.AcceptRoutesFilter, err)
}
rs := &router.Config{
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes),
SNATSubnetRoutes: !prefs.NoSNAT,
NetfilterMode: prefs.NetfilterMode,
Routes: peerRoutes(cfg.Peers, singleRouteThreshold),
Routes: b.filterRoutes(peerRoutes(cfg.Peers, singleRouteThreshold), acceptRoutesFilterSet),
}
if distro.Get() == distro.Synology {

View File

@ -17,6 +17,7 @@ import (
"runtime"
"strings"
"go4.org/netipx"
"tailscale.com/atomicfile"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netaddr"
@ -70,6 +71,13 @@ type Prefs struct {
// controlled by ExitNodeID/IP below.
RouteAll bool
// AcceptRoutesFilter specifies an ordered list of IP ranges that are to be
// included or excluded from peer routes. The value is comma-seprated IP CIDRs
// with an optional leading `-` prefix indicating an exclusion, e.g.
// "0.0.0.0/0,-192.168.20.0/24" meaning "all routes except those intersecting
// 192.168.20.0/24".
AcceptRoutesFilter string
// AllowSingleHosts specifies whether to install routes for each
// node IP on the tailscale network, in addition to a route for
// the whole network.
@ -206,6 +214,7 @@ type MaskedPrefs struct {
ControlURLSet bool `json:",omitempty"`
RouteAllSet bool `json:",omitempty"`
AcceptRoutesFilterSet bool `json:",omitempty"`
AllowSingleHostsSet bool `json:",omitempty"`
ExitNodeIDSet bool `json:",omitempty"`
ExitNodeIPSet bool `json:",omitempty"`
@ -293,6 +302,9 @@ func (p *Prefs) pretty(goos string) string {
var sb strings.Builder
sb.WriteString("Prefs{")
fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
if p.RouteAll || p.AcceptRoutesFilter != "" {
fmt.Fprintf(&sb, "acceptfilter=%q ", p.AcceptRoutesFilter)
}
if !p.AllowSingleHosts {
sb.WriteString("mesh=false ")
}
@ -366,6 +378,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
return p != nil && p2 != nil &&
p.ControlURL == p2.ControlURL &&
p.RouteAll == p2.RouteAll &&
p.AcceptRoutesFilter == p2.AcceptRoutesFilter &&
p.AllowSingleHosts == p2.AllowSingleHosts &&
p.ExitNodeID == p2.ExitNodeID &&
p.ExitNodeIP == p2.ExitNodeIP &&
@ -423,11 +436,12 @@ func NewPrefs() *Prefs {
// later anyway.
ControlURL: "",
RouteAll: true,
AllowSingleHosts: true,
CorpDNS: true,
WantRunning: false,
NetfilterMode: preftype.NetfilterOn,
RouteAll: true,
AcceptRoutesFilter: "0.0.0.0/0,::/0",
AllowSingleHosts: true,
CorpDNS: true,
WantRunning: false,
NetfilterMode: preftype.NetfilterOn,
}
}
@ -644,3 +658,28 @@ func SavePrefs(filename string, p *Prefs) {
log.Printf("SavePrefs: %v\n", err)
}
}
func ParseAcceptRoutesFilter(acceptFilter string) (*netipx.IPSet, error) {
var acceptFilterBuilder netipx.IPSetBuilder
for _, af := range strings.Split(acceptFilter, ",") {
af = strings.TrimSpace(af)
if af == "" {
continue
}
includeRange := true
if strings.HasPrefix(af, "-") {
includeRange = false
af = af[1:]
}
pfx, err := netip.ParsePrefix(af)
if err != nil {
return nil, err
}
if includeRange {
acceptFilterBuilder.AddPrefix(pfx)
} else {
acceptFilterBuilder.RemovePrefix(pfx)
}
}
return acceptFilterBuilder.IPSet()
}

View File

@ -38,6 +38,7 @@ func TestPrefsEqual(t *testing.T) {
prefsHandles := []string{
"ControlURL",
"RouteAll",
"AcceptRoutesFilter",
"AllowSingleHosts",
"ExitNodeID",
"ExitNodeIP",