mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-22 17:01:43 +00:00
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>
This commit is contained in:
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,
|
||||||
|
27
ipn/prefs.go
27
ipn/prefs.go
@ -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…
x
Reference in New Issue
Block a user