Compare commits
22 Commits
bradfitz/a
...
Xe/do-wind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85605128fb | ||
|
|
15784dd123 | ||
|
|
4cc84f62c6 | ||
|
|
a6a834b5fe | ||
|
|
8ec50db8ef | ||
|
|
305ae4d8ad | ||
|
|
187fc6f1c5 | ||
|
|
a905ce5607 | ||
|
|
359055d3fa | ||
|
|
b5628cee4e | ||
|
|
edf64e0901 | ||
|
|
ec77b80c53 | ||
|
|
b5b4992eff | ||
|
|
d3dd7c6270 | ||
|
|
187e22a756 | ||
|
|
ab9cccb292 | ||
|
|
78338ac029 | ||
|
|
b405644f5d | ||
|
|
5fe5402fcd | ||
|
|
e4c075cd95 | ||
|
|
edce91a8a6 | ||
|
|
51bd1feae4 |
62
api.md
62
api.md
@@ -5,7 +5,28 @@ The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JS
|
||||
# Authentication
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
|
||||
|
||||
# APIS
|
||||
# APIs
|
||||
|
||||
* **[Devices](#device)**
|
||||
- [GET device](#device-get)
|
||||
- [DELETE device](#device-delete)
|
||||
- Routes
|
||||
- [GET device routes](#device-routes-get)
|
||||
- [POST device routes](#device-routes-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [DNS](#tailnet-dns)
|
||||
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
|
||||
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
|
||||
- [GET tailnet DNS preferences](#tailnet-dns-preferences-get)
|
||||
- [POST tailnet DNS preferences](#tailnet-dns-preferences-post)
|
||||
- [GET tailnet DNS searchpaths](#tailnet-dns-searchpaths-get)
|
||||
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
|
||||
|
||||
## Device
|
||||
<!-- TODO: description about what devices are -->
|
||||
@@ -16,6 +37,8 @@ To find the deviceID of a particular device, you can use the ["GET /devices"](#g
|
||||
Find the device you're looking for and get the "id" field.
|
||||
This is your deviceID.
|
||||
|
||||
<a name=device-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
Returns the details for the specified device.
|
||||
Supply the device of interest in the path using its ID.
|
||||
@@ -103,6 +126,7 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-delete></div>
|
||||
|
||||
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
|
||||
Deletes the provided device from its tailnet.
|
||||
@@ -139,6 +163,8 @@ If the device is not owned by your tailnet:
|
||||
```
|
||||
|
||||
|
||||
<a name=device-routes-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
|
||||
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
|
||||
@@ -166,6 +192,8 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-routes-post></div>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
|
||||
|
||||
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
|
||||
@@ -210,7 +238,8 @@ A tailnet is the name of your Tailscale network.
|
||||
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
|
||||
|
||||
|
||||
"alice@example.com" belongs to the "example.com" tailnet and would use the following format for API calls:
|
||||
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
|
||||
@@ -218,21 +247,21 @@ curl https://api.tailscale.com/api/v2/tailnet/example.com/...
|
||||
|
||||
|
||||
For solo plans, the tailnet is the email you signed up with.
|
||||
So "alice@gmail.com" has the tailnet "alice@gmail.com" since @gmail.com is a shared email host.
|
||||
So `alice@gmail.com` has the tailnet `alice@gmail.com` since `@gmail.com` is a shared email host.
|
||||
Her API calls would have the following format:
|
||||
```
|
||||
GET /api/v2/tailnet/alice@gmail.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
|
||||
```
|
||||
|
||||
Tailnets are a top level resource. ACL is an example of a resource that is tied to a top level tailnet.
|
||||
|
||||
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
|
||||
|
||||
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
|
||||
|
||||
|
||||
### ACL
|
||||
|
||||
<a name=tailnet-acl-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/acl` - fetch ACL for a tailnet
|
||||
|
||||
Retrieves the ACL that is currently set for the given tailnet. Supply the tailnet of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
|
||||
@@ -334,6 +363,8 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl` - set ACL for a tailnet
|
||||
|
||||
Sets the ACL for the given tailnet. HuJSON and JSON are both accepted inputs. An `If-Match` header can be set to avoid missed updates.
|
||||
@@ -405,6 +436,8 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-preview-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/preview` - preview rule matches on an ACL for a resource
|
||||
Determines what rules match for a user on an ACL without saving the ACL to the server.
|
||||
|
||||
@@ -449,8 +482,12 @@ Response
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
|
||||
<a name=tailnet-devices-get></a>
|
||||
|
||||
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
|
||||
Lists the devices in a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -531,9 +568,12 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns></a>
|
||||
|
||||
### DNS
|
||||
|
||||
<a name=tailnet-dns-nameservers-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
|
||||
Lists the DNS nameservers for a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -556,6 +596,8 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-nameservers-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
|
||||
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -608,6 +650,8 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-preferences-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/preferences` - retrieves the DNS preferences for a tailnet
|
||||
Retrieves the DNS preferences that are currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -629,6 +673,8 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-preferences-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
|
||||
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
|
||||
Note that MagicDNS is dependent on DNS servers.
|
||||
@@ -673,6 +719,8 @@ If there are DNS servers:
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-searchpaths-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
|
||||
Retrieves the list of search paths that is currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
@@ -695,6 +743,8 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-searchpaths-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
|
||||
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.
|
||||
|
||||
|
||||
@@ -120,6 +120,11 @@ func checkIPForwarding() {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
@@ -139,6 +144,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
@@ -149,8 +155,18 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routes = append(routes, ipp)
|
||||
}
|
||||
if default4 && !default6 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
|
||||
|
||||
@@ -744,6 +744,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
}
|
||||
resp.Peers = filtered
|
||||
}
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
// We need at least one endpoint here for now else
|
||||
// other code doesn't even create the discoEndpoint.
|
||||
// TODO(bradfitz): fix that and then just nil this out.
|
||||
p.Endpoints = []string{"127.9.9.9:456"}
|
||||
}
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||
@@ -972,19 +980,21 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
|
||||
var Debug = initDebug()
|
||||
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
use := os.Getenv("TS_DEBUG_USE_DISCO")
|
||||
return debug{
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1065,6 +1075,24 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
|
||||
if mapRes.PeerSeenChange != nil {
|
||||
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
||||
for _, n := range newFull {
|
||||
peerByID[n.ID] = n
|
||||
}
|
||||
now := time.Now()
|
||||
for nodeID, seen := range mapRes.PeerSeenChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
if seen {
|
||||
n.LastSeen = &now
|
||||
} else {
|
||||
n.LastSeen = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
|
||||
@@ -312,12 +312,14 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Bits == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
logf("[v1] wgcfg: not accepting default route from %q (%v)",
|
||||
nodeDebugName(peer), peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)",
|
||||
allowedIP, nodeDebugName(peer), peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -328,6 +330,20 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func nodeDebugName(n *tailcfg.Node) string {
|
||||
name := n.Name
|
||||
if name == "" {
|
||||
name = n.Hostinfo.Hostname
|
||||
}
|
||||
if i := strings.Index(name, "."); i != -1 {
|
||||
name = name[:i]
|
||||
}
|
||||
if name == "" && len(n.Addresses) != 0 {
|
||||
return n.Addresses[0].String()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// cidrIsSubnet reports whether cidr is a non-default-route subnet
|
||||
// exported by node that is not one of its own self addresses.
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {
|
||||
|
||||
@@ -70,7 +70,7 @@ func Parse(p []byte) (Message, error) {
|
||||
case TypePong:
|
||||
return parsePong(ver, p)
|
||||
case TypeCallMeMaybe:
|
||||
return CallMeMaybe{}, nil
|
||||
return parseCallMeMaybe(ver, p)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
|
||||
}
|
||||
@@ -122,13 +122,57 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
|
||||
//
|
||||
// The recipient may choose to not open a path back, if it's already
|
||||
// happy with its path. But usually it will.
|
||||
type CallMeMaybe struct{}
|
||||
type CallMeMaybe struct {
|
||||
// MyNumber is what the peer believes its endpoints are.
|
||||
//
|
||||
// Prior to Tailscale 1.4, the endpoints were exchanged purely
|
||||
// between nodes and the control server.
|
||||
//
|
||||
// Starting with Tailscale 1.4, clients advertise their endpoints.
|
||||
// Older clients won't use this, but newer clients should
|
||||
// use any endpoints in here that aren't included from control.
|
||||
//
|
||||
// Control might have sent stale endpoints if the client was idle
|
||||
// before contacting us. In that case, the client likely did a STUN
|
||||
// request immediately before sending the CallMeMaybe to recreate
|
||||
// their NAT port mapping, and that new good endpoint is included
|
||||
// in this field, but might not yet be in control's endpoints.
|
||||
// (And in the future, control will stop distributing endpoints
|
||||
// when clients are suitably new.)
|
||||
MyNumber []netaddr.IPPort
|
||||
}
|
||||
|
||||
func (CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0)
|
||||
const epLength = 16 + 2 // 16 byte IP address + 2 byte port
|
||||
|
||||
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
|
||||
for _, ipp := range m.MyNumber {
|
||||
a := ipp.IP.As16()
|
||||
copy(p[:], a[:])
|
||||
binary.BigEndian.PutUint16(p[16:], ipp.Port)
|
||||
p = p[epLength:]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
|
||||
m = new(CallMeMaybe)
|
||||
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
m.MyNumber = make([]netaddr.IPPort, 0, len(p)/epLength)
|
||||
for len(p) > 0 {
|
||||
var a [16]byte
|
||||
copy(a[:], p)
|
||||
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
|
||||
IP: netaddr.IPFrom16(a),
|
||||
Port: binary.BigEndian.Uint16(p[16:18]),
|
||||
})
|
||||
p = p[epLength:]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Pong is a response a Ping.
|
||||
//
|
||||
// It includes the sender's source IP + port, so it's effectively a
|
||||
@@ -171,7 +215,7 @@ func MessageSummary(m Message) string {
|
||||
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
|
||||
case *Pong:
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case CallMeMaybe:
|
||||
case *CallMeMaybe:
|
||||
return "call-me-maybe"
|
||||
default:
|
||||
return fmt.Sprintf("%#v", m)
|
||||
|
||||
@@ -44,9 +44,19 @@ func TestMarshalAndParse(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe",
|
||||
m: CallMeMaybe{},
|
||||
m: &CallMeMaybe{},
|
||||
want: "03 00",
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe_endpoints",
|
||||
m: &CallMeMaybe{
|
||||
MyNumber: []netaddr.IPPort{
|
||||
netaddr.MustParseIPPort("1.2.3.4:567"),
|
||||
netaddr.MustParseIPPort("[2001::3456]:789"),
|
||||
},
|
||||
},
|
||||
want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -429,6 +429,10 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
return fmt.Errorf("loading requested state: %v", err)
|
||||
}
|
||||
|
||||
if b.prefs.LogServer == "" {
|
||||
b.prefs.LogServer = "https://log.tailscale.io"
|
||||
}
|
||||
os.Setenv("TAILSCALE_LOG_TARGET", b.prefs.LogServer)
|
||||
b.inServerMode = b.prefs.ForceDaemon
|
||||
b.serverURL = b.prefs.ControlURL
|
||||
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
|
||||
|
||||
@@ -124,6 +124,10 @@ type Prefs struct {
|
||||
// We can maybe do that once we're sure which module should persist
|
||||
// it (backend or frontend?)
|
||||
Persist *controlclient.Persist `json:"Config"`
|
||||
|
||||
// The target server where logs should be sent. If not set this will
|
||||
// default to https://log.tailscale.io.
|
||||
LogServer string
|
||||
}
|
||||
|
||||
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
|
||||
|
||||
@@ -48,4 +48,5 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
NoSNAT bool
|
||||
NetfilterMode router.NetfilterMode
|
||||
Persist *controlclient.Persist
|
||||
LogServer string
|
||||
}{})
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -387,6 +388,13 @@ func New(collection string) *Policy {
|
||||
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
|
||||
}
|
||||
|
||||
if val, ok := os.LookupEnv("TAILSCALE_LOG_TARGET"); ok {
|
||||
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
|
||||
c.BaseURL = val
|
||||
u, _ := url.Parse(val)
|
||||
c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)}
|
||||
}
|
||||
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
|
||||
@@ -32,7 +32,8 @@ import (
|
||||
// 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
|
||||
// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side
|
||||
// 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains
|
||||
const CurrentMapRequestVersion = 9
|
||||
// 10: 2021-01-17: client understands MapResponse.PeerSeenChange
|
||||
const CurrentMapRequestVersion = 10
|
||||
|
||||
type ID int64
|
||||
|
||||
@@ -636,6 +637,11 @@ type MapResponse struct {
|
||||
// PeersRemoved are the NodeIDs that are no longer in the peer list.
|
||||
PeersRemoved []NodeID `json:",omitempty"`
|
||||
|
||||
// PeerSeenChange contains information on how to update peers' LastSeen
|
||||
// times. If the value is false, the peer is gone. If the value is true,
|
||||
// the LastSeen time is now. Absent means unchanged.
|
||||
PeerSeenChange map[NodeID]bool `json:",omitempty"`
|
||||
|
||||
// DNS is the same as DNSConfig.Nameservers.
|
||||
//
|
||||
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
|
||||
|
||||
@@ -132,7 +132,7 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
|
||||
logf(format, args...)
|
||||
case warn:
|
||||
// For the warning, log the specific format string
|
||||
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, fmt.Sprintf(format, args...))
|
||||
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, strings.TrimSpace(fmt.Sprintf(format, args...)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,8 +438,7 @@ func (a *addrSet) DstToBytes() []byte {
|
||||
return packIPPort(a.dst())
|
||||
}
|
||||
func (a *addrSet) DstToString() string {
|
||||
dst := a.dst()
|
||||
return dst.String()
|
||||
return a.Addrs()
|
||||
}
|
||||
func (a *addrSet) DstIP() net.IP {
|
||||
return a.dst().IP.IPAddr().IP // TODO: add netaddr accessor to cut an alloc here?
|
||||
|
||||
@@ -161,6 +161,11 @@ type Conn struct {
|
||||
// whether a DERP channel read should be done.
|
||||
derpRecvCountLast int64 // owned by ReceiveIPv4
|
||||
|
||||
// ippEndpoint4 and ippEndpoint6 are owned by ReceiveIPv4 and
|
||||
// ReceiveIPv6, respectively, to cache an IPPort->endpoint for
|
||||
// hot flows.
|
||||
ippEndpoint4, ippEndpoint6 ippEndpointCache
|
||||
|
||||
// ============================================================
|
||||
mu sync.Mutex // guards all following fields; see userspaceEngine lock ordering rules
|
||||
muCond *sync.Cond
|
||||
@@ -168,6 +173,15 @@ type Conn struct {
|
||||
started bool // Start was called
|
||||
closed bool // Close was called
|
||||
|
||||
// derpCleanupTimer is the timer that fires to occasionally clean
|
||||
// up idle DERP connections. It's only used when there is a non-home
|
||||
// DERP connection in use.
|
||||
derpCleanupTimer *time.Timer
|
||||
|
||||
// derpCleanupTimerArmed is whether derpCleanupTimer is
|
||||
// scheduled to fire within derpCleanStaleInterval.
|
||||
derpCleanupTimerArmed bool
|
||||
|
||||
// endpointsUpdateActive indicates that updateEndpoints is
|
||||
// currently running. It's used to deduplicate concurrent endpoint
|
||||
// update requests.
|
||||
@@ -483,7 +497,6 @@ func (c *Conn) Start() {
|
||||
if !version.IsMobile() {
|
||||
go c.periodicReSTUN()
|
||||
}
|
||||
go c.periodicDerpCleanup()
|
||||
}
|
||||
|
||||
// ignoreSTUNPackets sets a STUN packet processing func that does nothing.
|
||||
@@ -1214,6 +1227,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
|
||||
c.activeDerp[regionID] = ad
|
||||
c.logActiveDerpLocked()
|
||||
c.setPeerLastDerpLocked(peer, regionID, regionID)
|
||||
c.scheduleCleanStaleDerpLocked()
|
||||
|
||||
// Build a startGate for the derp reader+writer
|
||||
// goroutines, so they don't start running until any
|
||||
@@ -1483,7 +1497,7 @@ func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, error) {
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr)); ok {
|
||||
if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr), &c.ippEndpoint6); ok {
|
||||
return n, ep, nil
|
||||
}
|
||||
}
|
||||
@@ -1520,28 +1534,37 @@ func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, err error) {
|
||||
}
|
||||
return 0, nil, err
|
||||
}
|
||||
if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr)); ok {
|
||||
if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr), &c.ippEndpoint4); ok {
|
||||
return n, ep, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// receiveIP is the shared bits of ReceiveIPv4 and ReceiveIPv6.
|
||||
func (c *Conn) receiveIP(b []byte, ua *net.UDPAddr) (ep conn.Endpoint, ok bool) {
|
||||
func (c *Conn) receiveIP(b []byte, ua *net.UDPAddr, cache *ippEndpointCache) (ep conn.Endpoint, ok bool) {
|
||||
ipp, ok := netaddr.FromStdAddr(ua.IP, ua.Port, ua.Zone)
|
||||
if !ok {
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
if stun.Is(b) {
|
||||
c.stunReceiveFunc.Load().(func([]byte, netaddr.IPPort))(b, ipp)
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
if c.handleDiscoMessage(b, ipp) {
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
ep = c.findEndpoint(ipp, ua, b)
|
||||
if ep == nil {
|
||||
return
|
||||
if cache.ipp == ipp && cache.de != nil && cache.gen == cache.de.numStopAndReset() {
|
||||
ep = cache.de
|
||||
} else {
|
||||
ep = c.findEndpoint(ipp, ua, b)
|
||||
if ep == nil {
|
||||
return nil, false
|
||||
}
|
||||
if de, ok := ep.(*discoEndpoint); ok {
|
||||
cache.ipp = ipp
|
||||
cache.de = de
|
||||
cache.gen = de.numStopAndReset()
|
||||
}
|
||||
}
|
||||
c.noteRecvActivityFromEndpoint(ep)
|
||||
return ep, true
|
||||
@@ -1608,7 +1631,7 @@ func (c *Conn) receiveIPv4DERP(b []byte) (n int, ep conn.Endpoint, err error) {
|
||||
c.mu.Lock()
|
||||
|
||||
discoEp = c.endpointOfDisco[dk]
|
||||
c.logf("magicsock: DERP packet received from idle peer %v; created=%v", dm.src.ShortString(), ep != nil)
|
||||
c.logf("magicsock: DERP packet received from idle peer %v; created=%v", dm.src.ShortString(), discoEp != nil)
|
||||
}
|
||||
}
|
||||
if !c.disableLegacy {
|
||||
@@ -1820,7 +1843,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
|
||||
return true
|
||||
}
|
||||
de.handlePongConnLocked(dm, src)
|
||||
case disco.CallMeMaybe:
|
||||
case *disco.CallMeMaybe:
|
||||
if src.IP != derpMagicIPAddr {
|
||||
// CallMeMaybe messages should only come via DERP.
|
||||
c.logf("[unexpected] CallMeMaybe packets should only come via DERP")
|
||||
@@ -1828,7 +1851,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
|
||||
}
|
||||
if de != nil {
|
||||
c.logf("magicsock: disco: %v<-%v (%v, %v) got call-me-maybe", c.discoShort, de.discoShort, de.publicKey.ShortString(), derpStr(src.String()))
|
||||
go de.handleCallMeMaybe()
|
||||
go de.handleCallMeMaybe(dm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1859,6 +1882,29 @@ func (c *Conn) handlePingLocked(dm *disco.Ping, de *discoEndpoint, src netaddr.I
|
||||
}, discoVerboseLog)
|
||||
}
|
||||
|
||||
// enqueueCallMeMaybe schedules a send of disco.CallMeMaybe to de via derpAddr
|
||||
// once we know that our STUN endpoint is fresh.
|
||||
//
|
||||
// derpAddr is de.derpAddr at the time of send. It's assumed the peer won't be
|
||||
// flipping primary DERPs in the 0-30ms it takes to confirm our STUN endpoint.
|
||||
// If they do, traffic will just go over DERP for a bit longer until the next
|
||||
// discovery round.
|
||||
func (c *Conn) enqueueCallMeMaybe(derpAddr netaddr.IPPort, de *discoEndpoint) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// TODO(bradfitz): do a fast/lite re-STUN if our STUN info is too old
|
||||
// Currently there's no "queue" despite the method name. There will be.
|
||||
|
||||
eps := make([]netaddr.IPPort, 0, len(c.lastEndpoints))
|
||||
for _, ep := range c.lastEndpoints {
|
||||
if ipp, err := netaddr.ParseIPPort(ep); err == nil {
|
||||
eps = append(eps, ipp)
|
||||
}
|
||||
}
|
||||
go de.sendDiscoMessage(derpAddr, &disco.CallMeMaybe{MyNumber: eps}, discoLog)
|
||||
}
|
||||
|
||||
// setAddrToDiscoLocked records that newk is at src.
|
||||
//
|
||||
// c.mu must be held.
|
||||
@@ -2185,9 +2231,14 @@ func (c *Conn) foreachActiveDerpSortedLocked(fn func(regionID int, ad activeDerp
|
||||
func (c *Conn) cleanStaleDerp() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
const inactivityTime = 60 * time.Second
|
||||
tooOld := time.Now().Add(-inactivityTime)
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
c.derpCleanupTimerArmed = false
|
||||
|
||||
tooOld := time.Now().Add(-derpInactiveCleanupTime)
|
||||
dirty := false
|
||||
someNonHomeOpen := false
|
||||
for i, ad := range c.activeDerp {
|
||||
if i == c.myDerp {
|
||||
continue
|
||||
@@ -2195,11 +2246,31 @@ func (c *Conn) cleanStaleDerp() {
|
||||
if ad.lastWrite.Before(tooOld) {
|
||||
c.closeDerpLocked(i, "idle")
|
||||
dirty = true
|
||||
} else {
|
||||
someNonHomeOpen = true
|
||||
}
|
||||
}
|
||||
if dirty {
|
||||
c.logActiveDerpLocked()
|
||||
}
|
||||
if someNonHomeOpen {
|
||||
c.scheduleCleanStaleDerpLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) scheduleCleanStaleDerpLocked() {
|
||||
if c.derpCleanupTimerArmed {
|
||||
// Already going to fire soon. Let the existing one
|
||||
// fire lest it get infinitely delayed by repeated
|
||||
// calls to scheduleCleanStaleDerpLocked.
|
||||
return
|
||||
}
|
||||
c.derpCleanupTimerArmed = true
|
||||
if c.derpCleanupTimer != nil {
|
||||
c.derpCleanupTimer.Reset(derpCleanStaleInterval)
|
||||
} else {
|
||||
c.derpCleanupTimer = time.AfterFunc(derpCleanStaleInterval, c.cleanStaleDerp)
|
||||
}
|
||||
}
|
||||
|
||||
// DERPs reports the number of active DERP connections.
|
||||
@@ -2222,6 +2293,9 @@ func (c *Conn) Close() error {
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
if c.derpCleanupTimerArmed {
|
||||
c.derpCleanupTimer.Stop()
|
||||
}
|
||||
|
||||
for _, ep := range c.endpointOfDisco {
|
||||
ep.stopAndReset()
|
||||
@@ -2245,13 +2319,6 @@ func (c *Conn) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// isClosed reports whether c is closed.
|
||||
func (c *Conn) isClosed() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.closed
|
||||
}
|
||||
|
||||
func (c *Conn) goroutinesRunningLocked() bool {
|
||||
if c.endpointsUpdateActive {
|
||||
return true
|
||||
@@ -2344,19 +2411,6 @@ func (c *Conn) periodicReSTUN() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) periodicDerpCleanup() {
|
||||
ticker := time.NewTicker(15 * time.Second) // arbitrary
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.donec:
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.cleanStaleDerp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReSTUN triggers an address discovery.
|
||||
// The provided why string is for debug logging only.
|
||||
func (c *Conn) ReSTUN(why string) {
|
||||
@@ -2781,7 +2835,8 @@ func udpAddrDebugString(ua net.UDPAddr) string {
|
||||
// advertise a DiscoKey and participate in active discovery.
|
||||
type discoEndpoint struct {
|
||||
// atomically accessed; declared first for alignment reasons
|
||||
lastRecvUnixAtomic int64
|
||||
lastRecvUnixAtomic int64
|
||||
numStopAndResetAtomic int64
|
||||
|
||||
// These fields are initialized once and never modified.
|
||||
c *Conn
|
||||
@@ -2809,6 +2864,7 @@ type discoEndpoint struct {
|
||||
trustBestAddrUntil time.Time // time when bestAddr expires
|
||||
sentPing map[stun.TxID]sentPing
|
||||
endpointState map[netaddr.IPPort]*endpointState
|
||||
isCallMeMaybeEP map[netaddr.IPPort]bool
|
||||
|
||||
pendingCLIPings []pendingCLIPing // any outstanding "tailscale ping" commands running
|
||||
}
|
||||
@@ -2848,6 +2904,14 @@ const (
|
||||
// goodEnoughLatency is the latency at or under which we don't
|
||||
// try to upgrade to a better path.
|
||||
goodEnoughLatency = 5 * time.Millisecond
|
||||
|
||||
// derpInactiveCleanupTime is how long a non-home DERP connection
|
||||
// needs to be idle (last written to) before we close it.
|
||||
derpInactiveCleanupTime = 60 * time.Second
|
||||
|
||||
// derpCleanStaleInterval is how often cleanStaleDerp runs when there
|
||||
// are potentially-stale DERP connections to close.
|
||||
derpCleanStaleInterval = 15 * time.Second
|
||||
)
|
||||
|
||||
// endpointState is some state and history for a specific endpoint of
|
||||
@@ -2865,6 +2929,10 @@ type endpointState struct {
|
||||
// updated and use it to discard old candidates.
|
||||
lastGotPing time.Time
|
||||
|
||||
// callMeMaybeTime, if non-zero, is the time this endpoint
|
||||
// was advertised last via a call-me-maybe disco message.
|
||||
callMeMaybeTime time.Time
|
||||
|
||||
recentPongs []pongReply // ring buffer up to pongHistoryCount entries
|
||||
recentPong uint16 // index into recentPongs of most recent; older before, wrapped
|
||||
|
||||
@@ -2878,11 +2946,13 @@ const indexSentinelDeleted = -1
|
||||
// shouldDeleteLocked reports whether we should delete this endpoint.
|
||||
func (st *endpointState) shouldDeleteLocked() bool {
|
||||
switch {
|
||||
case !st.callMeMaybeTime.IsZero():
|
||||
return false
|
||||
case st.lastGotPing.IsZero():
|
||||
// This was an endpoint from the network map. Is it still in the network map?
|
||||
return st.index == indexSentinelDeleted
|
||||
default:
|
||||
// Thiw was an endpoint discovered at runtime.
|
||||
// This was an endpoint discovered at runtime.
|
||||
return time.Since(st.lastGotPing) > sessionActiveTimeout
|
||||
}
|
||||
}
|
||||
@@ -2982,10 +3052,6 @@ func (de *discoEndpoint) heartbeat() {
|
||||
|
||||
de.heartBeatTimer = nil
|
||||
|
||||
if de.c.isClosed() {
|
||||
return
|
||||
}
|
||||
|
||||
if de.lastSend.IsZero() {
|
||||
// Shouldn't happen.
|
||||
return
|
||||
@@ -3200,13 +3266,12 @@ func (de *discoEndpoint) sendPingsLocked(now time.Time, sendCallMeMaybe bool) {
|
||||
}
|
||||
derpAddr := de.derpAddr
|
||||
if sentAny && sendCallMeMaybe && !derpAddr.IsZero() {
|
||||
// In just a bit of a time (for goroutines above to schedule and run),
|
||||
// send a message to peer via DERP informing them that we've sent
|
||||
// so our firewall ports are probably open and now would be a good time
|
||||
// for them to connect.
|
||||
time.AfterFunc(5*time.Millisecond, func() {
|
||||
de.sendDiscoMessage(derpAddr, disco.CallMeMaybe{}, discoLog)
|
||||
})
|
||||
// Have our magicsock.Conn figure out its STUN endpoint (if
|
||||
// it doesn't know already) and then send a CallMeMaybe
|
||||
// message to our peer via DERP informing them that we've
|
||||
// sent so our firewall ports are probably open and now
|
||||
// would be a good time for them to connect.
|
||||
go de.c.enqueueCallMeMaybe(derpAddr, de)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3388,10 +3453,34 @@ func (st *endpointState) addPongReplyLocked(r pongReply) {
|
||||
// DERP. The contract for use of this message is that the peer has
|
||||
// already sent to us via UDP, so their stateful firewall should be
|
||||
// open. Now we can Ping back and make it through.
|
||||
func (de *discoEndpoint) handleCallMeMaybe() {
|
||||
func (de *discoEndpoint) handleCallMeMaybe(m *disco.CallMeMaybe) {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for ep := range de.isCallMeMaybeEP {
|
||||
de.isCallMeMaybeEP[ep] = false // mark for deletion
|
||||
}
|
||||
if de.isCallMeMaybeEP == nil {
|
||||
de.isCallMeMaybeEP = map[netaddr.IPPort]bool{}
|
||||
}
|
||||
for _, ep := range m.MyNumber {
|
||||
de.isCallMeMaybeEP[ep] = true
|
||||
if es, ok := de.endpointState[ep]; ok {
|
||||
es.callMeMaybeTime = now
|
||||
} else {
|
||||
de.endpointState[ep] = &endpointState{callMeMaybeTime: now}
|
||||
}
|
||||
}
|
||||
// Delete any prior CalllMeMaybe endpoints that weren't included
|
||||
// in this message.
|
||||
for ep, want := range de.isCallMeMaybeEP {
|
||||
if !want {
|
||||
delete(de.isCallMeMaybeEP, ep)
|
||||
de.deleteEndpointLocked(ep)
|
||||
}
|
||||
}
|
||||
|
||||
// Zero out all the lastPing times to force sendPingsLocked to send new ones,
|
||||
// even if it's been less than 5 seconds ago.
|
||||
for _, st := range de.endpointState {
|
||||
@@ -3420,6 +3509,7 @@ func (de *discoEndpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
// It's called when a discovery endpoint is no longer present in the NetworkMap,
|
||||
// or when magicsock is transition from running to stopped state (via SetPrivateKey(zero))
|
||||
func (de *discoEndpoint) stopAndReset() {
|
||||
atomic.AddInt64(&de.numStopAndResetAtomic, 1)
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
@@ -3447,6 +3537,10 @@ func (de *discoEndpoint) stopAndReset() {
|
||||
de.pendingCLIPings = nil
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) numStopAndReset() int64 {
|
||||
return atomic.LoadInt64(&de.numStopAndResetAtomic)
|
||||
}
|
||||
|
||||
// derpStr replaces DERP IPs in s with "derp-".
|
||||
func derpStr(s string) string { return strings.ReplaceAll(s, "127.3.3.40:", "derp-") }
|
||||
|
||||
@@ -3468,3 +3562,11 @@ func (c *Conn) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok
|
||||
}
|
||||
return nil, u, false
|
||||
}
|
||||
|
||||
// ippEndpointCache is a mutex-free single-element cache, mapping from
|
||||
// a single netaddr.IPPort to a single endpoint.
|
||||
type ippEndpointCache struct {
|
||||
ipp netaddr.IPPort
|
||||
gen int64
|
||||
de *discoEndpoint
|
||||
}
|
||||
|
||||
@@ -896,7 +896,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
|
||||
// This gets reassigned inside every test, so that the connections
|
||||
// all log using the "current" t.Logf function. Sigh.
|
||||
logf, setT := makeNestable(t)
|
||||
nestedLogf, setT := makeNestable(t)
|
||||
|
||||
logf, closeLogf := logger.LogfCloser(nestedLogf)
|
||||
defer closeLogf()
|
||||
|
||||
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
|
||||
defer cleanup()
|
||||
|
||||
Reference in New Issue
Block a user