mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-29 18:33:05 +00:00
Compare commits
17 commits
ddadb3c0d3
...
4ef8355823
Author | SHA1 | Date | |
---|---|---|---|
|
4ef8355823 | ||
|
edf9e25001 | ||
|
c6336adb01 | ||
|
aa85454185 | ||
|
59f47913fe | ||
|
f5feff7c22 | ||
|
5fbf3f8327 | ||
|
6275399327 | ||
|
29119bb7f4 | ||
|
93ba21ede5 | ||
|
a7874af3d0 | ||
|
e7245856c5 | ||
|
2345c38e1e | ||
|
8cfaa6bdac | ||
|
4e44d57bf7 | ||
|
0089ceaf1d | ||
|
9a46c5763c |
54 changed files with 1703 additions and 637 deletions
1
.github/workflows/test-integration.yaml
vendored
1
.github/workflows/test-integration.yaml
vendored
|
@ -38,6 +38,7 @@ jobs:
|
||||||
- TestNodeMoveCommand
|
- TestNodeMoveCommand
|
||||||
- TestPolicyCommand
|
- TestPolicyCommand
|
||||||
- TestPolicyBrokenConfigCommand
|
- TestPolicyBrokenConfigCommand
|
||||||
|
- TestDERPVerifyEndpoint
|
||||||
- TestResolveMagicDNS
|
- TestResolveMagicDNS
|
||||||
- TestValidateResolvConf
|
- TestValidateResolvConf
|
||||||
- TestDERPServerScenario
|
- TestDERPServerScenario
|
||||||
|
|
|
@ -27,6 +27,7 @@ linters:
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- musttag # causes issues with imported libs
|
- musttag # causes issues with imported libs
|
||||||
- depguard
|
- depguard
|
||||||
|
- exportloopref
|
||||||
|
|
||||||
# We should strive to enable these:
|
# We should strive to enable these:
|
||||||
- wrapcheck
|
- wrapcheck
|
||||||
|
@ -56,9 +57,14 @@ linters-settings:
|
||||||
- ok
|
- ok
|
||||||
- c
|
- c
|
||||||
- tt
|
- tt
|
||||||
|
- tx
|
||||||
|
- rx
|
||||||
|
|
||||||
gocritic:
|
gocritic:
|
||||||
disabled-checks:
|
disabled-checks:
|
||||||
- appendAssign
|
- appendAssign
|
||||||
# TODO(kradalby): Remove this
|
# TODO(kradalby): Remove this
|
||||||
- ifElseChain
|
- ifElseChain
|
||||||
|
|
||||||
|
nlreturn:
|
||||||
|
block-size: 4
|
||||||
|
|
|
@ -154,7 +154,7 @@ kos:
|
||||||
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
|
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
|
||||||
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}-debug{{ end }}"
|
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}-debug{{ end }}"
|
||||||
- "{{ if not .Prerelease }}v{{ .Major }}-debug{{ end }}"
|
- "{{ if not .Prerelease }}v{{ .Major }}-debug{{ end }}"
|
||||||
- "{{ if not .Prerelease }}stable{{ else }}unstable-debug{{ end }}"
|
- "{{ if not .Prerelease }}stable-debug{{ else }}unstable-debug{{ end }}"
|
||||||
- "{{ .Tag }}-debug"
|
- "{{ .Tag }}-debug"
|
||||||
- '{{ trimprefix .Tag "v" }}-debug'
|
- '{{ trimprefix .Tag "v" }}-debug'
|
||||||
- "sha-{{ .ShortCommit }}-debug"
|
- "sha-{{ .ShortCommit }}-debug"
|
||||||
|
@ -177,7 +177,7 @@ kos:
|
||||||
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
|
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
|
||||||
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}-debug{{ end }}"
|
- "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}-debug{{ end }}"
|
||||||
- "{{ if not .Prerelease }}v{{ .Major }}-debug{{ end }}"
|
- "{{ if not .Prerelease }}v{{ .Major }}-debug{{ end }}"
|
||||||
- "{{ if not .Prerelease }}stable{{ else }}unstable-debug{{ end }}"
|
- "{{ if not .Prerelease }}stable-debug{{ else }}unstable-debug{{ end }}"
|
||||||
- "{{ .Tag }}-debug"
|
- "{{ .Tag }}-debug"
|
||||||
- '{{ trimprefix .Tag "v" }}-debug'
|
- '{{ trimprefix .Tag "v" }}-debug'
|
||||||
- "sha-{{ .ShortCommit }}-debug"
|
- "sha-{{ .ShortCommit }}-debug"
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
.github/workflows/test-integration-v2*
|
.github/workflows/test-integration-v2*
|
||||||
docs/about/features.md
|
docs/about/features.md
|
||||||
|
docs/ref/remote-cli.md
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
- Fixed processing of fields in post request in MoveNode rpc [#2179](https://github.com/juanfont/headscale/pull/2179)
|
- Fixed processing of fields in post request in MoveNode rpc [#2179](https://github.com/juanfont/headscale/pull/2179)
|
||||||
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied [#2198](https://github.com/juanfont/headscale/pull/2198)
|
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied [#2198](https://github.com/juanfont/headscale/pull/2198)
|
||||||
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)
|
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)
|
||||||
|
- Fixed missing `stable-debug` container tag [#2232](https://github.com/juanfont/headscale/pr/2232)
|
||||||
|
- Loosened up `server_url` and `base_domain` check. It was overly strict in some cases.
|
||||||
|
|
||||||
## 0.23.0 (2024-09-18)
|
## 0.23.0 (2024-09-18)
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ event.
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||||
may be reported to the community leaders responsible for enforcement
|
may be reported to the community leaders responsible for enforcement
|
||||||
at our Discord channel. All complaints
|
on our [Discord server](https://discord.gg/c84AZQhmpx). All complaints
|
||||||
will be reviewed and investigated promptly and fairly.
|
will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and
|
All community leaders are obligated to respect the privacy and
|
||||||
|
|
19
Dockerfile.derper
Normal file
19
Dockerfile.derper
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# For testing purposes only
|
||||||
|
|
||||||
|
FROM golang:alpine AS build-env
|
||||||
|
|
||||||
|
WORKDIR /go/src
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
ARG VERSION_BRANCH=main
|
||||||
|
RUN git clone https://github.com/tailscale/tailscale.git --branch=$VERSION_BRANCH --depth=1
|
||||||
|
WORKDIR /go/src/tailscale
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
RUN GOARCH=$TARGETARCH go install -v ./cmd/derper
|
||||||
|
|
||||||
|
FROM alpine:3.18
|
||||||
|
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
|
||||||
|
|
||||||
|
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||||
|
ENTRYPOINT [ "/usr/local/bin/derper" ]
|
|
@ -28,7 +28,9 @@ ARG VERSION_GIT_HASH=""
|
||||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
RUN GOARCH=$TARGETARCH go install -ldflags="\
|
ARG BUILD_TAGS=""
|
||||||
|
|
||||||
|
RUN GOARCH=$TARGETARCH go install -tags="${BUILD_TAGS}" -ldflags="\
|
||||||
-X tailscale.com/version.longStamp=$VERSION_LONG \
|
-X tailscale.com/version.longStamp=$VERSION_LONG \
|
||||||
-X tailscale.com/version.shortStamp=$VERSION_SHORT \
|
-X tailscale.com/version.shortStamp=$VERSION_SHORT \
|
||||||
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
|
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
An open source, self-hosted implementation of the Tailscale control server.
|
An open source, self-hosted implementation of the Tailscale control server.
|
||||||
|
|
||||||
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat.
|
Join our [Discord server](https://discord.gg/c84AZQhmpx) for a chat.
|
||||||
|
|
||||||
**Note:** Always select the same GitHub tag as the released version you use
|
**Note:** Always select the same GitHub tag as the released version you use
|
||||||
to ensure you have the correct example configuration and documentation.
|
to ensure you have the correct example configuration and documentation.
|
||||||
|
|
|
@ -13,3 +13,4 @@ headscale.
|
||||||
| Android | Yes (see [docs](../usage/connect/android.md)) |
|
| Android | Yes (see [docs](../usage/connect/android.md)) |
|
||||||
| macOS | Yes (see [docs](../usage/connect/apple.md#macos) and `/apple` on your headscale for more information) |
|
| macOS | Yes (see [docs](../usage/connect/apple.md#macos) and `/apple` on your headscale for more information) |
|
||||||
| iOS | Yes (see [docs](../usage/connect/apple.md#ios) and `/apple` on your headscale for more information) |
|
| iOS | Yes (see [docs](../usage/connect/apple.md#ios) and `/apple` on your headscale for more information) |
|
||||||
|
| tvOS | Yes (see [docs](../usage/connect/apple.md#tvos) and `/apple` on your headscale for more information) |
|
||||||
|
|
|
@ -41,13 +41,15 @@ In addition to that, you may use packages provided by the community or from dist
|
||||||
[installation guide using community packages](../setup/install/community.md).
|
[installation guide using community packages](../setup/install/community.md).
|
||||||
|
|
||||||
For convenience, we also [build Docker images with headscale](../setup/install/container.md). But **please be aware that
|
For convenience, we also [build Docker images with headscale](../setup/install/container.md). But **please be aware that
|
||||||
we don't officially support deploying headscale using Docker**. We have a [Discord
|
we don't officially support deploying headscale using Docker**. On our [Discord server](https://discord.gg/c84AZQhmpx)
|
||||||
channel](https://discord.com/channels/896711691637780480/1070619770942148618) where you can ask for Docker-specific help
|
we have a "docker-issues" channel where you can ask for Docker-specific help to the community.
|
||||||
to the community.
|
|
||||||
|
|
||||||
## Why is my reverse proxy not working with headscale?
|
## Why is my reverse proxy not working with headscale?
|
||||||
|
|
||||||
We don't know. We don't use reverse proxies with headscale ourselves, so we don't have any experience with them. We have [community documentation](../ref/integration/reverse-proxy.md) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community.
|
We don't know. We don't use reverse proxies with headscale ourselves, so we don't have any experience with them. We have
|
||||||
|
[community documentation](../ref/integration/reverse-proxy.md) on how to configure various reverse proxies, and a
|
||||||
|
dedicated "reverse-proxy-issues" channel on our [Discord server](https://discord.gg/c84AZQhmpx) where you can ask for
|
||||||
|
help to the community.
|
||||||
|
|
||||||
## Can I use headscale and tailscale on the same machine?
|
## Can I use headscale and tailscale on the same machine?
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
# Getting help
|
# Getting help
|
||||||
|
|
||||||
Join our Discord server for announcements and community support:
|
Join our [Discord server](https://discord.gg/c84AZQhmpx) for announcements and community support.
|
||||||
|
|
||||||
- [announcements](https://discord.com/channels/896711691637780480/896711692120129538)
|
|
||||||
- [general](https://discord.com/channels/896711691637780480/896711692120129540)
|
|
||||||
- [docker-issues](https://discord.com/channels/896711691637780480/1070619770942148618)
|
|
||||||
- [reverse-proxy-issues](https://discord.com/channels/896711691637780480/1070619818346164324)
|
|
||||||
- [web-interfaces](https://discord.com/channels/896711691637780480/1105842846386356294)
|
|
||||||
|
|
||||||
Please report bugs via [GitHub issues](https://github.com/juanfont/headscale/issues)
|
Please report bugs via [GitHub issues](https://github.com/juanfont/headscale/issues)
|
||||||
|
|
|
@ -6,5 +6,4 @@ code archives. Container images are available on [Docker Hub](https://hub.docker
|
||||||
|
|
||||||
An Atom/RSS feed of headscale releases is available [here](https://github.com/juanfont/headscale/releases.atom).
|
An Atom/RSS feed of headscale releases is available [here](https://github.com/juanfont/headscale/releases.atom).
|
||||||
|
|
||||||
Join the ["announcements" channel on Discord](https://discord.com/channels/896711691637780480/896711692120129538) for
|
See the "announcements" channel on our [Discord server](https://discord.gg/c84AZQhmpx) for news about headscale.
|
||||||
news about headscale.
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ Headscale is an open source, self-hosted implementation of the Tailscale control
|
||||||
|
|
||||||
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](./about/faq.md).
|
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](./about/faq.md).
|
||||||
|
|
||||||
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support.
|
Join our [Discord server](https://discord.gg/c84AZQhmpx) for a chat and community support.
|
||||||
|
|
||||||
## Design goal
|
## Design goal
|
||||||
|
|
||||||
|
|
|
@ -45,11 +45,11 @@ headscale server.
|
||||||
|
|
||||||
ACLs have to be written in [huJSON](https://github.com/tailscale/hujson).
|
ACLs have to be written in [huJSON](https://github.com/tailscale/hujson).
|
||||||
|
|
||||||
When registering the servers we will need to add the flag
|
When [registering the servers](../usage/getting-started.md#register-a-node) we
|
||||||
`--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user that is
|
will need to add the flag `--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user
|
||||||
registering the server should be allowed to do it. Since anyone can add tags to
|
that is registering the server should be allowed to do it. Since anyone can add
|
||||||
a server they can register, the check of the tags is done on headscale server
|
tags to a server they can register, the check of the tags is done on headscale
|
||||||
and only valid tags are applied. A tag is valid if the user that is
|
server and only valid tags are applied. A tag is valid if the user that is
|
||||||
registering it is allowed to do it.
|
registering it is allowed to do it.
|
||||||
|
|
||||||
To use ACLs in headscale, you must edit your `config.yaml` file. In there you will find a `policy.path` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/).
|
To use ACLs in headscale, you must edit your `config.yaml` file. In there you will find a `policy.path` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/).
|
||||||
|
|
12
docs/ref/integration/tools.md
Normal file
12
docs/ref/integration/tools.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Tools related to headscale
|
||||||
|
|
||||||
|
!!! warning "Community contributions"
|
||||||
|
|
||||||
|
This page contains community contributions. The projects listed here are not
|
||||||
|
maintained by the headscale authors and are written by community members.
|
||||||
|
|
||||||
|
This page collects third-party tools and scripts related to headscale.
|
||||||
|
|
||||||
|
| Name | Repository Link | Description |
|
||||||
|
| ----------------- | --------------------------------------------------------------- | ------------------------------------------------- |
|
||||||
|
| tailscale-manager | [Github](https://github.com/singlestore-labs/tailscale-manager) | Dynamically manage Tailscale route advertisements |
|
|
@ -1,17 +1,19 @@
|
||||||
# Headscale web interface
|
# Web interfaces for headscale
|
||||||
|
|
||||||
!!! warning "Community contributions"
|
!!! warning "Community contributions"
|
||||||
|
|
||||||
This page contains community contributions. The projects listed here are not
|
This page contains community contributions. The projects listed here are not
|
||||||
maintained by the headscale authors and are written by community members.
|
maintained by the headscale authors and are written by community members.
|
||||||
|
|
||||||
| Name | Repository Link | Description | Status |
|
Headscale doesn't provide a built-in web interface but users may pick one from the available options.
|
||||||
| --------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------ |
|
|
||||||
| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple headscale web UI for small-scale deployments. | Alpha |
|
|
||||||
| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server | Alpha |
|
|
||||||
| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required | Alpha |
|
|
||||||
| Headplane | [GitHub](https://github.com/tale/headplane) | An advanced Tailscale inspired frontend for headscale | Alpha |
|
|
||||||
| headscale-admin | [Github](https://github.com/GoodiesHQ/headscale-admin) | Headscale-Admin is meant to be a simple, modern web interface for headscale | Beta |
|
|
||||||
| ouroboros | [Github](https://github.com/yellowsink/ouroboros) | Ouroboros is designed for users to manage their own devices, rather than for admins | Stable |
|
|
||||||
|
|
||||||
You can ask for support on our dedicated [Discord channel](https://discord.com/channels/896711691637780480/1105842846386356294).
|
| Name | Repository Link | Description |
|
||||||
|
| --------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
|
| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple headscale web UI for small-scale deployments. |
|
||||||
|
| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server |
|
||||||
|
| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required |
|
||||||
|
| Headplane | [GitHub](https://github.com/tale/headplane) | An advanced Tailscale inspired frontend for headscale |
|
||||||
|
| headscale-admin | [Github](https://github.com/GoodiesHQ/headscale-admin) | Headscale-Admin is meant to be a simple, modern web interface for headscale |
|
||||||
|
| ouroboros | [Github](https://github.com/yellowsink/ouroboros) | Ouroboros is designed for users to manage their own devices, rather than for admins |
|
||||||
|
|
||||||
|
You can ask for support on our [Discord server](https://discord.gg/c84AZQhmpx) in the "web-interfaces" channel.
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
# Controlling headscale with remote CLI
|
# Controlling headscale with remote CLI
|
||||||
|
|
||||||
This documentation has the goal of showing a user how-to set control a headscale instance
|
This documentation has the goal of showing a user how-to control a headscale instance
|
||||||
from a remote machine with the `headscale` command line binary.
|
from a remote machine with the `headscale` command line binary.
|
||||||
|
|
||||||
## Prerequisite
|
## Prerequisite
|
||||||
|
|
||||||
- A workstation to run headscale (could be Linux, macOS, other supported platforms)
|
- A workstation to run `headscale` (any supported platform, e.g. Linux).
|
||||||
- A headscale server (version `0.13.0` or newer)
|
- A headscale server with gRPC enabled.
|
||||||
- Access to create API keys (local access to the headscale server)
|
- Connections to the gRPC port (default: `50443`) are allowed.
|
||||||
- headscale _must_ be served over TLS/HTTPS
|
- Remote access requires an encrypted connection via TLS.
|
||||||
- Remote access does _not_ support unencrypted traffic.
|
- An API key to authenticate with the headscale server.
|
||||||
- Port `50443` must be open in the firewall (or port overridden by `grpc_listen_addr` option)
|
|
||||||
|
|
||||||
## Create an API key
|
## Create an API key
|
||||||
|
|
||||||
We need to create an API key to authenticate our remote headscale when using it from our workstation.
|
We need to create an API key to authenticate with the remote headscale server when using it from our workstation.
|
||||||
|
|
||||||
To create a API key, log into your headscale server and generate a key:
|
To create an API key, log into your headscale server and generate a key:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale apikeys create --expiration 90d
|
headscale apikeys create --expiration 90d
|
||||||
|
@ -25,7 +24,7 @@ headscale apikeys create --expiration 90d
|
||||||
Copy the output of the command and save it for later. Please note that you can not retrieve a key again,
|
Copy the output of the command and save it for later. Please note that you can not retrieve a key again,
|
||||||
if the key is lost, expire the old one, and create a new key.
|
if the key is lost, expire the old one, and create a new key.
|
||||||
|
|
||||||
To list the keys currently assosicated with the server:
|
To list the keys currently associated with the server:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
headscale apikeys list
|
headscale apikeys list
|
||||||
|
@ -39,7 +38,8 @@ headscale apikeys expire --prefix "<PREFIX>"
|
||||||
|
|
||||||
## Download and configure headscale
|
## Download and configure headscale
|
||||||
|
|
||||||
1. Download the latest [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases):
|
1. Download the [`headscale` binary from GitHub's release page](https://github.com/juanfont/headscale/releases). Make
|
||||||
|
sure to use the same version as on the server.
|
||||||
|
|
||||||
1. Put the binary somewhere in your `PATH`, e.g. `/usr/local/bin/headscale`
|
1. Put the binary somewhere in your `PATH`, e.g. `/usr/local/bin/headscale`
|
||||||
|
|
||||||
|
@ -49,25 +49,32 @@ headscale apikeys expire --prefix "<PREFIX>"
|
||||||
chmod +x /usr/local/bin/headscale
|
chmod +x /usr/local/bin/headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Configure the CLI through environment variables
|
1. Provide the connection parameters for the remote headscale server either via a minimal YAML configuration file or via
|
||||||
|
environment variables:
|
||||||
|
|
||||||
```shell
|
=== "Minimal YAML configuration file"
|
||||||
export HEADSCALE_CLI_ADDRESS="<HEADSCALE ADDRESS>:<PORT>"
|
|
||||||
export HEADSCALE_CLI_API_KEY="<API KEY FROM PREVIOUS STAGE>"
|
```yaml
|
||||||
|
cli:
|
||||||
|
address: <HEADSCALE_ADDRESS>:<PORT>
|
||||||
|
api_key: <API_KEY_FROM_PREVIOUS_STEP>
|
||||||
```
|
```
|
||||||
|
|
||||||
for example:
|
=== "Environment variables"
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443"
|
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_ADDRESS>:<PORT>"
|
||||||
export HEADSCALE_CLI_API_KEY="abcde12345"
|
export HEADSCALE_CLI_API_KEY="<API_KEY_FROM_PREVIOUS_STEP>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This will tell the `headscale` binary to connect to a remote instance, instead of looking
|
!!! bug
|
||||||
for a local instance (which is what it does on the server).
|
|
||||||
|
|
||||||
The API key is needed to make sure that you are allowed to access the server. The key is _not_
|
Headscale 0.23.0 requires at least an empty configuration file when environment variables are used to
|
||||||
needed when running directly on the server, as the connection is local.
|
specify connection details. See [issue 2193](https://github.com/juanfont/headscale/issues/2193) for more
|
||||||
|
information.
|
||||||
|
|
||||||
|
This instructs the `headscale` binary to connect to a remote instance at `<HEADSCALE_ADDRESS>:<PORT>`, instead of
|
||||||
|
connecting to the local instance.
|
||||||
|
|
||||||
1. Test the connection
|
1. Test the connection
|
||||||
|
|
||||||
|
@ -89,10 +96,10 @@ While this is _not a supported_ feature, an example on how this can be set up on
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
Checklist:
|
- Make sure you have the _same_ headscale version on your server and workstation.
|
||||||
|
- Ensure that connections to the gRPC port are allowed.
|
||||||
- Make sure you have the _same_ headscale version on your server and workstation
|
- Verify that your TLS certificate is valid and trusted.
|
||||||
- Make sure you use version `0.13.0` or newer.
|
- If you don't have access to a trusted certificate (e.g. from Let's Encrypt), either:
|
||||||
- Verify that your TLS certificate is valid and trusted
|
- Add your self-signed certificate to the trust store of your OS _or_
|
||||||
- If you do not have access to a trusted certificate (e.g. from Let's Encrypt), add your self signed certificate to the trust store of your OS or
|
- Disable certificate verification by either setting `cli.insecure: true` in the configuration file or by setting
|
||||||
- Set `HEADSCALE_CLI_INSECURE` to 0 in your environment
|
`HEADSCALE_CLI_INSECURE=1` via an environment variable. We do **not** recommend to disable certificate validation.
|
||||||
|
|
|
@ -9,6 +9,8 @@ tls_cert_path: ""
|
||||||
tls_key_path: ""
|
tls_key_path: ""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The certificate should contain the full chain, else some clients, like the Tailscale Android client, will reject it.
|
||||||
|
|
||||||
## Let's Encrypt / ACME
|
## Let's Encrypt / ACME
|
||||||
|
|
||||||
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
|
||||||
|
|
|
@ -28,7 +28,7 @@ development version.
|
||||||
|
|
||||||
## Fedora, RHEL, CentOS
|
## Fedora, RHEL, CentOS
|
||||||
|
|
||||||
A 3rd-party repository for various RPM based distributions is available at:
|
A third-party repository for various RPM based distributions is available at:
|
||||||
<https://copr.fedorainfracloud.org/coprs/jonathanspw/headscale/>. The site provides detailed setup and installation
|
<https://copr.fedorainfracloud.org/coprs/jonathanspw/headscale/>. The site provides detailed setup and installation
|
||||||
instructions.
|
instructions.
|
||||||
|
|
||||||
|
|
|
@ -47,3 +47,23 @@ tailscale login --login-server <YOUR_HEADSCALE_URL>
|
||||||
- Under `Custom Login Server`, select `Add Account...`
|
- Under `Custom Login Server`, select `Add Account...`
|
||||||
- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) and press `Add Account`
|
- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) and press `Add Account`
|
||||||
- Follow the login procedure in the browser
|
- Follow the login procedure in the browser
|
||||||
|
|
||||||
|
## tvOS
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the official Tailscale tvOS client from the [App Store](https://apps.apple.com/app/tailscale/id1470499037).
|
||||||
|
|
||||||
|
!!! danger
|
||||||
|
|
||||||
|
**Don't** open the Tailscale App after installation!
|
||||||
|
|
||||||
|
### Configuring the headscale URL
|
||||||
|
|
||||||
|
- Open Settings (the Apple tvOS settings) > Apps > Tailscale
|
||||||
|
- Under `ALTERNATE COORDINATION SERVER URL`, select `URL`
|
||||||
|
- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) and press `OK`
|
||||||
|
- Return to the tvOS Home screen
|
||||||
|
- Open Tailscale
|
||||||
|
- Click the button `Install VPN configuration` and confirm the appearing popup by clicking the `Allow` button
|
||||||
|
- Scan the QR code and follow the login procedure
|
||||||
|
|
|
@ -9,6 +9,8 @@ This page helps you get started with headscale and provides a few usage examples
|
||||||
installation instructions.
|
installation instructions.
|
||||||
* The configuration file exists and is adjusted to suit your environment, see
|
* The configuration file exists and is adjusted to suit your environment, see
|
||||||
[Configuration](../ref/configuration.md) for details.
|
[Configuration](../ref/configuration.md) for details.
|
||||||
|
* Headscale is reachable from the Internet. Verify this by opening client specific setup instructions in your
|
||||||
|
browser, e.g. https://headscale.example.com/windows
|
||||||
* The Tailscale client is installed, see [Client and operating system support](../about/clients.md) for more
|
* The Tailscale client is installed, see [Client and operating system support](../about/clients.md) for more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
|
|
12
flake.lock
12
flake.lock
|
@ -5,11 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1726560853,
|
"lastModified": 1731533236,
|
||||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -20,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1729850857,
|
"lastModified": 1731763621,
|
||||||
"narHash": "sha256-WvLXzNNnnw+qpFOmgaM3JUlNEH+T4s22b5i2oyyCpXE=",
|
"narHash": "sha256-ddcX4lQL0X05AYkrkV2LMFgGdRvgap7Ho8kgon3iWZk=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "41dea55321e5a999b17033296ac05fe8a8b5a257",
|
"rev": "c69a9bffbecde46b4b939465422ddc59493d3e4d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
|
|
||||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
||||||
# update this if you have a mismatch after doing a change to thos files.
|
# update this if you have a mismatch after doing a change to thos files.
|
||||||
vendorHash = "sha256-CMkYTRjmhvTTrB7JbLj0cj9VEyzpG0iUWXkaOagwYTk=";
|
vendorHash = "sha256-Qoqu2k4vvnbRFLmT/v8lI+HCEWqJsHFs8uZRfNmwQpo=";
|
||||||
|
|
||||||
subPackages = ["cmd/headscale"];
|
subPackages = ["cmd/headscale"];
|
||||||
|
|
||||||
|
|
|
@ -457,9 +457,12 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
|
||||||
router.HandleFunc("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1).
|
router.HandleFunc("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1).
|
||||||
Methods(http.MethodGet)
|
Methods(http.MethodGet)
|
||||||
|
|
||||||
|
router.HandleFunc("/verify", h.VerifyHandler).Methods(http.MethodPost)
|
||||||
|
|
||||||
if h.cfg.DERP.ServerEnabled {
|
if h.cfg.DERP.ServerEnabled {
|
||||||
router.HandleFunc("/derp", h.DERPServer.DERPHandler)
|
router.HandleFunc("/derp", h.DERPServer.DERPHandler)
|
||||||
router.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
|
router.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
|
||||||
|
router.HandleFunc("/derp/latency-check", derpServer.DERPProbeHandler)
|
||||||
router.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.DERPMap))
|
router.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.DERPMap))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"zgo.at/zcache/v2"
|
"zgo.at/zcache/v2"
|
||||||
)
|
)
|
||||||
|
@ -44,7 +45,7 @@ func TestMigrations(t *testing.T) {
|
||||||
routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) {
|
routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) {
|
||||||
return GetRoutes(rx)
|
return GetRoutes(rx)
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, routes, 10)
|
assert.Len(t, routes, 10)
|
||||||
want := types.Routes{
|
want := types.Routes{
|
||||||
|
@ -70,7 +71,7 @@ func TestMigrations(t *testing.T) {
|
||||||
routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) {
|
routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) {
|
||||||
return GetRoutes(rx)
|
return GetRoutes(rx)
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, routes, 4)
|
assert.Len(t, routes, 4)
|
||||||
want := types.Routes{
|
want := types.Routes{
|
||||||
|
@ -132,7 +133,7 @@ func TestMigrations(t *testing.T) {
|
||||||
|
|
||||||
return append(kratest, testkra...), nil
|
return append(kratest, testkra...), nil
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, keys, 5)
|
assert.Len(t, keys, 5)
|
||||||
want := []types.PreAuthKey{
|
want := []types.PreAuthKey{
|
||||||
|
@ -177,7 +178,7 @@ func TestMigrations(t *testing.T) {
|
||||||
nodes, err := Read(h.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
nodes, err := Read(h.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
||||||
return ListNodes(rx)
|
return ListNodes(rx)
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
assert.Falsef(t, node.MachineKey.IsZero(), "expected non zero machinekey")
|
assert.Falsef(t, node.MachineKey.IsZero(), "expected non zero machinekey")
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
)
|
)
|
||||||
|
@ -457,7 +458,12 @@ func TestBackfillIPAddresses(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
db := tt.dbFunc()
|
db := tt.dbFunc()
|
||||||
|
|
||||||
alloc, err := NewIPAllocator(db, tt.prefix4, tt.prefix6, types.IPAllocationStrategySequential)
|
alloc, err := NewIPAllocator(
|
||||||
|
db,
|
||||||
|
tt.prefix4,
|
||||||
|
tt.prefix6,
|
||||||
|
types.IPAllocationStrategySequential,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to set up ip alloc: %s", err)
|
t.Fatalf("failed to set up ip alloc: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -482,24 +488,29 @@ func TestBackfillIPAddresses(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIPAllocatorNextNoReservedIPs(t *testing.T) {
|
func TestIPAllocatorNextNoReservedIPs(t *testing.T) {
|
||||||
alloc, err := NewIPAllocator(db, ptr.To(tsaddr.CGNATRange()), ptr.To(tsaddr.TailscaleULARange()), types.IPAllocationStrategySequential)
|
alloc, err := NewIPAllocator(
|
||||||
|
db,
|
||||||
|
ptr.To(tsaddr.CGNATRange()),
|
||||||
|
ptr.To(tsaddr.TailscaleULARange()),
|
||||||
|
types.IPAllocationStrategySequential,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to set up ip alloc: %s", err)
|
t.Fatalf("failed to set up ip alloc: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that we do not give out 100.100.100.100
|
// Validate that we do not give out 100.100.100.100
|
||||||
nextQuad100, err := alloc.next(na("100.100.100.99"), ptr.To(tsaddr.CGNATRange()))
|
nextQuad100, err := alloc.next(na("100.100.100.99"), ptr.To(tsaddr.CGNATRange()))
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, na("100.100.100.101"), *nextQuad100)
|
assert.Equal(t, na("100.100.100.101"), *nextQuad100)
|
||||||
|
|
||||||
// Validate that we do not give out fd7a:115c:a1e0::53
|
// Validate that we do not give out fd7a:115c:a1e0::53
|
||||||
nextQuad100v6, err := alloc.next(na("fd7a:115c:a1e0::52"), ptr.To(tsaddr.TailscaleULARange()))
|
nextQuad100v6, err := alloc.next(na("fd7a:115c:a1e0::52"), ptr.To(tsaddr.TailscaleULARange()))
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, na("fd7a:115c:a1e0::54"), *nextQuad100v6)
|
assert.Equal(t, na("fd7a:115c:a1e0::54"), *nextQuad100v6)
|
||||||
|
|
||||||
// Validate that we do not give out fd7a:115c:a1e0::53
|
// Validate that we do not give out fd7a:115c:a1e0::53
|
||||||
nextChrome, err := alloc.next(na("100.115.91.255"), ptr.To(tsaddr.CGNATRange()))
|
nextChrome, err := alloc.next(na("100.115.91.255"), ptr.To(tsaddr.CGNATRange()))
|
||||||
t.Logf("chrome: %s", nextChrome.String())
|
t.Logf("chrome: %s", nextChrome.String())
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, na("100.115.94.0"), *nextChrome)
|
assert.Equal(t, na("100.115.94.0"), *nextChrome)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
|
@ -558,17 +559,17 @@ func TestAutoApproveRoutes(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
adb, err := newTestDB()
|
adb, err := newTestDB()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pol, err := policy.LoadACLPolicyFromBytes([]byte(tt.acl))
|
pol, err := policy.LoadACLPolicyFromBytes([]byte(tt.acl))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, pol)
|
assert.NotNil(t, pol)
|
||||||
|
|
||||||
user, err := adb.CreateUser("test")
|
user, err := adb.CreateUser("test")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pak, err := adb.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := adb.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodeKey := key.NewNode()
|
nodeKey := key.NewNode()
|
||||||
machineKey := key.NewMachine()
|
machineKey := key.NewMachine()
|
||||||
|
@ -590,21 +591,21 @@ func TestAutoApproveRoutes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
trx := adb.DB.Save(&node)
|
trx := adb.DB.Save(&node)
|
||||||
assert.NoError(t, trx.Error)
|
require.NoError(t, trx.Error)
|
||||||
|
|
||||||
sendUpdate, err := adb.SaveNodeRoutes(&node)
|
sendUpdate, err := adb.SaveNodeRoutes(&node)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, sendUpdate)
|
assert.False(t, sendUpdate)
|
||||||
|
|
||||||
node0ByID, err := adb.GetNodeByID(0)
|
node0ByID, err := adb.GetNodeByID(0)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// TODO(kradalby): Check state update
|
// TODO(kradalby): Check state update
|
||||||
err = adb.EnableAutoApprovedRoutes(pol, node0ByID)
|
err = adb.EnableAutoApprovedRoutes(pol, node0ByID)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
enabledRoutes, err := adb.GetEnabledRoutes(node0ByID)
|
enabledRoutes, err := adb.GetEnabledRoutes(node0ByID)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, enabledRoutes, len(tt.want))
|
assert.Len(t, enabledRoutes, len(tt.want))
|
||||||
|
|
||||||
tsaddr.SortPrefixes(enabledRoutes)
|
tsaddr.SortPrefixes(enabledRoutes)
|
||||||
|
@ -697,13 +698,13 @@ func TestListEphemeralNodes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pakEph, err := db.CreatePreAuthKey(user.Name, false, true, nil, nil)
|
pakEph, err := db.CreatePreAuthKey(user.Name, false, true, nil, nil)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
|
@ -726,16 +727,16 @@ func TestListEphemeralNodes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.DB.Save(&node).Error
|
err = db.DB.Save(&node).Error
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = db.DB.Save(&nodeEph).Error
|
err = db.DB.Save(&nodeEph).Error
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err := db.ListNodes()
|
nodes, err := db.ListNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ephemeralNodes, err := db.ListEphemeralNodes()
|
ephemeralNodes, err := db.ListEphemeralNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, nodes, 2)
|
assert.Len(t, nodes, 2)
|
||||||
assert.Len(t, ephemeralNodes, 1)
|
assert.Len(t, ephemeralNodes, 1)
|
||||||
|
@ -753,10 +754,10 @@ func TestRenameNode(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := db.CreateUser("test")
|
user, err := db.CreateUser("test")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2, err := db.CreateUser("test2")
|
user2, err := db.CreateUser("test2")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
|
@ -777,10 +778,10 @@ func TestRenameNode(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.DB.Save(&node).Error
|
err = db.DB.Save(&node).Error
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = db.DB.Save(&node2).Error
|
err = db.DB.Save(&node2).Error
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = db.DB.Transaction(func(tx *gorm.DB) error {
|
err = db.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
_, err := RegisterNode(tx, node, nil, nil)
|
_, err := RegisterNode(tx, node, nil, nil)
|
||||||
|
@ -790,10 +791,10 @@ func TestRenameNode(t *testing.T) {
|
||||||
_, err = RegisterNode(tx, node2, nil, nil)
|
_, err = RegisterNode(tx, node2, nil, nil)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err := db.ListNodes()
|
nodes, err := db.ListNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, nodes, 2)
|
assert.Len(t, nodes, 2)
|
||||||
|
|
||||||
|
@ -815,26 +816,26 @@ func TestRenameNode(t *testing.T) {
|
||||||
err = db.Write(func(tx *gorm.DB) error {
|
err = db.Write(func(tx *gorm.DB) error {
|
||||||
return RenameNode(tx, nodes[0].ID, "newname")
|
return RenameNode(tx, nodes[0].ID, "newname")
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err = db.ListNodes()
|
nodes, err = db.ListNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, nodes, 2)
|
assert.Len(t, nodes, 2)
|
||||||
assert.Equal(t, nodes[0].Hostname, "test")
|
assert.Equal(t, "test", nodes[0].Hostname)
|
||||||
assert.Equal(t, nodes[0].GivenName, "newname")
|
assert.Equal(t, "newname", nodes[0].GivenName)
|
||||||
|
|
||||||
// Nodes can reuse name that is no longer used
|
// Nodes can reuse name that is no longer used
|
||||||
err = db.Write(func(tx *gorm.DB) error {
|
err = db.Write(func(tx *gorm.DB) error {
|
||||||
return RenameNode(tx, nodes[1].ID, "test")
|
return RenameNode(tx, nodes[1].ID, "test")
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err = db.ListNodes()
|
nodes, err = db.ListNodes()
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, nodes, 2)
|
assert.Len(t, nodes, 2)
|
||||||
assert.Equal(t, nodes[0].Hostname, "test")
|
assert.Equal(t, "test", nodes[0].Hostname)
|
||||||
assert.Equal(t, nodes[0].GivenName, "newname")
|
assert.Equal(t, "newname", nodes[0].GivenName)
|
||||||
assert.Equal(t, nodes[1].GivenName, "test")
|
assert.Equal(t, "test", nodes[1].GivenName)
|
||||||
|
|
||||||
// Nodes cannot be renamed to used names
|
// Nodes cannot be renamed to used names
|
||||||
err = db.Write(func(tx *gorm.DB) error {
|
err = db.Write(func(tx *gorm.DB) error {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -56,6 +57,65 @@ func parseCabailityVersion(req *http.Request) (tailcfg.CapabilityVersion, error)
|
||||||
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
|
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) handleVerifyRequest(
|
||||||
|
req *http.Request,
|
||||||
|
) (bool, error) {
|
||||||
|
body, err := io.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("cannot read request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
|
||||||
|
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
|
||||||
|
return false, fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := h.db.ListNodes()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("cannot list nodes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes.ContainsNodeKey(derpAdmitClientRequest.NodePublic), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159, Derp use verifyClientsURL to verify whether a client is allowed to connect to the DERP server.
|
||||||
|
func (h *Headscale) VerifyHandler(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
) {
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug().
|
||||||
|
Str("handler", "/verify").
|
||||||
|
Msg("verify client")
|
||||||
|
|
||||||
|
allow, err := h.handleVerifyRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to verify client")
|
||||||
|
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := tailcfg.DERPAdmitClientResponse{
|
||||||
|
Allow: allow,
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "application/json")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
err = json.NewEncoder(writer).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// KeyHandler provides the Headscale pub key
|
// KeyHandler provides the Headscale pub key
|
||||||
// Listens in /key.
|
// Listens in /key.
|
||||||
func (h *Headscale) KeyHandler(
|
func (h *Headscale) KeyHandler(
|
||||||
|
|
|
@ -78,7 +78,7 @@ func prometheusMiddleware(next http.Handler) http.Handler {
|
||||||
|
|
||||||
// Ignore streaming and noise sessions
|
// Ignore streaming and noise sessions
|
||||||
// it has its own router further down.
|
// it has its own router further down.
|
||||||
if path == "/ts2021" || path == "/machine/map" || path == "/derp" || path == "/derp/probe" || path == "/bootstrap-dns" {
|
if path == "/ts2021" || path == "/machine/map" || path == "/derp" || path == "/derp/probe" || path == "/derp/latency-check" || path == "/bootstrap-dns" {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -488,7 +488,7 @@ func (a *AuthProviderOIDC) registerNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby):
|
// TODO(kradalby):
|
||||||
// Rewrite in elem-go
|
// Rewrite in elem-go.
|
||||||
func renderOIDCCallbackTemplate(
|
func renderOIDCCallbackTemplate(
|
||||||
user *types.User,
|
user *types.User,
|
||||||
) (*bytes.Buffer, error) {
|
) (*bytes.Buffer, error) {
|
||||||
|
|
|
@ -599,7 +599,7 @@ func (pol *ACLPolicy) ExpandAlias(
|
||||||
// TODO(kradalby): It is quite hard to understand what this function is doing,
|
// TODO(kradalby): It is quite hard to understand what this function is doing,
|
||||||
// it seems like it trying to ensure that we dont include nodes that are tagged
|
// it seems like it trying to ensure that we dont include nodes that are tagged
|
||||||
// when we look up the nodes owned by a user.
|
// when we look up the nodes owned by a user.
|
||||||
// This should be refactored to be more clear as part of the Tags work in #1369
|
// This should be refactored to be more clear as part of the Tags work in #1369.
|
||||||
func excludeCorrectlyTaggedNodes(
|
func excludeCorrectlyTaggedNodes(
|
||||||
aclPolicy *ACLPolicy,
|
aclPolicy *ACLPolicy,
|
||||||
nodes types.Nodes,
|
nodes types.Nodes,
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/require"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
|
@ -1824,12 +1824,20 @@ func TestTheInternet(t *testing.T) {
|
||||||
|
|
||||||
for i := range internetPrefs {
|
for i := range internetPrefs {
|
||||||
if internetPrefs[i].String() != hsExitNodeDest[i].IP {
|
if internetPrefs[i].String() != hsExitNodeDest[i].IP {
|
||||||
t.Errorf("prefix from internet set %q != hsExit list %q", internetPrefs[i].String(), hsExitNodeDest[i].IP)
|
t.Errorf(
|
||||||
|
"prefix from internet set %q != hsExit list %q",
|
||||||
|
internetPrefs[i].String(),
|
||||||
|
hsExitNodeDest[i].IP,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(internetPrefs) != len(hsExitNodeDest) {
|
if len(internetPrefs) != len(hsExitNodeDest) {
|
||||||
t.Fatalf("expected same length of prefixes, internet: %d, hsExit: %d", len(internetPrefs), len(hsExitNodeDest))
|
t.Fatalf(
|
||||||
|
"expected same length of prefixes, internet: %d, hsExit: %d",
|
||||||
|
len(internetPrefs),
|
||||||
|
len(hsExitNodeDest),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2036,7 +2044,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: []tailcfg.FilterRule{
|
want: []tailcfg.FilterRule{
|
||||||
{
|
{
|
||||||
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
|
SrcIPs: []string{
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"100.64.0.2/32",
|
||||||
|
"fd7a:115c:a1e0::1/128",
|
||||||
|
"fd7a:115c:a1e0::2/128",
|
||||||
|
},
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
{
|
{
|
||||||
IP: "100.64.0.100/32",
|
IP: "100.64.0.100/32",
|
||||||
|
@ -2049,7 +2062,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
|
SrcIPs: []string{
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"100.64.0.2/32",
|
||||||
|
"fd7a:115c:a1e0::1/128",
|
||||||
|
"fd7a:115c:a1e0::2/128",
|
||||||
|
},
|
||||||
DstPorts: hsExitNodeDest,
|
DstPorts: hsExitNodeDest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -2132,7 +2150,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: []tailcfg.FilterRule{
|
want: []tailcfg.FilterRule{
|
||||||
{
|
{
|
||||||
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
|
SrcIPs: []string{
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"100.64.0.2/32",
|
||||||
|
"fd7a:115c:a1e0::1/128",
|
||||||
|
"fd7a:115c:a1e0::2/128",
|
||||||
|
},
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
{
|
{
|
||||||
IP: "100.64.0.100/32",
|
IP: "100.64.0.100/32",
|
||||||
|
@ -2145,7 +2168,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
|
SrcIPs: []string{
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"100.64.0.2/32",
|
||||||
|
"fd7a:115c:a1e0::1/128",
|
||||||
|
"fd7a:115c:a1e0::2/128",
|
||||||
|
},
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
{IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
{IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
||||||
{IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
{IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
||||||
|
@ -2217,7 +2245,10 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
IPv6: iap("fd7a:115c:a1e0::100"),
|
IPv6: iap("fd7a:115c:a1e0::100"),
|
||||||
User: types.User{Name: "user100"},
|
User: types.User{Name: "user100"},
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/16"), netip.MustParsePrefix("16.0.0.0/16")},
|
RoutableIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("8.0.0.0/16"),
|
||||||
|
netip.MustParsePrefix("16.0.0.0/16"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
peers: types.Nodes{
|
peers: types.Nodes{
|
||||||
|
@ -2234,7 +2265,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: []tailcfg.FilterRule{
|
want: []tailcfg.FilterRule{
|
||||||
{
|
{
|
||||||
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
|
SrcIPs: []string{
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"100.64.0.2/32",
|
||||||
|
"fd7a:115c:a1e0::1/128",
|
||||||
|
"fd7a:115c:a1e0::2/128",
|
||||||
|
},
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
{
|
{
|
||||||
IP: "100.64.0.100/32",
|
IP: "100.64.0.100/32",
|
||||||
|
@ -2247,7 +2283,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
|
SrcIPs: []string{
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"100.64.0.2/32",
|
||||||
|
"fd7a:115c:a1e0::1/128",
|
||||||
|
"fd7a:115c:a1e0::2/128",
|
||||||
|
},
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
{
|
{
|
||||||
IP: "8.0.0.0/8",
|
IP: "8.0.0.0/8",
|
||||||
|
@ -2294,7 +2335,10 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
IPv6: iap("fd7a:115c:a1e0::100"),
|
IPv6: iap("fd7a:115c:a1e0::100"),
|
||||||
User: types.User{Name: "user100"},
|
User: types.User{Name: "user100"},
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/8"), netip.MustParsePrefix("16.0.0.0/8")},
|
RoutableIPs: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("8.0.0.0/8"),
|
||||||
|
netip.MustParsePrefix("16.0.0.0/8"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
peers: types.Nodes{
|
peers: types.Nodes{
|
||||||
|
@ -2311,7 +2355,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: []tailcfg.FilterRule{
|
want: []tailcfg.FilterRule{
|
||||||
{
|
{
|
||||||
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
|
SrcIPs: []string{
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"100.64.0.2/32",
|
||||||
|
"fd7a:115c:a1e0::1/128",
|
||||||
|
"fd7a:115c:a1e0::2/128",
|
||||||
|
},
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
{
|
{
|
||||||
IP: "100.64.0.100/32",
|
IP: "100.64.0.100/32",
|
||||||
|
@ -2324,7 +2373,12 @@ func TestReduceFilterRules(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
|
SrcIPs: []string{
|
||||||
|
"100.64.0.1/32",
|
||||||
|
"100.64.0.2/32",
|
||||||
|
"fd7a:115c:a1e0::1/128",
|
||||||
|
"fd7a:115c:a1e0::2/128",
|
||||||
|
},
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
{
|
{
|
||||||
IP: "8.0.0.0/16",
|
IP: "8.0.0.0/16",
|
||||||
|
@ -3299,7 +3353,11 @@ func TestSSHRules(t *testing.T) {
|
||||||
SSHUsers: map[string]string{
|
SSHUsers: map[string]string{
|
||||||
"autogroup:nonroot": "=",
|
"autogroup:nonroot": "=",
|
||||||
},
|
},
|
||||||
Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true, AllowLocalPortForwarding: true},
|
Action: &tailcfg.SSHAction{
|
||||||
|
Accept: true,
|
||||||
|
AllowAgentForwarding: true,
|
||||||
|
AllowLocalPortForwarding: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SSHUsers: map[string]string{
|
SSHUsers: map[string]string{
|
||||||
|
@ -3310,7 +3368,11 @@ func TestSSHRules(t *testing.T) {
|
||||||
Any: true,
|
Any: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true, AllowLocalPortForwarding: true},
|
Action: &tailcfg.SSHAction{
|
||||||
|
Accept: true,
|
||||||
|
AllowAgentForwarding: true,
|
||||||
|
AllowLocalPortForwarding: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Principals: []*tailcfg.SSHPrincipal{
|
Principals: []*tailcfg.SSHPrincipal{
|
||||||
|
@ -3321,7 +3383,11 @@ func TestSSHRules(t *testing.T) {
|
||||||
SSHUsers: map[string]string{
|
SSHUsers: map[string]string{
|
||||||
"autogroup:nonroot": "=",
|
"autogroup:nonroot": "=",
|
||||||
},
|
},
|
||||||
Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true, AllowLocalPortForwarding: true},
|
Action: &tailcfg.SSHAction{
|
||||||
|
Accept: true,
|
||||||
|
AllowAgentForwarding: true,
|
||||||
|
AllowLocalPortForwarding: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SSHUsers: map[string]string{
|
SSHUsers: map[string]string{
|
||||||
|
@ -3332,7 +3398,11 @@ func TestSSHRules(t *testing.T) {
|
||||||
Any: true,
|
Any: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true, AllowLocalPortForwarding: true},
|
Action: &tailcfg.SSHAction{
|
||||||
|
Accept: true,
|
||||||
|
AllowAgentForwarding: true,
|
||||||
|
AllowLocalPortForwarding: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
@ -3392,7 +3462,7 @@ func TestSSHRules(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := tt.pol.CompileSSHPolicy(&tt.node, tt.peers)
|
got, err := tt.pol.CompileSSHPolicy(&tt.node, tt.peers)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||||
t.Errorf("TestSSHRules() unexpected result (-want +got):\n%s", diff)
|
t.Errorf("TestSSHRules() unexpected result (-want +got):\n%s", diff)
|
||||||
|
@ -3499,7 +3569,7 @@ func TestValidExpandTagOwnersInSources(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{})
|
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
want := []tailcfg.FilterRule{
|
want := []tailcfg.FilterRule{
|
||||||
{
|
{
|
||||||
|
@ -3550,7 +3620,7 @@ func TestInvalidTagValidUser(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{})
|
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
want := []tailcfg.FilterRule{
|
want := []tailcfg.FilterRule{
|
||||||
{
|
{
|
||||||
|
@ -3609,7 +3679,7 @@ func TestValidExpandTagOwnersInDestinations(t *testing.T) {
|
||||||
// c.Assert(rules[0].DstPorts[0].IP, check.Equals, "100.64.0.1/32")
|
// c.Assert(rules[0].DstPorts[0].IP, check.Equals, "100.64.0.1/32")
|
||||||
|
|
||||||
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{})
|
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
want := []tailcfg.FilterRule{
|
want := []tailcfg.FilterRule{
|
||||||
{
|
{
|
||||||
|
@ -3679,7 +3749,7 @@ func TestValidTagInvalidUser(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{nodes2})
|
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{nodes2})
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
want := []tailcfg.FilterRule{
|
want := []tailcfg.FilterRule{
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,9 +17,13 @@ func Apple(url string) *elem.Element {
|
||||||
headerOne("headscale: iOS configuration"),
|
headerOne("headscale: iOS configuration"),
|
||||||
headerTwo("GUI"),
|
headerTwo("GUI"),
|
||||||
elem.Ol(nil,
|
elem.Ol(nil,
|
||||||
elem.Li(nil,
|
elem.Li(
|
||||||
|
nil,
|
||||||
elem.Text("Install the official Tailscale iOS client from the "),
|
elem.Text("Install the official Tailscale iOS client from the "),
|
||||||
elem.A(attrs.Props{attrs.Href: "https://apps.apple.com/app/tailscale/id1470499037"},
|
elem.A(
|
||||||
|
attrs.Props{
|
||||||
|
attrs.Href: "https://apps.apple.com/app/tailscale/id1470499037",
|
||||||
|
},
|
||||||
elem.Text("App store"),
|
elem.Text("App store"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -31,27 +35,47 @@ func Apple(url string) *elem.Element {
|
||||||
elem.Li(nil,
|
elem.Li(nil,
|
||||||
elem.Text("Open Settings on the iOS device"),
|
elem.Text("Open Settings on the iOS device"),
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
elem.Li(
|
||||||
elem.Text(`Scroll down to the "third party apps" section, under "Game Center" or "TV Provider"`),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
`Scroll down to the "third party apps" section, under "Game Center" or "TV Provider"`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
elem.Li(nil,
|
||||||
elem.Text("Find Tailscale and select it"),
|
elem.Text("Find Tailscale and select it"),
|
||||||
elem.Ul(nil,
|
elem.Ul(nil,
|
||||||
elem.Li(nil,
|
elem.Li(
|
||||||
elem.Text(`If the iOS device was previously logged into Tailscale, switch the "Reset Keychain" toggle to "on"`),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
`If the iOS device was previously logged into Tailscale, switch the "Reset Keychain" toggle to "on"`,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
|
||||||
elem.Text(fmt.Sprintf(`Enter "%s" under "Alternate Coordination Server URL"`,url)),
|
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
elem.Li(
|
||||||
elem.Text("Restart the app by closing it from the iOS app switcher, open the app and select the regular sign in option "),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
fmt.Sprintf(
|
||||||
|
`Enter "%s" under "Alternate Coordination Server URL"`,
|
||||||
|
url,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elem.Li(
|
||||||
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
"Restart the app by closing it from the iOS app switcher, open the app and select the regular sign in option ",
|
||||||
|
),
|
||||||
elem.I(nil, elem.Text("(non-SSO)")),
|
elem.I(nil, elem.Text("(non-SSO)")),
|
||||||
elem.Text(". It should open up to the headscale authentication page."),
|
elem.Text(
|
||||||
|
". It should open up to the headscale authentication page.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elem.Li(
|
||||||
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
"Enter your credentials and log in. Headscale should now be working on your iOS device",
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
|
||||||
elem.Text("Enter your credentials and log in. Headscale should now be working on your iOS device"),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
headerOne("headscale: macOS configuration"),
|
headerOne("headscale: macOS configuration"),
|
||||||
|
@ -66,34 +90,58 @@ func Apple(url string) *elem.Element {
|
||||||
),
|
),
|
||||||
headerTwo("GUI"),
|
headerTwo("GUI"),
|
||||||
elem.Ol(nil,
|
elem.Ol(nil,
|
||||||
elem.Li(nil,
|
elem.Li(
|
||||||
elem.Text("ALT + Click the Tailscale icon in the menu and hover over the Debug menu"),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
"ALT + Click the Tailscale icon in the menu and hover over the Debug menu",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
elem.Li(nil,
|
||||||
elem.Text(`Under "Custom Login Server", select "Add Account..."`),
|
elem.Text(`Under "Custom Login Server", select "Add Account..."`),
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
elem.Li(
|
||||||
elem.Text(fmt.Sprintf(`Enter "%s" of the headscale instance and press "Add Account"`,url)),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
fmt.Sprintf(
|
||||||
|
`Enter "%s" of the headscale instance and press "Add Account"`,
|
||||||
|
url,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
elem.Li(nil,
|
||||||
elem.Text(`Follow the login procedure in the browser`),
|
elem.Text(`Follow the login procedure in the browser`),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
headerTwo("Profiles"),
|
headerTwo("Profiles"),
|
||||||
elem.P(nil,
|
elem.P(
|
||||||
elem.Text("Headscale can be set to the default server by installing a Headscale configuration profile:"),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
"Headscale can be set to the default server by installing a Headscale configuration profile:",
|
||||||
),
|
),
|
||||||
elem.P(nil,
|
),
|
||||||
elem.A(attrs.Props{attrs.Href: "/apple/macos-app-store", attrs.Download: "headscale_macos.mobileconfig"},
|
elem.P(
|
||||||
|
nil,
|
||||||
|
elem.A(
|
||||||
|
attrs.Props{
|
||||||
|
attrs.Href: "/apple/macos-app-store",
|
||||||
|
attrs.Download: "headscale_macos.mobileconfig",
|
||||||
|
},
|
||||||
elem.Text("macOS AppStore profile "),
|
elem.Text("macOS AppStore profile "),
|
||||||
),
|
),
|
||||||
elem.A(attrs.Props{attrs.Href: "/apple/macos-standalone", attrs.Download: "headscale_macos.mobileconfig"},
|
elem.A(
|
||||||
|
attrs.Props{
|
||||||
|
attrs.Href: "/apple/macos-standalone",
|
||||||
|
attrs.Download: "headscale_macos.mobileconfig",
|
||||||
|
},
|
||||||
elem.Text("macOS Standalone profile"),
|
elem.Text("macOS Standalone profile"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
elem.Ol(nil,
|
elem.Ol(nil,
|
||||||
elem.Li(nil,
|
elem.Li(
|
||||||
elem.Text("Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed"),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
"Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
elem.Li(nil,
|
||||||
elem.Text(`Open System Preferences and go to "Profiles"`),
|
elem.Text(`Open System Preferences and go to "Profiles"`),
|
||||||
|
@ -106,20 +154,35 @@ func Apple(url string) *elem.Element {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
elem.P(nil, elem.Text("Or")),
|
elem.P(nil, elem.Text("Or")),
|
||||||
elem.P(nil,
|
elem.P(
|
||||||
elem.Text("Use your terminal to configure the default setting for Tailscale by issuing:"),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
"Use your terminal to configure the default setting for Tailscale by issuing:",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elem.Ul(nil,
|
elem.Ul(nil,
|
||||||
elem.Li(nil,
|
elem.Li(nil,
|
||||||
elem.Text(`for app store client:`),
|
elem.Text(`for app store client:`),
|
||||||
elem.Code(nil,
|
elem.Code(
|
||||||
elem.Text(fmt.Sprintf(`defaults write io.tailscale.ipn.macos ControlURL %s`,url)),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
fmt.Sprintf(
|
||||||
|
`defaults write io.tailscale.ipn.macos ControlURL %s`,
|
||||||
|
url,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
elem.Li(nil,
|
elem.Li(nil,
|
||||||
elem.Text(`for standalone client:`),
|
elem.Text(`for standalone client:`),
|
||||||
elem.Code(nil,
|
elem.Code(
|
||||||
elem.Text(fmt.Sprintf(`defaults write io.tailscale.ipn.macsys ControlURL %s`,url)),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
fmt.Sprintf(
|
||||||
|
`defaults write io.tailscale.ipn.macsys ControlURL %s`,
|
||||||
|
url,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -127,8 +190,11 @@ func Apple(url string) *elem.Element {
|
||||||
elem.Text("Restart Tailscale.app and log in."),
|
elem.Text("Restart Tailscale.app and log in."),
|
||||||
),
|
),
|
||||||
headerThree("Caution"),
|
headerThree("Caution"),
|
||||||
elem.P(nil,
|
elem.P(
|
||||||
elem.Text("You should always download and inspect the profile before installing it:"),
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
"You should always download and inspect the profile before installing it:",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elem.Ul(nil,
|
elem.Ul(nil,
|
||||||
elem.Li(nil,
|
elem.Li(nil,
|
||||||
|
@ -144,6 +210,53 @@ func Apple(url string) *elem.Element {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
headerOne("headscale: tvOS configuration"),
|
||||||
|
headerTwo("GUI"),
|
||||||
|
elem.Ol(nil,
|
||||||
|
elem.Li(
|
||||||
|
nil,
|
||||||
|
elem.Text("Install the official Tailscale tvOS client from the "),
|
||||||
|
elem.A(
|
||||||
|
attrs.Props{
|
||||||
|
attrs.Href: "https://apps.apple.com/app/tailscale/id1470499037",
|
||||||
|
},
|
||||||
|
elem.Text("App store"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elem.Li(
|
||||||
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
"Open Settings (the Apple tvOS settings) > Apps > Tailscale",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elem.Li(
|
||||||
|
nil,
|
||||||
|
elem.Text(
|
||||||
|
fmt.Sprintf(
|
||||||
|
`Enter "%s" under "ALTERNATE COORDINATION SERVER URL"`,
|
||||||
|
url,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elem.Li(nil,
|
||||||
|
elem.Text("Return to the tvOS Home screen"),
|
||||||
|
),
|
||||||
|
elem.Li(nil,
|
||||||
|
elem.Text("Open Tailscale"),
|
||||||
|
),
|
||||||
|
elem.Li(nil,
|
||||||
|
elem.Text(`Select "Install VPN configuration"`),
|
||||||
|
),
|
||||||
|
elem.Li(nil,
|
||||||
|
elem.Text(`Select "Allow"`),
|
||||||
|
),
|
||||||
|
elem.Li(nil,
|
||||||
|
elem.Text("Scan the QR code and follow the login procedure"),
|
||||||
|
),
|
||||||
|
elem.Li(nil,
|
||||||
|
elem.Text("Headscale should now be working on your tvOS device"),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,8 @@ func Windows(url string) *elem.Element {
|
||||||
elem.A(attrs.Props{
|
elem.A(attrs.Props{
|
||||||
attrs.Href: "https://tailscale.com/download/windows",
|
attrs.Href: "https://tailscale.com/download/windows",
|
||||||
attrs.Rel: "noreferrer noopener",
|
attrs.Rel: "noreferrer noopener",
|
||||||
attrs.Target: "_blank"},
|
attrs.Target: "_blank",
|
||||||
|
},
|
||||||
elem.Text("Tailscale for Windows ")),
|
elem.Text("Tailscale for Windows ")),
|
||||||
elem.Text("and install it."),
|
elem.Text("and install it."),
|
||||||
),
|
),
|
||||||
|
|
|
@ -28,8 +28,9 @@ const (
|
||||||
maxDuration time.Duration = 1<<63 - 1
|
maxDuration time.Duration = 1<<63 - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var errOidcMutuallyExclusive = errors.New(
|
var (
|
||||||
"oidc_client_secret and oidc_client_secret_path are mutually exclusive",
|
errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive")
|
||||||
|
errServerURLSuffix = errors.New("server_url cannot be part of base_domain in a way that could make the DERP and headscale server unreachable")
|
||||||
)
|
)
|
||||||
|
|
||||||
type IPAllocationStrategy string
|
type IPAllocationStrategy string
|
||||||
|
@ -827,11 +828,10 @@ func LoadServerConfig() (*Config, error) {
|
||||||
// - DERP run on their own domains
|
// - DERP run on their own domains
|
||||||
// - Control plane runs on login.tailscale.com/controlplane.tailscale.com
|
// - Control plane runs on login.tailscale.com/controlplane.tailscale.com
|
||||||
// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net)
|
// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net)
|
||||||
if dnsConfig.BaseDomain != "" &&
|
if dnsConfig.BaseDomain != "" {
|
||||||
strings.Contains(serverURL, dnsConfig.BaseDomain) {
|
if err := isSafeServerURL(serverURL, dnsConfig.BaseDomain); err != nil {
|
||||||
return nil, errors.New(
|
return nil, err
|
||||||
"server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.",
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
|
@ -924,6 +924,37 @@ func LoadServerConfig() (*Config, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BaseDomain cannot be a suffix of the server URL.
|
||||||
|
// This is because Tailscale takes over the domain in BaseDomain,
|
||||||
|
// causing the headscale server and DERP to be unreachable.
|
||||||
|
// For Tailscale upstream, the following is true:
|
||||||
|
// - DERP run on their own domains.
|
||||||
|
// - Control plane runs on login.tailscale.com/controlplane.tailscale.com.
|
||||||
|
// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net).
|
||||||
|
func isSafeServerURL(serverURL, baseDomain string) error {
|
||||||
|
server, err := url.Parse(serverURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverDomainParts := strings.Split(server.Host, ".")
|
||||||
|
baseDomainParts := strings.Split(baseDomain, ".")
|
||||||
|
|
||||||
|
if len(serverDomainParts) <= len(baseDomainParts) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := len(serverDomainParts)
|
||||||
|
b := len(baseDomainParts)
|
||||||
|
for i := range len(baseDomainParts) {
|
||||||
|
if serverDomainParts[s-i-1] != baseDomainParts[b-i-1] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errServerURLSuffix
|
||||||
|
}
|
||||||
|
|
||||||
type deprecator struct {
|
type deprecator struct {
|
||||||
warns set.Set[string]
|
warns set.Set[string]
|
||||||
fatals set.Set[string]
|
fatals set.Set[string]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -8,6 +9,7 @@ import (
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
)
|
)
|
||||||
|
@ -35,8 +37,17 @@ func TestReadConfig(t *testing.T) {
|
||||||
MagicDNS: true,
|
MagicDNS: true,
|
||||||
BaseDomain: "example.com",
|
BaseDomain: "example.com",
|
||||||
Nameservers: Nameservers{
|
Nameservers: Nameservers{
|
||||||
Global: []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001", "https://dns.nextdns.io/abc123"},
|
Global: []string{
|
||||||
Split: map[string][]string{"darp.headscale.net": {"1.1.1.1", "8.8.8.8"}, "foo.bar.com": {"1.1.1.1"}},
|
"1.1.1.1",
|
||||||
|
"1.0.0.1",
|
||||||
|
"2606:4700:4700::1111",
|
||||||
|
"2606:4700:4700::1001",
|
||||||
|
"https://dns.nextdns.io/abc123",
|
||||||
|
},
|
||||||
|
Split: map[string][]string{
|
||||||
|
"darp.headscale.net": {"1.1.1.1", "8.8.8.8"},
|
||||||
|
"foo.bar.com": {"1.1.1.1"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ExtraRecords: []tailcfg.DNSRecord{
|
ExtraRecords: []tailcfg.DNSRecord{
|
||||||
{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"},
|
{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"},
|
||||||
|
@ -91,8 +102,17 @@ func TestReadConfig(t *testing.T) {
|
||||||
MagicDNS: false,
|
MagicDNS: false,
|
||||||
BaseDomain: "example.com",
|
BaseDomain: "example.com",
|
||||||
Nameservers: Nameservers{
|
Nameservers: Nameservers{
|
||||||
Global: []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001", "https://dns.nextdns.io/abc123"},
|
Global: []string{
|
||||||
Split: map[string][]string{"darp.headscale.net": {"1.1.1.1", "8.8.8.8"}, "foo.bar.com": {"1.1.1.1"}},
|
"1.1.1.1",
|
||||||
|
"1.0.0.1",
|
||||||
|
"2606:4700:4700::1111",
|
||||||
|
"2606:4700:4700::1001",
|
||||||
|
"https://dns.nextdns.io/abc123",
|
||||||
|
},
|
||||||
|
Split: map[string][]string{
|
||||||
|
"darp.headscale.net": {"1.1.1.1", "8.8.8.8"},
|
||||||
|
"foo.bar.com": {"1.1.1.1"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ExtraRecords: []tailcfg.DNSRecord{
|
ExtraRecords: []tailcfg.DNSRecord{
|
||||||
{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"},
|
{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"},
|
||||||
|
@ -139,7 +159,7 @@ func TestReadConfig(t *testing.T) {
|
||||||
return LoadServerConfig()
|
return LoadServerConfig()
|
||||||
},
|
},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.",
|
wantErr: errServerURLSuffix.Error(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "base-domain-not-in-server-url",
|
name: "base-domain-not-in-server-url",
|
||||||
|
@ -186,7 +206,7 @@ func TestReadConfig(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
viper.Reset()
|
viper.Reset()
|
||||||
err := LoadConfig(tt.configPath, true)
|
err := LoadConfig(tt.configPath, true)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
conf, err := tt.setup(t)
|
conf, err := tt.setup(t)
|
||||||
|
|
||||||
|
@ -196,7 +216,7 @@ func TestReadConfig(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, conf); diff != "" {
|
if diff := cmp.Diff(tt.want, conf); diff != "" {
|
||||||
t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
|
t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
|
||||||
|
@ -276,10 +296,10 @@ func TestReadConfigFromEnv(t *testing.T) {
|
||||||
|
|
||||||
viper.Reset()
|
viper.Reset()
|
||||||
err := LoadConfig("testdata/minimal.yaml", true)
|
err := LoadConfig("testdata/minimal.yaml", true)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
conf, err := tt.setup(t)
|
conf, err := tt.setup(t)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.want, conf); diff != "" {
|
if diff := cmp.Diff(tt.want, conf); diff != "" {
|
||||||
t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
|
t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff)
|
||||||
|
@ -310,13 +330,25 @@ noise:
|
||||||
|
|
||||||
// Check configuration validation errors (1)
|
// Check configuration validation errors (1)
|
||||||
err = LoadConfig(tmpDir, false)
|
err = LoadConfig(tmpDir, false)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = validateServerConfig()
|
err = validateServerConfig()
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both")
|
assert.Contains(
|
||||||
assert.Contains(t, err.Error(), "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are")
|
t,
|
||||||
assert.Contains(t, err.Error(), "Fatal config error: server_url must start with https:// or http://")
|
err.Error(),
|
||||||
|
"Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both",
|
||||||
|
)
|
||||||
|
assert.Contains(
|
||||||
|
t,
|
||||||
|
err.Error(),
|
||||||
|
"Fatal config error: the only supported values for tls_letsencrypt_challenge_type are",
|
||||||
|
)
|
||||||
|
assert.Contains(
|
||||||
|
t,
|
||||||
|
err.Error(),
|
||||||
|
"Fatal config error: server_url must start with https:// or http://",
|
||||||
|
)
|
||||||
|
|
||||||
// Check configuration validation errors (2)
|
// Check configuration validation errors (2)
|
||||||
configYaml = []byte(`---
|
configYaml = []byte(`---
|
||||||
|
@ -331,5 +363,66 @@ tls_letsencrypt_challenge_type: TLS-ALPN-01
|
||||||
t.Fatalf("Couldn't write file %s", configFilePath)
|
t.Fatalf("Couldn't write file %s", configFilePath)
|
||||||
}
|
}
|
||||||
err = LoadConfig(tmpDir, false)
|
err = LoadConfig(tmpDir, false)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK
|
||||||
|
// server_url: headscale.com, base: clients.headscale.com
|
||||||
|
// server_url: headscale.com, base: headscale.net
|
||||||
|
//
|
||||||
|
// NOT OK
|
||||||
|
// server_url: server.headscale.com, base: headscale.com.
|
||||||
|
func TestSafeServerURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
serverURL, baseDomain,
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
serverURL: "https://example.com",
|
||||||
|
baseDomain: "example.org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://headscale.com",
|
||||||
|
baseDomain: "headscale.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://headscale.com",
|
||||||
|
baseDomain: "clients.headscale.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://headscale.com",
|
||||||
|
baseDomain: "clients.subdomain.headscale.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://headscale.kristoffer.com",
|
||||||
|
baseDomain: "mybase",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://server.headscale.com",
|
||||||
|
baseDomain: "headscale.com",
|
||||||
|
wantErr: errServerURLSuffix.Error(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "https://server.subdomain.headscale.com",
|
||||||
|
baseDomain: "headscale.com",
|
||||||
|
wantErr: errServerURLSuffix.Error(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverURL: "http://foo\x00",
|
||||||
|
wantErr: `parse "http://foo\x00": net/url: invalid control character in URL`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
testName := fmt.Sprintf("server=%s domain=%s", tt.serverURL, tt.baseDomain)
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
err := isSafeServerURL(tt.serverURL, tt.baseDomain)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
assert.EqualError(t, err, tt.wantErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,6 +223,16 @@ func (nodes Nodes) FilterByIP(ip netip.Addr) Nodes {
|
||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (nodes Nodes) ContainsNodeKey(nodeKey key.NodePublic) bool {
|
||||||
|
for _, node := range nodes {
|
||||||
|
if node.NodeKey == nodeKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (node *Node) Proto() *v1.Node {
|
func (node *Node) Proto() *v1.Node {
|
||||||
nodeProto := &v1.Node{
|
nodeProto := &v1.Node{
|
||||||
Id: uint64(node.ID),
|
Id: uint64(node.ID),
|
||||||
|
|
|
@ -8,7 +8,7 @@ prefixes:
|
||||||
database:
|
database:
|
||||||
type: sqlite3
|
type: sqlite3
|
||||||
|
|
||||||
server_url: "https://derp.no"
|
server_url: "https://server.derp.no"
|
||||||
|
|
||||||
dns:
|
dns:
|
||||||
magic_dns: true
|
magic_dns: true
|
||||||
|
|
|
@ -4,12 +4,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGenerateRandomStringDNSSafe(t *testing.T) {
|
func TestGenerateRandomStringDNSSafe(t *testing.T) {
|
||||||
for i := 0; i < 100000; i++ {
|
for i := 0; i < 100000; i++ {
|
||||||
str, err := GenerateRandomStringDNSSafe(8)
|
str, err := GenerateRandomStringDNSSafe(8)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, str, 8)
|
assert.Len(t, str, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,10 @@ Tests are located in files ending with `_test.go` and the framework are located
|
||||||
|
|
||||||
## Running integration tests locally
|
## Running integration tests locally
|
||||||
|
|
||||||
The easiest way to run tests locally is to use `[act](INSERT LINK)`, a local GitHub Actions runner:
|
The easiest way to run tests locally is to use [act](https://github.com/nektos/act), a local GitHub Actions runner:
|
||||||
|
|
||||||
```
|
```
|
||||||
act pull_request -W .github/workflows/test-integration-v2-TestPingAllByIP.yaml
|
act pull_request -W .github/workflows/test-integration.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, the `docker run` command in each GitHub workflow file can be used.
|
Alternatively, the `docker run` command in each GitHub workflow file can be used.
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var veryLargeDestination = []string{
|
var veryLargeDestination = []string{
|
||||||
|
@ -54,7 +55,7 @@ func aclScenario(
|
||||||
) *Scenario {
|
) *Scenario {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": clientsPerUser,
|
"user1": clientsPerUser,
|
||||||
|
@ -77,10 +78,10 @@ func aclScenario(
|
||||||
hsic.WithACLPolicy(policy),
|
hsic.WithACLPolicy(policy),
|
||||||
hsic.WithTestName("acl"),
|
hsic.WithTestName("acl"),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = scenario.ListTailscaleClientsFQDNs()
|
_, err = scenario.ListTailscaleClientsFQDNs()
|
||||||
assertNoErrListFQDN(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return scenario
|
return scenario
|
||||||
}
|
}
|
||||||
|
@ -267,7 +268,7 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||||
for name, testCase := range tests {
|
for name, testCase := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
spec := testCase.users
|
spec := testCase.users
|
||||||
|
|
||||||
|
@ -275,22 +276,22 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
||||||
[]tsic.Option{},
|
[]tsic.Option{},
|
||||||
hsic.WithACLPolicy(&testCase.policy),
|
hsic.WithACLPolicy(&testCase.policy),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSyncWithPeerCount(testCase.want["user1"])
|
err = scenario.WaitForTailscaleSyncWithPeerCount(testCase.want["user1"])
|
||||||
assertNoErrSync(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
status, err := client.Status()
|
status, err := client.Status()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user := status.User[status.Self.UserID].LoginName
|
user := status.User[status.Self.UserID].LoginName
|
||||||
|
|
||||||
assert.Equal(t, (testCase.want[user]), len(status.Peer))
|
assert.Len(t, status.Peer, (testCase.want[user]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -319,23 +320,23 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test that user1 can visit all user2
|
// Test that user1 can visit all user2
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,14 +344,14 @@ func TestACLAllowUser80Dst(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -376,10 +377,10 @@ func TestACLDenyAllPort80(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
for _, hostname := range allHostnames {
|
for _, hostname := range allHostnames {
|
||||||
|
@ -394,7 +395,7 @@ func TestACLDenyAllPort80(t *testing.T) {
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -420,23 +421,23 @@ func TestACLAllowUserDst(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test that user1 can visit all user2
|
// Test that user1 can visit all user2
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,14 +445,14 @@ func TestACLAllowUserDst(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -476,23 +477,23 @@ func TestACLAllowStarDst(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test that user1 can visit all user2
|
// Test that user1 can visit all user2
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,14 +501,14 @@ func TestACLAllowStarDst(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -537,23 +538,23 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test that user1 can visit all user2
|
// Test that user1 can visit all user2
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -561,14 +562,14 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -679,10 +680,10 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test1ip4 := netip.MustParseAddr("100.64.0.1")
|
test1ip4 := netip.MustParseAddr("100.64.0.1")
|
||||||
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
||||||
test1, err := scenario.FindTailscaleClientByIP(test1ip6)
|
test1, err := scenario.FindTailscaleClientByIP(test1ip6)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test1fqdn, err := test1.FQDN()
|
test1fqdn, err := test1.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test1ip4URL := fmt.Sprintf("http://%s/etc/hostname", test1ip4.String())
|
test1ip4URL := fmt.Sprintf("http://%s/etc/hostname", test1ip4.String())
|
||||||
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
||||||
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
||||||
|
@ -690,10 +691,10 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test2ip4 := netip.MustParseAddr("100.64.0.2")
|
test2ip4 := netip.MustParseAddr("100.64.0.2")
|
||||||
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
||||||
test2, err := scenario.FindTailscaleClientByIP(test2ip6)
|
test2, err := scenario.FindTailscaleClientByIP(test2ip6)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test2fqdn, err := test2.FQDN()
|
test2fqdn, err := test2.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test2ip4URL := fmt.Sprintf("http://%s/etc/hostname", test2ip4.String())
|
test2ip4URL := fmt.Sprintf("http://%s/etc/hostname", test2ip4.String())
|
||||||
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
||||||
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
||||||
|
@ -701,10 +702,10 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip4 := netip.MustParseAddr("100.64.0.3")
|
test3ip4 := netip.MustParseAddr("100.64.0.3")
|
||||||
test3ip6 := netip.MustParseAddr("fd7a:115c:a1e0::3")
|
test3ip6 := netip.MustParseAddr("fd7a:115c:a1e0::3")
|
||||||
test3, err := scenario.FindTailscaleClientByIP(test3ip6)
|
test3, err := scenario.FindTailscaleClientByIP(test3ip6)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test3fqdn, err := test3.FQDN()
|
test3fqdn, err := test3.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test3ip4URL := fmt.Sprintf("http://%s/etc/hostname", test3ip4.String())
|
test3ip4URL := fmt.Sprintf("http://%s/etc/hostname", test3ip4.String())
|
||||||
test3ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test3ip6.String())
|
test3ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test3ip6.String())
|
||||||
test3fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test3fqdn)
|
test3fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test3fqdn)
|
||||||
|
@ -719,7 +720,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip4URL,
|
test3ip4URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test3ip6URL)
|
result, err = test1.Curl(test3ip6URL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -730,7 +731,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip6URL,
|
test3ip6URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test3fqdnURL)
|
result, err = test1.Curl(test3fqdnURL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -741,7 +742,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3fqdnURL,
|
test3fqdnURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// test2 can query test3
|
// test2 can query test3
|
||||||
result, err = test2.Curl(test3ip4URL)
|
result, err = test2.Curl(test3ip4URL)
|
||||||
|
@ -753,7 +754,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip4URL,
|
test3ip4URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test3ip6URL)
|
result, err = test2.Curl(test3ip6URL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -764,7 +765,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3ip6URL,
|
test3ip6URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test3fqdnURL)
|
result, err = test2.Curl(test3fqdnURL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -775,33 +776,33 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test3fqdnURL,
|
test3fqdnURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// test3 cannot query test1
|
// test3 cannot query test1
|
||||||
result, err = test3.Curl(test1ip4URL)
|
result, err = test3.Curl(test1ip4URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test3.Curl(test1ip6URL)
|
result, err = test3.Curl(test1ip6URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test3.Curl(test1fqdnURL)
|
result, err = test3.Curl(test1fqdnURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// test3 cannot query test2
|
// test3 cannot query test2
|
||||||
result, err = test3.Curl(test2ip4URL)
|
result, err = test3.Curl(test2ip4URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test3.Curl(test2ip6URL)
|
result, err = test3.Curl(test2ip6URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test3.Curl(test2fqdnURL)
|
result, err = test3.Curl(test2fqdnURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// test1 can query test2
|
// test1 can query test2
|
||||||
result, err = test1.Curl(test2ip4URL)
|
result, err = test1.Curl(test2ip4URL)
|
||||||
|
@ -814,7 +815,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
result, err = test1.Curl(test2ip6URL)
|
result, err = test1.Curl(test2ip6URL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
t,
|
t,
|
||||||
|
@ -824,7 +825,7 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test2ip6URL,
|
test2ip6URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test2fqdnURL)
|
result, err = test1.Curl(test2fqdnURL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -835,20 +836,20 @@ func TestACLNamedHostsCanReach(t *testing.T) {
|
||||||
test2fqdnURL,
|
test2fqdnURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// test2 cannot query test1
|
// test2 cannot query test1
|
||||||
result, err = test2.Curl(test1ip4URL)
|
result, err = test2.Curl(test1ip4URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1ip6URL)
|
result, err = test2.Curl(test1ip6URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1fqdnURL)
|
result, err = test2.Curl(test1fqdnURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -946,10 +947,10 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
test1ip6 := netip.MustParseAddr("fd7a:115c:a1e0::1")
|
||||||
test1, err := scenario.FindTailscaleClientByIP(test1ip)
|
test1, err := scenario.FindTailscaleClientByIP(test1ip)
|
||||||
assert.NotNil(t, test1)
|
assert.NotNil(t, test1)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test1fqdn, err := test1.FQDN()
|
test1fqdn, err := test1.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test1ipURL := fmt.Sprintf("http://%s/etc/hostname", test1ip.String())
|
test1ipURL := fmt.Sprintf("http://%s/etc/hostname", test1ip.String())
|
||||||
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
test1ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test1ip6.String())
|
||||||
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
test1fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test1fqdn)
|
||||||
|
@ -958,10 +959,10 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
test2ip6 := netip.MustParseAddr("fd7a:115c:a1e0::2")
|
||||||
test2, err := scenario.FindTailscaleClientByIP(test2ip)
|
test2, err := scenario.FindTailscaleClientByIP(test2ip)
|
||||||
assert.NotNil(t, test2)
|
assert.NotNil(t, test2)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
test2fqdn, err := test2.FQDN()
|
test2fqdn, err := test2.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
test2ipURL := fmt.Sprintf("http://%s/etc/hostname", test2ip.String())
|
test2ipURL := fmt.Sprintf("http://%s/etc/hostname", test2ip.String())
|
||||||
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
test2ip6URL := fmt.Sprintf("http://[%s]/etc/hostname", test2ip6.String())
|
||||||
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
test2fqdnURL := fmt.Sprintf("http://%s/etc/hostname", test2fqdn)
|
||||||
|
@ -976,7 +977,7 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test2ipURL,
|
test2ipURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test2ip6URL)
|
result, err = test1.Curl(test2ip6URL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -987,7 +988,7 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test2ip6URL,
|
test2ip6URL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test1.Curl(test2fqdnURL)
|
result, err = test1.Curl(test2fqdnURL)
|
||||||
assert.Lenf(
|
assert.Lenf(
|
||||||
|
@ -998,19 +999,19 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) {
|
||||||
test2fqdnURL,
|
test2fqdnURL,
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1ipURL)
|
result, err = test2.Curl(test1ipURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1ip6URL)
|
result, err = test2.Curl(test1ip6URL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
result, err = test2.Curl(test1fqdnURL)
|
result, err = test2.Curl(test1fqdnURL)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1020,7 +1021,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1046,19 +1047,19 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
"HEADSCALE_POLICY_MODE": "database",
|
"HEADSCALE_POLICY_MODE": "database",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = scenario.ListTailscaleClientsFQDNs()
|
_, err = scenario.ListTailscaleClientsFQDNs()
|
||||||
assertNoErrListFQDN(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleSync()
|
err = scenario.WaitForTailscaleSync()
|
||||||
assertNoErrSync(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user1Clients, err := scenario.ListTailscaleClients("user1")
|
user1Clients, err := scenario.ListTailscaleClients("user1")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
user2Clients, err := scenario.ListTailscaleClients("user2")
|
user2Clients, err := scenario.ListTailscaleClients("user2")
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
all := append(user1Clients, user2Clients...)
|
all := append(user1Clients, user2Clients...)
|
||||||
|
|
||||||
|
@ -1070,19 +1071,19 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
p := policy.ACLPolicy{
|
p := policy.ACLPolicy{
|
||||||
ACLs: []policy.ACL{
|
ACLs: []policy.ACL{
|
||||||
|
@ -1100,7 +1101,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
policyFilePath := "/etc/headscale/policy.json"
|
policyFilePath := "/etc/headscale/policy.json"
|
||||||
|
|
||||||
err = headscale.WriteFile(policyFilePath, pBytes)
|
err = headscale.WriteFile(policyFilePath, pBytes)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// No policy is present at this time.
|
// No policy is present at this time.
|
||||||
// Add a new policy from a file.
|
// Add a new policy from a file.
|
||||||
|
@ -1113,7 +1114,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
policyFilePath,
|
policyFilePath,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the current policy and check
|
// Get the current policy and check
|
||||||
// if it is the same as the one we set.
|
// if it is the same as the one we set.
|
||||||
|
@ -1129,7 +1130,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
},
|
},
|
||||||
&output,
|
&output,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, output.ACLs, 1)
|
assert.Len(t, output.ACLs, 1)
|
||||||
|
|
||||||
|
@ -1141,14 +1142,14 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
for _, client := range user1Clients {
|
for _, client := range user1Clients {
|
||||||
for _, peer := range user2Clients {
|
for _, peer := range user2Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Len(t, result, 13)
|
assert.Len(t, result, 13)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1156,14 +1157,14 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
|
||||||
for _, client := range user2Clients {
|
for _, client := range user2Clients {
|
||||||
for _, peer := range user1Clients {
|
for _, peer := range user1Clients {
|
||||||
fqdn, err := peer.FQDN()
|
fqdn, err := peer.FQDN()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
url := fmt.Sprintf("http://%s/etc/hostname", fqdn)
|
||||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||||
|
|
||||||
result, err := client.Curl(url)
|
result, err := client.Curl(url)
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
assert.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
|
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
|
||||||
|
@ -34,7 +35,7 @@ func TestUserCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -43,10 +44,10 @@ func TestUserCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listUsers []v1.User
|
var listUsers []v1.User
|
||||||
err = executeAndUnmarshal(headscale,
|
err = executeAndUnmarshal(headscale,
|
||||||
|
@ -59,7 +60,7 @@ func TestUserCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listUsers,
|
&listUsers,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result := []string{listUsers[0].GetName(), listUsers[1].GetName()}
|
result := []string{listUsers[0].GetName(), listUsers[1].GetName()}
|
||||||
sort.Strings(result)
|
sort.Strings(result)
|
||||||
|
@ -81,7 +82,7 @@ func TestUserCommand(t *testing.T) {
|
||||||
"newname",
|
"newname",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listAfterRenameUsers []v1.User
|
var listAfterRenameUsers []v1.User
|
||||||
err = executeAndUnmarshal(headscale,
|
err = executeAndUnmarshal(headscale,
|
||||||
|
@ -94,7 +95,7 @@ func TestUserCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAfterRenameUsers,
|
&listAfterRenameUsers,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
|
result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
|
||||||
sort.Strings(result)
|
sort.Strings(result)
|
||||||
|
@ -114,7 +115,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
count := 3
|
count := 3
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -122,13 +123,13 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipak"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipak"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
keys := make([]*v1.PreAuthKey, count)
|
keys := make([]*v1.PreAuthKey, count)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index := 0; index < count; index++ {
|
for index := 0; index < count; index++ {
|
||||||
var preAuthKey v1.PreAuthKey
|
var preAuthKey v1.PreAuthKey
|
||||||
|
@ -150,7 +151,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&preAuthKey,
|
&preAuthKey,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
keys[index] = &preAuthKey
|
keys[index] = &preAuthKey
|
||||||
}
|
}
|
||||||
|
@ -171,7 +172,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedPreAuthKeys,
|
&listedPreAuthKeys,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||||
assert.Len(t, listedPreAuthKeys, 4)
|
assert.Len(t, listedPreAuthKeys, 4)
|
||||||
|
@ -212,7 +213,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, listedPreAuthKeys[index].GetAclTags(), []string{"tag:test1", "tag:test2"})
|
assert.Equal(t, []string{"tag:test1", "tag:test2"}, listedPreAuthKeys[index].GetAclTags())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test key expiry
|
// Test key expiry
|
||||||
|
@ -226,7 +227,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
listedPreAuthKeys[1].GetKey(),
|
listedPreAuthKeys[1].GetKey(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -242,7 +243,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedPreAuthKeysAfterExpire,
|
&listedPreAuthKeysAfterExpire,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now()))
|
assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now()))
|
||||||
assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now()))
|
assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now()))
|
||||||
|
@ -256,7 +257,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||||
user := "pre-auth-key-without-exp-user"
|
user := "pre-auth-key-without-exp-user"
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -264,10 +265,10 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipaknaexp"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipaknaexp"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var preAuthKey v1.PreAuthKey
|
var preAuthKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -284,7 +285,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||||
},
|
},
|
||||||
&preAuthKey,
|
&preAuthKey,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listedPreAuthKeys []v1.PreAuthKey
|
var listedPreAuthKeys []v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -300,7 +301,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedPreAuthKeys,
|
&listedPreAuthKeys,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||||
assert.Len(t, listedPreAuthKeys, 2)
|
assert.Len(t, listedPreAuthKeys, 2)
|
||||||
|
@ -319,7 +320,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
user := "pre-auth-key-reus-ephm-user"
|
user := "pre-auth-key-reus-ephm-user"
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -327,10 +328,10 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipakresueeph"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipakresueeph"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var preAuthReusableKey v1.PreAuthKey
|
var preAuthReusableKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -347,7 +348,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
},
|
},
|
||||||
&preAuthReusableKey,
|
&preAuthReusableKey,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var preAuthEphemeralKey v1.PreAuthKey
|
var preAuthEphemeralKey v1.PreAuthKey
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -364,7 +365,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
},
|
},
|
||||||
&preAuthEphemeralKey,
|
&preAuthEphemeralKey,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, preAuthEphemeralKey.GetEphemeral())
|
assert.True(t, preAuthEphemeralKey.GetEphemeral())
|
||||||
assert.False(t, preAuthEphemeralKey.GetReusable())
|
assert.False(t, preAuthEphemeralKey.GetReusable())
|
||||||
|
@ -383,7 +384,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedPreAuthKeys,
|
&listedPreAuthKeys,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||||
assert.Len(t, listedPreAuthKeys, 3)
|
assert.Len(t, listedPreAuthKeys, 3)
|
||||||
|
@ -397,7 +398,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
user2 := "user2"
|
user2 := "user2"
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -413,10 +414,10 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
hsic.WithTLS(),
|
hsic.WithTLS(),
|
||||||
hsic.WithHostnameAsServerURL(),
|
hsic.WithHostnameAsServerURL(),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var user2Key v1.PreAuthKey
|
var user2Key v1.PreAuthKey
|
||||||
|
|
||||||
|
@ -438,10 +439,10 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&user2Key,
|
&user2Key,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
allClients, err := scenario.ListTailscaleClients()
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
assertNoErrListClients(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, allClients, 1)
|
assert.Len(t, allClients, 1)
|
||||||
|
|
||||||
|
@ -449,22 +450,22 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
|
|
||||||
// Log out from user1
|
// Log out from user1
|
||||||
err = client.Logout()
|
err = client.Logout()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = scenario.WaitForTailscaleLogout()
|
err = scenario.WaitForTailscaleLogout()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
status, err := client.Status()
|
status, err := client.Status()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
if status.BackendState == "Starting" || status.BackendState == "Running" {
|
if status.BackendState == "Starting" || status.BackendState == "Running" {
|
||||||
t.Fatalf("expected node to be logged out, backend state: %s", status.BackendState)
|
t.Fatalf("expected node to be logged out, backend state: %s", status.BackendState)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
|
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
status, err = client.Status()
|
status, err = client.Status()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
if status.BackendState != "Running" {
|
if status.BackendState != "Running" {
|
||||||
t.Fatalf("expected node to be logged in, backend state: %s", status.BackendState)
|
t.Fatalf("expected node to be logged in, backend state: %s", status.BackendState)
|
||||||
}
|
}
|
||||||
|
@ -485,7 +486,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listNodes,
|
&listNodes,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, listNodes, 1)
|
assert.Len(t, listNodes, 1)
|
||||||
|
|
||||||
assert.Equal(t, "user2", listNodes[0].GetUser().GetName())
|
assert.Equal(t, "user2", listNodes[0].GetUser().GetName())
|
||||||
|
@ -498,7 +499,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
count := 5
|
count := 5
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -507,10 +508,10 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
keys := make([]string, count)
|
keys := make([]string, count)
|
||||||
|
|
||||||
|
@ -526,7 +527,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, apiResult)
|
assert.NotEmpty(t, apiResult)
|
||||||
|
|
||||||
keys[idx] = apiResult
|
keys[idx] = apiResult
|
||||||
|
@ -545,7 +546,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedAPIKeys,
|
&listedAPIKeys,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listedAPIKeys, 5)
|
assert.Len(t, listedAPIKeys, 5)
|
||||||
|
|
||||||
|
@ -601,7 +602,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
listedAPIKeys[idx].GetPrefix(),
|
listedAPIKeys[idx].GetPrefix(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
|
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
|
||||||
}
|
}
|
||||||
|
@ -617,7 +618,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedAfterExpireAPIKeys,
|
&listedAfterExpireAPIKeys,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index := range listedAfterExpireAPIKeys {
|
for index := range listedAfterExpireAPIKeys {
|
||||||
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok {
|
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok {
|
||||||
|
@ -643,7 +644,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
"--prefix",
|
"--prefix",
|
||||||
listedAPIKeys[0].GetPrefix(),
|
listedAPIKeys[0].GetPrefix(),
|
||||||
})
|
})
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var listedAPIKeysAfterDelete []v1.ApiKey
|
var listedAPIKeysAfterDelete []v1.ApiKey
|
||||||
err = executeAndUnmarshal(headscale,
|
err = executeAndUnmarshal(headscale,
|
||||||
|
@ -656,7 +657,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listedAPIKeysAfterDelete,
|
&listedAPIKeysAfterDelete,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listedAPIKeysAfterDelete, 4)
|
assert.Len(t, listedAPIKeysAfterDelete, 4)
|
||||||
}
|
}
|
||||||
|
@ -666,7 +667,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -674,17 +675,17 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||||
"mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
"mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c",
|
||||||
}
|
}
|
||||||
nodes := make([]*v1.Node, len(machineKeys))
|
nodes := make([]*v1.Node, len(machineKeys))
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, err := headscale.Execute(
|
_, err := headscale.Execute(
|
||||||
|
@ -702,7 +703,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -720,7 +721,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes[index] = &node
|
nodes[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -739,7 +740,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, []string{"tag:test"}, node.GetForcedTags())
|
assert.Equal(t, []string{"tag:test"}, node.GetForcedTags())
|
||||||
|
|
||||||
|
@ -753,7 +754,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
"--output", "json",
|
"--output", "json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(t, err, "tag must start with the string 'tag:'")
|
require.ErrorContains(t, err, "tag must start with the string 'tag:'")
|
||||||
|
|
||||||
// Test list all nodes after added seconds
|
// Test list all nodes after added seconds
|
||||||
resultMachines := make([]*v1.Node, len(machineKeys))
|
resultMachines := make([]*v1.Node, len(machineKeys))
|
||||||
|
@ -767,7 +768,7 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&resultMachines,
|
&resultMachines,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
for _, node := range resultMachines {
|
for _, node := range resultMachines {
|
||||||
if node.GetForcedTags() != nil {
|
if node.GetForcedTags() != nil {
|
||||||
|
@ -778,9 +779,8 @@ func TestNodeTagCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(
|
assert.True(
|
||||||
t,
|
t,
|
||||||
true,
|
|
||||||
found,
|
found,
|
||||||
"should find a node with the tag 'tag:test' in the list of nodes",
|
"should find a node with the tag 'tag:test' in the list of nodes",
|
||||||
)
|
)
|
||||||
|
@ -791,18 +791,22 @@ func TestNodeAdvertiseTagNoACLCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": 1,
|
"user1": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:test"})}, hsic.WithTestName("cliadvtags"))
|
err = scenario.CreateHeadscaleEnv(
|
||||||
assertNoErr(t, err)
|
spec,
|
||||||
|
[]tsic.Option{tsic.WithTags([]string{"tag:test"})},
|
||||||
|
hsic.WithTestName("cliadvtags"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test list all nodes after added seconds
|
// Test list all nodes after added seconds
|
||||||
resultMachines := make([]*v1.Node, spec["user1"])
|
resultMachines := make([]*v1.Node, spec["user1"])
|
||||||
|
@ -817,7 +821,7 @@ func TestNodeAdvertiseTagNoACLCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&resultMachines,
|
&resultMachines,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
for _, node := range resultMachines {
|
for _, node := range resultMachines {
|
||||||
if node.GetInvalidTags() != nil {
|
if node.GetInvalidTags() != nil {
|
||||||
|
@ -828,9 +832,8 @@ func TestNodeAdvertiseTagNoACLCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(
|
assert.True(
|
||||||
t,
|
t,
|
||||||
true,
|
|
||||||
found,
|
found,
|
||||||
"should not find a node with the tag 'tag:test' in the list of nodes",
|
"should not find a node with the tag 'tag:test' in the list of nodes",
|
||||||
)
|
)
|
||||||
|
@ -841,14 +844,18 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
"user1": 1,
|
"user1": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:exists"})}, hsic.WithTestName("cliadvtags"), hsic.WithACLPolicy(
|
err = scenario.CreateHeadscaleEnv(
|
||||||
|
spec,
|
||||||
|
[]tsic.Option{tsic.WithTags([]string{"tag:exists"})},
|
||||||
|
hsic.WithTestName("cliadvtags"),
|
||||||
|
hsic.WithACLPolicy(
|
||||||
&policy.ACLPolicy{
|
&policy.ACLPolicy{
|
||||||
ACLs: []policy.ACL{
|
ACLs: []policy.ACL{
|
||||||
{
|
{
|
||||||
|
@ -861,11 +868,12 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) {
|
||||||
"tag:exists": {"user1"},
|
"tag:exists": {"user1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
))
|
),
|
||||||
assertNoErr(t, err)
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test list all nodes after added seconds
|
// Test list all nodes after added seconds
|
||||||
resultMachines := make([]*v1.Node, spec["user1"])
|
resultMachines := make([]*v1.Node, spec["user1"])
|
||||||
|
@ -880,7 +888,7 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&resultMachines,
|
&resultMachines,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
found := false
|
found := false
|
||||||
for _, node := range resultMachines {
|
for _, node := range resultMachines {
|
||||||
if node.GetValidTags() != nil {
|
if node.GetValidTags() != nil {
|
||||||
|
@ -891,9 +899,8 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(
|
assert.True(
|
||||||
t,
|
t,
|
||||||
true,
|
|
||||||
found,
|
found,
|
||||||
"should not find a node with the tag 'tag:exists' in the list of nodes",
|
"should not find a node with the tag 'tag:exists' in the list of nodes",
|
||||||
)
|
)
|
||||||
|
@ -904,7 +911,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -913,10 +920,10 @@ func TestNodeCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Pregenerated machine keys
|
// Pregenerated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
|
@ -927,7 +934,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
"mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084",
|
||||||
}
|
}
|
||||||
nodes := make([]*v1.Node, len(machineKeys))
|
nodes := make([]*v1.Node, len(machineKeys))
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, err := headscale.Execute(
|
_, err := headscale.Execute(
|
||||||
|
@ -945,7 +952,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -963,7 +970,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes[index] = &node
|
nodes[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -983,7 +990,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAll,
|
&listAll,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAll, 5)
|
assert.Len(t, listAll, 5)
|
||||||
|
|
||||||
|
@ -1004,7 +1011,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"mkey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
|
"mkey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
|
||||||
}
|
}
|
||||||
otherUserMachines := make([]*v1.Node, len(otherUserMachineKeys))
|
otherUserMachines := make([]*v1.Node, len(otherUserMachineKeys))
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index, machineKey := range otherUserMachineKeys {
|
for index, machineKey := range otherUserMachineKeys {
|
||||||
_, err := headscale.Execute(
|
_, err := headscale.Execute(
|
||||||
|
@ -1022,7 +1029,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1040,7 +1047,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
otherUserMachines[index] = &node
|
otherUserMachines[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -1060,7 +1067,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAllWithotherUser,
|
&listAllWithotherUser,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// All nodes, nodes + otherUser
|
// All nodes, nodes + otherUser
|
||||||
assert.Len(t, listAllWithotherUser, 7)
|
assert.Len(t, listAllWithotherUser, 7)
|
||||||
|
@ -1086,7 +1093,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listOnlyotherUserMachineUser,
|
&listOnlyotherUserMachineUser,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listOnlyotherUserMachineUser, 2)
|
assert.Len(t, listOnlyotherUserMachineUser, 2)
|
||||||
|
|
||||||
|
@ -1118,7 +1125,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
"--force",
|
"--force",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test: list main user after node is deleted
|
// Test: list main user after node is deleted
|
||||||
var listOnlyMachineUserAfterDelete []v1.Node
|
var listOnlyMachineUserAfterDelete []v1.Node
|
||||||
|
@ -1135,7 +1142,7 @@ func TestNodeCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listOnlyMachineUserAfterDelete,
|
&listOnlyMachineUserAfterDelete,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listOnlyMachineUserAfterDelete, 4)
|
assert.Len(t, listOnlyMachineUserAfterDelete, 4)
|
||||||
}
|
}
|
||||||
|
@ -1145,7 +1152,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1153,10 +1160,10 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Pregenerated machine keys
|
// Pregenerated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
|
@ -1184,7 +1191,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1202,7 +1209,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes[index] = &node
|
nodes[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -1221,7 +1228,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAll,
|
&listAll,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAll, 5)
|
assert.Len(t, listAll, 5)
|
||||||
|
|
||||||
|
@ -1241,7 +1248,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
fmt.Sprintf("%d", listAll[idx].GetId()),
|
fmt.Sprintf("%d", listAll[idx].GetId()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var listAllAfterExpiry []v1.Node
|
var listAllAfterExpiry []v1.Node
|
||||||
|
@ -1256,7 +1263,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAllAfterExpiry,
|
&listAllAfterExpiry,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAllAfterExpiry, 5)
|
assert.Len(t, listAllAfterExpiry, 5)
|
||||||
|
|
||||||
|
@ -1272,7 +1279,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1280,10 +1287,10 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Pregenerated machine keys
|
// Pregenerated machine keys
|
||||||
machineKeys := []string{
|
machineKeys := []string{
|
||||||
|
@ -1294,7 +1301,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
"mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe",
|
||||||
}
|
}
|
||||||
nodes := make([]*v1.Node, len(machineKeys))
|
nodes := make([]*v1.Node, len(machineKeys))
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for index, machineKey := range machineKeys {
|
for index, machineKey := range machineKeys {
|
||||||
_, err := headscale.Execute(
|
_, err := headscale.Execute(
|
||||||
|
@ -1312,7 +1319,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1330,7 +1337,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes[index] = &node
|
nodes[index] = &node
|
||||||
}
|
}
|
||||||
|
@ -1349,7 +1356,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAll,
|
&listAll,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAll, 5)
|
assert.Len(t, listAll, 5)
|
||||||
|
|
||||||
|
@ -1370,7 +1377,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
fmt.Sprintf("newnode-%d", idx+1),
|
fmt.Sprintf("newnode-%d", idx+1),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Contains(t, res, "Node renamed")
|
assert.Contains(t, res, "Node renamed")
|
||||||
}
|
}
|
||||||
|
@ -1387,7 +1394,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAllAfterRename,
|
&listAllAfterRename,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAllAfterRename, 5)
|
assert.Len(t, listAllAfterRename, 5)
|
||||||
|
|
||||||
|
@ -1408,7 +1415,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
strings.Repeat("t", 64),
|
strings.Repeat("t", 64),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(t, err, "not be over 63 chars")
|
require.ErrorContains(t, err, "not be over 63 chars")
|
||||||
|
|
||||||
var listAllAfterRenameAttempt []v1.Node
|
var listAllAfterRenameAttempt []v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1422,7 +1429,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&listAllAfterRenameAttempt,
|
&listAllAfterRenameAttempt,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, listAllAfterRenameAttempt, 5)
|
assert.Len(t, listAllAfterRenameAttempt, 5)
|
||||||
|
|
||||||
|
@ -1438,7 +1445,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1447,10 +1454,10 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins"))
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Randomly generated node key
|
// Randomly generated node key
|
||||||
machineKey := "mkey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
|
machineKey := "mkey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa"
|
||||||
|
@ -1470,7 +1477,7 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var node v1.Node
|
var node v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1488,11 +1495,11 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, uint64(1), node.GetId())
|
assert.Equal(t, uint64(1), node.GetId())
|
||||||
assert.Equal(t, "nomad-node", node.GetName())
|
assert.Equal(t, "nomad-node", node.GetName())
|
||||||
assert.Equal(t, node.GetUser().GetName(), "old-user")
|
assert.Equal(t, "old-user", node.GetUser().GetName())
|
||||||
|
|
||||||
nodeID := fmt.Sprintf("%d", node.GetId())
|
nodeID := fmt.Sprintf("%d", node.GetId())
|
||||||
|
|
||||||
|
@ -1511,9 +1518,9 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, node.GetUser().GetName(), "new-user")
|
assert.Equal(t, "new-user", node.GetUser().GetName())
|
||||||
|
|
||||||
var allNodes []v1.Node
|
var allNodes []v1.Node
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
|
@ -1527,13 +1534,13 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&allNodes,
|
&allNodes,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, allNodes, 1)
|
assert.Len(t, allNodes, 1)
|
||||||
|
|
||||||
assert.Equal(t, allNodes[0].GetId(), node.GetId())
|
assert.Equal(t, allNodes[0].GetId(), node.GetId())
|
||||||
assert.Equal(t, allNodes[0].GetUser(), node.GetUser())
|
assert.Equal(t, allNodes[0].GetUser(), node.GetUser())
|
||||||
assert.Equal(t, allNodes[0].GetUser().GetName(), "new-user")
|
assert.Equal(t, "new-user", allNodes[0].GetUser().GetName())
|
||||||
|
|
||||||
_, err = headscale.Execute(
|
_, err = headscale.Execute(
|
||||||
[]string{
|
[]string{
|
||||||
|
@ -1548,12 +1555,12 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(
|
require.ErrorContains(
|
||||||
t,
|
t,
|
||||||
err,
|
err,
|
||||||
"user not found",
|
"user not found",
|
||||||
)
|
)
|
||||||
assert.Equal(t, node.GetUser().GetName(), "new-user")
|
assert.Equal(t, "new-user", node.GetUser().GetName())
|
||||||
|
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
headscale,
|
headscale,
|
||||||
|
@ -1570,9 +1577,9 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, node.GetUser().GetName(), "old-user")
|
assert.Equal(t, "old-user", node.GetUser().GetName())
|
||||||
|
|
||||||
err = executeAndUnmarshal(
|
err = executeAndUnmarshal(
|
||||||
headscale,
|
headscale,
|
||||||
|
@ -1589,9 +1596,9 @@ func TestNodeMoveCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&node,
|
&node,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, node.GetUser().GetName(), "old-user")
|
assert.Equal(t, "old-user", node.GetUser().GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolicyCommand(t *testing.T) {
|
func TestPolicyCommand(t *testing.T) {
|
||||||
|
@ -1599,7 +1606,7 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1614,10 +1621,10 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
"HEADSCALE_POLICY_MODE": "database",
|
"HEADSCALE_POLICY_MODE": "database",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
p := policy.ACLPolicy{
|
p := policy.ACLPolicy{
|
||||||
ACLs: []policy.ACL{
|
ACLs: []policy.ACL{
|
||||||
|
@ -1637,7 +1644,7 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
policyFilePath := "/etc/headscale/policy.json"
|
policyFilePath := "/etc/headscale/policy.json"
|
||||||
|
|
||||||
err = headscale.WriteFile(policyFilePath, pBytes)
|
err = headscale.WriteFile(policyFilePath, pBytes)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// No policy is present at this time.
|
// No policy is present at this time.
|
||||||
// Add a new policy from a file.
|
// Add a new policy from a file.
|
||||||
|
@ -1651,7 +1658,7 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Get the current policy and check
|
// Get the current policy and check
|
||||||
// if it is the same as the one we set.
|
// if it is the same as the one we set.
|
||||||
|
@ -1667,11 +1674,11 @@ func TestPolicyCommand(t *testing.T) {
|
||||||
},
|
},
|
||||||
&output,
|
&output,
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, output.TagOwners, 1)
|
assert.Len(t, output.TagOwners, 1)
|
||||||
assert.Len(t, output.ACLs, 1)
|
assert.Len(t, output.ACLs, 1)
|
||||||
assert.Equal(t, output.TagOwners["tag:exists"], []string{"policy-user"})
|
assert.Equal(t, []string{"policy-user"}, output.TagOwners["tag:exists"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolicyBrokenConfigCommand(t *testing.T) {
|
func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
|
@ -1679,7 +1686,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
scenario, err := NewScenario(dockertestMaxWait())
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
spec := map[string]int{
|
spec := map[string]int{
|
||||||
|
@ -1694,10 +1701,10 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
"HEADSCALE_POLICY_MODE": "database",
|
"HEADSCALE_POLICY_MODE": "database",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
headscale, err := scenario.Headscale()
|
headscale, err := scenario.Headscale()
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
p := policy.ACLPolicy{
|
p := policy.ACLPolicy{
|
||||||
ACLs: []policy.ACL{
|
ACLs: []policy.ACL{
|
||||||
|
@ -1719,7 +1726,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
policyFilePath := "/etc/headscale/policy.json"
|
policyFilePath := "/etc/headscale/policy.json"
|
||||||
|
|
||||||
err = headscale.WriteFile(policyFilePath, pBytes)
|
err = headscale.WriteFile(policyFilePath, pBytes)
|
||||||
assertNoErr(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// No policy is present at this time.
|
// No policy is present at this time.
|
||||||
// Add a new policy from a file.
|
// Add a new policy from a file.
|
||||||
|
@ -1732,7 +1739,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
policyFilePath,
|
policyFilePath,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(t, err, "verifying policy rules: invalid action")
|
require.ErrorContains(t, err, "verifying policy rules: invalid action")
|
||||||
|
|
||||||
// The new policy was invalid, the old one should still be in place, which
|
// The new policy was invalid, the old one should still be in place, which
|
||||||
// is none.
|
// is none.
|
||||||
|
@ -1745,5 +1752,5 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
||||||
"json",
|
"json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.ErrorContains(t, err, "acl policy not found")
|
require.ErrorContains(t, err, "acl policy not found")
|
||||||
}
|
}
|
||||||
|
|
96
integration/derp_verify_endpoint_test.go
Normal file
96
integration/derp_verify_endpoint_test.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
"github.com/juanfont/headscale/integration/dsic"
|
||||||
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDERPVerifyEndpoint(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
|
||||||
|
// Generate random hostname for the headscale instance
|
||||||
|
hash, err := util.GenerateRandomStringDNSSafe(6)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
testName := "derpverify"
|
||||||
|
hostname := fmt.Sprintf("hs-%s-%s", testName, hash)
|
||||||
|
|
||||||
|
headscalePort := 8080
|
||||||
|
|
||||||
|
// Create cert for headscale
|
||||||
|
certHeadscale, keyHeadscale, err := integrationutil.CreateCertificate(hostname)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
|
assertNoErr(t, err)
|
||||||
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|
||||||
|
spec := map[string]int{
|
||||||
|
"user1": len(MustTestVersions),
|
||||||
|
}
|
||||||
|
|
||||||
|
derper, err := scenario.CreateDERPServer("head",
|
||||||
|
dsic.WithCACert(certHeadscale),
|
||||||
|
dsic.WithVerifyClientURL(fmt.Sprintf("https://%s/verify", net.JoinHostPort(hostname, strconv.Itoa(headscalePort)))),
|
||||||
|
)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
|
||||||
|
derpMap := tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
900: {
|
||||||
|
RegionID: 900,
|
||||||
|
RegionCode: "test-derpverify",
|
||||||
|
RegionName: "TestDerpVerify",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{
|
||||||
|
Name: "TestDerpVerify",
|
||||||
|
RegionID: 900,
|
||||||
|
HostName: derper.GetHostname(),
|
||||||
|
STUNPort: derper.GetSTUNPort(),
|
||||||
|
STUNOnly: false,
|
||||||
|
DERPPort: derper.GetDERPPort(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithCACert(derper.GetCert())},
|
||||||
|
hsic.WithHostname(hostname),
|
||||||
|
hsic.WithPort(headscalePort),
|
||||||
|
hsic.WithCustomTLS(certHeadscale, keyHeadscale),
|
||||||
|
hsic.WithHostnameAsServerURL(),
|
||||||
|
hsic.WithDERPConfig(derpMap))
|
||||||
|
assertNoErrHeadscaleEnv(t, err)
|
||||||
|
|
||||||
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
|
assertNoErrListClients(t, err)
|
||||||
|
|
||||||
|
for _, client := range allClients {
|
||||||
|
report, err := client.DebugDERPRegion("test-derpverify")
|
||||||
|
assertNoErr(t, err)
|
||||||
|
successful := false
|
||||||
|
for _, line := range report.Info {
|
||||||
|
if strings.Contains(line, "Successfully established a DERP connection with node") {
|
||||||
|
successful = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !successful {
|
||||||
|
stJSON, err := json.Marshal(report)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
t.Errorf("Client %s could not establish a DERP connection: %s", client.Hostname(), string(stJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
321
integration/dsic/dsic.go
Normal file
321
integration/dsic/dsic.go
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
package dsic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dsicHashLength = 6
|
||||||
|
dockerContextPath = "../."
|
||||||
|
caCertRoot = "/usr/local/share/ca-certificates"
|
||||||
|
DERPerCertRoot = "/usr/local/share/derper-certs"
|
||||||
|
dockerExecuteTimeout = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var errDERPerStatusCodeNotOk = errors.New("DERPer status code not OK")
|
||||||
|
|
||||||
|
// DERPServerInContainer represents DERP Server in Container (DSIC).
|
||||||
|
type DERPServerInContainer struct {
|
||||||
|
version string
|
||||||
|
hostname string
|
||||||
|
|
||||||
|
pool *dockertest.Pool
|
||||||
|
container *dockertest.Resource
|
||||||
|
network *dockertest.Network
|
||||||
|
|
||||||
|
stunPort int
|
||||||
|
derpPort int
|
||||||
|
caCerts [][]byte
|
||||||
|
tlsCert []byte
|
||||||
|
tlsKey []byte
|
||||||
|
withExtraHosts []string
|
||||||
|
withVerifyClientURL string
|
||||||
|
workdir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option represent optional settings that can be given to a
|
||||||
|
// DERPer instance.
|
||||||
|
type Option = func(c *DERPServerInContainer)
|
||||||
|
|
||||||
|
// WithCACert adds it to the trusted surtificate of the Tailscale container.
|
||||||
|
func WithCACert(cert []byte) Option {
|
||||||
|
return func(dsic *DERPServerInContainer) {
|
||||||
|
dsic.caCerts = append(dsic.caCerts, cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOrCreateNetwork sets the Docker container network to use with
|
||||||
|
// the DERPer instance, if the parameter is nil, a new network,
|
||||||
|
// isolating the DERPer, will be created. If a network is
|
||||||
|
// passed, the DERPer instance will join the given network.
|
||||||
|
func WithOrCreateNetwork(network *dockertest.Network) Option {
|
||||||
|
return func(tsic *DERPServerInContainer) {
|
||||||
|
if network != nil {
|
||||||
|
tsic.network = network
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
network, err := dockertestutil.GetFirstOrCreateNetwork(
|
||||||
|
tsic.pool,
|
||||||
|
tsic.hostname+"-network",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create network: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tsic.network = network
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDockerWorkdir allows the docker working directory to be set.
|
||||||
|
func WithDockerWorkdir(dir string) Option {
|
||||||
|
return func(tsic *DERPServerInContainer) {
|
||||||
|
tsic.workdir = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithVerifyClientURL sets the URL to verify the client.
|
||||||
|
func WithVerifyClientURL(url string) Option {
|
||||||
|
return func(tsic *DERPServerInContainer) {
|
||||||
|
tsic.withVerifyClientURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExtraHosts adds extra hosts to the container.
|
||||||
|
func WithExtraHosts(hosts []string) Option {
|
||||||
|
return func(tsic *DERPServerInContainer) {
|
||||||
|
tsic.withExtraHosts = hosts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new TailscaleInContainer instance.
|
||||||
|
func New(
|
||||||
|
pool *dockertest.Pool,
|
||||||
|
version string,
|
||||||
|
network *dockertest.Network,
|
||||||
|
opts ...Option,
|
||||||
|
) (*DERPServerInContainer, error) {
|
||||||
|
hash, err := util.GenerateRandomStringDNSSafe(dsicHashLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := fmt.Sprintf("derp-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
|
||||||
|
tlsCert, tlsKey, err := integrationutil.CreateCertificate(hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create certificates for headscale test: %w", err)
|
||||||
|
}
|
||||||
|
dsic := &DERPServerInContainer{
|
||||||
|
version: version,
|
||||||
|
hostname: hostname,
|
||||||
|
pool: pool,
|
||||||
|
network: network,
|
||||||
|
tlsCert: tlsCert,
|
||||||
|
tlsKey: tlsKey,
|
||||||
|
stunPort: 3478, //nolint
|
||||||
|
derpPort: 443, //nolint
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(dsic)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdArgs strings.Builder
|
||||||
|
fmt.Fprintf(&cmdArgs, "--hostname=%s", hostname)
|
||||||
|
fmt.Fprintf(&cmdArgs, " --certmode=manual")
|
||||||
|
fmt.Fprintf(&cmdArgs, " --certdir=%s", DERPerCertRoot)
|
||||||
|
fmt.Fprintf(&cmdArgs, " --a=:%d", dsic.derpPort)
|
||||||
|
fmt.Fprintf(&cmdArgs, " --stun=true")
|
||||||
|
fmt.Fprintf(&cmdArgs, " --stun-port=%d", dsic.stunPort)
|
||||||
|
if dsic.withVerifyClientURL != "" {
|
||||||
|
fmt.Fprintf(&cmdArgs, " --verify-client-url=%s", dsic.withVerifyClientURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
runOptions := &dockertest.RunOptions{
|
||||||
|
Name: hostname,
|
||||||
|
Networks: []*dockertest.Network{dsic.network},
|
||||||
|
ExtraHosts: dsic.withExtraHosts,
|
||||||
|
// we currently need to give us some time to inject the certificate further down.
|
||||||
|
Entrypoint: []string{"/bin/sh", "-c", "/bin/sleep 3 ; update-ca-certificates ; derper " + cmdArgs.String()},
|
||||||
|
ExposedPorts: []string{
|
||||||
|
"80/tcp",
|
||||||
|
fmt.Sprintf("%d/tcp", dsic.derpPort),
|
||||||
|
fmt.Sprintf("%d/udp", dsic.stunPort),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if dsic.workdir != "" {
|
||||||
|
runOptions.WorkingDir = dsic.workdir
|
||||||
|
}
|
||||||
|
|
||||||
|
// dockertest isnt very good at handling containers that has already
|
||||||
|
// been created, this is an attempt to make sure this container isnt
|
||||||
|
// present.
|
||||||
|
err = pool.RemoveContainerByName(hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var container *dockertest.Resource
|
||||||
|
buildOptions := &dockertest.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile.derper",
|
||||||
|
ContextDir: dockerContextPath,
|
||||||
|
BuildArgs: []docker.BuildArg{},
|
||||||
|
}
|
||||||
|
switch version {
|
||||||
|
case "head":
|
||||||
|
buildOptions.BuildArgs = append(buildOptions.BuildArgs, docker.BuildArg{
|
||||||
|
Name: "VERSION_BRANCH",
|
||||||
|
Value: "main",
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
buildOptions.BuildArgs = append(buildOptions.BuildArgs, docker.BuildArg{
|
||||||
|
Name: "VERSION_BRANCH",
|
||||||
|
Value: "v" + version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
container, err = pool.BuildAndRunWithBuildOptions(
|
||||||
|
buildOptions,
|
||||||
|
runOptions,
|
||||||
|
dockertestutil.DockerRestartPolicy,
|
||||||
|
dockertestutil.DockerAllowLocalIPv6,
|
||||||
|
dockertestutil.DockerAllowNetworkAdministration,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%s could not start tailscale DERPer container (version: %s): %w",
|
||||||
|
hostname,
|
||||||
|
version,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
|
dsic.container = container
|
||||||
|
|
||||||
|
for i, cert := range dsic.caCerts {
|
||||||
|
err = dsic.WriteFile(fmt.Sprintf("%s/user-%d.crt", caCertRoot, i), cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(dsic.tlsCert) != 0 {
|
||||||
|
err = dsic.WriteFile(fmt.Sprintf("%s/%s.crt", DERPerCertRoot, dsic.hostname), dsic.tlsCert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(dsic.tlsKey) != 0 {
|
||||||
|
err = dsic.WriteFile(fmt.Sprintf("%s/%s.key", DERPerCertRoot, dsic.hostname), dsic.tlsKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS key to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown stops and cleans up the DERPer container.
|
||||||
|
func (t *DERPServerInContainer) Shutdown() error {
|
||||||
|
err := t.SaveLog("/tmp/control")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"Failed to save log from %s: %s",
|
||||||
|
t.hostname,
|
||||||
|
fmt.Errorf("failed to save log: %w", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.pool.Purge(t.container)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCert returns the TLS certificate of the DERPer instance.
|
||||||
|
func (t *DERPServerInContainer) GetCert() []byte {
|
||||||
|
return t.tlsCert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname returns the hostname of the DERPer instance.
|
||||||
|
func (t *DERPServerInContainer) Hostname() string {
|
||||||
|
return t.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the running DERPer version of the instance.
|
||||||
|
func (t *DERPServerInContainer) Version() string {
|
||||||
|
return t.version
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the Docker container ID of the DERPServerInContainer
|
||||||
|
// instance.
|
||||||
|
func (t *DERPServerInContainer) ID() string {
|
||||||
|
return t.container.Container.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DERPServerInContainer) GetHostname() string {
|
||||||
|
return t.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSTUNPort returns the STUN port of the DERPer instance.
|
||||||
|
func (t *DERPServerInContainer) GetSTUNPort() int {
|
||||||
|
return t.stunPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDERPPort returns the DERP port of the DERPer instance.
|
||||||
|
func (t *DERPServerInContainer) GetDERPPort() int {
|
||||||
|
return t.derpPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForRunning blocks until the DERPer instance is ready to be used.
|
||||||
|
func (t *DERPServerInContainer) WaitForRunning() error {
|
||||||
|
url := "https://" + net.JoinHostPort(t.GetHostname(), strconv.Itoa(t.GetDERPPort())) + "/"
|
||||||
|
log.Printf("waiting for DERPer to be ready at %s", url)
|
||||||
|
|
||||||
|
insecureTransport := http.DefaultTransport.(*http.Transport).Clone() //nolint
|
||||||
|
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint
|
||||||
|
client := &http.Client{Transport: insecureTransport}
|
||||||
|
|
||||||
|
return t.pool.Retry(func() error {
|
||||||
|
resp, err := client.Get(url) //nolint
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("headscale is not ready: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errDERPerStatusCodeNotOk
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectToNetwork connects the DERPer instance to a network.
|
||||||
|
func (t *DERPServerInContainer) ConnectToNetwork(network *dockertest.Network) error {
|
||||||
|
return t.container.ConnectToNetwork(network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile save file inside the container.
|
||||||
|
func (t *DERPServerInContainer) WriteFile(path string, data []byte) error {
|
||||||
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveLog saves the current stdout log of the container to a path
|
||||||
|
// on the host system.
|
||||||
|
func (t *DERPServerInContainer) SaveLog(path string) error {
|
||||||
|
_, _, err := dockertestutil.SaveLog(t.pool, t.container, path)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -55,7 +55,7 @@ func TestDERPServerWebsocketScenario(t *testing.T) {
|
||||||
spec := map[string]ClientsSpec{
|
spec := map[string]ClientsSpec{
|
||||||
"user1": {
|
"user1": {
|
||||||
Plain: 0,
|
Plain: 0,
|
||||||
WebsocketDERP: len(MustTestVersions),
|
WebsocketDERP: 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,10 +239,13 @@ func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv(
|
||||||
|
|
||||||
if clientCount.WebsocketDERP > 0 {
|
if clientCount.WebsocketDERP > 0 {
|
||||||
// Containers that use DERP-over-WebSocket
|
// Containers that use DERP-over-WebSocket
|
||||||
|
// Note that these clients *must* be built
|
||||||
|
// from source, which is currently
|
||||||
|
// only done for HEAD.
|
||||||
err = s.CreateTailscaleIsolatedNodesInUser(
|
err = s.CreateTailscaleIsolatedNodesInUser(
|
||||||
hash,
|
hash,
|
||||||
userName,
|
userName,
|
||||||
"all",
|
tsic.VersionHead,
|
||||||
clientCount.WebsocketDERP,
|
clientCount.WebsocketDERP,
|
||||||
tsic.WithWebsocketDERP(true),
|
tsic.WithWebsocketDERP(true),
|
||||||
)
|
)
|
||||||
|
@ -307,7 +310,7 @@ func (s *EmbeddedDERPServerScenario) CreateTailscaleIsolatedNodesInUser(
|
||||||
cert := hsServer.GetCert()
|
cert := hsServer.GetCert()
|
||||||
|
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
tsic.WithHeadscaleTLS(cert),
|
tsic.WithCACert(cert),
|
||||||
)
|
)
|
||||||
|
|
||||||
user.createWaitGroup.Go(func() error {
|
user.createWaitGroup.Go(func() error {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
|
@ -244,7 +245,11 @@ func TestEphemeral(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEphemeralInAlternateTimezone(t *testing.T) {
|
func TestEphemeralInAlternateTimezone(t *testing.T) {
|
||||||
testEphemeralWithOptions(t, hsic.WithTestName("ephemeral-tz"), hsic.WithTimezone("America/Los_Angeles"))
|
testEphemeralWithOptions(
|
||||||
|
t,
|
||||||
|
hsic.WithTestName("ephemeral-tz"),
|
||||||
|
hsic.WithTimezone("America/Los_Angeles"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
|
func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
|
||||||
|
@ -1164,10 +1169,10 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||||
},
|
},
|
||||||
&nodeList,
|
&nodeList,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, nodeList, 2)
|
assert.Len(t, nodeList, 2)
|
||||||
assert.True(t, nodeList[0].Online)
|
assert.True(t, nodeList[0].GetOnline())
|
||||||
assert.True(t, nodeList[1].Online)
|
assert.True(t, nodeList[1].GetOnline())
|
||||||
|
|
||||||
// Delete the first node, which is online
|
// Delete the first node, which is online
|
||||||
_, err = headscale.Execute(
|
_, err = headscale.Execute(
|
||||||
|
@ -1177,13 +1182,13 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||||
"delete",
|
"delete",
|
||||||
"--identifier",
|
"--identifier",
|
||||||
// Delete the last added machine
|
// Delete the last added machine
|
||||||
fmt.Sprintf("%d", nodeList[0].Id),
|
fmt.Sprintf("%d", nodeList[0].GetId()),
|
||||||
"--output",
|
"--output",
|
||||||
"json",
|
"json",
|
||||||
"--force",
|
"--force",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
@ -1200,9 +1205,8 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||||
},
|
},
|
||||||
&nodeListAfter,
|
&nodeListAfter,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, nodeListAfter, 1)
|
assert.Len(t, nodeListAfter, 1)
|
||||||
assert.True(t, nodeListAfter[0].Online)
|
assert.True(t, nodeListAfter[0].GetOnline())
|
||||||
assert.Equal(t, nodeList[1].Id, nodeListAfter[0].Id)
|
assert.Equal(t, nodeList[1].GetId(), nodeListAfter[0].GetId())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
package hsic
|
package hsic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -32,11 +25,14 @@ import (
|
||||||
"github.com/juanfont/headscale/integration/integrationutil"
|
"github.com/juanfont/headscale/integration/integrationutil"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/ory/dockertest/v3/docker"
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hsicHashLength = 6
|
hsicHashLength = 6
|
||||||
dockerContextPath = "../."
|
dockerContextPath = "../."
|
||||||
|
caCertRoot = "/usr/local/share/ca-certificates"
|
||||||
aclPolicyPath = "/etc/headscale/acl.hujson"
|
aclPolicyPath = "/etc/headscale/acl.hujson"
|
||||||
tlsCertPath = "/etc/headscale/tls.cert"
|
tlsCertPath = "/etc/headscale/tls.cert"
|
||||||
tlsKeyPath = "/etc/headscale/tls.key"
|
tlsKeyPath = "/etc/headscale/tls.key"
|
||||||
|
@ -64,6 +60,7 @@ type HeadscaleInContainer struct {
|
||||||
// optional config
|
// optional config
|
||||||
port int
|
port int
|
||||||
extraPorts []string
|
extraPorts []string
|
||||||
|
caCerts [][]byte
|
||||||
hostPortBindings map[string][]string
|
hostPortBindings map[string][]string
|
||||||
aclPolicy *policy.ACLPolicy
|
aclPolicy *policy.ACLPolicy
|
||||||
env map[string]string
|
env map[string]string
|
||||||
|
@ -88,18 +85,29 @@ func WithACLPolicy(acl *policy.ACLPolicy) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithCACert adds it to the trusted surtificate of the container.
|
||||||
|
func WithCACert(cert []byte) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.caCerts = append(hsic.caCerts, cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithTLS creates certificates and enables HTTPS.
|
// WithTLS creates certificates and enables HTTPS.
|
||||||
func WithTLS() Option {
|
func WithTLS() Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
cert, key, err := createCertificate(hsic.hostname)
|
cert, key, err := integrationutil.CreateCertificate(hsic.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
log.Fatalf("failed to create certificates for headscale test: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby): Move somewhere appropriate
|
hsic.tlsCert = cert
|
||||||
hsic.env["HEADSCALE_TLS_CERT_PATH"] = tlsCertPath
|
hsic.tlsKey = key
|
||||||
hsic.env["HEADSCALE_TLS_KEY_PATH"] = tlsKeyPath
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCustomTLS uses the given certificates for the Headscale instance.
|
||||||
|
func WithCustomTLS(cert, key []byte) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.tlsCert = cert
|
hsic.tlsCert = cert
|
||||||
hsic.tlsKey = key
|
hsic.tlsKey = key
|
||||||
}
|
}
|
||||||
|
@ -146,6 +154,13 @@ func WithTestName(testName string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHostname sets the hostname of the Headscale instance.
|
||||||
|
func WithHostname(hostname string) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
hsic.hostname = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithHostnameAsServerURL sets the Headscale ServerURL based on
|
// WithHostnameAsServerURL sets the Headscale ServerURL based on
|
||||||
// the Hostname.
|
// the Hostname.
|
||||||
func WithHostnameAsServerURL() Option {
|
func WithHostnameAsServerURL() Option {
|
||||||
|
@ -203,6 +218,34 @@ func WithEmbeddedDERPServerOnly() Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDERPConfig configures Headscale use a custom
|
||||||
|
// DERP server only.
|
||||||
|
func WithDERPConfig(derpMap tailcfg.DERPMap) Option {
|
||||||
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
contents, err := yaml.Marshal(derpMap)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to marshal DERP map: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hsic.env["HEADSCALE_DERP_PATHS"] = "/etc/headscale/derp.yml"
|
||||||
|
hsic.filesInContainer = append(hsic.filesInContainer,
|
||||||
|
fileInContainer{
|
||||||
|
path: "/etc/headscale/derp.yml",
|
||||||
|
contents: contents,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disable global DERP server and embedded DERP server
|
||||||
|
hsic.env["HEADSCALE_DERP_URLS"] = ""
|
||||||
|
hsic.env["HEADSCALE_DERP_SERVER_ENABLED"] = "false"
|
||||||
|
|
||||||
|
// Envknob for enabling DERP debug logs
|
||||||
|
hsic.env["DERP_DEBUG_LOGS"] = "true"
|
||||||
|
hsic.env["DERP_PROBER_DEBUG_LOGS"] = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithTuning allows changing the tuning settings easily.
|
// WithTuning allows changing the tuning settings easily.
|
||||||
func WithTuning(batchTimeout time.Duration, mapSessionChanSize int) Option {
|
func WithTuning(batchTimeout time.Duration, mapSessionChanSize int) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
|
@ -300,6 +343,10 @@ func New(
|
||||||
"HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS=1",
|
"HEADSCALE_DEBUG_HIGH_CARDINALITY_METRICS=1",
|
||||||
"HEADSCALE_DEBUG_DUMP_CONFIG=1",
|
"HEADSCALE_DEBUG_DUMP_CONFIG=1",
|
||||||
}
|
}
|
||||||
|
if hsic.hasTLS() {
|
||||||
|
hsic.env["HEADSCALE_TLS_CERT_PATH"] = tlsCertPath
|
||||||
|
hsic.env["HEADSCALE_TLS_KEY_PATH"] = tlsKeyPath
|
||||||
|
}
|
||||||
for key, value := range hsic.env {
|
for key, value := range hsic.env {
|
||||||
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
||||||
}
|
}
|
||||||
|
@ -313,7 +360,7 @@ func New(
|
||||||
// Cmd: []string{"headscale", "serve"},
|
// Cmd: []string{"headscale", "serve"},
|
||||||
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
// TODO(kradalby): Get rid of this hack, we currently need to give us some
|
||||||
// to inject the headscale configuration further down.
|
// to inject the headscale configuration further down.
|
||||||
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve ; /bin/sleep 30"},
|
Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; update-ca-certificates ; headscale serve ; /bin/sleep 30"},
|
||||||
Env: env,
|
Env: env,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,6 +398,14 @@ func New(
|
||||||
|
|
||||||
hsic.container = container
|
hsic.container = container
|
||||||
|
|
||||||
|
// Write the CA certificates to the container
|
||||||
|
for i, cert := range hsic.caCerts {
|
||||||
|
err = hsic.WriteFile(fmt.Sprintf("%s/user-%d.crt", caCertRoot, i), cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(MinimumConfigYAML()))
|
err = hsic.WriteFile("/etc/headscale/config.yaml", []byte(MinimumConfigYAML()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
|
return nil, fmt.Errorf("failed to write headscale config to container: %w", err)
|
||||||
|
@ -749,86 +804,3 @@ func (t *HeadscaleInContainer) SendInterrupt() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint
|
|
||||||
func createCertificate(hostname string) ([]byte, []byte, error) {
|
|
||||||
// From:
|
|
||||||
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
|
||||||
|
|
||||||
ca := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(2019),
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{"Headscale testing INC"},
|
|
||||||
Country: []string{"NL"},
|
|
||||||
Locality: []string{"Leiden"},
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(60 * time.Hour),
|
|
||||||
IsCA: true,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
||||||
x509.ExtKeyUsageClientAuth,
|
|
||||||
x509.ExtKeyUsageServerAuth,
|
|
||||||
},
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cert := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1658),
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: hostname,
|
|
||||||
Organization: []string{"Headscale testing INC"},
|
|
||||||
Country: []string{"NL"},
|
|
||||||
Locality: []string{"Leiden"},
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(60 * time.Minute),
|
|
||||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
||||||
DNSNames: []string{hostname},
|
|
||||||
}
|
|
||||||
|
|
||||||
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
certBytes, err := x509.CreateCertificate(
|
|
||||||
rand.Reader,
|
|
||||||
cert,
|
|
||||||
ca,
|
|
||||||
&certPrivKey.PublicKey,
|
|
||||||
caPrivKey,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
certPEM := new(bytes.Buffer)
|
|
||||||
|
|
||||||
err = pem.Encode(certPEM, &pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: certBytes,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
certPrivKeyPEM := new(bytes.Buffer)
|
|
||||||
|
|
||||||
err = pem.Encode(certPrivKeyPEM, &pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return certPEM.Bytes(), certPrivKeyPEM.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,9 +3,16 @@ package integrationutil
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
|
@ -93,3 +100,86 @@ func FetchPathFromContainer(
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
func CreateCertificate(hostname string) ([]byte, []byte, error) {
|
||||||
|
// From:
|
||||||
|
// https://shaneutt.com/blog/golang-ca-and-signed-cert-go/
|
||||||
|
|
||||||
|
ca := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(2019),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"Headscale testing INC"},
|
||||||
|
Country: []string{"NL"},
|
||||||
|
Locality: []string{"Leiden"},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(60 * time.Hour),
|
||||||
|
IsCA: true,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||||
|
x509.ExtKeyUsageClientAuth,
|
||||||
|
x509.ExtKeyUsageServerAuth,
|
||||||
|
},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1658),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: hostname,
|
||||||
|
Organization: []string{"Headscale testing INC"},
|
||||||
|
Country: []string{"NL"},
|
||||||
|
Locality: []string{"Leiden"},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(60 * time.Minute),
|
||||||
|
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
DNSNames: []string{hostname},
|
||||||
|
}
|
||||||
|
|
||||||
|
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes, err := x509.CreateCertificate(
|
||||||
|
rand.Reader,
|
||||||
|
cert,
|
||||||
|
ca,
|
||||||
|
&certPrivKey.PublicKey,
|
||||||
|
caPrivKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certPEM := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err = pem.Encode(certPEM, &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certBytes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certPrivKeyPEM := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err = pem.Encode(certPrivKeyPEM, &pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certPEM.Bytes(), certPrivKeyPEM.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
|
@ -92,9 +92,9 @@ func TestEnablingRoutes(t *testing.T) {
|
||||||
assert.Len(t, routes, 3)
|
assert.Len(t, routes, 3)
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
assert.Equal(t, false, route.GetEnabled())
|
assert.False(t, route.GetEnabled())
|
||||||
assert.Equal(t, false, route.GetIsPrimary())
|
assert.False(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that no routes has been sent to the client,
|
// Verify that no routes has been sent to the client,
|
||||||
|
@ -139,9 +139,9 @@ func TestEnablingRoutes(t *testing.T) {
|
||||||
assert.Len(t, enablingRoutes, 3)
|
assert.Len(t, enablingRoutes, 3)
|
||||||
|
|
||||||
for _, route := range enablingRoutes {
|
for _, route := range enablingRoutes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
assert.Equal(t, true, route.GetEnabled())
|
assert.True(t, route.GetEnabled())
|
||||||
assert.Equal(t, true, route.GetIsPrimary())
|
assert.True(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
@ -212,18 +212,18 @@ func TestEnablingRoutes(t *testing.T) {
|
||||||
assertNoErr(t, err)
|
assertNoErr(t, err)
|
||||||
|
|
||||||
for _, route := range disablingRoutes {
|
for _, route := range disablingRoutes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
|
|
||||||
if route.GetId() == routeToBeDisabled.GetId() {
|
if route.GetId() == routeToBeDisabled.GetId() {
|
||||||
assert.Equal(t, false, route.GetEnabled())
|
assert.False(t, route.GetEnabled())
|
||||||
|
|
||||||
// since this is the only route of this cidr,
|
// since this is the only route of this cidr,
|
||||||
// it will not failover, and remain Primary
|
// it will not failover, and remain Primary
|
||||||
// until something can replace it.
|
// until something can replace it.
|
||||||
assert.Equal(t, true, route.GetIsPrimary())
|
assert.True(t, route.GetIsPrimary())
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, true, route.GetEnabled())
|
assert.True(t, route.GetEnabled())
|
||||||
assert.Equal(t, true, route.GetIsPrimary())
|
assert.True(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,9 +342,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
t.Logf("initial routes %#v", routes)
|
t.Logf("initial routes %#v", routes)
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
assert.Equal(t, false, route.GetEnabled())
|
assert.False(t, route.GetEnabled())
|
||||||
assert.Equal(t, false, route.GetIsPrimary())
|
assert.False(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that no routes has been sent to the client,
|
// Verify that no routes has been sent to the client,
|
||||||
|
@ -391,14 +391,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, enablingRoutes, 2)
|
assert.Len(t, enablingRoutes, 2)
|
||||||
|
|
||||||
// Node 1 is primary
|
// Node 1 is primary
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetAdvertised())
|
assert.True(t, enablingRoutes[0].GetAdvertised())
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetEnabled())
|
assert.True(t, enablingRoutes[0].GetEnabled())
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary")
|
assert.True(t, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary")
|
||||||
|
|
||||||
// Node 2 is not primary
|
// Node 2 is not primary
|
||||||
assert.Equal(t, true, enablingRoutes[1].GetAdvertised())
|
assert.True(t, enablingRoutes[1].GetAdvertised())
|
||||||
assert.Equal(t, true, enablingRoutes[1].GetEnabled())
|
assert.True(t, enablingRoutes[1].GetEnabled())
|
||||||
assert.Equal(t, false, enablingRoutes[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be non-primary")
|
assert.False(t, enablingRoutes[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be non-primary")
|
||||||
|
|
||||||
// Verify that the client has routes from the primary machine
|
// Verify that the client has routes from the primary machine
|
||||||
srs1, err := subRouter1.Status()
|
srs1, err := subRouter1.Status()
|
||||||
|
@ -446,14 +446,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfterMove, 2)
|
assert.Len(t, routesAfterMove, 2)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfterMove[0].GetAdvertised())
|
assert.True(t, routesAfterMove[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterMove[0].GetEnabled())
|
assert.True(t, routesAfterMove[0].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfterMove[0].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
assert.False(t, routesAfterMove[0].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
assert.Equal(t, true, routesAfterMove[1].GetAdvertised())
|
assert.True(t, routesAfterMove[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterMove[1].GetEnabled())
|
assert.True(t, routesAfterMove[1].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterMove[1].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
assert.True(t, routesAfterMove[1].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
||||||
|
|
||||||
srs2, err = subRouter2.Status()
|
srs2, err = subRouter2.Status()
|
||||||
|
|
||||||
|
@ -501,16 +501,16 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfterBothDown, 2)
|
assert.Len(t, routesAfterBothDown, 2)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfterBothDown[0].GetAdvertised())
|
assert.True(t, routesAfterBothDown[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterBothDown[0].GetEnabled())
|
assert.True(t, routesAfterBothDown[0].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfterBothDown[0].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
|
assert.False(t, routesAfterBothDown[0].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
// if the node goes down, but no other suitable route is
|
// if the node goes down, but no other suitable route is
|
||||||
// available, keep the last known good route.
|
// available, keep the last known good route.
|
||||||
assert.Equal(t, true, routesAfterBothDown[1].GetAdvertised())
|
assert.True(t, routesAfterBothDown[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterBothDown[1].GetEnabled())
|
assert.True(t, routesAfterBothDown[1].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterBothDown[1].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
|
assert.True(t, routesAfterBothDown[1].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
|
||||||
|
|
||||||
// TODO(kradalby): Check client status
|
// TODO(kradalby): Check client status
|
||||||
// Both are expected to be down
|
// Both are expected to be down
|
||||||
|
@ -560,14 +560,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfter1Up, 2)
|
assert.Len(t, routesAfter1Up, 2)
|
||||||
|
|
||||||
// Node 1 is primary
|
// Node 1 is primary
|
||||||
assert.Equal(t, true, routesAfter1Up[0].GetAdvertised())
|
assert.True(t, routesAfter1Up[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfter1Up[0].GetEnabled())
|
assert.True(t, routesAfter1Up[0].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfter1Up[0].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
assert.True(t, routesAfter1Up[0].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
||||||
|
|
||||||
// Node 2 is not primary
|
// Node 2 is not primary
|
||||||
assert.Equal(t, true, routesAfter1Up[1].GetAdvertised())
|
assert.True(t, routesAfter1Up[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfter1Up[1].GetEnabled())
|
assert.True(t, routesAfter1Up[1].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfter1Up[1].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
assert.False(t, routesAfter1Up[1].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -614,14 +614,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfter2Up, 2)
|
assert.Len(t, routesAfter2Up, 2)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfter2Up[0].GetAdvertised())
|
assert.True(t, routesAfter2Up[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfter2Up[0].GetEnabled())
|
assert.True(t, routesAfter2Up[0].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfter2Up[0].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
assert.True(t, routesAfter2Up[0].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
assert.Equal(t, true, routesAfter2Up[1].GetAdvertised())
|
assert.True(t, routesAfter2Up[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfter2Up[1].GetEnabled())
|
assert.True(t, routesAfter2Up[1].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfter2Up[1].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
assert.False(t, routesAfter2Up[1].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -677,14 +677,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
t.Logf("routes after disabling r1 %#v", routesAfterDisabling1)
|
t.Logf("routes after disabling r1 %#v", routesAfterDisabling1)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfterDisabling1[0].GetAdvertised())
|
assert.True(t, routesAfterDisabling1[0].GetAdvertised())
|
||||||
assert.Equal(t, false, routesAfterDisabling1[0].GetEnabled())
|
assert.False(t, routesAfterDisabling1[0].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfterDisabling1[0].GetIsPrimary())
|
assert.False(t, routesAfterDisabling1[0].GetIsPrimary())
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
assert.Equal(t, true, routesAfterDisabling1[1].GetAdvertised())
|
assert.True(t, routesAfterDisabling1[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterDisabling1[1].GetEnabled())
|
assert.True(t, routesAfterDisabling1[1].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterDisabling1[1].GetIsPrimary())
|
assert.True(t, routesAfterDisabling1[1].GetIsPrimary())
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -735,14 +735,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
assert.Len(t, routesAfterEnabling1, 2)
|
assert.Len(t, routesAfterEnabling1, 2)
|
||||||
|
|
||||||
// Node 1 is not primary
|
// Node 1 is not primary
|
||||||
assert.Equal(t, true, routesAfterEnabling1[0].GetAdvertised())
|
assert.True(t, routesAfterEnabling1[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterEnabling1[0].GetEnabled())
|
assert.True(t, routesAfterEnabling1[0].GetEnabled())
|
||||||
assert.Equal(t, false, routesAfterEnabling1[0].GetIsPrimary())
|
assert.False(t, routesAfterEnabling1[0].GetIsPrimary())
|
||||||
|
|
||||||
// Node 2 is primary
|
// Node 2 is primary
|
||||||
assert.Equal(t, true, routesAfterEnabling1[1].GetAdvertised())
|
assert.True(t, routesAfterEnabling1[1].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterEnabling1[1].GetEnabled())
|
assert.True(t, routesAfterEnabling1[1].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterEnabling1[1].GetIsPrimary())
|
assert.True(t, routesAfterEnabling1[1].GetIsPrimary())
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -795,9 +795,9 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||||
t.Logf("routes after deleting r2 %#v", routesAfterDeleting2)
|
t.Logf("routes after deleting r2 %#v", routesAfterDeleting2)
|
||||||
|
|
||||||
// Node 1 is primary
|
// Node 1 is primary
|
||||||
assert.Equal(t, true, routesAfterDeleting2[0].GetAdvertised())
|
assert.True(t, routesAfterDeleting2[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routesAfterDeleting2[0].GetEnabled())
|
assert.True(t, routesAfterDeleting2[0].GetEnabled())
|
||||||
assert.Equal(t, true, routesAfterDeleting2[0].GetIsPrimary())
|
assert.True(t, routesAfterDeleting2[0].GetIsPrimary())
|
||||||
|
|
||||||
// Verify that the route is announced from subnet router 1
|
// Verify that the route is announced from subnet router 1
|
||||||
clientStatus, err = client.Status()
|
clientStatus, err = client.Status()
|
||||||
|
@ -893,9 +893,9 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) {
|
||||||
assert.Len(t, routes, 1)
|
assert.Len(t, routes, 1)
|
||||||
|
|
||||||
// All routes should be auto approved and enabled
|
// All routes should be auto approved and enabled
|
||||||
assert.Equal(t, true, routes[0].GetAdvertised())
|
assert.True(t, routes[0].GetAdvertised())
|
||||||
assert.Equal(t, true, routes[0].GetEnabled())
|
assert.True(t, routes[0].GetEnabled())
|
||||||
assert.Equal(t, true, routes[0].GetIsPrimary())
|
assert.True(t, routes[0].GetIsPrimary())
|
||||||
|
|
||||||
// Stop advertising route
|
// Stop advertising route
|
||||||
command = []string{
|
command = []string{
|
||||||
|
@ -924,9 +924,9 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) {
|
||||||
assert.Len(t, notAdvertisedRoutes, 1)
|
assert.Len(t, notAdvertisedRoutes, 1)
|
||||||
|
|
||||||
// Route is no longer advertised
|
// Route is no longer advertised
|
||||||
assert.Equal(t, false, notAdvertisedRoutes[0].GetAdvertised())
|
assert.False(t, notAdvertisedRoutes[0].GetAdvertised())
|
||||||
assert.Equal(t, false, notAdvertisedRoutes[0].GetEnabled())
|
assert.False(t, notAdvertisedRoutes[0].GetEnabled())
|
||||||
assert.Equal(t, true, notAdvertisedRoutes[0].GetIsPrimary())
|
assert.True(t, notAdvertisedRoutes[0].GetIsPrimary())
|
||||||
|
|
||||||
// Advertise route again
|
// Advertise route again
|
||||||
command = []string{
|
command = []string{
|
||||||
|
@ -955,9 +955,9 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) {
|
||||||
assert.Len(t, reAdvertisedRoutes, 1)
|
assert.Len(t, reAdvertisedRoutes, 1)
|
||||||
|
|
||||||
// All routes should be auto approved and enabled
|
// All routes should be auto approved and enabled
|
||||||
assert.Equal(t, true, reAdvertisedRoutes[0].GetAdvertised())
|
assert.True(t, reAdvertisedRoutes[0].GetAdvertised())
|
||||||
assert.Equal(t, true, reAdvertisedRoutes[0].GetEnabled())
|
assert.True(t, reAdvertisedRoutes[0].GetEnabled())
|
||||||
assert.Equal(t, true, reAdvertisedRoutes[0].GetIsPrimary())
|
assert.True(t, reAdvertisedRoutes[0].GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoApprovedSubRoute2068(t *testing.T) {
|
func TestAutoApprovedSubRoute2068(t *testing.T) {
|
||||||
|
@ -1163,9 +1163,9 @@ func TestSubnetRouteACL(t *testing.T) {
|
||||||
assert.Len(t, routes, 1)
|
assert.Len(t, routes, 1)
|
||||||
|
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
assert.Equal(t, true, route.GetAdvertised())
|
assert.True(t, route.GetAdvertised())
|
||||||
assert.Equal(t, false, route.GetEnabled())
|
assert.False(t, route.GetEnabled())
|
||||||
assert.Equal(t, false, route.GetIsPrimary())
|
assert.False(t, route.GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that no routes has been sent to the client,
|
// Verify that no routes has been sent to the client,
|
||||||
|
@ -1212,9 +1212,9 @@ func TestSubnetRouteACL(t *testing.T) {
|
||||||
assert.Len(t, enablingRoutes, 1)
|
assert.Len(t, enablingRoutes, 1)
|
||||||
|
|
||||||
// Node 1 has active route
|
// Node 1 has active route
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetAdvertised())
|
assert.True(t, enablingRoutes[0].GetAdvertised())
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetEnabled())
|
assert.True(t, enablingRoutes[0].GetEnabled())
|
||||||
assert.Equal(t, true, enablingRoutes[0].GetIsPrimary())
|
assert.True(t, enablingRoutes[0].GetIsPrimary())
|
||||||
|
|
||||||
// Verify that the client has routes from the primary machine
|
// Verify that the client has routes from the primary machine
|
||||||
srs1, _ := subRouter1.Status()
|
srs1, _ := subRouter1.Status()
|
||||||
|
|
|
@ -14,12 +14,14 @@ import (
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||||
|
"github.com/juanfont/headscale/integration/dsic"
|
||||||
"github.com/juanfont/headscale/integration/hsic"
|
"github.com/juanfont/headscale/integration/hsic"
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
"github.com/juanfont/headscale/integration/tsic"
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
)
|
)
|
||||||
|
@ -140,6 +142,7 @@ type Scenario struct {
|
||||||
// TODO(kradalby): support multiple headcales for later, currently only
|
// TODO(kradalby): support multiple headcales for later, currently only
|
||||||
// use one.
|
// use one.
|
||||||
controlServers *xsync.MapOf[string, ControlServer]
|
controlServers *xsync.MapOf[string, ControlServer]
|
||||||
|
derpServers []*dsic.DERPServerInContainer
|
||||||
|
|
||||||
users map[string]*User
|
users map[string]*User
|
||||||
|
|
||||||
|
@ -203,11 +206,11 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
|
||||||
|
|
||||||
if t != nil {
|
if t != nil {
|
||||||
stdout, err := os.ReadFile(stdoutPath)
|
stdout, err := os.ReadFile(stdoutPath)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotContains(t, string(stdout), "panic")
|
assert.NotContains(t, string(stdout), "panic")
|
||||||
|
|
||||||
stderr, err := os.ReadFile(stderrPath)
|
stderr, err := os.ReadFile(stderrPath)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotContains(t, string(stderr), "panic")
|
assert.NotContains(t, string(stderr), "panic")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,6 +227,13 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, derp := range s.derpServers {
|
||||||
|
err := derp.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to tear down derp server: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.pool.RemoveNetwork(s.network); err != nil {
|
if err := s.pool.RemoveNetwork(s.network); err != nil {
|
||||||
log.Printf("failed to remove network: %s", err)
|
log.Printf("failed to remove network: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -352,7 +362,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
||||||
hostname := headscale.GetHostname()
|
hostname := headscale.GetHostname()
|
||||||
|
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
tsic.WithHeadscaleTLS(cert),
|
tsic.WithCACert(cert),
|
||||||
tsic.WithHeadscaleName(hostname),
|
tsic.WithHeadscaleName(hostname),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -651,3 +661,20 @@ func (s *Scenario) WaitForTailscaleLogout() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateDERPServer creates a new DERP server in a container.
|
||||||
|
func (s *Scenario) CreateDERPServer(version string, opts ...dsic.Option) (*dsic.DERPServerInContainer, error) {
|
||||||
|
derp, err := dsic.New(s.pool, version, s.network, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create DERP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = derp.WaitForRunning()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to reach DERP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.derpServers = append(s.derpServers, derp)
|
||||||
|
|
||||||
|
return derp, nil
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ type TailscaleClient interface {
|
||||||
FQDN() (string, error)
|
FQDN() (string, error)
|
||||||
Status(...bool) (*ipnstate.Status, error)
|
Status(...bool) (*ipnstate.Status, error)
|
||||||
Netmap() (*netmap.NetworkMap, error)
|
Netmap() (*netmap.NetworkMap, error)
|
||||||
|
DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error)
|
||||||
Netcheck() (*netcheck.Report, error)
|
Netcheck() (*netcheck.Report, error)
|
||||||
WaitForNeedsLogin() error
|
WaitForNeedsLogin() error
|
||||||
WaitForRunning() error
|
WaitForRunning() error
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -32,7 +33,7 @@ const (
|
||||||
defaultPingTimeout = 300 * time.Millisecond
|
defaultPingTimeout = 300 * time.Millisecond
|
||||||
defaultPingCount = 10
|
defaultPingCount = 10
|
||||||
dockerContextPath = "../."
|
dockerContextPath = "../."
|
||||||
headscaleCertPath = "/usr/local/share/ca-certificates/headscale.crt"
|
caCertRoot = "/usr/local/share/ca-certificates"
|
||||||
dockerExecuteTimeout = 60 * time.Second
|
dockerExecuteTimeout = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,6 +45,11 @@ var (
|
||||||
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
|
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
|
||||||
errTailscaleNotConnected = errors.New("tailscale not connected")
|
errTailscaleNotConnected = errors.New("tailscale not connected")
|
||||||
errTailscaledNotReadyForLogin = errors.New("tailscaled not ready for login")
|
errTailscaledNotReadyForLogin = errors.New("tailscaled not ready for login")
|
||||||
|
errInvalidClientConfig = errors.New("verifiably invalid client config requested")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
VersionHead = "head"
|
||||||
)
|
)
|
||||||
|
|
||||||
func errTailscaleStatus(hostname string, err error) error {
|
func errTailscaleStatus(hostname string, err error) error {
|
||||||
|
@ -65,7 +71,7 @@ type TailscaleInContainer struct {
|
||||||
fqdn string
|
fqdn string
|
||||||
|
|
||||||
// optional config
|
// optional config
|
||||||
headscaleCert []byte
|
caCerts [][]byte
|
||||||
headscaleHostname string
|
headscaleHostname string
|
||||||
withWebsocketDERP bool
|
withWebsocketDERP bool
|
||||||
withSSH bool
|
withSSH bool
|
||||||
|
@ -74,17 +80,23 @@ type TailscaleInContainer struct {
|
||||||
withExtraHosts []string
|
withExtraHosts []string
|
||||||
workdir string
|
workdir string
|
||||||
netfilter string
|
netfilter string
|
||||||
|
|
||||||
|
// build options, solely for HEAD
|
||||||
|
buildConfig TailscaleInContainerBuildConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type TailscaleInContainerBuildConfig struct {
|
||||||
|
tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option represent optional settings that can be given to a
|
// Option represent optional settings that can be given to a
|
||||||
// Tailscale instance.
|
// Tailscale instance.
|
||||||
type Option = func(c *TailscaleInContainer)
|
type Option = func(c *TailscaleInContainer)
|
||||||
|
|
||||||
// WithHeadscaleTLS takes the certificate of the Headscale instance
|
// WithCACert adds it to the trusted surtificate of the Tailscale container.
|
||||||
// and adds it to the trusted surtificate of the Tailscale container.
|
func WithCACert(cert []byte) Option {
|
||||||
func WithHeadscaleTLS(cert []byte) Option {
|
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.headscaleCert = cert
|
tsic.caCerts = append(tsic.caCerts, cert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +125,7 @@ func WithOrCreateNetwork(network *dockertest.Network) Option {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithHeadscaleName set the name of the headscale instance,
|
// WithHeadscaleName set the name of the headscale instance,
|
||||||
// mostly useful in combination with TLS and WithHeadscaleTLS.
|
// mostly useful in combination with TLS and WithCACert.
|
||||||
func WithHeadscaleName(hsName string) Option {
|
func WithHeadscaleName(hsName string) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.headscaleHostname = hsName
|
tsic.headscaleHostname = hsName
|
||||||
|
@ -175,6 +187,22 @@ func WithNetfilter(state string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithBuildTag adds an additional value to the `-tags=` parameter
|
||||||
|
// of the Go compiler, allowing callers to customize the Tailscale client build.
|
||||||
|
// This option is only meaningful when invoked on **HEAD** versions of the client.
|
||||||
|
// Attempts to use it with any other version is a bug in the calling code.
|
||||||
|
func WithBuildTag(tag string) Option {
|
||||||
|
return func(tsic *TailscaleInContainer) {
|
||||||
|
if tsic.version != VersionHead {
|
||||||
|
panic(errInvalidClientConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
tsic.buildConfig.tags = append(
|
||||||
|
tsic.buildConfig.tags, tag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// New returns a new TailscaleInContainer instance.
|
// New returns a new TailscaleInContainer instance.
|
||||||
func New(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
|
@ -219,18 +247,20 @@ func New(
|
||||||
}
|
}
|
||||||
|
|
||||||
if tsic.withWebsocketDERP {
|
if tsic.withWebsocketDERP {
|
||||||
|
if version != VersionHead {
|
||||||
|
return tsic, errInvalidClientConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
WithBuildTag("ts_debug_websockets")(tsic)
|
||||||
|
|
||||||
tailscaleOptions.Env = append(
|
tailscaleOptions.Env = append(
|
||||||
tailscaleOptions.Env,
|
tailscaleOptions.Env,
|
||||||
fmt.Sprintf("TS_DEBUG_DERP_WS_CLIENT=%t", tsic.withWebsocketDERP),
|
fmt.Sprintf("TS_DEBUG_DERP_WS_CLIENT=%t", tsic.withWebsocketDERP),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tsic.headscaleHostname != "" {
|
tailscaleOptions.ExtraHosts = append(tailscaleOptions.ExtraHosts,
|
||||||
tailscaleOptions.ExtraHosts = []string{
|
"host.docker.internal:host-gateway")
|
||||||
"host.docker.internal:host-gateway",
|
|
||||||
fmt.Sprintf("%s:host-gateway", tsic.headscaleHostname),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tsic.workdir != "" {
|
if tsic.workdir != "" {
|
||||||
tailscaleOptions.WorkingDir = tsic.workdir
|
tailscaleOptions.WorkingDir = tsic.workdir
|
||||||
|
@ -245,14 +275,36 @@ func New(
|
||||||
}
|
}
|
||||||
|
|
||||||
var container *dockertest.Resource
|
var container *dockertest.Resource
|
||||||
|
|
||||||
|
if version != VersionHead {
|
||||||
|
// build options are not meaningful with pre-existing images,
|
||||||
|
// let's not lead anyone astray by pretending otherwise.
|
||||||
|
defaultBuildConfig := TailscaleInContainerBuildConfig{}
|
||||||
|
hasBuildConfig := !reflect.DeepEqual(defaultBuildConfig, tsic.buildConfig)
|
||||||
|
if hasBuildConfig {
|
||||||
|
return tsic, errInvalidClientConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch version {
|
switch version {
|
||||||
case "head":
|
case VersionHead:
|
||||||
buildOptions := &dockertest.BuildOptions{
|
buildOptions := &dockertest.BuildOptions{
|
||||||
Dockerfile: "Dockerfile.tailscale-HEAD",
|
Dockerfile: "Dockerfile.tailscale-HEAD",
|
||||||
ContextDir: dockerContextPath,
|
ContextDir: dockerContextPath,
|
||||||
BuildArgs: []docker.BuildArg{},
|
BuildArgs: []docker.BuildArg{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildTags := strings.Join(tsic.buildConfig.tags, ",")
|
||||||
|
if len(buildTags) > 0 {
|
||||||
|
buildOptions.BuildArgs = append(
|
||||||
|
buildOptions.BuildArgs,
|
||||||
|
docker.BuildArg{
|
||||||
|
Name: "BUILD_TAGS",
|
||||||
|
Value: buildTags,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
container, err = pool.BuildAndRunWithBuildOptions(
|
container, err = pool.BuildAndRunWithBuildOptions(
|
||||||
buildOptions,
|
buildOptions,
|
||||||
tailscaleOptions,
|
tailscaleOptions,
|
||||||
|
@ -294,8 +346,8 @@ func New(
|
||||||
|
|
||||||
tsic.container = container
|
tsic.container = container
|
||||||
|
|
||||||
if tsic.hasTLS() {
|
for i, cert := range tsic.caCerts {
|
||||||
err = tsic.WriteFile(headscaleCertPath, tsic.headscaleCert)
|
err = tsic.WriteFile(fmt.Sprintf("%s/user-%d.crt", caCertRoot, i), cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -304,10 +356,6 @@ func New(
|
||||||
return tsic, nil
|
return tsic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TailscaleInContainer) hasTLS() bool {
|
|
||||||
return len(t.headscaleCert) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown stops and cleans up the Tailscale container.
|
// Shutdown stops and cleans up the Tailscale container.
|
||||||
func (t *TailscaleInContainer) Shutdown() error {
|
func (t *TailscaleInContainer) Shutdown() error {
|
||||||
err := t.SaveLog("/tmp/control")
|
err := t.SaveLog("/tmp/control")
|
||||||
|
@ -682,6 +730,34 @@ func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error) {
|
||||||
|
if !util.TailscaleVersionNewerOrEqual("1.34", t.version) {
|
||||||
|
panic("tsic.DebugDERPRegion() called with unsupported version: " + t.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"tailscale",
|
||||||
|
"debug",
|
||||||
|
"derp",
|
||||||
|
region,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, stderr, err := t.Execute(command)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("stderr: %s\n", stderr) // nolint
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to execute tailscale debug derp command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var report ipnstate.DebugDERPRegionReport
|
||||||
|
err = json.Unmarshal([]byte(result), &report)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal tailscale derp region report: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &report, err
|
||||||
|
}
|
||||||
|
|
||||||
// Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance.
|
// Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Netcheck() (*netcheck.Report, error) {
|
func (t *TailscaleInContainer) Netcheck() (*netcheck.Report, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
|
|
|
@ -183,3 +183,4 @@ nav:
|
||||||
- Integration:
|
- Integration:
|
||||||
- Reverse proxy: ref/integration/reverse-proxy.md
|
- Reverse proxy: ref/integration/reverse-proxy.md
|
||||||
- Web UI: ref/integration/web-ui.md
|
- Web UI: ref/integration/web-ui.md
|
||||||
|
- Tools: ref/integration/tools.md
|
||||||
|
|
Loading…
Reference in a new issue