Update
This commit is contained in:
3
vendor/tailscale.com/.gitignore
generated
vendored
3
vendor/tailscale.com/.gitignore
generated
vendored
@@ -49,3 +49,6 @@ client/web/build/assets
|
||||
*.xcworkspacedata
|
||||
/tstest/tailmac/bin
|
||||
/tstest/tailmac/build
|
||||
|
||||
# Ignore personal IntelliJ settings
|
||||
.idea/
|
||||
|
||||
187
vendor/tailscale.com/.golangci.yml
generated
vendored
187
vendor/tailscale.com/.golangci.yml
generated
vendored
@@ -1,97 +1,110 @@
|
||||
version: "2"
|
||||
# Configuration for how we run golangci-lint
|
||||
# Timeout of 5m was the default in v1.
|
||||
run:
|
||||
timeout: 5m
|
||||
linters:
|
||||
# Don't enable any linters by default; just the ones that we explicitly
|
||||
# enable in the list below.
|
||||
disable-all: true
|
||||
default: none
|
||||
enable:
|
||||
- bidichk
|
||||
- gofmt
|
||||
- goimports
|
||||
- govet
|
||||
- misspell
|
||||
- revive
|
||||
|
||||
# Configuration for how we run golangci-lint
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
issues:
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||
exclude-rules:
|
||||
# These are forks of an upstream package and thus are exempt from stylistic
|
||||
# changes that would make pulling in upstream changes harder.
|
||||
- path: tempfork/.*\.go
|
||||
text: "File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`"
|
||||
- path: util/singleflight/.*\.go
|
||||
text: "File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`"
|
||||
|
||||
# Per-linter settings are contained in this top-level key
|
||||
linters-settings:
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: 'interface{}'
|
||||
replacement: 'any'
|
||||
|
||||
govet:
|
||||
settings:
|
||||
# Matches what we use in corp as of 2023-12-07
|
||||
enable:
|
||||
- asmdecl
|
||||
- assign
|
||||
- atomic
|
||||
- bools
|
||||
- buildtag
|
||||
- cgocall
|
||||
- copylocks
|
||||
- deepequalerrors
|
||||
- errorsas
|
||||
- framepointer
|
||||
- httpresponse
|
||||
- ifaceassert
|
||||
- loopclosure
|
||||
- lostcancel
|
||||
- nilfunc
|
||||
- nilness
|
||||
- printf
|
||||
- reflectvaluecompare
|
||||
- shift
|
||||
- sigchanyzer
|
||||
- sortslice
|
||||
- stdmethods
|
||||
- stringintconv
|
||||
- structtag
|
||||
- testinggoroutine
|
||||
- tests
|
||||
- unmarshal
|
||||
- unreachable
|
||||
- unsafeptr
|
||||
- unusedresult
|
||||
settings:
|
||||
printf:
|
||||
# List of print function names to check (in addition to default)
|
||||
funcs:
|
||||
- github.com/tailscale/tailscale/types/logger.Discard
|
||||
# NOTE(andrew-d): this doesn't currently work because the printf
|
||||
# analyzer doesn't support type declarations
|
||||
#- github.com/tailscale/tailscale/types/logger.Logf
|
||||
|
||||
revive:
|
||||
enable-all-rules: false
|
||||
ignore-generated-header: true
|
||||
govet:
|
||||
enable:
|
||||
- asmdecl
|
||||
- assign
|
||||
- atomic
|
||||
- bools
|
||||
- buildtag
|
||||
- cgocall
|
||||
- copylocks
|
||||
- deepequalerrors
|
||||
- errorsas
|
||||
- framepointer
|
||||
- httpresponse
|
||||
- ifaceassert
|
||||
- loopclosure
|
||||
- lostcancel
|
||||
- nilfunc
|
||||
- nilness
|
||||
- printf
|
||||
- reflectvaluecompare
|
||||
- shift
|
||||
- sigchanyzer
|
||||
- sortslice
|
||||
- stdmethods
|
||||
- stringintconv
|
||||
- structtag
|
||||
- testinggoroutine
|
||||
- tests
|
||||
- unmarshal
|
||||
- unreachable
|
||||
- unsafeptr
|
||||
- unusedresult
|
||||
settings:
|
||||
printf:
|
||||
# List of print function names to check (in addition to default)
|
||||
funcs:
|
||||
- github.com/tailscale/tailscale/types/logger.Discard
|
||||
# NOTE(andrew-d): this doesn't currently work because the printf
|
||||
# analyzer doesn't support type declarations
|
||||
#- github.com/tailscale/tailscale/types/logger.Logf
|
||||
revive:
|
||||
enable-all-rules: false
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: context-keys-type
|
||||
- name: defer
|
||||
arguments: [[
|
||||
# Calling 'recover' at the time a defer is registered (i.e. "defer recover()") has no effect.
|
||||
"immediate-recover",
|
||||
# Calling 'recover' outside of a deferred function has no effect
|
||||
"recover",
|
||||
# Returning values from a deferred function has no effect
|
||||
"return",
|
||||
]]
|
||||
- name: duplicated-imports
|
||||
- name: errorf
|
||||
- name: string-of-int
|
||||
- name: time-equal
|
||||
- name: unconditional-recursion
|
||||
- name: useless-break
|
||||
- name: waitgroup-by-value
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: context-keys-type
|
||||
- name: defer
|
||||
arguments: [[
|
||||
# Calling 'recover' at the time a defer is registered (i.e. "defer recover()") has no effect.
|
||||
"immediate-recover",
|
||||
# Calling 'recover' outside of a deferred function has no effect
|
||||
"recover",
|
||||
# Returning values from a deferred function has no effect
|
||||
"return",
|
||||
]]
|
||||
- name: duplicated-imports
|
||||
- name: errorf
|
||||
- name: string-of-int
|
||||
- name: time-equal
|
||||
- name: unconditional-recursion
|
||||
- name: useless-break
|
||||
- name: waitgroup-by-value
|
||||
# These are forks of an upstream package and thus are exempt from stylistic
|
||||
# changes that would make pulling in upstream changes harder.
|
||||
- path: tempfork/.*\.go
|
||||
text: File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`
|
||||
- path: util/singleflight/.*\.go
|
||||
text: File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
settings:
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: interface{}
|
||||
replacement: any
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
2
vendor/tailscale.com/ALPINE.txt
generated
vendored
2
vendor/tailscale.com/ALPINE.txt
generated
vendored
@@ -1 +1 @@
|
||||
3.19
|
||||
3.22
|
||||
160
vendor/tailscale.com/CODE_OF_CONDUCT.md
generated
vendored
160
vendor/tailscale.com/CODE_OF_CONDUCT.md
generated
vendored
@@ -1,135 +1,103 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
# Tailscale Community Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation
|
||||
in our community a harassment-free experience for everyone, regardless
|
||||
of age, body size, visible or invisible disability, ethnicity, sex
|
||||
characteristics, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance,
|
||||
race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open,
|
||||
welcoming, diverse, inclusive, and healthy community.
|
||||
We are committed to creating an open, welcoming, diverse, inclusive, healthy and respectful community.
|
||||
Unacceptable, harmful and inappropriate behavior will not be tolerated.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for
|
||||
our community include:
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our
|
||||
mistakes, and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
- Demonstrating empathy and kindness toward other people.
|
||||
- Being respectful of differing opinions, viewpoints, and experiences.
|
||||
- Giving and gracefully accepting constructive feedback.
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience.
|
||||
- Focusing on what is best not just for us as individuals, but for the overall community.
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
Examples of unacceptable behavior include without limitation:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or
|
||||
political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in
|
||||
a professional setting
|
||||
- The use of language, imagery or emojis (collectively "content") that is racist, sexist, homophobic, transphobic, or otherwise harassing or discriminatory based on any protected characteristic.
|
||||
- The use of sexualized content and sexual attention or advances of any kind.
|
||||
- The use of violent, intimidating or bullying content.
|
||||
- Trolling, concern trolling, insulting or derogatory comments, and personal or political attacks.
|
||||
- Public or private harassment.
|
||||
- Publishing others' personal information, such as a photo, physical address, email address, online profile information, or other personal information, without their explicit permission or with the intent to bully or harass the other person.
|
||||
- Posting deep fake or other AI generated content about or involving another person without the explicit permission.
|
||||
- Spamming community channels and members, such as sending repeat messages, low-effort content, or automated messages.
|
||||
- Phishing or any similar activity.
|
||||
- Distributing or promoting malware.
|
||||
- The use of any coded or suggestive content to hide or provoke otherwise unacceptable behavior.
|
||||
- Other conduct which could reasonably be considered harmful, illegal, or inappropriate in a professional setting.
|
||||
|
||||
## Enforcement Responsibilities
|
||||
Please also see the Tailscale Acceptable Use Policy, available at [tailscale.com/tailscale-aup](https://tailscale.com/tailscale-aup).
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our
|
||||
standards of acceptable behavior and will take appropriate and fair
|
||||
corrective action in response to any behavior that they deem
|
||||
inappropriate, threatening, offensive, or harmful.
|
||||
## Reporting Incidents
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit,
|
||||
or reject comments, commits, code, wiki edits, issues, and other
|
||||
contributions that are not aligned to this Code of Conduct, and will
|
||||
communicate reasons for moderation decisions when appropriate.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to Tailscale directly via <info@tailscale.com>, or to the community leaders or moderators via DM or similar.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
We will respect the privacy and safety of the reporter of any issues.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also
|
||||
applies when an individual is officially representing the community in
|
||||
public spaces. Examples of representing our community include using an
|
||||
official e-mail address, posting via an official social media account,
|
||||
or acting as an appointed representative at an online or offline
|
||||
event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||
may be reported to the community leaders responsible for enforcement
|
||||
at [info@tailscale.com](mailto:info@tailscale.com). All complaints
|
||||
will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and
|
||||
security of the reporter of any incident.
|
||||
Please note that this community is not moderated by staff 24/7, and we do not have, and do not undertake, any obligation to prescreen, monitor, edit, or remove any content or data, or to actively seek facts or circumstances indicating illegal activity.
|
||||
While we strive to keep the community safe and welcoming, moderation may not be immediate at all hours.
|
||||
If you encounter any issues, report them using the appropriate channels.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in
|
||||
determining the consequences for any action they deem in violation of
|
||||
this Code of Conduct:
|
||||
Community leaders and moderators are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders and moderators have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Community Code of Conduct.
|
||||
Tailscale retains full discretion to take action (or not) in response to a violation of these guidelines with or without notice or liability to you.
|
||||
We will interpret our policies and resolve disputes in favor of protecting users, customers, the public, our community and our company, as a whole.
|
||||
|
||||
Community leaders will follow these community enforcement guidelines in determining the consequences for any action they deem in violation of this Code of Conduct,
|
||||
and retain full discretion to apply the enforcement guidelines as necessary depending on the circumstances:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior
|
||||
deemed unprofessional or unwelcome in the community.
|
||||
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders,
|
||||
providing clarity around the nature of the violation and an
|
||||
explanation of why the behavior was inappropriate. A public apology
|
||||
may be requested.
|
||||
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate.
|
||||
A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
Community Impact: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued
|
||||
behavior. No interaction with the people involved, including
|
||||
unsolicited interaction with those enforcing the Code of Conduct, for
|
||||
a specified period of time. This includes avoiding interactions in
|
||||
community spaces as well as external channels like social
|
||||
media. Violating these terms may lead to a temporary or permanent ban.
|
||||
Consequence: A warning with consequences for continued behavior.
|
||||
No interaction with the people involved, including unsolicited interaction with those enforcing this Community Code of Conduct, for a specified period of time.
|
||||
This includes avoiding interactions in community spaces as well as external channels like social media.
|
||||
Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards,
|
||||
including sustained inappropriate behavior.
|
||||
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or
|
||||
public communication with the community for a specified period of
|
||||
time. No public or private interaction with the people involved,
|
||||
including unsolicited interaction with those enforcing the Code of
|
||||
Conduct, is allowed during this period. Violating these terms may lead
|
||||
to a permanent ban.
|
||||
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time.
|
||||
No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of
|
||||
community standards, including sustained inappropriate behavior,
|
||||
harassment of an individual, or aggression toward or disparagement of
|
||||
classes of individuals.
|
||||
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction
|
||||
within the community.
|
||||
Consequence: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Acceptable Use Policy
|
||||
|
||||
Violation of this Community Code of Conduct may also violate the Tailscale Acceptable Use Policy, which may result in suspension or termination of your Tailscale account.
|
||||
For more information, please see the Tailscale Acceptable Use Policy, available at [tailscale.com/tailscale-aup](https://tailscale.com/tailscale-aup).
|
||||
|
||||
## Privacy
|
||||
|
||||
Please see the Tailscale [Privacy Policy](https://tailscale.com/privacy-policy) for more information about how Tailscale collects, uses, discloses and protects information.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor
|
||||
Covenant][homepage], version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at <https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of
|
||||
conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the
|
||||
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
||||
available at https://www.contributor-covenant.org/translations.
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at <https://www.contributor-covenant.org/faq>.
|
||||
Translations are available at <https://www.contributor-covenant.org/translations>.
|
||||
|
||||
22
vendor/tailscale.com/Dockerfile
generated
vendored
22
vendor/tailscale.com/Dockerfile
generated
vendored
@@ -7,6 +7,15 @@
|
||||
# Tailscale images are currently built using https://github.com/tailscale/mkctr,
|
||||
# and the build script can be found in ./build_docker.sh.
|
||||
#
|
||||
# If you want to build local images for testing, you can use make.
|
||||
#
|
||||
# To build a Tailscale image and push to the local docker registry:
|
||||
#
|
||||
# $ REPO=local/tailscale TAGS=v0.0.1 PLATFORM=local make publishdevimage
|
||||
#
|
||||
# To build a Tailscale image and push to a remote docker registry:
|
||||
#
|
||||
# $ REPO=<your-registry>/<your-repo>/tailscale TAGS=v0.0.1 make publishdevimage
|
||||
#
|
||||
# This Dockerfile includes all the tailscale binaries.
|
||||
#
|
||||
@@ -27,7 +36,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.24-alpine AS build-env
|
||||
FROM golang:1.25-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
@@ -62,10 +71,15 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
|
||||
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
|
||||
|
||||
FROM alpine:3.19
|
||||
FROM alpine:3.22
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
RUN rm /sbin/iptables && ln -s /sbin/iptables-legacy /sbin/iptables
|
||||
RUN rm /sbin/ip6tables && ln -s /sbin/ip6tables-legacy /sbin/ip6tables
|
||||
# Alpine 3.19 replaced legacy iptables with nftables based implementation.
|
||||
# Tailscale is used on some hosts that don't support nftables, such as Synology
|
||||
# NAS, so link iptables back to legacy version. Hosts that don't require legacy
|
||||
# iptables should be able to use Tailscale in nftables mode. See
|
||||
# https://github.com/tailscale/tailscale/issues/17854
|
||||
RUN rm /usr/sbin/iptables && ln -s /usr/sbin/iptables-legacy /usr/sbin/iptables
|
||||
RUN rm /usr/sbin/ip6tables && ln -s /usr/sbin/ip6tables-legacy /usr/sbin/ip6tables
|
||||
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
# For compat with the previous run.sh, although ideally you should be
|
||||
|
||||
16
vendor/tailscale.com/Dockerfile.base
generated
vendored
16
vendor/tailscale.com/Dockerfile.base
generated
vendored
@@ -1,12 +1,12 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
FROM alpine:3.19
|
||||
FROM alpine:3.22
|
||||
RUN apk add --no-cache ca-certificates iptables iptables-legacy iproute2 ip6tables iputils
|
||||
# Alpine 3.19 replaces legacy iptables with nftables based implementation. We
|
||||
# can't be certain that all hosts that run Tailscale containers currently
|
||||
# suppport nftables, so link back to legacy for backwards compatibility reasons.
|
||||
# TODO(irbekrm): add some way how to determine if we still run on nodes that
|
||||
# don't support nftables, so that we can eventually remove these symlinks.
|
||||
RUN rm /sbin/iptables && ln -s /sbin/iptables-legacy /sbin/iptables
|
||||
RUN rm /sbin/ip6tables && ln -s /sbin/ip6tables-legacy /sbin/ip6tables
|
||||
# Alpine 3.19 replaced legacy iptables with nftables based implementation.
|
||||
# Tailscale is used on some hosts that don't support nftables, such as Synology
|
||||
# NAS, so link iptables back to legacy version. Hosts that don't require legacy
|
||||
# iptables should be able to use Tailscale in nftables mode. See
|
||||
# https://github.com/tailscale/tailscale/issues/17854
|
||||
RUN rm /usr/sbin/iptables && ln -s /usr/sbin/iptables-legacy /usr/sbin/iptables
|
||||
RUN rm /usr/sbin/ip6tables && ln -s /usr/sbin/ip6tables-legacy /usr/sbin/ip6tables
|
||||
|
||||
89
vendor/tailscale.com/Makefile
generated
vendored
89
vendor/tailscale.com/Makefile
generated
vendored
@@ -8,8 +8,9 @@ PLATFORM ?= "flyio" ## flyio==linux/amd64. Set to "" to build all platforms.
|
||||
vet: ## Run go vet
|
||||
./tool/go vet ./...
|
||||
|
||||
tidy: ## Run go mod tidy
|
||||
tidy: ## Run go mod tidy and update nix flake hashes
|
||||
./tool/go mod tidy
|
||||
./update-flake.sh
|
||||
|
||||
lint: ## Run golangci-lint
|
||||
./tool/go run github.com/golangci/golangci-lint/cmd/golangci-lint run
|
||||
@@ -17,22 +18,36 @@ lint: ## Run golangci-lint
|
||||
updatedeps: ## Update depaware deps
|
||||
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
|
||||
# it finds in its $$PATH is the right one.
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --internal \
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --vendor --internal \
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper \
|
||||
tailscale.com/cmd/k8s-operator \
|
||||
tailscale.com/cmd/stund
|
||||
tailscale.com/cmd/stund \
|
||||
tailscale.com/cmd/tsidp
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --goos=linux,darwin,windows,android,ios --vendor --internal \
|
||||
tailscale.com/tsnet
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --file=depaware-minbox.txt --goos=linux --tags="$$(./tool/go run ./cmd/featuretags --min --add=cli)" --vendor --internal \
|
||||
tailscale.com/cmd/tailscaled
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --file=depaware-min.txt --goos=linux --tags="$$(./tool/go run ./cmd/featuretags --min)" --vendor --internal \
|
||||
tailscale.com/cmd/tailscaled
|
||||
|
||||
depaware: ## Run depaware checks
|
||||
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
|
||||
# it finds in its $$PATH is the right one.
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --internal \
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --vendor --internal \
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper \
|
||||
tailscale.com/cmd/k8s-operator \
|
||||
tailscale.com/cmd/stund
|
||||
tailscale.com/cmd/stund \
|
||||
tailscale.com/cmd/tsidp
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --goos=linux,darwin,windows,android,ios --vendor --internal \
|
||||
tailscale.com/tsnet
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --file=depaware-minbox.txt --goos=linux --tags="$$(./tool/go run ./cmd/featuretags --min --add=cli)" --vendor --internal \
|
||||
tailscale.com/cmd/tailscaled
|
||||
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --file=depaware-min.txt --goos=linux --tags="$$(./tool/go run ./cmd/featuretags --min)" --vendor --internal \
|
||||
tailscale.com/cmd/tailscaled
|
||||
|
||||
buildwindows: ## Build tailscale CLI for windows/amd64
|
||||
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
@@ -58,7 +73,7 @@ buildmultiarchimage: ## Build (and optionally push) multiarch docker image
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm ## Perform basic checks and compilation tests
|
||||
|
||||
staticcheck: ## Run staticcheck.io checks
|
||||
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
|
||||
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go run ./tool/listpkgs --ignore-3p ./...)
|
||||
|
||||
kube-generate-all: kube-generate-deepcopy ## Refresh generated files for Tailscale Kubernetes Operator
|
||||
./tool/go generate ./cmd/k8s-operator
|
||||
@@ -86,42 +101,60 @@ pushspk: spk ## Push and install synology package on ${SYNO_HOST} host
|
||||
scp tailscale.spk root@${SYNO_HOST}:
|
||||
ssh root@${SYNO_HOST} /usr/syno/bin/synopkg install tailscale.spk
|
||||
|
||||
publishdevimage: ## Build and publish tailscale image to location specified by ${REPO}
|
||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
||||
.PHONY: check-image-repo
|
||||
check-image-repo:
|
||||
@if [ -z "$(REPO)" ]; then \
|
||||
echo "REPO=... required; e.g. REPO=ghcr.io/$$USER/tailscale" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
@for repo in tailscale/tailscale ghcr.io/tailscale/tailscale \
|
||||
tailscale/k8s-operator ghcr.io/tailscale/k8s-operator \
|
||||
tailscale/k8s-nameserver ghcr.io/tailscale/k8s-nameserver \
|
||||
tailscale/tsidp ghcr.io/tailscale/tsidp \
|
||||
tailscale/k8s-proxy ghcr.io/tailscale/k8s-proxy; do \
|
||||
if [ "$(REPO)" = "$$repo" ]; then \
|
||||
echo "REPO=... must not be $$repo" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
publishdevimage: check-image-repo ## Build and publish tailscale image to location specified by ${REPO}
|
||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=client ./build_docker.sh
|
||||
|
||||
publishdevoperator: ## Build and publish k8s-operator image to location specified by ${REPO}
|
||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
||||
publishdevoperator: check-image-repo ## Build and publish k8s-operator image to location specified by ${REPO}
|
||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-operator ./build_docker.sh
|
||||
|
||||
publishdevnameserver: ## Build and publish k8s-nameserver image to location specified by ${REPO}
|
||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/k8s-nameserver" || (echo "REPO=... must not be tailscale/k8s-nameserver" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-nameserver" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-nameserver" && exit 1)
|
||||
publishdevnameserver: check-image-repo ## Build and publish k8s-nameserver image to location specified by ${REPO}
|
||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-nameserver ./build_docker.sh
|
||||
|
||||
publishdevtsidp: check-image-repo ## Build and publish tsidp image to location specified by ${REPO}
|
||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=tsidp ./build_docker.sh
|
||||
|
||||
publishdevproxy: check-image-repo ## Build and publish k8s-proxy image to location specified by ${REPO}
|
||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-proxy ./build_docker.sh
|
||||
|
||||
.PHONY: sshintegrationtest
|
||||
sshintegrationtest: ## Run the SSH integration tests in various Docker containers
|
||||
@GOOS=linux GOARCH=amd64 ./tool/go test -tags integrationtest -c ./ssh/tailssh -o ssh/tailssh/testcontainers/tailssh.test && \
|
||||
GOOS=linux GOARCH=amd64 ./tool/go build -o ssh/tailssh/testcontainers/tailscaled ./cmd/tailscaled && \
|
||||
@GOOS=linux GOARCH=amd64 CGO_ENABLED=0 ./tool/go test -tags integrationtest -c ./ssh/tailssh -o ssh/tailssh/testcontainers/tailssh.test && \
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 ./tool/go build -o ssh/tailssh/testcontainers/tailscaled ./cmd/tailscaled && \
|
||||
echo "Testing on ubuntu:focal" && docker build --build-arg="BASE=ubuntu:focal" -t ssh-ubuntu-focal ssh/tailssh/testcontainers && \
|
||||
echo "Testing on ubuntu:jammy" && docker build --build-arg="BASE=ubuntu:jammy" -t ssh-ubuntu-jammy ssh/tailssh/testcontainers && \
|
||||
echo "Testing on ubuntu:noble" && docker build --build-arg="BASE=ubuntu:noble" -t ssh-ubuntu-noble ssh/tailssh/testcontainers && \
|
||||
echo "Testing on alpine:latest" && docker build --build-arg="BASE=alpine:latest" -t ssh-alpine-latest ssh/tailssh/testcontainers
|
||||
|
||||
.PHONY: generate
|
||||
generate: ## Generate code
|
||||
./tool/go generate ./...
|
||||
|
||||
.PHONY: pin-github-actions
|
||||
pin-github-actions:
|
||||
./tool/go tool github.com/stacklok/frizbee actions .github/workflows
|
||||
|
||||
help: ## Show this help
|
||||
@echo "\nSpecify a command. The choices are:\n"
|
||||
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
@echo "Specify a command. The choices are:"
|
||||
@echo ""
|
||||
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
.PHONY: help
|
||||
|
||||
|
||||
5
vendor/tailscale.com/README.md
generated
vendored
5
vendor/tailscale.com/README.md
generated
vendored
@@ -37,7 +37,7 @@ not open source.
|
||||
|
||||
## Building
|
||||
|
||||
We always require the latest Go release, currently Go 1.23. (While we build
|
||||
We always require the latest Go release, currently Go 1.25. (While we build
|
||||
releases with our [Go fork](https://github.com/tailscale/go/), its use is not
|
||||
required.)
|
||||
|
||||
@@ -71,8 +71,7 @@ We require [Developer Certificate of
|
||||
Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
`Signed-off-by` lines in commits.
|
||||
|
||||
See `git log` for our commit message style. It's basically the same as
|
||||
[Go's style](https://go.dev/wiki/CommitMessage).
|
||||
See [commit-messages.md](docs/commit-messages.md) (or skim `git log`) for our commit message style.
|
||||
|
||||
## About Us
|
||||
|
||||
|
||||
2
vendor/tailscale.com/VERSION.txt
generated
vendored
2
vendor/tailscale.com/VERSION.txt
generated
vendored
@@ -1 +1 @@
|
||||
1.82.0
|
||||
1.94.1
|
||||
|
||||
329
vendor/tailscale.com/appc/appconnector.go
generated
vendored
329
vendor/tailscale.com/appc/appconnector.go
generated
vendored
@@ -12,19 +12,20 @@ package appc
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/execqueue"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
@@ -115,19 +116,6 @@ func metricStoreRoutes(rate, nRoutes int64) {
|
||||
recordMetric(nRoutes, metricStoreRoutesNBuckets, metricStoreRoutesN)
|
||||
}
|
||||
|
||||
// RouteInfo is a data structure used to persist the in memory state of an AppConnector
|
||||
// so that we can know, even after a restart, which routes came from ACLs and which were
|
||||
// learned from domains.
|
||||
type RouteInfo struct {
|
||||
// Control is the routes from the 'routes' section of an app connector acl.
|
||||
Control []netip.Prefix `json:",omitempty"`
|
||||
// Domains are the routes discovered by observing DNS lookups for configured domains.
|
||||
Domains map[string][]netip.Addr `json:",omitempty"`
|
||||
// Wildcards are the configured DNS lookup domains to observe. When a DNS query matches Wildcards,
|
||||
// its result is added to Domains.
|
||||
Wildcards []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// AppConnector is an implementation of an AppConnector that performs
|
||||
// its function as a subsystem inside of a tailscale node. At the control plane
|
||||
// side App Connector routing is configured in terms of domains rather than IP
|
||||
@@ -138,14 +126,20 @@ type RouteInfo struct {
|
||||
// routes not yet served by the AppConnector the local node configuration is
|
||||
// updated to advertise the new route.
|
||||
type AppConnector struct {
|
||||
// These fields are immutable after initialization.
|
||||
logf logger.Logf
|
||||
eventBus *eventbus.Bus
|
||||
routeAdvertiser RouteAdvertiser
|
||||
pubClient *eventbus.Client
|
||||
updatePub *eventbus.Publisher[appctype.RouteUpdate]
|
||||
storePub *eventbus.Publisher[appctype.RouteInfo]
|
||||
|
||||
// storeRoutesFunc will be called to persist routes if it is not nil.
|
||||
storeRoutesFunc func(*RouteInfo) error
|
||||
// hasStoredRoutes records whether the connector was initialized with
|
||||
// persisted route information.
|
||||
hasStoredRoutes bool
|
||||
|
||||
// mu guards the fields that follow
|
||||
mu sync.Mutex
|
||||
mu syncs.Mutex
|
||||
|
||||
// domains is a map of lower case domain names with no trailing dot, to an
|
||||
// ordered list of resolved IP addresses.
|
||||
@@ -164,53 +158,83 @@ type AppConnector struct {
|
||||
writeRateDay *rateLogger
|
||||
}
|
||||
|
||||
// Config carries the settings for an [AppConnector].
|
||||
type Config struct {
|
||||
// Logf is the logger to which debug logs from the connector will be sent.
|
||||
// It must be non-nil.
|
||||
Logf logger.Logf
|
||||
|
||||
// EventBus receives events when the collection of routes maintained by the
|
||||
// connector is updated. It must be non-nil.
|
||||
EventBus *eventbus.Bus
|
||||
|
||||
// RouteAdvertiser allows the connector to update the set of advertised routes.
|
||||
RouteAdvertiser RouteAdvertiser
|
||||
|
||||
// RouteInfo, if non-nil, use used as the initial set of routes for the
|
||||
// connector. If nil, the connector starts empty.
|
||||
RouteInfo *appctype.RouteInfo
|
||||
|
||||
// HasStoredRoutes indicates that the connector should assume stored routes.
|
||||
HasStoredRoutes bool
|
||||
}
|
||||
|
||||
// NewAppConnector creates a new AppConnector.
|
||||
func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser, routeInfo *RouteInfo, storeRoutesFunc func(*RouteInfo) error) *AppConnector {
|
||||
func NewAppConnector(c Config) *AppConnector {
|
||||
switch {
|
||||
case c.Logf == nil:
|
||||
panic("missing logger")
|
||||
case c.EventBus == nil:
|
||||
panic("missing event bus")
|
||||
}
|
||||
ec := c.EventBus.Client("appc.AppConnector")
|
||||
|
||||
ac := &AppConnector{
|
||||
logf: logger.WithPrefix(logf, "appc: "),
|
||||
routeAdvertiser: routeAdvertiser,
|
||||
storeRoutesFunc: storeRoutesFunc,
|
||||
logf: logger.WithPrefix(c.Logf, "appc: "),
|
||||
eventBus: c.EventBus,
|
||||
pubClient: ec,
|
||||
updatePub: eventbus.Publish[appctype.RouteUpdate](ec),
|
||||
storePub: eventbus.Publish[appctype.RouteInfo](ec),
|
||||
routeAdvertiser: c.RouteAdvertiser,
|
||||
hasStoredRoutes: c.HasStoredRoutes,
|
||||
}
|
||||
if routeInfo != nil {
|
||||
ac.domains = routeInfo.Domains
|
||||
ac.wildcards = routeInfo.Wildcards
|
||||
ac.controlRoutes = routeInfo.Control
|
||||
if c.RouteInfo != nil {
|
||||
ac.domains = c.RouteInfo.Domains
|
||||
ac.wildcards = c.RouteInfo.Wildcards
|
||||
ac.controlRoutes = c.RouteInfo.Control
|
||||
}
|
||||
ac.writeRateMinute = newRateLogger(time.Now, time.Minute, func(c int64, s time.Time, l int64) {
|
||||
ac.logf("routeInfo write rate: %d in minute starting at %v (%d routes)", c, s, l)
|
||||
metricStoreRoutes(c, l)
|
||||
ac.writeRateMinute = newRateLogger(time.Now, time.Minute, func(c int64, s time.Time, ln int64) {
|
||||
ac.logf("routeInfo write rate: %d in minute starting at %v (%d routes)", c, s, ln)
|
||||
metricStoreRoutes(c, ln)
|
||||
})
|
||||
ac.writeRateDay = newRateLogger(time.Now, 24*time.Hour, func(c int64, s time.Time, l int64) {
|
||||
ac.logf("routeInfo write rate: %d in 24 hours starting at %v (%d routes)", c, s, l)
|
||||
ac.writeRateDay = newRateLogger(time.Now, 24*time.Hour, func(c int64, s time.Time, ln int64) {
|
||||
ac.logf("routeInfo write rate: %d in 24 hours starting at %v (%d routes)", c, s, ln)
|
||||
})
|
||||
return ac
|
||||
}
|
||||
|
||||
// ShouldStoreRoutes returns true if the appconnector was created with the controlknob on
|
||||
// and is storing its discovered routes persistently.
|
||||
func (e *AppConnector) ShouldStoreRoutes() bool {
|
||||
return e.storeRoutesFunc != nil
|
||||
}
|
||||
func (e *AppConnector) ShouldStoreRoutes() bool { return e.hasStoredRoutes }
|
||||
|
||||
// storeRoutesLocked takes the current state of the AppConnector and persists it
|
||||
func (e *AppConnector) storeRoutesLocked() error {
|
||||
if !e.ShouldStoreRoutes() {
|
||||
return nil
|
||||
}
|
||||
func (e *AppConnector) storeRoutesLocked() {
|
||||
if e.storePub.ShouldPublish() {
|
||||
// log write rate and write size
|
||||
numRoutes := int64(len(e.controlRoutes))
|
||||
for _, rs := range e.domains {
|
||||
numRoutes += int64(len(rs))
|
||||
}
|
||||
e.writeRateMinute.update(numRoutes)
|
||||
e.writeRateDay.update(numRoutes)
|
||||
|
||||
// log write rate and write size
|
||||
numRoutes := int64(len(e.controlRoutes))
|
||||
for _, rs := range e.domains {
|
||||
numRoutes += int64(len(rs))
|
||||
e.storePub.Publish(appctype.RouteInfo{
|
||||
// Clone here, as the subscriber will handle these outside our lock.
|
||||
Control: slices.Clone(e.controlRoutes),
|
||||
Domains: maps.Clone(e.domains),
|
||||
Wildcards: slices.Clone(e.wildcards),
|
||||
})
|
||||
}
|
||||
e.writeRateMinute.update(numRoutes)
|
||||
e.writeRateDay.update(numRoutes)
|
||||
|
||||
return e.storeRoutesFunc(&RouteInfo{
|
||||
Control: e.controlRoutes,
|
||||
Domains: e.domains,
|
||||
Wildcards: e.wildcards,
|
||||
})
|
||||
}
|
||||
|
||||
// ClearRoutes removes all route state from the AppConnector.
|
||||
@@ -220,7 +244,8 @@ func (e *AppConnector) ClearRoutes() error {
|
||||
e.controlRoutes = nil
|
||||
e.domains = nil
|
||||
e.wildcards = nil
|
||||
return e.storeRoutesLocked()
|
||||
e.storeRoutesLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDomainsAndRoutes starts an asynchronous update of the configuration
|
||||
@@ -249,6 +274,18 @@ func (e *AppConnector) Wait(ctx context.Context) {
|
||||
e.queue.Wait(ctx)
|
||||
}
|
||||
|
||||
// Close closes the connector and cleans up resources associated with it.
|
||||
// It is safe (and a noop) to call Close on nil.
|
||||
func (e *AppConnector) Close() {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.queue.Shutdown() // TODO(creachadair): Should we wait for it too?
|
||||
e.pubClient.Close()
|
||||
}
|
||||
|
||||
func (e *AppConnector) updateDomains(domains []string) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -280,20 +317,26 @@ func (e *AppConnector) updateDomains(domains []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Everything left in oldDomains is a domain we're no longer tracking
|
||||
// and if we are storing route info we can unadvertise the routes
|
||||
if e.ShouldStoreRoutes() {
|
||||
// Everything left in oldDomains is a domain we're no longer tracking and we
|
||||
// can unadvertise the routes.
|
||||
if e.hasStoredRoutes {
|
||||
toRemove := []netip.Prefix{}
|
||||
for _, addrs := range oldDomains {
|
||||
for _, a := range addrs {
|
||||
toRemove = append(toRemove, netip.PrefixFrom(a, a.BitLen()))
|
||||
}
|
||||
}
|
||||
e.queue.Add(func() {
|
||||
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
|
||||
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
|
||||
|
||||
if len(toRemove) != 0 {
|
||||
if ra := e.routeAdvertiser; ra != nil {
|
||||
e.queue.Add(func() {
|
||||
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
|
||||
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
e.updatePub.Publish(appctype.RouteUpdate{Unadvertise: toRemove})
|
||||
}
|
||||
}
|
||||
|
||||
e.logf("handling domains: %v and wildcards: %v", slicesx.MapKeys(e.domains), e.wildcards)
|
||||
@@ -314,11 +357,10 @@ func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
|
||||
|
||||
var toRemove []netip.Prefix
|
||||
|
||||
// If we're storing routes and know e.controlRoutes is a good
|
||||
// representation of what should be in AdvertisedRoutes we can stop
|
||||
// advertising routes that used to be in e.controlRoutes but are not
|
||||
// in routes.
|
||||
if e.ShouldStoreRoutes() {
|
||||
// If we know e.controlRoutes is a good representation of what should be in
|
||||
// AdvertisedRoutes we can stop advertising routes that used to be in
|
||||
// e.controlRoutes but are not in routes.
|
||||
if e.hasStoredRoutes {
|
||||
toRemove = routesWithout(e.controlRoutes, routes)
|
||||
}
|
||||
|
||||
@@ -335,19 +377,23 @@ nextRoute:
|
||||
}
|
||||
}
|
||||
|
||||
e.queue.Add(func() {
|
||||
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
|
||||
e.logf("failed to advertise routes: %v: %v", routes, err)
|
||||
}
|
||||
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
|
||||
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
|
||||
}
|
||||
if e.routeAdvertiser != nil {
|
||||
e.queue.Add(func() {
|
||||
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
|
||||
e.logf("failed to advertise routes: %v: %v", routes, err)
|
||||
}
|
||||
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
|
||||
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
e.updatePub.Publish(appctype.RouteUpdate{
|
||||
Advertise: routes,
|
||||
Unadvertise: toRemove,
|
||||
})
|
||||
|
||||
e.controlRoutes = routes
|
||||
if err := e.storeRoutesLocked(); err != nil {
|
||||
e.logf("failed to store route info: %v", err)
|
||||
}
|
||||
e.storeRoutesLocked()
|
||||
}
|
||||
|
||||
// Domains returns the currently configured domain list.
|
||||
@@ -372,124 +418,6 @@ func (e *AppConnector) DomainRoutes() map[string][]netip.Addr {
|
||||
return drCopy
|
||||
}
|
||||
|
||||
// ObserveDNSResponse is a callback invoked by the DNS resolver when a DNS
|
||||
// response is being returned over the PeerAPI. The response is parsed and
|
||||
// matched against the configured domains, if matched the routeAdvertiser is
|
||||
// advised to advertise the discovered route.
|
||||
func (e *AppConnector) ObserveDNSResponse(res []byte) error {
|
||||
var p dnsmessage.Parser
|
||||
if _, err := p.Start(res); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.SkipAllQuestions(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// cnameChain tracks a chain of CNAMEs for a given query in order to reverse
|
||||
// a CNAME chain back to the original query for flattening. The keys are
|
||||
// CNAME record targets, and the value is the name the record answers, so
|
||||
// for www.example.com CNAME example.com, the map would contain
|
||||
// ["example.com"] = "www.example.com".
|
||||
var cnameChain map[string]string
|
||||
|
||||
// addressRecords is a list of address records found in the response.
|
||||
var addressRecords map[string][]netip.Addr
|
||||
|
||||
for {
|
||||
h, err := p.AnswerHeader()
|
||||
if err == dnsmessage.ErrSectionDone {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.Class != dnsmessage.ClassINET {
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeCNAME, dnsmessage.TypeA, dnsmessage.TypeAAAA:
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
domain := strings.TrimSuffix(strings.ToLower(h.Name.String()), ".")
|
||||
if len(domain) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if h.Type == dnsmessage.TypeCNAME {
|
||||
res, err := p.CNAMEResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cname := strings.TrimSuffix(strings.ToLower(res.CNAME.String()), ".")
|
||||
if len(cname) == 0 {
|
||||
continue
|
||||
}
|
||||
mak.Set(&cnameChain, cname, domain)
|
||||
continue
|
||||
}
|
||||
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeA:
|
||||
r, err := p.AResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr := netip.AddrFrom4(r.A)
|
||||
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
|
||||
case dnsmessage.TypeAAAA:
|
||||
r, err := p.AAAAResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr := netip.AddrFrom16(r.AAAA)
|
||||
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
for domain, addrs := range addressRecords {
|
||||
domain, isRouted := e.findRoutedDomainLocked(domain, cnameChain)
|
||||
|
||||
// domain and none of the CNAMEs in the chain are routed
|
||||
if !isRouted {
|
||||
continue
|
||||
}
|
||||
|
||||
// advertise each address we have learned for the routed domain, that
|
||||
// was not already known.
|
||||
var toAdvertise []netip.Prefix
|
||||
for _, addr := range addrs {
|
||||
if !e.isAddrKnownLocked(domain, addr) {
|
||||
toAdvertise = append(toAdvertise, netip.PrefixFrom(addr, addr.BitLen()))
|
||||
}
|
||||
}
|
||||
|
||||
if len(toAdvertise) > 0 {
|
||||
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
|
||||
e.scheduleAdvertisement(domain, toAdvertise...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// starting from the given domain that resolved to an address, find it, or any
|
||||
// of the domains in the CNAME chain toward resolving it, that are routed
|
||||
// domains, returning the routed domain name and a bool indicating whether a
|
||||
@@ -544,10 +472,13 @@ func (e *AppConnector) isAddrKnownLocked(domain string, addr netip.Addr) bool {
|
||||
// associated with the given domain.
|
||||
func (e *AppConnector) scheduleAdvertisement(domain string, routes ...netip.Prefix) {
|
||||
e.queue.Add(func() {
|
||||
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
|
||||
e.logf("failed to advertise routes for %s: %v: %v", domain, routes, err)
|
||||
return
|
||||
if e.routeAdvertiser != nil {
|
||||
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
|
||||
e.logf("failed to advertise routes for %s: %v: %v", domain, routes, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
e.updatePub.Publish(appctype.RouteUpdate{Advertise: routes})
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
@@ -561,9 +492,7 @@ func (e *AppConnector) scheduleAdvertisement(domain string, routes ...netip.Pref
|
||||
e.logf("[v2] advertised route for %v: %v", domain, addr)
|
||||
}
|
||||
}
|
||||
if err := e.storeRoutesLocked(); err != nil {
|
||||
e.logf("failed to store route info: %v", err)
|
||||
}
|
||||
e.storeRoutesLocked()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -581,8 +510,8 @@ func (e *AppConnector) addDomainAddrLocked(domain string, addr netip.Addr) {
|
||||
slices.SortFunc(e.domains[domain], compareAddr)
|
||||
}
|
||||
|
||||
func compareAddr(l, r netip.Addr) int {
|
||||
return l.Compare(r)
|
||||
func compareAddr(a, b netip.Addr) int {
|
||||
return a.Compare(b)
|
||||
}
|
||||
|
||||
// routesWithout returns a without b where a and b
|
||||
|
||||
110
vendor/tailscale.com/appc/conn25.go
generated
vendored
Normal file
110
vendor/tailscale.com/appc/conn25.go
generated
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package appc
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// Conn25 holds the developing state for the as yet nascent next generation app connector.
|
||||
// There is currently (2025-12-08) no actual app connecting functionality.
|
||||
type Conn25 struct {
|
||||
mu sync.Mutex
|
||||
transitIPs map[tailcfg.NodeID]map[netip.Addr]netip.Addr
|
||||
}
|
||||
|
||||
const dupeTransitIPMessage = "Duplicate transit address in ConnectorTransitIPRequest"
|
||||
|
||||
// HandleConnectorTransitIPRequest creates a ConnectorTransitIPResponse in response to a ConnectorTransitIPRequest.
|
||||
// It updates the connectors mapping of TransitIP->DestinationIP per peer (tailcfg.NodeID).
|
||||
// If a peer has stored this mapping in the connector Conn25 will route traffic to TransitIPs to DestinationIPs for that peer.
|
||||
func (c *Conn25) HandleConnectorTransitIPRequest(nid tailcfg.NodeID, ctipr ConnectorTransitIPRequest) ConnectorTransitIPResponse {
|
||||
resp := ConnectorTransitIPResponse{}
|
||||
seen := map[netip.Addr]bool{}
|
||||
for _, each := range ctipr.TransitIPs {
|
||||
if seen[each.TransitIP] {
|
||||
resp.TransitIPs = append(resp.TransitIPs, TransitIPResponse{
|
||||
Code: OtherFailure,
|
||||
Message: dupeTransitIPMessage,
|
||||
})
|
||||
continue
|
||||
}
|
||||
tipresp := c.handleTransitIPRequest(nid, each)
|
||||
seen[each.TransitIP] = true
|
||||
resp.TransitIPs = append(resp.TransitIPs, tipresp)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (c *Conn25) handleTransitIPRequest(nid tailcfg.NodeID, tipr TransitIPRequest) TransitIPResponse {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.transitIPs == nil {
|
||||
c.transitIPs = make(map[tailcfg.NodeID]map[netip.Addr]netip.Addr)
|
||||
}
|
||||
peerMap, ok := c.transitIPs[nid]
|
||||
if !ok {
|
||||
peerMap = make(map[netip.Addr]netip.Addr)
|
||||
c.transitIPs[nid] = peerMap
|
||||
}
|
||||
peerMap[tipr.TransitIP] = tipr.DestinationIP
|
||||
return TransitIPResponse{}
|
||||
}
|
||||
|
||||
func (c *Conn25) transitIPTarget(nid tailcfg.NodeID, tip netip.Addr) netip.Addr {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.transitIPs[nid][tip]
|
||||
}
|
||||
|
||||
// TransitIPRequest details a single TransitIP allocation request from a client to a
|
||||
// connector.
|
||||
type TransitIPRequest struct {
|
||||
// TransitIP is the intermediate destination IP that will be received at this
|
||||
// connector and will be replaced by DestinationIP when performing DNAT.
|
||||
TransitIP netip.Addr `json:"transitIP,omitzero"`
|
||||
|
||||
// DestinationIP is the final destination IP that connections to the TransitIP
|
||||
// should be mapped to when performing DNAT.
|
||||
DestinationIP netip.Addr `json:"destinationIP,omitzero"`
|
||||
}
|
||||
|
||||
// ConnectorTransitIPRequest is the request body for a PeerAPI request to
|
||||
// /connector/transit-ip and can include zero or more TransitIP allocation requests.
|
||||
type ConnectorTransitIPRequest struct {
|
||||
// TransitIPs is the list of requested mappings.
|
||||
TransitIPs []TransitIPRequest `json:"transitIPs,omitempty"`
|
||||
}
|
||||
|
||||
// TransitIPResponseCode appears in TransitIPResponse and signifies success or failure status.
|
||||
type TransitIPResponseCode int
|
||||
|
||||
const (
|
||||
// OK indicates that the mapping was created as requested.
|
||||
OK TransitIPResponseCode = 0
|
||||
|
||||
// OtherFailure indicates that the mapping failed for a reason that does not have
|
||||
// another relevant [TransitIPResponsecode].
|
||||
OtherFailure TransitIPResponseCode = 1
|
||||
)
|
||||
|
||||
// TransitIPResponse is the response to a TransitIPRequest
|
||||
type TransitIPResponse struct {
|
||||
// Code is an error code indicating success or failure of the [TransitIPRequest].
|
||||
Code TransitIPResponseCode `json:"code,omitzero"`
|
||||
// Message is an error message explaining what happened, suitable for logging but
|
||||
// not necessarily suitable for displaying in a UI to non-technical users. It
|
||||
// should be empty when [Code] is [OK].
|
||||
Message string `json:"message,omitzero"`
|
||||
}
|
||||
|
||||
// ConnectorTransitIPResponse is the response to a ConnectorTransitIPRequest
|
||||
type ConnectorTransitIPResponse struct {
|
||||
// TransitIPs is the list of outcomes for each requested mapping. Elements
|
||||
// correspond to the order of [ConnectorTransitIPRequest.TransitIPs].
|
||||
TransitIPs []TransitIPResponse `json:"transitIPs,omitempty"`
|
||||
}
|
||||
61
vendor/tailscale.com/appc/ippool.go
generated
vendored
Normal file
61
vendor/tailscale.com/appc/ippool.go
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package appc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
// errPoolExhausted is returned when there are no more addresses to iterate over.
|
||||
var errPoolExhausted = errors.New("ip pool exhausted")
|
||||
|
||||
// ippool allows for iteration over all the addresses within a netipx.IPSet.
|
||||
// netipx.IPSet has a Ranges call that returns the "minimum and sorted set of IP ranges that covers [the set]".
|
||||
// netipx.IPRange is "an inclusive range of IP addresses from the same address family.". So we can iterate over
|
||||
// all the addresses in the set by keeping a track of the last address we returned, calling Next on the last address
|
||||
// to get the new one, and if we run off the edge of the current range, starting on the next one.
|
||||
type ippool struct {
|
||||
// ranges defines the addresses in the pool
|
||||
ranges []netipx.IPRange
|
||||
// last is internal tracking of which the last address provided was.
|
||||
last netip.Addr
|
||||
// rangeIdx is internal tracking of which netipx.IPRange from the IPSet we are currently on.
|
||||
rangeIdx int
|
||||
}
|
||||
|
||||
func newIPPool(ipset *netipx.IPSet) *ippool {
|
||||
if ipset == nil {
|
||||
return &ippool{}
|
||||
}
|
||||
return &ippool{ranges: ipset.Ranges()}
|
||||
}
|
||||
|
||||
// next returns the next address from the set, or errPoolExhausted if we have
|
||||
// iterated over the whole set.
|
||||
func (ipp *ippool) next() (netip.Addr, error) {
|
||||
if ipp.rangeIdx >= len(ipp.ranges) {
|
||||
// ipset is empty or we have iterated off the end
|
||||
return netip.Addr{}, errPoolExhausted
|
||||
}
|
||||
if !ipp.last.IsValid() {
|
||||
// not initialized yet
|
||||
ipp.last = ipp.ranges[0].From()
|
||||
return ipp.last, nil
|
||||
}
|
||||
currRange := ipp.ranges[ipp.rangeIdx]
|
||||
if ipp.last == currRange.To() {
|
||||
// then we need to move to the next range
|
||||
ipp.rangeIdx++
|
||||
if ipp.rangeIdx >= len(ipp.ranges) {
|
||||
return netip.Addr{}, errPoolExhausted
|
||||
}
|
||||
ipp.last = ipp.ranges[ipp.rangeIdx].From()
|
||||
return ipp.last, nil
|
||||
}
|
||||
ipp.last = ipp.last.Next()
|
||||
return ipp.last, nil
|
||||
}
|
||||
132
vendor/tailscale.com/appc/observe.go
generated
vendored
Normal file
132
vendor/tailscale.com/appc/observe.go
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_appconnectors
|
||||
|
||||
package appc
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// ObserveDNSResponse is a callback invoked by the DNS resolver when a DNS
|
||||
// response is being returned over the PeerAPI. The response is parsed and
|
||||
// matched against the configured domains, if matched the routeAdvertiser is
|
||||
// advised to advertise the discovered route.
|
||||
func (e *AppConnector) ObserveDNSResponse(res []byte) error {
|
||||
var p dnsmessage.Parser
|
||||
if _, err := p.Start(res); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.SkipAllQuestions(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// cnameChain tracks a chain of CNAMEs for a given query in order to reverse
|
||||
// a CNAME chain back to the original query for flattening. The keys are
|
||||
// CNAME record targets, and the value is the name the record answers, so
|
||||
// for www.example.com CNAME example.com, the map would contain
|
||||
// ["example.com"] = "www.example.com".
|
||||
var cnameChain map[string]string
|
||||
|
||||
// addressRecords is a list of address records found in the response.
|
||||
var addressRecords map[string][]netip.Addr
|
||||
|
||||
for {
|
||||
h, err := p.AnswerHeader()
|
||||
if err == dnsmessage.ErrSectionDone {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.Class != dnsmessage.ClassINET {
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeCNAME, dnsmessage.TypeA, dnsmessage.TypeAAAA:
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
domain := strings.TrimSuffix(strings.ToLower(h.Name.String()), ".")
|
||||
if len(domain) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if h.Type == dnsmessage.TypeCNAME {
|
||||
res, err := p.CNAMEResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cname := strings.TrimSuffix(strings.ToLower(res.CNAME.String()), ".")
|
||||
if len(cname) == 0 {
|
||||
continue
|
||||
}
|
||||
mak.Set(&cnameChain, cname, domain)
|
||||
continue
|
||||
}
|
||||
|
||||
switch h.Type {
|
||||
case dnsmessage.TypeA:
|
||||
r, err := p.AResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr := netip.AddrFrom4(r.A)
|
||||
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
|
||||
case dnsmessage.TypeAAAA:
|
||||
r, err := p.AAAAResource()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr := netip.AddrFrom16(r.AAAA)
|
||||
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
|
||||
default:
|
||||
if err := p.SkipAnswer(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
for domain, addrs := range addressRecords {
|
||||
domain, isRouted := e.findRoutedDomainLocked(domain, cnameChain)
|
||||
|
||||
// domain and none of the CNAMEs in the chain are routed
|
||||
if !isRouted {
|
||||
continue
|
||||
}
|
||||
|
||||
// advertise each address we have learned for the routed domain, that
|
||||
// was not already known.
|
||||
var toAdvertise []netip.Prefix
|
||||
for _, addr := range addrs {
|
||||
if !e.isAddrKnownLocked(domain, addr) {
|
||||
toAdvertise = append(toAdvertise, netip.PrefixFrom(addr, addr.BitLen()))
|
||||
}
|
||||
}
|
||||
|
||||
if len(toAdvertise) > 0 {
|
||||
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
|
||||
e.scheduleAdvertisement(domain, toAdvertise...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
8
vendor/tailscale.com/appc/observe_disabled.go
generated
vendored
Normal file
8
vendor/tailscale.com/appc/observe_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_appconnectors
|
||||
|
||||
package appc
|
||||
|
||||
func (e *AppConnector) ObserveDNSResponse(res []byte) error { return nil }
|
||||
6
vendor/tailscale.com/atomicfile/atomicfile.go
generated
vendored
6
vendor/tailscale.com/atomicfile/atomicfile.go
generated
vendored
@@ -48,5 +48,9 @@ func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return rename(tmpName, filename)
|
||||
return Rename(tmpName, filename)
|
||||
}
|
||||
|
||||
// Rename srcFile to dstFile, similar to [os.Rename] but preserving file
|
||||
// attributes and ACLs on Windows.
|
||||
func Rename(srcFile, dstFile string) error { return rename(srcFile, dstFile) }
|
||||
|
||||
2
vendor/tailscale.com/atomicfile/zsyscall_windows.go
generated
vendored
2
vendor/tailscale.com/atomicfile/zsyscall_windows.go
generated
vendored
@@ -44,7 +44,7 @@ var (
|
||||
)
|
||||
|
||||
func replaceFileW(replaced *uint16, replacement *uint16, backup *uint16, flags uint32, exclude unsafe.Pointer, reserved unsafe.Pointer) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procReplaceFileW.Addr(), 6, uintptr(unsafe.Pointer(replaced)), uintptr(unsafe.Pointer(replacement)), uintptr(unsafe.Pointer(backup)), uintptr(flags), uintptr(exclude), uintptr(reserved))
|
||||
r1, _, e1 := syscall.SyscallN(procReplaceFileW.Addr(), uintptr(unsafe.Pointer(replaced)), uintptr(unsafe.Pointer(replacement)), uintptr(unsafe.Pointer(backup)), uintptr(flags), uintptr(exclude), uintptr(reserved))
|
||||
if int32(r1) == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
|
||||
24
vendor/tailscale.com/build_dist.sh
generated
vendored
24
vendor/tailscale.com/build_dist.sh
generated
vendored
@@ -18,7 +18,7 @@ fi
|
||||
|
||||
eval `CGO_ENABLED=0 GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion`
|
||||
|
||||
if [ "$1" = "shellvars" ]; then
|
||||
if [ "$#" -ge 1 ] && [ "$1" = "shellvars" ]; then
|
||||
cat <<EOF
|
||||
VERSION_MINOR="$VERSION_MINOR"
|
||||
VERSION_SHORT="$VERSION_SHORT"
|
||||
@@ -28,18 +28,34 @@ EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tags=""
|
||||
tags="${TAGS:-}"
|
||||
ldflags="-X tailscale.com/version.longStamp=${VERSION_LONG} -X tailscale.com/version.shortStamp=${VERSION_SHORT}"
|
||||
|
||||
# build_dist.sh arguments must precede go build arguments.
|
||||
while [ "$#" -gt 1 ]; do
|
||||
case "$1" in
|
||||
--extra-small)
|
||||
if [ ! -z "${TAGS:-}" ]; then
|
||||
echo "set either --extra-small or \$TAGS, but not both"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
ldflags="$ldflags -w -s"
|
||||
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture"
|
||||
tags="${tags:+$tags,},$(GOOS= GOARCH= $go run ./cmd/featuretags --min --add=osrouter)"
|
||||
;;
|
||||
--min)
|
||||
# --min is like --extra-small but even smaller, removing all features,
|
||||
# even if it results in a useless binary (e.g. removing both netstack +
|
||||
# osrouter). It exists for benchmarking purposes only.
|
||||
shift
|
||||
ldflags="$ldflags -w -s"
|
||||
tags="${tags:+$tags,},$(GOOS= GOARCH= $go run ./cmd/featuretags --min)"
|
||||
;;
|
||||
--box)
|
||||
if [ ! -z "${TAGS:-}" ]; then
|
||||
echo "set either --box or \$TAGS, but not both"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
tags="${tags:+$tags,}ts_include_cli"
|
||||
;;
|
||||
@@ -49,4 +65,4 @@ while [ "$#" -gt 1 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
exec $go build ${tags:+-tags=$tags} -ldflags "$ldflags" "$@"
|
||||
exec $go build ${tags:+-tags=$tags} -trimpath -ldflags "$ldflags" "$@"
|
||||
|
||||
54
vendor/tailscale.com/build_docker.sh
generated
vendored
54
vendor/tailscale.com/build_docker.sh
generated
vendored
@@ -6,6 +6,16 @@
|
||||
# hash of this repository as produced by ./cmd/mkversion.
|
||||
# This is the image build mechanim used to build the official Tailscale
|
||||
# container images.
|
||||
#
|
||||
# If you want to build local images for testing, you can use make, which provides few convenience wrappers around this script.
|
||||
#
|
||||
# To build a Tailscale image and push to the local docker registry:
|
||||
|
||||
# $ REPO=local/tailscale TAGS=v0.0.1 PLATFORM=local make publishdevimage
|
||||
#
|
||||
# To build a Tailscale image and push to a remote docker registry:
|
||||
#
|
||||
# $ REPO=<your-registry>/<your-repo>/tailscale TAGS=v0.0.1 make publishdevimage
|
||||
|
||||
set -eu
|
||||
|
||||
@@ -16,7 +26,7 @@ eval "$(./build_dist.sh shellvars)"
|
||||
|
||||
DEFAULT_TARGET="client"
|
||||
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
|
||||
DEFAULT_BASE="tailscale/alpine-base:3.19"
|
||||
DEFAULT_BASE="tailscale/alpine-base:3.22"
|
||||
# Set a few pre-defined OCI annotations. The source annotation is used by tools such as Renovate that scan the linked
|
||||
# Github repo to find release notes for any new image tags. Note that for official Tailscale images the default
|
||||
# annotations defined here will be overriden by release scripts that call this script.
|
||||
@@ -28,6 +38,7 @@ TARGET="${TARGET:-${DEFAULT_TARGET}}"
|
||||
TAGS="${TAGS:-${DEFAULT_TAGS}}"
|
||||
BASE="${BASE:-${DEFAULT_BASE}}"
|
||||
PLATFORM="${PLATFORM:-}" # default to all platforms
|
||||
FILES="${FILES:-}" # default to no extra files
|
||||
# OCI annotations that will be added to the image.
|
||||
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
|
||||
ANNOTATIONS="${ANNOTATIONS:-${DEFAULT_ANNOTATIONS}}"
|
||||
@@ -52,6 +63,7 @@ case "$TARGET" in
|
||||
--push="${PUSH}" \
|
||||
--target="${PLATFORM}" \
|
||||
--annotations="${ANNOTATIONS}" \
|
||||
--files="${FILES}" \
|
||||
/usr/local/bin/containerboot
|
||||
;;
|
||||
k8s-operator)
|
||||
@@ -70,6 +82,7 @@ case "$TARGET" in
|
||||
--push="${PUSH}" \
|
||||
--target="${PLATFORM}" \
|
||||
--annotations="${ANNOTATIONS}" \
|
||||
--files="${FILES}" \
|
||||
/usr/local/bin/operator
|
||||
;;
|
||||
k8s-nameserver)
|
||||
@@ -88,8 +101,47 @@ case "$TARGET" in
|
||||
--push="${PUSH}" \
|
||||
--target="${PLATFORM}" \
|
||||
--annotations="${ANNOTATIONS}" \
|
||||
--files="${FILES}" \
|
||||
/usr/local/bin/k8s-nameserver
|
||||
;;
|
||||
tsidp)
|
||||
DEFAULT_REPOS="tailscale/tsidp"
|
||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
go run github.com/tailscale/mkctr \
|
||||
--gopaths="tailscale.com/cmd/tsidp:/usr/local/bin/tsidp" \
|
||||
--ldflags=" \
|
||||
-X tailscale.com/version.longStamp=${VERSION_LONG} \
|
||||
-X tailscale.com/version.shortStamp=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--gotags="ts_package_container" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
--target="${PLATFORM}" \
|
||||
--annotations="${ANNOTATIONS}" \
|
||||
--files="${FILES}" \
|
||||
/usr/local/bin/tsidp
|
||||
;;
|
||||
k8s-proxy)
|
||||
DEFAULT_REPOS="tailscale/k8s-proxy"
|
||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
go run github.com/tailscale/mkctr \
|
||||
--gopaths="tailscale.com/cmd/k8s-proxy:/usr/local/bin/k8s-proxy" \
|
||||
--ldflags=" \
|
||||
-X tailscale.com/version.longStamp=${VERSION_LONG} \
|
||||
-X tailscale.com/version.shortStamp=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--gotags="ts_kube,ts_package_container" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
--target="${PLATFORM}" \
|
||||
--annotations="${ANNOTATIONS}" \
|
||||
--files="${FILES}" \
|
||||
/usr/local/bin/k8s-proxy
|
||||
;;
|
||||
*)
|
||||
echo "unknown target: $TARGET"
|
||||
exit 1
|
||||
|
||||
151
vendor/tailscale.com/client/local/cert.go
generated
vendored
Normal file
151
vendor/tailscale.com/client/local/cert.go
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js && !ts_omit_acme
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
)
|
||||
|
||||
// SetDNS adds a DNS TXT record for the given domain name, containing
|
||||
// the provided TXT value. The intended use case is answering
|
||||
// LetsEncrypt/ACME dns-01 challenges.
|
||||
//
|
||||
// The control plane will only permit SetDNS requests with very
|
||||
// specific names and values. The name should be
|
||||
// "_acme-challenge." + your node's MagicDNS name. It's expected that
|
||||
// clients cache the certs from LetsEncrypt (or whichever CA is
|
||||
// providing them) and only request new ones as needed; the control plane
|
||||
// rate limits SetDNS requests.
|
||||
//
|
||||
// This is a low-level interface; it's expected that most Tailscale
|
||||
// users use a higher level interface to getting/using TLS
|
||||
// certificates.
|
||||
func (lc *Client) SetDNS(ctx context.Context, name, value string) error {
|
||||
v := url.Values{}
|
||||
v.Set("name", name)
|
||||
v.Set("value", value)
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// CertPair returns a cert and private key for the provided DNS domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// Deprecated: use [Client.CertPair].
|
||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
return defaultClient.CertPair(ctx, domain)
|
||||
}
|
||||
|
||||
// CertPair returns a cert and private key for the provided DNS domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
return lc.CertPairWithValidity(ctx, domain, 0)
|
||||
}
|
||||
|
||||
// CertPairWithValidity returns a cert and private key for the provided DNS
|
||||
// domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
// When minValidity is non-zero, the returned certificate will be valid for at
|
||||
// least the given duration, if permitted by the CA. If the certificate is
|
||||
// valid, but for less than minValidity, it will be synchronously renewed.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// with ?type=pair, the response PEM is first the one private
|
||||
// key PEM block, then the cert PEM blocks.
|
||||
i := mem.Index(mem.B(res), mem.S("--\n--"))
|
||||
if i == -1 {
|
||||
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
|
||||
}
|
||||
i += len("--\n")
|
||||
keyPEM, certPEM = res[:i], res[i:]
|
||||
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
|
||||
return nil, nil, fmt.Errorf("unexpected output: key in cert")
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// It's the right signature to use as the value of
|
||||
// [tls.Config.GetCertificate].
|
||||
//
|
||||
// Deprecated: use [Client.GetCertificate].
|
||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return defaultClient.GetCertificate(hi)
|
||||
}
|
||||
|
||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// It's the right signature to use as the value of
|
||||
// [tls.Config.GetCertificate].
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi == nil || hi.ServerName == "" {
|
||||
return nil, errors.New("no SNI ServerName")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
name := hi.ServerName
|
||||
if !strings.Contains(name, ".") {
|
||||
if v, ok := lc.ExpandSNIName(ctx, name); ok {
|
||||
name = v
|
||||
}
|
||||
}
|
||||
certPEM, keyPEM, err := lc.CertPair(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
||||
//
|
||||
// Deprecated: use [Client.ExpandSNIName].
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
return defaultClient.ExpandSNIName(ctx, name)
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
||||
func (lc *Client) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
st, err := lc.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
for _, d := range st.CertDomains {
|
||||
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
|
||||
return d, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
84
vendor/tailscale.com/client/local/debugportmapper.go
generated
vendored
Normal file
84
vendor/tailscale.com/client/local/debugportmapper.go
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debugportmapper
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
)
|
||||
|
||||
// DebugPortmapOpts contains options for the [Client.DebugPortmap] command.
|
||||
type DebugPortmapOpts struct {
|
||||
// Duration is how long the mapping should be created for. It defaults
|
||||
// to 5 seconds if not set.
|
||||
Duration time.Duration
|
||||
|
||||
// Type is the kind of portmap to debug. The empty string instructs the
|
||||
// portmap client to perform all known types. Other valid options are
|
||||
// "pmp", "pcp", and "upnp".
|
||||
Type string
|
||||
|
||||
// GatewayAddr specifies the gateway address used during portmapping.
|
||||
// If set, SelfAddr must also be set. If unset, it will be
|
||||
// autodetected.
|
||||
GatewayAddr netip.Addr
|
||||
|
||||
// SelfAddr specifies the gateway address used during portmapping. If
|
||||
// set, GatewayAddr must also be set. If unset, it will be
|
||||
// autodetected.
|
||||
SelfAddr netip.Addr
|
||||
|
||||
// LogHTTP instructs the debug-portmap endpoint to print all HTTP
|
||||
// requests and responses made to the logs.
|
||||
LogHTTP bool
|
||||
}
|
||||
|
||||
// DebugPortmap invokes the debug-portmap endpoint, and returns an
|
||||
// io.ReadCloser that can be used to read the logs that are printed during this
|
||||
// process.
|
||||
//
|
||||
// opts can be nil; if so, default values will be used.
|
||||
func (lc *Client) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) {
|
||||
vals := make(url.Values)
|
||||
if opts == nil {
|
||||
opts = &DebugPortmapOpts{}
|
||||
}
|
||||
|
||||
vals.Set("duration", cmp.Or(opts.Duration, 5*time.Second).String())
|
||||
vals.Set("type", opts.Type)
|
||||
vals.Set("log_http", strconv.FormatBool(opts.LogHTTP))
|
||||
|
||||
if opts.GatewayAddr.IsValid() != opts.SelfAddr.IsValid() {
|
||||
return nil, fmt.Errorf("both GatewayAddr and SelfAddr must be provided if one is")
|
||||
} else if opts.GatewayAddr.IsValid() {
|
||||
vals.Set("gateway_and_self", fmt.Sprintf("%s/%s", opts.GatewayAddr, opts.SelfAddr))
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := lc.doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
|
||||
return res.Body, nil
|
||||
}
|
||||
649
vendor/tailscale.com/client/local/local.go
generated
vendored
649
vendor/tailscale.com/client/local/local.go
generated
vendored
@@ -1,21 +1,20 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build go1.22
|
||||
|
||||
// Package local contains a Go client for the Tailscale LocalAPI.
|
||||
package local
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
@@ -28,21 +27,24 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/udprelay/status"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/eventbus"
|
||||
)
|
||||
|
||||
// defaultClient is the default Client when using the legacy
|
||||
@@ -294,7 +296,7 @@ func (lc *Client) get200(ctx context.Context, path string) ([]byte, error) {
|
||||
|
||||
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
|
||||
//
|
||||
// Deprecated: use Client.WhoIs.
|
||||
// Deprecated: use [Client.WhoIs].
|
||||
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
||||
return defaultClient.WhoIs(ctx, remoteAddr)
|
||||
}
|
||||
@@ -309,7 +311,7 @@ func decodeJSON[T any](b []byte) (ret T, err error) {
|
||||
|
||||
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
|
||||
//
|
||||
// If not found, the error is ErrPeerNotFound.
|
||||
// If not found, the error is [ErrPeerNotFound].
|
||||
//
|
||||
// For connections proxied by tailscaled, this looks up the owner of the given
|
||||
// address as TCP first, falling back to UDP; if you want to only check a
|
||||
@@ -325,7 +327,8 @@ func (lc *Client) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsR
|
||||
return decodeJSON[*apitype.WhoIsResponse](body)
|
||||
}
|
||||
|
||||
// ErrPeerNotFound is returned by WhoIs and WhoIsNodeKey when a peer is not found.
|
||||
// ErrPeerNotFound is returned by [Client.WhoIs], [Client.WhoIsNodeKey] and
|
||||
// [Client.WhoIsProto] when a peer is not found.
|
||||
var ErrPeerNotFound = errors.New("peer not found")
|
||||
|
||||
// WhoIsNodeKey returns the owner of the given wireguard public key.
|
||||
@@ -345,7 +348,7 @@ func (lc *Client) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*apityp
|
||||
// WhoIsProto returns the owner of the remoteAddr, which must be an IP or
|
||||
// IP:port, for the given protocol (tcp or udp).
|
||||
//
|
||||
// If not found, the error is ErrPeerNotFound.
|
||||
// If not found, the error is [ErrPeerNotFound].
|
||||
func (lc *Client) WhoIsProto(ctx context.Context, proto, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/whois?proto="+url.QueryEscape(proto)+"&addr="+url.QueryEscape(remoteAddr))
|
||||
if err != nil {
|
||||
@@ -380,18 +383,42 @@ func (lc *Client) UserMetrics(ctx context.Context) ([]byte, error) {
|
||||
//
|
||||
// IncrementCounter does not support gauge metrics or negative delta values.
|
||||
func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int) error {
|
||||
type metricUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value"` // amount to increment by
|
||||
if !buildfeatures.HasClientMetrics {
|
||||
return nil
|
||||
}
|
||||
if delta < 0 {
|
||||
return errors.New("negative delta not allowed")
|
||||
}
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]metricUpdate{{
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
|
||||
Name: name,
|
||||
Type: "counter",
|
||||
Value: delta,
|
||||
Op: "add",
|
||||
}}))
|
||||
return err
|
||||
}
|
||||
|
||||
// IncrementGauge increments the value of a Tailscale daemon's gauge
|
||||
// metric by the given delta. If the metric has yet to exist, a new gauge
|
||||
// metric is created and initialized to delta. The delta value can be negative.
|
||||
func (lc *Client) IncrementGauge(ctx context.Context, name string, delta int) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
|
||||
Name: name,
|
||||
Type: "gauge",
|
||||
Value: delta,
|
||||
Op: "add",
|
||||
}}))
|
||||
return err
|
||||
}
|
||||
|
||||
// SetGauge sets the value of a Tailscale daemon's gauge metric to the given value.
|
||||
// If the metric has yet to exist, a new gauge metric is created and initialized to value.
|
||||
func (lc *Client) SetGauge(ctx context.Context, name string, value int) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
|
||||
Name: name,
|
||||
Type: "gauge",
|
||||
Value: value,
|
||||
Op: "set",
|
||||
}}))
|
||||
return err
|
||||
}
|
||||
@@ -413,6 +440,50 @@ func (lc *Client) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
|
||||
return res.Body, nil
|
||||
}
|
||||
|
||||
// EventBusGraph returns a graph of active publishers and subscribers in the eventbus
|
||||
// as a [eventbus.DebugTopics]
|
||||
func (lc *Client) EventBusGraph(ctx context.Context) ([]byte, error) {
|
||||
return lc.get200(ctx, "/localapi/v0/debug-bus-graph")
|
||||
}
|
||||
|
||||
// StreamBusEvents returns an iterator of Tailscale bus events as they arrive.
|
||||
// Each pair is a valid event and a nil error, or a zero event a non-nil error.
|
||||
// In case of error, the iterator ends after the pair reporting the error.
|
||||
// Iteration stops if ctx ends.
|
||||
func (lc *Client) StreamBusEvents(ctx context.Context) iter.Seq2[eventbus.DebugEvent, error] {
|
||||
return func(yield func(eventbus.DebugEvent, error) bool) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET",
|
||||
"http://"+apitype.LocalAPIHost+"/localapi/v0/debug-bus-events", nil)
|
||||
if err != nil {
|
||||
yield(eventbus.DebugEvent{}, err)
|
||||
return
|
||||
}
|
||||
res, err := lc.doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
yield(eventbus.DebugEvent{}, err)
|
||||
return
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
yield(eventbus.DebugEvent{}, errors.New(res.Status))
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
dec := json.NewDecoder(bufio.NewReader(res.Body))
|
||||
for {
|
||||
var evt eventbus.DebugEvent
|
||||
if err := dec.Decode(&evt); err == io.EOF {
|
||||
return
|
||||
} else if err != nil {
|
||||
yield(eventbus.DebugEvent{}, err)
|
||||
return
|
||||
}
|
||||
if !yield(evt, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pprof returns a pprof profile of the Tailscale daemon.
|
||||
func (lc *Client) Pprof(ctx context.Context, pprofType string, sec int) ([]byte, error) {
|
||||
var secArg string
|
||||
@@ -490,7 +561,7 @@ func (lc *Client) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (st
|
||||
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
//
|
||||
// This is the same as calling BugReportWithOpts and only specifying the Note
|
||||
// This is the same as calling [Client.BugReportWithOpts] and only specifying the Note
|
||||
// field.
|
||||
func (lc *Client) BugReport(ctx context.Context, note string) (string, error) {
|
||||
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
|
||||
@@ -531,68 +602,17 @@ func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, erro
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// DebugPortmapOpts contains options for the DebugPortmap command.
|
||||
type DebugPortmapOpts struct {
|
||||
// Duration is how long the mapping should be created for. It defaults
|
||||
// to 5 seconds if not set.
|
||||
Duration time.Duration
|
||||
|
||||
// Type is the kind of portmap to debug. The empty string instructs the
|
||||
// portmap client to perform all known types. Other valid options are
|
||||
// "pmp", "pcp", and "upnp".
|
||||
Type string
|
||||
|
||||
// GatewayAddr specifies the gateway address used during portmapping.
|
||||
// If set, SelfAddr must also be set. If unset, it will be
|
||||
// autodetected.
|
||||
GatewayAddr netip.Addr
|
||||
|
||||
// SelfAddr specifies the gateway address used during portmapping. If
|
||||
// set, GatewayAddr must also be set. If unset, it will be
|
||||
// autodetected.
|
||||
SelfAddr netip.Addr
|
||||
|
||||
// LogHTTP instructs the debug-portmap endpoint to print all HTTP
|
||||
// requests and responses made to the logs.
|
||||
LogHTTP bool
|
||||
}
|
||||
|
||||
// DebugPortmap invokes the debug-portmap endpoint, and returns an
|
||||
// io.ReadCloser that can be used to read the logs that are printed during this
|
||||
// process.
|
||||
//
|
||||
// opts can be nil; if so, default values will be used.
|
||||
func (lc *Client) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) {
|
||||
vals := make(url.Values)
|
||||
if opts == nil {
|
||||
opts = &DebugPortmapOpts{}
|
||||
}
|
||||
|
||||
vals.Set("duration", cmp.Or(opts.Duration, 5*time.Second).String())
|
||||
vals.Set("type", opts.Type)
|
||||
vals.Set("log_http", strconv.FormatBool(opts.LogHTTP))
|
||||
|
||||
if opts.GatewayAddr.IsValid() != opts.SelfAddr.IsValid() {
|
||||
return nil, fmt.Errorf("both GatewayAddr and SelfAddr must be provided if one is")
|
||||
} else if opts.GatewayAddr.IsValid() {
|
||||
vals.Set("gateway_and_self", fmt.Sprintf("%s/%s", opts.GatewayAddr, opts.SelfAddr))
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil)
|
||||
// QueryOptionalFeatures queries the optional features supported by the Tailscale daemon.
|
||||
func (lc *Client) QueryOptionalFeatures(ctx context.Context) (*apitype.OptionalFeatures, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-optional-features", 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
var x apitype.OptionalFeatures
|
||||
if err := json.Unmarshal(body, &x); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := lc.doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
|
||||
return res.Body, nil
|
||||
return &x, nil
|
||||
}
|
||||
|
||||
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
|
||||
@@ -612,6 +632,9 @@ func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) er
|
||||
// the provided duration. If the duration is in the past, the debug logging
|
||||
// is disabled.
|
||||
func (lc *Client) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
|
||||
if !buildfeatures.HasDebug {
|
||||
return feature.ErrUnavailable
|
||||
}
|
||||
body, err := lc.send(ctx, "POST",
|
||||
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
|
||||
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
|
||||
@@ -677,7 +700,7 @@ func (lc *Client) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, erro
|
||||
return lc.AwaitWaitingFiles(ctx, 0)
|
||||
}
|
||||
|
||||
// AwaitWaitingFiles is like WaitingFiles but takes a duration to await for an answer.
|
||||
// AwaitWaitingFiles is like [Client.WaitingFiles] but takes a duration to await for an answer.
|
||||
// If the duration is 0, it will return immediately. The duration is respected at second
|
||||
// granularity only. If no files are available, it returns (nil, nil).
|
||||
func (lc *Client) AwaitWaitingFiles(ctx context.Context, d time.Duration) ([]apitype.WaitingFile, error) {
|
||||
@@ -751,6 +774,9 @@ func (lc *Client) PushFile(ctx context.Context, target tailcfg.StableNodeID, siz
|
||||
// machine is properly configured to forward IP packets as a subnet router
|
||||
// or exit node.
|
||||
func (lc *Client) CheckIPForwarding(ctx context.Context) error {
|
||||
if !buildfeatures.HasAdvertiseRoutes {
|
||||
return nil
|
||||
}
|
||||
body, err := lc.get200(ctx, "/localapi/v0/check-ip-forwarding")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -787,6 +813,25 @@ func (lc *Client) CheckUDPGROForwarding(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckReversePathFiltering asks the local Tailscale daemon whether strict
|
||||
// reverse path filtering is enabled, which would break exit node usage on Linux.
|
||||
func (lc *Client) CheckReversePathFiltering(ctx context.Context) error {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/check-reverse-path-filtering")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var jres struct {
|
||||
Warning string
|
||||
}
|
||||
if err := json.Unmarshal(body, &jres); err != nil {
|
||||
return fmt.Errorf("invalid JSON from check-reverse-path-filtering: %w", err)
|
||||
}
|
||||
if jres.Warning != "" {
|
||||
return errors.New(jres.Warning)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUDPGROForwarding enables UDP GRO forwarding for the main interface of this
|
||||
// node. This can be done to improve performance of tailnet nodes acting as exit
|
||||
// nodes or subnet routers.
|
||||
@@ -844,36 +889,12 @@ func (lc *Client) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Pref
|
||||
return decodeJSON[*ipn.Prefs](body)
|
||||
}
|
||||
|
||||
// GetEffectivePolicy returns the effective policy for the specified scope.
|
||||
func (lc *Client) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
|
||||
scopeID, err := scope.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := lc.get200(ctx, "/localapi/v0/policy/"+string(scopeID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*setting.Snapshot](body)
|
||||
}
|
||||
|
||||
// ReloadEffectivePolicy reloads the effective policy for the specified scope
|
||||
// by reading and merging policy settings from all applicable policy sources.
|
||||
func (lc *Client) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
|
||||
scopeID, err := scope.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/policy/"+string(scopeID), 200, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*setting.Snapshot](body)
|
||||
}
|
||||
|
||||
// GetDNSOSConfig returns the system DNS configuration for the current device.
|
||||
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
|
||||
func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return nil, feature.ErrUnavailable
|
||||
}
|
||||
body, err := lc.get200(ctx, "/localapi/v0/dns-osconfig")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -889,6 +910,9 @@ func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, err
|
||||
// It returns the raw DNS response bytes and the resolvers that were used to answer the query
|
||||
// (often just one, but can be more if we raced multiple resolvers).
|
||||
func (lc *Client) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return nil, nil, feature.ErrUnavailable
|
||||
}
|
||||
body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -919,34 +943,12 @@ func (lc *Client) Logout(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SetDNS adds a DNS TXT record for the given domain name, containing
|
||||
// the provided TXT value. The intended use case is answering
|
||||
// LetsEncrypt/ACME dns-01 challenges.
|
||||
//
|
||||
// The control plane will only permit SetDNS requests with very
|
||||
// specific names and values. The name should be
|
||||
// "_acme-challenge." + your node's MagicDNS name. It's expected that
|
||||
// clients cache the certs from LetsEncrypt (or whichever CA is
|
||||
// providing them) and only request new ones as needed; the control plane
|
||||
// rate limits SetDNS requests.
|
||||
//
|
||||
// This is a low-level interface; it's expected that most Tailscale
|
||||
// users use a higher level interface to getting/using TLS
|
||||
// certificates.
|
||||
func (lc *Client) SetDNS(ctx context.Context, name, value string) error {
|
||||
v := url.Values{}
|
||||
v.Set("name", name)
|
||||
v.Set("value", value)
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// DialTCP connects to the host's port via Tailscale.
|
||||
//
|
||||
// The host may be a base DNS name (resolved from the netmap inside
|
||||
// tailscaled), a FQDN, or an IP address.
|
||||
//
|
||||
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
|
||||
// The ctx is only used for the duration of the call, not the lifetime of the [net.Conn].
|
||||
func (lc *Client) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
|
||||
return lc.UserDial(ctx, "tcp", host, port)
|
||||
}
|
||||
@@ -957,7 +959,7 @@ func (lc *Client) DialTCP(ctx context.Context, host string, port uint16) (net.Co
|
||||
// a FQDN, or an IP address.
|
||||
//
|
||||
// The ctx is only used for the duration of the call, not the lifetime of the
|
||||
// net.Conn.
|
||||
// [net.Conn].
|
||||
func (lc *Client) UserDial(ctx context.Context, network, host string, port uint16) (net.Conn, error) {
|
||||
connCh := make(chan net.Conn, 1)
|
||||
trace := httptrace.ClientTrace{
|
||||
@@ -1021,117 +1023,6 @@ func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error)
|
||||
return &derpMap, nil
|
||||
}
|
||||
|
||||
// CertPair returns a cert and private key for the provided DNS domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// Deprecated: use Client.CertPair.
|
||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
return defaultClient.CertPair(ctx, domain)
|
||||
}
|
||||
|
||||
// CertPair returns a cert and private key for the provided DNS domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
return lc.CertPairWithValidity(ctx, domain, 0)
|
||||
}
|
||||
|
||||
// CertPairWithValidity returns a cert and private key for the provided DNS
|
||||
// domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
// When minValidity is non-zero, the returned certificate will be valid for at
|
||||
// least the given duration, if permitted by the CA. If the certificate is
|
||||
// valid, but for less than minValidity, it will be synchronously renewed.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// with ?type=pair, the response PEM is first the one private
|
||||
// key PEM block, then the cert PEM blocks.
|
||||
i := mem.Index(mem.B(res), mem.S("--\n--"))
|
||||
if i == -1 {
|
||||
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
|
||||
}
|
||||
i += len("--\n")
|
||||
keyPEM, certPEM = res[:i], res[i:]
|
||||
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
|
||||
return nil, nil, fmt.Errorf("unexpected output: key in cert")
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// It's the right signature to use as the value of
|
||||
// tls.Config.GetCertificate.
|
||||
//
|
||||
// Deprecated: use Client.GetCertificate.
|
||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return defaultClient.GetCertificate(hi)
|
||||
}
|
||||
|
||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// It's the right signature to use as the value of
|
||||
// tls.Config.GetCertificate.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *Client) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi == nil || hi.ServerName == "" {
|
||||
return nil, errors.New("no SNI ServerName")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
name := hi.ServerName
|
||||
if !strings.Contains(name, ".") {
|
||||
if v, ok := lc.ExpandSNIName(ctx, name); ok {
|
||||
name = v
|
||||
}
|
||||
}
|
||||
certPEM, keyPEM, err := lc.CertPair(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
||||
//
|
||||
// Deprecated: use Client.ExpandSNIName.
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
return defaultClient.ExpandSNIName(ctx, name)
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
||||
func (lc *Client) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
st, err := lc.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
for _, d := range st.CertDomains {
|
||||
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
|
||||
return d, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// PingOpts contains options for the ping request.
|
||||
//
|
||||
// The zero value is valid, which means to use defaults.
|
||||
@@ -1165,197 +1056,6 @@ func (lc *Client) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.Ping
|
||||
return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{})
|
||||
}
|
||||
|
||||
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
|
||||
func (lc *Client) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
}
|
||||
|
||||
// NetworkLockInit initializes the tailnet key authority.
|
||||
//
|
||||
// TODO(tom): Plumb through disablement secrets.
|
||||
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
|
||||
var b bytes.Buffer
|
||||
type initRequest struct {
|
||||
Keys []tka.Key
|
||||
DisablementValues [][]byte
|
||||
SupportDisablement []byte
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
}
|
||||
|
||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||
// enable unattended bringup in the locked tailnet.
|
||||
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
|
||||
encodedPrivate, err := tkaKey.MarshalText()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
type wrapRequest struct {
|
||||
TSKey string
|
||||
TKAKey string // key.NLPrivate.MarshalText
|
||||
}
|
||||
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
|
||||
var b bytes.Buffer
|
||||
type modifyRequest struct {
|
||||
AddKeys []tka.Key
|
||||
RemoveKeys []tka.Key
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
|
||||
// rotationPublic, if specified, must be an ed25519 public key.
|
||||
func (lc *Client) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
|
||||
var b bytes.Buffer
|
||||
type signRequest struct {
|
||||
NodeKey key.NodePublic
|
||||
RotationPublic []byte
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
|
||||
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[[]tkatype.MarshaledSignature](body)
|
||||
}
|
||||
|
||||
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||
v := url.Values{}
|
||||
v.Set("limit", fmt.Sprint(maxEntries))
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
|
||||
}
|
||||
|
||||
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
|
||||
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error {
|
||||
// This endpoint expects an empty JSON stanza as the payload.
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
|
||||
// in url and returns information extracted from it.
|
||||
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
|
||||
vr := struct {
|
||||
URL string
|
||||
}{url}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
|
||||
}
|
||||
|
||||
return decodeJSON[*tka.DeeplinkValidationResult](body)
|
||||
}
|
||||
|
||||
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
|
||||
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
|
||||
vr := struct {
|
||||
Keys []tkatype.KeyID
|
||||
ForkFrom string
|
||||
}{removeKeys, forkFrom.String()}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
|
||||
func (lc *Client) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
|
||||
r := bytes.NewReader(aum.Serialize())
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
|
||||
func (lc *Client) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
|
||||
r := bytes.NewReader(aum.Serialize())
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetServeConfig sets or replaces the serving settings.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||
h := make(http.Header)
|
||||
if config != nil {
|
||||
h.Set("If-Match", config.ETag)
|
||||
}
|
||||
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending serve config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
|
||||
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
|
||||
// to another replica whilst there is still some grace period for the existing connections to terminate.
|
||||
@@ -1367,40 +1067,6 @@ func (lc *Client) DisconnectControl(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockDisable shuts down network-lock across the tailnet.
|
||||
func (lc *Client) NetworkLockDisable(ctx context.Context, secret []byte) error {
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServeConfig return the current serve config.
|
||||
//
|
||||
// If the serve config is empty, it returns (nil, nil).
|
||||
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
|
||||
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting serve config: %w", err)
|
||||
}
|
||||
sc, err := getServeConfigFromJSON(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
sc.ETag = h.Get("Etag")
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {
|
||||
if err := json.Unmarshal(body, &sc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// tailscaledConnectHint gives a little thing about why tailscaled (or
|
||||
// platform equivalent) is not answering localapi connections.
|
||||
//
|
||||
@@ -1502,9 +1168,9 @@ func (lc *Client) SwitchProfile(ctx context.Context, profile ipn.ProfileID) erro
|
||||
|
||||
// DeleteProfile removes the profile with the given ID.
|
||||
// If the profile is the current profile, an empty profile
|
||||
// will be selected as if SwitchToEmptyProfile was called.
|
||||
// will be selected as if [Client.SwitchToEmptyProfile] was called.
|
||||
func (lc *Client) DeleteProfile(ctx context.Context, profile ipn.ProfileID) error {
|
||||
_, err := lc.send(ctx, "DELETE", "/localapi/v0/profiles"+url.PathEscape(string(profile)), http.StatusNoContent, nil)
|
||||
_, err := lc.send(ctx, "DELETE", "/localapi/v0/profiles/"+url.PathEscape(string(profile)), http.StatusNoContent, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1556,10 +1222,20 @@ func (lc *Client) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DebugPeerRelaySessions returns debug information about the current peer
|
||||
// relay sessions running through this node.
|
||||
func (lc *Client) DebugPeerRelaySessions(ctx context.Context) (*status.ServerStatus, error) {
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/debug-peer-relay-sessions", 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[*status.ServerStatus](body)
|
||||
}
|
||||
|
||||
// StreamDebugCapture streams a pcap-formatted packet capture.
|
||||
//
|
||||
// The provided context does not determine the lifetime of the
|
||||
// returned io.ReadCloser.
|
||||
// returned [io.ReadCloser].
|
||||
func (lc *Client) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-capture", nil)
|
||||
if err != nil {
|
||||
@@ -1582,7 +1258,7 @@ func (lc *Client) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error)
|
||||
// The context is used for the life of the watch, not just the call to
|
||||
// WatchIPNBus.
|
||||
//
|
||||
// The returned IPNBusWatcher's Close method must be called when done to release
|
||||
// The returned [IPNBusWatcher]'s Close method must be called when done to release
|
||||
// resources.
|
||||
//
|
||||
// A default set of ipn.Notify messages are returned but the set can be modified by mask.
|
||||
@@ -1609,7 +1285,7 @@ func (lc *Client) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IP
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckUpdate returns a tailcfg.ClientVersion indicating whether or not an update is available
|
||||
// CheckUpdate returns a [*tailcfg.ClientVersion] indicating whether or not an update is available
|
||||
// to be installed via the LocalAPI. In case the LocalAPI can't install updates, it returns a
|
||||
// ClientVersion that says that we are up to date.
|
||||
func (lc *Client) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion, error) {
|
||||
@@ -1685,7 +1361,7 @@ func (lc *Client) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
|
||||
}
|
||||
|
||||
// IPNBusWatcher is an active subscription (watch) of the local tailscaled IPN bus.
|
||||
// It's returned by Client.WatchIPNBus.
|
||||
// It's returned by [Client.WatchIPNBus].
|
||||
//
|
||||
// It must be closed when done.
|
||||
type IPNBusWatcher struct {
|
||||
@@ -1693,7 +1369,7 @@ type IPNBusWatcher struct {
|
||||
httpRes *http.Response
|
||||
dec *json.Decoder
|
||||
|
||||
mu sync.Mutex
|
||||
mu syncs.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
@@ -1729,3 +1405,34 @@ func (lc *Client) SuggestExitNode(ctx context.Context) (apitype.ExitNodeSuggesti
|
||||
}
|
||||
return decodeJSON[apitype.ExitNodeSuggestionResponse](body)
|
||||
}
|
||||
|
||||
// CheckSOMarkInUse reports whether the socket mark option is in use. This will only
|
||||
// be true if tailscale is running on Linux and tailscaled uses SO_MARK.
|
||||
func (lc *Client) CheckSOMarkInUse(ctx context.Context) (bool, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/check-so-mark-in-use")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var res struct {
|
||||
UseSOMark bool `json:"useSoMark"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &res); err != nil {
|
||||
return false, fmt.Errorf("invalid JSON from check-so-mark-in-use: %w", err)
|
||||
}
|
||||
return res.UseSOMark, nil
|
||||
}
|
||||
|
||||
// ShutdownTailscaled requests a graceful shutdown of tailscaled.
|
||||
func (lc *Client) ShutdownTailscaled(ctx context.Context) error {
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/shutdown", 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (lc *Client) GetAppConnectorRouteInfo(ctx context.Context) (appctype.RouteInfo, error) {
|
||||
body, err := lc.get200(ctx, "/localapi/v0/appc-route-info")
|
||||
if err != nil {
|
||||
return appctype.RouteInfo{}, err
|
||||
}
|
||||
return decodeJSON[appctype.RouteInfo](body)
|
||||
}
|
||||
|
||||
55
vendor/tailscale.com/client/local/serve.go
generated
vendored
Normal file
55
vendor/tailscale.com/client/local/serve.go
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
// GetServeConfig return the current serve config.
|
||||
//
|
||||
// If the serve config is empty, it returns (nil, nil).
|
||||
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
|
||||
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting serve config: %w", err)
|
||||
}
|
||||
sc, err := getServeConfigFromJSON(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
sc.ETag = h.Get("Etag")
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {
|
||||
if err := json.Unmarshal(body, &sc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// SetServeConfig sets or replaces the serving settings.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||
h := make(http.Header)
|
||||
if config != nil {
|
||||
h.Set("If-Match", config.ETag)
|
||||
}
|
||||
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending serve config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
40
vendor/tailscale.com/client/local/syspolicy.go
generated
vendored
Normal file
40
vendor/tailscale.com/client/local/syspolicy.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_syspolicy
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
// GetEffectivePolicy returns the effective policy for the specified scope.
|
||||
func (lc *Client) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
|
||||
scopeID, err := scope.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := lc.get200(ctx, "/localapi/v0/policy/"+string(scopeID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*setting.Snapshot](body)
|
||||
}
|
||||
|
||||
// ReloadEffectivePolicy reloads the effective policy for the specified scope
|
||||
// by reading and merging policy settings from all applicable policy sources.
|
||||
func (lc *Client) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
|
||||
scopeID, err := scope.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/policy/"+string(scopeID), 200, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeJSON[*setting.Snapshot](body)
|
||||
}
|
||||
204
vendor/tailscale.com/client/local/tailnetlock.go
generated
vendored
Normal file
204
vendor/tailscale.com/client/local/tailnetlock.go
generated
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
|
||||
func (lc *Client) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
}
|
||||
|
||||
// NetworkLockInit initializes the tailnet key authority.
|
||||
//
|
||||
// TODO(tom): Plumb through disablement secrets.
|
||||
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
|
||||
var b bytes.Buffer
|
||||
type initRequest struct {
|
||||
Keys []tka.Key
|
||||
DisablementValues [][]byte
|
||||
SupportDisablement []byte
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
}
|
||||
|
||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||
// enable unattended bringup in the locked tailnet.
|
||||
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
|
||||
encodedPrivate, err := tkaKey.MarshalText()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
type wrapRequest struct {
|
||||
TSKey string
|
||||
TKAKey string // key.NLPrivate.MarshalText
|
||||
}
|
||||
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
|
||||
var b bytes.Buffer
|
||||
type modifyRequest struct {
|
||||
AddKeys []tka.Key
|
||||
RemoveKeys []tka.Key
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
|
||||
// rotationPublic, if specified, must be an ed25519 public key.
|
||||
func (lc *Client) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
|
||||
var b bytes.Buffer
|
||||
type signRequest struct {
|
||||
NodeKey key.NodePublic
|
||||
RotationPublic []byte
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
|
||||
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[[]tkatype.MarshaledSignature](body)
|
||||
}
|
||||
|
||||
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||
v := url.Values{}
|
||||
v.Set("limit", fmt.Sprint(maxEntries))
|
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
|
||||
}
|
||||
|
||||
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
|
||||
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error {
|
||||
// This endpoint expects an empty JSON stanza as the payload.
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
|
||||
// in url and returns information extracted from it.
|
||||
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
|
||||
vr := struct {
|
||||
URL string
|
||||
}{url}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
|
||||
}
|
||||
|
||||
return decodeJSON[*tka.DeeplinkValidationResult](body)
|
||||
}
|
||||
|
||||
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
|
||||
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
|
||||
vr := struct {
|
||||
Keys []tkatype.KeyID
|
||||
ForkFrom string
|
||||
}{removeKeys, forkFrom.String()}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
|
||||
func (lc *Client) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
|
||||
r := bytes.NewReader(aum.Serialize())
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
|
||||
func (lc *Client) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
|
||||
r := bytes.NewReader(aum.Serialize())
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockDisable shuts down network-lock across the tailnet.
|
||||
func (lc *Client) NetworkLockDisable(ctx context.Context, secret []byte) error {
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
10
vendor/tailscale.com/client/tailscale/apitype/apitype.go
generated
vendored
10
vendor/tailscale.com/client/tailscale/apitype/apitype.go
generated
vendored
@@ -94,3 +94,13 @@ type DNSQueryResponse struct {
|
||||
// Resolvers is the list of resolvers that the forwarder deemed able to resolve the query.
|
||||
Resolvers []*dnstype.Resolver
|
||||
}
|
||||
|
||||
// OptionalFeatures describes which optional features are enabled in the build.
|
||||
type OptionalFeatures struct {
|
||||
// Features is the map of optional feature names to whether they are
|
||||
// enabled.
|
||||
//
|
||||
// Disabled features may be absent from the map. (That is, false values
|
||||
// are not guaranteed to be present.)
|
||||
Features map[string]bool
|
||||
}
|
||||
|
||||
49
vendor/tailscale.com/client/tailscale/apitype/controltype.go
generated
vendored
49
vendor/tailscale.com/client/tailscale/apitype/controltype.go
generated
vendored
@@ -3,17 +3,50 @@
|
||||
|
||||
package apitype
|
||||
|
||||
// DNSConfig is the DNS configuration for a tailnet
|
||||
// used in /tailnet/{tailnet}/dns/config.
|
||||
type DNSConfig struct {
|
||||
Resolvers []DNSResolver `json:"resolvers"`
|
||||
FallbackResolvers []DNSResolver `json:"fallbackResolvers"`
|
||||
Routes map[string][]DNSResolver `json:"routes"`
|
||||
Domains []string `json:"domains"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
Proxied bool `json:"proxied"`
|
||||
TempCorpIssue13969 string `json:"TempCorpIssue13969,omitempty"`
|
||||
// Resolvers are the global DNS resolvers to use
|
||||
// overriding the local OS configuration.
|
||||
Resolvers []DNSResolver `json:"resolvers"`
|
||||
|
||||
// FallbackResolvers are used as global resolvers when
|
||||
// the client is unable to determine the OS's preferred DNS servers.
|
||||
FallbackResolvers []DNSResolver `json:"fallbackResolvers"`
|
||||
|
||||
// Routes map DNS name suffixes to a set of DNS resolvers,
|
||||
// used for Split DNS and other advanced routing overlays.
|
||||
Routes map[string][]DNSResolver `json:"routes"`
|
||||
|
||||
// Domains are the search domains to use.
|
||||
Domains []string `json:"domains"`
|
||||
|
||||
// Proxied means MagicDNS is enabled.
|
||||
Proxied bool `json:"proxied"`
|
||||
|
||||
// TempCorpIssue13969 is from an internal hack day prototype,
|
||||
// See tailscale/corp#13969.
|
||||
TempCorpIssue13969 string `json:"TempCorpIssue13969,omitempty"`
|
||||
|
||||
// Nameservers are the IP addresses of global nameservers to use.
|
||||
// This is a deprecated format but may still be found in tailnets
|
||||
// that were configured a long time ago. When making updates,
|
||||
// set Resolvers and leave Nameservers empty.
|
||||
Nameservers []string `json:"nameservers"`
|
||||
}
|
||||
|
||||
// DNSResolver is a DNS resolver in a DNS configuration.
|
||||
type DNSResolver struct {
|
||||
Addr string `json:"addr"`
|
||||
// Addr is the address of the DNS resolver.
|
||||
// It is usually an IP address or a DoH URL.
|
||||
// See dnstype.Resolver.Addr for full details.
|
||||
Addr string `json:"addr"`
|
||||
|
||||
// BootstrapResolution is an optional suggested resolution for
|
||||
// the DoT/DoH resolver.
|
||||
BootstrapResolution []string `json:"bootstrapResolution,omitempty"`
|
||||
|
||||
// UseWithExitNode signals this resolver should be used
|
||||
// even when a tailscale exit node is configured on a device.
|
||||
UseWithExitNode bool `json:"useWithExitNode,omitempty"`
|
||||
}
|
||||
|
||||
34
vendor/tailscale.com/client/tailscale/cert.go
generated
vendored
Normal file
34
vendor/tailscale.com/client/tailscale/cert.go
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js && !ts_omit_acme
|
||||
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
)
|
||||
|
||||
// GetCertificate is an alias for [tailscale.com/client/local.GetCertificate].
|
||||
//
|
||||
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.GetCertificate].
|
||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return local.GetCertificate(hi)
|
||||
}
|
||||
|
||||
// CertPair is an alias for [tailscale.com/client/local.CertPair].
|
||||
//
|
||||
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.CertPair].
|
||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
return local.CertPair(ctx, domain)
|
||||
}
|
||||
|
||||
// ExpandSNIName is an alias for [tailscale.com/client/local.ExpandSNIName].
|
||||
//
|
||||
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.ExpandSNIName].
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
return local.ExpandSNIName(ctx, name)
|
||||
}
|
||||
71
vendor/tailscale.com/client/tailscale/localclient_aliases.go
generated
vendored
71
vendor/tailscale.com/client/tailscale/localclient_aliases.go
generated
vendored
@@ -5,102 +5,75 @@ package tailscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
// ErrPeerNotFound is an alias for tailscale.com/client/local.
|
||||
// ErrPeerNotFound is an alias for [tailscale.com/client/local.ErrPeerNotFound].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
var ErrPeerNotFound = local.ErrPeerNotFound
|
||||
|
||||
// LocalClient is an alias for tailscale.com/client/local.
|
||||
// LocalClient is an alias for [tailscale.com/client/local.Client].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
type LocalClient = local.Client
|
||||
|
||||
// IPNBusWatcher is an alias for tailscale.com/client/local.
|
||||
// IPNBusWatcher is an alias for [tailscale.com/client/local.IPNBusWatcher].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
type IPNBusWatcher = local.IPNBusWatcher
|
||||
|
||||
// BugReportOpts is an alias for tailscale.com/client/local.
|
||||
// BugReportOpts is an alias for [tailscale.com/client/local.BugReportOpts].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
type BugReportOpts = local.BugReportOpts
|
||||
|
||||
// DebugPortMapOpts is an alias for tailscale.com/client/local.
|
||||
// PingOpts is an alias for [tailscale.com/client/local.PingOpts].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
type DebugPortmapOpts = local.DebugPortmapOpts
|
||||
|
||||
// PingOpts is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
type PingOpts = local.PingOpts
|
||||
|
||||
// GetCertificate is an alias for tailscale.com/client/local.
|
||||
// SetVersionMismatchHandler is an alias for [tailscale.com/client/local.SetVersionMismatchHandler].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return local.GetCertificate(hi)
|
||||
}
|
||||
|
||||
// SetVersionMismatchHandler is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
|
||||
local.SetVersionMismatchHandler(f)
|
||||
}
|
||||
|
||||
// IsAccessDeniedError is an alias for tailscale.com/client/local.
|
||||
// IsAccessDeniedError is an alias for [tailscale.com/client/local.IsAccessDeniedError].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
func IsAccessDeniedError(err error) bool {
|
||||
return local.IsAccessDeniedError(err)
|
||||
}
|
||||
|
||||
// IsPreconditionsFailedError is an alias for tailscale.com/client/local.
|
||||
// IsPreconditionsFailedError is an alias for [tailscale.com/client/local.IsPreconditionsFailedError].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
func IsPreconditionsFailedError(err error) bool {
|
||||
return local.IsPreconditionsFailedError(err)
|
||||
}
|
||||
|
||||
// WhoIs is an alias for tailscale.com/client/local.
|
||||
// WhoIs is an alias for [tailscale.com/client/local.WhoIs].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.WhoIs].
|
||||
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
||||
return local.WhoIs(ctx, remoteAddr)
|
||||
}
|
||||
|
||||
// Status is an alias for tailscale.com/client/local.
|
||||
// Status is an alias for [tailscale.com/client/local.Status].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return local.Status(ctx)
|
||||
}
|
||||
|
||||
// StatusWithoutPeers is an alias for tailscale.com/client/local.
|
||||
// StatusWithoutPeers is an alias for [tailscale.com/client/local.StatusWithoutPeers].
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return local.StatusWithoutPeers(ctx)
|
||||
}
|
||||
|
||||
// CertPair is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
return local.CertPair(ctx, domain)
|
||||
}
|
||||
|
||||
// ExpandSNIName is an alias for tailscale.com/client/local.
|
||||
//
|
||||
// Deprecated: import tailscale.com/client/local instead.
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
return local.ExpandSNIName(ctx, name)
|
||||
}
|
||||
|
||||
22
vendor/tailscale.com/client/tailscale/tailscale.go
generated
vendored
22
vendor/tailscale.com/client/tailscale/tailscale.go
generated
vendored
@@ -8,7 +8,7 @@
|
||||
// This package is only intended for internal and transitional use.
|
||||
//
|
||||
// Deprecated: the official control plane client is available at
|
||||
// tailscale.com/client/tailscale/v2.
|
||||
// [tailscale.com/client/tailscale/v2].
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
|
||||
// for now. This package is being replaced by tailscale.com/client/tailscale/v2.
|
||||
// for now. This package is being replaced by [tailscale.com/client/tailscale/v2].
|
||||
var I_Acknowledge_This_API_Is_Unstable = false
|
||||
|
||||
// TODO: use url.PathEscape() for deviceID and tailnets when constructing requests.
|
||||
@@ -34,10 +34,10 @@ const maxReadSize = 10 << 20
|
||||
|
||||
// Client makes API calls to the Tailscale control plane API server.
|
||||
//
|
||||
// Use NewClient to instantiate one. Exported fields should be set before
|
||||
// Use [NewClient] to instantiate one. Exported fields should be set before
|
||||
// the client is used and not changed thereafter.
|
||||
//
|
||||
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
|
||||
// Deprecated: use [tailscale.com/client/tailscale/v2] instead.
|
||||
type Client struct {
|
||||
// tailnet is the globally unique identifier for a Tailscale network, such
|
||||
// as "example.com" or "user@gmail.com".
|
||||
@@ -51,7 +51,7 @@ type Client struct {
|
||||
BaseURL string
|
||||
|
||||
// HTTPClient optionally specifies an alternate HTTP client to use.
|
||||
// If nil, http.DefaultClient is used.
|
||||
// If nil, [http.DefaultClient] is used.
|
||||
HTTPClient *http.Client
|
||||
|
||||
// UserAgent optionally specifies an alternate User-Agent header
|
||||
@@ -119,7 +119,7 @@ type AuthMethod interface {
|
||||
modifyRequest(req *http.Request)
|
||||
}
|
||||
|
||||
// APIKey is an AuthMethod for NewClient that authenticates requests
|
||||
// APIKey is an [AuthMethod] for [NewClient] that authenticates requests
|
||||
// using an authkey.
|
||||
type APIKey string
|
||||
|
||||
@@ -133,15 +133,15 @@ func (c *Client) setAuth(r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient is a convenience method for instantiating a new Client.
|
||||
// NewClient is a convenience method for instantiating a new [Client].
|
||||
//
|
||||
// tailnet is the globally unique identifier for a Tailscale network, such
|
||||
// as "example.com" or "user@gmail.com".
|
||||
// If httpClient is nil, then http.DefaultClient is used.
|
||||
// If httpClient is nil, then [http.DefaultClient] is used.
|
||||
// "api.tailscale.com" is set as the BaseURL for the returned client
|
||||
// and can be changed manually by the user.
|
||||
//
|
||||
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
|
||||
// Deprecated: use [tailscale.com/client/tailscale/v2] instead.
|
||||
func NewClient(tailnet string, auth AuthMethod) *Client {
|
||||
return &Client{
|
||||
tailnet: tailnet,
|
||||
@@ -193,9 +193,9 @@ func (e ErrResponse) Error() string {
|
||||
}
|
||||
|
||||
// HandleErrorResponse decodes the error message from the server and returns
|
||||
// an ErrResponse from it.
|
||||
// an [ErrResponse] from it.
|
||||
//
|
||||
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
|
||||
// Deprecated: use [tailscale.com/client/tailscale/v2] instead.
|
||||
func HandleErrorResponse(b []byte, resp *http.Response) error {
|
||||
var errResp ErrResponse
|
||||
if err := json.Unmarshal(b, &errResp); err != nil {
|
||||
|
||||
2
vendor/tailscale.com/client/web/auth.go
generated
vendored
2
vendor/tailscale.com/client/web/auth.go
generated
vendored
@@ -192,7 +192,7 @@ func (s *Server) controlSupportsCheckMode(ctx context.Context) bool {
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
controlURL, err := url.Parse(prefs.ControlURLOrDefault())
|
||||
controlURL, err := url.Parse(prefs.ControlURLOrDefault(s.polc))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
8
vendor/tailscale.com/client/web/package.json
generated
vendored
8
vendor/tailscale.com/client/web/package.json
generated
vendored
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "18.20.4",
|
||||
"node": "22.14.0",
|
||||
"yarn": "1.22.19"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -20,7 +20,7 @@
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
@@ -34,10 +34,10 @@
|
||||
"prettier-plugin-organize-imports": "^3.2.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.7",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^3.5.0",
|
||||
"vitest": "^1.3.1"
|
||||
"vitest": "^1.6.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
|
||||
175
vendor/tailscale.com/client/web/web.go
generated
vendored
175
vendor/tailscale.com/client/web/web.go
generated
vendored
@@ -5,8 +5,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -14,19 +14,20 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/envknob/featureknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -50,6 +52,7 @@ type Server struct {
|
||||
mode ServerMode
|
||||
|
||||
logf logger.Logf
|
||||
polc policyclient.Client // must be non-nil
|
||||
lc *local.Client
|
||||
timeNow func() time.Time
|
||||
|
||||
@@ -60,6 +63,12 @@ type Server struct {
|
||||
cgiMode bool
|
||||
pathPrefix string
|
||||
|
||||
// originOverride is the origin that the web UI is accessible from.
|
||||
// This value is used in the fallback CSRF checks when Sec-Fetch-Site is not
|
||||
// available. In this case the application will compare Host and Origin
|
||||
// header values to determine if the request is from the same origin.
|
||||
originOverride string
|
||||
|
||||
apiHandler http.Handler // serves api endpoints; csrf-protected
|
||||
assetsHandler http.Handler // serves frontend assets
|
||||
assetsCleanup func() // called from Server.Shutdown
|
||||
@@ -134,9 +143,13 @@ type ServerOpts struct {
|
||||
TimeNow func() time.Time
|
||||
|
||||
// Logf optionally provides a logger function.
|
||||
// log.Printf is used as default.
|
||||
// If nil, log.Printf is used as default.
|
||||
Logf logger.Logf
|
||||
|
||||
// PolicyClient, if non-nil, will be used to fetch policy settings.
|
||||
// If nil, the default policy client will be used.
|
||||
PolicyClient policyclient.Client
|
||||
|
||||
// The following two fields are required and used exclusively
|
||||
// in ManageServerMode to facilitate the control server login
|
||||
// check step for authorizing browser sessions.
|
||||
@@ -150,6 +163,9 @@ type ServerOpts struct {
|
||||
// as completed.
|
||||
// This field is required for ManageServerMode mode.
|
||||
WaitAuthURL func(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
|
||||
|
||||
// OriginOverride specifies the origin that the web UI will be accessible from if hosted behind a reverse proxy or CGI.
|
||||
OriginOverride string
|
||||
}
|
||||
|
||||
// NewServer constructs a new Tailscale web client server.
|
||||
@@ -169,15 +185,17 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
opts.LocalClient = &local.Client{}
|
||||
}
|
||||
s = &Server{
|
||||
mode: opts.Mode,
|
||||
logf: opts.Logf,
|
||||
devMode: envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
|
||||
lc: opts.LocalClient,
|
||||
cgiMode: opts.CGIMode,
|
||||
pathPrefix: opts.PathPrefix,
|
||||
timeNow: opts.TimeNow,
|
||||
newAuthURL: opts.NewAuthURL,
|
||||
waitAuthURL: opts.WaitAuthURL,
|
||||
mode: opts.Mode,
|
||||
polc: cmp.Or(opts.PolicyClient, policyclient.Get()),
|
||||
logf: opts.Logf,
|
||||
devMode: envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
|
||||
lc: opts.LocalClient,
|
||||
cgiMode: opts.CGIMode,
|
||||
pathPrefix: opts.PathPrefix,
|
||||
timeNow: opts.TimeNow,
|
||||
newAuthURL: opts.NewAuthURL,
|
||||
waitAuthURL: opts.WaitAuthURL,
|
||||
originOverride: opts.OriginOverride,
|
||||
}
|
||||
if opts.PathPrefix != "" {
|
||||
// Enforce that path prefix always has a single leading '/'
|
||||
@@ -205,7 +223,7 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
|
||||
var metric string
|
||||
s.apiHandler, metric = s.modeAPIHandler(s.mode)
|
||||
s.apiHandler = s.withCSRF(s.apiHandler)
|
||||
s.apiHandler = s.csrfProtect(s.apiHandler)
|
||||
|
||||
// Don't block startup on reporting metric.
|
||||
// Report in separate go routine with 5 second timeout.
|
||||
@@ -218,23 +236,64 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) withCSRF(h http.Handler) http.Handler {
|
||||
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
||||
|
||||
// ref https://github.com/tailscale/tailscale/pull/14822
|
||||
// signal to the CSRF middleware that the request is being served over
|
||||
// plaintext HTTP to skip TLS-only header checks.
|
||||
withSetPlaintext := func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r = csrf.PlaintextHTTPRequest(r)
|
||||
func (s *Server) csrfProtect(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// CSRF is not required for GET, HEAD, or OPTIONS requests.
|
||||
if slices.Contains([]string{"GET", "HEAD", "OPTIONS"}, r.Method) {
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NB: the order of the withSetPlaintext and csrfProtect calls is important
|
||||
// to ensure that we signal to the CSRF middleware that the request is being
|
||||
// served over plaintext HTTP and not over TLS as it presumes by default.
|
||||
return withSetPlaintext(csrfProtect(h))
|
||||
// first attempt to use Sec-Fetch-Site header (sent by all modern
|
||||
// browsers to "potentially trustworthy" origins i.e. localhost or those
|
||||
// served over HTTPS)
|
||||
secFetchSite := r.Header.Get("Sec-Fetch-Site")
|
||||
if secFetchSite == "same-origin" {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
} else if secFetchSite != "" {
|
||||
http.Error(w, fmt.Sprintf("CSRF request denied with Sec-Fetch-Site %q", secFetchSite), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// if Sec-Fetch-Site is not available we presume we are operating over HTTP.
|
||||
// We fall back to comparing the Origin & Host headers.
|
||||
|
||||
// use the Host header to determine the expected origin
|
||||
// (use the override if set to allow for reverse proxying)
|
||||
host := r.Host
|
||||
if host == "" {
|
||||
http.Error(w, "CSRF request denied with no Host header", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if s.originOverride != "" {
|
||||
host = s.originOverride
|
||||
}
|
||||
|
||||
originHeader := r.Header.Get("Origin")
|
||||
if originHeader == "" {
|
||||
http.Error(w, "CSRF request denied with no Origin header", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
parsedOrigin, err := url.Parse(originHeader)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("CSRF request denied with invalid Origin %q", r.Header.Get("Origin")), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
origin := parsedOrigin.Host
|
||||
if origin == "" {
|
||||
http.Error(w, "CSRF request denied with no host in the Origin header", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if origin != host {
|
||||
http.Error(w, fmt.Sprintf("CSRF request denied with mismatched Origin %q and Host %q", origin, host), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
|
||||
@@ -438,6 +497,10 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
|
||||
// Client using system-specific auth.
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
if !buildfeatures.HasSynology {
|
||||
// Synology support not built in.
|
||||
return false
|
||||
}
|
||||
authorized, _ := authorizeSynology(r)
|
||||
return authorized
|
||||
case distro.QNAP:
|
||||
@@ -452,7 +515,6 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
|
||||
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
|
||||
// which protects the handler using gorilla csrf.
|
||||
func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
switch {
|
||||
case r.URL.Path == "/api/data" && r.Method == httpm.GET:
|
||||
s.serveGetNodeData(w, r)
|
||||
@@ -575,7 +637,6 @@ func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api")
|
||||
switch {
|
||||
case path == "/data" && r.Method == httpm.GET:
|
||||
@@ -902,7 +963,7 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
||||
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
|
||||
RunningSSHServer: prefs.RunSSH,
|
||||
URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
|
||||
ControlAdminURL: prefs.AdminPageURL(),
|
||||
ControlAdminURL: prefs.AdminPageURL(s.polc),
|
||||
LicensesURL: licenses.LicensesURL(),
|
||||
Features: availableFeatures(),
|
||||
|
||||
@@ -922,9 +983,18 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
||||
data.ClientVersion = cv
|
||||
}
|
||||
|
||||
if st.CurrentTailnet != nil {
|
||||
data.TailnetName = st.CurrentTailnet.MagicDNSSuffix
|
||||
data.DomainName = st.CurrentTailnet.Name
|
||||
profile, _, err := s.lc.ProfileStatus(r.Context())
|
||||
if err != nil {
|
||||
s.logf("error fetching profiles: %v", err)
|
||||
// If for some reason we can't fetch profiles,
|
||||
// continue to use st.CurrentTailnet if set.
|
||||
if st.CurrentTailnet != nil {
|
||||
data.TailnetName = st.CurrentTailnet.MagicDNSSuffix
|
||||
data.DomainName = st.CurrentTailnet.Name
|
||||
}
|
||||
} else {
|
||||
data.TailnetName = profile.NetworkProfile.MagicDNSName
|
||||
data.DomainName = profile.NetworkProfile.DisplayNameOrDefault()
|
||||
}
|
||||
if st.Self.Tags != nil {
|
||||
data.Tags = st.Self.Tags.AsSlice()
|
||||
@@ -984,7 +1054,7 @@ func availableFeatures() map[string]bool {
|
||||
"advertise-routes": true, // available on all platforms
|
||||
"use-exit-node": featureknob.CanUseExitNode() == nil,
|
||||
"ssh": featureknob.CanRunTailscaleSSH() == nil,
|
||||
"auto-update": version.IsUnstableBuild() && clientupdate.CanAutoUpdate(),
|
||||
"auto-update": version.IsUnstableBuild() && feature.CanAutoUpdate(),
|
||||
}
|
||||
return features
|
||||
}
|
||||
@@ -1276,37 +1346,6 @@ func (s *Server) proxyRequestToLocalAPI(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// csrfKey returns a key that can be used for CSRF protection.
|
||||
// If an error occurs during key creation, the error is logged and the active process terminated.
|
||||
// If the server is running in CGI mode, the key is cached to disk and reused between requests.
|
||||
// If an error occurs during key storage, the error is logged and the active process terminated.
|
||||
func (s *Server) csrfKey() []byte {
|
||||
csrfFile := filepath.Join(os.TempDir(), "tailscale-web-csrf.key")
|
||||
|
||||
// if running in CGI mode, try to read from disk, but ignore errors
|
||||
if s.cgiMode {
|
||||
key, _ := os.ReadFile(csrfFile)
|
||||
if len(key) == 32 {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
// create a new key
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
log.Fatalf("error generating CSRF key: %v", err)
|
||||
}
|
||||
|
||||
// if running in CGI mode, try to write the newly created key to disk, and exit if it fails.
|
||||
if s.cgiMode {
|
||||
if err := os.WriteFile(csrfFile, key, 0600); err != nil {
|
||||
log.Fatalf("unable to store CSRF key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// enforcePrefix returns a HandlerFunc that enforces a given path prefix is used in requests,
|
||||
// then strips it before invoking h.
|
||||
// Unlike http.StripPrefix, it does not return a 404 if the prefix is not present.
|
||||
|
||||
739
vendor/tailscale.com/client/web/yarn.lock
generated
vendored
739
vendor/tailscale.com/client/web/yarn.lock
generated
vendored
File diff suppressed because it is too large
Load Diff
1228
vendor/tailscale.com/clientupdate/clientupdate.go
generated
vendored
1228
vendor/tailscale.com/clientupdate/clientupdate.go
generated
vendored
File diff suppressed because it is too large
Load Diff
20
vendor/tailscale.com/clientupdate/clientupdate_downloads.go
generated
vendored
20
vendor/tailscale.com/clientupdate/clientupdate_downloads.go
generated
vendored
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (linux && !android) || windows
|
||||
|
||||
package clientupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/clientupdate/distsign"
|
||||
)
|
||||
|
||||
func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) {
|
||||
c, err := distsign.NewClient(up.Logf, up.PkgsAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Download(context.Background(), pathSrc, fileDst)
|
||||
}
|
||||
10
vendor/tailscale.com/clientupdate/clientupdate_not_downloads.go
generated
vendored
10
vendor/tailscale.com/clientupdate/clientupdate_not_downloads.go
generated
vendored
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !((linux && !android) || windows)
|
||||
|
||||
package clientupdate
|
||||
|
||||
func (up *Updater) downloadURLToFile(pathSrc, fileDst string) (ret error) {
|
||||
panic("unreachable")
|
||||
}
|
||||
225
vendor/tailscale.com/clientupdate/clientupdate_windows.go
generated
vendored
225
vendor/tailscale.com/clientupdate/clientupdate_windows.go
generated
vendored
@@ -1,225 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Windows-specific stuff that can't go in clientupdate.go because it needs
|
||||
// x/sys/windows.
|
||||
|
||||
package clientupdate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/util/winutil/authenticode"
|
||||
)
|
||||
|
||||
const (
|
||||
// winMSIEnv is the environment variable that, if set, is the MSI file for
|
||||
// the update command to install. It's passed like this so we can stop the
|
||||
// tailscale.exe process from running before the msiexec process runs and
|
||||
// tries to overwrite ourselves.
|
||||
winMSIEnv = "TS_UPDATE_WIN_MSI"
|
||||
// winExePathEnv is the environment variable that is set along with
|
||||
// winMSIEnv and carries the full path of the calling tailscale.exe binary.
|
||||
// It is used to re-launch the GUI process (tailscale-ipn.exe) after
|
||||
// install is complete.
|
||||
winExePathEnv = "TS_UPDATE_WIN_EXE_PATH"
|
||||
)
|
||||
|
||||
func makeSelfCopy() (origPathExe, tmpPathExe string, err error) {
|
||||
selfExe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
f, err := os.Open(selfExe)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer f.Close()
|
||||
f2, err := os.CreateTemp("", "tailscale-updater-*.exe")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := markTempFileWindows(f2.Name()); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if _, err := io.Copy(f2, f); err != nil {
|
||||
f2.Close()
|
||||
return "", "", err
|
||||
}
|
||||
return selfExe, f2.Name(), f2.Close()
|
||||
}
|
||||
|
||||
func markTempFileWindows(name string) error {
|
||||
name16 := windows.StringToUTF16Ptr(name)
|
||||
return windows.MoveFileEx(name16, nil, windows.MOVEFILE_DELAY_UNTIL_REBOOT)
|
||||
}
|
||||
|
||||
const certSubjectTailscale = "Tailscale Inc."
|
||||
|
||||
func verifyAuthenticode(path string) error {
|
||||
return authenticode.Verify(path, certSubjectTailscale)
|
||||
}
|
||||
|
||||
func (up *Updater) updateWindows() error {
|
||||
if msi := os.Getenv(winMSIEnv); msi != "" {
|
||||
// stdout/stderr from this part of the install could be lost since the
|
||||
// parent tailscaled is replaced. Create a temp log file to have some
|
||||
// output to debug with in case update fails.
|
||||
close, err := up.switchOutputToFile()
|
||||
if err != nil {
|
||||
up.Logf("failed to create log file for installation: %v; proceeding with existing outputs", err)
|
||||
} else {
|
||||
defer close.Close()
|
||||
}
|
||||
|
||||
up.Logf("installing %v ...", msi)
|
||||
if err := up.installMSI(msi); err != nil {
|
||||
up.Logf("MSI install failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
up.Logf("success.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !winutil.IsCurrentProcessElevated() {
|
||||
return errors.New(`update must be run as Administrator
|
||||
|
||||
you can run the command prompt as Administrator one of these ways:
|
||||
* right-click cmd.exe, select 'Run as administrator'
|
||||
* press Windows+x, then press a
|
||||
* press Windows+r, type in "cmd", then press Ctrl+Shift+Enter`)
|
||||
}
|
||||
ver, err := requestedTailscaleVersion(up.Version, up.Track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arch := runtime.GOARCH
|
||||
if arch == "386" {
|
||||
arch = "x86"
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
return nil
|
||||
}
|
||||
|
||||
tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
|
||||
msiDir := filepath.Join(tsDir, "MSICache")
|
||||
if fi, err := os.Stat(tsDir); err != nil {
|
||||
return fmt.Errorf("expected %s to exist, got stat error: %w", tsDir, err)
|
||||
} else if !fi.IsDir() {
|
||||
return fmt.Errorf("expected %s to be a directory; got %v", tsDir, fi.Mode())
|
||||
}
|
||||
if err := os.MkdirAll(msiDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
up.cleanupOldDownloads(filepath.Join(msiDir, "*.msi"))
|
||||
pkgsPath := fmt.Sprintf("%s/tailscale-setup-%s-%s.msi", up.Track, ver, arch)
|
||||
msiTarget := filepath.Join(msiDir, path.Base(pkgsPath))
|
||||
if err := up.downloadURLToFile(pkgsPath, msiTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
up.Logf("verifying MSI authenticode...")
|
||||
if err := verifyAuthenticode(msiTarget); err != nil {
|
||||
return fmt.Errorf("authenticode verification of %s failed: %w", msiTarget, err)
|
||||
}
|
||||
up.Logf("authenticode verification succeeded")
|
||||
|
||||
up.Logf("making tailscale.exe copy to switch to...")
|
||||
up.cleanupOldDownloads(filepath.Join(os.TempDir(), "tailscale-updater-*.exe"))
|
||||
selfOrig, selfCopy, err := makeSelfCopy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(selfCopy)
|
||||
up.Logf("running tailscale.exe copy for final install...")
|
||||
|
||||
cmd := exec.Command(selfCopy, "update")
|
||||
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget, winExePathEnv+"="+selfOrig)
|
||||
cmd.Stdout = up.Stderr
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Once it's started, exit ourselves, so the binary is free
|
||||
// to be replaced.
|
||||
os.Exit(0)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (up *Updater) installMSI(msi string) error {
|
||||
var err error
|
||||
for tries := 0; tries < 2; tries++ {
|
||||
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/norestart", "/qn")
|
||||
cmd.Dir = filepath.Dir(msi)
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
up.Logf("Install attempt failed: %v", err)
|
||||
uninstallVersion := up.currentVersion
|
||||
if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
|
||||
uninstallVersion = v
|
||||
}
|
||||
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
|
||||
up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion)
|
||||
cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
up.Logf("msiexec uninstall: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func msiUUIDForVersion(ver string) string {
|
||||
arch := runtime.GOARCH
|
||||
if arch == "386" {
|
||||
arch = "x86"
|
||||
}
|
||||
track, err := versionToTrack(ver)
|
||||
if err != nil {
|
||||
track = UnstableTrack
|
||||
}
|
||||
msiURL := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", track, ver, arch)
|
||||
return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
|
||||
}
|
||||
|
||||
func (up *Updater) switchOutputToFile() (io.Closer, error) {
|
||||
var logFilePath string
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
logFilePath = filepath.Join(os.TempDir(), "tailscale-updater.log")
|
||||
} else {
|
||||
logFilePath = strings.TrimSuffix(exePath, ".exe") + ".log"
|
||||
}
|
||||
|
||||
up.Logf("writing update output to %q", logFilePath)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
up.Logf = func(m string, args ...any) {
|
||||
fmt.Fprintf(logFile, m+"\n", args...)
|
||||
}
|
||||
up.Stdout = logFile
|
||||
up.Stderr = logFile
|
||||
return logFile, nil
|
||||
}
|
||||
486
vendor/tailscale.com/clientupdate/distsign/distsign.go
generated
vendored
486
vendor/tailscale.com/clientupdate/distsign/distsign.go
generated
vendored
@@ -1,486 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package distsign implements signature and validation of arbitrary
|
||||
// distributable files.
|
||||
//
|
||||
// There are 3 parties in this exchange:
|
||||
// - builder, which creates files, signs them with signing keys and publishes
|
||||
// to server
|
||||
// - server, which distributes public signing keys, files and signatures
|
||||
// - client, which downloads files and signatures from server, and validates
|
||||
// the signatures
|
||||
//
|
||||
// There are 2 types of keys:
|
||||
// - signing keys, that sign individual distributable files on the builder
|
||||
// - root keys, that sign signing keys and are kept offline
|
||||
//
|
||||
// root keys -(sign)-> signing keys -(sign)-> files
|
||||
//
|
||||
// All keys are asymmetric Ed25519 key pairs.
|
||||
//
|
||||
// The server serves static files under some known prefix. The kinds of files are:
|
||||
// - distsign.pub - bundle of PEM-encoded public signing keys
|
||||
// - distsign.pub.sig - signature of distsign.pub using one of the root keys
|
||||
// - $file - any distributable file
|
||||
// - $file.sig - signature of $file using any of the signing keys
|
||||
//
|
||||
// The root public keys are baked into the client software at compile time.
|
||||
// These keys are long-lived and prove the validity of current signing keys
|
||||
// from distsign.pub. To rotate root keys, a new client release must be
|
||||
// published, they are not rotated dynamically. There are multiple root keys in
|
||||
// different locations specifically to allow this rotation without using the
|
||||
// discarded root key for any new signatures.
|
||||
//
|
||||
// The signing public keys are fetched by the client dynamically before every
|
||||
// download and can be rotated more readily, assuming that most deployed
|
||||
// clients trust the root keys used to issue fresh signing keys.
|
||||
package distsign
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hdevalence/ed25519consensus"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
const (
|
||||
pemTypeRootPrivate = "ROOT PRIVATE KEY"
|
||||
pemTypeRootPublic = "ROOT PUBLIC KEY"
|
||||
pemTypeSigningPrivate = "SIGNING PRIVATE KEY"
|
||||
pemTypeSigningPublic = "SIGNING PUBLIC KEY"
|
||||
|
||||
downloadSizeLimit = 1 << 29 // 512MB
|
||||
signingKeysSizeLimit = 1 << 20 // 1MB
|
||||
signatureSizeLimit = ed25519.SignatureSize
|
||||
)
|
||||
|
||||
// RootKey is a root key used to sign signing keys.
|
||||
type RootKey struct {
|
||||
k ed25519.PrivateKey
|
||||
}
|
||||
|
||||
// GenerateRootKey generates a new root key pair and encodes it as PEM.
|
||||
func GenerateRootKey() (priv, pub []byte, err error) {
|
||||
pub, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: pemTypeRootPrivate,
|
||||
Bytes: []byte(priv),
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: pemTypeRootPublic,
|
||||
Bytes: []byte(pub),
|
||||
}), nil
|
||||
}
|
||||
|
||||
// ParseRootKey parses the PEM-encoded private root key. The key must be in the
|
||||
// same format as returned by GenerateRootKey.
|
||||
func ParseRootKey(privKey []byte) (*RootKey, error) {
|
||||
k, err := parsePrivateKey(privKey, pemTypeRootPrivate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse root key: %w", err)
|
||||
}
|
||||
return &RootKey{k: k}, nil
|
||||
}
|
||||
|
||||
// SignSigningKeys signs the bundle of public signing keys. The bundle must be
|
||||
// a sequence of PEM blocks joined with newlines.
|
||||
func (r *RootKey) SignSigningKeys(pubBundle []byte) ([]byte, error) {
|
||||
if _, err := ParseSigningKeyBundle(pubBundle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ed25519.Sign(r.k, pubBundle), nil
|
||||
}
|
||||
|
||||
// SigningKey is a signing key used to sign packages.
|
||||
type SigningKey struct {
|
||||
k ed25519.PrivateKey
|
||||
}
|
||||
|
||||
// GenerateSigningKey generates a new signing key pair and encodes it as PEM.
|
||||
func GenerateSigningKey() (priv, pub []byte, err error) {
|
||||
pub, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: pemTypeSigningPrivate,
|
||||
Bytes: []byte(priv),
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: pemTypeSigningPublic,
|
||||
Bytes: []byte(pub),
|
||||
}), nil
|
||||
}
|
||||
|
||||
// ParseSigningKey parses the PEM-encoded private signing key. The key must be
|
||||
// in the same format as returned by GenerateSigningKey.
|
||||
func ParseSigningKey(privKey []byte) (*SigningKey, error) {
|
||||
k, err := parsePrivateKey(privKey, pemTypeSigningPrivate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse root key: %w", err)
|
||||
}
|
||||
return &SigningKey{k: k}, nil
|
||||
}
|
||||
|
||||
// SignPackageHash signs the hash and the length of a package. Use PackageHash
|
||||
// to compute the inputs.
|
||||
func (s *SigningKey) SignPackageHash(hash []byte, len int64) ([]byte, error) {
|
||||
if len <= 0 {
|
||||
return nil, fmt.Errorf("package length must be positive, got %d", len)
|
||||
}
|
||||
msg := binary.LittleEndian.AppendUint64(hash, uint64(len))
|
||||
return ed25519.Sign(s.k, msg), nil
|
||||
}
|
||||
|
||||
// PackageHash is a hash.Hash that counts the number of bytes written. Use it
|
||||
// to get the hash and length inputs to SigningKey.SignPackageHash.
|
||||
type PackageHash struct {
|
||||
hash.Hash
|
||||
len int64
|
||||
}
|
||||
|
||||
// NewPackageHash returns an initialized PackageHash using BLAKE2s.
|
||||
func NewPackageHash() *PackageHash {
|
||||
h, err := blake2s.New256(nil)
|
||||
if err != nil {
|
||||
// Should never happen with a nil key passed to blake2s.
|
||||
panic(err)
|
||||
}
|
||||
return &PackageHash{Hash: h}
|
||||
}
|
||||
|
||||
func (ph *PackageHash) Write(b []byte) (int, error) {
|
||||
ph.len += int64(len(b))
|
||||
return ph.Hash.Write(b)
|
||||
}
|
||||
|
||||
// Reset the PackageHash to its initial state.
|
||||
func (ph *PackageHash) Reset() {
|
||||
ph.len = 0
|
||||
ph.Hash.Reset()
|
||||
}
|
||||
|
||||
// Len returns the total number of bytes written.
|
||||
func (ph *PackageHash) Len() int64 { return ph.len }
|
||||
|
||||
// Client downloads and validates files from a distribution server.
|
||||
type Client struct {
|
||||
logf logger.Logf
|
||||
roots []ed25519.PublicKey
|
||||
pkgsAddr *url.URL
|
||||
}
|
||||
|
||||
// NewClient returns a new client for distribution server located at pkgsAddr,
|
||||
// and uses embedded root keys from the roots/ subdirectory of this package.
|
||||
func NewClient(logf logger.Logf, pkgsAddr string) (*Client, error) {
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
u, err := url.Parse(pkgsAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid pkgsAddr %q: %w", pkgsAddr, err)
|
||||
}
|
||||
return &Client{logf: logf, roots: roots(), pkgsAddr: u}, nil
|
||||
}
|
||||
|
||||
func (c *Client) url(path string) string {
|
||||
return c.pkgsAddr.JoinPath(path).String()
|
||||
}
|
||||
|
||||
// Download fetches a file at path srcPath from pkgsAddr passed in NewClient.
|
||||
// The file is downloaded to dstPath and its signature is validated using the
|
||||
// embedded root keys. Download returns an error if anything goes wrong with
|
||||
// the actual file download or with signature validation.
|
||||
func (c *Client) Download(ctx context.Context, srcPath, dstPath string) error {
|
||||
// Always fetch a fresh signing key.
|
||||
sigPub, err := c.signingKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcURL := c.url(srcPath)
|
||||
sigURL := srcURL + ".sig"
|
||||
|
||||
c.logf("Downloading %q", srcURL)
|
||||
dstPathUnverified := dstPath + ".unverified"
|
||||
hash, len, err := c.download(ctx, srcURL, dstPathUnverified, downloadSizeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logf("Downloading %q", sigURL)
|
||||
sig, err := fetch(sigURL, signatureSizeLimit)
|
||||
if err != nil {
|
||||
// Best-effort clean up of downloaded package.
|
||||
os.Remove(dstPathUnverified)
|
||||
return err
|
||||
}
|
||||
msg := binary.LittleEndian.AppendUint64(hash, uint64(len))
|
||||
if !VerifyAny(sigPub, msg, sig) {
|
||||
// Best-effort clean up of downloaded package.
|
||||
os.Remove(dstPathUnverified)
|
||||
return fmt.Errorf("signature %q for file %q does not validate with the current release signing key; either you are under attack, or attempting to download an old version of Tailscale which was signed with an older signing key", sigURL, srcURL)
|
||||
}
|
||||
c.logf("Signature OK")
|
||||
|
||||
if err := os.Rename(dstPathUnverified, dstPath); err != nil {
|
||||
return fmt.Errorf("failed to move %q to %q after signature validation", dstPathUnverified, dstPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateLocalBinary fetches the latest signature associated with the binary
|
||||
// at srcURLPath and uses it to validate the file located on disk via
|
||||
// localFilePath. ValidateLocalBinary returns an error if anything goes wrong
|
||||
// with the signature download or with signature validation.
|
||||
func (c *Client) ValidateLocalBinary(srcURLPath, localFilePath string) error {
|
||||
// Always fetch a fresh signing key.
|
||||
sigPub, err := c.signingKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcURL := c.url(srcURLPath)
|
||||
sigURL := srcURL + ".sig"
|
||||
|
||||
localFile, err := os.Open(localFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer localFile.Close()
|
||||
|
||||
h := NewPackageHash()
|
||||
_, err = io.Copy(h, localFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, hashLen := h.Sum(nil), h.Len()
|
||||
|
||||
c.logf("Downloading %q", sigURL)
|
||||
sig, err := fetch(sigURL, signatureSizeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := binary.LittleEndian.AppendUint64(hash, uint64(hashLen))
|
||||
if !VerifyAny(sigPub, msg, sig) {
|
||||
return fmt.Errorf("signature %q for file %q does not validate with the current release signing key; either you are under attack, or attempting to download an old version of Tailscale which was signed with an older signing key", sigURL, localFilePath)
|
||||
}
|
||||
c.logf("Signature OK")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// signingKeys fetches current signing keys from the server and validates them
|
||||
// against the roots. Should be called before validation of any downloaded file
|
||||
// to get the fresh keys.
|
||||
func (c *Client) signingKeys() ([]ed25519.PublicKey, error) {
|
||||
keyURL := c.url("distsign.pub")
|
||||
sigURL := keyURL + ".sig"
|
||||
raw, err := fetch(keyURL, signingKeysSizeLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig, err := fetch(sigURL, signatureSizeLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !VerifyAny(c.roots, raw, sig) {
|
||||
return nil, fmt.Errorf("signature %q for key %q does not validate with any known root key; either you are under attack, or running a very old version of Tailscale with outdated root keys", sigURL, keyURL)
|
||||
}
|
||||
|
||||
keys, err := ParseSigningKeyBundle(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse signing key bundle from %q: %w", keyURL, err)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// fetch reads the response body from url into memory, up to limit bytes.
|
||||
func fetch(url string, limit int64) ([]byte, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return io.ReadAll(io.LimitReader(resp.Body, limit))
|
||||
}
|
||||
|
||||
// download writes the response body of url into a local file at dst, up to
|
||||
// limit bytes. On success, the returned value is a BLAKE2s hash of the file.
|
||||
func (c *Client) download(ctx context.Context, url, dst string, limit int64) ([]byte, int64, error) {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
defer tr.CloseIdleConnections()
|
||||
hc := &http.Client{Transport: tr}
|
||||
|
||||
quickCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
headReq := must.Get(http.NewRequestWithContext(quickCtx, httpm.HEAD, url, nil))
|
||||
|
||||
res, err := hc.Do(headReq)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, 0, fmt.Errorf("HEAD %q: %v", url, res.Status)
|
||||
}
|
||||
if res.ContentLength <= 0 {
|
||||
return nil, 0, fmt.Errorf("HEAD %q: unexpected Content-Length %v", url, res.ContentLength)
|
||||
}
|
||||
c.logf("Download size: %v", res.ContentLength)
|
||||
|
||||
dlReq := must.Get(http.NewRequestWithContext(ctx, httpm.GET, url, nil))
|
||||
dlRes, err := hc.Do(dlReq)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer dlRes.Body.Close()
|
||||
// TODO(bradfitz): resume from existing partial file on disk
|
||||
if dlRes.StatusCode != http.StatusOK {
|
||||
return nil, 0, fmt.Errorf("GET %q: %v", url, dlRes.Status)
|
||||
}
|
||||
|
||||
of, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer of.Close()
|
||||
pw := &progressWriter{total: res.ContentLength, logf: c.logf}
|
||||
h := NewPackageHash()
|
||||
n, err := io.Copy(io.MultiWriter(of, h, pw), io.LimitReader(dlRes.Body, limit))
|
||||
if err != nil {
|
||||
return nil, n, err
|
||||
}
|
||||
if n != res.ContentLength {
|
||||
return nil, n, fmt.Errorf("GET %q: downloaded %v, want %v", url, n, res.ContentLength)
|
||||
}
|
||||
if err := dlRes.Body.Close(); err != nil {
|
||||
return nil, n, err
|
||||
}
|
||||
if err := of.Close(); err != nil {
|
||||
return nil, n, err
|
||||
}
|
||||
pw.print()
|
||||
|
||||
return h.Sum(nil), h.Len(), nil
|
||||
}
|
||||
|
||||
type progressWriter struct {
|
||||
done int64
|
||||
total int64
|
||||
lastPrint time.Time
|
||||
logf logger.Logf
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (n int, err error) {
|
||||
pw.done += int64(len(p))
|
||||
if time.Since(pw.lastPrint) > 2*time.Second {
|
||||
pw.print()
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (pw *progressWriter) print() {
|
||||
pw.lastPrint = time.Now()
|
||||
pw.logf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
|
||||
}
|
||||
|
||||
func parsePrivateKey(data []byte, typeTag string) (ed25519.PrivateKey, error) {
|
||||
b, rest := pem.Decode(data)
|
||||
if b == nil {
|
||||
return nil, errors.New("failed to decode PEM data")
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, errors.New("trailing PEM data")
|
||||
}
|
||||
if b.Type != typeTag {
|
||||
return nil, fmt.Errorf("PEM type is %q, want %q", b.Type, typeTag)
|
||||
}
|
||||
if len(b.Bytes) != ed25519.PrivateKeySize {
|
||||
return nil, errors.New("private key has incorrect length for an Ed25519 private key")
|
||||
}
|
||||
return ed25519.PrivateKey(b.Bytes), nil
|
||||
}
|
||||
|
||||
// ParseSigningKeyBundle parses the bundle of PEM-encoded public signing keys.
|
||||
func ParseSigningKeyBundle(bundle []byte) ([]ed25519.PublicKey, error) {
|
||||
return parsePublicKeyBundle(bundle, pemTypeSigningPublic)
|
||||
}
|
||||
|
||||
// ParseRootKeyBundle parses the bundle of PEM-encoded public root keys.
|
||||
func ParseRootKeyBundle(bundle []byte) ([]ed25519.PublicKey, error) {
|
||||
return parsePublicKeyBundle(bundle, pemTypeRootPublic)
|
||||
}
|
||||
|
||||
func parsePublicKeyBundle(bundle []byte, typeTag string) ([]ed25519.PublicKey, error) {
|
||||
var keys []ed25519.PublicKey
|
||||
for len(bundle) > 0 {
|
||||
pub, rest, err := parsePublicKey(bundle, typeTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys = append(keys, pub)
|
||||
bundle = rest
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, errors.New("no signing keys found in the bundle")
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func parseSinglePublicKey(data []byte, typeTag string) (ed25519.PublicKey, error) {
|
||||
pub, rest, err := parsePublicKey(data, typeTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, errors.New("trailing PEM data")
|
||||
}
|
||||
return pub, err
|
||||
}
|
||||
|
||||
func parsePublicKey(data []byte, typeTag string) (pub ed25519.PublicKey, rest []byte, retErr error) {
|
||||
b, rest := pem.Decode(data)
|
||||
if b == nil {
|
||||
return nil, nil, errors.New("failed to decode PEM data")
|
||||
}
|
||||
if b.Type != typeTag {
|
||||
return nil, nil, fmt.Errorf("PEM type is %q, want %q", b.Type, typeTag)
|
||||
}
|
||||
if len(b.Bytes) != ed25519.PublicKeySize {
|
||||
return nil, nil, errors.New("public key has incorrect length for an Ed25519 public key")
|
||||
}
|
||||
return ed25519.PublicKey(b.Bytes), rest, nil
|
||||
}
|
||||
|
||||
// VerifyAny verifies whether sig is valid for msg using any of the keys.
|
||||
// VerifyAny will panic if any of the keys have the wrong size for Ed25519.
|
||||
func VerifyAny(keys []ed25519.PublicKey, msg, sig []byte) bool {
|
||||
for _, k := range keys {
|
||||
if ed25519consensus.Verify(k, msg, sig) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
54
vendor/tailscale.com/clientupdate/distsign/roots.go
generated
vendored
54
vendor/tailscale.com/clientupdate/distsign/roots.go
generated
vendored
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package distsign
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed roots
|
||||
var rootsFS embed.FS
|
||||
|
||||
var roots = sync.OnceValue(func() []ed25519.PublicKey {
|
||||
roots, err := parseRoots()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return roots
|
||||
})
|
||||
|
||||
func parseRoots() ([]ed25519.PublicKey, error) {
|
||||
files, err := rootsFS.ReadDir("roots")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var keys []ed25519.PublicKey
|
||||
for _, f := range files {
|
||||
if !f.Type().IsRegular() {
|
||||
continue
|
||||
}
|
||||
if filepath.Ext(f.Name()) != ".pem" {
|
||||
continue
|
||||
}
|
||||
raw, err := rootsFS.ReadFile(path.Join("roots", f.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := parseSinglePublicKey(raw, pemTypeRootPublic)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing root key %q: %w", f.Name(), err)
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, errors.New("no embedded root keys, please check clientupdate/distsign/roots/")
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
3
vendor/tailscale.com/clientupdate/distsign/roots/crawshaw-root.pem
generated
vendored
3
vendor/tailscale.com/clientupdate/distsign/roots/crawshaw-root.pem
generated
vendored
@@ -1,3 +0,0 @@
|
||||
-----BEGIN ROOT PUBLIC KEY-----
|
||||
Psrabv2YNiEDhPlnLVSMtB5EKACm7zxvKxfvYD4i7X8=
|
||||
-----END ROOT PUBLIC KEY-----
|
||||
3
vendor/tailscale.com/clientupdate/distsign/roots/distsign-prod-root-1-pub.pem
generated
vendored
3
vendor/tailscale.com/clientupdate/distsign/roots/distsign-prod-root-1-pub.pem
generated
vendored
@@ -1,3 +0,0 @@
|
||||
-----BEGIN ROOT PUBLIC KEY-----
|
||||
ZjjKhUHBtLNRSO1dhOTjrXJGJ8lDe1594WM2XDuheVQ=
|
||||
-----END ROOT PUBLIC KEY-----
|
||||
3
vendor/tailscale.com/control/controlbase/conn.go
generated
vendored
3
vendor/tailscale.com/control/controlbase/conn.go
generated
vendored
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"golang.org/x/crypto/blake2s"
|
||||
chp "golang.org/x/crypto/chacha20poly1305"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
@@ -48,7 +49,7 @@ type Conn struct {
|
||||
|
||||
// rxState is all the Conn state that Read uses.
|
||||
type rxState struct {
|
||||
sync.Mutex
|
||||
syncs.Mutex
|
||||
cipher cipher.AEAD
|
||||
nonce nonce
|
||||
buf *maxMsgBuffer // or nil when reads exhausted
|
||||
|
||||
139
vendor/tailscale.com/control/controlclient/auto.go
generated
vendored
139
vendor/tailscale.com/control/controlclient/auto.go
generated
vendored
@@ -12,7 +12,6 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
@@ -21,8 +20,10 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/util/backoff"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/execqueue"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
type LoginGoal struct {
|
||||
@@ -117,14 +118,13 @@ type Auto struct {
|
||||
logf logger.Logf
|
||||
closed bool
|
||||
updateCh chan struct{} // readable when we should inform the server of a change
|
||||
observer Observer // called to update Client status; always non-nil
|
||||
observer Observer // if non-nil, called to update Client status
|
||||
observerQueue execqueue.ExecQueue
|
||||
shutdownFn func() // to be called prior to shutdown or nil
|
||||
|
||||
unregisterHealthWatch func()
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
|
||||
started bool // whether [Auto.Start] has been called
|
||||
wantLoggedIn bool // whether the user wants to be logged in per last method call
|
||||
urlToVisit string // the last url we were told to visit
|
||||
expiry time.Time
|
||||
@@ -140,7 +140,6 @@ type Auto struct {
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
inMapPoll bool // true once we get the first MapResponse in a stream; false when HTTP response ends
|
||||
state State // TODO(bradfitz): delete this, make it computed by method from other state
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap and update requests
|
||||
@@ -153,15 +152,21 @@ type Auto struct {
|
||||
|
||||
// New creates and starts a new Auto.
|
||||
func New(opts Options) (*Auto, error) {
|
||||
c, err := NewNoStart(opts)
|
||||
if c != nil {
|
||||
c.Start()
|
||||
c, err := newNoStart(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.StartPaused {
|
||||
c.SetPaused(true)
|
||||
}
|
||||
if !opts.SkipStartForTests {
|
||||
c.start()
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// NewNoStart creates a new Auto, but without calling Start on it.
|
||||
func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
// newNoStart creates a new Auto, but without calling Start on it.
|
||||
func newNoStart(opts Options) (_ *Auto, err error) {
|
||||
direct, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -172,9 +177,6 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
if opts.Observer == nil {
|
||||
return nil, errors.New("missing required Options.Observer")
|
||||
}
|
||||
if opts.Logf == nil {
|
||||
opts.Logf = func(fmt string, args ...any) {}
|
||||
}
|
||||
@@ -192,15 +194,14 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
observer: opts.Observer,
|
||||
shutdownFn: opts.Shutdown,
|
||||
}
|
||||
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
||||
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
||||
|
||||
c.unregisterHealthWatch = opts.HealthTracker.RegisterWatcher(direct.ReportHealthChange)
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
@@ -225,10 +226,21 @@ func (c *Auto) SetPaused(paused bool) {
|
||||
c.unpauseWaiters = nil
|
||||
}
|
||||
|
||||
// Start starts the client's goroutines.
|
||||
// StartForTest starts the client's goroutines.
|
||||
//
|
||||
// It should only be called for clients created by NewNoStart.
|
||||
func (c *Auto) Start() {
|
||||
// It should only be called for clients created with [Options.SkipStartForTests].
|
||||
func (c *Auto) StartForTest() {
|
||||
testenv.AssertInTest()
|
||||
c.start()
|
||||
}
|
||||
|
||||
func (c *Auto) start() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.started {
|
||||
return
|
||||
}
|
||||
c.started = true
|
||||
go c.authRoutine()
|
||||
go c.mapRoutine()
|
||||
go c.updateRoutine()
|
||||
@@ -302,10 +314,11 @@ func (c *Auto) authRoutine() {
|
||||
c.mu.Lock()
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
loggedIn := c.loggedIn
|
||||
if goal != nil {
|
||||
c.logf("[v1] authRoutine: %s; wantLoggedIn=%v", c.state, true)
|
||||
c.logf("[v1] authRoutine: loggedIn=%v; wantLoggedIn=%v", loggedIn, true)
|
||||
} else {
|
||||
c.logf("[v1] authRoutine: %s; goal=nil paused=%v", c.state, c.paused)
|
||||
c.logf("[v1] authRoutine: loggedIn=%v; goal=nil paused=%v", loggedIn, c.paused)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -328,11 +341,6 @@ func (c *Auto) authRoutine() {
|
||||
|
||||
c.mu.Lock()
|
||||
c.urlToVisit = goal.url
|
||||
if goal.url != "" {
|
||||
c.state = StateURLVisitRequired
|
||||
} else {
|
||||
c.state = StateAuthenticating
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
var url string
|
||||
@@ -366,7 +374,6 @@ func (c *Auto) authRoutine() {
|
||||
flags: LoginDefault,
|
||||
url: url,
|
||||
}
|
||||
c.state = StateURLVisitRequired
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine-url", err, url, nil)
|
||||
@@ -386,7 +393,6 @@ func (c *Auto) authRoutine() {
|
||||
c.urlToVisit = ""
|
||||
c.loggedIn = true
|
||||
c.loginGoal = nil
|
||||
c.state = StateAuthenticated
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine-success", nil, "", nil)
|
||||
@@ -419,6 +425,11 @@ func (c *Auto) unpausedChanLocked() <-chan bool {
|
||||
return unpaused
|
||||
}
|
||||
|
||||
// ClientID returns the ClientID of the direct controlClient
|
||||
func (c *Auto) ClientID() int64 {
|
||||
return c.direct.ClientID()
|
||||
}
|
||||
|
||||
// mapRoutineState is the state of Auto.mapRoutine while it's running.
|
||||
type mapRoutineState struct {
|
||||
c *Auto
|
||||
@@ -431,21 +442,17 @@ func (mrs mapRoutineState) UpdateFullNetmap(nm *netmap.NetworkMap) {
|
||||
c := mrs.c
|
||||
|
||||
c.mu.Lock()
|
||||
ctx := c.mapCtx
|
||||
c.inMapPoll = true
|
||||
if c.loggedIn {
|
||||
c.state = StateSynchronized
|
||||
}
|
||||
c.expiry = nm.Expiry
|
||||
c.expiry = nm.SelfKeyExpiry()
|
||||
stillAuthed := c.loggedIn
|
||||
c.logf("[v1] mapRoutine: netmap received: %s", c.state)
|
||||
c.logf("[v1] mapRoutine: netmap received: loggedIn=%v inMapPoll=true", stillAuthed)
|
||||
c.mu.Unlock()
|
||||
|
||||
if stillAuthed {
|
||||
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
|
||||
}
|
||||
// Reset the backoff timer if we got a netmap.
|
||||
mrs.bo.BackOff(ctx, nil)
|
||||
mrs.bo.Reset()
|
||||
}
|
||||
|
||||
func (mrs mapRoutineState) UpdateNetmapDelta(muts []netmap.NodeMutation) bool {
|
||||
@@ -486,8 +493,8 @@ func (c *Auto) mapRoutine() {
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.logf("[v1] mapRoutine: %s", c.state)
|
||||
loggedIn := c.loggedIn
|
||||
c.logf("[v1] mapRoutine: loggedIn=%v", loggedIn)
|
||||
ctx := c.mapCtx
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -518,9 +525,6 @@ func (c *Auto) mapRoutine() {
|
||||
c.direct.health.SetOutOfPollNetMap()
|
||||
c.mu.Lock()
|
||||
c.inMapPoll = false
|
||||
if c.state == StateSynchronized {
|
||||
c.state = StateAuthenticated
|
||||
}
|
||||
paused := c.paused
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -586,12 +590,12 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
state := c.state
|
||||
loggedIn := c.loggedIn
|
||||
inMapPoll := c.inMapPoll
|
||||
loginGoal := c.loginGoal
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
c.logf("[v1] sendStatus: %s: loggedIn=%v inMapPoll=%v", who, loggedIn, inMapPoll)
|
||||
|
||||
var p persist.PersistView
|
||||
if nm != nil && loggedIn && inMapPoll {
|
||||
@@ -602,18 +606,31 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
nm = nil
|
||||
}
|
||||
newSt := &Status{
|
||||
URL: url,
|
||||
Persist: p,
|
||||
NetMap: nm,
|
||||
Err: err,
|
||||
state: state,
|
||||
URL: url,
|
||||
Persist: p,
|
||||
NetMap: nm,
|
||||
Err: err,
|
||||
LoggedIn: loggedIn && loginGoal == nil,
|
||||
InMapPoll: inMapPoll,
|
||||
}
|
||||
|
||||
if c.observer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.lastStatus.Store(newSt)
|
||||
|
||||
// Launch a new goroutine to avoid blocking the caller while the observer
|
||||
// does its thing, which may result in a call back into the client.
|
||||
metricQueued.Add(1)
|
||||
c.observerQueue.Add(func() {
|
||||
c.mu.Lock()
|
||||
closed := c.closed
|
||||
c.mu.Unlock()
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
|
||||
if canSkipStatus(newSt, c.lastStatus.Load()) {
|
||||
metricSkippable.Add(1)
|
||||
if !c.direct.controlKnobs.DisableSkipStatusQueue.Load() {
|
||||
@@ -657,14 +674,15 @@ func canSkipStatus(s1, s2 *Status) bool {
|
||||
// we can't skip it.
|
||||
return false
|
||||
}
|
||||
if s1.Err != nil || s1.URL != "" {
|
||||
// If s1 has an error or a URL, we shouldn't skip it, lest the error go
|
||||
// away in s2 or in-between. We want to make sure all the subsystems see
|
||||
// it. Plus there aren't many of these, so not worth skipping.
|
||||
if s1.Err != nil || s1.URL != "" || s1.LoggedIn {
|
||||
// If s1 has an error, a URL, or LoginFinished set, we shouldn't skip it,
|
||||
// lest the error go away in s2 or in-between. We want to make sure all
|
||||
// the subsystems see it. Plus there aren't many of these, so not worth
|
||||
// skipping.
|
||||
return false
|
||||
}
|
||||
if !s1.Persist.Equals(s2.Persist) || s1.state != s2.state {
|
||||
// If s1 has a different Persist or state than s2,
|
||||
if !s1.Persist.Equals(s2.Persist) || s1.LoggedIn != s2.LoggedIn || s1.InMapPoll != s2.InMapPoll || s1.URL != s2.URL {
|
||||
// If s1 has a different Persist, LoginFinished, Synced, or URL than s2,
|
||||
// don't skip it. We only care about skipping the typical
|
||||
// entries where the only difference is the NetMap.
|
||||
return false
|
||||
@@ -726,7 +744,6 @@ func (c *Auto) Logout(ctx context.Context) error {
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.loggedIn = false
|
||||
c.state = StateNotAuthenticated
|
||||
c.cancelAuthCtxLocked()
|
||||
c.cancelMapCtxLocked()
|
||||
c.mu.Unlock()
|
||||
@@ -750,6 +767,13 @@ func (c *Auto) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetDiscoPublicKey sets the client's Disco public to key and sends the change
|
||||
// to the control server.
|
||||
func (c *Auto) SetDiscoPublicKey(key key.DiscoPublic) {
|
||||
c.direct.SetDiscoPublicKey(key)
|
||||
c.updateControl()
|
||||
}
|
||||
|
||||
func (c *Auto) Shutdown() {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
@@ -774,7 +798,6 @@ func (c *Auto) Shutdown() {
|
||||
shutdownFn()
|
||||
}
|
||||
|
||||
c.unregisterHealthWatch()
|
||||
<-c.authDone
|
||||
<-c.mapDone
|
||||
<-c.updateDone
|
||||
@@ -813,13 +836,3 @@ func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
func (c *Auto) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
return c.direct.DoNoiseRequest(req)
|
||||
}
|
||||
|
||||
// GetSingleUseNoiseRoundTripper returns a RoundTripper that can be only be used
|
||||
// once (and must be used once) to make a single HTTP request over the noise
|
||||
// channel to the coordination server.
|
||||
//
|
||||
// In addition to the RoundTripper, it returns the HTTP/2 channel's early noise
|
||||
// payload, if any.
|
||||
func (c *Auto) GetSingleUseNoiseRoundTripper(ctx context.Context) (http.RoundTripper, *tailcfg.EarlyNoise, error) {
|
||||
return c.direct.GetSingleUseNoiseRoundTripper(ctx)
|
||||
}
|
||||
|
||||
9
vendor/tailscale.com/control/controlclient/client.go
generated
vendored
9
vendor/tailscale.com/control/controlclient/client.go
generated
vendored
@@ -12,6 +12,7 @@ import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// LoginFlags is a bitmask of options to change the behavior of Client.Login
|
||||
@@ -80,7 +81,15 @@ type Client interface {
|
||||
// TODO: a server-side change would let us simply upload this
|
||||
// in a separate http request. It has nothing to do with the rest of
|
||||
// the state machine.
|
||||
// Note: the auto client uploads the new endpoints to control immediately.
|
||||
UpdateEndpoints(endpoints []tailcfg.Endpoint)
|
||||
// SetDiscoPublicKey updates the disco public key that will be sent in
|
||||
// future map requests. This should be called after rotating the discovery key.
|
||||
// Note: the auto client uploads the new key to control immediately.
|
||||
SetDiscoPublicKey(key.DiscoPublic)
|
||||
// ClientID returns the ClientID of a client. This ID is meant to
|
||||
// distinguish one client from another.
|
||||
ClientID() int64
|
||||
}
|
||||
|
||||
// UserVisibleError is an error that should be shown to users.
|
||||
|
||||
537
vendor/tailscale.com/control/controlclient/direct.go
generated
vendored
537
vendor/tailscale.com/control/controlclient/direct.go
generated
vendored
@@ -4,9 +4,11 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -16,19 +18,20 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/control/ts2021"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -37,11 +40,11 @@ import (
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/netx"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tempfork/httprec"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -51,62 +54,70 @@ import (
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/singleflight"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/util/zstdframe"
|
||||
)
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
controlKnobs *controlknobs.Knobs // always non-nil
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // non-nil
|
||||
health *health.Tracker
|
||||
discoPubKey key.DiscoPublic
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
onClientVersion func(*tailcfg.ClientVersion) // or nil
|
||||
onControlTime func(time.Time) // or nil
|
||||
onTailnetDefaultAutoUpdate func(bool) // or nil
|
||||
panicOnUse bool // if true, panic if client is used (for testing)
|
||||
closedCtx context.Context // alive until Direct.Close is called
|
||||
closeCtx context.CancelFunc // cancels closedCtx
|
||||
httpc *http.Client // HTTP client used to do TLS requests to control (just https://controlplane.tailscale.com/key?v=123)
|
||||
interceptedDial *atomic.Bool // if non-nil, pointer to bool whether ScreenTime intercepted our dial
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
controlKnobs *controlknobs.Knobs // always non-nil
|
||||
serverURL string // URL of the tailcontrol server
|
||||
clock tstime.Clock
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // non-nil
|
||||
health *health.Tracker
|
||||
busClient *eventbus.Client
|
||||
clientVersionPub *eventbus.Publisher[tailcfg.ClientVersion]
|
||||
autoUpdatePub *eventbus.Publisher[AutoUpdate]
|
||||
controlTimePub *eventbus.Publisher[ControlTime]
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
skipIPForwardingCheck bool
|
||||
pinger Pinger
|
||||
popBrowser func(url string) // or nil
|
||||
polc policyclient.Client // always non-nil
|
||||
c2nHandler http.Handler // or nil
|
||||
panicOnUse bool // if true, panic if client is used (for testing)
|
||||
closedCtx context.Context // alive until Direct.Close is called
|
||||
closeCtx context.CancelFunc // cancels closedCtx
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
mu syncs.Mutex // mutex guards the following fields
|
||||
serverLegacyKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key; only used for signRegisterRequest on Windows now
|
||||
serverNoiseKey key.MachinePublic
|
||||
discoPubKey key.DiscoPublic // protected by mu; can be updated via [SetDiscoPublicKey]
|
||||
|
||||
sfGroup singleflight.Group[struct{}, *NoiseClient] // protects noiseClient creation.
|
||||
noiseClient *NoiseClient
|
||||
sfGroup singleflight.Group[struct{}, *ts2021.Client] // protects noiseClient creation.
|
||||
noiseClient *ts2021.Client // also protected by mu
|
||||
|
||||
persist persist.PersistView
|
||||
authKey string
|
||||
tryingNewKey key.NodePrivate
|
||||
expiry time.Time // or zero value if none/unknown
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
netinfo *tailcfg.NetInfo
|
||||
endpoints []tailcfg.Endpoint
|
||||
tkaHead string
|
||||
lastPingURL string // last PingRequest.URL received, for dup suppression
|
||||
persist persist.PersistView
|
||||
authKey string
|
||||
tryingNewKey key.NodePrivate
|
||||
expiry time.Time // or zero value if none/unknown
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
netinfo *tailcfg.NetInfo
|
||||
endpoints []tailcfg.Endpoint
|
||||
tkaHead string
|
||||
lastPingURL string // last PingRequest.URL received, for dup suppression
|
||||
connectionHandleForTest string // sent in MapRequest.ConnectionHandleForTest
|
||||
|
||||
controlClientID int64 // Random ID used to differentiate clients for consumers of messages.
|
||||
}
|
||||
|
||||
// Observer is implemented by users of the control client (such as LocalBackend)
|
||||
// to get notified of changes in the control client's status.
|
||||
//
|
||||
// If an implementation of Observer also implements [NetmapDeltaUpdater], they get
|
||||
// delta updates as well as full netmap updates.
|
||||
type Observer interface {
|
||||
// SetControlClientStatus is called when the client has a new status to
|
||||
// report. The Client is provided to allow the Observer to track which
|
||||
@@ -116,28 +127,36 @@ type Observer interface {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Persist persist.Persist // initial persistent data
|
||||
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
Clock tstime.Clock
|
||||
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
|
||||
DiscoPublicKey key.DiscoPublic
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
HealthTracker *health.Tracker
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status
|
||||
OnControlTime func(time.Time) // optional func to notify callers of new time from control
|
||||
OnTailnetDefaultAutoUpdate func(bool) // optional func to inform GUI of default auto-update setting for the tailnet
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
C2NHandler http.Handler // or nil
|
||||
ControlKnobs *controlknobs.Knobs // or nil to ignore
|
||||
Persist persist.Persist // initial persistent data
|
||||
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
Clock tstime.Clock
|
||||
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
|
||||
DiscoPublicKey key.DiscoPublic
|
||||
PolicyClient policyclient.Client // or nil for none
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
HealthTracker *health.Tracker
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
C2NHandler http.Handler // or nil
|
||||
ControlKnobs *controlknobs.Knobs // or nil to ignore
|
||||
Bus *eventbus.Bus // non-nil, for setting up publishers
|
||||
|
||||
SkipStartForTests bool // if true, don't call [Auto.Start] to avoid any background goroutines (for tests only)
|
||||
|
||||
// StartPaused indicates whether the client should start in a paused state
|
||||
// where it doesn't do network requests. This primarily exists for testing
|
||||
// but not necessarily "go test" tests, so it isn't restricted to only
|
||||
// being used in tests.
|
||||
StartPaused bool
|
||||
|
||||
// Observer is called when there's a change in status to report
|
||||
// from the control client.
|
||||
// If nil, no status updates are reported.
|
||||
Observer Observer
|
||||
|
||||
// SkipIPForwardingCheck declares that the host's IP
|
||||
@@ -213,6 +232,8 @@ type NetmapDeltaUpdater interface {
|
||||
UpdateNetmapDelta([]netmap.NodeMutation) (ok bool)
|
||||
}
|
||||
|
||||
var nextControlClientID atomic.Int64
|
||||
|
||||
// NewDirect returns a new Direct client.
|
||||
func NewDirect(opts Options) (*Direct, error) {
|
||||
if opts.ServerURL == "" {
|
||||
@@ -238,10 +259,6 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
opts.ControlKnobs = &controlknobs.Knobs{}
|
||||
}
|
||||
opts.ServerURL = strings.TrimRight(opts.ServerURL, "/")
|
||||
serverURL, err := url.Parse(opts.ServerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.Clock == nil {
|
||||
opts.Clock = tstime.StdClock{}
|
||||
}
|
||||
@@ -269,10 +286,14 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
var interceptedDial *atomic.Bool
|
||||
if httpc == nil {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), opts.HealthTracker, tr.TLSClientConfig)
|
||||
var dialFunc dialFunc
|
||||
if buildfeatures.HasUseProxy {
|
||||
tr.Proxy = feature.HookProxyFromEnvironment.GetOrNil()
|
||||
if f, ok := feature.HookProxySetTransportGetProxyConnectHeader.GetOk(); ok {
|
||||
f(tr)
|
||||
}
|
||||
}
|
||||
tr.TLSClientConfig = tlsdial.Config(opts.HealthTracker, tr.TLSClientConfig)
|
||||
var dialFunc netx.DialFunc
|
||||
dialFunc, interceptedDial = makeScreenTimeDetectingDialFunc(opts.Dialer.SystemDial)
|
||||
tr.DialContext = dnscache.Dialer(dialFunc, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialFunc, dnsCache, tr.TLSClientConfig)
|
||||
@@ -286,32 +307,32 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
}
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
interceptedDial: interceptedDial,
|
||||
controlKnobs: opts.ControlKnobs,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
netMon: netMon,
|
||||
health: opts.HealthTracker,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
onClientVersion: opts.OnClientVersion,
|
||||
onTailnetDefaultAutoUpdate: opts.OnTailnetDefaultAutoUpdate,
|
||||
onControlTime: opts.OnControlTime,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
httpc: httpc,
|
||||
interceptedDial: interceptedDial,
|
||||
controlKnobs: opts.ControlKnobs,
|
||||
getMachinePrivKey: opts.GetMachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
clock: opts.Clock,
|
||||
logf: opts.Logf,
|
||||
persist: opts.Persist.View(),
|
||||
authKey: opts.AuthKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
netMon: netMon,
|
||||
health: opts.HealthTracker,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
polc: cmp.Or(opts.PolicyClient, policyclient.Client(policyclient.NoPolicyClient{})),
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: dnsCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
}
|
||||
c.discoPubKey = opts.DiscoPublicKey
|
||||
c.closedCtx, c.closeCtx = context.WithCancel(context.Background())
|
||||
|
||||
c.controlClientID = nextControlClientID.Add(1)
|
||||
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
} else {
|
||||
@@ -321,7 +342,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
}
|
||||
}
|
||||
if opts.NoiseTestClient != nil {
|
||||
c.noiseClient = &NoiseClient{
|
||||
c.noiseClient = &ts2021.Client{
|
||||
Client: opts.NoiseTestClient,
|
||||
}
|
||||
c.serverNoiseKey = key.NewMachine().Public() // prevent early error before hitting test client
|
||||
@@ -329,6 +350,12 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
if strings.Contains(opts.ServerURL, "controlplane.tailscale.com") && envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") {
|
||||
c.panicOnUse = true
|
||||
}
|
||||
|
||||
c.busClient = opts.Bus.Client("controlClient.direct")
|
||||
c.clientVersionPub = eventbus.Publish[tailcfg.ClientVersion](c.busClient)
|
||||
c.autoUpdatePub = eventbus.Publish[AutoUpdate](c.busClient)
|
||||
c.controlTimePub = eventbus.Publish[ControlTime](c.busClient)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -338,15 +365,14 @@ func (c *Direct) Close() error {
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.busClient.Close()
|
||||
if c.noiseClient != nil {
|
||||
if err := c.noiseClient.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.noiseClient = nil
|
||||
if tr, ok := c.httpc.Transport.(*http.Transport); ok {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
c.httpc.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -387,7 +413,7 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetNetInfo stores a new TKA head value for next update.
|
||||
// SetTKAHead stores a new TKA head value for next update.
|
||||
// It reports whether the TKA head changed.
|
||||
func (c *Direct) SetTKAHead(tkaHead string) bool {
|
||||
c.mu.Lock()
|
||||
@@ -402,6 +428,14 @@ func (c *Direct) SetTKAHead(tkaHead string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetConnectionHandleForTest stores a new MapRequest.ConnectionHandleForTest
|
||||
// value for the next update.
|
||||
func (c *Direct) SetConnectionHandleForTest(handle string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.connectionHandleForTest = handle
|
||||
}
|
||||
|
||||
func (c *Direct) GetPersist() persist.PersistView {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -523,7 +557,9 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
} else {
|
||||
if expired {
|
||||
c.logf("Old key expired -> regen=true")
|
||||
systemd.Status("key expired; run 'tailscale up' to authenticate")
|
||||
if f, ok := feature.HookSystemdStatus.GetOk(); ok {
|
||||
f("key expired; run 'tailscale up' to authenticate")
|
||||
}
|
||||
regen = true
|
||||
}
|
||||
if (opt.Flags & LoginInteractive) != 0 {
|
||||
@@ -582,6 +618,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if persist.NetworkLockKey.IsZero() {
|
||||
persist.NetworkLockKey = key.NewNLPrivate()
|
||||
}
|
||||
|
||||
nlPub := persist.NetworkLockKey.Public()
|
||||
|
||||
if tryingNewKey.IsZero() {
|
||||
@@ -611,7 +648,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
return regen, opt.URL, nil, err
|
||||
}
|
||||
|
||||
tailnet, err := syspolicy.GetString(syspolicy.Tailnet, "")
|
||||
tailnet, err := c.polc.GetString(pkey.Tailnet, "")
|
||||
if err != nil {
|
||||
c.logf("unable to provide Tailnet field in register request. err: %v", err)
|
||||
}
|
||||
@@ -641,7 +678,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
AuthKey: authKey,
|
||||
}
|
||||
}
|
||||
err = signRegisterRequest(&request, c.serverURL, c.serverLegacyKey, machinePrivKey.Public())
|
||||
err = signRegisterRequest(c.polc, &request, c.serverURL, c.serverLegacyKey, machinePrivKey.Public())
|
||||
if err != nil {
|
||||
// If signing failed, clear all related fields
|
||||
request.SignatureType = tailcfg.SignatureNone
|
||||
@@ -678,8 +715,8 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if err != nil {
|
||||
return regen, opt.URL, nil, err
|
||||
}
|
||||
addLBHeader(req, request.OldNodeKey)
|
||||
addLBHeader(req, request.NodeKey)
|
||||
ts2021.AddLBHeader(req, request.OldNodeKey)
|
||||
ts2021.AddLBHeader(req, request.NodeKey)
|
||||
|
||||
res, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
@@ -816,6 +853,31 @@ func (c *Direct) SendUpdate(ctx context.Context) error {
|
||||
return c.sendMapRequest(ctx, false, nil)
|
||||
}
|
||||
|
||||
// SetDiscoPublicKey updates the disco public key in local state.
|
||||
// It does not implicitly trigger [SendUpdate]; callers should arrange for that.
|
||||
func (c *Direct) SetDiscoPublicKey(key key.DiscoPublic) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.discoPubKey = key
|
||||
}
|
||||
|
||||
// ClientID returns the controlClientID of the controlClient.
|
||||
func (c *Direct) ClientID() int64 {
|
||||
return c.controlClientID
|
||||
}
|
||||
|
||||
// AutoUpdate is an eventbus value, reporting the value of tailcfg.MapResponse.DefaultAutoUpdate.
|
||||
type AutoUpdate struct {
|
||||
ClientID int64 // The ID field is used for consumers to differentiate instances of Direct.
|
||||
Value bool // The Value represents DefaultAutoUpdate from [tailcfg.MapResponse].
|
||||
}
|
||||
|
||||
// ControlTime is an eventbus value, reporting the value of tailcfg.MapResponse.ControlTime.
|
||||
type ControlTime struct {
|
||||
ClientID int64 // The ID field is used for consumers to differentiate instances of Direct.
|
||||
Value time.Time // The Value represents ControlTime from [tailcfg.MapResponse].
|
||||
}
|
||||
|
||||
// If we go more than watchdogTimeout without hearing from the server,
|
||||
// end the long poll. We should be receiving a keep alive ping
|
||||
// every minute.
|
||||
@@ -848,8 +910,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
discoKey := c.discoPubKey
|
||||
hi := c.hostInfoLocked()
|
||||
backendLogID := hi.BackendLogID
|
||||
connectionHandleForTest := c.connectionHandleForTest
|
||||
tkaHead := c.tkaHead
|
||||
var epStrs []string
|
||||
var eps []netip.AddrPort
|
||||
var epTypes []tailcfg.EndpointType
|
||||
@@ -889,21 +954,44 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
}
|
||||
|
||||
nodeKey := persist.PublicNodeKey()
|
||||
|
||||
request := &tailcfg.MapRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
KeepAlive: true,
|
||||
NodeKey: nodeKey,
|
||||
DiscoKey: c.discoPubKey,
|
||||
Endpoints: eps,
|
||||
EndpointTypes: epTypes,
|
||||
Stream: isStreaming,
|
||||
Hostinfo: hi,
|
||||
DebugFlags: c.debugFlags,
|
||||
OmitPeers: nu == nil,
|
||||
TKAHead: c.tkaHead,
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
KeepAlive: true,
|
||||
NodeKey: nodeKey,
|
||||
DiscoKey: discoKey,
|
||||
Endpoints: eps,
|
||||
EndpointTypes: epTypes,
|
||||
Stream: isStreaming,
|
||||
Hostinfo: hi,
|
||||
DebugFlags: c.debugFlags,
|
||||
OmitPeers: nu == nil,
|
||||
TKAHead: tkaHead,
|
||||
ConnectionHandleForTest: connectionHandleForTest,
|
||||
}
|
||||
|
||||
// If we have a hardware attestation key, sign the node key with it and send
|
||||
// the key & signature in the map request.
|
||||
if buildfeatures.HasTPM {
|
||||
if k := persist.AsStruct().AttestationKey; k != nil && !k.IsZero() {
|
||||
hwPub := key.HardwareAttestationPublicFromPlatformKey(k)
|
||||
request.HardwareAttestationKey = hwPub
|
||||
|
||||
t := c.clock.Now()
|
||||
msg := fmt.Sprintf("%d|%s", t.Unix(), nodeKey.String())
|
||||
digest := sha256.Sum256([]byte(msg))
|
||||
sig, err := k.Sign(nil, digest[:], crypto.SHA256)
|
||||
if err != nil {
|
||||
c.logf("failed to sign node key with hardware attestation key: %v", err)
|
||||
} else {
|
||||
request.HardwareAttestationKeySignature = sig
|
||||
request.HardwareAttestationKeySignatureTimestamp = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extraDebugFlags []string
|
||||
if hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
|
||||
if buildfeatures.HasAdvertiseRoutes && hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
|
||||
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
|
||||
}
|
||||
@@ -967,7 +1055,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addLBHeader(req, nodeKey)
|
||||
ts2021.AddLBHeader(req, nodeKey)
|
||||
|
||||
res, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
@@ -1015,7 +1103,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
c.persist = newPersist.View()
|
||||
persist = c.persist
|
||||
}
|
||||
c.expiry = nm.Expiry
|
||||
c.expiry = nm.SelfKeyExpiry()
|
||||
}
|
||||
|
||||
// gotNonKeepAliveMessage is whether we've yet received a MapResponse message without
|
||||
@@ -1047,7 +1135,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
vlogf("netmap: read body after %v", time.Since(t0).Round(time.Millisecond))
|
||||
|
||||
var resp tailcfg.MapResponse
|
||||
if err := c.decodeMsg(msg, &resp); err != nil {
|
||||
if err := sess.decodeMsg(msg, &resp); err != nil {
|
||||
vlogf("netmap: decode error: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -1072,21 +1160,19 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
c.logf("netmap: control says to open URL %v; no popBrowser func", u)
|
||||
}
|
||||
}
|
||||
if resp.ClientVersion != nil && c.onClientVersion != nil {
|
||||
c.onClientVersion(resp.ClientVersion)
|
||||
if resp.ClientVersion != nil {
|
||||
c.clientVersionPub.Publish(*resp.ClientVersion)
|
||||
}
|
||||
if resp.ControlTime != nil && !resp.ControlTime.IsZero() {
|
||||
c.logf.JSON(1, "controltime", resp.ControlTime.UTC())
|
||||
if c.onControlTime != nil {
|
||||
c.onControlTime(*resp.ControlTime)
|
||||
}
|
||||
c.controlTimePub.Publish(ControlTime{c.controlClientID, *resp.ControlTime})
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
vlogf("netmap: got keep-alive")
|
||||
} else {
|
||||
vlogf("netmap: got new map")
|
||||
}
|
||||
if resp.ControlDialPlan != nil {
|
||||
if resp.ControlDialPlan != nil && !ignoreDialPlan() {
|
||||
if c.dialPlan != nil {
|
||||
c.logf("netmap: got new dial plan from control")
|
||||
c.dialPlan.Store(resp.ControlDialPlan)
|
||||
@@ -1098,11 +1184,21 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
metricMapResponseKeepAlives.Add(1)
|
||||
continue
|
||||
}
|
||||
if au, ok := resp.DefaultAutoUpdate.Get(); ok {
|
||||
if c.onTailnetDefaultAutoUpdate != nil {
|
||||
c.onTailnetDefaultAutoUpdate(au)
|
||||
|
||||
// DefaultAutoUpdate in its CapMap and deprecated top-level field forms.
|
||||
if self := resp.Node; self != nil {
|
||||
for _, v := range self.CapMap[tailcfg.NodeAttrDefaultAutoUpdate] {
|
||||
switch v {
|
||||
case "true", "false":
|
||||
c.autoUpdatePub.Publish(AutoUpdate{c.controlClientID, v == "true"})
|
||||
default:
|
||||
c.logf("netmap: [unexpected] unknown %s in CapMap: %q", tailcfg.NodeAttrDefaultAutoUpdate, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if au, ok := resp.DeprecatedDefaultAutoUpdate.Get(); ok {
|
||||
c.autoUpdatePub.Publish(AutoUpdate{c.controlClientID, au})
|
||||
}
|
||||
|
||||
metricMapResponseMap.Add(1)
|
||||
if gotNonKeepAliveMessage {
|
||||
@@ -1125,12 +1221,33 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetmapFromMapResponseForDebug returns a NetworkMap from the given MapResponse.
|
||||
// It is intended for debugging only.
|
||||
func NetmapFromMapResponseForDebug(ctx context.Context, pr persist.PersistView, resp *tailcfg.MapResponse) (*netmap.NetworkMap, error) {
|
||||
if resp == nil {
|
||||
return nil, errors.New("nil MapResponse")
|
||||
}
|
||||
if resp.Node == nil {
|
||||
return nil, errors.New("MapResponse lacks Node")
|
||||
}
|
||||
|
||||
nu := &rememberLastNetmapUpdater{}
|
||||
sess := newMapSession(pr.PrivateNodeKey(), nu, nil)
|
||||
defer sess.Close()
|
||||
|
||||
if err := sess.HandleNonKeepAliveMapResponse(ctx, resp); err != nil {
|
||||
return nil, fmt.Errorf("HandleNonKeepAliveMapResponse: %w", err)
|
||||
}
|
||||
|
||||
return sess.netmap(), nil
|
||||
}
|
||||
|
||||
func (c *Direct) handleDebugMessage(ctx context.Context, debug *tailcfg.Debug) error {
|
||||
if code := debug.Exit; code != nil {
|
||||
c.logf("exiting process with status %v per controlplane", *code)
|
||||
os.Exit(*code)
|
||||
}
|
||||
if debug.DisableLogTail {
|
||||
if buildfeatures.HasLogTail && debug.DisableLogTail {
|
||||
logtail.Disable()
|
||||
envknob.SetNoLogsNoSupport()
|
||||
}
|
||||
@@ -1179,12 +1296,23 @@ func decode(res *http.Response, v any) error {
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
const justKeepAliveStr = `{"KeepAlive":true}`
|
||||
|
||||
// decodeMsg is responsible for uncompressing msg and unmarshaling into v.
|
||||
func (c *Direct) decodeMsg(compressedMsg []byte, v any) error {
|
||||
func (sess *mapSession) decodeMsg(compressedMsg []byte, v *tailcfg.MapResponse) error {
|
||||
// Fast path for common case of keep-alive message.
|
||||
// See tailscale/tailscale#17343.
|
||||
if sess.keepAliveZ != nil && bytes.Equal(compressedMsg, sess.keepAliveZ) {
|
||||
v.KeepAlive = true
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := zstdframe.AppendDecode(nil, compressedMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sess.ztdDecodesForTest++
|
||||
|
||||
if DevKnob.DumpNetMaps() {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
@@ -1197,6 +1325,9 @@ func (c *Direct) decodeMsg(compressedMsg []byte, v any) error {
|
||||
if err := json.Unmarshal(b, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
if v.KeepAlive && string(b) == justKeepAliveStr {
|
||||
sess.keepAliveZ = compressedMsg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1244,7 +1375,7 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||
out = tailcfg.OverTLSPublicKeyResponse{}
|
||||
k, err := key.ParseMachinePublicUntyped(mem.B(b))
|
||||
if err != nil {
|
||||
return nil, multierr.New(jsonErr, err)
|
||||
return nil, errors.Join(jsonErr, err)
|
||||
}
|
||||
out.LegacyPublicKey = k
|
||||
return &out, nil
|
||||
@@ -1314,6 +1445,10 @@ func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// HookAnswerC2NPing is where feature/c2n conditionally registers support
|
||||
// for handling C2N (control-to-node) HTTP requests.
|
||||
var HookAnswerC2NPing feature.Hook[func(logger.Logf, http.Handler, *http.Client, *tailcfg.PingRequest)]
|
||||
|
||||
func (c *Direct) answerPing(pr *tailcfg.PingRequest) {
|
||||
httpc := c.httpc
|
||||
useNoise := pr.URLIsNoise || pr.Types == "c2n"
|
||||
@@ -1334,11 +1469,16 @@ func (c *Direct) answerPing(pr *tailcfg.PingRequest) {
|
||||
answerHeadPing(c.logf, httpc, pr)
|
||||
return
|
||||
case "c2n":
|
||||
if !buildfeatures.HasC2N {
|
||||
return
|
||||
}
|
||||
if !useNoise && !envknob.Bool("TS_DEBUG_PERMIT_HTTP_C2N") {
|
||||
c.logf("refusing to answer c2n ping without noise")
|
||||
return
|
||||
}
|
||||
answerC2NPing(c.logf, c.c2nHandler, httpc, pr)
|
||||
if f, ok := HookAnswerC2NPing.GetOk(); ok {
|
||||
f(c.logf, c.c2nHandler, httpc, pr)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, t := range strings.Split(pr.Types, ",") {
|
||||
@@ -1373,54 +1513,6 @@ func answerHeadPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
if c2nHandler == nil {
|
||||
logf("answerC2NPing: c2nHandler not defined")
|
||||
return
|
||||
}
|
||||
hreq, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(pr.Payload)))
|
||||
if err != nil {
|
||||
logf("answerC2NPing: ReadRequest: %v", err)
|
||||
return
|
||||
}
|
||||
if pr.Log {
|
||||
logf("answerC2NPing: got c2n request for %v ...", hreq.RequestURI)
|
||||
}
|
||||
handlerTimeout := time.Minute
|
||||
if v := hreq.Header.Get("C2n-Handler-Timeout"); v != "" {
|
||||
handlerTimeout, _ = time.ParseDuration(v)
|
||||
}
|
||||
handlerCtx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
||||
defer cancel()
|
||||
hreq = hreq.WithContext(handlerCtx)
|
||||
rec := httprec.NewRecorder()
|
||||
c2nHandler.ServeHTTP(rec, hreq)
|
||||
cancel()
|
||||
|
||||
c2nResBuf := new(bytes.Buffer)
|
||||
rec.Result().Write(c2nResBuf)
|
||||
|
||||
replyCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(replyCtx, "POST", pr.URL, c2nResBuf)
|
||||
if err != nil {
|
||||
logf("answerC2NPing: NewRequestWithContext: %v", err)
|
||||
return
|
||||
}
|
||||
if pr.Log {
|
||||
logf("answerC2NPing: sending POST ping to %v ...", pr.URL)
|
||||
}
|
||||
t0 := clock.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
logf("answerC2NPing error: %v to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("answerC2NPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
}
|
||||
|
||||
// sleepAsRequest implements the sleep for a tailcfg.Debug message requesting
|
||||
// that the client sleep. The complication is that while we're sleeping (if for
|
||||
// a long time), we need to periodically reset the watchdog timer before it
|
||||
@@ -1445,7 +1537,7 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, d time.Duration, cl
|
||||
}
|
||||
|
||||
// getNoiseClient returns the noise client, creating one if one doesn't exist.
|
||||
func (c *Direct) getNoiseClient() (*NoiseClient, error) {
|
||||
func (c *Direct) getNoiseClient() (*ts2021.Client, error) {
|
||||
c.mu.Lock()
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
nc := c.noiseClient
|
||||
@@ -1460,13 +1552,13 @@ func (c *Direct) getNoiseClient() (*NoiseClient, error) {
|
||||
if c.dialPlan != nil {
|
||||
dp = c.dialPlan.Load
|
||||
}
|
||||
nc, err, _ := c.sfGroup.Do(struct{}{}, func() (*NoiseClient, error) {
|
||||
nc, err, _ := c.sfGroup.Do(struct{}{}, func() (*ts2021.Client, error) {
|
||||
k, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.logf("[v1] creating new noise client")
|
||||
nc, err := NewNoiseClient(NoiseOpts{
|
||||
nc, err := ts2021.NewClient(ts2021.ClientOpts{
|
||||
PrivKey: k,
|
||||
ServerPubKey: serverNoiseKey,
|
||||
ServerURL: c.serverURL,
|
||||
@@ -1500,7 +1592,7 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := nc.post(ctx, "/machine/set-dns", newReq.NodeKey, &newReq)
|
||||
res, err := nc.Post(ctx, "/machine/set-dns", newReq.NodeKey, &newReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1521,6 +1613,9 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
|
||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||
// requesting a DNS record be created or updated.
|
||||
func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err error) {
|
||||
if !buildfeatures.HasACME {
|
||||
return feature.ErrUnavailable
|
||||
}
|
||||
metricSetDNS.Add(1)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
@@ -1541,20 +1636,6 @@ func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
return nc.Do(req)
|
||||
}
|
||||
|
||||
// GetSingleUseNoiseRoundTripper returns a RoundTripper that can be only be used
|
||||
// once (and must be used once) to make a single HTTP request over the noise
|
||||
// channel to the coordination server.
|
||||
//
|
||||
// In addition to the RoundTripper, it returns the HTTP/2 channel's early noise
|
||||
// payload, if any.
|
||||
func (c *Direct) GetSingleUseNoiseRoundTripper(ctx context.Context) (http.RoundTripper, *tailcfg.EarlyNoise, error) {
|
||||
nc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nc.GetSingleUseRoundTripper(ctx)
|
||||
}
|
||||
|
||||
// doPingerPing sends a Ping to pr.IP using pinger, and sends an http request back to
|
||||
// pr.URL with ping response data.
|
||||
func doPingerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger, pingType tailcfg.PingType) {
|
||||
@@ -1611,47 +1692,6 @@ func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailc
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportHealthChange reports to the control plane a change to this node's
|
||||
// health. w must be non-nil. us can be nil to indicate a healthy state for w.
|
||||
func (c *Direct) ReportHealthChange(w *health.Warnable, us *health.UnhealthyState) {
|
||||
if w == health.NetworkStatusWarnable || w == health.IPNStateWarnable || w == health.LoginStateWarnable {
|
||||
// We don't report these. These include things like the network is down
|
||||
// (in which case we can't report anyway) or the user wanted things
|
||||
// stopped, as opposed to the more unexpected failure types in the other
|
||||
// subsystems.
|
||||
return
|
||||
}
|
||||
np, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
// Don't report errors to control if the server doesn't support noise.
|
||||
return
|
||||
}
|
||||
nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
// TODO(angott): at some point, update `Subsys` in the request to be `Warnable`
|
||||
req := &tailcfg.HealthChangeRequest{
|
||||
Subsys: string(w.Code),
|
||||
NodeKey: nodeKey,
|
||||
}
|
||||
if us != nil {
|
||||
req.Error = us.Text
|
||||
}
|
||||
|
||||
// Best effort, no logging:
|
||||
ctx, cancel := context.WithTimeout(c.closedCtx, 5*time.Second)
|
||||
defer cancel()
|
||||
res, err := np.post(ctx, "/machine/update-health", nodeKey, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
// SetDeviceAttrs does a synchronous call to the control plane to update
|
||||
// the node's attributes.
|
||||
//
|
||||
@@ -1690,7 +1730,7 @@ func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) e
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
res, err := nc.doWithBody(ctx, "PATCH", "/machine/set-device-attr", nodeKey, req)
|
||||
res, err := nc.DoWithBody(ctx, "PATCH", "/machine/set-device-attr", nodeKey, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1731,7 +1771,7 @@ func (c *Direct) sendAuditLog(ctx context.Context, auditLog tailcfg.AuditLogRequ
|
||||
panic("tainted client")
|
||||
}
|
||||
|
||||
res, err := nc.post(ctx, "/machine/audit-log", nodeKey, req)
|
||||
res, err := nc.Post(ctx, "/machine/audit-log", nodeKey, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errHTTPPostFailure, err)
|
||||
}
|
||||
@@ -1743,20 +1783,12 @@ func (c *Direct) sendAuditLog(ctx context.Context, auditLog tailcfg.AuditLogRequ
|
||||
return nil
|
||||
}
|
||||
|
||||
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
|
||||
if !nodeKey.IsZero() {
|
||||
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
||||
}
|
||||
}
|
||||
|
||||
type dialFunc = func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// makeScreenTimeDetectingDialFunc returns dialFunc, optionally wrapped (on
|
||||
// Apple systems) with a func that sets the returned atomic.Bool for whether
|
||||
// Screen Time seemed to intercept the connection.
|
||||
//
|
||||
// The returned *atomic.Bool is nil on non-Apple systems.
|
||||
func makeScreenTimeDetectingDialFunc(dial dialFunc) (dialFunc, *atomic.Bool) {
|
||||
func makeScreenTimeDetectingDialFunc(dial netx.DialFunc) (netx.DialFunc, *atomic.Bool) {
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "ios":
|
||||
// Continue below.
|
||||
@@ -1774,6 +1806,13 @@ func makeScreenTimeDetectingDialFunc(dial dialFunc) (dialFunc, *atomic.Bool) {
|
||||
}, ab
|
||||
}
|
||||
|
||||
func ignoreDialPlan() bool {
|
||||
// If we're running in v86 (a JavaScript-based emulation of a 32-bit x86)
|
||||
// our networking is very limited. Let's ignore the dial plan since it's too
|
||||
// complicated to race that many IPs anyway.
|
||||
return hostinfo.IsInVM86()
|
||||
}
|
||||
|
||||
func isTCPLoopback(a net.Addr) bool {
|
||||
if ta, ok := a.(*net.TCPAddr); ok {
|
||||
return ta.IP.IsLoopback()
|
||||
|
||||
75
vendor/tailscale.com/control/controlclient/map.go
generated
vendored
75
vendor/tailscale.com/control/controlclient/map.go
generated
vendored
@@ -6,7 +6,10 @@ package controlclient
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"reflect"
|
||||
@@ -19,6 +22,7 @@ import (
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -53,6 +57,9 @@ type mapSession struct {
|
||||
altClock tstime.Clock // if nil, regular time is used
|
||||
cancel context.CancelFunc // always non-nil, shuts down caller's base long poll context
|
||||
|
||||
keepAliveZ []byte // if non-nil, the learned zstd encoding of the just-KeepAlive message for this session
|
||||
ztdDecodesForTest int // for testing
|
||||
|
||||
// sessionAliveCtx is a Background-based context that's alive for the
|
||||
// duration of the mapSession that we own the lifetime of. It's closed by
|
||||
// sessionAliveCtxClose.
|
||||
@@ -86,6 +93,7 @@ type mapSession struct {
|
||||
lastDomain string
|
||||
lastDomainAuditLogID string
|
||||
lastHealth []string
|
||||
lastDisplayMessages map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
|
||||
lastPopBrowserURL string
|
||||
lastTKAInfo *tailcfg.TKAInfo
|
||||
lastNetmapSummary string // from NetworkMap.VeryConcise
|
||||
@@ -308,6 +316,31 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
}
|
||||
}
|
||||
|
||||
// In the copy/v86 wasm environment with limited networking, if the
|
||||
// control plane didn't pick our DERP home for us, do it ourselves and
|
||||
// mark all but the lowest region as NoMeasureNoHome. For prod, this
|
||||
// will be Region 1, NYC, a compromise between the US and Europe. But
|
||||
// really the control plane should pick this. This is only a fallback.
|
||||
if hostinfo.IsInVM86() {
|
||||
numCanMeasure := 0
|
||||
lowest := 0
|
||||
for rid, r := range dm.Regions {
|
||||
if !r.NoMeasureNoHome {
|
||||
numCanMeasure++
|
||||
if lowest == 0 || rid < lowest {
|
||||
lowest = rid
|
||||
}
|
||||
}
|
||||
}
|
||||
if numCanMeasure > 1 {
|
||||
for rid, r := range dm.Regions {
|
||||
if rid != lowest {
|
||||
r.NoMeasureNoHome = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zero-valued fields in a DERPMap mean that we're not changing
|
||||
// anything and are using the previous value(s).
|
||||
if ldm := ms.lastDERPMap; ldm != nil {
|
||||
@@ -383,6 +416,21 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
|
||||
if resp.Health != nil {
|
||||
ms.lastHealth = resp.Health
|
||||
}
|
||||
if resp.DisplayMessages != nil {
|
||||
if v, ok := resp.DisplayMessages["*"]; ok && v == nil {
|
||||
ms.lastDisplayMessages = nil
|
||||
}
|
||||
for k, v := range resp.DisplayMessages {
|
||||
if k == "*" {
|
||||
continue
|
||||
}
|
||||
if v != nil {
|
||||
mak.Set(&ms.lastDisplayMessages, k, *v)
|
||||
} else {
|
||||
delete(ms.lastDisplayMessages, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.TKAInfo != nil {
|
||||
ms.lastTKAInfo = resp.TKAInfo
|
||||
}
|
||||
@@ -802,9 +850,23 @@ func (ms *mapSession) sortedPeers() []tailcfg.NodeView {
|
||||
func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
peerViews := ms.sortedPeers()
|
||||
|
||||
var msgs map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
|
||||
if len(ms.lastDisplayMessages) != 0 {
|
||||
msgs = ms.lastDisplayMessages
|
||||
} else if len(ms.lastHealth) > 0 {
|
||||
// Convert all ms.lastHealth to the new [netmap.NetworkMap.DisplayMessages]
|
||||
for _, h := range ms.lastHealth {
|
||||
id := "health-" + strhash(h) // Unique ID in case there is more than one health message
|
||||
mak.Set(&msgs, tailcfg.DisplayMessageID(id), tailcfg.DisplayMessage{
|
||||
Title: "Coordination server reports an issue",
|
||||
Severity: tailcfg.SeverityMedium,
|
||||
Text: "The coordination server is reporting a health issue: " + h,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: ms.publicNodeKey,
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: peerViews,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfileView),
|
||||
@@ -816,7 +878,7 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
ControlHealth: ms.lastHealth,
|
||||
DisplayMessages: msgs,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
}
|
||||
|
||||
@@ -829,8 +891,6 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
|
||||
if node := ms.lastNode; node.Valid() {
|
||||
nm.SelfNode = node
|
||||
nm.Expiry = node.KeyExpiry()
|
||||
nm.Name = node.Name()
|
||||
nm.AllCaps = ms.lastCapSet
|
||||
}
|
||||
|
||||
@@ -842,5 +902,12 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
|
||||
if DevKnob.ForceProxyDNS() {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
|
||||
return nm
|
||||
}
|
||||
|
||||
func strhash(h string) string {
|
||||
s := sha256.New()
|
||||
io.WriteString(s, h)
|
||||
return hex.EncodeToString(s.Sum(nil))
|
||||
}
|
||||
|
||||
418
vendor/tailscale.com/control/controlclient/noise.go
generated
vendored
418
vendor/tailscale.com/control/controlclient/noise.go
generated
vendored
@@ -1,418 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/internal/noiseconn"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/singleflight"
|
||||
)
|
||||
|
||||
// NoiseClient provides a http.Client to connect to tailcontrol over
|
||||
// the ts2021 protocol.
|
||||
type NoiseClient struct {
|
||||
// Client is an HTTP client to talk to the coordination server.
|
||||
// It automatically makes a new Noise connection as needed.
|
||||
// It does not support node key proofs. To do that, call
|
||||
// noiseClient.getConn instead to make a connection.
|
||||
*http.Client
|
||||
|
||||
// h2t is the HTTP/2 transport we use a bit to create new
|
||||
// *http2.ClientConns. We don't use its connection pool and we don't use its
|
||||
// dialing. We use it for exactly one reason: its idle timeout that can only
|
||||
// be configured via the HTTP/1 config. And then we call NewClientConn (with
|
||||
// an existing Noise connection) on the http2.Transport which sets up an
|
||||
// http2.ClientConn using that idle timeout from an http1.Transport.
|
||||
h2t *http2.Transport
|
||||
|
||||
// sfDial ensures that two concurrent requests for a noise connection only
|
||||
// produce one shared one between the two callers.
|
||||
sfDial singleflight.Group[struct{}, *noiseconn.Conn]
|
||||
|
||||
dialer *tsdial.Dialer
|
||||
dnsCache *dnscache.Resolver
|
||||
privKey key.MachinePrivate
|
||||
serverPubKey key.MachinePublic
|
||||
host string // the host part of serverURL
|
||||
httpPort string // the default port to dial
|
||||
httpsPort string // the fallback Noise-over-https port or empty if none
|
||||
|
||||
// dialPlan optionally returns a ControlDialPlan previously received
|
||||
// from the control server; either the function or the return value can
|
||||
// be nil.
|
||||
dialPlan func() *tailcfg.ControlDialPlan
|
||||
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor
|
||||
health *health.Tracker
|
||||
|
||||
// mu only protects the following variables.
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
last *noiseconn.Conn // or nil
|
||||
nextID int
|
||||
connPool map[int]*noiseconn.Conn // active connections not yet closed; see noiseconn.Conn.Close
|
||||
}
|
||||
|
||||
// NoiseOpts contains options for the NewNoiseClient function. All fields are
|
||||
// required unless otherwise specified.
|
||||
type NoiseOpts struct {
|
||||
// PrivKey is this node's private key.
|
||||
PrivKey key.MachinePrivate
|
||||
// ServerPubKey is the public key of the server.
|
||||
ServerPubKey key.MachinePublic
|
||||
// ServerURL is the URL of the server to connect to.
|
||||
ServerURL string
|
||||
// Dialer's SystemDial function is used to connect to the server.
|
||||
Dialer *tsdial.Dialer
|
||||
// DNSCache is the caching Resolver to use to connect to the server.
|
||||
//
|
||||
// This field can be nil.
|
||||
DNSCache *dnscache.Resolver
|
||||
// Logf is the log function to use. This field can be nil.
|
||||
Logf logger.Logf
|
||||
// NetMon is the network monitor that, if set, will be used to get the
|
||||
// network interface state. This field can be nil; if so, the current
|
||||
// state will be looked up dynamically.
|
||||
NetMon *netmon.Monitor
|
||||
// HealthTracker, if non-nil, is the health tracker to use.
|
||||
HealthTracker *health.Tracker
|
||||
// DialPlan, if set, is a function that should return an explicit plan
|
||||
// on how to connect to the server.
|
||||
DialPlan func() *tailcfg.ControlDialPlan
|
||||
}
|
||||
|
||||
// NewNoiseClient returns a new noiseClient for the provided server and machine key.
|
||||
// serverURL is of the form https://<host>:<port> (no trailing slash).
|
||||
//
|
||||
// netMon may be nil, if non-nil it's used to do faster interface lookups.
|
||||
// dialPlan may be nil
|
||||
func NewNoiseClient(opts NoiseOpts) (*NoiseClient, error) {
|
||||
logf := opts.Logf
|
||||
u, err := url.Parse(opts.ServerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return nil, errors.New("invalid ServerURL scheme, must be http or https")
|
||||
}
|
||||
|
||||
var httpPort string
|
||||
var httpsPort string
|
||||
addr, _ := netip.ParseAddr(u.Hostname())
|
||||
isPrivateHost := addr.IsPrivate() || addr.IsLoopback() || u.Hostname() == "localhost"
|
||||
if port := u.Port(); port != "" {
|
||||
// If there is an explicit port specified, entirely rely on the scheme,
|
||||
// unless it's http with a private host in which case we never try using HTTPS.
|
||||
if u.Scheme == "https" {
|
||||
httpPort = ""
|
||||
httpsPort = port
|
||||
} else if u.Scheme == "http" {
|
||||
httpPort = port
|
||||
httpsPort = "443"
|
||||
if isPrivateHost {
|
||||
logf("setting empty HTTPS port with http scheme and private host %s", u.Hostname())
|
||||
httpsPort = ""
|
||||
}
|
||||
}
|
||||
} else if u.Scheme == "http" && isPrivateHost {
|
||||
// Whenever the scheme is http and the hostname is an IP address, do not set the HTTPS port,
|
||||
// as there cannot be a TLS certificate issued for an IP, unless it's a public IP.
|
||||
httpPort = "80"
|
||||
httpsPort = ""
|
||||
} else {
|
||||
// Otherwise, use the standard ports
|
||||
httpPort = "80"
|
||||
httpsPort = "443"
|
||||
}
|
||||
|
||||
np := &NoiseClient{
|
||||
serverPubKey: opts.ServerPubKey,
|
||||
privKey: opts.PrivKey,
|
||||
host: u.Hostname(),
|
||||
httpPort: httpPort,
|
||||
httpsPort: httpsPort,
|
||||
dialer: opts.Dialer,
|
||||
dnsCache: opts.DNSCache,
|
||||
dialPlan: opts.DialPlan,
|
||||
logf: opts.Logf,
|
||||
netMon: opts.NetMon,
|
||||
health: opts.HealthTracker,
|
||||
}
|
||||
|
||||
// Create the HTTP/2 Transport using a net/http.Transport
|
||||
// (which only does HTTP/1) because it's the only way to
|
||||
// configure certain properties on the http2.Transport. But we
|
||||
// never actually use the net/http.Transport for any HTTP/1
|
||||
// requests.
|
||||
h2Transport, err := http2.ConfigureTransports(&http.Transport{
|
||||
IdleConnTimeout: time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
np.h2t = h2Transport
|
||||
|
||||
np.Client = &http.Client{Transport: np}
|
||||
return np, nil
|
||||
}
|
||||
|
||||
// GetSingleUseRoundTripper returns a RoundTripper that can be only be used once
|
||||
// (and must be used once) to make a single HTTP request over the noise channel
|
||||
// to the coordination server.
|
||||
//
|
||||
// In addition to the RoundTripper, it returns the HTTP/2 channel's early noise
|
||||
// payload, if any.
|
||||
func (nc *NoiseClient) GetSingleUseRoundTripper(ctx context.Context) (http.RoundTripper, *tailcfg.EarlyNoise, error) {
|
||||
for tries := 0; tries < 3; tries++ {
|
||||
conn, err := nc.getConn(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ok, earlyPayloadMaybeNil, err := conn.ReserveNewRequest(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ok {
|
||||
return conn, earlyPayloadMaybeNil, nil
|
||||
}
|
||||
}
|
||||
return nil, nil, errors.New("[unexpected] failed to reserve a request on a connection")
|
||||
}
|
||||
|
||||
// contextErr is an error that wraps another error and is used to indicate that
|
||||
// the error was because a context expired.
|
||||
type contextErr struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e contextErr) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e contextErr) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// getConn returns a noiseconn.Conn that can be used to make requests to the
|
||||
// coordination server. It may return a cached connection or create a new one.
|
||||
// Dials are singleflighted, so concurrent calls to getConn may only dial once.
|
||||
// As such, context values may not be respected as there are no guarantees that
|
||||
// the context passed to getConn is the same as the context passed to dial.
|
||||
func (nc *NoiseClient) getConn(ctx context.Context) (*noiseconn.Conn, error) {
|
||||
nc.mu.Lock()
|
||||
if last := nc.last; last != nil && last.CanTakeNewRequest() {
|
||||
nc.mu.Unlock()
|
||||
return last, nil
|
||||
}
|
||||
nc.mu.Unlock()
|
||||
|
||||
for {
|
||||
// We singeflight the dial to avoid making multiple connections, however
|
||||
// that means that we can't simply cancel the dial if the context is
|
||||
// canceled. Instead, we have to additionally check that the context
|
||||
// which was canceled is our context and retry if our context is still
|
||||
// valid.
|
||||
conn, err, _ := nc.sfDial.Do(struct{}{}, func() (*noiseconn.Conn, error) {
|
||||
c, err := nc.dial(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, contextErr{ctx.Err()}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
})
|
||||
var ce contextErr
|
||||
if err == nil || !errors.As(err, &ce) {
|
||||
return conn, err
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
// The dial failed because of a context error, but our context
|
||||
// is still valid. Retry.
|
||||
continue
|
||||
}
|
||||
// The dial failed because our context was canceled. Return the
|
||||
// underlying error.
|
||||
return nil, ce.Unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
func (nc *NoiseClient) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
ctx := req.Context()
|
||||
conn, err := nc.getConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn.RoundTrip(req)
|
||||
}
|
||||
|
||||
// connClosed removes the connection with the provided ID from the pool
|
||||
// of active connections.
|
||||
func (nc *NoiseClient) connClosed(id int) {
|
||||
nc.mu.Lock()
|
||||
defer nc.mu.Unlock()
|
||||
conn := nc.connPool[id]
|
||||
if conn != nil {
|
||||
delete(nc.connPool, id)
|
||||
if nc.last == conn {
|
||||
nc.last = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes all the underlying noise connections.
|
||||
// It is a no-op and returns nil if the connection is already closed.
|
||||
func (nc *NoiseClient) Close() error {
|
||||
nc.mu.Lock()
|
||||
nc.closed = true
|
||||
conns := nc.connPool
|
||||
nc.connPool = nil
|
||||
nc.mu.Unlock()
|
||||
|
||||
var errors []error
|
||||
for _, c := range conns {
|
||||
if err := c.Close(); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
return multierr.New(errors...)
|
||||
}
|
||||
|
||||
// dial opens a new connection to tailcontrol, fetching the server noise key
|
||||
// if not cached.
|
||||
func (nc *NoiseClient) dial(ctx context.Context) (*noiseconn.Conn, error) {
|
||||
nc.mu.Lock()
|
||||
connID := nc.nextID
|
||||
nc.nextID++
|
||||
nc.mu.Unlock()
|
||||
|
||||
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
|
||||
// Panic, because a test should have started failing several
|
||||
// thousand version numbers before getting to this point.
|
||||
panic("capability version is too high to fit in the wire protocol")
|
||||
}
|
||||
|
||||
var dialPlan *tailcfg.ControlDialPlan
|
||||
if nc.dialPlan != nil {
|
||||
dialPlan = nc.dialPlan()
|
||||
}
|
||||
|
||||
// If we have a dial plan, then set our timeout as slightly longer than
|
||||
// the maximum amount of time contained therein; we assume that
|
||||
// explicit instructions on timeouts are more useful than a single
|
||||
// hard-coded timeout.
|
||||
//
|
||||
// The default value of 5 is chosen so that, when there's no dial plan,
|
||||
// we retain the previous behaviour of 10 seconds end-to-end timeout.
|
||||
timeoutSec := 5.0
|
||||
if dialPlan != nil {
|
||||
for _, c := range dialPlan.Candidates {
|
||||
if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
|
||||
timeoutSec = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After we establish a connection, we need some time to actually
|
||||
// upgrade it into a Noise connection. With a ballpark worst-case RTT
|
||||
// of 1000ms, give ourselves an extra 5 seconds to complete the
|
||||
// handshake.
|
||||
timeoutSec += 5
|
||||
|
||||
// Be extremely defensive and ensure that the timeout is in the range
|
||||
// [5, 60] seconds (e.g. if we accidentally get a negative number).
|
||||
if timeoutSec > 60 {
|
||||
timeoutSec = 60
|
||||
} else if timeoutSec < 5 {
|
||||
timeoutSec = 5
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
clientConn, err := (&controlhttp.Dialer{
|
||||
Hostname: nc.host,
|
||||
HTTPPort: nc.httpPort,
|
||||
HTTPSPort: cmp.Or(nc.httpsPort, controlhttp.NoPort),
|
||||
MachineKey: nc.privKey,
|
||||
ControlKey: nc.serverPubKey,
|
||||
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
|
||||
Dialer: nc.dialer.SystemDial,
|
||||
DNSCache: nc.dnsCache,
|
||||
DialPlan: dialPlan,
|
||||
Logf: nc.logf,
|
||||
NetMon: nc.netMon,
|
||||
HealthTracker: nc.health,
|
||||
Clock: tstime.StdClock{},
|
||||
}).Dial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ncc, err := noiseconn.New(clientConn.Conn, nc.h2t, connID, nc.connClosed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc.mu.Lock()
|
||||
if nc.closed {
|
||||
nc.mu.Unlock()
|
||||
ncc.Close() // Needs to be called without holding the lock.
|
||||
return nil, errors.New("noise client closed")
|
||||
}
|
||||
defer nc.mu.Unlock()
|
||||
mak.Set(&nc.connPool, connID, ncc)
|
||||
nc.last = ncc
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
// post does a POST to the control server at the given path, JSON-encoding body.
|
||||
// The provided nodeKey is an optional load balancing hint.
|
||||
func (nc *NoiseClient) post(ctx context.Context, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
|
||||
return nc.doWithBody(ctx, "POST", path, nodeKey, body)
|
||||
}
|
||||
|
||||
func (nc *NoiseClient) doWithBody(ctx context.Context, method, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
|
||||
jbody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, "https://"+nc.host+path, bytes.NewReader(jbody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addLBHeader(req, nodeKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
conn, err := nc.getConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn.RoundTrip(req)
|
||||
}
|
||||
11
vendor/tailscale.com/control/controlclient/sign_supported.go
generated
vendored
11
vendor/tailscale.com/control/controlclient/sign_supported.go
generated
vendored
@@ -18,7 +18,8 @@ import (
|
||||
"github.com/tailscale/certstore"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
)
|
||||
|
||||
// getMachineCertificateSubject returns the exact name of a Subject that needs
|
||||
@@ -30,8 +31,8 @@ import (
|
||||
// each RegisterRequest will be unsigned.
|
||||
//
|
||||
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
||||
func getMachineCertificateSubject() string {
|
||||
machineCertSubject, _ := syspolicy.GetString(syspolicy.MachineCertificateSubject, "")
|
||||
func getMachineCertificateSubject(polc policyclient.Client) string {
|
||||
machineCertSubject, _ := polc.GetString(pkey.MachineCertificateSubject, "")
|
||||
return machineCertSubject
|
||||
}
|
||||
|
||||
@@ -136,7 +137,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
|
||||
// using that identity's public key. In addition to the signature, the full
|
||||
// certificate chain is included so that the control server can validate the
|
||||
// certificate from a copy of the root CA's certificate.
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) (err error) {
|
||||
func signRegisterRequest(polc policyclient.Client, req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("signRegisterRequest: %w", err)
|
||||
@@ -147,7 +148,7 @@ func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverP
|
||||
return errBadRequest
|
||||
}
|
||||
|
||||
machineCertificateSubject := getMachineCertificateSubject()
|
||||
machineCertificateSubject := getMachineCertificateSubject(polc)
|
||||
if machineCertificateSubject == "" {
|
||||
return errCertificateNotConfigured
|
||||
}
|
||||
|
||||
3
vendor/tailscale.com/control/controlclient/sign_unsupported.go
generated
vendored
3
vendor/tailscale.com/control/controlclient/sign_unsupported.go
generated
vendored
@@ -8,9 +8,10 @@ package controlclient
|
||||
import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
)
|
||||
|
||||
// signRegisterRequest on non-supported platforms always returns errNoCertStore.
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) error {
|
||||
func signRegisterRequest(polc policyclient.Client, req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) error {
|
||||
return errNoCertStore
|
||||
}
|
||||
|
||||
90
vendor/tailscale.com/control/controlclient/status.go
generated
vendored
90
vendor/tailscale.com/control/controlclient/status.go
generated
vendored
@@ -4,8 +4,6 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -13,57 +11,6 @@ import (
|
||||
"tailscale.com/types/structs"
|
||||
)
|
||||
|
||||
// State is the high-level state of the client. It is used only in
|
||||
// unit tests for proper sequencing, don't depend on it anywhere else.
|
||||
//
|
||||
// TODO(apenwarr): eliminate the state, as it's now obsolete.
|
||||
//
|
||||
// apenwarr: Historical note: controlclient.Auto was originally
|
||||
// intended to be the state machine for the whole tailscale client, but that
|
||||
// turned out to not be the right abstraction layer, and it moved to
|
||||
// ipn.Backend. Since ipn.Backend now has a state machine, it would be
|
||||
// much better if controlclient could be a simple stateless API. But the
|
||||
// current server-side API (two interlocking polling https calls) makes that
|
||||
// very hard to implement. A server side API change could untangle this and
|
||||
// remove all the statefulness.
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateNew = State(iota)
|
||||
StateNotAuthenticated
|
||||
StateAuthenticating
|
||||
StateURLVisitRequired
|
||||
StateAuthenticated
|
||||
StateSynchronized // connected and received map update
|
||||
)
|
||||
|
||||
func (s State) AppendText(b []byte) ([]byte, error) {
|
||||
return append(b, s.String()...), nil
|
||||
}
|
||||
|
||||
func (s State) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateNew:
|
||||
return "state:new"
|
||||
case StateNotAuthenticated:
|
||||
return "state:not-authenticated"
|
||||
case StateAuthenticating:
|
||||
return "state:authenticating"
|
||||
case StateURLVisitRequired:
|
||||
return "state:url-visit-required"
|
||||
case StateAuthenticated:
|
||||
return "state:authenticated"
|
||||
case StateSynchronized:
|
||||
return "state:synchronized"
|
||||
default:
|
||||
return fmt.Sprintf("state:unknown:%d", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
_ structs.Incomparable
|
||||
|
||||
@@ -76,6 +23,14 @@ type Status struct {
|
||||
// URL, if non-empty, is the interactive URL to visit to finish logging in.
|
||||
URL string
|
||||
|
||||
// LoggedIn, if true, indicates that serveRegister has completed and no
|
||||
// other login change is in progress.
|
||||
LoggedIn bool
|
||||
|
||||
// InMapPoll, if true, indicates that we've received at least one netmap
|
||||
// and are connected to receive updates.
|
||||
InMapPoll bool
|
||||
|
||||
// NetMap is the latest server-pushed state of the tailnet network.
|
||||
NetMap *netmap.NetworkMap
|
||||
|
||||
@@ -83,26 +38,8 @@ type Status struct {
|
||||
//
|
||||
// TODO(bradfitz,maisem): clarify this.
|
||||
Persist persist.PersistView
|
||||
|
||||
// state is the internal state. It should not be exposed outside this
|
||||
// package, but we have some automated tests elsewhere that need to
|
||||
// use it via the StateForTest accessor.
|
||||
// TODO(apenwarr): Unexport or remove these.
|
||||
state State
|
||||
}
|
||||
|
||||
// LoginFinished reports whether the controlclient is in its "StateAuthenticated"
|
||||
// state where it's in a happy register state but not yet in a map poll.
|
||||
//
|
||||
// TODO(bradfitz): delete this and everything around Status.state.
|
||||
func (s *Status) LoginFinished() bool { return s.state == StateAuthenticated }
|
||||
|
||||
// StateForTest returns the internal state of s for tests only.
|
||||
func (s *Status) StateForTest() State { return s.state }
|
||||
|
||||
// SetStateForTest sets the internal state of s for tests only.
|
||||
func (s *Status) SetStateForTest(state State) { s.state = state }
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
func (s *Status) Equal(s2 *Status) bool {
|
||||
if s == nil && s2 == nil {
|
||||
@@ -111,15 +48,8 @@ func (s *Status) Equal(s2 *Status) bool {
|
||||
return s != nil && s2 != nil &&
|
||||
s.Err == s2.Err &&
|
||||
s.URL == s2.URL &&
|
||||
s.state == s2.state &&
|
||||
s.LoggedIn == s2.LoggedIn &&
|
||||
s.InMapPoll == s2.InMapPoll &&
|
||||
reflect.DeepEqual(s.Persist, s2.Persist) &&
|
||||
reflect.DeepEqual(s.NetMap, s2.NetMap)
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
b, err := json.MarshalIndent(s, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.state.String() + " " + string(b)
|
||||
}
|
||||
|
||||
272
vendor/tailscale.com/control/controlhttp/client.go
generated
vendored
272
vendor/tailscale.com/control/controlhttp/client.go
generated
vendored
@@ -20,37 +20,37 @@
|
||||
package controlhttp
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/control/controlhttp/controlhttpcommon"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/netx"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
var stdDialer net.Dialer
|
||||
@@ -81,7 +81,7 @@ func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||
if a.proxyFunc != nil {
|
||||
return a.proxyFunc
|
||||
}
|
||||
return tshttpproxy.ProxyFromEnvironment
|
||||
return feature.HookProxyFromEnvironment.GetOrNil()
|
||||
}
|
||||
|
||||
// httpsFallbackDelay is how long we'll wait for a.HTTPPort to work before
|
||||
@@ -103,157 +103,71 @@ func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
|
||||
// host we know about.
|
||||
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
|
||||
if !useDialPlan || a.DialPlan == nil || len(a.DialPlan.Candidates) == 0 {
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
return a.dialHost(ctx)
|
||||
}
|
||||
candidates := a.DialPlan.Candidates
|
||||
|
||||
// Otherwise, we try dialing per the plan. Store the highest priority
|
||||
// in the list, so that if we get a connection to one of those
|
||||
// candidates we can return quickly.
|
||||
var highestPriority int = math.MinInt
|
||||
for _, c := range candidates {
|
||||
if c.Priority > highestPriority {
|
||||
highestPriority = c.Priority
|
||||
}
|
||||
}
|
||||
|
||||
// This context allows us to cancel in-flight connections if we get a
|
||||
// highest-priority connection before we're all done.
|
||||
// Create a context to be canceled as we return, so once we get a good connection,
|
||||
// we can drop all the other ones.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Now, for each candidate, kick off a dial in parallel.
|
||||
type dialResult struct {
|
||||
conn *ClientConn
|
||||
err error
|
||||
addr netip.Addr
|
||||
priority int
|
||||
}
|
||||
resultsCh := make(chan dialResult, len(candidates))
|
||||
|
||||
var pending atomic.Int32
|
||||
pending.Store(int32(len(candidates)))
|
||||
for _, c := range candidates {
|
||||
go func(ctx context.Context, c tailcfg.ControlIPCandidate) {
|
||||
var (
|
||||
conn *ClientConn
|
||||
err error
|
||||
)
|
||||
|
||||
// Always send results back to our channel.
|
||||
defer func() {
|
||||
resultsCh <- dialResult{conn, err, c.IP, c.Priority}
|
||||
if pending.Add(-1) == 0 {
|
||||
close(resultsCh)
|
||||
}
|
||||
}()
|
||||
|
||||
// If non-zero, wait the configured start timeout
|
||||
// before we do anything.
|
||||
if c.DialStartDelaySec > 0 {
|
||||
a.logf("[v2] controlhttp: waiting %.2f seconds before dialing %q @ %v", c.DialStartDelaySec, a.Hostname, c.IP)
|
||||
tmr, tmrChannel := a.clock().NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
|
||||
defer tmr.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
return
|
||||
case <-tmrChannel:
|
||||
}
|
||||
}
|
||||
|
||||
// Now, create a sub-context with the given timeout and
|
||||
// try dialing the provided host.
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(c.DialTimeoutSec*float64(time.Second)))
|
||||
defer cancel()
|
||||
|
||||
// This will dial, and the defer above sends it back to our parent.
|
||||
a.logf("[v2] controlhttp: trying to dial %q @ %v", a.Hostname, c.IP)
|
||||
conn, err = a.dialHost(ctx, c.IP)
|
||||
}(ctx, c)
|
||||
}
|
||||
|
||||
var results []dialResult
|
||||
for res := range resultsCh {
|
||||
// If we get a response that has the highest priority, we don't
|
||||
// need to wait for any of the other connections to finish; we
|
||||
// can just return this connection.
|
||||
//
|
||||
// TODO(andrew): we could make this better by keeping track of
|
||||
// the highest remaining priority dynamically, instead of just
|
||||
// checking for the highest total
|
||||
if res.priority == highestPriority && res.conn != nil {
|
||||
a.logf("[v1] controlhttp: high-priority success dialing %q @ %v from dial plan", a.Hostname, res.addr)
|
||||
|
||||
// Drain the channel and any existing connections in
|
||||
// the background.
|
||||
go func() {
|
||||
for _, res := range results {
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
for res := range resultsCh {
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
if a.drainFinished != nil {
|
||||
close(a.drainFinished)
|
||||
}
|
||||
}()
|
||||
return res.conn, nil
|
||||
}
|
||||
|
||||
// This isn't a highest-priority result, so just store it until
|
||||
// we're done.
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
// After we finish this function, close any remaining open connections.
|
||||
defer func() {
|
||||
for _, result := range results {
|
||||
// Note: below, we nil out the returned connection (if
|
||||
// any) in the slice so we don't close it.
|
||||
if result.conn != nil {
|
||||
result.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// We don't drain asynchronously after this point, so notify our
|
||||
// channel when we return.
|
||||
if a.drainFinished != nil {
|
||||
close(a.drainFinished)
|
||||
}
|
||||
}()
|
||||
|
||||
// Sort by priority, then take the first non-error response.
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
// NOTE: intentionally inverted so that the highest priority
|
||||
// item comes first
|
||||
return results[i].priority > results[j].priority
|
||||
})
|
||||
|
||||
var (
|
||||
conn *ClientConn
|
||||
errs []error
|
||||
)
|
||||
for i, result := range results {
|
||||
if result.err != nil {
|
||||
errs = append(errs, result.err)
|
||||
continue
|
||||
err error
|
||||
}
|
||||
resultsCh := make(chan dialResult) // unbuffered, never closed
|
||||
|
||||
dialCand := func(cand tailcfg.ControlIPCandidate) (*ClientConn, error) {
|
||||
if cand.ACEHost != "" {
|
||||
a.logf("[v2] controlhttp: waited %.2f seconds, dialing %q via ACE %s (%s)", cand.DialStartDelaySec, a.Hostname, cand.ACEHost, cmp.Or(cand.IP.String(), "dns"))
|
||||
} else {
|
||||
a.logf("[v2] controlhttp: waited %.2f seconds, dialing %q @ %s", cand.DialStartDelaySec, a.Hostname, cand.IP.String())
|
||||
}
|
||||
|
||||
a.logf("[v1] controlhttp: succeeded dialing %q @ %v from dial plan", a.Hostname, result.addr)
|
||||
conn = result.conn
|
||||
results[i].conn = nil // so we don't close it in the defer
|
||||
return conn, nil
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(cand.DialTimeoutSec*float64(time.Second)))
|
||||
defer cancel()
|
||||
return a.dialHostOpt(ctx, cand.IP, cand.ACEHost)
|
||||
}
|
||||
merr := multierr.New(errs...)
|
||||
|
||||
// If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
|
||||
a.logf("controlhttp: failed dialing using DialPlan, falling back to DNS; errs=%s", merr.Error())
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
for _, cand := range candidates {
|
||||
timer := time.AfterFunc(time.Duration(cand.DialStartDelaySec*float64(time.Second)), func() {
|
||||
go func() {
|
||||
conn, err := dialCand(cand)
|
||||
select {
|
||||
case resultsCh <- dialResult{conn, err}:
|
||||
if err == nil {
|
||||
a.logf("[v1] controlhttp: succeeded dialing %q @ %v from dial plan", a.Hostname, cmp.Or(cand.ACEHost, cand.IP.String()))
|
||||
}
|
||||
case <-ctx.Done():
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
defer timer.Stop()
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for {
|
||||
select {
|
||||
case res := <-resultsCh:
|
||||
if res.err == nil {
|
||||
return res.conn, nil
|
||||
}
|
||||
errs = append(errs, res.err)
|
||||
if len(errs) == len(candidates) {
|
||||
// If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
|
||||
a.logf("controlhttp: failed dialing using DialPlan, falling back to DNS; errs=%s", errors.Join(errs...))
|
||||
return a.dialHost(ctx)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
a.logf("controlhttp: context aborted dialing")
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The TS_FORCE_NOISE_443 envknob forces the controlclient noise dialer to
|
||||
@@ -270,6 +184,15 @@ var forceNoise443 = envknob.RegisterBool("TS_FORCE_NOISE_443")
|
||||
// use HTTPS connections as its underlay connection (double crypto). This can
|
||||
// be necessary when networks or middle boxes are messing with port 80.
|
||||
func (d *Dialer) forceNoise443() bool {
|
||||
if runtime.GOOS == "plan9" {
|
||||
// For running demos of Plan 9 in a browser with network relays,
|
||||
// we want to minimize the number of connections we're making.
|
||||
// The main reason to use port 80 is to avoid double crypto
|
||||
// costs server-side but the costs are tiny and number of Plan 9
|
||||
// users doesn't make it worth it. Just disable this and always use
|
||||
// HTTPS for Plan 9. That also reduces some log spam.
|
||||
return true
|
||||
}
|
||||
if forceNoise443() {
|
||||
return true
|
||||
}
|
||||
@@ -301,10 +224,19 @@ var debugNoiseDial = envknob.RegisterBool("TS_DEBUG_NOISE_DIAL")
|
||||
|
||||
// dialHost connects to the configured Dialer.Hostname and upgrades the
|
||||
// connection into a controlbase.Conn.
|
||||
func (a *Dialer) dialHost(ctx context.Context) (*ClientConn, error) {
|
||||
return a.dialHostOpt(ctx,
|
||||
netip.Addr{}, // no pre-resolved IP
|
||||
"", // don't use ACE
|
||||
)
|
||||
}
|
||||
|
||||
// dialHostOpt connects to the configured Dialer.Hostname and upgrades the
|
||||
// connection into a controlbase.Conn.
|
||||
//
|
||||
// If optAddr is valid, then no DNS is used and the connection will be made to the
|
||||
// provided address.
|
||||
func (a *Dialer) dialHost(ctx context.Context, optAddr netip.Addr) (*ClientConn, error) {
|
||||
func (a *Dialer) dialHostOpt(ctx context.Context, optAddr netip.Addr, optACEHost string) (*ClientConn, 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.
|
||||
@@ -326,7 +258,7 @@ func (a *Dialer) dialHost(ctx context.Context, optAddr netip.Addr) (*ClientConn,
|
||||
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPSPort, "443")),
|
||||
Path: serverUpgradePath,
|
||||
}
|
||||
if a.HTTPSPort == NoPort {
|
||||
if a.HTTPSPort == NoPort || optACEHost != "" {
|
||||
u443 = nil
|
||||
}
|
||||
|
||||
@@ -338,11 +270,11 @@ func (a *Dialer) dialHost(ctx context.Context, optAddr netip.Addr) (*ClientConn,
|
||||
ch := make(chan tryURLRes) // must be unbuffered
|
||||
try := func(u *url.URL) {
|
||||
if debugNoiseDial() {
|
||||
a.logf("trying noise dial (%v, %v) ...", u, optAddr)
|
||||
a.logf("trying noise dial (%v, %v) ...", u, cmp.Or(optACEHost, optAddr.String()))
|
||||
}
|
||||
cbConn, err := a.dialURL(ctx, u, optAddr)
|
||||
cbConn, err := a.dialURL(ctx, u, optAddr, optACEHost)
|
||||
if debugNoiseDial() {
|
||||
a.logf("noise dial (%v, %v) = (%v, %v)", u, optAddr, cbConn, err)
|
||||
a.logf("noise dial (%v, %v) = (%v, %v)", u, cmp.Or(optACEHost, optAddr.String()), cbConn, err)
|
||||
}
|
||||
select {
|
||||
case ch <- tryURLRes{u, cbConn, err}:
|
||||
@@ -373,6 +305,9 @@ func (a *Dialer) dialHost(ctx context.Context, optAddr netip.Addr) (*ClientConn,
|
||||
}
|
||||
|
||||
var err80, err443 error
|
||||
if forceTLS {
|
||||
err80 = errors.New("TLS forced: no port 80 dialed")
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -408,12 +343,12 @@ func (a *Dialer) dialHost(ctx context.Context, optAddr netip.Addr) (*ClientConn,
|
||||
//
|
||||
// If optAddr is valid, then no DNS is used and the connection will be made to the
|
||||
// provided address.
|
||||
func (a *Dialer) dialURL(ctx context.Context, u *url.URL, optAddr netip.Addr) (*ClientConn, error) {
|
||||
func (a *Dialer) dialURL(ctx context.Context, u *url.URL, optAddr netip.Addr, optACEHost string) (*ClientConn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.MachineKey, a.ControlKey, a.ProtocolVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, optAddr, init)
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, optAddr, optACEHost, init)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -459,13 +394,15 @@ var macOSScreenTime = health.Register(&health.Warnable{
|
||||
ImpactsConnectivity: true,
|
||||
})
|
||||
|
||||
var HookMakeACEDialer feature.Hook[func(dialer netx.DialFunc, aceHost string, optIP netip.Addr) netx.DialFunc]
|
||||
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
|
||||
//
|
||||
// If optAddr is valid, then no DNS is used and the connection will be made to
|
||||
// the provided address.
|
||||
//
|
||||
// Only the provided ctx is used, not a.ctx.
|
||||
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Addr, init []byte) (_ net.Conn, retErr error) {
|
||||
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Addr, optACEHost string, init []byte) (_ net.Conn, retErr error) {
|
||||
var dns *dnscache.Resolver
|
||||
|
||||
// If we were provided an address to dial, then create a resolver that just
|
||||
@@ -480,13 +417,24 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
|
||||
dns = a.resolver()
|
||||
}
|
||||
|
||||
var dialer dnscache.DialContextFunc
|
||||
var dialer netx.DialFunc
|
||||
if a.Dialer != nil {
|
||||
dialer = a.Dialer
|
||||
} else {
|
||||
dialer = stdDialer.DialContext
|
||||
}
|
||||
|
||||
if optACEHost != "" {
|
||||
if !buildfeatures.HasACE {
|
||||
return nil, feature.ErrUnavailable
|
||||
}
|
||||
f, ok := HookMakeACEDialer.GetOk()
|
||||
if !ok {
|
||||
return nil, feature.ErrUnavailable
|
||||
}
|
||||
dialer = f(dialer, optACEHost, optAddr)
|
||||
}
|
||||
|
||||
// On macOS, see if Screen Time is blocking things.
|
||||
if runtime.GOOS == "darwin" {
|
||||
var proxydIntercepted atomic.Bool // intercepted by macOS webfilterproxyd
|
||||
@@ -513,13 +461,25 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
|
||||
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
defer tr.CloseIdleConnections()
|
||||
tr.Proxy = a.getProxyFunc()
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer, dns)
|
||||
if optACEHost != "" {
|
||||
// If using ACE, we don't want to use any HTTP proxy.
|
||||
// ACE is already a tunnel+proxy.
|
||||
// TODO(tailscale/corp#32483): use system proxy too?
|
||||
tr.Proxy = nil
|
||||
tr.DialContext = dialer
|
||||
} else {
|
||||
if buildfeatures.HasUseProxy {
|
||||
tr.Proxy = a.getProxyFunc()
|
||||
if set, ok := feature.HookProxySetTransportGetProxyConnectHeader.GetOk(); ok {
|
||||
set(tr)
|
||||
}
|
||||
}
|
||||
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.Hostname, a.HealthTracker, tr.TLSClientConfig)
|
||||
tr.TLSClientConfig = tlsdial.Config(a.HealthTracker, tr.TLSClientConfig)
|
||||
if !tr.TLSClientConfig.InsecureSkipVerify {
|
||||
panic("unexpected") // should be set by tlsdial.Config
|
||||
}
|
||||
|
||||
8
vendor/tailscale.com/control/controlhttp/constants.go
generated
vendored
8
vendor/tailscale.com/control/controlhttp/constants.go
generated
vendored
@@ -12,6 +12,7 @@ import (
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netx"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -66,7 +67,7 @@ type Dialer struct {
|
||||
// Dialer is the dialer used to make outbound connections.
|
||||
//
|
||||
// If not specified, this defaults to net.Dialer.DialContext.
|
||||
Dialer dnscache.DialContextFunc
|
||||
Dialer netx.DialFunc
|
||||
|
||||
// DNSCache is the caching Resolver used by this Dialer.
|
||||
//
|
||||
@@ -77,8 +78,8 @@ type Dialer struct {
|
||||
// dropped.
|
||||
Logf logger.Logf
|
||||
|
||||
// NetMon is the [netmon.Monitor] to use for this Dialer. It must be
|
||||
// non-nil.
|
||||
// NetMon is the [netmon.Monitor] to use for this Dialer.
|
||||
// It is optional.
|
||||
NetMon *netmon.Monitor
|
||||
|
||||
// HealthTracker, if non-nil, is the health tracker to use.
|
||||
@@ -97,7 +98,6 @@ type Dialer struct {
|
||||
logPort80Failure atomic.Bool
|
||||
|
||||
// For tests only
|
||||
drainFinished chan struct{}
|
||||
omitCertErrorLogging bool
|
||||
testFallbackDelay time.Duration
|
||||
|
||||
|
||||
28
vendor/tailscale.com/control/controlknobs/controlknobs.go
generated
vendored
28
vendor/tailscale.com/control/controlknobs/controlknobs.go
generated
vendored
@@ -62,8 +62,9 @@ type Knobs struct {
|
||||
// netfiltering, unless overridden by the user.
|
||||
LinuxForceNfTables atomic.Bool
|
||||
|
||||
// SeamlessKeyRenewal is whether to enable the alpha functionality of
|
||||
// renewing node keys without breaking connections.
|
||||
// SeamlessKeyRenewal is whether to renew node keys without breaking connections.
|
||||
// This is enabled by default in 1.90 and later, but we but we can remotely disable
|
||||
// it from the control plane if there's a problem.
|
||||
// http://go/seamless-key-renewal
|
||||
SeamlessKeyRenewal atomic.Bool
|
||||
|
||||
@@ -98,10 +99,6 @@ type Knobs struct {
|
||||
// allows us to disable the new behavior remotely if needed.
|
||||
DisableLocalDNSOverrideViaNRPT atomic.Bool
|
||||
|
||||
// DisableCryptorouting indicates that the node should not use the
|
||||
// magicsock crypto routing feature.
|
||||
DisableCryptorouting atomic.Bool
|
||||
|
||||
// DisableCaptivePortalDetection is whether the node should not perform captive portal detection
|
||||
// automatically when the network state changes.
|
||||
DisableCaptivePortalDetection atomic.Bool
|
||||
@@ -132,12 +129,12 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
forceIPTables = has(tailcfg.NodeAttrLinuxMustUseIPTables)
|
||||
forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables)
|
||||
seamlessKeyRenewal = has(tailcfg.NodeAttrSeamlessKeyRenewal)
|
||||
disableSeamlessKeyRenewal = has(tailcfg.NodeAttrDisableSeamlessKeyRenewal)
|
||||
probeUDPLifetime = has(tailcfg.NodeAttrProbeUDPLifetime)
|
||||
appCStoreRoutes = has(tailcfg.NodeAttrStoreAppCRoutes)
|
||||
userDialUseRoutes = has(tailcfg.NodeAttrUserDialUseRoutes)
|
||||
disableSplitDNSWhenNoCustomResolvers = has(tailcfg.NodeAttrDisableSplitDNSWhenNoCustomResolvers)
|
||||
disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT)
|
||||
disableCryptorouting = has(tailcfg.NodeAttrDisableMagicSockCryptoRouting)
|
||||
disableCaptivePortalDetection = has(tailcfg.NodeAttrDisableCaptivePortalDetection)
|
||||
disableSkipStatusQueue = has(tailcfg.NodeAttrDisableSkipStatusQueue)
|
||||
)
|
||||
@@ -159,15 +156,28 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
k.SilentDisco.Store(silentDisco)
|
||||
k.LinuxForceIPTables.Store(forceIPTables)
|
||||
k.LinuxForceNfTables.Store(forceNfTables)
|
||||
k.SeamlessKeyRenewal.Store(seamlessKeyRenewal)
|
||||
k.ProbeUDPLifetime.Store(probeUDPLifetime)
|
||||
k.AppCStoreRoutes.Store(appCStoreRoutes)
|
||||
k.UserDialUseRoutes.Store(userDialUseRoutes)
|
||||
k.DisableSplitDNSWhenNoCustomResolvers.Store(disableSplitDNSWhenNoCustomResolvers)
|
||||
k.DisableLocalDNSOverrideViaNRPT.Store(disableLocalDNSOverrideViaNRPT)
|
||||
k.DisableCryptorouting.Store(disableCryptorouting)
|
||||
k.DisableCaptivePortalDetection.Store(disableCaptivePortalDetection)
|
||||
k.DisableSkipStatusQueue.Store(disableSkipStatusQueue)
|
||||
|
||||
// If both attributes are present, then "enable" should win. This reflects
|
||||
// the history of seamless key renewal.
|
||||
//
|
||||
// Before 1.90, seamless was a private alpha, opt-in feature. Devices would
|
||||
// only seamless do if customers opted in using the seamless renewal attr.
|
||||
//
|
||||
// In 1.90 and later, seamless is the default behaviour, and devices will use
|
||||
// seamless unless explicitly told not to by control (e.g. if we discover
|
||||
// a bug and want clients to use the prior behaviour).
|
||||
//
|
||||
// If a customer has opted in to the pre-1.90 seamless implementation, we
|
||||
// don't want to switch it off for them -- we only want to switch it off for
|
||||
// devices that haven't opted in.
|
||||
k.SeamlessKeyRenewal.Store(seamlessKeyRenewal || !disableSeamlessKeyRenewal)
|
||||
}
|
||||
|
||||
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
|
||||
|
||||
312
vendor/tailscale.com/control/ts2021/client.go
generated
vendored
Normal file
312
vendor/tailscale.com/control/ts2021/client.go
generated
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ts2021
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// Client provides a http.Client to connect to tailcontrol over
|
||||
// the ts2021 protocol.
|
||||
type Client struct {
|
||||
// Client is an HTTP client to talk to the coordination server.
|
||||
// It automatically makes a new Noise connection as needed.
|
||||
*http.Client
|
||||
|
||||
logf logger.Logf // non-nil
|
||||
opts ClientOpts
|
||||
host string // the host part of serverURL
|
||||
httpPort string // the default port to dial
|
||||
httpsPort string // the fallback Noise-over-https port or empty if none
|
||||
|
||||
// mu protects the following
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
connPool set.HandleSet[*Conn] // all live connections
|
||||
}
|
||||
|
||||
// ClientOpts contains options for the [NewClient] function. All fields are
|
||||
// required unless otherwise specified.
|
||||
type ClientOpts struct {
|
||||
// ServerURL is the URL of the server to connect to.
|
||||
ServerURL string
|
||||
|
||||
// PrivKey is this node's private key.
|
||||
PrivKey key.MachinePrivate
|
||||
|
||||
// ServerPubKey is the public key of the server.
|
||||
// It is of the form https://<host>:<port> (no trailing slash).
|
||||
ServerPubKey key.MachinePublic
|
||||
|
||||
// Dialer's SystemDial function is used to connect to the server.
|
||||
Dialer *tsdial.Dialer
|
||||
|
||||
// Optional fields follow
|
||||
|
||||
// Logf is the log function to use.
|
||||
// If nil, log.Printf is used.
|
||||
Logf logger.Logf
|
||||
|
||||
// NetMon is the network monitor that will be used to get the
|
||||
// network interface state. This field can be nil; if so, the current
|
||||
// state will be looked up dynamically.
|
||||
NetMon *netmon.Monitor
|
||||
|
||||
// DNSCache is the caching Resolver to use to connect to the server.
|
||||
//
|
||||
// This field can be nil.
|
||||
DNSCache *dnscache.Resolver
|
||||
|
||||
// HealthTracker, if non-nil, is the health tracker to use.
|
||||
HealthTracker *health.Tracker
|
||||
|
||||
// DialPlan, if set, is a function that should return an explicit plan
|
||||
// on how to connect to the server.
|
||||
DialPlan func() *tailcfg.ControlDialPlan
|
||||
|
||||
// ProtocolVersion, if non-zero, specifies an alternate
|
||||
// protocol version to use instead of the default,
|
||||
// of [tailcfg.CurrentCapabilityVersion].
|
||||
ProtocolVersion uint16
|
||||
}
|
||||
|
||||
// NewClient returns a new noiseClient for the provided server and machine key.
|
||||
//
|
||||
// netMon may be nil, if non-nil it's used to do faster interface lookups.
|
||||
// dialPlan may be nil
|
||||
func NewClient(opts ClientOpts) (*Client, error) {
|
||||
logf := opts.Logf
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
if opts.ServerURL == "" {
|
||||
return nil, errors.New("ServerURL is required")
|
||||
}
|
||||
if opts.PrivKey.IsZero() {
|
||||
return nil, errors.New("PrivKey is required")
|
||||
}
|
||||
if opts.ServerPubKey.IsZero() {
|
||||
return nil, errors.New("ServerPubKey is required")
|
||||
}
|
||||
if opts.Dialer == nil {
|
||||
return nil, errors.New("Dialer is required")
|
||||
}
|
||||
|
||||
u, err := url.Parse(opts.ServerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ClientOpts.ServerURL: %w", err)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return nil, errors.New("invalid ServerURL scheme, must be http or https")
|
||||
}
|
||||
|
||||
httpPort, httpsPort := "80", "443"
|
||||
addr, _ := netip.ParseAddr(u.Hostname())
|
||||
isPrivateHost := addr.IsPrivate() || addr.IsLoopback() || u.Hostname() == "localhost"
|
||||
if port := u.Port(); port != "" {
|
||||
// If there is an explicit port specified, entirely rely on the scheme,
|
||||
// unless it's http with a private host in which case we never try using HTTPS.
|
||||
if u.Scheme == "https" {
|
||||
httpPort = ""
|
||||
httpsPort = port
|
||||
} else if u.Scheme == "http" {
|
||||
httpPort = port
|
||||
httpsPort = "443"
|
||||
if isPrivateHost {
|
||||
logf("setting empty HTTPS port with http scheme and private host %s", u.Hostname())
|
||||
httpsPort = ""
|
||||
}
|
||||
}
|
||||
} else if u.Scheme == "http" && isPrivateHost {
|
||||
// Whenever the scheme is http and the hostname is an IP address, do not set the HTTPS port,
|
||||
// as there cannot be a TLS certificate issued for an IP, unless it's a public IP.
|
||||
httpPort = "80"
|
||||
httpsPort = ""
|
||||
}
|
||||
|
||||
np := &Client{
|
||||
opts: opts,
|
||||
host: u.Hostname(),
|
||||
httpPort: httpPort,
|
||||
httpsPort: httpsPort,
|
||||
logf: logf,
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
Protocols: new(http.Protocols),
|
||||
MaxConnsPerHost: 1,
|
||||
}
|
||||
// We force only HTTP/2 for this transport, which is what the control server
|
||||
// speaks inside the ts2021 Noise encryption. But Go doesn't know about that,
|
||||
// so we use "SetUnencryptedHTTP2" even though it's actually encrypted.
|
||||
tr.Protocols.SetUnencryptedHTTP2(true)
|
||||
tr.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return np.dial(ctx)
|
||||
}
|
||||
|
||||
np.Client = &http.Client{Transport: tr}
|
||||
return np, nil
|
||||
}
|
||||
|
||||
// Close closes all the underlying noise connections.
|
||||
// It is a no-op and returns nil if the connection is already closed.
|
||||
func (nc *Client) Close() error {
|
||||
nc.mu.Lock()
|
||||
live := nc.connPool
|
||||
nc.closed = true
|
||||
nc.connPool = nil // stop noteConnClosed from mutating it as we loop over it (in live) below
|
||||
nc.mu.Unlock()
|
||||
|
||||
for _, c := range live {
|
||||
c.Close()
|
||||
}
|
||||
nc.Client.CloseIdleConnections()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dial opens a new connection to tailcontrol, fetching the server noise key
|
||||
// if not cached.
|
||||
func (nc *Client) dial(ctx context.Context) (*Conn, error) {
|
||||
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
|
||||
// Panic, because a test should have started failing several
|
||||
// thousand version numbers before getting to this point.
|
||||
panic("capability version is too high to fit in the wire protocol")
|
||||
}
|
||||
|
||||
var dialPlan *tailcfg.ControlDialPlan
|
||||
if nc.opts.DialPlan != nil {
|
||||
dialPlan = nc.opts.DialPlan()
|
||||
}
|
||||
|
||||
// If we have a dial plan, then set our timeout as slightly longer than
|
||||
// the maximum amount of time contained therein; we assume that
|
||||
// explicit instructions on timeouts are more useful than a single
|
||||
// hard-coded timeout.
|
||||
//
|
||||
// The default value of 5 is chosen so that, when there's no dial plan,
|
||||
// we retain the previous behaviour of 10 seconds end-to-end timeout.
|
||||
timeoutSec := 5.0
|
||||
if dialPlan != nil {
|
||||
for _, c := range dialPlan.Candidates {
|
||||
if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
|
||||
timeoutSec = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After we establish a connection, we need some time to actually
|
||||
// upgrade it into a Noise connection. With a ballpark worst-case RTT
|
||||
// of 1000ms, give ourselves an extra 5 seconds to complete the
|
||||
// handshake.
|
||||
timeoutSec += 5
|
||||
|
||||
// Be extremely defensive and ensure that the timeout is in the range
|
||||
// [5, 60] seconds (e.g. if we accidentally get a negative number).
|
||||
if timeoutSec > 60 {
|
||||
timeoutSec = 60
|
||||
} else if timeoutSec < 5 {
|
||||
timeoutSec = 5
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
chd := &controlhttp.Dialer{
|
||||
Hostname: nc.host,
|
||||
HTTPPort: nc.httpPort,
|
||||
HTTPSPort: cmp.Or(nc.httpsPort, controlhttp.NoPort),
|
||||
MachineKey: nc.opts.PrivKey,
|
||||
ControlKey: nc.opts.ServerPubKey,
|
||||
ProtocolVersion: cmp.Or(nc.opts.ProtocolVersion, uint16(tailcfg.CurrentCapabilityVersion)),
|
||||
Dialer: nc.opts.Dialer.SystemDial,
|
||||
DNSCache: nc.opts.DNSCache,
|
||||
DialPlan: dialPlan,
|
||||
Logf: nc.logf,
|
||||
NetMon: nc.opts.NetMon,
|
||||
HealthTracker: nc.opts.HealthTracker,
|
||||
Clock: tstime.StdClock{},
|
||||
}
|
||||
clientConn, err := chd.Dial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nc.mu.Lock()
|
||||
|
||||
handle := set.NewHandle()
|
||||
ncc := NewConn(clientConn.Conn, func() { nc.noteConnClosed(handle) })
|
||||
mak.Set(&nc.connPool, handle, ncc)
|
||||
|
||||
if nc.closed {
|
||||
nc.mu.Unlock()
|
||||
ncc.Close() // Needs to be called without holding the lock.
|
||||
return nil, errors.New("noise client closed")
|
||||
}
|
||||
|
||||
defer nc.mu.Unlock()
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
// noteConnClosed notes that the *Conn with the given handle has closed and
|
||||
// should be removed from the live connPool (which is usually of size 0 or 1,
|
||||
// except perhaps briefly 2 during a network failure and reconnect).
|
||||
func (nc *Client) noteConnClosed(handle set.Handle) {
|
||||
nc.mu.Lock()
|
||||
defer nc.mu.Unlock()
|
||||
nc.connPool.Delete(handle)
|
||||
}
|
||||
|
||||
// post does a POST to the control server at the given path, JSON-encoding body.
|
||||
// The provided nodeKey is an optional load balancing hint.
|
||||
func (nc *Client) Post(ctx context.Context, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
|
||||
return nc.DoWithBody(ctx, "POST", path, nodeKey, body)
|
||||
}
|
||||
|
||||
func (nc *Client) DoWithBody(ctx context.Context, method, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
|
||||
jbody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, "https://"+nc.host+path, bytes.NewReader(jbody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
AddLBHeader(req, nodeKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return nc.Do(req)
|
||||
}
|
||||
|
||||
// AddLBHeader adds the load balancer header to req if nodeKey is non-zero.
|
||||
func AddLBHeader(req *http.Request, nodeKey key.NodePublic) {
|
||||
if !nodeKey.IsZero() {
|
||||
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package noiseconn contains an internal-only wrapper around controlbase.Conn
|
||||
// that properly handles the early payload sent by the server before the HTTP/2
|
||||
// session begins.
|
||||
//
|
||||
// See the documentation on the Conn type for more details.
|
||||
package noiseconn
|
||||
// Package ts2021 handles the details of the Tailscale 2021 control protocol
|
||||
// that are after (above) the Noise layer. In particular, the
|
||||
// "tailcfg.EarlyNoise" message and the subsequent HTTP/2 connection.
|
||||
package ts2021
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -15,10 +13,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
@@ -29,12 +25,13 @@ import (
|
||||
// the pool when the connection is closed, properly handles an optional "early
|
||||
// payload" that's sent prior to beginning the HTTP/2 session, and provides a
|
||||
// way to return a connection to a pool when the connection is closed.
|
||||
//
|
||||
// Use [NewConn] to build a new Conn if you want [Conn.GetEarlyPayload] to work.
|
||||
// Otherwise making a Conn directly, only setting Conn, is fine.
|
||||
type Conn struct {
|
||||
*controlbase.Conn
|
||||
id int
|
||||
onClose func(int)
|
||||
h2cc *http2.ClientConn
|
||||
|
||||
onClose func() // or nil
|
||||
readHeaderOnce sync.Once // guards init of reader field
|
||||
reader io.Reader // (effectively Conn.Reader after header)
|
||||
earlyPayloadReady chan struct{} // closed after earlyPayload is set (including set to nil)
|
||||
@@ -42,31 +39,19 @@ type Conn struct {
|
||||
earlyPayloadErr error
|
||||
}
|
||||
|
||||
// New creates a new Conn that wraps the given controlbase.Conn.
|
||||
// NewConn creates a new Conn that wraps the given controlbase.Conn.
|
||||
//
|
||||
// h2t is the HTTP/2 transport to use for the connection; a new
|
||||
// http2.ClientConn will be created that reads from the returned Conn.
|
||||
//
|
||||
// connID should be a unique ID for this connection. When the Conn is closed,
|
||||
// the onClose function will be called with the connID if it is non-nil.
|
||||
func New(conn *controlbase.Conn, h2t *http2.Transport, connID int, onClose func(int)) (*Conn, error) {
|
||||
ncc := &Conn{
|
||||
// the onClose function will be called if it is non-nil.
|
||||
func NewConn(conn *controlbase.Conn, onClose func()) *Conn {
|
||||
return &Conn{
|
||||
Conn: conn,
|
||||
id: connID,
|
||||
onClose: onClose,
|
||||
earlyPayloadReady: make(chan struct{}),
|
||||
onClose: sync.OnceFunc(onClose),
|
||||
}
|
||||
h2cc, err := h2t.NewClientConn(ncc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ncc.h2cc = h2cc
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface.
|
||||
func (c *Conn) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return c.h2cc.RoundTrip(r)
|
||||
}
|
||||
|
||||
// GetEarlyPayload waits for the early Noise payload to arrive.
|
||||
@@ -76,6 +61,15 @@ func (c *Conn) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
// early Noise payload is ready (if any) and will return the same result for
|
||||
// the lifetime of the Conn.
|
||||
func (c *Conn) GetEarlyPayload(ctx context.Context) (*tailcfg.EarlyNoise, error) {
|
||||
if c.earlyPayloadReady == nil {
|
||||
return nil, errors.New("Conn was not created with NewConn; early payload not supported")
|
||||
}
|
||||
select {
|
||||
case <-c.earlyPayloadReady:
|
||||
return c.earlyPayload, c.earlyPayloadErr
|
||||
default:
|
||||
go c.readHeaderOnce.Do(c.readHeader)
|
||||
}
|
||||
select {
|
||||
case <-c.earlyPayloadReady:
|
||||
return c.earlyPayload, c.earlyPayloadErr
|
||||
@@ -84,28 +78,6 @@ func (c *Conn) GetEarlyPayload(ctx context.Context) (*tailcfg.EarlyNoise, error)
|
||||
}
|
||||
}
|
||||
|
||||
// ReserveNewRequest will reserve a new concurrent request on the connection.
|
||||
//
|
||||
// It returns whether the reservation was successful, and any early Noise
|
||||
// payload if present. If a reservation was not successful, it will return
|
||||
// false and nil for the early payload.
|
||||
func (c *Conn) ReserveNewRequest(ctx context.Context) (bool, *tailcfg.EarlyNoise, error) {
|
||||
earlyPayloadMaybeNil, err := c.GetEarlyPayload(ctx)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if c.h2cc.ReserveNewRequest() {
|
||||
return true, earlyPayloadMaybeNil, nil
|
||||
}
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
// CanTakeNewRequest reports whether the underlying HTTP/2 connection can take
|
||||
// a new request, meaning it has not been closed or received or sent a GOAWAY.
|
||||
func (c *Conn) CanTakeNewRequest() bool {
|
||||
return c.h2cc.CanTakeNewRequest()
|
||||
}
|
||||
|
||||
// The first 9 bytes from the server to client over Noise are either an HTTP/2
|
||||
// settings frame (a normal HTTP/2 setup) or, as we added later, an "early payload"
|
||||
// header that's also 9 bytes long: 5 bytes (EarlyPayloadMagic) followed by 4 bytes
|
||||
@@ -133,6 +105,14 @@ func (c *Conn) Read(p []byte) (n int, err error) {
|
||||
return c.reader.Read(p)
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *Conn) Close() error {
|
||||
if c.onClose != nil {
|
||||
defer c.onClose()
|
||||
}
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
// readHeader reads the optional "early payload" from the server that arrives
|
||||
// after the Noise handshake but before the HTTP/2 session begins.
|
||||
//
|
||||
@@ -140,7 +120,9 @@ func (c *Conn) Read(p []byte) (n int, err error) {
|
||||
// c.earlyPayload, closing c.earlyPayloadReady, and initializing c.reader for
|
||||
// future reads.
|
||||
func (c *Conn) readHeader() {
|
||||
defer close(c.earlyPayloadReady)
|
||||
if c.earlyPayloadReady != nil {
|
||||
defer close(c.earlyPayloadReady)
|
||||
}
|
||||
|
||||
setErr := func(err error) {
|
||||
c.reader = returnErrReader{err}
|
||||
@@ -174,14 +156,3 @@ func (c *Conn) readHeader() {
|
||||
}
|
||||
c.reader = c.Conn
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *Conn) Close() error {
|
||||
if err := c.Conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.onClose != nil {
|
||||
c.onClose(c.id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
111
vendor/tailscale.com/derp/derp.go
generated
vendored
111
vendor/tailscale.com/derp/derp.go
generated
vendored
@@ -27,27 +27,31 @@ import (
|
||||
// including its on-wire framing overhead)
|
||||
const MaxPacketSize = 64 << 10
|
||||
|
||||
// magic is the DERP magic number, sent in the frameServerKey frame
|
||||
// Magic is the DERP Magic number, sent in the frameServerKey frame
|
||||
// upon initial connection.
|
||||
const magic = "DERP🔑" // 8 bytes: 0x44 45 52 50 f0 9f 94 91
|
||||
const Magic = "DERP🔑" // 8 bytes: 0x44 45 52 50 f0 9f 94 91
|
||||
|
||||
const (
|
||||
nonceLen = 24
|
||||
frameHeaderLen = 1 + 4 // frameType byte + 4 byte length
|
||||
keyLen = 32
|
||||
maxInfoLen = 1 << 20
|
||||
keepAlive = 60 * time.Second
|
||||
NonceLen = 24
|
||||
FrameHeaderLen = 1 + 4 // frameType byte + 4 byte length
|
||||
KeyLen = 32
|
||||
MaxInfoLen = 1 << 20
|
||||
)
|
||||
|
||||
// KeepAlive is the minimum frequency at which the DERP server sends
|
||||
// keep alive frames. The server adds some jitter, so this timing is not
|
||||
// exact, but 2x this value can be considered a missed keep alive.
|
||||
const KeepAlive = 60 * time.Second
|
||||
|
||||
// ProtocolVersion is bumped whenever there's a wire-incompatible change.
|
||||
// - version 1 (zero on wire): consistent box headers, in use by employee dev nodes a bit
|
||||
// - version 2: received packets have src addrs in frameRecvPacket at beginning
|
||||
const ProtocolVersion = 2
|
||||
|
||||
// frameType is the one byte frame type at the beginning of the frame
|
||||
// FrameType is the one byte frame type at the beginning of the frame
|
||||
// header. The second field is a big-endian uint32 describing the
|
||||
// length of the remaining frame (not including the initial 5 bytes).
|
||||
type frameType byte
|
||||
type FrameType byte
|
||||
|
||||
/*
|
||||
Protocol flow:
|
||||
@@ -65,14 +69,14 @@ Steady state:
|
||||
* server then sends frameRecvPacket to recipient
|
||||
*/
|
||||
const (
|
||||
frameServerKey = frameType(0x01) // 8B magic + 32B public key + (0+ bytes future use)
|
||||
frameClientInfo = frameType(0x02) // 32B pub key + 24B nonce + naclbox(json)
|
||||
frameServerInfo = frameType(0x03) // 24B nonce + naclbox(json)
|
||||
frameSendPacket = frameType(0x04) // 32B dest pub key + packet bytes
|
||||
frameForwardPacket = frameType(0x0a) // 32B src pub key + 32B dst pub key + packet bytes
|
||||
frameRecvPacket = frameType(0x05) // v0/1: packet bytes, v2: 32B src pub key + packet bytes
|
||||
frameKeepAlive = frameType(0x06) // no payload, no-op (to be replaced with ping/pong)
|
||||
frameNotePreferred = frameType(0x07) // 1 byte payload: 0x01 or 0x00 for whether this is client's home node
|
||||
FrameServerKey = FrameType(0x01) // 8B magic + 32B public key + (0+ bytes future use)
|
||||
FrameClientInfo = FrameType(0x02) // 32B pub key + 24B nonce + naclbox(json)
|
||||
FrameServerInfo = FrameType(0x03) // 24B nonce + naclbox(json)
|
||||
FrameSendPacket = FrameType(0x04) // 32B dest pub key + packet bytes
|
||||
FrameForwardPacket = FrameType(0x0a) // 32B src pub key + 32B dst pub key + packet bytes
|
||||
FrameRecvPacket = FrameType(0x05) // v0/1: packet bytes, v2: 32B src pub key + packet bytes
|
||||
FrameKeepAlive = FrameType(0x06) // no payload, no-op (to be replaced with ping/pong)
|
||||
FrameNotePreferred = FrameType(0x07) // 1 byte payload: 0x01 or 0x00 for whether this is client's home node
|
||||
|
||||
// framePeerGone is sent from server to client to signal that
|
||||
// a previous sender is no longer connected. That is, if A
|
||||
@@ -81,7 +85,7 @@ const (
|
||||
// exists on that connection to get back to A. It is also sent
|
||||
// if A tries to send a CallMeMaybe to B and the server has no
|
||||
// record of B
|
||||
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
|
||||
FramePeerGone = FrameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
|
||||
|
||||
// framePeerPresent is like framePeerGone, but for other members of the DERP
|
||||
// region when they're meshed up together.
|
||||
@@ -92,7 +96,7 @@ const (
|
||||
// remaining after that, it's a PeerPresentFlags byte.
|
||||
// While current servers send 41 bytes, old servers will send fewer, and newer
|
||||
// servers might send more.
|
||||
framePeerPresent = frameType(0x09)
|
||||
FramePeerPresent = FrameType(0x09)
|
||||
|
||||
// frameWatchConns is how one DERP node in a regional mesh
|
||||
// subscribes to the others in the region.
|
||||
@@ -100,30 +104,30 @@ const (
|
||||
// is closed. Otherwise, the client is initially flooded with
|
||||
// framePeerPresent for all connected nodes, and then a stream of
|
||||
// framePeerPresent & framePeerGone has peers connect and disconnect.
|
||||
frameWatchConns = frameType(0x10)
|
||||
FrameWatchConns = FrameType(0x10)
|
||||
|
||||
// frameClosePeer is a privileged frame type (requires the
|
||||
// mesh key for now) that closes the provided peer's
|
||||
// connection. (To be used for cluster load balancing
|
||||
// purposes, when clients end up on a non-ideal node)
|
||||
frameClosePeer = frameType(0x11) // 32B pub key of peer to close.
|
||||
FrameClosePeer = FrameType(0x11) // 32B pub key of peer to close.
|
||||
|
||||
framePing = frameType(0x12) // 8 byte ping payload, to be echoed back in framePong
|
||||
framePong = frameType(0x13) // 8 byte payload, the contents of the ping being replied to
|
||||
FramePing = FrameType(0x12) // 8 byte ping payload, to be echoed back in framePong
|
||||
FramePong = FrameType(0x13) // 8 byte payload, the contents of the ping being replied to
|
||||
|
||||
// frameHealth is sent from server to client to tell the client
|
||||
// if their connection is unhealthy somehow. Currently the only unhealthy state
|
||||
// is whether the connection is detected as a duplicate.
|
||||
// The entire frame body is the text of the error message. An empty message
|
||||
// clears the error state.
|
||||
frameHealth = frameType(0x14)
|
||||
FrameHealth = FrameType(0x14)
|
||||
|
||||
// frameRestarting is sent from server to client for the
|
||||
// server to declare that it's restarting. Payload is two big
|
||||
// endian uint32 durations in milliseconds: when to reconnect,
|
||||
// and how long to try total. See ServerRestartingMessage docs for
|
||||
// more details on how the client should interpret them.
|
||||
frameRestarting = frameType(0x15)
|
||||
FrameRestarting = FrameType(0x15)
|
||||
)
|
||||
|
||||
// PeerGoneReasonType is a one byte reason code explaining why a
|
||||
@@ -150,6 +154,18 @@ const (
|
||||
PeerPresentNotIdeal = 1 << 3 // client said derp server is not its Region.Nodes[0] ideal node
|
||||
)
|
||||
|
||||
// IdealNodeHeader is the HTTP request header sent on DERP HTTP client requests
|
||||
// to indicate that they're connecting to their ideal (Region.Nodes[0]) node.
|
||||
// The HTTP header value is the name of the node they wish they were connected
|
||||
// to. This is an optional header.
|
||||
const IdealNodeHeader = "Ideal-Node"
|
||||
|
||||
// FastStartHeader is the header (with value "1") that signals to the HTTP
|
||||
// server that the DERP HTTP client does not want the HTTP 101 response
|
||||
// headers and it will begin writing & reading the DERP protocol immediately
|
||||
// following its HTTP request.
|
||||
const FastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
var bin = binary.BigEndian
|
||||
|
||||
func writeUint32(bw *bufio.Writer, v uint32) error {
|
||||
@@ -182,15 +198,24 @@ func readUint32(br *bufio.Reader) (uint32, error) {
|
||||
return bin.Uint32(b[:]), nil
|
||||
}
|
||||
|
||||
func readFrameTypeHeader(br *bufio.Reader, wantType frameType) (frameLen uint32, err error) {
|
||||
gotType, frameLen, err := readFrameHeader(br)
|
||||
// ReadFrameTypeHeader reads a frame header from br and
|
||||
// verifies that the frame type matches wantType.
|
||||
//
|
||||
// If it does, it returns the frame length (not including
|
||||
// the 5 byte header) and a nil error.
|
||||
//
|
||||
// If it doesn't, it returns an error and a zero length.
|
||||
func ReadFrameTypeHeader(br *bufio.Reader, wantType FrameType) (frameLen uint32, err error) {
|
||||
gotType, frameLen, err := ReadFrameHeader(br)
|
||||
if err == nil && wantType != gotType {
|
||||
err = fmt.Errorf("bad frame type 0x%X, want 0x%X", gotType, wantType)
|
||||
}
|
||||
return frameLen, err
|
||||
}
|
||||
|
||||
func readFrameHeader(br *bufio.Reader) (t frameType, frameLen uint32, err error) {
|
||||
// ReadFrameHeader reads the header of a DERP frame,
|
||||
// reading 5 bytes from br.
|
||||
func ReadFrameHeader(br *bufio.Reader) (t FrameType, frameLen uint32, err error) {
|
||||
tb, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
@@ -199,7 +224,7 @@ func readFrameHeader(br *bufio.Reader) (t frameType, frameLen uint32, err error)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return frameType(tb), frameLen, nil
|
||||
return FrameType(tb), frameLen, nil
|
||||
}
|
||||
|
||||
// readFrame reads a frame header and then reads its payload into
|
||||
@@ -212,8 +237,8 @@ func readFrameHeader(br *bufio.Reader) (t frameType, frameLen uint32, err error)
|
||||
// bytes are read, err will be io.ErrShortBuffer, and frameLen and t
|
||||
// will both be set. That is, callers need to explicitly handle when
|
||||
// they get more data than expected.
|
||||
func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t frameType, frameLen uint32, err error) {
|
||||
t, frameLen, err = readFrameHeader(br)
|
||||
func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t FrameType, frameLen uint32, err error) {
|
||||
t, frameLen, err = ReadFrameHeader(br)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -235,19 +260,26 @@ func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t frameType, frameLe
|
||||
return t, frameLen, err
|
||||
}
|
||||
|
||||
func writeFrameHeader(bw *bufio.Writer, t frameType, frameLen uint32) error {
|
||||
// WriteFrameHeader writes a frame header to bw.
|
||||
//
|
||||
// The frame header is 5 bytes: a one byte frame type
|
||||
// followed by a big-endian uint32 length of the
|
||||
// remaining frame (not including the 5 byte header).
|
||||
//
|
||||
// It does not flush bw.
|
||||
func WriteFrameHeader(bw *bufio.Writer, t FrameType, frameLen uint32) error {
|
||||
if err := bw.WriteByte(byte(t)); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeUint32(bw, frameLen)
|
||||
}
|
||||
|
||||
// writeFrame writes a complete frame & flushes it.
|
||||
func writeFrame(bw *bufio.Writer, t frameType, b []byte) error {
|
||||
// WriteFrame writes a complete frame & flushes it.
|
||||
func WriteFrame(bw *bufio.Writer, t FrameType, b []byte) error {
|
||||
if len(b) > 10<<20 {
|
||||
return errors.New("unreasonably large frame write")
|
||||
}
|
||||
if err := writeFrameHeader(bw, t, uint32(len(b))); err != nil {
|
||||
if err := WriteFrameHeader(bw, t, uint32(len(b))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bw.Write(b); err != nil {
|
||||
@@ -266,3 +298,12 @@ type Conn interface {
|
||||
SetReadDeadline(time.Time) error
|
||||
SetWriteDeadline(time.Time) error
|
||||
}
|
||||
|
||||
// ServerInfo is the message sent from the server to clients during
|
||||
// the connection setup.
|
||||
type ServerInfo struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
|
||||
TokenBucketBytesPerSecond int `json:",omitempty"`
|
||||
TokenBucketBytesBurst int `json:",omitempty"`
|
||||
}
|
||||
|
||||
97
vendor/tailscale.com/derp/derp_client.go
generated
vendored
97
vendor/tailscale.com/derp/derp_client.go
generated
vendored
@@ -30,7 +30,7 @@ type Client struct {
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
meshKey key.DERPMesh
|
||||
canAckPings bool
|
||||
isProber bool
|
||||
|
||||
@@ -56,7 +56,7 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
MeshKey key.DERPMesh
|
||||
ServerPub key.NodePublic
|
||||
CanAckPings bool
|
||||
IsProber bool
|
||||
@@ -66,7 +66,7 @@ type clientOpt struct {
|
||||
// access to join the mesh.
|
||||
//
|
||||
// An empty key means to not use a mesh key.
|
||||
func MeshKey(key string) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = key }) }
|
||||
func MeshKey(k key.DERPMesh) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = k }) }
|
||||
|
||||
// IsProber returns a ClientOpt to pass to the DERP server during connect to
|
||||
// declare that this client is a a prober.
|
||||
@@ -133,17 +133,17 @@ func (c *Client) recvServerKey() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flen < uint32(len(buf)) || t != frameServerKey || string(buf[:len(magic)]) != magic {
|
||||
if flen < uint32(len(buf)) || t != FrameServerKey || string(buf[:len(Magic)]) != Magic {
|
||||
return errors.New("invalid server greeting")
|
||||
}
|
||||
c.serverKey = key.NodePublicFromRaw32(mem.B(buf[len(magic):]))
|
||||
c.serverKey = key.NodePublicFromRaw32(mem.B(buf[len(Magic):]))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
const maxLength = nonceLen + maxInfoLen
|
||||
func (c *Client) parseServerInfo(b []byte) (*ServerInfo, error) {
|
||||
const maxLength = NonceLen + MaxInfoLen
|
||||
fl := len(b)
|
||||
if fl < nonceLen {
|
||||
if fl < NonceLen {
|
||||
return nil, fmt.Errorf("short serverInfo frame")
|
||||
}
|
||||
if fl > maxLength {
|
||||
@@ -153,19 +153,21 @@ func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to open naclbox from server key %s", c.serverKey)
|
||||
}
|
||||
info := new(serverInfo)
|
||||
info := new(ServerInfo)
|
||||
if err := json.Unmarshal(msg, info); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON: %v", err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type clientInfo struct {
|
||||
// ClientInfo is the information a DERP client sends to the server
|
||||
// about itself when it connects.
|
||||
type ClientInfo struct {
|
||||
// MeshKey optionally specifies a pre-shared key used by
|
||||
// trusted clients. It's required to subscribe to the
|
||||
// connection list & forward packets. It's empty for regular
|
||||
// users.
|
||||
MeshKey string `json:"meshKey,omitempty"`
|
||||
MeshKey key.DERPMesh `json:"meshKey,omitempty,omitzero"`
|
||||
|
||||
// Version is the DERP protocol version that the client was built with.
|
||||
// See the ProtocolVersion const.
|
||||
@@ -179,8 +181,19 @@ type clientInfo struct {
|
||||
IsProber bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Equal reports if two clientInfo values are equal.
|
||||
func (c *ClientInfo) Equal(other *ClientInfo) bool {
|
||||
if c == nil || other == nil {
|
||||
return c == other
|
||||
}
|
||||
if c.Version != other.Version || c.CanAckPings != other.CanAckPings || c.IsProber != other.IsProber {
|
||||
return false
|
||||
}
|
||||
return c.MeshKey.Equal(other.MeshKey)
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
msg, err := json.Marshal(ClientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
CanAckPings: c.canAckPings,
|
||||
@@ -191,10 +204,10 @@ func (c *Client) sendClientKey() error {
|
||||
}
|
||||
msgbox := c.privateKey.SealTo(c.serverKey, msg)
|
||||
|
||||
buf := make([]byte, 0, keyLen+len(msgbox))
|
||||
buf := make([]byte, 0, KeyLen+len(msgbox))
|
||||
buf = c.publicKey.AppendTo(buf)
|
||||
buf = append(buf, msgbox...)
|
||||
return writeFrame(c.bw, frameClientInfo, buf)
|
||||
return WriteFrame(c.bw, FrameClientInfo, buf)
|
||||
}
|
||||
|
||||
// ServerPublicKey returns the server's public key.
|
||||
@@ -219,12 +232,12 @@ func (c *Client) send(dstKey key.NodePublic, pkt []byte) (ret error) {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if c.rate != nil {
|
||||
pktLen := frameHeaderLen + key.NodePublicRawLen + len(pkt)
|
||||
pktLen := FrameHeaderLen + key.NodePublicRawLen + len(pkt)
|
||||
if !c.rate.AllowN(c.clock.Now(), pktLen) {
|
||||
return nil // drop
|
||||
}
|
||||
}
|
||||
if err := writeFrameHeader(c.bw, frameSendPacket, uint32(key.NodePublicRawLen+len(pkt))); err != nil {
|
||||
if err := WriteFrameHeader(c.bw, FrameSendPacket, uint32(key.NodePublicRawLen+len(pkt))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {
|
||||
@@ -253,7 +266,7 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
|
||||
timer := c.clock.AfterFunc(5*time.Second, c.writeTimeoutFired)
|
||||
defer timer.Stop()
|
||||
|
||||
if err := writeFrameHeader(c.bw, frameForwardPacket, uint32(keyLen*2+len(pkt))); err != nil {
|
||||
if err := WriteFrameHeader(c.bw, FrameForwardPacket, uint32(KeyLen*2+len(pkt))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(srcKey.AppendTo(nil)); err != nil {
|
||||
@@ -271,17 +284,17 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
|
||||
func (c *Client) writeTimeoutFired() { c.nc.Close() }
|
||||
|
||||
func (c *Client) SendPing(data [8]byte) error {
|
||||
return c.sendPingOrPong(framePing, data)
|
||||
return c.sendPingOrPong(FramePing, data)
|
||||
}
|
||||
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
return c.sendPingOrPong(framePong, data)
|
||||
return c.sendPingOrPong(FramePong, data)
|
||||
}
|
||||
|
||||
func (c *Client) sendPingOrPong(typ frameType, data [8]byte) error {
|
||||
func (c *Client) sendPingOrPong(typ FrameType, data [8]byte) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := writeFrameHeader(c.bw, typ, 8); err != nil {
|
||||
if err := WriteFrameHeader(c.bw, typ, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(data[:]); err != nil {
|
||||
@@ -303,7 +316,7 @@ func (c *Client) NotePreferred(preferred bool) (err error) {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
|
||||
if err := writeFrameHeader(c.bw, frameNotePreferred, 1); err != nil {
|
||||
if err := WriteFrameHeader(c.bw, FrameNotePreferred, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
var b byte = 0x00
|
||||
@@ -321,7 +334,7 @@ func (c *Client) NotePreferred(preferred bool) (err error) {
|
||||
func (c *Client) WatchConnectionChanges() error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := writeFrameHeader(c.bw, frameWatchConns, 0); err != nil {
|
||||
if err := WriteFrameHeader(c.bw, FrameWatchConns, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.bw.Flush()
|
||||
@@ -332,7 +345,7 @@ func (c *Client) WatchConnectionChanges() error {
|
||||
func (c *Client) ClosePeer(target key.NodePublic) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
return writeFrame(c.bw, frameClosePeer, target.AppendTo(nil))
|
||||
return WriteFrame(c.bw, FrameClosePeer, target.AppendTo(nil))
|
||||
}
|
||||
|
||||
// ReceivedMessage represents a type returned by Client.Recv. Unless
|
||||
@@ -491,7 +504,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
c.peeked = 0
|
||||
}
|
||||
|
||||
t, n, err := readFrameHeader(c.br)
|
||||
t, n, err := ReadFrameHeader(c.br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -522,7 +535,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
switch t {
|
||||
default:
|
||||
continue
|
||||
case frameServerInfo:
|
||||
case FrameServerInfo:
|
||||
// Server sends this at start-up. Currently unused.
|
||||
// Just has a JSON message saying "version: 2",
|
||||
// but the protocol seems extensible enough as-is without
|
||||
@@ -539,29 +552,29 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
}
|
||||
c.setSendRateLimiter(sm)
|
||||
return sm, nil
|
||||
case frameKeepAlive:
|
||||
case FrameKeepAlive:
|
||||
// A one-way keep-alive message that doesn't require an acknowledgement.
|
||||
// This predated framePing/framePong.
|
||||
return KeepAliveMessage{}, nil
|
||||
case framePeerGone:
|
||||
if n < keyLen {
|
||||
case FramePeerGone:
|
||||
if n < KeyLen {
|
||||
c.logf("[unexpected] dropping short peerGone frame from DERP server")
|
||||
continue
|
||||
}
|
||||
// Backward compatibility for the older peerGone without reason byte
|
||||
reason := PeerGoneReasonDisconnected
|
||||
if n > keyLen {
|
||||
reason = PeerGoneReasonType(b[keyLen])
|
||||
if n > KeyLen {
|
||||
reason = PeerGoneReasonType(b[KeyLen])
|
||||
}
|
||||
pg := PeerGoneMessage{
|
||||
Peer: key.NodePublicFromRaw32(mem.B(b[:keyLen])),
|
||||
Peer: key.NodePublicFromRaw32(mem.B(b[:KeyLen])),
|
||||
Reason: reason,
|
||||
}
|
||||
return pg, nil
|
||||
|
||||
case framePeerPresent:
|
||||
case FramePeerPresent:
|
||||
remain := b
|
||||
chunk, remain, ok := cutLeadingN(remain, keyLen)
|
||||
chunk, remain, ok := cutLeadingN(remain, KeyLen)
|
||||
if !ok {
|
||||
c.logf("[unexpected] dropping short peerPresent frame from DERP server")
|
||||
continue
|
||||
@@ -589,17 +602,17 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
msg.Flags = PeerPresentFlags(chunk[0])
|
||||
return msg, nil
|
||||
|
||||
case frameRecvPacket:
|
||||
case FrameRecvPacket:
|
||||
var rp ReceivedPacket
|
||||
if n < keyLen {
|
||||
if n < KeyLen {
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
}
|
||||
rp.Source = key.NodePublicFromRaw32(mem.B(b[:keyLen]))
|
||||
rp.Data = b[keyLen:n]
|
||||
rp.Source = key.NodePublicFromRaw32(mem.B(b[:KeyLen]))
|
||||
rp.Data = b[KeyLen:n]
|
||||
return rp, nil
|
||||
|
||||
case framePing:
|
||||
case FramePing:
|
||||
var pm PingMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short ping frame")
|
||||
@@ -608,7 +621,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
|
||||
case framePong:
|
||||
case FramePong:
|
||||
var pm PongMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short ping frame")
|
||||
@@ -617,10 +630,10 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
|
||||
case frameHealth:
|
||||
case FrameHealth:
|
||||
return HealthMessage{Problem: string(b[:])}, nil
|
||||
|
||||
case frameRestarting:
|
||||
case FrameRestarting:
|
||||
var m ServerRestartingMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short server restarting frame")
|
||||
|
||||
2387
vendor/tailscale.com/derp/derp_server.go
generated
vendored
2387
vendor/tailscale.com/derp/derp_server.go
generated
vendored
File diff suppressed because it is too large
Load Diff
13
vendor/tailscale.com/derp/derp_server_default.go
generated
vendored
13
vendor/tailscale.com/derp/derp_server_default.go
generated
vendored
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package derp
|
||||
|
||||
import "context"
|
||||
|
||||
func (c *sclient) startStatsLoop(ctx context.Context) {
|
||||
// Nothing to do
|
||||
return
|
||||
}
|
||||
89
vendor/tailscale.com/derp/derp_server_linux.go
generated
vendored
89
vendor/tailscale.com/derp/derp_server_linux.go
generated
vendored
@@ -1,89 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package derp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/tcpinfo"
|
||||
)
|
||||
|
||||
func (c *sclient) startStatsLoop(ctx context.Context) {
|
||||
// Get the RTT initially to verify it's supported.
|
||||
conn := c.tcpConn()
|
||||
if conn == nil {
|
||||
c.s.tcpRtt.Add("non-tcp", 1)
|
||||
return
|
||||
}
|
||||
if _, err := tcpinfo.RTT(conn); err != nil {
|
||||
c.logf("error fetching initial RTT: %v", err)
|
||||
c.s.tcpRtt.Add("error", 1)
|
||||
return
|
||||
}
|
||||
|
||||
const statsInterval = 10 * time.Second
|
||||
|
||||
// Don't launch a goroutine; use a timer instead.
|
||||
var gatherStats func()
|
||||
gatherStats = func() {
|
||||
// Do nothing if the context is finished.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Reschedule ourselves when this stats gathering is finished.
|
||||
defer c.s.clock.AfterFunc(statsInterval, gatherStats)
|
||||
|
||||
// Gather TCP RTT information.
|
||||
rtt, err := tcpinfo.RTT(conn)
|
||||
if err == nil {
|
||||
c.s.tcpRtt.Add(durationToLabel(rtt), 1)
|
||||
}
|
||||
|
||||
// TODO(andrew): more metrics?
|
||||
}
|
||||
|
||||
// Kick off the initial timer.
|
||||
c.s.clock.AfterFunc(statsInterval, gatherStats)
|
||||
}
|
||||
|
||||
// tcpConn attempts to get the underlying *net.TCPConn from this client's
|
||||
// Conn; if it cannot, then it will return nil.
|
||||
func (c *sclient) tcpConn() *net.TCPConn {
|
||||
nc := c.nc
|
||||
for {
|
||||
switch v := nc.(type) {
|
||||
case *net.TCPConn:
|
||||
return v
|
||||
case *tls.Conn:
|
||||
nc = v.NetConn()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func durationToLabel(dur time.Duration) string {
|
||||
switch {
|
||||
case dur <= 10*time.Millisecond:
|
||||
return "10ms"
|
||||
case dur <= 20*time.Millisecond:
|
||||
return "20ms"
|
||||
case dur <= 50*time.Millisecond:
|
||||
return "50ms"
|
||||
case dur <= 100*time.Millisecond:
|
||||
return "100ms"
|
||||
case dur <= 150*time.Millisecond:
|
||||
return "150ms"
|
||||
case dur <= 250*time.Millisecond:
|
||||
return "250ms"
|
||||
case dur <= 500*time.Millisecond:
|
||||
return "500ms"
|
||||
default:
|
||||
return "inf"
|
||||
}
|
||||
}
|
||||
11
vendor/tailscale.com/derp/derpconst/derpconst.go
generated
vendored
Normal file
11
vendor/tailscale.com/derp/derpconst/derpconst.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package derpconst contains constants used by the DERP client and server.
|
||||
package derpconst
|
||||
|
||||
// MetaCertCommonNamePrefix is the prefix that the DERP server
|
||||
// puts on for the common name of its "metacert". The suffix of
|
||||
// the common name after "derpkey" is the hex key.NodePublic
|
||||
// of the DERP server.
|
||||
const MetaCertCommonNamePrefix = "derpkey"
|
||||
38
vendor/tailscale.com/derp/derphttp/derphttp_client.go
generated
vendored
38
vendor/tailscale.com/derp/derphttp/derphttp_client.go
generated
vendored
@@ -30,14 +30,17 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derpconst"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/netx"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
@@ -55,7 +58,7 @@ type Client struct {
|
||||
TLSConfig *tls.Config // optional; nil means default
|
||||
HealthTracker *health.Tracker // optional; used if non-nil only
|
||||
DNSCache *dnscache.Resolver // optional; nil means no caching
|
||||
MeshKey string // optional; for trusted clients
|
||||
MeshKey key.DERPMesh // optional; for trusted clients
|
||||
IsProber bool // optional; for probers to optional declare themselves as such
|
||||
|
||||
// WatchConnectionChanges is whether the client wishes to subscribe to
|
||||
@@ -520,7 +523,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
// just to get routed into the server's HTTP Handler so it
|
||||
// can Hijack the request, but we signal with a special header
|
||||
// that we don't want to deal with its HTTP response.
|
||||
req.Header.Set(fastStartHeader, "1") // suppresses the server's HTTP response
|
||||
req.Header.Set(derp.FastStartHeader, "1") // suppresses the server's HTTP response
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -587,7 +590,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
//
|
||||
// The primary use for this is the derper mesh mode to connect to each
|
||||
// other over a VPC network.
|
||||
func (c *Client) SetURLDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
|
||||
func (c *Client) SetURLDialer(dialer netx.DialFunc) {
|
||||
c.dialer = dialer
|
||||
}
|
||||
|
||||
@@ -645,7 +648,10 @@ func (c *Client) dialRegion(ctx context.Context, reg *tailcfg.DERPRegion) (net.C
|
||||
}
|
||||
|
||||
func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
tlsConf := tlsdial.Config(c.tlsServerName(node), c.HealthTracker, c.TLSConfig)
|
||||
tlsConf := tlsdial.Config(c.HealthTracker, c.TLSConfig)
|
||||
// node is allowed to be nil here, tlsServerName falls back to using the URL
|
||||
// if node is nil.
|
||||
tlsConf.ServerName = c.tlsServerName(node)
|
||||
if node != nil {
|
||||
if node.InsecureForTests {
|
||||
tlsConf.InsecureSkipVerify = true
|
||||
@@ -729,8 +735,12 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
Path: "/", // unused
|
||||
},
|
||||
}
|
||||
if proxyURL, err := tshttpproxy.ProxyFromEnvironment(proxyReq); err == nil && proxyURL != nil {
|
||||
return c.dialNodeUsingProxy(ctx, n, proxyURL)
|
||||
if buildfeatures.HasUseProxy {
|
||||
if proxyFromEnv, ok := feature.HookProxyFromEnvironment.GetOk(); ok {
|
||||
if proxyURL, err := proxyFromEnv(proxyReq); err == nil && proxyURL != nil {
|
||||
return c.dialNodeUsingProxy(ctx, n, proxyURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type res struct {
|
||||
@@ -860,10 +870,14 @@ func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, pr
|
||||
target := net.JoinHostPort(n.HostName, "443")
|
||||
|
||||
var authHeader string
|
||||
if v, err := tshttpproxy.GetAuthHeader(pu); err != nil {
|
||||
c.logf("derphttp: error getting proxy auth header for %v: %v", proxyURL, err)
|
||||
} else if v != "" {
|
||||
authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v)
|
||||
if buildfeatures.HasUseProxy {
|
||||
if getAuthHeader, ok := feature.HookProxyGetAuthHeader.GetOk(); ok {
|
||||
if v, err := getAuthHeader(pu); err != nil {
|
||||
c.logf("derphttp: error getting proxy auth header for %v: %v", proxyURL, err)
|
||||
} else if v != "" {
|
||||
authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, target, authHeader); err != nil {
|
||||
@@ -1151,7 +1165,7 @@ var ErrClientClosed = errors.New("derphttp.Client closed")
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.NodePublic, serverProtoVersion int) {
|
||||
for _, cert := range certs {
|
||||
// Look for derpkey prefix added by initMetacert() on the server side.
|
||||
if pubHex, ok := strings.CutPrefix(cert.Subject.CommonName, "derpkey"); ok {
|
||||
if pubHex, ok := strings.CutPrefix(cert.Subject.CommonName, derpconst.MetaCertCommonNamePrefix); ok {
|
||||
var err error
|
||||
serverPub, err = key.ParseNodePublicUntyped(mem.S(pubHex))
|
||||
if err == nil && cert.SerialNumber.BitLen() <= 8 { // supports up to version 255
|
||||
|
||||
115
vendor/tailscale.com/derp/derphttp/derphttp_server.go
generated
vendored
115
vendor/tailscale.com/derp/derphttp/derphttp_server.go
generated
vendored
@@ -1,115 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/derp"
|
||||
)
|
||||
|
||||
// fastStartHeader is the header (with value "1") that signals to the HTTP
|
||||
// server that the DERP HTTP client does not want the HTTP 101 response
|
||||
// headers and it will begin writing & reading the DERP protocol immediately
|
||||
// following its HTTP request.
|
||||
const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
// Handler returns an http.Handler to be mounted at /derp, serving s.
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// These are installed both here and in cmd/derper. The check here
|
||||
// catches both cmd/derper run with DERP disabled (STUN only mode) as
|
||||
// well as DERP being run in tests with derphttp.Handler directly,
|
||||
// as netcheck still assumes this replies.
|
||||
switch r.URL.Path {
|
||||
case "/derp/probe", "/derp/latency-check":
|
||||
ProbeHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
if up != "websocket" && up != "derp" {
|
||||
if up != "" {
|
||||
log.Printf("Weird upgrade: %q", up)
|
||||
}
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
|
||||
fastStart := r.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
netConn, conn, err := h.Hijack()
|
||||
if err != nil {
|
||||
log.Printf("Hijack failed: %v", err)
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
if !fastStart {
|
||||
pubKey := s.PublicKey()
|
||||
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %s\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKey.UntypedHexString())
|
||||
}
|
||||
|
||||
if v := r.Header.Get(derp.IdealNodeHeader); v != "" {
|
||||
ctx = derp.IdealNodeContextKey.WithValue(ctx, v)
|
||||
}
|
||||
|
||||
s.Accept(ctx, netConn, conn, netConn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
|
||||
// ProbeHandler is the endpoint that clients without UDP access (including js/wasm) hit to measure
|
||||
// DERP latency, as a replacement for UDP STUN queries.
|
||||
func ProbeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "HEAD", "GET":
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
default:
|
||||
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// ServeNoContent generates the /generate_204 response used by Tailscale's
|
||||
// captive portal detection.
|
||||
func ServeNoContent(w http.ResponseWriter, r *http.Request) {
|
||||
if challenge := r.Header.Get(NoContentChallengeHeader); challenge != "" {
|
||||
badChar := strings.IndexFunc(challenge, func(r rune) bool {
|
||||
return !isChallengeChar(r)
|
||||
}) != -1
|
||||
if len(challenge) <= 64 && !badChar {
|
||||
w.Header().Set(NoContentResponseHeader, "response "+challenge)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, no-transform, max-age=0")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func isChallengeChar(c rune) bool {
|
||||
// Semi-randomly chosen as a limited set of valid characters
|
||||
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
|
||||
('0' <= c && c <= '9') ||
|
||||
c == '.' || c == '-' || c == '_' || c == ':'
|
||||
}
|
||||
|
||||
const (
|
||||
NoContentChallengeHeader = "X-Tailscale-Challenge"
|
||||
NoContentResponseHeader = "X-Tailscale-Response"
|
||||
)
|
||||
15
vendor/tailscale.com/derp/derphttp/mesh_client.go
generated
vendored
15
vendor/tailscale.com/derp/derphttp/mesh_client.go
generated
vendored
@@ -31,6 +31,9 @@ var testHookWatchLookConnectResult func(connectError error, wasSelfConnect bool)
|
||||
// This behavior will likely change. Callers should do their own accounting
|
||||
// and dup suppression as needed.
|
||||
//
|
||||
// If set the notifyError func is called with any error that occurs within the ctx
|
||||
// main loop connection setup, or the inner loop receiving messages via RecvDetail.
|
||||
//
|
||||
// infoLogf, if non-nil, is the logger to write periodic status updates about
|
||||
// how many peers are on the server. Error log output is set to the c's logger,
|
||||
// regardless of infoLogf's value.
|
||||
@@ -42,10 +45,11 @@ var testHookWatchLookConnectResult func(connectError error, wasSelfConnect bool)
|
||||
// initialized Client.WatchConnectionChanges to true.
|
||||
//
|
||||
// If the DERP connection breaks and reconnects, remove will be called for all
|
||||
// previously seen peers, with Reason type PeerGoneReasonSynthetic. Those
|
||||
// previously seen peers, with Reason type PeerGoneReasonMeshConnBroke. Those
|
||||
// clients are likely still connected and their add message will appear after
|
||||
// reconnect.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add func(derp.PeerPresentMessage), remove func(derp.PeerGoneMessage)) {
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf,
|
||||
add func(derp.PeerPresentMessage), remove func(derp.PeerGoneMessage), notifyError func(error)) {
|
||||
if !c.WatchConnectionChanges {
|
||||
if c.isStarted() {
|
||||
panic("invalid use of RunWatchConnectionLoop on already-started Client without setting Client.RunWatchConnectionLoop")
|
||||
@@ -121,6 +125,10 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
// Make sure we're connected before calling s.ServerPublicKey.
|
||||
_, _, err := c.connect(ctx, "RunWatchConnectionLoop")
|
||||
if err != nil {
|
||||
logf("mesh connect: %v", err)
|
||||
if notifyError != nil {
|
||||
notifyError(err)
|
||||
}
|
||||
if f := testHookWatchLookConnectResult; f != nil && !f(err, false) {
|
||||
return
|
||||
}
|
||||
@@ -141,6 +149,9 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("Recv: %v", err)
|
||||
if notifyError != nil {
|
||||
notifyError(err)
|
||||
}
|
||||
sleep(retryInterval)
|
||||
break
|
||||
}
|
||||
|
||||
388
vendor/tailscale.com/disco/disco.go
generated
vendored
388
vendor/tailscale.com/disco/disco.go
generated
vendored
@@ -25,6 +25,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/types/key"
|
||||
@@ -41,9 +42,15 @@ const NonceLen = 24
|
||||
type MessageType byte
|
||||
|
||||
const (
|
||||
TypePing = MessageType(0x01)
|
||||
TypePong = MessageType(0x02)
|
||||
TypeCallMeMaybe = MessageType(0x03)
|
||||
TypePing = MessageType(0x01)
|
||||
TypePong = MessageType(0x02)
|
||||
TypeCallMeMaybe = MessageType(0x03)
|
||||
TypeBindUDPRelayEndpoint = MessageType(0x04)
|
||||
TypeBindUDPRelayEndpointChallenge = MessageType(0x05)
|
||||
TypeBindUDPRelayEndpointAnswer = MessageType(0x06)
|
||||
TypeCallMeMaybeVia = MessageType(0x07)
|
||||
TypeAllocateUDPRelayEndpointRequest = MessageType(0x08)
|
||||
TypeAllocateUDPRelayEndpointResponse = MessageType(0x09)
|
||||
)
|
||||
|
||||
const v0 = byte(0)
|
||||
@@ -77,12 +84,25 @@ func Parse(p []byte) (Message, error) {
|
||||
}
|
||||
t, ver, p := MessageType(p[0]), p[1], p[2:]
|
||||
switch t {
|
||||
// TODO(jwhited): consider using a signature matching encoding.BinaryUnmarshaler
|
||||
case TypePing:
|
||||
return parsePing(ver, p)
|
||||
case TypePong:
|
||||
return parsePong(ver, p)
|
||||
case TypeCallMeMaybe:
|
||||
return parseCallMeMaybe(ver, p)
|
||||
case TypeBindUDPRelayEndpoint:
|
||||
return parseBindUDPRelayEndpoint(ver, p)
|
||||
case TypeBindUDPRelayEndpointChallenge:
|
||||
return parseBindUDPRelayEndpointChallenge(ver, p)
|
||||
case TypeBindUDPRelayEndpointAnswer:
|
||||
return parseBindUDPRelayEndpointAnswer(ver, p)
|
||||
case TypeCallMeMaybeVia:
|
||||
return parseCallMeMaybeVia(ver, p)
|
||||
case TypeAllocateUDPRelayEndpointRequest:
|
||||
return parseAllocateUDPRelayEndpointRequest(ver, p)
|
||||
case TypeAllocateUDPRelayEndpointResponse:
|
||||
return parseAllocateUDPRelayEndpointResponse(ver, p)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
|
||||
}
|
||||
@@ -91,6 +111,7 @@ func Parse(p []byte) (Message, error) {
|
||||
// Message a discovery message.
|
||||
type Message interface {
|
||||
// AppendMarshal appends the message's marshaled representation.
|
||||
// TODO(jwhited): consider using a signature matching encoding.BinaryAppender
|
||||
AppendMarshal([]byte) []byte
|
||||
}
|
||||
|
||||
@@ -266,7 +287,368 @@ func MessageSummary(m Message) string {
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case *CallMeMaybe:
|
||||
return "call-me-maybe"
|
||||
case *CallMeMaybeVia:
|
||||
return "call-me-maybe-via"
|
||||
case *BindUDPRelayEndpoint:
|
||||
return "bind-udp-relay-endpoint"
|
||||
case *BindUDPRelayEndpointChallenge:
|
||||
return "bind-udp-relay-endpoint-challenge"
|
||||
case *BindUDPRelayEndpointAnswer:
|
||||
return "bind-udp-relay-endpoint-answer"
|
||||
case *AllocateUDPRelayEndpointRequest:
|
||||
return "allocate-udp-relay-endpoint-request"
|
||||
case *AllocateUDPRelayEndpointResponse:
|
||||
return "allocate-udp-relay-endpoint-response"
|
||||
default:
|
||||
return fmt.Sprintf("%#v", m)
|
||||
}
|
||||
}
|
||||
|
||||
// BindUDPRelayHandshakeState represents the state of the 3-way bind handshake
|
||||
// between UDP relay client and UDP relay server. Its potential values include
|
||||
// those for both participants, UDP relay client and UDP relay server. A UDP
|
||||
// relay server implementation can be found in net/udprelay. This is currently
|
||||
// considered experimental.
|
||||
type BindUDPRelayHandshakeState int
|
||||
|
||||
const (
|
||||
// BindUDPRelayHandshakeStateInit represents the initial state prior to any
|
||||
// message being transmitted.
|
||||
BindUDPRelayHandshakeStateInit BindUDPRelayHandshakeState = iota
|
||||
// BindUDPRelayHandshakeStateBindSent is the first client state after
|
||||
// transmitting a BindUDPRelayEndpoint message to a UDP relay server.
|
||||
BindUDPRelayHandshakeStateBindSent
|
||||
// BindUDPRelayHandshakeStateChallengeSent is the first server state after
|
||||
// receiving a BindUDPRelayEndpoint message from a UDP relay client and
|
||||
// replying with a BindUDPRelayEndpointChallenge.
|
||||
BindUDPRelayHandshakeStateChallengeSent
|
||||
// BindUDPRelayHandshakeStateAnswerSent is a client state that is entered
|
||||
// after transmitting a BindUDPRelayEndpointAnswer message towards a UDP
|
||||
// relay server in response to a BindUDPRelayEndpointChallenge message.
|
||||
BindUDPRelayHandshakeStateAnswerSent
|
||||
// BindUDPRelayHandshakeStateAnswerReceived is a server state that is
|
||||
// entered after it has received a correct BindUDPRelayEndpointAnswer
|
||||
// message from a UDP relay client in response to a
|
||||
// BindUDPRelayEndpointChallenge message.
|
||||
BindUDPRelayHandshakeStateAnswerReceived
|
||||
)
|
||||
|
||||
// bindUDPRelayEndpointCommonLen is the length of a marshalled
|
||||
// [BindUDPRelayEndpointCommon], without the message header.
|
||||
const bindUDPRelayEndpointCommonLen = 72
|
||||
|
||||
// BindUDPRelayChallengeLen is the length of the Challenge field carried in
|
||||
// [BindUDPRelayEndpointChallenge] & [BindUDPRelayEndpointAnswer] messages.
|
||||
const BindUDPRelayChallengeLen = 32
|
||||
|
||||
// BindUDPRelayEndpointCommon contains fields that are common across all 3
|
||||
// UDP relay handshake message types. All 4 field values are expected to be
|
||||
// consistent for the lifetime of a handshake besides Challenge, which is
|
||||
// irrelevant in a [BindUDPRelayEndpoint] message.
|
||||
type BindUDPRelayEndpointCommon struct {
|
||||
// VNI is the Geneve header Virtual Network Identifier field value, which
|
||||
// must match this disco-sealed value upon reception. If they are
|
||||
// non-matching it indicates the cleartext Geneve header was tampered with
|
||||
// and/or mangled.
|
||||
VNI uint32
|
||||
// Generation represents the handshake generation. Clients must set a new,
|
||||
// nonzero value at the start of every handshake.
|
||||
Generation uint32
|
||||
// RemoteKey is the disco key of the remote peer participating over this
|
||||
// relay endpoint.
|
||||
RemoteKey key.DiscoPublic
|
||||
// Challenge is set by the server in a [BindUDPRelayEndpointChallenge]
|
||||
// message, and expected to be echoed back by the client in a
|
||||
// [BindUDPRelayEndpointAnswer] message. Its value is irrelevant in a
|
||||
// [BindUDPRelayEndpoint] message, where it simply serves a padding purpose
|
||||
// ensuring all handshake messages are equal in size.
|
||||
Challenge [BindUDPRelayChallengeLen]byte
|
||||
}
|
||||
|
||||
// encode encodes m in b. b must be at least bindUDPRelayEndpointCommonLen bytes
|
||||
// long.
|
||||
func (m *BindUDPRelayEndpointCommon) encode(b []byte) {
|
||||
binary.BigEndian.PutUint32(b, m.VNI)
|
||||
b = b[4:]
|
||||
binary.BigEndian.PutUint32(b, m.Generation)
|
||||
b = b[4:]
|
||||
m.RemoteKey.AppendTo(b[:0])
|
||||
b = b[key.DiscoPublicRawLen:]
|
||||
copy(b, m.Challenge[:])
|
||||
}
|
||||
|
||||
// decode decodes m from b.
|
||||
func (m *BindUDPRelayEndpointCommon) decode(b []byte) error {
|
||||
if len(b) < bindUDPRelayEndpointCommonLen {
|
||||
return errShort
|
||||
}
|
||||
m.VNI = binary.BigEndian.Uint32(b)
|
||||
b = b[4:]
|
||||
m.Generation = binary.BigEndian.Uint32(b)
|
||||
b = b[4:]
|
||||
m.RemoteKey = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen]))
|
||||
b = b[key.DiscoPublicRawLen:]
|
||||
copy(m.Challenge[:], b[:BindUDPRelayChallengeLen])
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindUDPRelayEndpoint is the first messaged transmitted from UDP relay client
|
||||
// towards UDP relay server as part of the 3-way bind handshake.
|
||||
type BindUDPRelayEndpoint struct {
|
||||
BindUDPRelayEndpointCommon
|
||||
}
|
||||
|
||||
func (m *BindUDPRelayEndpoint) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpoint, v0, bindUDPRelayEndpointCommonLen)
|
||||
m.BindUDPRelayEndpointCommon.encode(d)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseBindUDPRelayEndpoint(ver uint8, p []byte) (m *BindUDPRelayEndpoint, err error) {
|
||||
m = new(BindUDPRelayEndpoint)
|
||||
err = m.BindUDPRelayEndpointCommon.decode(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// BindUDPRelayEndpointChallenge is transmitted from UDP relay server towards
|
||||
// UDP relay client in response to a BindUDPRelayEndpoint message as part of the
|
||||
// 3-way bind handshake.
|
||||
type BindUDPRelayEndpointChallenge struct {
|
||||
BindUDPRelayEndpointCommon
|
||||
}
|
||||
|
||||
func (m *BindUDPRelayEndpointChallenge) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpointChallenge, v0, bindUDPRelayEndpointCommonLen)
|
||||
m.BindUDPRelayEndpointCommon.encode(d)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseBindUDPRelayEndpointChallenge(ver uint8, p []byte) (m *BindUDPRelayEndpointChallenge, err error) {
|
||||
m = new(BindUDPRelayEndpointChallenge)
|
||||
err = m.BindUDPRelayEndpointCommon.decode(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// BindUDPRelayEndpointAnswer is transmitted from UDP relay client to UDP relay
|
||||
// server in response to a BindUDPRelayEndpointChallenge message.
|
||||
type BindUDPRelayEndpointAnswer struct {
|
||||
BindUDPRelayEndpointCommon
|
||||
}
|
||||
|
||||
func (m *BindUDPRelayEndpointAnswer) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpointAnswer, v0, bindUDPRelayEndpointCommonLen)
|
||||
m.BindUDPRelayEndpointCommon.encode(d)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseBindUDPRelayEndpointAnswer(ver uint8, p []byte) (m *BindUDPRelayEndpointAnswer, err error) {
|
||||
m = new(BindUDPRelayEndpointAnswer)
|
||||
err = m.BindUDPRelayEndpointCommon.decode(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// AllocateUDPRelayEndpointRequest is a message sent only over DERP to request
|
||||
// allocation of a relay endpoint on a [tailscale.com/net/udprelay.Server]
|
||||
type AllocateUDPRelayEndpointRequest struct {
|
||||
// ClientDisco are the Disco public keys of the clients that should be
|
||||
// permitted to handshake with the endpoint.
|
||||
ClientDisco [2]key.DiscoPublic
|
||||
// Generation represents the allocation request generation. The server must
|
||||
// echo it back in the [AllocateUDPRelayEndpointResponse] to enable request
|
||||
// and response alignment client-side.
|
||||
Generation uint32
|
||||
}
|
||||
|
||||
// allocateUDPRelayEndpointRequestLen is the length of a marshaled
|
||||
// [AllocateUDPRelayEndpointRequest] message without the message header.
|
||||
const allocateUDPRelayEndpointRequestLen = key.DiscoPublicRawLen*2 + // ClientDisco
|
||||
4 // Generation
|
||||
|
||||
func (m *AllocateUDPRelayEndpointRequest) AppendMarshal(b []byte) []byte {
|
||||
ret, p := appendMsgHeader(b, TypeAllocateUDPRelayEndpointRequest, v0, allocateUDPRelayEndpointRequestLen)
|
||||
for i := 0; i < len(m.ClientDisco); i++ {
|
||||
disco := m.ClientDisco[i].AppendTo(nil)
|
||||
copy(p, disco)
|
||||
p = p[key.DiscoPublicRawLen:]
|
||||
}
|
||||
binary.BigEndian.PutUint32(p, m.Generation)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseAllocateUDPRelayEndpointRequest(ver uint8, p []byte) (m *AllocateUDPRelayEndpointRequest, err error) {
|
||||
m = new(AllocateUDPRelayEndpointRequest)
|
||||
if ver != 0 {
|
||||
return
|
||||
}
|
||||
if len(p) < allocateUDPRelayEndpointRequestLen {
|
||||
return m, errShort
|
||||
}
|
||||
for i := 0; i < len(m.ClientDisco); i++ {
|
||||
m.ClientDisco[i] = key.DiscoPublicFromRaw32(mem.B(p[:key.DiscoPublicRawLen]))
|
||||
p = p[key.DiscoPublicRawLen:]
|
||||
}
|
||||
m.Generation = binary.BigEndian.Uint32(p)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// AllocateUDPRelayEndpointResponse is a message sent only over DERP in response
|
||||
// to a [AllocateUDPRelayEndpointRequest].
|
||||
type AllocateUDPRelayEndpointResponse struct {
|
||||
// Generation represents the allocation request generation. The server must
|
||||
// echo back the [AllocateUDPRelayEndpointRequest.Generation] here to enable
|
||||
// request and response alignment client-side.
|
||||
Generation uint32
|
||||
UDPRelayEndpoint
|
||||
}
|
||||
|
||||
func (m *AllocateUDPRelayEndpointResponse) AppendMarshal(b []byte) []byte {
|
||||
endpointsLen := epLength * len(m.AddrPorts)
|
||||
generationLen := 4
|
||||
ret, d := appendMsgHeader(b, TypeAllocateUDPRelayEndpointResponse, v0, generationLen+udpRelayEndpointLenMinusAddrPorts+endpointsLen)
|
||||
binary.BigEndian.PutUint32(d, m.Generation)
|
||||
m.encode(d[4:])
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseAllocateUDPRelayEndpointResponse(ver uint8, p []byte) (m *AllocateUDPRelayEndpointResponse, err error) {
|
||||
m = new(AllocateUDPRelayEndpointResponse)
|
||||
if ver != 0 {
|
||||
return m, nil
|
||||
}
|
||||
if len(p) < 4 {
|
||||
return m, errShort
|
||||
}
|
||||
m.Generation = binary.BigEndian.Uint32(p)
|
||||
err = m.decode(p[4:])
|
||||
return m, err
|
||||
}
|
||||
|
||||
const udpRelayEndpointLenMinusAddrPorts = key.DiscoPublicRawLen + // ServerDisco
|
||||
(key.DiscoPublicRawLen * 2) + // ClientDisco
|
||||
8 + // LamportID
|
||||
4 + // VNI
|
||||
8 + // BindLifetime
|
||||
8 // SteadyStateLifetime
|
||||
|
||||
// UDPRelayEndpoint is a mirror of [tailscale.com/net/udprelay/endpoint.ServerEndpoint],
|
||||
// refer to it for field documentation. [UDPRelayEndpoint] is carried in both
|
||||
// [CallMeMaybeVia] and [AllocateUDPRelayEndpointResponse] messages.
|
||||
type UDPRelayEndpoint struct {
|
||||
// ServerDisco is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.ServerDisco]
|
||||
ServerDisco key.DiscoPublic
|
||||
// ClientDisco is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.ClientDisco]
|
||||
ClientDisco [2]key.DiscoPublic
|
||||
// LamportID is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.LamportID]
|
||||
LamportID uint64
|
||||
// VNI is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.VNI]
|
||||
VNI uint32
|
||||
// BindLifetime is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.BindLifetime]
|
||||
BindLifetime time.Duration
|
||||
// SteadyStateLifetime is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.SteadyStateLifetime]
|
||||
SteadyStateLifetime time.Duration
|
||||
// AddrPorts is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.AddrPorts]
|
||||
AddrPorts []netip.AddrPort
|
||||
}
|
||||
|
||||
// encode encodes m in b. b must be at least [udpRelayEndpointLenMinusAddrPorts]
|
||||
// + [epLength] * len(m.AddrPorts) bytes long.
|
||||
func (m *UDPRelayEndpoint) encode(b []byte) {
|
||||
disco := m.ServerDisco.AppendTo(nil)
|
||||
copy(b, disco)
|
||||
b = b[key.DiscoPublicRawLen:]
|
||||
for i := 0; i < len(m.ClientDisco); i++ {
|
||||
disco = m.ClientDisco[i].AppendTo(nil)
|
||||
copy(b, disco)
|
||||
b = b[key.DiscoPublicRawLen:]
|
||||
}
|
||||
binary.BigEndian.PutUint64(b[:8], m.LamportID)
|
||||
b = b[8:]
|
||||
binary.BigEndian.PutUint32(b[:4], m.VNI)
|
||||
b = b[4:]
|
||||
binary.BigEndian.PutUint64(b[:8], uint64(m.BindLifetime))
|
||||
b = b[8:]
|
||||
binary.BigEndian.PutUint64(b[:8], uint64(m.SteadyStateLifetime))
|
||||
b = b[8:]
|
||||
for _, ipp := range m.AddrPorts {
|
||||
a := ipp.Addr().As16()
|
||||
copy(b, a[:])
|
||||
binary.BigEndian.PutUint16(b[16:18], ipp.Port())
|
||||
b = b[epLength:]
|
||||
}
|
||||
}
|
||||
|
||||
// decode decodes m from b.
|
||||
func (m *UDPRelayEndpoint) decode(b []byte) error {
|
||||
if len(b) < udpRelayEndpointLenMinusAddrPorts+epLength ||
|
||||
(len(b)-udpRelayEndpointLenMinusAddrPorts)%epLength != 0 {
|
||||
return errShort
|
||||
}
|
||||
m.ServerDisco = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen]))
|
||||
b = b[key.DiscoPublicRawLen:]
|
||||
for i := 0; i < len(m.ClientDisco); i++ {
|
||||
m.ClientDisco[i] = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen]))
|
||||
b = b[key.DiscoPublicRawLen:]
|
||||
}
|
||||
m.LamportID = binary.BigEndian.Uint64(b[:8])
|
||||
b = b[8:]
|
||||
m.VNI = binary.BigEndian.Uint32(b[:4])
|
||||
b = b[4:]
|
||||
m.BindLifetime = time.Duration(binary.BigEndian.Uint64(b[:8]))
|
||||
b = b[8:]
|
||||
m.SteadyStateLifetime = time.Duration(binary.BigEndian.Uint64(b[:8]))
|
||||
b = b[8:]
|
||||
m.AddrPorts = make([]netip.AddrPort, 0, len(b)-udpRelayEndpointLenMinusAddrPorts/epLength)
|
||||
for len(b) > 0 {
|
||||
var a [16]byte
|
||||
copy(a[:], b)
|
||||
m.AddrPorts = append(m.AddrPorts, netip.AddrPortFrom(
|
||||
netip.AddrFrom16(a).Unmap(),
|
||||
binary.BigEndian.Uint16(b[16:18])))
|
||||
b = b[epLength:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallMeMaybeVia is a message sent only over DERP to request that the recipient
|
||||
// try to open up a magicsock path back to the sender. The 'Via' in
|
||||
// CallMeMaybeVia highlights that candidate paths are served through an
|
||||
// intermediate relay, likely a [tailscale.com/net/udprelay.Server].
|
||||
//
|
||||
// Usage of the candidate paths in magicsock requires a 3-way handshake
|
||||
// involving [BindUDPRelayEndpoint], [BindUDPRelayEndpointChallenge], and
|
||||
// [BindUDPRelayEndpointAnswer].
|
||||
//
|
||||
// CallMeMaybeVia mirrors [tailscale.com/net/udprelay/endpoint.ServerEndpoint],
|
||||
// which contains field documentation.
|
||||
//
|
||||
// The recipient may choose to not open a path back if it's already happy with
|
||||
// its path. Direct connections, e.g. [CallMeMaybe]-signaled, take priority over
|
||||
// CallMeMaybeVia paths.
|
||||
type CallMeMaybeVia struct {
|
||||
UDPRelayEndpoint
|
||||
}
|
||||
|
||||
func (m *CallMeMaybeVia) AppendMarshal(b []byte) []byte {
|
||||
endpointsLen := epLength * len(m.AddrPorts)
|
||||
ret, p := appendMsgHeader(b, TypeCallMeMaybeVia, v0, udpRelayEndpointLenMinusAddrPorts+endpointsLen)
|
||||
m.encode(p)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseCallMeMaybeVia(ver uint8, p []byte) (m *CallMeMaybeVia, err error) {
|
||||
m = new(CallMeMaybeVia)
|
||||
if ver != 0 {
|
||||
return m, nil
|
||||
}
|
||||
err = m.decode(p)
|
||||
return m, err
|
||||
}
|
||||
|
||||
79
vendor/tailscale.com/doctor/doctor.go
generated
vendored
79
vendor/tailscale.com/doctor/doctor.go
generated
vendored
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package doctor contains more in-depth healthchecks that can be run to aid in
|
||||
// diagnosing Tailscale issues.
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Check is the interface defining a singular check.
|
||||
//
|
||||
// A check should log information that it gathers using the provided log
|
||||
// function, and should attempt to make as much progress as possible in error
|
||||
// conditions.
|
||||
type Check interface {
|
||||
// Name should return a name describing this check, in lower-kebab-case
|
||||
// (i.e. "my-check", not "MyCheck" or "my_check").
|
||||
Name() string
|
||||
// Run executes the check, logging diagnostic information to the
|
||||
// provided logger function.
|
||||
Run(context.Context, logger.Logf) error
|
||||
}
|
||||
|
||||
// RunChecks runs a list of checks in parallel, and logs any returned errors
|
||||
// after all checks have returned.
|
||||
func RunChecks(ctx context.Context, log logger.Logf, checks ...Check) {
|
||||
if len(checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type namedErr struct {
|
||||
name string
|
||||
err error
|
||||
}
|
||||
errs := make(chan namedErr, len(checks))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(checks))
|
||||
for _, check := range checks {
|
||||
go func(c Check) {
|
||||
defer wg.Done()
|
||||
|
||||
plog := logger.WithPrefix(log, c.Name()+": ")
|
||||
errs <- namedErr{
|
||||
name: c.Name(),
|
||||
err: c.Run(ctx, plog),
|
||||
}
|
||||
}(check)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
for n := range errs {
|
||||
if n.err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
log("check %s: %v", n.name, n.err)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckFunc creates a Check from a name and a function.
|
||||
func CheckFunc(name string, run func(context.Context, logger.Logf) error) Check {
|
||||
return checkFunc{name, run}
|
||||
}
|
||||
|
||||
type checkFunc struct {
|
||||
name string
|
||||
run func(context.Context, logger.Logf) error
|
||||
}
|
||||
|
||||
func (c checkFunc) Name() string { return c.name }
|
||||
func (c checkFunc) Run(ctx context.Context, log logger.Logf) error { return c.run(ctx, log) }
|
||||
23
vendor/tailscale.com/doctor/ethtool/ethtool.go
generated
vendored
23
vendor/tailscale.com/doctor/ethtool/ethtool.go
generated
vendored
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ethtool provides a doctor.Check that prints diagnostic information
|
||||
// obtained from the 'ethtool' utility on the current system.
|
||||
package ethtool
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Check implements the doctor.Check interface.
|
||||
type Check struct{}
|
||||
|
||||
func (Check) Name() string {
|
||||
return "ethtool"
|
||||
}
|
||||
|
||||
func (Check) Run(_ context.Context, logf logger.Logf) error {
|
||||
return ethtoolImpl(logf)
|
||||
}
|
||||
78
vendor/tailscale.com/doctor/ethtool/ethtool_linux.go
generated
vendored
78
vendor/tailscale.com/doctor/ethtool/ethtool_linux.go
generated
vendored
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ethtool
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"sort"
|
||||
|
||||
"github.com/safchain/ethtool"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
func ethtoolImpl(logf logger.Logf) error {
|
||||
et, err := ethtool.NewEthtool()
|
||||
if err != nil {
|
||||
logf("could not create ethtool: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer et.Close()
|
||||
|
||||
netmon.ForeachInterface(func(iface netmon.Interface, _ []netip.Prefix) {
|
||||
ilogf := logger.WithPrefix(logf, iface.Name+": ")
|
||||
features, err := et.Features(iface.Name)
|
||||
if err == nil {
|
||||
enabled := []string{}
|
||||
for feature, value := range features {
|
||||
if value {
|
||||
enabled = append(enabled, feature)
|
||||
}
|
||||
}
|
||||
sort.Strings(enabled)
|
||||
ilogf("features: %v", enabled)
|
||||
} else {
|
||||
ilogf("features: error: %v", err)
|
||||
}
|
||||
|
||||
stats, err := et.Stats(iface.Name)
|
||||
if err == nil {
|
||||
printStats(ilogf, stats)
|
||||
} else {
|
||||
ilogf("stats: error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats that should be printed if non-zero
|
||||
var nonzeroStats = set.SetOf([]string{
|
||||
// AWS ENA driver statistics; see:
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/monitoring-network-performance-ena.html
|
||||
"bw_in_allowance_exceeded",
|
||||
"bw_out_allowance_exceeded",
|
||||
"conntrack_allowance_exceeded",
|
||||
"linklocal_allowance_exceeded",
|
||||
"pps_allowance_exceeded",
|
||||
})
|
||||
|
||||
// Stats that should be printed if zero
|
||||
var zeroStats = set.SetOf([]string{
|
||||
// AWS ENA driver statistics; see:
|
||||
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/monitoring-network-performance-ena.html
|
||||
"conntrack_allowance_available",
|
||||
})
|
||||
|
||||
func printStats(logf logger.Logf, stats map[string]uint64) {
|
||||
for name, value := range stats {
|
||||
if value != 0 && nonzeroStats.Contains(name) {
|
||||
logf("stats: warning: %s = %d > 0", name, value)
|
||||
}
|
||||
if value == 0 && zeroStats.Contains(name) {
|
||||
logf("stats: warning: %s = %d == 0", name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
vendor/tailscale.com/doctor/ethtool/ethtool_other.go
generated
vendored
17
vendor/tailscale.com/doctor/ethtool/ethtool_other.go
generated
vendored
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package ethtool
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func ethtoolImpl(logf logger.Logf) error {
|
||||
logf("unsupported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
return nil
|
||||
}
|
||||
59
vendor/tailscale.com/doctor/permissions/permissions.go
generated
vendored
59
vendor/tailscale.com/doctor/permissions/permissions.go
generated
vendored
@@ -1,59 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package permissions provides a doctor.Check that prints the process
|
||||
// permissions for the running process.
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Check implements the doctor.Check interface.
|
||||
type Check struct{}
|
||||
|
||||
func (Check) Name() string {
|
||||
return "permissions"
|
||||
}
|
||||
|
||||
func (Check) Run(_ context.Context, logf logger.Logf) error {
|
||||
return permissionsImpl(logf)
|
||||
}
|
||||
|
||||
//lint:ignore U1000 used in non-windows implementations.
|
||||
func formatUserID[T constraints.Integer](id T) string {
|
||||
idStr := fmt.Sprint(id)
|
||||
if uu, err := user.LookupId(idStr); err != nil {
|
||||
return idStr + "(<unknown>)"
|
||||
} else {
|
||||
return fmt.Sprintf("%s(%q)", idStr, uu.Username)
|
||||
}
|
||||
}
|
||||
|
||||
//lint:ignore U1000 used in non-windows implementations.
|
||||
func formatGroupID[T constraints.Integer](id T) string {
|
||||
idStr := fmt.Sprint(id)
|
||||
if g, err := user.LookupGroupId(idStr); err != nil {
|
||||
return idStr + "(<unknown>)"
|
||||
} else {
|
||||
return fmt.Sprintf("%s(%q)", idStr, g.Name)
|
||||
}
|
||||
}
|
||||
|
||||
//lint:ignore U1000 used in non-windows implementations.
|
||||
func formatGroups[T constraints.Integer](groups []T) string {
|
||||
var buf strings.Builder
|
||||
for i, group := range groups {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
buf.WriteString(formatGroupID(group))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
23
vendor/tailscale.com/doctor/permissions/permissions_bsd.go
generated
vendored
23
vendor/tailscale.com/doctor/permissions/permissions_bsd.go
generated
vendored
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build darwin || freebsd || openbsd
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func permissionsImpl(logf logger.Logf) error {
|
||||
groups, _ := unix.Getgroups()
|
||||
logf("uid=%s euid=%s gid=%s egid=%s groups=%s",
|
||||
formatUserID(unix.Getuid()),
|
||||
formatUserID(unix.Geteuid()),
|
||||
formatGroupID(unix.Getgid()),
|
||||
formatGroupID(unix.Getegid()),
|
||||
formatGroups(groups),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
62
vendor/tailscale.com/doctor/permissions/permissions_linux.go
generated
vendored
62
vendor/tailscale.com/doctor/permissions/permissions_linux.go
generated
vendored
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func permissionsImpl(logf logger.Logf) error {
|
||||
// NOTE: getresuid and getresgid never fail unless passed an
|
||||
// invalid address.
|
||||
var ruid, euid, suid uint64
|
||||
unix.Syscall(unix.SYS_GETRESUID,
|
||||
uintptr(unsafe.Pointer(&ruid)),
|
||||
uintptr(unsafe.Pointer(&euid)),
|
||||
uintptr(unsafe.Pointer(&suid)),
|
||||
)
|
||||
|
||||
var rgid, egid, sgid uint64
|
||||
unix.Syscall(unix.SYS_GETRESGID,
|
||||
uintptr(unsafe.Pointer(&rgid)),
|
||||
uintptr(unsafe.Pointer(&egid)),
|
||||
uintptr(unsafe.Pointer(&sgid)),
|
||||
)
|
||||
|
||||
groups, _ := unix.Getgroups()
|
||||
|
||||
var buf strings.Builder
|
||||
fmt.Fprintf(&buf, "ruid=%s euid=%s suid=%s rgid=%s egid=%s sgid=%s groups=%s",
|
||||
formatUserID(ruid), formatUserID(euid), formatUserID(suid),
|
||||
formatGroupID(rgid), formatGroupID(egid), formatGroupID(sgid),
|
||||
formatGroups(groups),
|
||||
)
|
||||
|
||||
// Get process capabilities
|
||||
var (
|
||||
capHeader = unix.CapUserHeader{
|
||||
Version: unix.LINUX_CAPABILITY_VERSION_3,
|
||||
Pid: 0, // 0 means 'ourselves'
|
||||
}
|
||||
capData unix.CapUserData
|
||||
)
|
||||
|
||||
if err := unix.Capget(&capHeader, &capData); err != nil {
|
||||
fmt.Fprintf(&buf, " caperr=%v", err)
|
||||
} else {
|
||||
fmt.Fprintf(&buf, " cap_effective=%08x cap_permitted=%08x cap_inheritable=%08x",
|
||||
capData.Effective, capData.Permitted, capData.Inheritable,
|
||||
)
|
||||
}
|
||||
|
||||
logf("%s", buf.String())
|
||||
return nil
|
||||
}
|
||||
17
vendor/tailscale.com/doctor/permissions/permissions_other.go
generated
vendored
17
vendor/tailscale.com/doctor/permissions/permissions_other.go
generated
vendored
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !(linux || darwin || freebsd || openbsd)
|
||||
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func permissionsImpl(logf logger.Logf) error {
|
||||
logf("unsupported on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
return nil
|
||||
}
|
||||
34
vendor/tailscale.com/doctor/routetable/routetable.go
generated
vendored
34
vendor/tailscale.com/doctor/routetable/routetable.go
generated
vendored
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package routetable provides a doctor.Check that dumps the current system's
|
||||
// route table to the log.
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/net/routetable"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// MaxRoutes is the maximum number of routes that will be displayed.
|
||||
const MaxRoutes = 1000
|
||||
|
||||
// Check implements the doctor.Check interface.
|
||||
type Check struct{}
|
||||
|
||||
func (Check) Name() string {
|
||||
return "routetable"
|
||||
}
|
||||
|
||||
func (Check) Run(_ context.Context, logf logger.Logf) error {
|
||||
rs, err := routetable.Get(MaxRoutes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range rs {
|
||||
logf("%s", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
47
vendor/tailscale.com/drive/drive_view.go
generated
vendored
47
vendor/tailscale.com/drive/drive_view.go
generated
vendored
@@ -6,9 +6,11 @@
|
||||
package drive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonv1 "encoding/json"
|
||||
"errors"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
@@ -42,8 +44,17 @@ func (v ShareView) AsStruct() *Share {
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v ShareView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
// MarshalJSON implements [jsonv1.Marshaler].
|
||||
func (v ShareView) MarshalJSON() ([]byte, error) {
|
||||
return jsonv1.Marshal(v.ж)
|
||||
}
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (v ShareView) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||
return jsonv2.MarshalEncode(enc, v.ж)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [jsonv1.Unmarshaler].
|
||||
func (v *ShareView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
@@ -52,16 +63,44 @@ func (v *ShareView) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
var x Share
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
if err := jsonv1.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
func (v *ShareView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
var x Share
|
||||
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name is how this share appears on remote nodes.
|
||||
func (v ShareView) Name() string { return v.ж.Name }
|
||||
|
||||
// Path is the path to the directory on this machine that's being shared.
|
||||
func (v ShareView) Path() string { return v.ж.Path }
|
||||
func (v ShareView) As() string { return v.ж.As }
|
||||
|
||||
// As is the UNIX or Windows username of the local account used for this
|
||||
// share. File read/write permissions are enforced based on this username.
|
||||
// Can be left blank to use the default value of "whoever is running the
|
||||
// Tailscale GUI".
|
||||
func (v ShareView) As() string { return v.ж.As }
|
||||
|
||||
// BookmarkData contains security-scoped bookmark data for the Sandboxed
|
||||
// Mac application. The Sandboxed Mac application gains permission to
|
||||
// access the Share's folder as a result of a user selecting it in a file
|
||||
// picker. In order to retain access to it across restarts, it needs to
|
||||
// hold on to a security-scoped bookmark. That bookmark is stored here. See
|
||||
// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox#4144043
|
||||
func (v ShareView) BookmarkData() views.ByteSlice[[]byte] {
|
||||
return views.ByteSliceOf(v.ж.BookmarkData)
|
||||
}
|
||||
|
||||
2
vendor/tailscale.com/drive/local.go
generated
vendored
2
vendor/tailscale.com/drive/local.go
generated
vendored
@@ -17,7 +17,7 @@ import (
|
||||
// Remote represents a remote Taildrive node.
|
||||
type Remote struct {
|
||||
Name string
|
||||
URL string
|
||||
URL func() string
|
||||
Available func() bool
|
||||
}
|
||||
|
||||
|
||||
24
vendor/tailscale.com/drive/remote.go
generated
vendored
24
vendor/tailscale.com/drive/remote.go
generated
vendored
@@ -9,7 +9,6 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -21,10 +20,6 @@ var (
|
||||
ErrInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
|
||||
)
|
||||
|
||||
var (
|
||||
shareNameRegex = regexp.MustCompile(`^[a-z0-9_\(\) ]+$`)
|
||||
)
|
||||
|
||||
// AllowShareAs reports whether sharing files as a specific user is allowed.
|
||||
func AllowShareAs() bool {
|
||||
return !DisallowShareAs && doAllowShareAs()
|
||||
@@ -125,9 +120,26 @@ func NormalizeShareName(name string) (string, error) {
|
||||
// Trim whitespace
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
if !shareNameRegex.MatchString(name) {
|
||||
if !validShareName(name) {
|
||||
return "", ErrInvalidShareName
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func validShareName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range name {
|
||||
if 'a' <= r && r <= 'z' || '0' <= r && r <= '9' {
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '_', ' ', '(', ')':
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
2
vendor/tailscale.com/drive/remote_permissions.go
generated
vendored
2
vendor/tailscale.com/drive/remote_permissions.go
generated
vendored
@@ -32,7 +32,7 @@ type grant struct {
|
||||
Access string
|
||||
}
|
||||
|
||||
// ParsePermissions builds a Permissions map from a lis of raw grants.
|
||||
// ParsePermissions builds a Permissions map from a list of raw grants.
|
||||
func ParsePermissions(rawGrants [][]byte) (Permissions, error) {
|
||||
permissions := make(Permissions)
|
||||
for _, rawGrant := range rawGrants {
|
||||
|
||||
22
vendor/tailscale.com/envknob/envknob.go
generated
vendored
22
vendor/tailscale.com/envknob/envknob.go
generated
vendored
@@ -28,18 +28,19 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/kube/kubetypes"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
mu syncs.Mutex
|
||||
// +checklocks:mu
|
||||
set = map[string]string{}
|
||||
// +checklocks:mu
|
||||
@@ -463,7 +464,12 @@ var allowRemoteUpdate = RegisterBool("TS_ALLOW_ADMIN_CONSOLE_REMOTE_UPDATE")
|
||||
// AllowsRemoteUpdate reports whether this node has opted-in to letting the
|
||||
// Tailscale control plane initiate a Tailscale update (e.g. on behalf of an
|
||||
// admin on the admin console).
|
||||
func AllowsRemoteUpdate() bool { return allowRemoteUpdate() }
|
||||
func AllowsRemoteUpdate() bool {
|
||||
if !buildfeatures.HasClientUpdate {
|
||||
return false
|
||||
}
|
||||
return allowRemoteUpdate()
|
||||
}
|
||||
|
||||
// SetNoLogsNoSupport enables no-logs-no-support mode.
|
||||
func SetNoLogsNoSupport() {
|
||||
@@ -474,6 +480,9 @@ func SetNoLogsNoSupport() {
|
||||
var notInInit atomic.Bool
|
||||
|
||||
func assertNotInInit() {
|
||||
if !buildfeatures.HasDebug {
|
||||
return
|
||||
}
|
||||
if notInInit.Load() {
|
||||
return
|
||||
}
|
||||
@@ -533,6 +542,11 @@ func ApplyDiskConfigError() error { return applyDiskConfigErr }
|
||||
// for App Store builds
|
||||
// - /etc/tailscale/tailscaled-env.txt for tailscaled-on-macOS (homebrew, etc)
|
||||
func ApplyDiskConfig() (err error) {
|
||||
if runtime.GOOS == "linux" && !(buildfeatures.HasDebug || buildfeatures.HasSynology) {
|
||||
// This function does nothing on Linux, unless you're
|
||||
// using TS_DEBUG_ENV_FILE or are on Synology.
|
||||
return nil
|
||||
}
|
||||
var f *os.File
|
||||
defer func() {
|
||||
if err != nil {
|
||||
@@ -593,7 +607,7 @@ func getPlatformEnvFiles() []string {
|
||||
filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt"),
|
||||
}
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology {
|
||||
if buildfeatures.HasSynology && distro.Get() == distro.Synology {
|
||||
return []string{"/etc/tailscale/tailscaled-env.txt"}
|
||||
}
|
||||
case "darwin":
|
||||
|
||||
16
vendor/tailscale.com/envknob/featureknob/featureknob.go
generated
vendored
16
vendor/tailscale.com/envknob/featureknob/featureknob.go
generated
vendored
@@ -10,7 +10,6 @@ import (
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -26,21 +25,13 @@ func CanRunTailscaleSSH() error {
|
||||
if distro.Get() == distro.QNAP && !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server does not run on QNAP.")
|
||||
}
|
||||
|
||||
// Setting SSH on Home Assistant causes trouble on startup
|
||||
// (since the flag is not being passed to `tailscale up`).
|
||||
// Although Tailscale SSH does work here,
|
||||
// it's not terribly useful since it's running in a separate container.
|
||||
if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn {
|
||||
return errors.New("The Tailscale SSH server does not run on HomeAssistant.")
|
||||
}
|
||||
// otherwise okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
case "freebsd", "openbsd":
|
||||
case "freebsd", "openbsd", "plan9":
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
}
|
||||
@@ -58,10 +49,5 @@ func CanUseExitNode() error {
|
||||
distro.QNAP:
|
||||
return errors.New("Tailscale exit nodes cannot be used on " + string(dist))
|
||||
}
|
||||
|
||||
if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn {
|
||||
return errors.New("Tailscale exit nodes cannot be used on HomeAssistant.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
10
vendor/tailscale.com/feature/buildfeatures/buildfeatures.go
generated
vendored
Normal file
10
vendor/tailscale.com/feature/buildfeatures/buildfeatures.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
// The buildfeatures package contains boolean constants indicating which
|
||||
// features were included in the binary (via build tags), for use in dead code
|
||||
// elimination when using separate build tag protected files is impractical
|
||||
// or undesirable.
|
||||
package buildfeatures
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_ace_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_ace_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_ace
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasACE is whether the binary was built with support for modular feature "Alternate Connectivity Endpoints".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_ace" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasACE = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_ace_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_ace_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_ace
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasACE is whether the binary was built with support for modular feature "Alternate Connectivity Endpoints".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_ace" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasACE = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_acme_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_acme_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_acme
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasACME is whether the binary was built with support for modular feature "ACME TLS certificate management".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_acme" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasACME = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_acme_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_acme_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_acme
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasACME is whether the binary was built with support for modular feature "ACME TLS certificate management".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_acme" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasACME = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_advertiseexitnode_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_advertiseexitnode_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_advertiseexitnode
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasAdvertiseExitNode is whether the binary was built with support for modular feature "Run an exit node".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_advertiseexitnode" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasAdvertiseExitNode = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_advertiseexitnode_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_advertiseexitnode_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_advertiseexitnode
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasAdvertiseExitNode is whether the binary was built with support for modular feature "Run an exit node".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_advertiseexitnode" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasAdvertiseExitNode = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_advertiseroutes_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_advertiseroutes_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_advertiseroutes
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasAdvertiseRoutes is whether the binary was built with support for modular feature "Advertise routes for other nodes to use".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_advertiseroutes" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasAdvertiseRoutes = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_advertiseroutes_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_advertiseroutes_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_advertiseroutes
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasAdvertiseRoutes is whether the binary was built with support for modular feature "Advertise routes for other nodes to use".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_advertiseroutes" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasAdvertiseRoutes = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_appconnectors_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_appconnectors_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_appconnectors
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasAppConnectors is whether the binary was built with support for modular feature "App Connectors support".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_appconnectors" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasAppConnectors = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_appconnectors_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_appconnectors_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_appconnectors
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasAppConnectors is whether the binary was built with support for modular feature "App Connectors support".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_appconnectors" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasAppConnectors = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_aws_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_aws_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_aws
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasAWS is whether the binary was built with support for modular feature "AWS integration".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_aws" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasAWS = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_aws_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_aws_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_aws
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasAWS is whether the binary was built with support for modular feature "AWS integration".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_aws" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasAWS = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_bakedroots_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_bakedroots_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_bakedroots
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasBakedRoots is whether the binary was built with support for modular feature "Embed CA (LetsEncrypt) x509 roots to use as fallback".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_bakedroots" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasBakedRoots = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_bakedroots_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_bakedroots_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_bakedroots
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasBakedRoots is whether the binary was built with support for modular feature "Embed CA (LetsEncrypt) x509 roots to use as fallback".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_bakedroots" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasBakedRoots = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_bird_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_bird_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_bird
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasBird is whether the binary was built with support for modular feature "Bird BGP integration".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_bird" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasBird = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_bird_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_bird_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_bird
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasBird is whether the binary was built with support for modular feature "Bird BGP integration".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_bird" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasBird = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_c2n_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_c2n_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_c2n
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasC2N is whether the binary was built with support for modular feature "Control-to-node (C2N) support".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_c2n" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasC2N = false
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_c2n_enabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_c2n_enabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_c2n
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasC2N is whether the binary was built with support for modular feature "Control-to-node (C2N) support".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_c2n" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasC2N = true
|
||||
13
vendor/tailscale.com/feature/buildfeatures/feature_cachenetmap_disabled.go
generated
vendored
Normal file
13
vendor/tailscale.com/feature/buildfeatures/feature_cachenetmap_disabled.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_cachenetmap
|
||||
|
||||
package buildfeatures
|
||||
|
||||
// HasCacheNetMap is whether the binary was built with support for modular feature "Cache the netmap on disk between runs".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_cachenetmap" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasCacheNetMap = false
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user