mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-17 06:19:51 +02:00
Compare commits
3 Commits
revert-141
...
v0.23.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b12ef62486 | ||
|
|
df8a85f65a | ||
|
|
848a9c27ae |
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,24 +6,19 @@ labels: ["bug"]
|
|||||||
assignees: ""
|
assignees: ""
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the bug report in this language. -->
|
||||||
Before posting a bug report, discuss the behaviour you are expecting with the Discord community
|
|
||||||
to make sure that it is truly a bug.
|
|
||||||
The issue tracker is not the place to ask for support or how to set up Headscale.
|
|
||||||
|
|
||||||
Bug reports without the sufficient information will be closed.
|
**Bug description**
|
||||||
|
|
||||||
Headscale is a multinational community across the globe. Our language is English.
|
|
||||||
All bug reports needs to be in English.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Bug description
|
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. Describe the expected bahavior
|
<!-- A clear and concise description of what the bug is. Describe the expected bahavior
|
||||||
and how it is currently different. If you are unsure if it is a bug, consider discussing
|
and how it is currently different. If you are unsure if it is a bug, consider discussing
|
||||||
it on our Discord server first. -->
|
it on our Discord server first. -->
|
||||||
|
|
||||||
## Environment
|
**To Reproduce**
|
||||||
|
|
||||||
|
<!-- Steps to reproduce the behavior. -->
|
||||||
|
|
||||||
|
**Context info**
|
||||||
|
|
||||||
<!-- Please add relevant information about your system. For example:
|
<!-- Please add relevant information about your system. For example:
|
||||||
- Version of headscale used
|
- Version of headscale used
|
||||||
@@ -33,20 +28,3 @@ All bug reports needs to be in English.
|
|||||||
- The relevant config parameters you used
|
- The relevant config parameters you used
|
||||||
- Log output
|
- Log output
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- OS:
|
|
||||||
- Headscale version:
|
|
||||||
- Tailscale version:
|
|
||||||
|
|
||||||
<!--
|
|
||||||
We do not support running Headscale in a container nor behind a (reverse) proxy.
|
|
||||||
If either of these are true for your environment, ask the community in Discord
|
|
||||||
instead of filing a bug report.
|
|
||||||
-->
|
|
||||||
|
|
||||||
- [ ] Headscale is behind a (reverse) proxy
|
|
||||||
- [ ] Headscale runs in a container
|
|
||||||
|
|
||||||
## To Reproduce
|
|
||||||
|
|
||||||
<!-- Steps to reproduce the behavior. -->
|
|
||||||
|
|||||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -6,21 +6,12 @@ labels: ["enhancement"]
|
|||||||
assignees: ""
|
assignees: ""
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the feature request in this language. -->
|
||||||
We typically have a clear roadmap for what we want to improve and reserve the right
|
|
||||||
to close feature requests that does not fit in the roadmap, or fit with the scope
|
|
||||||
of the project, or we actually want to implement ourselves.
|
|
||||||
|
|
||||||
Headscale is a multinational community across the globe. Our language is English.
|
**Feature request**
|
||||||
All bug reports needs to be in English.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
<!-- Include the reason, why you would need the feature. E.g. what problem
|
|
||||||
does it solve? Or which workflow is currently frustrating and will be improved by
|
|
||||||
this? -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- A clear and precise description of what new or changed feature you want. -->
|
<!-- A clear and precise description of what new or changed feature you want. -->
|
||||||
|
|
||||||
|
<!-- Please include the reason, why you would need the feature. E.g. what problem
|
||||||
|
does it solve? Or which workflow is currently frustrating and will be improved by
|
||||||
|
this? -->
|
||||||
|
|||||||
30
.github/ISSUE_TEMPLATE/other_issue.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/other_issue.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: "Other issue"
|
||||||
|
about: "Report a different issue"
|
||||||
|
title: ""
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Headscale is a multinational community across the globe. Our common language is English. Please consider raising the issue in this language. -->
|
||||||
|
|
||||||
|
<!-- If you have a question, please consider using our Discord for asking questions -->
|
||||||
|
|
||||||
|
**Issue description**
|
||||||
|
|
||||||
|
<!-- Please add your issue description. -->
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
|
||||||
|
<!-- Steps to reproduce the behavior. -->
|
||||||
|
|
||||||
|
**Context info**
|
||||||
|
|
||||||
|
<!-- Please add relevant information about your system. For example:
|
||||||
|
- Version of headscale used
|
||||||
|
- Version of tailscale client
|
||||||
|
- OS (e.g. Linux, Mac, Cygwin, WSL, etc.) and version
|
||||||
|
- Kernel version
|
||||||
|
- The relevant config parameters you used
|
||||||
|
- Log output
|
||||||
|
-->
|
||||||
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -1,15 +1,3 @@
|
|||||||
<!--
|
|
||||||
Headscale is "Open Source, acknowledged contribution", this means that any
|
|
||||||
contribution will have to be discussed with the Maintainers before being submitted.
|
|
||||||
|
|
||||||
This model has been chosen to reduce the risk of burnout by limiting the
|
|
||||||
maintenance overhead of reviewing and validating third-party code.
|
|
||||||
|
|
||||||
Headscale is open to code contributions for bug fixes without discussion.
|
|
||||||
|
|
||||||
If you find mistakes in the documentation, please submit a fix to the documentation.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Please tick if the following things apply. You… -->
|
<!-- Please tick if the following things apply. You… -->
|
||||||
|
|
||||||
- [ ] read the [CONTRIBUTING guidelines](README.md#contributing)
|
- [ ] read the [CONTRIBUTING guidelines](README.md#contributing)
|
||||||
|
|||||||
26
.github/renovate.json
vendored
26
.github/renovate.json
vendored
@@ -6,27 +6,31 @@
|
|||||||
"onboarding": false,
|
"onboarding": false,
|
||||||
"extends": ["config:base", ":rebaseStalePrs"],
|
"extends": ["config:base", ":rebaseStalePrs"],
|
||||||
"ignorePresets": [":prHourlyLimit2"],
|
"ignorePresets": [":prHourlyLimit2"],
|
||||||
"enabledManagers": ["dockerfile", "gomod", "github-actions", "regex"],
|
"enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ],
|
||||||
"includeForks": true,
|
"includeForks": true,
|
||||||
"repositories": ["juanfont/headscale"],
|
"repositories": ["juanfont/headscale"],
|
||||||
"platform": "github",
|
"platform": "github",
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchDatasources": ["go"],
|
"matchDatasources": ["go"],
|
||||||
"groupName": "Go modules",
|
"groupName": "Go modules",
|
||||||
"groupSlug": "gomod",
|
"groupSlug": "gomod",
|
||||||
"separateMajorMinor": false
|
"separateMajorMinor": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchDatasources": ["docker"],
|
"matchDatasources": ["docker"],
|
||||||
"groupName": "Dockerfiles",
|
"groupName": "Dockerfiles",
|
||||||
"groupSlug": "dockerfiles"
|
"groupSlug": "dockerfiles"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"regexManagers": [
|
"regexManagers": [
|
||||||
{
|
{
|
||||||
"fileMatch": [".github/workflows/.*.yml$"],
|
"fileMatch": [
|
||||||
"matchStrings": ["\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"],
|
".github/workflows/.*.yml$"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"
|
||||||
|
],
|
||||||
"datasourceTemplate": "golang-version",
|
"datasourceTemplate": "golang-version",
|
||||||
"depNameTemplate": "actions/go-version"
|
"depNameTemplate": "actions/go-version"
|
||||||
}
|
}
|
||||||
|
|||||||
138
.github/workflows/release-docker.yml
vendored
138
.github/workflows/release-docker.yml
vendored
@@ -1,138 +0,0 @@
|
|||||||
---
|
|
||||||
name: Release Docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*" # triggers only if push new tag version
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Set up QEMU for multiple platforms
|
|
||||||
uses: docker/setup-qemu-action@master
|
|
||||||
with:
|
|
||||||
platforms: arm64,amd64
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v3
|
|
||||||
with:
|
|
||||||
# list of Docker images to use as base name for tags
|
|
||||||
images: |
|
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/headscale
|
|
||||||
ghcr.io/${{ github.repository_owner }}/headscale
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=sha
|
|
||||||
type=raw,value=develop
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
context: .
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ steps.meta.outputs.version }}
|
|
||||||
- name: Prepare cache for next build
|
|
||||||
run: |
|
|
||||||
rm -rf /tmp/.buildx-cache
|
|
||||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
|
||||||
|
|
||||||
docker-debug-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Set up QEMU for multiple platforms
|
|
||||||
uses: docker/setup-qemu-action@master
|
|
||||||
with:
|
|
||||||
platforms: arm64,amd64
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache-debug
|
|
||||||
key: ${{ runner.os }}-buildx-debug-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-debug-
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta-debug
|
|
||||||
uses: docker/metadata-action@v3
|
|
||||||
with:
|
|
||||||
# list of Docker images to use as base name for tags
|
|
||||||
images: |
|
|
||||||
${{ secrets.DOCKERHUB_USERNAME }}/headscale
|
|
||||||
ghcr.io/${{ github.repository_owner }}/headscale
|
|
||||||
flavor: |
|
|
||||||
suffix=-debug,onlatest=true
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=sha
|
|
||||||
type=raw,value=develop
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Login to GHCR
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.debug
|
|
||||||
tags: ${{ steps.meta-debug.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta-debug.outputs.labels }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache-debug
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-debug-new
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ steps.meta-debug.outputs.version }}
|
|
||||||
- name: Prepare cache for next build
|
|
||||||
run: |
|
|
||||||
rm -rf /tmp/.buildx-cache-debug
|
|
||||||
mv /tmp/.buildx-cache-debug-new /tmp/.buildx-cache-debug
|
|
||||||
@@ -36,6 +36,7 @@ archives:
|
|||||||
format: binary
|
format: binary
|
||||||
|
|
||||||
source:
|
source:
|
||||||
|
rlcp: true
|
||||||
enabled: true
|
enabled: true
|
||||||
name_template: "{{ .ProjectName }}_{{ .Version }}"
|
name_template: "{{ .ProjectName }}_{{ .Version }}"
|
||||||
format: tar.gz
|
format: tar.gz
|
||||||
@@ -63,7 +64,6 @@ nfpms:
|
|||||||
bindir: /usr/bin
|
bindir: /usr/bin
|
||||||
formats:
|
formats:
|
||||||
- deb
|
- deb
|
||||||
# - rpm
|
|
||||||
contents:
|
contents:
|
||||||
- src: ./config-example.yaml
|
- src: ./config-example.yaml
|
||||||
dst: /etc/headscale/config.yaml
|
dst: /etc/headscale/config.yaml
|
||||||
@@ -71,7 +71,7 @@ nfpms:
|
|||||||
file_info:
|
file_info:
|
||||||
mode: 0644
|
mode: 0644
|
||||||
- src: ./docs/packaging/headscale.systemd.service
|
- src: ./docs/packaging/headscale.systemd.service
|
||||||
dst: /usr/lib/systemd/system/headscale.service
|
dst: /etc/systemd/system/headscale.service
|
||||||
- dst: /var/lib/headscale
|
- dst: /var/lib/headscale
|
||||||
type: dir
|
type: dir
|
||||||
- dst: /var/run/headscale
|
- dst: /var/run/headscale
|
||||||
@@ -80,6 +80,36 @@ nfpms:
|
|||||||
postinstall: ./docs/packaging/postinstall.sh
|
postinstall: ./docs/packaging/postinstall.sh
|
||||||
postremove: ./docs/packaging/postremove.sh
|
postremove: ./docs/packaging/postremove.sh
|
||||||
|
|
||||||
|
kos:
|
||||||
|
- id: ghcr
|
||||||
|
build: headscale
|
||||||
|
base_image: gcr.io/distroless/base-debian11
|
||||||
|
repository: ghcr.io/juanfont/headscale
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/386
|
||||||
|
- linux/arm64
|
||||||
|
- linux/arm/v7
|
||||||
|
- linux/arm/v6
|
||||||
|
- linux/arm/v5
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- '{{.Tag}}'
|
||||||
|
- id: dockerhub
|
||||||
|
build: headscale
|
||||||
|
base_image: gcr.io/distroless/base-debian11
|
||||||
|
repository: headscale/headscale
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/386
|
||||||
|
- linux/arm64
|
||||||
|
- linux/arm/v7
|
||||||
|
- linux/arm/v6
|
||||||
|
- linux/arm/v5
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- '{{.Tag}}'
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
snapshot:
|
snapshot:
|
||||||
|
|||||||
@@ -6,16 +6,12 @@
|
|||||||
|
|
||||||
- Add environment flags to enable pprof (profiling) [#1382](https://github.com/juanfont/headscale/pull/1382)
|
- Add environment flags to enable pprof (profiling) [#1382](https://github.com/juanfont/headscale/pull/1382)
|
||||||
- Profiles are continously generated in our integration tests.
|
- Profiles are continously generated in our integration tests.
|
||||||
- Fix systemd service file location in `.deb` packages [#1391](https://github.com/juanfont/headscale/pull/1391)
|
|
||||||
- Improvements on Noise implementation [#1379](https://github.com/juanfont/headscale/pull/1379)
|
|
||||||
- Replace node filter logic, ensuring nodes with access can see eachother [#1381](https://github.com/juanfont/headscale/pull/1381)
|
|
||||||
|
|
||||||
## 0.22.1 (2023-04-20)
|
## 0.22.1 (2023-04-20)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Fix issue where systemd could not bind to port 80 [#1365](https://github.com/juanfont/headscale/pull/1365)
|
- Fix issue where SystemD could not bind to port 80 [#1365](https://github.com/juanfont/headscale/pull/1365)
|
||||||
- Disable (or delete) both exit routes at the same time [#1428](https://github.com/juanfont/headscale/pull/1428)
|
|
||||||
|
|
||||||
## 0.22.0 (2023-04-20)
|
## 0.22.0 (2023-04-20)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# Builder image
|
# This Dockerfile and the images produced are for testing headscale,
|
||||||
|
# and are in no way endorsed by Headscale's maintainers as an
|
||||||
|
# official nor supported release or distribution.
|
||||||
|
|
||||||
FROM docker.io/golang:1.20-bullseye AS build
|
FROM docker.io/golang:1.20-bullseye AS build
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV GOPATH /go
|
ENV GOPATH /go
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# Builder image
|
# This Dockerfile and the images produced are for testing headscale,
|
||||||
|
# and are in no way endorsed by Headscale's maintainers as an
|
||||||
|
# official nor supported release or distribution.
|
||||||
|
|
||||||
FROM docker.io/golang:1.20-bullseye AS build
|
FROM docker.io/golang:1.20-bullseye AS build
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV GOPATH /go
|
ENV GOPATH /go
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
FROM ubuntu:22.04
|
# This Dockerfile and the images produced are for testing headscale,
|
||||||
|
# and are in no way endorsed by Headscale's maintainers as an
|
||||||
|
# official nor supported release or distribution.
|
||||||
|
|
||||||
|
FROM ubuntu:latest
|
||||||
|
|
||||||
ARG TAILSCALE_VERSION=*
|
ARG TAILSCALE_VERSION=*
|
||||||
ARG TAILSCALE_CHANNEL=stable
|
ARG TAILSCALE_CHANNEL=stable
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y gnupg curl ssh dnsutils ca-certificates \
|
&& apt-get install -y gnupg curl ssh \
|
||||||
&& adduser --shell=/bin/bash ssh-it-user
|
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
|
||||||
|
|
||||||
# Tailscale is deliberately split into a second stage so we can cash utils as a seperate layer.
|
|
||||||
RUN curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \
|
|
||||||
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
&& curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y tailscale=${TAILSCALE_VERSION} \
|
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN adduser --shell=/bin/bash ssh-it-user
|
||||||
|
|
||||||
|
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
|
||||||
|
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
|
||||||
|
|
||||||
|
RUN update-ca-certificates
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
# This Dockerfile and the images produced are for testing headscale,
|
||||||
|
# and are in no way endorsed by Headscale's maintainers as an
|
||||||
|
# official nor supported release or distribution.
|
||||||
|
|
||||||
FROM golang:latest
|
FROM golang:latest
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y dnsutils git iptables ssh ca-certificates \
|
&& apt-get install -y ca-certificates dnsutils git iptables ssh \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN useradd --shell=/bin/bash --create-home ssh-it-user
|
RUN useradd --shell=/bin/bash --create-home ssh-it-user
|
||||||
@@ -10,8 +14,15 @@ RUN git clone https://github.com/tailscale/tailscale.git
|
|||||||
|
|
||||||
WORKDIR /go/tailscale
|
WORKDIR /go/tailscale
|
||||||
|
|
||||||
RUN git checkout main \
|
RUN git checkout main
|
||||||
&& sh build_dist.sh tailscale.com/cmd/tailscale \
|
|
||||||
&& sh build_dist.sh tailscale.com/cmd/tailscaled \
|
RUN sh build_dist.sh tailscale.com/cmd/tailscale
|
||||||
&& cp tailscale /usr/local/bin/ \
|
RUN sh build_dist.sh tailscale.com/cmd/tailscaled
|
||||||
&& cp tailscaled /usr/local/bin/
|
|
||||||
|
RUN cp tailscale /usr/local/bin/
|
||||||
|
RUN cp tailscaled /usr/local/bin/
|
||||||
|
|
||||||
|
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
|
||||||
|
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
|
||||||
|
|
||||||
|
RUN update-ca-certificates
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -32,18 +32,22 @@ organisation.
|
|||||||
|
|
||||||
## Design goal
|
## Design goal
|
||||||
|
|
||||||
Headscale aims to implement a self-hosted, open source alternative to the Tailscale
|
`headscale` aims to implement a self-hosted, open source alternative to the Tailscale
|
||||||
control server.
|
control server. `headscale` has a narrower scope and an instance of `headscale`
|
||||||
Headscale's goal is to provide self-hosters and hobbyists with an open-source
|
implements a _single_ Tailnet, which is typically what a single organisation, or
|
||||||
server they can use for their projects and labs.
|
home/personal setup would use.
|
||||||
It implements a narrow scope, a single Tailnet, suitable for a personal use, or a small
|
|
||||||
open-source organisation.
|
|
||||||
|
|
||||||
## Supporting Headscale
|
`headscale` uses terms that maps to Tailscale's control server, consult the
|
||||||
|
[glossary](./docs/glossary.md) for explainations.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
If you like `headscale` and find it useful, there is a sponsorship and donation
|
If you like `headscale` and find it useful, there is a sponsorship and donation
|
||||||
buttons available in the repo.
|
buttons available in the repo.
|
||||||
|
|
||||||
|
If you would like to sponsor features, bugs or prioritisation, reach out to
|
||||||
|
one of the maintainers.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Full "base" support of Tailscale's features
|
- Full "base" support of Tailscale's features
|
||||||
@@ -75,10 +79,7 @@ buttons available in the repo.
|
|||||||
|
|
||||||
## Running headscale
|
## Running headscale
|
||||||
|
|
||||||
**Please note that we do not support nor encourage the use of reverse proxies
|
Please have a look at the documentation under [`docs/`](docs/).
|
||||||
and container to run Headscale.**
|
|
||||||
|
|
||||||
Please have a look at the [`documentation`](https://headscale.net/).
|
|
||||||
|
|
||||||
## Graphical Control Panels
|
## Graphical Control Panels
|
||||||
|
|
||||||
@@ -96,23 +97,11 @@ These are community projects not directly affiliated with the Headscale project.
|
|||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
1. This project is not associated with Tailscale Inc.
|
1. We have nothing to do with Tailscale, or Tailscale Inc.
|
||||||
2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel.
|
2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Headscale is "Open Source, acknowledged contribution", this means that any
|
|
||||||
contribution will have to be discussed with the Maintainers before being submitted.
|
|
||||||
|
|
||||||
This model has been chosen to reduce the risk of burnout by limiting the
|
|
||||||
maintenance overhead of reviewing and validating third-party code.
|
|
||||||
|
|
||||||
Headscale is open to code contributions for bug fixes without discussion.
|
|
||||||
|
|
||||||
If you find mistakes in the documentation, please submit a fix to the documentation.
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
|
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
|
||||||
and [Buf](https://buf.build)(Protobuf generator).
|
and [Buf](https://buf.build)(Protobuf generator).
|
||||||
|
|
||||||
@@ -120,6 +109,8 @@ We recommend using [Nix](https://nixos.org/) to setup a development environment.
|
|||||||
be done with `nix develop`, which will install the tools and give you a shell.
|
be done with `nix develop`, which will install the tools and give you a shell.
|
||||||
This guarantees that you will have the same dev env as `headscale` maintainers.
|
This guarantees that you will have the same dev env as `headscale` maintainers.
|
||||||
|
|
||||||
|
PRs and suggestions are welcome.
|
||||||
|
|
||||||
### Code style
|
### Code style
|
||||||
|
|
||||||
To ensure we have some consistency with a growing number of contributions,
|
To ensure we have some consistency with a growing number of contributions,
|
||||||
|
|||||||
492
acls.go
492
acls.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -127,14 +128,21 @@ func (h *Headscale) UpdateACLRules() error {
|
|||||||
return errEmptyPolicy
|
return errEmptyPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
rules, err := h.aclPolicy.generateFilterRules(machines, h.cfg.OIDC.StripEmaildomain)
|
rules, err := generateACLRules(machines, *h.aclPolicy, h.cfg.OIDC.StripEmaildomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
||||||
h.aclRules = rules
|
h.aclRules = rules
|
||||||
|
|
||||||
|
// Precompute a map of which sources can reach each destination, this is
|
||||||
|
// to provide quicker lookup when we calculate the peerlist for the map
|
||||||
|
// response to nodes.
|
||||||
|
aclPeerCacheMap := generateACLPeerCacheMap(rules)
|
||||||
|
h.aclPeerCacheMapRW.Lock()
|
||||||
|
h.aclPeerCacheMap = aclPeerCacheMap
|
||||||
|
h.aclPeerCacheMapRW.Unlock()
|
||||||
|
|
||||||
if featureEnableSSH() {
|
if featureEnableSSH() {
|
||||||
sshRules, err := h.generateSSHRules()
|
sshRules, err := h.generateSSHRules()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -152,28 +160,88 @@ func (h *Headscale) UpdateACLRules() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateFilterRules takes a set of machines and an ACLPolicy and generates a
|
// generateACLPeerCacheMap takes a list of Tailscale filter rules and generates a map
|
||||||
// set of Tailscale compatible FilterRules used to allow traffic on clients.
|
// of which Sources ("*" and IPs) can access destinations. This is to speed up the
|
||||||
func (pol *ACLPolicy) generateFilterRules(
|
// process of generating MapResponses when deciding which Peers to inform nodes about.
|
||||||
|
func generateACLPeerCacheMap(rules []tailcfg.FilterRule) map[string][]string {
|
||||||
|
aclCachePeerMap := make(map[string][]string)
|
||||||
|
for _, rule := range rules {
|
||||||
|
for _, srcIP := range rule.SrcIPs {
|
||||||
|
for _, ip := range expandACLPeerAddr(srcIP) {
|
||||||
|
if data, ok := aclCachePeerMap[ip]; ok {
|
||||||
|
for _, dstPort := range rule.DstPorts {
|
||||||
|
data = append(data, dstPort.IP)
|
||||||
|
}
|
||||||
|
aclCachePeerMap[ip] = data
|
||||||
|
} else {
|
||||||
|
dstPortsMap := make([]string, 0)
|
||||||
|
for _, dstPort := range rule.DstPorts {
|
||||||
|
dstPortsMap = append(dstPortsMap, dstPort.IP)
|
||||||
|
}
|
||||||
|
aclCachePeerMap[ip] = dstPortsMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Interface("ACL Cache Map", aclCachePeerMap).Msg("ACL Peer Cache Map generated")
|
||||||
|
|
||||||
|
return aclCachePeerMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandACLPeerAddr takes a "tailcfg.FilterRule" "IP" and expands it into
|
||||||
|
// something our cache logic can look up, which is "*" or single IP addresses.
|
||||||
|
// This is probably quite inefficient, but it is a result of
|
||||||
|
// "make it work, then make it fast", and a lot of the ACL stuff does not
|
||||||
|
// work, but people have tried to make it fast.
|
||||||
|
func expandACLPeerAddr(srcIP string) []string {
|
||||||
|
if ip, err := netip.ParseAddr(srcIP); err == nil {
|
||||||
|
return []string{ip.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cidr, err := netip.ParsePrefix(srcIP); err == nil {
|
||||||
|
addrs := []string{}
|
||||||
|
|
||||||
|
ipRange := netipx.RangeOfPrefix(cidr)
|
||||||
|
|
||||||
|
from := ipRange.From()
|
||||||
|
too := ipRange.To()
|
||||||
|
|
||||||
|
if from == too {
|
||||||
|
return []string{from.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
for from != too && from.Less(too) {
|
||||||
|
addrs = append(addrs, from.String())
|
||||||
|
from = from.Next()
|
||||||
|
}
|
||||||
|
addrs = append(addrs, too.String()) // Add the last IP address in the range
|
||||||
|
|
||||||
|
return addrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// probably "*" or other string based "IP"
|
||||||
|
return []string{srcIP}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateACLRules(
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
stripEmailDomain bool,
|
aclPolicy ACLPolicy,
|
||||||
|
stripEmaildomain bool,
|
||||||
) ([]tailcfg.FilterRule, error) {
|
) ([]tailcfg.FilterRule, error) {
|
||||||
rules := []tailcfg.FilterRule{}
|
rules := []tailcfg.FilterRule{}
|
||||||
|
|
||||||
for index, acl := range pol.ACLs {
|
for index, acl := range aclPolicy.ACLs {
|
||||||
if acl.Action != "accept" {
|
if acl.Action != "accept" {
|
||||||
return nil, errInvalidAction
|
return nil, errInvalidAction
|
||||||
}
|
}
|
||||||
|
|
||||||
srcIPs := []string{}
|
srcIPs := []string{}
|
||||||
for srcIndex, src := range acl.Sources {
|
for innerIndex, src := range acl.Sources {
|
||||||
srcs, err := pol.getIPsFromSource(src, machines, stripEmailDomain)
|
srcs, err := generateACLPolicySrc(machines, aclPolicy, src, stripEmaildomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Interface("src", src).
|
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
|
||||||
Int("ACL index", index).
|
|
||||||
Int("Src index", srcIndex).
|
|
||||||
Msgf("Error parsing ACL")
|
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -189,19 +257,17 @@ func (pol *ACLPolicy) generateFilterRules(
|
|||||||
}
|
}
|
||||||
|
|
||||||
destPorts := []tailcfg.NetPortRange{}
|
destPorts := []tailcfg.NetPortRange{}
|
||||||
for destIndex, dest := range acl.Destinations {
|
for innerIndex, dest := range acl.Destinations {
|
||||||
dests, err := pol.getNetPortRangeFromDestination(
|
dests, err := generateACLPolicyDest(
|
||||||
dest,
|
|
||||||
machines,
|
machines,
|
||||||
|
aclPolicy,
|
||||||
|
dest,
|
||||||
needsWildcard,
|
needsWildcard,
|
||||||
stripEmailDomain,
|
stripEmaildomain,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Interface("dest", dest).
|
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
|
||||||
Int("ACL index", index).
|
|
||||||
Int("dest index", destIndex).
|
|
||||||
Msgf("Error parsing ACL")
|
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -272,41 +338,22 @@ func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) {
|
|||||||
|
|
||||||
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
|
principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources))
|
||||||
for innerIndex, rawSrc := range sshACL.Sources {
|
for innerIndex, rawSrc := range sshACL.Sources {
|
||||||
if isWildcard(rawSrc) {
|
expandedSrcs, err := expandAlias(
|
||||||
|
machines,
|
||||||
|
*h.aclPolicy,
|
||||||
|
rawSrc,
|
||||||
|
h.cfg.OIDC.StripEmaildomain,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, expandedSrc := range expandedSrcs {
|
||||||
principals = append(principals, &tailcfg.SSHPrincipal{
|
principals = append(principals, &tailcfg.SSHPrincipal{
|
||||||
Any: true,
|
NodeIP: expandedSrc,
|
||||||
})
|
})
|
||||||
} else if isGroup(rawSrc) {
|
|
||||||
users, err := h.aclPolicy.getUsersInGroup(rawSrc, h.cfg.OIDC.StripEmaildomain)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, user := range users {
|
|
||||||
principals = append(principals, &tailcfg.SSHPrincipal{
|
|
||||||
UserLogin: user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expandedSrcs, err := h.aclPolicy.expandAlias(
|
|
||||||
machines,
|
|
||||||
rawSrc,
|
|
||||||
h.cfg.OIDC.StripEmaildomain,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Msgf("Error parsing SSH %d, Source %d", index, innerIndex)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, expandedSrc := range expandedSrcs.Prefixes() {
|
|
||||||
principals = append(principals, &tailcfg.SSHPrincipal{
|
|
||||||
NodeIP: expandedSrc.Addr().String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,9 +362,10 @@ func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) {
|
|||||||
userMap[user] = "="
|
userMap[user] = "="
|
||||||
}
|
}
|
||||||
rules = append(rules, &tailcfg.SSHRule{
|
rules = append(rules, &tailcfg.SSHRule{
|
||||||
Principals: principals,
|
RuleExpires: nil,
|
||||||
SSHUsers: userMap,
|
Principals: principals,
|
||||||
Action: &action,
|
SSHUsers: userMap,
|
||||||
|
Action: &action,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,32 +389,19 @@ func sshCheckAction(duration string) (*tailcfg.SSHAction, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getIPsFromSource returns a set of Source IPs that would be associated
|
func generateACLPolicySrc(
|
||||||
// with the given src alias.
|
|
||||||
func (pol *ACLPolicy) getIPsFromSource(
|
|
||||||
src string,
|
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
|
src string,
|
||||||
stripEmaildomain bool,
|
stripEmaildomain bool,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
ipSet, err := pol.expandAlias(machines, src, stripEmaildomain)
|
return expandAlias(machines, aclPolicy, src, stripEmaildomain)
|
||||||
if err != nil {
|
|
||||||
return []string{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prefixes := []string{}
|
|
||||||
|
|
||||||
for _, prefix := range ipSet.Prefixes() {
|
|
||||||
prefixes = append(prefixes, prefix.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return prefixes, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getNetPortRangeFromDestination returns a set of tailcfg.NetPortRange
|
func generateACLPolicyDest(
|
||||||
// which are associated with the dest alias.
|
|
||||||
func (pol *ACLPolicy) getNetPortRangeFromDestination(
|
|
||||||
dest string,
|
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
|
dest string,
|
||||||
needsWildcard bool,
|
needsWildcard bool,
|
||||||
stripEmaildomain bool,
|
stripEmaildomain bool,
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
) ([]tailcfg.NetPortRange, error) {
|
||||||
@@ -413,8 +448,9 @@ func (pol *ACLPolicy) getNetPortRangeFromDestination(
|
|||||||
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
expanded, err := pol.expandAlias(
|
expanded, err := expandAlias(
|
||||||
machines,
|
machines,
|
||||||
|
aclPolicy,
|
||||||
alias,
|
alias,
|
||||||
stripEmaildomain,
|
stripEmaildomain,
|
||||||
)
|
)
|
||||||
@@ -427,11 +463,11 @@ func (pol *ACLPolicy) getNetPortRangeFromDestination(
|
|||||||
}
|
}
|
||||||
|
|
||||||
dests := []tailcfg.NetPortRange{}
|
dests := []tailcfg.NetPortRange{}
|
||||||
for _, dest := range expanded.Prefixes() {
|
for _, d := range expanded {
|
||||||
for _, port := range *ports {
|
for _, p := range *ports {
|
||||||
pr := tailcfg.NetPortRange{
|
pr := tailcfg.NetPortRange{
|
||||||
IP: dest.String(),
|
IP: d,
|
||||||
Ports: port,
|
Ports: p,
|
||||||
}
|
}
|
||||||
dests = append(dests, pr)
|
dests = append(dests, pr)
|
||||||
}
|
}
|
||||||
@@ -498,64 +534,135 @@ func parseProtocol(protocol string) ([]int, bool, error) {
|
|||||||
// - an ip
|
// - an ip
|
||||||
// - a cidr
|
// - a cidr
|
||||||
// and transform these in IPAddresses.
|
// and transform these in IPAddresses.
|
||||||
func (pol *ACLPolicy) expandAlias(
|
func expandAlias(
|
||||||
machines Machines,
|
machines Machines,
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
alias string,
|
alias string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
) (*netipx.IPSet, error) {
|
) ([]string, error) {
|
||||||
if isWildcard(alias) {
|
ips := []string{}
|
||||||
return parseIPSet("*", nil)
|
if alias == "*" {
|
||||||
|
return []string{"*"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
build := netipx.IPSetBuilder{}
|
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("alias", alias).
|
Str("alias", alias).
|
||||||
Msg("Expanding")
|
Msg("Expanding")
|
||||||
|
|
||||||
// if alias is a group
|
if strings.HasPrefix(alias, "group:") {
|
||||||
if isGroup(alias) {
|
users, err := expandGroup(aclPolicy, alias, stripEmailDomain)
|
||||||
return pol.getIPsFromGroup(alias, machines, stripEmailDomain)
|
if err != nil {
|
||||||
|
return ips, err
|
||||||
|
}
|
||||||
|
for _, n := range users {
|
||||||
|
nodes := filterMachinesByUser(machines, n)
|
||||||
|
for _, node := range nodes {
|
||||||
|
ips = append(ips, node.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is a tag
|
if strings.HasPrefix(alias, "tag:") {
|
||||||
if isTag(alias) {
|
// check for forced tags
|
||||||
return pol.getIPsFromTag(alias, machines, stripEmailDomain)
|
for _, machine := range machines {
|
||||||
|
if contains(machine.ForcedTags, alias) {
|
||||||
|
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find tag owners
|
||||||
|
owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errInvalidTag) {
|
||||||
|
if len(ips) == 0 {
|
||||||
|
return ips, fmt.Errorf(
|
||||||
|
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
|
||||||
|
errInvalidTag,
|
||||||
|
alias,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips, nil
|
||||||
|
} else {
|
||||||
|
return ips, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out machines per tag owner
|
||||||
|
for _, user := range owners {
|
||||||
|
machines := filterMachinesByUser(machines, user)
|
||||||
|
for _, machine := range machines {
|
||||||
|
hi := machine.GetHostInfo()
|
||||||
|
if contains(hi.RequestTags, alias) {
|
||||||
|
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is a user
|
// if alias is a user
|
||||||
if ips, err := pol.getIPsForUser(alias, machines, stripEmailDomain); ips != nil {
|
nodes := filterMachinesByUser(machines, alias)
|
||||||
return ips, err
|
nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain)
|
||||||
|
|
||||||
|
for _, n := range nodes {
|
||||||
|
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
if len(ips) > 0 {
|
||||||
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an host
|
// if alias is an host
|
||||||
// Note, this is recursive.
|
if h, ok := aclPolicy.Hosts[alias]; ok {
|
||||||
if h, ok := pol.Hosts[alias]; ok {
|
|
||||||
log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry")
|
log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry")
|
||||||
|
|
||||||
return pol.expandAlias(machines, h.String(), stripEmailDomain)
|
return expandAlias(machines, aclPolicy, h.String(), stripEmailDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an IP
|
// if alias is an IP
|
||||||
if ip, err := netip.ParseAddr(alias); err == nil {
|
if ip, err := netip.ParseAddr(alias); err == nil {
|
||||||
return pol.getIPsFromSingleIP(ip, machines)
|
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip")
|
||||||
|
ips := []string{ip.String()}
|
||||||
|
matches := machines.FilterByIP(ip)
|
||||||
|
|
||||||
|
for _, machine := range matches {
|
||||||
|
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Uniq(ips), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if alias is an IP Prefix (CIDR)
|
if cidr, err := netip.ParsePrefix(alias); err == nil {
|
||||||
if prefix, err := netip.ParsePrefix(alias); err == nil {
|
log.Trace().Str("cidr", cidr.String()).Msg("expandAlias got cidr")
|
||||||
return pol.getIPsFromIPPrefix(prefix, machines)
|
val := []string{cidr.String()}
|
||||||
|
// This is suboptimal and quite expensive, but if we only add the cidr, we will miss all the relevant IPv6
|
||||||
|
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
|
||||||
|
for _, machine := range machines {
|
||||||
|
for _, ip := range machine.IPAddresses {
|
||||||
|
// log.Trace().
|
||||||
|
// Msgf("checking if machine ip (%s) is part of cidr (%s): %v, is single ip cidr (%v), addr: %s", ip.String(), cidr.String(), cidr.Contains(ip), cidr.IsSingleIP(), cidr.Addr().String())
|
||||||
|
if cidr.Contains(ip) {
|
||||||
|
val = append(val, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Uniq(val), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
log.Warn().Msgf("No IPs found with the alias %v", alias)
|
||||||
|
|
||||||
return build.IPSet()
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
|
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
|
||||||
// that are correctly tagged since they should not be listed as being in the user
|
// that are correctly tagged since they should not be listed as being in the user
|
||||||
// we assume in this function that we only have nodes from 1 user.
|
// we assume in this function that we only have nodes from 1 user.
|
||||||
func excludeCorrectlyTaggedNodes(
|
func excludeCorrectlyTaggedNodes(
|
||||||
aclPolicy *ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
nodes []Machine,
|
nodes []Machine,
|
||||||
user string,
|
user string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
@@ -563,7 +670,7 @@ func excludeCorrectlyTaggedNodes(
|
|||||||
out := []Machine{}
|
out := []Machine{}
|
||||||
tags := []string{}
|
tags := []string{}
|
||||||
for tag := range aclPolicy.TagOwners {
|
for tag := range aclPolicy.TagOwners {
|
||||||
owners, _ := getTagOwners(aclPolicy, user, stripEmailDomain)
|
owners, _ := expandTagOwners(aclPolicy, user, stripEmailDomain)
|
||||||
ns := append(owners, user)
|
ns := append(owners, user)
|
||||||
if contains(ns, user) {
|
if contains(ns, user) {
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
@@ -593,7 +700,7 @@ func excludeCorrectlyTaggedNodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
|
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
|
||||||
if isWildcard(portsStr) {
|
if portsStr == "*" {
|
||||||
return &[]tailcfg.PortRange{
|
return &[]tailcfg.PortRange{
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -651,15 +758,15 @@ func filterMachinesByUser(machines []Machine, user string) []Machine {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTagOwners will return a list of user. An owner can be either a user or a group
|
// expandTagOwners will return a list of user. An owner can be either a user or a group
|
||||||
// a group cannot be composed of groups.
|
// a group cannot be composed of groups.
|
||||||
func getTagOwners(
|
func expandTagOwners(
|
||||||
pol *ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
tag string,
|
tag string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
var owners []string
|
var owners []string
|
||||||
ows, ok := pol.TagOwners[tag]
|
ows, ok := aclPolicy.TagOwners[tag]
|
||||||
if !ok {
|
if !ok {
|
||||||
return []string{}, fmt.Errorf(
|
return []string{}, fmt.Errorf(
|
||||||
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
|
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
|
||||||
@@ -668,8 +775,8 @@ func getTagOwners(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
for _, owner := range ows {
|
for _, owner := range ows {
|
||||||
if isGroup(owner) {
|
if strings.HasPrefix(owner, "group:") {
|
||||||
gs, err := pol.getUsersInGroup(owner, stripEmailDomain)
|
gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
@@ -682,15 +789,15 @@ func getTagOwners(
|
|||||||
return owners, nil
|
return owners, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUsersInGroup will return the list of user inside the group
|
// expandGroup will return the list of user inside the group
|
||||||
// after some validation.
|
// after some validation.
|
||||||
func (pol *ACLPolicy) getUsersInGroup(
|
func expandGroup(
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
group string,
|
group string,
|
||||||
stripEmailDomain bool,
|
stripEmailDomain bool,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
users := []string{}
|
outGroups := []string{}
|
||||||
log.Trace().Caller().Interface("pol", pol).Msg("test")
|
aclGroups, ok := aclPolicy.Groups[group]
|
||||||
aclGroups, ok := pol.Groups[group]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return []string{}, fmt.Errorf(
|
return []string{}, fmt.Errorf(
|
||||||
"group %v isn't registered. %w",
|
"group %v isn't registered. %w",
|
||||||
@@ -699,7 +806,7 @@ func (pol *ACLPolicy) getUsersInGroup(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
for _, group := range aclGroups {
|
for _, group := range aclGroups {
|
||||||
if isGroup(group) {
|
if strings.HasPrefix(group, "group:") {
|
||||||
return []string{}, fmt.Errorf(
|
return []string{}, fmt.Errorf(
|
||||||
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
|
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
|
||||||
errInvalidGroup,
|
errInvalidGroup,
|
||||||
@@ -713,151 +820,8 @@ func (pol *ACLPolicy) getUsersInGroup(
|
|||||||
errInvalidGroup,
|
errInvalidGroup,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
users = append(users, grp)
|
outGroups = append(outGroups, grp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return outGroups, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (pol *ACLPolicy) getIPsFromGroup(
|
|
||||||
group string,
|
|
||||||
machines Machines,
|
|
||||||
stripEmailDomain bool,
|
|
||||||
) (*netipx.IPSet, error) {
|
|
||||||
build := netipx.IPSetBuilder{}
|
|
||||||
|
|
||||||
users, err := pol.getUsersInGroup(group, stripEmailDomain)
|
|
||||||
if err != nil {
|
|
||||||
return &netipx.IPSet{}, err
|
|
||||||
}
|
|
||||||
for _, user := range users {
|
|
||||||
filteredMachines := filterMachinesByUser(machines, user)
|
|
||||||
for _, machine := range filteredMachines {
|
|
||||||
machine.IPAddresses.AppendToIPSet(&build)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return build.IPSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pol *ACLPolicy) getIPsFromTag(
|
|
||||||
alias string,
|
|
||||||
machines Machines,
|
|
||||||
stripEmailDomain bool,
|
|
||||||
) (*netipx.IPSet, error) {
|
|
||||||
build := netipx.IPSetBuilder{}
|
|
||||||
|
|
||||||
// check for forced tags
|
|
||||||
for _, machine := range machines {
|
|
||||||
if contains(machine.ForcedTags, alias) {
|
|
||||||
machine.IPAddresses.AppendToIPSet(&build)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// find tag owners
|
|
||||||
owners, err := getTagOwners(pol, alias, stripEmailDomain)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, errInvalidTag) {
|
|
||||||
ipSet, _ := build.IPSet()
|
|
||||||
if len(ipSet.Prefixes()) == 0 {
|
|
||||||
return ipSet, fmt.Errorf(
|
|
||||||
"%w. %v isn't owned by a TagOwner and no forced tags are defined",
|
|
||||||
errInvalidTag,
|
|
||||||
alias,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return build.IPSet()
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter out machines per tag owner
|
|
||||||
for _, user := range owners {
|
|
||||||
machines := filterMachinesByUser(machines, user)
|
|
||||||
for _, machine := range machines {
|
|
||||||
hi := machine.GetHostInfo()
|
|
||||||
if contains(hi.RequestTags, alias) {
|
|
||||||
machine.IPAddresses.AppendToIPSet(&build)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return build.IPSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pol *ACLPolicy) getIPsForUser(
|
|
||||||
user string,
|
|
||||||
machines Machines,
|
|
||||||
stripEmailDomain bool,
|
|
||||||
) (*netipx.IPSet, error) {
|
|
||||||
build := netipx.IPSetBuilder{}
|
|
||||||
|
|
||||||
filteredMachines := filterMachinesByUser(machines, user)
|
|
||||||
filteredMachines = excludeCorrectlyTaggedNodes(pol, filteredMachines, user, stripEmailDomain)
|
|
||||||
|
|
||||||
// shortcurcuit if we have no machines to get ips from.
|
|
||||||
if len(filteredMachines) == 0 {
|
|
||||||
return nil, nil //nolint
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, machine := range filteredMachines {
|
|
||||||
machine.IPAddresses.AppendToIPSet(&build)
|
|
||||||
}
|
|
||||||
|
|
||||||
return build.IPSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pol *ACLPolicy) getIPsFromSingleIP(
|
|
||||||
ip netip.Addr,
|
|
||||||
machines Machines,
|
|
||||||
) (*netipx.IPSet, error) {
|
|
||||||
log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip")
|
|
||||||
|
|
||||||
matches := machines.FilterByIP(ip)
|
|
||||||
|
|
||||||
build := netipx.IPSetBuilder{}
|
|
||||||
build.Add(ip)
|
|
||||||
|
|
||||||
for _, machine := range matches {
|
|
||||||
machine.IPAddresses.AppendToIPSet(&build)
|
|
||||||
}
|
|
||||||
|
|
||||||
return build.IPSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pol *ACLPolicy) getIPsFromIPPrefix(
|
|
||||||
prefix netip.Prefix,
|
|
||||||
machines Machines,
|
|
||||||
) (*netipx.IPSet, error) {
|
|
||||||
log.Trace().Str("prefix", prefix.String()).Msg("expandAlias got prefix")
|
|
||||||
build := netipx.IPSetBuilder{}
|
|
||||||
build.AddPrefix(prefix)
|
|
||||||
|
|
||||||
// This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6
|
|
||||||
// addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers.
|
|
||||||
for _, machine := range machines {
|
|
||||||
for _, ip := range machine.IPAddresses {
|
|
||||||
// log.Trace().
|
|
||||||
// Msgf("checking if machine ip (%s) is part of prefix (%s): %v, is single ip prefix (%v), addr: %s", ip.String(), prefix.String(), prefix.Contains(ip), prefix.IsSingleIP(), prefix.Addr().String())
|
|
||||||
if prefix.Contains(ip) {
|
|
||||||
machine.IPAddresses.AppendToIPSet(&build)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return build.IPSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isWildcard(str string) bool {
|
|
||||||
return str == "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
func isGroup(str string) bool {
|
|
||||||
return strings.HasPrefix(str, "group:")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTag(str string) bool {
|
|
||||||
return strings.HasPrefix(str, "tag:")
|
|
||||||
}
|
}
|
||||||
|
|||||||
557
acls_test.go
557
acls_test.go
File diff suppressed because it is too large
Load Diff
@@ -111,8 +111,8 @@ func (hosts *Hosts) UnmarshalYAML(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsZero is perhaps a bit naive here.
|
// IsZero is perhaps a bit naive here.
|
||||||
func (pol ACLPolicy) IsZero() bool {
|
func (policy ACLPolicy) IsZero() bool {
|
||||||
if len(pol.Groups) == 0 && len(pol.Hosts) == 0 && len(pol.ACLs) == 0 {
|
if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
app.go
8
app.go
@@ -84,9 +84,11 @@ type Headscale struct {
|
|||||||
DERPMap *tailcfg.DERPMap
|
DERPMap *tailcfg.DERPMap
|
||||||
DERPServer *DERPServer
|
DERPServer *DERPServer
|
||||||
|
|
||||||
aclPolicy *ACLPolicy
|
aclPolicy *ACLPolicy
|
||||||
aclRules []tailcfg.FilterRule
|
aclRules []tailcfg.FilterRule
|
||||||
sshPolicy *tailcfg.SSHPolicy
|
aclPeerCacheMapRW sync.RWMutex
|
||||||
|
aclPeerCacheMap map[string][]string
|
||||||
|
sshPolicy *tailcfg.SSHPolicy
|
||||||
|
|
||||||
lastStateChange *xsync.MapOf[string, time.Time]
|
lastStateChange *xsync.MapOf[string, time.Time]
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/juanfont/headscale/integration"
|
|
||||||
"github.com/juanfont/headscale/integration/tsic"
|
|
||||||
"github.com/ory/dockertest/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.Printf("creating docker pool")
|
|
||||||
pool, err := dockertest.NewPool("")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("could not connect to docker: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("creating docker network")
|
|
||||||
network, err := pool.CreateNetwork("docker-integration-net")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to create or get network: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, version := range integration.TailscaleVersions {
|
|
||||||
log.Printf("creating container image for Tailscale (%s)", version)
|
|
||||||
|
|
||||||
tsClient, err := tsic.New(
|
|
||||||
pool,
|
|
||||||
version,
|
|
||||||
network,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to create tailscale node: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tsClient.Shutdown()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to shut down container: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
network.Close()
|
|
||||||
err = pool.RemoveNetwork(network)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to remove network: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,12 +58,11 @@ noise:
|
|||||||
# List of IP prefixes to allocate tailaddresses from.
|
# List of IP prefixes to allocate tailaddresses from.
|
||||||
# Each prefix consists of either an IPv4 or IPv6 address,
|
# Each prefix consists of either an IPv4 or IPv6 address,
|
||||||
# and the associated prefix length, delimited by a slash.
|
# and the associated prefix length, delimited by a slash.
|
||||||
# It must be within IP ranges supported by the Tailscale
|
# While this looks like it can take arbitrary values, it
|
||||||
# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48.
|
# needs to be within IP ranges supported by the Tailscale
|
||||||
# See below:
|
# client.
|
||||||
# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
|
# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
|
||||||
# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
|
# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
|
||||||
# Any other range is NOT supported, and it will cause unexpected issues.
|
|
||||||
ip_prefixes:
|
ip_prefixes:
|
||||||
- fd7a:115c:a1e0::/48
|
- fd7a:115c:a1e0::/48
|
||||||
- 100.64.0.0/10
|
- 100.64.0.0/10
|
||||||
|
|||||||
24
config.go
24
config.go
@@ -16,7 +16,6 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"tailscale.com/net/tsaddr"
|
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
)
|
)
|
||||||
@@ -516,29 +515,6 @@ func GetHeadscaleConfig() (*Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
|
panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if prefix.Addr().Is4() {
|
|
||||||
builder := netipx.IPSetBuilder{}
|
|
||||||
builder.AddPrefix(tsaddr.CGNATRange())
|
|
||||||
ipSet, _ := builder.IPSet()
|
|
||||||
if !ipSet.ContainsPrefix(prefix) {
|
|
||||||
log.Warn().
|
|
||||||
Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.",
|
|
||||||
prefixInConfig, tsaddr.CGNATRange())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefix.Addr().Is6() {
|
|
||||||
builder := netipx.IPSetBuilder{}
|
|
||||||
builder.AddPrefix(tsaddr.TailscaleULARange())
|
|
||||||
ipSet, _ := builder.IPSet()
|
|
||||||
if !ipSet.ContainsPrefix(prefix) {
|
|
||||||
log.Warn().
|
|
||||||
Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.",
|
|
||||||
prefixInConfig, tsaddr.TailscaleULARange())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedPrefixes = append(parsedPrefixes, prefix)
|
parsedPrefixes = append(parsedPrefixes, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ If the node is already registered, it can advertise exit capabilities like this:
|
|||||||
$ sudo tailscale set --advertise-exit-node
|
$ sudo tailscale set --advertise-exit-node
|
||||||
```
|
```
|
||||||
|
|
||||||
To use a node as an exit node, IP forwarding must be enabled on the node. Check the official [Tailscale documentation](https://tailscale.com/kb/1019/subnets/?tab=linux#enable-ip-forwarding) for how to enable IP fowarding.
|
|
||||||
|
|
||||||
## On the control server
|
## On the control server
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
|||||||
53
docs/faq.md
53
docs/faq.md
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
hide:
|
|
||||||
- navigation
|
|
||||||
---
|
|
||||||
|
|
||||||
# Frequently Asked Questions
|
|
||||||
|
|
||||||
## What is the design goal of headscale?
|
|
||||||
|
|
||||||
`headscale` aims to implement a self-hosted, open source alternative to the [Tailscale](https://tailscale.com/)
|
|
||||||
control server.
|
|
||||||
`headscale`'s goal is to provide self-hosters and hobbyists with an open-source
|
|
||||||
server they can use for their projects and labs.
|
|
||||||
It implements a narrow scope, a _single_ Tailnet, suitable for a personal use, or a small
|
|
||||||
open-source organisation.
|
|
||||||
|
|
||||||
## How can I contribute?
|
|
||||||
|
|
||||||
Headscale is "Open Source, acknowledged contribution", this means that any
|
|
||||||
contribution will have to be discussed with the Maintainers before being submitted.
|
|
||||||
|
|
||||||
Headscale is open to code contributions for bug fixes without discussion.
|
|
||||||
|
|
||||||
If you find mistakes in the documentation, please also submit a fix to the documentation.
|
|
||||||
|
|
||||||
## Why is 'acknowledged contribution' the chosen model?
|
|
||||||
|
|
||||||
Both maintainers have full-time jobs and families, and we want to avoid burnout. We also want to avoid frustration from contributors when their PRs are not accepted.
|
|
||||||
|
|
||||||
We are more than happy to exchange emails, or to have dedicated calls before a PR is submitted.
|
|
||||||
|
|
||||||
## When/Why is Feature X going to be implemented?
|
|
||||||
|
|
||||||
We don't know. We might be working on it. If you want to help, please send us a PR.
|
|
||||||
|
|
||||||
Please be aware that there are a number of reasons why we might not accept specific contributions:
|
|
||||||
|
|
||||||
- It is not possible to implement the feature in a way that makes sense in a self-hosted environment.
|
|
||||||
- Given that we are reverse-engineering Tailscale to satify our own curiosity, we might be interested in implementing the feature ourselves.
|
|
||||||
- You are not sending unit and integration tests with it.
|
|
||||||
|
|
||||||
## Do you support Y method of deploying Headscale?
|
|
||||||
|
|
||||||
We currently support deploying `headscale` using our binaries and the DEB packages. Both can be found in the
|
|
||||||
[GitHub releases page](https://github.com/juanfont/headscale/releases).
|
|
||||||
|
|
||||||
In addition to that, there are semi-official RPM packages by the Fedora infra team https://copr.fedorainfracloud.org/coprs/jonathanspw/headscale/
|
|
||||||
|
|
||||||
For convenience, we also build Docker images with `headscale`. But **please be aware that we don't officially support deploying `headscale` using Docker**. We have a [Discord channel](https://discord.com/channels/896711691637780480/1070619770942148618) where you can ask for Docker-specific help to the community.
|
|
||||||
|
|
||||||
## 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](https://headscale.net/reverse-proxy/) 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.
|
|
||||||
@@ -4,40 +4,9 @@ hide:
|
|||||||
- toc
|
- toc
|
||||||
---
|
---
|
||||||
|
|
||||||
# headscale
|
# headscale documentation
|
||||||
|
|
||||||
`headscale` is an open source, self-hosted implementation of the Tailscale control server.
|
This site contains the official and community contributed documentation for `headscale`.
|
||||||
|
|
||||||
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](/faq/).
|
If you are having trouble with following the documentation or get unexpected results,
|
||||||
|
please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue.
|
||||||
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support.
|
|
||||||
|
|
||||||
## Design goal
|
|
||||||
|
|
||||||
Headscale aims to implement a self-hosted, open source alternative to the Tailscale
|
|
||||||
control server.
|
|
||||||
Headscale's goal is to provide self-hosters and hobbyists with an open-source
|
|
||||||
server they can use for their projects and labs.
|
|
||||||
It implements a narrower scope, a single Tailnet, suitable for a personal use, or a small
|
|
||||||
open-source organisation.
|
|
||||||
|
|
||||||
## Supporting headscale
|
|
||||||
|
|
||||||
If you like `headscale` and find it useful, there is a sponsorship and donation
|
|
||||||
buttons available in the repo.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Headscale is "Open Source, acknowledged contribution", this means that any
|
|
||||||
contribution will have to be discussed with the Maintainers before being submitted.
|
|
||||||
|
|
||||||
This model has been chosen to reduce the risk of burnout by limiting the
|
|
||||||
maintenance overhead of reviewing and validating third-party code.
|
|
||||||
|
|
||||||
Headscale is open to code contributions for bug fixes without discussion.
|
|
||||||
|
|
||||||
If you find mistakes in the documentation, please submit a fix to the documentation.
|
|
||||||
|
|
||||||
## About
|
|
||||||
|
|
||||||
`headscale` is maintained by [Kristoffer Dalby](https://kradalby.no/) and [Juan Font](https://font.eu).
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ configuration (`/etc/headscale/config.yaml`).
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page](https://github.com/juanfont/headscale/releases):
|
1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page]():
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
wget --output-document=headscale.deb \
|
wget --output-document=headscale.deb \
|
||||||
|
|||||||
@@ -36,7 +36,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.
|
||||||
vendorSha256 = "sha256-cmDNYWYTgQp6CPgpL4d3TbkpAe7rhNAF+o8njJsgL7E=";
|
vendorSha256 = "sha256-Gu0RhzXnrZ5705X/1CbTfmZlIG9PkxBZzs1bqWQPqWg=";
|
||||||
|
|
||||||
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ];
|
||||||
};
|
};
|
||||||
@@ -99,7 +99,6 @@
|
|||||||
goreleaser
|
goreleaser
|
||||||
nfpm
|
nfpm
|
||||||
gotestsum
|
gotestsum
|
||||||
gotests
|
|
||||||
|
|
||||||
# 'dot' is needed for pprof graphs
|
# 'dot' is needed for pprof graphs
|
||||||
# go tool pprof -http=: <source>
|
# go tool pprof -http=: <source>
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -11,7 +11,6 @@ require (
|
|||||||
github.com/efekarakus/termcolor v1.0.1
|
github.com/efekarakus/termcolor v1.0.1
|
||||||
github.com/glebarez/sqlite v1.7.0
|
github.com/glebarez/sqlite v1.7.0
|
||||||
github.com/gofrs/uuid/v5 v5.0.0
|
github.com/gofrs/uuid/v5 v5.0.0
|
||||||
github.com/google/go-cmp v0.5.9
|
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
|
||||||
@@ -74,6 +73,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
|
||||||
@@ -143,7 +143,6 @@ require (
|
|||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gotest.tools/v3 v3.4.0 // indirect
|
|
||||||
modernc.org/libc v1.22.2 // indirect
|
modernc.org/libc v1.22.2 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
|||||||
3
go.sum
3
go.sum
@@ -900,8 +900,7 @@ gorm.io/driver/postgres v1.4.8/go.mod h1:O9MruWGNLUBUWVYfWuBClpf3HeGjOoybY0SNmCs
|
|||||||
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
|
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
|
||||||
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
|
gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
|
||||||
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
@@ -12,39 +12,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var veryLargeDestination = []string{
|
|
||||||
"0.0.0.0/5:*",
|
|
||||||
"8.0.0.0/7:*",
|
|
||||||
"11.0.0.0/8:*",
|
|
||||||
"12.0.0.0/6:*",
|
|
||||||
"16.0.0.0/4:*",
|
|
||||||
"32.0.0.0/3:*",
|
|
||||||
"64.0.0.0/2:*",
|
|
||||||
"128.0.0.0/3:*",
|
|
||||||
"160.0.0.0/5:*",
|
|
||||||
"168.0.0.0/6:*",
|
|
||||||
"172.0.0.0/12:*",
|
|
||||||
"172.32.0.0/11:*",
|
|
||||||
"172.64.0.0/10:*",
|
|
||||||
"172.128.0.0/9:*",
|
|
||||||
"173.0.0.0/8:*",
|
|
||||||
"174.0.0.0/7:*",
|
|
||||||
"176.0.0.0/4:*",
|
|
||||||
"192.0.0.0/9:*",
|
|
||||||
"192.128.0.0/11:*",
|
|
||||||
"192.160.0.0/13:*",
|
|
||||||
"192.169.0.0/16:*",
|
|
||||||
"192.170.0.0/15:*",
|
|
||||||
"192.172.0.0/14:*",
|
|
||||||
"192.176.0.0/12:*",
|
|
||||||
"192.192.0.0/10:*",
|
|
||||||
"193.0.0.0/8:*",
|
|
||||||
"194.0.0.0/7:*",
|
|
||||||
"196.0.0.0/6:*",
|
|
||||||
"200.0.0.0/5:*",
|
|
||||||
"208.0.0.0/4:*",
|
|
||||||
}
|
|
||||||
|
|
||||||
func aclScenario(t *testing.T, policy *headscale.ACLPolicy, clientsPerUser int) *Scenario {
|
func aclScenario(t *testing.T, policy *headscale.ACLPolicy, clientsPerUser int) *Scenario {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
scenario, err := NewScenario()
|
scenario, err := NewScenario()
|
||||||
@@ -209,34 +176,6 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
|||||||
"user2": 3, // ns1 + ns2 (return path)
|
"user2": 3, // ns1 + ns2 (return path)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"very-large-destination-prefix-1372": {
|
|
||||||
users: map[string]int{
|
|
||||||
"user1": 2,
|
|
||||||
"user2": 2,
|
|
||||||
},
|
|
||||||
policy: headscale.ACLPolicy{
|
|
||||||
ACLs: []headscale.ACL{
|
|
||||||
{
|
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"user1"},
|
|
||||||
Destinations: append([]string{"user1:*"}, veryLargeDestination...),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"user2"},
|
|
||||||
Destinations: append([]string{"user2:*"}, veryLargeDestination...),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: "accept",
|
|
||||||
Sources: []string{"user1"},
|
|
||||||
Destinations: append([]string{"user2:*"}, veryLargeDestination...),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, want: map[string]int{
|
|
||||||
"user1": 3, // ns1 + ns2
|
|
||||||
"user2": 3, // ns1 + ns2 (return path)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, testCase := range tests {
|
for name, testCase := range tests {
|
||||||
@@ -249,6 +188,7 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
|||||||
err = scenario.CreateHeadscaleEnv(spec,
|
err = scenario.CreateHeadscaleEnv(spec,
|
||||||
[]tsic.Option{},
|
[]tsic.Option{},
|
||||||
hsic.WithACLPolicy(&testCase.policy),
|
hsic.WithACLPolicy(&testCase.policy),
|
||||||
|
// hsic.WithTestName(fmt.Sprintf("aclinnetmap%s", name)),
|
||||||
)
|
)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -258,6 +198,9 @@ func TestACLHostsInNetMapTable(t *testing.T) {
|
|||||||
err = scenario.WaitForTailscaleSync()
|
err = scenario.WaitForTailscaleSync()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// allHostnames, err := scenario.ListTailscaleClientsFQDNs()
|
||||||
|
// assert.NoError(t, err)
|
||||||
|
|
||||||
for _, client := range allClients {
|
for _, client := range allClients {
|
||||||
status, err := client.Status()
|
status, err := client.Status()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ var (
|
|||||||
tailscaleVersions2021 = []string{
|
tailscaleVersions2021 = []string{
|
||||||
"head",
|
"head",
|
||||||
"unstable",
|
"unstable",
|
||||||
"1.40.0",
|
|
||||||
"1.38.4",
|
"1.38.4",
|
||||||
"1.36.2",
|
"1.36.2",
|
||||||
"1.34.2",
|
"1.34.2",
|
||||||
@@ -280,7 +279,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||||||
|
|
||||||
headscale, err := s.Headscale()
|
headscale, err := s.Headscale()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create tailscale node (version: %s): %w", version, err)
|
return fmt.Errorf("failed to create tailscale node: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cert := headscale.GetCert()
|
cert := headscale.GetCert()
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ func TestSSUserOnlyIsolation(t *testing.T) {
|
|||||||
// TODO(kradalby,evenh): ACLs do currently not cover reject
|
// TODO(kradalby,evenh): ACLs do currently not cover reject
|
||||||
// cases properly, and currently will accept all incomming connections
|
// cases properly, and currently will accept all incomming connections
|
||||||
// as long as a rule is present.
|
// as long as a rule is present.
|
||||||
|
//
|
||||||
// for _, client := range ssh1Clients {
|
// for _, client := range ssh1Clients {
|
||||||
// for _, peer := range ssh2Clients {
|
// for _, peer := range ssh2Clients {
|
||||||
// if client.Hostname() == peer.Hostname() {
|
// if client.Hostname() == peer.Hostname() {
|
||||||
|
|||||||
@@ -212,11 +212,7 @@ func New(
|
|||||||
dockertestutil.DockerAllowNetworkAdministration,
|
dockertestutil.DockerAllowNetworkAdministration,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("could not start tailscale container: %w", err)
|
||||||
"could not start tailscale container (version: %s): %w",
|
|
||||||
version,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
log.Printf("Created %s container\n", hostname)
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
|
|||||||
166
machine.go
166
machine.go
@@ -4,16 +4,17 @@ import (
|
|||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"go4.org/netipx"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@@ -98,14 +99,6 @@ func (ma MachineAddresses) ToStringSlice() []string {
|
|||||||
return strSlice
|
return strSlice
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendToIPSet adds the individual ips in MachineAddresses to a
|
|
||||||
// given netipx.IPSetBuilder.
|
|
||||||
func (ma MachineAddresses) AppendToIPSet(build *netipx.IPSetBuilder) {
|
|
||||||
for _, ip := range ma {
|
|
||||||
build.Add(ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *MachineAddresses) Scan(destination interface{}) error {
|
func (ma *MachineAddresses) Scan(destination interface{}) error {
|
||||||
switch value := destination.(type) {
|
switch value := destination.(type) {
|
||||||
case string:
|
case string:
|
||||||
@@ -169,48 +162,149 @@ func (machine *Machine) isEphemeral() bool {
|
|||||||
return machine.AuthKey != nil && machine.AuthKey.Ephemeral
|
return machine.AuthKey != nil && machine.AuthKey.Ephemeral
|
||||||
}
|
}
|
||||||
|
|
||||||
func (machine *Machine) canAccess(filter []tailcfg.FilterRule, machine2 *Machine) bool {
|
|
||||||
for _, rule := range filter {
|
|
||||||
// TODO(kradalby): Cache or pregen this
|
|
||||||
matcher := MatchFromFilterRule(rule)
|
|
||||||
|
|
||||||
if !matcher.SrcsContainsIPs([]netip.Addr(machine.IPAddresses)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if matcher.DestsContainsIP([]netip.Addr(machine2.IPAddresses)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterMachinesByACL wrapper function to not have devs pass around locks and maps
|
// filterMachinesByACL wrapper function to not have devs pass around locks and maps
|
||||||
// related to the application outside of tests.
|
// related to the application outside of tests.
|
||||||
func (h *Headscale) filterMachinesByACL(currentMachine *Machine, peers Machines) Machines {
|
func (h *Headscale) filterMachinesByACL(currentMachine *Machine, peers Machines) Machines {
|
||||||
return filterMachinesByACL(currentMachine, peers, h.aclRules)
|
return filterMachinesByACL(currentMachine, peers, &h.aclPeerCacheMapRW, h.aclPeerCacheMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterMachinesByACL returns the list of peers authorized to be accessed from a given machine.
|
// filterMachinesByACL returns the list of peers authorized to be accessed from a given machine.
|
||||||
func filterMachinesByACL(
|
func filterMachinesByACL(
|
||||||
machine *Machine,
|
machine *Machine,
|
||||||
machines Machines,
|
machines Machines,
|
||||||
filter []tailcfg.FilterRule,
|
lock *sync.RWMutex,
|
||||||
|
aclPeerCacheMap map[string][]string,
|
||||||
) Machines {
|
) Machines {
|
||||||
result := Machines{}
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("self", machine.Hostname).
|
||||||
|
Str("input", machines.String()).
|
||||||
|
Msg("Finding peers filtered by ACLs")
|
||||||
|
|
||||||
for index, peer := range machines {
|
peers := make(map[uint64]Machine)
|
||||||
|
// Aclfilter peers here. We are itering through machines in all users and search through the computed aclRules
|
||||||
|
// for match between rule SrcIPs and DstPorts. If the rule is a match we allow the machine to be viewable.
|
||||||
|
machineIPs := machine.IPAddresses.ToStringSlice()
|
||||||
|
|
||||||
|
// TODO(kradalby): Remove this lock, I suspect its not a good idea, and might not be necessary,
|
||||||
|
// we only set this at startup atm (reading ACLs) and it might become a bottleneck.
|
||||||
|
lock.RLock()
|
||||||
|
|
||||||
|
for _, peer := range machines {
|
||||||
if peer.ID == machine.ID {
|
if peer.ID == machine.ID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
peerIPs := peer.IPAddresses.ToStringSlice()
|
||||||
|
|
||||||
if machine.canAccess(filter, &machines[index]) || peer.canAccess(filter, machine) {
|
if dstMap, ok := aclPeerCacheMap["*"]; ok {
|
||||||
result = append(result, peer)
|
// match source and all destination
|
||||||
|
|
||||||
|
for _, dst := range dstMap {
|
||||||
|
if dst == "*" {
|
||||||
|
peers[peer.ID] = peer
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match source and all destination
|
||||||
|
for _, peerIP := range peerIPs {
|
||||||
|
for _, dst := range dstMap {
|
||||||
|
_, cdr, _ := net.ParseCIDR(dst)
|
||||||
|
ip := net.ParseIP(peerIP)
|
||||||
|
if dst == peerIP || (cdr != nil && ip != nil && cdr.Contains(ip)) {
|
||||||
|
peers[peer.ID] = peer
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match all sources and source
|
||||||
|
for _, machineIP := range machineIPs {
|
||||||
|
for _, dst := range dstMap {
|
||||||
|
_, cdr, _ := net.ParseCIDR(dst)
|
||||||
|
ip := net.ParseIP(machineIP)
|
||||||
|
if dst == machineIP || (cdr != nil && ip != nil && cdr.Contains(ip)) {
|
||||||
|
peers[peer.ID] = peer
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, machineIP := range machineIPs {
|
||||||
|
if dstMap, ok := aclPeerCacheMap[machineIP]; ok {
|
||||||
|
// match source and all destination
|
||||||
|
for _, dst := range dstMap {
|
||||||
|
if dst == "*" {
|
||||||
|
peers[peer.ID] = peer
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match source and destination
|
||||||
|
for _, peerIP := range peerIPs {
|
||||||
|
for _, dst := range dstMap {
|
||||||
|
_, cdr, _ := net.ParseCIDR(dst)
|
||||||
|
ip := net.ParseIP(peerIP)
|
||||||
|
if dst == peerIP || (cdr != nil && ip != nil && cdr.Contains(ip)) {
|
||||||
|
peers[peer.ID] = peer
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peerIP := range peerIPs {
|
||||||
|
if dstMap, ok := aclPeerCacheMap[peerIP]; ok {
|
||||||
|
// match source and all destination
|
||||||
|
for _, dst := range dstMap {
|
||||||
|
if dst == "*" {
|
||||||
|
peers[peer.ID] = peer
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match return path
|
||||||
|
for _, machineIP := range machineIPs {
|
||||||
|
for _, dst := range dstMap {
|
||||||
|
_, cdr, _ := net.ParseCIDR(dst)
|
||||||
|
ip := net.ParseIP(machineIP)
|
||||||
|
if dst == machineIP || (cdr != nil && ip != nil && cdr.Contains(ip)) {
|
||||||
|
peers[peer.ID] = peer
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
lock.RUnlock()
|
||||||
|
|
||||||
|
authorizedPeers := make(Machines, 0, len(peers))
|
||||||
|
for _, m := range peers {
|
||||||
|
authorizedPeers = append(authorizedPeers, m)
|
||||||
|
}
|
||||||
|
sort.Slice(
|
||||||
|
authorizedPeers,
|
||||||
|
func(i, j int) bool { return authorizedPeers[i].ID < authorizedPeers[j].ID },
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Trace().
|
||||||
|
Caller().
|
||||||
|
Str("self", machine.Hostname).
|
||||||
|
Str("peers", authorizedPeers.String()).
|
||||||
|
Msg("Authorized peers")
|
||||||
|
|
||||||
|
return authorizedPeers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
|
func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
|
||||||
@@ -799,7 +893,7 @@ func getTags(
|
|||||||
validTagMap := make(map[string]bool)
|
validTagMap := make(map[string]bool)
|
||||||
invalidTagMap := make(map[string]bool)
|
invalidTagMap := make(map[string]bool)
|
||||||
for _, tag := range machine.HostInfo.RequestTags {
|
for _, tag := range machine.HostInfo.RequestTags {
|
||||||
owners, err := getTagOwners(aclPolicy, tag, stripEmailDomain)
|
owners, err := expandTagOwners(*aclPolicy, tag, stripEmailDomain)
|
||||||
if errors.Is(err, errInvalidTag) {
|
if errors.Is(err, errInvalidTag) {
|
||||||
invalidTagMap[tag] = true
|
invalidTagMap[tag] = true
|
||||||
|
|
||||||
@@ -1113,7 +1207,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error {
|
|||||||
if approvedAlias == machine.User.Name {
|
if approvedAlias == machine.User.Name {
|
||||||
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
||||||
} else {
|
} else {
|
||||||
approvedIps, err := h.aclPolicy.expandAlias([]Machine{*machine}, approvedAlias, h.cfg.OIDC.StripEmaildomain)
|
approvedIps, err := expandAlias([]Machine{*machine}, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).
|
log.Err(err).
|
||||||
Str("alias", approvedAlias).
|
Str("alias", approvedAlias).
|
||||||
@@ -1123,7 +1217,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// approvedIPs should contain all of machine's IPs if it matches the rule, so check for first
|
// approvedIPs should contain all of machine's IPs if it matches the rule, so check for first
|
||||||
if approvedIps.Contains(machine.IPAddresses[0]) {
|
if contains(approvedIps, machine.IPAddresses[0].String()) {
|
||||||
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
approvedRoutes = append(approvedRoutes, advertisedRoute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
135
machine_test.go
135
machine_test.go
@@ -6,6 +6,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -1040,12 +1041,16 @@ func Test_getFilteredByACLPeers(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
var lock sync.RWMutex
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
aclRulesMap := generateACLPeerCacheMap(tt.args.rules)
|
||||||
|
|
||||||
got := filterMachinesByACL(
|
got := filterMachinesByACL(
|
||||||
tt.args.machine,
|
tt.args.machine,
|
||||||
tt.args.machines,
|
tt.args.machines,
|
||||||
tt.args.rules,
|
&lock,
|
||||||
|
aclRulesMap,
|
||||||
)
|
)
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("filterMachinesByACL() = %v, want %v", got, tt.want)
|
t.Errorf("filterMachinesByACL() = %v, want %v", got, tt.want)
|
||||||
@@ -1259,131 +1264,3 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
|
|||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(enabledRoutes, check.HasLen, 3)
|
c.Assert(enabledRoutes, check.HasLen, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMachine_canAccess(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
filter []tailcfg.FilterRule
|
|
||||||
machine2 *Machine
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
machine Machine
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no-rules",
|
|
||||||
machine: Machine{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("10.0.0.1"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
filter: []tailcfg.FilterRule{},
|
|
||||||
machine2: &Machine{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("10.0.0.2"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard",
|
|
||||||
machine: Machine{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("10.0.0.1"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
filter: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"*"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{
|
|
||||||
IP: "*",
|
|
||||||
Ports: tailcfg.PortRange{
|
|
||||||
First: 0,
|
|
||||||
Last: 65535,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
machine2: &Machine{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("10.0.0.2"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit-m1-to-m2",
|
|
||||||
machine: Machine{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("10.0.0.1"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
filter: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"10.0.0.1"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{
|
|
||||||
IP: "10.0.0.2",
|
|
||||||
Ports: tailcfg.PortRange{
|
|
||||||
First: 0,
|
|
||||||
Last: 65535,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
machine2: &Machine{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("10.0.0.2"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit-m2-to-m1",
|
|
||||||
machine: Machine{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("10.0.0.1"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
filter: []tailcfg.FilterRule{
|
|
||||||
{
|
|
||||||
SrcIPs: []string{"10.0.0.2"},
|
|
||||||
DstPorts: []tailcfg.NetPortRange{
|
|
||||||
{
|
|
||||||
IP: "10.0.0.1",
|
|
||||||
Ports: tailcfg.PortRange{
|
|
||||||
First: 0,
|
|
||||||
Last: 65535,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
machine2: &Machine{
|
|
||||||
IPAddresses: MachineAddresses{
|
|
||||||
netip.MustParseAddr("10.0.0.2"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := tt.machine.canAccess(tt.args.filter, tt.args.machine2); got != tt.want {
|
|
||||||
t.Errorf("Machine.canAccess() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
142
matcher.go
142
matcher.go
@@ -1,142 +0,0 @@
|
|||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go4.org/netipx"
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is borrowed from, and updated to use IPSet
|
|
||||||
// https://github.com/tailscale/tailscale/blob/71029cea2ddf82007b80f465b256d027eab0f02d/wgengine/filter/tailcfg.go#L97-L162
|
|
||||||
// TODO(kradalby): contribute upstream and make public.
|
|
||||||
var (
|
|
||||||
zeroIP4 = netip.AddrFrom4([4]byte{})
|
|
||||||
zeroIP6 = netip.AddrFrom16([16]byte{})
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseIPSet parses arg as one:
|
|
||||||
//
|
|
||||||
// - an IP address (IPv4 or IPv6)
|
|
||||||
// - the string "*" to match everything (both IPv4 & IPv6)
|
|
||||||
// - a CIDR (e.g. "192.168.0.0/16")
|
|
||||||
// - a range of two IPs, inclusive, separated by hyphen ("2eff::1-2eff::0800")
|
|
||||||
//
|
|
||||||
// bits, if non-nil, is the legacy SrcBits CIDR length to make a IP
|
|
||||||
// address (without a slash) treated as a CIDR of *bits length.
|
|
||||||
// nolint
|
|
||||||
func parseIPSet(arg string, bits *int) (*netipx.IPSet, error) {
|
|
||||||
var ipSet netipx.IPSetBuilder
|
|
||||||
if arg == "*" {
|
|
||||||
ipSet.AddPrefix(netip.PrefixFrom(zeroIP4, 0))
|
|
||||||
ipSet.AddPrefix(netip.PrefixFrom(zeroIP6, 0))
|
|
||||||
|
|
||||||
return ipSet.IPSet()
|
|
||||||
}
|
|
||||||
if strings.Contains(arg, "/") {
|
|
||||||
pfx, err := netip.ParsePrefix(arg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if pfx != pfx.Masked() {
|
|
||||||
return nil, fmt.Errorf("%v contains non-network bits set", pfx)
|
|
||||||
}
|
|
||||||
|
|
||||||
ipSet.AddPrefix(pfx)
|
|
||||||
|
|
||||||
return ipSet.IPSet()
|
|
||||||
}
|
|
||||||
if strings.Count(arg, "-") == 1 {
|
|
||||||
ip1s, ip2s, _ := strings.Cut(arg, "-")
|
|
||||||
|
|
||||||
ip1, err := netip.ParseAddr(ip1s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ip2, err := netip.ParseAddr(ip2s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r := netipx.IPRangeFrom(ip1, ip2)
|
|
||||||
if !r.IsValid() {
|
|
||||||
return nil, fmt.Errorf("invalid IP range %q", arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, prefix := range r.Prefixes() {
|
|
||||||
ipSet.AddPrefix(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipSet.IPSet()
|
|
||||||
}
|
|
||||||
ip, err := netip.ParseAddr(arg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid IP address %q", arg)
|
|
||||||
}
|
|
||||||
bits8 := uint8(ip.BitLen())
|
|
||||||
if bits != nil {
|
|
||||||
if *bits < 0 || *bits > int(bits8) {
|
|
||||||
return nil, fmt.Errorf("invalid CIDR size %d for IP %q", *bits, arg)
|
|
||||||
}
|
|
||||||
bits8 = uint8(*bits)
|
|
||||||
}
|
|
||||||
|
|
||||||
ipSet.AddPrefix(netip.PrefixFrom(ip, int(bits8)))
|
|
||||||
|
|
||||||
return ipSet.IPSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Match struct {
|
|
||||||
Srcs *netipx.IPSet
|
|
||||||
Dests *netipx.IPSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func MatchFromFilterRule(rule tailcfg.FilterRule) Match {
|
|
||||||
srcs := new(netipx.IPSetBuilder)
|
|
||||||
dests := new(netipx.IPSetBuilder)
|
|
||||||
|
|
||||||
for _, srcIP := range rule.SrcIPs {
|
|
||||||
set, _ := parseIPSet(srcIP, nil)
|
|
||||||
|
|
||||||
srcs.AddSet(set)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dest := range rule.DstPorts {
|
|
||||||
set, _ := parseIPSet(dest.IP, nil)
|
|
||||||
|
|
||||||
dests.AddSet(set)
|
|
||||||
}
|
|
||||||
|
|
||||||
srcsSet, _ := srcs.IPSet()
|
|
||||||
destsSet, _ := dests.IPSet()
|
|
||||||
|
|
||||||
match := Match{
|
|
||||||
Srcs: srcsSet,
|
|
||||||
Dests: destsSet,
|
|
||||||
}
|
|
||||||
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Match) SrcsContainsIPs(ips []netip.Addr) bool {
|
|
||||||
for _, ip := range ips {
|
|
||||||
if m.Srcs.Contains(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Match) DestsContainsIP(ips []netip.Addr) bool {
|
|
||||||
for _, ip := range ips {
|
|
||||||
if m.Dests.Contains(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
119
matcher_test.go
119
matcher_test.go
@@ -1,119 +0,0 @@
|
|||||||
package headscale
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go4.org/netipx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_parseIPSet(t *testing.T) {
|
|
||||||
set := func(ips []string, prefixes []string) *netipx.IPSet {
|
|
||||||
var builder netipx.IPSetBuilder
|
|
||||||
|
|
||||||
for _, ip := range ips {
|
|
||||||
builder.Add(netip.MustParseAddr(ip))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pre := range prefixes {
|
|
||||||
builder.AddPrefix(netip.MustParsePrefix(pre))
|
|
||||||
}
|
|
||||||
|
|
||||||
s, _ := builder.IPSet()
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
arg string
|
|
||||||
bits *int
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want *netipx.IPSet
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple ip4",
|
|
||||||
args: args{
|
|
||||||
arg: "10.0.0.1",
|
|
||||||
bits: nil,
|
|
||||||
},
|
|
||||||
want: set([]string{
|
|
||||||
"10.0.0.1",
|
|
||||||
}, []string{}),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simple ip6",
|
|
||||||
args: args{
|
|
||||||
arg: "2001:db8:abcd:1234::2",
|
|
||||||
bits: nil,
|
|
||||||
},
|
|
||||||
want: set([]string{
|
|
||||||
"2001:db8:abcd:1234::2",
|
|
||||||
}, []string{}),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard",
|
|
||||||
args: args{
|
|
||||||
arg: "*",
|
|
||||||
bits: nil,
|
|
||||||
},
|
|
||||||
want: set([]string{}, []string{
|
|
||||||
"0.0.0.0/0",
|
|
||||||
"::/0",
|
|
||||||
}),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefix4",
|
|
||||||
args: args{
|
|
||||||
arg: "192.168.0.0/16",
|
|
||||||
bits: nil,
|
|
||||||
},
|
|
||||||
want: set([]string{}, []string{
|
|
||||||
"192.168.0.0/16",
|
|
||||||
}),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefix6",
|
|
||||||
args: args{
|
|
||||||
arg: "2001:db8:abcd:1234::/64",
|
|
||||||
bits: nil,
|
|
||||||
},
|
|
||||||
want: set([]string{}, []string{
|
|
||||||
"2001:db8:abcd:1234::/64",
|
|
||||||
}),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "range4",
|
|
||||||
args: args{
|
|
||||||
arg: "192.168.0.0-192.168.255.255",
|
|
||||||
bits: nil,
|
|
||||||
},
|
|
||||||
want: set([]string{}, []string{
|
|
||||||
"192.168.0.0/16",
|
|
||||||
}),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := parseIPSet(tt.args.arg, tt.args.bits)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("parseIPSet() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("parseIPSet() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
site_name: Headscale
|
site_name: Headscale
|
||||||
site_url: https://juanfont.github.io/headscale
|
site_url: https://juanfont.github.io/headscale
|
||||||
edit_uri: blob/main/docs/ # Change the master branch to main as we are using main as a main branch
|
|
||||||
site_author: Headscale authors
|
site_author: Headscale authors
|
||||||
site_description: >-
|
site_description: >-
|
||||||
An open source, self-hosted implementation of the Tailscale control server.
|
An open source, self-hosted implementation of the Tailscale control server.
|
||||||
@@ -122,7 +121,6 @@ markdown_extensions:
|
|||||||
# Page tree
|
# Page tree
|
||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- FAQ: faq.md
|
|
||||||
- Getting started:
|
- Getting started:
|
||||||
- Installation:
|
- Installation:
|
||||||
- Linux: running-headscale-linux.md
|
- Linux: running-headscale-linux.md
|
||||||
|
|||||||
112
noise.go
112
noise.go
@@ -1,9 +1,6 @@
|
|||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -12,37 +9,18 @@ import (
|
|||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
"tailscale.com/control/controlbase"
|
"tailscale.com/control/controlbase"
|
||||||
"tailscale.com/control/controlhttp"
|
"tailscale.com/control/controlhttp"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/net/netutil"
|
||||||
"tailscale.com/types/key"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade.
|
// ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade.
|
||||||
ts2021UpgradePath = "/ts2021"
|
ts2021UpgradePath = "/ts2021"
|
||||||
|
|
||||||
// The first 9 bytes from the server to client over Noise are either an HTTP/2
|
|
||||||
// settings frame (a normal HTTP/2 setup) or, as Tailscale added later, an "early payload"
|
|
||||||
// header that's also 9 bytes long: 5 bytes (earlyPayloadMagic) followed by 4 bytes
|
|
||||||
// of length. Then that many bytes of JSON-encoded tailcfg.EarlyNoise.
|
|
||||||
// The early payload is optional. Some servers may not send it... But we do!
|
|
||||||
earlyPayloadMagic = "\xff\xff\xffTS"
|
|
||||||
|
|
||||||
// EarlyNoise was added in protocol version 49.
|
|
||||||
earlyNoiseCapabilityVersion = 49
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type noiseServer struct {
|
type ts2021App struct {
|
||||||
headscale *Headscale
|
headscale *Headscale
|
||||||
|
|
||||||
httpBaseConfig *http.Server
|
conn *controlbase.Conn
|
||||||
http2Server *http2.Server
|
|
||||||
conn *controlbase.Conn
|
|
||||||
machineKey key.MachinePublic
|
|
||||||
nodeKey key.NodePublic
|
|
||||||
|
|
||||||
// EarlyNoise-related stuff
|
|
||||||
challenge key.ChallengePrivate
|
|
||||||
protocolVersion int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn
|
// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn
|
||||||
@@ -66,18 +44,7 @@ func (h *Headscale) NoiseUpgradeHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
noiseServer := noiseServer{
|
noiseConn, err := controlhttp.AcceptHTTP(req.Context(), writer, req, *h.noisePrivateKey, nil)
|
||||||
headscale: h,
|
|
||||||
challenge: key.NewChallenge(),
|
|
||||||
}
|
|
||||||
|
|
||||||
noiseConn, err := controlhttp.AcceptHTTP(
|
|
||||||
req.Context(),
|
|
||||||
writer,
|
|
||||||
req,
|
|
||||||
*h.noisePrivateKey,
|
|
||||||
noiseServer.earlyNoise,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("noise upgrade failed")
|
log.Error().Err(err).Msg("noise upgrade failed")
|
||||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||||
@@ -85,9 +52,10 @@ func (h *Headscale) NoiseUpgradeHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
noiseServer.conn = noiseConn
|
ts2021App := ts2021App{
|
||||||
noiseServer.machineKey = noiseServer.conn.Peer()
|
headscale: h,
|
||||||
noiseServer.protocolVersion = noiseServer.conn.ProtocolVersion()
|
conn: noiseConn,
|
||||||
|
}
|
||||||
|
|
||||||
// This router is served only over the Noise connection, and exposes only the new API.
|
// This router is served only over the Noise connection, and exposes only the new API.
|
||||||
//
|
//
|
||||||
@@ -95,70 +63,16 @@ func (h *Headscale) NoiseUpgradeHandler(
|
|||||||
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
|
// a single hijacked connection from /ts2021, using netutil.NewOneConnListener
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler).
|
router.HandleFunc("/machine/register", ts2021App.NoiseRegistrationHandler).
|
||||||
Methods(http.MethodPost)
|
Methods(http.MethodPost)
|
||||||
router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler)
|
router.HandleFunc("/machine/map", ts2021App.NoisePollNetMapHandler)
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
ReadTimeout: HTTPReadTimeout,
|
ReadTimeout: HTTPReadTimeout,
|
||||||
}
|
}
|
||||||
|
server.Handler = h2c.NewHandler(router, &http2.Server{})
|
||||||
noiseServer.httpBaseConfig = &http.Server{
|
err = server.Serve(netutil.NewOneConnListener(noiseConn, nil))
|
||||||
Handler: router,
|
|
||||||
ReadHeaderTimeout: HTTPReadTimeout,
|
|
||||||
}
|
|
||||||
noiseServer.http2Server = &http2.Server{}
|
|
||||||
|
|
||||||
server.Handler = h2c.NewHandler(router, noiseServer.http2Server)
|
|
||||||
|
|
||||||
noiseServer.http2Server.ServeConn(
|
|
||||||
noiseConn,
|
|
||||||
&http2.ServeConnOpts{
|
|
||||||
BaseConfig: noiseServer.httpBaseConfig,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error {
|
|
||||||
log.Trace().
|
|
||||||
Caller().
|
|
||||||
Int("protocol_version", protocolVersion).
|
|
||||||
Str("challenge", ns.challenge.Public().String()).
|
|
||||||
Msg("earlyNoise called")
|
|
||||||
|
|
||||||
if protocolVersion < earlyNoiseCapabilityVersion {
|
|
||||||
log.Trace().
|
|
||||||
Caller().
|
|
||||||
Msgf("protocol version %d does not support early noise", protocolVersion)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
earlyJSON, err := json.Marshal(&tailcfg.EarlyNoise{
|
|
||||||
NodeKeyChallenge: ns.challenge.Public(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Info().Err(err).Msg("The HTTP2 server was closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5 bytes that won't be mistaken for an HTTP/2 frame:
|
|
||||||
// https://httpwg.org/specs/rfc7540.html#rfc.section.4.1 (Especially not
|
|
||||||
// an HTTP/2 settings frame, which isn't of type 'T')
|
|
||||||
var notH2Frame [5]byte
|
|
||||||
copy(notH2Frame[:], earlyPayloadMagic)
|
|
||||||
var lenBuf [4]byte
|
|
||||||
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(earlyJSON)))
|
|
||||||
// These writes are all buffered by caller, so fine to do them
|
|
||||||
// separately:
|
|
||||||
if _, err := writer.Write(notH2Frame[:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := writer.Write(lenBuf[:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := writer.Write(earlyJSON); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// // NoiseRegistrationHandler handles the actual registration process of a machine.
|
// // NoiseRegistrationHandler handles the actual registration process of a machine.
|
||||||
func (ns *noiseServer) NoiseRegistrationHandler(
|
func (t *ts2021App) NoiseRegistrationHandler(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
@@ -20,11 +20,6 @@ func (ns *noiseServer) NoiseRegistrationHandler(
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().
|
|
||||||
Any("headers", req.Header).
|
|
||||||
Msg("Headers")
|
|
||||||
|
|
||||||
body, _ := io.ReadAll(req.Body)
|
body, _ := io.ReadAll(req.Body)
|
||||||
registerRequest := tailcfg.RegisterRequest{}
|
registerRequest := tailcfg.RegisterRequest{}
|
||||||
if err := json.Unmarshal(body, ®isterRequest); err != nil {
|
if err := json.Unmarshal(body, ®isterRequest); err != nil {
|
||||||
@@ -38,7 +33,5 @@ func (ns *noiseServer) NoiseRegistrationHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ns.nodeKey = registerRequest.NodeKey
|
t.headscale.handleRegisterCommon(writer, req, registerRequest, t.conn.Peer(), true)
|
||||||
|
|
||||||
ns.headscale.handleRegisterCommon(writer, req, registerRequest, ns.conn.Peer(), true)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,18 +21,13 @@ import (
|
|||||||
// only after their first request (marked with the ReadOnly field).
|
// only after their first request (marked with the ReadOnly field).
|
||||||
//
|
//
|
||||||
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
|
// At this moment the updates are sent in a quite horrendous way, but they kinda work.
|
||||||
func (ns *noiseServer) NoisePollNetMapHandler(
|
func (t *ts2021App) NoisePollNetMapHandler(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Str("handler", "NoisePollNetMap").
|
Str("handler", "NoisePollNetMap").
|
||||||
Msg("PollNetMapHandler called")
|
Msg("PollNetMapHandler called")
|
||||||
|
|
||||||
log.Trace().
|
|
||||||
Any("headers", req.Header).
|
|
||||||
Msg("Headers")
|
|
||||||
|
|
||||||
body, _ := io.ReadAll(req.Body)
|
body, _ := io.ReadAll(req.Body)
|
||||||
|
|
||||||
mapRequest := tailcfg.MapRequest{}
|
mapRequest := tailcfg.MapRequest{}
|
||||||
@@ -46,9 +41,7 @@ func (ns *noiseServer) NoisePollNetMapHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ns.nodeKey = mapRequest.NodeKey
|
machine, err := t.headscale.GetMachineByAnyKey(t.conn.Peer(), mapRequest.NodeKey, key.NodePublic{})
|
||||||
|
|
||||||
machine, err := ns.headscale.GetMachineByAnyKey(ns.conn.Peer(), mapRequest.NodeKey, key.NodePublic{})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.Warn().
|
log.Warn().
|
||||||
@@ -70,5 +63,5 @@ func (ns *noiseServer) NoisePollNetMapHandler(
|
|||||||
Str("machine", machine.Hostname).
|
Str("machine", machine.Hostname).
|
||||||
Msg("A machine is entering polling via the Noise protocol")
|
Msg("A machine is entering polling via the Noise protocol")
|
||||||
|
|
||||||
ns.headscale.handlePollCommon(writer, req.Context(), machine, mapRequest, true)
|
t.headscale.handlePollCommon(writer, req.Context(), machine, mapRequest, true)
|
||||||
}
|
}
|
||||||
|
|||||||
54
routes.go
54
routes.go
@@ -106,36 +106,13 @@ func (h *Headscale) DisableRoute(id uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tailscale requires both IPv4 and IPv6 exit routes to
|
route.Enabled = false
|
||||||
// be enabled at the same time, as per
|
route.IsPrimary = false
|
||||||
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
|
err = h.db.Save(route).Error
|
||||||
if !route.isExitRoute() {
|
|
||||||
route.Enabled = false
|
|
||||||
route.IsPrimary = false
|
|
||||||
err = h.db.Save(route).Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.handlePrimarySubnetFailover()
|
|
||||||
}
|
|
||||||
|
|
||||||
routes, err := h.GetMachineRoutes(&route.Machine)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range routes {
|
|
||||||
if routes[i].isExitRoute() {
|
|
||||||
routes[i].Enabled = false
|
|
||||||
routes[i].IsPrimary = false
|
|
||||||
err = h.db.Save(&routes[i]).Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.handlePrimarySubnetFailover()
|
return h.handlePrimarySubnetFailover()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,30 +122,7 @@ func (h *Headscale) DeleteRoute(id uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tailscale requires both IPv4 and IPv6 exit routes to
|
if err := h.db.Unscoped().Delete(&route).Error; err != nil {
|
||||||
// be enabled at the same time, as per
|
|
||||||
// https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002
|
|
||||||
if !route.isExitRoute() {
|
|
||||||
if err := h.db.Unscoped().Delete(&route).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.handlePrimarySubnetFailover()
|
|
||||||
}
|
|
||||||
|
|
||||||
routes, err := h.GetMachineRoutes(&route.Machine)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
routesToDelete := []Route{}
|
|
||||||
for _, r := range routes {
|
|
||||||
if r.isExitRoute() {
|
|
||||||
routesToDelete = append(routesToDelete, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.db.Unscoped().Delete(&routesToDelete).Error; err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -457,37 +457,6 @@ func (s *Suite) TestAllowedIPRoutes(c *check.C) {
|
|||||||
|
|
||||||
c.Assert(foundExitNodeV4, check.Equals, true)
|
c.Assert(foundExitNodeV4, check.Equals, true)
|
||||||
c.Assert(foundExitNodeV6, check.Equals, true)
|
c.Assert(foundExitNodeV6, check.Equals, true)
|
||||||
|
|
||||||
// Now we disable only one of the exit routes
|
|
||||||
// and we see if both are disabled
|
|
||||||
var exitRouteV4 Route
|
|
||||||
for _, route := range routes {
|
|
||||||
if route.isExitRoute() && netip.Prefix(route.Prefix) == prefixExitNodeV4 {
|
|
||||||
exitRouteV4 = route
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = app.DisableRoute(uint64(exitRouteV4.ID))
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
enabledRoutes1, err = app.GetEnabledRoutes(&machine1)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
c.Assert(len(enabledRoutes1), check.Equals, 1)
|
|
||||||
|
|
||||||
// and now we delete only one of the exit routes
|
|
||||||
// and we check if both are deleted
|
|
||||||
routes, err = app.GetMachineRoutes(&machine1)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
c.Assert(len(routes), check.Equals, 4)
|
|
||||||
|
|
||||||
err = app.DeleteRoute(uint64(exitRouteV4.ID))
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
routes, err = app.GetMachineRoutes(&machine1)
|
|
||||||
c.Assert(err, check.IsNil)
|
|
||||||
c.Assert(len(routes), check.Equals, 2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestDeleteRoutes(c *check.C) {
|
func (s *Suite) TestDeleteRoutes(c *check.C) {
|
||||||
|
|||||||
Reference in New Issue
Block a user