Compare commits
1 Commits
vm
...
walterp-ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c92913ef |
2
.github/workflows/cifuzz.yml
vendored
2
.github/workflows/cifuzz.yml
vendored
@@ -2,7 +2,7 @@ name: CIFuzz
|
||||
on: [pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
4
.github/workflows/vm.yml
vendored
4
.github/workflows/vm.yml
vendored
@@ -11,7 +11,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
ubuntu2004-LTS-cloud-base:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: [ self-hosted, linux, vm ]
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
env:
|
||||
HOME: "/tmp"
|
||||
TMPDIR: "/tmp"
|
||||
XDG_CACHE_HOME: "$HOME/.cache"
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
132
api.md
132
api.md
@@ -3,11 +3,12 @@
|
||||
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
|
||||
|
||||
# Authentication
|
||||
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://login.tailscale.com/admin) and navigate to the `Settings` 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 (leave the password blank).
|
||||
|
||||
# APIs
|
||||
|
||||
* **[Devices](#device)**
|
||||
- **[Devices](#device)**
|
||||
- [GET device](#device-get)
|
||||
- [DELETE device](#device-delete)
|
||||
- Routes
|
||||
@@ -19,12 +20,12 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [POST device tags](#device-tags-post)
|
||||
- Key
|
||||
- [POST device key](#device-key-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post)
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post)
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post)
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post)
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [Keys](#tailnet-keys)
|
||||
@@ -41,7 +42,9 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
|
||||
|
||||
## Device
|
||||
|
||||
<!-- TODO: description about what devices are -->
|
||||
|
||||
Each Tailscale-connected device has a globally-unique identifier number which we refer as the "deviceID" or sometimes, just "id".
|
||||
You can use the deviceID to specify operations on a specific device, like retrieving its subnet routes.
|
||||
|
||||
@@ -52,20 +55,23 @@ This is your deviceID.
|
||||
<a name=device-get></a>
|
||||
|
||||
#### `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.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
* `all`: returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
- `all`: returns all fields in the response.
|
||||
- `default`: return all fields except:
|
||||
- `enabledRoutes`
|
||||
- `advertisedRoutes`
|
||||
- `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
@@ -73,6 +79,7 @@ For example, for `fields=default,all`, all fields are returned.
|
||||
If the `fields` parameter is not provided, then the default option is used.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/device/12345
|
||||
curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
|
||||
@@ -80,6 +87,7 @@ curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"addresses":[
|
||||
@@ -141,16 +149,18 @@ Response
|
||||
<a name=device-delete></a>
|
||||
|
||||
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
|
||||
|
||||
Deletes the provided device from its tailnet.
|
||||
The device must belong to the user's tailnet.
|
||||
Deleting shared/external devices is not supported.
|
||||
Supply the device of interest in the path using its ID.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
DELETE /api/v2/device/12345
|
||||
curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
|
||||
@@ -160,6 +170,7 @@ curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
|
||||
Response
|
||||
|
||||
If successful, the response should be empty:
|
||||
|
||||
```
|
||||
< HTTP/1.1 200 OK
|
||||
...
|
||||
@@ -168,13 +179,13 @@ If successful, the response should be empty:
|
||||
```
|
||||
|
||||
If the device is not owned by your tailnet:
|
||||
|
||||
```
|
||||
< HTTP/1.1 501 Not Implemented
|
||||
...
|
||||
{"message":"cannot delete devices outside of your tailnet"}
|
||||
```
|
||||
|
||||
|
||||
<a name=device-routes-get></a>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
@@ -193,6 +204,7 @@ curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
@@ -213,7 +225,9 @@ Sets which subnet routes are enabled to be routed by a device by replacing the e
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`routes` - The new list of enabled subnet routes in JSON.
|
||||
|
||||
```
|
||||
{
|
||||
"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]
|
||||
@@ -254,7 +268,9 @@ Marks a device as authorized, for Tailnets where device authorization is require
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`authorized` - whether the device is authorized; only `true` is currently supported.
|
||||
|
||||
```
|
||||
{
|
||||
"authorized": true
|
||||
@@ -335,9 +351,9 @@ The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
## Tailnet
|
||||
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.
|
||||
|
||||
A tailnet is the name of your Tailscale network.
|
||||
You can find it in the [Settings](https://login.tailscale.com/admin/settings/) page of the admin console.
|
||||
|
||||
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
|
||||
|
||||
@@ -346,10 +362,10 @@ GET /api/v2/tailnet/example.com/...
|
||||
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.
|
||||
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/...
|
||||
@@ -370,15 +386,19 @@ Retrieves the ACL that is currently set for the given tailnet. Supply the tailne
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
|
||||
`Accept` - Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns the ACL HuJSON by default. Returns a parsed JSON of the ACL (sans comments) if the `Accept` type is explicitly set to `application/json`. An `ETag` header is also sent in the response, which can be optionally used in POST requests to avoid missed updates.
|
||||
|
||||
<!-- TODO (chungdaniel): define error types and a set of docs for them -->
|
||||
|
||||
##### Example
|
||||
|
||||
###### Requesting a HuJSON response:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
@@ -388,6 +408,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
...
|
||||
Content-Type: application/hujson
|
||||
@@ -426,6 +447,7 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
```
|
||||
|
||||
###### Requesting a JSON response:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
@@ -435,6 +457,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
...
|
||||
Content-Type: application/json
|
||||
@@ -477,6 +500,7 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
|
||||
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
|
||||
|
||||
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
@@ -486,15 +510,16 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
|
||||
The POST body should be a JSON or [HuJSON](https://github.com/tailscale/hujson#hujson---human-json) formatted JSON object.
|
||||
An ACL policy may contain the following top-level properties:
|
||||
|
||||
* `Groups` - Static groups of users which can be used for ACL rules.
|
||||
* `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
|
||||
* `ACLs` - Access control lists.
|
||||
* `TagOwners` - Defines who is allowed to use which tags.
|
||||
* `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
|
||||
- `Groups` - Static groups of users which can be used for ACL rules.
|
||||
- `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
|
||||
- `ACLs` - Access control lists.
|
||||
- `TagOwners` - Defines who is allowed to use which tags.
|
||||
- `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
|
||||
|
||||
See https://tailscale.com/kb/1018/acls for more information on those properties.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
@@ -524,6 +549,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
@@ -549,6 +575,7 @@ Response:
|
||||
```
|
||||
|
||||
Failed test error response:
|
||||
|
||||
```
|
||||
{
|
||||
"message": "test(s) failed",
|
||||
@@ -572,14 +599,17 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
|
||||
`type` - can be 'user' or 'ipport'
|
||||
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
|
||||
The provided ACL is queried with this parameter to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFor=user1@example.com&type=user' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
@@ -607,6 +637,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFo
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
@@ -631,6 +662,7 @@ The POST body should be a JSON formatted array of ACL Tests.
|
||||
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
|
||||
|
||||
##### Example with tests
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/validate
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
@@ -642,6 +674,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
```
|
||||
|
||||
##### Example with an ACL body
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/validate
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
@@ -684,21 +717,23 @@ An empty body or a JSON object with no `message` is returned on success.
|
||||
<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.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
* `all`: Returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
- `all`: Returns all fields in the response.
|
||||
- `default`: return all fields except:
|
||||
- `enabledRoutes`
|
||||
- `advertisedRoutes`
|
||||
- `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
@@ -714,6 +749,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/devices' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"devices":[
|
||||
@@ -776,6 +812,7 @@ for the user who owns the API key used to perform this query.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
@@ -792,6 +829,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{"keys": [
|
||||
{"id": "kYKVU14CNTRL"},
|
||||
@@ -812,7 +850,9 @@ Supply the tailnet in the path.
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`capabilities` - A mapping of resources to permissible actions.
|
||||
|
||||
```
|
||||
{
|
||||
"capabilities": {
|
||||
@@ -859,6 +899,7 @@ echo '{
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
@@ -877,6 +918,7 @@ Returns a JSON object with information about specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
@@ -893,6 +935,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
@@ -922,9 +965,11 @@ Deletes a specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
|
||||
This reports status 200 upon success.
|
||||
|
||||
##### Example
|
||||
@@ -941,10 +986,12 @@ curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k12345
|
||||
<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.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
@@ -956,6 +1003,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"dns": ["8.8.8.8"],
|
||||
@@ -965,13 +1013,17 @@ 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.
|
||||
Note that changing the list of DNS nameservers may also affect the status of MagicDNS (if MagicDNS is on).
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`dns` - The new list of DNS nameservers in JSON.
|
||||
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"]
|
||||
@@ -979,12 +1031,15 @@ Note that changing the list of DNS nameservers may also affect the status of Mag
|
||||
```
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns the new list of nameservers and the status of MagicDNS.
|
||||
|
||||
If all nameservers have been removed, MagicDNS will be automatically disabled (until explicitly turned back on by the user).
|
||||
|
||||
##### Example
|
||||
|
||||
###### Adding DNS nameservers with the MagicDNS on:
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
@@ -993,6 +1048,7 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameserve
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"],
|
||||
@@ -1001,6 +1057,7 @@ Response:
|
||||
```
|
||||
|
||||
###### Removing all DNS nameservers with the MagicDNS on:
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
@@ -1009,6 +1066,7 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameserve
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"dns":[],
|
||||
@@ -1019,13 +1077,16 @@ 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.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/preferences
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
@@ -1033,6 +1094,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"magicDNS":false,
|
||||
@@ -1042,6 +1104,7 @@ 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.
|
||||
|
||||
@@ -1051,9 +1114,12 @@ Note that removing all nameservers will turn off MagicDNS.
|
||||
To reenable it, nameservers must be added back, and MagicDNS must be explicitly turned on.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
|
||||
`magicDNS` - Automatically registers DNS names for devices in your tailnet.
|
||||
`magicDNS` - Automatically registers DNS names for devices in your tailnet.
|
||||
|
||||
```
|
||||
{
|
||||
"magicDNS": true
|
||||
@@ -1061,6 +1127,7 @@ The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/preferences
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
@@ -1068,10 +1135,10 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferenc
|
||||
--data-binary '{"magicDNS": true}'
|
||||
```
|
||||
|
||||
|
||||
Response:
|
||||
|
||||
If there are no DNS servers, it returns an error message:
|
||||
|
||||
```
|
||||
{
|
||||
"message":"need at least one nameserver to enable MagicDNS"
|
||||
@@ -1079,6 +1146,7 @@ If there are no DNS servers, it returns an error message:
|
||||
```
|
||||
|
||||
If there are DNS servers:
|
||||
|
||||
```
|
||||
{
|
||||
"magicDNS":true,
|
||||
@@ -1088,14 +1156,16 @@ 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.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
@@ -1103,6 +1173,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com"],
|
||||
@@ -1112,12 +1183,15 @@ 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.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`searchPaths` - A list of searchpaths in JSON.
|
||||
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"]
|
||||
@@ -1125,6 +1199,7 @@ Replaces the list of searchpaths with the list supplied by the user and returns
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
@@ -1133,6 +1208,7 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpat
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"],
|
||||
|
||||
@@ -24,17 +24,11 @@ func New(socket string) (*BIRDClient, error) {
|
||||
return newWithTimeout(socket, responseTimeout)
|
||||
}
|
||||
|
||||
func newWithTimeout(socket string, timeout time.Duration) (_ *BIRDClient, err error) {
|
||||
func newWithTimeout(socket string, timeout time.Duration) (*BIRDClient, error) {
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
b := &BIRDClient{
|
||||
socket: socket,
|
||||
conn: conn,
|
||||
|
||||
@@ -267,47 +267,15 @@ func (lc *LocalClient) Profile(ctx context.Context, pprofType string, sec int) (
|
||||
return lc.get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
|
||||
}
|
||||
|
||||
// BugReportOpts contains options to pass to the Tailscale daemon when
|
||||
// generating a bug report.
|
||||
type BugReportOpts struct {
|
||||
// Note contains an optional user-provided note to add to the logs.
|
||||
Note string
|
||||
|
||||
// Diagnose specifies whether to print additional diagnostic information to
|
||||
// the logs when generating this bugreport.
|
||||
Diagnose bool
|
||||
}
|
||||
|
||||
// BugReportWithOpts logs and returns a log marker that can be shared by the
|
||||
// user with support.
|
||||
//
|
||||
// The opts type specifies options to pass to the Tailscale daemon when
|
||||
// generating this bug report.
|
||||
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
|
||||
var qparams url.Values
|
||||
if opts.Note != "" {
|
||||
qparams.Set("note", opts.Note)
|
||||
}
|
||||
if opts.Diagnose {
|
||||
qparams.Set("diagnose", "true")
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
|
||||
body, err := lc.send(ctx, "POST", uri, 200, nil)
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
//
|
||||
// This is the same as calling BugReportWithOpts and only specifying the Note
|
||||
// field.
|
||||
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
|
||||
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
|
||||
}
|
||||
|
||||
// DebugAction invokes a debug action, such as "rebind" or "restun".
|
||||
// These are development tools and subject to change or removal over time.
|
||||
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
|
||||
|
||||
@@ -51,7 +51,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
|
||||
@@ -110,12 +110,11 @@ func runSpeedtest(ctx context.Context, args []string) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
|
||||
fmt.Println("Results:")
|
||||
fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
|
||||
startTime := results[0].IntervalStart
|
||||
for _, r := range results {
|
||||
if r.Total {
|
||||
fmt.Fprintln(w, "-------------------------------------------------------------------------")
|
||||
}
|
||||
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds(), r.MegaBits(), r.MBitsPerSecond())
|
||||
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.MegaBits(), r.MBitsPerSecond())
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
|
||||
@@ -7,10 +7,8 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var bugReportCmd = &ffcli.Command{
|
||||
@@ -18,15 +16,6 @@ var bugReportCmd = &ffcli.Command{
|
||||
Exec: runBugReport,
|
||||
ShortHelp: "Print a shareable identifier to help diagnose issues",
|
||||
ShortUsage: "bugreport [note]",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("bugreport")
|
||||
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var bugReportArgs struct {
|
||||
diagnose bool
|
||||
}
|
||||
|
||||
func runBugReport(ctx context.Context, args []string) error {
|
||||
@@ -38,10 +27,7 @@ func runBugReport(ctx context.Context, args []string) error {
|
||||
default:
|
||||
return errors.New("unknown argumets")
|
||||
}
|
||||
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{
|
||||
Note: note,
|
||||
Diagnose: bugReportArgs.diagnose,
|
||||
})
|
||||
logMarker, err := localClient.BugReport(ctx, note)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,11 +48,11 @@ func runConfigureHost(ctx context.Context, args []string) error {
|
||||
if uid := os.Getuid(); uid != 0 {
|
||||
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
|
||||
}
|
||||
hi := hostinfo.New()
|
||||
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
|
||||
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
|
||||
osVer := hostinfo.GetOSVersion()
|
||||
isDSM6 := strings.HasPrefix(osVer, "Synology 6")
|
||||
isDSM7 := strings.HasPrefix(osVer, "Synology 7")
|
||||
if !isDSM6 && !isDSM7 {
|
||||
return fmt.Errorf("unsupported DSM version %q", hi.DistroVersion)
|
||||
return fmt.Errorf("unsupported DSM version %q", osVer)
|
||||
}
|
||||
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll("/dev/net", 0755); err != nil {
|
||||
|
||||
@@ -133,9 +133,6 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
printf("\t* PortMapping: %v\n", portMapping(report))
|
||||
if report.CaptivePortal != "" {
|
||||
printf("\t* CaptivePortal: %v\n", report.CaptivePortal)
|
||||
}
|
||||
|
||||
// When DERP latency checking failed,
|
||||
// magicsock will try to pick the DERP server that
|
||||
|
||||
@@ -380,6 +380,7 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
// Do this after validations to avoid the 5s delay if we're going to error
|
||||
// out anyway.
|
||||
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
|
||||
fmt.Println("wantSSH", wantSSH, "haveSSH", haveSSH)
|
||||
if wantSSH != haveSSH && isSSHOverTailscale() {
|
||||
if wantSSH {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, env.upArgs.acceptedRisks)
|
||||
|
||||
@@ -100,7 +100,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
L tailscale.com/util/strs from tailscale.com/hostinfo
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
|
||||
@@ -109,7 +109,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -190,8 +190,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/envknob from tailscale.com/control/controlclient+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
@@ -232,7 +230,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
@@ -275,7 +272,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
|
||||
@@ -7,7 +7,7 @@ After=network-pre.target NetworkManager.service systemd-resolved.service
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscaled
|
||||
ExecStartPre=/usr/sbin/tailscaled --cleanup
|
||||
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=${PORT} $FLAGS
|
||||
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
|
||||
ExecStopPost=/usr/sbin/tailscaled --cleanup
|
||||
|
||||
Restart=on-failure
|
||||
|
||||
@@ -46,7 +46,7 @@ function SSHSession({
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
runSSHSession(ref.current, def, ipn, onDone, (err) => console.error(err))
|
||||
runSSHSession(ref.current, def, ipn, onDone)
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import { WebLinksAddon } from "xterm-addon-web-links"
|
||||
export type SSHSessionDef = {
|
||||
username: string
|
||||
hostname: string
|
||||
/** Defaults to 5 seconds */
|
||||
timeoutSeconds?: number
|
||||
}
|
||||
|
||||
export function runSSHSession(
|
||||
@@ -14,7 +12,6 @@ export function runSSHSession(
|
||||
def: SSHSessionDef,
|
||||
ipn: IPN,
|
||||
onDone: () => void,
|
||||
onError?: (err: string) => void,
|
||||
terminalOptions?: ITerminalOptions
|
||||
) {
|
||||
const parentWindow = termContainerNode.ownerDocument.defaultView ?? window
|
||||
@@ -49,7 +46,7 @@ export function runSSHSession(
|
||||
term.write(input)
|
||||
},
|
||||
writeErrorFn(err) {
|
||||
onError?.(err)
|
||||
console.error(err)
|
||||
term.write(err)
|
||||
},
|
||||
setReadFn(hook) {
|
||||
@@ -65,7 +62,6 @@ export function runSSHSession(
|
||||
}
|
||||
onDone()
|
||||
},
|
||||
timeoutSeconds: def.timeoutSeconds,
|
||||
})
|
||||
|
||||
// Make terminal and SSH session track the size of the containing DOM node.
|
||||
|
||||
2
cmd/tsconnect/src/types/wasm_js.d.ts
vendored
2
cmd/tsconnect/src/types/wasm_js.d.ts
vendored
@@ -23,8 +23,6 @@ declare global {
|
||||
setReadFn: (readFn: (data: string) => void) => void
|
||||
rows: number
|
||||
cols: number
|
||||
/** Defaults to 5 seconds */
|
||||
timeoutSeconds?: number
|
||||
onDone: () => void
|
||||
}
|
||||
): IPNSSHSession
|
||||
|
||||
@@ -360,10 +360,6 @@ func (s *jsSSHSession) Run() {
|
||||
setReadFn := s.termConfig.Get("setReadFn")
|
||||
rows := s.termConfig.Get("rows").Int()
|
||||
cols := s.termConfig.Get("cols").Int()
|
||||
timeoutSeconds := 5.0
|
||||
if jsTimeoutSeconds := s.termConfig.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber {
|
||||
timeoutSeconds = jsTimeoutSeconds.Float()
|
||||
}
|
||||
onDone := s.termConfig.Get("onDone")
|
||||
defer onDone.Invoke()
|
||||
|
||||
@@ -371,7 +367,7 @@ func (s *jsSSHSession) Run() {
|
||||
writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second)))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
c, err := s.jsIPN.dialer.UserDial(ctx, "tcp", net.JoinHostPort(s.host, "22"))
|
||||
if err != nil {
|
||||
|
||||
@@ -114,11 +114,19 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Auto) onHealthChange(sys health.Subsystem, err error) {
|
||||
if sys == health.SysOverall {
|
||||
return
|
||||
}
|
||||
c.logf("controlclient: restarting map request for %q health change to new state: %v", sys, err)
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
//
|
||||
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/goroutines"
|
||||
)
|
||||
|
||||
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
@@ -21,7 +22,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(goroutines.ScrubbedGoroutineDump())
|
||||
zw.Write(scrubbedGoroutineDump())
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
@@ -39,3 +40,83 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
||||
}
|
||||
}
|
||||
|
||||
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||
// values of arguments scrubbed out, lest it contain some private key material.
|
||||
func scrubbedGoroutineDump() []byte {
|
||||
var buf []byte
|
||||
// Grab stacks multiple times into increasingly larger buffer sizes
|
||||
// to minimize the risk that we blow past our iOS memory limit.
|
||||
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
|
||||
buf = make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
if len(buf) < size {
|
||||
// It fit.
|
||||
break
|
||||
}
|
||||
}
|
||||
return scrubHex(buf)
|
||||
}
|
||||
|
||||
func scrubHex(buf []byte) []byte {
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
|
||||
foreachHexAddress(buf, func(in []byte) {
|
||||
if string(in) == "0x0" {
|
||||
return
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
copy(in, v)
|
||||
return
|
||||
}
|
||||
inStr := string(in)
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
if err != nil {
|
||||
in[0] = '?'
|
||||
return
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[inStr] = v
|
||||
copy(in, v)
|
||||
})
|
||||
return buf
|
||||
}
|
||||
|
||||
var ohx = []byte("0x")
|
||||
|
||||
// foreachHexAddress calls f with each subslice of b that matches
|
||||
// regexp `0x[0-9a-f]*`.
|
||||
func foreachHexAddress(b []byte, f func([]byte)) {
|
||||
for len(b) > 0 {
|
||||
i := bytes.Index(b, ohx)
|
||||
if i == -1 {
|
||||
return
|
||||
}
|
||||
b = b[i:]
|
||||
hx := hexPrefix(b)
|
||||
f(hx)
|
||||
b = b[len(hx):]
|
||||
}
|
||||
}
|
||||
|
||||
func hexPrefix(b []byte) []byte {
|
||||
for i, c := range b {
|
||||
if i < 2 {
|
||||
continue
|
||||
}
|
||||
if !isHexByte(c) {
|
||||
return b[:i]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isHexByte(b byte) bool {
|
||||
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package goroutines
|
||||
package controlclient
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestScrubbedGoroutineDump(t *testing.T) {
|
||||
t.Logf("Got:\n%s\n", ScrubbedGoroutineDump())
|
||||
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
|
||||
}
|
||||
|
||||
func TestScrubHex(t *testing.T) {
|
||||
@@ -76,8 +76,6 @@ type Direct struct {
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
|
||||
serverNoiseKey key.MachinePublic
|
||||
@@ -108,7 +106,6 @@ type Options struct {
|
||||
KeepAlive bool
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
@@ -135,34 +132,6 @@ type Options struct {
|
||||
// MapResponse.PingRequest queries from the control plane.
|
||||
// If nil, PingRequest queries are not answered.
|
||||
Pinger Pinger
|
||||
|
||||
// DialPlan contains and stores a previous dial plan that we received
|
||||
// from the control server; if nil, we fall back to using DNS.
|
||||
//
|
||||
// If we receive a new DialPlan from the server, this value will be
|
||||
// updated.
|
||||
DialPlan ControlDialPlanner
|
||||
}
|
||||
|
||||
// ControlDialPlanner is the interface optionally supplied when creating a
|
||||
// control client to control exactly how TCP connections to the control plane
|
||||
// are dialed.
|
||||
//
|
||||
// It is usually implemented by an atomic.Pointer.
|
||||
type ControlDialPlanner interface {
|
||||
// Load returns the current plan for how to connect to control.
|
||||
//
|
||||
// The returned plan can be nil. If so, connections should be made by
|
||||
// resolving the control URL using DNS.
|
||||
Load() *tailcfg.ControlDialPlan
|
||||
|
||||
// Store updates the dial plan with new directions from the control
|
||||
// server.
|
||||
//
|
||||
// The dial plan can span multiple connections to the control server.
|
||||
// That is, a dial plan received when connected over Wi-Fi is still
|
||||
// valid for a subsequent connection over LTE after a network switch.
|
||||
Store(*tailcfg.ControlDialPlan)
|
||||
}
|
||||
|
||||
// Pinger is the LocalBackend.Ping method.
|
||||
@@ -246,7 +215,6 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dialPlan: opts.DialPlan,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
@@ -258,12 +226,6 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
c.SetNetInfo(ni)
|
||||
}
|
||||
}
|
||||
if opts.NoiseTestClient != nil {
|
||||
c.noiseClient = &noiseClient{
|
||||
Client: opts.NoiseTestClient,
|
||||
}
|
||||
c.serverNoiseKey = key.NewMachine().Public() // prevent early error before hitting test client
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -946,14 +908,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
} else {
|
||||
vlogf("netmap: got new map")
|
||||
}
|
||||
if resp.ControlDialPlan != nil {
|
||||
if c.dialPlan != nil {
|
||||
c.logf("netmap: got new dial plan from control")
|
||||
c.dialPlan.Store(resp.ControlDialPlan)
|
||||
} else {
|
||||
c.logf("netmap: [unexpected] new dial plan; nowhere to store it")
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case timeoutReset <- struct{}{}:
|
||||
@@ -1404,17 +1358,12 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
if nc != nil {
|
||||
return nc, nil
|
||||
}
|
||||
var dp func() *tailcfg.ControlDialPlan
|
||||
if c.dialPlan != nil {
|
||||
dp = c.dialPlan.Load
|
||||
}
|
||||
nc, err, _ := c.sfGroup.Do(struct{}{}, func() (*noiseClient, error) {
|
||||
k, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.logf("creating new noise client")
|
||||
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp)
|
||||
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1434,11 +1383,15 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
newReq := *req
|
||||
newReq.Version = tailcfg.CurrentCapabilityVersion
|
||||
nc, err := c.getNoiseClient()
|
||||
np, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := nc.post(ctx, "/machine/set-dns", &newReq)
|
||||
bodyData, err := json.Marshal(newReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := np.Post(fmt.Sprintf("https://%v/%v", np.host, "machine/set-dns"), "application/json", bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1586,38 +1539,6 @@ func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailc
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportHealthChange reports to the control plane a change to this node's
|
||||
// health.
|
||||
func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
|
||||
if sys == health.SysOverall {
|
||||
// We don't report these. These include things like the network is down
|
||||
// (in which case we can't report anyway) or the user wanted things
|
||||
// stopped, as opposed to the more unexpected failure types in the other
|
||||
// subsystems.
|
||||
return
|
||||
}
|
||||
np, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
// Don't report errors to control if the server doesn't support noise.
|
||||
return
|
||||
}
|
||||
req := &tailcfg.HealthChangeRequest{
|
||||
Subsys: string(sys),
|
||||
}
|
||||
if sysErr != nil {
|
||||
req.Error = sysErr.Error()
|
||||
}
|
||||
|
||||
// Best effort, no logging:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
res, err := np.post(ctx, "/machine/update-health", req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
var (
|
||||
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ type mapSession struct {
|
||||
lastHealth []string
|
||||
lastPopBrowserURL string
|
||||
stickyDebug tailcfg.Debug // accumulated opt.Bool values
|
||||
lastTKAInfo *tailcfg.TKAInfo
|
||||
|
||||
// netMapBuilding is non-nil during a netmapForResponse call,
|
||||
// containing the value to be returned, once fully populated.
|
||||
@@ -116,9 +115,6 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
if resp.Health != nil {
|
||||
ms.lastHealth = resp.Health
|
||||
}
|
||||
if resp.TKAInfo != nil {
|
||||
ms.lastTKAInfo = resp.TKAInfo
|
||||
}
|
||||
|
||||
debug := resp.Debug
|
||||
if debug != nil {
|
||||
@@ -156,17 +152,9 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
|
||||
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
|
||||
if err := nm.TKAHead.UnmarshalText([]byte(ms.lastTKAInfo.Head)); err != nil {
|
||||
ms.logf("error unmarshalling TKAHead: %v", err)
|
||||
nm.TKAEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Node != nil {
|
||||
ms.lastNode = resp.Node
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -55,11 +53,6 @@ type noiseClient struct {
|
||||
httpPort string // the default port to call
|
||||
httpsPort string // the fallback Noise-over-https port
|
||||
|
||||
// dialPlan optionally returns a ControlDialPlan previously received
|
||||
// from the control server; either the function or the return value can
|
||||
// be nil.
|
||||
dialPlan func() *tailcfg.ControlDialPlan
|
||||
|
||||
// mu only protects the following variables.
|
||||
mu sync.Mutex
|
||||
nextID int
|
||||
@@ -68,9 +61,7 @@ type noiseClient struct {
|
||||
|
||||
// newNoiseClient returns a new noiseClient for the provided server and machine key.
|
||||
// serverURL is of the form https://<host>:<port> (no trailing slash).
|
||||
//
|
||||
// dialPlan may be nil
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*noiseClient, error) {
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer) (*noiseClient, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -98,7 +89,6 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
|
||||
httpPort: httpPort,
|
||||
httpsPort: httpsPort,
|
||||
dialer: dialer,
|
||||
dialPlan: dialPlan,
|
||||
}
|
||||
|
||||
// Create the HTTP/2 Transport using a net/http.Transport
|
||||
@@ -165,51 +155,16 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
nc.nextID++
|
||||
nc.mu.Unlock()
|
||||
|
||||
// Timeout is a little arbitrary, but plenty long enough for even the
|
||||
// highest latency links.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
|
||||
// Panic, because a test should have started failing several
|
||||
// thousand version numbers before getting to this point.
|
||||
panic("capability version is too high to fit in the wire protocol")
|
||||
}
|
||||
|
||||
var dialPlan *tailcfg.ControlDialPlan
|
||||
if nc.dialPlan != nil {
|
||||
dialPlan = nc.dialPlan()
|
||||
}
|
||||
|
||||
// If we have a dial plan, then set our timeout as slightly longer than
|
||||
// the maximum amount of time contained therein; we assume that
|
||||
// explicit instructions on timeouts are more useful than a single
|
||||
// hard-coded timeout.
|
||||
//
|
||||
// The default value of 5 is chosen so that, when there's no dial plan,
|
||||
// we retain the previous behaviour of 10 seconds end-to-end timeout.
|
||||
timeoutSec := 5.0
|
||||
if dialPlan != nil {
|
||||
for _, c := range dialPlan.Candidates {
|
||||
if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
|
||||
timeoutSec = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After we establish a connection, we need some time to actually
|
||||
// upgrade it into a Noise connection. With a ballpark worst-case RTT
|
||||
// of 1000ms, give ourselves an extra 5 seconds to complete the
|
||||
// handshake.
|
||||
timeoutSec += 5
|
||||
|
||||
// Be extremely defensive and ensure that the timeout is in the range
|
||||
// [5, 60] seconds (e.g. if we accidentally get a negative number).
|
||||
if timeoutSec > 60 {
|
||||
timeoutSec = 60
|
||||
} else if timeoutSec < 5 {
|
||||
timeoutSec = 5
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := (&controlhttp.Dialer{
|
||||
Hostname: nc.host,
|
||||
HTTPPort: nc.httpPort,
|
||||
@@ -218,7 +173,6 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
ControlKey: nc.serverPubKey,
|
||||
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
|
||||
Dialer: nc.dialer.SystemDial,
|
||||
DialPlan: dialPlan,
|
||||
}).Dial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -230,16 +184,3 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
mak.Set(&nc.connPool, ncc.id, ncc)
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
func (nc *noiseClient) post(ctx context.Context, path string, body any) (*http.Response, error) {
|
||||
jbody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+nc.host+path, bytes.NewReader(jbody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return nc.Do(req)
|
||||
}
|
||||
|
||||
@@ -28,25 +28,18 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
var stdDialer net.Dialer
|
||||
@@ -89,170 +82,7 @@ func (a *Dialer) httpsFallbackDelay() time.Duration {
|
||||
return 500 * time.Millisecond
|
||||
}
|
||||
|
||||
var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
|
||||
|
||||
func (a *Dialer) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
// If we don't have a dial plan, just fall back to dialing the single
|
||||
// host we know about.
|
||||
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
|
||||
if !useDialPlan || a.DialPlan == nil || len(a.DialPlan.Candidates) == 0 {
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
}
|
||||
candidates := a.DialPlan.Candidates
|
||||
|
||||
// Otherwise, we try dialing per the plan. Store the highest priority
|
||||
// in the list, so that if we get a connection to one of those
|
||||
// candidates we can return quickly.
|
||||
var highestPriority int = math.MinInt
|
||||
for _, c := range candidates {
|
||||
if c.Priority > highestPriority {
|
||||
highestPriority = c.Priority
|
||||
}
|
||||
}
|
||||
|
||||
// This context allows us to cancel in-flight connections if we get a
|
||||
// highest-priority connection before we're all done.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Now, for each candidate, kick off a dial in parallel.
|
||||
type dialResult struct {
|
||||
conn *controlbase.Conn
|
||||
err error
|
||||
addr netip.Addr
|
||||
priority int
|
||||
}
|
||||
resultsCh := make(chan dialResult, len(candidates))
|
||||
|
||||
var pending atomic.Int32
|
||||
pending.Store(int32(len(candidates)))
|
||||
for _, c := range candidates {
|
||||
go func(ctx context.Context, c tailcfg.ControlIPCandidate) {
|
||||
var (
|
||||
conn *controlbase.Conn
|
||||
err error
|
||||
)
|
||||
|
||||
// Always send results back to our channel.
|
||||
defer func() {
|
||||
resultsCh <- dialResult{conn, err, c.IP, c.Priority}
|
||||
if pending.Add(-1) == 0 {
|
||||
close(resultsCh)
|
||||
}
|
||||
}()
|
||||
|
||||
// If non-zero, wait the configured start timeout
|
||||
// before we do anything.
|
||||
if c.DialStartDelaySec > 0 {
|
||||
a.logf("[v2] controlhttp: waiting %.2f seconds before dialing %q @ %v", c.DialStartDelaySec, a.Hostname, c.IP)
|
||||
tmr := time.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
|
||||
defer tmr.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
return
|
||||
case <-tmr.C:
|
||||
}
|
||||
}
|
||||
|
||||
// Now, create a sub-context with the given timeout and
|
||||
// try dialing the provided host.
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(c.DialTimeoutSec*float64(time.Second)))
|
||||
defer cancel()
|
||||
|
||||
// This will dial, and the defer above sends it back to our parent.
|
||||
a.logf("[v2] controlhttp: trying to dial %q @ %v", a.Hostname, c.IP)
|
||||
conn, err = a.dialHost(ctx, c.IP)
|
||||
}(ctx, c)
|
||||
}
|
||||
|
||||
var results []dialResult
|
||||
for res := range resultsCh {
|
||||
// If we get a response that has the highest priority, we don't
|
||||
// need to wait for any of the other connections to finish; we
|
||||
// can just return this connection.
|
||||
//
|
||||
// TODO(andrew): we could make this better by keeping track of
|
||||
// the highest remaining priority dynamically, instead of just
|
||||
// checking for the highest total
|
||||
if res.priority == highestPriority && res.conn != nil {
|
||||
a.logf("[v1] controlhttp: high-priority success dialing %q @ %v from dial plan", a.Hostname, res.addr)
|
||||
|
||||
// Drain the channel and any existing connections in
|
||||
// the background.
|
||||
go func() {
|
||||
for _, res := range results {
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
for res := range resultsCh {
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
if a.drainFinished != nil {
|
||||
close(a.drainFinished)
|
||||
}
|
||||
}()
|
||||
return res.conn, nil
|
||||
}
|
||||
|
||||
// This isn't a highest-priority result, so just store it until
|
||||
// we're done.
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
// After we finish this function, close any remaining open connections.
|
||||
defer func() {
|
||||
for _, result := range results {
|
||||
// Note: below, we nil out the returned connection (if
|
||||
// any) in the slice so we don't close it.
|
||||
if result.conn != nil {
|
||||
result.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// We don't drain asynchronously after this point, so notify our
|
||||
// channel when we return.
|
||||
if a.drainFinished != nil {
|
||||
close(a.drainFinished)
|
||||
}
|
||||
}()
|
||||
|
||||
// Sort by priority, then take the first non-error response.
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
// NOTE: intentionally inverted so that the highest priority
|
||||
// item comes first
|
||||
return results[i].priority > results[j].priority
|
||||
})
|
||||
|
||||
var (
|
||||
conn *controlbase.Conn
|
||||
errs []error
|
||||
)
|
||||
for i, result := range results {
|
||||
if result.err != nil {
|
||||
errs = append(errs, result.err)
|
||||
continue
|
||||
}
|
||||
|
||||
a.logf("[v1] controlhttp: succeeded dialing %q @ %v from dial plan", a.Hostname, result.addr)
|
||||
conn = result.conn
|
||||
results[i].conn = nil // so we don't close it in the defer
|
||||
return conn, nil
|
||||
}
|
||||
merr := multierr.New(errs...)
|
||||
|
||||
// If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
|
||||
a.logf("controlhttp: failed dialing using DialPlan, falling back to DNS; errs=%s", merr.Error())
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
}
|
||||
|
||||
// dialHost connects to the configured Dialer.Hostname and upgrades the
|
||||
// connection into a controlbase.Conn. If addr is valid, then no DNS is used
|
||||
// and the connection will be made to the provided address.
|
||||
func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Conn, error) {
|
||||
// Create one shared context used by both port 80 and port 443 dials.
|
||||
// If port 80 is still in flight when 443 returns, this deferred cancel
|
||||
// will stop the port 80 dial.
|
||||
@@ -280,7 +110,7 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Co
|
||||
}
|
||||
ch := make(chan tryURLRes) // must be unbuffered
|
||||
try := func(u *url.URL) {
|
||||
cbConn, err := a.dialURL(ctx, u, addr)
|
||||
cbConn, err := a.dialURL(ctx, u)
|
||||
select {
|
||||
case ch <- tryURLRes{u, cbConn, err}:
|
||||
case <-ctx.Done():
|
||||
@@ -331,12 +161,12 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Co
|
||||
}
|
||||
|
||||
// dialURL attempts to connect to the given URL.
|
||||
func (a *Dialer) dialURL(ctx context.Context, u *url.URL, addr netip.Addr) (*controlbase.Conn, error) {
|
||||
func (a *Dialer) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.MachineKey, a.ControlKey, a.ProtocolVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, addr, init)
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, init)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -348,27 +178,14 @@ func (a *Dialer) dialURL(ctx context.Context, u *url.URL, addr netip.Addr) (*con
|
||||
return cbConn, nil
|
||||
}
|
||||
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn. If addr
|
||||
// is valid, then no DNS is used and the connection will be made to the
|
||||
// provided address.
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
|
||||
//
|
||||
// Only the provided ctx is used, not a.ctx.
|
||||
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr, init []byte) (net.Conn, error) {
|
||||
var dns *dnscache.Resolver
|
||||
|
||||
// If we were provided an address to dial, then create a resolver that just
|
||||
// returns that value; otherwise, fall back to DNS.
|
||||
if addr.IsValid() {
|
||||
dns = &dnscache.Resolver{
|
||||
SingleHostStaticResult: []netip.Addr{addr},
|
||||
SingleHost: u.Hostname(),
|
||||
}
|
||||
} else {
|
||||
dns = &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
}
|
||||
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
|
||||
dns := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
}
|
||||
|
||||
var dialer dnscache.DialContextFunc
|
||||
|
||||
@@ -29,11 +29,9 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
|
||||
wsScheme := "wss"
|
||||
host := d.Hostname
|
||||
// If using a custom control server (on a non-standard port), prefer that.
|
||||
// This mirrors the port selection in newNoiseClient from noise.go.
|
||||
if d.HTTPPort != "" && d.HTTPPort != "80" && d.HTTPSPort == "443" {
|
||||
if host == "localhost" {
|
||||
wsScheme = "ws"
|
||||
host = net.JoinHostPort(host, d.HTTPPort)
|
||||
host = net.JoinHostPort(host, strDef(d.HTTPPort, "80"))
|
||||
}
|
||||
wsURL := &url.URL{
|
||||
Scheme: wsScheme,
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -71,15 +70,9 @@ type Dialer struct {
|
||||
// dropped.
|
||||
Logf logger.Logf
|
||||
|
||||
// DialPlan, if set, contains instructions from the control server on
|
||||
// how to connect to it. If present, we will try the methods in this
|
||||
// plan before falling back to DNS.
|
||||
DialPlan *tailcfg.ControlDialPlan
|
||||
|
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
|
||||
// For tests only
|
||||
drainFinished chan struct{}
|
||||
insecureTLS bool
|
||||
testFallbackDelay time.Duration
|
||||
}
|
||||
|
||||
@@ -13,21 +13,16 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type httpTestParam struct {
|
||||
@@ -449,263 +444,3 @@ func brokenMITMHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.(http.Flusher).Flush()
|
||||
<-r.Context().Done()
|
||||
}
|
||||
|
||||
func TestDialPlan(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("only works on Linux due to multiple localhost addresses")
|
||||
}
|
||||
|
||||
client, server := key.NewMachine(), key.NewMachine()
|
||||
|
||||
const (
|
||||
testProtocolVersion = 1
|
||||
|
||||
// We need consistent ports for each address; these are chosen
|
||||
// randomly and we hope that they won't conflict during this test.
|
||||
httpPort = "40080"
|
||||
httpsPort = "40443"
|
||||
)
|
||||
|
||||
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
|
||||
done := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
close(done)
|
||||
})
|
||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := AcceptHTTP(context.Background(), w, r, server)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
defer conn.Close()
|
||||
}
|
||||
w.Header().Set("X-Handler-Name", name)
|
||||
<-done
|
||||
})
|
||||
if wrap != nil {
|
||||
handler = wrap(handler)
|
||||
}
|
||||
|
||||
httpLn, err := net.Listen("tcp", host.String()+":"+httpPort)
|
||||
if err != nil {
|
||||
t.Fatalf("HTTP listen: %v", err)
|
||||
}
|
||||
httpsLn, err := net.Listen("tcp", host.String()+":"+httpsPort)
|
||||
if err != nil {
|
||||
t.Fatalf("HTTPS listen: %v", err)
|
||||
}
|
||||
|
||||
httpServer := &http.Server{Handler: handler}
|
||||
go httpServer.Serve(httpLn)
|
||||
t.Cleanup(func() {
|
||||
httpServer.Close()
|
||||
})
|
||||
|
||||
httpsServer := &http.Server{
|
||||
Handler: handler,
|
||||
TLSConfig: tlsConfig(t),
|
||||
ErrorLog: logger.StdLogger(logger.WithPrefix(t.Logf, "http.Server.ErrorLog: ")),
|
||||
}
|
||||
go httpsServer.ServeTLS(httpsLn, "", "")
|
||||
t.Cleanup(func() {
|
||||
httpsServer.Close()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fallbackAddr := netip.MustParseAddr("127.0.0.1")
|
||||
goodAddr := netip.MustParseAddr("127.0.0.2")
|
||||
otherAddr := netip.MustParseAddr("127.0.0.3")
|
||||
other2Addr := netip.MustParseAddr("127.0.0.4")
|
||||
brokenAddr := netip.MustParseAddr("127.0.0.10")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
plan *tailcfg.ControlDialPlan
|
||||
wrap func(http.Handler) http.Handler
|
||||
want netip.Addr
|
||||
|
||||
allowFallback bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: goodAddr,
|
||||
},
|
||||
{
|
||||
name: "broken-then-good",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Dials the broken one, which fails, and then
|
||||
// eventually dials the good one and succeeds
|
||||
{IP: brokenAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10, DialStartDelaySec: 1},
|
||||
}},
|
||||
want: goodAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple-priority-fast-path",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Dials some good IPs and our bad one (which
|
||||
// hangs forever), which then hits the fast
|
||||
// path where we bail without waiting.
|
||||
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: otherAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple-priority-slow-path",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Our broken address is the highest priority,
|
||||
// so we don't hit our fast path.
|
||||
{IP: brokenAddr, Priority: 10, DialTimeoutSec: 10},
|
||||
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: otherAddr,
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 1},
|
||||
}},
|
||||
want: fallbackAddr,
|
||||
allowFallback: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
makeHandler(t, "fallback", fallbackAddr, nil)
|
||||
makeHandler(t, "good", goodAddr, nil)
|
||||
makeHandler(t, "other", otherAddr, nil)
|
||||
makeHandler(t, "other2", other2Addr, nil)
|
||||
makeHandler(t, "broken", brokenAddr, func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(brokenMITMHandler)
|
||||
})
|
||||
|
||||
dialer := closeTrackDialer{
|
||||
t: t,
|
||||
inner: new(tsdial.Dialer).SystemDial,
|
||||
conns: make(map[*closeTrackConn]bool),
|
||||
}
|
||||
defer dialer.Done()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// By default, we intentionally point to something that
|
||||
// we know won't connect, since we want a fallback to
|
||||
// DNS to be an error.
|
||||
host := "example.com"
|
||||
if tt.allowFallback {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
drained := make(chan struct{})
|
||||
a := &Dialer{
|
||||
Hostname: host,
|
||||
HTTPPort: httpPort,
|
||||
HTTPSPort: httpsPort,
|
||||
MachineKey: client,
|
||||
ControlKey: server.Public(),
|
||||
ProtocolVersion: testProtocolVersion,
|
||||
Dialer: dialer.Dial,
|
||||
Logf: t.Logf,
|
||||
DialPlan: tt.plan,
|
||||
proxyFunc: func(*http.Request) (*url.URL, error) { return nil, nil },
|
||||
drainFinished: drained,
|
||||
insecureTLS: true,
|
||||
testFallbackDelay: 50 * time.Millisecond,
|
||||
}
|
||||
|
||||
conn, err := a.dial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("dialing controlhttp: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
raddr := conn.RemoteAddr().(*net.TCPAddr)
|
||||
|
||||
got, ok := netip.AddrFromSlice(raddr.IP)
|
||||
if !ok {
|
||||
t.Errorf("invalid remote IP: %v", raddr.IP)
|
||||
} else if got != tt.want {
|
||||
t.Errorf("got connection from %q; want %q", got, tt.want)
|
||||
} else {
|
||||
t.Logf("successfully connected to %q", raddr.String())
|
||||
}
|
||||
|
||||
// Wait until our dialer drains so we can verify that
|
||||
// all connections are closed.
|
||||
<-drained
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type closeTrackDialer struct {
|
||||
t testing.TB
|
||||
inner dnscache.DialContextFunc
|
||||
mu sync.Mutex
|
||||
conns map[*closeTrackConn]bool
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
c, err := d.inner(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct := &closeTrackConn{Conn: c, d: d}
|
||||
|
||||
d.mu.Lock()
|
||||
d.conns[ct] = true
|
||||
d.mu.Unlock()
|
||||
return ct, nil
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) Done() {
|
||||
// Unfortunately, tsdial.Dialer.SystemDial closes connections
|
||||
// asynchronously in a goroutine, so we can't assume that everything is
|
||||
// closed by the time we get here.
|
||||
//
|
||||
// Sleep/wait a few times on the assumption that things will close
|
||||
// "eventually".
|
||||
const iters = 100
|
||||
for i := 0; i < iters; i++ {
|
||||
d.mu.Lock()
|
||||
if len(d.conns) == 0 {
|
||||
d.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Only error on last iteration
|
||||
if i != iters-1 {
|
||||
d.mu.Unlock()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
for conn := range d.conns {
|
||||
d.t.Errorf("expected close of conn %p; RemoteAddr=%q", conn, conn.RemoteAddr().String())
|
||||
}
|
||||
d.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) noteClose(c *closeTrackConn) {
|
||||
d.mu.Lock()
|
||||
delete(d.conns, c) // safe if already deleted
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
type closeTrackConn struct {
|
||||
net.Conn
|
||||
d *closeTrackDialer
|
||||
}
|
||||
|
||||
func (c *closeTrackConn) Close() error {
|
||||
c.d.noteClose(c)
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: TS_AUTH_KEY
|
||||
key: AUTH_KEY
|
||||
optional: true
|
||||
- name: TS_DEST_IP
|
||||
value: "{{TS_DEST_IP}}"
|
||||
|
||||
@@ -17,11 +17,10 @@ TS_KUBE_SECRET="${TS_KUBE_SECRET:-tailscale}"
|
||||
TS_SOCKS5_SERVER="${TS_SOCKS5_SERVER:-}"
|
||||
TS_OUTBOUND_HTTP_PROXY_LISTEN="${TS_OUTBOUND_HTTP_PROXY_LISTEN:-}"
|
||||
TS_TAILSCALED_EXTRA_ARGS="${TS_TAILSCALED_EXTRA_ARGS:-}"
|
||||
TS_SOCKET="${TS_SOCKET:-/tmp/tailscaled.sock}"
|
||||
|
||||
set -e
|
||||
|
||||
TAILSCALED_ARGS="--socket=${TS_SOCKET}"
|
||||
TAILSCALED_ARGS="--socket=/tmp/tailscaled.sock"
|
||||
|
||||
if [[ ! -z "${KUBERNETES_SERVICE_HOST}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=kube:${TS_KUBE_SECRET} --statedir=${TS_STATE_DIR:-/tmp}"
|
||||
@@ -82,11 +81,11 @@ if [[ ! -z "${TS_EXTRA_ARGS}" ]]; then
|
||||
fi
|
||||
|
||||
echo "Running tailscale up"
|
||||
tailscale --socket="${TS_SOCKET}" up ${UP_ARGS}
|
||||
tailscale --socket=/tmp/tailscaled.sock up ${UP_ARGS}
|
||||
|
||||
if [[ ! -z "${TS_DEST_IP}" ]]; then
|
||||
echo "Adding iptables rule for DNAT"
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=${TS_SOCKET} ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
|
||||
fi
|
||||
|
||||
echo "Waiting for tailscaled to exit"
|
||||
|
||||
@@ -23,7 +23,7 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: TS_AUTH_KEY
|
||||
key: AUTH_KEY
|
||||
optional: true
|
||||
- name: TS_ROUTES
|
||||
value: "{{TS_ROUTES}}"
|
||||
|
||||
@@ -26,5 +26,5 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: TS_AUTH_KEY
|
||||
key: AUTH_KEY
|
||||
optional: true
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package doctor contains more in-depth healthchecks that can be run to aid in
|
||||
// diagnosing Tailscale issues.
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Check is the interface defining a singular check.
|
||||
//
|
||||
// A check should log information that it gathers using the provided log
|
||||
// function, and should attempt to make as much progress as possible in error
|
||||
// conditions.
|
||||
type Check interface {
|
||||
// Name should return a name describing this check, in lower-kebab-case
|
||||
// (i.e. "my-check", not "MyCheck" or "my_check").
|
||||
Name() string
|
||||
// Run executes the check, logging diagnostic information to the
|
||||
// provided logger function.
|
||||
Run(context.Context, logger.Logf) error
|
||||
}
|
||||
|
||||
// RunChecks runs a list of checks in parallel, and logs any returned errors
|
||||
// after all checks have returned.
|
||||
func RunChecks(ctx context.Context, log logger.Logf, checks ...Check) {
|
||||
if len(checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type namedErr struct {
|
||||
name string
|
||||
err error
|
||||
}
|
||||
errs := make(chan namedErr, len(checks))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(checks))
|
||||
for _, check := range checks {
|
||||
go func(c Check) {
|
||||
defer wg.Done()
|
||||
|
||||
plog := logger.WithPrefix(log, c.Name()+": ")
|
||||
errs <- namedErr{
|
||||
name: c.Name(),
|
||||
err: c.Run(ctx, plog),
|
||||
}
|
||||
}(check)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
for n := range errs {
|
||||
if n.err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
log("check %s: %v", n.name, n.err)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckFunc creates a Check from a name and a function.
|
||||
func CheckFunc(name string, run func(context.Context, logger.Logf) error) Check {
|
||||
return checkFunc{name, run}
|
||||
}
|
||||
|
||||
type checkFunc struct {
|
||||
name string
|
||||
run func(context.Context, logger.Logf) error
|
||||
}
|
||||
|
||||
func (c checkFunc) Name() string { return c.name }
|
||||
func (c checkFunc) Run(ctx context.Context, log logger.Logf) error { return c.run(ctx, log) }
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestRunChecks(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
var (
|
||||
mu sync.Mutex
|
||||
lines []string
|
||||
)
|
||||
logf := func(format string, args ...any) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lines = append(lines, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
RunChecks(ctx, logf,
|
||||
testCheck1{},
|
||||
CheckFunc("testcheck2", func(_ context.Context, log logger.Logf) error {
|
||||
log("check 2")
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
c.Assert(lines, qt.Contains, "testcheck1: check 1")
|
||||
c.Assert(lines, qt.Contains, "testcheck2: check 2")
|
||||
}
|
||||
|
||||
type testCheck1 struct{}
|
||||
|
||||
func (t testCheck1) Name() string { return "testcheck1" }
|
||||
func (t testCheck1) Run(_ context.Context, log logger.Logf) error {
|
||||
log("check 1")
|
||||
return nil
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package routetable provides a doctor.Check that dumps the current system's
|
||||
// route table to the log.
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/net/routetable"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// MaxRoutes is the maximum number of routes that will be displayed.
|
||||
const MaxRoutes = 1000
|
||||
|
||||
// Check implements the doctor.Check interface.
|
||||
type Check struct{}
|
||||
|
||||
func (Check) Name() string {
|
||||
return "routetable"
|
||||
}
|
||||
|
||||
func (Check) Run(_ context.Context, logf logger.Logf) error {
|
||||
rs, err := routetable.Get(MaxRoutes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range rs {
|
||||
logf("%s", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -10,28 +10,14 @@ import (
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/goroutines"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON := func(v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/echo":
|
||||
// Test handler.
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
w.Write(body)
|
||||
case "/debug/goroutines":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump())
|
||||
case "/debug/prefs":
|
||||
writeJSON(b.Prefs())
|
||||
case "/debug/metrics":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
case "/ssh/usernames":
|
||||
var req tailcfg.C2NSSHUsernamesRequest
|
||||
if r.Method == "POST" {
|
||||
@@ -45,7 +31,8 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
writeJSON(res)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
default:
|
||||
http.Error(w, "unknown c2n path", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ import (
|
||||
"go4.org/netipx"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/doctor"
|
||||
"tailscale.com/doctor/routetable"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -191,10 +189,6 @@ type LocalBackend struct {
|
||||
// statusChanged.Broadcast().
|
||||
statusLock sync.Mutex
|
||||
statusChanged *sync.Cond
|
||||
|
||||
// dialPlan is any dial plan that we've received from the control
|
||||
// server during a previous connection; it is cleared on logout.
|
||||
dialPlan atomic.Pointer[tailcfg.ControlDialPlan]
|
||||
}
|
||||
|
||||
// clientGen is a func that creates a control plane client.
|
||||
@@ -690,9 +684,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
}
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
if err := b.tkaSyncIfNeededLocked(st.NetMap); err != nil {
|
||||
b.logf("[v1] TKA sync error: %v", err)
|
||||
}
|
||||
if b.findExitNodeIDLocked(st.NetMap) {
|
||||
prefsChanged = true
|
||||
}
|
||||
@@ -1093,7 +1084,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
Dialer: b.Dialer(),
|
||||
Status: b.setClientStatus,
|
||||
C2NHandler: http.HandlerFunc(b.handleC2N),
|
||||
DialPlan: &b.dialPlan, // pointer because it can't be copied
|
||||
|
||||
// Don't warn about broken Linux IP forwarding when
|
||||
// netstack is being used.
|
||||
@@ -3119,9 +3109,6 @@ func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
|
||||
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
|
||||
})
|
||||
|
||||
// Clear any previous dial plan(s), if set.
|
||||
b.dialPlan.Store(nil)
|
||||
|
||||
if cc == nil {
|
||||
// Double Logout can happen via repeated IPN
|
||||
// connections to ipnserver making it repeatedly
|
||||
@@ -3238,17 +3225,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// operatorUserName returns the current pref's OperatorUser's name, or the
|
||||
// empty string if none.
|
||||
func (b *LocalBackend) operatorUserName() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.prefs == nil {
|
||||
return ""
|
||||
}
|
||||
return b.prefs.OperatorUser
|
||||
}
|
||||
|
||||
// OperatorUserID returns the current pref's OperatorUser's ID (in
|
||||
// os/user.User.Uid string form), or the empty string if none.
|
||||
func (b *LocalBackend) OperatorUserID() string {
|
||||
@@ -3617,17 +3593,6 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
|
||||
return cc.DoNoiseRequest(req)
|
||||
}
|
||||
|
||||
// tailscaleSSHEnabled reports whether Tailscale SSH is currently enabled based
|
||||
// on prefs. It returns false if there are no prefs set.
|
||||
func (b *LocalBackend) tailscaleSSHEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.prefs == nil {
|
||||
return false
|
||||
}
|
||||
return b.prefs.RunSSH
|
||||
}
|
||||
|
||||
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@@ -3686,19 +3651,3 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
io.WriteString(w, "</ul>\n")
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) {
|
||||
var checks []doctor.Check
|
||||
|
||||
checks = append(checks, routetable.Check{})
|
||||
|
||||
// TODO(andrew): more
|
||||
|
||||
numChecks := len(checks)
|
||||
checks = append(checks, doctor.CheckFunc("numchecks", func(_ context.Context, log logger.Logf) error {
|
||||
log("%d checks", numChecks)
|
||||
return nil
|
||||
}))
|
||||
|
||||
doctor.RunChecks(ctx, logf, checks...)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
@@ -33,116 +31,6 @@ type tkaState struct {
|
||||
storage *tka.FS
|
||||
}
|
||||
|
||||
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
|
||||
// performing the steps necessary to synchronize local tka state.
|
||||
//
|
||||
// There are 4 scenarios handled here:
|
||||
// - Enablement: nm.TKAEnabled but b.tka == nil
|
||||
// ∴ reach out to /machine/tka/boostrap to get the genesis AUM, then
|
||||
// initialize TKA.
|
||||
// - Disablement: !nm.TKAEnabled but b.tka != nil
|
||||
// ∴ reach out to /machine/tka/boostrap to read the disablement secret,
|
||||
// then verify and clear tka local state.
|
||||
// - Sync needed: b.tka.Head != nm.TKAHead
|
||||
// ∴ complete multi-step synchronization flow.
|
||||
// - Everything up to date: All other cases.
|
||||
// ∴ no action necessary.
|
||||
//
|
||||
// b.mu must be held. b.mu will be stepped out of (and back in) during network
|
||||
// RPCs.
|
||||
func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
|
||||
if !networkLockAvailable() {
|
||||
// If the feature flag is not enabled, pretend we don't exist.
|
||||
return nil
|
||||
}
|
||||
|
||||
isEnabled := b.tka != nil
|
||||
wantEnabled := nm.TKAEnabled
|
||||
if isEnabled != wantEnabled {
|
||||
var ourHead tka.AUMHash
|
||||
if b.tka != nil {
|
||||
ourHead = b.tka.authority.Head()
|
||||
}
|
||||
|
||||
// Regardless of whether we are moving to disabled or enabled, we
|
||||
// need information from the tka bootstrap endpoint.
|
||||
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
|
||||
b.mu.Unlock()
|
||||
bs, err := b.tkaFetchBootstrap(ourNodeKey, ourHead)
|
||||
b.mu.Lock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching bootstrap: %v", err)
|
||||
}
|
||||
|
||||
if wantEnabled && !isEnabled {
|
||||
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM); err != nil {
|
||||
return fmt.Errorf("bootstrap: %v", err)
|
||||
}
|
||||
isEnabled = true
|
||||
} else if !wantEnabled && isEnabled {
|
||||
if b.tka.authority.ValidDisablement(bs.DisablementSecret) {
|
||||
b.tka = nil
|
||||
isEnabled = false
|
||||
|
||||
if err := os.RemoveAll(b.chonkPath()); err != nil {
|
||||
return fmt.Errorf("os.RemoveAll: %v", err)
|
||||
}
|
||||
} else {
|
||||
b.logf("Disablement secret did not verify, leaving TKA enabled.")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
|
||||
}
|
||||
}
|
||||
|
||||
if isEnabled && b.tka.authority.Head() != nm.TKAHead {
|
||||
// TODO(tom): Implement sync
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// chonkPath returns the absolute path to the directory in which TKA
|
||||
// state (the 'tailchonk') is stored.
|
||||
func (b *LocalBackend) chonkPath() string {
|
||||
return filepath.Join(b.TailscaleVarRoot(), "tka")
|
||||
}
|
||||
|
||||
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
|
||||
// tailnet key authority, based on the given genesis AUM.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) error {
|
||||
if !b.CanSupportNetworkLock() {
|
||||
return errors.New("network lock not supported in this configuration")
|
||||
}
|
||||
|
||||
var genesis tka.AUM
|
||||
if err := genesis.Unserialize(g); err != nil {
|
||||
return fmt.Errorf("reading genesis: %v", err)
|
||||
}
|
||||
|
||||
chonkDir := b.chonkPath()
|
||||
if err := os.Mkdir(chonkDir, 0755); err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("mkdir: %v", err)
|
||||
}
|
||||
|
||||
chonk, err := tka.ChonkDir(chonkDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chonk: %v", err)
|
||||
}
|
||||
authority, err := tka.Bootstrap(chonk, genesis)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tka bootstrap: %v", err)
|
||||
}
|
||||
|
||||
b.tka = &tkaState{
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanSupportNetworkLock returns true if tailscaled is able to operate
|
||||
// a local tailnet key authority (and hence enforce network lock).
|
||||
func (b *LocalBackend) CanSupportNetworkLock() bool {
|
||||
@@ -200,15 +88,9 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
if !b.CanSupportNetworkLock() {
|
||||
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?")
|
||||
}
|
||||
|
||||
var ourNodeKey key.NodePublic
|
||||
b.mu.Lock()
|
||||
if b.prefs != nil {
|
||||
ourNodeKey = b.prefs.Persist.PrivateNodeKey.Public()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if ourNodeKey.IsZero() {
|
||||
return errors.New("no node-key: is tailscale logged in?")
|
||||
nm := b.NetMap()
|
||||
if nm == nil {
|
||||
return errors.New("no netmap: are you logged into tailscale?")
|
||||
}
|
||||
|
||||
// Generates a genesis AUM representing trust in the provided keys.
|
||||
@@ -230,7 +112,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
}
|
||||
|
||||
// Phase 1/2 of initialization: Transmit the genesis AUM to Control.
|
||||
initResp, err := b.tkaInitBegin(ourNodeKey, genesisAUM)
|
||||
initResp, err := b.tkaInitBegin(nm, genesisAUM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tka init-begin RPC: %w", err)
|
||||
}
|
||||
@@ -251,7 +133,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
}
|
||||
|
||||
// Finalize enablement by transmitting signature for all nodes to Control.
|
||||
_, err = b.tkaInitFinish(ourNodeKey, sigs)
|
||||
_, err = b.tkaInitFinish(nm, sigs)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -274,11 +156,10 @@ func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeK
|
||||
return &sig, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
||||
func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
NodeID: nm.SelfNode.ID,
|
||||
GenesisAUM: aum.Serialize(),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
@@ -316,11 +197,10 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
NodeID: nm.SelfNode.ID,
|
||||
Signatures: nks,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
@@ -357,51 +237,3 @@ func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
|
||||
// tkaFetchBootstrap sends a /machine/tka/bootstrap RPC to the control plane
|
||||
// over noise. This is used to get values necessary to enable or disable TKA.
|
||||
func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
|
||||
bootstrapReq := tailcfg.TKABootstrapRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
}
|
||||
if !head.IsZero() {
|
||||
head, err := head.MarshalText()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("head.MarshalText failed: %v", err)
|
||||
}
|
||||
bootstrapReq.Head = string(head)
|
||||
}
|
||||
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(bootstrapReq); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("ctx: %w", err)
|
||||
}
|
||||
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/bootstrap", &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
res, err := b.DoNoiseRequest(req2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKABootstrapResponse)
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
)
|
||||
|
||||
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
k := key.NewMachine()
|
||||
opts := controlclient.Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
HTTPTestClient: c,
|
||||
NoiseTestClient: c,
|
||||
Status: func(controlclient.Status) {},
|
||||
}
|
||||
|
||||
cc, err := controlclient.NewNoStart(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
// NOTE: URLs must have a https scheme and example.com domain to work with the underlying
|
||||
// httptest plumbing, despite the domain being unused in the actual noise request transport.
|
||||
func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *http.Client) {
|
||||
ts := httptest.NewUnstartedServer(handler)
|
||||
ts.StartTLS()
|
||||
client := ts.Client()
|
||||
client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||
client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, ts.Listener.Addr().String())
|
||||
}
|
||||
return ts, client
|
||||
}
|
||||
|
||||
func TestTKAEnablementFlow(t *testing.T) {
|
||||
networkLockAvailable = func() bool { return true } // Enable the feature flag
|
||||
nodePriv := key.NewNode()
|
||||
|
||||
// Make a fake TKA authority, getting a usable genesis AUM which
|
||||
// our mock server can communicate.
|
||||
nlPriv := key.NewNLPrivate()
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
a1, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
|
||||
Keys: []tka.Key{key},
|
||||
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
case "/machine/tka/bootstrap":
|
||||
body := new(tailcfg.TKABootstrapRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Version != tailcfg.CurrentCapabilityVersion {
|
||||
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
if body.NodeKey != nodePriv.Public() {
|
||||
t.Errorf("bootstrap nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
|
||||
}
|
||||
if body.Head != "" {
|
||||
t.Errorf("bootstrap head=%s, want empty hash", body.Head)
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
out := tailcfg.TKABootstrapResponse{
|
||||
GenesisAUM: genesisAUM.Serialize(),
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
temp := t.TempDir()
|
||||
|
||||
cc := fakeControlClient(t, client)
|
||||
b := LocalBackend{
|
||||
varRoot: temp,
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
prefs: &ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
TKAEnabled: true,
|
||||
TKAHead: tka.AUMHash{},
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
if b.tka == nil {
|
||||
t.Fatal("tka was not initialized")
|
||||
}
|
||||
if b.tka.authority.Head() != a1.Head() {
|
||||
t.Errorf("authority.Head() = %x, want %x", b.tka.authority.Head(), a1.Head())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTKADisablementFlow(t *testing.T) {
|
||||
networkLockAvailable = func() bool { return true } // Enable the feature flag
|
||||
temp := t.TempDir()
|
||||
os.Mkdir(filepath.Join(temp, "tka"), 0755)
|
||||
nodePriv := key.NewNode()
|
||||
|
||||
// Make a fake TKA authority, to seed local state.
|
||||
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||
nlPriv := key.NewNLPrivate()
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
authority, _, err := tka.Create(chonk, tka.State{
|
||||
Keys: []tka.Key{key},
|
||||
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
returnWrongSecret := false
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
case "/machine/tka/bootstrap":
|
||||
body := new(tailcfg.TKABootstrapRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Version != tailcfg.CurrentCapabilityVersion {
|
||||
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
if body.NodeKey != nodePriv.Public() {
|
||||
t.Errorf("nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
|
||||
}
|
||||
var head tka.AUMHash
|
||||
if err := head.UnmarshalText([]byte(body.Head)); err != nil {
|
||||
t.Fatalf("failed unmarshal of body.Head: %v", err)
|
||||
}
|
||||
if head != authority.Head() {
|
||||
t.Errorf("reported head = %x, want %x", head, authority.Head())
|
||||
}
|
||||
|
||||
var disablement []byte
|
||||
if returnWrongSecret {
|
||||
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
|
||||
} else {
|
||||
disablement = disablementSecret
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
out := tailcfg.TKABootstrapResponse{
|
||||
DisablementSecret: disablement,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cc := fakeControlClient(t, client)
|
||||
b := LocalBackend{
|
||||
varRoot: temp,
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
},
|
||||
prefs: &ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}
|
||||
|
||||
// Test that the wrong disablement secret does not shut down the authority.
|
||||
returnWrongSecret = true
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
if b.tka == nil {
|
||||
t.Error("TKA was disabled despite incorrect disablement secret")
|
||||
}
|
||||
|
||||
// Test the correct disablement secret shuts down the authority.
|
||||
returnWrongSecret = false
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
|
||||
if b.tka != nil {
|
||||
t.Fatal("tka was not shut down")
|
||||
}
|
||||
if _, err := os.Stat(b.chonkPath()); err == nil || !os.IsNotExist(err) {
|
||||
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
|
||||
}
|
||||
}
|
||||
@@ -38,16 +38,15 @@ import (
|
||||
// running as root.
|
||||
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
||||
|
||||
// getSSHUsernames discovers and returns the list of usernames that are
|
||||
// potential Tailscale SSH user targets.
|
||||
//
|
||||
// Invariant: must not be called with b.mu held.
|
||||
func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
|
||||
res := new(tailcfg.C2NSSHUsernamesResponse)
|
||||
if !b.tailscaleSSHEnabled() {
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.sshServer == nil {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
max := 10
|
||||
if req != nil && req.Max != 0 {
|
||||
max = req.Max
|
||||
@@ -71,8 +70,8 @@ func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*ta
|
||||
res.Usernames = append(res.Usernames, u)
|
||||
}
|
||||
|
||||
if opUser := b.operatorUserName(); opUser != "" {
|
||||
add(opUser)
|
||||
if b.prefs != nil && b.prefs.OperatorUser != "" {
|
||||
add(b.prefs.OperatorUser)
|
||||
}
|
||||
|
||||
// Check popular usernames and see if they exist with a real shell.
|
||||
|
||||
@@ -772,7 +772,7 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
|
||||
})
|
||||
|
||||
if root := b.TailscaleVarRoot(); root != "" {
|
||||
chonkDir := filepath.Join(root, "tka")
|
||||
chonkDir := filepath.Join(root, "chonk")
|
||||
if _, err := os.Stat(chonkDir); err == nil {
|
||||
// The directory exists, which means network-lock has been initialized.
|
||||
storage, err := tka.ChonkDir(chonkDir)
|
||||
|
||||
@@ -221,9 +221,6 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
if note := r.FormValue("note"); len(note) > 0 {
|
||||
h.logf("user bugreport note: %s", note)
|
||||
}
|
||||
if defBool(r.FormValue("diagnose"), false) {
|
||||
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintln(w, logMarker)
|
||||
}
|
||||
|
||||
47
licenses/win.md
Normal file
47
licenses/win.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Tailscale for Windows dependencies
|
||||
|
||||
The following open source dependencies are used to build the [Tailscale client
|
||||
for windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
|
||||
[Tailscale client for windows]: https://tailscale.com/kb/1022/install-windows/
|
||||
[Tailscale CLI]: ./tailscale.md
|
||||
|
||||
## Go Packages
|
||||
|
||||
|
||||
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0-rc.1/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
|
||||
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
|
||||
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/v1.0.0/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.15.5/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.15.5/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.15.5/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/lxn/walk](https://pkg.go.dev/github.com/lxn/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/ed127cfb919a/LICENSE))
|
||||
- [github.com/lxn/win](https://pkg.go.dev/github.com/lxn/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/c3f813abca9f/LICENSE))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.6.0/LICENSE.md))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.2.3/LICENSE.md))
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/0de741cf:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=415007cec224))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.4.10))
|
||||
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/f81723ceac3f/LICENSE))
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
- [Nullsoft Scriptable Install System](https://nsis.sourceforge.io/) ([zlib/libpng](https://nsis.sourceforge.io/License))
|
||||
- [Wintun](https://www.wintun.net/) ([Prebuilt Binaries License](https://git.zx2c4.com/wintun/tree/prebuilt-binaries-license.txt))
|
||||
- [wireguard-windows](https://git.zx2c4.com/wireguard-windows/) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING))
|
||||
@@ -5,12 +5,9 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -100,44 +97,6 @@ func (a OSConfig) Equal(b OSConfig) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface to ensure that Hosts is
|
||||
// printed correctly (i.e. not as a bunch of pointers).
|
||||
//
|
||||
// Fixes https://github.com/tailscale/tailscale/issues/5669
|
||||
func (a OSConfig) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
w.WriteString(`{Nameservers:[`)
|
||||
for i, ns := range a.Nameservers {
|
||||
if i != 0 {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%+v", ns)
|
||||
}
|
||||
w.WriteString(`] SearchDomains:[`)
|
||||
for i, domain := range a.SearchDomains {
|
||||
if i != 0 {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%+v", domain)
|
||||
}
|
||||
w.WriteString(`] MatchDomains:[`)
|
||||
for i, domain := range a.MatchDomains {
|
||||
if i != 0 {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%+v", domain)
|
||||
}
|
||||
w.WriteString(`] Hosts:[`)
|
||||
for i, host := range a.Hosts {
|
||||
if i != 0 {
|
||||
w.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(w, "%+v", host)
|
||||
}
|
||||
w.WriteString(`]}`)
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// ErrGetBaseConfigNotSupported is the error
|
||||
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
|
||||
// doesn't support reading the underlying configuration out of the OS.
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
func TestOSConfigPrintable(t *testing.T) {
|
||||
ocfg := OSConfig{
|
||||
Hosts: []*HostEntry{
|
||||
{
|
||||
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 3}),
|
||||
Hosts: []string{"server", "client"},
|
||||
},
|
||||
{
|
||||
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 4}),
|
||||
Hosts: []string{"otherhost"},
|
||||
},
|
||||
},
|
||||
Nameservers: []netip.Addr{
|
||||
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
|
||||
},
|
||||
SearchDomains: []dnsname.FQDN{
|
||||
dnsname.FQDN("foo.beta.tailscale.net."),
|
||||
dnsname.FQDN("bar.beta.tailscale.net."),
|
||||
},
|
||||
MatchDomains: []dnsname.FQDN{
|
||||
dnsname.FQDN("ts.com."),
|
||||
},
|
||||
}
|
||||
s := fmt.Sprintf("%+v", ocfg)
|
||||
|
||||
const expected = `{Nameservers:[8.8.8.8] SearchDomains:[foo.beta.tailscale.net. bar.beta.tailscale.net.] MatchDomains:[ts.com.] Hosts:[&{Addr:100.1.2.3 Hosts:[server client]} &{Addr:100.1.2.4 Hosts:[otherhost]}]}`
|
||||
if s != expected {
|
||||
t.Errorf("format mismatch:\n got: %s\n want: %s", s, expected)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -113,10 +112,6 @@ type Report struct {
|
||||
GlobalV4 string // ip:port of global IPv4
|
||||
GlobalV6 string // [ip]:port of global IPv6
|
||||
|
||||
// CaptivePortal is set when we think there's a captive portal that is
|
||||
// intercepting HTTP traffic.
|
||||
CaptivePortal opt.Bool
|
||||
|
||||
// TODO: update Clone when adding new fields
|
||||
}
|
||||
|
||||
@@ -180,10 +175,6 @@ type Client struct {
|
||||
// If nil, portmap discovery is not done.
|
||||
PortMapper *portmapper.Client // lazily initialized on first use
|
||||
|
||||
// For tests
|
||||
testEnoughRegions int
|
||||
testCaptivePortalDelay time.Duration
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
nextFull bool // do a full region scan, even if last != nil
|
||||
prev map[time.Time]*Report // some previous reports
|
||||
@@ -201,9 +192,6 @@ type STUNConn interface {
|
||||
}
|
||||
|
||||
func (c *Client) enoughRegions() int {
|
||||
if c.testEnoughRegions > 0 {
|
||||
return c.testEnoughRegions
|
||||
}
|
||||
if c.Verbose {
|
||||
// Abuse verbose a bit here so netcheck can show all region latencies
|
||||
// in verbose mode.
|
||||
@@ -212,14 +200,6 @@ func (c *Client) enoughRegions() int {
|
||||
return 3
|
||||
}
|
||||
|
||||
func (c *Client) captivePortalDelay() time.Duration {
|
||||
if c.testCaptivePortalDelay > 0 {
|
||||
return c.testCaptivePortalDelay
|
||||
}
|
||||
// Chosen semi-arbitrarily
|
||||
return 200 * time.Millisecond
|
||||
}
|
||||
|
||||
func (c *Client) logf(format string, a ...any) {
|
||||
if c.Logf != nil {
|
||||
c.Logf(format, a...)
|
||||
@@ -803,35 +783,13 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
|
||||
}
|
||||
c.curState = rs
|
||||
last := c.last
|
||||
|
||||
// Even if we're doing a non-incremental update, we may want to try our
|
||||
// preferred DERP region for captive portal detection. Save that, if we
|
||||
// have it.
|
||||
var preferredDERP int
|
||||
if last != nil {
|
||||
preferredDERP = last.PreferredDERP
|
||||
}
|
||||
|
||||
now := c.timeNow()
|
||||
|
||||
doFull := false
|
||||
if c.nextFull || now.Sub(c.lastFull) > 5*time.Minute {
|
||||
doFull = true
|
||||
}
|
||||
// If the last report had a captive portal and reported no UDP access,
|
||||
// it's possible that we didn't get a useful netcheck due to the
|
||||
// captive portal blocking us. If so, make this report a full
|
||||
// (non-incremental) one.
|
||||
if !doFull && last != nil {
|
||||
doFull = !last.UDP && last.CaptivePortal.EqualBool(true)
|
||||
}
|
||||
if doFull {
|
||||
last = nil // causes makeProbePlan below to do a full (initial) plan
|
||||
c.nextFull = false
|
||||
c.lastFull = now
|
||||
metricNumGetReportFull.Add(1)
|
||||
}
|
||||
|
||||
rs.incremental = last != nil
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -916,48 +874,6 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
|
||||
|
||||
plan := makeProbePlan(dm, ifState, last)
|
||||
|
||||
// If we're doing a full probe, also check for a captive portal. We
|
||||
// delay by a bit to wait for UDP STUN to finish, to avoid the probe if
|
||||
// it's unnecessary.
|
||||
captivePortalDone := syncs.ClosedChan()
|
||||
captivePortalStop := func() {}
|
||||
if !rs.incremental {
|
||||
// NOTE(andrew): we can't simply add this goroutine to the
|
||||
// `NewWaitGroupChan` below, since we don't wait for that
|
||||
// waitgroup to finish when exiting this function and thus get
|
||||
// a data race.
|
||||
ch := make(chan struct{})
|
||||
captivePortalDone = ch
|
||||
|
||||
tmr := time.AfterFunc(c.captivePortalDelay(), func() {
|
||||
defer close(ch)
|
||||
found, err := c.checkCaptivePortal(ctx, dm, preferredDERP)
|
||||
if err != nil {
|
||||
c.logf("[v1] checkCaptivePortal: %v", err)
|
||||
return
|
||||
}
|
||||
rs.report.CaptivePortal.Set(found)
|
||||
})
|
||||
|
||||
captivePortalStop = func() {
|
||||
// Don't cancel our captive portal check if we're
|
||||
// explicitly doing a verbose netcheck.
|
||||
if c.Verbose {
|
||||
return
|
||||
}
|
||||
|
||||
if tmr.Stop() {
|
||||
// Stopped successfully; need to close the
|
||||
// signal channel ourselves.
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
|
||||
// Did not stop; do nothing and it'll finish by itself
|
||||
// and close the signal channel.
|
||||
}
|
||||
}
|
||||
|
||||
wg := syncs.NewWaitGroupChan()
|
||||
wg.Add(len(plan))
|
||||
for _, probeSet := range plan {
|
||||
@@ -978,17 +894,9 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
|
||||
case <-stunTimer.C:
|
||||
case <-ctx.Done():
|
||||
case <-wg.DoneChan():
|
||||
// All of our probes finished, so if we have >0 responses, we
|
||||
// stop our captive portal check.
|
||||
if rs.anyUDP() {
|
||||
captivePortalStop()
|
||||
}
|
||||
case <-rs.stopProbeCh:
|
||||
// Saw enough regions.
|
||||
c.vlogf("saw enough regions; not waiting for rest")
|
||||
// We can stop the captive portal check since we know that we
|
||||
// got a bunch of STUN responses.
|
||||
captivePortalStop()
|
||||
}
|
||||
|
||||
rs.waitHairCheck(ctx)
|
||||
@@ -1057,9 +965,6 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (_ *Report,
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Wait for captive portal check before finishing the report.
|
||||
<-captivePortalDone
|
||||
|
||||
return c.finishAndStoreReport(rs, dm), nil
|
||||
}
|
||||
|
||||
@@ -1074,54 +979,6 @@ func (c *Client) finishAndStoreReport(rs *reportState, dm *tailcfg.DERPMap) *Rep
|
||||
return report
|
||||
}
|
||||
|
||||
var noRedirectClient = &http.Client{
|
||||
// No redirects allowed
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
|
||||
// Remaining fields are the same as the default client.
|
||||
Transport: http.DefaultClient.Transport,
|
||||
Jar: http.DefaultClient.Jar,
|
||||
Timeout: http.DefaultClient.Timeout,
|
||||
}
|
||||
|
||||
// checkCaptivePortal reports whether or not we think the system is behind a
|
||||
// captive portal, detected by making a request to a URL that we know should
|
||||
// return a "204 No Content" response and checking if that's what we get.
|
||||
//
|
||||
// The boolean return is whether we think we have a captive portal.
|
||||
func (c *Client) checkCaptivePortal(ctx context.Context, dm *tailcfg.DERPMap, preferredDERP int) (bool, error) {
|
||||
defer noRedirectClient.CloseIdleConnections()
|
||||
|
||||
// If we have a preferred DERP region with more than one node, try
|
||||
// that; otherwise, pick a random one not marked as "Avoid".
|
||||
if preferredDERP == 0 || dm.Regions[preferredDERP] == nil ||
|
||||
(preferredDERP != 0 && len(dm.Regions[preferredDERP].Nodes) == 0) {
|
||||
rids := make([]int, 0, len(dm.Regions))
|
||||
for id, reg := range dm.Regions {
|
||||
if reg == nil || reg.Avoid || len(reg.Nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
rids = append(rids, id)
|
||||
}
|
||||
preferredDERP = rids[rand.Intn(len(rids))]
|
||||
}
|
||||
|
||||
node := dm.Regions[preferredDERP].Nodes[0]
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+node.HostName+"/generate_204", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
r, err := noRedirectClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
c.logf("[v2] checkCaptivePortal url=%q status_code=%d", req.URL.String(), r.StatusCode)
|
||||
|
||||
return r.StatusCode != 204, nil
|
||||
}
|
||||
|
||||
// runHTTPOnlyChecks is the netcheck done by environments that can
|
||||
// only do HTTP requests, such as ws/wasm.
|
||||
func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *reportState, dm *tailcfg.DERPMap) error {
|
||||
@@ -1188,8 +1045,6 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
|
||||
var ip netip.Addr
|
||||
|
||||
dc := derphttp.NewNetcheckClient(c.logf)
|
||||
defer dc.Close()
|
||||
|
||||
tlsConn, tcpConn, node, err := dc.DialRegionTLS(ctx, reg)
|
||||
if err != nil {
|
||||
return 0, ip, err
|
||||
@@ -1345,9 +1200,6 @@ func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
|
||||
if r.GlobalV6 != "" {
|
||||
fmt.Fprintf(w, " v6a=%v", r.GlobalV6)
|
||||
}
|
||||
if r.CaptivePortal != "" {
|
||||
fmt.Fprintf(w, " captiveportal=%v", r.CaptivePortal)
|
||||
}
|
||||
fmt.Fprintf(w, " derp=%v", r.PreferredDERP)
|
||||
if r.PreferredDERP != 0 {
|
||||
fmt.Fprintf(w, " derpdist=")
|
||||
|
||||
@@ -9,13 +9,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -117,9 +115,6 @@ func TestWorksWhenUDPBlocked(t *testing.T) {
|
||||
// OS IPv6 test is irrelevant here, accept whatever the current
|
||||
// machine has.
|
||||
want.OSHasIPv6 = r.OSHasIPv6
|
||||
// Captive portal test is irrelevant; accept what the current report
|
||||
// has.
|
||||
want.CaptivePortal = r.CaptivePortal
|
||||
|
||||
if !reflect.DeepEqual(r, want) {
|
||||
t.Errorf("mismatch\n got: %+v\nwant: %+v\n", r, want)
|
||||
@@ -666,57 +661,3 @@ func TestSortRegions(t *testing.T) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCaptivePortalWhenUDP(t *testing.T) {
|
||||
// Override noRedirectClient to handle the /generate_204 endpoint
|
||||
var generate204Called atomic.Bool
|
||||
tr := RoundTripFunc(func(req *http.Request) *http.Response {
|
||||
if !strings.HasSuffix(req.URL.String(), "/generate_204") {
|
||||
panic("bad URL: " + req.URL.String())
|
||||
}
|
||||
generate204Called.Store(true)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNoContent,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
})
|
||||
|
||||
oldTransport := noRedirectClient.Transport
|
||||
t.Cleanup(func() { noRedirectClient.Transport = oldTransport })
|
||||
noRedirectClient.Transport = tr
|
||||
|
||||
stunAddr, cleanup := stuntest.Serve(t)
|
||||
defer cleanup()
|
||||
|
||||
c := &Client{
|
||||
Logf: t.Logf,
|
||||
UDPBindAddr: "127.0.0.1:0",
|
||||
testEnoughRegions: 1,
|
||||
|
||||
// Set the delay long enough that we have time to cancel it
|
||||
// when our STUN probe succeeds.
|
||||
testCaptivePortalDelay: 10 * time.Second,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r, err := c.GetReport(ctx, stuntest.DERPMapOf(stunAddr.String()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should not have called our captive portal function.
|
||||
if generate204Called.Load() {
|
||||
t.Errorf("captive portal check called; expected no call")
|
||||
}
|
||||
if r.CaptivePortal != "" {
|
||||
t.Errorf("got CaptivePortal=%q, want empty", r.CaptivePortal)
|
||||
}
|
||||
}
|
||||
|
||||
type RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (l *Listener) Accept() (net.Conn, error) {
|
||||
// The provided Context must be non-nil. If the context expires before the
|
||||
// connection is complete, an error is returned. Once successfully connected
|
||||
// any expiration of the context will not affect the connection.
|
||||
func (l *Listener) Dial(ctx context.Context, network, addr string) (_ net.Conn, err error) {
|
||||
func (l *Listener) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if !strings.HasSuffix(network, "tcp") {
|
||||
return nil, net.UnknownNetworkError(network)
|
||||
}
|
||||
@@ -72,13 +72,6 @@ func (l *Listener) Dial(ctx context.Context, network, addr string) (_ net.Conn,
|
||||
}
|
||||
}
|
||||
c, s := NewConn(addr, bufferSize)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.Close()
|
||||
s.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
const unknown = ipproto.Unknown
|
||||
|
||||
// RFC1858: prevent overlapping fragment attacks.
|
||||
const minFragBlks = (60 + 20) / 8 // max IPv4 header + basic TCP header in fragment blocks (8 bytes each)
|
||||
const minFrag = 60 + 20 // max IPv4 header + basic TCP header
|
||||
|
||||
type TCPFlag uint8
|
||||
|
||||
@@ -152,12 +152,11 @@ func (q *Parsed) decode4(b []byte) {
|
||||
// it as Unknown. We can also treat any subsequent fragment that starts
|
||||
// at such a low offset as Unknown.
|
||||
fragFlags := binary.BigEndian.Uint16(b[6:8])
|
||||
moreFrags := (fragFlags & 0x2000) != 0
|
||||
moreFrags := (fragFlags & 0x20) != 0
|
||||
fragOfs := fragFlags & 0x1FFF
|
||||
|
||||
if fragOfs == 0 {
|
||||
// This is the first fragment
|
||||
if moreFrags && len(sub) < minFragBlks {
|
||||
if moreFrags && len(sub) < minFrag {
|
||||
// Suspiciously short first fragment, dump it.
|
||||
q.IPProto = unknown
|
||||
return
|
||||
@@ -217,7 +216,7 @@ func (q *Parsed) decode4(b []byte) {
|
||||
}
|
||||
} else {
|
||||
// This is a fragment other than the first one.
|
||||
if fragOfs < minFragBlks {
|
||||
if fragOfs < minFrag {
|
||||
// First frag was suspiciously short, so we can't
|
||||
// trust the followup either.
|
||||
q.IPProto = unknown
|
||||
|
||||
@@ -37,9 +37,9 @@ func mustIPPort(s string) netip.AddrPort {
|
||||
var icmp4RequestBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x27, 0xde, 0xad, 0x00, 0x00, 0x40, 0x01, 0x8c, 0x15,
|
||||
// source IP
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination IP
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// ICMP header
|
||||
0x08, 0x00, 0x7d, 0x22,
|
||||
@@ -61,9 +61,9 @@ var icmp4RequestDecode = Parsed{
|
||||
|
||||
var icmp4ReplyBuffer = []byte{
|
||||
0x45, 0x00, 0x00, 0x25, 0x21, 0x52, 0x00, 0x00, 0x40, 0x01, 0x49, 0x73,
|
||||
// source IP
|
||||
// source ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// destination IP
|
||||
// destination ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// ICMP header
|
||||
0x00, 0x00, 0xe6, 0x9e,
|
||||
@@ -119,9 +119,9 @@ var unknownPacketDecode = Parsed{
|
||||
var tcp4PacketBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x37, 0xde, 0xad, 0x00, 0x00, 0x40, 0x06, 0x49, 0x5f,
|
||||
// source IP
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination IP
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// TCP header with SYN, ACK set
|
||||
0x00, 0x7b, 0x02, 0x37, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00,
|
||||
@@ -172,9 +172,9 @@ var tcp6RequestDecode = Parsed{
|
||||
var udp4RequestBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x2b, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
|
||||
// source IP
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination IP
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// UDP header
|
||||
0x00, 0x7b, 0x02, 0x37, 0x00, 0x17, 0x72, 0x1d,
|
||||
@@ -197,9 +197,9 @@ var udp4RequestDecode = Parsed{
|
||||
var invalid4RequestBuffer = []byte{
|
||||
// IP header up to checksum. IHL field points beyond end of packet.
|
||||
0x4a, 0x00, 0x00, 0x14, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
|
||||
// source IP
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination IP
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
}
|
||||
|
||||
@@ -244,9 +244,9 @@ var udp6RequestDecode = Parsed{
|
||||
var udp4ReplyBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x29, 0x21, 0x52, 0x00, 0x00, 0x40, 0x11, 0x49, 0x5f,
|
||||
// source IP
|
||||
// source ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// destination IP
|
||||
// destination ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// UDP header
|
||||
0x02, 0x37, 0x00, 0x7b, 0x00, 0x15, 0xd3, 0x9d,
|
||||
@@ -265,59 +265,6 @@ var udp4ReplyDecode = Parsed{
|
||||
Dst: mustIPPort("5.6.7.8:123"),
|
||||
}
|
||||
|
||||
// First TCP fragment of a packet with leading 24 bytes of 'a's
|
||||
var tcp4MediumFragmentBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x20, 0x00, 0x4c, 0x2c, 0x62, 0x20, 0x00, 0x22, 0x06, 0x3a, 0x0f,
|
||||
// source IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// TCP header
|
||||
0x00, 0x50, 0xf3, 0x8c, 0x58, 0xad, 0x60, 0x94, 0x25, 0xe4, 0x23, 0xa8, 0x80,
|
||||
0x10, 0x01, 0xfd, 0xc6, 0x6e, 0x00, 0x00, 0x01, 0x01, 0x08, 0x0a, 0xff, 0x60,
|
||||
0xfb, 0xfe, 0xba, 0x31, 0x78, 0x6a,
|
||||
// data
|
||||
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
|
||||
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
|
||||
}
|
||||
|
||||
var tcp4MediumFragmentDecode = Parsed{
|
||||
b: tcp4MediumFragmentBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 52,
|
||||
length: len(tcp4MediumFragmentBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: TCP,
|
||||
Src: mustIPPort("1.2.3.4:80"),
|
||||
Dst: mustIPPort("5.6.7.8:62348"),
|
||||
TCPFlags: 0x10,
|
||||
}
|
||||
|
||||
var tcp4ShortFragmentBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x20, 0x00, 0x1e, 0x2c, 0x62, 0x20, 0x00, 0x22, 0x06, 0x3c, 0x4f,
|
||||
// source IP
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination IP
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// partial TCP header
|
||||
0x00, 0x50, 0xf3, 0x8c, 0x58, 0xad, 0x60, 0x94, 0x00, 0x00,
|
||||
}
|
||||
|
||||
var tcp4ShortFragmentDecode = Parsed{
|
||||
b: tcp4ShortFragmentBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 0,
|
||||
length: len(tcp4ShortFragmentBuffer),
|
||||
// short fragments are rejected (marked unknown) to avoid header attacks as described in RFC 1858
|
||||
IPProto: ipproto.Unknown,
|
||||
IPVersion: 4,
|
||||
Src: mustIPPort("1.2.3.4:0"),
|
||||
Dst: mustIPPort("5.6.7.8:0"),
|
||||
}
|
||||
|
||||
var igmpPacketBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x46, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x02, 0x41, 0x22,
|
||||
@@ -457,8 +404,6 @@ func TestDecode(t *testing.T) {
|
||||
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
|
||||
{"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode},
|
||||
{"ipv4_sctp", sctpBuffer, sctpDecode},
|
||||
{"ipv4_frag", tcp4MediumFragmentBuffer, tcp4MediumFragmentDecode},
|
||||
{"ipv4_fragtooshort", tcp4ShortFragmentBuffer, tcp4ShortFragmentDecode},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package routetable provides functions that operate on the system's route
|
||||
// table.
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultRouteIPv4 = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 0)}
|
||||
defaultRouteIPv6 = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 0)}
|
||||
)
|
||||
|
||||
// RouteEntry contains common cross-platform fields describing an entry in the
|
||||
// system route table.
|
||||
type RouteEntry struct {
|
||||
// Family is the IP family of the route; it will be either 4 or 6.
|
||||
Family int
|
||||
// Type is the type of this route.
|
||||
Type RouteType
|
||||
// Dst is the destination of the route.
|
||||
Dst RouteDestination
|
||||
// Gatewayis the gateway address specified for this route.
|
||||
// This value will be invalid (where !r.Gateway.IsValid()) in cases
|
||||
// where there is no gateway address for this route.
|
||||
Gateway netip.Addr
|
||||
// Interface is the name of the network interface to use when sending
|
||||
// packets that match this route. This field can be empty.
|
||||
Interface string
|
||||
// Sys contains platform-specific information about this route.
|
||||
Sys any
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface.
|
||||
func (r RouteEntry) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
switch r.Family {
|
||||
case 4:
|
||||
fmt.Fprintf(w, "{Family: IPv4")
|
||||
case 6:
|
||||
fmt.Fprintf(w, "{Family: IPv6")
|
||||
default:
|
||||
fmt.Fprintf(w, "{Family: unknown(%d)", r.Family)
|
||||
}
|
||||
|
||||
// Match 'ip route' and other tools by not printing the route
|
||||
// type if it's a unicast route.
|
||||
if r.Type != RouteTypeUnicast {
|
||||
fmt.Fprintf(w, ", Type: %s", r.Type)
|
||||
}
|
||||
|
||||
if r.Dst.IsValid() {
|
||||
fmt.Fprintf(w, ", Dst: %s", r.Dst)
|
||||
} else {
|
||||
w.WriteString(", Dst: invalid")
|
||||
}
|
||||
|
||||
if r.Gateway.IsValid() {
|
||||
fmt.Fprintf(w, ", Gateway: %s", r.Gateway)
|
||||
}
|
||||
|
||||
if r.Interface != "" {
|
||||
fmt.Fprintf(w, ", Interface: %s", r.Interface)
|
||||
}
|
||||
|
||||
if r.Sys != nil {
|
||||
var formatVerb string
|
||||
switch {
|
||||
case f.Flag('#'):
|
||||
formatVerb = "%#v"
|
||||
case f.Flag('+'):
|
||||
formatVerb = "%+v"
|
||||
default:
|
||||
formatVerb = "%v"
|
||||
}
|
||||
fmt.Fprintf(w, ", Sys: "+formatVerb, r.Sys)
|
||||
}
|
||||
|
||||
w.WriteString("}")
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// RouteDestination is the destination of a route.
|
||||
//
|
||||
// This is similar to net/netip.Prefix, but also contains an optional IPv6
|
||||
// zone.
|
||||
type RouteDestination struct {
|
||||
netip.Prefix
|
||||
Zone string
|
||||
}
|
||||
|
||||
func (r RouteDestination) String() string {
|
||||
ip := r.Prefix.Addr()
|
||||
if r.Zone != "" {
|
||||
ip = ip.WithZone(r.Zone)
|
||||
}
|
||||
return ip.String() + "/" + strconv.Itoa(r.Prefix.Bits())
|
||||
}
|
||||
|
||||
// RouteType describes the type of a route.
|
||||
type RouteType int
|
||||
|
||||
const (
|
||||
// RouteTypeUnspecified is the unspecified route type.
|
||||
RouteTypeUnspecified RouteType = iota
|
||||
// RouteTypeLocal indicates that the destination of this route is an
|
||||
// address that belongs to this system.
|
||||
RouteTypeLocal
|
||||
// RouteTypeUnicast indicates that the destination of this route is a
|
||||
// "regular" address--one that neither belongs to this host, nor is a
|
||||
// broadcast/multicast/etc. address.
|
||||
RouteTypeUnicast
|
||||
// RouteTypeBroadcast indicates that the destination of this route is a
|
||||
// broadcast address.
|
||||
RouteTypeBroadcast
|
||||
// RouteTypeMulticast indicates that the destination of this route is a
|
||||
// multicast address.
|
||||
RouteTypeMulticast
|
||||
// RouteTypeOther indicates that the route is of some other valid type;
|
||||
// see the Sys field for the OS-provided route information to determine
|
||||
// the exact type.
|
||||
RouteTypeOther
|
||||
)
|
||||
|
||||
func (r RouteType) String() string {
|
||||
switch r {
|
||||
case RouteTypeUnspecified:
|
||||
return "unspecified"
|
||||
case RouteTypeLocal:
|
||||
return "local"
|
||||
case RouteTypeUnicast:
|
||||
return "unicast"
|
||||
case RouteTypeBroadcast:
|
||||
return "broadcast"
|
||||
case RouteTypeMulticast:
|
||||
return "multicast"
|
||||
case RouteTypeOther:
|
||||
return "other"
|
||||
default:
|
||||
return "invalid"
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type RouteEntryBSD struct {
|
||||
// GatewayInterface is the name of the interface specified as a gateway
|
||||
// for this route, if any.
|
||||
GatewayInterface string
|
||||
// GatewayIdx is the index of the interface specified as a gateway for
|
||||
// this route, if any.
|
||||
GatewayIdx int
|
||||
// GatewayAddr is the link-layer address of the gateway for this route,
|
||||
// if any.
|
||||
GatewayAddr string
|
||||
// Flags contains a string representation of common flags for this
|
||||
// route.
|
||||
Flags []string
|
||||
// RawFlags contains the raw flags that were returned by the operating
|
||||
// system for this route.
|
||||
RawFlags int
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface.
|
||||
func (r RouteEntryBSD) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
var pstart bool
|
||||
pr := func(format string, args ...any) {
|
||||
if pstart {
|
||||
fmt.Fprintf(w, ", "+format, args...)
|
||||
} else {
|
||||
fmt.Fprintf(w, format, args...)
|
||||
pstart = true
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteString("{")
|
||||
if r.GatewayInterface != "" {
|
||||
pr("GatewayInterface: %s", r.GatewayInterface)
|
||||
}
|
||||
if r.GatewayIdx > 0 {
|
||||
pr("GatewayIdx: %d", r.GatewayIdx)
|
||||
}
|
||||
if r.GatewayAddr != "" {
|
||||
pr("GatewayAddr: %s", r.GatewayAddr)
|
||||
}
|
||||
pr("Flags: %v", r.Flags)
|
||||
|
||||
w.WriteString("}")
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// ipFromRMAddr returns a netip.Addr converted from one of the
|
||||
// route.Inet{4,6}Addr types.
|
||||
func ipFromRMAddr(ifs map[int]interfaces.Interface, addr any) netip.Addr {
|
||||
switch v := addr.(type) {
|
||||
case *route.Inet4Addr:
|
||||
return netip.AddrFrom4(v.IP)
|
||||
|
||||
case *route.Inet6Addr:
|
||||
ip := netip.AddrFrom16(v.IP)
|
||||
if v.ZoneID != 0 {
|
||||
if iif, ok := ifs[v.ZoneID]; ok {
|
||||
ip = ip.WithZone(iif.Name)
|
||||
} else {
|
||||
ip = ip.WithZone(fmt.Sprint(v.ZoneID))
|
||||
}
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
// populateGateway populates gateway fields on a RouteEntry/RouteEntryBSD.
|
||||
func populateGateway(re *RouteEntry, reSys *RouteEntryBSD, ifs map[int]interfaces.Interface, addr any) {
|
||||
// If the address type has a valid IP, use that.
|
||||
if ip := ipFromRMAddr(ifs, addr); ip.IsValid() {
|
||||
re.Gateway = ip
|
||||
return
|
||||
}
|
||||
|
||||
switch v := addr.(type) {
|
||||
case *route.LinkAddr:
|
||||
reSys.GatewayIdx = v.Index
|
||||
if iif, ok := ifs[v.Index]; ok {
|
||||
reSys.GatewayInterface = iif.Name
|
||||
}
|
||||
var sb strings.Builder
|
||||
for i, x := range v.Addr {
|
||||
if i != 0 {
|
||||
sb.WriteByte(':')
|
||||
}
|
||||
fmt.Fprintf(&sb, "%02x", x)
|
||||
}
|
||||
reSys.GatewayAddr = sb.String()
|
||||
}
|
||||
}
|
||||
|
||||
// populateDestination populates the 'Dst' field on a RouteEntry based on the
|
||||
// RouteMessage's destination and netmask fields.
|
||||
func populateDestination(re *RouteEntry, ifs map[int]interfaces.Interface, rm *route.RouteMessage) {
|
||||
dst := rm.Addrs[unix.RTAX_DST]
|
||||
if dst == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ip := ipFromRMAddr(ifs, dst)
|
||||
if !ip.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
if ip.Is4() {
|
||||
re.Family = 4
|
||||
} else {
|
||||
re.Family = 6
|
||||
}
|
||||
re.Dst = RouteDestination{
|
||||
Prefix: netip.PrefixFrom(ip, 32), // default if nothing more specific
|
||||
}
|
||||
|
||||
// If the RTF_HOST flag is set, then this is a host route and there's
|
||||
// no netmask in this RouteMessage.
|
||||
if rm.Flags&unix.RTF_HOST != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// As above if there's no netmask in the list of addrs
|
||||
if len(rm.Addrs) < unix.RTAX_NETMASK || rm.Addrs[unix.RTAX_NETMASK] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
nm := ipFromRMAddr(ifs, rm.Addrs[unix.RTAX_NETMASK])
|
||||
if !ip.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
// Count the number of bits in the netmask IP and use that to make our prefix.
|
||||
ones, _ /* bits */ := net.IPMask(nm.AsSlice()).Size()
|
||||
|
||||
// Print this ourselves instead of using netip.Prefix so that we don't
|
||||
// lose the zone (since netip.Prefix strips that).
|
||||
//
|
||||
// NOTE(andrew): this doesn't print the same values as the 'netstat' tool
|
||||
// for some addresses on macOS, and I have no idea why. Specifically,
|
||||
// 'netstat -rn' will show something like:
|
||||
// ff00::/8 ::1 UmCI lo0
|
||||
//
|
||||
// But we will get:
|
||||
// destination=ff00::/40 [...]
|
||||
//
|
||||
// The netmask that we get back from FetchRIB has 32 more bits in it
|
||||
// than netstat prints, but only for multicast routes.
|
||||
//
|
||||
// For consistency's sake, we're going to do the same here so that we
|
||||
// get the same values as netstat returns.
|
||||
if runtime.GOOS == "darwin" && ip.Is6() && ip.IsMulticast() && ones > 32 {
|
||||
ones -= 32
|
||||
}
|
||||
re.Dst = RouteDestination{
|
||||
Prefix: netip.PrefixFrom(ip, ones),
|
||||
Zone: ip.Zone(),
|
||||
}
|
||||
}
|
||||
|
||||
// routeEntryFromMsg returns a RouteEntry from a single route.Message
|
||||
// returned by the operating system.
|
||||
func routeEntryFromMsg(ifsByIdx map[int]interfaces.Interface, msg route.Message) (RouteEntry, bool) {
|
||||
rm, ok := msg.(*route.RouteMessage)
|
||||
if !ok {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
|
||||
// Ignore things that we don't understand
|
||||
if rm.Version < 3 || rm.Version > 5 {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
if rm.Type != rmExpectedType {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
if len(rm.Addrs) < unix.RTAX_GATEWAY {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
|
||||
if rm.Flags&skipFlags != 0 {
|
||||
return RouteEntry{}, false
|
||||
}
|
||||
|
||||
reSys := RouteEntryBSD{
|
||||
RawFlags: rm.Flags,
|
||||
}
|
||||
for fv, fs := range flags {
|
||||
if rm.Flags&fv == fv {
|
||||
reSys.Flags = append(reSys.Flags, fs)
|
||||
}
|
||||
}
|
||||
sort.Strings(reSys.Flags)
|
||||
|
||||
re := RouteEntry{}
|
||||
hasFlag := func(f int) bool { return rm.Flags&f != 0 }
|
||||
switch {
|
||||
case hasFlag(unix.RTF_LOCAL):
|
||||
re.Type = RouteTypeLocal
|
||||
case hasFlag(unix.RTF_BROADCAST):
|
||||
re.Type = RouteTypeBroadcast
|
||||
case hasFlag(unix.RTF_MULTICAST):
|
||||
re.Type = RouteTypeMulticast
|
||||
|
||||
// From the manpage: "host entry (net otherwise)"
|
||||
case !hasFlag(unix.RTF_HOST):
|
||||
re.Type = RouteTypeUnicast
|
||||
|
||||
default:
|
||||
re.Type = RouteTypeOther
|
||||
}
|
||||
populateDestination(&re, ifsByIdx, rm)
|
||||
if unix.RTAX_GATEWAY < len(rm.Addrs) {
|
||||
populateGateway(&re, &reSys, ifsByIdx, rm.Addrs[unix.RTAX_GATEWAY])
|
||||
}
|
||||
|
||||
if outif, ok := ifsByIdx[rm.Index]; ok {
|
||||
re.Interface = outif.Name
|
||||
}
|
||||
|
||||
re.Sys = reSys
|
||||
return re, true
|
||||
}
|
||||
|
||||
// Get returns route entries from the system route table, limited to at most
|
||||
// 'max' results.
|
||||
func Get(max int) ([]RouteEntry, error) {
|
||||
// Fetching the list of interfaces can race with fetching our route
|
||||
// table, but we do it anyway since it's helpful for debugging.
|
||||
ifs, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ifsByIdx := make(map[int]interfaces.Interface)
|
||||
for _, iif := range ifs {
|
||||
ifsByIdx[iif.Index] = iif
|
||||
}
|
||||
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, ribType, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgs, err := route.ParseRIB(parseType, rib)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []RouteEntry
|
||||
for _, m := range msgs {
|
||||
re, ok := routeEntryFromMsg(ifsByIdx, m)
|
||||
if ok {
|
||||
ret = append(ret, re)
|
||||
if len(ret) == max {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
func TestRouteEntryFromMsg(t *testing.T) {
|
||||
ifs := map[int]interfaces.Interface{
|
||||
1: {
|
||||
Interface: &net.Interface{
|
||||
Name: "iface0",
|
||||
},
|
||||
},
|
||||
2: {
|
||||
Interface: &net.Interface{
|
||||
Name: "tailscale0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ip4 := func(s string) *route.Inet4Addr {
|
||||
ip := netip.MustParseAddr(s)
|
||||
return &route.Inet4Addr{IP: ip.As4()}
|
||||
}
|
||||
ip6 := func(s string) *route.Inet6Addr {
|
||||
ip := netip.MustParseAddr(s)
|
||||
return &route.Inet6Addr{IP: ip.As16()}
|
||||
}
|
||||
ip6zone := func(s string, idx int) *route.Inet6Addr {
|
||||
ip := netip.MustParseAddr(s)
|
||||
return &route.Inet6Addr{IP: ip.As16(), ZoneID: idx}
|
||||
}
|
||||
link := func(idx int, addr string) *route.LinkAddr {
|
||||
if _, found := ifs[idx]; !found {
|
||||
panic("index not found")
|
||||
}
|
||||
|
||||
ret := &route.LinkAddr{
|
||||
Index: idx,
|
||||
}
|
||||
if addr != "" {
|
||||
ret.Addr = make([]byte, 6)
|
||||
fmt.Sscanf(addr, "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
&ret.Addr[0],
|
||||
&ret.Addr[1],
|
||||
&ret.Addr[2],
|
||||
&ret.Addr[3],
|
||||
&ret.Addr[4],
|
||||
&ret.Addr[5],
|
||||
)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
msg *route.RouteMessage
|
||||
want RouteEntry
|
||||
fail bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "BasicIPv4",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
ip4("1.2.3.1"), // gateway
|
||||
ip4("255.255.255.0"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
Gateway: netip.MustParseAddr("1.2.3.1"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BasicIPv6",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip6("fd7a:115c:a1e0::"), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("ffff:ffff:ffff::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fd7a:115c:a1e0::/48")},
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6WithZone",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip6zone("fe80::", 2), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("ffff:ffff:ffff:ffff::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast, // TODO
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fe80::/64"), Zone: "tailscale0"},
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6WithUnknownZone",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip6zone("fe80::", 4), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("ffff:ffff:ffff:ffff::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast, // TODO
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fe80::/64"), Zone: "4"},
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultIPv4",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("0.0.0.0"), // dst
|
||||
ip4("1.2.3.4"), // gateway
|
||||
ip4("0.0.0.0"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: defaultRouteIPv4,
|
||||
Gateway: netip.MustParseAddr("1.2.3.4"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DefaultIPv6",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip6("0::"), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("0::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: defaultRouteIPv6,
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShortAddrs",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/32")},
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TailscaleIPv4",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("100.64.0.0"), // dst
|
||||
link(2, ""),
|
||||
ip4("255.192.0.0"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
|
||||
Sys: RouteEntryBSD{
|
||||
GatewayInterface: "tailscale0",
|
||||
GatewayIdx: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Flags",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
ip4("1.2.3.1"), // gateway
|
||||
ip4("255.255.255.0"), // netmask
|
||||
},
|
||||
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY | unix.RTF_UP,
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
Gateway: netip.MustParseAddr("1.2.3.1"),
|
||||
Sys: RouteEntryBSD{
|
||||
Flags: []string{"gateway", "static", "up"},
|
||||
RawFlags: unix.RTF_STATIC | unix.RTF_GATEWAY | unix.RTF_UP,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SkipNoAddrs",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{},
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "SkipBadVersion",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 1,
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "SkipBadType",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType + 1,
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
name: "OutputIface",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Index: 1,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/32")},
|
||||
Interface: "iface0",
|
||||
Sys: RouteEntryBSD{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GatewayMAC",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("100.64.0.0"), // dst
|
||||
link(1, "01:02:03:04:05:06"),
|
||||
ip4("255.192.0.0"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
|
||||
Sys: RouteEntryBSD{
|
||||
GatewayAddr: "01:02:03:04:05:06",
|
||||
GatewayInterface: "iface0",
|
||||
GatewayIdx: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
testCases = append(testCases,
|
||||
testCase{
|
||||
name: "SkipFlags",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Addrs: []route.Addr{
|
||||
ip4("1.2.3.4"), // dst
|
||||
ip4("1.2.3.1"), // gateway
|
||||
ip4("255.255.255.0"), // netmask
|
||||
},
|
||||
Flags: unix.RTF_UP | skipFlags,
|
||||
},
|
||||
fail: true,
|
||||
},
|
||||
testCase{
|
||||
name: "NetmaskAdjust",
|
||||
msg: &route.RouteMessage{
|
||||
Version: 3,
|
||||
Type: rmExpectedType,
|
||||
Flags: unix.RTF_MULTICAST,
|
||||
Addrs: []route.Addr{
|
||||
ip6("ff00::"), // dst
|
||||
ip6("1234::"), // gateway
|
||||
ip6("ffff:ffff:ff00::"), // netmask
|
||||
},
|
||||
},
|
||||
want: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeMulticast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("ff00::/8")},
|
||||
Gateway: netip.MustParseAddr("1234::"),
|
||||
Sys: RouteEntryBSD{
|
||||
Flags: []string{"multicast"},
|
||||
RawFlags: unix.RTF_MULTICAST,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
re, ok := routeEntryFromMsg(ifs, tc.msg)
|
||||
if wantOk := !tc.fail; ok != wantOk {
|
||||
t.Fatalf("ok = %v; want %v", ok, wantOk)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(re, tc.want) {
|
||||
t.Fatalf("RouteEntry mismatch:\n got: %+v\nwant: %+v", re, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteEntryFormatting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
re RouteEntry
|
||||
want string
|
||||
}{
|
||||
{
|
||||
re: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.0/24")},
|
||||
Interface: "en0",
|
||||
Sys: RouteEntryBSD{
|
||||
GatewayInterface: "en0",
|
||||
Flags: []string{"static", "up"},
|
||||
},
|
||||
},
|
||||
want: `{Family: IPv4, Dst: 1.2.3.0/24, Interface: en0, Sys: {GatewayInterface: en0, Flags: [static up]}}`,
|
||||
},
|
||||
{
|
||||
re: RouteEntry{
|
||||
Family: 6,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fd7a:115c:a1e0::/24")},
|
||||
Interface: "en0",
|
||||
Sys: RouteEntryBSD{
|
||||
GatewayIdx: 3,
|
||||
Flags: []string{"static", "up"},
|
||||
},
|
||||
},
|
||||
want: `{Family: IPv6, Dst: fd7a:115c:a1e0::/24, Interface: en0, Sys: {GatewayIdx: 3, Flags: [static up]}}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got := fmt.Sprint(tc.re)
|
||||
if got != tc.want {
|
||||
t.Fatalf("RouteEntry.String() mismatch\n got: %q\nwant: %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRouteTable(t *testing.T) {
|
||||
routes, err := Get(1000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Basic assertion: we have at least one 'default' route
|
||||
var (
|
||||
hasDefault bool
|
||||
)
|
||||
for _, route := range routes {
|
||||
if route.Dst == defaultRouteIPv4 || route.Dst == defaultRouteIPv6 {
|
||||
hasDefault = true
|
||||
}
|
||||
}
|
||||
if !hasDefault {
|
||||
t.Errorf("expected at least one default route; routes=%v", routes)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package routetable
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
ribType = unix.NET_RT_DUMP2
|
||||
parseType = unix.NET_RT_IFLIST2
|
||||
rmExpectedType = unix.RTM_GET2
|
||||
|
||||
// Skip routes that were cloned from a parent
|
||||
skipFlags = unix.RTF_WASCLONED
|
||||
)
|
||||
|
||||
var flags = map[int]string{
|
||||
unix.RTF_BLACKHOLE: "blackhole",
|
||||
unix.RTF_BROADCAST: "broadcast",
|
||||
unix.RTF_GATEWAY: "gateway",
|
||||
unix.RTF_GLOBAL: "global",
|
||||
unix.RTF_HOST: "host",
|
||||
unix.RTF_IFSCOPE: "ifscope",
|
||||
unix.RTF_MULTICAST: "multicast",
|
||||
unix.RTF_REJECT: "reject",
|
||||
unix.RTF_ROUTER: "router",
|
||||
unix.RTF_STATIC: "static",
|
||||
unix.RTF_UP: "up",
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package routetable
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
ribType = unix.NET_RT_DUMP
|
||||
parseType = unix.NET_RT_IFLIST
|
||||
rmExpectedType = unix.RTM_GET
|
||||
|
||||
// Nothing to skip
|
||||
skipFlags = 0
|
||||
)
|
||||
|
||||
var flags = map[int]string{
|
||||
unix.RTF_BLACKHOLE: "blackhole",
|
||||
unix.RTF_BROADCAST: "broadcast",
|
||||
unix.RTF_GATEWAY: "gateway",
|
||||
unix.RTF_HOST: "host",
|
||||
unix.RTF_MULTICAST: "multicast",
|
||||
unix.RTF_REJECT: "reject",
|
||||
unix.RTF_STATIC: "static",
|
||||
unix.RTF_UP: "up",
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
"github.com/tailscale/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// RouteEntryLinux is the structure that makes up the Sys field of the
|
||||
// RouteEntry structure.
|
||||
type RouteEntryLinux struct {
|
||||
// Type is the raw type of the route.
|
||||
Type int
|
||||
// Table is the routing table index of this route.
|
||||
Table int
|
||||
// Src is the source of the route (if any).
|
||||
Src netip.Addr
|
||||
// Proto describes the source of the route--i.e. what caused this route
|
||||
// to be added to the route table.
|
||||
Proto netlink.RouteProtocol
|
||||
// Priority is the route's priority.
|
||||
Priority int
|
||||
// Scope is the route's scope.
|
||||
Scope int
|
||||
// InputInterfaceIdx is the input interface index.
|
||||
InputInterfaceIdx int
|
||||
// InputInterfaceName is the input interface name (if available).
|
||||
InputInterfaceName string
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface.
|
||||
func (r RouteEntryLinux) Format(f fmt.State, verb rune) {
|
||||
logger.ArgWriter(func(w *bufio.Writer) {
|
||||
// TODO(andrew): should we skip printing anything if type is unicast?
|
||||
fmt.Fprintf(w, "{Type: %s", r.TypeName())
|
||||
|
||||
// Match 'ip route' behaviour when printing these fields
|
||||
if r.Table != unix.RT_TABLE_MAIN {
|
||||
fmt.Fprintf(w, ", Table: %s", r.TableName())
|
||||
}
|
||||
if r.Proto != unix.RTPROT_BOOT {
|
||||
fmt.Fprintf(w, ", Proto: %s", r.Proto)
|
||||
}
|
||||
|
||||
if r.Src.IsValid() {
|
||||
fmt.Fprintf(w, ", Src: %s", r.Src)
|
||||
}
|
||||
if r.Priority != 0 {
|
||||
fmt.Fprintf(w, ", Priority: %d", r.Priority)
|
||||
}
|
||||
if r.Scope != unix.RT_SCOPE_UNIVERSE {
|
||||
fmt.Fprintf(w, ", Scope: %s", r.ScopeName())
|
||||
}
|
||||
if r.InputInterfaceName != "" {
|
||||
fmt.Fprintf(w, ", InputInterfaceName: %s", r.InputInterfaceName)
|
||||
} else if r.InputInterfaceIdx != 0 {
|
||||
fmt.Fprintf(w, ", InputInterfaceIdx: %d", r.InputInterfaceIdx)
|
||||
}
|
||||
w.WriteString("}")
|
||||
}).Format(f, verb)
|
||||
}
|
||||
|
||||
// TypeName returns the string representation of this route's Type.
|
||||
func (r RouteEntryLinux) TypeName() string {
|
||||
switch r.Type {
|
||||
case unix.RTN_UNSPEC:
|
||||
return "none"
|
||||
case unix.RTN_UNICAST:
|
||||
return "unicast"
|
||||
case unix.RTN_LOCAL:
|
||||
return "local"
|
||||
case unix.RTN_BROADCAST:
|
||||
return "broadcast"
|
||||
case unix.RTN_ANYCAST:
|
||||
return "anycast"
|
||||
case unix.RTN_MULTICAST:
|
||||
return "multicast"
|
||||
case unix.RTN_BLACKHOLE:
|
||||
return "blackhole"
|
||||
case unix.RTN_UNREACHABLE:
|
||||
return "unreachable"
|
||||
case unix.RTN_PROHIBIT:
|
||||
return "prohibit"
|
||||
case unix.RTN_THROW:
|
||||
return "throw"
|
||||
case unix.RTN_NAT:
|
||||
return "nat"
|
||||
case unix.RTN_XRESOLVE:
|
||||
return "xresolve"
|
||||
default:
|
||||
return strconv.Itoa(r.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the string representation of this route's Table.
|
||||
func (r RouteEntryLinux) TableName() string {
|
||||
switch r.Table {
|
||||
case unix.RT_TABLE_DEFAULT:
|
||||
return "default"
|
||||
case unix.RT_TABLE_MAIN:
|
||||
return "main"
|
||||
case unix.RT_TABLE_LOCAL:
|
||||
return "local"
|
||||
default:
|
||||
return strconv.Itoa(r.Table)
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeName returns the string representation of this route's Scope.
|
||||
func (r RouteEntryLinux) ScopeName() string {
|
||||
switch r.Scope {
|
||||
case unix.RT_SCOPE_UNIVERSE:
|
||||
return "global"
|
||||
case unix.RT_SCOPE_NOWHERE:
|
||||
return "nowhere"
|
||||
case unix.RT_SCOPE_HOST:
|
||||
return "host"
|
||||
case unix.RT_SCOPE_LINK:
|
||||
return "link"
|
||||
case unix.RT_SCOPE_SITE:
|
||||
return "site"
|
||||
default:
|
||||
return strconv.Itoa(r.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns route entries from the system route table, limited to at most
|
||||
// max results.
|
||||
func Get(max int) ([]RouteEntry, error) {
|
||||
// Fetching the list of interfaces can race with fetching our route
|
||||
// table, but we do it anyway since it's helpful for debugging.
|
||||
ifs, err := interfaces.GetList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ifsByIdx := make(map[int]interfaces.Interface)
|
||||
for _, iif := range ifs {
|
||||
ifsByIdx[iif.Index] = iif
|
||||
}
|
||||
|
||||
filter := &netlink.Route{}
|
||||
routes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL, filter, netlink.RT_FILTER_TABLE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []RouteEntry
|
||||
for _, route := range routes {
|
||||
if route.Family != netlink.FAMILY_V4 && route.Family != netlink.FAMILY_V6 {
|
||||
continue
|
||||
}
|
||||
|
||||
re := RouteEntry{}
|
||||
if route.Family == netlink.FAMILY_V4 {
|
||||
re.Family = 4
|
||||
} else {
|
||||
re.Family = 6
|
||||
}
|
||||
switch route.Type {
|
||||
case unix.RTN_UNSPEC:
|
||||
re.Type = RouteTypeUnspecified
|
||||
case unix.RTN_UNICAST:
|
||||
re.Type = RouteTypeUnicast
|
||||
case unix.RTN_LOCAL:
|
||||
re.Type = RouteTypeLocal
|
||||
case unix.RTN_BROADCAST:
|
||||
re.Type = RouteTypeBroadcast
|
||||
case unix.RTN_MULTICAST:
|
||||
re.Type = RouteTypeMulticast
|
||||
default:
|
||||
re.Type = RouteTypeOther
|
||||
}
|
||||
if route.Dst != nil {
|
||||
if d, ok := netaddr.FromStdIPNet(route.Dst); ok {
|
||||
re.Dst = RouteDestination{Prefix: d}
|
||||
}
|
||||
} else if route.Family == netlink.FAMILY_V4 {
|
||||
re.Dst = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 0)}
|
||||
} else {
|
||||
re.Dst = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 0)}
|
||||
}
|
||||
if gw := route.Gw; gw != nil {
|
||||
if gwa, ok := netip.AddrFromSlice(gw); ok {
|
||||
re.Gateway = gwa
|
||||
}
|
||||
}
|
||||
if outif, ok := ifsByIdx[route.LinkIndex]; ok {
|
||||
re.Interface = outif.Name
|
||||
} else if route.LinkIndex > 0 {
|
||||
re.Interface = fmt.Sprintf("link#%d", route.LinkIndex)
|
||||
}
|
||||
reSys := RouteEntryLinux{
|
||||
Type: route.Type,
|
||||
Table: route.Table,
|
||||
Proto: route.Protocol,
|
||||
Priority: route.Priority,
|
||||
Scope: int(route.Scope),
|
||||
InputInterfaceIdx: route.ILinkIndex,
|
||||
}
|
||||
if src, ok := netip.AddrFromSlice(route.Src); ok {
|
||||
reSys.Src = src
|
||||
}
|
||||
if iif, ok := ifsByIdx[route.ILinkIndex]; ok {
|
||||
reSys.InputInterfaceName = iif.Name
|
||||
}
|
||||
|
||||
re.Sys = reSys
|
||||
ret = append(ret, re)
|
||||
|
||||
// Stop after we've reached the maximum number of routes
|
||||
if len(ret) == max {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestGetRouteTable(t *testing.T) {
|
||||
routes, err := Get(1000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Basic assertion: we have at least one 'default' route in the main table
|
||||
var (
|
||||
hasDefault bool
|
||||
)
|
||||
for _, route := range routes {
|
||||
if route.Dst == defaultRouteIPv4 && route.Sys.(RouteEntryLinux).Table == unix.RT_TABLE_MAIN {
|
||||
hasDefault = true
|
||||
}
|
||||
}
|
||||
if !hasDefault {
|
||||
t.Errorf("expected at least one default route; routes=%v", routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteEntryFormatting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
re RouteEntry
|
||||
want string
|
||||
}{
|
||||
{
|
||||
re: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeMulticast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
|
||||
Gateway: netip.MustParseAddr("1.2.3.1"),
|
||||
Interface: "tailscale0",
|
||||
Sys: RouteEntryLinux{
|
||||
Type: unix.RTN_UNICAST,
|
||||
Table: 52,
|
||||
Proto: unix.RTPROT_STATIC,
|
||||
Src: netip.MustParseAddr("1.2.3.4"),
|
||||
Priority: 555,
|
||||
},
|
||||
},
|
||||
want: `{Family: IPv4, Type: multicast, Dst: 100.64.0.0/10, Gateway: 1.2.3.1, Interface: tailscale0, Sys: {Type: unicast, Table: 52, Proto: static, Src: 1.2.3.4, Priority: 555}}`,
|
||||
},
|
||||
{
|
||||
re: RouteEntry{
|
||||
Family: 4,
|
||||
Type: RouteTypeUnicast,
|
||||
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.0/24")},
|
||||
Gateway: netip.MustParseAddr("1.2.3.1"),
|
||||
Sys: RouteEntryLinux{
|
||||
Type: unix.RTN_UNICAST,
|
||||
Table: unix.RT_TABLE_MAIN,
|
||||
Proto: unix.RTPROT_BOOT,
|
||||
},
|
||||
},
|
||||
want: `{Family: IPv4, Dst: 1.2.3.0/24, Gateway: 1.2.3.1, Sys: {Type: unicast}}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got := fmt.Sprint(tc.re)
|
||||
if got != tc.want {
|
||||
t.Fatalf("RouteEntry.String() = %q; want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !linux && !darwin && !freebsd
|
||||
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var errUnsupported = errors.New("cannot get route table on platform " + runtime.GOOS)
|
||||
|
||||
func Get(max int) ([]RouteEntry, error) {
|
||||
return nil, errUnsupported
|
||||
}
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
blockSize = 2 * 1024 * 1024 // size of the block of data to send
|
||||
blockSize = 32000 // size of the block of data to send
|
||||
MinDuration = 5 * time.Second // minimum duration for a test
|
||||
DefaultDuration = MinDuration // default duration for a test
|
||||
MaxDuration = 30 * time.Second // maximum duration for a test
|
||||
version = 2 // value used when comparing client and server versions
|
||||
version = 1 // value used when comparing client and server versions
|
||||
increment = time.Second // increment to display results for, in seconds
|
||||
minInterval = 10 * time.Millisecond // minimum interval length for a result to be included
|
||||
DefaultPort = 20333
|
||||
@@ -37,14 +37,14 @@ type configResponse struct {
|
||||
|
||||
// This represents the Result of a speedtest within a specific interval
|
||||
type Result struct {
|
||||
Bytes int // number of bytes sent/received during the interval
|
||||
IntervalStart time.Time // start of the interval
|
||||
IntervalEnd time.Time // end of the interval
|
||||
Total bool // if true, this result struct represents the entire test, rather than a segment of the test
|
||||
Bytes int // number of bytes sent/received during the interval
|
||||
IntervalStart time.Duration // duration between the start of the interval and the start of the test
|
||||
IntervalEnd time.Duration // duration between the end of the interval and the start of the test
|
||||
Total bool // if true, this result struct represents the entire test, rather than a segment of the test
|
||||
}
|
||||
|
||||
func (r Result) MBitsPerSecond() float64 {
|
||||
return r.MegaBits() / r.IntervalEnd.Sub(r.IntervalStart).Seconds()
|
||||
return r.MegaBits() / (r.IntervalEnd - r.IntervalStart).Seconds()
|
||||
}
|
||||
|
||||
func (r Result) MegaBytes() float64 {
|
||||
@@ -56,7 +56,7 @@ func (r Result) MegaBits() float64 {
|
||||
}
|
||||
|
||||
func (r Result) Interval() time.Duration {
|
||||
return r.IntervalEnd.Sub(r.IntervalStart)
|
||||
return r.IntervalEnd - r.IntervalStart
|
||||
}
|
||||
|
||||
type Direction int
|
||||
|
||||
@@ -81,6 +81,9 @@ func doTest(conn net.Conn, conf config) ([]Result, error) {
|
||||
var currentTime time.Time
|
||||
var results []Result
|
||||
|
||||
startTime := time.Now()
|
||||
lastCalculated := startTime
|
||||
|
||||
if conf.Direction == Download {
|
||||
conn.SetReadDeadline(time.Now().Add(conf.TestDuration).Add(5 * time.Second))
|
||||
} else {
|
||||
@@ -91,9 +94,6 @@ func doTest(conn net.Conn, conf config) ([]Result, error) {
|
||||
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
lastCalculated := startTime
|
||||
|
||||
SpeedTestLoop:
|
||||
for {
|
||||
var n int
|
||||
@@ -110,37 +110,48 @@ SpeedTestLoop:
|
||||
return nil, fmt.Errorf("unexpected error has occurred: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Need to change the data a little bit, to avoid any compression.
|
||||
for i := range bufferData {
|
||||
bufferData[i]++
|
||||
}
|
||||
n, err = conn.Write(bufferData)
|
||||
if err != nil {
|
||||
// If the write failed, there is most likely something wrong with the connection.
|
||||
return nil, fmt.Errorf("upload failed: %w", err)
|
||||
}
|
||||
}
|
||||
currentTime = time.Now()
|
||||
intervalBytes += n
|
||||
|
||||
currentTime = time.Now()
|
||||
// checks if the current time is more or equal to the lastCalculated time plus the increment
|
||||
if currentTime.Sub(lastCalculated) >= increment {
|
||||
results = append(results, Result{Bytes: intervalBytes, IntervalStart: lastCalculated, IntervalEnd: currentTime, Total: false})
|
||||
if currentTime.After(lastCalculated.Add(increment)) {
|
||||
intervalStart := lastCalculated.Sub(startTime)
|
||||
intervalEnd := currentTime.Sub(startTime)
|
||||
if (intervalEnd - intervalStart) > minInterval {
|
||||
results = append(results, Result{Bytes: intervalBytes, IntervalStart: intervalStart, IntervalEnd: intervalEnd, Total: false})
|
||||
}
|
||||
lastCalculated = currentTime
|
||||
totalBytes += intervalBytes
|
||||
intervalBytes = 0
|
||||
}
|
||||
|
||||
if conf.Direction == Upload && currentTime.Sub(startTime) > conf.TestDuration {
|
||||
if conf.Direction == Upload && time.Since(startTime) > conf.TestDuration {
|
||||
break SpeedTestLoop
|
||||
}
|
||||
}
|
||||
|
||||
// get last segment
|
||||
if currentTime.Sub(lastCalculated) > minInterval {
|
||||
results = append(results, Result{Bytes: intervalBytes, IntervalStart: lastCalculated, IntervalEnd: currentTime, Total: false})
|
||||
intervalStart := lastCalculated.Sub(startTime)
|
||||
intervalEnd := currentTime.Sub(startTime)
|
||||
if (intervalEnd - intervalStart) > minInterval {
|
||||
results = append(results, Result{Bytes: intervalBytes, IntervalStart: intervalStart, IntervalEnd: intervalEnd, Total: false})
|
||||
}
|
||||
|
||||
// get total
|
||||
totalBytes += intervalBytes
|
||||
if currentTime.Sub(startTime) > minInterval {
|
||||
results = append(results, Result{Bytes: totalBytes, IntervalStart: startTime, IntervalEnd: currentTime, Total: true})
|
||||
intervalEnd = currentTime.Sub(startTime)
|
||||
if intervalEnd > minInterval {
|
||||
results = append(results, Result{Bytes: totalBytes, IntervalStart: 0, IntervalEnd: intervalEnd, Total: true})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
||||
@@ -7,7 +7,6 @@ package speedtest
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDownload(t *testing.T) {
|
||||
@@ -24,9 +23,9 @@ func TestDownload(t *testing.T) {
|
||||
type state struct {
|
||||
err error
|
||||
}
|
||||
displayResult := func(t *testing.T, r Result, start time.Time) {
|
||||
displayResult := func(t *testing.T, r Result) {
|
||||
t.Helper()
|
||||
t.Logf("{ Megabytes: %.2f, Start: %.1f, End: %.1f, Total: %t }", r.MegaBytes(), r.IntervalStart.Sub(start).Seconds(), r.IntervalEnd.Sub(start).Seconds(), r.Total)
|
||||
t.Logf("{ Megabytes: %.2f, Start: %.1f, End: %.1f, Total: %t }", r.MegaBytes(), r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.Total)
|
||||
}
|
||||
stateChan := make(chan state, 1)
|
||||
|
||||
@@ -50,9 +49,8 @@ func TestDownload(t *testing.T) {
|
||||
t.Fatalf("download results: expected length: %d, actual length: %d", expectedLen, len(results))
|
||||
}
|
||||
|
||||
start := results[0].IntervalStart
|
||||
for _, result := range results {
|
||||
displayResult(t, result, start)
|
||||
displayResult(t, result)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -68,9 +66,8 @@ func TestDownload(t *testing.T) {
|
||||
t.Fatalf("upload results: expected length: %d, actual length: %d", expectedLen, len(results))
|
||||
}
|
||||
|
||||
start := results[0].IntervalStart
|
||||
for _, result := range results {
|
||||
displayResult(t, result, start)
|
||||
displayResult(t, result)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -79,10 +79,7 @@ type CapabilityVersion int
|
||||
// - 40: 2022-08-22: added Node.KeySignature, PeersChangedPatch.KeySignature
|
||||
// - 41: 2022-08-30: uses 100.100.100.100 for route-less ExtraRecords if global nameservers is set
|
||||
// - 42: 2022-09-06: NextDNS DoH support; see https://github.com/tailscale/tailscale/pull/5556
|
||||
// - 43: 2022-09-21: clients can return usernames for SSH
|
||||
// - 44: 2022-09-22: MapResponse.ControlDialPlan
|
||||
// - 45: 2022-09-26: c2n /debug/{goroutines,prefs,metrics}
|
||||
const CurrentCapabilityVersion CapabilityVersion = 45
|
||||
const CurrentCapabilityVersion CapabilityVersion = 42
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -237,9 +234,6 @@ type Node struct {
|
||||
ComputedName string `json:",omitempty"` // MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
|
||||
computedHostIfDifferent string // hostname, if different than ComputedName, otherwise empty
|
||||
ComputedNameWithHost string `json:",omitempty"` // either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set
|
||||
|
||||
// DataPlaneAuditLogID is the per-node logtail ID used for data plane audit logging.
|
||||
DataPlaneAuditLogID string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// DisplayName returns the user-facing name for a node which should
|
||||
@@ -1378,47 +1372,9 @@ type MapResponse struct {
|
||||
// indicates no change from the value sent earlier.
|
||||
TKAInfo *TKAInfo `json:",omitempty"`
|
||||
|
||||
// DomainDataPlaneAuditLogID, if non-empty, is the per-tailnet log ID to be
|
||||
// used when writing data plane audit logs.
|
||||
DomainDataPlaneAuditLogID string `json:",omitempty"`
|
||||
|
||||
// Debug is normally nil, except for when the control server
|
||||
// is setting debug settings on a node.
|
||||
Debug *Debug `json:",omitempty"`
|
||||
|
||||
// ControlDialPlan tells the client how to connect to the control
|
||||
// server. An initial nil is equivalent to new(ControlDialPlan).
|
||||
// A subsequent streamed nil means no change.
|
||||
ControlDialPlan *ControlDialPlan `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ControlDialPlan is instructions from the control server to the client on how
|
||||
// to connect to the control server; this is useful for maintaining connection
|
||||
// if the client's network state changes after the initial connection, or due
|
||||
// to the configuration that the control server pushes.
|
||||
type ControlDialPlan struct {
|
||||
// An empty list means the default: use DNS (unspecified which DNS).
|
||||
Candidates []ControlIPCandidate
|
||||
}
|
||||
|
||||
// ControlIPCandidate represents a single candidate address to use when
|
||||
// connecting to the control server.
|
||||
type ControlIPCandidate struct {
|
||||
// IP is the address to attempt connecting to.
|
||||
IP netip.Addr
|
||||
|
||||
// DialStartSec is the number of seconds after the beginning of the
|
||||
// connection process to wait before trying this candidate.
|
||||
DialStartDelaySec float64 `json:",omitempty"`
|
||||
|
||||
// DialTimeoutSec is the timeout for a connection to this candidate,
|
||||
// starting after DialStartDelaySec.
|
||||
DialTimeoutSec float64 `json:",omitempty"`
|
||||
|
||||
// Priority is the relative priority of this candidate; candidates with
|
||||
// a higher priority are preferred over candidates with a lower
|
||||
// priority.
|
||||
Priority int `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Debug are instructions from the control server to the client
|
||||
@@ -1685,13 +1641,6 @@ type SetDNSRequest struct {
|
||||
// SetDNSResponse is the response to a SetDNSRequest.
|
||||
type SetDNSResponse struct{}
|
||||
|
||||
// HealthChangeRequest is the JSON request body type used to report
|
||||
// node health changes to https://<control>/machine/<mkey hex>/update-health.
|
||||
type HealthChangeRequest struct {
|
||||
Subsys string // a health.Subsystem value in string form
|
||||
Error string // or empty if cleared
|
||||
}
|
||||
|
||||
// SSHPolicy is the policy for how to handle incoming SSH connections
|
||||
// over Tailscale.
|
||||
type SSHPolicy struct {
|
||||
|
||||
@@ -95,7 +95,6 @@ var _NodeCloneNeedsRegeneration = Node(struct {
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
ComputedNameWithHost string
|
||||
DataPlaneAuditLogID string
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Hostinfo.
|
||||
|
||||
@@ -332,7 +332,6 @@ func TestNodeEqual(t *testing.T) {
|
||||
"LastSeen", "Online", "KeepAlive", "MachineAuthorized",
|
||||
"Capabilities",
|
||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||
"DataPlaneAuditLogID",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
|
||||
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
|
||||
@@ -173,7 +173,6 @@ func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthor
|
||||
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
|
||||
func (v NodeView) ComputedName() string { return v.ж.ComputedName }
|
||||
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
|
||||
func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID }
|
||||
func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
@@ -204,7 +203,6 @@ var _NodeViewNeedsRegeneration = Node(struct {
|
||||
ComputedName string
|
||||
computedHostIfDifferent string
|
||||
ComputedNameWithHost string
|
||||
DataPlaneAuditLogID string
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of Hostinfo.
|
||||
|
||||
@@ -12,11 +12,9 @@ import (
|
||||
// TKAInitBeginRequest submits a genesis AUM to seed the creation of the
|
||||
// tailnet's key authority.
|
||||
type TKAInitBeginRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
// NodeID is the node of the initiating client.
|
||||
// It must match the machine key being used to communicate over noise.
|
||||
NodeID NodeID
|
||||
|
||||
// GenesisAUM is the initial (genesis) AUM that the node generated
|
||||
// to bootstrap tailnet key authority state.
|
||||
@@ -60,11 +58,8 @@ type TKAInitBeginResponse struct {
|
||||
// This RPC finalizes initialization of the tailnet key authority
|
||||
// by submitting node-key signatures for all existing nodes.
|
||||
type TKAInitFinishRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
// NodeID is the node ID of the initiating client.
|
||||
NodeID NodeID
|
||||
|
||||
// Signatures are serialized tka.NodeKeySignatures for all nodes
|
||||
// in the tailnet.
|
||||
@@ -106,12 +101,6 @@ type TKAInfo struct {
|
||||
// TKABootstrapRequest is sent by a node to get information necessary for
|
||||
// enabling or disabling the tailnet key authority.
|
||||
type TKABootstrapRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
|
||||
// Head represents the node's head AUMHash (tka.Authority.Head), if
|
||||
// network lock is enabled.
|
||||
Head string
|
||||
@@ -131,12 +120,6 @@ type TKABootstrapResponse struct {
|
||||
// state (TKA). Values of type tka.AUMHash are encoded as strings in their
|
||||
// MarshalText form.
|
||||
type TKASyncOfferRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
|
||||
// Head represents the node's head AUMHash (tka.Authority.Head). This
|
||||
// corresponds to tka.SyncOffer.Head.
|
||||
Head string
|
||||
@@ -164,12 +147,6 @@ type TKASyncOfferResponse struct {
|
||||
// TKASyncSendRequest encodes AUMs that a node believes the control plane
|
||||
// is missing.
|
||||
type TKASyncSendRequest struct {
|
||||
// Version is the client's capabilities.
|
||||
Version CapabilityVersion
|
||||
|
||||
// NodeKey is the client's current node key.
|
||||
NodeKey key.NodePublic
|
||||
|
||||
// MissingAUMs encodes AUMs that the node believes the control plane
|
||||
// is missing.
|
||||
MissingAUMs []tkatype.MarshaledAUM
|
||||
|
||||
@@ -45,11 +45,6 @@ func (h AUMHash) MarshalText() ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// IsZero returns true if the hash is the empty value.
|
||||
func (h AUMHash) IsZero() bool {
|
||||
return h == (AUMHash{})
|
||||
}
|
||||
|
||||
// AUMKind describes valid AUM types.
|
||||
type AUMKind uint8
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ package tka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
@@ -93,62 +92,8 @@ func (b *UpdateBuilder) SetKeyMeta(keyID tkatype.KeyID, meta map[string]string)
|
||||
return b.mkUpdate(AUM{MessageKind: AUMUpdateKey, Meta: meta, KeyID: keyID})
|
||||
}
|
||||
|
||||
func (b *UpdateBuilder) generateCheckpoint() error {
|
||||
// Compute the checkpoint state.
|
||||
state := b.a.state
|
||||
for i, update := range b.out {
|
||||
var err error
|
||||
if state, err = state.applyVerifiedAUM(update); err != nil {
|
||||
return fmt.Errorf("applying update %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Checkpoints cant specify a parent AUM.
|
||||
state.LastAUMHash = nil
|
||||
return b.mkUpdate(AUM{MessageKind: AUMCheckpoint, State: &state})
|
||||
}
|
||||
|
||||
// checkpointEvery sets how often a checkpoint AUM should be generated.
|
||||
const checkpointEvery = 50
|
||||
|
||||
// Finalize returns the set of update message to actuate the update.
|
||||
func (b *UpdateBuilder) Finalize(storage Chonk) ([]AUM, error) {
|
||||
var (
|
||||
needCheckpoint bool = true
|
||||
cursor AUMHash = b.a.Head()
|
||||
)
|
||||
for i := len(b.out); i < checkpointEvery; i++ {
|
||||
aum, err := storage.AUM(cursor)
|
||||
if err != nil {
|
||||
if err == os.ErrNotExist {
|
||||
// The available chain is shorter than the interval to checkpoint at.
|
||||
needCheckpoint = false
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("reading AUM: %v", err)
|
||||
}
|
||||
|
||||
if aum.MessageKind == AUMCheckpoint {
|
||||
needCheckpoint = false
|
||||
break
|
||||
}
|
||||
|
||||
parent, hasParent := aum.Parent()
|
||||
if !hasParent {
|
||||
// We've hit the genesis update, so the chain is shorter than the interval to checkpoint at.
|
||||
needCheckpoint = false
|
||||
break
|
||||
}
|
||||
cursor = parent
|
||||
}
|
||||
|
||||
if needCheckpoint {
|
||||
if err := b.generateCheckpoint(); err != nil {
|
||||
return nil, fmt.Errorf("generating checkpoint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check no AUMs were applied in the meantime
|
||||
func (b *UpdateBuilder) Finalize() ([]AUM, error) {
|
||||
if len(b.out) > 0 {
|
||||
if parent, _ := b.out[0].Parent(); parent != b.a.Head() {
|
||||
return nil, fmt.Errorf("updates no longer apply to head: based on %x but head is %x", parent, b.a.Head())
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
if err != nil {
|
||||
t.Fatalf("Create() failed: %v", err)
|
||||
@@ -44,7 +44,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
|
||||
if err := b.AddKey(key2); err != nil {
|
||||
t.Fatalf("AddKey(%v) failed: %v", key2, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key, key2},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
if err != nil {
|
||||
t.Fatalf("Create() failed: %v", err)
|
||||
@@ -78,7 +78,7 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
|
||||
if err := b.RemoveKey(key2.ID()); err != nil {
|
||||
t.Fatalf("RemoveKey(%v) failed: %v", key2, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) {
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
if err != nil {
|
||||
t.Fatalf("Create() failed: %v", err)
|
||||
@@ -110,7 +110,7 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) {
|
||||
if err := b.SetKeyVote(key.ID(), 5); err != nil {
|
||||
t.Fatalf("SetKeyVote(%v) failed: %v", key.ID(), err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
if err != nil {
|
||||
t.Fatalf("Create() failed: %v", err)
|
||||
@@ -146,7 +146,7 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
|
||||
if err := b.SetKeyMeta(key.ID(), map[string]string{"b": "c"}); err != nil {
|
||||
t.Fatalf("SetKeyMeta(%v) failed: %v", key, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
if err != nil {
|
||||
t.Fatalf("Create() failed: %v", err)
|
||||
@@ -191,7 +191,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
|
||||
if err := b.RemoveKey(key.ID()); err != nil {
|
||||
t.Fatalf("RemoveKey(%v) failed: %v", key, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
updates, err := b.Finalize()
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
@@ -212,60 +212,3 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
|
||||
t.Errorf("GetKey(key).err = %v, want %v", err, ErrNoSuchKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorityBuilderCheckpointsAfterXUpdates(t *testing.T) {
|
||||
pub, priv := testingKey25519(t, 1)
|
||||
key := Key{Kind: Key25519, Public: pub, Votes: 2}
|
||||
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
if err != nil {
|
||||
t.Fatalf("Create() failed: %v", err)
|
||||
}
|
||||
|
||||
for i := 0; i <= checkpointEvery; i++ {
|
||||
pub2, _ := testingKey25519(t, int64(i+2))
|
||||
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
|
||||
|
||||
b := a.NewUpdater(signer25519(priv))
|
||||
if err := b.AddKey(key2); err != nil {
|
||||
t.Fatalf("AddKey(%v) failed: %v", key2, err)
|
||||
}
|
||||
updates, err := b.Finalize(storage)
|
||||
if err != nil {
|
||||
t.Fatalf("Finalize() failed: %v", err)
|
||||
}
|
||||
// See if the update is valid by applying it to the authority
|
||||
// + checking if the new key is there.
|
||||
if err := a.Inform(storage, updates); err != nil {
|
||||
t.Fatalf("could not apply generated updates: %v", err)
|
||||
}
|
||||
if _, err := a.state.GetKey(key2.ID()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantKind := AUMAddKey
|
||||
if i == checkpointEvery-1 { // Genesis + 49 updates == 50 (the value of checkpointEvery)
|
||||
wantKind = AUMCheckpoint
|
||||
}
|
||||
lastAUM, err := storage.AUM(a.Head())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if lastAUM.MessageKind != wantKind {
|
||||
t.Errorf("[%d] HeadAUM.MessageKind = %v, want %v", i, lastAUM.MessageKind, wantKind)
|
||||
}
|
||||
}
|
||||
|
||||
// Try starting an authority just based on storage.
|
||||
a2, err := Open(storage)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open from stored AUMs: %v", err)
|
||||
}
|
||||
if a.Head() != a2.Head() {
|
||||
t.Errorf("stored and computed HEAD differ: got %v, want %v", a2.Head(), a.Head())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func testScenario(t *testing.T, sharedChain string, sharedOptions ...testchainOp
|
||||
sharedOptions = append(sharedOptions,
|
||||
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}}),
|
||||
optKey("key", key, priv),
|
||||
optSignAllUsing("key"))
|
||||
|
||||
@@ -226,7 +226,7 @@ func TestSigCredential(t *testing.T) {
|
||||
a, _ := Open(newTestchain(t, "G1\nG1.template = genesis",
|
||||
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
|
||||
Keys: []Key{k},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}})).Chonk())
|
||||
if err := a.NodeKeyAuthorized(node.Public(), nestedSig.Serialize()); err == nil {
|
||||
t.Error("NodeKeyAuthorized(SigCredential, node) did not fail")
|
||||
|
||||
10
tka/state.go
10
tka/state.go
@@ -93,13 +93,7 @@ const disablementLength = 32
|
||||
|
||||
var disablementSalt = []byte("tailscale network-lock disablement salt")
|
||||
|
||||
// DisablementKDF computes a public value which can be stored in a
|
||||
// key authority, but cannot be reversed to find the input secret.
|
||||
//
|
||||
// When the output of this function is stored in tka state (i.e. in
|
||||
// tka.State.DisablementSecrets) a call to Authority.ValidDisablement()
|
||||
// with the input of this function as the argument will return true.
|
||||
func DisablementKDF(secret []byte) []byte {
|
||||
func disablementKDF(secret []byte) []byte {
|
||||
// time = 4 (3 recommended, booped to 4 to compensate for less memory)
|
||||
// memory = 16 (32 recommended)
|
||||
// threads = 4
|
||||
@@ -109,7 +103,7 @@ func DisablementKDF(secret []byte) []byte {
|
||||
|
||||
// checkDisablement returns true for a valid disablement secret.
|
||||
func (s State) checkDisablement(secret []byte) bool {
|
||||
derived := DisablementKDF(secret)
|
||||
derived := disablementKDF(secret)
|
||||
for _, candidate := range s.DisablementSecrets {
|
||||
if bytes.Equal(derived, candidate) {
|
||||
return true
|
||||
|
||||
@@ -342,7 +342,7 @@ func TestSyncSimpleE2E(t *testing.T) {
|
||||
`,
|
||||
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}}),
|
||||
optKey("key", key, priv),
|
||||
optSignAllUsing("key"))
|
||||
|
||||
17
tka/tka.go
17
tka/tka.go
@@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
@@ -182,18 +181,6 @@ func advanceByPrimary(state State, candidates []AUM) (next *AUM, out State, err
|
||||
}
|
||||
|
||||
aum := pickNextAUM(state, candidates)
|
||||
|
||||
// TODO(tom): Remove this before GA, this is just a correctness check during implementation.
|
||||
// Post-GA, we want clients to not error if they dont recognize additional fields in State.
|
||||
if aum.MessageKind == AUMCheckpoint {
|
||||
dupe := state
|
||||
dupe.LastAUMHash = nil
|
||||
// aum.State is non-nil (see aum.StaticValidate).
|
||||
if !reflect.DeepEqual(dupe, *aum.State) {
|
||||
return nil, State{}, errors.New("checkpoint includes changes not represented in earlier AUMs")
|
||||
}
|
||||
}
|
||||
|
||||
if state, err = state.applyVerifiedAUM(aum); err != nil {
|
||||
return nil, State{}, fmt.Errorf("advancing state: %v", err)
|
||||
}
|
||||
@@ -257,6 +244,10 @@ func fastForward(storage Chonk, maxIter int, startState State, done func(curAUM
|
||||
|
||||
// computeStateAt returns the State at wantHash.
|
||||
func computeStateAt(storage Chonk, maxIter int, wantHash AUMHash) (State, error) {
|
||||
// TODO(tom): This is going to get expensive for really long
|
||||
// chains. We should make nodes emit a checkpoint every
|
||||
// X updates or something.
|
||||
|
||||
topAUM, err := storage.AUM(wantHash)
|
||||
if err != nil {
|
||||
return State{}, err
|
||||
|
||||
@@ -305,7 +305,7 @@ func TestAuthorityValidDisablement(t *testing.T) {
|
||||
`,
|
||||
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}}),
|
||||
)
|
||||
|
||||
@@ -321,7 +321,7 @@ func TestCreateBootstrapAuthority(t *testing.T) {
|
||||
|
||||
a1, genesisAUM, err := Create(&Mem{}, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
if err != nil {
|
||||
t.Fatalf("Create() failed: %v", err)
|
||||
@@ -361,7 +361,7 @@ func TestAuthorityInformNonLinear(t *testing.T) {
|
||||
`,
|
||||
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}}),
|
||||
optKey("key", key, priv),
|
||||
optSignAllUsing("key"))
|
||||
@@ -406,7 +406,7 @@ func TestAuthorityInformLinear(t *testing.T) {
|
||||
`,
|
||||
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}}),
|
||||
optKey("key", key, priv),
|
||||
optSignAllUsing("key"))
|
||||
|
||||
@@ -810,7 +810,6 @@ func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
|
||||
"HTTPS_PROXY="+n.env.TrafficTrapServer.URL,
|
||||
"TS_DEBUG_TAILSCALED_IPN_GOOS="+ipnGOOS,
|
||||
"TS_LOGS_DIR="+t.TempDir(),
|
||||
"TS_NETCHECK_GENERATE_204_URL="+n.env.ControlServer.URL+"/generate_204",
|
||||
)
|
||||
cmd.Stderr = &nodeOutputParser{n: n}
|
||||
if *verboseTailscaled {
|
||||
|
||||
@@ -200,9 +200,6 @@ func (s *Server) logf(format string, a ...any) {
|
||||
func (s *Server) initMux() {
|
||||
s.mux = http.NewServeMux()
|
||||
s.mux.HandleFunc("/", s.serveUnhandled)
|
||||
s.mux.HandleFunc("/generate_204", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
s.mux.HandleFunc("/key", s.serveKey)
|
||||
s.mux.HandleFunc("/machine/", s.serveMachine)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import (
|
||||
func init() {
|
||||
expvar.Publish("process_start_unix_time", expvar.Func(func() any { return timeStart.Unix() }))
|
||||
expvar.Publish("version", expvar.Func(func() any { return version.Long }))
|
||||
expvar.Publish("go_version", expvar.Func(func() any { return runtime.Version() }))
|
||||
expvar.Publish("counter_uptime_sec", expvar.Func(func() any { return int64(Uptime().Seconds()) }))
|
||||
expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() }))
|
||||
}
|
||||
@@ -185,9 +184,9 @@ type ReturnHandler interface {
|
||||
}
|
||||
|
||||
type HandlerOptions struct {
|
||||
QuietLoggingIfSuccessful bool // if set, do not log successfully handled HTTP requests (200 and 304 status codes)
|
||||
Logf logger.Logf
|
||||
Now func() time.Time // if nil, defaults to time.Now
|
||||
Quiet200s bool // if set, do not log successfully handled HTTP requests
|
||||
Logf logger.Logf
|
||||
Now func() time.Time // if nil, defaults to time.Now
|
||||
|
||||
// If non-nil, StatusCodeCounters maintains counters
|
||||
// of status codes for handled responses.
|
||||
@@ -317,7 +316,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if !h.opts.QuietLoggingIfSuccessful || (msg.Code != http.StatusOK && msg.Code != http.StatusNotModified) {
|
||||
if msg.Code != 200 || !h.opts.Quiet200s {
|
||||
h.opts.Logf("%s", msg)
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ func BenchmarkLogNot200(b *testing.B) {
|
||||
// Implicit 200 OK.
|
||||
return nil
|
||||
})
|
||||
h := StdHandler(rh, HandlerOptions{QuietLoggingIfSuccessful: true})
|
||||
h := StdHandler(rh, HandlerOptions{Quiet200s: true})
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
rw := new(httptest.ResponseRecorder)
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -138,8 +138,6 @@ var rateFree = []string{
|
||||
"SetPrefs: %v",
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
// debug messages printed by 'tailscale bugreport'
|
||||
"diag: ",
|
||||
}
|
||||
|
||||
// RateLimitedFn is a wrapper for RateLimitedFnWithClock that includes the
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
@@ -62,13 +61,6 @@ type NetworkMap struct {
|
||||
// check problems.
|
||||
ControlHealth []string
|
||||
|
||||
// TKAEnabled indicates whether the tailnet key authority should be
|
||||
// enabled, from the perspective of the control plane.
|
||||
TKAEnabled bool
|
||||
// TKAHead indicates the control plane's understanding of 'head' (the
|
||||
// hash of the latest update message to tick through TKA).
|
||||
TKAHead tka.AUMHash
|
||||
|
||||
// ACLs
|
||||
|
||||
User tailcfg.UserID
|
||||
|
||||
@@ -106,20 +106,9 @@ func (s1 *Sum) xor(s2 Sum) {
|
||||
}
|
||||
|
||||
func (s Sum) String() string {
|
||||
// Note: if we change this, keep in sync with AppendTo
|
||||
return hex.EncodeToString(s.sum[:])
|
||||
}
|
||||
|
||||
// AppendTo appends the string encoding of this sum (as returned by the String
|
||||
// method) to the provided byte slice and returns the extended buffer.
|
||||
func (s Sum) AppendTo(b []byte) []byte {
|
||||
// TODO: switch to upstream implementation if accepted:
|
||||
// https://github.com/golang/go/issues/53693
|
||||
var lb [len(s.sum) * 2]byte
|
||||
hex.Encode(lb[:], s.sum[:])
|
||||
return append(b, lb[:]...)
|
||||
}
|
||||
|
||||
var (
|
||||
seedOnce sync.Once
|
||||
seed uint64
|
||||
|
||||
@@ -575,7 +575,7 @@ func TestGetTypeHasher(t *testing.T) {
|
||||
{
|
||||
name: "tailcfg.Node",
|
||||
val: &tailcfg.Node{},
|
||||
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
out: "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\tn\x88\xf1\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -1050,25 +1050,3 @@ func FuzzAddr(f *testing.F) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppendTo(t *testing.T) {
|
||||
v := getVal()
|
||||
h := Hash(v)
|
||||
sum := h.AppendTo(nil)
|
||||
|
||||
if s := h.String(); s != string(sum) {
|
||||
t.Errorf("hash sum mismatch; h.String()=%q h.AppendTo()=%q", s, string(sum))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendTo(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
v := getVal()
|
||||
h := Hash(v)
|
||||
|
||||
hashBuf := make([]byte, 0, 100)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
hashBuf = h.AppendTo(hashBuf[:0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The goroutines package contains utilities for getting active goroutines.
|
||||
package goroutines
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ScrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||
// values of arguments scrubbed out, lest it contain some private key material.
|
||||
func ScrubbedGoroutineDump() []byte {
|
||||
var buf []byte
|
||||
// Grab stacks multiple times into increasingly larger buffer sizes
|
||||
// to minimize the risk that we blow past our iOS memory limit.
|
||||
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
|
||||
buf = make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
if len(buf) < size {
|
||||
// It fit.
|
||||
break
|
||||
}
|
||||
}
|
||||
return scrubHex(buf)
|
||||
}
|
||||
|
||||
func scrubHex(buf []byte) []byte {
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
|
||||
foreachHexAddress(buf, func(in []byte) {
|
||||
if string(in) == "0x0" {
|
||||
return
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
copy(in, v)
|
||||
return
|
||||
}
|
||||
inStr := string(in)
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
if err != nil {
|
||||
in[0] = '?'
|
||||
return
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[inStr] = v
|
||||
copy(in, v)
|
||||
})
|
||||
return buf
|
||||
}
|
||||
|
||||
var ohx = []byte("0x")
|
||||
|
||||
// foreachHexAddress calls f with each subslice of b that matches
|
||||
// regexp `0x[0-9a-f]*`.
|
||||
func foreachHexAddress(b []byte, f func([]byte)) {
|
||||
for len(b) > 0 {
|
||||
i := bytes.Index(b, ohx)
|
||||
if i == -1 {
|
||||
return
|
||||
}
|
||||
b = b[i:]
|
||||
hx := hexPrefix(b)
|
||||
f(hx)
|
||||
b = b[len(hx):]
|
||||
}
|
||||
}
|
||||
|
||||
func hexPrefix(b []byte) []byte {
|
||||
for i, c := range b {
|
||||
if i < 2 {
|
||||
continue
|
||||
}
|
||||
if !isHexByte(c) {
|
||||
return b[:i]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isHexByte(b byte) bool {
|
||||
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
|
||||
}
|
||||
@@ -1318,9 +1318,6 @@ func (c *Conn) derpWriteChanOfAddr(addr netip.AddrPort, peer key.NodePublic) cha
|
||||
if !c.wantDerpLocked() || c.closed {
|
||||
return nil
|
||||
}
|
||||
if c.derpMap == nil || c.derpMap.Regions[regionID] == nil {
|
||||
return nil
|
||||
}
|
||||
if c.privateKey.IsZero() {
|
||||
c.logf("magicsock: DERP lookup of %v with no private key; ignoring", addr)
|
||||
return nil
|
||||
@@ -1365,6 +1362,9 @@ func (c *Conn) derpWriteChanOfAddr(addr netip.AddrPort, peer key.NodePublic) cha
|
||||
c.activeDerp = make(map[int]activeDerp)
|
||||
c.prevDerp = make(map[int]*syncs.WaitGroupChan)
|
||||
}
|
||||
if c.derpMap == nil || c.derpMap.Regions[regionID] == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note that derphttp.NewRegionClient does not dial the server
|
||||
// (it doesn't block) so it is safe to do under the c.mu lock.
|
||||
|
||||
@@ -664,11 +664,7 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
|
||||
}
|
||||
|
||||
destIP := p.Dst.Addr()
|
||||
|
||||
// If this is an echo request and we're a subnet router, handle pings
|
||||
// ourselves instead of forwarding the packet on.
|
||||
pingIP, handlePing := ns.shouldHandlePing(p)
|
||||
if handlePing {
|
||||
if p.IsEchoRequest() && ns.ProcessSubnets && !tsaddr.IsTailscaleIP(destIP) {
|
||||
var pong []byte // the reply to the ping, if our relayed ping works
|
||||
if destIP.Is4() {
|
||||
h := p.ICMP4Header()
|
||||
@@ -679,7 +675,7 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
|
||||
h.ToResponse()
|
||||
pong = packet.Generate(&h, p.Payload())
|
||||
}
|
||||
go ns.userPing(pingIP, pong)
|
||||
go ns.userPing(destIP, pong)
|
||||
return filter.DropSilently
|
||||
}
|
||||
|
||||
@@ -708,59 +704,6 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
|
||||
return filter.DropSilently
|
||||
}
|
||||
|
||||
// shouldHandlePing returns whether or not netstack should handle an incoming
|
||||
// ICMP echo request packet, and the IP address that should be pinged from this
|
||||
// process. The IP address can be different from the destination in the packet
|
||||
// if the destination is a 4via6 address.
|
||||
func (ns *Impl) shouldHandlePing(p *packet.Parsed) (_ netip.Addr, ok bool) {
|
||||
if !p.IsEchoRequest() {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
destIP := p.Dst.Addr()
|
||||
|
||||
// We need to handle pings for all 4via6 addresses, even if this
|
||||
// netstack instance normally isn't responsible for processing subnets.
|
||||
//
|
||||
// For example, on Linux, subnet router traffic could be handled via
|
||||
// tun+iptables rules for most packets, but we still need to handle
|
||||
// ICMP echo requests over 4via6 since the host networking stack
|
||||
// doesn't know what to do with a 4via6 address.
|
||||
//
|
||||
// shouldProcessInbound returns 'true' to say that we should process
|
||||
// all IPv6 packets with a destination address in the 'via' range, so
|
||||
// check before we check the "ProcessSubnets" boolean below.
|
||||
if viaRange.Contains(destIP) {
|
||||
// The input echo request was to a 4via6 address, which we cannot
|
||||
// simply ping as-is from this process. Translate the destination to an
|
||||
// IPv4 address, so that our relayed ping (in userPing) is pinging the
|
||||
// underlying destination IP.
|
||||
//
|
||||
// ICMPv4 and ICMPv6 are different protocols with different on-the-wire
|
||||
// representations, so normally you can't send an ICMPv6 message over
|
||||
// IPv4 and expect to get a useful result. However, in this specific
|
||||
// case things are safe because the 'userPing' function doesn't make
|
||||
// use of the input packet.
|
||||
return tsaddr.UnmapVia(destIP), true
|
||||
}
|
||||
|
||||
// If we get here, we don't do anything unless this netstack instance
|
||||
// is responsible for processing subnet traffic.
|
||||
if !ns.ProcessSubnets {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
// For non-4via6 addresses, we don't handle pings if they're destined
|
||||
// for a Tailscale IP.
|
||||
if tsaddr.IsTailscaleIP(destIP) {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
// This netstack instance is processing subnet traffic, so handle the
|
||||
// ping ourselves.
|
||||
return destIP, true
|
||||
}
|
||||
|
||||
func netaddrIPFromNetstackIP(s tcpip.Address) netip.Addr {
|
||||
switch len(s) {
|
||||
case 4:
|
||||
@@ -987,19 +930,16 @@ func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) {
|
||||
}
|
||||
dstAddr, ok := ipPortOfNetstackAddr(sess.LocalAddress, sess.LocalPort)
|
||||
if !ok {
|
||||
ep.Close()
|
||||
return
|
||||
}
|
||||
srcAddr, ok := ipPortOfNetstackAddr(sess.RemoteAddress, sess.RemotePort)
|
||||
if !ok {
|
||||
ep.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle magicDNS traffic (via UDP) here.
|
||||
if dst := dstAddr.Addr(); dst == magicDNSIP || dst == magicDNSIPv6 {
|
||||
if dstAddr.Port() != 53 {
|
||||
ep.Close()
|
||||
return // Only MagicDNS traffic runs on the service IPs for now.
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package netstack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"testing"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
@@ -86,169 +84,3 @@ func TestNetstackLeakMode(t *testing.T) {
|
||||
t.Fatalf("refs.leakMode is 0, want a non-zero value")
|
||||
}
|
||||
}
|
||||
|
||||
func makeNetstack(t *testing.T, config func(*Impl)) *Impl {
|
||||
tunDev := tstun.NewFake()
|
||||
dialer := new(tsdial.Dialer)
|
||||
logf := func(format string, args ...any) {
|
||||
if !t.Failed() {
|
||||
t.Helper()
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: tunDev,
|
||||
Dialer: dialer,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { eng.Close() })
|
||||
ig, ok := eng.(wgengine.InternalsGetter)
|
||||
if !ok {
|
||||
t.Fatal("not an InternalsGetter")
|
||||
}
|
||||
tunWrap, magicSock, dns, ok := ig.GetInternals()
|
||||
if !ok {
|
||||
t.Fatal("failed to get internals")
|
||||
}
|
||||
|
||||
ns, err := Create(logf, tunWrap, eng, magicSock, dialer, dns)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { ns.Close() })
|
||||
|
||||
ns.atomicIsLocalIPFunc.Store(func(netip.Addr) bool { return true })
|
||||
config(ns)
|
||||
|
||||
if err := ns.Start(); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
func TestShouldHandlePing(t *testing.T) {
|
||||
srcIP := netip.AddrFrom4([4]byte{1, 2, 3, 4})
|
||||
|
||||
t.Run("ICMP4", func(t *testing.T) {
|
||||
dst := netip.MustParseAddr("5.6.7.8")
|
||||
icmph := packet.ICMP4Header{
|
||||
IP4Header: packet.IP4Header{
|
||||
IPProto: ipproto.ICMPv4,
|
||||
Src: srcIP,
|
||||
Dst: dst,
|
||||
},
|
||||
Type: packet.ICMP4EchoRequest,
|
||||
Code: packet.ICMP4NoCode,
|
||||
}
|
||||
_, payload := packet.ICMPEchoPayload(nil)
|
||||
icmpPing := packet.Generate(icmph, payload)
|
||||
pkt := &packet.Parsed{}
|
||||
pkt.Decode(icmpPing)
|
||||
|
||||
impl := makeNetstack(t, func(impl *Impl) {
|
||||
impl.ProcessSubnets = true
|
||||
})
|
||||
pingDst, ok := impl.shouldHandlePing(pkt)
|
||||
if !ok {
|
||||
t.Errorf("expected shouldHandlePing==true")
|
||||
}
|
||||
if pingDst != dst {
|
||||
t.Errorf("got dst %s; want %s", pingDst, dst)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ICMP6-no-via", func(t *testing.T) {
|
||||
dst := netip.MustParseAddr("2a09:8280:1::4169")
|
||||
icmph := packet.ICMP6Header{
|
||||
IP6Header: packet.IP6Header{
|
||||
IPProto: ipproto.ICMPv6,
|
||||
Src: srcIP,
|
||||
Dst: dst,
|
||||
},
|
||||
Type: packet.ICMP6EchoRequest,
|
||||
Code: packet.ICMP6NoCode,
|
||||
}
|
||||
_, payload := packet.ICMPEchoPayload(nil)
|
||||
icmpPing := packet.Generate(icmph, payload)
|
||||
pkt := &packet.Parsed{}
|
||||
pkt.Decode(icmpPing)
|
||||
|
||||
impl := makeNetstack(t, func(impl *Impl) {
|
||||
impl.ProcessSubnets = true
|
||||
})
|
||||
pingDst, ok := impl.shouldHandlePing(pkt)
|
||||
|
||||
// Expect that we handle this since it's going out onto the
|
||||
// network.
|
||||
if !ok {
|
||||
t.Errorf("expected shouldHandlePing==true")
|
||||
}
|
||||
if pingDst != dst {
|
||||
t.Errorf("got dst %s; want %s", pingDst, dst)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ICMP6-tailscale-addr", func(t *testing.T) {
|
||||
dst := netip.MustParseAddr("fd7a:115c:a1e0:ab12::1")
|
||||
icmph := packet.ICMP6Header{
|
||||
IP6Header: packet.IP6Header{
|
||||
IPProto: ipproto.ICMPv6,
|
||||
Src: srcIP,
|
||||
Dst: dst,
|
||||
},
|
||||
Type: packet.ICMP6EchoRequest,
|
||||
Code: packet.ICMP6NoCode,
|
||||
}
|
||||
_, payload := packet.ICMPEchoPayload(nil)
|
||||
icmpPing := packet.Generate(icmph, payload)
|
||||
pkt := &packet.Parsed{}
|
||||
pkt.Decode(icmpPing)
|
||||
|
||||
impl := makeNetstack(t, func(impl *Impl) {
|
||||
impl.ProcessSubnets = true
|
||||
})
|
||||
_, ok := impl.shouldHandlePing(pkt)
|
||||
|
||||
// We don't handle this because it's a Tailscale IP and not 4via6
|
||||
if ok {
|
||||
t.Errorf("expected shouldHandlePing==false")
|
||||
}
|
||||
})
|
||||
|
||||
// Handle pings for 4via6 addresses regardless of ProcessSubnets
|
||||
for _, subnets := range []bool{true, false} {
|
||||
t.Run("ICMP6-4via6-ProcessSubnets-"+fmt.Sprint(subnets), func(t *testing.T) {
|
||||
// The 4via6 route 10.1.1.0/24 siteid 7, and then the IP
|
||||
// 10.1.1.9 within that route.
|
||||
dst := netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:7:a01:109")
|
||||
expectedPingDst := netip.MustParseAddr("10.1.1.9")
|
||||
icmph := packet.ICMP6Header{
|
||||
IP6Header: packet.IP6Header{
|
||||
IPProto: ipproto.ICMPv6,
|
||||
Src: srcIP,
|
||||
Dst: dst,
|
||||
},
|
||||
Type: packet.ICMP6EchoRequest,
|
||||
Code: packet.ICMP6NoCode,
|
||||
}
|
||||
_, payload := packet.ICMPEchoPayload(nil)
|
||||
icmpPing := packet.Generate(icmph, payload)
|
||||
pkt := &packet.Parsed{}
|
||||
pkt.Decode(icmpPing)
|
||||
|
||||
impl := makeNetstack(t, func(impl *Impl) {
|
||||
impl.ProcessSubnets = subnets
|
||||
})
|
||||
pingDst, ok := impl.shouldHandlePing(pkt)
|
||||
|
||||
// Handled due to being 4via6
|
||||
if !ok {
|
||||
t.Errorf("expected shouldHandlePing==true")
|
||||
} else if pingDst != expectedPingDst {
|
||||
t.Errorf("got dst %s; want %s", pingDst, expectedPingDst)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user