ipn: program exit node into the data plane according to user pref.

Part of #1153, #1154. Fixes #1224.

Signed-off-by: David Anderson <danderson@tailscale.com>
pull/1280/head
David Anderson 4 years ago committed by Dave Anderson
parent fb6b0e247c
commit b9c2231fdf

@ -45,6 +45,7 @@ specify any flags, options are reset to their default.
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes") upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel") 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.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections") upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication") upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)") upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
@ -74,6 +75,7 @@ var upArgs struct {
acceptRoutes bool acceptRoutes bool
acceptDNS bool acceptDNS bool
singleRoutes bool singleRoutes bool
exitNodeIP string
shieldsUp bool shieldsUp bool
forceReauth bool forceReauth bool
advertiseRoutes string advertiseRoutes string
@ -138,6 +140,9 @@ func runUp(ctx context.Context, args []string) error {
if upArgs.acceptRoutes { if upArgs.acceptRoutes {
return errors.New("--accept-routes is " + notSupported) return errors.New("--accept-routes is " + notSupported)
} }
if upArgs.exitNodeIP != "" {
return errors.New("--exit-node is " + notSupported)
}
if upArgs.netfilterMode != "off" { if upArgs.netfilterMode != "off" {
return errors.New("--netfilter-mode values besides \"off\" " + notSupported) return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
} }
@ -170,6 +175,15 @@ func runUp(ctx context.Context, args []string) error {
checkIPForwarding() checkIPForwarding()
} }
var exitNodeIP netaddr.IP
if upArgs.exitNodeIP != "" {
var err error
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
if err != nil {
fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
}
}
var tags []string var tags []string
if upArgs.advertiseTags != "" { if upArgs.advertiseTags != "" {
tags = strings.Split(upArgs.advertiseTags, ",") tags = strings.Split(upArgs.advertiseTags, ",")
@ -190,6 +204,7 @@ func runUp(ctx context.Context, args []string) error {
prefs.ControlURL = upArgs.server prefs.ControlURL = upArgs.server
prefs.WantRunning = true prefs.WantRunning = true
prefs.RouteAll = upArgs.acceptRoutes prefs.RouteAll = upArgs.acceptRoutes
prefs.ExitNodeIP = exitNodeIP
prefs.CorpDNS = upArgs.acceptDNS prefs.CorpDNS = upArgs.acceptDNS
prefs.AllowSingleHosts = upArgs.singleRoutes prefs.AllowSingleHosts = upArgs.singleRoutes
prefs.ShieldsUp = upArgs.shieldsUp prefs.ShieldsUp = upArgs.shieldsUp

@ -45,7 +45,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/packet from tailscale.com/wgengine/filter tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/stun from tailscale.com/net/netcheck tailscale.com/net/stun from tailscale.com/net/netcheck
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli tailscale.com/paths from tailscale.com/cmd/tailscale/cli
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli

@ -14,6 +14,7 @@ import (
"time" "time"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/wgkey" "tailscale.com/types/wgkey"
@ -249,7 +250,6 @@ type WGConfigFlags int
const ( const (
AllowSingleHosts WGConfigFlags = 1 << iota AllowSingleHosts WGConfigFlags = 1 << iota
AllowSubnetRoutes AllowSubnetRoutes
AllowDefaultRoute
) )
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key // EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
@ -271,10 +271,6 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
if Debug.OnlyDisco && peer.DiscoKey.IsZero() { if Debug.OnlyDisco && peer.DiscoKey.IsZero() {
continue continue
} }
if (flags&AllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
logf("wgcfg: %v skipping a single-host peer.", peer.Key.ShortString())
continue
}
cfg.Peers = append(cfg.Peers, wgcfg.Peer{ cfg.Peers = append(cfg.Peers, wgcfg.Peer{
PublicKey: wgcfg.Key(peer.Key), PublicKey: wgcfg.Key(peer.Key),
}) })
@ -298,13 +294,12 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
} }
} }
} }
for _, allowedIP := range peer.AllowedIPs { for _, allowedIP := range peer.AllowedIPs {
if allowedIP.Bits == 0 { if allowedIP.IsSingleIP() && tsaddr.IsTailscaleIP(allowedIP.IP) && (flags&AllowSingleHosts) == 0 {
if (flags & AllowDefaultRoute) == 0 { logf("[v1] wgcfg: skipping node IP %v from %q (%v)",
logf("[v1] wgcfg: not accepting default route from %q (%v)", allowedIP.IP, nodeDebugName(peer), peer.Key.ShortString())
nodeDebugName(peer), peer.Key.ShortString()) continue
continue
}
} else if cidrIsSubnet(peer, allowedIP) { } else if cidrIsSubnet(peer, allowedIP) {
if (flags & AllowSubnetRoutes) == 0 { if (flags & AllowSubnetRoutes) == 0 {
logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)", logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)",

@ -306,8 +306,10 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
prefsChanged = true prefsChanged = true
} }
if st.NetMap != nil { if st.NetMap != nil {
if b.keepOneExitNodeLocked(st.NetMap) {
prefsChanged = true
}
b.setNetMapLocked(st.NetMap) b.setNetMapLocked(st.NetMap)
} }
if st.URL != "" { if st.URL != "" {
b.authURL = st.URL b.authURL = st.URL
@ -365,6 +367,57 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.authReconfig() b.authReconfig()
} }
// keepOneExitNodeLocked edits nm to retain only the default
// routes provided by the exit node specified in b.prefs. It returns
// whether prefs was mutated as part of the process, due to an exit
// node IP being converted into a node ID.
func (b *LocalBackend) keepOneExitNodeLocked(nm *controlclient.NetworkMap) (prefsChanged bool) {
if b.prefs.ExitNodeID == "" && b.prefs.ExitNodeIP.IsZero() {
return false
}
// If we have a desired IP on file, try to find the corresponding
// node.
if !b.prefs.ExitNodeIP.IsZero() {
// IP takes precedence over ID, so if both are set, clear ID.
if b.prefs.ExitNodeID != "" {
b.prefs.ExitNodeID = ""
prefsChanged = true
}
peerLoop:
for _, peer := range nm.Peers {
for _, addr := range peer.Addresses {
if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP {
continue
}
// Found the node being referenced, upgrade prefs to
// reference it directly for next time.
b.prefs.ExitNodeID = peer.StableID
b.prefs.ExitNodeIP = netaddr.IP{}
prefsChanged = true
break peerLoop
}
}
}
// At this point, we have a node ID if the requested node is in
// the netmap. If not, the ID will be empty, and we'll strip out
// all default routes.
for _, peer := range nm.Peers {
out := peer.AllowedIPs[:0]
for _, allowedIP := range peer.AllowedIPs {
if allowedIP.Bits == 0 && peer.StableID != b.prefs.ExitNodeID {
continue
}
out = append(out, allowedIP)
}
peer.AllowedIPs = out
}
return prefsChanged
}
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status. // setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
// This updates the endpoints both in the backend and in the control client. // This updates the endpoints both in the backend and in the control client.
func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) { func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
@ -1203,8 +1256,6 @@ func (b *LocalBackend) authReconfig() {
var flags controlclient.WGConfigFlags var flags controlclient.WGConfigFlags
if uc.RouteAll { if uc.RouteAll {
flags |= controlclient.AllowDefaultRoute
// TODO(apenwarr): Make subnet routes a different pref?
flags |= controlclient.AllowSubnetRoutes flags |= controlclient.AllowSubnetRoutes
} }
if uc.AllowSingleHosts { if uc.AllowSingleHosts {
@ -1256,6 +1307,11 @@ func magicDNSRootDomains(nm *controlclient.NetworkMap) []string {
return nil return nil
} }
var (
ipv4Default = netaddr.MustParseIPPrefix("0.0.0.0/0")
ipv6Default = netaddr.MustParseIPPrefix("::/0")
)
// routerConfig produces a router.Config from a wireguard config and IPN prefs. // routerConfig produces a router.Config from a wireguard config and IPN prefs.
func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
rs := &router.Config{ rs := &router.Config{
@ -1269,6 +1325,32 @@ func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...) rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...)
} }
// Sanity check: we expect the control server to program both a v4
// and a v6 default route, if default routing is on. Fill in
// blackhole routes appropriately if we're missing some. This is
// likely to break some functionality, but if the user expressed a
// preference for routing remotely, we want to avoid leaking
// traffic at the expense of functionality.
if prefs.ExitNodeID != "" || !prefs.ExitNodeIP.IsZero() {
var default4, default6 bool
for _, route := range rs.Routes {
if route == ipv4Default {
default4 = true
} else if route == ipv6Default {
default6 = true
}
if default4 && default6 {
break
}
}
if !default4 {
rs.Routes = append(rs.Routes, ipv4Default)
}
if !default6 {
rs.Routes = append(rs.Routes, ipv6Default)
}
}
rs.Routes = append(rs.Routes, netaddr.IPPrefix{ rs.Routes = append(rs.Routes, netaddr.IPPrefix{
IP: tsaddr.TailscaleServiceIP(), IP: tsaddr.TailscaleServiceIP(),
Bits: 32, Bits: 32,

@ -18,6 +18,7 @@ import (
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/atomicfile" "tailscale.com/atomicfile"
"tailscale.com/control/controlclient" "tailscale.com/control/controlclient"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
) )
@ -28,8 +29,10 @@ type Prefs struct {
// ControlURL is the URL of the control server to use. // ControlURL is the URL of the control server to use.
ControlURL string ControlURL string
// RouteAll specifies whether to accept subnet and default routes // RouteAll specifies whether to accept subnets advertised by
// advertised by other nodes on the Tailscale network. // other nodes on the Tailscale network. Note that this does not
// include default routes (0.0.0.0/0 and ::/0), those are
// controlled by ExitNodeID/IP below.
RouteAll bool RouteAll bool
// AllowSingleHosts specifies whether to install routes for each // AllowSingleHosts specifies whether to install routes for each
@ -44,6 +47,24 @@ type Prefs struct {
// packets stop flowing. What's up with that? // packets stop flowing. What's up with that?
AllowSingleHosts bool AllowSingleHosts bool
// ExitNodeID and ExitNodeIP specify the node that should be used
// as an exit node for internet traffic. At most one of these
// should be non-zero.
//
// The preferred way to express the chosen node is ExitNodeID, but
// in some cases it's not possible to use that ID (e.g. in the
// linux CLI, before tailscaled has a netmap). For those
// situations, we allow specifying the exit node by IP, and
// ipnlocal.LocalBackend will translate the IP into an ID when the
// node is found in the netmap.
//
// If the selected exit node doesn't exist (e.g. it's not part of
// the current tailnet), or it doesn't offer exit node services, a
// blackhole route will be installed on the local system to
// prevent any traffic escaping to the local network.
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
// CorpDNS specifies whether to install the Tailscale network's // CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists. // DNS configuration, if it exists.
CorpDNS bool CorpDNS bool
@ -191,6 +212,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.ControlURL == p2.ControlURL && p.ControlURL == p2.ControlURL &&
p.RouteAll == p2.RouteAll && p.RouteAll == p2.RouteAll &&
p.AllowSingleHosts == p2.AllowSingleHosts && p.AllowSingleHosts == p2.AllowSingleHosts &&
p.ExitNodeID == p2.ExitNodeID &&
p.ExitNodeIP == p2.ExitNodeIP &&
p.CorpDNS == p2.CorpDNS && p.CorpDNS == p2.CorpDNS &&
p.WantRunning == p2.WantRunning && p.WantRunning == p2.WantRunning &&
p.NotepadURLs == p2.NotepadURLs && p.NotepadURLs == p2.NotepadURLs &&

@ -9,6 +9,7 @@ package ipn
import ( import (
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/control/controlclient" "tailscale.com/control/controlclient"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
) )
@ -35,6 +36,8 @@ var _PrefsNeedsRegeneration = Prefs(struct {
ControlURL string ControlURL string
RouteAll bool RouteAll bool
AllowSingleHosts bool AllowSingleHosts bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
CorpDNS bool CorpDNS bool
WantRunning bool WantRunning bool
ShieldsUp bool ShieldsUp bool

@ -30,7 +30,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestPrefsEqual(t *testing.T) { func TestPrefsEqual(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"} prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) { if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n", t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, prefsHandles) have, prefsHandles)
@ -99,6 +99,28 @@ func TestPrefsEqual(t *testing.T) {
true, true,
}, },
{
&Prefs{ExitNodeID: "n1234"},
&Prefs{},
false,
},
{
&Prefs{ExitNodeID: "n1234"},
&Prefs{ExitNodeID: "n1234"},
true,
},
{
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
&Prefs{},
false,
},
{
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
true,
},
{ {
&Prefs{CorpDNS: true}, &Prefs{CorpDNS: true},
&Prefs{CorpDNS: false}, &Prefs{CorpDNS: false},

Loading…
Cancel
Save