Compare commits
103 Commits
netstat-un
...
c22wen/api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f1591d68 | ||
|
|
309d113f93 | ||
|
|
8fc11d582d | ||
|
|
14af677332 | ||
|
|
86fe22a1b1 | ||
|
|
56a7652dc9 | ||
|
|
13b554fed9 | ||
|
|
c2edb2865b | ||
|
|
70f14af21e | ||
|
|
0d94fe5f69 | ||
|
|
1e88050403 | ||
|
|
cf2ac2d123 | ||
|
|
2fe770ed72 | ||
|
|
ff2b3d02e6 | ||
|
|
a9a80ab372 | ||
|
|
1a42cef3a2 | ||
|
|
bfbd6b9241 | ||
|
|
80c94168ae | ||
|
|
cb96b14bf4 | ||
|
|
d0baece5fa | ||
|
|
ef15096a7d | ||
|
|
2b2a16d9a2 | ||
|
|
b4f70d8232 | ||
|
|
15c064f76f | ||
|
|
f9659323df | ||
|
|
053a1d1340 | ||
|
|
57dd247376 | ||
|
|
d97ee12179 | ||
|
|
83f45ae2dd | ||
|
|
c348fb554f | ||
|
|
90c8519765 | ||
|
|
ca676ea645 | ||
|
|
03a039d48d | ||
|
|
f5e33ad761 | ||
|
|
89be4037bb | ||
|
|
baa7937998 | ||
|
|
294ceb513c | ||
|
|
891110e64c | ||
|
|
aa353b8d0f | ||
|
|
f0b0a62873 | ||
|
|
c8c493f3d9 | ||
|
|
0ad109f63d | ||
|
|
f873da5b16 | ||
|
|
58fcd103c4 | ||
|
|
65ae66260f | ||
|
|
c9b9afd761 | ||
|
|
5f07da4854 | ||
|
|
741c513e51 | ||
|
|
554a20becb | ||
|
|
da1bad51cd | ||
|
|
fa412c8760 | ||
|
|
afcf134812 | ||
|
|
0681c6da49 | ||
|
|
2485faf69a | ||
|
|
7ea809897d | ||
|
|
9cee0bfa8c | ||
|
|
34a0292433 | ||
|
|
ce4d68b416 | ||
|
|
a6cad71fb2 | ||
|
|
a0a8b9d76a | ||
|
|
b895bf853a | ||
|
|
8a57f920ae | ||
|
|
6db9c4a173 | ||
|
|
0dc295a640 | ||
|
|
d854fe95d2 | ||
|
|
4749a96a5b | ||
|
|
338fd44657 | ||
|
|
274d32d0aa | ||
|
|
943860fde7 | ||
|
|
bce865b61b | ||
|
|
57cd7738c2 | ||
|
|
9cb6ee3777 | ||
|
|
08f94b3b50 | ||
|
|
442d1873ec | ||
|
|
19c2c6403d | ||
|
|
b3c7b631c2 | ||
|
|
05e5233e07 | ||
|
|
9503be083d | ||
|
|
88179121e3 | ||
|
|
7b92f8e718 | ||
|
|
713cbe84c1 | ||
|
|
be6fe393c5 | ||
|
|
dfbde3d3aa | ||
|
|
4c8ccd6dd6 | ||
|
|
c0af7deb86 | ||
|
|
ab482118ad | ||
|
|
c431382720 | ||
|
|
3a7402aa2d | ||
|
|
cd6099113f | ||
|
|
72e082aaf5 | ||
|
|
2c48b4ee14 | ||
|
|
0710fca0cd | ||
|
|
aa9d7f4665 | ||
|
|
a5dd0bcb09 | ||
|
|
b65eee0745 | ||
|
|
1ebbaaaebb | ||
|
|
eccc167733 | ||
|
|
8f76548fd9 | ||
|
|
5b338bf011 | ||
|
|
acade77c86 | ||
|
|
5d96ecd5e6 | ||
|
|
c8939ab7c7 | ||
|
|
883a11f2a8 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,8 +6,6 @@
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
cmd/relaynode/relaynode
|
||||
cmd/taillogin/taillogin
|
||||
cmd/tailscale/tailscale
|
||||
cmd/tailscaled/tailscaled
|
||||
|
||||
@@ -19,3 +17,7 @@ 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
|
||||
|
||||
661
api.md
Normal file
661
api.md
Normal file
@@ -0,0 +1,661 @@
|
||||
# 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
|
||||
|
||||
## Device
|
||||
<!-- TODO: description about what devices are -->
|
||||
|
||||
#### `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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `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" : []
|
||||
}
|
||||
```
|
||||
|
||||
#### `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"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Domain
|
||||
<!---
|
||||
TODO: ctrl+f domain, replace with {workgroup/tailnet/other}
|
||||
Domain is a top level resource. ACL is an example of a resource that is tied to a top level domain.
|
||||
--->
|
||||
|
||||
### ACL
|
||||
|
||||
#### `GET /api/v2/domain/:domain/acl` - fetch ACL for a domain
|
||||
|
||||
Retrieves the ACL that is currently set for the given domain. Supply the domain 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/domain/example.com/acl
|
||||
curl https://api.tailscale.com/api/v2/domain/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/domain/example.com/acl
|
||||
curl https://api.tailscale.com/api/v2/domain/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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/acl` - set ACL for a domain
|
||||
|
||||
Sets the ACL for the given domain. 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/domain/example.com/acl
|
||||
curl https://api.tailscale.com/api/v2/domain/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": ["*:*"] },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/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/domain/example.com/acl/preiew
|
||||
curl https://api.tailscale.com/api/v2/domain/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"}
|
||||
```
|
||||
|
||||
### Devices
|
||||
#### `GET /api/v2/domain/:domain/devices` - list the devices for a domain
|
||||
Lists the devices for a domain.
|
||||
Supply the domain 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/domain/example.com/devices
|
||||
curl https://api.tailscale.com/api/v2/domain/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,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### DNS
|
||||
|
||||
#### `GET /api/v2/domain/:domain/dns/nameservers` - list the DNS nameservers for a domain
|
||||
Lists the DNS nameservers for a domain.
|
||||
Supply the domain of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/domain/example.com/dns/nameservers
|
||||
curl https://api.tailscale.com/api/v2/domain/example.com/dns/nameservers \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"dns": ["8.8.8.8"],
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/dns/nameservers` - replaces the list of DNS nameservers for a domain
|
||||
Replaces the list of DNS nameservers for the given domain with the list supplied by the user.
|
||||
Supply the domain 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/domain/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/domain/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/domain/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": []}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":[],
|
||||
"magicDNS": false,
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/v2/domain/:domain/dns/preferences` - retrieves the DNS preferences for a domain
|
||||
Retrieves the DNS preferences that are currently set for the given domain.
|
||||
Supply the domain of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/domain/example.com/dns/preferences
|
||||
curl 'https://api.tailscale.com/api/v2/domain/example.com/dns/preferences' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"magicDNS":false,
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/dns/preferences` - replaces the DNS preferences for a domain
|
||||
Replaces the DNS preferences for a domain, 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 explicity 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 network.
|
||||
```
|
||||
{
|
||||
"magicDNS": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/domain/example.com/dns/preferences
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/domain/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,
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/v2/domain/:domain/dns/searchpaths` - retrieves the search paths for a domain
|
||||
Retrieves the list of search paths that is currently set for the given domain.
|
||||
Supply the domain of interest in the path.
|
||||
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/domain/example.com/dns/searchpaths
|
||||
curl 'https://api.tailscale.com/api/v2/domain/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com"],
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v2/domain/:domain/dns/searchpaths` - replaces the search paths for a domain
|
||||
Replaces the list of search paths with the list supplied by the user and returns an error otherwise.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`searchPaths` - A list of searchpaths in JSON format.
|
||||
```
|
||||
{
|
||||
"searchPaths: ["user1.example.com", "user2.example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/domain/example.com/dns/searchpaths
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/domain/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"searchPaths": ["user1.example.com", "user2.example.com"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"],
|
||||
}
|
||||
```
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
@@ -35,6 +34,7 @@ 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 wgcfg.PrivateKey
|
||||
PrivateKey wgkey.Private
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
@@ -77,8 +77,8 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewKey() wgcfg.PrivateKey {
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
func mustNewKey() wgkey.Private {
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -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 connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
|
||||
@@ -169,6 +169,9 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -85,29 +84,6 @@ 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"
|
||||
}
|
||||
@@ -162,19 +138,18 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
var routes []wgcfg.CIDR
|
||||
var routes []netaddr.IPPrefix
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
cidr, ok := parseIPOrCIDR(s)
|
||||
ipp, err := netaddr.ParseIPPrefix(s) // parse it with other pawith both packages
|
||||
if !ok || err != nil {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if 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, cidr)
|
||||
routes = append(routes, ipp)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
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+
|
||||
@@ -27,15 +28,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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
|
||||
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
|
||||
@@ -67,7 +65,6 @@ 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+
|
||||
@@ -76,8 +73,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/types/wgkey 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/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
|
||||
@@ -85,12 +84,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
|
||||
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
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
@@ -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 github.com/tailscale/wireguard-go/tun/wintun+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
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 github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
@@ -171,6 +170,7 @@ 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+
|
||||
@@ -197,6 +197,7 @@ 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+
|
||||
|
||||
@@ -10,7 +10,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
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,6 +19,7 @@ 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
|
||||
@@ -30,14 +31,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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
|
||||
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+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
@@ -74,7 +72,6 @@ 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
|
||||
@@ -84,10 +81,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/types/wgkey 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+
|
||||
@@ -95,12 +94,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor 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
|
||||
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+
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
@@ -111,7 +110,6 @@ 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+
|
||||
@@ -130,11 +128,12 @@ 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 github.com/tailscale/wireguard-go/tun/wintun+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
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 github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
@@ -182,6 +181,7 @@ 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+
|
||||
|
||||
@@ -64,6 +64,7 @@ var args struct {
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
verbose int
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -76,6 +77,7 @@ 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")
|
||||
@@ -118,6 +120,7 @@ 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)
|
||||
|
||||
@@ -18,6 +18,18 @@ StateDirectory=tailscale
|
||||
StateDirectoryMode=0750
|
||||
CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
Type=notify
|
||||
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
PrivateTmp=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
|
||||
@@ -182,7 +182,8 @@ func (c *Client) SetPaused(paused bool) {
|
||||
}
|
||||
c.paused = paused
|
||||
if paused {
|
||||
// Just cancel the map routine. The auth routine isn't expensive.
|
||||
// Only cancel the map routine. (The auth routine isn't expensive
|
||||
// so it's fine to keep it running.)
|
||||
c.cancelMapLocked()
|
||||
} else {
|
||||
for _, ch := range c.unpauseWaiters {
|
||||
@@ -230,7 +231,7 @@ func (c *Client) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logf("cancelMapSafely: synced=%v", c.synced)
|
||||
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
|
||||
|
||||
if c.inPollNetMap {
|
||||
// received at least one netmap since the last
|
||||
@@ -252,12 +253,12 @@ func (c *Client) cancelMapSafely() {
|
||||
// request.
|
||||
select {
|
||||
case c.newMapCh <- struct{}{}:
|
||||
c.logf("cancelMapSafely: wrote to channel")
|
||||
c.logf("[v1] 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("cancelMapSafely: channel was full")
|
||||
c.logf("[v1] cancelMapSafely: channel was full")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,22 +269,24 @@ func (c *Client) authRoutine() {
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
c.logf("authRoutine: %s", c.state)
|
||||
expiry := c.expiry
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
synced := c.synced
|
||||
if goal != nil {
|
||||
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
} else {
|
||||
c.logf("authRoutine: %s; goal=nil", c.state)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
c.logf("authRoutine: quit")
|
||||
c.logf("[v1] authRoutine: quit")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("%s: %v", msg, err)
|
||||
c.logf("[v1] %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.
|
||||
@@ -293,51 +296,13 @@ func (c *Client) authRoutine() {
|
||||
}
|
||||
|
||||
if goal == nil {
|
||||
// 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.")
|
||||
// Wait for user to Login or Logout.
|
||||
<-ctx.Done()
|
||||
c.logf("[v1] authRoutine: context done.")
|
||||
continue
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: c.loggedIn,
|
||||
}
|
||||
c.loggedIn = false
|
||||
c.expiry = nil
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
} else if !goal.wantLoggedIn {
|
||||
if !goal.wantLoggedIn {
|
||||
err := c.direct.TryLogout(ctx)
|
||||
if err != nil {
|
||||
report(err, "TryLogout")
|
||||
@@ -469,7 +434,7 @@ func (c *Client) mapRoutine() {
|
||||
}
|
||||
|
||||
report := func(err error, msg string) {
|
||||
c.logf("%s: %v", msg, err)
|
||||
c.logf("[v1] %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.
|
||||
@@ -504,7 +469,7 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
select {
|
||||
case <-c.newMapCh:
|
||||
c.logf("mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.logf("[v1] mapRoutine: new map request during PollNetMap. canceling.")
|
||||
c.cancelMapLocked()
|
||||
|
||||
// Don't emit this netmap; we're
|
||||
@@ -526,7 +491,7 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("mapRoutine: netmap received: %s", state)
|
||||
c.logf("[v1] mapRoutine: netmap received: %s", state)
|
||||
if stillAuthed {
|
||||
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
|
||||
}
|
||||
@@ -607,7 +572,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
c.inSendStatus++
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("sendStatus: %s: %v", who, state)
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
|
||||
var p *Persist
|
||||
var fin *empty.Message
|
||||
@@ -700,7 +665,7 @@ func (c *Client) Shutdown() {
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
// used exclusively in tests.
|
||||
func (c *Client) TestOnlyNodePublicKey() wgcfg.Key {
|
||||
func (c *Client) TestOnlyNodePublicKey() wgkey.Key {
|
||||
priv := c.direct.GetPersist()
|
||||
return priv.PrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
@@ -44,7 +43,10 @@ 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 {
|
||||
@@ -59,10 +61,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 wgcfg.PrivateKey `json:"PrivateMachineKey"`
|
||||
LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"`
|
||||
|
||||
PrivateNodeKey wgcfg.PrivateKey
|
||||
OldPrivateNodeKey wgcfg.PrivateKey // needed to request key rotation
|
||||
PrivateNodeKey wgkey.Private
|
||||
OldPrivateNodeKey wgkey.Private // needed to request key rotation
|
||||
Provider string
|
||||
LoginName string
|
||||
}
|
||||
@@ -83,7 +85,7 @@ func (p *Persist) Equals(p2 *Persist) bool {
|
||||
}
|
||||
|
||||
func (p *Persist) Pretty() string {
|
||||
var mk, ok, nk wgcfg.Key
|
||||
var mk, ok, nk wgkey.Key
|
||||
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
mk = p.LegacyFrontendPrivateMachineKey.Public()
|
||||
}
|
||||
@@ -93,7 +95,7 @@ func (p *Persist) Pretty() string {
|
||||
if !p.PrivateNodeKey.IsZero() {
|
||||
nk = p.PrivateNodeKey.Public()
|
||||
}
|
||||
ss := func(k wgcfg.Key) string {
|
||||
ss := func(k wgkey.Key) string {
|
||||
if k.IsZero() {
|
||||
return ""
|
||||
}
|
||||
@@ -113,14 +115,14 @@ type Direct struct {
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgcfg.PrivateKey
|
||||
machinePrivKey wgkey.Private
|
||||
debugFlags []string
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgcfg.Key
|
||||
serverKey wgkey.Key
|
||||
persist Persist
|
||||
authKey string
|
||||
tryingNewKey wgcfg.PrivateKey
|
||||
tryingNewKey wgkey.Private
|
||||
expiry *time.Time
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
@@ -131,7 +133,7 @@ type Direct struct {
|
||||
|
||||
type Options struct {
|
||||
Persist Persist // initial persistent data
|
||||
MachinePrivateKey wgcfg.PrivateKey // the machine key to use
|
||||
MachinePrivateKey wgkey.Private // 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
|
||||
@@ -308,6 +310,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -328,6 +331,7 @@ 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 {
|
||||
@@ -336,7 +340,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
}
|
||||
|
||||
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, url != "")
|
||||
if serverKey == (wgcfg.Key{}) {
|
||||
if serverKey.IsZero() {
|
||||
var err error
|
||||
serverKey, err = loadServerKey(ctx, c.httpc, c.serverURL)
|
||||
if err != nil {
|
||||
@@ -348,12 +352,12 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
var oldNodeKey wgcfg.Key
|
||||
var oldNodeKey wgkey.Key
|
||||
if url != "" {
|
||||
} else if regen || persist.PrivateNodeKey.IsZero() {
|
||||
c.logf("Generating a new nodekey.")
|
||||
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
c.logf("login keygen: %v", err)
|
||||
return regen, url, err
|
||||
@@ -533,15 +537,17 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}
|
||||
|
||||
allowStream := maxPolls != 1
|
||||
c.logf("PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
|
||||
c.logf("[v1] 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: 5,
|
||||
Version: 8,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
DiscoKey: c.discoPubKey,
|
||||
@@ -636,6 +642,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
|
||||
var lastDERPMap *tailcfg.DERPMap
|
||||
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
|
||||
var lastParsedPacketFilter []filter.Match
|
||||
|
||||
// If allowStream, then the server will use an HTTP long poll to
|
||||
// return incremental results. There is always one response right
|
||||
@@ -676,7 +683,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
case timeoutReset <- struct{}{}:
|
||||
vlogf("netmap: sent timer reset")
|
||||
case <-ctx.Done():
|
||||
c.logf("netmap: not resetting timer; context done: %v", ctx.Err())
|
||||
c.logf("[v1] netmap: not resetting timer; context done: %v", ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
@@ -713,6 +720,10 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
resp.Peers = filtered
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||
}
|
||||
|
||||
nm := &NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
@@ -727,7 +738,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
Domain: resp.Domain,
|
||||
DNS: resp.DNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
|
||||
PacketFilter: lastParsedPacketFilter,
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
@@ -750,7 +761,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
nm.MachineStatus = tailcfg.MachineUnauthorized
|
||||
}
|
||||
if len(resp.DNS) > 0 {
|
||||
nm.DNS.Nameservers = wgIPToNetaddr(resp.DNS)
|
||||
nm.DNS.Nameservers = resp.DNS
|
||||
}
|
||||
if len(resp.SearchPaths) > 0 {
|
||||
nm.DNS.Domains = resp.SearchPaths
|
||||
@@ -767,7 +778,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
now := c.timeNow()
|
||||
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
|
||||
c.lastPrintMap = now
|
||||
c.logf("new network map[%d]:\n%s", i, nm.Concise())
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -782,7 +793,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
return nil
|
||||
}
|
||||
|
||||
func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
|
||||
func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
|
||||
defer res.Body.Close()
|
||||
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
@@ -837,7 +848,7 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
|
||||
}
|
||||
|
||||
func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) error {
|
||||
func decodeMsg(msg []byte, v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) error {
|
||||
decrypted, err := decryptMsg(msg, serverKey, mkey)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -851,7 +862,7 @@ func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.Priv
|
||||
return nil
|
||||
}
|
||||
|
||||
func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
|
||||
func decryptMsg(msg []byte, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
if len(msg) < len(nonce)+1 {
|
||||
return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
|
||||
@@ -867,7 +878,7 @@ func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byt
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte, error) {
|
||||
func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -886,42 +897,31 @@ func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgcfg.Key, error) {
|
||||
func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (wgkey.Key, error) {
|
||||
req, err := http.NewRequest("GET", serverURL+"/key", nil)
|
||||
if err != nil {
|
||||
return wgcfg.Key{}, fmt.Errorf("create control key request: %v", err)
|
||||
return wgkey.Key{}, fmt.Errorf("create control key request: %v", err)
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
res, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
return wgkey.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 wgcfg.Key{}, fmt.Errorf("fetch control key response: %v", err)
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key response: %v", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
|
||||
return wgkey.Key{}, fmt.Errorf("fetch control key: %d: %s", res.StatusCode, string(b))
|
||||
}
|
||||
key, err := wgcfg.ParseHexKey(string(b))
|
||||
key, err := wgkey.ParseHex(string(b))
|
||||
if err != nil {
|
||||
return wgcfg.Key{}, fmt.Errorf("fetch control key: %v", err)
|
||||
return wgkey.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()
|
||||
@@ -1080,11 +1080,12 @@ func TrimWGConfig() opt.Bool {
|
||||
// and will definitely not work for the routes provided.
|
||||
//
|
||||
// It should not return false positives.
|
||||
func ipForwardingBroken(routes []wgcfg.CIDR) bool {
|
||||
func ipForwardingBroken(routes []netaddr.IPPrefix) 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
|
||||
@@ -1092,17 +1093,57 @@ func ipForwardingBroken(routes []wgcfg.CIDR) bool {
|
||||
// already in the admin panel.
|
||||
return false
|
||||
}
|
||||
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()
|
||||
|
||||
v4Routes, v6Routes := false, false
|
||||
for _, r := range routes {
|
||||
if r.IP.Is4() {
|
||||
v4Routes = true
|
||||
} else {
|
||||
v6Routes = 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 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
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(string(out)) == "0"
|
||||
// TODO: also check IPv6 if 'routes' contains any IPv6 routes
|
||||
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
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func osVersionLinux() string {
|
||||
return fmt.Sprintf("%s%s", bytes.TrimSpace(cr), attr)
|
||||
}
|
||||
fallthrough
|
||||
case "fedora", "rhel", "alpine":
|
||||
case "fedora", "rhel", "alpine", "nixos":
|
||||
// Their PRETTY_NAME is fine as-is for all versions I tested.
|
||||
fallthrough
|
||||
default:
|
||||
|
||||
@@ -14,8 +14,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -23,11 +25,11 @@ type NetworkMap struct {
|
||||
// Core networking
|
||||
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
PrivateKey wgkey.Private
|
||||
Expiry time.Time
|
||||
// Name is the DNS name assigned to this node.
|
||||
Name string
|
||||
Addresses []wgcfg.CIDR
|
||||
Addresses []netaddr.IPPrefix
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
MachineKey tailcfg.MachineKey
|
||||
@@ -240,7 +242,7 @@ const EndpointDiscoSuffix = ".disco.tailscale:12345"
|
||||
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
|
||||
cfg := &wgcfg.Config{
|
||||
Name: "tailscale",
|
||||
PrivateKey: nm.PrivateKey,
|
||||
PrivateKey: wgcfg.PrivateKey(nm.PrivateKey),
|
||||
Addresses: nm.Addresses,
|
||||
ListenPort: nm.LocalPort,
|
||||
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
|
||||
@@ -278,14 +280,14 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
}
|
||||
}
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Mask == 0 {
|
||||
if allowedIP.Bits == 0 {
|
||||
if (flags & AllowDefaultRoute) == 0 {
|
||||
logf("wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
logf("[v1] wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
logf("[v1] wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -298,17 +300,11 @@ 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 wgcfg.CIDR) bool {
|
||||
if cidr.Mask == 0 {
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool {
|
||||
if cidr.Bits == 0 {
|
||||
return false
|
||||
}
|
||||
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.
|
||||
if !cidr.IsSingleIP() {
|
||||
return true
|
||||
}
|
||||
for _, selfCIDR := range node.Addresses {
|
||||
@@ -351,7 +347,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 []wgcfg.CIDR) bool {
|
||||
func eqCIDRsIgnoreNil(a, b []netaddr.IPPrefix) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ package controlclient
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -250,7 +251,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: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
|
||||
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -263,7 +264,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: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
|
||||
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -282,3 +283,15 @@ 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"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestPersistEqual(t *testing.T) {
|
||||
@@ -18,8 +18,8 @@ func TestPersistEqual(t *testing.T) {
|
||||
have, persistHandles)
|
||||
}
|
||||
|
||||
newPrivate := func() wgcfg.PrivateKey {
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
newPrivate := func() wgkey.Private {
|
||||
k, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
32
go.mod
32
go.mod
@@ -1,6 +1,6 @@
|
||||
module tailscale.com
|
||||
|
||||
go 1.14
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
|
||||
@@ -13,29 +13,33 @@ require (
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/google/go-cmp v0.5.4
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.1
|
||||
github.com/mdlayher/netlink v1.1.0
|
||||
github.com/mdlayher/netlink v1.2.0
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
|
||||
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-20201003033024-5d95aab075be
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb // indirect
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd
|
||||
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/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
119
go.sum
119
go.sum
@@ -1,12 +1,8 @@
|
||||
cloud.google.com/go v0.0.0-20170206221025-ce650573d812/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190129172621-c8b1d7a94ddf/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
|
||||
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/aclements/go-gg v0.0.0-20170118225347-6dbb4e4fefb0/go.mod h1:55qNq4vcpkIuHowELi5C8e+1yUHtoLoOUR9QU5j7Tes=
|
||||
github.com/aclements/go-moremath v0.0.0-20161014184102-0ff62e0875ff/go.mod h1:idZL3yvz4kzx1dsBOAC+oYv6L92P1oFEhUXUB1A/lwQ=
|
||||
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=
|
||||
@@ -27,6 +23,7 @@ github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmeka
|
||||
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/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
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/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
@@ -35,27 +32,24 @@ github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE
|
||||
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-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
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/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/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc=
|
||||
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg=
|
||||
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks=
|
||||
github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A=
|
||||
github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
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.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
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.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w=
|
||||
github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
@@ -63,6 +57,9 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
|
||||
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/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=
|
||||
@@ -76,13 +73,17 @@ github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJ
|
||||
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mattn/go-sqlite3 v0.0.0-20161215041557-2d44decb4941/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
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/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
@@ -106,14 +107,15 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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/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-20201220011020-db78fad0bebf h1:HuBwLWbDNIh/G72KSImSEx+dnd7FPGFI1e60LMJtLjU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201220011020-db78fad0bebf/go.mod h1:9PbAnF5CAklkURoO0uQhm+YUjDmm9T9oCyTGlCHuTPQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d h1:ha3qx0YBsEYM1VpLoAxVyLsz74H2a/Kv/id+2Bo/WLU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d/go.mod h1:FEGDKc5yHNWtTS5ugWnHMNF0d9LlaHv/zQwOrVogo2U=
|
||||
github.com/tailscale/wireguard-go v0.0.20201119-0.20201228205120-066446d1733a h1:RUJeuZlAm1DT6Mhk9UTsaHrDeDZhPrbKfNsaEtKF6+0=
|
||||
github.com/tailscale/wireguard-go v0.0.20201119-0.20201228205120-066446d1733a/go.mod h1:UIAx57STfAZOrNVj8QGP2zG3ovWPMTD4DDubFHqMlYI=
|
||||
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=
|
||||
@@ -122,25 +124,36 @@ github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
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=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc h1:paujszgN6SpsO/UsXC7xax3gQAKz/XQKCYZLQdU34Tw=
|
||||
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/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
|
||||
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-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-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-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-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/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 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
|
||||
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/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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-20180724234803-3673e40ba225/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -148,26 +161,27 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
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-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-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/oauth2 v0.0.0-20170207211851-4464e7848382/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
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-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20200918155509-d949658356f9 h1:yVBHF5pcQLKR9B+y+dOJ6y68nqJBDWaZ9DhB1Ohg0qE=
|
||||
golang.org/x/perf v0.0.0-20200918155509-d949658356f9/go.mod h1:FrqOtQDO3iMDVUtw5nNTDFpR1HUCGh00M3kj2wiSzLQ=
|
||||
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-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-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=
|
||||
@@ -176,22 +190,29 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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/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 h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
|
||||
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.3.0/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=
|
||||
@@ -201,38 +222,46 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/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-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
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/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/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.20200715051853-507f148e1c42 h1:SrR1hmxGKKarHEEDvaHxatwnqE3uT+7jvMcin6SHOkw=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
|
||||
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.20201004085714-dd60d0447f81 h1:cT2oWlz8v9g7bjFZclT362akxJJfGv9d7ccKu6GQUbA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
|
||||
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.0.0-20170206182103-3d017632ea10/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
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/grpc v0.0.0-20170208002647-2a6bf6142e96/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
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/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=
|
||||
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=
|
||||
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-20200810144936-56928fe48a98/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
inet.af/netaddr v0.0.0-20201218162718-658fec415e52/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
inet.af/netaddr v0.0.0-20201223185330-97d366981fac h1:aqMW8vft7VmOIhtQhsTWhAuZzOBGYBv+Otyvwj+VGSU=
|
||||
inet.af/netaddr v0.0.0-20201223185330-97d366981fac/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201224214825-a55841caa437 h1:Li2QBwaT/hU3wE7GdyoqaX+TzIlI+V1zs/CuWrjX8e4=
|
||||
inet.af/netaddr v0.0.0-20201224214825-a55841caa437/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201226233944-2d1876c01610 h1:9Nnw3NS9SL4SlFtBWSdv7onMbdY+B8nflRNZvhgxuMY=
|
||||
inet.af/netaddr v0.0.0-20201226233944-2d1876c01610/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
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-20201231012616-c5dc91d2a016 h1:CEeeAJW60aRKE6gGJC5krs2xC/uM2l8SasvgeDXFN5Q=
|
||||
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016/go.mod h1:lbePDLSB5c45kkUmF7ETNE5X9z/yuQvWJIv1hhb5rFI=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
tailscale.com v1.2.10/go.mod h1:JEJiCce3MHtPCTdX2ahLc4tcnxZ7b5etish1Yt0B6+w=
|
||||
|
||||
@@ -37,7 +37,7 @@ func getVal() []interface{} {
|
||||
return []interface{}{
|
||||
&wgcfg.Config{
|
||||
Name: "foo",
|
||||
Addresses: []wgcfg.CIDR{{Mask: 5, IP: wgcfg.IP{Addr: [16]byte{3: 3}}}},
|
||||
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
|
||||
ListenPort: 5,
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
|
||||
@@ -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() []wgcfg.CIDR {
|
||||
func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
@@ -126,7 +126,7 @@ func (h *Handle) LocalAddrs() []wgcfg.CIDR {
|
||||
if nm != nil {
|
||||
return nm.Addresses
|
||||
}
|
||||
return []wgcfg.CIDR{}
|
||||
return []netaddr.IPPrefix{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *controlclient.NetworkMap {
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -265,12 +266,14 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
}
|
||||
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("incoming control connection")
|
||||
logf("[v1] incoming control connection")
|
||||
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(br)
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
logf("[v1] ReadMsg: %v", err)
|
||||
} else if ctx.Err() == nil {
|
||||
logf("ReadMsg: %v", err)
|
||||
}
|
||||
return
|
||||
@@ -589,6 +592,7 @@ 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
|
||||
@@ -724,6 +728,10 @@ 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 {
|
||||
|
||||
@@ -64,6 +64,12 @@ 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
|
||||
@@ -218,6 +224,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if st.KeepAlive {
|
||||
e.KeepAlive = true
|
||||
}
|
||||
if st.ShareeNode {
|
||||
e.ShareeNode = true
|
||||
}
|
||||
}
|
||||
|
||||
type StatusUpdater interface {
|
||||
|
||||
89
ipn/local.go
89
ipn/local.go
@@ -29,6 +29,8 @@ 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"
|
||||
@@ -82,7 +84,7 @@ type LocalBackend struct {
|
||||
userID string // current controlling user ID (for Windows, primarily)
|
||||
prefs *Prefs
|
||||
inServerMode bool
|
||||
machinePrivKey wgcfg.PrivateKey
|
||||
machinePrivKey wgkey.Private
|
||||
state State
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
@@ -208,8 +210,14 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
lastSeen = *p.LastSeen
|
||||
}
|
||||
var tailAddr string
|
||||
if len(p.Addresses) > 0 {
|
||||
tailAddr = strings.TrimSuffix(p.Addresses[0].String(), "/32")
|
||||
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
|
||||
}
|
||||
}
|
||||
sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
@@ -221,6 +229,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -243,7 +252,11 @@ 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.
|
||||
b.logf("Received error: %v", st.Err)
|
||||
if st.Err == "EOF" {
|
||||
b.logf("[v1] Received error: EOF")
|
||||
} else {
|
||||
b.logf("Received error: %v", st.Err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if st.LoginFinished != nil {
|
||||
@@ -307,7 +320,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
if netMap != nil {
|
||||
diff := st.NetMap.ConciseDiffFrom(netMap)
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
b.logf("netmap diff: (none)")
|
||||
b.logf("[v1] netmap diff: (none)")
|
||||
} else {
|
||||
b.logf("netmap diff:\n%v", diff)
|
||||
}
|
||||
@@ -522,9 +535,9 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
// quite hard to debug, so save yourself the trouble.
|
||||
var (
|
||||
haveNetmap = netMap != nil
|
||||
addrs []wgcfg.CIDR
|
||||
addrs []netaddr.IPPrefix
|
||||
packetFilter []filter.Match
|
||||
advRoutes []wgcfg.CIDR
|
||||
advRoutes []netaddr.IPPrefix
|
||||
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
|
||||
)
|
||||
if haveNetmap {
|
||||
@@ -546,7 +559,7 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
return
|
||||
}
|
||||
|
||||
localNets := wgCIDRsToNetaddr(netMap.Addresses, advRoutes)
|
||||
localNets := unmapIPPrefixes(netMap.Addresses, advRoutes)
|
||||
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
@@ -560,7 +573,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 []wgcfg.CIDR) bool {
|
||||
func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool {
|
||||
if len(newAddr) != len(oldAddr) {
|
||||
return false
|
||||
}
|
||||
@@ -614,11 +627,11 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
}
|
||||
|
||||
nameToIP := make(map[string]netaddr.IP)
|
||||
set := func(name string, addrs []wgcfg.CIDR) {
|
||||
set := func(name string, addrs []netaddr.IPPrefix) {
|
||||
if len(addrs) == 0 || name == "" {
|
||||
return
|
||||
}
|
||||
nameToIP[name] = netaddr.IPFrom16(addrs[0].IP.Addr)
|
||||
nameToIP[name] = addrs[0].IP
|
||||
}
|
||||
|
||||
for _, peer := range netMap.Peers {
|
||||
@@ -725,7 +738,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var legacyMachineKey wgcfg.PrivateKey
|
||||
var legacyMachineKey wgkey.Private
|
||||
if b.prefs.Persist != nil {
|
||||
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
|
||||
}
|
||||
@@ -760,7 +773,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
} else {
|
||||
b.logf("generating new machine key")
|
||||
var err error
|
||||
b.machinePrivKey, err = wgcfg.NewPrivateKey()
|
||||
b.machinePrivKey, err = wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing new machine key: %w", err)
|
||||
}
|
||||
@@ -995,8 +1008,8 @@ 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("peer keys: %s", strings.TrimSpace(peerKeys.String()))
|
||||
b.statsLogf("v%v peers: %v", version.Long, strings.TrimSpace(peerStats.String()))
|
||||
b.keyLogf("[v1] peer keys: %s", strings.TrimSpace(peerKeys.String()))
|
||||
b.statsLogf("[v1] v%v peers: %v", version.Long, strings.TrimSpace(peerStats.String()))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -1052,7 +1065,7 @@ func (b *LocalBackend) SetPrefs(newp *Prefs) {
|
||||
|
||||
oldHi := b.hostinfo
|
||||
newHi := oldHi.Clone()
|
||||
newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...)
|
||||
newHi.RoutableIPs = append([]netaddr.IPPrefix(nil), b.prefs.AdvertiseRoutes...)
|
||||
applyPrefsToHostinfo(newHi, newp)
|
||||
b.hostinfo = newHi
|
||||
hostInfoChanged := !oldHi.Equal(newHi)
|
||||
@@ -1219,7 +1232,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
if err == wgengine.ErrNoChanges {
|
||||
return
|
||||
}
|
||||
b.logf("authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
|
||||
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
|
||||
}
|
||||
|
||||
// domainsForProxying produces a list of search domains for proxied DNS.
|
||||
@@ -1252,23 +1265,15 @@ func domainsForProxying(nm *controlclient.NetworkMap) []string {
|
||||
|
||||
// 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: wgCIDRsToNetaddr(addrs),
|
||||
SubnetRoutes: wgCIDRsToNetaddr(prefs.AdvertiseRoutes),
|
||||
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
|
||||
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT,
|
||||
NetfilterMode: prefs.NetfilterMode,
|
||||
}
|
||||
|
||||
for _, peer := range cfg.Peers {
|
||||
rs.Routes = append(rs.Routes, wgCIDRsToNetaddr(peer.AllowedIPs)...)
|
||||
rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...)
|
||||
}
|
||||
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
|
||||
@@ -1279,15 +1284,10 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
|
||||
return rs
|
||||
}
|
||||
|
||||
func wgCIDRsToNetaddr(cidrLists ...[]wgcfg.CIDR) (ret []netaddr.IPPrefix) {
|
||||
for _, cidrs := range cidrLists {
|
||||
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)
|
||||
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})
|
||||
}
|
||||
}
|
||||
return ret
|
||||
@@ -1303,6 +1303,7 @@ 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
|
||||
@@ -1320,6 +1321,8 @@ 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 {
|
||||
@@ -1337,6 +1340,7 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
|
||||
switch newState {
|
||||
case NeedsLogin:
|
||||
systemd.Status("Needs login: %s", authURL)
|
||||
b.blockEngineUpdates(true)
|
||||
fallthrough
|
||||
case Stopped:
|
||||
@@ -1344,12 +1348,20 @@ 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:
|
||||
break
|
||||
var addrs []string
|
||||
for _, addr := range b.netMap.Addresses {
|
||||
addrs = append(addrs, addr.IP.String())
|
||||
}
|
||||
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrs, " "))
|
||||
default:
|
||||
b.logf("[unexpected] unknown newState %#v", newState)
|
||||
}
|
||||
@@ -1548,7 +1560,6 @@ 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 {
|
||||
//lint:ignore S1008 for comments
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "ios", "android":
|
||||
// iOS, macOS, Android users can't downgrade anyway.
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
func TestLocalLogLines(t *testing.T) {
|
||||
logListen := tstest.NewLogLineTracker(t.Logf, []string{
|
||||
"SetPrefs: %v",
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
"[v1] peer keys: %s",
|
||||
"[v1] v%v peers: %v",
|
||||
})
|
||||
|
||||
logid := func(hex byte) logtail.PublicID {
|
||||
@@ -71,7 +71,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
prefs.Persist = persist
|
||||
lb.SetPrefs(prefs)
|
||||
|
||||
t.Run("after_prefs", testWantRemain("peer keys: %s", "v%v peers: %v"))
|
||||
t.Run("after_prefs", testWantRemain("[v1] peer keys: %s", "[v1] v%v peers: %v"))
|
||||
|
||||
// log peers, peer keys
|
||||
status := &wgengine.Status{
|
||||
|
||||
19
ipn/prefs.go
19
ipn/prefs.go
@@ -5,6 +5,7 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -54,7 +55,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.
|
||||
// connections. This overrides tailcfg.Hostinfo's ShieldsUp.
|
||||
ShieldsUp bool
|
||||
|
||||
// AdvertiseTags specifies groups that this node wants to join, for
|
||||
@@ -99,7 +100,7 @@ type Prefs struct {
|
||||
// AdvertiseRoutes specifies CIDR prefixes to advertise into the
|
||||
// Tailscale network as reachable through the current
|
||||
// node.
|
||||
AdvertiseRoutes []wgcfg.CIDR
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
|
||||
// NoSNAT specifies whether to source NAT traffic going to
|
||||
// destinations in AdvertiseRoutes. The default is to apply source
|
||||
@@ -205,12 +206,12 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.Persist.Equals(p2.Persist)
|
||||
}
|
||||
|
||||
func compareIPNets(a, b []wgcfg.CIDR) bool {
|
||||
func compareIPNets(a, b []netaddr.IPPrefix) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if !a[i].IP.Equal(b[i].IP) || a[i].Mask != b[i].Mask {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -276,6 +277,14 @@ 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)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -44,7 +44,7 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
DeviceModel string
|
||||
NotepadURLs bool
|
||||
ForceDaemon bool
|
||||
AdvertiseRoutes []wgcfg.CIDR
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
NoSNAT bool
|
||||
NetfilterMode router.NetfilterMode
|
||||
Persist *controlclient.Persist
|
||||
|
||||
@@ -7,14 +7,16 @@ package ipn
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
@@ -34,9 +36,9 @@ func TestPrefsEqual(t *testing.T) {
|
||||
have, prefsHandles)
|
||||
}
|
||||
|
||||
nets := func(strs ...string) (ns []wgcfg.CIDR) {
|
||||
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
|
||||
for _, s := range strs {
|
||||
n, err := wgcfg.ParseCIDR(s)
|
||||
n, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -165,12 +167,12 @@ func TestPrefsEqual(t *testing.T) {
|
||||
|
||||
{
|
||||
&Prefs{AdvertiseRoutes: nil},
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
&Prefs{AdvertiseRoutes: []wgcfg.CIDR{}},
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
&Prefs{AdvertiseRoutes: []netaddr.IPPrefix{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
@@ -346,7 +348,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{
|
||||
PrivateNodeKey: wgcfg.PrivateKey{1: 1},
|
||||
PrivateNodeKey: wgkey.Private{1: 1},
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
@@ -371,3 +373,25 @@ 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)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ var ErrStateNotExist = errors.New("no state with given ID")
|
||||
|
||||
const (
|
||||
// MachineKeyStateKey is the key under which we store the machine key,
|
||||
// in its wgcfg.PrivateKey.MarshalText representation.
|
||||
// in its wgkey.Private.MarshalText representation.
|
||||
MachineKeyStateKey = StateKey("_machinekey")
|
||||
|
||||
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
@@ -49,7 +49,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 +311,7 @@ func tryFixLogStateLocation(dir, cmdname string) {
|
||||
// given collection name.
|
||||
func New(collection string) *Policy {
|
||||
var lflags int
|
||||
if terminal.IsTerminal(2) || runtime.GOOS == "windows" {
|
||||
if term.IsTerminal(2) || runtime.GOOS == "windows" {
|
||||
lflags = 0
|
||||
} else {
|
||||
lflags = log.LstdFlags
|
||||
@@ -391,7 +391,7 @@ func New(collection string) *Policy {
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
}
|
||||
lw := logtail.Log(c, log.Printf)
|
||||
lw := logtail.NewLogger(c, log.Printf)
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
log.SetOutput(lw)
|
||||
|
||||
@@ -413,6 +413,15 @@ 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())
|
||||
|
||||
@@ -31,7 +31,7 @@ func main() {
|
||||
log.Fatalf("logtail: bad -privateid: %v", err)
|
||||
}
|
||||
|
||||
logger := logtail.Log(logtail.Config{
|
||||
logger := logtail.NewLogger(logtail.Config{
|
||||
Collection: *collection,
|
||||
PrivateID: id,
|
||||
}, log.Printf)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
@@ -24,34 +25,6 @@ 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
|
||||
@@ -66,15 +39,16 @@ 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 autmatic uploading of new logs,
|
||||
// DrainLogs, if non-nil, disables automatic uploading of new logs,
|
||||
// so that logs are only uploaded when a token is sent to DrainLogs.
|
||||
DrainLogs <-chan struct{}
|
||||
}
|
||||
|
||||
func Log(cfg Config, logf tslogger.Logf) Logger {
|
||||
func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = "https://" + DefaultHost
|
||||
}
|
||||
@@ -94,8 +68,9 @@ func Log(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,
|
||||
@@ -122,8 +97,11 @@ func Log(cfg Config, logf tslogger.Logf) Logger {
|
||||
return l
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
// Logger writes logs, splitting them as configured between local
|
||||
// logging facilities and uploading to a log server.
|
||||
type Logger struct {
|
||||
stderr io.Writer
|
||||
stderrLevel int
|
||||
httpc *http.Client
|
||||
url string
|
||||
lowMem bool
|
||||
@@ -141,7 +119,22 @@ type logger struct {
|
||||
shutdownDone chan struct{} // closd when shutdown complete
|
||||
}
|
||||
|
||||
func (l *logger) Shutdown(ctx context.Context) error {
|
||||
// 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 {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
@@ -163,7 +156,11 @@ func (l *logger) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *logger) Close() {
|
||||
// Close shuts down this logger object, the background log uploader
|
||||
// process, and any associated goroutines.
|
||||
//
|
||||
// Deprecated: use Shutdown
|
||||
func (l *Logger) Close() {
|
||||
l.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
@@ -174,7 +171,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:
|
||||
@@ -193,7 +190,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
|
||||
|
||||
@@ -254,13 +251,22 @@ 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()
|
||||
if l.zstdEncoder != nil {
|
||||
body = l.zstdEncoder.EncodeAll(body, nil)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
for len(body) > 0 {
|
||||
@@ -269,7 +275,7 @@ func (l *logger) uploading(ctx context.Context) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
uploaded, err := l.upload(ctx, body)
|
||||
uploaded, err := l.upload(ctx, body, origlen)
|
||||
if err != nil {
|
||||
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
|
||||
}
|
||||
@@ -287,7 +293,10 @@ func (l *logger) uploading(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err error) {
|
||||
// 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) {
|
||||
req, err := http.NewRequest("POST", l.url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
// I know of no conditions under which this could fail.
|
||||
@@ -295,8 +304,9 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
|
||||
// TODO record logs to disk
|
||||
panic("logtail: cannot build http request: " + err.Error())
|
||||
}
|
||||
if l.zstdEncoder != nil {
|
||||
if origlen != -1 {
|
||||
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
|
||||
|
||||
@@ -306,7 +316,7 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
compressedNote := "not-compressed"
|
||||
if l.zstdEncoder != nil {
|
||||
if origlen != -1 {
|
||||
compressedNote = "compressed"
|
||||
}
|
||||
|
||||
@@ -333,11 +343,13 @@ func (l *logger) upload(ctx context.Context, body []byte) (uploaded bool, err er
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (l *logger) Flush() error {
|
||||
// Flush uploads all logs to the server.
|
||||
// It blocks until complete or there is an unrecoverable 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 {
|
||||
@@ -350,7 +362,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
|
||||
@@ -406,7 +418,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
|
||||
}
|
||||
@@ -447,11 +459,18 @@ func (l *logger) encode(buf []byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (l *logger) Write(buf []byte) (int, error) {
|
||||
// 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) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if l.stderr != nil && l.stderr != ioutil.Discard {
|
||||
level, buf := parseAndRemoveLogLevel(buf)
|
||||
if l.stderr != nil && l.stderr != ioutil.Discard && level <= l.stderrLevel {
|
||||
if buf[len(buf)-1] == '\n' {
|
||||
l.stderr.Write(buf)
|
||||
} else {
|
||||
@@ -465,3 +484,23 @@ 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
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestFastShutdown(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
l := Log(Config{
|
||||
l := NewLogger(Config{
|
||||
BaseURL: "http://localhost:1234",
|
||||
}, t.Logf)
|
||||
l.Shutdown(ctx)
|
||||
@@ -23,7 +23,7 @@ func TestFastShutdown(t *testing.T) {
|
||||
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)
|
||||
@@ -34,7 +34,7 @@ func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoggerWriteLength(t *testing.T) {
|
||||
lg := &logger{
|
||||
lg := &Logger{
|
||||
timeNow: time.Now,
|
||||
buffer: NewMemoryBuffer(1024),
|
||||
}
|
||||
|
||||
@@ -94,11 +94,6 @@ 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,6 +5,7 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
|
||||
"go4.org/mem"
|
||||
@@ -62,8 +63,12 @@ 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")
|
||||
|
||||
@@ -692,7 +692,7 @@ func (rs *reportState) probePortMapServices() {
|
||||
port1900 := netaddr.IPPort{IP: gw, Port: 1900}.UDPAddr()
|
||||
port5351 := netaddr.IPPort{IP: gw, Port: 5351}.UDPAddr()
|
||||
|
||||
rs.c.logf("probePortMapServices: me %v -> gw %v", myIP, gw)
|
||||
rs.c.logf("[v1] 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,6 +710,7 @@ 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 {
|
||||
@@ -727,11 +728,14 @@ func (rs *reportState) probePortMapServices() {
|
||||
if n == 60 && res[0] == 0x02 { // right length and version 2
|
||||
rs.setOptBool(&rs.report.PCP, 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,6 +751,7 @@ 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)
|
||||
@@ -754,17 +759,24 @@ 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[:])
|
||||
rand.Read(pkt[24 : 24+12])
|
||||
pkt[36] = udpProtoNumber
|
||||
binary.BigEndian.PutUint16(pkt[40:], uint16(mapToLocalPort))
|
||||
|
||||
// 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))
|
||||
v4unspec16 := v4unspec.As16()
|
||||
copy(pkt[40:], v4unspec16[:])
|
||||
copy(mapOp[20:], v4unspec16[:])
|
||||
return pkt
|
||||
}
|
||||
|
||||
@@ -830,7 +842,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
|
||||
ifState, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
c.logf("interfaces: %v", err)
|
||||
c.logf("[v1] interfaces: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -939,7 +951,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("netcheck: measuring HTTPS latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err)
|
||||
c.logf("[v1] netcheck: measuring HTTPS latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err)
|
||||
} else {
|
||||
rs.mu.Lock()
|
||||
rs.report.RegionLatency[reg.RegionID] = d
|
||||
@@ -1033,7 +1045,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
|
||||
}
|
||||
|
||||
func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) {
|
||||
c.logf("%v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
c.logf("[v1] report: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
fmt.Fprintf(w, "udp=%v", r.UDP)
|
||||
if !r.IPv4 {
|
||||
fmt.Fprintf(w, " v4=%v", r.IPv4)
|
||||
|
||||
@@ -554,7 +554,7 @@ 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 := buf.String(); got != tt.want {
|
||||
if got := strings.TrimPrefix(buf.String(), "[v1] report: "); got != tt.want {
|
||||
t.Errorf("unexpected result.\n got: %#q\nwant: %#q\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -93,7 +93,7 @@ func (t *Table) addEntries(fam int) error {
|
||||
}
|
||||
buf = buf[:size]
|
||||
|
||||
numEntries := *(*uint32)(unsafe.Pointer(&buf[0]))
|
||||
numEntries := endian.Native.Uint32(buf[:4])
|
||||
buf = buf[4:]
|
||||
|
||||
var recSize int
|
||||
|
||||
@@ -6,47 +6,11 @@ package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// IP4 is an IPv4 address.
|
||||
type IP4 uint32
|
||||
|
||||
// IPFromNetaddr converts a netaddr.IP to an IP4. Panics if !ip.Is4.
|
||||
func IP4FromNetaddr(ip netaddr.IP) IP4 {
|
||||
ipbytes := ip.As4()
|
||||
return IP4(binary.BigEndian.Uint32(ipbytes[:]))
|
||||
}
|
||||
|
||||
// Netaddr converts ip to a netaddr.IP.
|
||||
func (ip IP4) Netaddr() netaddr.IP {
|
||||
return netaddr.IPv4(byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
|
||||
}
|
||||
|
||||
func (ip IP4) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
|
||||
}
|
||||
|
||||
// IsMulticast returns whether ip is a multicast address.
|
||||
func (ip IP4) IsMulticast() bool {
|
||||
return byte(ip>>24)&0xf0 == 0xe0
|
||||
}
|
||||
|
||||
// IsLinkLocalUnicast returns whether ip is a link-local unicast
|
||||
// address.
|
||||
func (ip IP4) IsLinkLocalUnicast() bool {
|
||||
return byte(ip>>24) == 169 && byte(ip>>16) == 254
|
||||
}
|
||||
|
||||
// IsMostLinkLocalUnicast returns whether ip is a link-local unicast
|
||||
// address other than the magical "169.254.169.254" address used by
|
||||
// GCP DNS.
|
||||
func (ip IP4) IsMostLinkLocalUnicast() bool {
|
||||
return ip.IsLinkLocalUnicast() && ip != 0xA9FEA9FE
|
||||
}
|
||||
|
||||
// ip4HeaderLength is the length of an IPv4 header with no IP options.
|
||||
const ip4HeaderLength = 20
|
||||
|
||||
@@ -54,8 +18,8 @@ const ip4HeaderLength = 20
|
||||
type IP4Header struct {
|
||||
IPProto IPProto
|
||||
IPID uint16
|
||||
SrcIP IP4
|
||||
DstIP IP4
|
||||
Src netaddr.IP
|
||||
Dst netaddr.IP
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
@@ -63,6 +27,8 @@ 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() {
|
||||
@@ -71,6 +37,9 @@ func (h IP4Header) Marshal(buf []byte) error {
|
||||
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
|
||||
@@ -83,8 +52,10 @@ func (h IP4Header) Marshal(buf []byte) error {
|
||||
// it later, because the checksum computation runs over these
|
||||
// bytes and expects them to be zero.
|
||||
binary.BigEndian.PutUint16(buf[10:12], 0)
|
||||
binary.BigEndian.PutUint32(buf[12:16], uint32(h.SrcIP)) // Src
|
||||
binary.BigEndian.PutUint32(buf[16:20], uint32(h.DstIP)) // Dst
|
||||
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
|
||||
|
||||
@@ -93,7 +64,7 @@ func (h IP4Header) Marshal(buf []byte) error {
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *IP4Header) ToResponse() {
|
||||
h.SrcIP, h.DstIP = h.DstIP, h.SrcIP
|
||||
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
|
||||
}
|
||||
@@ -135,8 +106,9 @@ func (h IP4Header) marshalPseudo(buf []byte) error {
|
||||
}
|
||||
|
||||
length := len(buf) - h.Len()
|
||||
binary.BigEndian.PutUint32(buf[8:12], uint32(h.SrcIP))
|
||||
binary.BigEndian.PutUint32(buf[12:16], uint32(h.DstIP))
|
||||
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))
|
||||
|
||||
@@ -6,45 +6,10 @@ package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// IP6 is an IPv6 address.
|
||||
type IP6 struct {
|
||||
Hi, Lo uint64
|
||||
}
|
||||
|
||||
// IP6FromNetaddr converts a netaddr.IP to an IP6. Panics if !ip.Is6.
|
||||
func IP6FromNetaddr(ip netaddr.IP) IP6 {
|
||||
if !ip.Is6() {
|
||||
panic(fmt.Sprintf("IP6FromNetaddr called with non-v6 addr %q", ip))
|
||||
}
|
||||
b := ip.As16()
|
||||
return IP6{binary.BigEndian.Uint64(b[:8]), binary.BigEndian.Uint64(b[8:])}
|
||||
}
|
||||
|
||||
// Netaddr converts ip to a netaddr.IP.
|
||||
func (ip IP6) Netaddr() netaddr.IP {
|
||||
var b [16]byte
|
||||
binary.BigEndian.PutUint64(b[:8], ip.Hi)
|
||||
binary.BigEndian.PutUint64(b[8:], ip.Lo)
|
||||
return netaddr.IPFrom16(b)
|
||||
}
|
||||
|
||||
func (ip IP6) String() string {
|
||||
return ip.Netaddr().String()
|
||||
}
|
||||
|
||||
func (ip IP6) IsMulticast() bool {
|
||||
return (ip.Hi >> 56) == 0xFF
|
||||
}
|
||||
|
||||
func (ip IP6) IsLinkLocalUnicast() bool {
|
||||
return (ip.Hi >> 48) == 0xFE80
|
||||
}
|
||||
|
||||
// ip6HeaderLength is the length of an IPv6 header with no IP options.
|
||||
const ip6HeaderLength = 40
|
||||
|
||||
@@ -52,8 +17,8 @@ const ip6HeaderLength = 40
|
||||
type IP6Header struct {
|
||||
IPProto IPProto
|
||||
IPID uint32 // only lower 20 bits used
|
||||
SrcIP IP6
|
||||
DstIP IP6
|
||||
Src netaddr.IP
|
||||
Dst netaddr.IP
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
@@ -75,17 +40,16 @@ func (h IP6Header) Marshal(buf []byte) error {
|
||||
binary.BigEndian.PutUint16(buf[4:6], uint16(len(buf)-ip6HeaderLength)) // Total length
|
||||
buf[6] = uint8(h.IPProto) // Inner protocol
|
||||
buf[7] = 64 // TTL
|
||||
binary.BigEndian.PutUint64(buf[8:16], h.SrcIP.Hi)
|
||||
binary.BigEndian.PutUint64(buf[16:24], h.SrcIP.Lo)
|
||||
binary.BigEndian.PutUint64(buf[24:32], h.DstIP.Hi)
|
||||
binary.BigEndian.PutUint64(buf[32:40], h.DstIP.Lo)
|
||||
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.SrcIP, h.DstIP = h.DstIP, h.SrcIP
|
||||
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
|
||||
}
|
||||
@@ -100,10 +64,9 @@ func (h IP6Header) marshalPseudo(buf []byte) error {
|
||||
return errLargePacket
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint64(buf[:8], h.SrcIP.Hi)
|
||||
binary.BigEndian.PutUint64(buf[8:16], h.SrcIP.Lo)
|
||||
binary.BigEndian.PutUint64(buf[16:24], h.DstIP.Hi)
|
||||
binary.BigEndian.PutUint64(buf[24:32], h.DstIP.Lo)
|
||||
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
|
||||
|
||||
@@ -7,8 +7,10 @@ package packet
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/strbuilder"
|
||||
)
|
||||
|
||||
@@ -38,64 +40,50 @@ type Parsed struct {
|
||||
IPVersion uint8
|
||||
// IPProto is the IP subprotocol (UDP, TCP, etc.). Valid iff IPVersion != 0.
|
||||
IPProto IPProto
|
||||
// SrcIP4 is the IPv4 source address. Valid iff IPVersion == 4.
|
||||
SrcIP4 IP4
|
||||
// DstIP4 is the IPv4 destination address. Valid iff IPVersion == 4.
|
||||
DstIP4 IP4
|
||||
// SrcIP6 is the IPv6 source address. Valid iff IPVersion == 6.
|
||||
SrcIP6 IP6
|
||||
// DstIP6 is the IPv6 destination address. Valid iff IPVersion == 6.
|
||||
DstIP6 IP6
|
||||
// SrcPort is the TCP/UDP source port. Valid iff IPProto == TCP || IPProto == UDP.
|
||||
SrcPort uint16
|
||||
// DstPort is the TCP/UDP source port. Valid iff IPProto == TCP || IPProto == UDP.
|
||||
DstPort uint16
|
||||
// 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 uint8
|
||||
}
|
||||
|
||||
func (p *Parsed) String() string {
|
||||
switch p.IPVersion {
|
||||
case 4:
|
||||
sb := strbuilder.Get()
|
||||
sb.WriteString(p.IPProto.String())
|
||||
sb.WriteByte('{')
|
||||
writeIP4Port(sb, p.SrcIP4, p.SrcPort)
|
||||
sb.WriteString(" > ")
|
||||
writeIP4Port(sb, p.DstIP4, p.DstPort)
|
||||
sb.WriteByte('}')
|
||||
return sb.String()
|
||||
case 6:
|
||||
sb := strbuilder.Get()
|
||||
sb.WriteString(p.IPProto.String())
|
||||
sb.WriteByte('{')
|
||||
writeIP6Port(sb, p.SrcIP6, p.SrcPort)
|
||||
sb.WriteString(" > ")
|
||||
writeIP6Port(sb, p.DstIP6, p.DstPort)
|
||||
sb.WriteByte('}')
|
||||
return sb.String()
|
||||
default:
|
||||
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()
|
||||
}
|
||||
|
||||
func writeIP4Port(sb *strbuilder.Builder, ip IP4, port uint16) {
|
||||
sb.WriteUint(uint64(byte(ip >> 24)))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(byte(ip >> 16)))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(byte(ip >> 8)))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(byte(ip)))
|
||||
sb.WriteByte(':')
|
||||
sb.WriteUint(uint64(port))
|
||||
}
|
||||
|
||||
func writeIP6Port(sb *strbuilder.Builder, ip IP6, port uint16) {
|
||||
sb.WriteByte('[')
|
||||
sb.WriteString(ip.Netaddr().String()) // TODO: faster?
|
||||
sb.WriteString("]:")
|
||||
sb.WriteUint(uint64(port))
|
||||
// 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.
|
||||
@@ -140,8 +128,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
}
|
||||
|
||||
// If it's valid IPv4, then the IP addresses are valid
|
||||
q.SrcIP4 = IP4(binary.BigEndian.Uint32(b[12:16]))
|
||||
q.DstIP4 = IP4(binary.BigEndian.Uint32(b[16:20]))
|
||||
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 {
|
||||
@@ -183,8 +171,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = 0
|
||||
q.DstPort = 0
|
||||
q.Src.Port = 0
|
||||
q.Dst.Port = 0
|
||||
q.dataofs = q.subofs + icmp4HeaderLength
|
||||
return
|
||||
case IGMP:
|
||||
@@ -196,8 +184,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.TCPFlags = sub[13] & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
@@ -207,8 +195,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
return
|
||||
default:
|
||||
@@ -249,10 +237,10 @@ func (q *Parsed) decode6(b []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
q.SrcIP6.Hi = binary.BigEndian.Uint64(b[8:16])
|
||||
q.SrcIP6.Lo = binary.BigEndian.Uint64(b[16:24])
|
||||
q.DstIP6.Hi = binary.BigEndian.Uint64(b[24:32])
|
||||
q.DstIP6.Lo = binary.BigEndian.Uint64(b[32:40])
|
||||
// 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
|
||||
@@ -276,16 +264,16 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = 0
|
||||
q.DstPort = 0
|
||||
q.Src.Port = 0
|
||||
q.Dst.Port = 0
|
||||
q.dataofs = q.subofs + icmp6HeaderLength
|
||||
case TCP:
|
||||
if len(sub) < tcpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.TCPFlags = sub[13] & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
@@ -295,8 +283,8 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
@@ -312,8 +300,8 @@ func (q *Parsed) IP4Header() IP4Header {
|
||||
return IP4Header{
|
||||
IPID: ipid,
|
||||
IPProto: q.IPProto,
|
||||
SrcIP: q.SrcIP4,
|
||||
DstIP: q.DstIP4,
|
||||
Src: q.Src.IP,
|
||||
Dst: q.Dst.IP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,8 +322,8 @@ func (q *Parsed) UDP4Header() UDP4Header {
|
||||
}
|
||||
return UDP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
SrcPort: q.SrcPort,
|
||||
DstPort: q.DstPort,
|
||||
SrcPort: q.Src.Port,
|
||||
DstPort: q.Dst.Port,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,54 +12,12 @@ import (
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func mustIP4(s string) IP4 {
|
||||
ip, err := netaddr.ParseIP(s)
|
||||
func mustIPPort(s string) netaddr.IPPort {
|
||||
ipp, err := netaddr.ParseIPPort(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return IP4FromNetaddr(ip)
|
||||
}
|
||||
|
||||
func mustIP6(s string) IP6 {
|
||||
ip, err := netaddr.ParseIP(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return IP6FromNetaddr(ip)
|
||||
}
|
||||
|
||||
func TestIP4String(t *testing.T) {
|
||||
const str = "1.2.3.4"
|
||||
ip := mustIP4(str)
|
||||
|
||||
var got string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
got = ip.String()
|
||||
})
|
||||
|
||||
if got != str {
|
||||
t.Errorf("got %q; want %q", got, str)
|
||||
}
|
||||
if allocs != 1 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIP6String(t *testing.T) {
|
||||
const str = "2607:f8b0:400a:809::200e"
|
||||
ip := mustIP6(str)
|
||||
|
||||
var got string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
got = ip.String()
|
||||
})
|
||||
|
||||
if got != str {
|
||||
t.Errorf("got %q; want %q", got, str)
|
||||
}
|
||||
if allocs != 2 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
return ipp
|
||||
}
|
||||
|
||||
var icmp4RequestBuffer = []byte{
|
||||
@@ -83,10 +41,8 @@ var icmp4RequestDecode = Parsed{
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: ICMPv4,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 0,
|
||||
DstPort: 0,
|
||||
Src: mustIPPort("1.2.3.4:0"),
|
||||
Dst: mustIPPort("5.6.7.8:0"),
|
||||
}
|
||||
|
||||
var icmp4ReplyBuffer = []byte{
|
||||
@@ -109,10 +65,8 @@ var icmp4ReplyDecode = Parsed{
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: ICMPv4,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 0,
|
||||
DstPort: 0,
|
||||
Src: mustIPPort("1.2.3.4:0"),
|
||||
Dst: mustIPPort("5.6.7.8:0"),
|
||||
}
|
||||
|
||||
// ICMPv6 Router Solicitation
|
||||
@@ -132,8 +86,8 @@ var icmp6PacketDecode = Parsed{
|
||||
length: len(icmp6PacketBuffer),
|
||||
IPVersion: 6,
|
||||
IPProto: ICMPv6,
|
||||
SrcIP6: mustIP6("fe80::fb57:1dea:9c39:8fb7"),
|
||||
DstIP6: mustIP6("ff02::2"),
|
||||
Src: mustIPPort("[fe80::fb57:1dea:9c39:8fb7]:0"),
|
||||
Dst: mustIPPort("[ff02::2]:0"),
|
||||
}
|
||||
|
||||
// This is a malformed IPv4 packet.
|
||||
@@ -170,10 +124,8 @@ var tcp4PacketDecode = Parsed{
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: TCP,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 123,
|
||||
DstPort: 567,
|
||||
Src: mustIPPort("1.2.3.4:123"),
|
||||
Dst: mustIPPort("5.6.7.8:567"),
|
||||
TCPFlags: TCPSynAck,
|
||||
}
|
||||
|
||||
@@ -198,10 +150,8 @@ var tcp6RequestDecode = Parsed{
|
||||
|
||||
IPVersion: 6,
|
||||
IPProto: TCP,
|
||||
SrcIP6: mustIP6("2001:559:bc13:5400:1749:4628:3934:e1b"),
|
||||
DstIP6: mustIP6("2607:f8b0:400a:809::200e"),
|
||||
SrcPort: 42080,
|
||||
DstPort: 80,
|
||||
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:42080"),
|
||||
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:80"),
|
||||
TCPFlags: TCPSyn,
|
||||
}
|
||||
|
||||
@@ -226,10 +176,8 @@ var udp4RequestDecode = Parsed{
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: UDP,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 123,
|
||||
DstPort: 567,
|
||||
Src: mustIPPort("1.2.3.4:123"),
|
||||
Dst: mustIPPort("5.6.7.8:567"),
|
||||
}
|
||||
|
||||
var invalid4RequestBuffer = []byte{
|
||||
@@ -250,8 +198,8 @@ var invalid4RequestDecode = Parsed{
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: Unknown,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
Src: mustIPPort("1.2.3.4:0"),
|
||||
Dst: mustIPPort("5.6.7.8:0"),
|
||||
}
|
||||
|
||||
var udp6RequestBuffer = []byte{
|
||||
@@ -275,10 +223,8 @@ var udp6RequestDecode = Parsed{
|
||||
|
||||
IPVersion: 6,
|
||||
IPProto: UDP,
|
||||
SrcIP6: mustIP6("2001:559:bc13:5400:1749:4628:3934:e1b"),
|
||||
DstIP6: mustIP6("2607:f8b0:400a:809::200e"),
|
||||
SrcPort: 54276,
|
||||
DstPort: 443,
|
||||
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:54276"),
|
||||
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:443"),
|
||||
}
|
||||
|
||||
var udp4ReplyBuffer = []byte{
|
||||
@@ -301,10 +247,8 @@ var udp4ReplyDecode = Parsed{
|
||||
length: len(udp4ReplyBuffer),
|
||||
|
||||
IPProto: UDP,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 567,
|
||||
DstPort: 123,
|
||||
Src: mustIPPort("1.2.3.4:567"),
|
||||
Dst: mustIPPort("5.6.7.8:123"),
|
||||
}
|
||||
|
||||
var igmpPacketBuffer = []byte{
|
||||
@@ -326,8 +270,8 @@ var igmpPacketDecode = Parsed{
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: IGMP,
|
||||
SrcIP4: mustIP4("192.168.1.82"),
|
||||
DstIP4: mustIP4("224.0.0.251"),
|
||||
Src: mustIPPort("192.168.1.82:0"),
|
||||
Dst: mustIPPort("224.0.0.251:0"),
|
||||
}
|
||||
|
||||
func TestParsed(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,32 @@
|
||||
|
||||
package tsaddr
|
||||
|
||||
import "testing"
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromeOSVMRange(t *testing.T) {
|
||||
if got, want := ChromeOSVMRange().String(), "100.115.92.0/23"; got != want {
|
||||
|
||||
@@ -8,9 +8,8 @@ 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
|
||||
|
||||
@@ -12,11 +12,10 @@ 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
24
shell.nix
Normal file
24
shell.nix
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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 (c *WaitGroupChan) DoneChan() <-chan struct{} { return c.done }
|
||||
func (wg *WaitGroupChan) DoneChan() <-chan struct{} { return wg.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 (c *WaitGroupChan) DoneChan() <-chan struct{} { return c.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 (c *WaitGroupChan) Add(delta int) {
|
||||
n := atomic.AddInt64(&c.n, int64(delta))
|
||||
func (wg *WaitGroupChan) Add(delta int) {
|
||||
n := atomic.AddInt64(&wg.n, int64(delta))
|
||||
if n == 0 {
|
||||
close(c.done)
|
||||
close(wg.done)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
@@ -112,8 +111,6 @@ type User struct {
|
||||
Logins []LoginID
|
||||
Roles []RoleID
|
||||
Created time.Time
|
||||
|
||||
// Note: be sure to update Clone when adding new fields
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
@@ -145,20 +142,17 @@ type Node struct {
|
||||
KeyExpiry time.Time
|
||||
Machine MachineKey
|
||||
DiscoKey DiscoKey
|
||||
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
|
||||
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
|
||||
Hostinfo Hostinfo
|
||||
Created time.Time
|
||||
LastSeen *time.Time `json:",omitempty"`
|
||||
|
||||
KeepAlive bool // open and keep open a connection to this peer
|
||||
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
|
||||
|
||||
MachineAuthorized bool // TODO(crawshaw): replace with MachineStatus
|
||||
|
||||
// NOTE: any new fields containing pointers in this type
|
||||
// require changes to Node.Clone.
|
||||
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
|
||||
}
|
||||
|
||||
type MachineStatus int
|
||||
@@ -275,9 +269,6 @@ 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.
|
||||
@@ -287,21 +278,23 @@ 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 // 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"`
|
||||
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"`
|
||||
|
||||
// NOTE: any new fields containing pointers in this type
|
||||
// require changes to Hostinfo.Clone and Hostinfo.Equal.
|
||||
// require changes to Hostinfo.Equal.
|
||||
}
|
||||
|
||||
// NetInfo contains information about the host's network state.
|
||||
@@ -353,7 +346,7 @@ type NetInfo struct {
|
||||
// the control plane.
|
||||
DERPLatency map[string]float64 `json:",omitempty"`
|
||||
|
||||
// Update Clone and BasicallyEqual when adding fields.
|
||||
// Update BasicallyEqual when adding fields.
|
||||
}
|
||||
|
||||
func (ni *NetInfo) String() string {
|
||||
@@ -481,7 +474,10 @@ type MapRequest struct {
|
||||
// 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
|
||||
// 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
|
||||
Version int
|
||||
Compress string // "zstd" or "" (no compression)
|
||||
KeepAlive bool // whether server should send keep-alives back to us
|
||||
@@ -504,6 +500,10 @@ 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
|
||||
@@ -518,6 +518,8 @@ type MapRequest struct {
|
||||
// router but their IP forwarding is broken.
|
||||
// * "v6-overlay": IPv6 development flag to have control send
|
||||
// v6 node addrs
|
||||
// * "minimize-netmap": have control minimize the netmap, removing
|
||||
// peers that are unreachable per ACLS.
|
||||
DebugFlags []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -529,11 +531,11 @@ type PortRange struct {
|
||||
|
||||
var PortRangeAny = PortRange{0, 65535}
|
||||
|
||||
// NetPortRange represents a single subnet:portrange.
|
||||
// NetPortRange represents a range of ports that's allowed for one or more IPs.
|
||||
type NetPortRange struct {
|
||||
_ structs.Incomparable
|
||||
IP string // "*" means all
|
||||
Bits *int // backward compatibility: if missing, means "all" bits
|
||||
IP string // IP, CIDR, Range, or "*" (same formats as FilterRule.SrcIPs)
|
||||
Bits *int // deprecated; the old way to turn IP into a CIDR
|
||||
Ports PortRange
|
||||
}
|
||||
|
||||
@@ -544,18 +546,25 @@ type NetPortRange struct {
|
||||
// allowed if a source IP is mathces of those CIDRs.
|
||||
type FilterRule struct {
|
||||
// SrcIPs are the source IPs/networks to match.
|
||||
// The special value "*" means to match all.
|
||||
//
|
||||
// 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 values correspond to the SrcIPs above.
|
||||
// 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 present at the same index, 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.
|
||||
// TODO: for IPv6, clarify default bits length.
|
||||
// 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
|
||||
|
||||
// DstPorts are the port ranges to allow once a source IP
|
||||
@@ -600,14 +609,15 @@ 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 DeltaPeers was set in the request.
|
||||
// Subsequent responses will be delta-encoded if MapRequest.Version >= 5 and server
|
||||
// chooses, in which case Peers will be nil or zero length.
|
||||
// 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 only set if MapRequest.DeltaPeers was true.
|
||||
// HTTP response. It's not used by the server if MapRequest.Version < 5.
|
||||
// PeersChanged is always returned sorted by Node.ID.
|
||||
PeersChanged []*Node `json:",omitempty"`
|
||||
// PeersRemoved are the NodeIDs that are no longer in the peer list.
|
||||
@@ -616,18 +626,32 @@ type MapResponse struct {
|
||||
// DNS is the same as DNSConfig.Nameservers.
|
||||
//
|
||||
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
|
||||
DNS []wgcfg.IP `json:",omitempty"`
|
||||
DNS []netaddr.IP `json:",omitempty"`
|
||||
// SearchPaths are the same as DNSConfig.Domains.
|
||||
//
|
||||
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
|
||||
SearchPaths []string `json:",omitempty"`
|
||||
DNSConfig DNSConfig `json:",omitempty"`
|
||||
|
||||
// ACLs
|
||||
Domain string
|
||||
// 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
|
||||
|
||||
// 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).
|
||||
PacketFilter []FilterRule
|
||||
UserProfiles []UserProfile // as of 1.1.541: may be new or updated user profiles only
|
||||
|
||||
UserProfiles []UserProfile // as of 1.1.541 (mapver 5): may be new or updated user profiles only
|
||||
Roles []Role // deprecated; clients should not rely on Roles
|
||||
|
||||
// TODO: Groups []Group
|
||||
// TODO: Capabilities []Capability
|
||||
|
||||
@@ -755,7 +779,7 @@ func eqStrings(a, b []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func eqCIDRs(a, b []wgcfg.CIDR) bool {
|
||||
func eqCIDRs(a, b []netaddr.IPPrefix) bool {
|
||||
if len(a) != len(b) || ((a == nil) != (b == nil)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
package tailcfg
|
||||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
@@ -69,8 +68,8 @@ var _NodeNeedsRegeneration = Node(struct {
|
||||
KeyExpiry time.Time
|
||||
Machine MachineKey
|
||||
DiscoKey DiscoKey
|
||||
Addresses []wgcfg.CIDR
|
||||
AllowedIPs []wgcfg.CIDR
|
||||
Addresses []netaddr.IPPrefix
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints []string
|
||||
DERP string
|
||||
Hostinfo Hostinfo
|
||||
@@ -105,8 +104,10 @@ var _HostinfoNeedsRegeneration = Hostinfo(struct {
|
||||
OSVersion string
|
||||
DeviceModel string
|
||||
Hostname string
|
||||
ShieldsUp bool
|
||||
ShareeNode bool
|
||||
GoArch string
|
||||
RoutableIPs []wgcfg.CIDR
|
||||
RoutableIPs []netaddr.IPPrefix
|
||||
RequestTags []string
|
||||
Services []Service
|
||||
NetInfo *NetInfo
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
@@ -23,18 +24,21 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
|
||||
func TestHostinfoEqual(t *testing.T) {
|
||||
hiHandles := []string{
|
||||
"IPNVersion", "FrontendLogID", "BackendLogID", "OS", "OSVersion",
|
||||
"DeviceModel", "Hostname", "GoArch", "RoutableIPs", "RequestTags", "Services",
|
||||
"NetInfo",
|
||||
"IPNVersion", "FrontendLogID", "BackendLogID",
|
||||
"OS", "OSVersion", "DeviceModel", "Hostname",
|
||||
"ShieldsUp", "ShareeNode",
|
||||
"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 []wgcfg.CIDR) {
|
||||
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
|
||||
for _, s := range strs {
|
||||
n, err := wgcfg.ParseCIDR(s)
|
||||
n, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -169,6 +173,11 @@ 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)
|
||||
@@ -185,9 +194,9 @@ func TestNodeEqual(t *testing.T) {
|
||||
have, nodeHandles)
|
||||
}
|
||||
|
||||
newPublicKey := func(t *testing.T) wgcfg.Key {
|
||||
newPublicKey := func(t *testing.T) wgkey.Key {
|
||||
t.Helper()
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
k, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -256,23 +265,23 @@ func TestNodeEqual(t *testing.T) {
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
&Node{Addresses: nil},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{AllowedIPs: []wgcfg.CIDR{}},
|
||||
&Node{AllowedIPs: []netaddr.IPPrefix{}},
|
||||
&Node{AllowedIPs: nil},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
&Node{Addresses: []wgcfg.CIDR{}},
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
&Node{Addresses: []netaddr.IPPrefix{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
@@ -421,8 +430,8 @@ func TestCloneNode(t *testing.T) {
|
||||
}{
|
||||
{"nil_fields", &Node{}},
|
||||
{"zero_fields", &Node{
|
||||
Addresses: make([]wgcfg.CIDR, 0),
|
||||
AllowedIPs: make([]wgcfg.CIDR, 0),
|
||||
Addresses: make([]netaddr.IPPrefix, 0),
|
||||
AllowedIPs: make([]netaddr.IPPrefix, 0),
|
||||
Endpoints: make([]string, 0),
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
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)
|
||||
}
|
||||
```
|
||||
@@ -1,23 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
@@ -1,797 +0,0 @@
|
||||
// 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"))
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// 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}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// 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}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// 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}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// 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}
|
||||
}
|
||||
@@ -44,11 +44,12 @@ func AddHandlers(mux *http.ServeMux) {
|
||||
func Cmdline(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
fmt.Fprintf(w, strings.Join(os.Args, "\x00"))
|
||||
fmt.Fprint(w, strings.Join(os.Args, "\x00"))
|
||||
}
|
||||
|
||||
func sleep(w http.ResponseWriter, d time.Duration) {
|
||||
var clientGone <-chan bool
|
||||
//lint:ignore SA1019 CloseNotifier is deprecated but it functions and this is a temporary fork
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
clientGone = cn.CloseNotify()
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// 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 windows
|
||||
|
||||
package registry
|
||||
|
||||
func (k Key) SetValue(name string, valtype uint32, data []byte) error {
|
||||
return k.setValue(name, valtype, data)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package registry_test
|
||||
|
||||
// Tailscale's redo-based build system doesn't know how to skip running Go tests
|
||||
// in directories that don't contain files for the current OS.
|
||||
//
|
||||
// https://github.com/tailscale/corp/issues/293
|
||||
//
|
||||
// So this is a dummy file for now to make it happy.
|
||||
@@ -1,204 +0,0 @@
|
||||
// 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 windows
|
||||
|
||||
// Package registry provides access to the Windows registry.
|
||||
//
|
||||
// Here is a simple example, opening a registry key and reading a string value from it.
|
||||
//
|
||||
// k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer k.Close()
|
||||
//
|
||||
// s, _, err := k.GetStringValue("SystemRoot")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Windows system root is %q\n", s)
|
||||
//
|
||||
package registry
|
||||
|
||||
import (
|
||||
"io"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Registry key security and access rights.
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724878.aspx
|
||||
// for details.
|
||||
ALL_ACCESS = 0xf003f
|
||||
CREATE_LINK = 0x00020
|
||||
CREATE_SUB_KEY = 0x00004
|
||||
ENUMERATE_SUB_KEYS = 0x00008
|
||||
EXECUTE = 0x20019
|
||||
NOTIFY = 0x00010
|
||||
QUERY_VALUE = 0x00001
|
||||
READ = 0x20019
|
||||
SET_VALUE = 0x00002
|
||||
WOW64_32KEY = 0x00200
|
||||
WOW64_64KEY = 0x00100
|
||||
WRITE = 0x20006
|
||||
)
|
||||
|
||||
// Key is a handle to an open Windows registry key.
|
||||
// Keys can be obtained by calling OpenKey; there are
|
||||
// also some predefined root keys such as CURRENT_USER.
|
||||
// Keys can be used directly in the Windows API.
|
||||
type Key syscall.Handle
|
||||
|
||||
const (
|
||||
// Windows defines some predefined root keys that are always open.
|
||||
// An application can use these keys as entry points to the registry.
|
||||
// Normally these keys are used in OpenKey to open new keys,
|
||||
// but they can also be used anywhere a Key is required.
|
||||
CLASSES_ROOT = Key(syscall.HKEY_CLASSES_ROOT)
|
||||
CURRENT_USER = Key(syscall.HKEY_CURRENT_USER)
|
||||
LOCAL_MACHINE = Key(syscall.HKEY_LOCAL_MACHINE)
|
||||
USERS = Key(syscall.HKEY_USERS)
|
||||
CURRENT_CONFIG = Key(syscall.HKEY_CURRENT_CONFIG)
|
||||
PERFORMANCE_DATA = Key(syscall.HKEY_PERFORMANCE_DATA)
|
||||
)
|
||||
|
||||
// Close closes open key k.
|
||||
func (k Key) Close() error {
|
||||
return syscall.RegCloseKey(syscall.Handle(k))
|
||||
}
|
||||
|
||||
// WaitChange waits for k to change using RegNotifyChangeKeyValue.
|
||||
// The subtree parameter is whether subtrees should also be watched.
|
||||
func (k Key) WaitChange(subtree bool) error {
|
||||
return regNotifyChangeKeyValue(syscall.Handle(k), subtree, 0, 0, false)
|
||||
}
|
||||
|
||||
// OpenKey opens a new key with path name relative to key k.
|
||||
// It accepts any open key, including CURRENT_USER and others,
|
||||
// and returns the new key and an error.
|
||||
// The access parameter specifies desired access rights to the
|
||||
// key to be opened.
|
||||
func OpenKey(k Key, path string, access uint32) (Key, error) {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var subkey syscall.Handle
|
||||
err = syscall.RegOpenKeyEx(syscall.Handle(k), p, 0, access, &subkey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return Key(subkey), nil
|
||||
}
|
||||
|
||||
// OpenRemoteKey opens a predefined registry key on another
|
||||
// computer pcname. The key to be opened is specified by k, but
|
||||
// can only be one of LOCAL_MACHINE, PERFORMANCE_DATA or USERS.
|
||||
// If pcname is "", OpenRemoteKey returns local computer key.
|
||||
func OpenRemoteKey(pcname string, k Key) (Key, error) {
|
||||
var err error
|
||||
var p *uint16
|
||||
if pcname != "" {
|
||||
p, err = syscall.UTF16PtrFromString(`\\` + pcname)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
var remoteKey syscall.Handle
|
||||
err = regConnectRegistry(p, syscall.Handle(k), &remoteKey)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return Key(remoteKey), nil
|
||||
}
|
||||
|
||||
// ReadSubKeyNames returns the names of subkeys of key k.
|
||||
// The parameter n controls the number of returned names,
|
||||
// analogous to the way os.File.Readdirnames works.
|
||||
func (k Key) ReadSubKeyNames(n int) ([]string, error) {
|
||||
names := make([]string, 0)
|
||||
// Registry key size limit is 255 bytes and described there:
|
||||
// https://msdn.microsoft.com/library/windows/desktop/ms724872.aspx
|
||||
buf := make([]uint16, 256) //plus extra room for terminating zero byte
|
||||
loopItems:
|
||||
for i := uint32(0); ; i++ {
|
||||
if n > 0 {
|
||||
if len(names) == n {
|
||||
return names, nil
|
||||
}
|
||||
}
|
||||
l := uint32(len(buf))
|
||||
for {
|
||||
err := syscall.RegEnumKeyEx(syscall.Handle(k), i, &buf[0], &l, nil, nil, nil, nil)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if err == syscall.ERROR_MORE_DATA {
|
||||
// Double buffer size and try again.
|
||||
l = uint32(2 * len(buf))
|
||||
buf = make([]uint16, l)
|
||||
continue
|
||||
}
|
||||
if err == _ERROR_NO_MORE_ITEMS {
|
||||
break loopItems
|
||||
}
|
||||
return names, err
|
||||
}
|
||||
names = append(names, syscall.UTF16ToString(buf[:l]))
|
||||
}
|
||||
if n > len(names) {
|
||||
return names, io.EOF
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// CreateKey creates a key named path under open key k.
|
||||
// CreateKey returns the new key and a boolean flag that reports
|
||||
// whether the key already existed.
|
||||
// The access parameter specifies the access rights for the key
|
||||
// to be created.
|
||||
func CreateKey(k Key, path string, access uint32) (newk Key, openedExisting bool, err error) {
|
||||
var h syscall.Handle
|
||||
var d uint32
|
||||
err = regCreateKeyEx(syscall.Handle(k), syscall.StringToUTF16Ptr(path),
|
||||
0, nil, _REG_OPTION_NON_VOLATILE, access, nil, &h, &d)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return Key(h), d == _REG_OPENED_EXISTING_KEY, nil
|
||||
}
|
||||
|
||||
// DeleteKey deletes the subkey path of key k and its values.
|
||||
func DeleteKey(k Key, path string) error {
|
||||
return regDeleteKey(syscall.Handle(k), syscall.StringToUTF16Ptr(path))
|
||||
}
|
||||
|
||||
// A KeyInfo describes the statistics of a key. It is returned by Stat.
|
||||
type KeyInfo struct {
|
||||
SubKeyCount uint32
|
||||
MaxSubKeyLen uint32 // size of the key's subkey with the longest name, in Unicode characters, not including the terminating zero byte
|
||||
ValueCount uint32
|
||||
MaxValueNameLen uint32 // size of the key's longest value name, in Unicode characters, not including the terminating zero byte
|
||||
MaxValueLen uint32 // longest data component among the key's values, in bytes
|
||||
lastWriteTime syscall.Filetime
|
||||
}
|
||||
|
||||
// ModTime returns the key's last write time.
|
||||
func (ki *KeyInfo) ModTime() time.Time {
|
||||
return time.Unix(0, ki.lastWriteTime.Nanoseconds())
|
||||
}
|
||||
|
||||
// Stat retrieves information about the open key k.
|
||||
func (k Key) Stat() (*KeyInfo, error) {
|
||||
var ki KeyInfo
|
||||
err := syscall.RegQueryInfoKey(syscall.Handle(k), nil, nil, nil,
|
||||
&ki.SubKeyCount, &ki.MaxSubKeyLen, nil, &ki.ValueCount,
|
||||
&ki.MaxValueNameLen, &ki.MaxValueLen, nil, &ki.lastWriteTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ki, nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// 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 generate
|
||||
|
||||
package registry
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall.go
|
||||
@@ -1,701 +0,0 @@
|
||||
// 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 windows
|
||||
|
||||
package registry_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"tailscale.com/tempfork/registry"
|
||||
)
|
||||
|
||||
func randKeyName(prefix string) string {
|
||||
const numbers = "0123456789"
|
||||
buf := make([]byte, 10)
|
||||
rand.Read(buf)
|
||||
for i, b := range buf {
|
||||
buf[i] = numbers[b%byte(len(numbers))]
|
||||
}
|
||||
return prefix + string(buf)
|
||||
}
|
||||
|
||||
func TestReadSubKeyNames(t *testing.T) {
|
||||
k, err := registry.OpenKey(registry.CLASSES_ROOT, "TypeLib", registry.ENUMERATE_SUB_KEYS)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
names, err := k.ReadSubKeyNames(-1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var foundStdOle bool
|
||||
for _, name := range names {
|
||||
// Every PC has "stdole 2.0 OLE Automation" library installed.
|
||||
if name == "{00020430-0000-0000-C000-000000000046}" {
|
||||
foundStdOle = true
|
||||
}
|
||||
}
|
||||
if !foundStdOle {
|
||||
t.Fatal("could not find stdole 2.0 OLE Automation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOpenDeleteKey(t *testing.T) {
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, "Software", registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
testKName := randKeyName("TestCreateOpenDeleteKey_")
|
||||
|
||||
testK, exist, err := registry.CreateKey(k, testKName, registry.CREATE_SUB_KEY)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer testK.Close()
|
||||
|
||||
if exist {
|
||||
t.Fatalf("key %q already exists", testKName)
|
||||
}
|
||||
|
||||
testKAgain, exist, err := registry.CreateKey(k, testKName, registry.CREATE_SUB_KEY)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer testKAgain.Close()
|
||||
|
||||
if !exist {
|
||||
t.Fatalf("key %q should already exist", testKName)
|
||||
}
|
||||
|
||||
testKOpened, err := registry.OpenKey(k, testKName, registry.ENUMERATE_SUB_KEYS)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer testKOpened.Close()
|
||||
|
||||
err = registry.DeleteKey(k, testKName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testKOpenedAgain, err := registry.OpenKey(k, testKName, registry.ENUMERATE_SUB_KEYS)
|
||||
if err == nil {
|
||||
defer testKOpenedAgain.Close()
|
||||
t.Fatalf("key %q should already been deleted", testKName)
|
||||
}
|
||||
if err != registry.ErrNotExist {
|
||||
t.Fatalf(`unexpected error ("not exist" expected): %v`, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, "Software", registry.QUERY_VALUE|registry.WRITE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
testKName := randKeyName("TestWatch_")
|
||||
testK, _, err := registry.CreateKey(k, testKName, registry.CREATE_SUB_KEY|registry.NOTIFY|registry.WRITE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer testK.Close()
|
||||
|
||||
timer := time.AfterFunc(100*time.Millisecond, func() {
|
||||
err := registry.DeleteKey(k, testKName)
|
||||
t.Logf("DeleteKey: %v", err)
|
||||
})
|
||||
defer timer.Stop()
|
||||
t.Logf("pre-wait")
|
||||
t0 := time.Now()
|
||||
err = testK.WaitChange(true)
|
||||
t.Logf("WaitChange after %v: %v", time.Since(t0).Round(time.Millisecond), err)
|
||||
}
|
||||
|
||||
func equalStringSlice(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
if a == nil {
|
||||
return true
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type ValueTest struct {
|
||||
Type uint32
|
||||
Name string
|
||||
Value interface{}
|
||||
WillFail bool
|
||||
}
|
||||
|
||||
var ValueTests = []ValueTest{
|
||||
{Type: registry.SZ, Name: "String1", Value: ""},
|
||||
{Type: registry.SZ, Name: "String2", Value: "\000", WillFail: true},
|
||||
{Type: registry.SZ, Name: "String3", Value: "Hello World"},
|
||||
{Type: registry.SZ, Name: "String4", Value: "Hello World\000", WillFail: true},
|
||||
{Type: registry.EXPAND_SZ, Name: "ExpString1", Value: ""},
|
||||
{Type: registry.EXPAND_SZ, Name: "ExpString2", Value: "\000", WillFail: true},
|
||||
{Type: registry.EXPAND_SZ, Name: "ExpString3", Value: "Hello World"},
|
||||
{Type: registry.EXPAND_SZ, Name: "ExpString4", Value: "Hello\000World", WillFail: true},
|
||||
{Type: registry.EXPAND_SZ, Name: "ExpString5", Value: "%PATH%"},
|
||||
{Type: registry.EXPAND_SZ, Name: "ExpString6", Value: "%NO_SUCH_VARIABLE%"},
|
||||
{Type: registry.EXPAND_SZ, Name: "ExpString7", Value: "%PATH%;."},
|
||||
{Type: registry.BINARY, Name: "Binary1", Value: []byte{}},
|
||||
{Type: registry.BINARY, Name: "Binary2", Value: []byte{1, 2, 3}},
|
||||
{Type: registry.BINARY, Name: "Binary3", Value: []byte{3, 2, 1, 0, 1, 2, 3}},
|
||||
{Type: registry.DWORD, Name: "Dword1", Value: uint64(0)},
|
||||
{Type: registry.DWORD, Name: "Dword2", Value: uint64(1)},
|
||||
{Type: registry.DWORD, Name: "Dword3", Value: uint64(0xff)},
|
||||
{Type: registry.DWORD, Name: "Dword4", Value: uint64(0xffff)},
|
||||
{Type: registry.QWORD, Name: "Qword1", Value: uint64(0)},
|
||||
{Type: registry.QWORD, Name: "Qword2", Value: uint64(1)},
|
||||
{Type: registry.QWORD, Name: "Qword3", Value: uint64(0xff)},
|
||||
{Type: registry.QWORD, Name: "Qword4", Value: uint64(0xffff)},
|
||||
{Type: registry.QWORD, Name: "Qword5", Value: uint64(0xffffff)},
|
||||
{Type: registry.QWORD, Name: "Qword6", Value: uint64(0xffffffff)},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString1", Value: []string{"a", "b", "c"}},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString2", Value: []string{"abc", "", "cba"}},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString3", Value: []string{""}},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString4", Value: []string{"abcdef"}},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString5", Value: []string{"\000"}, WillFail: true},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString6", Value: []string{"a\000b"}, WillFail: true},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString7", Value: []string{"ab", "\000", "cd"}, WillFail: true},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString8", Value: []string{"\000", "cd"}, WillFail: true},
|
||||
{Type: registry.MULTI_SZ, Name: "MultiString9", Value: []string{"ab", "\000"}, WillFail: true},
|
||||
}
|
||||
|
||||
func setValues(t *testing.T, k registry.Key) {
|
||||
for _, test := range ValueTests {
|
||||
var err error
|
||||
switch test.Type {
|
||||
case registry.SZ:
|
||||
err = k.SetStringValue(test.Name, test.Value.(string))
|
||||
case registry.EXPAND_SZ:
|
||||
err = k.SetExpandStringValue(test.Name, test.Value.(string))
|
||||
case registry.MULTI_SZ:
|
||||
err = k.SetStringsValue(test.Name, test.Value.([]string))
|
||||
case registry.BINARY:
|
||||
err = k.SetBinaryValue(test.Name, test.Value.([]byte))
|
||||
case registry.DWORD:
|
||||
err = k.SetDWordValue(test.Name, uint32(test.Value.(uint64)))
|
||||
case registry.QWORD:
|
||||
err = k.SetQWordValue(test.Name, test.Value.(uint64))
|
||||
default:
|
||||
t.Fatalf("unsupported type %d for %s value", test.Type, test.Name)
|
||||
}
|
||||
if test.WillFail {
|
||||
if err == nil {
|
||||
t.Fatalf("setting %s value %q should fail, but succeeded", test.Name, test.Value)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enumerateValues(t *testing.T, k registry.Key) {
|
||||
names, err := k.ReadValueNames(-1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
haveNames := make(map[string]bool)
|
||||
for _, n := range names {
|
||||
haveNames[n] = false
|
||||
}
|
||||
for _, test := range ValueTests {
|
||||
wantFound := !test.WillFail
|
||||
_, haveFound := haveNames[test.Name]
|
||||
if wantFound && !haveFound {
|
||||
t.Errorf("value %s is not found while enumerating", test.Name)
|
||||
}
|
||||
if haveFound && !wantFound {
|
||||
t.Errorf("value %s is found while enumerating, but expected to fail", test.Name)
|
||||
}
|
||||
if haveFound {
|
||||
delete(haveNames, test.Name)
|
||||
}
|
||||
}
|
||||
for n, v := range haveNames {
|
||||
t.Errorf("value %s (%v) is found while enumerating, but has not been cretaed", n, v)
|
||||
}
|
||||
}
|
||||
|
||||
func testErrNotExist(t *testing.T, name string, err error) {
|
||||
if err == nil {
|
||||
t.Errorf("%s value should not exist", name)
|
||||
return
|
||||
}
|
||||
if err != registry.ErrNotExist {
|
||||
t.Errorf("reading %s value should return 'not exist' error, but got: %s", name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testErrUnexpectedType(t *testing.T, test ValueTest, gottype uint32, err error) {
|
||||
if err == nil {
|
||||
t.Errorf("GetXValue(%q) should not succeed", test.Name)
|
||||
return
|
||||
}
|
||||
if err != registry.ErrUnexpectedType {
|
||||
t.Errorf("reading %s value should return 'unexpected key value type' error, but got: %s", test.Name, err)
|
||||
return
|
||||
}
|
||||
if gottype != test.Type {
|
||||
t.Errorf("want %s value type %v, got %v", test.Name, test.Type, gottype)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testGetStringValue(t *testing.T, k registry.Key, test ValueTest) {
|
||||
got, gottype, err := k.GetStringValue(test.Name)
|
||||
if err != nil {
|
||||
t.Errorf("GetStringValue(%s) failed: %v", test.Name, err)
|
||||
return
|
||||
}
|
||||
if got != test.Value {
|
||||
t.Errorf("want %s value %q, got %q", test.Name, test.Value, got)
|
||||
return
|
||||
}
|
||||
if gottype != test.Type {
|
||||
t.Errorf("want %s value type %v, got %v", test.Name, test.Type, gottype)
|
||||
return
|
||||
}
|
||||
if gottype == registry.EXPAND_SZ {
|
||||
_, err = registry.ExpandString(got)
|
||||
if err != nil {
|
||||
t.Errorf("ExpandString(%s) failed: %v", got, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetIntegerValue(t *testing.T, k registry.Key, test ValueTest) {
|
||||
got, gottype, err := k.GetIntegerValue(test.Name)
|
||||
if err != nil {
|
||||
t.Errorf("GetIntegerValue(%s) failed: %v", test.Name, err)
|
||||
return
|
||||
}
|
||||
if got != test.Value.(uint64) {
|
||||
t.Errorf("want %s value %v, got %v", test.Name, test.Value, got)
|
||||
return
|
||||
}
|
||||
if gottype != test.Type {
|
||||
t.Errorf("want %s value type %v, got %v", test.Name, test.Type, gottype)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testGetBinaryValue(t *testing.T, k registry.Key, test ValueTest) {
|
||||
got, gottype, err := k.GetBinaryValue(test.Name)
|
||||
if err != nil {
|
||||
t.Errorf("GetBinaryValue(%s) failed: %v", test.Name, err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, test.Value.([]byte)) {
|
||||
t.Errorf("want %s value %v, got %v", test.Name, test.Value, got)
|
||||
return
|
||||
}
|
||||
if gottype != test.Type {
|
||||
t.Errorf("want %s value type %v, got %v", test.Name, test.Type, gottype)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testGetStringsValue(t *testing.T, k registry.Key, test ValueTest) {
|
||||
got, gottype, err := k.GetStringsValue(test.Name)
|
||||
if err != nil {
|
||||
t.Errorf("GetStringsValue(%s) failed: %v", test.Name, err)
|
||||
return
|
||||
}
|
||||
if !equalStringSlice(got, test.Value.([]string)) {
|
||||
t.Errorf("want %s value %#v, got %#v", test.Name, test.Value, got)
|
||||
return
|
||||
}
|
||||
if gottype != test.Type {
|
||||
t.Errorf("want %s value type %v, got %v", test.Name, test.Type, gottype)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testGetValue(t *testing.T, k registry.Key, test ValueTest, size int) {
|
||||
if size <= 0 {
|
||||
return
|
||||
}
|
||||
// read data with no buffer
|
||||
gotsize, gottype, err := k.GetValue(test.Name, nil)
|
||||
if err != nil {
|
||||
t.Errorf("GetValue(%s, [%d]byte) failed: %v", test.Name, size, err)
|
||||
return
|
||||
}
|
||||
if gotsize != size {
|
||||
t.Errorf("want %s value size of %d, got %v", test.Name, size, gotsize)
|
||||
return
|
||||
}
|
||||
if gottype != test.Type {
|
||||
t.Errorf("want %s value type %v, got %v", test.Name, test.Type, gottype)
|
||||
return
|
||||
}
|
||||
// read data with short buffer
|
||||
gotsize, gottype, err = k.GetValue(test.Name, make([]byte, size-1))
|
||||
if err == nil {
|
||||
t.Errorf("GetValue(%s, [%d]byte) should fail, but succeeded", test.Name, size-1)
|
||||
return
|
||||
}
|
||||
if err != registry.ErrShortBuffer {
|
||||
t.Errorf("reading %s value should return 'short buffer' error, but got: %s", test.Name, err)
|
||||
return
|
||||
}
|
||||
if gotsize != size {
|
||||
t.Errorf("want %s value size of %d, got %v", test.Name, size, gotsize)
|
||||
return
|
||||
}
|
||||
if gottype != test.Type {
|
||||
t.Errorf("want %s value type %v, got %v", test.Name, test.Type, gottype)
|
||||
return
|
||||
}
|
||||
// read full data
|
||||
gotsize, gottype, err = k.GetValue(test.Name, make([]byte, size))
|
||||
if err != nil {
|
||||
t.Errorf("GetValue(%s, [%d]byte) failed: %v", test.Name, size, err)
|
||||
return
|
||||
}
|
||||
if gotsize != size {
|
||||
t.Errorf("want %s value size of %d, got %v", test.Name, size, gotsize)
|
||||
return
|
||||
}
|
||||
if gottype != test.Type {
|
||||
t.Errorf("want %s value type %v, got %v", test.Name, test.Type, gottype)
|
||||
return
|
||||
}
|
||||
// check GetValue returns ErrNotExist as required
|
||||
_, _, err = k.GetValue(test.Name+"_not_there", make([]byte, size))
|
||||
if err == nil {
|
||||
t.Errorf("GetValue(%q) should not succeed", test.Name)
|
||||
return
|
||||
}
|
||||
if err != registry.ErrNotExist {
|
||||
t.Errorf("GetValue(%q) should return 'not exist' error, but got: %s", test.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testValues(t *testing.T, k registry.Key) {
|
||||
for _, test := range ValueTests {
|
||||
switch test.Type {
|
||||
case registry.SZ, registry.EXPAND_SZ:
|
||||
if test.WillFail {
|
||||
_, _, err := k.GetStringValue(test.Name)
|
||||
testErrNotExist(t, test.Name, err)
|
||||
} else {
|
||||
testGetStringValue(t, k, test)
|
||||
_, gottype, err := k.GetIntegerValue(test.Name)
|
||||
testErrUnexpectedType(t, test, gottype, err)
|
||||
// Size of utf16 string in bytes is not perfect,
|
||||
// but correct for current test values.
|
||||
// Size also includes terminating 0.
|
||||
testGetValue(t, k, test, (len(test.Value.(string))+1)*2)
|
||||
}
|
||||
_, _, err := k.GetStringValue(test.Name + "_string_not_created")
|
||||
testErrNotExist(t, test.Name+"_string_not_created", err)
|
||||
case registry.DWORD, registry.QWORD:
|
||||
testGetIntegerValue(t, k, test)
|
||||
_, gottype, err := k.GetBinaryValue(test.Name)
|
||||
testErrUnexpectedType(t, test, gottype, err)
|
||||
_, _, err = k.GetIntegerValue(test.Name + "_int_not_created")
|
||||
testErrNotExist(t, test.Name+"_int_not_created", err)
|
||||
size := 8
|
||||
if test.Type == registry.DWORD {
|
||||
size = 4
|
||||
}
|
||||
testGetValue(t, k, test, size)
|
||||
case registry.BINARY:
|
||||
testGetBinaryValue(t, k, test)
|
||||
_, gottype, err := k.GetStringsValue(test.Name)
|
||||
testErrUnexpectedType(t, test, gottype, err)
|
||||
_, _, err = k.GetBinaryValue(test.Name + "_byte_not_created")
|
||||
testErrNotExist(t, test.Name+"_byte_not_created", err)
|
||||
testGetValue(t, k, test, len(test.Value.([]byte)))
|
||||
case registry.MULTI_SZ:
|
||||
if test.WillFail {
|
||||
_, _, err := k.GetStringsValue(test.Name)
|
||||
testErrNotExist(t, test.Name, err)
|
||||
} else {
|
||||
testGetStringsValue(t, k, test)
|
||||
_, gottype, err := k.GetStringValue(test.Name)
|
||||
testErrUnexpectedType(t, test, gottype, err)
|
||||
size := 0
|
||||
for _, s := range test.Value.([]string) {
|
||||
size += len(s) + 1 // nil terminated
|
||||
}
|
||||
size += 1 // extra nil at the end
|
||||
size *= 2 // count bytes, not uint16
|
||||
testGetValue(t, k, test, size)
|
||||
}
|
||||
_, _, err := k.GetStringsValue(test.Name + "_strings_not_created")
|
||||
testErrNotExist(t, test.Name+"_strings_not_created", err)
|
||||
default:
|
||||
t.Errorf("unsupported type %d for %s value", test.Type, test.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testStat(t *testing.T, k registry.Key) {
|
||||
subk, _, err := registry.CreateKey(k, "subkey", registry.CREATE_SUB_KEY)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer subk.Close()
|
||||
|
||||
defer registry.DeleteKey(k, "subkey")
|
||||
|
||||
ki, err := k.Stat()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if ki.SubKeyCount != 1 {
|
||||
t.Error("key must have 1 subkey")
|
||||
}
|
||||
if ki.MaxSubKeyLen != 6 {
|
||||
t.Error("key max subkey name length must be 6")
|
||||
}
|
||||
if ki.ValueCount != 24 {
|
||||
t.Errorf("key must have 24 values, but is %d", ki.ValueCount)
|
||||
}
|
||||
if ki.MaxValueNameLen != 12 {
|
||||
t.Errorf("key max value name length must be 10, but is %d", ki.MaxValueNameLen)
|
||||
}
|
||||
if ki.MaxValueLen != 38 {
|
||||
t.Errorf("key max value length must be 38, but is %d", ki.MaxValueLen)
|
||||
}
|
||||
if mt, ct := ki.ModTime(), time.Now(); ct.Sub(mt) > 100*time.Millisecond {
|
||||
t.Errorf("key mod time is not close to current time: mtime=%v current=%v delta=%v", mt, ct, ct.Sub(mt))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteValues(t *testing.T, k registry.Key) {
|
||||
for _, test := range ValueTests {
|
||||
if test.WillFail {
|
||||
continue
|
||||
}
|
||||
err := k.DeleteValue(test.Name)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
names, err := k.ReadValueNames(-1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if len(names) != 0 {
|
||||
t.Errorf("some values remain after deletion: %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
softwareK, err := registry.OpenKey(registry.CURRENT_USER, "Software", registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer softwareK.Close()
|
||||
|
||||
testKName := randKeyName("TestValues_")
|
||||
|
||||
k, exist, err := registry.CreateKey(softwareK, testKName, registry.CREATE_SUB_KEY|registry.QUERY_VALUE|registry.SET_VALUE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
if exist {
|
||||
t.Fatalf("key %q already exists", testKName)
|
||||
}
|
||||
|
||||
defer registry.DeleteKey(softwareK, testKName)
|
||||
|
||||
setValues(t, k)
|
||||
|
||||
enumerateValues(t, k)
|
||||
|
||||
testValues(t, k)
|
||||
|
||||
testStat(t, k)
|
||||
|
||||
deleteValues(t, k)
|
||||
}
|
||||
|
||||
func TestExpandString(t *testing.T) {
|
||||
got, err := registry.ExpandString("%PATH%")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := os.Getenv("PATH")
|
||||
if got != want {
|
||||
t.Errorf("want %q string expanded, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidValues(t *testing.T) {
|
||||
softwareK, err := registry.OpenKey(registry.CURRENT_USER, "Software", registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer softwareK.Close()
|
||||
|
||||
testKName := randKeyName("TestInvalidValues_")
|
||||
|
||||
k, exist, err := registry.CreateKey(softwareK, testKName, registry.CREATE_SUB_KEY|registry.QUERY_VALUE|registry.SET_VALUE)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
if exist {
|
||||
t.Fatalf("key %q already exists", testKName)
|
||||
}
|
||||
|
||||
defer registry.DeleteKey(softwareK, testKName)
|
||||
|
||||
var tests = []struct {
|
||||
Type uint32
|
||||
Name string
|
||||
Data []byte
|
||||
}{
|
||||
{registry.DWORD, "Dword1", nil},
|
||||
{registry.DWORD, "Dword2", []byte{1, 2, 3}},
|
||||
{registry.QWORD, "Qword1", nil},
|
||||
{registry.QWORD, "Qword2", []byte{1, 2, 3}},
|
||||
{registry.QWORD, "Qword3", []byte{1, 2, 3, 4, 5, 6, 7}},
|
||||
{registry.MULTI_SZ, "MultiString1", nil},
|
||||
{registry.MULTI_SZ, "MultiString2", []byte{0}},
|
||||
{registry.MULTI_SZ, "MultiString3", []byte{'a', 'b', 0}},
|
||||
{registry.MULTI_SZ, "MultiString4", []byte{'a', 0, 0, 'b', 0}},
|
||||
{registry.MULTI_SZ, "MultiString5", []byte{'a', 0, 0}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := k.SetValue(test.Name, test.Type, test.Data)
|
||||
if err != nil {
|
||||
t.Fatalf("SetValue for %q failed: %v", test.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
switch test.Type {
|
||||
case registry.DWORD, registry.QWORD:
|
||||
value, valType, err := k.GetIntegerValue(test.Name)
|
||||
if err == nil {
|
||||
t.Errorf("GetIntegerValue(%q) succeeded. Returns type=%d value=%v", test.Name, valType, value)
|
||||
}
|
||||
case registry.MULTI_SZ:
|
||||
value, valType, err := k.GetStringsValue(test.Name)
|
||||
if err == nil {
|
||||
if len(value) != 0 {
|
||||
t.Errorf("GetStringsValue(%q) succeeded. Returns type=%d value=%v", test.Name, valType, value)
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.Errorf("unsupported type %d for %s value", test.Type, test.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMUIStringValue(t *testing.T) {
|
||||
if err := registry.LoadRegLoadMUIString(); err != nil {
|
||||
t.Skip("regLoadMUIString not supported; skipping")
|
||||
}
|
||||
if err := procGetDynamicTimeZoneInformation.Find(); err != nil {
|
||||
t.Skipf("%s not supported; skipping", procGetDynamicTimeZoneInformation.Name)
|
||||
}
|
||||
var dtzi DynamicTimezoneinformation
|
||||
if _, err := GetDynamicTimeZoneInformation(&dtzi); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tzKeyName := syscall.UTF16ToString(dtzi.TimeZoneKeyName[:])
|
||||
timezoneK, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
||||
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\`+tzKeyName, registry.READ)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer timezoneK.Close()
|
||||
|
||||
type testType struct {
|
||||
name string
|
||||
want string
|
||||
}
|
||||
var tests = []testType{
|
||||
{"MUI_Std", syscall.UTF16ToString(dtzi.StandardName[:])},
|
||||
}
|
||||
if dtzi.DynamicDaylightTimeDisabled == 0 {
|
||||
tests = append(tests, testType{"MUI_Dlt", syscall.UTF16ToString(dtzi.DaylightName[:])})
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got, err := timezoneK.GetMUIStringValue(test.name)
|
||||
if err != nil {
|
||||
t.Error("GetMUIStringValue:", err)
|
||||
}
|
||||
|
||||
if got != test.want {
|
||||
t.Errorf("GetMUIStringValue: %s: Got %q, want %q", test.name, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DynamicTimezoneinformation struct {
|
||||
Bias int32
|
||||
StandardName [32]uint16
|
||||
StandardDate syscall.Systemtime
|
||||
StandardBias int32
|
||||
DaylightName [32]uint16
|
||||
DaylightDate syscall.Systemtime
|
||||
DaylightBias int32
|
||||
TimeZoneKeyName [128]uint16
|
||||
DynamicDaylightTimeDisabled uint8
|
||||
}
|
||||
|
||||
var (
|
||||
kernel32DLL = syscall.NewLazyDLL("kernel32")
|
||||
|
||||
procGetDynamicTimeZoneInformation = kernel32DLL.NewProc("GetDynamicTimeZoneInformation")
|
||||
)
|
||||
|
||||
func GetDynamicTimeZoneInformation(dtzi *DynamicTimezoneinformation) (rc uint32, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procGetDynamicTimeZoneInformation.Addr(), 1, uintptr(unsafe.Pointer(dtzi)), 0, 0)
|
||||
rc = uint32(r0)
|
||||
if rc == 0xffffffff {
|
||||
if e1 != 0 {
|
||||
err = error(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// 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 windows
|
||||
|
||||
package registry
|
||||
|
||||
import "syscall"
|
||||
|
||||
const (
|
||||
_REG_OPTION_NON_VOLATILE = 0
|
||||
|
||||
_REG_CREATED_NEW_KEY = 1
|
||||
_REG_OPENED_EXISTING_KEY = 2
|
||||
|
||||
_ERROR_NO_MORE_ITEMS syscall.Errno = 259
|
||||
)
|
||||
|
||||
func LoadRegLoadMUIString() error {
|
||||
return procRegLoadMUIStringW.Find()
|
||||
}
|
||||
|
||||
//sys regCreateKeyEx(key syscall.Handle, subkey *uint16, reserved uint32, class *uint16, options uint32, desired uint32, sa *syscall.SecurityAttributes, result *syscall.Handle, disposition *uint32) (regerrno error) = advapi32.RegCreateKeyExW
|
||||
//sys regDeleteKey(key syscall.Handle, subkey *uint16) (regerrno error) = advapi32.RegDeleteKeyW
|
||||
//sys regSetValueEx(key syscall.Handle, valueName *uint16, reserved uint32, vtype uint32, buf *byte, bufsize uint32) (regerrno error) = advapi32.RegSetValueExW
|
||||
//sys regEnumValue(key syscall.Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, valtype *uint32, buf *byte, buflen *uint32) (regerrno error) = advapi32.RegEnumValueW
|
||||
//sys regDeleteValue(key syscall.Handle, name *uint16) (regerrno error) = advapi32.RegDeleteValueW
|
||||
//sys regLoadMUIString(key syscall.Handle, name *uint16, buf *uint16, buflen uint32, buflenCopied *uint32, flags uint32, dir *uint16) (regerrno error) = advapi32.RegLoadMUIStringW
|
||||
//sys regConnectRegistry(machinename *uint16, key syscall.Handle, result *syscall.Handle) (regerrno error) = advapi32.RegConnectRegistryW
|
||||
//sys regNotifyChangeKeyValue(key syscall.Handle, watchSubtree bool, notifyFilter uint32, event syscall.Handle, async bool) (regerrno error) = advapi32.RegNotifyChangeKeyValue
|
||||
|
||||
//sys expandEnvironmentStrings(src *uint16, dst *uint16, size uint32) (n uint32, err error) = kernel32.ExpandEnvironmentStringsW
|
||||
@@ -1,386 +0,0 @@
|
||||
// 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 windows
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Registry value types.
|
||||
NONE = 0
|
||||
SZ = 1
|
||||
EXPAND_SZ = 2
|
||||
BINARY = 3
|
||||
DWORD = 4
|
||||
DWORD_BIG_ENDIAN = 5
|
||||
LINK = 6
|
||||
MULTI_SZ = 7
|
||||
RESOURCE_LIST = 8
|
||||
FULL_RESOURCE_DESCRIPTOR = 9
|
||||
RESOURCE_REQUIREMENTS_LIST = 10
|
||||
QWORD = 11
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrShortBuffer is returned when the buffer was too short for the operation.
|
||||
ErrShortBuffer = syscall.ERROR_MORE_DATA
|
||||
|
||||
// ErrNotExist is returned when a registry key or value does not exist.
|
||||
ErrNotExist = syscall.ERROR_FILE_NOT_FOUND
|
||||
|
||||
// ErrUnexpectedType is returned by Get*Value when the value's type was unexpected.
|
||||
ErrUnexpectedType = errors.New("unexpected key value type")
|
||||
)
|
||||
|
||||
// GetValue retrieves the type and data for the specified value associated
|
||||
// with an open key k. It fills up buffer buf and returns the retrieved
|
||||
// byte count n. If buf is too small to fit the stored value it returns
|
||||
// ErrShortBuffer error along with the required buffer size n.
|
||||
// If no buffer is provided, it returns true and actual buffer size n.
|
||||
// If no buffer is provided, GetValue returns the value's type only.
|
||||
// If the value does not exist, the error returned is ErrNotExist.
|
||||
//
|
||||
// GetValue is a low level function. If value's type is known, use the appropriate
|
||||
// Get*Value function instead.
|
||||
func (k Key) GetValue(name string, buf []byte) (n int, valtype uint32, err error) {
|
||||
pname, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
var pbuf *byte
|
||||
if len(buf) > 0 {
|
||||
pbuf = (*byte)(unsafe.Pointer(&buf[0]))
|
||||
}
|
||||
l := uint32(len(buf))
|
||||
err = syscall.RegQueryValueEx(syscall.Handle(k), pname, nil, &valtype, pbuf, &l)
|
||||
if err != nil {
|
||||
return int(l), valtype, err
|
||||
}
|
||||
return int(l), valtype, nil
|
||||
}
|
||||
|
||||
func (k Key) getValue(name string, buf []byte) (data []byte, valtype uint32, err error) {
|
||||
p, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var t uint32
|
||||
n := uint32(len(buf))
|
||||
for {
|
||||
err = syscall.RegQueryValueEx(syscall.Handle(k), p, nil, &t, (*byte)(unsafe.Pointer(&buf[0])), &n)
|
||||
if err == nil {
|
||||
return buf[:n], t, nil
|
||||
}
|
||||
if err != syscall.ERROR_MORE_DATA {
|
||||
return nil, 0, err
|
||||
}
|
||||
if n <= uint32(len(buf)) {
|
||||
return nil, 0, err
|
||||
}
|
||||
buf = make([]byte, n)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStringValue retrieves the string value for the specified
|
||||
// value name associated with an open key k. It also returns the value's type.
|
||||
// If value does not exist, GetStringValue returns ErrNotExist.
|
||||
// If value is not SZ or EXPAND_SZ, it will return the correct value
|
||||
// type and ErrUnexpectedType.
|
||||
func (k Key) GetStringValue(name string) (val string, valtype uint32, err error) {
|
||||
data, typ, err2 := k.getValue(name, make([]byte, 64))
|
||||
if err2 != nil {
|
||||
return "", typ, err2
|
||||
}
|
||||
switch typ {
|
||||
case SZ, EXPAND_SZ:
|
||||
default:
|
||||
return "", typ, ErrUnexpectedType
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return "", typ, nil
|
||||
}
|
||||
u := (*[1 << 29]uint16)(unsafe.Pointer(&data[0]))[: len(data)/2 : len(data)/2]
|
||||
return syscall.UTF16ToString(u), typ, nil
|
||||
}
|
||||
|
||||
// GetMUIStringValue retrieves the localized string value for
|
||||
// the specified value name associated with an open key k.
|
||||
// If the value name doesn't exist or the localized string value
|
||||
// can't be resolved, GetMUIStringValue returns ErrNotExist.
|
||||
// GetMUIStringValue panics if the system doesn't support
|
||||
// regLoadMUIString; use LoadRegLoadMUIString to check if
|
||||
// regLoadMUIString is supported before calling this function.
|
||||
func (k Key) GetMUIStringValue(name string) (string, error) {
|
||||
pname, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := make([]uint16, 1024)
|
||||
var buflen uint32
|
||||
var pdir *uint16
|
||||
|
||||
err = regLoadMUIString(syscall.Handle(k), pname, &buf[0], uint32(len(buf)), &buflen, 0, pdir)
|
||||
if err == syscall.ERROR_FILE_NOT_FOUND { // Try fallback path
|
||||
|
||||
// Try to resolve the string value using the system directory as
|
||||
// a DLL search path; this assumes the string value is of the form
|
||||
// @[path]\dllname,-strID but with no path given, e.g. @tzres.dll,-320.
|
||||
|
||||
// This approach works with tzres.dll but may have to be revised
|
||||
// in the future to allow callers to provide custom search paths.
|
||||
|
||||
var s string
|
||||
s, err = ExpandString("%SystemRoot%\\system32\\")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pdir, err = syscall.UTF16PtrFromString(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = regLoadMUIString(syscall.Handle(k), pname, &buf[0], uint32(len(buf)), &buflen, 0, pdir)
|
||||
}
|
||||
|
||||
for err == syscall.ERROR_MORE_DATA { // Grow buffer if needed
|
||||
if buflen <= uint32(len(buf)) {
|
||||
break // Buffer not growing, assume race; break
|
||||
}
|
||||
buf = make([]uint16, buflen)
|
||||
err = regLoadMUIString(syscall.Handle(k), pname, &buf[0], uint32(len(buf)), &buflen, 0, pdir)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return syscall.UTF16ToString(buf), nil
|
||||
}
|
||||
|
||||
// ExpandString expands environment-variable strings and replaces
|
||||
// them with the values defined for the current user.
|
||||
// Use ExpandString to expand EXPAND_SZ strings.
|
||||
func ExpandString(value string) (string, error) {
|
||||
if value == "" {
|
||||
return "", nil
|
||||
}
|
||||
p, err := syscall.UTF16PtrFromString(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
r := make([]uint16, 100)
|
||||
for {
|
||||
n, err := expandEnvironmentStrings(p, &r[0], uint32(len(r)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n <= uint32(len(r)) {
|
||||
return syscall.UTF16ToString(r[:n]), nil
|
||||
}
|
||||
r = make([]uint16, n)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStringsValue retrieves the []string value for the specified
|
||||
// value name associated with an open key k. It also returns the value's type.
|
||||
// If value does not exist, GetStringsValue returns ErrNotExist.
|
||||
// If value is not MULTI_SZ, it will return the correct value
|
||||
// type and ErrUnexpectedType.
|
||||
func (k Key) GetStringsValue(name string) (val []string, valtype uint32, err error) {
|
||||
data, typ, err2 := k.getValue(name, make([]byte, 64))
|
||||
if err2 != nil {
|
||||
return nil, typ, err2
|
||||
}
|
||||
if typ != MULTI_SZ {
|
||||
return nil, typ, ErrUnexpectedType
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, typ, nil
|
||||
}
|
||||
p := (*[1 << 29]uint16)(unsafe.Pointer(&data[0]))[: len(data)/2 : len(data)/2]
|
||||
if len(p) == 0 {
|
||||
return nil, typ, nil
|
||||
}
|
||||
if p[len(p)-1] == 0 {
|
||||
p = p[:len(p)-1] // remove terminating null
|
||||
}
|
||||
val = make([]string, 0, 5)
|
||||
from := 0
|
||||
for i, c := range p {
|
||||
if c == 0 {
|
||||
val = append(val, string(utf16.Decode(p[from:i])))
|
||||
from = i + 1
|
||||
}
|
||||
}
|
||||
return val, typ, nil
|
||||
}
|
||||
|
||||
// GetIntegerValue retrieves the integer value for the specified
|
||||
// value name associated with an open key k. It also returns the value's type.
|
||||
// If value does not exist, GetIntegerValue returns ErrNotExist.
|
||||
// If value is not DWORD or QWORD, it will return the correct value
|
||||
// type and ErrUnexpectedType.
|
||||
func (k Key) GetIntegerValue(name string) (val uint64, valtype uint32, err error) {
|
||||
data, typ, err2 := k.getValue(name, make([]byte, 8))
|
||||
if err2 != nil {
|
||||
return 0, typ, err2
|
||||
}
|
||||
switch typ {
|
||||
case DWORD:
|
||||
if len(data) != 4 {
|
||||
return 0, typ, errors.New("DWORD value is not 4 bytes long")
|
||||
}
|
||||
var val32 uint32
|
||||
copy((*[4]byte)(unsafe.Pointer(&val32))[:], data)
|
||||
return uint64(val32), DWORD, nil
|
||||
case QWORD:
|
||||
if len(data) != 8 {
|
||||
return 0, typ, errors.New("QWORD value is not 8 bytes long")
|
||||
}
|
||||
copy((*[8]byte)(unsafe.Pointer(&val))[:], data)
|
||||
return val, QWORD, nil
|
||||
default:
|
||||
return 0, typ, ErrUnexpectedType
|
||||
}
|
||||
}
|
||||
|
||||
// GetBinaryValue retrieves the binary value for the specified
|
||||
// value name associated with an open key k. It also returns the value's type.
|
||||
// If value does not exist, GetBinaryValue returns ErrNotExist.
|
||||
// If value is not BINARY, it will return the correct value
|
||||
// type and ErrUnexpectedType.
|
||||
func (k Key) GetBinaryValue(name string) (val []byte, valtype uint32, err error) {
|
||||
data, typ, err2 := k.getValue(name, make([]byte, 64))
|
||||
if err2 != nil {
|
||||
return nil, typ, err2
|
||||
}
|
||||
if typ != BINARY {
|
||||
return nil, typ, ErrUnexpectedType
|
||||
}
|
||||
return data, typ, nil
|
||||
}
|
||||
|
||||
func (k Key) setValue(name string, valtype uint32, data []byte) error {
|
||||
p, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return regSetValueEx(syscall.Handle(k), p, 0, valtype, nil, 0)
|
||||
}
|
||||
return regSetValueEx(syscall.Handle(k), p, 0, valtype, &data[0], uint32(len(data)))
|
||||
}
|
||||
|
||||
// SetDWordValue sets the data and type of a name value
|
||||
// under key k to value and DWORD.
|
||||
func (k Key) SetDWordValue(name string, value uint32) error {
|
||||
return k.setValue(name, DWORD, (*[4]byte)(unsafe.Pointer(&value))[:])
|
||||
}
|
||||
|
||||
// SetQWordValue sets the data and type of a name value
|
||||
// under key k to value and QWORD.
|
||||
func (k Key) SetQWordValue(name string, value uint64) error {
|
||||
return k.setValue(name, QWORD, (*[8]byte)(unsafe.Pointer(&value))[:])
|
||||
}
|
||||
|
||||
func (k Key) setStringValue(name string, valtype uint32, value string) error {
|
||||
v, err := syscall.UTF16FromString(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf := (*[1 << 29]byte)(unsafe.Pointer(&v[0]))[: len(v)*2 : len(v)*2]
|
||||
return k.setValue(name, valtype, buf)
|
||||
}
|
||||
|
||||
// SetStringValue sets the data and type of a name value
|
||||
// under key k to value and SZ. The value must not contain a zero byte.
|
||||
func (k Key) SetStringValue(name, value string) error {
|
||||
return k.setStringValue(name, SZ, value)
|
||||
}
|
||||
|
||||
// SetExpandStringValue sets the data and type of a name value
|
||||
// under key k to value and EXPAND_SZ. The value must not contain a zero byte.
|
||||
func (k Key) SetExpandStringValue(name, value string) error {
|
||||
return k.setStringValue(name, EXPAND_SZ, value)
|
||||
}
|
||||
|
||||
// SetStringsValue sets the data and type of a name value
|
||||
// under key k to value and MULTI_SZ. The value strings
|
||||
// must not contain a zero byte.
|
||||
func (k Key) SetStringsValue(name string, value []string) error {
|
||||
ss := ""
|
||||
for _, s := range value {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == 0 {
|
||||
return errors.New("string cannot have 0 inside")
|
||||
}
|
||||
}
|
||||
ss += s + "\x00"
|
||||
}
|
||||
v := utf16.Encode([]rune(ss + "\x00"))
|
||||
buf := (*[1 << 29]byte)(unsafe.Pointer(&v[0]))[: len(v)*2 : len(v)*2]
|
||||
return k.setValue(name, MULTI_SZ, buf)
|
||||
}
|
||||
|
||||
// SetBinaryValue sets the data and type of a name value
|
||||
// under key k to value and BINARY.
|
||||
func (k Key) SetBinaryValue(name string, value []byte) error {
|
||||
return k.setValue(name, BINARY, value)
|
||||
}
|
||||
|
||||
// DeleteValue removes a named value from the key k.
|
||||
func (k Key) DeleteValue(name string) error {
|
||||
return regDeleteValue(syscall.Handle(k), syscall.StringToUTF16Ptr(name))
|
||||
}
|
||||
|
||||
// ReadValueNames returns the value names of key k.
|
||||
// The parameter n controls the number of returned names,
|
||||
// analogous to the way os.File.Readdirnames works.
|
||||
func (k Key) ReadValueNames(n int) ([]string, error) {
|
||||
ki, err := k.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := make([]string, 0, ki.ValueCount)
|
||||
buf := make([]uint16, ki.MaxValueNameLen+1) // extra room for terminating null character
|
||||
loopItems:
|
||||
for i := uint32(0); ; i++ {
|
||||
if n > 0 {
|
||||
if len(names) == n {
|
||||
return names, nil
|
||||
}
|
||||
}
|
||||
l := uint32(len(buf))
|
||||
for {
|
||||
err := regEnumValue(syscall.Handle(k), i, &buf[0], &l, nil, nil, nil, nil)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if err == syscall.ERROR_MORE_DATA {
|
||||
// Double buffer size and try again.
|
||||
l = uint32(2 * len(buf))
|
||||
buf = make([]uint16, l)
|
||||
continue
|
||||
}
|
||||
if err == _ERROR_NO_MORE_ITEMS {
|
||||
break loopItems
|
||||
}
|
||||
return names, err
|
||||
}
|
||||
names = append(names, syscall.UTF16ToString(buf[:l]))
|
||||
}
|
||||
if n > len(names) {
|
||||
return names, io.EOF
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return nil
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
|
||||
procRegCreateKeyExW = modadvapi32.NewProc("RegCreateKeyExW")
|
||||
procRegDeleteKeyW = modadvapi32.NewProc("RegDeleteKeyW")
|
||||
procRegSetValueExW = modadvapi32.NewProc("RegSetValueExW")
|
||||
procRegEnumValueW = modadvapi32.NewProc("RegEnumValueW")
|
||||
procRegDeleteValueW = modadvapi32.NewProc("RegDeleteValueW")
|
||||
procRegLoadMUIStringW = modadvapi32.NewProc("RegLoadMUIStringW")
|
||||
procRegConnectRegistryW = modadvapi32.NewProc("RegConnectRegistryW")
|
||||
procRegNotifyChangeKeyValue = modadvapi32.NewProc("RegNotifyChangeKeyValue")
|
||||
procExpandEnvironmentStringsW = modkernel32.NewProc("ExpandEnvironmentStringsW")
|
||||
)
|
||||
|
||||
func regCreateKeyEx(key syscall.Handle, subkey *uint16, reserved uint32, class *uint16, options uint32, desired uint32, sa *syscall.SecurityAttributes, result *syscall.Handle, disposition *uint32) (regerrno error) {
|
||||
r0, _, _ := syscall.Syscall9(procRegCreateKeyExW.Addr(), 9, uintptr(key), uintptr(unsafe.Pointer(subkey)), uintptr(reserved), uintptr(unsafe.Pointer(class)), uintptr(options), uintptr(desired), uintptr(unsafe.Pointer(sa)), uintptr(unsafe.Pointer(result)), uintptr(unsafe.Pointer(disposition)))
|
||||
if r0 != 0 {
|
||||
regerrno = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func regDeleteKey(key syscall.Handle, subkey *uint16) (regerrno error) {
|
||||
r0, _, _ := syscall.Syscall(procRegDeleteKeyW.Addr(), 2, uintptr(key), uintptr(unsafe.Pointer(subkey)), 0)
|
||||
if r0 != 0 {
|
||||
regerrno = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func regSetValueEx(key syscall.Handle, valueName *uint16, reserved uint32, vtype uint32, buf *byte, bufsize uint32) (regerrno error) {
|
||||
r0, _, _ := syscall.Syscall6(procRegSetValueExW.Addr(), 6, uintptr(key), uintptr(unsafe.Pointer(valueName)), uintptr(reserved), uintptr(vtype), uintptr(unsafe.Pointer(buf)), uintptr(bufsize))
|
||||
if r0 != 0 {
|
||||
regerrno = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func regEnumValue(key syscall.Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, valtype *uint32, buf *byte, buflen *uint32) (regerrno error) {
|
||||
r0, _, _ := syscall.Syscall9(procRegEnumValueW.Addr(), 8, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(valtype)), uintptr(unsafe.Pointer(buf)), uintptr(unsafe.Pointer(buflen)), 0)
|
||||
if r0 != 0 {
|
||||
regerrno = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func regDeleteValue(key syscall.Handle, name *uint16) (regerrno error) {
|
||||
r0, _, _ := syscall.Syscall(procRegDeleteValueW.Addr(), 2, uintptr(key), uintptr(unsafe.Pointer(name)), 0)
|
||||
if r0 != 0 {
|
||||
regerrno = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func regLoadMUIString(key syscall.Handle, name *uint16, buf *uint16, buflen uint32, buflenCopied *uint32, flags uint32, dir *uint16) (regerrno error) {
|
||||
r0, _, _ := syscall.Syscall9(procRegLoadMUIStringW.Addr(), 7, uintptr(key), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buf)), uintptr(buflen), uintptr(unsafe.Pointer(buflenCopied)), uintptr(flags), uintptr(unsafe.Pointer(dir)), 0, 0)
|
||||
if r0 != 0 {
|
||||
regerrno = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func regConnectRegistry(machinename *uint16, key syscall.Handle, result *syscall.Handle) (regerrno error) {
|
||||
r0, _, _ := syscall.Syscall(procRegConnectRegistryW.Addr(), 3, uintptr(unsafe.Pointer(machinename)), uintptr(key), uintptr(unsafe.Pointer(result)))
|
||||
if r0 != 0 {
|
||||
regerrno = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func regNotifyChangeKeyValue(key syscall.Handle, watchSubtree bool, notifyFilter uint32, event syscall.Handle, async bool) (regerrno error) {
|
||||
var _p0 uint32
|
||||
if watchSubtree {
|
||||
_p0 = 1
|
||||
} else {
|
||||
_p0 = 0
|
||||
}
|
||||
var _p1 uint32
|
||||
if async {
|
||||
_p1 = 1
|
||||
} else {
|
||||
_p1 = 0
|
||||
}
|
||||
r0, _, _ := syscall.Syscall6(procRegNotifyChangeKeyValue.Addr(), 5, uintptr(key), uintptr(_p0), uintptr(notifyFilter), uintptr(event), uintptr(_p1), 0)
|
||||
if r0 != 0 {
|
||||
regerrno = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func expandEnvironmentStrings(src *uint16, dst *uint16, size uint32) (n uint32, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procExpandEnvironmentStringsW.Addr(), 3, uintptr(unsafe.Pointer(src)), uintptr(unsafe.Pointer(dst)), uintptr(size))
|
||||
n = uint32(r0)
|
||||
if n == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
119
tstime/tstime.go
119
tstime/tstime.go
@@ -8,109 +8,119 @@ package tstime
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
)
|
||||
|
||||
var memZ = mem.S("Z")
|
||||
|
||||
// zoneOf returns the RFC3339 zone suffix (either "Z" or like
|
||||
// "+08:30"), or the empty string if it's invalid or not something we
|
||||
// want to cache.
|
||||
func zoneOf(s string) string {
|
||||
if strings.HasSuffix(s, "Z") {
|
||||
return "Z"
|
||||
func zoneOf(s mem.RO) mem.RO {
|
||||
if mem.HasSuffix(s, memZ) {
|
||||
return memZ
|
||||
}
|
||||
if len(s) < len("2020-04-05T15:56:00+08:00") {
|
||||
if s.Len() < len("2020-04-05T15:56:00+08:00") {
|
||||
// Too short, invalid? Let time.Parse fail on it.
|
||||
return ""
|
||||
return mem.S("")
|
||||
}
|
||||
zone := s[len(s)-len("+08:00"):]
|
||||
if c := zone[0]; c == '+' || c == '-' {
|
||||
min := zone[len("+08:"):]
|
||||
switch min {
|
||||
case "00", "15", "30":
|
||||
zone := s.SliceFrom(s.Len() - len("+08:00"))
|
||||
if c := zone.At(0); c == '+' || c == '-' {
|
||||
min := zone.SliceFrom(len("+08:"))
|
||||
if min.EqualString("00") || min.EqualString("15") || min.EqualString("30") {
|
||||
return zone
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return mem.S("")
|
||||
}
|
||||
|
||||
// locCache maps from zone offset suffix string ("+08:00") =>
|
||||
// *time.Location (from FixedLocation).
|
||||
// locCache maps from hash of zone offset suffix string ("+08:00") =>
|
||||
// {zone string, *time.Location (from FixedLocation)}.
|
||||
var locCache sync.Map
|
||||
|
||||
func getLocation(zone, timeValue string) (*time.Location, error) {
|
||||
if zone == "Z" {
|
||||
type locCacheEntry struct {
|
||||
zone string
|
||||
loc *time.Location
|
||||
}
|
||||
|
||||
func getLocation(zone, timeValue mem.RO) (*time.Location, error) {
|
||||
if zone.EqualString("Z") {
|
||||
return time.UTC, nil
|
||||
}
|
||||
if loci, ok := locCache.Load(zone); ok {
|
||||
return loci.(*time.Location), nil
|
||||
key := zone.MapHash()
|
||||
if entry, ok := locCache.Load(key); ok {
|
||||
// We're keying only on a hash; double-check zone to ensure no spurious collisions.
|
||||
e := entry.(locCacheEntry)
|
||||
if zone.EqualString(e.zone) {
|
||||
return e.loc, nil
|
||||
}
|
||||
}
|
||||
// TODO(bradfitz): just parse it and call time.FixedLocation.
|
||||
// For now, just have time.Parse do it once:
|
||||
t, err := time.Parse(time.RFC3339Nano, timeValue)
|
||||
t, err := time.Parse(time.RFC3339Nano, timeValue.StringCopy())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loc := t.Location()
|
||||
locCache.LoadOrStore(zone, loc)
|
||||
locCache.LoadOrStore(key, locCacheEntry{zone: zone.StringCopy(), loc: loc})
|
||||
return loc, nil
|
||||
|
||||
}
|
||||
|
||||
// Parse3339 is a wrapper around time.Parse(time.RFC3339Nano, s) that caches
|
||||
// timezone Locations for future parses.
|
||||
func Parse3339(s string) (time.Time, error) {
|
||||
func parse3339m(s mem.RO) (time.Time, error) {
|
||||
zone := zoneOf(s)
|
||||
if zone == "" {
|
||||
if zone.Len() == 0 {
|
||||
// Invalid or weird timezone offset. Use slow path,
|
||||
// which'll probably return an error.
|
||||
return time.Parse(time.RFC3339Nano, s)
|
||||
return time.Parse(time.RFC3339Nano, s.StringCopy())
|
||||
}
|
||||
loc, err := getLocation(zone, s)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
s = s[:len(s)-len(zone)] // remove zone suffix
|
||||
s = s.SliceTo(s.Len() - zone.Len()) // remove zone suffix
|
||||
var year, mon, day, hr, min, sec, nsec int
|
||||
const baseLen = len("2020-04-05T15:56:00")
|
||||
if len(s) < baseLen ||
|
||||
!parseInt(s[:4], &year) ||
|
||||
s[4] != '-' ||
|
||||
!parseInt(s[5:7], &mon) ||
|
||||
s[7] != '-' ||
|
||||
!parseInt(s[8:10], &day) ||
|
||||
s[10] != 'T' ||
|
||||
!parseInt(s[11:13], &hr) ||
|
||||
s[13] != ':' ||
|
||||
!parseInt(s[14:16], &min) ||
|
||||
s[16] != ':' ||
|
||||
!parseInt(s[17:19], &sec) {
|
||||
if s.Len() < baseLen ||
|
||||
!parseInt(s.SliceTo(4), &year) ||
|
||||
s.At(4) != '-' ||
|
||||
!parseInt(s.Slice(5, 7), &mon) ||
|
||||
s.At(7) != '-' ||
|
||||
!parseInt(s.Slice(8, 10), &day) ||
|
||||
s.At(10) != 'T' ||
|
||||
!parseInt(s.Slice(11, 13), &hr) ||
|
||||
s.At(13) != ':' ||
|
||||
!parseInt(s.Slice(14, 16), &min) ||
|
||||
s.At(16) != ':' ||
|
||||
!parseInt(s.Slice(17, 19), &sec) {
|
||||
return time.Time{}, errors.New("invalid time")
|
||||
}
|
||||
nsStr := s[baseLen:]
|
||||
if nsStr != "" {
|
||||
if nsStr[0] != '.' {
|
||||
nsStr := s.SliceFrom(baseLen)
|
||||
if nsStr.Len() != 0 {
|
||||
if nsStr.At(0) != '.' {
|
||||
return time.Time{}, errors.New("invalid optional nanosecond prefix")
|
||||
}
|
||||
if !parseInt(nsStr[1:], &nsec) {
|
||||
return time.Time{}, fmt.Errorf("invalid optional nanosecond number %q", nsStr[1:])
|
||||
nsStr = nsStr.SliceFrom(1)
|
||||
if !parseInt(nsStr, &nsec) {
|
||||
return time.Time{}, fmt.Errorf("invalid optional nanosecond number %q", nsStr.StringCopy())
|
||||
}
|
||||
for i := 0; i < len("999999999")-(len(nsStr)-1); i++ {
|
||||
for i := 0; i < len("999999999")-nsStr.Len(); i++ {
|
||||
nsec *= 10
|
||||
}
|
||||
}
|
||||
return time.Date(year, time.Month(mon), day, hr, min, sec, nsec, loc), nil
|
||||
}
|
||||
|
||||
func parseInt(s string, dst *int) bool {
|
||||
if len(s) == 0 || len(s) > len("999999999") {
|
||||
func parseInt(s mem.RO, dst *int) bool {
|
||||
if s.Len() == 0 || s.Len() > len("999999999") {
|
||||
*dst = 0
|
||||
return false
|
||||
}
|
||||
n := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
d := s[i] - '0'
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
d := s.At(i) - '0'
|
||||
if d > 9 {
|
||||
*dst = 0
|
||||
return false
|
||||
@@ -120,3 +130,14 @@ func parseInt(s string, dst *int) bool {
|
||||
*dst = n
|
||||
return true
|
||||
}
|
||||
|
||||
// Parse3339 is a wrapper around time.Parse(time.RFC3339Nano, s) that caches
|
||||
// timezone Locations for future parses.
|
||||
func Parse3339(s string) (time.Time, error) {
|
||||
return parse3339m(mem.S(s))
|
||||
}
|
||||
|
||||
// Parse3339B is Parse3339 but for byte slices.
|
||||
func Parse3339B(b []byte) (time.Time, error) {
|
||||
return parse3339m(mem.B(b))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ package tstime
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
)
|
||||
|
||||
func TestParse3339(t *testing.T) {
|
||||
@@ -70,8 +72,8 @@ func TestZoneOf(t *testing.T) {
|
||||
{"+08:00", ""}, // too short
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := zoneOf(tt.in); got != tt.want {
|
||||
t.Errorf("zoneOf(%q) = %q; want %q", tt.in, got, tt.want)
|
||||
if got := zoneOf(mem.S(tt.in)); !got.EqualString(tt.want) {
|
||||
t.Errorf("zoneOf(%q) = %q; want %q", tt.in, got.StringCopy(), tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +95,7 @@ func TestParseInt(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
var got int
|
||||
gotRet := parseInt(tt.in, &got)
|
||||
gotRet := parseInt(mem.S(tt.in), &got)
|
||||
if gotRet != tt.ret || got != tt.want {
|
||||
t.Errorf("parseInt(%q) = %v, %d; want %v, %d", tt.in, gotRet, got, tt.ret, tt.want)
|
||||
}
|
||||
@@ -182,6 +184,6 @@ func BenchmarkParse3339(b *testing.B) {
|
||||
func BenchmarkParseInt(b *testing.B) {
|
||||
var out int
|
||||
for i := 0; i < b.N; i++ {
|
||||
parseInt("148487491", &out)
|
||||
parseInt(mem.S("148487491"), &out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,16 @@ func registerCommonDebug(mux *http.ServeMux) {
|
||||
mux.Handle("/debug/pprof/", Protected(http.DefaultServeMux)) // to net/http/pprof
|
||||
mux.Handle("/debug/vars", Protected(http.DefaultServeMux)) // to expvar
|
||||
mux.Handle("/debug/varz", Protected(http.HandlerFunc(varzHandler)))
|
||||
mux.Handle("/debug/gc", Protected(http.HandlerFunc(gcHandler)))
|
||||
}
|
||||
|
||||
func gcHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("running GC...\n"))
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
runtime.GC()
|
||||
w.Write([]byte("Done.\n"))
|
||||
}
|
||||
|
||||
func DefaultCertDir(leafDir string) string {
|
||||
|
||||
@@ -7,7 +7,7 @@ package key
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestTextUnmarshal(t *testing.T) {
|
||||
@@ -28,10 +28,10 @@ func TestTextUnmarshal(t *testing.T) {
|
||||
func TestClamping(t *testing.T) {
|
||||
t.Run("NewPrivate", func(t *testing.T) { testClamping(t, NewPrivate) })
|
||||
|
||||
// Also test the wgcfg package, as their behavior should match.
|
||||
t.Run("wgcfg", func(t *testing.T) {
|
||||
// Also test the wgkey package, as their behavior should match.
|
||||
t.Run("wgkey", func(t *testing.T) {
|
||||
testClamping(t, func() Private {
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
k, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
|
||||
logf(format, args...)
|
||||
case warn:
|
||||
// For the warning, log the specific format string
|
||||
logf("[RATE LIMITED] %s", format)
|
||||
logf("[RATE LIMITED] format string \"%s\"", format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ func TestRateLimiter(t *testing.T) {
|
||||
"templated format string no. 0",
|
||||
"boring string with constant formatting (constant)",
|
||||
"templated format string no. 1",
|
||||
"[RATE LIMITED] boring string with constant formatting %s",
|
||||
"[RATE LIMITED] templated format string no. %d",
|
||||
"[RATE LIMITED] format string \"boring string with constant formatting %s\"",
|
||||
"[RATE LIMITED] format string \"templated format string no. %d\"",
|
||||
"Make sure this string makes it through the rest (that are blocked) 4",
|
||||
"4 shouldn't get filtered.",
|
||||
}
|
||||
|
||||
241
types/wgkey/key.go
Normal file
241
types/wgkey/key.go
Normal file
@@ -0,0 +1,241 @@
|
||||
// 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 wgkey contains types and helpers for WireGuard keys.
|
||||
// It is very similar to package tailscale.com/types/key,
|
||||
// which is also used for curve25519 keys.
|
||||
// These keys are used for WireGuard clients;
|
||||
// those keys are used in other curve25519 clients.
|
||||
package wgkey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
// Size is the number of bytes in a curve25519 key.
|
||||
const Size = 32
|
||||
|
||||
// A Key is a curve25519 key.
|
||||
// It is used by WireGuard to represent public keys.
|
||||
type Key [Size]byte
|
||||
|
||||
// NewPreshared generates a new random Key.
|
||||
func NewPreshared() (*Key, error) {
|
||||
var k [Size]byte
|
||||
_, err := rand.Read(k[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return (*Key)(&k), nil
|
||||
}
|
||||
|
||||
func Parse(b64 string) (*Key, error) { return parseBase64(base64.StdEncoding, b64) }
|
||||
|
||||
func ParseHex(s string) (Key, error) {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return Key{}, fmt.Errorf("invalid hex key (%q): %w", s, err)
|
||||
}
|
||||
if len(b) != Size {
|
||||
return Key{}, fmt.Errorf("invalid hex key (%q): length=%d, want %d", s, len(b), Size)
|
||||
}
|
||||
|
||||
var key Key
|
||||
copy(key[:], b)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func ParsePrivateHex(v string) (Private, error) {
|
||||
k, err := ParseHex(v)
|
||||
if err != nil {
|
||||
return Private{}, err
|
||||
}
|
||||
pk := Private(k)
|
||||
if pk.IsZero() {
|
||||
// Do not clamp a zero key, pass the zero through
|
||||
// (much like NaN propagation) so that IsZero reports
|
||||
// a useful result.
|
||||
return pk, nil
|
||||
}
|
||||
pk.clamp()
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
func (k Key) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k Key) String() string { return k.ShortString() }
|
||||
func (k Key) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *Key) ShortString() string {
|
||||
long := k.Base64()
|
||||
return "[" + long[0:5] + "]"
|
||||
}
|
||||
|
||||
func (k *Key) IsZero() bool {
|
||||
if k == nil {
|
||||
return true
|
||||
}
|
||||
var zeros Key
|
||||
return subtle.ConstantTimeCompare(zeros[:], k[:]) == 1
|
||||
}
|
||||
|
||||
func (k *Key) MarshalJSON() ([]byte, error) {
|
||||
if k == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
// TODO(josharian): use encoding/hex instead?
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `"%x"`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *Key) UnmarshalJSON(b []byte) error {
|
||||
if k == nil {
|
||||
return errors.New("wgkey.Key: UnmarshalJSON on nil pointer")
|
||||
}
|
||||
if len(b) < 3 || b[0] != '"' || b[len(b)-1] != '"' {
|
||||
return errors.New("wgkey.Key: UnmarshalJSON not given a string")
|
||||
}
|
||||
b = b[1 : len(b)-1]
|
||||
key, err := ParseHex(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgkey.Key: UnmarshalJSON: %v", err)
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Key) LessThan(b *Key) bool {
|
||||
for i := range a {
|
||||
if a[i] < b[i] {
|
||||
return true
|
||||
} else if a[i] > b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Private is a curve25519 key.
|
||||
// It is used by WireGuard to represent private keys.
|
||||
type Private [Size]byte
|
||||
|
||||
// NewPrivate generates a new curve25519 secret key.
|
||||
// It conforms to the format described on https://cr.yp.to/ecdh.html.
|
||||
func NewPrivate() (Private, error) {
|
||||
k, err := NewPreshared()
|
||||
if err != nil {
|
||||
return Private{}, err
|
||||
}
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
return (Private)(*k), nil
|
||||
}
|
||||
|
||||
func ParsePrivate(b64 string) (*Private, error) {
|
||||
k, err := parseBase64(base64.StdEncoding, b64)
|
||||
return (*Private)(k), err
|
||||
}
|
||||
|
||||
func (k *Private) String() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k *Private) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k *Private) Equal(k2 Private) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *Private) IsZero() bool {
|
||||
pk := Key(*k)
|
||||
return pk.IsZero()
|
||||
}
|
||||
|
||||
func (k *Private) clamp() {
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
}
|
||||
|
||||
// Public computes the public key matching this curve25519 secret key.
|
||||
func (k *Private) Public() Key {
|
||||
pk := Key(*k)
|
||||
if pk.IsZero() {
|
||||
panic("Tried to generate emptyPrivate.Public()")
|
||||
}
|
||||
var p [Size]byte
|
||||
curve25519.ScalarBaseMult(&p, (*[Size]byte)(k))
|
||||
return (Key)(p)
|
||||
}
|
||||
|
||||
func (k Private) MarshalText() ([]byte, error) {
|
||||
// TODO(josharian): use encoding/hex instead?
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `privkey:%x`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *Private) UnmarshalText(b []byte) error {
|
||||
s := string(b)
|
||||
if !strings.HasPrefix(s, `privkey:`) {
|
||||
return errors.New("wgkey.Private: UnmarshalText not given a private-key string")
|
||||
}
|
||||
s = strings.TrimPrefix(s, `privkey:`)
|
||||
key, err := ParseHex(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgkey.Private: UnmarshalText: %v", err)
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBase64(enc *base64.Encoding, s string) (*Key, error) {
|
||||
k, err := enc.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid key (%q): %w", s, err)
|
||||
}
|
||||
if len(k) != Size {
|
||||
return nil, fmt.Errorf("invalid key (%q): length=%d, want %d", s, len(k), Size)
|
||||
}
|
||||
var key Key
|
||||
copy(key[:], k)
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func ParseSymmetric(b64 string) (Symmetric, error) {
|
||||
k, err := parseBase64(base64.StdEncoding, b64)
|
||||
if err != nil {
|
||||
return Symmetric{}, err
|
||||
}
|
||||
return Symmetric(*k), nil
|
||||
}
|
||||
|
||||
func ParseSymmetricHex(s string) (Symmetric, error) {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return Symmetric{}, fmt.Errorf("invalid symmetric hex key (%q): %w", s, err)
|
||||
}
|
||||
if len(b) != chacha20poly1305.KeySize {
|
||||
return Symmetric{}, fmt.Errorf("invalid symmetric hex key length (%q): length=%d, want %d", s, len(b), chacha20poly1305.KeySize)
|
||||
}
|
||||
var key Symmetric
|
||||
copy(key[:], b)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Symmetric is a chacha20poly1305 key.
|
||||
// It is used by WireGuard to represent pre-shared symmetric keys.
|
||||
type Symmetric [chacha20poly1305.KeySize]byte
|
||||
|
||||
func (k Symmetric) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k Symmetric) String() string { return "sym:" + k.Base64()[:8] }
|
||||
func (k Symmetric) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k Symmetric) IsZero() bool { return k.Equal(Symmetric{}) }
|
||||
func (k Symmetric) Equal(k2 Symmetric) bool {
|
||||
return subtle.ConstantTimeCompare(k[:], k2[:]) == 1
|
||||
}
|
||||
111
types/wgkey/key_test.go
Normal file
111
types/wgkey/key_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 wgkey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeyBasics(t *testing.T) {
|
||||
k1, err := NewPreshared()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := k1.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("JSON round-trip", func(t *testing.T) {
|
||||
// should preserve the keys
|
||||
k2 := new(Key)
|
||||
if err := k2.UnmarshalJSON(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(k1[:], k2[:]) {
|
||||
t.Fatalf("k1 %v != k2 %v", k1[:], k2[:])
|
||||
}
|
||||
if b1, b2 := k1.String(), k2.String(); b1 != b2 {
|
||||
t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON incompatible with PrivateKey", func(t *testing.T) {
|
||||
k2 := new(Private)
|
||||
if err := k2.UnmarshalText(b); err == nil {
|
||||
t.Fatalf("successfully decoded key as private key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second key", func(t *testing.T) {
|
||||
// A second call to NewPreshared should make a new key.
|
||||
k3, err := NewPreshared()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(k1[:], k3[:]) {
|
||||
t.Fatalf("k1 %v == k3 %v", k1[:], k3[:])
|
||||
}
|
||||
// Check for obvious comparables to make sure we are not generating bad strings somewhere.
|
||||
if b1, b2 := k1.String(), k3.String(); b1 == b2 {
|
||||
t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
|
||||
}
|
||||
})
|
||||
}
|
||||
func TestPrivateKeyBasics(t *testing.T) {
|
||||
pri, err := NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := pri.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("JSON round-trip", func(t *testing.T) {
|
||||
// should preserve the keys
|
||||
pri2 := new(Private)
|
||||
if err := pri2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(pri[:], pri2[:]) {
|
||||
t.Fatalf("pri %v != pri2 %v", pri[:], pri2[:])
|
||||
}
|
||||
if b1, b2 := pri.String(), pri2.String(); b1 != b2 {
|
||||
t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
|
||||
}
|
||||
if pub1, pub2 := pri.Public().String(), pri2.Public().String(); pub1 != pub2 {
|
||||
t.Fatalf("base64-encoded public keys do not match: %s, %s", pub1, pub2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON incompatible with Key", func(t *testing.T) {
|
||||
k2 := new(Key)
|
||||
if err := k2.UnmarshalJSON(b); err == nil {
|
||||
t.Fatalf("successfully decoded private key as key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second key", func(t *testing.T) {
|
||||
// A second call to New should make a new key.
|
||||
pri3, err := NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(pri[:], pri3[:]) {
|
||||
t.Fatalf("pri %v == pri3 %v", pri[:], pri3[:])
|
||||
}
|
||||
// Check for obvious comparables to make sure we are not generating bad strings somewhere.
|
||||
if b1, b2 := pri.String(), pri3.String(); b1 == b2 {
|
||||
t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
|
||||
}
|
||||
if pub1, pub2 := pri.Public().String(), pri3.Public().String(); pub1 == pub2 {
|
||||
t.Fatalf("base64-encoded public keys match: %s, %s", pub1, pub2)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -6,5 +6,10 @@
|
||||
|
||||
package endian
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// Big is whether the current platform is big endian.
|
||||
const Big = true
|
||||
|
||||
// Native is the platform's native byte order.
|
||||
var Native = binary.BigEndian
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
|
||||
package endian
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// Big is whether the current platform is big endian.
|
||||
const Big = false
|
||||
|
||||
// Native is the platform's native byte order.
|
||||
var Native = binary.LittleEndian
|
||||
|
||||
17
util/jsonutil/types.go
Normal file
17
util/jsonutil/types.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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 jsonutil
|
||||
|
||||
// Bytes is a byte slice in a json-encoded struct.
|
||||
// encoding/json assumes that []byte fields are hex-encoded.
|
||||
// Bytes are not hex-encoded; they are treated the same as strings.
|
||||
// This can avoid unnecessary allocations due to a round trip through strings.
|
||||
type Bytes []byte
|
||||
|
||||
func (b *Bytes) UnmarshalText(text []byte) error {
|
||||
// Copy the contexts of text.
|
||||
*b = append(*b, text...)
|
||||
return nil
|
||||
}
|
||||
90
util/jsonutil/unmarshal.go
Normal file
90
util/jsonutil/unmarshal.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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 jsonutil provides utilities to improve JSON performance.
|
||||
// It includes an Unmarshal wrapper that amortizes allocated garbage over subsequent runs
|
||||
// and a Bytes type to reduce allocations when unmarshalling a non-hex-encoded string into a []byte.
|
||||
package jsonutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// decoder is a re-usable json decoder.
|
||||
type decoder struct {
|
||||
dec *json.Decoder
|
||||
r *bytes.Reader
|
||||
}
|
||||
|
||||
var readerPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewReader(nil)
|
||||
},
|
||||
}
|
||||
|
||||
var decoderPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
var d decoder
|
||||
d.r = readerPool.Get().(*bytes.Reader)
|
||||
d.dec = json.NewDecoder(d.r)
|
||||
return &d
|
||||
},
|
||||
}
|
||||
|
||||
// Unmarshal is similar to encoding/json.Unmarshal.
|
||||
// There are three major differences:
|
||||
//
|
||||
// On error, encoding/json.Unmarshal zeros v.
|
||||
// This Unmarshal may leave partial data in v.
|
||||
// Always check the error before using v!
|
||||
// (Future improvements may remove this bug.)
|
||||
//
|
||||
// The errors they return don't always match perfectly.
|
||||
// If you do error matching more precise than err != nil,
|
||||
// don't use this Unmarshal.
|
||||
//
|
||||
// This Unmarshal allocates considerably less memory.
|
||||
func Unmarshal(b []byte, v interface{}) error {
|
||||
d := decoderPool.Get().(*decoder)
|
||||
d.r.Reset(b)
|
||||
off := d.dec.InputOffset()
|
||||
err := d.dec.Decode(v)
|
||||
d.r.Reset(nil) // don't keep a reference to b
|
||||
// In case of error, report the offset in this byte slice,
|
||||
// instead of in the totality of all bytes this decoder has processed.
|
||||
// It is not possible to make all errors match json.Unmarshal exactly,
|
||||
// but we can at least try.
|
||||
switch jsonerr := err.(type) {
|
||||
case *json.SyntaxError:
|
||||
jsonerr.Offset -= off
|
||||
case *json.UnmarshalTypeError:
|
||||
jsonerr.Offset -= off
|
||||
case nil:
|
||||
// json.Unmarshal fails if there's any extra junk in the input.
|
||||
// json.Decoder does not; see https://github.com/golang/go/issues/36225.
|
||||
// We need to check for anything left over in the buffer.
|
||||
if d.dec.More() {
|
||||
// TODO: Provide a better error message.
|
||||
// Unfortunately, we can't set the msg field.
|
||||
// The offset doesn't perfectly match json:
|
||||
// Ours is at the end of the valid data,
|
||||
// and theirs is at the beginning of the extra data after whitespace.
|
||||
// Close enough, though.
|
||||
err = &json.SyntaxError{Offset: d.dec.InputOffset() - off}
|
||||
|
||||
// TODO: zero v. This is hard; see encoding/json.indirect.
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
decoderPool.Put(d)
|
||||
} else {
|
||||
// There might be junk left in the decoder's buffer.
|
||||
// There's no way to flush it, no Reset method.
|
||||
// Abandoned the decoder but reuse the reader.
|
||||
readerPool.Put(d.r)
|
||||
}
|
||||
return err
|
||||
}
|
||||
65
util/jsonutil/unmarshal_test.go
Normal file
65
util/jsonutil/unmarshal_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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 jsonutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompareToStd(t *testing.T) {
|
||||
tests := []string{
|
||||
`{}`,
|
||||
`{"a": 1}`,
|
||||
`{]`,
|
||||
`"abc"`,
|
||||
`5`,
|
||||
`{"a": 1} `,
|
||||
`{"a": 1} {}`,
|
||||
`{} bad data`,
|
||||
`{"a": 1} "hello"`,
|
||||
`[]`,
|
||||
` {"x": {"t": [3,4,5]}}`,
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
b := []byte(test)
|
||||
var ourV, stdV interface{}
|
||||
ourErr := Unmarshal(b, &ourV)
|
||||
stdErr := json.Unmarshal(b, &stdV)
|
||||
if (ourErr == nil) != (stdErr == nil) {
|
||||
t.Errorf("Unmarshal(%q): our err = %#[2]v (%[2]T), std err = %#[3]v (%[3]T)", test, ourErr, stdErr)
|
||||
}
|
||||
// if !reflect.DeepEqual(ourErr, stdErr) {
|
||||
// t.Logf("Unmarshal(%q): our err = %#[2]v (%[2]T), std err = %#[3]v (%[3]T)", test, ourErr, stdErr)
|
||||
// }
|
||||
if ourErr != nil {
|
||||
// TODO: if we zero ourV on error, remove this continue.
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(ourV, stdV) {
|
||||
t.Errorf("Unmarshal(%q): our val = %v, std val = %v", test, ourV, stdV)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshal(b *testing.B) {
|
||||
var m interface{}
|
||||
j := []byte("5")
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Unmarshal(j, &m)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStdUnmarshal(b *testing.B) {
|
||||
var m interface{}
|
||||
j := []byte("5")
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
json.Unmarshal(j, &m)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,11 @@ func File(name string, fn func(line []byte) error) error {
|
||||
return Reader(f, fn)
|
||||
}
|
||||
|
||||
// Reader calls fn for each line.
|
||||
// If fn returns an error, Reader stops reading and returns that error.
|
||||
// Reader may also return errors encountered reading and parsing from r.
|
||||
// To stop reading early, use a sentinel "stop" error value and ignore
|
||||
// it when returned from Reader.
|
||||
func Reader(r io.Reader, fn func(line []byte) error) error {
|
||||
bs := bufio.NewScanner(r)
|
||||
for bs.Scan() {
|
||||
|
||||
14
util/systemd/doc.go
Normal file
14
util/systemd/doc.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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 systemd contains a minimal wrapper around systemd-notify to enable
|
||||
applications to signal readiness and status to systemd.
|
||||
|
||||
This package will only have effect on Linux systems running Tailscale in a
|
||||
systemd unit with the Type=notify flag set. On other operating systems (or
|
||||
when running in a Linux distro without being run from inside systemd) this
|
||||
package will become a no-op.
|
||||
*/
|
||||
package systemd
|
||||
81
util/systemd/systemd_linux.go
Normal file
81
util/systemd/systemd_linux.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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.
|
||||
|
||||
// +build linux
|
||||
|
||||
package systemd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/mdlayher/sdnotify"
|
||||
)
|
||||
|
||||
var getNotifyOnce struct {
|
||||
sync.Once
|
||||
v *sdnotify.Notifier
|
||||
}
|
||||
|
||||
type logOnce struct {
|
||||
sync.Once
|
||||
}
|
||||
|
||||
func (l *logOnce) logf(format string, args ...interface{}) {
|
||||
l.Once.Do(func() {
|
||||
log.Printf(format, args...)
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
readyOnce = &logOnce{}
|
||||
statusOnce = &logOnce{}
|
||||
)
|
||||
|
||||
func notifier() *sdnotify.Notifier {
|
||||
getNotifyOnce.Do(func() {
|
||||
sock := os.Getenv(sdnotify.Socket)
|
||||
if sock == "" {
|
||||
// Not running under systemd probably. Bail out before logging.
|
||||
return
|
||||
}
|
||||
var err error
|
||||
getNotifyOnce.v, err = sdnotify.Open(sock)
|
||||
if err != nil {
|
||||
log.Printf("systemd: systemd-notifier error: %v", err)
|
||||
}
|
||||
})
|
||||
return getNotifyOnce.v
|
||||
}
|
||||
|
||||
// Ready signals readiness to systemd. This will unblock service dependents from starting.
|
||||
func Ready() {
|
||||
err := notifier().Notify(sdnotify.Ready)
|
||||
if err != nil {
|
||||
readyOnce.logf("systemd: error notifying: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Status sends a single line status update to systemd so that information shows up
|
||||
// in systemctl output. For example:
|
||||
//
|
||||
// $ systemctl status tailscale
|
||||
// ● tailscale.service - Tailscale client daemon
|
||||
// Loaded: loaded (/nix/store/qc312qcy907wz80fqrgbbm8a9djafmlg-unit-tailscale.service/tailscale.service; enabled; vendor preset: enabled)
|
||||
// Active: active (running) since Tue 2020-11-24 17:54:07 EST; 13h ago
|
||||
// Main PID: 26741 (.tailscaled-wra)
|
||||
// Status: "Connected; user@host.domain.tld; 100.101.102.103"
|
||||
// IP: 0B in, 0B out
|
||||
// Tasks: 22 (limit: 4915)
|
||||
// Memory: 30.9M
|
||||
// CPU: 2min 38.469s
|
||||
// CGroup: /system.slice/tailscale.service
|
||||
// └─26741 /nix/store/sv6cj4mw2jajm9xkbwj07k29dj30lh0n-tailscale-date.20200727/bin/tailscaled --port 41641
|
||||
func Status(format string, args ...interface{}) {
|
||||
err := notifier().Notify(sdnotify.Statusf(format, args...))
|
||||
if err != nil {
|
||||
statusOnce.logf("systemd: error notifying: %v", err)
|
||||
}
|
||||
}
|
||||
10
util/systemd/systemd_nonlinux.go
Normal file
10
util/systemd/systemd_nonlinux.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package systemd
|
||||
|
||||
func Ready() {}
|
||||
func Status(string, ...interface{}) {}
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
Arch = Distro("arch")
|
||||
Synology = Distro("synology")
|
||||
OpenWrt = Distro("openwrt")
|
||||
NixOS = Distro("nixos")
|
||||
)
|
||||
|
||||
// Get returns the current distro, or the empty string if unknown.
|
||||
@@ -27,18 +28,28 @@ func Get() Distro {
|
||||
return ""
|
||||
}
|
||||
|
||||
func have(file string) bool {
|
||||
_, err := os.Stat(file)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func haveDir(file string) bool {
|
||||
fi, err := os.Stat(file)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func linuxDistro() Distro {
|
||||
if fi, err := os.Stat("/usr/syno"); err == nil && fi.IsDir() {
|
||||
switch {
|
||||
case haveDir("usr/syno"):
|
||||
return Synology
|
||||
}
|
||||
if _, err := os.Stat("/etc/debian_version"); err == nil {
|
||||
case have("/etc/debian_version"):
|
||||
return Debian
|
||||
}
|
||||
if _, err := os.Stat("/etc/arch-release"); err == nil {
|
||||
case have("/etc/arch-release"):
|
||||
return Arch
|
||||
}
|
||||
if _, err := os.Stat("/etc/openwrt_version"); err == nil {
|
||||
case have("/etc/openwrt_version"):
|
||||
return OpenWrt
|
||||
case have("/run/current-system/sw/bin/nixos-version"):
|
||||
return NixOS
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ package version
|
||||
// Long is a full version number for this build, of the form
|
||||
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
|
||||
// provided.
|
||||
const Long = "date.20201107"
|
||||
const Long = "date.20201230"
|
||||
|
||||
// Short is a short version number for this build, of the form
|
||||
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
|
||||
|
||||
@@ -31,7 +31,7 @@ case $# in
|
||||
if [ -z "$extra_hash_or_dir" ]; then
|
||||
# Nothing, empty extra hash is fine.
|
||||
extra_hash=""
|
||||
elif [ -d "$extra_hash_or_dir/.git" ]; then
|
||||
elif [ -e "$extra_hash_or_dir/.git" ]; then
|
||||
extra_hash=$(git_hash_dirty "$extra_hash_or_dir" HEAD)
|
||||
elif ! expr "$extra_hash_or_dir" : "^[0-9a-f]*$"; then
|
||||
echo "Invalid extra hash '$extra_hash_or_dir', must be a git commit or path to a git repo" >&2
|
||||
|
||||
@@ -25,45 +25,33 @@ type Filter struct {
|
||||
// tailscale must have a destination within local4 or local6,
|
||||
// regardless of the policy filter below. Zero values reject
|
||||
// all incoming traffic.
|
||||
local4 []net4
|
||||
local6 []net6
|
||||
local4 []netaddr.IPPrefix
|
||||
local6 []netaddr.IPPrefix
|
||||
// matches4 and matches6 are lists of match->action rules
|
||||
// applied to all packets arriving over tailscale
|
||||
// tunnels. Matches are checked in order, and processing stops
|
||||
// at the first matching rule. The default policy if no rules
|
||||
// match is to drop the packet.
|
||||
matches4 matches4
|
||||
matches6 matches6
|
||||
matches4 matches
|
||||
matches6 matches
|
||||
// state is the connection tracking state attached to this
|
||||
// filter. It is used to allow incoming traffic that is a response
|
||||
// to an outbound connection that this node made, even if those
|
||||
// incoming packets don't get accepted by matches above.
|
||||
state4 *filterState
|
||||
state6 *filterState
|
||||
state *filterState
|
||||
}
|
||||
|
||||
// tuple4 is a 4-tuple of source and destination IPv4 and port. It's
|
||||
// used as a lookup key in filterState.
|
||||
type tuple4 struct {
|
||||
SrcIP packet.IP4
|
||||
DstIP packet.IP4
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
}
|
||||
|
||||
// tuple6 is a 4-tuple of source and destination IPv6 and port. It's
|
||||
// used as a lookup key in filterState.
|
||||
type tuple6 struct {
|
||||
SrcIP packet.IP6
|
||||
DstIP packet.IP6
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
// tuple is a 4-tuple of source and destination IP and port. It's used
|
||||
// as a lookup key in filterState.
|
||||
type tuple struct {
|
||||
Src netaddr.IPPort
|
||||
Dst netaddr.IPPort
|
||||
}
|
||||
|
||||
// filterState is a state cache of past seen packets.
|
||||
type filterState struct {
|
||||
mu sync.Mutex
|
||||
lru *lru.Cache // of tuple4 or tuple6
|
||||
lru *lru.Cache // of tuple
|
||||
}
|
||||
|
||||
// lruMax is the size of the LRU cache in filterState.
|
||||
@@ -148,30 +136,58 @@ func NewAllowNone(logf logger.Logf) *Filter {
|
||||
// shares state with the previous one, to enable changing rules at
|
||||
// runtime without breaking existing stateful flows.
|
||||
func New(matches []Match, localNets []netaddr.IPPrefix, shareStateWith *Filter, logf logger.Logf) *Filter {
|
||||
var state4, state6 *filterState
|
||||
var state *filterState
|
||||
if shareStateWith != nil {
|
||||
state4 = shareStateWith.state4
|
||||
state6 = shareStateWith.state6
|
||||
state = shareStateWith.state
|
||||
} else {
|
||||
state4 = &filterState{
|
||||
lru: lru.New(lruMax),
|
||||
}
|
||||
state6 = &filterState{
|
||||
state = &filterState{
|
||||
lru: lru.New(lruMax),
|
||||
}
|
||||
}
|
||||
f := &Filter{
|
||||
logf: logf,
|
||||
matches4: newMatches4(matches),
|
||||
matches6: newMatches6(matches),
|
||||
local4: nets4FromIPPrefixes(localNets),
|
||||
local6: nets6FromIPPrefixes(localNets),
|
||||
state4: state4,
|
||||
state6: state6,
|
||||
matches4: matchesFamily(matches, netaddr.IP.Is4),
|
||||
matches6: matchesFamily(matches, netaddr.IP.Is6),
|
||||
local4: netsFamily(localNets, netaddr.IP.Is4),
|
||||
local6: netsFamily(localNets, netaddr.IP.Is6),
|
||||
state: state,
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func netsFamily(nets []netaddr.IPPrefix, keep func(netaddr.IP) bool) []netaddr.IPPrefix {
|
||||
var ret []netaddr.IPPrefix
|
||||
for _, net := range nets {
|
||||
if keep(net.IP) {
|
||||
ret = append(ret, net)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// matchesFamily returns the subset of ms for which keep(srcNet.IP)
|
||||
// and keep(dstNet.IP) are both true.
|
||||
func matchesFamily(ms matches, keep func(netaddr.IP) bool) matches {
|
||||
var ret matches
|
||||
for _, m := range ms {
|
||||
var retm Match
|
||||
for _, src := range m.Srcs {
|
||||
if keep(src.IP) {
|
||||
retm.Srcs = append(retm.Srcs, src)
|
||||
}
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if keep(dst.Net.IP) {
|
||||
retm.Dsts = append(retm.Dsts, dst)
|
||||
}
|
||||
}
|
||||
if len(retm.Srcs) > 0 && len(retm.Dsts) > 0 {
|
||||
ret = append(ret, retm)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func maybeHexdump(flag RunFlags, b []byte) string {
|
||||
if flag == 0 {
|
||||
return ""
|
||||
@@ -229,19 +245,17 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response {
|
||||
return Drop
|
||||
case srcIP.Is4():
|
||||
pkt.IPVersion = 4
|
||||
pkt.SrcIP4 = packet.IP4FromNetaddr(srcIP)
|
||||
pkt.DstIP4 = packet.IP4FromNetaddr(dstIP)
|
||||
case srcIP.Is6():
|
||||
pkt.IPVersion = 6
|
||||
pkt.SrcIP6 = packet.IP6FromNetaddr(srcIP)
|
||||
pkt.DstIP6 = packet.IP6FromNetaddr(dstIP)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
pkt.Src.IP = srcIP
|
||||
pkt.Dst.IP = dstIP
|
||||
pkt.IPProto = packet.TCP
|
||||
pkt.TCPFlags = packet.TCPSyn
|
||||
pkt.SrcPort = 0
|
||||
pkt.DstPort = dstPort
|
||||
pkt.Src.Port = 0
|
||||
pkt.Dst.Port = dstPort
|
||||
|
||||
return f.RunIn(pkt, 0)
|
||||
}
|
||||
@@ -287,7 +301,7 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||
// A compromised peer could try to send us packets for
|
||||
// destinations we didn't explicitly advertise. This check is to
|
||||
// prevent that.
|
||||
if !ip4InList(q.DstIP4, f.local4) {
|
||||
if !ipInList(q.Dst.IP, f.local4) {
|
||||
return Drop, "destination not allowed"
|
||||
}
|
||||
|
||||
@@ -320,11 +334,11 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||
return Accept, "tcp ok"
|
||||
}
|
||||
case packet.UDP:
|
||||
t := tuple4{q.SrcIP4, q.DstIP4, q.SrcPort, q.DstPort}
|
||||
t := tuple{q.Src, q.Dst}
|
||||
|
||||
f.state4.mu.Lock()
|
||||
_, ok := f.state4.lru.Get(t)
|
||||
f.state4.mu.Unlock()
|
||||
f.state.mu.Lock()
|
||||
_, ok := f.state.lru.Get(t)
|
||||
f.state.mu.Unlock()
|
||||
|
||||
if ok {
|
||||
return Accept, "udp cached"
|
||||
@@ -342,7 +356,7 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
||||
// A compromised peer could try to send us packets for
|
||||
// destinations we didn't explicitly advertise. This check is to
|
||||
// prevent that.
|
||||
if !ip6InList(q.DstIP6, f.local6) {
|
||||
if !ipInList(q.Dst.IP, f.local6) {
|
||||
return Drop, "destination not allowed"
|
||||
}
|
||||
|
||||
@@ -375,11 +389,11 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
||||
return Accept, "tcp ok"
|
||||
}
|
||||
case packet.UDP:
|
||||
t := tuple6{q.SrcIP6, q.DstIP6, q.SrcPort, q.DstPort}
|
||||
t := tuple{q.Src, q.Dst}
|
||||
|
||||
f.state6.mu.Lock()
|
||||
_, ok := f.state6.lru.Get(t)
|
||||
f.state6.mu.Unlock()
|
||||
f.state.mu.Lock()
|
||||
_, ok := f.state.lru.Get(t)
|
||||
f.state.mu.Unlock()
|
||||
|
||||
if ok {
|
||||
return Accept, "udp cached"
|
||||
@@ -399,20 +413,11 @@ func (f *Filter) runOut(q *packet.Parsed) (r Response, why string) {
|
||||
return Accept, "ok out"
|
||||
}
|
||||
|
||||
switch q.IPVersion {
|
||||
case 4:
|
||||
t := tuple4{q.DstIP4, q.SrcIP4, q.DstPort, q.SrcPort}
|
||||
var ti interface{} = t // allocate once, rather than twice inside mutex
|
||||
f.state4.mu.Lock()
|
||||
f.state4.lru.Add(ti, ti)
|
||||
f.state4.mu.Unlock()
|
||||
case 6:
|
||||
t := tuple6{q.DstIP6, q.SrcIP6, q.DstPort, q.SrcPort}
|
||||
var ti interface{} = t // allocate once, rather than twice inside mutex
|
||||
f.state6.mu.Lock()
|
||||
f.state6.lru.Add(ti, ti)
|
||||
f.state6.mu.Unlock()
|
||||
}
|
||||
t := tuple{q.Dst, q.Src}
|
||||
var ti interface{} = t // allocate once, rather than twice inside mutex
|
||||
f.state.mu.Lock()
|
||||
f.state.lru.Add(ti, ti)
|
||||
f.state.mu.Unlock()
|
||||
return Accept, "ok out"
|
||||
}
|
||||
|
||||
@@ -436,6 +441,8 @@ func (d direction) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
var gcpDNSAddr = netaddr.IPv4(169, 254, 169, 254)
|
||||
|
||||
// pre runs the direction-agnostic filter logic. dir is only used for
|
||||
// logging.
|
||||
func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) Response {
|
||||
@@ -448,25 +455,13 @@ func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) Response {
|
||||
return Drop
|
||||
}
|
||||
|
||||
switch q.IPVersion {
|
||||
case 4:
|
||||
if q.DstIP4.IsMulticast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "multicast")
|
||||
return Drop
|
||||
}
|
||||
if q.DstIP4.IsMostLinkLocalUnicast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
||||
return Drop
|
||||
}
|
||||
case 6:
|
||||
if q.DstIP6.IsMulticast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "multicast")
|
||||
return Drop
|
||||
}
|
||||
if q.DstIP6.IsLinkLocalUnicast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
||||
return Drop
|
||||
}
|
||||
if q.Dst.IP.IsMulticast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "multicast")
|
||||
return Drop
|
||||
}
|
||||
if q.Dst.IP.IsLinkLocalUnicast() && q.Dst.IP != gcpDNSAddr {
|
||||
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
||||
return Drop
|
||||
}
|
||||
|
||||
switch q.IPProto {
|
||||
@@ -493,12 +488,5 @@ func omitDropLogging(p *packet.Parsed, dir direction) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
switch p.IPVersion {
|
||||
case 4:
|
||||
return p.DstIP4.IsMulticast() || p.DstIP4.IsMostLinkLocalUnicast() || p.IPProto == packet.IGMP
|
||||
case 6:
|
||||
return p.DstIP6.IsMulticast() || p.DstIP6.IsLinkLocalUnicast()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return p.Dst.IP.IsMulticast() || (p.Dst.IP.IsLinkLocalUnicast() && p.Dst.IP != gcpDNSAddr) || p.IPProto == packet.IGMP
|
||||
}
|
||||
|
||||
@@ -94,9 +94,9 @@ func TestFilter(t *testing.T) {
|
||||
if test.p.IPProto == packet.TCP {
|
||||
var got Response
|
||||
if test.p.IPVersion == 4 {
|
||||
got = acl.CheckTCP(test.p.SrcIP4.Netaddr(), test.p.DstIP4.Netaddr(), test.p.DstPort)
|
||||
got = acl.CheckTCP(test.p.Src.IP, test.p.Dst.IP, test.p.Dst.Port)
|
||||
} else {
|
||||
got = acl.CheckTCP(test.p.SrcIP6.Netaddr(), test.p.DstIP6.Netaddr(), test.p.DstPort)
|
||||
got = acl.CheckTCP(test.p.Src.IP, test.p.Dst.IP, test.p.Dst.Port)
|
||||
}
|
||||
if test.want != got {
|
||||
t.Errorf("#%d CheckTCP got=%v want=%v packet:%v", i, got, test.want, test.p)
|
||||
@@ -194,7 +194,7 @@ func TestNoAllocs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIP(t *testing.T) {
|
||||
func TestParseIPSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
bits int
|
||||
@@ -203,23 +203,33 @@ func TestParseIP(t *testing.T) {
|
||||
}{
|
||||
{"8.8.8.8", 24, pfx("8.8.8.8/24"), ""},
|
||||
{"2601:1234::", 64, pfx("2601:1234::/64"), ""},
|
||||
{"8.8.8.8", 33, nil, `invalid CIDR size 33 for host "8.8.8.8"`},
|
||||
{"8.8.8.8", -1, nil, `invalid CIDR size -1 for host "8.8.8.8"`},
|
||||
{"2601:1234::", 129, nil, `invalid CIDR size 129 for host "2601:1234::"`},
|
||||
{"0.0.0.0", 24, nil, `ports="0.0.0.0": to allow all IP addresses, use *:port, not 0.0.0.0:port`},
|
||||
{"::", 64, nil, `ports="::": to allow all IP addresses, use *:port, not [::]:port`},
|
||||
{"8.8.8.8", 33, nil, `invalid CIDR size 33 for IP "8.8.8.8"`},
|
||||
{"8.8.8.8", -1, pfx("8.8.8.8/32"), ""},
|
||||
{"8.8.8.8", 32, pfx("8.8.8.8/32"), ""},
|
||||
{"8.8.8.8/24", -1, nil, "8.8.8.8/24 contains non-network bits set"},
|
||||
{"8.8.8.0/24", 18, pfx("8.8.8.0/24"), ""}, // the 18 is ignored
|
||||
{"1.0.0.0-1.255.255.255", 5, pfx("1.0.0.0/8"), ""},
|
||||
{"1.0.0.0-2.1.2.3", 5, pfx("1.0.0.0/8", "2.0.0.0/16", "2.1.0.0/23", "2.1.2.0/30"), ""},
|
||||
{"1.0.0.2-1.0.0.1", -1, nil, "invalid IP range \"1.0.0.2-1.0.0.1\""},
|
||||
{"2601:1234::", 129, nil, `invalid CIDR size 129 for IP "2601:1234::"`},
|
||||
{"0.0.0.0", 24, pfx("0.0.0.0/24"), ""},
|
||||
{"::", 64, pfx("::/64"), ""},
|
||||
{"*", 24, pfx("0.0.0.0/0", "::/0"), ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := parseIP(tt.host, tt.bits)
|
||||
var bits *int
|
||||
if tt.bits != -1 {
|
||||
bits = &tt.bits
|
||||
}
|
||||
got, err := parseIPSet(tt.host, bits)
|
||||
if err != nil {
|
||||
if err.Error() == tt.wantErr {
|
||||
continue
|
||||
}
|
||||
t.Errorf("parseIP(%q, %v) error: %v; want error %q", tt.host, tt.bits, err, tt.wantErr)
|
||||
t.Errorf("parseIPSet(%q, %v) error: %v; want error %q", tt.host, tt.bits, err, tt.wantErr)
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.want, cmp.Comparer(func(a, b netaddr.IP) bool { return a == b })); diff != "" {
|
||||
t.Errorf("parseIP(%q, %v) = %s; want %s", tt.host, tt.bits, got, tt.want)
|
||||
t.Errorf("parseIPSet(%q, %v) = %s; want %s", tt.host, tt.bits, got, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -335,19 +345,19 @@ func TestOmitDropLogging(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "v4_multicast_out_low",
|
||||
pkt: &packet.Parsed{IPVersion: 4, DstIP4: mustIP4("224.0.0.0")},
|
||||
pkt: &packet.Parsed{IPVersion: 4, Dst: mustIPPort("224.0.0.0:0")},
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "v4_multicast_out_high",
|
||||
pkt: &packet.Parsed{IPVersion: 4, DstIP4: mustIP4("239.255.255.255")},
|
||||
pkt: &packet.Parsed{IPVersion: 4, Dst: mustIPPort("239.255.255.255:0")},
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "v4_link_local_unicast",
|
||||
pkt: &packet.Parsed{IPVersion: 4, DstIP4: mustIP4("169.254.1.2")},
|
||||
pkt: &packet.Parsed{IPVersion: 4, Dst: mustIPPort("169.254.1.2:0")},
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
@@ -377,18 +387,16 @@ func parsed(proto packet.IPProto, src, dst string, sport, dport uint16) packet.P
|
||||
var ret packet.Parsed
|
||||
ret.Decode(dummyPacket)
|
||||
ret.IPProto = proto
|
||||
ret.SrcPort = sport
|
||||
ret.DstPort = dport
|
||||
ret.Src.IP = sip
|
||||
ret.Src.Port = sport
|
||||
ret.Dst.IP = dip
|
||||
ret.Dst.Port = dport
|
||||
ret.TCPFlags = packet.TCPSyn
|
||||
|
||||
if sip.Is4() {
|
||||
ret.IPVersion = 4
|
||||
ret.SrcIP4 = packet.IP4FromNetaddr(sip)
|
||||
ret.DstIP4 = packet.IP4FromNetaddr(dip)
|
||||
} else {
|
||||
ret.IPVersion = 6
|
||||
ret.SrcIP6 = packet.IP6FromNetaddr(sip)
|
||||
ret.DstIP6 = packet.IP6FromNetaddr(dip)
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -397,8 +405,8 @@ func parsed(proto packet.IPProto, src, dst string, sport, dport uint16) packet.P
|
||||
func raw6(proto packet.IPProto, src, dst string, sport, dport uint16, trimLen int) []byte {
|
||||
u := packet.UDP6Header{
|
||||
IP6Header: packet.IP6Header{
|
||||
SrcIP: packet.IP6FromNetaddr(mustIP(src)),
|
||||
DstIP: packet.IP6FromNetaddr(mustIP(dst)),
|
||||
Src: mustIP(src),
|
||||
Dst: mustIP(dst),
|
||||
},
|
||||
SrcPort: sport,
|
||||
DstPort: dport,
|
||||
@@ -426,8 +434,8 @@ func raw6(proto packet.IPProto, src, dst string, sport, dport uint16, trimLen in
|
||||
func raw4(proto packet.IPProto, src, dst string, sport, dport uint16, trimLength int) []byte {
|
||||
u := packet.UDP4Header{
|
||||
IP4Header: packet.IP4Header{
|
||||
SrcIP: packet.IP4FromNetaddr(mustIP(src)),
|
||||
DstIP: packet.IP4FromNetaddr(mustIP(dst)),
|
||||
Src: mustIP(src),
|
||||
Dst: mustIP(dst),
|
||||
},
|
||||
SrcPort: sport,
|
||||
DstPort: dport,
|
||||
@@ -478,12 +486,12 @@ func parseHexPkt(t *testing.T, h string) *packet.Parsed {
|
||||
return p
|
||||
}
|
||||
|
||||
func mustIP4(s string) packet.IP4 {
|
||||
ip, err := netaddr.ParseIP(s)
|
||||
func mustIPPort(s string) netaddr.IPPort {
|
||||
ipp, err := netaddr.ParseIPPort(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return packet.IP4FromNetaddr(ip)
|
||||
return ipp
|
||||
}
|
||||
|
||||
func pfx(strs ...string) (ret []netaddr.IPPrefix) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
// PortRange is a range of TCP and UDP ports.
|
||||
@@ -71,3 +72,46 @@ func (m Match) String() string {
|
||||
}
|
||||
return fmt.Sprintf("%v=>%v", ss, ds)
|
||||
}
|
||||
|
||||
type matches []Match
|
||||
|
||||
func (ms matches) match(q *packet.Parsed) bool {
|
||||
for _, m := range ms {
|
||||
if !ipInList(q.Src.IP, m.Srcs) {
|
||||
continue
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if !dst.Net.Contains(q.Dst.IP) {
|
||||
continue
|
||||
}
|
||||
if !dst.Ports.contains(q.Dst.Port) {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ms matches) matchIPsOnly(q *packet.Parsed) bool {
|
||||
for _, m := range ms {
|
||||
if !ipInList(q.Src.IP, m.Srcs) {
|
||||
continue
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if dst.Net.Contains(q.Dst.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ipInList(ip netaddr.IP, netlist []netaddr.IPPrefix) bool {
|
||||
for _, net := range netlist {
|
||||
if net.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,151 +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 filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
type net4 struct {
|
||||
ip packet.IP4
|
||||
mask packet.IP4
|
||||
}
|
||||
|
||||
func net4FromIPPrefix(pfx netaddr.IPPrefix) net4 {
|
||||
if !pfx.IP.Is4() {
|
||||
panic("net4FromIPPrefix given non-ipv4 prefix")
|
||||
}
|
||||
return net4{
|
||||
ip: packet.IP4FromNetaddr(pfx.IP),
|
||||
mask: netmask4(pfx.Bits),
|
||||
}
|
||||
}
|
||||
|
||||
func nets4FromIPPrefixes(pfxs []netaddr.IPPrefix) (ret []net4) {
|
||||
for _, pfx := range pfxs {
|
||||
if pfx.IP.Is4() {
|
||||
ret = append(ret, net4FromIPPrefix(pfx))
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (n net4) Contains(ip packet.IP4) bool {
|
||||
return (n.ip & n.mask) == (ip & n.mask)
|
||||
}
|
||||
|
||||
func (n net4) Bits() int {
|
||||
return 32 - bits.TrailingZeros32(uint32(n.mask))
|
||||
}
|
||||
|
||||
func (n net4) String() string {
|
||||
b := n.Bits()
|
||||
if b == 32 {
|
||||
return n.ip.String()
|
||||
} else if b == 0 {
|
||||
return "*"
|
||||
} else {
|
||||
return fmt.Sprintf("%s/%d", n.ip, b)
|
||||
}
|
||||
}
|
||||
|
||||
type npr4 struct {
|
||||
net net4
|
||||
ports PortRange
|
||||
}
|
||||
|
||||
func (npr npr4) String() string {
|
||||
return fmt.Sprintf("%s:%s", npr.net, npr.ports)
|
||||
}
|
||||
|
||||
type match4 struct {
|
||||
srcs []net4
|
||||
dsts []npr4
|
||||
}
|
||||
|
||||
type matches4 []match4
|
||||
|
||||
func (ms matches4) String() string {
|
||||
var b strings.Builder
|
||||
for _, m := range ms {
|
||||
fmt.Fprintf(&b, "%s => %s\n", m.srcs, m.dsts)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func newMatches4(ms []Match) (ret matches4) {
|
||||
for _, m := range ms {
|
||||
var m4 match4
|
||||
for _, src := range m.Srcs {
|
||||
if src.IP.Is4() {
|
||||
m4.srcs = append(m4.srcs, net4FromIPPrefix(src))
|
||||
}
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if dst.Net.IP.Is4() {
|
||||
m4.dsts = append(m4.dsts, npr4{net4FromIPPrefix(dst.Net), dst.Ports})
|
||||
}
|
||||
}
|
||||
if len(m4.srcs) > 0 && len(m4.dsts) > 0 {
|
||||
ret = append(ret, m4)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// match returns whether q's source IP and destination IP:port match
|
||||
// any of ms.
|
||||
func (ms matches4) match(q *packet.Parsed) bool {
|
||||
for _, m := range ms {
|
||||
if !ip4InList(q.SrcIP4, m.srcs) {
|
||||
continue
|
||||
}
|
||||
for _, dst := range m.dsts {
|
||||
if !dst.net.Contains(q.DstIP4) {
|
||||
continue
|
||||
}
|
||||
if !dst.ports.contains(q.DstPort) {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchIPsOnly returns whether q's source and destination IP match
|
||||
// any of ms.
|
||||
func (ms matches4) matchIPsOnly(q *packet.Parsed) bool {
|
||||
for _, m := range ms {
|
||||
if !ip4InList(q.SrcIP4, m.srcs) {
|
||||
continue
|
||||
}
|
||||
for _, dst := range m.dsts {
|
||||
if dst.net.Contains(q.DstIP4) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func netmask4(bits uint8) packet.IP4 {
|
||||
b := ^uint32((1 << (32 - bits)) - 1)
|
||||
return packet.IP4(b)
|
||||
}
|
||||
|
||||
func ip4InList(ip packet.IP4, netlist []net4) bool {
|
||||
for _, net := range netlist {
|
||||
if net.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,171 +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 filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
type net6 struct {
|
||||
ip packet.IP6
|
||||
mask packet.IP6
|
||||
}
|
||||
|
||||
func net6FromIPPrefix(pfx netaddr.IPPrefix) net6 {
|
||||
if !pfx.IP.Is6() {
|
||||
panic("net6FromIPPrefix given non-ipv6 prefix")
|
||||
}
|
||||
var mask packet.IP6
|
||||
if pfx.Bits > 64 {
|
||||
mask.Hi = ^uint64(0)
|
||||
mask.Lo = (^uint64(0) << (128 - pfx.Bits))
|
||||
} else {
|
||||
mask.Hi = (^uint64(0) << (64 - pfx.Bits))
|
||||
}
|
||||
|
||||
return net6{
|
||||
ip: packet.IP6FromNetaddr(pfx.IP),
|
||||
mask: mask,
|
||||
}
|
||||
}
|
||||
|
||||
func nets6FromIPPrefixes(pfxs []netaddr.IPPrefix) (ret []net6) {
|
||||
for _, pfx := range pfxs {
|
||||
if pfx.IP.Is6() {
|
||||
ret = append(ret, net6FromIPPrefix(pfx))
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (n net6) Contains(ip packet.IP6) bool {
|
||||
// This is equivalent to the more straightforward implementation:
|
||||
// ((n.ip.Hi & n.mask.Hi) == (ip.Hi & n.mask.Hi) &&
|
||||
// (n.ip.Lo & n.mask.Lo) == (ip.Lo & n.mask.Lo))
|
||||
//
|
||||
// This implementation runs significantly faster because it
|
||||
// eliminates branches and minimizes the required
|
||||
// bit-twiddling.
|
||||
a := (n.ip.Hi ^ ip.Hi) & n.mask.Hi
|
||||
b := (n.ip.Lo ^ ip.Lo) & n.mask.Lo
|
||||
return (a | b) == 0
|
||||
}
|
||||
|
||||
func (n net6) Bits() int {
|
||||
return 128 - bits.TrailingZeros64(n.mask.Hi) - bits.TrailingZeros64(n.mask.Lo)
|
||||
}
|
||||
|
||||
func (n net6) String() string {
|
||||
switch n.Bits() {
|
||||
case 128:
|
||||
return n.ip.String()
|
||||
case 0:
|
||||
return "*"
|
||||
default:
|
||||
return fmt.Sprintf("%s/%d", n.ip, n.Bits())
|
||||
}
|
||||
}
|
||||
|
||||
type npr6 struct {
|
||||
net net6
|
||||
ports PortRange
|
||||
}
|
||||
|
||||
func (npr npr6) String() string {
|
||||
return fmt.Sprintf("%s:%s", npr.net, npr.ports)
|
||||
}
|
||||
|
||||
type match6 struct {
|
||||
srcs []net6
|
||||
dsts []npr6
|
||||
}
|
||||
|
||||
type matches6 []match6
|
||||
|
||||
func (ms matches6) String() string {
|
||||
var b strings.Builder
|
||||
for _, m := range ms {
|
||||
fmt.Fprintf(&b, "%s => %s\n", m.srcs, m.dsts)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func newMatches6(ms []Match) (ret matches6) {
|
||||
for _, m := range ms {
|
||||
var m6 match6
|
||||
for _, src := range m.Srcs {
|
||||
if src.IP.Is6() {
|
||||
m6.srcs = append(m6.srcs, net6FromIPPrefix(src))
|
||||
}
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if dst.Net.IP.Is6() {
|
||||
m6.dsts = append(m6.dsts, npr6{net6FromIPPrefix(dst.Net), dst.Ports})
|
||||
}
|
||||
}
|
||||
if len(m6.srcs) > 0 && len(m6.dsts) > 0 {
|
||||
ret = append(ret, m6)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ms matches6) match(q *packet.Parsed) bool {
|
||||
outer:
|
||||
for i := range ms {
|
||||
srcs := ms[i].srcs
|
||||
for j := range srcs {
|
||||
if srcs[j].Contains(q.SrcIP6) {
|
||||
dsts := ms[i].dsts
|
||||
for k := range dsts {
|
||||
if dsts[k].net.Contains(q.DstIP6) && dsts[k].ports.contains(q.DstPort) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// We hit on src, but missed on all
|
||||
// dsts. No need to try other srcs,
|
||||
// they'll never fully match.
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ms matches6) matchIPsOnly(q *packet.Parsed) bool {
|
||||
outer:
|
||||
for i := range ms {
|
||||
srcs := ms[i].srcs
|
||||
for j := range srcs {
|
||||
if srcs[j].Contains(q.SrcIP6) {
|
||||
dsts := ms[i].dsts
|
||||
for k := range dsts {
|
||||
if dsts[k].net.Contains(q.DstIP6) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// We hit on src, but missed on all
|
||||
// dsts. No need to try other srcs,
|
||||
// they'll never fully match.
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ip6InList(ip packet.IP6, netlist []net6) bool {
|
||||
for _, net := range netlist {
|
||||
if net.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,37 +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 filter
|
||||
|
||||
import "testing"
|
||||
|
||||
// Verifies that the fast bit-twiddling implementation of Contains
|
||||
// works the same as the easy-to-read implementation. Since we can't
|
||||
// sensibly check it on 128 bits, the test runs over 4-bit
|
||||
// "IPs". Bit-twiddling is the same at any width, so this adequately
|
||||
// proves that the implementations are equivalent.
|
||||
func TestOptimizedContains(t *testing.T) {
|
||||
for ipHi := 0; ipHi < 0xf; ipHi++ {
|
||||
for ipLo := 0; ipLo < 0xf; ipLo++ {
|
||||
for nIPHi := 0; nIPHi < 0xf; nIPHi++ {
|
||||
for nIPLo := 0; nIPLo < 0xf; nIPLo++ {
|
||||
for maskHi := 0; maskHi < 0xf; maskHi++ {
|
||||
for maskLo := 0; maskLo < 0xf; maskLo++ {
|
||||
|
||||
a := (nIPHi ^ ipHi) & maskHi
|
||||
b := (nIPLo ^ ipLo) & maskLo
|
||||
got := (a | b) == 0
|
||||
|
||||
want := ((nIPHi&maskHi) == (ipHi&maskHi) && (nIPLo&maskLo) == (ipLo&maskLo))
|
||||
|
||||
if got != want {
|
||||
t.Errorf("mask %1x%1x/%1x%1x %1x%1x got=%v want=%v", nIPHi, nIPLo, maskHi, maskLo, ipHi, ipLo, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -22,11 +23,11 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
|
||||
m := Match{}
|
||||
|
||||
for i, s := range r.SrcIPs {
|
||||
bits := 32
|
||||
var bits *int
|
||||
if len(r.SrcBits) > i {
|
||||
bits = r.SrcBits[i]
|
||||
bits = &r.SrcBits[i]
|
||||
}
|
||||
nets, err := parseIP(s, bits)
|
||||
nets, err := parseIPSet(s, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
@@ -35,11 +36,7 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
|
||||
}
|
||||
|
||||
for _, d := range r.DstPorts {
|
||||
bits := 32
|
||||
if d.Bits != nil {
|
||||
bits = *d.Bits
|
||||
}
|
||||
nets, err := parseIP(d.IP, bits)
|
||||
nets, err := parseIPSet(d.IP, d.Bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
@@ -65,35 +62,65 @@ var (
|
||||
zeroIP6 = netaddr.IPFrom16([16]byte{})
|
||||
)
|
||||
|
||||
func parseIP(host string, defaultBits int) ([]netaddr.IPPrefix, error) {
|
||||
if host == "*" {
|
||||
// User explicitly requested wildcard dst ip.
|
||||
// parseIPSet parses arg as one:
|
||||
//
|
||||
// * 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")
|
||||
//
|
||||
// bits, if non-nil, is the legacy SrcBits CIDR length to make a IP
|
||||
// address (without a slash) treated as a CIDR of *bits length.
|
||||
//
|
||||
// TODO(bradfitz): make this return an IPSet and plumb that all
|
||||
// around, and ultimately use a new version of IPSet.ContainsFunc like
|
||||
// Contains16Func that works in [16]byte address, so we we can match
|
||||
// at runtime without allocating?
|
||||
func parseIPSet(arg string, bits *int) ([]netaddr.IPPrefix, error) {
|
||||
if arg == "*" {
|
||||
// User explicitly requested wildcard.
|
||||
return []netaddr.IPPrefix{
|
||||
{IP: zeroIP4, Bits: 0},
|
||||
{IP: zeroIP6, Bits: 0},
|
||||
}, nil
|
||||
}
|
||||
|
||||
ip, err := netaddr.ParseIP(host)
|
||||
if strings.Contains(arg, "/") {
|
||||
pfx, err := netaddr.ParseIPPrefix(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pfx != pfx.Masked() {
|
||||
return nil, fmt.Errorf("%v contains non-network bits set", pfx)
|
||||
}
|
||||
return []netaddr.IPPrefix{pfx}, nil
|
||||
}
|
||||
if strings.Count(arg, "-") == 1 {
|
||||
i := strings.Index(arg, "-")
|
||||
ip1s, ip2s := arg[:i], arg[i+1:]
|
||||
ip1, err := netaddr.ParseIP(ip1s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ip2, err := netaddr.ParseIP(ip2s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := netaddr.IPRange{From: ip1, To: ip2}
|
||||
if !r.Valid() {
|
||||
return nil, fmt.Errorf("invalid IP range %q", arg)
|
||||
}
|
||||
return r.Prefixes(), nil
|
||||
}
|
||||
ip, err := netaddr.ParseIP(arg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ports=%#v: invalid IP address", host)
|
||||
return nil, fmt.Errorf("invalid IP address %q", arg)
|
||||
}
|
||||
if ip == zeroIP4 {
|
||||
// For clarity, reject 0.0.0.0 as an input
|
||||
return nil, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", host)
|
||||
bits8 := ip.BitLen()
|
||||
if bits != nil {
|
||||
if *bits < 0 || *bits > int(bits8) {
|
||||
return nil, fmt.Errorf("invalid CIDR size %d for IP %q", *bits, arg)
|
||||
}
|
||||
bits8 = uint8(*bits)
|
||||
}
|
||||
if ip == zeroIP6 {
|
||||
// For clarity, reject :: as an input
|
||||
return nil, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not [::]:port", host)
|
||||
}
|
||||
|
||||
if defaultBits < 0 || (ip.Is4() && defaultBits > 32) || (ip.Is6() && defaultBits > 128) {
|
||||
return nil, fmt.Errorf("invalid CIDR size %d for host %q", defaultBits, host)
|
||||
}
|
||||
return []netaddr.IPPrefix{
|
||||
{
|
||||
IP: ip,
|
||||
Bits: uint8(defaultBits),
|
||||
},
|
||||
}, nil
|
||||
return []netaddr.IPPrefix{{IP: ip, Bits: bits8}}, nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user