|
|
@@ -7,6 +7,7 @@ package ipn
|
|
|
import (
|
|
|
"bytes"
|
|
|
"encoding/json"
|
|
|
+ "errors"
|
|
|
"fmt"
|
|
|
"io/ioutil"
|
|
|
"log"
|
|
|
@@ -18,10 +19,12 @@ import (
|
|
|
|
|
|
"inet.af/netaddr"
|
|
|
"tailscale.com/atomicfile"
|
|
|
+ "tailscale.com/ipn/ipnstate"
|
|
|
"tailscale.com/net/tsaddr"
|
|
|
"tailscale.com/tailcfg"
|
|
|
"tailscale.com/types/persist"
|
|
|
"tailscale.com/types/preftype"
|
|
|
+ "tailscale.com/util/dnsname"
|
|
|
)
|
|
|
|
|
|
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
|
|
@@ -31,6 +34,12 @@ import (
|
|
|
// The default control plane is the hosted version run by Tailscale.com.
|
|
|
const DefaultControlURL = "https://controlplane.tailscale.com"
|
|
|
|
|
|
+var (
|
|
|
+ // ErrExitNodeIDAlreadySet is returned from (*Prefs).SetExitNodeIP when the
|
|
|
+ // Prefs.ExitNodeID field is already set.
|
|
|
+ ErrExitNodeIDAlreadySet = errors.New("cannot set ExitNodeIP when ExitNodeID is already set")
|
|
|
+)
|
|
|
+
|
|
|
// IsLoginServerSynonym reports whether a URL is a drop-in replacement
|
|
|
// for the primary Tailscale login server.
|
|
|
func IsLoginServerSynonym(val interface{}) bool {
|
|
|
@@ -467,6 +476,109 @@ func (p *Prefs) SetAdvertiseExitNode(runExit bool) {
|
|
|
netaddr.IPPrefixFrom(netaddr.IPv6Unspecified(), 0))
|
|
|
}
|
|
|
|
|
|
+// peerWithTailscaleIP returns the peer in st with the provided
|
|
|
+// Tailscale IP.
|
|
|
+func peerWithTailscaleIP(st *ipnstate.Status, ip netaddr.IP) (ps *ipnstate.PeerStatus, ok bool) {
|
|
|
+ for _, ps := range st.Peer {
|
|
|
+ for _, ip2 := range ps.TailscaleIPs {
|
|
|
+ if ip == ip2 {
|
|
|
+ return ps, true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil, false
|
|
|
+}
|
|
|
+
|
|
|
+func isRemoteIP(st *ipnstate.Status, ip netaddr.IP) bool {
|
|
|
+ for _, selfIP := range st.TailscaleIPs {
|
|
|
+ if ip == selfIP {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// ClearExitNode sets the ExitNodeID and ExitNodeIP to their zero values.
|
|
|
+func (p *Prefs) ClearExitNode() {
|
|
|
+ p.ExitNodeID = ""
|
|
|
+ p.ExitNodeIP = netaddr.IP{}
|
|
|
+}
|
|
|
+
|
|
|
+// ExitNodeLocalIPError is returned when the requested IP address for an exit
|
|
|
+// node belongs to the local machine.
|
|
|
+type ExitNodeLocalIPError struct {
|
|
|
+ hostOrIP string
|
|
|
+}
|
|
|
+
|
|
|
+func (e ExitNodeLocalIPError) Error() string {
|
|
|
+ return fmt.Sprintf("cannot use %s as an exit node as it is a local IP address to this machine", e.hostOrIP)
|
|
|
+}
|
|
|
+
|
|
|
+func exitNodeIPOfArg(s string, st *ipnstate.Status) (ip netaddr.IP, err error) {
|
|
|
+ if s == "" {
|
|
|
+ return ip, os.ErrInvalid
|
|
|
+ }
|
|
|
+ ip, err = netaddr.ParseIP(s)
|
|
|
+ if err == nil {
|
|
|
+ // If we're online already and have a netmap, double check that the IP
|
|
|
+ // address specified is valid.
|
|
|
+ if st.BackendState == "Running" {
|
|
|
+ ps, ok := peerWithTailscaleIP(st, ip)
|
|
|
+ if !ok {
|
|
|
+ return ip, fmt.Errorf("no node found in netmap with IP %v", ip)
|
|
|
+ }
|
|
|
+ if !ps.ExitNodeOption {
|
|
|
+ return ip, fmt.Errorf("node %v is not advertising an exit node", ip)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if !isRemoteIP(st, ip) {
|
|
|
+ return ip, ExitNodeLocalIPError{s}
|
|
|
+ }
|
|
|
+ return ip, nil
|
|
|
+ }
|
|
|
+ match := 0
|
|
|
+ for _, ps := range st.Peer {
|
|
|
+ baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
|
|
+ if !strings.EqualFold(s, baseName) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ match++
|
|
|
+ if len(ps.TailscaleIPs) == 0 {
|
|
|
+ return ip, fmt.Errorf("node %q has no Tailscale IP?", s)
|
|
|
+ }
|
|
|
+ if !ps.ExitNodeOption {
|
|
|
+ return ip, fmt.Errorf("node %q is not advertising an exit node", s)
|
|
|
+ }
|
|
|
+ ip = ps.TailscaleIPs[0]
|
|
|
+ }
|
|
|
+ switch match {
|
|
|
+ case 0:
|
|
|
+ return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", s)
|
|
|
+ case 1:
|
|
|
+ if !isRemoteIP(st, ip) {
|
|
|
+ return ip, ExitNodeLocalIPError{s}
|
|
|
+ }
|
|
|
+ return ip, nil
|
|
|
+ default:
|
|
|
+ return ip, fmt.Errorf("ambiguous exit node name %q", s)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// SetExitNodeIP validates and sets the ExitNodeIP from a user-provided string
|
|
|
+// specifying either an IP address or a MagicDNS base name ("foo", as opposed to
|
|
|
+// "foo.bar.beta.tailscale.net"). This method does not mutate ExitNodeID and
|
|
|
+// will fail if ExitNodeID is already set.
|
|
|
+func (p *Prefs) SetExitNodeIP(s string, st *ipnstate.Status) error {
|
|
|
+ if !p.ExitNodeID.IsZero() {
|
|
|
+ return ErrExitNodeIDAlreadySet
|
|
|
+ }
|
|
|
+ ip, err := exitNodeIPOfArg(s, st)
|
|
|
+ if err == nil {
|
|
|
+ p.ExitNodeIP = ip
|
|
|
+ }
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
// PrefsFromBytes deserializes Prefs from a JSON blob. If
|
|
|
// enforceDefaults is true, Prefs.RouteAll and Prefs.AllowSingleHosts
|
|
|
// are forced on.
|