Compare commits
11 Commits
bradfitz/a
...
v1.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d7ae3146c | ||
|
|
76c2982d88 | ||
|
|
3d64eef37b | ||
|
|
4f292740b0 | ||
|
|
e1327154bb | ||
|
|
a702921620 | ||
|
|
b9b7fbdd21 | ||
|
|
9446e5c170 | ||
|
|
75cd82791e | ||
|
|
4d5d5f89a3 | ||
|
|
bb058703ee |
48
.github/workflows/coverage.yml
vendored
48
.github/workflows/coverage.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Code Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
# https://markphelps.me/2019/11/speed-up-your-go-builds-with-actions-cache/
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@preview
|
||||
id: cache
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux with coverage data
|
||||
run: go test -race -coverprofile=coverage.txt -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: coveralls.io
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.COVERALLS_BOT_PUBLIC_REPO_TOKEN }}
|
||||
with:
|
||||
path-to-profile: ./coverage.txt
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,11 +1,12 @@
|
||||
# Binaries for programs and plugins
|
||||
*~
|
||||
*.tmp
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
cmd/relaynode/relaynode
|
||||
cmd/taillogin/taillogin
|
||||
cmd/tailscale/tailscale
|
||||
cmd/tailscaled/tailscaled
|
||||
|
||||
@@ -17,7 +18,3 @@ cmd/tailscaled/tailscaled
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# direnv config, this may be different for other people so it's probably safer
|
||||
# to make this nonspecific.
|
||||
.envrc
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -2,23 +2,6 @@
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
|
||||
# This Dockerfile includes all the tailscale binaries.
|
||||
#
|
||||
# To build the Dockerfile:
|
||||
@@ -38,7 +21,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.15-alpine AS build-env
|
||||
FROM golang:1.14-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
|
||||
11
README.md
11
README.md
@@ -63,13 +63,8 @@ Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
|
||||
## About Us
|
||||
|
||||
[Tailscale](https://tailscale.com/) is primarily developed by the
|
||||
people at https://github.com/orgs/tailscale/people. For other contributors,
|
||||
see:
|
||||
|
||||
* https://github.com/tailscale/tailscale/graphs/contributors
|
||||
* https://github.com/tailscale/tailscale-android/graphs/contributors
|
||||
|
||||
## Legal
|
||||
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney, josharian
|
||||
from Tailscale Inc.
|
||||
You can learn more about us from [our website](https://tailscale.com).
|
||||
|
||||
WireGuard is a registered trademark of Jason A. Donenfeld.
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.3.0
|
||||
1.2.3
|
||||
|
||||
774
api.md
774
api.md
@@ -1,774 +0,0 @@
|
||||
# Tailscale API
|
||||
|
||||
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://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
|
||||
|
||||
# APIs
|
||||
|
||||
* **[Devices](#device)**
|
||||
- [GET device](#device-get)
|
||||
- [DELETE device](#device-delete)
|
||||
- Routes
|
||||
- [GET device routes](#device-routes-get)
|
||||
- [POST device routes](#device-routes-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [DNS](#tailnet-dns)
|
||||
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
|
||||
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
|
||||
- [GET tailnet DNS preferences](#tailnet-dns-preferences-get)
|
||||
- [POST tailnet DNS preferences](#tailnet-dns-preferences-post)
|
||||
- [GET tailnet DNS searchpaths](#tailnet-dns-searchpaths-get)
|
||||
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
|
||||
|
||||
## Device
|
||||
<!-- TODO: description about what devices are -->
|
||||
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.
|
||||
|
||||
To find the deviceID of a particular device, you can use the ["GET /devices"](#getdevices) API call and generate a list of devices on your network.
|
||||
Find the device you're looking for and get the "id" field.
|
||||
This is your deviceID.
|
||||
|
||||
<a name=device-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
Returns the details for the specified device.
|
||||
Supply the device of interest in the path using its ID.
|
||||
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`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
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' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"addresses":[
|
||||
"100.105.58.116"
|
||||
],
|
||||
"id":"12345",
|
||||
"user":"user1@example.com",
|
||||
"name":"user1-device.example.com",
|
||||
"hostname":"User1-Device",
|
||||
"clientVersion":"date.20201107",
|
||||
"updateAvailable":false,
|
||||
"os":"macOS",
|
||||
"created":"2020-11-20T20:56:49Z",
|
||||
"lastSeen":"2020-11-20T16:15:55-05:00",
|
||||
"keyExpiryDisabled":false,
|
||||
"expires":"2021-05-19T20:56:49Z",
|
||||
"authorized":true,
|
||||
"isExternal":false,
|
||||
"machineKey":"mkey:user1-machine-key",
|
||||
"nodeKey":"nodekey:user1-node-key",
|
||||
"blocksIncomingConnections":false,
|
||||
"enabledRoutes":[
|
||||
|
||||
],
|
||||
"advertisedRoutes":[
|
||||
|
||||
],
|
||||
"clientConnectivity": {
|
||||
"endpoints":[
|
||||
"209.195.87.231:59128",
|
||||
"192.168.0.173:59128"
|
||||
],
|
||||
"derp":"",
|
||||
"mappingVariesByDestIP":false,
|
||||
"latency":{
|
||||
"Dallas":{
|
||||
"latencyMs":60.463043
|
||||
},
|
||||
"New York City":{
|
||||
"preferred":true,
|
||||
"latencyMs":31.323811
|
||||
},
|
||||
"San Francisco":{
|
||||
"latencyMs":81.313389
|
||||
}
|
||||
},
|
||||
"clientSupports":{
|
||||
"hairPinning":false,
|
||||
"ipv6":false,
|
||||
"pcp":false,
|
||||
"pmp":false,
|
||||
"udp":true,
|
||||
"upnp":false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-delete></div>
|
||||
|
||||
#### `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' \
|
||||
-u "tskey-yourapikey123:" -v
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
If successful, the response should be empty:
|
||||
```
|
||||
< HTTP/1.1 200 OK
|
||||
...
|
||||
* Connection #0 to host left intact
|
||||
* Closing connection 0
|
||||
```
|
||||
|
||||
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></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
|
||||
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
],
|
||||
"enabledRoutes" : []
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-routes-post></div>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
|
||||
|
||||
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
|
||||
|
||||
##### 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"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]}'
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
],
|
||||
"enabledRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
|
||||
```
|
||||
|
||||
|
||||
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/...
|
||||
```
|
||||
|
||||
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
|
||||
|
||||
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
|
||||
|
||||
### ACL
|
||||
|
||||
<a name=tailnet-acl-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/acl` - fetch ACL for a tailnet
|
||||
|
||||
Retrieves the ACL that is currently set for the given tailnet. Supply the tailnet of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
|
||||
|
||||
##### 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' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Accept: application/hujson" \
|
||||
-v
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
...
|
||||
Content-Type: application/hujson
|
||||
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
...
|
||||
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
"Tests": [],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{
|
||||
"Action": "accept",
|
||||
"Users": [
|
||||
"*"
|
||||
],
|
||||
"Ports": [
|
||||
"*:*"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
###### Requesting a JSON response:
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Accept: application/json" \
|
||||
-v
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
...
|
||||
Content-Type: application/json
|
||||
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
...
|
||||
{
|
||||
"acls" : [
|
||||
{
|
||||
"action" : "accept",
|
||||
"ports" : [
|
||||
"*:*"
|
||||
],
|
||||
"users" : [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups" : {
|
||||
"group:example" : [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
]
|
||||
},
|
||||
"hosts" : {
|
||||
"example-host-1" : "100.100.100.100"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl` - set ACL for a tailnet
|
||||
|
||||
Sets the ACL for the given tailnet. HuJSON and JSON are both accepted inputs. An `If-Match` header can be set to avoid missed updates.
|
||||
|
||||
Returns error for invalid ACLs.
|
||||
Returns error if using an `If-Match` header and the ETag does not match.
|
||||
|
||||
##### 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.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "If-Match: \"e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c\""
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-preview-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/preview` - preview rule matches on an ACL for a resource
|
||||
Determines what rules match for a user on an ACL without saving the ACL to the server.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
`user` - A user's email. The provided ACL is queried with this user to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/preiew
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl?user=user1@example.com' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
|
||||
<a name=tailnet-devices-get></a>
|
||||
|
||||
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
|
||||
Lists the devices in a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
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`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
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/tailnet/example.com/devices
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/devices' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"devices":[
|
||||
{
|
||||
"addresses":[
|
||||
"100.68.203.125"
|
||||
],
|
||||
"clientVersion":"date.20201107",
|
||||
"os":"macOS",
|
||||
"name":"user1-device.example.com",
|
||||
"created":"2020-11-30T22:20:04Z",
|
||||
"lastSeen":"2020-11-30T17:20:04-05:00",
|
||||
"hostname":"User1-Device",
|
||||
"machineKey":"mkey:user1-node-key",
|
||||
"nodeKey":"nodekey:user1-node-key",
|
||||
"id":"12345",
|
||||
"user":"user1@example.com",
|
||||
"expires":"2021-05-29T22:20:04Z",
|
||||
"keyExpiryDisabled":false,
|
||||
"authorized":false,
|
||||
"isExternal":false,
|
||||
"updateAvailable":false,
|
||||
"blocksIncomingConnections":false,
|
||||
},
|
||||
{
|
||||
"addresses":[
|
||||
"100.111.63.90"
|
||||
],
|
||||
"clientVersion":"date.20201107",
|
||||
"os":"macOS",
|
||||
"name":"user2-device.example.com",
|
||||
"created":"2020-11-30T22:21:03Z",
|
||||
"lastSeen":"2020-11-30T17:21:03-05:00",
|
||||
"hostname":"User2-Device",
|
||||
"machineKey":"mkey:user2-machine-key",
|
||||
"nodeKey":"nodekey:user2-node-key",
|
||||
"id":"48810",
|
||||
"user":"user2@example.com",
|
||||
"expires":"2021-05-29T22:21:03Z",
|
||||
"keyExpiryDisabled":false,
|
||||
"authorized":false,
|
||||
"isExternal":false,
|
||||
"updateAvailable":false,
|
||||
"blocksIncomingConnections":false,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns></a>
|
||||
|
||||
### DNS
|
||||
|
||||
<a name=tailnet-dns-nameservers-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
|
||||
Lists the DNS nameservers for a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"dns": ["8.8.8.8"],
|
||||
}
|
||||
```
|
||||
|
||||
<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"]
|
||||
}
|
||||
```
|
||||
|
||||
##### 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' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": ["8.8.8.8"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"],
|
||||
"magicDNS":true,
|
||||
}
|
||||
```
|
||||
|
||||
###### 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' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": []}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":[],
|
||||
"magicDNS": false,
|
||||
}
|
||||
```
|
||||
|
||||
<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' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"magicDNS":false,
|
||||
}
|
||||
```
|
||||
|
||||
<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.
|
||||
|
||||
If there is at least one DNS server, then MagicDNS can be enabled.
|
||||
Otherwise, it returns an error.
|
||||
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": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/preferences
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--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"
|
||||
}
|
||||
```
|
||||
|
||||
If there are DNS servers:
|
||||
```
|
||||
{
|
||||
"magicDNS":true,
|
||||
}
|
||||
```
|
||||
|
||||
<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' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com"],
|
||||
}
|
||||
```
|
||||
|
||||
<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"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"searchPaths": ["user1.example.com", "user2.example.com"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"],
|
||||
}
|
||||
```
|
||||
@@ -140,7 +140,7 @@ func main() {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := ioutil.WriteFile(output, out, 0644); err != nil {
|
||||
if err := ioutil.WriteFile(output, out, 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
@@ -34,7 +35,6 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ var (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
PrivateKey wgkey.Private
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
@@ -77,8 +77,8 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewKey() wgkey.Private {
|
||||
key, err := wgkey.NewPrivate()
|
||||
func mustNewKey() wgcfg.PrivateKey {
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func writeNewConfig() config {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := atomicfile.WriteFile(*configPath, b, 0600); err != nil {
|
||||
if err := atomicfile.WriteFile(*configPath, b, 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return cfg
|
||||
|
||||
@@ -99,7 +99,7 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
}
|
||||
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -43,7 +44,9 @@ var netcheckArgs struct {
|
||||
}
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{}
|
||||
c := &netcheck.Client{
|
||||
DNSCache: dnscache.Get(),
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
c.Verbose = true
|
||||
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
@@ -23,7 +21,6 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
@@ -37,7 +34,6 @@ var statusCmd = &ffcli.Command{
|
||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
||||
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
||||
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
||||
return fs
|
||||
@@ -51,7 +47,6 @@ var statusArgs struct {
|
||||
browser bool // in web mode, whether to open browser
|
||||
active bool // in CLI mode, filter output to only peers with active sessions
|
||||
self bool // in CLI mode, show status of local machine
|
||||
peers bool // in CLI mode, show status of peer machines
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
@@ -141,30 +136,30 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
active := peerActive(ps)
|
||||
f("%-15s %-20s %-12s %-7s ",
|
||||
ps.TailAddr,
|
||||
dnsOrQuoteHostname(st, ps),
|
||||
ownerLogin(st, ps),
|
||||
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
||||
ps.PublicKey.ShortString(),
|
||||
ps.OS,
|
||||
ps.TailAddr,
|
||||
ps.SimpleHostName(),
|
||||
ps.TxBytes,
|
||||
ps.RxBytes,
|
||||
)
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
if !active {
|
||||
if anyTraffic {
|
||||
f("idle")
|
||||
} else {
|
||||
f("-")
|
||||
}
|
||||
if active && relay != "" && ps.CurAddr == "" {
|
||||
relay = "*" + relay + "*"
|
||||
} else {
|
||||
f("active; ")
|
||||
if relay != "" && ps.CurAddr == "" {
|
||||
f("relay %q", relay)
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct %s", ps.CurAddr)
|
||||
}
|
||||
relay = " " + relay
|
||||
}
|
||||
if anyTraffic {
|
||||
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
||||
f("%-6s", relay)
|
||||
for i, addr := range ps.Addrs {
|
||||
if i != 0 {
|
||||
f(", ")
|
||||
}
|
||||
if addr == ps.CurAddr {
|
||||
f("*%s*", addr)
|
||||
} else {
|
||||
f("%s", addr)
|
||||
}
|
||||
}
|
||||
f("\n")
|
||||
}
|
||||
@@ -172,23 +167,13 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
if statusArgs.self && st.Self != nil {
|
||||
printPS(st.Self)
|
||||
}
|
||||
if statusArgs.peers {
|
||||
var peers []*ipnstate.PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
|
||||
for _, ps := range peers {
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
}
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
return nil
|
||||
@@ -200,37 +185,3 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
func peerActive(ps *ipnstate.PeerStatus) bool {
|
||||
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
}
|
||||
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
if i := strings.Index(ps.DNSName, "."); i != -1 && dnsname.HasSuffix(ps.DNSName, st.MagicDNSSuffix) {
|
||||
return ps.DNSName[:i]
|
||||
}
|
||||
if ps.DNSName != "" {
|
||||
return strings.TrimRight(ps.DNSName, ".")
|
||||
}
|
||||
return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_"))
|
||||
}
|
||||
|
||||
func sortKey(ps *ipnstate.PeerStatus) string {
|
||||
if ps.DNSName != "" {
|
||||
return ps.DNSName
|
||||
}
|
||||
if ps.HostName != "" {
|
||||
return ps.HostName
|
||||
}
|
||||
return ps.TailAddr
|
||||
}
|
||||
|
||||
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
if ps.UserID.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
u, ok := st.User[ps.UserID]
|
||||
if !ok {
|
||||
return fmt.Sprint(ps.UserID)
|
||||
}
|
||||
if i := strings.Index(u.LoginName, "@"); i != -1 {
|
||||
return u.LoginName[:i+1]
|
||||
}
|
||||
return u.LoginName
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -84,6 +85,29 @@ var upArgs struct {
|
||||
hostname string
|
||||
}
|
||||
|
||||
// parseIPOrCIDR parses an IP address or a CIDR prefix. If the input
|
||||
// is an IP address, it is returned in CIDR form with a /32 mask for
|
||||
// IPv4 or a /128 mask for IPv6.
|
||||
func parseIPOrCIDR(s string) (wgcfg.CIDR, bool) {
|
||||
if strings.Contains(s, "/") {
|
||||
ret, err := wgcfg.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return wgcfg.CIDR{}, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
ip, ok := wgcfg.ParseIP(s)
|
||||
if !ok {
|
||||
return wgcfg.CIDR{}, false
|
||||
}
|
||||
if ip.Is4() {
|
||||
return wgcfg.CIDR{IP: ip, Mask: 32}, true
|
||||
} else {
|
||||
return wgcfg.CIDR{IP: ip, Mask: 128}, true
|
||||
}
|
||||
}
|
||||
|
||||
func isBSD(s string) bool {
|
||||
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
|
||||
}
|
||||
@@ -138,18 +162,19 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
var routes []wgcfg.CIDR
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
cidr, ok := parseIPOrCIDR(s)
|
||||
ipp, err := netaddr.ParseIPPrefix(s) // parse it with other pawith both packages
|
||||
if !ok || err != nil {
|
||||
fatalf("%q is not a valid IP address or CIDR prefix", s)
|
||||
}
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
routes = append(routes, ipp)
|
||||
routes = append(routes, cidr)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
@@ -157,11 +182,19 @@ func runUp(ctx context.Context, args []string) error {
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
fatalf("tag: %q: %s", tag, err)
|
||||
for i, tag := range tags {
|
||||
if strings.HasPrefix(tag, "tag:") {
|
||||
// Accept fully-qualified tags (starting with
|
||||
// "tag:"), as we do in the ACL file.
|
||||
if err := tailcfg.CheckTag(tag); err != nil {
|
||||
fatalf("tag: %q: %v", tag, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := tailcfg.CheckTagSuffix(tag); err != nil {
|
||||
fatalf("tag: %q: %v", tag, err)
|
||||
}
|
||||
tags[i] = "tag:" + tag
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
@@ -24,15 +24,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/iphlpapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/namespaceapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/nci from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/setupapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
@@ -50,12 +53,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
tailscale.com/net/dnscache from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
@@ -65,27 +66,25 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine from tailscale.com/ipn
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/packet from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
@@ -119,11 +118,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
@@ -171,7 +170,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate
|
||||
io from bufio+
|
||||
io/ioutil from crypto/tls+
|
||||
@@ -198,7 +196,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
|
||||
@@ -9,8 +9,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||
@@ -19,7 +19,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
@@ -28,44 +27,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/iphlpapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/namespaceapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/nci from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/setupapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/link/channel+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/tcpip+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
@@ -85,13 +58,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
@@ -102,6 +73,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
@@ -109,26 +81,21 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/control/controlclient+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/packet from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
@@ -142,6 +109,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/crypto/ssh/terminal from tailscale.com/logpolicy
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
@@ -160,19 +128,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
compress/zlib from debug/elf+
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -214,7 +180,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from html/template+
|
||||
html/template from net/http/pprof
|
||||
io from bufio+
|
||||
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
@@ -65,7 +64,6 @@ var args struct {
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
verbose int
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -78,7 +76,6 @@ func main() {
|
||||
}
|
||||
|
||||
printVersion := false
|
||||
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
|
||||
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
||||
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
|
||||
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
||||
@@ -121,7 +118,6 @@ func run() error {
|
||||
var err error
|
||||
|
||||
pol := logpolicy.New("tailnode.log.tailscale.io")
|
||||
pol.SetVerbosityLevel(args.verbose)
|
||||
defer func() {
|
||||
// Finish uploading logs after closing everything else.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
@@ -148,11 +144,7 @@ func run() error {
|
||||
|
||||
var e wgengine.Engine
|
||||
if args.fake {
|
||||
var impl wgengine.FakeImplFunc
|
||||
if args.tunname == "userspace-networking" {
|
||||
impl = netstack.Impl
|
||||
}
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0, impl)
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, args.port)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ After=network-pre.target
|
||||
|
||||
[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
|
||||
ExecStopPost=/usr/sbin/tailscaled --cleanup
|
||||
|
||||
@@ -18,24 +17,6 @@ StateDirectory=tailscale
|
||||
StateDirectoryMode=0750
|
||||
CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
Type=notify
|
||||
|
||||
DeviceAllow=/dev/net/tun
|
||||
DeviceAllow=/dev/null
|
||||
DeviceAllow=/dev/random
|
||||
DeviceAllow=/dev/urandom
|
||||
DevicePolicy=strict
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
PrivateTmp=true
|
||||
ProtectClock=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/etc/
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// State is the high-level state of the client. It is used only in
|
||||
@@ -117,16 +117,15 @@ type Client struct {
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
statusFunc func(Status) // called to update Client status
|
||||
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
@@ -183,8 +182,7 @@ func (c *Client) SetPaused(paused bool) {
|
||||
}
|
||||
c.paused = paused
|
||||
if paused {
|
||||
// Only cancel the map routine. (The auth routine isn't expensive
|
||||
// so it's fine to keep it running.)
|
||||
// Just cancel the map routine. The auth routine isn't expensive.
|
||||
c.cancelMapLocked()
|
||||
} else {
|
||||
for _, ch := range c.unpauseWaiters {
|
||||
@@ -202,50 +200,6 @@ func (c *Client) Start() {
|
||||
go c.mapRoutine()
|
||||
}
|
||||
|
||||
// sendNewMapRequest either sends a new OmitPeers, non-streaming map request
|
||||
// (to just send Hostinfo/Netinfo/Endpoints info, while keeping an existing
|
||||
// streaming response open), or start a new streaming one if necessary.
|
||||
//
|
||||
// It should be called whenever there's something new to tell the server.
|
||||
func (c *Client) sendNewMapRequest() {
|
||||
c.mu.Lock()
|
||||
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
// in a lite update, then tear down everything and start a new stream
|
||||
// (which starts by sending a new map request)
|
||||
if !c.inPollNetMap || c.inLiteMapUpdate {
|
||||
c.mu.Unlock()
|
||||
c.cancelMapSafely()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, send a lite update that doesn't keep a
|
||||
// long-running stream response.
|
||||
defer c.mu.Unlock()
|
||||
c.inLiteMapUpdate = true
|
||||
ctx, cancel := context.WithTimeout(c.mapCtx, 10*time.Second)
|
||||
go func() {
|
||||
defer cancel()
|
||||
t0 := time.Now()
|
||||
err := c.direct.SendLiteMapUpdate(ctx)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
c.mu.Lock()
|
||||
c.inLiteMapUpdate = false
|
||||
c.mu.Unlock()
|
||||
if err == nil {
|
||||
c.logf("[v1] successful lite map update in %v", d)
|
||||
return
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
c.logf("lite map update after %v: %v", d, err)
|
||||
}
|
||||
// Fall back to restarting the long-polling map
|
||||
// request (the old heavy way) if the lite update
|
||||
// failed for any reason.
|
||||
c.cancelMapSafely()
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Client) cancelAuth() {
|
||||
c.mu.Lock()
|
||||
if c.authCancel != nil {
|
||||
@@ -276,7 +230,7 @@ func (c *Client) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
|
||||
c.logf("cancelMapSafely: synced=%v", c.synced)
|
||||
|
||||
if c.inPollNetMap {
|
||||
// received at least one netmap since the last
|
||||
@@ -298,12 +252,12 @@ func (c *Client) cancelMapSafely() {
|
||||
// request.
|
||||
select {
|
||||
case c.newMapCh <- struct{}{}:
|
||||
c.logf("[v1] cancelMapSafely: wrote to channel")
|
||||
c.logf("cancelMapSafely: wrote to channel")
|
||||
default:
|
||||
// if channel write failed, then there was already
|
||||
// an outstanding newMapCh request. One is enough,
|
||||
// since it'll always use the latest endpoints.
|
||||
c.logf("[v1] cancelMapSafely: channel was full")
|
||||
c.logf("cancelMapSafely: channel was full")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,24 +268,22 @@ func (c *Client) authRoutine() {
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
c.logf("authRoutine: %s", c.state)
|
||||
expiry := c.expiry
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
if goal != nil {
|
||||
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
} else {
|
||||
c.logf("authRoutine: %s; goal=nil", c.state)
|
||||
}
|
||||
synced := c.synced
|
||||
c.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
c.logf("[v1] authRoutine: quit")
|
||||
c.logf("authRoutine: quit")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("[v1] %s: %v", msg, err)
|
||||
c.logf("%s: %v", msg, err)
|
||||
err = fmt.Errorf("%s: %v", msg, err)
|
||||
// don't send status updates for context errors,
|
||||
// since context cancelation is always on purpose.
|
||||
@@ -341,13 +293,51 @@ func (c *Client) authRoutine() {
|
||||
}
|
||||
|
||||
if goal == nil {
|
||||
// Wait for user to Login or Logout.
|
||||
<-ctx.Done()
|
||||
c.logf("[v1] authRoutine: context done.")
|
||||
continue
|
||||
}
|
||||
// Wait for something interesting to happen
|
||||
var exp <-chan time.Time
|
||||
var expTimer *time.Timer
|
||||
if expiry != nil && !expiry.IsZero() {
|
||||
// if expiry is in the future, don't delay
|
||||
// past that time.
|
||||
// If it's in the past, then it's already
|
||||
// being handled by someone, so no need to
|
||||
// wake ourselves up again.
|
||||
now := c.timeNow()
|
||||
if expiry.Before(now) {
|
||||
delay := expiry.Sub(now)
|
||||
if delay > 5*time.Second {
|
||||
delay = time.Second
|
||||
}
|
||||
expTimer = time.NewTimer(delay)
|
||||
exp = expTimer.C
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if expTimer != nil {
|
||||
expTimer.Stop()
|
||||
}
|
||||
c.logf("authRoutine: context done.")
|
||||
case <-exp:
|
||||
// Unfortunately the key expiry isn't provided
|
||||
// by the control server until mapRequest.
|
||||
// So we have to do some hackery with c.expiry
|
||||
// in here.
|
||||
// TODO(apenwarr): add a key expiry field in RegisterResponse.
|
||||
c.logf("authRoutine: key expiration check.")
|
||||
if synced && expiry != nil && !expiry.IsZero() && expiry.Before(c.timeNow()) {
|
||||
c.logf("Key expired; setting loggedIn=false.")
|
||||
|
||||
if !goal.wantLoggedIn {
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: c.loggedIn,
|
||||
}
|
||||
c.loggedIn = false
|
||||
c.expiry = nil
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
} else if !goal.wantLoggedIn {
|
||||
err := c.direct.TryLogout(ctx)
|
||||
if err != nil {
|
||||
report(err, "TryLogout")
|
||||
@@ -479,7 +469,7 @@ func (c *Client) mapRoutine() {
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("[v1] %s: %v", msg, err)
|
||||
c.logf("%s: %v", msg, err)
|
||||
err = fmt.Errorf("%s: %v", msg, err)
|
||||
// don't send status updates for context errors,
|
||||
// since context cancelation is always on purpose.
|
||||
@@ -514,7 +504,7 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
select {
|
||||
case <-c.newMapCh:
|
||||
c.logf("[v1] mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.logf("mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.cancelMapLocked()
|
||||
|
||||
// Don't emit this netmap; we're
|
||||
@@ -536,7 +526,7 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("[v1] mapRoutine: netmap received: %s", state)
|
||||
c.logf("mapRoutine: netmap received: %s", state)
|
||||
if stillAuthed {
|
||||
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
|
||||
}
|
||||
@@ -590,7 +580,7 @@ func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
c.logf("Hostinfo: %v", hi)
|
||||
|
||||
// Send new Hostinfo to server
|
||||
c.sendNewMapRequest()
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
@@ -598,12 +588,13 @@ func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
panic("nil NetInfo")
|
||||
}
|
||||
if !c.direct.SetNetInfo(ni) {
|
||||
c.logf("[unexpected] duplicate NetInfo: %v", ni)
|
||||
return
|
||||
}
|
||||
c.logf("NetInfo: %v", ni)
|
||||
|
||||
// Send new Hostinfo (which includes NetInfo) to server
|
||||
c.sendNewMapRequest()
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
@@ -616,7 +607,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
c.inSendStatus++
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
c.logf("sendStatus: %s: %v", who, state)
|
||||
|
||||
var p *Persist
|
||||
var fin *empty.Message
|
||||
@@ -680,7 +671,7 @@ func (c *Client) Logout() {
|
||||
func (c *Client) UpdateEndpoints(localPort uint16, endpoints []string) {
|
||||
changed := c.direct.SetEndpoints(localPort, endpoints)
|
||||
if changed {
|
||||
c.sendNewMapRequest()
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,7 +700,7 @@ func (c *Client) Shutdown() {
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
// used exclusively in tests.
|
||||
func (c *Client) TestOnlyNodePublicKey() wgkey.Key {
|
||||
func (c *Client) TestOnlyNodePublicKey() wgcfg.Key {
|
||||
priv := c.direct.GetPersist()
|
||||
return priv.PrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
@@ -42,11 +42,6 @@ func TestStatusEqual(t *testing.T) {
|
||||
&Status{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Status{},
|
||||
&Status{},
|
||||
|
||||
@@ -31,11 +31,11 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
@@ -43,10 +43,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
type Persist struct {
|
||||
@@ -61,10 +58,10 @@ type Persist struct {
|
||||
// needed. This field should be considered read-only from GUI
|
||||
// frontends. The real value should not be written back in
|
||||
// this field, lest the frontend persist it to disk.
|
||||
LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"`
|
||||
LegacyFrontendPrivateMachineKey wgcfg.PrivateKey `json:"PrivateMachineKey"`
|
||||
|
||||
PrivateNodeKey wgkey.Private
|
||||
OldPrivateNodeKey wgkey.Private // needed to request key rotation
|
||||
PrivateNodeKey wgcfg.PrivateKey
|
||||
OldPrivateNodeKey wgcfg.PrivateKey // needed to request key rotation
|
||||
Provider string
|
||||
LoginName string
|
||||
}
|
||||
@@ -85,7 +82,7 @@ func (p *Persist) Equals(p2 *Persist) bool {
|
||||
}
|
||||
|
||||
func (p *Persist) Pretty() string {
|
||||
var mk, ok, nk wgkey.Key
|
||||
var mk, ok, nk wgcfg.Key
|
||||
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
mk = p.LegacyFrontendPrivateMachineKey.Public()
|
||||
}
|
||||
@@ -95,7 +92,7 @@ func (p *Persist) Pretty() string {
|
||||
if !p.PrivateNodeKey.IsZero() {
|
||||
nk = p.PrivateNodeKey.Public()
|
||||
}
|
||||
ss := func(k wgkey.Key) string {
|
||||
ss := func(k wgcfg.Key) string {
|
||||
if k.IsZero() {
|
||||
return ""
|
||||
}
|
||||
@@ -107,23 +104,22 @@ func (p *Persist) Pretty() string {
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
serverURL string // URL of the tailcontrol server
|
||||
timeNow func() time.Time
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgkey.Private
|
||||
debugFlags []string
|
||||
keepSharerAndUserSplit bool
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
serverURL string // URL of the tailcontrol server
|
||||
timeNow func() time.Time
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgcfg.PrivateKey
|
||||
debugFlags []string
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgkey.Key
|
||||
serverKey wgcfg.Key
|
||||
persist Persist
|
||||
authKey string
|
||||
tryingNewKey wgkey.Private
|
||||
tryingNewKey wgcfg.PrivateKey
|
||||
expiry *time.Time
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
@@ -134,7 +130,7 @@ type Direct struct {
|
||||
|
||||
type Options struct {
|
||||
Persist Persist // initial persistent data
|
||||
MachinePrivateKey wgkey.Private // the machine key to use
|
||||
MachinePrivateKey wgcfg.PrivateKey // the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
TimeNow func() time.Time // time.Now implementation used by Client
|
||||
@@ -145,10 +141,6 @@ type Options struct {
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
|
||||
// KeepSharerAndUserSplit controls whether the client
|
||||
// understands Node.Sharer. If false, the Sharer is mapped to the User.
|
||||
KeepSharerAndUserSplit bool
|
||||
}
|
||||
|
||||
type Decompressor interface {
|
||||
@@ -180,33 +172,28 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
|
||||
httpc := opts.HTTPTestClient
|
||||
if httpc == nil {
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
}
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.DialContext = dialer.DialContext
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
|
||||
httpc = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
machinePrivKey: opts.MachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
newDecompressor: opts.NewDecompressor,
|
||||
keepAlive: opts.KeepAlive,
|
||||
persist: opts.Persist,
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
|
||||
httpc: httpc,
|
||||
machinePrivKey: opts.MachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
newDecompressor: opts.NewDecompressor,
|
||||
keepAlive: opts.KeepAlive,
|
||||
persist: opts.Persist,
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
@@ -316,7 +303,6 @@ func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags Logi
|
||||
if mustregen {
|
||||
_, url, err = c.doLogin(ctx, t, flags, true, url)
|
||||
}
|
||||
|
||||
return url, err
|
||||
}
|
||||
|
||||
@@ -337,7 +323,6 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
|
||||
if expired {
|
||||
c.logf("Old key expired -> regen=true")
|
||||
systemd.Status("key expired; run 'tailscale up' to authenticate")
|
||||
regen = true
|
||||
}
|
||||
if (flags & LoginInteractive) != 0 {
|
||||
@@ -346,7 +331,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
}
|
||||
|
||||
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, url != "")
|
||||
if serverKey.IsZero() {
|
||||
if serverKey == (wgcfg.Key{}) {
|
||||
var err error
|
||||
serverKey, err = loadServerKey(ctx, c.httpc, c.serverURL)
|
||||
if err != nil {
|
||||
@@ -358,12 +343,12 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
var oldNodeKey wgkey.Key
|
||||
var oldNodeKey wgcfg.Key
|
||||
if url != "" {
|
||||
} else if regen || persist.PrivateNodeKey.IsZero() {
|
||||
c.logf("Generating a new nodekey.")
|
||||
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
||||
key, err := wgkey.NewPrivate()
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
c.logf("login keygen: %v", err)
|
||||
return regen, url, err
|
||||
@@ -527,18 +512,6 @@ func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
// maxPolls is how many network maps to download; common values are 1
|
||||
// or -1 (to keep a long-poll query open to the server).
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
|
||||
return c.sendMapRequest(ctx, maxPolls, cb)
|
||||
}
|
||||
|
||||
// SendLiteMapUpdate makes a /map request to update the server of our latest state,
|
||||
// but does not fetch anything. It returns an error if the server did not return a
|
||||
// successful 200 OK response.
|
||||
func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
|
||||
return c.sendMapRequest(ctx, 1, nil)
|
||||
}
|
||||
|
||||
// cb nil means to omit peers.
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
@@ -555,17 +528,15 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
}
|
||||
|
||||
allowStream := maxPolls != 1
|
||||
c.logf("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
|
||||
c.logf("PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
|
||||
|
||||
vlogf := logger.Discard
|
||||
if Debug.NetMap {
|
||||
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
|
||||
// want to upload it always.
|
||||
vlogf = c.logf
|
||||
}
|
||||
|
||||
request := tailcfg.MapRequest{
|
||||
Version: tailcfg.CurrentMapRequestVersion,
|
||||
Version: 5,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
DiscoKey: c.discoPubKey,
|
||||
@@ -573,7 +544,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
DebugFlags: c.debugFlags,
|
||||
OmitPeers: cb == nil,
|
||||
}
|
||||
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
|
||||
old := request.DebugFlags
|
||||
@@ -626,11 +596,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if cb == nil {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we go more than pollTimeout without hearing from the server,
|
||||
// end the long poll. We should be receiving a keep alive ping
|
||||
// every minute.
|
||||
@@ -666,8 +631,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
|
||||
var lastDERPMap *tailcfg.DERPMap
|
||||
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
|
||||
var lastParsedPacketFilter []filter.Match
|
||||
var collectServices bool
|
||||
|
||||
// If allowStream, then the server will use an HTTP long poll to
|
||||
// return incremental results. There is always one response right
|
||||
@@ -708,7 +671,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
case timeoutReset <- struct{}{}:
|
||||
vlogf("netmap: sent timer reset")
|
||||
case <-ctx.Done():
|
||||
c.logf("[v1] netmap: not resetting timer; context done: %v", ctx.Err())
|
||||
c.logf("netmap: not resetting timer; context done: %v", ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
@@ -745,40 +708,23 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
resp.Peers = filtered
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||
}
|
||||
|
||||
if v, ok := resp.CollectServices.Get(); ok {
|
||||
collectServices = v
|
||||
}
|
||||
|
||||
// Get latest localPort. This might've changed if
|
||||
// a lite map update occured meanwhile. This only affects
|
||||
// the end-to-end test.
|
||||
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
|
||||
c.mu.Lock()
|
||||
localPort = c.localPort
|
||||
c.mu.Unlock()
|
||||
|
||||
nm := &NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
Expiry: resp.Node.KeyExpiry,
|
||||
Name: resp.Node.Name,
|
||||
Addresses: resp.Node.Addresses,
|
||||
Peers: resp.Peers,
|
||||
LocalPort: localPort,
|
||||
User: resp.Node.User,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: resp.Domain,
|
||||
DNS: resp.DNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: lastParsedPacketFilter,
|
||||
CollectServices: collectServices,
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
Expiry: resp.Node.KeyExpiry,
|
||||
Name: resp.Node.Name,
|
||||
Addresses: resp.Node.Addresses,
|
||||
Peers: resp.Peers,
|
||||
LocalPort: localPort,
|
||||
User: resp.Node.User,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: resp.Domain,
|
||||
DNS: resp.DNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
addUserProfile := func(userID tailcfg.UserID) {
|
||||
if _, dup := nm.UserProfiles[userID]; dup {
|
||||
@@ -791,13 +737,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
}
|
||||
addUserProfile(nm.User)
|
||||
for _, peer := range resp.Peers {
|
||||
if !peer.Sharer.IsZero() {
|
||||
if c.keepSharerAndUserSplit {
|
||||
addUserProfile(peer.Sharer)
|
||||
} else {
|
||||
peer.User = peer.Sharer
|
||||
}
|
||||
}
|
||||
addUserProfile(peer.User)
|
||||
}
|
||||
if resp.Node.MachineAuthorized {
|
||||
@@ -806,7 +745,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
nm.MachineStatus = tailcfg.MachineUnauthorized
|
||||
}
|
||||
if len(resp.DNS) > 0 {
|
||||
nm.DNS.Nameservers = resp.DNS
|
||||
nm.DNS.Nameservers = wgIPToNetaddr(resp.DNS)
|
||||
}
|
||||
if len(resp.SearchPaths) > 0 {
|
||||
nm.DNS.Domains = resp.SearchPaths
|
||||
@@ -823,7 +762,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
now := c.timeNow()
|
||||
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
|
||||
c.lastPrintMap = now
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
|
||||
c.logf("new network map[%d]:\n%s", i, nm.Concise())
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -838,7 +777,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
|
||||
return nil
|
||||
}
|
||||
|
||||
func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
|
||||
func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
|
||||
defer res.Body.Close()
|
||||
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
@@ -852,8 +791,6 @@ func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey
|
||||
|
||||
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
c.mu.Lock()
|
||||
serverKey := c.serverKey
|
||||
@@ -882,10 +819,6 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
json.Indent(&buf, b, "", " ")
|
||||
log.Printf("MapResponse: %s", buf.Bytes())
|
||||
}
|
||||
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient.Direct.decodeMsg into %T: %q", v, b)
|
||||
}
|
||||
if err := json.Unmarshal(b, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
@@ -893,21 +826,18 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
|
||||
}
|
||||
|
||||
func decodeMsg(msg []byte, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
|
||||
func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
|
||||
decrypted, err := decryptMsg(msg, serverKey, mkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Contains(decrypted, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
|
||||
}
|
||||
if err := json.Unmarshal(decrypted, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decryptMsg(msg []byte, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
|
||||
func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
if len(msg) < len(nonce)+1 {
|
||||
return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
|
||||
@@ -923,7 +853,7 @@ func decryptMsg(msg []byte, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte,
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
|
||||
func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -942,31 +872,42 @@ func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, e
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgkey.Key, error) {
|
||||
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgcfg.Key, error) {
|
||||
req, err := http.NewRequest("GET", serverURL+"/key", nil)
|
||||
if err != nil {
|
||||
return wgkey.Key{}, fmt.Errorf("create control key request: %v", err)
|
||||
return wgcfg.Key{}, fmt.Errorf("create control key request: %v", err)
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
res, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<16))
|
||||
if err != nil {
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key response: %v", err)
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key response: %v", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
|
||||
}
|
||||
key, err := wgkey.ParseHex(string(b))
|
||||
key, err := wgcfg.ParseHexKey(string(b))
|
||||
if err != nil {
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
|
||||
for _, ip := range ips {
|
||||
nip, ok := netaddr.FromStdIP(ip.IP())
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IP failed", ip))
|
||||
}
|
||||
ret = append(ret, nip.Unmap())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Debug contains temporary internal-only debug knobs.
|
||||
// They're unexported to not draw attention to them.
|
||||
var Debug = initDebug()
|
||||
@@ -1125,12 +1066,11 @@ func TrimWGConfig() opt.Bool {
|
||||
// and will definitely not work for the routes provided.
|
||||
//
|
||||
// It should not return false positives.
|
||||
func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
func ipForwardingBroken(routes []wgcfg.CIDR) bool {
|
||||
if len(routes) == 0 {
|
||||
// Nothing to route, so no need to warn.
|
||||
return false
|
||||
}
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
// We only do subnet routing on Linux for now.
|
||||
// It might work on darwin/macOS when building from source, so
|
||||
@@ -1138,57 +1078,17 @@ func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
// already in the admin panel.
|
||||
return false
|
||||
}
|
||||
|
||||
v4Routes, v6Routes := false, false
|
||||
for _, r := range routes {
|
||||
if r.IP.Is4() {
|
||||
v4Routes = true
|
||||
} else {
|
||||
v6Routes = true
|
||||
}
|
||||
out, err := ioutil.ReadFile("/proc/sys/net/ipv4/ip_forward")
|
||||
if err != nil {
|
||||
// Try another way.
|
||||
out, err = exec.Command("sysctl", "-n", "net.ipv4.ip_forward").Output()
|
||||
}
|
||||
|
||||
if v4Routes {
|
||||
out, err := ioutil.ReadFile("/proc/sys/net/ipv4/ip_forward")
|
||||
if err != nil {
|
||||
// Try another way.
|
||||
out, err = exec.Command("sysctl", "-n", "net.ipv4.ip_forward").Output()
|
||||
}
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is just for debugging.
|
||||
// We don't want false positives.
|
||||
// TODO: maybe we want a different warning for inability to check?
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(string(out)) == "0" {
|
||||
return true
|
||||
}
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is just for debugging.
|
||||
// We don't want false positives.
|
||||
// TODO: maybe we want a different warning for inability to check?
|
||||
return false
|
||||
}
|
||||
if v6Routes {
|
||||
// Note: you might be wondering why we check only the state of
|
||||
// conf.all.forwarding, rather than per-interface forwarding
|
||||
// configuration. According to kernel documentation, it seems
|
||||
// that to actually forward packets, you need to enable
|
||||
// forwarding globally, and the per-interface forwarding
|
||||
// setting only alters other things such as how router
|
||||
// advertisements are handled. The kernel itself warns that
|
||||
// enabling forwarding per-interface and not globally will
|
||||
// probably not work, so I feel okay calling those configs
|
||||
// broken until we have proof otherwise.
|
||||
out, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/forwarding")
|
||||
if err != nil {
|
||||
out, err = exec.Command("sysctl", "-n", "net.ipv6.conf.all.forwarding").Output()
|
||||
}
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is just for debugging.
|
||||
// We don't want false positives.
|
||||
// TODO: maybe we want a different warning for inability to check?
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(string(out)) == "0" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return strings.TrimSpace(string(out)) == "0"
|
||||
// TODO: also check IPv6 if 'routes' contains any IPv6 routes
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
@@ -92,67 +91,3 @@ func formatNodes(nodes []*tailcfg.Node) string {
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
opts := Options{ServerURL: "https://example.com", MachinePrivateKey: key, Hostinfo: hi}
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.serverURL != opts.ServerURL {
|
||||
t.Errorf("c.serverURL got %v want %v", c.serverURL, opts.ServerURL)
|
||||
}
|
||||
|
||||
if !hi.Equal(c.hostinfo) {
|
||||
t.Errorf("c.hostinfo got %v want %v", c.hostinfo, hi)
|
||||
}
|
||||
|
||||
changed := c.SetNetInfo(&ni)
|
||||
if changed {
|
||||
t.Errorf("c.SetNetInfo(ni) want false got %v", changed)
|
||||
}
|
||||
ni = tailcfg.NetInfo{LinkType: "wifi"}
|
||||
changed = c.SetNetInfo(&ni)
|
||||
if !changed {
|
||||
t.Errorf("c.SetNetInfo(ni) want true got %v", changed)
|
||||
}
|
||||
|
||||
changed = c.SetHostinfo(hi)
|
||||
if changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want false got %v", changed)
|
||||
}
|
||||
hi = NewHostinfo()
|
||||
hi.Hostname = "different host name"
|
||||
changed = c.SetHostinfo(hi)
|
||||
if !changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want true got %v", changed)
|
||||
}
|
||||
|
||||
endpoints := []string{"1", "2", "3"}
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(12) want true got %v", changed)
|
||||
}
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
if changed {
|
||||
t.Errorf("c.newEndpoints(12) want false got %v", changed)
|
||||
}
|
||||
changed = c.newEndpoints(13, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
}
|
||||
endpoints = []string{"4", "5", "6"}
|
||||
changed = c.newEndpoints(13, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// Parse a backward-compatible FilterRule used by control's wire
|
||||
// format, producing the most current filter format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) []filter.Match {
|
||||
// Parse a backward-compatible FilterRule used by control's wire format,
|
||||
// producing the most current filter.Matches format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) filter.Matches {
|
||||
mm, err := filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
c.logf("parsePacketFilter: %s\n", err)
|
||||
|
||||
@@ -73,7 +73,7 @@ func osVersionLinux() string {
|
||||
return fmt.Sprintf("%s%s", bytes.TrimSpace(cr), attr)
|
||||
}
|
||||
fallthrough
|
||||
case "fedora", "rhel", "alpine", "nixos":
|
||||
case "fedora", "rhel", "alpine":
|
||||
// Their PRETTY_NAME is fine as-is for all versions I tested.
|
||||
fallthrough
|
||||
default:
|
||||
|
||||
@@ -14,11 +14,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -26,24 +23,18 @@ type NetworkMap struct {
|
||||
// Core networking
|
||||
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgkey.Private
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
Expiry time.Time
|
||||
// Name is the DNS name assigned to this node.
|
||||
Name string
|
||||
Addresses []netaddr.IPPrefix
|
||||
Addresses []wgcfg.CIDR
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
MachineKey tailcfg.MachineKey
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
DNS tailcfg.DNSConfig
|
||||
Hostinfo tailcfg.Hostinfo
|
||||
PacketFilter []filter.Match
|
||||
|
||||
// CollectServices reports whether this node's Tailnet has
|
||||
// requested that info about services be included in HostInfo.
|
||||
// If set, Hostinfo.ShieldsUp blocks services collection; that
|
||||
// takes precedence over this field.
|
||||
CollectServices bool
|
||||
PacketFilter filter.Matches
|
||||
|
||||
// DERPMap is the last DERP server map received. It's reused
|
||||
// between updates and should not be modified.
|
||||
@@ -63,30 +54,7 @@ type NetworkMap struct {
|
||||
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
||||
}
|
||||
|
||||
// MagicDNSSuffix returns the domain's MagicDNS suffix, or empty if none.
|
||||
// If non-empty, it will neither start nor end with a period.
|
||||
func (nm *NetworkMap) MagicDNSSuffix() string {
|
||||
searchPathUsedAsDNSSuffix := func(suffix string) bool {
|
||||
if dnsname.HasSuffix(nm.Name, suffix) {
|
||||
return true
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if dnsname.HasSuffix(p.Name, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, d := range nm.DNS.Domains {
|
||||
if searchPathUsedAsDNSSuffix(d) {
|
||||
return strings.Trim(d, ".")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (nm *NetworkMap) String() string {
|
||||
func (nm NetworkMap) String() string {
|
||||
return nm.Concise()
|
||||
}
|
||||
|
||||
@@ -272,7 +240,7 @@ const EndpointDiscoSuffix = ".disco.tailscale:12345"
|
||||
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
|
||||
cfg := &wgcfg.Config{
|
||||
Name: "tailscale",
|
||||
PrivateKey: wgcfg.PrivateKey(nm.PrivateKey),
|
||||
PrivateKey: nm.PrivateKey,
|
||||
Addresses: nm.Addresses,
|
||||
ListenPort: nm.LocalPort,
|
||||
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
|
||||
@@ -298,7 +266,7 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
if err := appendEndpoint(cpeer, fmt.Sprintf("%x%s", peer.DiscoKey[:], EndpointDiscoSuffix)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cpeer.Endpoints = fmt.Sprintf("%x.disco.tailscale:12345", peer.DiscoKey[:])
|
||||
cpeer.Endpoints = []wgcfg.Endpoint{{Host: fmt.Sprintf("%x.disco.tailscale", peer.DiscoKey[:]), Port: 12345}}
|
||||
} else {
|
||||
if err := appendEndpoint(cpeer, peer.DERP); err != nil {
|
||||
return nil, err
|
||||
@@ -310,14 +278,14 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
}
|
||||
}
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Bits == 0 {
|
||||
if allowedIP.Mask == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
logf("wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
logf("wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -330,11 +298,17 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
|
||||
// cidrIsSubnet reports whether cidr is a non-default-route subnet
|
||||
// exported by node that is not one of its own self addresses.
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {
|
||||
if cidr.Bits == 0 {
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr wgcfg.CIDR) bool {
|
||||
if cidr.Mask == 0 {
|
||||
return false
|
||||
}
|
||||
if !cidr.IsSingleIP() {
|
||||
if cidr.Mask < 32 {
|
||||
// Fast path for IPv4, to avoid loop below.
|
||||
//
|
||||
// TODO: if cidr.IP is an IPv6 address, we could do "< 128"
|
||||
// to avoid the range over node.Addresses. Or we could
|
||||
// just remove this fast path and unconditionally do the range
|
||||
// loop.
|
||||
return true
|
||||
}
|
||||
for _, selfCIDR := range node.Addresses {
|
||||
@@ -349,18 +323,15 @@ func appendEndpoint(peer *wgcfg.Peer, epStr string) error {
|
||||
if epStr == "" {
|
||||
return nil
|
||||
}
|
||||
_, port, err := net.SplitHostPort(epStr)
|
||||
host, port, err := net.SplitHostPort(epStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
_, err = strconv.ParseUint(port, 10, 16)
|
||||
port16, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port in endpoint %q for peer %v", epStr, peer.PublicKey.ShortString())
|
||||
}
|
||||
if peer.Endpoints != "" {
|
||||
peer.Endpoints += ","
|
||||
}
|
||||
peer.Endpoints += epStr
|
||||
peer.Endpoints = append(peer.Endpoints, wgcfg.Endpoint{Host: host, Port: uint16(port16)})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -380,7 +351,7 @@ func eqStringsIgnoreNil(a, b []string) bool {
|
||||
|
||||
// eqCIDRsIgnoreNil reports whether a and b have the same length and
|
||||
// contents, but ignore whether a or b are nil.
|
||||
func eqCIDRsIgnoreNil(a, b []netaddr.IPPrefix) bool {
|
||||
func eqCIDRsIgnoreNil(a, b []wgcfg.CIDR) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@ package controlclient
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -251,7 +250,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("f00f00f00f"),
|
||||
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -264,7 +263,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("ba4ba4ba4b"),
|
||||
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -283,15 +282,3 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/wgkey"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
)
|
||||
|
||||
func TestPersistEqual(t *testing.T) {
|
||||
@@ -18,8 +18,8 @@ func TestPersistEqual(t *testing.T) {
|
||||
have, persistHandles)
|
||||
}
|
||||
|
||||
newPrivate := func() wgkey.Private {
|
||||
k, err := wgkey.NewPrivate()
|
||||
newPrivate := func() wgcfg.PrivateKey {
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -358,7 +358,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
|
||||
dialer := netns.NewDialer()
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, _, err := c.DNSCache.LookupIP(ctx, host)
|
||||
ip, err := c.DNSCache.LookupIP(ctx, host)
|
||||
if err == nil {
|
||||
hostOrIP = ip.String()
|
||||
}
|
||||
|
||||
37
go.mod
37
go.mod
@@ -1,6 +1,6 @@
|
||||
module tailscale.com
|
||||
|
||||
go 1.15
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
|
||||
@@ -12,33 +12,30 @@ require (
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/google/go-cmp v0.5.4
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1
|
||||
github.com/mdlayher/netlink v1.2.0
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
|
||||
github.com/kr/pty v1.1.1
|
||||
github.com/mdlayher/netlink v1.1.0
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d
|
||||
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
461
go.sum
461
go.sum
@@ -1,34 +1,8 @@
|
||||
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.52.1-0.20200122224058-0482b626c726/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
|
||||
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
@@ -44,530 +18,183 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/cenkalti/backoff v1.1.1-0.20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
|
||||
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
|
||||
github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
|
||||
github.com/containerd/containerd v1.3.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY=
|
||||
github.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
|
||||
github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
|
||||
github.com/containerd/ttrpc v0.0.0-20200121165050-0be804eadb15/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
|
||||
github.com/containerd/typeurl v0.0.0-20200205145503-b45ef1f1f737/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
|
||||
github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRYm38=
|
||||
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20191028175130-9e7d5ac5ea55/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/dpjacques/clockwork v0.1.1-0.20200827220843-c1f524b839be/go.mod h1:D8mP2A8vVT2GkXqPorSBmhnshhkFBYgzhA90KmJt25Y=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
|
||||
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3-0.20201020212313-ab46b8bd0abd/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github/v28 v28.1.2-0.20191108005307-e555eab49ce8/go.mod h1:g82e6OHbJ0WYrYeOrid1MMfHAtqjxBz+N74tfAt9KrQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
|
||||
github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w=
|
||||
github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391 h1:Dqu/4JhMV1vpXHDjzQCuDCEsjNi0xfuSmQlMOyqayKA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
||||
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1 h1:zc0R6cOw98cMengLA0fvU55mqbnN7sd/tBMLzSejp+M=
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
|
||||
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
|
||||
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
|
||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
||||
github.com/mdlayher/netlink v1.2.0 h1:zPolhRjfuabdf8ofZsl56eoU+92cvSlAn13lw4veCZ0=
|
||||
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jqIxaVL5t6gSq1hjPiaWH7TOcA0Z+uNo=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2-0.20181111125026-1722abf79c2f/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e h1:ZXbXfVJOhSq4/Gt7TnqwXBPCctzYXkWXo3oQS7LZ40I=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98 h1:khwYPK1eT+4pmEFyCjpf6Br/0JWjdVT3uQ+ILFJPTRo=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551 h1:hjBVxvVa145kVflAFkVcTr/zwUzBO4SqfSS6xhbcMv8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4Ui+lzHm6gOq7s2Oe4ksxkbUYtS/JuoJ2Nce8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
|
||||
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be h1:ZKe3kVGbu/goUVxXcaCPbQ4b0STQ5NsCpG90CG6mw/c=
|
||||
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be/go.mod h1:jissDaJNHiyV2tFdr3QyNEfsZrax/i2yQiSO+CljThI=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200921221757-11a958a67bdd h1:yEWpro9EdxGgkt24NInVnONIJxRLURH5c37Ki5+06EE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200921221757-11a958a67bdd/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859 h1:Z7bXXCYRg/8sjSyKTk0V8Yso/gQjNvPb10DBemKuz+A=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f h1:KMx58dbn2YCutzOvjNHgmvbwQH7nGE8H+J42Nenjl/M=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vishvananda/netlink v1.0.1-0.20190930145447-2ec5bdc52b86/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7 h1:yeDrXaQ3VRXbTN7lHj70DxW4LdPow83MVwPPRjpP70U=
|
||||
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb h1:yuqO0E4bHRsTPUocDpRKXfLE40lwWplVxENQ2WOV7Gc=
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20210101010959-7cab76ca296a h1:28p852HIWWaOS019DYK/A3yTmpm1HJaUce63pvll4C8=
|
||||
go4.org/intern v0.0.0-20210101010959-7cab76ca296a/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e h1:ExUmGi0ZsQmiVo9giDQqXkr7vreeXPMkOGIusfsfbzI=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc h1:paujszgN6SpsO/UsXC7xax3gQAKz/XQKCYZLQdU34Tw=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo=
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201021000207-d49c4edd7d96/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d h1:vWQvJ/Z0Lu+9/8oQ/pAYXNzbc7CMnBl+tULGVHOy3oE=
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42 h1:SrR1hmxGKKarHEEDvaHxatwnqE3uT+7jvMcin6SHOkw=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81 h1:cT2oWlz8v9g7bjFZclT362akxJJfGv9d7ccKu6GQUbA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b h1:jEdfCm+8YTWSYgU4L7Nq0jjU+q9RxIhi0cXLTY+Ih3A=
|
||||
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b/go.mod h1:hFxJC2f0epmp1elRCiEGJTKAWbwxZ2nvqZdHl3FQXCY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6 h1:H5EvGkFG+pgAAbZMV8Me3Gy+HUYdaDcGXKWWixZ0EE8=
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6/go.mod h1:5DEMKRjYDiM24fvDUWPjBpABm9ROMcv/kEcox3fHtm0=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
|
||||
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
|
||||
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf h1:0eHZ8v6j5wIiOVyoYPd70ueZ/RPEQtRlzi60uneDbRU=
|
||||
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d h1:6f0242aW/6x2enQBOSKgDS8KQNw6Tp7IVR8eG3x0Jc8=
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d/go.mod h1:jPZo7Jy4nke2cCgISa4fKJKa5T7+EO8k5fWwWghzneg=
|
||||
k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs=
|
||||
k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
|
||||
k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
|
||||
k8s.io/client-go v0.16.13/go.mod h1:UKvVT4cajC2iN7DCjLgT0KVY/cbY6DGdUCyRiIfws5M=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20200410163147-594e756bea31/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98 h1:bWyWDZP0l6VnQ1TDKf6yNwuiEDV6Q3q1Mv34m+lzT1I=
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
||||
@@ -37,11 +37,16 @@ func getVal() []interface{} {
|
||||
return []interface{}{
|
||||
&wgcfg.Config{
|
||||
Name: "foo",
|
||||
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
|
||||
Addresses: []wgcfg.CIDR{{Mask: 5, IP: wgcfg.IP{Addr: [16]byte{3: 3}}}},
|
||||
ListenPort: 5,
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
Endpoints: "foo:5",
|
||||
Endpoints: []wgcfg.Endpoint{
|
||||
{
|
||||
Host: "foo",
|
||||
Port: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -118,7 +118,7 @@ func (h *Handle) EngineStatus() EngineStatus {
|
||||
return h.engineStatusCache
|
||||
}
|
||||
|
||||
func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
|
||||
func (h *Handle) LocalAddrs() []wgcfg.CIDR {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
@@ -126,7 +126,7 @@ func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
|
||||
if nm != nil {
|
||||
return nm.Addresses
|
||||
}
|
||||
return []netaddr.IPPrefix{}
|
||||
return []wgcfg.CIDR{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *controlclient.NetworkMap {
|
||||
|
||||
@@ -1,49 +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.
|
||||
|
||||
// +build linux
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func isReadonlyConn(c net.Conn, logf logger.Logf) (ro bool) {
|
||||
ro = true // conservative default for naked returns below
|
||||
uc, ok := c.(*net.UnixConn)
|
||||
if !ok {
|
||||
logf("unexpected connection type %T", c)
|
||||
return
|
||||
}
|
||||
raw, err := uc.SyscallConn()
|
||||
if err != nil {
|
||||
logf("SyscallConn: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var cred *unix.Ucred
|
||||
cerr := raw.Control(func(fd uintptr) {
|
||||
cred, err = unix.GetsockoptUcred(int(fd),
|
||||
unix.SOL_SOCKET,
|
||||
unix.SO_PEERCRED)
|
||||
})
|
||||
if cerr != nil {
|
||||
logf("raw.Control: %v", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logf("raw.Control: %v", err)
|
||||
return
|
||||
}
|
||||
if cred.Uid == 0 {
|
||||
// root is not read-only.
|
||||
return false
|
||||
}
|
||||
logf("non-root connection from %v (read-only)", cred.Uid)
|
||||
return true
|
||||
}
|
||||
@@ -1,27 +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.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func isReadonlyConn(c net.Conn, logf logger.Logf) bool {
|
||||
// Windows doesn't need/use this mechanism, at least yet. It
|
||||
// has a different last-user-wins auth model.
|
||||
|
||||
// And on Darwin, we're not using it yet, as the Darwin
|
||||
// tailscaled port isn't yet done, and unix.Ucred and
|
||||
// unix.GetsockoptUcred aren't in x/sys/unix.
|
||||
|
||||
// TODO(bradfitz): OpenBSD and FreeBSD should implement this too.
|
||||
// But their x/sys/unix package is different than Linux, so
|
||||
// I didn't include it for now.
|
||||
return false
|
||||
}
|
||||
@@ -34,7 +34,6 @@ import (
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -266,24 +265,18 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
}
|
||||
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("[v1] incoming control connection")
|
||||
|
||||
if isReadonlyConn(c, logf) {
|
||||
ctx = ipn.ReadonlyContextOf(ctx)
|
||||
}
|
||||
logf("incoming control connection")
|
||||
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(br)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
logf("[v1] ReadMsg: %v", err)
|
||||
} else if ctx.Err() == nil {
|
||||
if ctx.Err() == nil {
|
||||
logf("ReadMsg: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.bsMu.Lock()
|
||||
if err := s.bs.GotCommandMsg(ctx, msg); err != nil {
|
||||
if err := s.bs.GotCommandMsg(msg); err != nil {
|
||||
logf("GotCommandMsg: %v", err)
|
||||
}
|
||||
gotQuit := s.bs.GotQuit
|
||||
@@ -359,7 +352,7 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
if doReset {
|
||||
s.logf("identity changed; resetting server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bs.Reset()
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}()
|
||||
@@ -411,7 +404,7 @@ func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bs.Reset()
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -585,7 +578,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
||||
|
||||
if opts.AutostartStateKey != "" {
|
||||
server.bs.GotCommand(context.TODO(), &ipn.Command{
|
||||
server.bs.GotCommand(&ipn.Command{
|
||||
Version: version.Long,
|
||||
Start: &ipn.StartArgs{
|
||||
Opts: ipn.Options{
|
||||
@@ -596,7 +589,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
})
|
||||
}
|
||||
|
||||
systemd.Ready()
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
var c net.Conn
|
||||
var err error
|
||||
@@ -732,10 +724,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
// pipe. We'll make a new one when we restart the subproc.
|
||||
wStdin.Close()
|
||||
|
||||
if os.Getenv("TS_DEBUG_RESTART_CRASHED") == "0" {
|
||||
log.Fatalf("Process ended.")
|
||||
}
|
||||
|
||||
if time.Since(startTime) < 60*time.Second {
|
||||
bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err))
|
||||
} else {
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0, nil)
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -25,10 +25,9 @@ import (
|
||||
|
||||
// Status represents the entire state of the IPN network.
|
||||
type Status struct {
|
||||
BackendState string
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
MagicDNSSuffix string // e.g. "userfoo.tailscale.net" (no surrounding dots)
|
||||
BackendState string
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
@@ -65,12 +64,6 @@ type PeerStatus struct {
|
||||
LastHandshake time.Time // with local wireguard
|
||||
KeepAlive bool
|
||||
|
||||
// ShareeNode indicates this node exists in the netmap because
|
||||
// it's owned by a shared-to user and that node might connect
|
||||
// to us. These nodes should be hidden by "tailscale status"
|
||||
// etc by default.
|
||||
ShareeNode bool `json:",omitempty"`
|
||||
|
||||
// InNetworkMap means that this peer was seen in our latest network map.
|
||||
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
|
||||
InNetworkMap bool
|
||||
@@ -104,12 +97,6 @@ func (sb *StatusBuilder) SetBackendState(v string) {
|
||||
sb.st.BackendState = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) SetMagicDNSSuffix(v string) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.MagicDNSSuffix = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) Status() *Status {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
@@ -231,9 +218,6 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if st.KeepAlive {
|
||||
e.KeepAlive = true
|
||||
}
|
||||
if st.ShareeNode {
|
||||
e.ShareeNode = true
|
||||
}
|
||||
}
|
||||
|
||||
type StatusUpdater interface {
|
||||
|
||||
189
ipn/local.go
189
ipn/local.go
@@ -29,8 +29,6 @@ import (
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -84,7 +82,7 @@ type LocalBackend struct {
|
||||
userID string // current controlling user ID (for Windows, primarily)
|
||||
prefs *Prefs
|
||||
inServerMode bool
|
||||
machinePrivKey wgkey.Private
|
||||
machinePrivKey wgcfg.PrivateKey
|
||||
state State
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
@@ -201,7 +199,6 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
// TODO: hostinfo, and its networkinfo
|
||||
// TODO: EngineStatus copy (and deprecate it?)
|
||||
if b.netMap != nil {
|
||||
sb.SetMagicDNSSuffix(b.netMap.MagicDNSSuffix())
|
||||
for id, up := range b.netMap.UserProfiles {
|
||||
sb.AddUser(id, up)
|
||||
}
|
||||
@@ -211,14 +208,8 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
lastSeen = *p.LastSeen
|
||||
}
|
||||
var tailAddr string
|
||||
for _, addr := range p.Addresses {
|
||||
// The peer struct currently only allows a single
|
||||
// Tailscale IP address. For compatibility with the
|
||||
// old display, make sure it's the IPv4 address.
|
||||
if addr.IP.Is4() && addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) {
|
||||
tailAddr = addr.IP.String()
|
||||
break
|
||||
}
|
||||
if len(p.Addresses) > 0 {
|
||||
tailAddr = strings.TrimSuffix(p.Addresses[0].String(), "/32")
|
||||
}
|
||||
sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
@@ -230,7 +221,6 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -253,11 +243,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// The following do not depend on any data for which we need to lock b.
|
||||
if st.Err != "" {
|
||||
// TODO(crawshaw): display in the UI.
|
||||
if st.Err == "EOF" {
|
||||
b.logf("[v1] Received error: EOF")
|
||||
} else {
|
||||
b.logf("Received error: %v", st.Err)
|
||||
}
|
||||
b.logf("Received error: %v", st.Err)
|
||||
return
|
||||
}
|
||||
if st.LoginFinished != nil {
|
||||
@@ -321,7 +307,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
if netMap != nil {
|
||||
diff := st.NetMap.ConciseDiffFrom(netMap)
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
b.logf("[v1] netmap diff: (none)")
|
||||
b.logf("netmap diff: (none)")
|
||||
} else {
|
||||
b.logf("netmap diff:\n%v", diff)
|
||||
}
|
||||
@@ -352,7 +338,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// This updates the endpoints both in the backend and in the control client.
|
||||
func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
if err != nil {
|
||||
b.logf("wgengine status error: %v", err)
|
||||
b.logf("wgengine status error: %#v", err)
|
||||
return
|
||||
}
|
||||
if s == nil {
|
||||
@@ -536,9 +522,9 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
// quite hard to debug, so save yourself the trouble.
|
||||
var (
|
||||
haveNetmap = netMap != nil
|
||||
addrs []netaddr.IPPrefix
|
||||
packetFilter []filter.Match
|
||||
advRoutes []netaddr.IPPrefix
|
||||
addrs []wgcfg.CIDR
|
||||
packetFilter filter.Matches
|
||||
advRoutes []wgcfg.CIDR
|
||||
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
|
||||
)
|
||||
if haveNetmap {
|
||||
@@ -560,11 +546,12 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
return
|
||||
}
|
||||
|
||||
localNets := unmapIPPrefixes(netMap.Addresses, advRoutes)
|
||||
localNets := wgCIDRsToFilter(netMap.Addresses, advRoutes)
|
||||
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(b.logf))
|
||||
var prevFilter *filter.Filter // don't reuse old filter state
|
||||
b.e.SetFilter(filter.New(filter.Matches{}, localNets, prevFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v", packetFilter)
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
|
||||
@@ -573,7 +560,7 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
|
||||
// dnsCIDRsEqual determines whether two CIDR lists are equal
|
||||
// for DNS map construction purposes (that is, only the first entry counts).
|
||||
func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool {
|
||||
func dnsCIDRsEqual(newAddr, oldAddr []wgcfg.CIDR) bool {
|
||||
if len(newAddr) != len(oldAddr) {
|
||||
return false
|
||||
}
|
||||
@@ -627,11 +614,11 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
}
|
||||
|
||||
nameToIP := make(map[string]netaddr.IP)
|
||||
set := func(name string, addrs []netaddr.IPPrefix) {
|
||||
set := func(name string, addrs []wgcfg.CIDR) {
|
||||
if len(addrs) == 0 || name == "" {
|
||||
return
|
||||
}
|
||||
nameToIP[name] = addrs[0].IP
|
||||
nameToIP[name] = netaddr.IPFrom16(addrs[0].IP.Addr)
|
||||
}
|
||||
|
||||
for _, peer := range netMap.Peers {
|
||||
@@ -639,7 +626,7 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
}
|
||||
set(netMap.Name, netMap.Addresses)
|
||||
|
||||
dnsMap := tsdns.NewMap(nameToIP, magicDNSRootDomains(netMap))
|
||||
dnsMap := tsdns.NewMap(nameToIP, domainsForProxying(netMap))
|
||||
// map diff will be logged in tsdns.Resolver.SetMap.
|
||||
b.e.SetDNSMap(dnsMap)
|
||||
}
|
||||
@@ -738,7 +725,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var legacyMachineKey wgkey.Private
|
||||
var legacyMachineKey wgcfg.PrivateKey
|
||||
if b.prefs.Persist != nil {
|
||||
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
|
||||
}
|
||||
@@ -773,7 +760,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
} else {
|
||||
b.logf("generating new machine key")
|
||||
var err error
|
||||
b.machinePrivKey, err = wgkey.NewPrivate()
|
||||
b.machinePrivKey, err = wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing new machine key: %w", err)
|
||||
}
|
||||
@@ -1008,24 +995,22 @@ func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret EngineStatus
|
||||
|
||||
// [GRINDER STATS LINES] - please don't remove (used for log parsing)
|
||||
if peerStats.Len() > 0 {
|
||||
b.keyLogf("[v1] peer keys: %s", strings.TrimSpace(peerKeys.String()))
|
||||
b.statsLogf("[v1] v%v peers: %v", version.Long, strings.TrimSpace(peerStats.String()))
|
||||
b.keyLogf("peer keys: %s", strings.TrimSpace(peerKeys.String()))
|
||||
b.statsLogf("v%v peers: %v", version.Long, strings.TrimSpace(peerStats.String()))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// shouldUploadServices reports whether this node should include services
|
||||
// in Hostinfo. When the user preferences currently request "shields up"
|
||||
// mode, all inbound connections are refused, so services are not reported.
|
||||
// Otherwise, shouldUploadServices respects NetMap.CollectServices.
|
||||
func (b *LocalBackend) shouldUploadServices() bool {
|
||||
// shieldsAreUp returns whether user preferences currently request
|
||||
// "shields up" mode, which disallows all inbound connections.
|
||||
func (b *LocalBackend) shieldsAreUp() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.prefs == nil || b.netMap == nil {
|
||||
return false // default to safest setting
|
||||
if b.prefs == nil {
|
||||
return true // default to safest setting
|
||||
}
|
||||
return !b.prefs.ShieldsUp && b.netMap.CollectServices
|
||||
return b.prefs.ShieldsUp
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SetCurrentUserID(uid string) {
|
||||
@@ -1067,7 +1052,7 @@ func (b *LocalBackend) SetPrefs(newp *Prefs) {
|
||||
|
||||
oldHi := b.hostinfo
|
||||
newHi := oldHi.Clone()
|
||||
newHi.RoutableIPs = append([]netaddr.IPPrefix(nil), b.prefs.AdvertiseRoutes...)
|
||||
newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...)
|
||||
applyPrefsToHostinfo(newHi, newp)
|
||||
b.hostinfo = newHi
|
||||
hostInfoChanged := !oldHi.Equal(newHi)
|
||||
@@ -1125,7 +1110,9 @@ func (b *LocalBackend) SetPrefs(newp *Prefs) {
|
||||
// painstakingly constructing it in twelvety other places.
|
||||
func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
|
||||
hi2 := *hi
|
||||
if !b.shouldUploadServices() {
|
||||
if b.shieldsAreUp() {
|
||||
// No local services are available, since ShieldsUp will block
|
||||
// them all.
|
||||
hi2.Services = []tailcfg.Service{}
|
||||
}
|
||||
|
||||
@@ -1167,7 +1154,6 @@ func (b *LocalBackend) authReconfig() {
|
||||
uc := b.prefs
|
||||
nm := b.netMap
|
||||
hasPAC := b.prevIfState.HasPAC()
|
||||
disableSubnetsIfPAC := nm != nil && nm.Debug != nil && nm.Debug.DisableSubnetsIfPAC.EqualBool(true)
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
@@ -1192,7 +1178,13 @@ func (b *LocalBackend) authReconfig() {
|
||||
if uc.AllowSingleHosts {
|
||||
flags |= controlclient.AllowSingleHosts
|
||||
}
|
||||
if hasPAC && disableSubnetsIfPAC {
|
||||
if hasPAC {
|
||||
// TODO(bradfitz): make this policy configurable per
|
||||
// domain, flesh out all the edge cases where subnet
|
||||
// routes might shadow corp HTTP proxies, DNS servers,
|
||||
// domain controllers, etc. For now we just want
|
||||
// Tailscale to stay enabled while laptops roam
|
||||
// between corp & non-corp networks.
|
||||
if flags&controlclient.AllowSubnetRoutes != 0 {
|
||||
b.logf("authReconfig: have PAC; disabling subnet routes")
|
||||
flags &^= controlclient.AllowSubnetRoutes
|
||||
@@ -1209,14 +1201,20 @@ func (b *LocalBackend) authReconfig() {
|
||||
|
||||
// If CorpDNS is false, rcfg.DNS remains the zero value.
|
||||
if uc.CorpDNS {
|
||||
domains := nm.DNS.Domains
|
||||
proxied := nm.DNS.Proxied
|
||||
if proxied && len(nm.DNS.Nameservers) == 0 {
|
||||
b.logf("[unexpected] dns proxied but no nameservers")
|
||||
proxied = false
|
||||
if proxied {
|
||||
if len(nm.DNS.Nameservers) == 0 {
|
||||
b.logf("[unexpected] dns proxied but no nameservers")
|
||||
proxied = false
|
||||
} else {
|
||||
// Domains for proxying should come first to avoid leaking queries.
|
||||
domains = append(domainsForProxying(nm), domains...)
|
||||
}
|
||||
}
|
||||
rcfg.DNS = dns.Config{
|
||||
Nameservers: nm.DNS.Nameservers,
|
||||
Domains: nm.DNS.Domains,
|
||||
Domains: domains,
|
||||
PerDomain: nm.DNS.PerDomain,
|
||||
Proxied: proxied,
|
||||
}
|
||||
@@ -1226,29 +1224,56 @@ func (b *LocalBackend) authReconfig() {
|
||||
if err == wgengine.ErrNoChanges {
|
||||
return
|
||||
}
|
||||
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
|
||||
b.logf("authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
|
||||
}
|
||||
|
||||
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
|
||||
// Each entry has a trailing period.
|
||||
func magicDNSRootDomains(nm *controlclient.NetworkMap) []string {
|
||||
if v := nm.MagicDNSSuffix(); v != "" {
|
||||
return []string{strings.Trim(v, ".") + "."}
|
||||
// domainsForProxying produces a list of search domains for proxied DNS.
|
||||
func domainsForProxying(nm *controlclient.NetworkMap) []string {
|
||||
var domains []string
|
||||
if idx := strings.IndexByte(nm.Name, '.'); idx != -1 {
|
||||
domains = append(domains, nm.Name[idx+1:])
|
||||
}
|
||||
return nil
|
||||
for _, peer := range nm.Peers {
|
||||
idx := strings.IndexByte(peer.Name, '.')
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
domain := peer.Name[idx+1:]
|
||||
seen := false
|
||||
// In theory this makes the function O(n^2) worst case,
|
||||
// but in practice we expect domains to contain very few elements
|
||||
// (only one until invitations are introduced).
|
||||
for _, seenDomain := range domains {
|
||||
if domain == seenDomain {
|
||||
seen = true
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
|
||||
func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
|
||||
var addrs []wgcfg.CIDR
|
||||
for _, addr := range cfg.Addresses {
|
||||
addrs = append(addrs, wgcfg.CIDR{
|
||||
IP: addr.IP,
|
||||
Mask: 32,
|
||||
})
|
||||
}
|
||||
|
||||
rs := &router.Config{
|
||||
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
|
||||
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes),
|
||||
LocalAddrs: wgCIDRToNetaddr(addrs),
|
||||
SubnetRoutes: wgCIDRToNetaddr(prefs.AdvertiseRoutes),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT,
|
||||
NetfilterMode: prefs.NetfilterMode,
|
||||
}
|
||||
|
||||
for _, peer := range cfg.Peers {
|
||||
rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...)
|
||||
rs.Routes = append(rs.Routes, wgCIDRToNetaddr(peer.AllowedIPs)...)
|
||||
}
|
||||
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
|
||||
@@ -1259,15 +1284,35 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
|
||||
return rs
|
||||
}
|
||||
|
||||
func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
|
||||
for _, ipps := range ippsList {
|
||||
for _, ipp := range ipps {
|
||||
ret = append(ret, netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits})
|
||||
// wgCIDRsToFilter converts lists of wgcfg.CIDR into a single list of
|
||||
// filter.Net.
|
||||
func wgCIDRsToFilter(cidrLists ...[]wgcfg.CIDR) (ret []filter.Net) {
|
||||
for _, cidrs := range cidrLists {
|
||||
for _, cidr := range cidrs {
|
||||
if !cidr.IP.Is4() {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, filter.Net{
|
||||
IP: filter.NewIP(cidr.IP.IP()),
|
||||
Mask: filter.Netmask(int(cidr.Mask)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func wgCIDRToNetaddr(cidrs []wgcfg.CIDR) (ret []netaddr.IPPrefix) {
|
||||
for _, cidr := range cidrs {
|
||||
ncidr, ok := netaddr.FromStdIPNet(cidr.IPNet())
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IPNet failed", cidr))
|
||||
}
|
||||
ncidr.IP = ncidr.IP.Unmap()
|
||||
ret = append(ret, ncidr)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) {
|
||||
if h := prefs.Hostname; h != "" {
|
||||
hi.Hostname = h
|
||||
@@ -1278,7 +1323,6 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) {
|
||||
if m := prefs.DeviceModel; m != "" {
|
||||
hi.DeviceModel = m
|
||||
}
|
||||
hi.ShieldsUp = prefs.ShieldsUp
|
||||
}
|
||||
|
||||
// enterState transitions the backend into newState, updating internal
|
||||
@@ -1296,8 +1340,6 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
notify := b.notify
|
||||
bc := b.c
|
||||
networkUp := b.prevIfState.AnyInterfaceUp()
|
||||
activeLogin := b.activeLogin
|
||||
authURL := b.authURL
|
||||
b.mu.Unlock()
|
||||
|
||||
if state == newState {
|
||||
@@ -1315,7 +1357,6 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
|
||||
switch newState {
|
||||
case NeedsLogin:
|
||||
systemd.Status("Needs login: %s", authURL)
|
||||
b.blockEngineUpdates(true)
|
||||
fallthrough
|
||||
case Stopped:
|
||||
@@ -1323,20 +1364,12 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
if err != nil {
|
||||
b.logf("Reconfig(down): %v", err)
|
||||
}
|
||||
|
||||
if authURL == "" {
|
||||
systemd.Status("Stopped; run 'tailscale up' to log in")
|
||||
}
|
||||
case Starting, NeedsMachineAuth:
|
||||
b.authReconfig()
|
||||
// Needed so that UpdateEndpoints can run
|
||||
b.e.RequestStatus()
|
||||
case Running:
|
||||
var addrs []string
|
||||
for _, addr := range b.netMap.Addresses {
|
||||
addrs = append(addrs, addr.IP.String())
|
||||
}
|
||||
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrs, " "))
|
||||
break
|
||||
default:
|
||||
b.logf("[unexpected] unknown newState %#v", newState)
|
||||
}
|
||||
@@ -1535,8 +1568,8 @@ func (b *LocalBackend) TestOnlyPublicKeys() (machineKey tailcfg.MachineKey, node
|
||||
// 1.0.x. But eventually we want to stop sending the machine key to
|
||||
// clients. We can't do that until 1.0.x is no longer supported.
|
||||
func temporarilySetMachineKeyInPersist() bool {
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "ios", "android":
|
||||
//lint:ignore S1008 for comments
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "android" {
|
||||
// iOS, macOS, Android users can't downgrade anyway.
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,119 +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.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tailcfg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNetworkMapCompare(t *testing.T) {
|
||||
prefix1, err := netaddr.ParseIPPrefix("192.168.0.0/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node1 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix1}}
|
||||
|
||||
prefix2, err := netaddr.ParseIPPrefix("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node2 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix2}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b *controlclient.NetworkMap
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"both nil",
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"b nil",
|
||||
&controlclient.NetworkMap{},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"a nil",
|
||||
nil,
|
||||
&controlclient.NetworkMap{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"both default",
|
||||
&controlclient.NetworkMap{},
|
||||
&controlclient.NetworkMap{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"names identical",
|
||||
&controlclient.NetworkMap{Name: "map1"},
|
||||
&controlclient.NetworkMap{Name: "map1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"names differ",
|
||||
&controlclient.NetworkMap{Name: "map1"},
|
||||
&controlclient.NetworkMap{Name: "map2"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Peers identical",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Peer list length",
|
||||
// length of Peers list differs
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{{}}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node lists identical",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node lists differ",
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node Users differ",
|
||||
// User field is not checked.
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
|
||||
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := dnsMapsEqual(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,9 @@ import (
|
||||
func TestLocalLogLines(t *testing.T) {
|
||||
logListen := tstest.NewLogLineTracker(t.Logf, []string{
|
||||
"SetPrefs: %v",
|
||||
"[v1] peer keys: %s",
|
||||
"[v1] v%v peers: %v",
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
})
|
||||
defer logListen.Close()
|
||||
|
||||
logid := func(hex byte) logtail.PublicID {
|
||||
var ret logtail.PublicID
|
||||
@@ -41,7 +40,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
store := &MemoryStore{
|
||||
cache: make(map[StateKey][]byte),
|
||||
}
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0, nil)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -72,7 +71,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
prefs.Persist = persist
|
||||
lb.SetPrefs(prefs)
|
||||
|
||||
t.Run("after_prefs", testWantRemain("[v1] peer keys: %s", "[v1] v%v peers: %v"))
|
||||
t.Run("after_prefs", testWantRemain("peer keys: %s", "v%v peers: %v"))
|
||||
|
||||
// log peers, peer keys
|
||||
status := &wgengine.Status{
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -21,26 +19,6 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
type readOnlyContextKey struct{}
|
||||
|
||||
// IsReadonlyContext reports whether ctx is a read-only context, as currently used
|
||||
// by Unix non-root users running the "tailscale" CLI command. They can run "status",
|
||||
// but not much else.
|
||||
func IsReadonlyContext(ctx context.Context) bool {
|
||||
return ctx.Value(readOnlyContextKey{}) != nil
|
||||
}
|
||||
|
||||
// ReadonlyContextOf returns ctx wrapped with a context value that
|
||||
// will make IsReadonlyContext reports true.
|
||||
func ReadonlyContextOf(ctx context.Context) context.Context {
|
||||
if IsReadonlyContext(ctx) {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, readOnlyContextKey{}, readOnlyContextKey{})
|
||||
}
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
type NoArgs struct{}
|
||||
|
||||
type StartArgs struct {
|
||||
@@ -107,9 +85,6 @@ func (bs *BackendServer) send(n Notify) {
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(notify): %v\n%#v", err, n)
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
|
||||
}
|
||||
bs.sendNotifyMsg(b)
|
||||
}
|
||||
|
||||
@@ -130,7 +105,7 @@ func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
|
||||
|
||||
// GotCommandMsg parses the incoming message b as a JSON Command and
|
||||
// calls GotCommand with it.
|
||||
func (bs *BackendServer) GotCommandMsg(ctx context.Context, b []byte) error {
|
||||
func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
cmd := &Command{}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
@@ -138,15 +113,15 @@ func (bs *BackendServer) GotCommandMsg(ctx context.Context, b []byte) error {
|
||||
if err := json.Unmarshal(b, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
return bs.GotCommand(cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error {
|
||||
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
|
||||
cmd.Version = version.Long
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
return bs.GotCommand(cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
|
||||
cmd.Version, version.Long)
|
||||
@@ -160,33 +135,12 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(bradfitz): finish plumbing context down to all the methods below;
|
||||
// currently we just check for read-only contexts in this method and
|
||||
// then never use contexts again.
|
||||
|
||||
// Actions permitted with a read-only context:
|
||||
if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.RequestStatus; c != nil {
|
||||
bs.b.RequestStatus()
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
}
|
||||
|
||||
if IsReadonlyContext(ctx) {
|
||||
msg := "permission denied"
|
||||
bs.send(Notify{ErrMessage: &msg})
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd.Quit != nil {
|
||||
bs.GotQuit = true
|
||||
return errors.New("Quit command received")
|
||||
} else if c := cmd.Start; c != nil {
|
||||
}
|
||||
|
||||
if c := cmd.Start; c != nil {
|
||||
opts := c.Opts
|
||||
opts.Notify = bs.send
|
||||
return bs.b.Start(opts)
|
||||
@@ -205,17 +159,27 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
} else if c := cmd.SetWantRunning; c != nil {
|
||||
bs.b.SetWantRunning(*c)
|
||||
return nil
|
||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.RequestStatus; c != nil {
|
||||
bs.b.RequestStatus()
|
||||
return nil
|
||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||
bs.b.FakeExpireAfter(c.Duration)
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
|
||||
func (bs *BackendServer) Reset(ctx context.Context) error {
|
||||
func (bs *BackendServer) Reset() error {
|
||||
// Tell the backend we got a Logout command, which will cause it
|
||||
// to forget all its authentication information.
|
||||
return bs.GotFakeCommand(ctx, &Command{Logout: &NoArgs{}})
|
||||
return bs.GotFakeCommand(&Command{Logout: &NoArgs{}})
|
||||
}
|
||||
|
||||
type BackendClient struct {
|
||||
@@ -240,9 +204,6 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
||||
// not interesting
|
||||
return
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendClient.GotNotifyMsg message: %q", b)
|
||||
}
|
||||
n := Notify{}
|
||||
if err := json.Unmarshal(b, &n); err != nil {
|
||||
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d)\n%#v", len(b), string(b))
|
||||
@@ -269,9 +230,6 @@ func (bc *BackendClient) send(cmd Command) {
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(cmd): %v\n%#v\n", err, cmd)
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendClient.send command: %q", b)
|
||||
}
|
||||
bc.sendCommandMsg(b)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -82,7 +81,7 @@ func TestClientServer(t *testing.T) {
|
||||
serverToClientCh <- append([]byte{}, b...)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
bs.GotCommandMsg(context.TODO(), b)
|
||||
bs.GotCommandMsg(b)
|
||||
}
|
||||
slogf := func(fmt string, args ...interface{}) {
|
||||
t.Logf("s: "+fmt, args...)
|
||||
|
||||
21
ipn/prefs.go
21
ipn/prefs.go
@@ -5,7 +5,6 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -15,7 +14,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -55,7 +54,7 @@ type Prefs struct {
|
||||
// ShieldsUp indicates whether to block all incoming connections,
|
||||
// regardless of the control-provided packet filter. If false, we
|
||||
// use the packet filter as provided. If true, we block incoming
|
||||
// connections. This overrides tailcfg.Hostinfo's ShieldsUp.
|
||||
// connections.
|
||||
ShieldsUp bool
|
||||
|
||||
// AdvertiseTags specifies groups that this node wants to join, for
|
||||
@@ -100,7 +99,7 @@ type Prefs struct {
|
||||
// AdvertiseRoutes specifies CIDR prefixes to advertise into the
|
||||
// Tailscale network as reachable through the current
|
||||
// node.
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
AdvertiseRoutes []wgcfg.CIDR
|
||||
|
||||
// NoSNAT specifies whether to source NAT traffic going to
|
||||
// destinations in AdvertiseRoutes. The default is to apply source
|
||||
@@ -206,12 +205,12 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.Persist.Equals(p2.Persist)
|
||||
}
|
||||
|
||||
func compareIPNets(a, b []netaddr.IPPrefix) bool {
|
||||
func compareIPNets(a, b []wgcfg.CIDR) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
if !a[i].IP.Equal(b[i].IP) || a[i].Mask != b[i].Mask {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -277,14 +276,6 @@ func LoadPrefs(filename string) (*Prefs, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
|
||||
}
|
||||
if bytes.Contains(data, jsonEscapedZero) {
|
||||
// Tailscale 1.2.0 - 1.2.8 on Windows had a memory corruption bug
|
||||
// in the backend process that ended up sending NULL bytes over JSON
|
||||
// to the frontend which wrote them out to JSON files on disk.
|
||||
// So if we see one, treat is as corrupt and the user will need
|
||||
// to log in again. (better than crashing)
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
p, err := PrefsFromBytes(data, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
|
||||
@@ -296,7 +287,7 @@ func SavePrefs(filename string, p *Prefs) {
|
||||
log.Printf("Saving prefs %v %v\n", filename, p.Pretty())
|
||||
data := p.ToBytes()
|
||||
os.MkdirAll(filepath.Dir(filename), 0700)
|
||||
if err := atomicfile.WriteFile(filename, data, 0600); err != nil {
|
||||
if err := atomicfile.WriteFile(filename, data, 0666); err != nil {
|
||||
log.Printf("SavePrefs: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -44,7 +44,7 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
DeviceModel string
|
||||
NotepadURLs bool
|
||||
ForceDaemon bool
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
AdvertiseRoutes []wgcfg.CIDR
|
||||
NoSNAT bool
|
||||
NetfilterMode router.NetfilterMode
|
||||
Persist *controlclient.Persist
|
||||
|
||||
@@ -7,16 +7,14 @@ package ipn
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
@@ -36,9 +34,9 @@ func TestPrefsEqual(t *testing.T) {
|
||||
have, prefsHandles)
|
||||
}
|
||||
|
||||
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
|
||||
nets := func(strs ...string) (ns []wgcfg.CIDR) {
|
||||
for _, s := range strs {
|
||||
n, err := netaddr.ParseIPPrefix(s)
|
||||
n, err := wgcfg.ParseCIDR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -167,12 +165,12 @@ func TestPrefsEqual(t *testing.T) {
|
||||
|
||||
{
|
||||
&Prefs{AdvertiseRoutes: nil},
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
@@ -348,7 +346,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{
|
||||
PrivateNodeKey: wgkey.Private{1: 1},
|
||||
PrivateNodeKey: wgcfg.PrivateKey{1: 1},
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
@@ -373,25 +371,3 @@ func TestLoadPrefsNotExist(t *testing.T) {
|
||||
}
|
||||
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
||||
}
|
||||
|
||||
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
|
||||
// See issue #954 for details.
|
||||
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "TestLoadPrefsFileWithZeroInIt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := f.Name()
|
||||
if _, err := f.Write(jsonEscapedZero); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
defer os.Remove(path)
|
||||
|
||||
p, err := LoadPrefs(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// expected.
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
||||
}
|
||||
|
||||
11
ipn/store.go
11
ipn/store.go
@@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -24,7 +23,7 @@ var ErrStateNotExist = errors.New("no state with given ID")
|
||||
|
||||
const (
|
||||
// MachineKeyStateKey is the key under which we store the machine key,
|
||||
// in its wgkey.Private.MarshalText representation.
|
||||
// in its wgcfg.PrivateKey.MarshalText representation.
|
||||
MachineKeyStateKey = StateKey("_machinekey")
|
||||
|
||||
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
|
||||
@@ -101,14 +100,6 @@ func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path
|
||||
// NewFileStore returns a new file store that persists to path.
|
||||
func NewFileStore(path string) (*FileStore, error) {
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
|
||||
// Treat an empty file as a missing file.
|
||||
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)
|
||||
if err == nil && len(bs) == 0 {
|
||||
log.Printf("ipn.NewFileStore(%q): file empty; treating it like a missing file [warning]", path)
|
||||
err = os.ErrNotExist
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Write out an initial file, to verify that we can write
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/term"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/racebuild"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -49,7 +48,7 @@ type Config struct {
|
||||
// Policy is a logger and its public ID.
|
||||
type Policy struct {
|
||||
// Logtail is the logger.
|
||||
Logtail *logtail.Logger
|
||||
Logtail logtail.Logger
|
||||
// PublicID is the logger's instance identifier.
|
||||
PublicID logtail.PublicID
|
||||
}
|
||||
@@ -311,7 +310,7 @@ func tryFixLogStateLocation(dir, cmdname string) {
|
||||
// given collection name.
|
||||
func New(collection string) *Policy {
|
||||
var lflags int
|
||||
if term.IsTerminal(2) || runtime.GOOS == "windows" {
|
||||
if terminal.IsTerminal(2) || runtime.GOOS == "windows" {
|
||||
lflags = 0
|
||||
} else {
|
||||
lflags = log.LstdFlags
|
||||
@@ -391,13 +390,13 @@ func New(collection string) *Policy {
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
}
|
||||
lw := logtail.NewLogger(c, log.Printf)
|
||||
lw := logtail.Log(c, log.Printf)
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
log.SetOutput(lw)
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.Long,
|
||||
goVersion(),
|
||||
strings.TrimPrefix(runtime.Version(), "go"),
|
||||
os.Args)
|
||||
log.Printf("LogID: %v", newc.PublicID)
|
||||
if filchErr != nil {
|
||||
@@ -413,15 +412,6 @@ func New(collection string) *Policy {
|
||||
}
|
||||
}
|
||||
|
||||
// SetVerbosityLevel controls the verbosity level that should be
|
||||
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
|
||||
// are increasingly verbose.
|
||||
//
|
||||
// It should not be changed concurrently with log writes.
|
||||
func (p *Policy) SetVerbosityLevel(level int) {
|
||||
p.Logtail.SetVerbosityLevel(level)
|
||||
}
|
||||
|
||||
// Close immediately shuts down the logger.
|
||||
func (p *Policy) Close() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -489,11 +479,3 @@ func newLogtailTransport(host string) *http.Transport {
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
func goVersion() string {
|
||||
v := strings.TrimPrefix(runtime.Version(), "go")
|
||||
if racebuild.On {
|
||||
return v + "-race"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func main() {
|
||||
log.Fatalf("logtail: bad -privateid: %v", err)
|
||||
}
|
||||
|
||||
logger := logtail.NewLogger(logtail.Config{
|
||||
logger := logtail.Log(logtail.Config{
|
||||
Collection: *collection,
|
||||
PrivateID: id,
|
||||
}, log.Printf)
|
||||
|
||||
@@ -131,11 +131,11 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
path1 := filePrefix + ".log1.txt"
|
||||
path2 := filePrefix + ".log2.txt"
|
||||
|
||||
f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0600)
|
||||
f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0600)
|
||||
f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
@@ -25,6 +24,34 @@ import (
|
||||
// Config.BaseURL isn't provided.
|
||||
const DefaultHost = "log.tailscale.io"
|
||||
|
||||
type Logger interface {
|
||||
// Write logs an encoded JSON blob.
|
||||
//
|
||||
// If the []byte passed to Write is not an encoded JSON blob,
|
||||
// then contents is fit into a JSON blob and written.
|
||||
//
|
||||
// This is intended as an interface for the stdlib "log" package.
|
||||
Write([]byte) (int, error)
|
||||
|
||||
// Flush uploads all logs to the server.
|
||||
// It blocks until complete or there is an unrecoverable error.
|
||||
Flush() error
|
||||
|
||||
// Shutdown gracefully shuts down the logger while completing any
|
||||
// remaining uploads.
|
||||
//
|
||||
// It will block, continuing to try and upload unless the passed
|
||||
// context object interrupts it by being done.
|
||||
// If the shutdown is interrupted, an error is returned.
|
||||
Shutdown(context.Context) error
|
||||
|
||||
// Close shuts down this logger object, the background log uploader
|
||||
// process, and any associated goroutines.
|
||||
//
|
||||
// DEPRECATED: use Shutdown
|
||||
Close()
|
||||
}
|
||||
|
||||
type Encoder interface {
|
||||
EncodeAll(src, dst []byte) []byte
|
||||
Close() error
|
||||
@@ -39,16 +66,15 @@ type Config struct {
|
||||
LowMemory bool // if true, logtail minimizes memory use
|
||||
TimeNow func() time.Time // if set, subsitutes uses of time.Now
|
||||
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
|
||||
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
|
||||
Buffer Buffer // temp storage, if nil a MemoryBuffer
|
||||
NewZstdEncoder func() Encoder // if set, used to compress logs for transmission
|
||||
|
||||
// DrainLogs, if non-nil, disables automatic uploading of new logs,
|
||||
// DrainLogs, if non-nil, disables autmatic uploading of new logs,
|
||||
// so that logs are only uploaded when a token is sent to DrainLogs.
|
||||
DrainLogs <-chan struct{}
|
||||
}
|
||||
|
||||
func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
func Log(cfg Config, logf tslogger.Logf) Logger {
|
||||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = "https://" + DefaultHost
|
||||
}
|
||||
@@ -68,9 +94,8 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
}
|
||||
cfg.Buffer = NewMemoryBuffer(pendingSize)
|
||||
}
|
||||
l := &Logger{
|
||||
l := &logger{
|
||||
stderr: cfg.Stderr,
|
||||
stderrLevel: cfg.StderrLevel,
|
||||
httpc: cfg.HTTPC,
|
||||
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
|
||||
lowMem: cfg.LowMemory,
|
||||
@@ -97,11 +122,8 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
return l
|
||||
}
|
||||
|
||||
// Logger writes logs, splitting them as configured between local
|
||||
// logging facilities and uploading to a log server.
|
||||
type Logger struct {
|
||||
type logger struct {
|
||||
stderr io.Writer
|
||||
stderrLevel int
|
||||
httpc *http.Client
|
||||
url string
|
||||
lowMem bool
|
||||
@@ -119,22 +141,7 @@ type Logger struct {
|
||||
shutdownDone chan struct{} // closd when shutdown complete
|
||||
}
|
||||
|
||||
// SetVerbosityLevel controls the verbosity level that should be
|
||||
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
|
||||
// are increasingly verbose.
|
||||
//
|
||||
// It should not be changed concurrently with log writes.
|
||||
func (l *Logger) SetVerbosityLevel(level int) {
|
||||
l.stderrLevel = level
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the logger while completing any
|
||||
// remaining uploads.
|
||||
//
|
||||
// It will block, continuing to try and upload unless the passed
|
||||
// context object interrupts it by being done.
|
||||
// If the shutdown is interrupted, an error is returned.
|
||||
func (l *Logger) Shutdown(ctx context.Context) error {
|
||||
func (l *logger) Shutdown(ctx context.Context) error {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
@@ -156,11 +163,7 @@ func (l *Logger) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close shuts down this logger object, the background log uploader
|
||||
// process, and any associated goroutines.
|
||||
//
|
||||
// Deprecated: use Shutdown
|
||||
func (l *Logger) Close() {
|
||||
func (l *logger) Close() {
|
||||
l.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
@@ -171,7 +174,7 @@ func (l *Logger) Close() {
|
||||
//
|
||||
// If the caller provides a DrainLogs channel, then unblock-drain-on-Write
|
||||
// is disabled, and it is up to the caller to trigger unblock the drain.
|
||||
func (l *Logger) drainBlock() (shuttingDown bool) {
|
||||
func (l *logger) drainBlock() (shuttingDown bool) {
|
||||
if l.drainLogs == nil {
|
||||
select {
|
||||
case <-l.shutdownStart:
|
||||
@@ -190,7 +193,7 @@ func (l *Logger) drainBlock() (shuttingDown bool) {
|
||||
|
||||
// drainPending drains and encodes a batch of logs from the buffer for upload.
|
||||
// If no logs are available, drainPending blocks until logs are available.
|
||||
func (l *Logger) drainPending() (res []byte) {
|
||||
func (l *logger) drainPending() (res []byte) {
|
||||
buf := new(bytes.Buffer)
|
||||
entries := 0
|
||||
|
||||
@@ -251,22 +254,13 @@ func (l *Logger) drainPending() (res []byte) {
|
||||
}
|
||||
|
||||
// This is the goroutine that repeatedly uploads logs in the background.
|
||||
func (l *Logger) uploading(ctx context.Context) {
|
||||
func (l *logger) uploading(ctx context.Context) {
|
||||
defer close(l.shutdownDone)
|
||||
|
||||
for {
|
||||
body := l.drainPending()
|
||||
origlen := -1 // sentinel value: uncompressed
|
||||
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
|
||||
if l.zstdEncoder != nil && len(body) > 256 {
|
||||
zbody := l.zstdEncoder.EncodeAll(body, nil)
|
||||
// Only send it compressed if the bandwidth savings are sufficient.
|
||||
// Just the extra headers associated with enabling compression
|
||||
// are 50 bytes by themselves.
|
||||
if len(body)-len(zbody) > 64 {
|
||||
origlen = len(body)
|
||||
body = zbody
|
||||
}
|
||||
if l.zstdEncoder != nil {
|
||||
body = l.zstdEncoder.EncodeAll(body, nil)
|
||||
}
|
||||
|
||||
for len(body) > 0 {
|
||||
@@ -275,7 +269,7 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
uploaded, err := l.upload(ctx, body, origlen)
|
||||
uploaded, err := l.upload(ctx, body)
|
||||
if err != nil {
|
||||
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
|
||||
}
|
||||
@@ -293,10 +287,7 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// upload uploads body to the log server.
|
||||
// origlen indicates the pre-compression body length.
|
||||
// origlen of -1 indicates that the body is not compressed.
|
||||
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded bool, err error) {
|
||||
func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err error) {
|
||||
req, err := http.NewRequest("POST", l.url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
// I know of no conditions under which this could fail.
|
||||
@@ -304,9 +295,8 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
|
||||
// TODO record logs to disk
|
||||
panic("logtail: cannot build http request: " + err.Error())
|
||||
}
|
||||
if origlen != -1 {
|
||||
if l.zstdEncoder != nil {
|
||||
req.Header.Add("Content-Encoding", "zstd")
|
||||
req.Header.Add("Orig-Content-Length", strconv.Itoa(origlen))
|
||||
}
|
||||
req.Header["User-Agent"] = nil // not worth writing one; save some bytes
|
||||
|
||||
@@ -316,7 +306,7 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
compressedNote := "not-compressed"
|
||||
if origlen != -1 {
|
||||
if l.zstdEncoder != nil {
|
||||
compressedNote = "compressed"
|
||||
}
|
||||
|
||||
@@ -343,13 +333,11 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Flush uploads all logs to the server.
|
||||
// It blocks until complete or there is an unrecoverable error.
|
||||
func (l *Logger) Flush() error {
|
||||
func (l *logger) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Logger) send(jsonBlob []byte) (int, error) {
|
||||
func (l *logger) send(jsonBlob []byte) (int, error) {
|
||||
n, err := l.buffer.Write(jsonBlob)
|
||||
if l.drainLogs == nil {
|
||||
select {
|
||||
@@ -362,7 +350,7 @@ func (l *Logger) send(jsonBlob []byte) (int, error) {
|
||||
|
||||
// TODO: instead of allocating, this should probably just append
|
||||
// directly into the output log buffer.
|
||||
func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
func (l *logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
now := l.timeNow()
|
||||
|
||||
// Factor in JSON encoding overhead to try to only do one alloc
|
||||
@@ -418,7 +406,7 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (l *Logger) encode(buf []byte) []byte {
|
||||
func (l *logger) encode(buf []byte) []byte {
|
||||
if buf[0] != '{' {
|
||||
return l.encodeText(buf, l.skipClientTime) // text fast-path
|
||||
}
|
||||
@@ -459,18 +447,11 @@ func (l *Logger) encode(buf []byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
// Write logs an encoded JSON blob.
|
||||
//
|
||||
// If the []byte passed to Write is not an encoded JSON blob,
|
||||
// then contents is fit into a JSON blob and written.
|
||||
//
|
||||
// This is intended as an interface for the stdlib "log" package.
|
||||
func (l *Logger) Write(buf []byte) (int, error) {
|
||||
func (l *logger) Write(buf []byte) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
level, buf := parseAndRemoveLogLevel(buf)
|
||||
if l.stderr != nil && l.stderr != ioutil.Discard && level <= l.stderrLevel {
|
||||
if l.stderr != nil && l.stderr != ioutil.Discard {
|
||||
if buf[len(buf)-1] == '\n' {
|
||||
l.stderr.Write(buf)
|
||||
} else {
|
||||
@@ -484,23 +465,3 @@ func (l *Logger) Write(buf []byte) (int, error) {
|
||||
_, err := l.send(b)
|
||||
return len(buf), err
|
||||
}
|
||||
|
||||
var (
|
||||
openBracketV = []byte("[v")
|
||||
v1 = []byte("[v1] ")
|
||||
v2 = []byte("[v2] ")
|
||||
)
|
||||
|
||||
// level 0 is normal (or unknown) level; 1+ are increasingly verbose
|
||||
func parseAndRemoveLogLevel(buf []byte) (level int, cleanBuf []byte) {
|
||||
if len(buf) == 0 || buf[0] == '{' || !bytes.Contains(buf, openBracketV) {
|
||||
return 0, buf
|
||||
}
|
||||
if bytes.Contains(buf, v1) {
|
||||
return 1, bytes.ReplaceAll(buf, v1, nil)
|
||||
}
|
||||
if bytes.Contains(buf, v2) {
|
||||
return 2, bytes.ReplaceAll(buf, v2, nil)
|
||||
}
|
||||
return 0, buf
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@ package logtail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -20,215 +14,16 @@ func TestFastShutdown(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
testServ := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer testServ.Close()
|
||||
|
||||
l := NewLogger(Config{
|
||||
BaseURL: testServ.URL,
|
||||
l := Log(Config{
|
||||
BaseURL: "http://localhost:1234",
|
||||
}, t.Logf)
|
||||
err := l.Shutdown(ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// maximum number of times a test will call l.Write()
|
||||
const logLines = 3
|
||||
|
||||
type LogtailTestServer struct {
|
||||
srv *httptest.Server // Log server
|
||||
uploaded chan []byte
|
||||
}
|
||||
|
||||
func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
|
||||
ts := LogtailTestServer{}
|
||||
|
||||
// max channel backlog = 1 "started" + #logLines x "log line" + 1 "closed"
|
||||
ts.uploaded = make(chan []byte, 2+logLines)
|
||||
|
||||
ts.srv = httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Error("failed to read HTTP request")
|
||||
}
|
||||
ts.uploaded <- body
|
||||
}))
|
||||
|
||||
t.Cleanup(ts.srv.Close)
|
||||
|
||||
l := NewLogger(Config{BaseURL: ts.srv.URL}, t.Logf)
|
||||
|
||||
// There is always an initial "logtail started" message
|
||||
body := <-ts.uploaded
|
||||
if !strings.Contains(string(body), "started") {
|
||||
t.Errorf("unknown start logging statement: %q", string(body))
|
||||
}
|
||||
|
||||
return &ts, l
|
||||
}
|
||||
|
||||
func TestDrainPendingMessages(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(t)
|
||||
|
||||
for i := 0; i < logLines; i++ {
|
||||
l.Write([]byte("log line"))
|
||||
}
|
||||
|
||||
// all of the "log line" messages usually arrive at once, but poll if needed.
|
||||
body := ""
|
||||
for i := 0; i <= logLines; i++ {
|
||||
body += string(<-ts.uploaded)
|
||||
count := strings.Count(body, "log line")
|
||||
if count == logLines {
|
||||
break
|
||||
}
|
||||
// if we never find count == logLines, the test will eventually time out.
|
||||
}
|
||||
|
||||
err := l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeAndUploadMessages(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
log string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"plain text",
|
||||
"log line",
|
||||
"log line",
|
||||
},
|
||||
{
|
||||
"simple JSON",
|
||||
`{"text": "log line"}`,
|
||||
"log line",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
io.WriteString(l, tt.log)
|
||||
body := <-ts.uploaded
|
||||
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
got := data["text"]
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
|
||||
}
|
||||
|
||||
ltail, ok := data["logtail"]
|
||||
if ok {
|
||||
logtailmap := ltail.(map[string]interface{})
|
||||
_, ok = logtailmap["client_time"]
|
||||
if !ok {
|
||||
t.Errorf("%s: no client_time present", tt.name)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%s: no logtail map present", tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
err := l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeSpecialCases(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(t)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// JSON log message already contains a logtail field.
|
||||
io.WriteString(l, `{"logtail": "LOGTAIL", "text": "text"}`)
|
||||
body := <-ts.uploaded
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
errorHasLogtail, ok := data["error_has_logtail"]
|
||||
if ok {
|
||||
if errorHasLogtail != "LOGTAIL" {
|
||||
t.Errorf("error_has_logtail: got:%q; want:%q",
|
||||
errorHasLogtail, "LOGTAIL")
|
||||
}
|
||||
} else {
|
||||
t.Errorf("no error_has_logtail field: %v", data)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// special characters
|
||||
io.WriteString(l, "\b\f\n\r\t"+`"\`)
|
||||
bodytext := string(<-ts.uploaded)
|
||||
// json.Unmarshal would unescape the characters, we have to look at the encoded text
|
||||
escaped := strings.Contains(bodytext, `\b\f\n\r\t\"\`)
|
||||
if !escaped {
|
||||
t.Errorf("special characters got %s", bodytext)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// skipClientTime to omit the logtail metadata
|
||||
l.skipClientTime = true
|
||||
io.WriteString(l, "text")
|
||||
body = <-ts.uploaded
|
||||
data = make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, ok = data["logtail"]
|
||||
if ok {
|
||||
t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// lowMem + long string
|
||||
l.skipClientTime = false
|
||||
l.lowMem = true
|
||||
longStr := strings.Repeat("0", 512)
|
||||
io.WriteString(l, longStr)
|
||||
body = <-ts.uploaded
|
||||
data = make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
text, ok := data["text"]
|
||||
if !ok {
|
||||
t.Errorf("lowMem: no text %v", data)
|
||||
}
|
||||
if n := len(text.(string)); n > 300 {
|
||||
t.Errorf("lowMem: got %d chars; want <300 chars", n)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
err = l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
l.Shutdown(ctx)
|
||||
}
|
||||
|
||||
var sink []byte
|
||||
|
||||
func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
lg := &Logger{timeNow: time.Now}
|
||||
lg := &logger{timeNow: time.Now}
|
||||
inBuf := []byte("some text to encode")
|
||||
n := testing.AllocsPerRun(1000, func() {
|
||||
sink = lg.encodeText(inBuf, false)
|
||||
@@ -239,7 +34,7 @@ func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoggerWriteLength(t *testing.T) {
|
||||
lg := &Logger{
|
||||
lg := &logger{
|
||||
timeNow: time.Now,
|
||||
buffer: NewMemoryBuffer(1024),
|
||||
}
|
||||
@@ -252,54 +47,3 @@ func TestLoggerWriteLength(t *testing.T) {
|
||||
t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndRemoveLogLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
log string
|
||||
wantLevel int
|
||||
wantLog string
|
||||
}{
|
||||
{
|
||||
"no level",
|
||||
0,
|
||||
"no level",
|
||||
},
|
||||
{
|
||||
"[v1] level 1",
|
||||
1,
|
||||
"level 1",
|
||||
},
|
||||
{
|
||||
"level 1 [v1] ",
|
||||
1,
|
||||
"level 1 ",
|
||||
},
|
||||
{
|
||||
"[v2] level 2",
|
||||
2,
|
||||
"level 2",
|
||||
},
|
||||
{
|
||||
"level [v2] 2",
|
||||
2,
|
||||
"level 2",
|
||||
},
|
||||
{
|
||||
"[v3] no level 3",
|
||||
0,
|
||||
"[v3] no level 3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
gotLevel, gotLog := parseAndRemoveLogLevel([]byte(tt.log))
|
||||
if gotLevel != tt.wantLevel {
|
||||
t.Errorf("parseAndRemoveLogLevel(%q): got:%d; want %d",
|
||||
tt.log, gotLevel, tt.wantLevel)
|
||||
}
|
||||
if string(gotLog) != tt.wantLog {
|
||||
t.Errorf("parseAndRemoveLogLevel(%q): got:%q; want %q",
|
||||
tt.log, gotLog, tt.wantLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,8 @@ package dnscache
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -28,7 +25,7 @@ func preferGoResolver() bool {
|
||||
// There does not appear to be a local resolver running
|
||||
// on iOS, and NetworkExtension is good at isolating DNS.
|
||||
// So do not use the Go resolver on macOS/iOS.
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -45,6 +42,8 @@ func preferGoResolver() bool {
|
||||
// Get returns a caching Resolver singleton.
|
||||
func Get() *Resolver { return single }
|
||||
|
||||
const fixedTTL = 10 * time.Minute
|
||||
|
||||
// Resolver is a minimal DNS caching resolver.
|
||||
//
|
||||
// The TTL is always fixed for now. It's not intended for general use.
|
||||
@@ -55,15 +54,6 @@ type Resolver struct {
|
||||
// If nil, net.DefaultResolver is used.
|
||||
Forward *net.Resolver
|
||||
|
||||
// TTL is how long to keep entries cached
|
||||
//
|
||||
// If zero, a default (currently 10 minutes) is used.
|
||||
TTL time.Duration
|
||||
|
||||
// UseLastGood controls whether a cached entry older than TTL is used
|
||||
// if a refresh fails.
|
||||
UseLastGood bool
|
||||
|
||||
sf singleflight.Group
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -71,8 +61,7 @@ type Resolver struct {
|
||||
}
|
||||
|
||||
type ipCacheEntry struct {
|
||||
ip net.IP // either v4 or v6
|
||||
ip6 net.IP // nil if no v4 or no v6
|
||||
ip net.IP
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
@@ -83,160 +72,74 @@ func (r *Resolver) fwd() *net.Resolver {
|
||||
return net.DefaultResolver
|
||||
}
|
||||
|
||||
func (r *Resolver) ttl() time.Duration {
|
||||
if r.TTL > 0 {
|
||||
return r.TTL
|
||||
}
|
||||
return 10 * time.Minute
|
||||
}
|
||||
|
||||
var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DNS_CACHE"))
|
||||
|
||||
// LookupIP returns the host's primary IP address (either IPv4 or
|
||||
// IPv6, but preferring IPv4) and optionally its IPv6 address, if
|
||||
// there is both IPv4 and IPv6.
|
||||
//
|
||||
// If err is nil, ip will be non-nil. The v6 address may be nil even
|
||||
// with a nil error.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, err error) {
|
||||
// LookupIP returns the first IPv4 address found, otherwise the first IPv6 address.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4, nil, nil
|
||||
return ip4, nil
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: %q is an IP", host)
|
||||
}
|
||||
return ip, nil, nil
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
if ip, ip6, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q = %v (cached)", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
if ip, ok := r.lookupIPCache(host); ok {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
type ipPair struct {
|
||||
ip, ip6 net.IP
|
||||
}
|
||||
ch := r.sf.DoChan(host, func() (interface{}, error) {
|
||||
ip, ip6, err := r.lookupIP(host)
|
||||
ip, err := r.lookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ipPair{ip, ip6}, nil
|
||||
return ip, nil
|
||||
})
|
||||
select {
|
||||
case res := <-ch:
|
||||
if res.Err != nil {
|
||||
if r.UseLastGood {
|
||||
if ip, ip6, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q using %v after error", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
|
||||
}
|
||||
return nil, nil, res.Err
|
||||
return nil, res.Err
|
||||
}
|
||||
pair := res.Val.(ipPair)
|
||||
return pair.ip, pair.ip6, nil
|
||||
return res.Val.(net.IP), nil
|
||||
case <-ctx.Done():
|
||||
if debug {
|
||||
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
|
||||
}
|
||||
return nil, nil, ctx.Err()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCache(host string) (ip, ip6 net.IP, ok bool) {
|
||||
func (r *Resolver) lookupIPCache(host string) (ip net.IP, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok && ent.expires.After(time.Now()) {
|
||||
return ent.ip, ent.ip6, true
|
||||
return ent.ip, true
|
||||
}
|
||||
return nil, nil, false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip, ip6 net.IP, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok {
|
||||
return ent.ip, ent.ip6, true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
if r.UseLastGood {
|
||||
if _, _, ok := r.lookupIPCacheExpired(host); ok {
|
||||
// If we have some previous good value for this host,
|
||||
// don't give this DNS lookup much time. If we're in a
|
||||
// situation where the user's DNS server is unreachable
|
||||
// (e.g. their corp DNS server is behind a subnet router
|
||||
// that can't come up due to Tailscale needing to
|
||||
// connect to itself), then we want to fail fast and let
|
||||
// our caller (who set UseLastGood) fall back to using
|
||||
// the last-known-good IP address.
|
||||
return 3 * time.Second
|
||||
}
|
||||
}
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
if ip, ip6, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q found in cache as %v", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
func (r *Resolver) lookupIP(host string) (net.IP, error) {
|
||||
if ip, ok := r.lookupIPCache(host); ok {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
ips, err := r.fwd().LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, nil, fmt.Errorf("no IPs for %q found", host)
|
||||
return nil, fmt.Errorf("no IPs for %q found", host)
|
||||
}
|
||||
|
||||
have4 := false
|
||||
for _, ipa := range ips {
|
||||
if ip4 := ipa.IP.To4(); ip4 != nil {
|
||||
if !have4 {
|
||||
ip6 = ip
|
||||
ip = ip4
|
||||
have4 = true
|
||||
}
|
||||
} else {
|
||||
if have4 {
|
||||
ip6 = ipa.IP
|
||||
} else {
|
||||
ip = ipa.IP
|
||||
}
|
||||
return r.addIPCache(host, ip4, fixedTTL), nil
|
||||
}
|
||||
}
|
||||
r.addIPCache(host, ip, ip6, r.ttl())
|
||||
return ip, ip6, nil
|
||||
return r.addIPCache(host, ips[0].IP, fixedTTL), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, d time.Duration) {
|
||||
func (r *Resolver) addIPCache(host string, ip net.IP, d time.Duration) net.IP {
|
||||
if isPrivateIP(ip) {
|
||||
// Don't cache obviously wrong entries from captive portals.
|
||||
// TODO: use DoH or DoT for the forwarding resolver?
|
||||
if debug {
|
||||
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("dnscache: %q resolved to IP %v; caching", host, ip)
|
||||
return ip
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
@@ -244,7 +147,8 @@ func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, d time.Duration) {
|
||||
if r.ipCache == nil {
|
||||
r.ipCache = make(map[string]ipCacheEntry)
|
||||
}
|
||||
r.ipCache[host] = ipCacheEntry{ip: ip, ip6: ip6, expires: time.Now().Add(d)}
|
||||
r.ipCache[host] = ipCacheEntry{ip: ip, expires: time.Now().Add(d)}
|
||||
return ip
|
||||
}
|
||||
|
||||
func mustCIDR(s string) *net.IPNet {
|
||||
@@ -264,39 +168,3 @@ var (
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
)
|
||||
|
||||
type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
|
||||
func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// Bogus. But just let the real dialer return an error rather than
|
||||
// inventing a similar one.
|
||||
return fwd(ctx, network, address)
|
||||
}
|
||||
ip, ip6, err := dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
}
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
|
||||
}
|
||||
c, err := fwd(ctx, network, dst)
|
||||
if err == nil || ctx.Err() != nil || ip6 == nil {
|
||||
return c, err
|
||||
}
|
||||
// Fall back to trying IPv6.
|
||||
// TODO(bradfitz): this is a primarily for IPv6-only
|
||||
// hosts; it's not supposed to be a real Happy
|
||||
// Eyeballs implementation. We should use the net
|
||||
// package's implementation of that by plumbing this
|
||||
// dnscache impl into net.Dialer.Resolver.Dial and
|
||||
// unmarshal/marshal DNS queries/responses to the net
|
||||
// package. This works for v6-only hosts for now.
|
||||
dst = net.JoinHostPort(ip6.String(), port)
|
||||
return fwd(ctx, network, dst)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +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.
|
||||
//
|
||||
// Original implementation (from same author) from which this was derived was:
|
||||
// https://github.com/golang/groupcache/blob/5b532d6fd5efaf7fa130d4e859a2fde0fc3a9e1b/lru/lru.go
|
||||
// ... which was Apache licensed:
|
||||
// https://github.com/golang/groupcache/blob/master/LICENSE
|
||||
|
||||
// Package flowtrack contains types for tracking TCP/UDP flows by 4-tuples.
|
||||
package flowtrack
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Tuple is a 4-tuple of source and destination IP and port.
|
||||
type Tuple struct {
|
||||
Src netaddr.IPPort
|
||||
Dst netaddr.IPPort
|
||||
}
|
||||
|
||||
func (t Tuple) String() string {
|
||||
return fmt.Sprintf("(%v => %v)", t.Src, t.Dst)
|
||||
}
|
||||
|
||||
// Cache is an LRU cache keyed by Tuple.
|
||||
//
|
||||
// The zero value is valid to use.
|
||||
//
|
||||
// It is not safe for concurrent access.
|
||||
type Cache struct {
|
||||
// MaxEntries is the maximum number of cache entries before
|
||||
// an item is evicted. Zero means no limit.
|
||||
MaxEntries int
|
||||
|
||||
ll *list.List
|
||||
m map[Tuple]*list.Element // of *entry
|
||||
}
|
||||
|
||||
// entry is the container/list element type.
|
||||
type entry struct {
|
||||
key Tuple
|
||||
value interface{}
|
||||
}
|
||||
|
||||
// Add adds a value to the cache, set or updating its assoicated
|
||||
// value.
|
||||
//
|
||||
// If MaxEntries is non-zero and the length of the cache is greater
|
||||
// after any addition, the least recently used value is evicted.
|
||||
func (c *Cache) Add(key Tuple, value interface{}) {
|
||||
if c.m == nil {
|
||||
c.m = make(map[Tuple]*list.Element)
|
||||
c.ll = list.New()
|
||||
}
|
||||
if ee, ok := c.m[key]; ok {
|
||||
c.ll.MoveToFront(ee)
|
||||
ee.Value.(*entry).value = value
|
||||
return
|
||||
}
|
||||
ele := c.ll.PushFront(&entry{key, value})
|
||||
c.m[key] = ele
|
||||
if c.MaxEntries != 0 && c.Len() > c.MaxEntries {
|
||||
c.RemoveOldest()
|
||||
}
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache, also reporting
|
||||
// whether it was present.
|
||||
func (c *Cache) Get(key Tuple) (value interface{}, ok bool) {
|
||||
if ele, hit := c.m[key]; hit {
|
||||
c.ll.MoveToFront(ele)
|
||||
return ele.Value.(*entry).value, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache if it was present.
|
||||
func (c *Cache) Remove(key Tuple) {
|
||||
if ele, hit := c.m[key]; hit {
|
||||
c.removeElement(ele)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache, if any.
|
||||
func (c *Cache) RemoveOldest() {
|
||||
if c.ll != nil {
|
||||
if ele := c.ll.Back(); ele != nil {
|
||||
c.removeElement(ele)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) removeElement(e *list.Element) {
|
||||
c.ll.Remove(e)
|
||||
delete(c.m, e.Value.(*entry).key)
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache) Len() int { return len(c.m) }
|
||||
@@ -1,82 +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.
|
||||
|
||||
package flowtrack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
c := &Cache{MaxEntries: 2}
|
||||
|
||||
k1 := Tuple{Src: netaddr.MustParseIPPort("1.1.1.1:1"), Dst: netaddr.MustParseIPPort("1.1.1.1:1")}
|
||||
k2 := Tuple{Src: netaddr.MustParseIPPort("1.1.1.1:1"), Dst: netaddr.MustParseIPPort("2.2.2.2:2")}
|
||||
k3 := Tuple{Src: netaddr.MustParseIPPort("1.1.1.1:1"), Dst: netaddr.MustParseIPPort("3.3.3.3:3")}
|
||||
k4 := Tuple{Src: netaddr.MustParseIPPort("1.1.1.1:1"), Dst: netaddr.MustParseIPPort("4.4.4.4:4")}
|
||||
|
||||
wantLen := func(want int) {
|
||||
t.Helper()
|
||||
if got := c.Len(); got != want {
|
||||
t.Fatalf("Len = %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
wantVal := func(key Tuple, want interface{}) {
|
||||
t.Helper()
|
||||
got, ok := c.Get(key)
|
||||
if !ok {
|
||||
t.Fatalf("Get(%q) failed; want value %v", key, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("Get(%q) = %v; want %v", key, got, want)
|
||||
}
|
||||
}
|
||||
wantMissing := func(key Tuple) {
|
||||
t.Helper()
|
||||
if got, ok := c.Get(key); ok {
|
||||
t.Fatalf("Get(%q) = %v; want absent from cache", key, got)
|
||||
}
|
||||
}
|
||||
|
||||
wantLen(0)
|
||||
c.RemoveOldest() // shouldn't panic
|
||||
c.Remove(k4) // shouldn't panic
|
||||
|
||||
c.Add(k1, 1)
|
||||
wantLen(1)
|
||||
c.Add(k2, 2)
|
||||
wantLen(2)
|
||||
c.Add(k3, 3)
|
||||
wantLen(2) // hit the max
|
||||
|
||||
wantMissing(k1)
|
||||
c.Remove(k1)
|
||||
wantLen(2) // no change; k1 should've been the deleted one per LRU
|
||||
|
||||
wantVal(k3, 3)
|
||||
|
||||
wantVal(k2, 2)
|
||||
c.Remove(k2)
|
||||
wantLen(1)
|
||||
wantMissing(k2)
|
||||
|
||||
c.Add(k3, 30)
|
||||
wantVal(k3, 30)
|
||||
wantLen(1)
|
||||
|
||||
allocs := int(testing.AllocsPerRun(1000, func() {
|
||||
got, ok := c.Get(k3)
|
||||
if !ok {
|
||||
t.Fatal("missing k3")
|
||||
}
|
||||
if got != 30 {
|
||||
t.Fatalf("got = %d; want 30", got)
|
||||
}
|
||||
}))
|
||||
if allocs != 0 {
|
||||
t.Errorf("allocs = %v; want 0", allocs)
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,11 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ip.Is6() {
|
||||
// TODO(crawshaw): IPv6 support.
|
||||
// Easy to do here, but we need good endpoint ordering logic.
|
||||
continue
|
||||
}
|
||||
// TODO(apenwarr): don't special case cgNAT.
|
||||
// In the general wireguard case, it might
|
||||
// very well be something we can route to
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"go4.org/mem"
|
||||
@@ -63,12 +62,8 @@ func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
|
||||
ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm)))
|
||||
if err == nil && isPrivateIP(ip) {
|
||||
ret = ip
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
var errStopReadingNetstatTable = errors.New("found private gateway")
|
||||
|
||||
@@ -7,7 +7,6 @@ package interfaces
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
@@ -177,12 +176,7 @@ func getPACWindows() string {
|
||||
return ""
|
||||
}
|
||||
defer globalFree.Call(uintptr(unsafe.Pointer(res)))
|
||||
s := windows.UTF16PtrToString(res)
|
||||
if _, err := url.Parse(s); err != nil {
|
||||
log.Printf("getPACWindows: invalid URL %q from winhttp; ignoring", s)
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
return windows.UTF16PtrToString(res)
|
||||
}
|
||||
const (
|
||||
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/stun"
|
||||
@@ -133,6 +134,10 @@ func cloneDurationMap(m map[int]time.Duration) map[int]time.Duration {
|
||||
|
||||
// Client generates a netcheck Report.
|
||||
type Client struct {
|
||||
// DNSCache optionally specifies a DNSCache to use.
|
||||
// If nil, a DNS cache is not used.
|
||||
DNSCache *dnscache.Resolver
|
||||
|
||||
// Verbose enables verbose logging.
|
||||
Verbose bool
|
||||
|
||||
@@ -692,7 +697,7 @@ func (rs *reportState) probePortMapServices() {
|
||||
port1900 := netaddr.IPPort{IP: gw, Port: 1900}.UDPAddr()
|
||||
port5351 := netaddr.IPPort{IP: gw, Port: 5351}.UDPAddr()
|
||||
|
||||
rs.c.logf("[v1] probePortMapServices: me %v -> gw %v", myIP, gw)
|
||||
rs.c.logf("probePortMapServices: me %v -> gw %v", myIP, gw)
|
||||
|
||||
// Create a UDP4 socket used just for querying for UPnP, NAT-PMP, and PCP.
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
@@ -710,7 +715,6 @@ func (rs *reportState) probePortMapServices() {
|
||||
uc.WriteTo(pcpPacket(myIP, tempPort, false), port5351)
|
||||
|
||||
res := make([]byte, 1500)
|
||||
sentPCPDelete := false
|
||||
for {
|
||||
n, addr, err := uc.ReadFrom(res)
|
||||
if err != nil {
|
||||
@@ -728,14 +732,11 @@ func (rs *reportState) probePortMapServices() {
|
||||
if n == 60 && res[0] == 0x02 { // right length and version 2
|
||||
rs.setOptBool(&rs.report.PCP, true)
|
||||
|
||||
if !sentPCPDelete {
|
||||
sentPCPDelete = true
|
||||
// And now delete the mapping.
|
||||
// (PCP is the only protocol of the three that requires
|
||||
// we cause a side effect to detect whether it's present,
|
||||
// so we need to redo that side effect now.)
|
||||
uc.WriteTo(pcpPacket(myIP, tempPort, true), port5351)
|
||||
}
|
||||
// And now delete the mapping.
|
||||
// (PCP is the only protocol of the three that requires
|
||||
// we cause a side effect to detect whether it's present,
|
||||
// so we need to redo that side effect now.)
|
||||
uc.WriteTo(pcpPacket(myIP, tempPort, true), port5351)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -751,7 +752,6 @@ var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
|
||||
var v4unspec, _ = netaddr.ParseIP("0.0.0.0")
|
||||
|
||||
// pcpPacket generates a PCP packet with a MAP opcode.
|
||||
func pcpPacket(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
|
||||
const udpProtoNumber = 17
|
||||
lifetimeSeconds := uint32(1)
|
||||
@@ -759,24 +759,17 @@ func pcpPacket(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
|
||||
lifetimeSeconds = 0
|
||||
}
|
||||
const opMap = 1
|
||||
|
||||
// 24 byte header + 36 byte map opcode
|
||||
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
|
||||
|
||||
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
|
||||
pkt[0] = 2 // version
|
||||
pkt[1] = opMap
|
||||
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
|
||||
myIP16 := myIP.As16()
|
||||
copy(pkt[8:], myIP16[:])
|
||||
|
||||
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
|
||||
mapOp := pkt[24:]
|
||||
rand.Read(mapOp[:12]) // 96 bit mappping nonce
|
||||
mapOp[12] = udpProtoNumber
|
||||
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
|
||||
rand.Read(pkt[24 : 24+12])
|
||||
pkt[36] = udpProtoNumber
|
||||
binary.BigEndian.PutUint16(pkt[40:], uint16(mapToLocalPort))
|
||||
v4unspec16 := v4unspec.As16()
|
||||
copy(mapOp[20:], v4unspec16[:])
|
||||
copy(pkt[40:], v4unspec16[:])
|
||||
return pkt
|
||||
}
|
||||
|
||||
@@ -842,7 +835,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
|
||||
ifState, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
c.logf("[v1] interfaces: %v", err)
|
||||
c.logf("interfaces: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -951,7 +944,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
go func(reg *tailcfg.DERPRegion) {
|
||||
defer wg.Done()
|
||||
if d, ip, err := c.measureHTTPSLatency(ctx, reg); err != nil {
|
||||
c.logf("[v1] netcheck: measuring HTTPS latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err)
|
||||
c.logf("netcheck: measuring HTTPS latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err)
|
||||
} else {
|
||||
rs.mu.Lock()
|
||||
rs.report.RegionLatency[reg.RegionID] = d
|
||||
@@ -1045,7 +1038,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
|
||||
}
|
||||
|
||||
func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
|
||||
c.logf("[v1] report: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
c.logf("%v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
fmt.Fprintf(w, "udp=%v", r.UDP)
|
||||
if !r.IPv4 {
|
||||
fmt.Fprintf(w, " v4=%v", r.IPv4)
|
||||
@@ -1102,10 +1095,6 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(r *Report) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var prevDERP int
|
||||
if c.last != nil {
|
||||
prevDERP = c.last.PreferredDERP
|
||||
}
|
||||
if c.prev == nil {
|
||||
c.prev = map[time.Time]*Report{}
|
||||
}
|
||||
@@ -1123,9 +1112,9 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(r *Report) {
|
||||
delete(c.prev, t)
|
||||
continue
|
||||
}
|
||||
for regionID, d := range pr.RegionLatency {
|
||||
if bd, ok := bestRecent[regionID]; !ok || d < bd {
|
||||
bestRecent[regionID] = d
|
||||
for hp, d := range pr.RegionLatency {
|
||||
if bd, ok := bestRecent[hp]; !ok || d < bd {
|
||||
bestRecent[hp] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1133,27 +1122,13 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(r *Report) {
|
||||
// Then, pick which currently-alive DERP server from the
|
||||
// current report has the best latency over the past maxAge.
|
||||
var bestAny time.Duration
|
||||
var oldRegionCurLatency time.Duration
|
||||
for regionID, d := range r.RegionLatency {
|
||||
if regionID == prevDERP {
|
||||
oldRegionCurLatency = d
|
||||
}
|
||||
best := bestRecent[regionID]
|
||||
for hp := range r.RegionLatency {
|
||||
best := bestRecent[hp]
|
||||
if r.PreferredDERP == 0 || best < bestAny {
|
||||
bestAny = best
|
||||
r.PreferredDERP = regionID
|
||||
r.PreferredDERP = hp
|
||||
}
|
||||
}
|
||||
|
||||
// If we're changing our preferred DERP but the old one's still
|
||||
// accessible and the new one's not much better, just stick with
|
||||
// where we are.
|
||||
if prevDERP != 0 &&
|
||||
r.PreferredDERP != prevDERP &&
|
||||
oldRegionCurLatency != 0 &&
|
||||
bestAny > oldRegionCurLatency/3*2 {
|
||||
r.PreferredDERP = prevDERP
|
||||
}
|
||||
}
|
||||
|
||||
func updateLatency(m map[int]time.Duration, regionID int, d time.Duration) {
|
||||
|
||||
@@ -195,24 +195,6 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) {
|
||||
wantPrevLen: 1, // t=[0123]s all gone. (too old, older than 10 min)
|
||||
wantDERP: 3, // only option
|
||||
},
|
||||
{
|
||||
name: "preferred_derp_hysteresis_no_switch",
|
||||
steps: []step{
|
||||
{0 * time.Second, report("d1", 4, "d2", 5)},
|
||||
{1 * time.Second, report("d1", 4, "d2", 3)},
|
||||
},
|
||||
wantPrevLen: 2,
|
||||
wantDERP: 1, // 2 didn't get fast enough
|
||||
},
|
||||
{
|
||||
name: "preferred_derp_hysteresis_do_switch",
|
||||
steps: []step{
|
||||
{0 * time.Second, report("d1", 4, "d2", 5)},
|
||||
{1 * time.Second, report("d1", 4, "d2", 1)},
|
||||
},
|
||||
wantPrevLen: 2,
|
||||
wantDERP: 2, // 2 got fast enough
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -572,47 +554,9 @@ func TestLogConciseReport(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Client{Logf: func(f string, a ...interface{}) { fmt.Fprintf(&buf, f, a...) }}
|
||||
c.logConciseReport(tt.r, dm)
|
||||
if got := strings.TrimPrefix(buf.String(), "[v1] report: "); got != tt.want {
|
||||
if got := buf.String(); got != tt.want {
|
||||
t.Errorf("unexpected result.\n got: %#q\nwant: %#q\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortRegions(t *testing.T) {
|
||||
unsortedMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{},
|
||||
}
|
||||
for rid := 1; rid <= 5; rid++ {
|
||||
var nodes []*tailcfg.DERPNode
|
||||
nodes = append(nodes, &tailcfg.DERPNode{
|
||||
Name: fmt.Sprintf("%da", rid),
|
||||
RegionID: rid,
|
||||
HostName: fmt.Sprintf("derp%d-1", rid),
|
||||
IPv4: fmt.Sprintf("%d.0.0.1", rid),
|
||||
IPv6: fmt.Sprintf("%d::1", rid),
|
||||
})
|
||||
unsortedMap.Regions[rid] = &tailcfg.DERPRegion{
|
||||
RegionID: rid,
|
||||
Nodes: nodes,
|
||||
}
|
||||
}
|
||||
report := newReport()
|
||||
report.RegionLatency[1] = time.Second * time.Duration(5)
|
||||
report.RegionLatency[2] = time.Second * time.Duration(3)
|
||||
report.RegionLatency[3] = time.Second * time.Duration(6)
|
||||
report.RegionLatency[4] = time.Second * time.Duration(0)
|
||||
report.RegionLatency[5] = time.Second * time.Duration(2)
|
||||
sortedMap := sortRegions(unsortedMap, report)
|
||||
|
||||
// Sorting by latency this should result in rid: 5, 2, 1, 3
|
||||
// rid 4 with latency 0 should be at the end
|
||||
want := []int{5, 2, 1, 3, 4}
|
||||
got := make([]int, len(sortedMap))
|
||||
for i, r := range sortedMap {
|
||||
got[i] = r.RegionID
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
package netns
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/endian"
|
||||
)
|
||||
|
||||
func interfaceIndex(iface *winipcfg.IPAdapterAddresses) uint32 {
|
||||
@@ -114,8 +114,7 @@ func bindSocket6(c syscall.RawConn, ifidx uint32) error {
|
||||
// representation, suitable for passing to Windows APIs that require a
|
||||
// mangled uint32.
|
||||
func nativeToBigEndian(i uint32) uint32 {
|
||||
if endian.Big {
|
||||
return i
|
||||
}
|
||||
return bits.ReverseBytes32(i)
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], i)
|
||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
package netstat
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/endian"
|
||||
)
|
||||
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable
|
||||
@@ -93,7 +92,7 @@ func (t *Table) addEntries(fam int) error {
|
||||
}
|
||||
buf = buf[:size]
|
||||
|
||||
numEntries := endian.Native.Uint32(buf[:4])
|
||||
numEntries := *(*uint32)(unsafe.Pointer(&buf[0]))
|
||||
buf = buf[4:]
|
||||
|
||||
var recSize int
|
||||
@@ -154,11 +153,9 @@ func state(v uint32) string {
|
||||
}
|
||||
|
||||
func ipport4(addr uint32, port uint16) netaddr.IPPort {
|
||||
if !endian.Big {
|
||||
addr = bits.ReverseBytes32(addr)
|
||||
}
|
||||
a4 := (*[4]byte)(unsafe.Pointer(&addr))
|
||||
return netaddr.IPPort{
|
||||
IP: netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
|
||||
IP: netaddr.IPv4(a4[0], a4[1], a4[2], a4[3]),
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
@@ -176,8 +173,6 @@ func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
|
||||
}
|
||||
|
||||
func port(v *uint32) uint16 {
|
||||
if !endian.Big {
|
||||
return uint16(bits.ReverseBytes32(*v) >> 16)
|
||||
}
|
||||
return uint16(*v >> 16)
|
||||
p := (*[4]byte)(unsafe.Pointer(v))
|
||||
return binary.BigEndian.Uint16(p[:2])
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (p *Pipe) Read(b []byte) (n int, err error) {
|
||||
for {
|
||||
p.mu.Lock()
|
||||
closed := p.closed
|
||||
timedout := !p.readTimeout.IsZero() && !time.Now().Before(p.readTimeout)
|
||||
timedout := !p.readTimeout.IsZero() && time.Now().After(p.readTimeout)
|
||||
blocked := p.blocked
|
||||
if !closed && !timedout && len(p.buf) > 0 {
|
||||
n2 := copy(b, p.buf)
|
||||
@@ -99,7 +99,7 @@ func (p *Pipe) Write(b []byte) (n int, err error) {
|
||||
for {
|
||||
p.mu.Lock()
|
||||
closed := p.closed
|
||||
timedout := !p.writeTimeout.IsZero() && !time.Now().Before(p.writeTimeout)
|
||||
timedout := !p.writeTimeout.IsZero() && time.Now().After(p.writeTimeout)
|
||||
blocked := p.blocked
|
||||
if !closed && !timedout {
|
||||
n2 := len(b)
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
p := NewPipe("p1", 1<<16)
|
||||
p.SetWriteDeadline(time.Now().Add(-1 * time.Second))
|
||||
n, err := p.Write([]byte{'h'})
|
||||
if !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
if err == nil || !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
t.Errorf("missing write timeout got err: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
@@ -49,7 +49,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
p.SetReadDeadline(time.Now().Add(-1 * time.Second))
|
||||
b := make([]byte, 1)
|
||||
n, err := p.Read(b)
|
||||
if !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
if err == nil || !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
t.Errorf("missing read timeout got err: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
@@ -65,7 +65,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
if err := p.Block(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := p.Write([]byte{'h'}); !errors.Is(err, ErrWriteTimeout) {
|
||||
if _, err := p.Write([]byte{'h'}); err == nil || !errors.Is(err, ErrWriteTimeout) {
|
||||
t.Fatalf("want write timeout got: %v", err)
|
||||
}
|
||||
})
|
||||
@@ -80,10 +80,11 @@ func TestPipeTimeout(t *testing.T) {
|
||||
if err := p.Block(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := p.Read(b); !errors.Is(err, ErrReadTimeout) {
|
||||
if _, err := p.Read(b); err == nil || !errors.Is(err, ErrReadTimeout) {
|
||||
t.Fatalf("want read timeout got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestLimit(t *testing.T) {
|
||||
@@ -116,8 +117,4 @@ func TestLimit(t *testing.T) {
|
||||
} else if n != 1 {
|
||||
t.Errorf("Read(%q): n=%d want 1", string(b), n)
|
||||
}
|
||||
|
||||
if err := <-errCh; err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet contains packet parsing and marshaling utilities.
|
||||
//
|
||||
// Parsed provides allocation-free minimal packet header decoding, for
|
||||
// use in packet filtering. The other types in the package are for
|
||||
// constructing and marshaling packets into []bytes.
|
||||
//
|
||||
// To support allocation-free parsing, this package defines IPv4 and
|
||||
// IPv6 address types. You should prefer to use netaddr's types,
|
||||
// except where you absolutely need allocation-free IP handling
|
||||
// (i.e. in the tunnel datapath) and are willing to implement all
|
||||
// codepaths and data structures twice, once per IP family.
|
||||
package packet
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// icmp4HeaderLength is the size of the ICMPv4 packet header, not
|
||||
// including the outer IP layer or the variable "response data"
|
||||
// trailer.
|
||||
const icmp4HeaderLength = 4
|
||||
|
||||
// ICMP4Type is an ICMPv4 type, as specified in
|
||||
// https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
|
||||
type ICMP4Type uint8
|
||||
|
||||
const (
|
||||
ICMP4EchoReply ICMP4Type = 0x00
|
||||
ICMP4EchoRequest ICMP4Type = 0x08
|
||||
ICMP4Unreachable ICMP4Type = 0x03
|
||||
ICMP4TimeExceeded ICMP4Type = 0x0b
|
||||
)
|
||||
|
||||
func (t ICMP4Type) String() string {
|
||||
switch t {
|
||||
case ICMP4EchoReply:
|
||||
return "EchoReply"
|
||||
case ICMP4EchoRequest:
|
||||
return "EchoRequest"
|
||||
case ICMP4Unreachable:
|
||||
return "Unreachable"
|
||||
case ICMP4TimeExceeded:
|
||||
return "TimeExceeded"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ICMP4Code is an ICMPv4 code, as specified in
|
||||
// https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
|
||||
type ICMP4Code uint8
|
||||
|
||||
const (
|
||||
ICMP4NoCode ICMP4Code = 0
|
||||
)
|
||||
|
||||
// ICMP4Header is an IPv4+ICMPv4 header.
|
||||
type ICMP4Header struct {
|
||||
IP4Header
|
||||
Type ICMP4Type
|
||||
Code ICMP4Code
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h ICMP4Header) Len() int {
|
||||
return h.IP4Header.Len() + icmp4HeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h ICMP4Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
// The caller does not need to set this.
|
||||
h.IPProto = ICMPv4
|
||||
|
||||
buf[20] = uint8(h.Type)
|
||||
buf[21] = uint8(h.Code)
|
||||
|
||||
h.IP4Header.Marshal(buf)
|
||||
|
||||
binary.BigEndian.PutUint16(buf[22:24], ip4Checksum(buf))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header. TODO: it doesn't implement it
|
||||
// correctly, instead it statically generates an ICMP Echo Reply
|
||||
// packet.
|
||||
func (h *ICMP4Header) ToResponse() {
|
||||
// TODO: this doesn't implement ToResponse correctly, as it
|
||||
// assumes the ICMP request type.
|
||||
h.Type = ICMP4EchoReply
|
||||
h.Code = ICMP4NoCode
|
||||
h.IP4Header.ToResponse()
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
// icmp6HeaderLength is the size of the ICMPv6 packet header, not
|
||||
// including the outer IP layer or the variable "response data"
|
||||
// trailer.
|
||||
const icmp6HeaderLength = 4
|
||||
|
||||
// ICMP6Type is an ICMPv6 type, as specified in
|
||||
// https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
|
||||
type ICMP6Type uint8
|
||||
|
||||
const (
|
||||
ICMP6Unreachable ICMP6Type = 1
|
||||
ICMP6TimeExceeded ICMP6Type = 3
|
||||
ICMP6EchoRequest ICMP6Type = 128
|
||||
ICMP6EchoReply ICMP6Type = 129
|
||||
)
|
||||
|
||||
func (t ICMP6Type) String() string {
|
||||
switch t {
|
||||
case ICMP6Unreachable:
|
||||
return "Unreachable"
|
||||
case ICMP6TimeExceeded:
|
||||
return "TimeExceeded"
|
||||
case ICMP6EchoRequest:
|
||||
return "EchoRequest"
|
||||
case ICMP6EchoReply:
|
||||
return "EchoReply"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ICMP6Code is an ICMPv6 code, as specified in
|
||||
// https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
|
||||
type ICMP6Code uint8
|
||||
|
||||
const (
|
||||
ICMP6NoCode ICMP6Code = 0
|
||||
)
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
// IPProto is an IP subprotocol as defined by the IANA protocol
|
||||
// numbers list
|
||||
// (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml),
|
||||
// or the special values Unknown or Fragment.
|
||||
type IPProto uint8
|
||||
|
||||
const (
|
||||
// Unknown represents an unknown or unsupported protocol; it's
|
||||
// deliberately the zero value. Strictly speaking the zero
|
||||
// value is IPv6 hop-by-hop extensions, but we don't support
|
||||
// those, so this is still technically correct.
|
||||
Unknown IPProto = 0x00
|
||||
|
||||
// Values from the IANA registry.
|
||||
ICMPv4 IPProto = 0x01
|
||||
IGMP IPProto = 0x02
|
||||
ICMPv6 IPProto = 0x3a
|
||||
TCP IPProto = 0x06
|
||||
UDP IPProto = 0x11
|
||||
|
||||
// TSMP is the Tailscale Message Protocol (our ICMP-ish
|
||||
// thing), an IP protocol used only between Tailscale nodes
|
||||
// (still encrypted by WireGuard) that communicates why things
|
||||
// failed, etc.
|
||||
//
|
||||
// Proto number 99 is reserved for "any private encryption
|
||||
// scheme". We never accept these from the host OS stack nor
|
||||
// send them to the host network stack. It's only used between
|
||||
// nodes.
|
||||
TSMP IPProto = 99
|
||||
|
||||
// Fragment represents any non-first IP fragment, for which we
|
||||
// don't have the sub-protocol header (and therefore can't
|
||||
// figure out what the sub-protocol is).
|
||||
//
|
||||
// 0xFF is reserved in the IANA registry, so we steal it for
|
||||
// internal use.
|
||||
Fragment IPProto = 0xFF
|
||||
)
|
||||
|
||||
func (p IPProto) String() string {
|
||||
switch p {
|
||||
case Fragment:
|
||||
return "Frag"
|
||||
case ICMPv4:
|
||||
return "ICMPv4"
|
||||
case IGMP:
|
||||
return "IGMP"
|
||||
case ICMPv6:
|
||||
return "ICMPv6"
|
||||
case UDP:
|
||||
return "UDP"
|
||||
case TCP:
|
||||
return "TCP"
|
||||
case TSMP:
|
||||
return "TSMP"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// ip4HeaderLength is the length of an IPv4 header with no IP options.
|
||||
const ip4HeaderLength = 20
|
||||
|
||||
// IP4Header represents an IPv4 packet header.
|
||||
type IP4Header struct {
|
||||
IPProto IPProto
|
||||
IPID uint16
|
||||
Src netaddr.IP
|
||||
Dst netaddr.IP
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h IP4Header) Len() int {
|
||||
return ip4HeaderLength
|
||||
}
|
||||
|
||||
var errWrongFamily = errors.New("wrong address family for src/dst IP")
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h IP4Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
if !h.Src.Is4() || !h.Dst.Is4() {
|
||||
return errWrongFamily
|
||||
}
|
||||
|
||||
buf[0] = 0x40 | (byte(h.Len() >> 2)) // IPv4 + IHL
|
||||
buf[1] = 0x00 // DSCP + ECN
|
||||
binary.BigEndian.PutUint16(buf[2:4], uint16(len(buf))) // Total length
|
||||
binary.BigEndian.PutUint16(buf[4:6], h.IPID) // ID
|
||||
binary.BigEndian.PutUint16(buf[6:8], 0) // Flags + fragment offset
|
||||
buf[8] = 64 // TTL
|
||||
buf[9] = uint8(h.IPProto) // Inner protocol
|
||||
// Blank checksum. This is necessary even though we overwrite
|
||||
// it later, because the checksum computation runs over these
|
||||
// bytes and expects them to be zero.
|
||||
binary.BigEndian.PutUint16(buf[10:12], 0)
|
||||
src := h.Src.As4()
|
||||
dst := h.Dst.As4()
|
||||
copy(buf[12:16], src[:])
|
||||
copy(buf[16:20], dst[:])
|
||||
|
||||
binary.BigEndian.PutUint16(buf[10:12], ip4Checksum(buf[0:20])) // Checksum
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *IP4Header) ToResponse() {
|
||||
h.Src, h.Dst = h.Dst, h.Src
|
||||
// Flip the bits in the IPID. If incoming IPIDs are distinct, so are these.
|
||||
h.IPID = ^h.IPID
|
||||
}
|
||||
|
||||
// ip4Checksum computes an IPv4 checksum, as specified in
|
||||
// https://tools.ietf.org/html/rfc1071
|
||||
func ip4Checksum(b []byte) uint16 {
|
||||
var ac uint32
|
||||
i := 0
|
||||
n := len(b)
|
||||
for n >= 2 {
|
||||
ac += uint32(binary.BigEndian.Uint16(b[i : i+2]))
|
||||
n -= 2
|
||||
i += 2
|
||||
}
|
||||
if n == 1 {
|
||||
ac += uint32(b[i]) << 8
|
||||
}
|
||||
for (ac >> 16) > 0 {
|
||||
ac = (ac >> 16) + (ac & 0xffff)
|
||||
}
|
||||
return uint16(^ac)
|
||||
}
|
||||
|
||||
// ip4PseudoHeaderOffset is the number of bytes by which the IPv4 UDP
|
||||
// pseudo-header is smaller than the real IPv4 header.
|
||||
const ip4PseudoHeaderOffset = 8
|
||||
|
||||
// marshalPseudo serializes h into buf in the "pseudo-header" form
|
||||
// required when calculating UDP checksums. The pseudo-header starts
|
||||
// at buf[ip4PseudoHeaderOffset] so as to abut the following UDP
|
||||
// header, while leaving enough space in buf for a full IPv4 header.
|
||||
func (h IP4Header) marshalPseudo(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
|
||||
length := len(buf) - h.Len()
|
||||
src, dst := h.Src.As4(), h.Dst.As4()
|
||||
copy(buf[8:12], src[:])
|
||||
copy(buf[12:16], dst[:])
|
||||
buf[16] = 0x0
|
||||
buf[17] = uint8(h.IPProto)
|
||||
binary.BigEndian.PutUint16(buf[18:20], uint16(length))
|
||||
return nil
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// ip6HeaderLength is the length of an IPv6 header with no IP options.
|
||||
const ip6HeaderLength = 40
|
||||
|
||||
// IP6Header represents an IPv6 packet header.
|
||||
type IP6Header struct {
|
||||
IPProto IPProto
|
||||
IPID uint32 // only lower 20 bits used
|
||||
Src netaddr.IP
|
||||
Dst netaddr.IP
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h IP6Header) Len() int {
|
||||
return ip6HeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h IP6Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(buf[:4], h.IPID&0x000FFFFF)
|
||||
buf[0] = 0x60
|
||||
binary.BigEndian.PutUint16(buf[4:6], uint16(len(buf)-ip6HeaderLength)) // Total length
|
||||
buf[6] = uint8(h.IPProto) // Inner protocol
|
||||
buf[7] = 64 // TTL
|
||||
src, dst := h.Src.As16(), h.Dst.As16()
|
||||
copy(buf[8:24], src[:])
|
||||
copy(buf[24:40], dst[:])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *IP6Header) ToResponse() {
|
||||
h.Src, h.Dst = h.Dst, h.Src
|
||||
// Flip the bits in the IPID. If incoming IPIDs are distinct, so are these.
|
||||
h.IPID = (^h.IPID) & 0x000FFFFF
|
||||
}
|
||||
|
||||
// marshalPseudo serializes h into buf in the "pseudo-header" form
|
||||
// required when calculating UDP checksums.
|
||||
func (h IP6Header) marshalPseudo(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
|
||||
src, dst := h.Src.As16(), h.Dst.As16()
|
||||
copy(buf[:16], src[:])
|
||||
copy(buf[16:32], dst[:])
|
||||
binary.BigEndian.PutUint32(buf[32:36], uint32(len(buf)-h.Len()))
|
||||
buf[36] = 0
|
||||
buf[37] = 0
|
||||
buf[38] = 0
|
||||
buf[39] = 17 // NextProto
|
||||
return nil
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/strbuilder"
|
||||
)
|
||||
|
||||
// RFC1858: prevent overlapping fragment attacks.
|
||||
const minFrag = 60 + 20 // max IPv4 header + basic TCP header
|
||||
|
||||
type TCPFlag uint8
|
||||
|
||||
const (
|
||||
TCPFin TCPFlag = 0x01
|
||||
TCPSyn TCPFlag = 0x02
|
||||
TCPRst TCPFlag = 0x04
|
||||
TCPPsh TCPFlag = 0x08
|
||||
TCPAck TCPFlag = 0x10
|
||||
TCPSynAck TCPFlag = TCPSyn | TCPAck
|
||||
)
|
||||
|
||||
// Parsed is a minimal decoding of a packet suitable for use in filters.
|
||||
type Parsed struct {
|
||||
// b is the byte buffer that this decodes.
|
||||
b []byte
|
||||
// subofs is the offset of IP subprotocol.
|
||||
subofs int
|
||||
// dataofs is the offset of IP subprotocol payload.
|
||||
dataofs int
|
||||
// length is the total length of the packet.
|
||||
// This is not the same as len(b) because b can have trailing zeros.
|
||||
length int
|
||||
|
||||
// IPVersion is the IP protocol version of the packet (4 or
|
||||
// 6), or 0 if the packet doesn't look like IPv4 or IPv6.
|
||||
IPVersion uint8
|
||||
// IPProto is the IP subprotocol (UDP, TCP, etc.). Valid iff IPVersion != 0.
|
||||
IPProto IPProto
|
||||
// SrcIP4 is the source address. Family matches IPVersion. Port is
|
||||
// valid iff IPProto == TCP || IPProto == UDP.
|
||||
Src netaddr.IPPort
|
||||
// DstIP4 is the destination address. Family matches IPVersion.
|
||||
Dst netaddr.IPPort
|
||||
// TCPFlags is the packet's TCP flag bigs. Valid iff IPProto == TCP.
|
||||
TCPFlags TCPFlag
|
||||
}
|
||||
|
||||
func (p *Parsed) String() string {
|
||||
if p.IPVersion != 4 && p.IPVersion != 6 {
|
||||
return "Unknown{???}"
|
||||
}
|
||||
|
||||
sb := strbuilder.Get()
|
||||
sb.WriteString(p.IPProto.String())
|
||||
sb.WriteByte('{')
|
||||
writeIPPort(sb, p.Src)
|
||||
sb.WriteString(" > ")
|
||||
writeIPPort(sb, p.Dst)
|
||||
sb.WriteByte('}')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// writeIPPort writes ipp.String() into sb, with fewer allocations.
|
||||
//
|
||||
// TODO: make netaddr more efficient in this area, and retire this func.
|
||||
func writeIPPort(sb *strbuilder.Builder, ipp netaddr.IPPort) {
|
||||
if ipp.IP.Is4() {
|
||||
raw := ipp.IP.As4()
|
||||
sb.WriteUint(uint64(raw[0]))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(raw[1]))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(raw[2]))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(raw[3]))
|
||||
sb.WriteByte(':')
|
||||
} else {
|
||||
sb.WriteByte('[')
|
||||
sb.WriteString(ipp.IP.String()) // TODO: faster?
|
||||
sb.WriteString("]:")
|
||||
}
|
||||
sb.WriteUint(uint64(ipp.Port))
|
||||
}
|
||||
|
||||
// Decode extracts data from the packet in b into q.
|
||||
// It performs extremely simple packet decoding for basic IPv4 packet types.
|
||||
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
|
||||
// and shouldn't need any memory allocation.
|
||||
func (q *Parsed) Decode(b []byte) {
|
||||
q.b = b
|
||||
|
||||
if len(b) < 1 {
|
||||
q.IPVersion = 0
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
q.IPVersion = b[0] >> 4
|
||||
switch q.IPVersion {
|
||||
case 4:
|
||||
q.decode4(b)
|
||||
case 6:
|
||||
q.decode6(b)
|
||||
default:
|
||||
q.IPVersion = 0
|
||||
q.IPProto = Unknown
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) decode4(b []byte) {
|
||||
if len(b) < ip4HeaderLength {
|
||||
q.IPVersion = 0
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
// Check that it's IPv4.
|
||||
q.IPProto = IPProto(b[9])
|
||||
q.length = int(binary.BigEndian.Uint16(b[2:4]))
|
||||
if len(b) < q.length {
|
||||
// Packet was cut off before full IPv4 length.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
// If it's valid IPv4, then the IP addresses are valid
|
||||
q.Src.IP = netaddr.IPv4(b[12], b[13], b[14], b[15])
|
||||
q.Dst.IP = netaddr.IPv4(b[16], b[17], b[18], b[19])
|
||||
|
||||
q.subofs = int((b[0] & 0x0F) << 2)
|
||||
if q.subofs > q.length {
|
||||
// next-proto starts beyond end of packet.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
sub := b[q.subofs:]
|
||||
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
|
||||
|
||||
// We don't care much about IP fragmentation, except insofar as it's
|
||||
// used for firewall bypass attacks. The trick is make the first
|
||||
// fragment of a TCP or UDP packet so short that it doesn't fit
|
||||
// the TCP or UDP header, so we can't read the port, in hope that
|
||||
// it'll sneak past. Then subsequent fragments fill it in, but we're
|
||||
// missing the first part of the header, so we can't read that either.
|
||||
//
|
||||
// A "perfectly correct" implementation would have to reassemble
|
||||
// fragments before deciding what to do. But the truth is there's
|
||||
// zero reason to send such a short first fragment, so we can treat
|
||||
// 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 & 0x20) != 0
|
||||
fragOfs := fragFlags & 0x1FFF
|
||||
if fragOfs == 0 {
|
||||
// This is the first fragment
|
||||
if moreFrags && len(sub) < minFrag {
|
||||
// Suspiciously short first fragment, dump it.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
// otherwise, this is either non-fragmented (the usual case)
|
||||
// or a big enough initial fragment that we can read the
|
||||
// whole subprotocol header.
|
||||
switch q.IPProto {
|
||||
case ICMPv4:
|
||||
if len(sub) < icmp4HeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = 0
|
||||
q.Dst.Port = 0
|
||||
q.dataofs = q.subofs + icmp4HeaderLength
|
||||
return
|
||||
case IGMP:
|
||||
// Keep IPProto, but don't parse anything else
|
||||
// out.
|
||||
return
|
||||
case TCP:
|
||||
if len(sub) < tcpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
case UDP:
|
||||
if len(sub) < udpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
return
|
||||
case TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// This is a fragment other than the first one.
|
||||
if fragOfs < minFrag {
|
||||
// First frag was suspiciously short, so we can't
|
||||
// trust the followup either.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
// otherwise, we have to permit the fragment to slide through.
|
||||
// Second and later fragments don't have sub-headers.
|
||||
// Ideally, we would drop fragments that we can't identify,
|
||||
// but that would require statefulness. Anyway, receivers'
|
||||
// kernels know to drop fragments where the initial fragment
|
||||
// doesn't arrive.
|
||||
q.IPProto = Fragment
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) decode6(b []byte) {
|
||||
if len(b) < ip6HeaderLength {
|
||||
q.IPVersion = 0
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
q.IPProto = IPProto(b[6])
|
||||
q.length = int(binary.BigEndian.Uint16(b[4:6])) + ip6HeaderLength
|
||||
if len(b) < q.length {
|
||||
// Packet was cut off before the full IPv6 length.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
// okay to ignore `ok` here, because IPs pulled from packets are
|
||||
// always well-formed stdlib IPs.
|
||||
q.Src.IP, _ = netaddr.FromStdIP(net.IP(b[8:24]))
|
||||
q.Dst.IP, _ = netaddr.FromStdIP(net.IP(b[24:40]))
|
||||
|
||||
// We don't support any IPv6 extension headers. Don't try to
|
||||
// be clever. Therefore, the IP subprotocol always starts at
|
||||
// byte 40.
|
||||
//
|
||||
// Note that this means we don't support fragmentation in
|
||||
// IPv6. This is fine, because IPv6 strongly mandates that you
|
||||
// should not fragment, which makes fragmentation on the open
|
||||
// internet extremely uncommon.
|
||||
//
|
||||
// This also means we don't support IPSec headers (AH/ESP), or
|
||||
// IPv6 jumbo frames. Those will get marked Unknown and
|
||||
// dropped.
|
||||
q.subofs = 40
|
||||
sub := b[q.subofs:]
|
||||
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
|
||||
|
||||
switch q.IPProto {
|
||||
case ICMPv6:
|
||||
if len(sub) < icmp6HeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = 0
|
||||
q.Dst.Port = 0
|
||||
q.dataofs = q.subofs + icmp6HeaderLength
|
||||
case TCP:
|
||||
if len(sub) < tcpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
case UDP:
|
||||
if len(sub) < udpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
case TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) IP4Header() IP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
}
|
||||
ipid := binary.BigEndian.Uint16(q.b[4:6])
|
||||
return IP4Header{
|
||||
IPID: ipid,
|
||||
IPProto: q.IPProto,
|
||||
Src: q.Src.IP,
|
||||
Dst: q.Dst.IP,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) ICMP4Header() ICMP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
}
|
||||
return ICMP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
Type: ICMP4Type(q.b[q.subofs+0]),
|
||||
Code: ICMP4Code(q.b[q.subofs+1]),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) UDP4Header() UDP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
}
|
||||
return UDP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
SrcPort: q.Src.Port,
|
||||
DstPort: q.Dst.Port,
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer returns the entire packet buffer.
|
||||
// This is a read-only view; that is, q retains the ownership of the buffer.
|
||||
func (q *Parsed) Buffer() []byte {
|
||||
return q.b
|
||||
}
|
||||
|
||||
// Payload returns the payload of the IP subprotocol section.
|
||||
// This is a read-only view; that is, q retains the ownership of the buffer.
|
||||
func (q *Parsed) Payload() []byte {
|
||||
return q.b[q.dataofs:q.length]
|
||||
}
|
||||
|
||||
// IsTCPSyn reports whether q is a TCP SYN packet
|
||||
// (i.e. the first packet in a new connection).
|
||||
func (q *Parsed) IsTCPSyn() bool {
|
||||
return (q.TCPFlags & TCPSynAck) == TCPSyn
|
||||
}
|
||||
|
||||
// IsError reports whether q is an ICMP "Error" packet.
|
||||
func (q *Parsed) IsError() bool {
|
||||
switch q.IPProto {
|
||||
case ICMPv4:
|
||||
if len(q.b) < q.subofs+8 {
|
||||
return false
|
||||
}
|
||||
t := ICMP4Type(q.b[q.subofs])
|
||||
return t == ICMP4Unreachable || t == ICMP4TimeExceeded
|
||||
case ICMPv6:
|
||||
if len(q.b) < q.subofs+8 {
|
||||
return false
|
||||
}
|
||||
t := ICMP6Type(q.b[q.subofs])
|
||||
return t == ICMP6Unreachable || t == ICMP6TimeExceeded
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an ICMP Echo Request.
|
||||
func (q *Parsed) IsEchoRequest() bool {
|
||||
switch q.IPProto {
|
||||
case ICMPv4:
|
||||
return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoRequest && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode
|
||||
case ICMPv6:
|
||||
return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoRequest && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
|
||||
func (q *Parsed) IsEchoResponse() bool {
|
||||
switch q.IPProto {
|
||||
case ICMPv4:
|
||||
return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoReply && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode
|
||||
case ICMPv6:
|
||||
return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoReply && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func Hexdump(b []byte) string {
|
||||
out := new(strings.Builder)
|
||||
for i := 0; i < len(b); i += 16 {
|
||||
if i > 0 {
|
||||
fmt.Fprintf(out, "\n")
|
||||
}
|
||||
fmt.Fprintf(out, " %04x ", i)
|
||||
j := 0
|
||||
for ; j < 16 && i+j < len(b); j++ {
|
||||
if j == 8 {
|
||||
fmt.Fprintf(out, " ")
|
||||
}
|
||||
fmt.Fprintf(out, "%02x ", b[i+j])
|
||||
}
|
||||
for ; j < 16; j++ {
|
||||
if j == 8 {
|
||||
fmt.Fprintf(out, " ")
|
||||
}
|
||||
fmt.Fprintf(out, " ")
|
||||
}
|
||||
fmt.Fprintf(out, " ")
|
||||
for j = 0; j < 16 && i+j < len(b); j++ {
|
||||
if b[i+j] >= 32 && b[i+j] < 128 {
|
||||
fmt.Fprintf(out, "%c", b[i+j])
|
||||
} else {
|
||||
fmt.Fprintf(out, ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func mustIPPort(s string) netaddr.IPPort {
|
||||
ipp, err := netaddr.ParseIPPort(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ipp
|
||||
}
|
||||
|
||||
var icmp4RequestBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x27, 0xde, 0xad, 0x00, 0x00, 0x40, 0x01, 0x8c, 0x15,
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// ICMP header
|
||||
0x08, 0x00, 0x7d, 0x22,
|
||||
// "request_payload"
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var icmp4RequestDecode = Parsed{
|
||||
b: icmp4RequestBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 24,
|
||||
length: len(icmp4RequestBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: ICMPv4,
|
||||
Src: mustIPPort("1.2.3.4:0"),
|
||||
Dst: mustIPPort("5.6.7.8:0"),
|
||||
}
|
||||
|
||||
var icmp4ReplyBuffer = []byte{
|
||||
0x45, 0x00, 0x00, 0x25, 0x21, 0x52, 0x00, 0x00, 0x40, 0x01, 0x49, 0x73,
|
||||
// source ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// destination ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// ICMP header
|
||||
0x00, 0x00, 0xe6, 0x9e,
|
||||
// "reply_payload"
|
||||
0x72, 0x65, 0x70, 0x6c, 0x79, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var icmp4ReplyDecode = Parsed{
|
||||
b: icmp4ReplyBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 24,
|
||||
length: len(icmp4ReplyBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: ICMPv4,
|
||||
Src: mustIPPort("1.2.3.4:0"),
|
||||
Dst: mustIPPort("5.6.7.8:0"),
|
||||
}
|
||||
|
||||
// ICMPv6 Router Solicitation
|
||||
var icmp6PacketBuffer = []byte{
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x08, 0x3a, 0xff,
|
||||
0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xfb, 0x57, 0x1d, 0xea, 0x9c, 0x39, 0x8f, 0xb7,
|
||||
0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
|
||||
0x85, 0x00, 0x38, 0x04, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
var icmp6PacketDecode = Parsed{
|
||||
b: icmp6PacketBuffer,
|
||||
subofs: 40,
|
||||
dataofs: 44,
|
||||
length: len(icmp6PacketBuffer),
|
||||
IPVersion: 6,
|
||||
IPProto: ICMPv6,
|
||||
Src: mustIPPort("[fe80::fb57:1dea:9c39:8fb7]:0"),
|
||||
Dst: mustIPPort("[ff02::2]:0"),
|
||||
}
|
||||
|
||||
// This is a malformed IPv4 packet.
|
||||
// Namely, the string "tcp_payload" follows the first byte of the IPv4 header.
|
||||
var unknownPacketBuffer = []byte{
|
||||
0x45, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var unknownPacketDecode = Parsed{
|
||||
b: unknownPacketBuffer,
|
||||
IPVersion: 0,
|
||||
IPProto: Unknown,
|
||||
}
|
||||
|
||||
var tcp4PacketBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x37, 0xde, 0xad, 0x00, 0x00, 0x40, 0x06, 0x49, 0x5f,
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// TCP header with SYN, ACK set
|
||||
0x00, 0x7b, 0x02, 0x37, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00,
|
||||
0x50, 0x12, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// "request_payload"
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var tcp4PacketDecode = Parsed{
|
||||
b: tcp4PacketBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 40,
|
||||
length: len(tcp4PacketBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: TCP,
|
||||
Src: mustIPPort("1.2.3.4:123"),
|
||||
Dst: mustIPPort("5.6.7.8:567"),
|
||||
TCPFlags: TCPSynAck,
|
||||
}
|
||||
|
||||
var tcp6RequestBuffer = []byte{
|
||||
// IPv6 header up to hop limit
|
||||
0x60, 0x06, 0xef, 0xcc, 0x00, 0x28, 0x06, 0x40,
|
||||
// Src addr
|
||||
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
|
||||
// Dst addr
|
||||
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
|
||||
// TCP SYN segment, no payload
|
||||
0xa4, 0x60, 0x00, 0x50, 0xf3, 0x82, 0xa1, 0x25, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfd, 0x20,
|
||||
0xb1, 0xc6, 0x00, 0x00, 0x02, 0x04, 0x05, 0xa0, 0x04, 0x02, 0x08, 0x0a, 0xca, 0x76, 0xa6, 0x8e,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
|
||||
}
|
||||
|
||||
var tcp6RequestDecode = Parsed{
|
||||
b: tcp6RequestBuffer,
|
||||
subofs: 40,
|
||||
dataofs: len(tcp6RequestBuffer),
|
||||
length: len(tcp6RequestBuffer),
|
||||
|
||||
IPVersion: 6,
|
||||
IPProto: TCP,
|
||||
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:42080"),
|
||||
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:80"),
|
||||
TCPFlags: TCPSyn,
|
||||
}
|
||||
|
||||
var udp4RequestBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x2b, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// UDP header
|
||||
0x00, 0x7b, 0x02, 0x37, 0x00, 0x17, 0x72, 0x1d,
|
||||
// "request_payload"
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var udp4RequestDecode = Parsed{
|
||||
b: udp4RequestBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 28,
|
||||
length: len(udp4RequestBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: UDP,
|
||||
Src: mustIPPort("1.2.3.4:123"),
|
||||
Dst: mustIPPort("5.6.7.8:567"),
|
||||
}
|
||||
|
||||
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
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
}
|
||||
|
||||
// Regression check for the IHL field pointing beyond the end of the
|
||||
// packet.
|
||||
var invalid4RequestDecode = Parsed{
|
||||
b: invalid4RequestBuffer,
|
||||
subofs: 40,
|
||||
length: len(invalid4RequestBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: Unknown,
|
||||
Src: mustIPPort("1.2.3.4:0"),
|
||||
Dst: mustIPPort("5.6.7.8:0"),
|
||||
}
|
||||
|
||||
var udp6RequestBuffer = []byte{
|
||||
// IPv6 header up to hop limit
|
||||
0x60, 0x0e, 0xc9, 0x67, 0x00, 0x29, 0x11, 0x40,
|
||||
// Src addr
|
||||
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
|
||||
// Dst addr
|
||||
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
|
||||
// UDP header
|
||||
0xd4, 0x04, 0x01, 0xbb, 0x00, 0x29, 0x96, 0x84,
|
||||
// Payload
|
||||
0x5c, 0x06, 0xae, 0x85, 0x02, 0xf5, 0xdb, 0x90, 0xe0, 0xe0, 0x93, 0xed, 0x9a, 0xd9, 0x92, 0x69, 0xbe, 0x36, 0x8a, 0x7d, 0xd7, 0xce, 0xd0, 0x8a, 0xf2, 0x51, 0x95, 0xff, 0xb6, 0x92, 0x70, 0x10, 0xd7,
|
||||
}
|
||||
|
||||
var udp6RequestDecode = Parsed{
|
||||
b: udp6RequestBuffer,
|
||||
subofs: 40,
|
||||
dataofs: 48,
|
||||
length: len(udp6RequestBuffer),
|
||||
|
||||
IPVersion: 6,
|
||||
IPProto: UDP,
|
||||
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:54276"),
|
||||
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:443"),
|
||||
}
|
||||
|
||||
var udp4ReplyBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x29, 0x21, 0x52, 0x00, 0x00, 0x40, 0x11, 0x49, 0x5f,
|
||||
// source ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// destination ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// UDP header
|
||||
0x02, 0x37, 0x00, 0x7b, 0x00, 0x15, 0xd3, 0x9d,
|
||||
// "reply_payload"
|
||||
0x72, 0x65, 0x70, 0x6c, 0x79, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var udp4ReplyDecode = Parsed{
|
||||
b: udp4ReplyBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 28,
|
||||
length: len(udp4ReplyBuffer),
|
||||
|
||||
IPProto: UDP,
|
||||
Src: mustIPPort("1.2.3.4:567"),
|
||||
Dst: mustIPPort("5.6.7.8:123"),
|
||||
}
|
||||
|
||||
var igmpPacketBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x46, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x02, 0x41, 0x22,
|
||||
// source IP
|
||||
0xc0, 0xa8, 0x01, 0x52,
|
||||
// destination IP
|
||||
0xe0, 0x00, 0x00, 0xfb,
|
||||
// IGMP Membership Report
|
||||
0x94, 0x04, 0x00, 0x00, 0x16, 0x00, 0x09, 0x04, 0xe0, 0x00, 0x00, 0xfb,
|
||||
//0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
var igmpPacketDecode = Parsed{
|
||||
b: igmpPacketBuffer,
|
||||
subofs: 24,
|
||||
length: len(igmpPacketBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: IGMP,
|
||||
Src: mustIPPort("192.168.1.82:0"),
|
||||
Dst: mustIPPort("224.0.0.251:0"),
|
||||
}
|
||||
|
||||
var ipv4TSMPBuffer = []byte{
|
||||
// IPv4 header:
|
||||
0x45, 0x00,
|
||||
0x00, 0x1b, // 20 + 7 bytes total
|
||||
0x00, 0x00, // ID
|
||||
0x00, 0x00, // Fragment
|
||||
0x40, // TTL
|
||||
byte(TSMP),
|
||||
0x5f, 0xc3, // header checksum (wrong here)
|
||||
// source IP:
|
||||
0x64, 0x5e, 0x0c, 0x0e,
|
||||
// dest IP:
|
||||
0x64, 0x4a, 0x46, 0x03,
|
||||
byte(TSMPTypeRejectedConn),
|
||||
byte(TCP),
|
||||
byte(RejectedDueToACLs),
|
||||
0x00, 123, // src port
|
||||
0x00, 80, // dst port
|
||||
}
|
||||
|
||||
var ipv4TSMPDecode = Parsed{
|
||||
b: ipv4TSMPBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 20,
|
||||
length: 27,
|
||||
IPVersion: 4,
|
||||
IPProto: TSMP,
|
||||
Src: mustIPPort("100.94.12.14:0"),
|
||||
Dst: mustIPPort("100.74.70.3:0"),
|
||||
}
|
||||
|
||||
func TestParsedString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
qdecode Parsed
|
||||
want string
|
||||
}{
|
||||
{"tcp4", tcp4PacketDecode, "TCP{1.2.3.4:123 > 5.6.7.8:567}"},
|
||||
{"tcp6", tcp6RequestDecode, "TCP{[2001:559:bc13:5400:1749:4628:3934:e1b]:42080 > [2607:f8b0:400a:809::200e]:80}"},
|
||||
{"udp4", udp4RequestDecode, "UDP{1.2.3.4:123 > 5.6.7.8:567}"},
|
||||
{"udp6", udp6RequestDecode, "UDP{[2001:559:bc13:5400:1749:4628:3934:e1b]:54276 > [2607:f8b0:400a:809::200e]:443}"},
|
||||
{"icmp4", icmp4RequestDecode, "ICMPv4{1.2.3.4:0 > 5.6.7.8:0}"},
|
||||
{"icmp6", icmp6PacketDecode, "ICMPv6{[fe80::fb57:1dea:9c39:8fb7]:0 > [ff02::2]:0}"},
|
||||
{"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"},
|
||||
{"unknown", unknownPacketDecode, "Unknown{???}"},
|
||||
{"ipv4_tsmp", ipv4TSMPDecode, "TSMP{100.94.12.14:0 > 100.74.70.3:0}"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.qdecode.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var sink string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
sink = tests[0].qdecode.String()
|
||||
})
|
||||
_ = sink
|
||||
if allocs != 1 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buf []byte
|
||||
want Parsed
|
||||
}{
|
||||
{"icmp4", icmp4RequestBuffer, icmp4RequestDecode},
|
||||
{"icmp6", icmp6PacketBuffer, icmp6PacketDecode},
|
||||
{"tcp4", tcp4PacketBuffer, tcp4PacketDecode},
|
||||
{"tcp6", tcp6RequestBuffer, tcp6RequestDecode},
|
||||
{"udp4", udp4RequestBuffer, udp4RequestDecode},
|
||||
{"udp6", udp6RequestBuffer, udp6RequestDecode},
|
||||
{"igmp", igmpPacketBuffer, igmpPacketDecode},
|
||||
{"unknown", unknownPacketBuffer, unknownPacketDecode},
|
||||
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
|
||||
{"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got Parsed
|
||||
got.Decode(tt.buf)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mismatch\n got: %s %#v\nwant: %s %#v", got.String(), got, tt.want.String(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
var got Parsed
|
||||
got.Decode(tests[0].buf)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Errorf("allocs = %v; want 0", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecode(b *testing.B) {
|
||||
benches := []struct {
|
||||
name string
|
||||
buf []byte
|
||||
}{
|
||||
{"tcp4", tcp4PacketBuffer},
|
||||
{"tcp6", tcp6RequestBuffer},
|
||||
{"udp4", udp4RequestBuffer},
|
||||
{"udp6", udp6RequestBuffer},
|
||||
{"icmp4", icmp4RequestBuffer},
|
||||
{"icmp6", icmp6PacketBuffer},
|
||||
{"igmp", igmpPacketBuffer},
|
||||
{"unknown", unknownPacketBuffer},
|
||||
}
|
||||
|
||||
for _, bench := range benches {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var p Parsed
|
||||
p.Decode(bench.buf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalRequest(t *testing.T) {
|
||||
// Too small to hold our packets, but only barely.
|
||||
var small [20]byte
|
||||
var large [64]byte
|
||||
|
||||
icmpHeader := icmp4RequestDecode.ICMP4Header()
|
||||
udpHeader := udp4RequestDecode.UDP4Header()
|
||||
tests := []struct {
|
||||
name string
|
||||
header Header
|
||||
want []byte
|
||||
}{
|
||||
{"icmp", &icmpHeader, icmp4RequestBuffer},
|
||||
{"udp", &udpHeader, udp4RequestBuffer},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.header.Marshal(small[:])
|
||||
if err != errSmallBuffer {
|
||||
t.Errorf("got err: nil; want: %s", errSmallBuffer)
|
||||
}
|
||||
|
||||
dataOffset := tt.header.Len()
|
||||
dataLength := copy(large[dataOffset:], []byte("request_payload"))
|
||||
end := dataOffset + dataLength
|
||||
err = tt.header.Marshal(large[:end])
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("got err: %s; want nil", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(large[:end], tt.want) {
|
||||
t.Errorf("got %x; want %x", large[:end], tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalResponse(t *testing.T) {
|
||||
var buf [64]byte
|
||||
|
||||
icmpHeader := icmp4RequestDecode.ICMP4Header()
|
||||
udpHeader := udp4RequestDecode.UDP4Header()
|
||||
|
||||
type HeaderToResponser interface {
|
||||
Header
|
||||
// ToResponse transforms the header into one for a response packet.
|
||||
// For instance, this swaps the source and destination IPs.
|
||||
ToResponse()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
header HeaderToResponser
|
||||
want []byte
|
||||
}{
|
||||
{"icmp", &icmpHeader, icmp4ReplyBuffer},
|
||||
{"udp", &udpHeader, udp4ReplyBuffer},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.header.ToResponse()
|
||||
|
||||
dataOffset := tt.header.Len()
|
||||
dataLength := copy(buf[dataOffset:], []byte("reply_payload"))
|
||||
end := dataOffset + dataLength
|
||||
err := tt.header.Marshal(buf[:end])
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("got err: %s; want nil", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:end], tt.want) {
|
||||
t.Errorf("got %x; want %x", buf[:end], tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,140 +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.
|
||||
|
||||
// TSMP is our ICMP-like "Tailscale Message Protocol" for signaling
|
||||
// Tailscale-specific messages between nodes. It uses IP protocol 99
|
||||
// (reserved for "any private encryption scheme") within
|
||||
// Wireguard's normal encryption between peers and never hits the host
|
||||
// network stack.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/flowtrack"
|
||||
)
|
||||
|
||||
// TailscaleRejectedHeader is a TSMP message that says that one
|
||||
// Tailscale node has rejected the connection from another. Unlike a
|
||||
// TCP RST, this includes a reason.
|
||||
//
|
||||
// On the wire, after the IP header, it's currently 7 bytes:
|
||||
// * '!'
|
||||
// * IPProto byte (IANA protocol number: TCP or UDP)
|
||||
// * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
|
||||
// * srcPort big endian uint16
|
||||
// * dstPort big endian uint16
|
||||
//
|
||||
// In the future it might also accept 16 byte IP flow src/dst IPs
|
||||
// after the header, if they're different than the IP-level ones.
|
||||
type TailscaleRejectedHeader struct {
|
||||
IPSrc netaddr.IP // IPv4 or IPv6 header's src IP
|
||||
IPDst netaddr.IP // IPv4 or IPv6 header's dst IP
|
||||
Src netaddr.IPPort // rejected flow's src
|
||||
Dst netaddr.IPPort // rejected flow's dst
|
||||
Proto IPProto // proto that was rejected (TCP or UDP)
|
||||
Reason TailscaleRejectReason // why the connection was rejected
|
||||
}
|
||||
|
||||
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
|
||||
return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst}
|
||||
}
|
||||
|
||||
func (rh TailscaleRejectedHeader) String() string {
|
||||
return fmt.Sprintf("TSMP-reject-flow{%s %s > %s}: %s", rh.Proto, rh.Src, rh.Dst, rh.Reason)
|
||||
}
|
||||
|
||||
type TSMPType uint8
|
||||
|
||||
const (
|
||||
TSMPTypeRejectedConn TSMPType = '!'
|
||||
)
|
||||
|
||||
type TailscaleRejectReason byte
|
||||
|
||||
const (
|
||||
RejectedDueToACLs TailscaleRejectReason = 'A'
|
||||
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
|
||||
)
|
||||
|
||||
func (r TailscaleRejectReason) String() string {
|
||||
switch r {
|
||||
case RejectedDueToACLs:
|
||||
return "acl"
|
||||
case RejectedDueToShieldsUp:
|
||||
return "shields"
|
||||
}
|
||||
return fmt.Sprintf("0x%02x", byte(r))
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Len() int {
|
||||
var ipHeaderLen int
|
||||
if h.IPSrc.Is4() {
|
||||
ipHeaderLen = ip4HeaderLength
|
||||
} else if h.IPSrc.Is6() {
|
||||
ipHeaderLen = ip6HeaderLength
|
||||
}
|
||||
return ipHeaderLen +
|
||||
1 + // TSMPType byte
|
||||
1 + // IPProto byte
|
||||
1 + // TailscaleRejectReason byte
|
||||
2*2 // 2 uint16 ports
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
if h.Src.IP.Is4() {
|
||||
iph := IP4Header{
|
||||
IPProto: TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip4HeaderLength:]
|
||||
} else if h.Src.IP.Is6() {
|
||||
iph := IP6Header{
|
||||
IPProto: TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip6HeaderLength:]
|
||||
} else {
|
||||
return errors.New("bogus src IP")
|
||||
}
|
||||
buf[0] = byte(TSMPTypeRejectedConn)
|
||||
buf[1] = byte(h.Proto)
|
||||
buf[2] = byte(h.Reason)
|
||||
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
|
||||
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsTailscaleRejectedHeader parses pp as an incoming rejection
|
||||
// connection TSMP message.
|
||||
//
|
||||
// ok reports whether pp was a valid TSMP rejection packet.
|
||||
func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok bool) {
|
||||
p := pp.Payload()
|
||||
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
|
||||
return
|
||||
}
|
||||
return TailscaleRejectedHeader{
|
||||
Proto: IPProto(p[1]),
|
||||
Reason: TailscaleRejectReason(p[2]),
|
||||
IPSrc: pp.Src.IP,
|
||||
IPDst: pp.Dst.IP,
|
||||
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
|
||||
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
|
||||
}, true
|
||||
}
|
||||
@@ -1,63 +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.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestTailscaleRejectedHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
h TailscaleRejectedHeader
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("5.5.5.5"),
|
||||
IPDst: netaddr.MustParseIP("1.2.3.4"),
|
||||
Src: netaddr.MustParseIPPort("1.2.3.4:567"),
|
||||
Dst: netaddr.MustParseIPPort("5.5.5.5:443"),
|
||||
Proto: TCP,
|
||||
Reason: RejectedDueToACLs,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{TCP 1.2.3.4:567 > 5.5.5.5:443}: acl",
|
||||
},
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("2::2"),
|
||||
IPDst: netaddr.MustParseIP("1::1"),
|
||||
Src: netaddr.MustParseIPPort("[1::1]:567"),
|
||||
Dst: netaddr.MustParseIPPort("[2::2]:443"),
|
||||
Proto: UDP,
|
||||
Reason: RejectedDueToShieldsUp,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
gotStr := tt.h.String()
|
||||
if gotStr != tt.wantStr {
|
||||
t.Errorf("%v. String = %q; want %q", i, gotStr, tt.wantStr)
|
||||
continue
|
||||
}
|
||||
pkt := make([]byte, tt.h.Len())
|
||||
tt.h.Marshal(pkt)
|
||||
|
||||
var p Parsed
|
||||
p.Decode(pkt)
|
||||
t.Logf("Parsed: %+v", p)
|
||||
t.Logf("Parsed: %s", p.String())
|
||||
back, ok := p.AsTailscaleRejectedHeader()
|
||||
if !ok {
|
||||
t.Errorf("%v. %q (%02x) didn't parse back", i, gotStr, pkt)
|
||||
continue
|
||||
}
|
||||
if back != tt.h {
|
||||
t.Errorf("%v. %q parsed back as %q", i, tt.h, back)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// udpHeaderLength is the size of the UDP packet header, not including
|
||||
// the outer IP header.
|
||||
const udpHeaderLength = 8
|
||||
|
||||
// UDP4Header is an IPv4+UDP header.
|
||||
type UDP4Header struct {
|
||||
IP4Header
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h UDP4Header) Len() int {
|
||||
return h.IP4Header.Len() + udpHeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h UDP4Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
// The caller does not need to set this.
|
||||
h.IPProto = UDP
|
||||
|
||||
length := len(buf) - h.IP4Header.Len()
|
||||
binary.BigEndian.PutUint16(buf[20:22], h.SrcPort)
|
||||
binary.BigEndian.PutUint16(buf[22:24], h.DstPort)
|
||||
binary.BigEndian.PutUint16(buf[24:26], uint16(length))
|
||||
binary.BigEndian.PutUint16(buf[26:28], 0) // blank checksum
|
||||
|
||||
// UDP checksum with IP pseudo header.
|
||||
h.IP4Header.marshalPseudo(buf)
|
||||
binary.BigEndian.PutUint16(buf[26:28], ip4Checksum(buf[ip4PseudoHeaderOffset:]))
|
||||
|
||||
h.IP4Header.Marshal(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *UDP4Header) ToResponse() {
|
||||
h.SrcPort, h.DstPort = h.DstPort, h.SrcPort
|
||||
h.IP4Header.ToResponse()
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2020 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 packet
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// UDP6Header is an IPv6+UDP header.
|
||||
type UDP6Header struct {
|
||||
IP6Header
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h UDP6Header) Len() int {
|
||||
return h.IP6Header.Len() + udpHeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h UDP6Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
// The caller does not need to set this.
|
||||
h.IPProto = UDP
|
||||
|
||||
length := len(buf) - h.IP6Header.Len()
|
||||
binary.BigEndian.PutUint16(buf[40:42], h.SrcPort)
|
||||
binary.BigEndian.PutUint16(buf[42:44], h.DstPort)
|
||||
binary.BigEndian.PutUint16(buf[44:46], uint16(length))
|
||||
binary.BigEndian.PutUint16(buf[46:48], 0) // blank checksum
|
||||
|
||||
// UDP checksum with IP pseudo header.
|
||||
h.IP6Header.marshalPseudo(buf)
|
||||
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
|
||||
|
||||
h.IP6Header.Marshal(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *UDP6Header) ToResponse() {
|
||||
h.SrcPort, h.DstPort = h.DstPort, h.SrcPort
|
||||
h.IP6Header.ToResponse()
|
||||
}
|
||||
@@ -4,32 +4,7 @@
|
||||
|
||||
package tsaddr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestInCrostiniRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
ip netaddr.IP
|
||||
want bool
|
||||
}{
|
||||
{netaddr.IPv4(192, 168, 0, 1), false},
|
||||
{netaddr.IPv4(100, 101, 102, 103), false},
|
||||
{netaddr.IPv4(100, 115, 92, 0), true},
|
||||
{netaddr.IPv4(100, 115, 92, 5), true},
|
||||
{netaddr.IPv4(100, 115, 92, 255), true},
|
||||
{netaddr.IPv4(100, 115, 93, 40), true},
|
||||
{netaddr.IPv4(100, 115, 94, 1), false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := ChromeOSVMRange().Contains(test.ip); got != test.want {
|
||||
t.Errorf("inCrostiniRange(%q) = %v, want %v", test.ip, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
import "testing"
|
||||
|
||||
func TestChromeOSVMRange(t *testing.T) {
|
||||
if got, want := ChromeOSVMRange().String(), "100.115.92.0/23"; got != want {
|
||||
|
||||
@@ -8,8 +8,9 @@ package portlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
exec "tailscale.com/tempfork/osexec"
|
||||
)
|
||||
|
||||
var osHideWindow func(*exec.Cmd) // non-nil on Windows; see portlist_windows.go
|
||||
|
||||
@@ -7,6 +7,7 @@ package portlist
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tailscale.com/version"
|
||||
@@ -33,7 +34,7 @@ type Poller struct {
|
||||
// NewPoller returns a new portlist Poller. It returns an error
|
||||
// if the portlist couldn't be obtained.
|
||||
func NewPoller() (*Poller, error) {
|
||||
if version.OS() == "iOS" {
|
||||
if runtime.GOOS == "darwin" && version.IsMobile() {
|
||||
return nil, errors.New("not available on iOS")
|
||||
}
|
||||
p := &Poller{
|
||||
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
exec "tailscale.com/tempfork/osexec"
|
||||
)
|
||||
|
||||
// We have to run netstat, which is a bit expensive, so don't do it too often.
|
||||
|
||||
@@ -47,153 +47,6 @@ func TestIgnoreLocallyBoundPorts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLessThan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b Port
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"Port a < b",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 101, Process: "proc1", inode: "inode1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Port a > b",
|
||||
Port{Proto: "tcp", Port: 101, Process: "proc1", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Proto a < b",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
Port{Proto: "udp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Proto a < b",
|
||||
Port{Proto: "udp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"inode a < b",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode2"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"inode a > b",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc2", inode: "inode2"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Process a < b",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc2", inode: "inode1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Process a > b",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc2", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Port evaluated first",
|
||||
Port{Proto: "udp", Port: 100, Process: "proc2", inode: "inode2"},
|
||||
Port{Proto: "tcp", Port: 101, Process: "proc1", inode: "inode1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Proto evaluated second",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc2", inode: "inode2"},
|
||||
Port{Proto: "udp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"inode evaluated third",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc2", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode2"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Process evaluated fourth",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc2", inode: "inode1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"equal",
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
Port{Proto: "tcp", Port: 100, Process: "proc1", inode: "inode1"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.a.lessThan(&tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameInodes(t *testing.T) {
|
||||
port1 := Port{Proto: "tcp", Port: 100, Process: "proc", inode: "inode1"}
|
||||
port2 := Port{Proto: "tcp", Port: 100, Process: "proc", inode: "inode1"}
|
||||
portProto := Port{Proto: "udp", Port: 100, Process: "proc", inode: "inode1"}
|
||||
portPort := Port{Proto: "tcp", Port: 101, Process: "proc", inode: "inode1"}
|
||||
portInode := Port{Proto: "tcp", Port: 100, Process: "proc", inode: "inode2"}
|
||||
portProcess := Port{Proto: "tcp", Port: 100, Process: "other", inode: "inode1"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b List
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"identical",
|
||||
List{port1, port1},
|
||||
List{port2, port2},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"proto differs",
|
||||
List{port1, port1},
|
||||
List{port2, portProto},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"port differs",
|
||||
List{port1, port1},
|
||||
List{port2, portPort},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"inode differs",
|
||||
List{port1, port1},
|
||||
List{port2, portInode},
|
||||
false,
|
||||
},
|
||||
{
|
||||
// SameInodes does not check the Process field
|
||||
"Process differs",
|
||||
List{port1, port1},
|
||||
List{port2, portProcess},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := tt.a.SameInodes(tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetList(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
package portlist
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
exec "tailscale.com/tempfork/osexec"
|
||||
)
|
||||
|
||||
// Forking on Windows is insanely expensive, so don't do it too often.
|
||||
|
||||
@@ -64,33 +64,10 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) {
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
os.Chmod(path, socketPermissionsForOS())
|
||||
os.Chmod(path, 0666)
|
||||
return pipe, 0, err
|
||||
}
|
||||
|
||||
// socketPermissionsForOS returns the permissions to use for the
|
||||
// tailscaled.sock.
|
||||
func socketPermissionsForOS() os.FileMode {
|
||||
if runtime.GOOS == "linux" {
|
||||
// On Linux, the ipn/ipnserver package looks at the Unix peer creds
|
||||
// and only permits read-only actions from non-root users, so we want
|
||||
// this opened up wider.
|
||||
//
|
||||
// TODO(bradfitz): unify this all one in place probably, moving some
|
||||
// of ipnserver (which does much of the "safe" bits) here. Maybe
|
||||
// instead of net.Listener, we should return a type that returns
|
||||
// an identity in addition to a net.Conn? (returning a wrapped net.Conn
|
||||
// would surprise downstream callers probably)
|
||||
//
|
||||
// TODO(bradfitz): if OpenBSD and FreeBSD do the equivalent peercreds
|
||||
// stuff that's in ipn/ipnserver/conn_ucred.go, they should also
|
||||
// return 0666 here.
|
||||
return 0666
|
||||
}
|
||||
// Otherwise, root only.
|
||||
return 0600
|
||||
}
|
||||
|
||||
// connectMacOSAppSandbox connects to the Tailscale Network Extension,
|
||||
// which is necessarily running within the macOS App Sandbox. Our
|
||||
// little dance to connect a regular user binary to the sandboxed
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
check_file() {
|
||||
got=$1
|
||||
|
||||
for year in `seq 2019 2021`; do
|
||||
for year in `seq 2019 2020`; do
|
||||
want=$(cat <<EOF
|
||||
// Copyright (c) $year Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
|
||||
24
shell.nix
24
shell.nix
@@ -1,24 +0,0 @@
|
||||
# This is a shell.nix file used to describe the environment that tailscale needs
|
||||
# for development. This includes a lot of the basic tools that you need in order
|
||||
# to get started. We hope this file will be useful for users of Nix on macOS or
|
||||
# Linux.
|
||||
#
|
||||
# For more information about this and why this file is useful, see here:
|
||||
# https://nixos.org/guides/nix-pills/developing-with-nix-shell.html
|
||||
#
|
||||
# Also look into direnv: https://direnv.net/, this can make it so that you can
|
||||
# automatically get your environment set up when you change folders into the
|
||||
# project.
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShell {
|
||||
# This specifies the tools that are needed for people to get started with
|
||||
# development. These tools include:
|
||||
# - The Go compiler toolchain (and all additional tooling with it)
|
||||
# - goimports, a robust formatting tool for Go source code
|
||||
# - gopls, the language server for Go to increase editor integration
|
||||
# - git, the version control program (used in some scripts)
|
||||
buildInputs = with pkgs; [
|
||||
go goimports gopls git
|
||||
];
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func NewWaitGroupChan() *WaitGroupChan {
|
||||
}
|
||||
|
||||
// DoneChan returns a channel that's closed on completion.
|
||||
func (wg *WaitGroupChan) DoneChan() <-chan struct{} { return wg.done }
|
||||
func (c *WaitGroupChan) DoneChan() <-chan struct{} { return c.done }
|
||||
|
||||
// Add adds delta, which may be negative, to the WaitGroupChan
|
||||
// counter. If the counter becomes zero, all goroutines blocked on
|
||||
@@ -46,10 +46,10 @@ func (wg *WaitGroupChan) DoneChan() <-chan struct{} { return wg.done }
|
||||
// than zero, may happen at any time. Typically this means the calls
|
||||
// to Add should execute before the statement creating the goroutine
|
||||
// or other event to be waited for.
|
||||
func (wg *WaitGroupChan) Add(delta int) {
|
||||
n := atomic.AddInt64(&wg.n, int64(delta))
|
||||
func (c *WaitGroupChan) Add(delta int) {
|
||||
n := atomic.AddInt64(&c.n, int64(delta))
|
||||
if n == 0 {
|
||||
close(wg.done)
|
||||
close(c.done)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
@@ -22,18 +24,6 @@ import (
|
||||
"tailscale.com/types/structs"
|
||||
)
|
||||
|
||||
// CurrentMapRequestVersion is the current MapRequest.Version value.
|
||||
//
|
||||
// History of versions:
|
||||
// 3: implicit compression, keep-alives
|
||||
// 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
|
||||
// 5: 2020-10-19, implies IncludeIPv6, delta Peers/UserProfiles, supports MagicDNS
|
||||
// 6: 2020-12-07: means MapResponse.PacketFilter nil means unchanged
|
||||
// 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
|
||||
// 8: 2020-12-19: client can receive IPv6 addresses and routes if beta enabled server-side
|
||||
// 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains
|
||||
const CurrentMapRequestVersion = 9
|
||||
|
||||
type ID int64
|
||||
|
||||
type UserID ID
|
||||
@@ -123,6 +113,8 @@ type User struct {
|
||||
Logins []LoginID
|
||||
Roles []RoleID
|
||||
Created time.Time
|
||||
|
||||
// Note: be sure to update Clone when adding new fields
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
@@ -147,32 +139,27 @@ type UserProfile struct {
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID NodeID
|
||||
Name string // DNS
|
||||
|
||||
// User is the user who created the node. If ACL tags are in
|
||||
// use for the node then it doesn't reflect the ACL identity
|
||||
// that the node is running as.
|
||||
User UserID
|
||||
|
||||
// Sharer, if non-zero, is the user who shared this node, if different than User.
|
||||
Sharer UserID `json:",omitempty"`
|
||||
|
||||
ID NodeID
|
||||
Name string // DNS
|
||||
User UserID
|
||||
Key NodeKey
|
||||
KeyExpiry time.Time
|
||||
Machine MachineKey
|
||||
DiscoKey DiscoKey
|
||||
Addresses []netaddr.IPPrefix // IP addresses of this Node directly
|
||||
AllowedIPs []netaddr.IPPrefix // range of IP addresses to route to this node
|
||||
Endpoints []string `json:",omitempty"` // IP+port (public via STUN, and local LANs)
|
||||
DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
|
||||
Addresses []wgcfg.CIDR // IP addresses of this Node directly
|
||||
AllowedIPs []wgcfg.CIDR // range of IP addresses to route to this node
|
||||
Endpoints []string `json:",omitempty"` // IP+port (public via STUN, and local LANs)
|
||||
DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
|
||||
Hostinfo Hostinfo
|
||||
Created time.Time
|
||||
LastSeen *time.Time `json:",omitempty"`
|
||||
|
||||
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
|
||||
KeepAlive bool // open and keep open a connection to this peer
|
||||
|
||||
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
|
||||
MachineAuthorized bool // TODO(crawshaw): replace with MachineStatus
|
||||
|
||||
// NOTE: any new fields containing pointers in this type
|
||||
// require changes to Node.Clone.
|
||||
}
|
||||
|
||||
type MachineStatus int
|
||||
@@ -224,15 +211,10 @@ func (m MachineStatus) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
func isNum(b byte) bool {
|
||||
return b >= '0' && b <= '9'
|
||||
}
|
||||
func isNum(r rune) bool { return r >= '0' && r <= '9' }
|
||||
func isAlpha(r rune) bool { return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') }
|
||||
|
||||
func isAlpha(b byte) bool {
|
||||
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
|
||||
}
|
||||
|
||||
// CheckTag validates tag for use as an ACL tag.
|
||||
// CheckTag valids whether a given string can be used as an ACL tag.
|
||||
// For now we allow only ascii alphanumeric tags, and they need to start
|
||||
// with a letter. No unicode shenanigans allowed, and we reserve punctuation
|
||||
// marks other than '-' for a possible future URI scheme.
|
||||
@@ -245,31 +227,32 @@ func CheckTag(tag string) error {
|
||||
if !strings.HasPrefix(tag, "tag:") {
|
||||
return errors.New("tags must start with 'tag:'")
|
||||
}
|
||||
tag = tag[4:]
|
||||
if tag == "" {
|
||||
return errors.New("tag names must not be empty")
|
||||
suffix := tag[len("tag:"):]
|
||||
if err := CheckTagSuffix(suffix); err != nil {
|
||||
return fmt.Errorf("invalid tag %q: %w", tag, err)
|
||||
}
|
||||
if !isAlpha(tag[0]) {
|
||||
return errors.New("tag names must start with a letter, after 'tag:'")
|
||||
}
|
||||
|
||||
for _, b := range []byte(tag) {
|
||||
if !isNum(b) && !isAlpha(b) && b != '-' {
|
||||
return errors.New("tag names can only contain numbers, letters, or dashes")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckRequestTags checks that all of h.RequestTags are valid.
|
||||
func (h *Hostinfo) CheckRequestTags() error {
|
||||
if h == nil {
|
||||
return nil
|
||||
// CheckTagSuffix checks whether tag is a valid tag suffix (the part
|
||||
// appearing after "tag:"). The error message does not reference
|
||||
// "tag:", so it's suitable for use by the "tailscale up" CLI tool
|
||||
// where the "tag:" isn't required. The returned error also does not
|
||||
// reference the tag itself, so the caller can wrap it as needed with
|
||||
// either the full or short form.
|
||||
func CheckTagSuffix(tag string) error {
|
||||
if tag == "" {
|
||||
return errors.New("tag names must not be empty")
|
||||
}
|
||||
for _, tag := range h.RequestTags {
|
||||
if err := CheckTag(tag); err != nil {
|
||||
return fmt.Errorf("tag(%#v): %w", tag, err)
|
||||
if i := strings.IndexFunc(tag, func(r rune) bool { return r >= utf8.RuneSelf }); i != -1 {
|
||||
return errors.New("tag names must only contain ASCII")
|
||||
}
|
||||
if !isAlpha(rune(tag[0])) {
|
||||
return errors.New("tag name must start with a letter")
|
||||
}
|
||||
for _, r := range tag {
|
||||
if !isNum(r) && !isAlpha(r) && r != '-' {
|
||||
return errors.New("tag names can only contain numbers, letters, or dashes")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -289,6 +272,9 @@ type Service struct {
|
||||
Description string `json:",omitempty"` // text description of service
|
||||
// TODO(apenwarr): allow advertising services on subnet IPs?
|
||||
// TODO(apenwarr): add "tags" here for each service?
|
||||
|
||||
// NOTE: any new fields containing pointers in this type
|
||||
// require changes to Hostinfo.Clone.
|
||||
}
|
||||
|
||||
// Hostinfo contains a summary of a Tailscale host.
|
||||
@@ -298,23 +284,21 @@ type Service struct {
|
||||
type Hostinfo struct {
|
||||
// TODO(crawshaw): mark all these fields ",omitempty" when all the
|
||||
// iOS apps are updated with the latest swift version of this struct.
|
||||
IPNVersion string `json:",omitempty"` // version of this code
|
||||
FrontendLogID string `json:",omitempty"` // logtail ID of frontend instance
|
||||
BackendLogID string `json:",omitempty"` // logtail ID of backend instance
|
||||
OS string // operating system the client runs on (a version.OS value)
|
||||
OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
|
||||
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
|
||||
Hostname string // name of the host the client runs on
|
||||
ShieldsUp bool `json:",omitempty"` // indicates whether the host is blocking incoming connections
|
||||
ShareeNode bool `json:",omitempty"` // indicates this node exists in netmap because it's owned by a shared-to user
|
||||
GoArch string `json:",omitempty"` // the host's GOARCH value (of the running binary)
|
||||
RoutableIPs []netaddr.IPPrefix `json:",omitempty"` // set of IP ranges this client can route
|
||||
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
|
||||
Services []Service `json:",omitempty"` // services advertised by this machine
|
||||
NetInfo *NetInfo `json:",omitempty"`
|
||||
IPNVersion string // version of this code
|
||||
FrontendLogID string `json:",omitempty"` // logtail ID of frontend instance
|
||||
BackendLogID string `json:",omitempty"` // logtail ID of backend instance
|
||||
OS string // operating system the client runs on (a version.OS value)
|
||||
OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
|
||||
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
|
||||
Hostname string // name of the host the client runs on
|
||||
GoArch string `json:",omitempty"` // the host's GOARCH value (of the running binary)
|
||||
RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route
|
||||
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
|
||||
Services []Service `json:",omitempty"` // services advertised by this machine
|
||||
NetInfo *NetInfo `json:",omitempty"`
|
||||
|
||||
// NOTE: any new fields containing pointers in this type
|
||||
// require changes to Hostinfo.Equal.
|
||||
// require changes to Hostinfo.Clone and Hostinfo.Equal.
|
||||
}
|
||||
|
||||
// NetInfo contains information about the host's network state.
|
||||
@@ -366,7 +350,7 @@ type NetInfo struct {
|
||||
// the control plane.
|
||||
DERPLatency map[string]float64 `json:",omitempty"`
|
||||
|
||||
// Update BasicallyEqual when adding fields.
|
||||
// Update Clone and BasicallyEqual when adding fields.
|
||||
}
|
||||
|
||||
func (ni *NetInfo) String() string {
|
||||
@@ -491,9 +475,11 @@ type MapRequest struct {
|
||||
// we want to signal to the control server that we're capable of something
|
||||
// different.
|
||||
//
|
||||
// For current values and history, see CurrentMapRequestVersion above.
|
||||
Version int
|
||||
|
||||
// History of versions:
|
||||
// 3: implicit compression, keep-alives
|
||||
// 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
|
||||
// 5: 2020-10-19, implies IncludeIPv6, DeltaPeers/DeltaUserProfiles, supports MagicDNS
|
||||
Version int
|
||||
Compress string // "zstd" or "" (no compression)
|
||||
KeepAlive bool // whether server should send keep-alives back to us
|
||||
NodeKey NodeKey
|
||||
@@ -515,10 +501,6 @@ type MapRequest struct {
|
||||
// OmitPeers is whether the client is okay with the Peers list
|
||||
// being omitted in the response. (For example, a client on
|
||||
// start up using ReadOnly to get the DERP map.)
|
||||
//
|
||||
// If OmitPeers is true, Stream is false, and ReadOnly is false,
|
||||
// then the server will let clients update their endpoints without
|
||||
// breaking existing long-polling (Stream == true) connections.
|
||||
OmitPeers bool `json:",omitempty"`
|
||||
|
||||
// DebugFlags is a list of strings specifying debugging and
|
||||
@@ -531,8 +513,8 @@ type MapRequest struct {
|
||||
// Current DebugFlags values are:
|
||||
// * "warn-ip-forwarding-off": client is trying to be a subnet
|
||||
// router but their IP forwarding is broken.
|
||||
// * "minimize-netmap": have control minimize the netmap, removing
|
||||
// peers that are unreachable per ACLS.
|
||||
// * "v6-overlay": IPv6 development flag to have control send
|
||||
// v6 node addrs
|
||||
DebugFlags []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -544,44 +526,18 @@ type PortRange struct {
|
||||
|
||||
var PortRangeAny = PortRange{0, 65535}
|
||||
|
||||
// NetPortRange represents a range of ports that's allowed for one or more IPs.
|
||||
// NetPortRange represents a single subnet:portrange.
|
||||
type NetPortRange struct {
|
||||
_ structs.Incomparable
|
||||
IP string // IP, CIDR, Range, or "*" (same formats as FilterRule.SrcIPs)
|
||||
Bits *int // deprecated; the old way to turn IP into a CIDR
|
||||
IP string // "*" means all
|
||||
Bits *int // backward compatibility: if missing, means "all" bits
|
||||
Ports PortRange
|
||||
}
|
||||
|
||||
// FilterRule represents one rule in a packet filter.
|
||||
//
|
||||
// A rule is logically a set of source CIDRs to match (described by
|
||||
// SrcIPs and SrcBits), and a set of destination targets that are then
|
||||
// allowed if a source IP is mathces of those CIDRs.
|
||||
type FilterRule struct {
|
||||
// SrcIPs are the source IPs/networks to match.
|
||||
//
|
||||
// It may take the following forms:
|
||||
// * an IP address (IPv4 or IPv6)
|
||||
// * the string "*" to match everything (both IPv4 & IPv6)
|
||||
// * a CIDR (e.g. "192.168.0.0/16")
|
||||
// * a range of two IPs, inclusive, separated by hyphen ("2eff::1-2eff::0800")
|
||||
SrcIPs []string
|
||||
|
||||
// SrcBits is deprecated; it's the old way to specify a CIDR
|
||||
// prior to MapRequest.Version 7. Its values correspond to the
|
||||
// SrcIPs above.
|
||||
//
|
||||
// If an entry of SrcBits is present for the same index as a
|
||||
// SrcIPs entry, it changes the SrcIP above to be a network
|
||||
// with /n CIDR bits. If the slice is nil or insufficiently
|
||||
// long, the default value (for an IPv4 address) for a
|
||||
// position is 32, as if the SrcIPs above were a /32 mask. For
|
||||
// a "*" SrcIPs value, the corresponding SrcBits value is
|
||||
// ignored.
|
||||
SrcBits []int `json:",omitempty"`
|
||||
|
||||
// DstPorts are the port ranges to allow once a source IP
|
||||
// matches (is in the CIDR described by SrcIPs & SrcBits).
|
||||
SrcIPs []string // "*" means all
|
||||
SrcBits []int
|
||||
DstPorts []NetPortRange
|
||||
}
|
||||
|
||||
@@ -622,15 +578,14 @@ type MapResponse struct {
|
||||
|
||||
// Peers, if non-empty, is the complete list of peers.
|
||||
// It will be set in the first MapResponse for a long-polled request/response.
|
||||
// Subsequent responses will be delta-encoded if MapRequest.Version >= 5 and server
|
||||
// chooses, in which case Peers will be nil or zero length.
|
||||
// Subsequent responses will be delta-encoded if DeltaPeers was set in the request.
|
||||
// If Peers is non-empty, PeersChanged and PeersRemoved should
|
||||
// be ignored (and should be empty).
|
||||
// Peers is always returned sorted by Node.ID.
|
||||
Peers []*Node `json:",omitempty"`
|
||||
// PeersChanged are the Nodes (identified by their ID) that
|
||||
// have changed or been added since the past update on the
|
||||
// HTTP response. It's not used by the server if MapRequest.Version < 5.
|
||||
// HTTP response. It's only set if MapRequest.DeltaPeers was true.
|
||||
// PeersChanged is always returned sorted by Node.ID.
|
||||
PeersChanged []*Node `json:",omitempty"`
|
||||
// PeersRemoved are the NodeIDs that are no longer in the peer list.
|
||||
@@ -639,47 +594,18 @@ type MapResponse struct {
|
||||
// DNS is the same as DNSConfig.Nameservers.
|
||||
//
|
||||
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
|
||||
DNS []netaddr.IP `json:",omitempty"`
|
||||
|
||||
// SearchPaths is the old way to specify DNS search
|
||||
// domains. Clients should use these values if set, but the
|
||||
// server will omit this field for clients with
|
||||
// MapRequest.Version >= 9. Clients should prefer to use
|
||||
// DNSConfig.Domains instead.
|
||||
SearchPaths []string `json:",omitempty"`
|
||||
|
||||
// DNSConfig contains the DNS settings for the client to use.
|
||||
DNS []wgcfg.IP `json:",omitempty"`
|
||||
// SearchPaths are the same as DNSConfig.Domains.
|
||||
//
|
||||
// TODO(bradfitz): make this a pointer and conditionally sent
|
||||
// only if changed, like DERPMap, PacketFilter, etc. It's
|
||||
// small, though.
|
||||
DNSConfig DNSConfig `json:",omitempty"`
|
||||
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
|
||||
SearchPaths []string `json:",omitempty"`
|
||||
DNSConfig DNSConfig `json:",omitempty"`
|
||||
|
||||
// Domain is the name of the network that this node is
|
||||
// in. It's either of the form "example.com" (for user
|
||||
// foo@example.com, for multi-user networks) or
|
||||
// "foo@gmail.com" (for siloed users on shared email
|
||||
// providers). Its exact form should not be depended on; new
|
||||
// forms are coming later.
|
||||
Domain string
|
||||
|
||||
// CollectServices reports whether this node's Tailnet has
|
||||
// requested that info about services be included in HostInfo.
|
||||
// If unset, the most recent non-empty MapResponse value in
|
||||
// the HTTP response stream is used.
|
||||
CollectServices opt.Bool `json:",omitempty"`
|
||||
|
||||
// PacketFilter are the firewall rules.
|
||||
//
|
||||
// For MapRequest.Version >= 6, a nil value means the most
|
||||
// previously streamed non-nil MapResponse.PacketFilter within
|
||||
// the same HTTP response. A non-nil but empty list always means
|
||||
// no PacketFilter (that is, to block everything).
|
||||
// ACLs
|
||||
Domain string
|
||||
PacketFilter []FilterRule
|
||||
|
||||
UserProfiles []UserProfile // as of 1.1.541 (mapver 5): may be new or updated user profiles only
|
||||
UserProfiles []UserProfile // as of 1.1.541: may be new or updated user profiles only
|
||||
Roles []Role // deprecated; clients should not rely on Roles
|
||||
|
||||
// TODO: Groups []Group
|
||||
// TODO: Capabilities []Capability
|
||||
|
||||
@@ -715,10 +641,6 @@ type Debug struct {
|
||||
// TrimWGConfig controls whether Tailscale does lazy, on-demand
|
||||
// wireguard configuration of peers.
|
||||
TrimWGConfig opt.Bool `json:",omitempty"`
|
||||
|
||||
// DisableSubnetsIfPAC controls whether subnet routers should be
|
||||
// disabled if WPAD is present on the network.
|
||||
DisableSubnetsIfPAC opt.Bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
|
||||
@@ -781,7 +703,6 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
n.ID == n2.ID &&
|
||||
n.Name == n2.Name &&
|
||||
n.User == n2.User &&
|
||||
n.Sharer == n2.Sharer &&
|
||||
n.Key == n2.Key &&
|
||||
n.KeyExpiry.Equal(n2.KeyExpiry) &&
|
||||
n.Machine == n2.Machine &&
|
||||
@@ -808,7 +729,7 @@ func eqStrings(a, b []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func eqCIDRs(a, b []netaddr.IPPrefix) bool {
|
||||
func eqCIDRs(a, b []wgcfg.CIDR) bool {
|
||||
if len(a) != len(b) || ((a == nil) != (b == nil)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package tailcfg
|
||||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
@@ -64,13 +65,12 @@ var _NodeNeedsRegeneration = Node(struct {
|
||||
ID NodeID
|
||||
Name string
|
||||
User UserID
|
||||
Sharer UserID
|
||||
Key NodeKey
|
||||
KeyExpiry time.Time
|
||||
Machine MachineKey
|
||||
DiscoKey DiscoKey
|
||||
Addresses []netaddr.IPPrefix
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Addresses []wgcfg.CIDR
|
||||
AllowedIPs []wgcfg.CIDR
|
||||
Endpoints []string
|
||||
DERP string
|
||||
Hostinfo Hostinfo
|
||||
@@ -105,10 +105,8 @@ var _HostinfoNeedsRegeneration = Hostinfo(struct {
|
||||
OSVersion string
|
||||
DeviceModel string
|
||||
Hostname string
|
||||
ShieldsUp bool
|
||||
ShareeNode bool
|
||||
GoArch string
|
||||
RoutableIPs []netaddr.IPPrefix
|
||||
RoutableIPs []wgcfg.CIDR
|
||||
RequestTags []string
|
||||
Services []Service
|
||||
NetInfo *NetInfo
|
||||
|
||||
@@ -11,8 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/wgkey"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
@@ -24,21 +23,18 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
|
||||
func TestHostinfoEqual(t *testing.T) {
|
||||
hiHandles := []string{
|
||||
"IPNVersion", "FrontendLogID", "BackendLogID",
|
||||
"OS", "OSVersion", "DeviceModel", "Hostname",
|
||||
"ShieldsUp", "ShareeNode",
|
||||
"GoArch",
|
||||
"RoutableIPs", "RequestTags",
|
||||
"Services", "NetInfo",
|
||||
"IPNVersion", "FrontendLogID", "BackendLogID", "OS", "OSVersion",
|
||||
"DeviceModel", "Hostname", "GoArch", "RoutableIPs", "RequestTags", "Services",
|
||||
"NetInfo",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
|
||||
t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, hiHandles)
|
||||
}
|
||||
|
||||
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
|
||||
nets := func(strs ...string) (ns []wgcfg.CIDR) {
|
||||
for _, s := range strs {
|
||||
n, err := netaddr.ParseIPPrefix(s)
|
||||
n, err := wgcfg.ParseCIDR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -173,11 +169,6 @@ func TestHostinfoEqual(t *testing.T) {
|
||||
&Hostinfo{Services: []Service{Service{Proto: TCP, Port: 1234, Description: "foo"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Hostinfo{ShareeNode: true},
|
||||
&Hostinfo{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.a.Equal(tt.b)
|
||||
@@ -188,20 +179,15 @@ func TestHostinfoEqual(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNodeEqual(t *testing.T) {
|
||||
nodeHandles := []string{
|
||||
"ID", "Name", "User", "Sharer",
|
||||
"Key", "KeyExpiry", "Machine", "DiscoKey",
|
||||
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
||||
"Created", "LastSeen", "KeepAlive", "MachineAuthorized",
|
||||
}
|
||||
nodeHandles := []string{"ID", "Name", "User", "Key", "KeyExpiry", "Machine", "DiscoKey", "Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo", "Created", "LastSeen", "KeepAlive", "MachineAuthorized"}
|
||||
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",
|
||||
have, nodeHandles)
|
||||
}
|
||||
|
||||
newPublicKey := func(t *testing.T) wgkey.Key {
|
||||
newPublicKey := func(t *testing.T) wgcfg.Key {
|
||||
t.Helper()
|
||||
k, err := wgkey.NewPrivate()
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -270,23 +256,23 @@ func TestNodeEqual(t *testing.T) {
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
&Node{Addresses: nil},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{AllowedIPs: []netaddr.IPPrefix{}},
|
||||
&Node{AllowedIPs: []wgcfg.CIDR{}},
|
||||
&Node{AllowedIPs: nil},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
@@ -435,8 +421,8 @@ func TestCloneNode(t *testing.T) {
|
||||
}{
|
||||
{"nil_fields", &Node{}},
|
||||
{"zero_fields", &Node{
|
||||
Addresses: make([]netaddr.IPPrefix, 0),
|
||||
AllowedIPs: make([]netaddr.IPPrefix, 0),
|
||||
Addresses: make([]wgcfg.CIDR, 0),
|
||||
AllowedIPs: make([]wgcfg.CIDR, 0),
|
||||
Endpoints: make([]string, 0),
|
||||
}},
|
||||
}
|
||||
|
||||
47
tempfork/osexec/README.md
Normal file
47
tempfork/osexec/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
This is a temporary fork of Go 1.13's os/exec package,
|
||||
to work around https://github.com/golang/go/issues/36644.
|
||||
|
||||
The main modification (outside of removing some tests that require
|
||||
internal-only packages to run) is:
|
||||
|
||||
```
|
||||
commit 3c66be240f1ee1f1b5f03bed79eb0d9f8c08965a
|
||||
Author: Avery Pennarun <apenwarr@gmail.com>
|
||||
Date: Sun Jan 19 03:17:30 2020 -0500
|
||||
|
||||
Cmd.Wait(): handle EINTR return code from os.Process.Wait().
|
||||
|
||||
This is probably not actually the correct fix; most likely
|
||||
os.Process.Wait() itself should be fixed to retry on EINTR so that it
|
||||
never leaks out of that function. But if we're going to patch a
|
||||
particular module, it's safer to patch a higher-level one like os/exec
|
||||
rather than the os module itself.
|
||||
|
||||
diff --git a/exec.go b/exec.go
|
||||
index 17ef003e..5375e673 100644
|
||||
--- a/exec.go
|
||||
+++ b/exec.go
|
||||
@@ -498,7 +498,21 @@ func (c *Cmd) Wait() error {
|
||||
}
|
||||
c.finished = true
|
||||
|
||||
- state, err := c.Process.Wait()
|
||||
+ var err error
|
||||
+ var state *os.ProcessState
|
||||
+ for {
|
||||
+ state, err = c.Process.Wait()
|
||||
+ if err != nil {
|
||||
+ xe, ok := err.(*os.SyscallError)
|
||||
+ if ok {
|
||||
+ if xe.Unwrap() == syscall.EINTR {
|
||||
+ // temporary error, retry wait syscall
|
||||
+ continue
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ break
|
||||
+ }
|
||||
if c.waitDone != nil {
|
||||
close(c.waitDone)
|
||||
}
|
||||
```
|
||||
23
tempfork/osexec/bench_test.go
Normal file
23
tempfork/osexec/bench_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2019 The Go 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 exec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkExecHostname(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
path, err := LookPath("hostname")
|
||||
if err != nil {
|
||||
b.Fatalf("could not find hostname: %v", err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := Command(path).Run(); err != nil {
|
||||
b.Fatalf("hostname: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
tempfork/osexec/env_test.go
Normal file
39
tempfork/osexec/env_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2017 The Go 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 exec
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDedupEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
noCase bool
|
||||
in []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
noCase: true,
|
||||
in: []string{"k1=v1", "k2=v2", "K1=v3"},
|
||||
want: []string{"K1=v3", "k2=v2"},
|
||||
},
|
||||
{
|
||||
noCase: false,
|
||||
in: []string{"k1=v1", "K1=V2", "k1=v3"},
|
||||
want: []string{"k1=v3", "K1=V2"},
|
||||
},
|
||||
{
|
||||
in: []string{"=a", "=b", "foo", "bar"},
|
||||
want: []string{"=b", "foo", "bar"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := dedupEnvCase(tt.noCase, tt.in)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Dedup(%v, %q) = %q; want %q", tt.noCase, tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
156
tempfork/osexec/example_test.go
Normal file
156
tempfork/osexec/example_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2012 The Go 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 exec_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExampleLookPath() {
|
||||
path, err := exec.LookPath("fortune")
|
||||
if err != nil {
|
||||
log.Fatal("installing fortune is in your future")
|
||||
}
|
||||
fmt.Printf("fortune is available at %s\n", path)
|
||||
}
|
||||
|
||||
func ExampleCommand() {
|
||||
cmd := exec.Command("tr", "a-z", "A-Z")
|
||||
cmd.Stdin = strings.NewReader("some input")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("in all caps: %q\n", out.String())
|
||||
}
|
||||
|
||||
func ExampleCommand_environment() {
|
||||
cmd := exec.Command("prog")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"FOO=duplicate_value", // ignored
|
||||
"FOO=actual_value", // this value is used
|
||||
)
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleCmd_Output() {
|
||||
out, err := exec.Command("date").Output()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("The date is %s\n", out)
|
||||
}
|
||||
|
||||
func ExampleCmd_Run() {
|
||||
cmd := exec.Command("sleep", "1")
|
||||
log.Printf("Running command and waiting for it to finish...")
|
||||
err := cmd.Run()
|
||||
log.Printf("Command finished with error: %v", err)
|
||||
}
|
||||
|
||||
func ExampleCmd_Start() {
|
||||
cmd := exec.Command("sleep", "5")
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Waiting for command to finish...")
|
||||
err = cmd.Wait()
|
||||
log.Printf("Command finished with error: %v", err)
|
||||
}
|
||||
|
||||
func ExampleCmd_StdoutPipe() {
|
||||
cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
if err := json.NewDecoder(stdout).Decode(&person); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%s is %d years old\n", person.Name, person.Age)
|
||||
}
|
||||
|
||||
func ExampleCmd_StdinPipe() {
|
||||
cmd := exec.Command("cat")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
io.WriteString(stdin, "values written to stdin are passed to cmd's standard input")
|
||||
}()
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
|
||||
func ExampleCmd_StderrPipe() {
|
||||
cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
slurp, _ := ioutil.ReadAll(stderr)
|
||||
fmt.Printf("%s\n", slurp)
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleCmd_CombinedOutput() {
|
||||
cmd := exec.Command("sh", "-c", "echo stdout; echo 1>&2 stderr")
|
||||
stdoutStderr, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%s\n", stdoutStderr)
|
||||
}
|
||||
|
||||
func ExampleCommandContext() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil {
|
||||
// This will fail after 100 milliseconds. The 5 second sleep
|
||||
// will be interrupted.
|
||||
}
|
||||
}
|
||||
797
tempfork/osexec/exec.go
Normal file
797
tempfork/osexec/exec.go
Normal file
@@ -0,0 +1,797 @@
|
||||
// Copyright 2009 The Go 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 exec runs external commands. It wraps os.StartProcess to make it
|
||||
// easier to remap stdin and stdout, connect I/O with pipes, and do other
|
||||
// adjustments.
|
||||
//
|
||||
// Unlike the "system" library call from C and other languages, the
|
||||
// os/exec package intentionally does not invoke the system shell and
|
||||
// does not expand any glob patterns or handle other expansions,
|
||||
// pipelines, or redirections typically done by shells. The package
|
||||
// behaves more like C's "exec" family of functions. To expand glob
|
||||
// patterns, either call the shell directly, taking care to escape any
|
||||
// dangerous input, or use the path/filepath package's Glob function.
|
||||
// To expand environment variables, use package os's ExpandEnv.
|
||||
//
|
||||
// Note that the examples in this package assume a Unix system.
|
||||
// They may not run on Windows, and they do not run in the Go Playground
|
||||
// used by golang.org and godoc.org.
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Error is returned by LookPath when it fails to classify a file as an
|
||||
// executable.
|
||||
type Error struct {
|
||||
// Name is the file name for which the error occurred.
|
||||
Name string
|
||||
// Err is the underlying error.
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return "exec: " + strconv.Quote(e.Name) + ": " + e.Err.Error()
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error { return e.Err }
|
||||
|
||||
// Cmd represents an external command being prepared or run.
|
||||
//
|
||||
// A Cmd cannot be reused after calling its Run, Output or CombinedOutput
|
||||
// methods.
|
||||
type Cmd struct {
|
||||
// Path is the path of the command to run.
|
||||
//
|
||||
// This is the only field that must be set to a non-zero
|
||||
// value. If Path is relative, it is evaluated relative
|
||||
// to Dir.
|
||||
Path string
|
||||
|
||||
// Args holds command line arguments, including the command as Args[0].
|
||||
// If the Args field is empty or nil, Run uses {Path}.
|
||||
//
|
||||
// In typical use, both Path and Args are set by calling Command.
|
||||
Args []string
|
||||
|
||||
// Env specifies the environment of the process.
|
||||
// Each entry is of the form "key=value".
|
||||
// If Env is nil, the new process uses the current process's
|
||||
// environment.
|
||||
// If Env contains duplicate environment keys, only the last
|
||||
// value in the slice for each duplicate key is used.
|
||||
// As a special case on Windows, SYSTEMROOT is always added if
|
||||
// missing and not explicitly set to the empty string.
|
||||
Env []string
|
||||
|
||||
// Dir specifies the working directory of the command.
|
||||
// If Dir is the empty string, Run runs the command in the
|
||||
// calling process's current directory.
|
||||
Dir string
|
||||
|
||||
// Stdin specifies the process's standard input.
|
||||
//
|
||||
// If Stdin is nil, the process reads from the null device (os.DevNull).
|
||||
//
|
||||
// If Stdin is an *os.File, the process's standard input is connected
|
||||
// directly to that file.
|
||||
//
|
||||
// Otherwise, during the execution of the command a separate
|
||||
// goroutine reads from Stdin and delivers that data to the command
|
||||
// over a pipe. In this case, Wait does not complete until the goroutine
|
||||
// stops copying, either because it has reached the end of Stdin
|
||||
// (EOF or a read error) or because writing to the pipe returned an error.
|
||||
Stdin io.Reader
|
||||
|
||||
// Stdout and Stderr specify the process's standard output and error.
|
||||
//
|
||||
// If either is nil, Run connects the corresponding file descriptor
|
||||
// to the null device (os.DevNull).
|
||||
//
|
||||
// If either is an *os.File, the corresponding output from the process
|
||||
// is connected directly to that file.
|
||||
//
|
||||
// Otherwise, during the execution of the command a separate goroutine
|
||||
// reads from the process over a pipe and delivers that data to the
|
||||
// corresponding Writer. In this case, Wait does not complete until the
|
||||
// goroutine reaches EOF or encounters an error.
|
||||
//
|
||||
// If Stdout and Stderr are the same writer, and have a type that can
|
||||
// be compared with ==, at most one goroutine at a time will call Write.
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
// ExtraFiles specifies additional open files to be inherited by the
|
||||
// new process. It does not include standard input, standard output, or
|
||||
// standard error. If non-nil, entry i becomes file descriptor 3+i.
|
||||
//
|
||||
// ExtraFiles is not supported on Windows.
|
||||
ExtraFiles []*os.File
|
||||
|
||||
// SysProcAttr holds optional, operating system-specific attributes.
|
||||
// Run passes it to os.StartProcess as the os.ProcAttr's Sys field.
|
||||
SysProcAttr *syscall.SysProcAttr
|
||||
|
||||
// Process is the underlying process, once started.
|
||||
Process *os.Process
|
||||
|
||||
// ProcessState contains information about an exited process,
|
||||
// available after a call to Wait or Run.
|
||||
ProcessState *os.ProcessState
|
||||
|
||||
ctx context.Context // nil means none
|
||||
lookPathErr error // LookPath error, if any.
|
||||
finished bool // when Wait was called
|
||||
childFiles []*os.File
|
||||
closeAfterStart []io.Closer
|
||||
closeAfterWait []io.Closer
|
||||
goroutine []func() error
|
||||
errch chan error // one send per goroutine
|
||||
waitDone chan struct{}
|
||||
}
|
||||
|
||||
// Command returns the Cmd struct to execute the named program with
|
||||
// the given arguments.
|
||||
//
|
||||
// It sets only the Path and Args in the returned structure.
|
||||
//
|
||||
// If name contains no path separators, Command uses LookPath to
|
||||
// resolve name to a complete path if possible. Otherwise it uses name
|
||||
// directly as Path.
|
||||
//
|
||||
// The returned Cmd's Args field is constructed from the command name
|
||||
// followed by the elements of arg, so arg should not include the
|
||||
// command name itself. For example, Command("echo", "hello").
|
||||
// Args[0] is always name, not the possibly resolved Path.
|
||||
//
|
||||
// On Windows, processes receive the whole command line as a single string
|
||||
// and do their own parsing. Command combines and quotes Args into a command
|
||||
// line string with an algorithm compatible with applications using
|
||||
// CommandLineToArgvW (which is the most common way). Notable exceptions are
|
||||
// msiexec.exe and cmd.exe (and thus, all batch files), which have a different
|
||||
// unquoting algorithm. In these or other similar cases, you can do the
|
||||
// quoting yourself and provide the full command line in SysProcAttr.CmdLine,
|
||||
// leaving Args empty.
|
||||
func Command(name string, arg ...string) *Cmd {
|
||||
cmd := &Cmd{
|
||||
Path: name,
|
||||
Args: append([]string{name}, arg...),
|
||||
}
|
||||
if filepath.Base(name) == name {
|
||||
if lp, err := LookPath(name); err != nil {
|
||||
cmd.lookPathErr = err
|
||||
} else {
|
||||
cmd.Path = lp
|
||||
}
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CommandContext is like Command but includes a context.
|
||||
//
|
||||
// The provided context is used to kill the process (by calling
|
||||
// os.Process.Kill) if the context becomes done before the command
|
||||
// completes on its own.
|
||||
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd {
|
||||
if ctx == nil {
|
||||
panic("nil Context")
|
||||
}
|
||||
cmd := Command(name, arg...)
|
||||
cmd.ctx = ctx
|
||||
return cmd
|
||||
}
|
||||
|
||||
// String returns a human-readable description of c.
|
||||
// It is intended only for debugging.
|
||||
// In particular, it is not suitable for use as input to a shell.
|
||||
// The output of String may vary across Go releases.
|
||||
func (c *Cmd) String() string {
|
||||
if c.lookPathErr != nil {
|
||||
// failed to resolve path; report the original requested path (plus args)
|
||||
return strings.Join(c.Args, " ")
|
||||
}
|
||||
// report the exact executable path (plus args)
|
||||
b := new(strings.Builder)
|
||||
b.WriteString(c.Path)
|
||||
for _, a := range c.Args[1:] {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// interfaceEqual protects against panics from doing equality tests on
|
||||
// two interfaces with non-comparable underlying types.
|
||||
func interfaceEqual(a, b interface{}) bool {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
return a == b
|
||||
}
|
||||
|
||||
func (c *Cmd) envv() []string {
|
||||
if c.Env != nil {
|
||||
return c.Env
|
||||
}
|
||||
return os.Environ()
|
||||
}
|
||||
|
||||
func (c *Cmd) argv() []string {
|
||||
if len(c.Args) > 0 {
|
||||
return c.Args
|
||||
}
|
||||
return []string{c.Path}
|
||||
}
|
||||
|
||||
// skipStdinCopyError optionally specifies a function which reports
|
||||
// whether the provided stdin copy error should be ignored.
|
||||
// It is non-nil everywhere but Plan 9, which lacks EPIPE. See exec_posix.go.
|
||||
var skipStdinCopyError func(error) bool
|
||||
|
||||
func (c *Cmd) stdin() (f *os.File, err error) {
|
||||
if c.Stdin == nil {
|
||||
f, err = os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.closeAfterStart = append(c.closeAfterStart, f)
|
||||
return
|
||||
}
|
||||
|
||||
if f, ok := c.Stdin.(*os.File); ok {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.closeAfterStart = append(c.closeAfterStart, pr)
|
||||
c.closeAfterWait = append(c.closeAfterWait, pw)
|
||||
c.goroutine = append(c.goroutine, func() error {
|
||||
_, err := io.Copy(pw, c.Stdin)
|
||||
if skip := skipStdinCopyError; skip != nil && skip(err) {
|
||||
err = nil
|
||||
}
|
||||
if err1 := pw.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
})
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
func (c *Cmd) stdout() (f *os.File, err error) {
|
||||
return c.writerDescriptor(c.Stdout)
|
||||
}
|
||||
|
||||
func (c *Cmd) stderr() (f *os.File, err error) {
|
||||
if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) {
|
||||
return c.childFiles[1], nil
|
||||
}
|
||||
return c.writerDescriptor(c.Stderr)
|
||||
}
|
||||
|
||||
func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
|
||||
if w == nil {
|
||||
f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.closeAfterStart = append(c.closeAfterStart, f)
|
||||
return
|
||||
}
|
||||
|
||||
if f, ok := w.(*os.File); ok {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.closeAfterStart = append(c.closeAfterStart, pw)
|
||||
c.closeAfterWait = append(c.closeAfterWait, pr)
|
||||
c.goroutine = append(c.goroutine, func() error {
|
||||
_, err := io.Copy(w, pr)
|
||||
pr.Close() // in case io.Copy stopped due to write error
|
||||
return err
|
||||
})
|
||||
return pw, nil
|
||||
}
|
||||
|
||||
func (c *Cmd) closeDescriptors(closers []io.Closer) {
|
||||
for _, fd := range closers {
|
||||
fd.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the specified command and waits for it to complete.
|
||||
//
|
||||
// The returned error is nil if the command runs, has no problems
|
||||
// copying stdin, stdout, and stderr, and exits with a zero exit
|
||||
// status.
|
||||
//
|
||||
// If the command starts but does not complete successfully, the error is of
|
||||
// type *ExitError. Other error types may be returned for other situations.
|
||||
//
|
||||
// If the calling goroutine has locked the operating system thread
|
||||
// with runtime.LockOSThread and modified any inheritable OS-level
|
||||
// thread state (for example, Linux or Plan 9 name spaces), the new
|
||||
// process will inherit the caller's thread state.
|
||||
func (c *Cmd) Run() error {
|
||||
if err := c.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Wait()
|
||||
}
|
||||
|
||||
// lookExtensions finds windows executable by its dir and path.
|
||||
// It uses LookPath to try appropriate extensions.
|
||||
// lookExtensions does not search PATH, instead it converts `prog` into `.\prog`.
|
||||
func lookExtensions(path, dir string) (string, error) {
|
||||
if filepath.Base(path) == path {
|
||||
path = filepath.Join(".", path)
|
||||
}
|
||||
if dir == "" {
|
||||
return LookPath(path)
|
||||
}
|
||||
if filepath.VolumeName(path) != "" {
|
||||
return LookPath(path)
|
||||
}
|
||||
if len(path) > 1 && os.IsPathSeparator(path[0]) {
|
||||
return LookPath(path)
|
||||
}
|
||||
dirandpath := filepath.Join(dir, path)
|
||||
// We assume that LookPath will only add file extension.
|
||||
lp, err := LookPath(dirandpath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ext := strings.TrimPrefix(lp, dirandpath)
|
||||
return path + ext, nil
|
||||
}
|
||||
|
||||
// Start starts the specified command but does not wait for it to complete.
|
||||
//
|
||||
// The Wait method will return the exit code and release associated resources
|
||||
// once the command exits.
|
||||
func (c *Cmd) Start() error {
|
||||
if c.lookPathErr != nil {
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return c.lookPathErr
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
lp, err := lookExtensions(c.Path, c.Dir)
|
||||
if err != nil {
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return err
|
||||
}
|
||||
c.Path = lp
|
||||
}
|
||||
if c.Process != nil {
|
||||
return errors.New("exec: already started")
|
||||
}
|
||||
if c.ctx != nil {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return c.ctx.Err()
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
c.childFiles = make([]*os.File, 0, 3+len(c.ExtraFiles))
|
||||
type F func(*Cmd) (*os.File, error)
|
||||
for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr} {
|
||||
fd, err := setupFd(c)
|
||||
if err != nil {
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return err
|
||||
}
|
||||
c.childFiles = append(c.childFiles, fd)
|
||||
}
|
||||
c.childFiles = append(c.childFiles, c.ExtraFiles...)
|
||||
|
||||
var err error
|
||||
c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{
|
||||
Dir: c.Dir,
|
||||
Files: c.childFiles,
|
||||
Env: addCriticalEnv(dedupEnv(c.envv())),
|
||||
Sys: c.SysProcAttr,
|
||||
})
|
||||
if err != nil {
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return err
|
||||
}
|
||||
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
|
||||
// Don't allocate the channel unless there are goroutines to fire.
|
||||
if len(c.goroutine) > 0 {
|
||||
c.errch = make(chan error, len(c.goroutine))
|
||||
for _, fn := range c.goroutine {
|
||||
go func(fn func() error) {
|
||||
c.errch <- fn()
|
||||
}(fn)
|
||||
}
|
||||
}
|
||||
|
||||
if c.ctx != nil {
|
||||
c.waitDone = make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
c.Process.Kill()
|
||||
case <-c.waitDone:
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// An ExitError reports an unsuccessful exit by a command.
|
||||
type ExitError struct {
|
||||
*os.ProcessState
|
||||
|
||||
// Stderr holds a subset of the standard error output from the
|
||||
// Cmd.Output method if standard error was not otherwise being
|
||||
// collected.
|
||||
//
|
||||
// If the error output is long, Stderr may contain only a prefix
|
||||
// and suffix of the output, with the middle replaced with
|
||||
// text about the number of omitted bytes.
|
||||
//
|
||||
// Stderr is provided for debugging, for inclusion in error messages.
|
||||
// Users with other needs should redirect Cmd.Stderr as needed.
|
||||
Stderr []byte
|
||||
}
|
||||
|
||||
func (e *ExitError) Error() string {
|
||||
return e.ProcessState.String()
|
||||
}
|
||||
|
||||
// Wait waits for the command to exit and waits for any copying to
|
||||
// stdin or copying from stdout or stderr to complete.
|
||||
//
|
||||
// The command must have been started by Start.
|
||||
//
|
||||
// The returned error is nil if the command runs, has no problems
|
||||
// copying stdin, stdout, and stderr, and exits with a zero exit
|
||||
// status.
|
||||
//
|
||||
// If the command fails to run or doesn't complete successfully, the
|
||||
// error is of type *ExitError. Other error types may be
|
||||
// returned for I/O problems.
|
||||
//
|
||||
// If any of c.Stdin, c.Stdout or c.Stderr are not an *os.File, Wait also waits
|
||||
// for the respective I/O loop copying to or from the process to complete.
|
||||
//
|
||||
// Wait releases any resources associated with the Cmd.
|
||||
func (c *Cmd) Wait() error {
|
||||
if c.Process == nil {
|
||||
return errors.New("exec: not started")
|
||||
}
|
||||
if c.finished {
|
||||
return errors.New("exec: Wait was already called")
|
||||
}
|
||||
c.finished = true
|
||||
|
||||
var err error
|
||||
var state *os.ProcessState
|
||||
for {
|
||||
state, err = c.Process.Wait()
|
||||
if err != nil {
|
||||
xe, ok := err.(*os.SyscallError)
|
||||
if ok {
|
||||
if xe.Unwrap() == syscall.EINTR {
|
||||
// temporary error, retry wait syscall
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if c.waitDone != nil {
|
||||
close(c.waitDone)
|
||||
}
|
||||
c.ProcessState = state
|
||||
|
||||
var copyError error
|
||||
for range c.goroutine {
|
||||
if err := <-c.errch; err != nil && copyError == nil {
|
||||
copyError = err
|
||||
}
|
||||
}
|
||||
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !state.Success() {
|
||||
return &ExitError{ProcessState: state}
|
||||
}
|
||||
|
||||
return copyError
|
||||
}
|
||||
|
||||
// Output runs the command and returns its standard output.
|
||||
// Any returned error will usually be of type *ExitError.
|
||||
// If c.Stderr was nil, Output populates ExitError.Stderr.
|
||||
func (c *Cmd) Output() ([]byte, error) {
|
||||
if c.Stdout != nil {
|
||||
return nil, errors.New("exec: Stdout already set")
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
c.Stdout = &stdout
|
||||
|
||||
captureErr := c.Stderr == nil
|
||||
if captureErr {
|
||||
c.Stderr = &prefixSuffixSaver{N: 32 << 10}
|
||||
}
|
||||
|
||||
err := c.Run()
|
||||
if err != nil && captureErr {
|
||||
if ee, ok := err.(*ExitError); ok {
|
||||
ee.Stderr = c.Stderr.(*prefixSuffixSaver).Bytes()
|
||||
}
|
||||
}
|
||||
return stdout.Bytes(), err
|
||||
}
|
||||
|
||||
// CombinedOutput runs the command and returns its combined standard
|
||||
// output and standard error.
|
||||
func (c *Cmd) CombinedOutput() ([]byte, error) {
|
||||
if c.Stdout != nil {
|
||||
return nil, errors.New("exec: Stdout already set")
|
||||
}
|
||||
if c.Stderr != nil {
|
||||
return nil, errors.New("exec: Stderr already set")
|
||||
}
|
||||
var b bytes.Buffer
|
||||
c.Stdout = &b
|
||||
c.Stderr = &b
|
||||
err := c.Run()
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
// StdinPipe returns a pipe that will be connected to the command's
|
||||
// standard input when the command starts.
|
||||
// The pipe will be closed automatically after Wait sees the command exit.
|
||||
// A caller need only call Close to force the pipe to close sooner.
|
||||
// For example, if the command being run will not exit until standard input
|
||||
// is closed, the caller must close the pipe.
|
||||
func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
|
||||
if c.Stdin != nil {
|
||||
return nil, errors.New("exec: Stdin already set")
|
||||
}
|
||||
if c.Process != nil {
|
||||
return nil, errors.New("exec: StdinPipe after process started")
|
||||
}
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Stdin = pr
|
||||
c.closeAfterStart = append(c.closeAfterStart, pr)
|
||||
wc := &closeOnce{File: pw}
|
||||
c.closeAfterWait = append(c.closeAfterWait, wc)
|
||||
return wc, nil
|
||||
}
|
||||
|
||||
type closeOnce struct {
|
||||
*os.File
|
||||
|
||||
once sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *closeOnce) Close() error {
|
||||
c.once.Do(c.close)
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *closeOnce) close() {
|
||||
c.err = c.File.Close()
|
||||
}
|
||||
|
||||
// StdoutPipe returns a pipe that will be connected to the command's
|
||||
// standard output when the command starts.
|
||||
//
|
||||
// Wait will close the pipe after seeing the command exit, so most callers
|
||||
// need not close the pipe themselves; however, an implication is that
|
||||
// it is incorrect to call Wait before all reads from the pipe have completed.
|
||||
// For the same reason, it is incorrect to call Run when using StdoutPipe.
|
||||
// See the example for idiomatic usage.
|
||||
func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
|
||||
if c.Stdout != nil {
|
||||
return nil, errors.New("exec: Stdout already set")
|
||||
}
|
||||
if c.Process != nil {
|
||||
return nil, errors.New("exec: StdoutPipe after process started")
|
||||
}
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Stdout = pw
|
||||
c.closeAfterStart = append(c.closeAfterStart, pw)
|
||||
c.closeAfterWait = append(c.closeAfterWait, pr)
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// StderrPipe returns a pipe that will be connected to the command's
|
||||
// standard error when the command starts.
|
||||
//
|
||||
// Wait will close the pipe after seeing the command exit, so most callers
|
||||
// need not close the pipe themselves; however, an implication is that
|
||||
// it is incorrect to call Wait before all reads from the pipe have completed.
|
||||
// For the same reason, it is incorrect to use Run when using StderrPipe.
|
||||
// See the StdoutPipe example for idiomatic usage.
|
||||
func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
|
||||
if c.Stderr != nil {
|
||||
return nil, errors.New("exec: Stderr already set")
|
||||
}
|
||||
if c.Process != nil {
|
||||
return nil, errors.New("exec: StderrPipe after process started")
|
||||
}
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Stderr = pw
|
||||
c.closeAfterStart = append(c.closeAfterStart, pw)
|
||||
c.closeAfterWait = append(c.closeAfterWait, pr)
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// prefixSuffixSaver is an io.Writer which retains the first N bytes
|
||||
// and the last N bytes written to it. The Bytes() methods reconstructs
|
||||
// it with a pretty error message.
|
||||
type prefixSuffixSaver struct {
|
||||
N int // max size of prefix or suffix
|
||||
prefix []byte
|
||||
suffix []byte // ring buffer once len(suffix) == N
|
||||
suffixOff int // offset to write into suffix
|
||||
skipped int64
|
||||
|
||||
// TODO(bradfitz): we could keep one large []byte and use part of it for
|
||||
// the prefix, reserve space for the '... Omitting N bytes ...' message,
|
||||
// then the ring buffer suffix, and just rearrange the ring buffer
|
||||
// suffix when Bytes() is called, but it doesn't seem worth it for
|
||||
// now just for error messages. It's only ~64KB anyway.
|
||||
}
|
||||
|
||||
func (w *prefixSuffixSaver) Write(p []byte) (n int, err error) {
|
||||
lenp := len(p)
|
||||
p = w.fill(&w.prefix, p)
|
||||
|
||||
// Only keep the last w.N bytes of suffix data.
|
||||
if overage := len(p) - w.N; overage > 0 {
|
||||
p = p[overage:]
|
||||
w.skipped += int64(overage)
|
||||
}
|
||||
p = w.fill(&w.suffix, p)
|
||||
|
||||
// w.suffix is full now if p is non-empty. Overwrite it in a circle.
|
||||
for len(p) > 0 { // 0, 1, or 2 iterations.
|
||||
n := copy(w.suffix[w.suffixOff:], p)
|
||||
p = p[n:]
|
||||
w.skipped += int64(n)
|
||||
w.suffixOff += n
|
||||
if w.suffixOff == w.N {
|
||||
w.suffixOff = 0
|
||||
}
|
||||
}
|
||||
return lenp, nil
|
||||
}
|
||||
|
||||
// fill appends up to len(p) bytes of p to *dst, such that *dst does not
|
||||
// grow larger than w.N. It returns the un-appended suffix of p.
|
||||
func (w *prefixSuffixSaver) fill(dst *[]byte, p []byte) (pRemain []byte) {
|
||||
if remain := w.N - len(*dst); remain > 0 {
|
||||
add := minInt(len(p), remain)
|
||||
*dst = append(*dst, p[:add]...)
|
||||
p = p[add:]
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (w *prefixSuffixSaver) Bytes() []byte {
|
||||
if w.suffix == nil {
|
||||
return w.prefix
|
||||
}
|
||||
if w.skipped == 0 {
|
||||
return append(w.prefix, w.suffix...)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
buf.Grow(len(w.prefix) + len(w.suffix) + 50)
|
||||
buf.Write(w.prefix)
|
||||
buf.WriteString("\n... omitting ")
|
||||
buf.WriteString(strconv.FormatInt(w.skipped, 10))
|
||||
buf.WriteString(" bytes ...\n")
|
||||
buf.Write(w.suffix[w.suffixOff:])
|
||||
buf.Write(w.suffix[:w.suffixOff])
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// dedupEnv returns a copy of env with any duplicates removed, in favor of
|
||||
// later values.
|
||||
// Items not of the normal environment "key=value" form are preserved unchanged.
|
||||
func dedupEnv(env []string) []string {
|
||||
return dedupEnvCase(runtime.GOOS == "windows", env)
|
||||
}
|
||||
|
||||
// dedupEnvCase is dedupEnv with a case option for testing.
|
||||
// If caseInsensitive is true, the case of keys is ignored.
|
||||
func dedupEnvCase(caseInsensitive bool, env []string) []string {
|
||||
out := make([]string, 0, len(env))
|
||||
saw := make(map[string]int, len(env)) // key => index into out
|
||||
for _, kv := range env {
|
||||
eq := strings.Index(kv, "=")
|
||||
if eq < 0 {
|
||||
out = append(out, kv)
|
||||
continue
|
||||
}
|
||||
k := kv[:eq]
|
||||
if caseInsensitive {
|
||||
k = strings.ToLower(k)
|
||||
}
|
||||
if dupIdx, isDup := saw[k]; isDup {
|
||||
out[dupIdx] = kv
|
||||
continue
|
||||
}
|
||||
saw[k] = len(out)
|
||||
out = append(out, kv)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// addCriticalEnv adds any critical environment variables that are required
|
||||
// (or at least almost always required) on the operating system.
|
||||
// Currently this is only used for Windows.
|
||||
func addCriticalEnv(env []string) []string {
|
||||
if runtime.GOOS != "windows" {
|
||||
return env
|
||||
}
|
||||
for _, kv := range env {
|
||||
eq := strings.Index(kv, "=")
|
||||
if eq < 0 {
|
||||
continue
|
||||
}
|
||||
k := kv[:eq]
|
||||
if strings.EqualFold(k, "SYSTEMROOT") {
|
||||
// We already have it.
|
||||
return env
|
||||
}
|
||||
}
|
||||
return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT"))
|
||||
}
|
||||
24
tempfork/osexec/exec_unix.go
Normal file
24
tempfork/osexec/exec_unix.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !plan9,!windows
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
skipStdinCopyError = func(err error) bool {
|
||||
// Ignore EPIPE errors copying to stdin if the program
|
||||
// completed successfully otherwise.
|
||||
// See Issue 9173.
|
||||
pe, ok := err.(*os.PathError)
|
||||
return ok &&
|
||||
pe.Op == "write" && pe.Path == "|1" &&
|
||||
pe.Err == syscall.EPIPE
|
||||
}
|
||||
}
|
||||
23
tempfork/osexec/exec_windows.go
Normal file
23
tempfork/osexec/exec_windows.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2017 The Go 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 exec
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
skipStdinCopyError = func(err error) bool {
|
||||
// Ignore ERROR_BROKEN_PIPE and ERROR_NO_DATA errors copying
|
||||
// to stdin if the program completed successfully otherwise.
|
||||
// See Issue 20445.
|
||||
const _ERROR_NO_DATA = syscall.Errno(0xe8)
|
||||
pe, ok := err.(*os.PathError)
|
||||
return ok &&
|
||||
pe.Op == "write" && pe.Path == "|1" &&
|
||||
(pe.Err == syscall.ERROR_BROKEN_PIPE || pe.Err == _ERROR_NO_DATA)
|
||||
}
|
||||
}
|
||||
61
tempfork/osexec/internal_test.go
Normal file
61
tempfork/osexec/internal_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2015 The Go 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 exec
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrefixSuffixSaver(t *testing.T) {
|
||||
tests := []struct {
|
||||
N int
|
||||
writes []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
N: 2,
|
||||
writes: nil,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
N: 2,
|
||||
writes: []string{"a"},
|
||||
want: "a",
|
||||
},
|
||||
{
|
||||
N: 2,
|
||||
writes: []string{"abc", "d"},
|
||||
want: "abcd",
|
||||
},
|
||||
{
|
||||
N: 2,
|
||||
writes: []string{"abc", "d", "e"},
|
||||
want: "ab\n... omitting 1 bytes ...\nde",
|
||||
},
|
||||
{
|
||||
N: 2,
|
||||
writes: []string{"ab______________________yz"},
|
||||
want: "ab\n... omitting 22 bytes ...\nyz",
|
||||
},
|
||||
{
|
||||
N: 2,
|
||||
writes: []string{"ab_______________________y", "z"},
|
||||
want: "ab\n... omitting 23 bytes ...\nyz",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
w := &prefixSuffixSaver{N: tt.N}
|
||||
for _, s := range tt.writes {
|
||||
n, err := io.WriteString(w, s)
|
||||
if err != nil || n != len(s) {
|
||||
t.Errorf("%d. WriteString(%q) = %v, %v; want %v, %v", i, s, n, err, len(s), nil)
|
||||
}
|
||||
}
|
||||
if got := string(w.Bytes()); got != tt.want {
|
||||
t.Errorf("%d. Bytes = %q; want %q", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
tempfork/osexec/lp_js.go
Normal file
23
tempfork/osexec/lp_js.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build js,wasm
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// ErrNotFound is the error resulting if a path search failed to find an executable file.
|
||||
var ErrNotFound = errors.New("executable file not found in $PATH")
|
||||
|
||||
// LookPath searches for an executable named file in the
|
||||
// directories named by the PATH environment variable.
|
||||
// If file contains a slash, it is tried directly and the PATH is not consulted.
|
||||
// The result may be an absolute path or a path relative to the current directory.
|
||||
func LookPath(file string) (string, error) {
|
||||
// Wasm can not execute processes, so act as if there are no executables at all.
|
||||
return "", &Error{file, ErrNotFound}
|
||||
}
|
||||
55
tempfork/osexec/lp_plan9.go
Normal file
55
tempfork/osexec/lp_plan9.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2011 The Go 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 exec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrNotFound is the error resulting if a path search failed to find an executable file.
|
||||
var ErrNotFound = errors.New("executable file not found in $path")
|
||||
|
||||
func findExecutable(file string) error {
|
||||
d, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
|
||||
return nil
|
||||
}
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
// LookPath searches for an executable named file in the
|
||||
// directories named by the path environment variable.
|
||||
// If file begins with "/", "#", "./", or "../", it is tried
|
||||
// directly and the path is not consulted.
|
||||
// The result may be an absolute path or a path relative to the current directory.
|
||||
func LookPath(file string) (string, error) {
|
||||
// skip the path lookup for these prefixes
|
||||
skip := []string{"/", "#", "./", "../"}
|
||||
|
||||
for _, p := range skip {
|
||||
if strings.HasPrefix(file, p) {
|
||||
err := findExecutable(file)
|
||||
if err == nil {
|
||||
return file, nil
|
||||
}
|
||||
return "", &Error{file, err}
|
||||
}
|
||||
}
|
||||
|
||||
path := os.Getenv("path")
|
||||
for _, dir := range filepath.SplitList(path) {
|
||||
path := filepath.Join(dir, file)
|
||||
if err := findExecutable(path); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", &Error{file, ErrNotFound}
|
||||
}
|
||||
33
tempfork/osexec/lp_test.go
Normal file
33
tempfork/osexec/lp_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2011 The Go 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 exec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var nonExistentPaths = []string{
|
||||
"some-non-existent-path",
|
||||
"non-existent-path/slashed",
|
||||
}
|
||||
|
||||
func TestLookPathNotFound(t *testing.T) {
|
||||
for _, name := range nonExistentPaths {
|
||||
path, err := LookPath(name)
|
||||
if err == nil {
|
||||
t.Fatalf("LookPath found %q in $PATH", name)
|
||||
}
|
||||
if path != "" {
|
||||
t.Fatalf("LookPath path == %q when err != nil", path)
|
||||
}
|
||||
perr, ok := err.(*Error)
|
||||
if !ok {
|
||||
t.Fatal("LookPath error is not an exec.Error")
|
||||
}
|
||||
if perr.Name != name {
|
||||
t.Fatalf("want Error name %q, got %q", name, perr.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
tempfork/osexec/lp_unix.go
Normal file
58
tempfork/osexec/lp_unix.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build aix darwin dragonfly freebsd linux nacl netbsd openbsd solaris
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrNotFound is the error resulting if a path search failed to find an executable file.
|
||||
var ErrNotFound = errors.New("executable file not found in $PATH")
|
||||
|
||||
func findExecutable(file string) error {
|
||||
d, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
|
||||
return nil
|
||||
}
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
// LookPath searches for an executable named file in the
|
||||
// directories named by the PATH environment variable.
|
||||
// If file contains a slash, it is tried directly and the PATH is not consulted.
|
||||
// The result may be an absolute path or a path relative to the current directory.
|
||||
func LookPath(file string) (string, error) {
|
||||
// NOTE(rsc): I wish we could use the Plan 9 behavior here
|
||||
// (only bypass the path if file begins with / or ./ or ../)
|
||||
// but that would not match all the Unix shells.
|
||||
|
||||
if strings.Contains(file, "/") {
|
||||
err := findExecutable(file)
|
||||
if err == nil {
|
||||
return file, nil
|
||||
}
|
||||
return "", &Error{file, err}
|
||||
}
|
||||
path := os.Getenv("PATH")
|
||||
for _, dir := range filepath.SplitList(path) {
|
||||
if dir == "" {
|
||||
// Unix shell semantics: path element "" means "."
|
||||
dir = "."
|
||||
}
|
||||
path := filepath.Join(dir, file)
|
||||
if err := findExecutable(path); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", &Error{file, ErrNotFound}
|
||||
}
|
||||
50
tempfork/osexec/lp_unix_test.go
Normal file
50
tempfork/osexec/lp_unix_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLookPathUnixEmptyPath(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal("Getwd failed: ", err)
|
||||
}
|
||||
err = os.Chdir(tmp)
|
||||
if err != nil {
|
||||
t.Fatal("Chdir failed: ", err)
|
||||
}
|
||||
defer os.Chdir(wd)
|
||||
|
||||
f, err := os.OpenFile("exec_me", os.O_CREATE|os.O_EXCL, 0700)
|
||||
if err != nil {
|
||||
t.Fatal("OpenFile failed: ", err)
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
t.Fatal("Close failed: ", err)
|
||||
}
|
||||
|
||||
pathenv := os.Getenv("PATH")
|
||||
defer os.Setenv("PATH", pathenv)
|
||||
|
||||
err = os.Setenv("PATH", "")
|
||||
if err != nil {
|
||||
t.Fatal("Setenv failed: ", err)
|
||||
}
|
||||
|
||||
path, err := LookPath("exec_me")
|
||||
if err == nil {
|
||||
t.Fatal("LookPath found exec_me in empty $PATH")
|
||||
}
|
||||
if path != "" {
|
||||
t.Fatalf("LookPath path == %q when err != nil", path)
|
||||
}
|
||||
}
|
||||
93
tempfork/osexec/lp_windows.go
Normal file
93
tempfork/osexec/lp_windows.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2010 The Go 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 exec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrNotFound is the error resulting if a path search failed to find an executable file.
|
||||
var ErrNotFound = errors.New("executable file not found in %PATH%")
|
||||
|
||||
func chkStat(file string) error {
|
||||
d, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasExt(file string) bool {
|
||||
i := strings.LastIndex(file, ".")
|
||||
if i < 0 {
|
||||
return false
|
||||
}
|
||||
return strings.LastIndexAny(file, `:\/`) < i
|
||||
}
|
||||
|
||||
func findExecutable(file string, exts []string) (string, error) {
|
||||
if len(exts) == 0 {
|
||||
return file, chkStat(file)
|
||||
}
|
||||
if hasExt(file) {
|
||||
if chkStat(file) == nil {
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
for _, e := range exts {
|
||||
if f := file + e; chkStat(f) == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
// LookPath searches for an executable named file in the
|
||||
// directories named by the PATH environment variable.
|
||||
// If file contains a slash, it is tried directly and the PATH is not consulted.
|
||||
// LookPath also uses PATHEXT environment variable to match
|
||||
// a suitable candidate.
|
||||
// The result may be an absolute path or a path relative to the current directory.
|
||||
func LookPath(file string) (string, error) {
|
||||
var exts []string
|
||||
x := os.Getenv(`PATHEXT`)
|
||||
if x != "" {
|
||||
for _, e := range strings.Split(strings.ToLower(x), `;`) {
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
if e[0] != '.' {
|
||||
e = "." + e
|
||||
}
|
||||
exts = append(exts, e)
|
||||
}
|
||||
} else {
|
||||
exts = []string{".com", ".exe", ".bat", ".cmd"}
|
||||
}
|
||||
|
||||
if strings.ContainsAny(file, `:\/`) {
|
||||
if f, err := findExecutable(file, exts); err == nil {
|
||||
return f, nil
|
||||
} else {
|
||||
return "", &Error{file, err}
|
||||
}
|
||||
}
|
||||
if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
path := os.Getenv("path")
|
||||
for _, dir := range filepath.SplitList(path) {
|
||||
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return "", &Error{file, ErrNotFound}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user