Compare commits

...

23 Commits

Author SHA1 Message Date
Walter Poupore
25c92913ef README.md: update how to find tailnet
Signed-off-by: Walter Poupore <walterp@tailscale.com>
2022-09-19 17:10:39 -07:00
Maisem Ali
6632504f45 cmd/tailscale/cli: [up] move lose-ssh check after other validations
The check was happening too early and in the case of error would wait 5
s and then error out. This makes it so that it does validations before
the SSH check.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-19 12:04:14 -07:00
Maisem Ali
054ef4de56 tailcfg: mark CapabilityFileSharingTarget as inter-node
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-19 11:08:34 -07:00
Brad Fitzpatrick
d045462dfb ipn/ipnlocal: add c2n method to get SSH username candidates
For control to fetch a list of Tailscale SSH username candidates to
filter against the Tailnet's SSH policy to present some valid
candidates to a user.

Updates #3802
Updates tailscale/corp#7007

Change-Id: I3dce57b7a35e66891d5e5572e13ae6ef3c898498
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-19 10:37:04 -07:00
Brad Fitzpatrick
d8eb111ac8 .github/workflows: add cross-android
This would've caught the regression from 7c49db02a before it was
submitted so 42f1d92ae0 wouldn't have been necessary to fix it.

Updates #4482

Change-Id: Ia4a9977e21853f68df96f043672c86a86c0181db
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-18 09:56:19 -07:00
Brad Fitzpatrick
832031d54b wgengine/magicsock: fix recently introduced data race
From 5c42990c2f, not yet released in a stable build.
Caught by existing tests.

Fixes #5685

Change-Id: Ia76bb328809d9644e8b96910767facf627830600
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-18 08:07:57 -07:00
Denton Gentry
42f1d92ae0 net/netns: implement UseSocketMark for Android.
Build fails on Android:
`../../../../go/pkg/mod/tailscale.com@v1.1.1-0.20220916223019-65c24b6334e9/wgengine/magicsock/magicsock_linux.go:133:12: undefined: netns.UseSocketMark`

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-09-17 23:19:24 -07:00
Brad Fitzpatrick
41bb47de0e cmd/tailscaled: respect $PORT on all platforms, not just Linux
Updates #5114

Change-Id: I6c6e28c493d6a026a03088157d08f9fd182ef373
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-17 12:30:29 -07:00
Brad Fitzpatrick
3562b5bdfa envknob, health: support Synology, show parse errors in status
Updates #5114

Change-Id: I8ac7a22a511f5a7d0dcb8cac470d4a403aa8c817
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-17 08:42:41 -07:00
phirework
5c42990c2f wgengine/magicsock: add client flag and envknob to disable heartbeat (#5638)
Baby steps towards turning off heartbeat pings entirely as per #540.
This doesn't change any current magicsock functionality and requires additional
changes to send/disco paths before the flag can be turned on.

Updates #540

Change-Id: Idc9a72748e74145b068d67e6dd4a4ffe3932efd0
Signed-off-by: Jenny Zhang <jz@tailscale.com>

Signed-off-by: Jenny Zhang <jz@tailscale.com>
2022-09-16 23:48:46 -04:00
Brad Fitzpatrick
65c24b6334 envknob: generalize Windows tailscaled-env.txt support
ipnserver previously had support for a Windows-only environment
variable mechanism that further only worked when Windows was running
as a service, not from a console.

But we want it to work from tailscaed too, and we want it to work on
macOS and Synology. So move it to envknob, now that envknob can change
values at runtime post-init.

A future change will wire this up for more platforms, and do something
more for CLI flags like --port, which the bug was originally about.

Updates #5114

Change-Id: I9fd69a9a91bb0f308fc264d4a6c33e0cbe352d71
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 15:30:19 -07:00
Brad Fitzpatrick
4bda41e701 Dockerfile: add test that build-env Alpine version matches go.mod
So things like #5660 don't happen in the future.

Change-Id: I01234f241e297d5b7bdd18da1bb3cc5420ad2225
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 12:19:09 -07:00
Andrew Dunham
9b71008ef2 control/controlhttp: move Dial options into options struct (#5661)
This turns 'dialParams' into something more like net.Dialer, where
configuration fields are public on the struct.

Split out of #5648

Change-Id: I0c56fd151dc5489c3c94fb40d18fd639e06473bc
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-16 15:06:25 -04:00
Luis Peralta
5623ef0271 Update Dockerfile to use golang:1.19-alpine
Tailscale @4a82b31 does not build in the container image due to using golang:1.18 image

Signed-off-by: Luis Peralta <luis.peralta@gmail.com>
2022-09-16 11:40:31 -07:00
Tyler Lee
486eecc063 Switched Secret snippet to match run.sh
Signed-off-by: Tyler Lee <tyler.lee@radius.ai>
2022-09-16 11:20:33 -07:00
Tyler Lee
b830c9975f Updated secret example in readme to match the sidecar key value
Signed-off-by: Tyler Lee <tyler.lee@radius.ai>
2022-09-16 11:20:33 -07:00
Brad Fitzpatrick
4a82b317b7 ipn/{ipnlocal,localapi}: use strs.CutPrefix, add more domain validation
The GitHub CodeQL scanner flagged the localapi's cert domain usage as a problem
because user input in the URL made it to disk stat checks.

The domain is validated against the ipnstate.Status later, and only
authenticated root/configured users can hit this, but add some
paranoia anyway.

Change-Id: I373ef23832f1d8b3a27208bc811b6588ae5a1ddd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 05:52:33 -07:00
Eng Zer Jun
f0347e841f refactor: move from io/ioutil to io and os packages
The io/ioutil package has been deprecated as of Go 1.16 [1]. This commit
replaces the existing io/ioutil functions with their new definitions in
io and os packages.

Reference: https://golang.org/doc/go1.16#ioutil
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2022-09-15 21:45:53 -07:00
Mihai Parparita
027111fb5a derp: update DERP acronym expansion
Makes the package description consistent with other documentation.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-15 16:08:12 -07:00
Mihai Parparita
1ce0e558a7 cmd/derper, control/controlhttp: disable WebSocket compression
The data that we send over WebSockets is encrypted and thus not
compressible. Additionally, Safari has a broken implementation of compression
(see nhooyr/websocket#218) that makes enabling it actively harmful.

Fixes tailscale/corp#6943

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-15 15:35:49 -07:00
Brad Fitzpatrick
74674b110d envknob: support changing envknobs post-init
Updates #5114

Change-Id: Ia423fc7486e1b3f3180a26308278be0086fae49b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 15:04:02 -07:00
Brad Fitzpatrick
33ee2c058e wgengine: update comments, remove redundant code in forceFullWireguardConfig
Change-Id: I464a0bce36e3a362c7d7ace0e8d2dd77fa825ee2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 13:03:18 -07:00
Brad Fitzpatrick
d34dd43562 ipn/ipnlocal: remove unused envknob
Change-Id: I6d18af2c469eb660e6ca81d1dcc2af33c9e628aa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 11:06:15 -07:00
108 changed files with 1385 additions and 524 deletions

54
.github/workflows/cross-android.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Android-Cross
on:
push:
branches:
- main
pull_request:
branches:
- '*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
id: go
- name: Android smoke build
# Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed
# and is only arm64. But it's a smoke build: it's not meant to catch everything. But it'll catch
# some Android breakages early.
# TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482
env:
GOOS: android
GOARCH: arm64
run: go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/interfaces ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

View File

@@ -32,7 +32,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.18-alpine AS build-env
FROM golang:1.19-alpine AS build-env
WORKDIR /go/src/tailscale

132
api.md
View File

@@ -3,11 +3,12 @@
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
# Authentication
Currently based on {some authentication method}. Visit the [admin panel](https://login.tailscale.com/admin) and navigate to the `Settings` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints (leave the password blank).
# APIs
* **[Devices](#device)**
- **[Devices](#device)**
- [GET device](#device-get)
- [DELETE device](#device-delete)
- Routes
@@ -19,12 +20,12 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
- [POST device tags](#device-tags-post)
- Key
- [POST device key](#device-key-post)
* **[Tailnets](#tailnet)**
- **[Tailnets](#tailnet)**
- ACLs
- [GET tailnet ACL](#tailnet-acl-get)
- [POST tailnet ACL](#tailnet-acl-post)
- [POST tailnet ACL preview](#tailnet-acl-preview-post)
- [POST tailnet ACL validate](#tailnet-acl-validate-post)
- [POST tailnet ACL validate](#tailnet-acl-validate-post)
- [Devices](#tailnet-devices)
- [GET tailnet devices](#tailnet-devices-get)
- [Keys](#tailnet-keys)
@@ -41,7 +42,9 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
## Device
<!-- TODO: description about what devices are -->
Each Tailscale-connected device has a globally-unique identifier number which we refer as the "deviceID" or sometimes, just "id".
You can use the deviceID to specify operations on a specific device, like retrieving its subnet routes.
@@ -52,20 +55,23 @@ This is your deviceID.
<a name=device-get></a>
#### `GET /api/v2/device/:deviceid` - lists the details for a device
Returns the details for the specified device.
Supply the device of interest in the path using its ID.
Use the `fields` query parameter to explicitly indicate which fields are returned.
##### Parameters
##### Query Parameters
`fields` - Controls which fields will be included in the returned response.
Currently, supported options are:
* `all`: returns all fields in the response.
* `default`: return all fields except:
* `enabledRoutes`
* `advertisedRoutes`
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
- `all`: returns all fields in the response.
- `default`: return all fields except:
- `enabledRoutes`
- `advertisedRoutes`
- `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
Use commas to separate multiple options.
If more than one option is indicated, then the union is used.
@@ -73,6 +79,7 @@ For example, for `fields=default,all`, all fields are returned.
If the `fields` parameter is not provided, then the default option is used.
##### Example
```
GET /api/v2/device/12345
curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
@@ -80,6 +87,7 @@ curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
```
Response
```
{
"addresses":[
@@ -141,16 +149,18 @@ Response
<a name=device-delete></a>
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
Deletes the provided device from its tailnet.
The device must belong to the user's tailnet.
Deleting shared/external devices is not supported.
Supply the device of interest in the path using its ID.
##### Parameters
No parameters.
##### Example
```
DELETE /api/v2/device/12345
curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
@@ -160,6 +170,7 @@ curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
Response
If successful, the response should be empty:
```
< HTTP/1.1 200 OK
...
@@ -168,13 +179,13 @@ If successful, the response should be empty:
```
If the device is not owned by your tailnet:
```
< HTTP/1.1 501 Not Implemented
...
{"message":"cannot delete devices outside of your tailnet"}
```
<a name=device-routes-get></a>
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
@@ -193,6 +204,7 @@ curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
```
Response
```
{
"advertisedRoutes" : [
@@ -213,7 +225,9 @@ Sets which subnet routes are enabled to be routed by a device by replacing the e
##### Parameters
###### POST Body
`routes` - The new list of enabled subnet routes in JSON.
```
{
"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]
@@ -254,7 +268,9 @@ Marks a device as authorized, for Tailnets where device authorization is require
##### Parameters
###### POST Body
`authorized` - whether the device is authorized; only `true` is currently supported.
```
{
"authorized": true
@@ -335,9 +351,9 @@ The response is 2xx on success. The response body is currently an empty JSON
object.
## Tailnet
A tailnet is the name of your Tailscale network.
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
A tailnet is the name of your Tailscale network.
You can find it in the [Settings](https://login.tailscale.com/admin/settings/) page of the admin console.
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
@@ -346,10 +362,10 @@ GET /api/v2/tailnet/example.com/...
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
```
For solo plans, the tailnet is the email you signed up with.
So `alice@gmail.com` has the tailnet `alice@gmail.com` since `@gmail.com` is a shared email host.
Her API calls would have the following format:
```
GET /api/v2/tailnet/alice@gmail.com/...
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
@@ -370,15 +386,19 @@ Retrieves the ACL that is currently set for the given tailnet. Supply the tailne
##### Parameters
###### Headers
`Accept` - Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
##### Returns
Returns the ACL HuJSON by default. Returns a parsed JSON of the ACL (sans comments) if the `Accept` type is explicitly set to `application/json`. An `ETag` header is also sent in the response, which can be optionally used in POST requests to avoid missed updates.
<!-- TODO (chungdaniel): define error types and a set of docs for them -->
##### Example
###### Requesting a HuJSON response:
```
GET /api/v2/tailnet/example.com/acl
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
@@ -388,6 +408,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
```
Response
```
...
Content-Type: application/hujson
@@ -426,6 +447,7 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
```
###### Requesting a JSON response:
```
GET /api/v2/tailnet/example.com/acl
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
@@ -435,6 +457,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
```
Response
```
...
Content-Type: application/json
@@ -477,6 +500,7 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
##### Parameters
###### Headers
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
@@ -486,15 +510,16 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
The POST body should be a JSON or [HuJSON](https://github.com/tailscale/hujson#hujson---human-json) formatted JSON object.
An ACL policy may contain the following top-level properties:
* `Groups` - Static groups of users which can be used for ACL rules.
* `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
* `ACLs` - Access control lists.
* `TagOwners` - Defines who is allowed to use which tags.
* `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
- `Groups` - Static groups of users which can be used for ACL rules.
- `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
- `ACLs` - Access control lists.
- `TagOwners` - Defines who is allowed to use which tags.
- `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
See https://tailscale.com/kb/1018/acls for more information on those properties.
##### Example
```
POST /api/v2/tailnet/example.com/acl
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
@@ -524,6 +549,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
```
Response:
```
// Example/default ACLs for unrestricted connections.
{
@@ -549,6 +575,7 @@ Response:
```
Failed test error response:
```
{
"message": "test(s) failed",
@@ -572,14 +599,17 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
##### Parameters
###### Query Parameters
`type` - can be 'user' or 'ipport'
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
The provided ACL is queried with this parameter to determine which rules match.
###### POST Body
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
##### Example
```
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFor=user1@example.com&type=user' \
-u "tskey-yourapikey123:" \
@@ -607,6 +637,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFo
```
Response:
```
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
```
@@ -631,6 +662,7 @@ The POST body should be a JSON formatted array of ACL Tests.
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
##### Example with tests
```
POST /api/v2/tailnet/example.com/acl/validate
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
@@ -642,6 +674,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
```
##### Example with an ACL body
```
POST /api/v2/tailnet/example.com/acl/validate
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
@@ -684,21 +717,23 @@ An empty body or a JSON object with no `message` is returned on success.
<a name=tailnet-devices-get></a>
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
Lists the devices in a tailnet.
Supply the tailnet of interest in the path.
Use the `fields` query parameter to explicitly indicate which fields are returned.
##### Parameters
###### Query Parameters
`fields` - Controls which fields will be included in the returned response.
Currently, supported options are:
* `all`: Returns all fields in the response.
* `default`: return all fields except:
* `enabledRoutes`
* `advertisedRoutes`
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
- `all`: Returns all fields in the response.
- `default`: return all fields except:
- `enabledRoutes`
- `advertisedRoutes`
- `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
Use commas to separate multiple options.
If more than one option is indicated, then the union is used.
@@ -714,6 +749,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/devices' \
```
Response
```
{
"devices":[
@@ -776,6 +812,7 @@ for the user who owns the API key used to perform this query.
Supply the tailnet of interest in the path.
##### Parameters
No parameters.
##### Returns
@@ -792,6 +829,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys' \
```
Response:
```
{"keys": [
{"id": "kYKVU14CNTRL"},
@@ -812,7 +850,9 @@ Supply the tailnet in the path.
##### Parameters
###### POST Body
`capabilities` - A mapping of resources to permissible actions.
```
{
"capabilities": {
@@ -859,6 +899,7 @@ echo '{
```
Response:
```
{
"id": "k123456CNTRL",
@@ -877,6 +918,7 @@ Returns a JSON object with information about specific key.
Supply the tailnet and key ID of interest in the path.
##### Parameters
No parameters.
##### Returns
@@ -893,6 +935,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
```
Response:
```
{
"id": "k123456CNTRL",
@@ -922,9 +965,11 @@ Deletes a specific key.
Supply the tailnet and key ID of interest in the path.
##### Parameters
No parameters.
##### Returns
This reports status 200 upon success.
##### Example
@@ -941,10 +986,12 @@ curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k12345
<a name=tailnet-dns-nameservers-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
Lists the DNS nameservers for a tailnet.
Supply the tailnet of interest in the path.
##### Parameters
No parameters.
##### Example
@@ -956,6 +1003,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
```
Response
```
{
"dns": ["8.8.8.8"],
@@ -965,13 +1013,17 @@ Response
<a name=tailnet-dns-nameservers-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
Supply the tailnet of interest in the path.
Note that changing the list of DNS nameservers may also affect the status of MagicDNS (if MagicDNS is on).
##### Parameters
###### POST Body
`dns` - The new list of DNS nameservers in JSON.
```
{
"dns":["8.8.8.8"]
@@ -979,12 +1031,15 @@ Note that changing the list of DNS nameservers may also affect the status of Mag
```
##### Returns
Returns the new list of nameservers and the status of MagicDNS.
If all nameservers have been removed, MagicDNS will be automatically disabled (until explicitly turned back on by the user).
##### Example
###### Adding DNS nameservers with the MagicDNS on:
```
POST /api/v2/tailnet/example.com/dns/nameservers
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
@@ -993,6 +1048,7 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameserve
```
Response:
```
{
"dns":["8.8.8.8"],
@@ -1001,6 +1057,7 @@ Response:
```
###### Removing all DNS nameservers with the MagicDNS on:
```
POST /api/v2/tailnet/example.com/dns/nameservers
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
@@ -1009,6 +1066,7 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameserve
```
Response:
```
{
"dns":[],
@@ -1019,13 +1077,16 @@ Response:
<a name=tailnet-dns-preferences-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/preferences` - retrieves the DNS preferences for a tailnet
Retrieves the DNS preferences that are currently set for the given tailnet.
Supply the tailnet of interest in the path.
##### Parameters
No parameters.
##### Example
```
GET /api/v2/tailnet/example.com/dns/preferences
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
@@ -1033,6 +1094,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
```
Response:
```
{
"magicDNS":false,
@@ -1042,6 +1104,7 @@ Response:
<a name=tailnet-dns-preferences-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
Note that MagicDNS is dependent on DNS servers.
@@ -1051,9 +1114,12 @@ Note that removing all nameservers will turn off MagicDNS.
To reenable it, nameservers must be added back, and MagicDNS must be explicitly turned on.
##### Parameters
###### POST Body
The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
`magicDNS` - Automatically registers DNS names for devices in your tailnet.
`magicDNS` - Automatically registers DNS names for devices in your tailnet.
```
{
"magicDNS": true
@@ -1061,6 +1127,7 @@ The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
```
##### Example
```
POST /api/v2/tailnet/example.com/dns/preferences
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
@@ -1068,10 +1135,10 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferenc
--data-binary '{"magicDNS": true}'
```
Response:
If there are no DNS servers, it returns an error message:
```
{
"message":"need at least one nameserver to enable MagicDNS"
@@ -1079,6 +1146,7 @@ If there are no DNS servers, it returns an error message:
```
If there are DNS servers:
```
{
"magicDNS":true,
@@ -1088,14 +1156,16 @@ If there are DNS servers:
<a name=tailnet-dns-searchpaths-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
Retrieves the list of search paths that is currently set for the given tailnet.
Supply the tailnet of interest in the path.
##### Parameters
No parameters.
##### Example
```
GET /api/v2/tailnet/example.com/dns/searchpaths
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
@@ -1103,6 +1173,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
```
Response:
```
{
"searchPaths": ["user1.example.com"],
@@ -1112,12 +1183,15 @@ Response:
<a name=tailnet-dns-searchpaths-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.
##### Parameters
###### POST Body
`searchPaths` - A list of searchpaths in JSON.
```
{
"searchPaths": ["user1.example.com", "user2.example.com"]
@@ -1125,6 +1199,7 @@ Replaces the list of searchpaths with the list supplied by the user and returns
```
##### Example
```
POST /api/v2/tailnet/example.com/dns/searchpaths
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
@@ -1133,6 +1208,7 @@ curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpat
```
Response:
```
{
"searchPaths": ["user1.example.com", "user2.example.com"],

View File

@@ -9,7 +9,6 @@
package atomicfile // import "tailscale.com/atomicfile"
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -18,7 +17,7 @@ import (
// WriteFile writes data to filename+some suffix, then renames it
// into filename. The perm argument is ignored on Windows.
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
if err != nil {
return err
}

View File

@@ -15,7 +15,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
@@ -137,7 +136,7 @@ func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Respons
onVersionMismatch(ipn.IPCVersion(), server)
}
if res.StatusCode == 403 {
all, _ := ioutil.ReadAll(res.Body)
all, _ := io.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
@@ -207,7 +206,7 @@ func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus
return nil, err
}
defer res.Body.Close()
slurp, err := ioutil.ReadAll(res.Body)
slurp, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
@@ -365,7 +364,7 @@ func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc
return nil, 0, fmt.Errorf("unexpected chunking")
}
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
}

View File

@@ -17,7 +17,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
)
@@ -131,7 +130,7 @@ func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error)
// Read response. Limit the response to 10MB.
body := io.LimitReader(resp.Body, maxReadSize+1)
b, err := ioutil.ReadAll(body)
b, err := io.ReadAll(body)
if len(b) > maxReadSize {
err = errors.New("API response too large")
}

View File

@@ -14,7 +14,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"net"
@@ -99,7 +98,7 @@ func loadConfig() config {
}
log.Printf("no config path specified; using %s", *configPath)
}
b, err := ioutil.ReadFile(*configPath)
b, err := os.ReadFile(*configPath)
switch {
case errors.Is(err, os.ErrNotExist):
return writeNewConfig()
@@ -155,7 +154,7 @@ func main() {
s.SetVerifyClient(*verifyClients)
if *meshPSKFile != "" {
b, err := ioutil.ReadFile(*meshPSKFile)
b, err := os.ReadFile(*meshPSKFile)
if err != nil {
log.Fatal(err)
}

View File

@@ -33,6 +33,12 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{"derp"},
OriginPatterns: []string{"*"},
// Disable compression because we transmit WireGuard messages that
// are not compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
log.Printf("websocket.Accept: %v", err)

View File

@@ -13,7 +13,6 @@ import (
"errors"
"flag"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
@@ -106,7 +105,7 @@ func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
func getTmpl() (*template.Template, error) {
if devMode() {
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
tmplData, err := os.ReadFile("hello.tmpl.html")
if os.IsNotExist(err) {
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
return tmpl, nil

View File

@@ -789,6 +789,10 @@ func TestUpdatePrefs(t *testing.T) {
curPrefs *ipn.Prefs
env upCheckEnv // empty goos means "linux"
// sshOverTailscale specifies if the cmd being run over SSH over Tailscale.
// It is used to test the --accept-risks flag.
sshOverTailscale bool
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
// updatePrefs might've mutated them (from applyImplicitPrefs).
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
@@ -916,15 +920,159 @@ func TestUpdatePrefs(t *testing.T) {
}
},
},
{
name: "enable_ssh",
flags: []string{"--ssh"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "disable_ssh",
flags: []string{"--ssh=false"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
RunSSH: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if newPrefs.RunSSH {
t.Errorf("RunSSH not set to false")
}
},
env: upCheckEnv{backendState: "Running", upArgs: upArgsT{
runSSH: true,
}},
},
{
name: "disable_ssh_over_ssh_no_risk",
flags: []string{"--ssh=false"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
RunSSH: true,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "enable_ssh_over_ssh_no_risk",
flags: []string{"--ssh=true"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "enable_ssh_over_ssh",
flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "disable_ssh_over_ssh",
flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
RunSSH: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if newPrefs.RunSSH {
t.Errorf("RunSSH not set to false")
}
},
env: upCheckEnv{backendState: "Running"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.sshOverTailscale {
old := getSSHClientEnvVar
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
t.Cleanup(func() { getSSHClientEnvVar = old })
}
if tt.env.goos == "" {
tt.env.goos = "linux"
}
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
flags := CleanUpArgs(tt.flags)
tt.env.flagSet.Parse(flags)
if err := tt.env.flagSet.Parse(flags); err != nil {
t.Fatal(err)
}
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
if err != nil {
@@ -939,6 +1087,8 @@ func TestUpdatePrefs(t *testing.T) {
return
}
t.Fatal(err)
} else if tt.wantErrSubtr != "" {
t.Fatalf("want error %q, got nil", tt.wantErrSubtr)
}
if tt.checkUpdatePrefsMutations != nil {
tt.checkUpdatePrefsMutations(t, newPrefs)
@@ -952,13 +1102,18 @@ func TestUpdatePrefs(t *testing.T) {
justEditMP.Prefs = ipn.Prefs{} // uninteresting
}
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was %+v", oldEditPrefs)
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was \n%v", asJSON(oldEditPrefs))
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
}
})
}
}
func asJSON(v any) string {
b, _ := json.MarshalIndent(v, "", "\t")
return string(b)
}
var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
return a == b
})

View File

@@ -489,7 +489,15 @@ func runTS2021(ctx context.Context, args []string) error {
return c, err
}
conn, err := controlhttp.Dial(ctx, ts2021Args.host, "80", "443", machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
conn, err := (&controlhttp.Dialer{
Hostname: ts2021Args.host,
HTTPPort: "80",
HTTPSPort: "443",
MachineKey: machinePrivate,
ControlKey: keys.PublicKey,
ProtocolVersion: uint16(ts2021Args.version),
Dialer: dialFunc,
}).Dial(ctx)
log.Printf("controlhttp.Dial = %p, %v", conn, err)
if err != nil {
return err

View File

@@ -22,9 +22,13 @@ var downCmd = &ffcli.Command{
FlagSet: newDownFlagSet(),
}
var downArgs struct {
acceptedRisks string
}
func newDownFlagSet() *flag.FlagSet {
downf := newFlagSet("down")
registerAcceptRiskFlag(downf)
registerAcceptRiskFlag(downf, &downArgs.acceptedRisks)
return downf
}
@@ -34,7 +38,7 @@ func runDown(ctx context.Context, args []string) error {
}
if isSSHOverTailscale() {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`, downArgs.acceptedRisks); err != nil {
return err
}
}

View File

@@ -10,7 +10,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"sort"
@@ -202,7 +201,7 @@ func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, err
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}

View File

@@ -16,9 +16,8 @@ import (
)
var (
riskTypes []string
acceptedRisks string
riskLoseSSH = registerRiskType("lose-ssh")
riskTypes []string
riskLoseSSH = registerRiskType("lose-ssh")
)
func registerRiskType(riskType string) string {
@@ -28,12 +27,13 @@ func registerRiskType(riskType string) string {
// registerAcceptRiskFlag registers the --accept-risk flag. Accepted risks are accounted for
// in presentRiskToUser.
func registerAcceptRiskFlag(f *flag.FlagSet) {
f.StringVar(&acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
func registerAcceptRiskFlag(f *flag.FlagSet, acceptedRisks *string) {
f.StringVar(acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
}
// riskAccepted reports whether riskType is in acceptedRisks.
func riskAccepted(riskType string) bool {
// isRiskAccepted reports whether riskType is in the comma-separated list of
// risks in acceptedRisks.
func isRiskAccepted(riskType, acceptedRisks string) bool {
for _, r := range strings.Split(acceptedRisks, ",") {
if r == riskType {
return true
@@ -49,12 +49,16 @@ var errAborted = errors.New("aborted, no changes made")
// It is used by the presentRiskToUser function below.
const riskAbortTimeSeconds = 5
// presentRiskToUser displays the risk message and waits for the user to
// cancel. It returns errorAborted if the user aborts.
func presentRiskToUser(riskType, riskMessage string) error {
if riskAccepted(riskType) {
// presentRiskToUser displays the risk message and waits for the user to cancel.
// It returns errorAborted if the user aborts. In tests it returns errAborted
// immediately unless the risk has been explicitly accepted.
func presentRiskToUser(riskType, riskMessage, acceptedRisks string) error {
if isRiskAccepted(riskType, acceptedRisks) {
return nil
}
if inTest() {
return errAborted
}
outln(riskMessage)
printf("To skip this warning, use --accept-risk=%s\n", riskType)

View File

@@ -116,7 +116,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
}
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
registerAcceptRiskFlag(upf)
registerAcceptRiskFlag(upf, &upArgs.acceptedRisks)
return upf
}
@@ -150,6 +150,7 @@ type upArgsT struct {
opUser string
json bool
timeout time.Duration
acceptedRisks string
}
func (a upArgsT) getAuthKey() (string, error) {
@@ -376,6 +377,21 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
}
// Do this after validations to avoid the 5s delay if we're going to error
// out anyway.
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
fmt.Println("wantSSH", wantSSH, "haveSSH", haveSSH)
if wantSSH != haveSSH && isSSHOverTailscale() {
if wantSSH {
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, env.upArgs.acceptedRisks)
} else {
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, env.upArgs.acceptedRisks)
}
if err != nil {
return false, nil, err
}
}
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
simpleUp = env.flagSet.NFlag() == 0 &&
@@ -475,17 +491,6 @@ func runUp(ctx context.Context, args []string) (retErr error) {
curExitNodeIP: exitNodeIP(curPrefs, st),
}
if upArgs.runSSH != curPrefs.RunSSH && isSSHOverTailscale() {
if upArgs.runSSH {
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`)
} else {
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`)
}
if err != nil {
return err
}
}
defer func() {
if retErr == nil {
checkSSHUpWarnings(ctx)

View File

@@ -15,7 +15,6 @@ import (
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -254,7 +253,7 @@ func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
return "", nil, err
}
defer resp.Body.Close()
out, err := ioutil.ReadAll(resp.Body)
out, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}

View File

@@ -15,7 +15,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -173,7 +172,7 @@ func checkDerp(ctx context.Context, derpRegion string) error {
return fmt.Errorf("fetch derp map failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return fmt.Errorf("fetch derp map failed: %w", err)
}

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -142,7 +141,7 @@ func installSystemDaemonDarwin(args []string) (err error) {
return err
}
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
return err
}

View File

@@ -26,6 +26,7 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
@@ -97,6 +98,20 @@ func defaultTunName() string {
return "tailscale0"
}
// defaultPort returns the default UDP port to listen on for disco+wireguard.
// By default it returns 0, to pick one randomly from the kernel.
// If the environment variable PORT is set, that's used instead.
// The PORT environment variable is chosen to match what the Linux systemd
// unit uses, to make documentation more consistent.
func defaultPort() uint16 {
if s := envknob.String("PORT"); s != "" {
if p, err := strconv.ParseUint(s, 10, 16); err == nil {
return uint16(p)
}
}
return 0
}
var args struct {
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
@@ -132,6 +147,9 @@ var subCommands = map[string]*func([]string) error{
var beCLI func() // non-nil if CLI is linked in
func main() {
envknob.PanicIfAnyEnvCheckedInInit()
envknob.ApplyDiskConfig()
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")
@@ -139,7 +157,7 @@ func main() {
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
@@ -308,6 +326,10 @@ func run() error {
pol.Shutdown(ctx)
}()
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
if isWindowsService() {
// Run the IPN server from the Windows service manager.
log.Printf("Running service...")
@@ -376,7 +398,7 @@ func run() error {
return fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = useNetstack
ns.ProcessSubnets = useNetstack || wrapNetstack
ns.ProcessSubnets = useNetstack || shouldWrapNetstack()
if useNetstack {
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
@@ -477,8 +499,6 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer)
return nil, false, multierr.New(errs...)
}
var wrapNetstack = shouldWrapNetstack()
func shouldWrapNetstack() bool {
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
return v
@@ -549,7 +569,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
}
conf.DNS = d
conf.Router = r
if wrapNetstack {
if shouldWrapNetstack() {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
}
}

View File

@@ -197,6 +197,9 @@ func beWindowsSubprocess() bool {
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
log.Printf("subproc mode: logid=%v", logid)
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
go func() {
b := make([]byte, 16)
@@ -274,7 +277,7 @@ func startIPNServer(ctx context.Context, logid string) error {
dev.Close()
return nil, nil, fmt.Errorf("router: %w", err)
}
if wrapNetstack {
if shouldWrapNetstack() {
r = netstack.NewSubnetRouterWrapper(r)
}
d, err := dns.NewOSConfigurator(logf, devName)
@@ -301,7 +304,7 @@ func startIPNServer(ctx context.Context, logid string) error {
return nil, nil, fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = false
ns.ProcessSubnets = wrapNetstack
ns.ProcessSubnets = shouldWrapNetstack()
if err := ns.Start(); err != nil {
return nil, nil, fmt.Errorf("failed to start netstack: %w", err)
}

View File

@@ -7,7 +7,6 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path"
@@ -47,7 +46,7 @@ func runBuild() {
if err != nil {
log.Fatalf("Cannot fix esbuild metadata paths: %v", err)
}
if err := ioutil.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
if err := os.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
log.Fatalf("Cannot write metadata: %v", err)
}

View File

@@ -6,7 +6,6 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"net"
"os"
@@ -183,7 +182,7 @@ func setupEsbuildWasm(build esbuild.PluginBuild, dev bool) {
func buildWasm(dev bool) ([]byte, error) {
start := time.Now()
outputFile, err := ioutil.TempFile("", "main.*.wasm")
outputFile, err := os.CreateTemp("", "main.*.wasm")
if err != nil {
return nil, fmt.Errorf("Cannot create main.wasm output file: %w", err)
}

View File

@@ -11,7 +11,6 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"net/http"
"os"
@@ -75,7 +74,7 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
}
defer esbuildMetadataFile.Close()
esbuildMetadataBytes, err := ioutil.ReadAll(esbuildMetadataFile)
esbuildMetadataBytes, err := io.ReadAll(esbuildMetadataFile)
if err != nil {
return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
}

View File

@@ -14,7 +14,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
@@ -490,7 +489,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("RegisterReq sign error: %v", err)
}
}
if debugRegister {
if debugRegister() {
j, _ := json.MarshalIndent(request, "", "\t")
c.logf("RegisterRequest: %s", j)
}
@@ -523,7 +522,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
return regen, opt.URL, fmt.Errorf("register request: %w", err)
}
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
msg, _ := io.ReadAll(res.Body)
res.Body.Close()
return regen, opt.URL, fmt.Errorf("register request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
@@ -533,7 +532,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return regen, opt.URL, fmt.Errorf("register request: %v", err)
}
if debugRegister {
if debugRegister() {
j, _ := json.MarshalIndent(resp, "", "\t")
c.logf("RegisterResponse: %s", j)
}
@@ -715,7 +714,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
c.logf("[v1] PollNetMap: stream=%v ep=%v", allowStream, epStrs)
vlogf := logger.Discard
if DevKnob.DumpNetMaps {
if DevKnob.DumpNetMaps() {
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
// want to upload it always.
vlogf = c.logf
@@ -804,7 +803,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
}
vlogf("netmap: Do = %v after %v", res.StatusCode, time.Since(t0).Round(time.Millisecond))
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
msg, _ := io.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("initial fetch failed %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
@@ -814,7 +813,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
health.NoteMapRequestHeard(request)
if cb == nil {
io.Copy(ioutil.Discard, res.Body)
io.Copy(io.Discard, res.Body)
return nil
}
@@ -963,12 +962,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
controlTrimWGConfig.Store(d.TrimWGConfig)
}
if DevKnob.StripEndpoints {
if DevKnob.StripEndpoints() {
for _, p := range resp.Peers {
p.Endpoints = nil
}
}
if DevKnob.StripCaps {
if DevKnob.StripCaps() {
nm.SelfNode.Capabilities = nil
}
@@ -998,7 +997,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
// it uses the serverKey and mkey to decode the message from the NaCl-crypto-box.
func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) error {
defer res.Body.Close()
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
msg, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return err
}
@@ -1012,8 +1011,8 @@ func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePubl
}
var (
debugMap = envknob.Bool("TS_DEBUG_MAP")
debugRegister = envknob.Bool("TS_DEBUG_REGISTER")
debugMap = envknob.RegisterBool("TS_DEBUG_MAP")
debugRegister = envknob.RegisterBool("TS_DEBUG_REGISTER")
)
var jsonEscapedZero = []byte(`\u0000`)
@@ -1051,7 +1050,7 @@ func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
return err
}
}
if debugMap {
if debugMap() {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
log.Printf("MapResponse: %s", buf.Bytes())
@@ -1088,7 +1087,7 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine
if err != nil {
return nil, err
}
if debugMap {
if debugMap() {
if _, ok := v.(*tailcfg.MapRequest); ok {
log.Printf("MapRequest: %s", b)
}
@@ -1110,7 +1109,7 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
return nil, fmt.Errorf("fetch control key: %v", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 64<<10))
b, err := io.ReadAll(io.LimitReader(res.Body, 64<<10))
if err != nil {
return nil, fmt.Errorf("fetch control key response: %v", err)
}
@@ -1139,18 +1138,18 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
var DevKnob = initDevKnob()
type devKnobs struct {
DumpNetMaps bool
ForceProxyDNS bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
StripCaps bool // strip all local node's control-provided capabilities
DumpNetMaps func() bool
ForceProxyDNS func() bool
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
StripCaps func() bool // strip all local node's control-provided capabilities
}
func initDevKnob() devKnobs {
return devKnobs{
DumpNetMaps: envknob.Bool("TS_DEBUG_NETMAP"),
ForceProxyDNS: envknob.Bool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.Bool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envknob.Bool("TS_DEBUG_STRIP_CAPS"),
DumpNetMaps: envknob.RegisterBool("TS_DEBUG_NETMAP"),
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
}
}
@@ -1398,7 +1397,7 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
msg, _ := io.ReadAll(res.Body)
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
}
var setDNSRes tailcfg.SetDNSResponse
@@ -1464,7 +1463,7 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
msg, _ := io.ReadAll(res.Body)
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
}
var setDNSRes tailcfg.SetDNSResponse

View File

@@ -190,7 +190,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
}
ms.addUserProfile(peer.User)
}
if DevKnob.ForceProxyDNS {
if DevKnob.ForceProxyDNS() {
nm.DNS.Proxied = true
}
ms.netMapBuilding = nil
@@ -356,13 +356,13 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
return v2
}
var debugSelfIPv6Only = envknob.Bool("TS_DEBUG_SELF_V6_ONLY")
var debugSelfIPv6Only = envknob.RegisterBool("TS_DEBUG_SELF_V6_ONLY")
func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
switch {
default:
return in
case debugSelfIPv6Only:
case debugSelfIPv6Only():
for _, a := range in {
if a.Addr().Is6() {
ret = append(ret, a)

View File

@@ -165,7 +165,15 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
// thousand version numbers before getting to this point.
panic("capability version is too high to fit in the wire protocol")
}
conn, err := controlhttp.Dial(ctx, nc.host, nc.httpPort, nc.httpsPort, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion), nc.dialer.SystemDial)
conn, err := (&controlhttp.Dialer{
Hostname: nc.host,
HTTPPort: nc.httpPort,
HTTPSPort: nc.httpsPort,
MachineKey: nc.privKey,
ControlKey: nc.serverPubKey,
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
Dialer: nc.dialer.SystemDial,
}).Dial(ctx)
if err != nil {
return nil, err
}

View File

@@ -40,57 +40,49 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/types/key"
)
// Dial connects to the HTTP server at host:httpPort, requests to switch to the
// Tailscale control protocol, and returns an established control
var stdDialer net.Dialer
// Dial connects to the HTTP server at this Dialer's Host:HTTPPort, requests to
// switch to the Tailscale control protocol, and returns an established control
// protocol connection.
//
// If Dial fails to connect using addr, it also tries to tunnel over
// TLS to host:httpsPort as a compatibility fallback.
// If Dial fails to connect using HTTP, it also tries to tunnel over TLS to the
// Dialer's Host:HTTPSPort as a compatibility fallback.
//
// The provided ctx is only used for the initial connection, until
// Dial returns. It does not affect the connection once established.
func Dial(ctx context.Context, host string, httpPort string, httpsPort string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
a := &dialParams{
host: host,
httpPort: httpPort,
httpsPort: httpsPort,
machineKey: machineKey,
controlKey: controlKey,
version: protocolVersion,
proxyFunc: tshttpproxy.ProxyFromEnvironment,
dialer: dialer,
func (a *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
if a.Hostname == "" {
return nil, errors.New("required Dialer.Hostname empty")
}
return a.dial(ctx)
}
type dialParams struct {
host string
httpPort string
httpsPort string
machineKey key.MachinePrivate
controlKey key.MachinePublic
version uint16
proxyFunc func(*http.Request) (*url.URL, error) // or nil
dialer dnscache.DialContextFunc
// For tests only
insecureTLS bool
testFallbackDelay time.Duration
func (a *Dialer) logf(format string, args ...any) {
if a.Logf != nil {
a.Logf(format, args...)
}
}
// httpsFallbackDelay is how long we'll wait for a.httpPort to work before
// starting to try a.httpsPort.
func (a *dialParams) httpsFallbackDelay() time.Duration {
func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
if a.proxyFunc != nil {
return a.proxyFunc
}
return tshttpproxy.ProxyFromEnvironment
}
// httpsFallbackDelay is how long we'll wait for a.HTTPPort to work before
// starting to try a.HTTPSPort.
func (a *Dialer) httpsFallbackDelay() time.Duration {
if v := a.testFallbackDelay; v != 0 {
return v
}
return 500 * time.Millisecond
}
func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
func (a *Dialer) dial(ctx context.Context) (*controlbase.Conn, error) {
// Create one shared context used by both port 80 and port 443 dials.
// If port 80 is still in flight when 443 returns, this deferred cancel
// will stop the port 80 dial.
@@ -102,12 +94,12 @@ func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
// we'll speak Noise.
u80 := &url.URL{
Scheme: "http",
Host: net.JoinHostPort(a.host, a.httpPort),
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPPort, "80")),
Path: serverUpgradePath,
}
u443 := &url.URL{
Scheme: "https",
Host: net.JoinHostPort(a.host, a.httpsPort),
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPSPort, "443")),
Path: serverUpgradePath,
}
@@ -169,8 +161,8 @@ func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
}
// dialURL attempts to connect to the given URL.
func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
func (a *Dialer) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(a.MachineKey, a.ControlKey, a.ProtocolVersion)
if err != nil {
return nil, err
}
@@ -189,26 +181,34 @@ func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
//
// Only the provided ctx is used, not a.ctx.
func (a *dialParams) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
dns := &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true,
}
var dialer dnscache.DialContextFunc
if a.Dialer != nil {
dialer = a.Dialer
} else {
dialer = stdDialer.DialContext
}
tr := http.DefaultTransport.(*http.Transport).Clone()
defer tr.CloseIdleConnections()
tr.Proxy = a.proxyFunc
tr.Proxy = a.getProxyFunc()
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.DialContext = dnscache.Dialer(a.dialer, dns)
tr.DialContext = dnscache.Dialer(dialer, dns)
// Disable HTTP2, since h2 can't do protocol switching.
tr.TLSClientConfig.NextProtos = []string{}
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
tr.TLSClientConfig = tlsdial.Config(a.host, tr.TLSClientConfig)
tr.TLSClientConfig = tlsdial.Config(a.Hostname, tr.TLSClientConfig)
if a.insecureTLS {
tr.TLSClientConfig.InsecureSkipVerify = true
tr.TLSClientConfig.VerifyConnection = nil
}
tr.DialTLSContext = dnscache.TLSDialer(a.dialer, dns, tr.TLSClientConfig)
tr.DialTLSContext = dnscache.TLSDialer(dialer, dns, tr.TLSClientConfig)
tr.DisableCompression = true
// (mis)use httptrace to extract the underlying net.Conn from the

View File

@@ -7,27 +7,31 @@ package controlhttp
import (
"context"
"encoding/base64"
"errors"
"net"
"net/url"
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/dnscache"
"tailscale.com/types/key"
)
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
// bi-directional communication over an HTTP connection when in JS.
func Dial(ctx context.Context, host string, httpPort string, httpsPort string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(machineKey, controlKey, protocolVersion)
func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
if d.Hostname == "" {
return nil, errors.New("required Dialer.Hostname empty")
}
init, cont, err := controlbase.ClientDeferred(d.MachineKey, d.ControlKey, d.ProtocolVersion)
if err != nil {
return nil, err
}
wsScheme := "wss"
host := d.Hostname
if host == "localhost" {
wsScheme = "ws"
host = net.JoinHostPort(host, httpPort)
host = net.JoinHostPort(host, strDef(d.HTTPPort, "80"))
}
wsURL := &url.URL{
Scheme: wsScheme,
@@ -52,5 +56,4 @@ func Dial(ctx context.Context, host string, httpPort string, httpsPort string, m
return nil, err
}
return cbConn, nil
}

View File

@@ -4,6 +4,16 @@
package controlhttp
import (
"net/http"
"net/url"
"time"
"tailscale.com/net/dnscache"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
const (
// upgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
@@ -18,3 +28,58 @@ const (
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"
)
// Dialer contains configuration on how to dial the Tailscale control server.
type Dialer struct {
// Hostname is the hostname to connect to, with no port number.
//
// This field is required.
Hostname string
// MachineKey contains the current machine's private key.
//
// This field is required.
MachineKey key.MachinePrivate
// ControlKey contains the expected public key for the control server.
//
// This field is required.
ControlKey key.MachinePublic
// ProtocolVersion is the expected protocol version to negotiate.
//
// This field is required.
ProtocolVersion uint16
// HTTPPort is the port number to use when making a HTTP connection.
//
// If not specified, this defaults to port 80.
HTTPPort string
// HTTPSPort is the port number to use when making a HTTPS connection.
//
// If not specified, this defaults to port 443.
HTTPSPort string
// Dialer is the dialer used to make outbound connections.
//
// If not specified, this defaults to net.Dialer.DialContext.
Dialer dnscache.DialContextFunc
// Logf, if set, is a logging function to use; if unset, logs are
// dropped.
Logf logger.Logf
proxyFunc func(*http.Request) (*url.URL, error) // or nil
// For tests only
insecureTLS bool
testFallbackDelay time.Duration
}
func strDef(v1, v2 string) string {
if v1 != "" {
return v1
}
return v2
}

View File

@@ -170,15 +170,16 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
defer cancel()
}
a := dialParams{
host: "localhost",
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
machineKey: client,
controlKey: server.Public(),
version: testProtocolVersion,
a := &Dialer{
Hostname: "localhost",
HTTPPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
HTTPSPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
MachineKey: client,
ControlKey: server.Public(),
ProtocolVersion: testProtocolVersion,
Dialer: new(tsdial.Dialer).SystemDial,
Logf: t.Logf,
insecureTLS: true,
dialer: new(tsdial.Dialer).SystemDial,
testFallbackDelay: 50 * time.Millisecond,
}

View File

@@ -82,6 +82,12 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{upgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)

View File

@@ -13,20 +13,18 @@ import (
)
// disableUPnP indicates whether to attempt UPnP mapping.
var disableUPnP atomic.Bool
var disableUPnPControl atomic.Bool
func init() {
SetDisableUPnP(envknob.Bool("TS_DISABLE_UPNP"))
}
var disableUPnpEnv = envknob.RegisterBool("TS_DISABLE_UPNP")
// DisableUPnP reports the last reported value from control
// whether UPnP portmapping should be disabled.
func DisableUPnP() bool {
return disableUPnP.Load()
return disableUPnPControl.Load() || disableUPnpEnv()
}
// SetDisableUPnP sets whether control says that UPnP should be
// disabled.
func SetDisableUPnP(v bool) {
disableUPnP.Store(v)
disableUPnPControl.Store(v)
}

View File

@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package derp implements DERP, the Detour Encrypted Routing Protocol.
// Package derp implements the Designated Encrypted Relay for Packets (DERP)
// protocol.
//
// DERP routes packets to clients using curve25519 keys as addresses.
//
@@ -18,7 +19,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"time"
)
@@ -194,7 +194,7 @@ func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t frameType, frameLe
}
remain := frameLen - uint32(n)
if remain > 0 {
if _, err := io.CopyN(ioutil.Discard, br, int64(remain)); err != nil {
if _, err := io.CopyN(io.Discard, br, int64(remain)); err != nil {
return 0, 0, err
}
err = io.ErrShortBuffer

View File

@@ -18,7 +18,6 @@ import (
"expvar"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"math/big"
@@ -47,8 +46,6 @@ import (
"tailscale.com/version"
)
var debug = envknob.Bool("DERP_DEBUG_LOGS")
// verboseDropKeys is the set of destination public keys that should
// verbosely log whenever DERP drops a packet.
var verboseDropKeys = map[key.NodePublic]bool{}
@@ -106,6 +103,7 @@ type Server struct {
limitedLogf logger.Logf
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
dupPolicy dupPolicy
debug bool
// Counters:
packetsSent, bytesSent expvar.Int
@@ -299,6 +297,7 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
runtime.ReadMemStats(&ms)
s := &Server{
debug: envknob.Bool("DERP_DEBUG_LOGS"),
privateKey: privateKey,
publicKey: privateKey.Public(),
logf: logf,
@@ -758,7 +757,7 @@ func (c *sclient) run(ctx context.Context) error {
}
func (c *sclient) handleUnknownFrame(ft frameType, fl uint32) error {
_, err := io.CopyN(ioutil.Discard, c.br, int64(fl))
_, err := io.CopyN(io.Discard, c.br, int64(fl))
return err
}
@@ -801,7 +800,7 @@ func (c *sclient) handleFramePing(ft frameType, fl uint32) error {
return err
}
if extra := int64(fl) - int64(len(m)); extra > 0 {
_, err = io.CopyN(ioutil.Discard, c.br, extra)
_, err = io.CopyN(io.Discard, c.br, extra)
}
select {
case c.sendPongCh <- [8]byte(m):
@@ -980,7 +979,7 @@ func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, r
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
s.limitedLogf(msg)
}
if debug {
if s.debug {
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
}
}
@@ -1828,7 +1827,7 @@ func (s *Server) ServeDebugTraffic(w http.ResponseWriter, r *http.Request) {
var bufioWriterPool = &sync.Pool{
New: func() any {
return bufio.NewWriterSize(ioutil.Discard, 2<<10)
return bufio.NewWriterSize(io.Discard, 2<<10)
},
}
@@ -1861,7 +1860,7 @@ func (w *lazyBufioWriter) Flush() error {
}
err := w.lbw.Flush()
w.lbw.Reset(ioutil.Discard)
w.lbw.Reset(io.Discard)
bufioWriterPool.Put(w.lbw)
w.lbw = nil

View File

@@ -15,9 +15,9 @@ import (
"expvar"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"reflect"
"sync"
"testing"
@@ -1240,7 +1240,7 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
}
func BenchmarkWriteUint32(b *testing.B) {
w := bufio.NewWriter(ioutil.Discard)
w := bufio.NewWriter(io.Discard)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -1279,9 +1279,9 @@ func waitConnect(t testing.TB, c *Client) {
}
func TestParseSSOutput(t *testing.T) {
contents, err := ioutil.ReadFile("testdata/example_ss.txt")
contents, err := os.ReadFile("testdata/example_ss.txt")
if err != nil {
t.Errorf("ioutil.Readfile(example_ss.txt) failed: %v", err)
t.Errorf("os.ReadFile(example_ss.txt) failed: %v", err)
}
seen := parseSSOutput(string(contents))
if len(seen) == 0 {

View File

@@ -19,7 +19,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/netip"
@@ -432,7 +431,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
return nil, 0, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
b, _ := ioutil.ReadAll(resp.Body)
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
}

View File

@@ -23,7 +23,7 @@ spec:
valueFrom:
secretKeyRef:
name: tailscale-auth
key: AUTH_KEY
key: TS_AUTH_KEY
optional: true
securityContext:
capabilities:

View File

@@ -17,30 +17,43 @@
package envknob
import (
"bufio"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"tailscale.com/types/opt"
"tailscale.com/version/distro"
)
var (
mu sync.Mutex
set = map[string]string{}
list []string
mu sync.Mutex
set = map[string]string{}
regStr = map[string]*string{}
regBool = map[string]*bool{}
regOptBool = map[string]*opt.Bool{}
)
func noteEnv(k, v string) {
if v == "" {
return
}
mu.Lock()
defer mu.Unlock()
if _, ok := set[k]; !ok {
list = append(list, k)
noteEnvLocked(k, v)
}
func noteEnvLocked(k, v string) {
if v != "" {
set[k] = v
} else {
delete(set, k)
}
set[k] = v
}
// logf is logger.Logf, but logger depends on envknob, so for circular
@@ -52,11 +65,39 @@ type logf = func(format string, args ...any)
func LogCurrent(logf logf) {
mu.Lock()
defer mu.Unlock()
list := make([]string, 0, len(set))
for k := range set {
list = append(list, k)
}
sort.Strings(list)
for _, k := range list {
logf("envknob: %s=%q", k, set[k])
}
}
// Setenv changes an environment variable.
//
// It is not safe for concurrent reading of environment variables via the
// Register functions. All Setenv calls are meant to happen early in main before
// any goroutines are started.
func Setenv(envVar, val string) {
mu.Lock()
defer mu.Unlock()
os.Setenv(envVar, val)
noteEnvLocked(envVar, val)
if p := regStr[envVar]; p != nil {
*p = val
}
if p := regBool[envVar]; p != nil {
setBoolLocked(p, envVar, val)
}
if p := regOptBool[envVar]; p != nil {
setOptBoolLocked(p, envVar, val)
}
}
// String returns the named environment variable, using os.Getenv.
//
// If the variable is non-empty, it's also tracked & logged as being
@@ -67,6 +108,82 @@ func String(envVar string) string {
return v
}
// RegisterString returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterString(envVar string) func() string {
mu.Lock()
defer mu.Unlock()
p, ok := regStr[envVar]
if !ok {
val := os.Getenv(envVar)
if val != "" {
noteEnvLocked(envVar, val)
}
p = &val
regStr[envVar] = p
}
return func() string { return *p }
}
// RegisterBool returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterBool(envVar string) func() bool {
mu.Lock()
defer mu.Unlock()
p, ok := regBool[envVar]
if !ok {
var b bool
p = &b
setBoolLocked(p, envVar, os.Getenv(envVar))
regBool[envVar] = p
}
return func() bool { return *p }
}
// RegisterOptBool returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterOptBool(envVar string) func() opt.Bool {
mu.Lock()
defer mu.Unlock()
p, ok := regOptBool[envVar]
if !ok {
var b opt.Bool
p = &b
setOptBoolLocked(p, envVar, os.Getenv(envVar))
regOptBool[envVar] = p
}
return func() opt.Bool { return *p }
}
func setBoolLocked(p *bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = false
return
}
var err error
*p, err = strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
}
}
func setOptBoolLocked(p *opt.Bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = ""
return
}
b, err := strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
}
p.Set(b)
}
// Bool returns the boolean value of the named environment variable.
// If the variable is not set, it returns false.
// An invalid value exits the binary with a failure.
@@ -81,6 +198,7 @@ func BoolDefaultTrue(envVar string) bool {
}
func boolOr(envVar string, implicitValue bool) bool {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return implicitValue
@@ -98,6 +216,7 @@ func boolOr(envVar string, implicitValue bool) bool {
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupBool(envVar string) (v bool, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return false, false
@@ -113,6 +232,7 @@ func LookupBool(envVar string) (v bool, ok bool) {
// OptBool is like Bool, but returns an opt.Bool, so the caller can
// distinguish between implicitly and explicitly false.
func OptBool(envVar string) opt.Bool {
assertNotInInit()
b, ok := LookupBool(envVar)
if !ok {
return ""
@@ -126,6 +246,7 @@ func OptBool(envVar string) opt.Bool {
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupInt(envVar string) (v int, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return 0, false
@@ -164,5 +285,142 @@ func NoLogsNoSupport() bool {
// SetNoLogsNoSupport enables no-logs-no-support mode.
func SetNoLogsNoSupport() {
os.Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
}
// notInInit is set true the first time we've seen a non-init stack trace.
var notInInit atomic.Bool
func assertNotInInit() {
if notInInit.Load() {
return
}
skip := 0
for {
pc, _, _, ok := runtime.Caller(skip)
if !ok {
notInInit.Store(true)
return
}
fu := runtime.FuncForPC(pc)
if fu == nil {
return
}
name := fu.Name()
name = strings.TrimRightFunc(name, func(r rune) bool { return r >= '0' && r <= '9' })
if strings.HasSuffix(name, ".init") || strings.HasSuffix(name, ".init.") {
stack := make([]byte, 1<<10)
stack = stack[:runtime.Stack(stack, false)]
envCheckedInInitStack = stack
}
skip++
}
}
var envCheckedInInitStack []byte
// PanicIfAnyEnvCheckedInInit panics if environment variables were read during
// init.
func PanicIfAnyEnvCheckedInInit() {
if envCheckedInInitStack != nil {
panic("envknob check of called from init function: " + string(envCheckedInInitStack))
}
}
var applyDiskConfigErr error
// ApplyDiskConfigError returns the most recent result of ApplyDiskConfig.
func ApplyDiskConfigError() error { return applyDiskConfigErr }
// ApplyDiskConfig returns a platform-specific config file of environment keys/values and
// applies them. On Linux and Unix operating systems, it's a no-op and always returns nil.
// If no platform-specific config file is found, it also returns nil.
//
// It exists primarily for Windows to make it easy to apply environment variables to
// a running service in a way similar to modifying /etc/default/tailscaled on Linux.
// On Windows, you use %ProgramData%\Tailscale\tailscaled-env.txt instead.
func ApplyDiskConfig() (err error) {
var f *os.File
defer func() {
if err != nil {
// Stash away our return error for the healthcheck package to use.
applyDiskConfigErr = fmt.Errorf("error parsing %s: %w", f.Name(), err)
}
}()
// First try the explicitly-provided value for development testing. Not
// useful for users to use on their own. (if they can set this, they can set
// any environment variable anyway)
if name := os.Getenv("TS_DEBUG_ENV_FILE"); name != "" {
f, err = os.Open(name)
if err != nil {
return fmt.Errorf("error opening explicitly configured TS_DEBUG_ENV_FILE: %w", err)
}
defer f.Close()
return applyKeyValueEnv(f)
}
name := getPlatformEnvFile()
if name == "" {
return nil
}
f, err = os.Open(name)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
defer f.Close()
return applyKeyValueEnv(f)
}
// getPlatformEnvFile returns the current platform's path to an optional
// tailscaled-env.txt file. It returns an empty string if none is defined
// for the platform.
func getPlatformEnvFile() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
case "linux":
if distro.Get() == distro.Synology {
return "/etc/tailscale/tailscaled-env.txt"
}
case "darwin":
// TODO(bradfitz): figure this out. There are three ways to run
// Tailscale on macOS (tailscaled, GUI App Store, GUI System Extension)
// and we should deal with all three.
}
return ""
}
// applyKeyValueEnv reads key=value lines r and calls Setenv for each.
//
// Empty lines and lines beginning with '#' are skipped.
//
// Values can be double quoted, in which case they're unquoted using
// strconv.Unquote.
func applyKeyValueEnv(r io.Reader) error {
bs := bufio.NewScanner(r)
for bs.Scan() {
line := strings.TrimSpace(bs.Text())
if line == "" || line[0] == '#' {
continue
}
k, v, ok := strings.Cut(line, "=")
k = strings.TrimSpace(k)
if !ok || k == "" {
continue
}
v = strings.TrimSpace(v)
if strings.HasPrefix(v, `"`) {
var err error
v, err = strconv.Unquote(v)
if err != nil {
return fmt.Errorf("invalid value in line %q: %v", line, err)
}
}
Setenv(k, v)
}
return bs.Err()
}

View File

@@ -325,7 +325,7 @@ func OverallError() error {
return overallErrorLocked()
}
var fakeErrForTesting = envknob.String("TS_DEBUG_FAKE_HEALTH_ERROR")
var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
func overallErrorLocked() error {
if !anyInterfaceUp {
@@ -383,7 +383,10 @@ func overallErrorLocked() error {
for _, s := range controlHealth {
errs = append(errs, errors.New(s))
}
if e := fakeErrForTesting; len(errs) == 0 && e != "" {
if err := envknob.ApplyDiskConfigError(); err != nil {
errs = append(errs, err)
}
if e := fakeErrForTesting(); len(errs) == 0 && e != "" {
return errors.New(e)
}
sort.Slice(errs, func(i, j int) bool {

View File

@@ -9,7 +9,6 @@ package hostinfo
import (
"bytes"
"io/ioutil"
"os"
"strings"
@@ -99,11 +98,11 @@ func linuxVersionMeta() (meta versionMeta) {
case distro.OpenWrt:
propFile = "/etc/openwrt_release"
case distro.WDMyCloud:
slurp, _ := ioutil.ReadFile("/etc/version")
slurp, _ := os.ReadFile("/etc/version")
meta.DistroVersion = string(bytes.TrimSpace(slurp))
return
case distro.QNAP:
slurp, _ := ioutil.ReadFile("/etc/version_info")
slurp, _ := os.ReadFile("/etc/version_info")
meta.DistroVersion = getQnapQtsVersion(string(slurp))
return
}
@@ -133,7 +132,7 @@ func linuxVersionMeta() (meta versionMeta) {
case "debian":
// Debian's VERSION_ID is just like "11". But /etc/debian_version has "11.5" normally.
// Or "bookworm/sid" on sid/testing.
slurp, _ := ioutil.ReadFile("/etc/debian_version")
slurp, _ := os.ReadFile("/etc/debian_version")
if v := string(bytes.TrimSpace(slurp)); v != "" {
if '0' <= v[0] && v[0] <= '9' {
meta.DistroVersion = v
@@ -143,7 +142,7 @@ func linuxVersionMeta() (meta versionMeta) {
}
case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
if meta.DistroVersion == "" {
if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
if cr, _ := os.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
meta.DistroVersion = string(bytes.TrimSpace(cr))
}
}

View File

@@ -5,8 +5,11 @@
package ipnlocal
import (
"encoding/json"
"io"
"net/http"
"tailscale.com/tailcfg"
)
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
@@ -15,6 +18,21 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
// Test handler.
body, _ := io.ReadAll(r.Body)
w.Write(body)
case "/ssh/usernames":
var req tailcfg.C2NSSHUsernamesRequest
if r.Method == "POST" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
res, err := b.getSSHUsernames(&req)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
default:
http.Error(w, "unknown c2n path", http.StatusBadRequest)
}

View File

@@ -24,7 +24,6 @@ import (
"time"
"go4.org/netipx"
"golang.org/x/exp/slices"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
@@ -68,7 +67,6 @@ import (
)
var controlDebugFlags = getControlDebugFlags()
var canSSH = envknob.CanSSHD()
func getControlDebugFlags() []string {
if e := envknob.String("TS_DEBUG_CONTROL_FLAGS"); e != "" {
@@ -575,6 +573,10 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.Use
func (b *LocalBackend) PeerCaps(src netip.Addr) []string {
b.mu.Lock()
defer b.mu.Unlock()
return b.peerCapsLocked(src)
}
func (b *LocalBackend) peerCapsLocked(src netip.Addr) []string {
if b.netMap == nil {
return nil
}
@@ -586,9 +588,9 @@ func (b *LocalBackend) PeerCaps(src netip.Addr) []string {
if !a.IsSingleIP() {
continue
}
dstIP := a.Addr()
if dstIP.BitLen() == src.BitLen() {
return filt.AppendCaps(nil, src, a.Addr())
dst := a.Addr()
if dst.BitLen() == src.BitLen() { // match on family
return filt.AppendCaps(nil, src, dst)
}
}
return nil
@@ -1510,12 +1512,12 @@ func (b *LocalBackend) tellClientToBrowseToURL(url string) {
}
// For testing lazy machine key generation.
var panicOnMachineKeyGeneration = envknob.Bool("TS_DEBUG_PANIC_MACHINE_KEY")
var panicOnMachineKeyGeneration = envknob.RegisterBool("TS_DEBUG_PANIC_MACHINE_KEY")
func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePrivate, error) {
var cache syncs.AtomicValue[key.MachinePrivate]
return func() (key.MachinePrivate, error) {
if panicOnMachineKeyGeneration {
if panicOnMachineKeyGeneration() {
panic("machine key generated")
}
if v, ok := cache.LoadOk(); ok {
@@ -1752,7 +1754,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
// from the prefs p, which may be nil.
func (b *LocalBackend) setAtomicValuesFromPrefs(p *ipn.Prefs) {
b.sshAtomicBool.Store(p != nil && p.RunSSH && canSSH)
b.sshAtomicBool.Store(p != nil && p.RunSSH && envknob.CanSSHD())
if p == nil {
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
@@ -1967,7 +1969,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
}
if !canSSH {
if !envknob.CanSSHD() {
return errors.New("The Tailscale SSH server has been administratively disabled.")
}
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
@@ -2032,7 +2034,7 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
b.logf("EditPrefs check error: %v", err)
return nil, err
}
if p1.RunSSH && !canSSH {
if p1.RunSSH && !envknob.CanSSHD() {
b.mu.Unlock()
b.logf("EditPrefs requests SSH, but disabled by envknob; returning error")
return nil, errors.New("Tailscale SSH server administratively disabled.")
@@ -2854,7 +2856,7 @@ func (b *LocalBackend) applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Pre
hi.ShieldsUp = prefs.ShieldsUp
var sshHostKeys []string
if prefs.RunSSH && canSSH {
if prefs.RunSSH && envknob.CanSSHD() {
// TODO(bradfitz): this is called with b.mu held. Not ideal.
// If the filesystem gets wedged or something we could block for
// a long time. But probably fine.
@@ -3073,7 +3075,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
b.setAtomicValuesFromPrefs(nil)
}
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && canSSH }
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
// ShouldHandleViaIP reports whether whether ip is an IPv6 address in the
// Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to
@@ -3305,13 +3307,15 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
return nil, errors.New("file sharing not enabled by Tailscale admin")
}
for _, p := range nm.Peers {
if p.User != nm.User && !slices.Contains(p.Capabilities, tailcfg.CapabilityFileSharingTarget) {
if len(p.Addresses) == 0 {
continue
}
if p.User != nm.User && b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.CapabilityFileSharing) {
continue
}
peerAPI := peerAPIBase(b.netMap, p)
if peerAPI == "" {
continue
}
ret = append(ret, &apitype.FileTarget{
Node: p,
@@ -3322,6 +3326,15 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
return ret, nil
}
func (b *LocalBackend) peerHasCapLocked(addr netip.Addr, wantCap string) bool {
for _, hasCap := range b.peerCapsLocked(addr) {
if hasCap == wantCap {
return true
}
}
return false
}
// SetDNS adds a DNS record for the given domain name & TXT record
// value.
//

View File

@@ -478,8 +478,8 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
// Issue 1573: don't generate a machine key if we don't want to be running.
func TestLazyMachineKeyGeneration(t *testing.T) {
defer func(old bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
panicOnMachineKeyGeneration = true
defer func(old func() bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
panicOnMachineKeyGeneration = func() bool { return true }
var logf logger.Logf = logger.Discard
store := new(mem.Store)

View File

@@ -24,7 +24,7 @@ import (
"tailscale.com/types/tkatype"
)
var networkLockAvailable = envknob.Bool("TS_EXPERIMENTAL_NETWORK_LOCK")
var networkLockAvailable = envknob.RegisterBool("TS_EXPERIMENTAL_NETWORK_LOCK")
type tkaState struct {
authority *tka.Authority
@@ -82,7 +82,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
if b.tka != nil {
return errors.New("network-lock is already initialized")
}
if !networkLockAvailable {
if !networkLockAvailable() {
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.")
}
if !b.CanSupportNetworkLock() {

View File

@@ -44,6 +44,7 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/strs"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
@@ -720,8 +721,8 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
return
}
rawPath := r.URL.EscapedPath()
suffix := strings.TrimPrefix(rawPath, "/v0/put/")
if suffix == rawPath {
suffix, ok := strs.CutPrefix(rawPath, "/v0/put/")
if !ok {
http.Error(w, "misconfigured internals", 500)
return
}

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
@@ -87,7 +86,7 @@ func fileHasContents(name string, want string) check {
return
}
path := filepath.Join(root, name)
got, err := ioutil.ReadFile(path)
got, err := os.ReadFile(path)
if err != nil {
t.Errorf("fileHasContents: %v", err)
return
@@ -517,7 +516,7 @@ func TestDeletedMarkers(t *testing.T) {
}
wantEmptyTempDir := func() {
t.Helper()
if fis, err := ioutil.ReadDir(dir); err != nil {
if fis, err := os.ReadDir(dir); err != nil {
t.Fatal(err)
} else if len(fis) > 0 && runtime.GOOS != "windows" {
for _, fi := range fis {

View File

@@ -18,24 +18,97 @@ import (
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/tailscale/golang-x-crypto/ssh"
"tailscale.com/envknob"
"go4.org/mem"
"golang.org/x/exp/slices"
"tailscale.com/tailcfg"
"tailscale.com/util/lineread"
"tailscale.com/util/mak"
)
var useHostKeys = envknob.Bool("TS_USE_SYSTEM_SSH_HOST_KEYS")
// keyTypes are the SSH key types that we either try to read from the
// system's OpenSSH keys or try to generate for ourselves when not
// running as root.
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
res := new(tailcfg.C2NSSHUsernamesResponse)
b.mu.Lock()
defer b.mu.Unlock()
if b.sshServer == nil {
return res, nil
}
max := 10
if req != nil && req.Max != 0 {
max = req.Max
}
add := func(u string) {
if req != nil && req.Exclude[u] {
return
}
switch u {
case "nobody", "daemon", "sync":
return
}
if slices.Contains(res.Usernames, u) {
return
}
if len(res.Usernames) > max {
// Enough for a hint.
return
}
res.Usernames = append(res.Usernames, u)
}
if b.prefs != nil && b.prefs.OperatorUser != "" {
add(b.prefs.OperatorUser)
}
// Check popular usernames and see if they exist with a real shell.
switch runtime.GOOS {
case "darwin":
out, err := exec.Command("dscl", ".", "list", "/Users").Output()
if err != nil {
return nil, err
}
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '_' {
return nil
}
add(string(line))
return nil
})
default:
lineread.File("/etc/passwd", func(line []byte) error {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
return nil
}
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
return nil
}
colon := bytes.IndexByte(line, ':')
if colon != -1 {
add(string(line[:colon]))
}
return nil
})
}
return res, nil
}
func (b *LocalBackend) GetSSH_HostKeys() (keys []ssh.Signer, err error) {
var existing map[string]ssh.Signer
if os.Geteuid() == 0 {
@@ -83,7 +156,7 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
defer keyGenMu.Unlock()
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
v, err := ioutil.ReadFile(path)
v, err := os.ReadFile(path)
if err == nil {
return v, nil
}
@@ -124,7 +197,7 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
func (b *LocalBackend) getSystemSSH_HostKeys() (ret map[string]ssh.Signer) {
for _, typ := range keyTypes {
filename := "/etc/ssh/ssh_host_" + typ + "_key"
hostKey, err := ioutil.ReadFile(filename)
hostKey, err := os.ReadFile(filename)
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
continue
}

View File

@@ -6,6 +6,16 @@
package ipnlocal
import (
"errors"
"tailscale.com/tailcfg"
)
func (b *LocalBackend) getSSHHostKeyPublicStrings() []string {
return nil
}
func (b *LocalBackend) getSSHUsernames(*tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
return nil, errors.New("not implemented")
}

View File

@@ -2,14 +2,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
// +build linux
//go:build linux || (darwin && !ios)
// +build linux darwin,!ios
package ipnlocal
import (
"encoding/json"
"reflect"
"testing"
"tailscale.com/tailcfg"
"tailscale.com/util/must"
)
func TestSSHKeyGen(t *testing.T) {
@@ -40,3 +44,17 @@ func TestSSHKeyGen(t *testing.T) {
t.Errorf("got different keys on second call")
}
}
type fakeSSHServer struct {
SSHServer
}
func TestGetSSHUsernames(t *testing.T) {
b := new(LocalBackend)
b.sshServer = fakeSSHServer{}
res, err := b.getSSHUsernames(new(tailcfg.C2NSSHUsernamesRequest))
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", must.Get(json.Marshal(res)))
}

View File

@@ -12,7 +12,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -215,7 +214,7 @@ func (s *Server) blockWhileInUse(conn io.Reader, ci connIdentity) {
s.logf("blocking client while server in use; connIdentity=%v", ci)
connDone := make(chan struct{})
go func() {
io.Copy(ioutil.Discard, conn)
io.Copy(io.Discard, conn)
close(connDone)
}()
ch := make(chan struct{}, 1)
@@ -933,14 +932,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
startTime := time.Now()
log.Printf("exec: %#v %v", executable, args)
cmd := exec.Command(executable, args...)
if runtime.GOOS == "windows" {
extraEnv, err := loadExtraEnv()
if err != nil {
logf("errors loading extra env file; ignoring: %v", err)
} else {
cmd.Env = append(os.Environ(), extraEnv...)
}
}
// Create a pipe object to use as the subproc's stdin.
// When the writer goes away, the reader gets EOF.
@@ -1175,7 +1166,7 @@ func findTrueNASTaildropDir(name string) (dir string, err error) {
}
// but if running on the host, it may be something like /mnt/Primary/Taildrop
fis, err := ioutil.ReadDir("/mnt")
fis, err := os.ReadDir("/mnt")
if err != nil {
return "", fmt.Errorf("error reading /mnt: %w", err)
}
@@ -1209,38 +1200,3 @@ func findQnapTaildropDir(name string) (string, error) {
}
return "", fmt.Errorf("shared folder %q not found", name)
}
func loadExtraEnv() (env []string, err error) {
if runtime.GOOS != "windows" {
return nil, nil
}
name := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
contents, err := os.ReadFile(name)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
for _, line := range strings.Split(string(contents), "\n") {
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok || k == "" {
continue
}
if strings.HasPrefix(v, `"`) {
var err error
v, err = strconv.Unquote(v)
if err != nil {
return nil, fmt.Errorf("invalid value in line %q: %v", line, err)
}
env = append(env, k+"="+v)
} else {
env = append(env, line)
}
}
return env, nil
}

View File

@@ -23,7 +23,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -38,6 +37,7 @@ import (
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/types/logger"
"tailscale.com/util/strs"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -73,7 +73,7 @@ func (h *Handler) certDir() (string, error) {
return full, nil
}
var acmeDebug = envknob.Bool("TS_DEBUG_ACME")
var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite && !h.PermitCert {
@@ -87,16 +87,19 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
return
}
domain := strings.TrimPrefix(r.URL.Path, "/localapi/v0/cert/")
if domain == r.URL.Path {
domain, ok := strs.CutPrefix(r.URL.Path, "/localapi/v0/cert/")
if !ok {
http.Error(w, "internal handler config wired wrong", 500)
return
}
if !validLookingCertDomain(domain) {
http.Error(w, "invalid domain", 400)
return
}
now := time.Now()
logf := logger.WithPrefix(h.logf, fmt.Sprintf("cert(%q): ", domain))
traceACME := func(v any) {
if !acmeDebug {
if !acmeDebug() {
return
}
j, _ := json.MarshalIndent(v, "", "\t")
@@ -165,6 +168,11 @@ func certFile(dir, domain string) string { return filepath.Join(dir, domain+".cr
// keypair for domain exists on disk in dir that is valid at the
// provided now time.
func (h *Handler) getCertPEMCached(dir, domain string, now time.Time) (p *keyPair, ok bool) {
if !validLookingCertDomain(domain) {
// Before we read files from disk using it, validate it's halfway
// reasonable looking.
return nil, false
}
if keyPEM, err := os.ReadFile(keyFile(dir, domain)); err == nil {
certPEM, _ := os.ReadFile(certFile(dir, domain))
if validCertPEM(domain, keyPEM, certPEM, now) {
@@ -293,7 +301,7 @@ func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME fu
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
return nil, err
}
if err := ioutil.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
if err := os.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
return nil, err
}
@@ -316,7 +324,7 @@ func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME fu
return nil, err
}
}
if err := ioutil.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
if err := os.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
return nil, err
}
@@ -372,7 +380,7 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) {
func acmeKey(dir string) (crypto.Signer, error) {
pemName := filepath.Join(dir, "acme-account.key.pem")
if v, err := ioutil.ReadFile(pemName); err == nil {
if v, err := os.ReadFile(pemName); err == nil {
priv, _ := pem.Decode(v)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, errors.New("acme/autocert: invalid account key found in cache")
@@ -388,7 +396,7 @@ func acmeKey(dir string) (crypto.Signer, error) {
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
return nil, err
}
if err := ioutil.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
if err := os.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
return nil, err
}
return privKey, nil
@@ -426,6 +434,21 @@ func validCertPEM(domain string, keyPEM, certPEM []byte, now time.Time) bool {
return err == nil
}
// validLookingCertDomain reports whether name looks like a valid domain name that
// we might be able to get a cert for.
//
// It's a light check primarily for double checking before it's used
// as part of a filesystem path. The actual validation happens in checkCertDomain.
func validLookingCertDomain(name string) bool {
if name == "" ||
strings.Contains(name, "..") ||
strings.ContainsAny(name, ":/\\\x00") ||
!strings.Contains(name, ".") {
return false
}
return true
}
func checkCertDomain(st *ipnstate.Status, domain string) error {
if domain == "" {
return errors.New("missing domain name")

30
ipn/localapi/cert_test.go Normal file
View File

@@ -0,0 +1,30 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !ios && !android && !js
// +build !ios,!android,!js
package localapi
import "testing"
func TestValidLookingCertDomain(t *testing.T) {
tests := []struct {
in string
want bool
}{
{"foo.com", true},
{"foo..com", false},
{"foo/com.com", false},
{"NUL", false},
{"", false},
{"foo\\bar.com", false},
{"foo\x00bar.com", false},
}
for _, tt := range tests {
if got := validLookingCertDomain(tt.in); got != tt.want {
t.Errorf("validLookingCertDomain(%q) = %v, want %v", tt.in, got, tt.want)
}
}
}

View File

@@ -9,7 +9,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/netip"
"os"
@@ -618,7 +617,7 @@ func PrefsFromBytes(b []byte) (*Prefs, error) {
// LoadPrefs loads a legacy relaynode config file into Prefs
// with sensible migration defaults set.
func LoadPrefs(filename string) (*Prefs, error) {
data, err := ioutil.ReadFile(filename)
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
}

View File

@@ -8,7 +8,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/netip"
"os"
"reflect"
@@ -474,7 +473,7 @@ func TestLoadPrefsNotExist(t *testing.T) {
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
// See issue #954 for details.
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
f, err := ioutil.TempFile("", "TestLoadPrefsFileWithZeroInIt")
f, err := os.CreateTemp("", "TestLoadPrefsFileWithZeroInIt")
if err != nil {
t.Fatal(err)
}

View File

@@ -9,7 +9,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -128,7 +127,7 @@ func NewFileStore(logf logger.Logf, path string) (ipn.StateStore, error) {
return nil, fmt.Errorf("creating state directory: %w", err)
}
bs, err := ioutil.ReadFile(path)
bs, err := os.ReadFile(path)
// Treat an empty file as a missing file.
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)

View File

@@ -9,7 +9,6 @@ package filelogger
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
@@ -186,12 +185,18 @@ func (w *logFileWriter) startNewFileLocked() {
//
// w.mu must be held.
func (w *logFileWriter) cleanLocked() {
fis, _ := ioutil.ReadDir(w.dir)
entries, _ := os.ReadDir(w.dir)
prefix := w.fileBasePrefix + "-"
fileSize := map[string]int64{}
var files []string
var sumSize int64
for _, fi := range fis {
for _, entry := range entries {
fi, err := entry.Info()
if err != nil {
w.wrappedLogf("error getting log file info: %v", err)
continue
}
baseName := filepath.Base(fi.Name())
if !strings.HasPrefix(baseName, prefix) {
continue

View File

@@ -16,7 +16,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -248,7 +247,7 @@ func logsDir(logf logger.Logf) string {
// No idea where to put stuff. Try to create a temp dir. It'll
// mean we might lose some logs and rotate through log IDs, but
// it's something.
tmp, err := ioutil.TempDir("", "tailscaled-log-*")
tmp, err := os.MkdirTemp("", "tailscaled-log-*")
if err != nil {
panic("no safe place found to store log state")
}
@@ -259,7 +258,7 @@ func logsDir(logf logger.Logf) string {
// runningUnderSystemd reports whether we're running under systemd.
func runningUnderSystemd() bool {
if runtime.GOOS == "linux" && os.Getppid() == 1 {
slurp, _ := ioutil.ReadFile("/proc/1/stat")
slurp, _ := os.ReadFile("/proc/1/stat")
return bytes.HasPrefix(slurp, []byte("1 (systemd) "))
}
return false

View File

@@ -6,7 +6,7 @@ package main
import (
"flag"
"io/ioutil"
"io"
"log"
"net/http"
"net/url"
@@ -39,7 +39,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
log.Fatalf("logadopt: response read failed %d: %v", resp.StatusCode, err)

View File

@@ -9,7 +9,7 @@ import (
"bufio"
"encoding/json"
"flag"
"io/ioutil"
"io"
"log"
"net/http"
"os"
@@ -50,7 +50,7 @@ func main() {
defer resp.Body.Close()
if resp.StatusCode != 200 {
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("logreprocess: read error %d: %v", resp.StatusCode, err)
}

View File

@@ -6,7 +6,7 @@ package filch
import (
"fmt"
"io/ioutil"
"io"
"os"
"runtime"
"strings"
@@ -195,7 +195,7 @@ func TestFilchStderr(t *testing.T) {
f.close(t)
pipeW.Close()
b, err := ioutil.ReadAll(pipeR)
b, err := io.ReadAll(pipeR)
if err != nil {
t.Fatal(err)
}

View File

@@ -13,7 +13,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
@@ -430,7 +429,7 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
if resp.StatusCode != 200 {
uploaded = resp.StatusCode == 400 // the server saved the logs anyway
b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
return uploaded, fmt.Errorf("log upload of %d bytes %s failed %d: %q", len(body), compressedNote, resp.StatusCode, b)
}
@@ -654,7 +653,7 @@ func (l *Logger) Write(buf []byte) (int, error) {
return 0, nil
}
level, buf := parseAndRemoveLogLevel(buf)
if l.stderr != nil && l.stderr != ioutil.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
if l.stderr != nil && l.stderr != io.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
if buf[len(buf)-1] == '\n' {
l.stderr.Write(buf)
} else {

View File

@@ -9,7 +9,6 @@ import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
@@ -52,7 +51,7 @@ func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
ts.srv = httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
body, err := io.ReadAll(r.Body)
if err != nil {
t.Error("failed to read HTTP request")
}

View File

@@ -12,7 +12,6 @@ import (
"bytes"
_ "embed"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -158,7 +157,7 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
if sc.Text() == resolvconfConfigName {
continue
}
bs, err := ioutil.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
bs, err := os.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
if err != nil {
if os.IsNotExist(err) {
// Probably raced with a deletion, that's okay.

View File

@@ -12,7 +12,6 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"net/netip"
"os"
"os/exec"
@@ -452,7 +451,7 @@ func (fs directFS) Rename(oldName, newName string) error {
func (fs directFS) Remove(name string) error { return os.Remove(fs.path(name)) }
func (fs directFS) ReadFile(name string) ([]byte, error) {
return ioutil.ReadFile(fs.path(name))
return os.ReadFile(fs.path(name))
}
func (fs directFS) Truncate(name string) error {
@@ -460,7 +459,7 @@ func (fs directFS) Truncate(name string) error {
}
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
return ioutil.WriteFile(fs.path(name), contents, perm)
return os.WriteFile(fs.path(name), contents, perm)
}
// runningAsGUIDesktopUser reports whether it seems that this code is

View File

@@ -6,14 +6,13 @@ package dns
import (
"fmt"
"io/ioutil"
"os"
"tailscale.com/types/logger"
)
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
bs, err := ioutil.ReadFile("/etc/resolv.conf")
bs, err := os.ReadFile("/etc/resolv.conf")
if os.IsNotExist(err) {
return newDirectManager(logf), nil
}

View File

@@ -32,7 +32,7 @@ const (
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
)
var configureWSL = envknob.Bool("TS_DEBUG_CONFIGURE_WSL")
var configureWSL = envknob.RegisterBool("TS_DEBUG_CONFIGURE_WSL")
type windowsManager struct {
logf logger.Logf
@@ -359,7 +359,7 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
// On initial setup of WSL, the restart caused by --shutdown is slow,
// so we do it out-of-line.
if configureWSL {
if configureWSL() {
go func() {
if err := m.wslManager.SetDNS(cfg); err != nil {
m.logf("WSL SetDNS: %v", err) // continue

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/http"
@@ -474,7 +473,7 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
metricDNSFwdDoHErrorCT.Add(1)
return nil, fmt.Errorf("unexpected response Content-Type %q", ct)
}
res, err := ioutil.ReadAll(hres.Body)
res, err := io.ReadAll(hres.Body)
if err != nil {
metricDNSFwdDoHErrorBody.Add(1)
}
@@ -484,13 +483,13 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
return res, err
}
var verboseDNSForward = envknob.Bool("TS_DEBUG_DNS_FORWARD_SEND")
var verboseDNSForward = envknob.RegisterBool("TS_DEBUG_DNS_FORWARD_SEND")
// send sends packet to dst. It is best effort.
//
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
if verboseDNSForward {
if verboseDNSForward() {
f.logf("forwarder.send(%q) ...", rr.name.Addr)
defer func() {
f.logf("forwarder.send(%q) = %v, %v", rr.name.Addr, len(ret), err)

View File

@@ -141,7 +141,7 @@ func (r *Resolver) ttl() time.Duration {
return 10 * time.Minute
}
var debug = envknob.Bool("TS_DEBUG_DNS_CACHE")
var debug = envknob.RegisterBool("TS_DEBUG_DNS_CACHE")
// LookupIP returns the host's primary IP address (either IPv4 or
// IPv6, but preferring IPv4) and optionally its IPv6 address, if
@@ -167,14 +167,14 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
}
if ip, err := netip.ParseAddr(host); err == nil {
ip = ip.Unmap()
if debug {
if debug() {
log.Printf("dnscache: %q is an IP", host)
}
return ip, zaddr, []netip.Addr{ip}, nil
}
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
if debug {
if debug() {
log.Printf("dnscache: %q = %v (cached)", host, ip)
}
return ip, ip6, allIPs, nil
@@ -192,13 +192,13 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
if res.Err != nil {
if r.UseLastGood {
if ip, ip6, allIPs, ok := r.lookupIPCacheExpired(host); ok {
if debug {
if debug() {
log.Printf("dnscache: %q using %v after error", host, ip)
}
return ip, ip6, allIPs, nil
}
}
if debug {
if debug() {
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
}
return zaddr, zaddr, nil, res.Err
@@ -206,7 +206,7 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
r := res.Val
return r.ip, r.ip6, r.allIPs, nil
case <-ctx.Done():
if debug {
if debug() {
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
}
return zaddr, zaddr, nil, ctx.Err()
@@ -250,7 +250,7 @@ func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
func (r *Resolver) lookupIP(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, err error) {
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
if debug {
if debug() {
log.Printf("dnscache: %q found in cache as %v", host, ip)
}
return ip, ip6, allIPs, nil
@@ -300,13 +300,13 @@ func (r *Resolver) addIPCache(host string, ip, ip6 netip.Addr, allIPs []netip.Ad
if ip.IsPrivate() {
// Don't cache obviously wrong entries from captive portals.
// TODO: use DoH or DoT for the forwarding resolver?
if debug {
if debug() {
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
}
return
}
if debug {
if debug() {
log.Printf("dnscache: %q resolved to IP %v; caching", host, ip)
}
@@ -382,7 +382,7 @@ func (d *dialer) DialContext(ctx context.Context, network, address string) (retC
}
i4s := v4addrs(allIPs)
if len(i4s) < 2 {
if debug {
if debug() {
log.Printf("dnscache: dialing %s, %s for %s", network, ip, address)
}
c, err := dc.dialOne(ctx, ip.Unmap())
@@ -406,7 +406,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
// Can't try bootstrap DNS if we don't have a fallback function
if d.dnsCache.LookupIPFallback == nil {
if debug {
if debug() {
log.Printf("dnscache: not using bootstrap DNS: no fallback")
}
return false
@@ -415,7 +415,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
// We can't retry if the context is canceled, since any further
// operations with this context will fail.
if ctxErr := ctx.Err(); ctxErr != nil {
if debug {
if debug() {
log.Printf("dnscache: not using bootstrap DNS: context error: %v", ctxErr)
}
return false
@@ -423,7 +423,7 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
wasTrustworthy := dc.dnsWasTrustworthy()
if wasTrustworthy {
if debug {
if debug() {
log.Printf("dnscache: not using bootstrap DNS: DNS was trustworthy")
}
return false

View File

@@ -167,10 +167,8 @@ func TestInterleaveSlices(t *testing.T) {
func TestShouldTryBootstrap(t *testing.T) {
oldDebug := debug
t.Cleanup(func() {
debug = oldDebug
})
debug = true
t.Cleanup(func() { debug = oldDebug })
debug = func() bool { return true }
type step struct {
ip netip.Addr // IP we pretended to dial

View File

@@ -10,7 +10,6 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
@@ -42,7 +41,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile("dns-fallback-servers.json", out, 0644); err != nil {
if err := os.WriteFile("dns-fallback-servers.json", out, 0644); err != nil {
log.Fatal(err)
}
}

View File

@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"testing"
@@ -23,7 +22,7 @@ func TestGoogleCloudRunDefaultRouteInterface(t *testing.T) {
buf := []byte("Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
"eth0\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n" +
"eth1\t00000000\t00000000\t0001\t0\t0\t0\t00000000\t0\t0\t0\n")
err := ioutil.WriteFile(procNetRoutePath, buf, 0644)
err := os.WriteFile(procNetRoutePath, buf, 0644)
if err != nil {
t.Fatal(err)
}
@@ -87,7 +86,7 @@ func TestAwsAppRunnerDefaultRouteInterface(t *testing.T) {
"ecs-eth0\t02AAFEA9\t01ACFEA9\t0007\t0\t0\t0\tFFFFFFFF\t0\t0\t0\n" +
"ecs-eth0\t00ACFEA9\t00000000\t0001\t0\t0\t0\t00FFFFFF\t0\t0\t0\n" +
"eth0\t00AFFEA9\t00000000\t0001\t0\t0\t0\t00FFFFFF\t0\t0\t0\n")
err := ioutil.WriteFile(procNetRoutePath, buf, 0644)
err := os.WriteFile(procNetRoutePath, buf, 0644)
if err != nil {
t.Fatal(err)
}

View File

@@ -12,7 +12,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -43,7 +42,7 @@ import (
// Debugging and experimentation tweakables.
var (
debugNetcheck = envknob.Bool("TS_DEBUG_NETCHECK")
debugNetcheck = envknob.RegisterBool("TS_DEBUG_NETCHECK")
)
// The various default timeouts for things.
@@ -210,7 +209,7 @@ func (c *Client) logf(format string, a ...any) {
}
func (c *Client) vlogf(format string, a ...any) {
if c.Verbose || debugNetcheck {
if c.Verbose || debugNetcheck() {
c.logf(format, a...)
}
}
@@ -1096,7 +1095,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
return 0, ip, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, resp.Status)
}
_, err = io.Copy(ioutil.Discard, io.LimitReader(resp.Body, 8<<10))
_, err = io.Copy(io.Discard, io.LimitReader(resp.Body, 8<<10))
if err != nil {
return 0, ip, err
}

View File

@@ -20,6 +20,11 @@ var (
androidProtectFunc func(fd int) error
)
// UseSocketMark reports whether SO_MARK is in use. Android does not use SO_MARK.
func UseSocketMark() bool {
return false
}
// SetAndroidProtectFunc register a func that Android provides that JNI calls into
// https://developer.android.com/reference/android/net/VpnService#protect(int)
// which is documented as:

View File

@@ -63,12 +63,12 @@ func socketMarkWorks() bool {
return true
}
var forceBindToDevice = envknob.Bool("TS_FORCE_LINUX_BIND_TO_DEVICE")
var forceBindToDevice = envknob.RegisterBool("TS_FORCE_LINUX_BIND_TO_DEVICE")
// UseSocketMark reports whether SO_MARK is in use.
// If it doesn't, we have to use SO_BINDTODEVICE on our sockets instead.
func UseSocketMark() bool {
if forceBindToDevice {
if forceBindToDevice() {
return false
}
socketMarkWorksOnce.Do(func() {

View File

@@ -32,7 +32,7 @@ var counterFallbackOK int32 // atomic
// See https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format
var sslKeyLogFile = os.Getenv("SSLKEYLOGFILE")
var debug = envknob.Bool("TS_DEBUG_TLS_DIAL")
var debug = envknob.RegisterBool("TS_DEBUG_TLS_DIAL")
// Config returns a tls.Config for connecting to a server.
// If base is non-nil, it's cloned as the base config before
@@ -77,7 +77,7 @@ func Config(host string, base *tls.Config) *tls.Config {
opts.Intermediates.AddCert(cert)
}
_, errSys := cs.PeerCertificates[0].Verify(opts)
if debug {
if debug() {
log.Printf("tlsdial(sys %q): %v", host, errSys)
}
if errSys == nil {
@@ -88,7 +88,7 @@ func Config(host string, base *tls.Config) *tls.Config {
// or broken, fall back to trying LetsEncrypt at least.
opts.Roots = bakedInRoots()
_, err := cs.PeerCertificates[0].Verify(opts)
if debug {
if debug() {
log.Printf("tlsdial(bake %q): %v", host, err)
}
if err == nil {
@@ -142,7 +142,7 @@ func SetConfigExpectedCert(c *tls.Config, certDNSName string) {
opts.Intermediates.AddCert(cert)
}
_, errSys := certs[0].Verify(opts)
if debug {
if debug() {
log.Printf("tlsdial(sys %q/%q): %v", c.ServerName, certDNSName, errSys)
}
if errSys == nil {
@@ -150,7 +150,7 @@ func SetConfigExpectedCert(c *tls.Config, certDNSName string) {
}
opts.Roots = bakedInRoots()
_, err := certs[0].Verify(opts)
if debug {
if debug() {
log.Printf("tlsdial(bake %q/%q): %v", c.ServerName, certDNSName, err)
}
if err == nil {

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
@@ -60,7 +59,7 @@ func TestSynologyProxyFromConfigCached(t *testing.T) {
cache.httpProxy = nil
cache.httpsProxy = nil
if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(`
if err := os.WriteFile(synologyProxyConfigPath, []byte(`
proxy_enabled=yes
http_host=10.0.0.55
http_port=80
@@ -116,7 +115,7 @@ https_port=443
cache.httpProxy = nil
cache.httpsProxy = nil
if err := ioutil.WriteFile(synologyProxyConfigPath, []byte(`
if err := os.WriteFile(synologyProxyConfigPath, []byte(`
proxy_enabled=yes
http_host=10.0.0.55
http_port=80

View File

@@ -20,14 +20,6 @@ import (
"tailscale.com/types/logger"
)
var tunMTU = DefaultMTU
func init() {
if mtu, ok := envknob.LookupInt("TS_DEBUG_MTU"); ok {
tunMTU = mtu
}
}
// createTAP is non-nil on Linux.
var createTAP func(tapName, bridgeName string) (tun.Device, error)
@@ -52,6 +44,10 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
}
dev, err = createTAP(tapName, bridgeName)
} else {
tunMTU := DefaultMTU
if mtu, ok := envknob.LookupInt("TS_DEBUG_MTU"); ok {
tunMTU = mtu
}
dev, err = tun.CreateTUN(tunName, tunMTU)
}
if err != nil {

View File

@@ -16,7 +16,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
@@ -74,7 +73,7 @@ func Read(r io.Reader) (*Info, error) {
}
// Exhaust the remainder of r, so that the summers see the entire file.
if _, err := io.Copy(ioutil.Discard, r); err != nil {
if _, err := io.Copy(io.Discard, r); err != nil {
return nil, fmt.Errorf("hashing file: %w", err)
}
@@ -117,7 +116,7 @@ func findControlTar(r io.Reader) (tarReader io.Reader, err error) {
if size%2 == 1 {
size++
}
if _, err := io.CopyN(ioutil.Discard, r, size); err != nil {
if _, err := io.CopyN(io.Discard, r, size); err != nil {
return nil, fmt.Errorf("seeking past file %q: %w", filename, err)
}
}
@@ -150,7 +149,7 @@ func findControlFile(r io.Reader) (control []byte, err error) {
break
}
bs, err := ioutil.ReadAll(tr)
bs, err := io.ReadAll(tr)
if err != nil {
return nil, fmt.Errorf("reading control file: %w", err)
}

View File

@@ -74,10 +74,10 @@ func (pl List) String() string {
return strings.TrimRight(sb.String(), "\n")
}
var debugDisablePortlist = envknob.Bool("TS_DEBUG_DISABLE_PORTLIST")
var debugDisablePortlist = envknob.RegisterBool("TS_DEBUG_DISABLE_PORTLIST")
func GetList(prev List) (List, error) {
if debugDisablePortlist {
if debugDisablePortlist() {
return nil, nil
}
pl, err := listPorts()

View File

@@ -8,7 +8,6 @@ import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -230,7 +229,7 @@ func addProcesses(pl []Port) ([]Port, error) {
pe := pm[string(targetBuf[:n])] // m[string([]byte)] avoids alloc
if pe != nil {
bs, err := ioutil.ReadFile(fmt.Sprintf("/proc/%s/cmdline", pid))
bs, err := os.ReadFile(fmt.Sprintf("/proc/%s/cmdline", pid))
if err != nil {
// Usually shouldn't happen. One possibility is
// the process has gone away, so let's skip it.

View File

@@ -9,7 +9,6 @@ import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -70,7 +69,7 @@ func localTCPPortAndTokenDarwin() (port int, token string, err error) {
// The current binary (this process) is sandboxed. The user is
// running the CLI via /Applications/Tailscale.app/Contents/MacOS/Tailscale
// which sets the TS_MACOS_CLI_SHARED_DIR environment variable.
fis, err := ioutil.ReadDir(dir)
fis, err := os.ReadDir(dir)
if err != nil {
return 0, "", err
}

View File

@@ -5,7 +5,7 @@
package smallzstd
import (
"io/ioutil"
"os"
"testing"
"github.com/klauspost/compress/zstd"
@@ -113,7 +113,7 @@ func benchDecoderWithConstruction(b *testing.B, mk func() (*zstd.Decoder, error)
func testdata(b *testing.B) []byte {
b.Helper()
in, err := ioutil.ReadFile("testdata")
in, err := os.ReadFile("testdata")
if err != nil {
b.Fatalf("reading testdata: %v", err)
}

View File

@@ -17,7 +17,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/netip"
@@ -46,9 +45,7 @@ import (
)
var (
debugPolicyFile = envknob.SSHPolicyFile()
debugIgnoreTailnetSSHPolicy = envknob.SSHIgnoreTailnetPolicy()
sshVerboseLogging = envknob.Bool("TS_DEBUG_SSH_VLOG")
sshVerboseLogging = envknob.RegisterBool("TS_DEBUG_SSH_VLOG")
)
type server struct {
@@ -384,9 +381,10 @@ func (c *conn) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
if nm == nil {
return nil, false
}
if pol := nm.SSHPolicy; pol != nil && !debugIgnoreTailnetSSHPolicy {
if pol := nm.SSHPolicy; pol != nil && !envknob.SSHIgnoreTailnetPolicy() {
return pol, true
}
debugPolicyFile := envknob.SSHPolicyFile()
if debugPolicyFile != "" {
c.logf("reading debug SSH policy file: %v", debugPolicyFile)
f, err := os.ReadFile(debugPolicyFile)
@@ -769,7 +767,7 @@ type sshSession struct {
}
func (ss *sshSession) vlogf(format string, args ...interface{}) {
if sshVerboseLogging {
if sshVerboseLogging() {
ss.logf(format, args...)
}
}
@@ -952,7 +950,7 @@ func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *user.User) err
// functionality and support off-node streaming.
//
// TODO(bradfitz,maisem): move this to SSHPolicy.
var recordSSH = envknob.Bool("TS_DEBUG_LOG_SSH")
var recordSSH = envknob.RegisterBool("TS_DEBUG_LOG_SSH")
// run is the entrypoint for a newly accepted SSH session.
//
@@ -1092,7 +1090,7 @@ func (ss *sshSession) shouldRecord() bool {
// TODO(bradfitz,maisem): make configurable on SSHPolicy and
// support recording non-pty stuff too.
_, _, isPtyReq := ss.Pty()
return recordSSH && isPtyReq
return recordSSH() && isPtyReq
}
type sshConnInfo struct {
@@ -1307,7 +1305,7 @@ func (ss *sshSession) startNewRecording() (*recording, error) {
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, err
}
f, err := ioutil.TempFile(dir, fmt.Sprintf("ssh-session-%v-*.cast", now.UnixNano()))
f, err := os.CreateTemp(dir, fmt.Sprintf("ssh-session-%v-*.cast", now.UnixNano()))
if err != nil {
return nil, err
}

36
tailcfg/c2ntypes.go Normal file
View File

@@ -0,0 +1,36 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// c2n (control-to-node) API types.
package tailcfg
// C2NSSHUsernamesRequest is the request for the /ssh/usernames.
// A GET request without a request body is equivalent to the zero value of this type.
// Otherwise, a POST request with a JSON-encoded request body is expected.
type C2NSSHUsernamesRequest struct {
// Exclude optionally specifies usernames to exclude
// from the response.
Exclude map[string]bool `json:",omitempty"`
// Max is the maximum number of usernames to return.
// If zero, a default limit is used.
Max int `json:",omitempty"`
}
// C2NSSHUsernamesResponse is the response (from node to control) from the
// /ssh/usernames handler.
//
// It returns username auto-complete suggestions for a user to SSH to this node.
// It's only shown to people who already have SSH access to the node. If this
// returns multiple usernames, only the usernames that would have access per the
// tailnet's ACLs are shown to the user so as to not leak the existence of
// usernames.
type C2NSSHUsernamesResponse struct {
// Usernames is the list of usernames to suggest. If the machine has many
// users, this list may be truncated. If getting the list of usernames might
// be too slow or unavailable, this list might be empty. This is effectively
// just a best effort set of hints.
Usernames []string
}

View File

@@ -1456,6 +1456,10 @@ type Debug struct {
// re-enabled for the lifetime of the process.
DisableLogTail bool `json:",omitempty"`
// EnableSilentDisco disables the use of heartBeatTimer in magicsock and attempts to
// handle disco silently. See issue #540 for details.
EnableSilentDisco bool `json:",omitempty"`
// Exit optionally specifies that the client should os.Exit
// with this code.
Exit *int `json:",omitempty"`
@@ -1585,15 +1589,11 @@ const (
CapabilitySSHRuleIn = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
CapabilityDataPlaneAuditLogs = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
// These are the capabilities that the peer nodes have as listed in
// MapResponse.Peers[].Capabilities.
// Inter-node capabilities as specified in the MapResponse.PacketFilter[].CapGrants.
// CapabilityFileSharingTarget grants the current node the ability to send
// files to the peer which has this capability.
CapabilityFileSharingTarget = "https://tailscale.com/cap/file-sharing-target"
// Inter-node capabilities as specified in the MapResponse.PacketFilter[].CapGrants.
// CapabilityFileSharingSend grants the ability to receive files from a
// node that's owned by a different user.
CapabilityFileSharingSend = "https://tailscale.com/cap/file-send"

View File

@@ -2,8 +2,8 @@ package ssh
import (
"io"
"io/ioutil"
"net"
"os"
"path"
"sync"
@@ -36,7 +36,7 @@ func AgentRequested(sess Session) bool {
// NewAgentListener sets up a temporary Unix socket that can be communicated
// to the session environment and used for forwarding connections.
func NewAgentListener() (net.Listener, error) {
dir, err := ioutil.TempDir("", agentTempDir)
dir, err := os.MkdirTemp("", agentTempDir)
if err != nil {
return nil, err
}

View File

@@ -3,7 +3,7 @@ package ssh_test
import (
"errors"
"io"
"io/ioutil"
"os"
"tailscale.com/tempfork/gliderlabs/ssh"
)
@@ -29,7 +29,7 @@ func ExampleNoPty() {
func ExamplePublicKeyAuth() {
ssh.ListenAndServe(":2222", nil,
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) error {
data, err := ioutil.ReadFile("/path/to/allowed/key.pub")
data, err := os.ReadFile("/path/to/allowed/key.pub")
if err != nil {
return err
}

View File

@@ -1,7 +1,7 @@
package ssh
import (
"io/ioutil"
"os"
gossh "github.com/tailscale/golang-x-crypto/ssh"
)
@@ -26,7 +26,7 @@ func PublicKeyAuth(fn PublicKeyHandler) Option {
// from a PEM file at filepath.
func HostKeyFile(filepath string) Option {
return func(srv *Server) error {
pemBytes, err := ioutil.ReadFile(filepath)
pemBytes, err := os.ReadFile(filepath)
if err != nil {
return err
}

View File

@@ -5,7 +5,6 @@ package ssh
import (
"bytes"
"io/ioutil"
"net"
"strconv"
"strings"
@@ -61,7 +60,7 @@ func TestLocalPortForwardingWorks(t *testing.T) {
if err != nil {
t.Fatalf("Error connecting to %v: %v", l.Addr().String(), err)
}
result, err := ioutil.ReadAll(conn)
result, err := io.ReadAll(conn)
if err != nil {
t.Fatal(err)
}

View File

@@ -6,7 +6,7 @@ package pprof
import (
"bytes"
"io/ioutil"
"io"
"net/http"
"net/http/httptest"
"runtime/pprof"
@@ -52,7 +52,7 @@ func TestHandlers(t *testing.T) {
t.Errorf("status code: got %d; want %d", got, want)
}
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Errorf("when reading response body, expected non-nil err; got %v", err)
}

View File

@@ -7,7 +7,6 @@ package tka
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
@@ -345,7 +344,7 @@ func (c *FS) LastActiveAncestor() (*AUMHash, error) {
c.mu.RLock()
defer c.mu.RUnlock()
hash, err := ioutil.ReadFile(filepath.Join(c.base, "last_active_ancestor"))
hash, err := os.ReadFile(filepath.Join(c.base, "last_active_ancestor"))
if err != nil {
if os.IsNotExist(err) {
return nil, nil // Not exist == none set.

View File

@@ -11,7 +11,6 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -252,7 +251,7 @@ func (s *Server) start() (reterr error) {
c := logtail.Config{
Collection: lpc.Collection,
PrivateID: lpc.PrivateID,
Stderr: ioutil.Discard, // log everything to Buffer
Stderr: io.Discard, // log everything to Buffer
Buffer: s.logbuffer,
NewZstdEncoder: func() logtail.Encoder {
w, err := smallzstd.NewEncoder(nil)

View File

@@ -11,7 +11,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
@@ -62,7 +61,7 @@ import (
fmt.Fprintf(&out, ")\n")
filename := fmt.Sprintf("tailscaled_deps_test_%s.go", goos)
err = ioutil.WriteFile(filename, out.Bytes(), 0644)
err = os.WriteFile(filename, out.Bytes(), 0644)
if err != nil {
log.Fatal(err)
}

View File

@@ -14,7 +14,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -88,7 +87,7 @@ var (
// buildTestBinaries builds tailscale and tailscaled.
// It returns the dir containing the binaries.
func buildTestBinaries() (string, error) {
bindir, err := ioutil.TempDir("", "")
bindir, err := os.MkdirTemp("", "")
if err != nil {
return "", err
}
@@ -288,7 +287,7 @@ func (lc *LogCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer dec.Close()
body = dec
}
bodyBytes, _ := ioutil.ReadAll(body)
bodyBytes, _ := io.ReadAll(body)
type Entry struct {
Logtail struct {

View File

@@ -14,7 +14,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
@@ -653,7 +652,7 @@ func newTestNode(t *testing.T, env *testEnv) *testNode {
func (n *testNode) diskPrefs() *ipn.Prefs {
t := n.env.t
t.Helper()
if _, err := ioutil.ReadFile(n.stateFile); err != nil {
if _, err := os.ReadFile(n.stateFile); err != nil {
t.Fatalf("reading prefs: %v", err)
}
fs, err := store.NewFileStore(nil, n.stateFile)
@@ -768,7 +767,7 @@ func (op *nodeOutputParser) parseLines() {
if len(buf) == 0 {
op.buf.Reset()
} else {
io.CopyN(ioutil.Discard, &op.buf, int64(op.buf.Len()-len(buf)))
io.CopyN(io.Discard, &op.buf, int64(op.buf.Len()-len(buf)))
}
}

View File

@@ -14,7 +14,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
@@ -426,7 +425,7 @@ func (s *Server) CompleteAuth(authPathOrURL string) bool {
}
func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key.MachinePublic) {
msg, err := ioutil.ReadAll(io.LimitReader(r.Body, msgLimit))
msg, err := io.ReadAll(io.LimitReader(r.Body, msgLimit))
r.Body.Close()
if err != nil {
http.Error(w, fmt.Sprintf("bad map request read: %v", err), 400)
@@ -597,7 +596,7 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey key.Machi
defer s.incrInServeMap(-1)
ctx := r.Context()
msg, err := ioutil.ReadAll(io.LimitReader(r.Body, msgLimit))
msg, err := io.ReadAll(io.LimitReader(r.Body, msgLimit))
if err != nil {
r.Body.Close()
http.Error(w, fmt.Sprintf("bad map request read: %v", err), 400)

View File

@@ -13,7 +13,6 @@ import (
"expvar"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
_ "net/http/pprof"
@@ -81,7 +80,7 @@ func AllowDebugAccess(r *http.Request) bool {
urlKey := r.FormValue("debugkey")
keyPath := envknob.String("TS_DEBUG_KEY_PATH")
if urlKey != "" && keyPath != "" {
slurp, err := ioutil.ReadFile(keyPath)
slurp, err := os.ReadFile(keyPath)
if err == nil && string(bytes.TrimSpace(slurp)) == urlKey {
return true
}

View File

@@ -14,7 +14,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"strings"
"sync"
@@ -129,8 +128,6 @@ type limitData struct {
ele *list.Element // list element used to access this string in the cache
}
var disableRateLimit = envknob.String("TS_DEBUG_LOG_RATE") == "all"
// rateFree are format string substrings that are exempt from rate limiting.
// Things should not be added to this unless they're already limited otherwise
// or are critical for generating important stats from the logs.
@@ -156,7 +153,7 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
// timeNow is a function that returns the current time, used for calculating
// rate limits.
func RateLimitedFnWithClock(logf Logf, f time.Duration, burst int, maxCache int, timeNow func() time.Time) Logf {
if disableRateLimit {
if envknob.String("TS_DEBUG_LOG_RATE") == "all" {
return logf
}
var (
@@ -270,7 +267,7 @@ func (fn ArgWriter) Format(f fmt.State, _ rune) {
argBufioPool.Put(bw)
}
var argBufioPool = &sync.Pool{New: func() any { return bufio.NewWriterSize(ioutil.Discard, 1024) }}
var argBufioPool = &sync.Pool{New: func() any { return bufio.NewWriterSize(io.Discard, 1024) }}
// Filtered returns a Logf that silently swallows some log lines.
// Each inbound format and args is evaluated and printed to a string s.

34
version_test.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tailscaleroot
import (
"fmt"
"os"
"regexp"
"strings"
"testing"
)
func TestDockerfileVersion(t *testing.T) {
goMod, err := os.ReadFile("go.mod")
if err != nil {
t.Fatal(err)
}
m := regexp.MustCompile(`(?m)^go (\d\.\d+)\r?$`).FindStringSubmatch(string(goMod))
if m == nil {
t.Fatalf("didn't find go version in go.mod")
}
goVersion := m[1]
dockerFile, err := os.ReadFile("Dockerfile")
if err != nil {
t.Fatal(err)
}
wantSub := fmt.Sprintf("FROM golang:%s-alpine AS build-env", goVersion)
if !strings.Contains(string(dockerFile), wantSub) {
t.Errorf("didn't find %q in Dockerfile", wantSub)
}
}

View File

@@ -11,28 +11,33 @@ import (
"tailscale.com/envknob"
)
const linkDebug = true
// Various debugging and experimental tweakables, set by environment
// variable.
var (
// debugDisco prints verbose logs of active discovery events as
// they happen.
debugDisco = envknob.Bool("TS_DEBUG_DISCO")
debugDisco = envknob.RegisterBool("TS_DEBUG_DISCO")
// debugOmitLocalAddresses removes all local interface addresses
// from magicsock's discovered local endpoints. Used in some tests.
debugOmitLocalAddresses = envknob.Bool("TS_DEBUG_OMIT_LOCAL_ADDRS")
debugOmitLocalAddresses = envknob.RegisterBool("TS_DEBUG_OMIT_LOCAL_ADDRS")
// debugUseDerpRoute temporarily (2020-03-22) controls whether DERP
// reverse routing is enabled (Issue 150).
debugUseDerpRoute = envknob.OptBool("TS_DEBUG_ENABLE_DERP_ROUTE")
debugUseDerpRoute = envknob.RegisterOptBool("TS_DEBUG_ENABLE_DERP_ROUTE")
// logDerpVerbose logs all received DERP packets, including their
// full payload.
logDerpVerbose = envknob.Bool("TS_DEBUG_DERP")
logDerpVerbose = envknob.RegisterBool("TS_DEBUG_DERP")
// debugReSTUNStopOnIdle unconditionally enables the "shut down
// STUN if magicsock is idle" behavior that normally only triggers
// on mobile devices, lowers the shutdown interval, and logs more
// verbosely about idle measurements.
debugReSTUNStopOnIdle = envknob.Bool("TS_DEBUG_RESTUN_STOP_ON_IDLE")
debugReSTUNStopOnIdle = envknob.RegisterBool("TS_DEBUG_RESTUN_STOP_ON_IDLE")
// debugAlwaysDERP disables the use of UDP, forcing all peer communication over DERP.
debugAlwaysDERP = envknob.Bool("TS_DEBUG_ALWAYS_USE_DERP")
debugAlwaysDERP = envknob.RegisterBool("TS_DEBUG_ALWAYS_USE_DERP")
// debugEnableSilentDisco disables the use of heartbeatTimer on the endpoint struct
// and attempts to handle disco silently. See issue #540 for details.
debugEnableSilentDisco = envknob.RegisterBool("TS_DEBUG_ENABLE_SILENT_DISCO")
)
// inTest reports whether the running program is a test that set the

Some files were not shown because too many files have changed in this diff Show More