Compare commits

..

23 Commits

Author SHA1 Message Date
Kristoffer Dalby e007ce2ffa policy/v2: rewrite tag-name first-letter check via De Morgan 2026-05-13 12:28:37 +00:00
Kristoffer Dalby 87c6d9b68e policy/v2: accept bare-IP sshTests dst, reject only explicit CIDR
SaaS accepts dst: ["100.64.0.16"] and dst: ["fd7a:115c:a1e0::10"] as
host addresses but rejects dst: ["10.0.0.0/24"]. The earlier typed-Alias
switch rejected every *Prefix and so dropped the bare-IP shape that the
Prefix parser materialises as a /BitLen prefix.

validateSSHTestDestination now distinguishes by *Prefix mask width:
Bits() == Addr().BitLen() accepts (single host, equivalent to bare IP),
anything narrower rejects with the existing ErrSSHTestDstDisallowedElement
wording. The Host branch already used the same width check for
hosts:-table aliases.

Adds two captures for the new shapes (bare IPv4 / IPv6) and parse-time
rows for the same in types_test. The IPv6 capture lands a SaaS-side
engine asymmetry (parse-accept, engine-reject "test(s) failed" while
the IPv4 mirror engine-passes) so it is parked in
knownSSHTesterDivergences for a follow-up.
2026-05-13 12:28:37 +00:00
Kristoffer Dalby 2865926028 policy/v2: use Alias and SSHUser types in SSHPolicyTest 2026-05-13 12:28:37 +00:00
Kristoffer Dalby b94936e129 CHANGELOG: rewrite sshTests entry to match policy tests style 2026-05-13 12:28:37 +00:00
Kristoffer Dalby c0a087461e policy/v2: drop useless comment on checkPeriod parse 2026-05-13 12:28:37 +00:00
Kristoffer Dalby 2b61b26772 policy/v2: match SaaS wording for group nesting rejection
SaaS rejects any group-in-group reference (chain, cycle, or
self-reference) with `groups["X"]: "Y": group members cannot be
recursive`. headscale rejected the same input but the message
surfaced as a generic JSON parse error from the unmarshal layer.
Groups.UnmarshalJSON now scans the raw map in descending
alphabetical order and reports the first group whose member is
itself a group, mirroring the (X, Y) pair SaaS picks (deepest
non-leaf parent first). Drop the now-unused ErrNestedGroups
sentinel and update the existing group-in-group test row plus add
chain, cycle, and self-cycle rows.
2026-05-13 12:28:37 +00:00
Kristoffer Dalby 079dca8924 policy/v2: reject non-ASCII tag names
SaaS rejects tag names whose first character after `tag:` is not an
ASCII letter (`[a-zA-Z]`) with `tagOwners["tag:X"]: tag names must
start with a letter, after 'tag:'`. headscale was accepting any
UTF-8. Tighten Tag.Validate to enforce the first-character rule and
reshape the surfaced error in unmarshalPolicy so the tagOwners key
appears in the SaaS-style prefix. Subsequent characters remain
unconstrained — only the leading byte is checked.
2026-05-13 12:28:37 +00:00
Kristoffer Dalby a79fb20372 policy/v2: reject hosts-table aliases as SSH dst
SaaS rejects any hosts-table alias on an SSH rule dst with
`invalid dst "alias"`, irrespective of whether the alias resolves
to a single IP or a CIDR. headscale was resolving the alias through
the same path as ACL dsts and accepting the policy. Validate at the
per-SSH-rule pass so the error body matches SaaS. The existing
host-alias-as-dst sshtest_test row tested the now-rejected shape
and is dropped along with its unused commonHosts fixture.
2026-05-13 12:28:36 +00:00
Kristoffer Dalby aea64b34de policy/v2: tolerate tag-owner cycles by resolving to empty
SaaS accepts tagOwners with circular references (tag:a -> tag:b ->
tag:a, tag:a -> tag:a) by dropping the cycle edge instead of failing
the policy. Each tag whose only path back is via the cycle resolves
to the empty owner set; a sibling non-tag owner survives. headscale
previously rejected the policy with ErrCircularReference at parse
time. Drop the sentinel, change flattenTags to return an empty owner
list on revisit, and update flattenTagOwners tests to capture the
new behaviour including the sibling-survives edge case.
2026-05-13 12:28:36 +00:00
Kristoffer Dalby 9f362d5be9 policy/v2: trim whitespace in SSH src and dst aliases
SaaS trims surrounding whitespace from src/dst entries before
dispatching to the alias lookup, so `"tag:server "` resolves to the
same tag and `" odin@example.com"` resolves to the same user.
headscale was treating the untrimmed strings as literals, which
either failed the tag/user lookup or dropped the affected node from
every rule referencing it. Trim inside AliasEnc.UnmarshalJSON so
ACL src/dst and SSH src/dst all benefit.
2026-05-13 12:28:36 +00:00
Kristoffer Dalby 76ba2de85a policy/v2: add ssh-edges captures from Tailscale SaaS
30 new captures recorded against SaaS covering multi-rule SSH
policies, group nesting, tag-owner cycles, empty references, unicode,
hosts table aliases, multi-src/dst, whitespace in src/dst, null and
missing fields, and sshTests edges. 20 captures already match
headscale behaviour. The remaining 10 land in sshSkipReasons and
sshRejectSkipReasons tracking 5 engine divergences (whitespace trim
in src/dst, tag-owner cycle tolerance, hosts-alias rejection on SSH
dst, non-ASCII tag-name rejection, group-nesting error wording);
follow-up commits close each gap and drain the corresponding skip
entries.
2026-05-13 12:28:36 +00:00
Kristoffer Dalby f8aa6c46ef policy/v2: trim whitespace and reject negative checkPeriod
SaaS trims leading whitespace in action and per-user entries before
matching, so headscale does too. Reject negative checkPeriod with
"must be a positive duration" matching SaaS body. The 168h upper
bound is inclusive.
2026-05-13 12:28:36 +00:00
Kristoffer Dalby 2180380fc1 policy/v2: align SSH rule validation with Tailscale SaaS
Drop autogroup/localpart strictness on SSH users. Drop checkPeriod
minimum. Reject empty/wildcard users and empty acceptEnv at parse.
Match SaaS wording for action and checkPeriod errors. 28 captures
under testdata/ssh_results/ssh-malformed-*.hujson lock the surface.
2026-05-13 12:28:36 +00:00
Kristoffer Dalby 9026f810fe policy/v2: branch tailscale_ssh_data_compat_test on APIResponseCode
Rejection captures (APIResponseCode != 200) now route through
NewPolicyManager + SetPolicy mirroring sshtester_compat_test.go; the
SaaS Message must be a substring of headscale's error. The accepted
path (200) keeps the existing per-node SSHRules comparison. Adds 28
ssh-malformed-* captures and a parallel sshRejectSkipReasons map for
4xx scenarios where headscale and SaaS legitimately disagree.
2026-05-13 12:28:13 +00:00
Kristoffer Dalby 4c4cebdc29 cmd/headscale/cli: mention sshTests in policy check help 2026-05-13 12:27:32 +00:00
Kristoffer Dalby 3eff2d5d0f policy/v2: address review on sshTests engine
Empty-dst-nodes fails loudly. Compat runner uses per-capture topology
so capture IPs match. Test rows tightened; stale godoc and goto fixed.
2026-05-13 12:27:32 +00:00
Kristoffer Dalby 48900f6c1a CHANGELOG: document sshTests evaluation (beta) 2026-05-13 12:27:32 +00:00
Kristoffer Dalby 1d477f4b8b integration: regenerate workflow for sshTests integration test 2026-05-13 12:27:32 +00:00
Kristoffer Dalby 4b79b03858 integration: reject failing sshTests at headscale policy set 2026-05-13 12:27:32 +00:00
Kristoffer Dalby 9e15565056 policy/v2: add sshtester captures from Tailscale SaaS
32 captures. knownSSHTesterDivergences tracks sshtest-user-wildcard
where SaaS rejects users:["*"] at parse and headscale does not.
2026-05-13 12:27:32 +00:00
Kristoffer Dalby 6f93e3b010 policy/v2: add sshtester compat runner
Replays captures under testdata/sshtest_results. 200 path requires
parse and SetPolicy to succeed; non-200 requires an error whose body
substring-matches the captured SaaS message.
2026-05-13 12:27:32 +00:00
Kristoffer Dalby 59755d496d policy/v2: evaluate sshTests at write boundary
accept and check count as reachable; check requires a check-action rule
specifically. SetPolicy rejects failing tests without mutating live
state; boot path warns.
2026-05-13 12:27:32 +00:00
Kristoffer Dalby 9205b02044 policy/v2: validate sshTests at parse
Reject empty src/dst, port-suffix dst, CIDR dst, autogroup:internet dst,
unknown-tag dst. Each error wraps errSSHPolicyTestsFailed for errors.Is.
2026-05-13 12:27:32 +00:00
366 changed files with 24611 additions and 104705 deletions
+3 -3
View File
@@ -42,7 +42,7 @@ jobs:
id: vendorhash
if: steps.changed-files.outputs.files == 'true'
run: |
nix develop --fallback --command -- go run ./cmd/vendorhash check | tee check-result
nix develop --command -- go run ./cmd/vendorhash check | tee check-result
{
grep '^expected_sri=' check-result || true
grep '^actual_sri=' check-result || true
@@ -63,7 +63,7 @@ jobs:
- name: Run nix build
if: steps.changed-files.outputs.files == 'true'
run: nix build --fallback
run: nix build
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: steps.changed-files.outputs.files == 'true'
@@ -91,7 +91,7 @@ jobs:
- name: Run go cross compile
env:
CGO_ENABLED: 0
run: env ${{ matrix.env }} nix develop --fallback --command -- go build -o "headscale"
run: env ${{ matrix.env }} nix develop --command -- go build -o "headscale"
./cmd/headscale
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
+3 -2
View File
@@ -28,7 +28,8 @@ jobs:
- '*.nix'
- 'go.*'
- '**/*.go'
- 'openapi/**'
- '**/*.proto'
- 'buf.gen.yaml'
- 'tools/**'
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
if: steps.changed-files.outputs.files == 'true'
@@ -40,7 +41,7 @@ jobs:
- name: Run make generate
if: steps.changed-files.outputs.files == 'true'
run: nix develop --fallback --command -- make generate
run: nix develop --command -- make generate
- name: Check for uncommitted changes
if: steps.changed-files.outputs.files == 'true'
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Generate and check integration tests
if: steps.changed-files.outputs.files == 'true'
run: |
nix develop --fallback --command bash -c "cd .github/workflows && go generate"
nix develop --command bash -c "cd .github/workflows && go generate"
git diff --exit-code .github/workflows/test-integration.yaml
- name: Show missing tests
+3 -3
View File
@@ -56,7 +56,7 @@ jobs:
KO_DEFAULTBASEIMAGE: gcr.io/distroless/base-debian13
CGO_ENABLED: "0"
run: |
nix develop --fallback --command -- ko build \
nix develop --command -- ko build \
--bare \
--platform=linux/amd64,linux/arm64 \
--tags=main-${GITHUB_SHA::7},development \
@@ -68,7 +68,7 @@ jobs:
KO_DEFAULTBASEIMAGE: gcr.io/distroless/base-debian13
CGO_ENABLED: "0"
run: |
nix develop --fallback --command -- ko build \
nix develop --command -- ko build \
--bare \
--platform=linux/amd64,linux/arm64 \
--tags=main-${GITHUB_SHA::7},development \
@@ -104,7 +104,7 @@ jobs:
CGO_ENABLED: "0"
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: nix develop --fallback --command -- go build -o headscale ./cmd/headscale
run: nix develop --command -- go build -o headscale ./cmd/headscale
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
@@ -14,7 +14,7 @@ import (
// Key is the test function name, value is a list of subtest prefixes.
// Each prefix becomes a separate CI job as "TestName/prefix".
//
// Example: [TestAutoApproveMultiNetwork] has subtests like:
// Example: TestAutoApproveMultiNetwork has subtests like:
// - TestAutoApproveMultiNetwork/authkey-tag-advertiseduringup-false-pol-database
// - TestAutoApproveMultiNetwork/webauth-user-advertiseduringup-true-pol-file
//
+15 -24
View File
@@ -51,11 +51,6 @@ jobs:
with:
name: tailscale-head-image
path: /tmp/artifacts
- name: Download tailscale released images
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: tailscale-released-images
path: /tmp/artifacts
- name: Download hi binary
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
@@ -72,26 +67,28 @@ jobs:
with:
name: postgres-image
path: /tmp/artifacts
- name: Force overlay2 storage driver
- name: Pin Docker to v28 (avoid v29 breaking changes)
run: |
sudo mkdir -p /etc/docker
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
# Docker 29 breaks docker build via Go client libraries and
# docker load/save with certain tarball formats.
# Pin to Docker 28.x until our tooling is updated.
# https://github.com/actions/runner-images/issues/13474
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -qq
VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
sudo apt-get install -y --allow-downgrades \
"docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
sudo systemctl restart docker
docker version
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Load Docker images, Go cache, and prepare binary
run: |
gunzip -c /tmp/artifacts/headscale-image.tar.gz | docker load
gunzip -c /tmp/artifacts/tailscale-head-image.tar.gz | docker load
gunzip -c /tmp/artifacts/tailscale-released-images.tar.gz | docker load
if [ -f /tmp/artifacts/postgres-image.tar.gz ]; then
gunzip -c /tmp/artifacts/postgres-image.tar.gz | docker load
fi
@@ -108,12 +105,6 @@ jobs:
HEADSCALE_INTEGRATION_POSTGRES_IMAGE: ${{ inputs.postgres_flag == '--postgres=1' && format('postgres:{0}', github.sha) || '' }}
HEADSCALE_INTEGRATION_GO_CACHE: /tmp/go-cache/go
HEADSCALE_INTEGRATION_GO_BUILD_CACHE: /tmp/go-cache/.cache/go-build
# Mirror the docker/login-action secrets into env so the
# dockertestutil.Credentials resolver picks them up directly
# (otherwise it falls back to parsing ~/.docker/config.json,
# which works but is one step further from the source).
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
run: /tmp/artifacts/hi run --stats --ts-memory-limit=300 --hs-memory-limit=1500 "^${{ inputs.test }}$" \
--timeout=120m \
${{ inputs.postgres_flag }}
+16 -2
View File
@@ -35,7 +35,7 @@ jobs:
- name: golangci-lint
if: steps.changed-files.outputs.files == 'true'
run: nix develop --fallback --command -- golangci-lint run
run: nix develop --command -- golangci-lint run
--new-from-rev=${{github.event.pull_request.base.sha}}
--output.text.path=stdout
--output.text.print-linter-name
@@ -75,5 +75,19 @@ jobs:
- name: Prettify code
if: steps.changed-files.outputs.files == 'true'
run: nix develop --fallback --command -- prettier --no-error-on-unmatched-pattern
run: nix develop --command -- prettier --no-error-on-unmatched-pattern
--ignore-unknown --check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}
proto-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
with:
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
'**/flake.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
- name: Buf lint
run: nix develop --command -- buf lint proto
+1 -1
View File
@@ -52,4 +52,4 @@ jobs:
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
run: |
echo "Running NixOS module integration test..."
nix build .#checks.x86_64-linux.headscale -L --fallback
nix build .#checks.x86_64-linux.headscale -L
+20 -1
View File
@@ -17,6 +17,25 @@ jobs:
with:
fetch-depth: 0
- name: Pin Docker to v28 (avoid v29 breaking changes)
run: |
# Docker 29 breaks docker build via Go client libraries and
# docker load/save with certain tarball formats.
# Pin to Docker 28.x until our tooling is updated.
# https://github.com/actions/runner-images/issues/13474
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -qq
VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
sudo apt-get install -y --allow-downgrades \
"docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
sudo systemctl restart docker
docker version
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
@@ -38,6 +57,6 @@ jobs:
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
- name: Run goreleaser
run: nix develop --fallback --command -- goreleaser release --clean
run: nix develop --command -- goreleaser release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+43 -117
View File
@@ -9,9 +9,6 @@ concurrency:
jobs:
# build: Builds binaries and Docker images once, uploads as artifacts for reuse.
# build-postgres: Pulls postgres image separately to avoid Docker Hub rate limits.
# build-tailscale-released: Pre-pulls released Tailscale images from ghcr.io
# so fork PRs (no DOCKERHUB_USERNAME secret) don't hit Docker Hub rate
# limits at test time.
# sqlite: Runs all integration tests with SQLite backend.
# postgres: Runs a subset of tests with PostgreSQL to verify database compatibility.
build:
@@ -47,7 +44,7 @@ jobs:
if: steps.changed-files.outputs.files == 'true'
run: |
# Build all Go binaries in one nix shell to maximize cache reuse
nix develop --fallback --command -- bash -c '
nix develop --command -- bash -c '
go build -o hi ./cmd/hi
CGO_ENABLED=0 GOOS=linux go build -o headscale ./cmd/headscale
# Build integration test binary to warm the cache with all dependencies
@@ -72,26 +69,25 @@ jobs:
name: go-cache
path: go-cache.tar.gz
retention-days: 10
- name: Force overlay2 storage driver
- name: Pin Docker to v28 (avoid v29 breaking changes)
if: steps.changed-files.outputs.files == 'true'
run: |
# Docker 29 runner images default to overlayfs, which breaks
# docker build via Go SDK libraries and docker save/load
# tarball formats. overlay2 is the long-standing default.
# Docker 29 breaks docker build via Go client libraries and
# docker load/save with certain tarball formats.
# Pin to Docker 28.x until our tooling is updated.
# https://github.com/actions/runner-images/issues/13474
sudo mkdir -p /etc/docker
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -qq
VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
sudo apt-get install -y --allow-downgrades \
"docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
sudo systemctl restart docker
docker version
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Build headscale image
if: steps.changed-files.outputs.files == 'true'
run: |
@@ -127,21 +123,24 @@ jobs:
needs: build
if: needs.build.outputs.files-changed == 'true'
steps:
- name: Force overlay2 storage driver
- name: Pin Docker to v28 (avoid v29 breaking changes)
run: |
sudo mkdir -p /etc/docker
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
# Docker 29 breaks docker build via Go client libraries and
# docker load/save with certain tarball formats.
# Pin to Docker 28.x until our tooling is updated.
# https://github.com/actions/runner-images/issues/13474
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -qq
VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
sudo apt-get install -y --allow-downgrades \
"docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
sudo systemctl restart docker
docker version
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Pull and save postgres image
run: |
docker pull postgres:latest
@@ -153,70 +152,8 @@ jobs:
name: postgres-image
path: postgres-image.tar.gz
retention-days: 10
build-tailscale-released:
runs-on: ubuntu-24.04-arm
needs: build
if: needs.build.outputs.files-changed == 'true'
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
with:
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
- name: Force overlay2 storage driver
run: |
sudo mkdir -p /etc/docker
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker version
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_CI_TOKEN }}
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: List Tailscale versions to pre-pull
id: versions
run: |
versions=$(nix develop --fallback --command go run ./cmd/hi list-versions --set=must --exclude=head)
echo "versions=${versions}" >> "$GITHUB_OUTPUT"
echo "Pre-pulling: ${versions}"
- name: Pull Tailscale images
run: |
# Releases come from ghcr.io (anonymous, unmetered). The
# "unstable" floating tag on ghcr.io has been stale since 2022,
# so it still needs to come from Docker Hub. xargs -P 0 fans
# out one process per tag and returns non-zero if any pull
# fails.
refs=""
for v in ${{ steps.versions.outputs.versions }}; do
if [ "${v}" = "unstable" ]; then
refs="${refs} tailscale/tailscale:${v}"
else
refs="${refs} ghcr.io/tailscale/tailscale:${v}"
fi
done
echo "${refs}" | tr ' ' '\n' | grep -v '^$' \
| xargs -P 0 -I{} docker pull "{}"
echo "REFS=${refs}" >> "$GITHUB_ENV"
- name: Save Tailscale images to tarball
run: |
# Single docker save with all refs: one consistent snapshot, no
# parallel-daemon race.
docker save ${REFS} | gzip > tailscale-released-images.tar.gz
ls -lh tailscale-released-images.tar.gz
- name: Upload Tailscale released images
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: tailscale-released-images
path: tailscale-released-images.tar.gz
retention-days: 10
sqlite:
needs: [build, build-tailscale-released]
needs: build
if: needs.build.outputs.files-changed == 'true'
strategy:
fail-fast: false
@@ -244,6 +181,8 @@ jobs:
- TestACLDynamicUnknownUserRemoval
- TestAPIAuthenticationBypass
- TestAPIAuthenticationBypassCurl
- TestGRPCAuthenticationBypass
- TestCLIWithConfigAuthenticationBypass
- TestAuthKeyLogoutAndReloginSameUser
- TestAuthKeyLogoutAndReloginNewUser
- TestAuthKeyLogoutAndReloginSameUserExpiredKey
@@ -263,32 +202,20 @@ jobs:
- TestAuthWebFlowAuthenticationPingAll
- TestAuthWebFlowLogoutAndReloginSameUser
- TestAuthWebFlowLogoutAndReloginNewUser
- TestApiKeyCommand
- TestApiKeyCommandValidation
- TestAuthCommandValidation
- TestNodeCommand
- TestNodeExpireCommand
- TestNodeRenameCommand
- TestPreAuthKeyCorrectUserLoggedInCommand
- TestTaggedNodesCLIOutput
- TestNodeExpireFlagsCommand
- TestNodeCommandValidation
- TestNodeTagCommand
- TestNodeRouteCommands
- TestNodeBackfillIPsCommand
- TestPolicyCheckCommand
- TestSSHTestsRejectFailingPolicy
- TestPolicyCommand
- TestPolicyBrokenConfigCommand
- TestUserCommand
- TestPreAuthKeyCommand
- TestPreAuthKeyCommandWithoutExpiry
- TestPreAuthKeyCommandReusableEphemeral
- TestPreAuthKeyDeleteCommand
- TestPreAuthKeyCommandValidation
- TestServerInfoCommands
- TestUserCommand
- TestUserCreateCommand
- TestUserCommandValidation
- TestPreAuthKeyCorrectUserLoggedInCommand
- TestTaggedNodesCLIOutput
- TestApiKeyCommand
- TestNodeCommand
- TestNodeExpireCommand
- TestNodeRenameCommand
- TestPolicyCommand
- TestPolicyBrokenConfigCommand
- TestDERPVerifyEndpoint
- TestResolveMagicDNS
- TestResolveMagicDNSExtraRecordsPath
@@ -342,7 +269,6 @@ jobs:
- TestSSHCheckModeUnapprovedTimeout
- TestSSHCheckModeCheckPeriodCLI
- TestSSHCheckModeAutoApprove
- TestSSHCheckModeSessionLossReDelegates
- TestSSHCheckModeNegativeCLI
- TestSSHLocalpart
- TestTagsAuthKeyWithTagRequestDifferentTag
@@ -385,7 +311,7 @@ jobs:
postgres_flag: "--postgres=0"
database_name: "sqlite"
postgres:
needs: [build, build-postgres, build-tailscale-released]
needs: [build, build-postgres]
if: needs.build.outputs.files-changed == 'true'
strategy:
fail-fast: false
+1 -1
View File
@@ -44,4 +44,4 @@ jobs:
# some of the database migration tests.
LC_ALL: "en_US.UTF-8"
LC_CTYPE: "en_US.UTF-8"
run: nix develop --fallback --command -- gotestsum
run: nix develop --command -- gotestsum
-12
View File
@@ -13,7 +13,6 @@ linters:
- gochecknoinits
- gocognit
- godox
- gomodguard
- interfacebloat
- ireturn
- lll
@@ -31,17 +30,6 @@ linters:
- wrapcheck
- wsl
settings:
goconst:
# Test fixtures repeat strings (IPs, tags, hostnames) by their
# nature; extracting them obscures the test rather than helping.
# Production code stays strict.
ignore-tests: true
# Default is 3. Bump so "happens thrice" cases that are not part
# of a shared vocabulary do not get extracted.
min-occurrences: 5
# Default is 3. Short literals ("set", "get", "new") read better
# at call sites than behind a named constant.
min-len: 6
forbidigo:
forbid:
# Forbid time.Sleep everywhere with context-appropriate alternatives
+10 -12
View File
@@ -75,9 +75,9 @@ make dev
# Individual targets
make build # build the headscale binary
make test # go test ./...
make fmt # format Go and docs
make lint # lint Go
make generate # regenerate ogen code (after changes to openapi/)
make fmt # format Go, docs, proto
make lint # lint Go, proto
make generate # regenerate protobuf code (after changes to proto/)
make clean # remove build artefacts
# Direct go test invocations
@@ -133,11 +133,9 @@ headscale/
### `hscontrol/` packages
- `app.go`, `handlers.go`, `noise.go`, `auth.go`, `oidc.go`,
- `app.go`, `handlers.go`, `grpcv1.go`, `noise.go`, `auth.go`, `oidc.go`,
`poll.go`, `metrics.go`, `debug.go`, `tailsql.go`, `platform_config.go`
— top-level server files
- `api/v1/` — the ogen-backed v1 HTTP API handlers (`server.go`,
`convert.go`, `errors.go`, per-resource handler files)
- `state/` — central coordinator (`state.go`) and the copy-on-write
`NodeStore` (`node_store.go`). All cross-subsystem operations go
through `State`.
@@ -211,7 +209,7 @@ both. This is a load-bearing architectural rule.
- `SetTags` validation is enforced by `validateNodeOwnership()` in
`hscontrol/state/tags.go`.
- Examples and edge cases live in `hscontrol/types/node_tags_test.go`
and the SetTags tests in `hscontrol/servertest/apiv1_nodes_test.go`.
and `hscontrol/grpcv1_test.go` (`TestSetTags_*`).
**Don't do this**:
@@ -266,9 +264,9 @@ Key reminders:
Not Conventional Commits. No `feat:`/`chore:`/`docs:` prefixes.
- **Code generation**: changes under `openapi/` require `make generate`
(which runs `ogen`) and should land in a **separate commit** from the
callers that use the regenerated types.
- **Protobuf regeneration**: changes under `proto/` require
`make generate` (which runs `buf generate`) and should land in a
**separate commit** from the callers that use the regenerated types.
- **Formatting** is enforced by `golangci-lint` with `golines` (width 88)
and `gofumpt`. Run `make fmt` or rely on the pre-commit hook.
- **Logging** uses `zerolog`. Prefer single-line chains
@@ -288,6 +286,6 @@ Key reminders:
- **`.claude/agents/` is deprecated.** Do not create new agent files
there. Put behavioural guidance in this file and procedural guidance
in the nearest README.
- **Do not edit `gen/`** — it is regenerated from `openapi/` by
- **Do not edit `gen/`** — it is regenerated from `proto/` by
`make generate`.
- **Spec changes + code changes should be two commits**, not one.
- **Proto changes + code changes should be two commits**, not one.
+70 -63
View File
@@ -1,20 +1,8 @@
# CHANGELOG
## 0.30.0 (202x-xx-xx)
## 0.29.0 (202x-xx-xx)
**Minimum supported Tailscale client version: v1.xx.0**
## 0.29.1 (2026-06-18)
**Minimum supported Tailscale client version: v1.80.0**
### Changes
- Fix nodes with `tags='null'` losing their assigned user on upgrade [#3325](https://github.com/juanfont/headscale/pull/3325)
## 0.29.0 (2026-06-17)
**Minimum supported Tailscale client version: v1.80.0**
**Minimum supported Tailscale client version: v1.76.0**
### Tailscale ACL compatibility improvements
@@ -58,31 +46,36 @@ This feature is **beta** while behavioural coverage against Tailscale SaaS broad
### SSH policy tests (beta)
Headscale now evaluates the `sshTests` block in a policy file. Each entry names a source, one or
more destination hosts, and three optional user lists: `accept` asserts the listed login users
reach every destination via an accept- or check-action SSH rule, `deny` asserts none of them
reach any destination, and `check` requires reachability specifically through a check-action
rule. Tests run on `headscale policy set`, on SIGHUP reload (`systemctl reload headscale` /
`kill -HUP $(pidof headscale)`), and on `headscale policy check`. A failing test rejects the
write before it is applied, with the same error message Tailscale SaaS would return for the same
policy.
Headscale now evaluates the `sshTests` block in a policy file. Tests assert which SSH login users
can connect from a named source to named destinations against the same SSH rules clients receive.
They run on `headscale policy set`, on SIGHUP reload (`systemctl reload headscale` /
`kill -HUP $(pidof headscale)`), and on `headscale policy check`. A failing test rejects the write
before it is applied, with the same error message Tailscale SaaS would return for the same policy.
An entry has the shape:
```hujson
"sshTests": [
{
"src": "alice@example.com",
"dst": ["tag:server"],
"accept": ["root"],
"deny": ["alice"],
"check": ["ubuntu"]
}
]
```
`accept` asserts the listed login users reach every dst via an accept- or check-action SSH rule,
`deny` asserts none of them reach any dst, and `check` requires reachability specifically via a
check-action rule.
At boot a stored policy whose sshTests no longer pass — for example because a referenced user was
deleted while the server was offline — logs a warning and the server keeps running. Fix the
policy and reload.
deleted while the server was offline — logs a warning and the server keeps running. Fix the policy
and reload.
This feature is **beta** while behavioural coverage against Tailscale SaaS broadens.
[#3263](https://github.com/juanfont/headscale/pull/3263)
### SSH rule validation
SSH rule parsing now trims surrounding whitespace on `action`, `users`, `src`, and `dst`,
rejects empty or wildcard entries in `users`, rejects empty `acceptEnv`, and rejects negative
`checkPeriod`. `hosts:` aliases are rejected as SSH destinations, non-ASCII tag names are
rejected at parse time, and the wording for group-nesting cycles matches Tailscale SaaS.
[#3263](https://github.com/juanfont/headscale/pull/3263)
### Grants
We now support [Tailscale grants](https://tailscale.com/docs/features/access-control/grants)
@@ -92,9 +85,9 @@ field steers traffic through specific tagged subnet routers or exit nodes. The `
an ACL rule. Grants can be mixed with ACLs in the same policy file.
[#2180](https://github.com/juanfont/headscale/pull/2180)
As part of this, we added `autogroup:danger-all`. It resolves to `0.0.0.0/0` and `::/0`, all IP
As part of this, we added `autogroup:danger-all`. It resolves to `0.0.0.0/0` and `::/0` all IP
addresses, including those outside the tailnet. This replaces the old behaviour where `*` matched
all IPs (see BREAKING below). The name is intentional: accepting traffic from the entire
all IPs (see BREAKING below). The name is intentionally scary: accepting traffic from the entire
internet is a security-sensitive choice. `autogroup:danger-all` can only be used as a source.
### Node attributes (`nodeAttrs`)
@@ -118,7 +111,7 @@ Frequently requested capabilities this unlocks include `magicdns-aaaa`,
`disable-relay-server`, `disable-captive-portal-detection`,
`nextdns:<profile>` / `nextdns:no-device-info`, `randomize-client-port`,
and the Taildrive `drive:share` / `drive:access` pair. The set is not
limited to these, any string-only cap an operator places in policy
limited to these any string-only cap an operator places in policy
reaches clients unchanged.
`randomizeClientPort` also lands as a top-level policy field that toggles
@@ -165,9 +158,28 @@ mode:
A wildcard `nodeAttrs` (`"target": ["*"]`) hands the caps to every
node when fine-grained control is not needed.
### Hostname sanitisation
### Hostname handling (cleanroom rewrite)
Hostnames are now santised using Tailscales `magicdns` sanitisation rules, matching Tailscale SaaS behavior. This means that hostnames with non-ASCII characters, special characters, or reserved DNS label characters are now transformed into valid DNS labels for MagicDNS. This improves our previously too strict sanitisation that rejected hostnames based on our guesswork and not based on the Tailscale upstream behaviour.
The hostname ingest pipeline has been rewritten to match Tailscale SaaS byte-for-byte.
Headscale previously had three overlapping regexes and two disagreeing entry points
(registration vs map-request update), which caused a recurring class of bugs: names
containing apostrophes, spaces, dots, or non-ASCII characters were alternately rejected
(dropping updates with log spam) or stored as `invalid-<rand>` surrogates
([#3188](https://github.com/juanfont/headscale/issues/3188),
[#2926](https://github.com/juanfont/headscale/issues/2926),
[#2343](https://github.com/juanfont/headscale/issues/2343),
[#2762](https://github.com/juanfont/headscale/issues/2762),
[#2177](https://github.com/juanfont/headscale/issues/2177),
[#2121](https://github.com/juanfont/headscale/issues/2121),
[#2449](https://github.com/juanfont/headscale/issues/2449),
[#363](https://github.com/juanfont/headscale/issues/363)).
What changed:
- Sanitisation and validation now come directly from
`tailscale.com/util/dnsname.SanitizeHostname` / `ValidLabel`.
- Admin rename (`headscale nodes rename`) now validates via `dnsname.ValidLabel` and
rejects labels already held by another node (previously coerced invalid input silently).
Examples that previously regressed and now work:
@@ -180,24 +192,11 @@ Examples that previously regressed and now work:
| `My-PC!` | `My-PC!` | `my-pc` |
| `我的电脑` | `我的电脑` | `node` |
[#3202](https://github.com/juanfont/headscale/pull/3202)
### HA subnet router health probing
Headscale now actively probes HA subnet routers to detect nodes that are connected but not
forwarding traffic. The control plane periodically pings HA subnet routers via the Noise
control channel and fails over to a healthy standby if the primary stops responding. This is
enabled by default (`node.routes.ha.probe_interval: 10s`, `probe_timeout: 5s`) and only
active when HA routes exist (2+ nodes advertising the same prefix). Set `probe_interval` to
`0` to disable. This complements the existing disconnect-based failover, catching "zombie
connected" routers that maintain their control session but cannot route packets.
[#3194](https://github.com/juanfont/headscale/pull/3194)
### BREAKING
#### Hostname handling
- The `GivenName` collision policy changed from an 8-char random hash suffix (`laptop-abc12xyz`) to a monotonic numeric suffix (`laptop`, `laptop-1`, `laptop-2`, …), matching Tailscale SaaS. Empty / all-non-ASCII hostnames now fall back to the literal `node` instead of `invalid-<rand>`. MagicDNS names change on upgrade for any node whose previous label was a random-suffix form; the raw `Hostname` column is unchanged. [#3202](https://github.com/juanfont/headscale/pull/3202)
- The `GivenName` collision policy changed from an 8-char random hash suffix (`laptop-abc12xyz`) to a monotonic numeric suffix (`laptop`, `laptop-1`, `laptop-2`, …), matching Tailscale SaaS. Empty / all-non-ASCII hostnames now fall back to the literal `node` instead of `invalid-<rand>`. MagicDNS names change on upgrade for any node whose previous label was a random-suffix form; the raw `Hostname` column is unchanged.
#### ACL Policy
@@ -206,8 +205,8 @@ connected" routers that maintain their control session but cannot route packets.
- Policies that need to match all IP addresses including non-Tailscale IPs should use `autogroup:danger-all` as a source, or explicit CIDR ranges as destinations [#2180](https://github.com/juanfont/headscale/pull/2180)
- `autogroup:danger-all` can only be used as a source; it cannot be used as a destination
- **Note**: Users with non-standard IP ranges configured in `prefixes.ipv4` or `prefixes.ipv6` (which is unsupported and produces a warning) will need to explicitly specify their CIDR ranges in ACL rules instead of using `*`
- Validate `autogroup:self` source restrictions matching Tailscale behavior - tags, hosts, and IPs are rejected as sources for `autogroup:self` destinations [#3036](https://github.com/juanfont/headscale/pull/3036)
- Policies using tags, hosts, or IP addresses as sources for `autogroup:self` destinations will now fail validation
- Validate autogroup:self source restrictions matching Tailscale behavior - tags, hosts, and IPs are rejected as sources for autogroup:self destinations [#3036](https://github.com/juanfont/headscale/pull/3036)
- Policies using tags, hosts, or IP addresses as sources for autogroup:self destinations will now fail validation
- The `proto:icmp` protocol name now only includes ICMPv4 (protocol 1), matching Tailscale behavior [#3036](https://github.com/juanfont/headscale/pull/3036)
- Previously, `proto:icmp` included both ICMPv4 and ICMPv6
- Use `proto:ipv6-icmp` or protocol number `58` explicitly for ICMPv6
@@ -223,9 +222,10 @@ connected" routers that maintain their control session but cannot route packets.
- The `randomize_client_port` server-config key was removed; the
toggle now lives in the policy file as a top-level
`randomizeClientPort` field, matching the Tailscale-hosted schema. [#3251](https://github.com/juanfont/headscale/pull/3251)
`randomizeClientPort` field, matching the Tailscale-hosted schema.
Headscale refuses to start when the old key is set. Move it to the
policy file referenced by `policy.path`:
policy file referenced by `policy.path` (defaults to
`/etc/headscale/policy.hujson`):
```jsonc
{
@@ -245,6 +245,16 @@ connected" routers that maintain their control session but cannot route packets.
- `headscale nodes register` is deprecated in favour of `headscale auth register --auth-id <id> --user <user>` [#1850](https://github.com/juanfont/headscale/pull/1850)
- The old command continues to work but will be removed in a future release
### HA subnet router health probing
Headscale now actively probes HA subnet routers to detect nodes that are connected but not
forwarding traffic. The control plane periodically pings HA subnet routers via the Noise
control channel and fails over to a healthy standby if the primary stops responding. This is
enabled by default (`node.routes.ha.probe_interval: 10s`, `probe_timeout: 5s`) and only
active when HA routes exist (2+ nodes advertising the same prefix). Set `probe_interval` to
`0` to disable. This complements the existing disconnect-based failover, catching "zombie
connected" routers that maintain their control session but cannot route packets.
### Changes
#### ACL Policy
@@ -285,7 +295,7 @@ connected" routers that maintain their control session but cannot route packets.
- `headscale policy check --bypass-grpc-and-access-database-directly` validates `user@` tokens against the live user database [#3160](https://github.com/juanfont/headscale/issues/3160)
- Remove deprecated `--namespace` flag from `nodes list`, `nodes register`, and `debug create-node` commands (use `--user` instead) [#3093](https://github.com/juanfont/headscale/pull/3093)
- Remove deprecated `namespace`/`ns` command aliases for `users` and `machine`/`machines` aliases for `nodes` [#3093](https://github.com/juanfont/headscale/pull/3093)
- Fix `DestroyUser` deleting all pre-auth keys in the database instead of only the target user's keys [#3155](https://github.com/juanfont/headscale/pull/3155)
- **User deletion**: Fix `DestroyUser` deleting all pre-auth keys in the database instead of only the target user's keys [#3155](https://github.com/juanfont/headscale/pull/3155)
- `headscale policy check` evaluates the `tests` block when invoked with `--bypass-grpc-and-access-database-directly`; without the flag it warns instead of running the tests against empty data [#1803](https://github.com/juanfont/headscale/issues/1803)
#### API
@@ -305,7 +315,6 @@ connected" routers that maintain their control session but cannot route packets.
- Tagged nodes (registered with tagged pre-auth keys) are exempt from default expiry
- `oidc.expiry` has been removed; use `node.expiry` instead (applies to all registration methods including OIDC)
- `ephemeral_node_inactivity_timeout` is deprecated in favour of `node.ephemeral.inactivity_timeout`
- Add `trusted_proxies` to gate `True-Client-IP` / `X-Real-IP` / `X-Forwarded-For` (previously honoured from any client) [#3268](https://github.com/juanfont/headscale/pull/3268)
#### Debug
@@ -317,10 +326,8 @@ connected" routers that maintain their control session but cannot route packets.
- Remove old migrations for the debian package [#3185](https://github.com/juanfont/headscale/pull/3185)
- Install `config-example.yaml` as example for the debian package [#3186](https://github.com/juanfont/headscale/pull/3186)
- Fix user-owned re-registration with zero client expiry and no default storing `0001-01-01 00:00:00` in the database instead of `NULL` [#3199](https://github.com/juanfont/headscale/pull/3199)
- Fix `tailscaled` restart on a node with no expiry resetting `NULL` to `0001-01-01 00:00:00` in the database, affecting both tagged and untagged nodes [#3197](https://github.com/juanfont/headscale/pull/3197)
- Backfill `nodes.expiry` rows persisted by older versions as `0001-01-01 00:00:00` to `NULL`, so nodes upgraded from <0.28 stop reporting as expired [#3284](https://github.com/juanfont/headscale/issues/3284)
- Update reverse proxy documentation for `trusted_proxies` configuration option [#3292](https://github.com/juanfont/headscale/pull/3292)
- **Node Expiry**: Fix user owned re registration with zero client expiry and no default storing `0001-01-01 00:00:00` in the database instead of NULL [#3199](https://github.com/juanfont/headscale/pull/3199)
- Pre-existing rows with `0001-01-01 00:00:00` are not backfilled; they clear themselves the next time the node re-registers
## 0.28.0 (2026-02-04)
+2 -2
View File
@@ -1,6 +1,6 @@
# For testing purposes only
FROM golang:1.26.4-alpine AS build-env
FROM golang:1.26.3-alpine AS build-env
WORKDIR /go/src
@@ -12,7 +12,7 @@ WORKDIR /go/src/tailscale
ARG TARGETARCH
RUN GOARCH=$TARGETARCH go install -v ./cmd/derper
FROM alpine:3.23
FROM alpine:3.22
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
COPY --from=build-env /go/bin/* /usr/local/bin/
+1 -1
View File
@@ -2,7 +2,7 @@
# and are in no way endorsed by Headscale's maintainers as an
# official nor supported release or distribution.
FROM docker.io/golang:1.26.4-trixie AS builder
FROM docker.io/golang:1.26.2-trixie AS builder
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale
+2 -2
View File
@@ -4,7 +4,7 @@
# This Dockerfile is more or less lifted from tailscale/tailscale
# to ensure a similar build process when testing the HEAD of tailscale.
FROM golang:1.26.4-alpine AS build-env
FROM golang:1.26.3-alpine AS build-env
WORKDIR /go/src
@@ -36,7 +36,7 @@ RUN GOARCH=$TARGETARCH go install -tags="${BUILD_TAGS}" -ldflags="\
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
FROM alpine:3.23
FROM alpine:3.22
# Upstream: ca-certificates ip6tables iptables iproute2
# Tests: curl python3 (traceroute via BusyBox)
RUN apk add --no-cache ca-certificates curl ip6tables iptables iproute2 python3
+2 -2
View File
@@ -1,4 +1,4 @@
FROM rust:1.95-trixie AS builder
FROM rust:1.94-bookworm AS builder
ARG TAILSCALE_RS_REPO=https://github.com/tailscale/tailscale-rs.git
ARG TAILSCALE_RS_REF=main
@@ -15,7 +15,7 @@ RUN sed -i '/^axum = \["dep:axum"\]/a insecure-keyfetch = ["ts_control/insecure-
RUN cargo build --release --features axum,insecure-keyfetch --example axum
FROM debian:trixie-slim
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
+20 -5
View File
@@ -20,6 +20,7 @@ endef
# Source file collections using shell find for better performance
GO_SOURCES := $(shell find . -name '*.go' -not -path './gen/*' -not -path './vendor/*')
PROTO_SOURCES := $(shell find . -name '*.proto' -not -path './gen/*' -not -path './vendor/*')
PRETTIER_SOURCES := $(shell find . \( -name '*.md' -o -name '*.yaml' -o -name '*.yml' -o -name '*.ts' -o -name '*.js' -o -name '*.html' -o -name '*.css' -o -name '*.scss' -o -name '*.sass' \) -not -path './gen/*' -not -path './vendor/*' -not -path './node_modules/*')
# Default target
@@ -34,6 +35,8 @@ check-deps:
$(call check_tool,gofumpt)
$(call check_tool,mdformat)
$(call check_tool,prettier)
$(call check_tool,clang-format)
$(call check_tool,buf)
# Build targets
.PHONY: build
@@ -50,7 +53,7 @@ test: check-deps $(GO_SOURCES) go.mod go.sum
# Formatting targets
.PHONY: fmt
fmt: fmt-go fmt-mdformat fmt-prettier
fmt: fmt-go fmt-mdformat fmt-prettier fmt-proto
.PHONY: fmt-go
fmt-go: check-deps $(GO_SOURCES)
@@ -68,15 +71,25 @@ fmt-prettier: check-deps $(PRETTIER_SOURCES)
@echo "Formatting markup and config files..."
prettier --write '**/*.{ts,js,md,yaml,yml,sass,css,scss,html}'
.PHONY: fmt-proto
fmt-proto: check-deps $(PROTO_SOURCES)
@echo "Formatting Protocol Buffer files..."
clang-format -i $(PROTO_SOURCES)
# Linting targets
.PHONY: lint
lint: lint-go
lint: lint-go lint-proto
.PHONY: lint-go
lint-go: check-deps $(GO_SOURCES) go.mod go.sum
@echo "Linting Go code..."
golangci-lint run --timeout 10m
.PHONY: lint-proto
lint-proto: check-deps $(PROTO_SOURCES)
@echo "Linting Protocol Buffer files..."
cd proto/ && buf lint
# Code generation
.PHONY: generate
generate: check-deps
@@ -106,9 +119,9 @@ help:
@echo " all - Run lint, test, and build (default)"
@echo " build - Build headscale binary"
@echo " test - Run Go tests"
@echo " fmt - Format all code (Go, docs)"
@echo " lint - Lint all code (Go)"
@echo " generate - Generate code from the OpenAPI spec"
@echo " fmt - Format all code (Go, docs, proto)"
@echo " lint - Lint all code (Go, proto)"
@echo " generate - Generate code from Protocol Buffers"
@echo " dev - Full development workflow (fmt + lint + test + build)"
@echo " clean - Clean build artifacts"
@echo ""
@@ -116,7 +129,9 @@ help:
@echo " fmt-go - Format Go code only"
@echo " fmt-mdformat - Format documentation only"
@echo " fmt-prettier - Format markup and config files only"
@echo " fmt-proto - Format Protocol Buffer files only"
@echo " lint-go - Lint Go code only"
@echo " lint-proto - Lint Protocol Buffer files only"
@echo ""
@echo "Dependencies:"
@echo " check-deps - Verify required tools are available"
+9 -3
View File
@@ -92,7 +92,8 @@ Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) file.
### Requirements
To contribute to headscale you would need the latest version of [Go](https://golang.org).
To contribute to headscale you would need the latest version of [Go](https://golang.org)
and [Buf](https://buf.build) (Protobuf generator).
We recommend using [Nix](https://nixos.org/) to setup a development environment. This can
be done with `nix develop`, which will install the tools and give you a shell.
@@ -109,6 +110,9 @@ formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
Please configure your editor to run the tools while developing and make sure to
run `make lint` and `make fmt` before committing any code.
The **Proto** code is linted with [`buf`](https://docs.buf.build/lint/overview) and
formatted with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html).
The **docs** are formatted with [`mdformat`](https://mdformat.readthedocs.io).
The **rest** (Markdown, YAML, etc) is formatted with [`prettier`](https://prettier.io).
@@ -118,6 +122,8 @@ Check out the `.golangci.yaml` and `Makefile` to see the specific configuration.
### Install development tools
- Go
- Buf
- Protobuf tools
Install and activate:
@@ -127,8 +133,8 @@ nix develop
### Testing and building
Some parts of the project require generated Go code from the OpenAPI spec
(if changes are made in `openapi/`) and it must be (re-)generated with:
Some parts of the project require the generation of Go code from Protobuf
(if changes are made in `proto/`) and it must be (re-)generated with:
```shell
make generate
+21
View File
@@ -0,0 +1,21 @@
version: v1
plugins:
- name: go
out: gen/go
opt:
- paths=source_relative
- name: go-grpc
out: gen/go
opt:
- paths=source_relative
- name: grpc-gateway
out: gen/go
opt:
- paths=source_relative
- generate_unbound_methods=true
# - name: gorm
# out: gen/go
# opt:
# - paths=source_relative,enums=string,gateway=true
- name: openapiv2
out: gen/openapiv2
+2 -1
View File
@@ -42,7 +42,8 @@ go tool mts node1 status
| `--port` | 8080 | Headscale listen port |
| `--keep` | false | Keep state directory on exit |
The metrics/debug port is `port + 1010` (default 9090).
The metrics/debug port is `port + 1010` (default 9090) and the gRPC
port is `port + 42363` (default 50443).
## What it does
+11 -9
View File
@@ -28,14 +28,17 @@ var errHealthTimeout = errors.New("health check timed out")
var errEmptyAuthKey = errors.New("empty auth key in response")
// maxDevPort is the highest --port value that keeps the derived metrics port
// (port+1010) inside the valid 1..65535 TCP range.
const maxDevPort = 64525
// maxDevPort is the highest --port value that keeps both the derived
// metrics port (port+1010) and gRPC port (port+42363) inside the valid
// 1..65535 TCP range.
const maxDevPort = 23172
const devConfig = `---
server_url: http://127.0.0.1:%d
listen_addr: 127.0.0.1:%d
metrics_listen_addr: 127.0.0.1:%d
grpc_listen_addr: 127.0.0.1:%d
grpc_allow_insecure: true
noise:
private_key_path: %s/noise_private.key
@@ -80,7 +83,7 @@ func main() {
if *port < 1 || *port > maxDevPort {
log.Fatalf(
"--port must be in 1..%d (higher values overflow the derived metrics port); got %d",
"--port must be in 1..%d (higher values overflow the derived gRPC port); got %d",
maxDevPort, *port,
)
}
@@ -98,6 +101,7 @@ func main() {
func run() error {
metricsPort := *port + 1010 // default 9090
grpcPort := *port + 42363 // default 50443
tmpDir, err := os.MkdirTemp("", "headscale-dev-")
if err != nil {
@@ -110,9 +114,8 @@ func run() error {
// Write config.
configPath := filepath.Join(tmpDir, "config.yaml")
configContent := fmt.Sprintf(
devConfig,
*port, *port, metricsPort,
configContent := fmt.Sprintf(devConfig,
*port, *port, metricsPort, grpcPort,
tmpDir, tmpDir, tmpDir,
)
@@ -190,8 +193,7 @@ func run() error {
}
// Print banner.
fmt.Printf(
`
fmt.Printf(`
=== Headscale Dev Environment ===
Server: http://127.0.0.1:%d
Metrics: http://127.0.0.1:%d
+38 -59
View File
@@ -2,12 +2,12 @@ package cli
import (
"context"
"errors"
"fmt"
"strconv"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
@@ -16,8 +16,6 @@ const (
DefaultAPIKeyExpiry = "90d"
)
var errAPIKeyIDNotFound = errors.New("no api key with id")
func init() {
rootCmd.AddCommand(apiKeysCmd)
apiKeysCmd.AddCommand(listAPIKeys)
@@ -43,33 +41,36 @@ var apiKeysCmd = &cobra.Command{
}
var listAPIKeys = &cobra.Command{
Use: cmdList,
Use: "list",
Short: "List the Api keys for headscale",
Aliases: []string{"ls", cmdShow},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
resp, err := client.ListApiKeys(ctx)
Aliases: []string{"ls", "show"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{})
if err != nil {
return fmt.Errorf("listing api keys: %w", err)
}
return printListOutput(cmd, resp.ApiKeys, func() error {
rows := make([][]string, 0, len(resp.ApiKeys))
for _, key := range resp.ApiKeys {
return printListOutput(cmd, response.GetApiKeys(), func() error {
tableData := pterm.TableData{
{"ID", "Prefix", "Expiration", "Created"},
}
for _, key := range response.GetApiKeys() {
expiration := "-"
if key.Expiration.Set {
expiration = ColourTime(key.Expiration.Value)
if key.GetExpiration() != nil {
expiration = ColourTime(key.GetExpiration().AsTime())
}
rows = append(rows, []string{
strconv.FormatUint(key.ID.Value, util.Base10),
key.Prefix.Value,
tableData = append(tableData, []string{
strconv.FormatUint(key.GetId(), util.Base10),
key.GetPrefix(),
expiration,
key.CreatedAt.Value.Format(HeadscaleDateTimeFormat),
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
})
}
return renderTable([]string{"ID", "Prefix", colExpiration, colCreated}, rows)
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
})
}),
}
@@ -81,21 +82,21 @@ var createAPIKeyCmd = &cobra.Command{
Creates a new Api key, the Api key is only visible on creation
and cannot be retrieved again.
If you lose a key, create a new one and revoke (expire) the old one.`,
Aliases: []string{"c", cmdNew},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
Aliases: []string{"c", "new"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
expiration, err := expirationFromFlag(cmd)
if err != nil {
return err
}
resp, err := client.CreateApiKey(ctx, &apiv1.CreateApiKeyReq{
response, err := client.CreateApiKey(ctx, &v1.CreateApiKeyRequest{
Expiration: expiration,
})
if err != nil {
return fmt.Errorf("creating api key: %w", err)
}
return printOutput(cmd, resp.ApiKey.Value, resp.ApiKey.Value)
return printOutput(cmd, response.GetApiKey(), response.GetApiKey())
}),
}
@@ -116,67 +117,45 @@ func apiKeyIDOrPrefix(cmd *cobra.Command) (uint64, string, error) {
}
var expireAPIKeyCmd = &cobra.Command{
Use: cmdExpire,
Use: "expire",
Short: "Expire an ApiKey",
Aliases: []string{"revoke", aliasExp, "e"},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
Aliases: []string{"revoke", "exp", "e"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
id, prefix, err := apiKeyIDOrPrefix(cmd)
if err != nil {
return err
}
err = client.ExpireApiKey(ctx, &apiv1.ExpireApiKeyReq{
ID: optUint64(id),
Prefix: optString(prefix),
response, err := client.ExpireApiKey(ctx, &v1.ExpireApiKeyRequest{
Id: id,
Prefix: prefix,
})
if err != nil {
return fmt.Errorf("expiring api key: %w", err)
}
return printOutput(cmd, map[string]string{colResult: "Key expired"}, "Key expired")
return printOutput(cmd, response, "Key expired")
}),
}
var deleteAPIKeyCmd = &cobra.Command{
Use: cmdDelete,
Use: "delete",
Short: "Delete an ApiKey",
Aliases: []string{"remove", aliasDel},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
Aliases: []string{"remove", "del"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
id, prefix, err := apiKeyIDOrPrefix(cmd)
if err != nil {
return err
}
// Delete is routed by prefix in the path, so resolve an --id to its
// prefix first; an empty path segment would 404 at the router.
if prefix == "" {
prefix, err = apiKeyPrefixByID(ctx, client, id)
if err != nil {
return err
}
}
err = client.DeleteApiKey(ctx, apiv1.DeleteApiKeyParams{Prefix: prefix})
response, err := client.DeleteApiKey(ctx, &v1.DeleteApiKeyRequest{
Id: id,
Prefix: prefix,
})
if err != nil {
return fmt.Errorf("deleting api key: %w", err)
}
return printOutput(cmd, map[string]string{colResult: "Key deleted"}, "Key deleted")
return printOutput(cmd, response, "Key deleted")
}),
}
// apiKeyPrefixByID looks up an API key's prefix by its numeric ID.
func apiKeyPrefixByID(ctx context.Context, client *apiv1.Client, id uint64) (string, error) {
resp, err := client.ListApiKeys(ctx)
if err != nil {
return "", fmt.Errorf("listing api keys: %w", err)
}
for _, key := range resp.GetApiKeys() {
if key.GetID().Or(0) == id {
return key.GetPrefix().Or(""), nil
}
}
return "", fmt.Errorf("%w: %d", errAPIKeyIDNotFound, id)
}
+38 -36
View File
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/spf13/cobra"
)
@@ -33,59 +33,61 @@ var authCmd = &cobra.Command{
var authRegisterCmd = &cobra.Command{
Use: "register",
Short: "Register a node to your network",
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
authID, _ := cmd.Flags().GetString("auth-id")
resp, err := client.AuthRegister(ctx, &apiv1.AuthRegisterReq{
AuthId: apiv1.NewOptString(authID),
User: apiv1.NewOptString(user),
})
request := &v1.AuthRegisterRequest{
AuthId: authID,
User: user,
}
response, err := client.AuthRegister(ctx, request)
if err != nil {
return fmt.Errorf("registering node: %w", err)
}
return printOutput(
cmd,
resp.Node.Value,
fmt.Sprintf("Node %s registered", resp.Node.Value.GivenName.Value),
)
response.GetNode(),
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()))
}),
}
// authDecisionRunE builds a RunE for an auth decision command (approve or
// reject) that reads the auth-id flag, invokes the given API call, and prints a
// result. errVerb is used in the error message; okMsg is printed on success.
func authDecisionRunE(
errVerb, okMsg string,
call func(ctx context.Context, client *apiv1.Client, authID string) error,
) func(*cobra.Command, []string) error {
return apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
authID, _ := cmd.Flags().GetString("auth-id")
err := call(ctx, client, authID)
if err != nil {
return fmt.Errorf("%s auth request: %w", errVerb, err)
}
return printOutput(cmd, map[string]string{colResult: okMsg}, okMsg)
})
}
var authApproveCmd = &cobra.Command{
Use: "approve",
Short: "Approve a pending authentication request",
RunE: authDecisionRunE("approving", "Auth request approved",
func(ctx context.Context, client *apiv1.Client, authID string) error {
return client.AuthApprove(ctx, &apiv1.AuthApproveReq{AuthId: apiv1.NewOptString(authID)})
}),
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
authID, _ := cmd.Flags().GetString("auth-id")
request := &v1.AuthApproveRequest{
AuthId: authID,
}
response, err := client.AuthApprove(ctx, request)
if err != nil {
return fmt.Errorf("approving auth request: %w", err)
}
return printOutput(cmd, response, "Auth request approved")
}),
}
var authRejectCmd = &cobra.Command{
Use: "reject",
Short: "Reject a pending authentication request",
RunE: authDecisionRunE("rejecting", "Auth request rejected",
func(ctx context.Context, client *apiv1.Client, authID string) error {
return client.AuthReject(ctx, &apiv1.AuthRejectReq{AuthId: apiv1.NewOptString(authID)})
}),
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
authID, _ := cmd.Flags().GetString("auth-id")
request := &v1.AuthRejectRequest{
AuthId: authID,
}
response, err := client.AuthReject(ctx, request)
if err != nil {
return fmt.Errorf("rejecting auth request: %w", err)
}
return printOutput(cmd, response, "Auth request rejected")
}),
}
-212
View File
@@ -1,212 +0,0 @@
package cli
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
"strings"
"time"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/prometheus/common/model"
"github.com/spf13/cobra"
)
// apiRunE wraps a cobra [cobra.Command.RunE] func, injecting a ready v1 API
// client and context. Connection lifecycle is managed by the wrapper.
func apiRunE(
fn func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error,
) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx, client, cancel, err := newHeadscaleAPIClient()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
return fn(ctx, client, cmd, args)
}
}
// withAPI opens a v1 API client, runs fn with it, and cancels the context
// afterwards. It is the building block for commands that branch on a flag
// before deciding to talk to the server.
func withAPI(fn func(ctx context.Context, client *apiv1.Client) error) error {
ctx, client, cancel, err := newHeadscaleAPIClient()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
return fn(ctx, client)
}
// newHeadscaleAPIClient builds a v1 HTTP API client. With no configured
// address it talks to the local unix socket (filesystem permissions are the
// trust boundary, no API key needed); otherwise it uses HTTPS with the API key.
func newHeadscaleAPIClient() (context.Context, *apiv1.Client, context.CancelFunc, error) {
cfg, err := types.LoadCLIConfig()
if err != nil {
return nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
if cfg.CLI.Address == "" {
client, err := localSocketClient(cfg.UnixSocket)
if err != nil {
cancel()
return nil, nil, nil, err
}
return ctx, client, cancel, nil
}
client, err := remoteClient(cfg.CLI.Address, cfg.CLI.APIKey, cfg.CLI.Insecure)
if err != nil {
cancel()
return nil, nil, nil, err
}
return ctx, client, cancel, nil
}
func localSocketClient(socketPath string) (*apiv1.Client, error) {
err := checkSocketPermissions(socketPath)
if err != nil {
return nil, err
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialSocketWaiting(ctx, socketPath)
},
},
}
// The socket bypasses bearer auth; the token is a placeholder.
return apiv1.NewClient("http://unix", cliToken("local-socket"), apiv1.WithClient(httpClient))
}
// dialSocketWaiting connects to the local unix socket, retrying while it is
// still absent. headscale removes and rebinds the socket during startup, so a
// CLI command issued right after "systemctl start" can beat the server to it.
// This mirrors the old blocking dial, which retried until the CLI timeout.
func dialSocketWaiting(ctx context.Context, socketPath string) (net.Conn, error) {
var dialer net.Dialer
for {
conn, err := dialer.DialContext(ctx, "unix", socketPath)
if err == nil {
return conn, nil
}
if ctx.Err() != nil {
return nil, err
}
select {
case <-ctx.Done():
return nil, err
case <-time.After(100 * time.Millisecond):
}
}
}
func remoteClient(address, apiKey string, insecure bool) (*apiv1.Client, error) {
if apiKey == "" {
return nil, errAPIKeyNotSet
}
transport := &http.Transport{}
if insecure {
//nolint:gosec // G402: insecure is an explicit, documented opt-in.
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
return apiv1.NewClient(
serverURLFromAddress(address),
cliToken(apiKey),
apiv1.WithClient(&http.Client{Transport: transport}),
)
}
// serverURLFromAddress turns a configured CLI address into a base URL,
// defaulting to https when no scheme is given.
func serverURLFromAddress(address string) string {
if strings.Contains(address, "://") {
return address
}
return "https://" + address
}
// checkSocketPermissions gives a friendlier error than a dial failure when the
// user cannot access the headscale socket.
func checkSocketPermissions(socketPath string) error {
socket, err := os.OpenFile(socketPath, os.O_WRONLY, SocketWritePermissions) //nolint
if err != nil {
if os.IsPermission(err) {
return fmt.Errorf(
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
socketPath, err,
)
}
// ENXIO and similar are expected for a socket opened with O_WRONLY; the
// real connection uses net.Dial which handles sockets properly.
return nil
}
socket.Close()
return nil
}
// optString / optUint64 / optTime build optional API request values from flag
// inputs, treating zero values as "unset".
func optString(s string) apiv1.OptString {
if s == "" {
return apiv1.OptString{}
}
return apiv1.NewOptString(s)
}
func optUint64(v uint64) apiv1.OptUint64 {
if v == 0 {
return apiv1.OptUint64{}
}
return apiv1.NewOptUint64(v)
}
// expirationFromFlag parses the --expiration flag as a Prometheus-style
// duration (e.g. "90d", "1h") and returns it as an absolute optional timestamp.
// An empty flag yields an unset value.
func expirationFromFlag(cmd *cobra.Command) (apiv1.OptDateTime, error) {
durationStr, _ := cmd.Flags().GetString("expiration")
if durationStr == "" {
return apiv1.OptDateTime{}, nil
}
duration, err := model.ParseDuration(durationStr)
if err != nil {
return apiv1.OptDateTime{}, fmt.Errorf("parsing duration: %w", err)
}
return apiv1.NewOptDateTime(time.Now().UTC().Add(time.Duration(duration))), nil
}
// cliToken is an [apiv1.SecuritySource] that supplies a fixed bearer token.
type cliToken string
func (t cliToken) BearerAuth(context.Context, apiv1.OperationName) (apiv1.BearerAuth, error) {
return apiv1.BearerAuth{Token: string(t)}, nil
}
+10 -8
View File
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/spf13/cobra"
)
@@ -32,7 +32,7 @@ var debugCmd = &cobra.Command{
var createNodeCmd = &cobra.Command{
Use: "create-node",
Short: "Create a node that can be registered with `auth register <>` command",
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
name, _ := cmd.Flags().GetString("name")
registrationID, _ := cmd.Flags().GetString("key")
@@ -44,16 +44,18 @@ var createNodeCmd = &cobra.Command{
routes, _ := cmd.Flags().GetStringSlice("route")
resp, err := client.DebugCreateNode(ctx, &apiv1.DebugCreateNodeReq{
Key: apiv1.NewOptString(registrationID),
Name: apiv1.NewOptString(name),
User: apiv1.NewOptString(user),
request := &v1.DebugCreateNodeRequest{
Key: registrationID,
Name: name,
User: user,
Routes: routes,
})
}
response, err := client.DebugCreateNode(ctx, request)
if err != nil {
return fmt.Errorf("creating node: %w", err)
}
return printOutput(cmd, resp.Node.Value, "Node created")
return printOutput(cmd, response.GetNode(), "Node created")
}),
}
+4 -4
View File
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/spf13/cobra"
)
@@ -16,12 +16,12 @@ var healthCmd = &cobra.Command{
Use: "health",
Short: "Check the health of the Headscale server",
Long: "Check the health of the Headscale server. This command will return an exit code of 0 if the server is healthy, or 1 if it is not.",
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
resp, err := client.Health(ctx)
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
response, err := client.Health(ctx, &v1.HealthRequest{})
if err != nil {
return fmt.Errorf("checking health: %w", err)
}
return printOutput(cmd, resp, "")
return printOutput(cmd, response, "")
}),
}
+11 -8
View File
@@ -3,7 +3,6 @@ package cli
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
@@ -17,14 +16,18 @@ import (
"github.com/spf13/cobra"
)
var (
errMockOidcClientIDNotDefined = errors.New("MOCKOIDC_CLIENT_ID not defined")
errMockOidcClientSecretNotDefined = errors.New("MOCKOIDC_CLIENT_SECRET not defined")
errMockOidcPortNotDefined = errors.New("MOCKOIDC_PORT not defined")
errMockOidcUsersNotDefined = errors.New("MOCKOIDC_USERS not defined")
)
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
type Error string
const refreshTTL = 60 * time.Minute
func (e Error) Error() string { return string(e) }
const (
errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined")
errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")
errMockOidcPortNotDefined = Error("MOCKOIDC_PORT not defined")
errMockOidcUsersNotDefined = Error("MOCKOIDC_USERS not defined")
refreshTTL = 60 * time.Minute
)
var accessTTL = 2 * time.Minute
+174 -109
View File
@@ -8,11 +8,12 @@ import (
"strings"
"time"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/pterm/pterm"
"github.com/samber/lo"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/timestamppb"
"tailscale.com/types/key"
)
@@ -66,40 +67,41 @@ var registerNodeCmd = &cobra.Command{
Use: "register",
Short: "Registers a node to your network",
Deprecated: "use 'headscale auth register --auth-id <id> --user <user>' instead",
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
registrationID, _ := cmd.Flags().GetString("key")
resp, err := client.RegisterNode(ctx, apiv1.RegisterNodeParams{
Key: optString(registrationID),
User: optString(user),
})
request := &v1.RegisterNodeRequest{
Key: registrationID,
User: user,
}
response, err := client.RegisterNode(ctx, request)
if err != nil {
return fmt.Errorf("registering node: %w", err)
}
return printOutput(
cmd,
resp.Node.Value,
fmt.Sprintf("Node %s registered", resp.Node.Value.GivenName.Value),
)
response.GetNode(),
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()))
}),
}
var listNodesCmd = &cobra.Command{
Use: cmdList,
Use: "list",
Short: "List nodes",
Aliases: []string{"ls", cmdShow},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
Aliases: []string{"ls", "show"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
resp, err := client.ListNodes(ctx, apiv1.ListNodesParams{User: optString(user)})
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{User: user})
if err != nil {
return fmt.Errorf("listing nodes: %w", err)
}
return printListOutput(cmd, resp.Nodes, func() error {
tableData, err := nodesToPtables(resp.Nodes)
return printListOutput(cmd, response.GetNodes(), func() error {
tableData, err := nodesToPtables(user, response.GetNodes())
if err != nil {
return fmt.Errorf("converting to table: %w", err)
}
@@ -113,27 +115,27 @@ var listNodeRoutesCmd = &cobra.Command{
Use: "list-routes",
Short: "List routes available on nodes",
Aliases: []string{"lsr", "routes"},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
resp, err := client.ListNodes(ctx, apiv1.ListNodesParams{})
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{})
if err != nil {
return fmt.Errorf("listing nodes: %w", err)
}
nodes := resp.Nodes
nodes := response.GetNodes()
if identifier != 0 {
for _, node := range resp.Nodes {
if node.ID.Value == identifier {
nodes = []apiv1.Node{node}
for _, node := range response.GetNodes() {
if node.GetId() == identifier {
nodes = []*v1.Node{node}
break
}
}
}
nodes = lo.Filter(nodes, func(n apiv1.Node, _ int) bool {
return len(n.SubnetRoutes) > 0 || len(n.ApprovedRoutes) > 0 || len(n.AvailableRoutes) > 0
nodes = lo.Filter(nodes, func(n *v1.Node, _ int) bool {
return (n.GetSubnetRoutes() != nil && len(n.GetSubnetRoutes()) > 0) || (n.GetApprovedRoutes() != nil && len(n.GetApprovedRoutes()) > 0) || (n.GetAvailableRoutes() != nil && len(n.GetAvailableRoutes()) > 0)
})
return printListOutput(cmd, nodes, func() error {
@@ -143,27 +145,29 @@ var listNodeRoutesCmd = &cobra.Command{
}
var expireNodeCmd = &cobra.Command{
Use: cmdExpire,
Use: "expire",
Short: "Expire (log out) a node in your network",
Long: `Expiring a node will keep the node in the database and force it to reauthenticate.
Use --disable to disable key expiry (node will never expire).`,
Aliases: []string{"logout", aliasExp, "e"},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
Aliases: []string{"logout", "exp", "e"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
disableExpiry, _ := cmd.Flags().GetBool("disable")
// Handle disable expiry - node will never expire.
if disableExpiry {
resp, err := client.ExpireNode(ctx, apiv1.ExpireNodeParams{
NodeID: identifier,
DisableExpiry: apiv1.NewOptBool(true),
})
request := &v1.ExpireNodeRequest{
NodeId: identifier,
DisableExpiry: true,
}
response, err := client.ExpireNode(ctx, request)
if err != nil {
return fmt.Errorf("disabling node expiry: %w", err)
}
return printOutput(cmd, resp.Node.Value, "Node expiry disabled")
return printOutput(cmd, response.GetNode(), "Node expiry disabled")
}
expiry, _ := cmd.Flags().GetString("expiry")
@@ -181,26 +185,28 @@ Use --disable to disable key expiry (node will never expire).`,
}
}
resp, err := client.ExpireNode(ctx, apiv1.ExpireNodeParams{
NodeID: identifier,
Expiry: apiv1.NewOptDateTime(expiryTime),
})
request := &v1.ExpireNodeRequest{
NodeId: identifier,
Expiry: timestamppb.New(expiryTime),
}
response, err := client.ExpireNode(ctx, request)
if err != nil {
return fmt.Errorf("expiring node: %w", err)
}
if now.Equal(expiryTime) || now.After(expiryTime) {
return printOutput(cmd, resp.Node.Value, "Node expired")
return printOutput(cmd, response.GetNode(), "Node expired")
}
return printOutput(cmd, resp.Node.Value, "Node expiration updated")
return printOutput(cmd, response.GetNode(), "Node expiration updated")
}),
}
var renameNodeCmd = &cobra.Command{
Use: "rename NEW_NAME",
Short: "Renames a node in your network",
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
newName := ""
@@ -208,45 +214,55 @@ var renameNodeCmd = &cobra.Command{
newName = args[0]
}
resp, err := client.RenameNode(ctx, apiv1.RenameNodeParams{
NodeID: identifier,
request := &v1.RenameNodeRequest{
NodeId: identifier,
NewName: newName,
})
}
response, err := client.RenameNode(ctx, request)
if err != nil {
return fmt.Errorf("renaming node: %w", err)
}
return printOutput(cmd, resp.Node.Value, "Node renamed")
return printOutput(cmd, response.GetNode(), "Node renamed")
}),
}
var deleteNodeCmd = &cobra.Command{
Use: cmdDelete,
Use: "delete",
Short: "Delete a node",
Aliases: []string{aliasDel},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
Aliases: []string{"del"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
getResp, err := client.GetNode(ctx, apiv1.GetNodeParams{NodeID: identifier})
getRequest := &v1.GetNodeRequest{
NodeId: identifier,
}
getResponse, err := client.GetNode(ctx, getRequest)
if err != nil {
return fmt.Errorf("getting node: %w", err)
}
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the node %s?",
getResp.Node.Value.Name.Value,
)) {
return printOutput(cmd, map[string]string{colResult: "Node not deleted"}, "Node not deleted")
deleteRequest := &v1.DeleteNodeRequest{
NodeId: identifier,
}
err = client.DeleteNode(ctx, apiv1.DeleteNodeParams{NodeID: identifier})
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the node %s?",
getResponse.GetNode().GetName(),
)) {
return printOutput(cmd, map[string]string{"Result": "Node not deleted"}, "Node not deleted")
}
_, err = client.DeleteNode(ctx, deleteRequest)
if err != nil {
return fmt.Errorf("deleting node: %w", err)
}
return printOutput(
cmd,
map[string]string{colResult: "Node deleted"},
map[string]string{"Result": "Node deleted"},
"Node deleted",
)
}),
@@ -267,23 +283,31 @@ all nodes that are missing.
If you remove IPv4 or IPv6 prefixes from the config,
it can be run to remove the IPs that should no longer
be assigned to nodes.`,
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
if !confirmAction(cmd, "Are you sure that you want to assign/remove IPs to/from nodes?") {
return nil
}
resp, err := client.BackfillNodeIPs(ctx, apiv1.BackfillNodeIPsParams{
Confirmed: apiv1.NewOptBool(true),
})
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
changes, err := client.BackfillNodeIPs(ctx, &v1.BackfillNodeIPsRequest{Confirmed: true})
if err != nil {
return fmt.Errorf("backfilling IPs: %w", err)
}
return printOutput(cmd, resp.Changes, "Node IPs backfilled successfully")
}),
return printOutput(cmd, changes, "Node IPs backfilled successfully")
},
}
func nodesToPtables(nodes []apiv1.Node) (pterm.TableData, error) {
func nodesToPtables(
currentUser string,
nodes []*v1.Node,
) (pterm.TableData, error) {
tableHeader := []string{
"ID",
"Hostname",
@@ -295,60 +319,88 @@ func nodesToPtables(nodes []apiv1.Node) (pterm.TableData, error) {
"IP addresses",
"Ephemeral",
"Last seen",
colExpiration,
"Expiration",
"Connected",
"Expired",
}
tableData := make(pterm.TableData, 1, 1+len(nodes))
tableData[0] = tableHeader
tableData := pterm.TableData{tableHeader}
for _, node := range nodes {
ephemeral := node.PreAuthKey.Set && node.PreAuthKey.Value.Ephemeral.Value
var lastSeenTime string
if node.LastSeen.Set {
lastSeenTime = node.LastSeen.Value.Format(HeadscaleDateTimeFormat)
var ephemeral bool
if node.GetPreAuthKey() != nil && node.GetPreAuthKey().GetEphemeral() {
ephemeral = true
}
expiryTime := "N/A"
if node.Expiry.Set {
expiryTime = node.Expiry.Value.Format(HeadscaleDateTimeFormat)
var (
lastSeen time.Time
lastSeenTime string
)
if node.GetLastSeen() != nil {
lastSeen = node.GetLastSeen().AsTime()
lastSeenTime = lastSeen.Format(HeadscaleDateTimeFormat)
}
var (
expiry time.Time
expiryTime string
)
if node.GetExpiry() != nil {
expiry = node.GetExpiry().AsTime()
expiryTime = expiry.Format(HeadscaleDateTimeFormat)
} else {
expiryTime = "N/A"
}
var machineKey key.MachinePublic
err := machineKey.UnmarshalText([]byte(node.MachineKey.Value))
err := machineKey.UnmarshalText(
[]byte(node.GetMachineKey()),
)
if err != nil {
machineKey = key.MachinePublic{}
}
var nodeKey key.NodePublic
err = nodeKey.UnmarshalText([]byte(node.NodeKey.Value))
err = nodeKey.UnmarshalText(
[]byte(node.GetNodeKey()),
)
if err != nil {
return nil, err
}
online := pterm.LightRed("offline")
if node.Online.Value {
var online string
if node.GetOnline() {
online = pterm.LightGreen("online")
} else {
online = pterm.LightRed("offline")
}
expired := pterm.LightGreen("no")
if node.Expiry.Set && node.Expiry.Value.Before(time.Now()) {
var expired string
if node.GetExpiry() != nil && node.GetExpiry().AsTime().Before(time.Now()) {
expired = pterm.LightRed("yes")
} else {
expired = pterm.LightGreen("no")
}
tags := strings.Join(node.Tags, "\n")
var tagsBuilder strings.Builder
for _, tag := range node.GetTags() {
tagsBuilder.WriteString("\n" + tag)
}
tags := strings.TrimLeft(tagsBuilder.String(), "\n")
var user string
if node.User.Set {
user = node.User.Value.Name.Value
if node.GetUser() != nil {
user = node.GetUser().GetName()
}
var ipBuilder strings.Builder
for _, addr := range node.IpAddresses {
for _, addr := range node.GetIpAddresses() {
ip, err := netip.ParseAddr(addr)
if err == nil {
if ipBuilder.Len() > 0 {
@@ -359,28 +411,35 @@ func nodesToPtables(nodes []apiv1.Node) (pterm.TableData, error) {
}
}
ipAddresses := ipBuilder.String()
nodeData := []string{
strconv.FormatUint(node.ID.Value, util.Base10),
node.Name.Value,
node.GivenName.Value,
strconv.FormatUint(node.GetId(), util.Base10),
node.GetName(),
node.GetGivenName(),
machineKey.ShortString(),
nodeKey.ShortString(),
user,
tags,
ipBuilder.String(),
ipAddresses,
strconv.FormatBool(ephemeral),
lastSeenTime,
expiryTime,
online,
expired,
}
tableData = append(tableData, nodeData)
tableData = append(
tableData,
nodeData,
)
}
return tableData, nil
}
func nodeRoutesToPtables(nodes []apiv1.Node) pterm.TableData {
func nodeRoutesToPtables(
nodes []*v1.Node,
) pterm.TableData {
tableHeader := []string{
"ID",
"Hostname",
@@ -388,18 +447,20 @@ func nodeRoutesToPtables(nodes []apiv1.Node) pterm.TableData {
"Available",
"Serving (Primary)",
}
tableData := make(pterm.TableData, 1, 1+len(nodes))
tableData[0] = tableHeader
tableData := pterm.TableData{tableHeader}
for _, node := range nodes {
nodeData := []string{
strconv.FormatUint(node.ID.Value, util.Base10),
node.GivenName.Value,
strings.Join(node.ApprovedRoutes, "\n"),
strings.Join(node.AvailableRoutes, "\n"),
strings.Join(node.SubnetRoutes, "\n"),
strconv.FormatUint(node.GetId(), util.Base10),
node.GetGivenName(),
strings.Join(node.GetApprovedRoutes(), "\n"),
strings.Join(node.GetAvailableRoutes(), "\n"),
strings.Join(node.GetSubnetRoutes(), "\n"),
}
tableData = append(tableData, nodeData)
tableData = append(
tableData,
nodeData,
)
}
return tableData
@@ -409,39 +470,43 @@ var tagCmd = &cobra.Command{
Use: "tag",
Short: "Manage the tags of a node",
Aliases: []string{"tags", "t"},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
tagsToSet, _ := cmd.Flags().GetStringSlice("tags")
resp, err := client.SetTags(
ctx,
&apiv1.SetTagsReq{Tags: tagsToSet},
apiv1.SetTagsParams{NodeID: identifier},
)
// Sending tags to node
request := &v1.SetTagsRequest{
NodeId: identifier,
Tags: tagsToSet,
}
resp, err := client.SetTags(ctx, request)
if err != nil {
return fmt.Errorf("setting tags: %w", err)
}
return printOutput(cmd, resp.Node.Value, "Node updated")
return printOutput(cmd, resp.GetNode(), "Node updated")
}),
}
var approveRoutesCmd = &cobra.Command{
Use: "approve-routes",
Short: "Manage the approved routes of a node",
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
routes, _ := cmd.Flags().GetStringSlice("routes")
resp, err := client.SetApprovedRoutes(
ctx,
&apiv1.SetApprovedRoutesReq{Routes: routes},
apiv1.SetApprovedRoutesParams{NodeID: identifier},
)
// Sending routes to node
request := &v1.SetApprovedRoutesRequest{
NodeId: identifier,
Routes: routes,
}
resp, err := client.SetApprovedRoutes(ctx, request)
if err != nil {
return fmt.Errorf("setting approved routes: %w", err)
}
return printOutput(cmd, resp.Node.Value, "Node updated")
return printOutput(cmd, resp.GetNode(), "Node updated")
}),
}
+57 -53
View File
@@ -1,12 +1,11 @@
package cli
import (
"context"
"errors"
"fmt"
"os"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
@@ -15,13 +14,13 @@ import (
)
const (
bypassFlag = "bypass-server-and-access-database-directly" //nolint:gosec // not a credential
bypassFlag = "bypass-grpc-and-access-database-directly" //nolint:gosec // not a credential
)
var errAborted = errors.New("command aborted by user")
// bypassDatabase loads the server config and opens the database directly,
// bypassing the running server. The caller is responsible for closing the
// bypassing the gRPC server. The caller is responsible for closing the
// returned database handle.
func bypassDatabase() (*db.HSDatabase, error) {
cfg, err := types.LoadServerConfig()
@@ -37,29 +36,19 @@ func bypassDatabase() (*db.HSDatabase, error) {
return d, nil
}
// openBypassDB confirms the destructive bypass action and opens the database
// directly. The caller is responsible for closing the returned handle.
func openBypassDB(cmd *cobra.Command) (*db.HSDatabase, error) {
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
return nil, errAborted
}
return bypassDatabase()
}
func init() {
rootCmd.AddCommand(policyCmd)
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running")
policyCmd.AddCommand(getPolicy)
setPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running")
mustMarkRequired(setPolicy, "file")
policyCmd.AddCommand(setPolicy)
checkPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no running server required) to resolve user references and to evaluate the policy's tests and sshTests blocks. Required when those checks are needed.")
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no gRPC, no running server) to validate user@ token references and to evaluate the policy's tests and sshTests blocks. Required when those checks are needed.")
mustMarkRequired(checkPolicy, "file")
policyCmd.AddCommand(checkPolicy)
}
@@ -72,12 +61,16 @@ var policyCmd = &cobra.Command{
var getPolicy = &cobra.Command{
Use: "get",
Short: "Print the current ACL Policy",
Aliases: []string{cmdShow, "view", "fetch"},
Aliases: []string{"show", "view", "fetch"},
RunE: func(cmd *cobra.Command, args []string) error {
var policyData string
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
d, err := openBypassDB(cmd)
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
return errAborted
}
d, err := bypassDatabase()
if err != nil {
return err
}
@@ -90,19 +83,19 @@ var getPolicy = &cobra.Command{
policyData = pol.Data
} else {
err := withAPI(func(ctx context.Context, client *apiv1.Client) error {
resp, err := client.GetPolicy(ctx)
if err != nil {
return fmt.Errorf("loading ACL policy: %w", err)
}
policyData = resp.Policy.Value
return nil
})
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return err
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
response, err := client.GetPolicy(ctx, &v1.GetPolicyRequest{})
if err != nil {
return fmt.Errorf("loading ACL policy: %w", err)
}
policyData = response.GetPolicy()
}
// This does not pass output format as we don't support yaml, json or
@@ -129,13 +122,17 @@ var setPolicy = &cobra.Command{
}
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
d, err := openBypassDB(cmd)
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
return errAborted
}
d, err := bypassDatabase()
if err != nil {
return err
}
defer d.Close()
users, err := d.ListUsers(nil)
users, err := d.ListUsers()
if err != nil {
return fmt.Errorf("loading users for policy validation: %w", err)
}
@@ -150,18 +147,18 @@ var setPolicy = &cobra.Command{
return fmt.Errorf("setting ACL policy: %w", err)
}
} else {
err := withAPI(func(ctx context.Context, client *apiv1.Client) error {
_, err := client.SetPolicy(ctx, &apiv1.SetPolicyReq{
Policy: apiv1.NewOptString(string(policyBytes)),
})
if err != nil {
return fmt.Errorf("setting ACL policy: %w", err)
}
request := &v1.SetPolicyRequest{Policy: string(policyBytes)}
return nil
})
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return err
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
_, err = client.SetPolicy(ctx, request)
if err != nil {
return fmt.Errorf("setting ACL policy: %w", err)
}
}
@@ -176,8 +173,8 @@ var checkPolicy = &cobra.Command{
Short: "Check the Policy file for errors",
Long: `
Check validates the policy against the server's live users and nodes,
running any "tests" or "sshTests" block. By default the command calls a
running headscale over its API; pass --` + bypassFlag + ` to
running any "tests" or "sshTests" block. By default the command is a
thin frontend for a gRPC call to a running headscale; pass --` + bypassFlag + ` to
open the database directly when headscale is not running.`,
RunE: func(cmd *cobra.Command, args []string) error {
policyPath, _ := cmd.Flags().GetString("file")
@@ -188,13 +185,17 @@ var checkPolicy = &cobra.Command{
}
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
d, err := openBypassDB(cmd)
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
return errAborted
}
d, err := bypassDatabase()
if err != nil {
return err
}
defer d.Close()
users, err := d.ListUsers(nil)
users, err := d.ListUsers()
if err != nil {
return fmt.Errorf("loading users: %w", err)
}
@@ -204,9 +205,9 @@ var checkPolicy = &cobra.Command{
return fmt.Errorf("loading nodes: %w", err)
}
// [policy.NewPolicyManager] validates structure and user references
// NewPolicyManager validates structure and user references
// but intentionally skips test evaluation (boot path).
// [policy.PolicyManager.SetPolicy] is the user-write boundary and is what runs the
// SetPolicy is the user-write boundary and is what runs the
// tests and sshTests blocks.
pm, err := policy.NewPolicyManager(policyBytes, users, nodes.ViewSlice())
if err != nil {
@@ -223,11 +224,14 @@ var checkPolicy = &cobra.Command{
return nil
}
err = withAPI(func(ctx context.Context, client *apiv1.Client) error {
return client.CheckPolicy(ctx, &apiv1.CheckPolicyReq{
Policy: apiv1.NewOptString(string(policyBytes)),
})
})
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
_, err = client.CheckPolicy(ctx, &v1.CheckPolicyRequest{Policy: string(policyBytes)})
if err != nil {
return err
}
+72 -68
View File
@@ -6,8 +6,9 @@ import (
"strconv"
"strings"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
@@ -41,56 +42,57 @@ var preauthkeysCmd = &cobra.Command{
}
var listPreAuthKeys = &cobra.Command{
Use: cmdList,
Use: "list",
Short: "List all preauthkeys",
Aliases: []string{"ls", cmdShow},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
resp, err := client.ListPreAuthKeys(ctx)
Aliases: []string{"ls", "show"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
if err != nil {
return fmt.Errorf("listing preauthkeys: %w", err)
}
return printListOutput(cmd, resp.PreAuthKeys, func() error {
rows := make([][]string, 0, len(resp.PreAuthKeys))
for _, key := range resp.PreAuthKeys {
return printListOutput(cmd, response.GetPreAuthKeys(), func() error {
tableData := pterm.TableData{
{
"ID",
"Key/Prefix",
"Reusable",
"Ephemeral",
"Used",
"Expiration",
"Created",
"Owner",
},
}
for _, key := range response.GetPreAuthKeys() {
expiration := "-"
if key.Expiration.Set {
expiration = ColourTime(key.Expiration.Value)
if key.GetExpiration() != nil {
expiration = ColourTime(key.GetExpiration().AsTime())
}
var owner string
switch {
case len(key.AclTags) > 0:
owner = strings.Join(key.AclTags, "\n")
case key.User.Set:
owner = key.User.Value.Name.Value
default:
if len(key.GetAclTags()) > 0 {
owner = strings.Join(key.GetAclTags(), "\n")
} else if key.GetUser() != nil {
owner = key.GetUser().GetName()
} else {
owner = "-"
}
rows = append(rows, []string{
strconv.FormatUint(key.ID.Value, util.Base10),
key.Key.Value,
strconv.FormatBool(key.Reusable.Value),
strconv.FormatBool(key.Ephemeral.Value),
strconv.FormatBool(key.Used.Value),
tableData = append(tableData, []string{
strconv.FormatUint(key.GetId(), util.Base10),
key.GetKey(),
strconv.FormatBool(key.GetReusable()),
strconv.FormatBool(key.GetEphemeral()),
strconv.FormatBool(key.GetUsed()),
expiration,
key.CreatedAt.Value.Format(HeadscaleDateTimeFormat),
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
owner,
})
}
return renderTable([]string{
"ID",
"Key/Prefix",
"Reusable",
"Ephemeral",
"Used",
colExpiration,
colCreated,
"Owner",
}, rows)
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
})
}),
}
@@ -98,8 +100,8 @@ var listPreAuthKeys = &cobra.Command{
var createPreAuthKeyCmd = &cobra.Command{
Use: "create",
Short: "Creates a new preauthkey",
Aliases: []string{"c", cmdNew},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
Aliases: []string{"c", "new"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetUint64("user")
reusable, _ := cmd.Flags().GetBool("reusable")
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
@@ -110,65 +112,67 @@ var createPreAuthKeyCmd = &cobra.Command{
return err
}
resp, err := client.CreatePreAuthKey(ctx, &apiv1.CreatePreAuthKeyReq{
User: optUint64(user),
Reusable: apiv1.NewOptBool(reusable),
Ephemeral: apiv1.NewOptBool(ephemeral),
request := &v1.CreatePreAuthKeyRequest{
User: user,
Reusable: reusable,
Ephemeral: ephemeral,
AclTags: tags,
Expiration: expiration,
})
}
response, err := client.CreatePreAuthKey(ctx, request)
if err != nil {
return fmt.Errorf("creating preauthkey: %w", err)
}
return printOutput(cmd, resp.PreAuthKey.Value, resp.PreAuthKey.Value.Key.Value)
return printOutput(cmd, response.GetPreAuthKey(), response.GetPreAuthKey().GetKey())
}),
}
// preAuthKeyID reads the required --id flag for preauthkey commands.
func preAuthKeyID(cmd *cobra.Command) (uint64, error) {
id, _ := cmd.Flags().GetUint64("id")
if id == 0 {
return 0, fmt.Errorf("missing --id parameter: %w", errMissingParameter)
}
return id, nil
}
var expirePreAuthKeyCmd = &cobra.Command{
Use: cmdExpire,
Use: "expire",
Short: "Expire a preauthkey",
Aliases: []string{"revoke", aliasExp, "e"},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
id, err := preAuthKeyID(cmd)
if err != nil {
return err
Aliases: []string{"revoke", "exp", "e"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
id, _ := cmd.Flags().GetUint64("id")
if id == 0 {
return fmt.Errorf("missing --id parameter: %w", errMissingParameter)
}
err = client.ExpirePreAuthKey(ctx, &apiv1.ExpirePreAuthKeyReq{ID: apiv1.NewOptUint64(id)})
request := &v1.ExpirePreAuthKeyRequest{
Id: id,
}
response, err := client.ExpirePreAuthKey(ctx, request)
if err != nil {
return fmt.Errorf("expiring preauthkey: %w", err)
}
return printOutput(cmd, map[string]string{colResult: "Key expired"}, "Key expired")
return printOutput(cmd, response, "Key expired")
}),
}
var deletePreAuthKeyCmd = &cobra.Command{
Use: cmdDelete,
Use: "delete",
Short: "Delete a preauthkey",
Aliases: []string{aliasDel, "rm", "d"},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
id, err := preAuthKeyID(cmd)
if err != nil {
return err
Aliases: []string{"del", "rm", "d"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
id, _ := cmd.Flags().GetUint64("id")
if id == 0 {
return fmt.Errorf("missing --id parameter: %w", errMissingParameter)
}
err = client.DeletePreAuthKey(ctx, apiv1.DeletePreAuthKeyParams{ID: apiv1.NewOptUint64(id)})
request := &v1.DeletePreAuthKeyRequest{
Id: id,
}
response, err := client.DeletePreAuthKey(ctx, request)
if err != nil {
return fmt.Errorf("deleting preauthkey: %w", err)
}
return printOutput(cmd, map[string]string{colResult: "Key deleted"}, "Key deleted")
return printOutput(cmd, response, "Key deleted")
}),
}
+15 -6
View File
@@ -3,7 +3,6 @@ package cli
import (
"os"
"runtime"
"slices"
"strings"
"github.com/juanfont/headscale/hscontrol/types"
@@ -31,7 +30,7 @@ func init() {
Bool("force", false, "Disable prompts and forces the execution")
// Re-enable usage output only for flag-parsing errors; runtime errors
// from [cobra.Command.RunE] should never dump usage text.
// from RunE should never dump usage text.
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
cmd.SilenceUsage = false
@@ -96,9 +95,13 @@ func initConfig() {
var prereleases = []string{"alpha", "beta", "rc", "dev"}
func isPreReleaseVersion(version string) bool {
return slices.ContainsFunc(prereleases, func(unstable string) bool {
return strings.Contains(version, unstable)
})
for _, unstable := range prereleases {
if strings.Contains(version, unstable) {
return true
}
}
return false
}
// filterPreReleasesIfStable returns a function that filters out
@@ -117,7 +120,13 @@ func filterPreReleasesIfStable(versionFunc func() string) func(string) bool {
}
// If we are on a stable release, filter out pre-releases.
return isPreReleaseVersion(tag)
for _, ignore := range prereleases {
if strings.Contains(tag, ignore) {
return true
}
}
return false
}
}
+46 -61
View File
@@ -4,19 +4,6 @@ import (
"testing"
)
const (
v23 = "0.23.0"
v23Alpha1 = "0.23.0-alpha.1"
v23Beta1 = "0.23.0-beta.1"
v23RC1 = "0.23.0-rc.1"
v23Dev = "0.23.0-dev"
v231 = "0.23.1"
v24Alpha1Tag = "v0.24.0-alpha.1"
v24RCTag = "v0.24.0-rc.1"
v24Tag = "v0.24.0"
)
func TestFilterPreReleasesIfStable(t *testing.T) {
tests := []struct {
name string
@@ -27,64 +14,64 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
}{
{
name: "stable version filters alpha tag",
currentVersion: v23,
tag: v24Alpha1Tag,
currentVersion: "0.23.0",
tag: "v0.24.0-alpha.1",
expectedFilter: true,
description: "When on stable release, alpha tags should be filtered",
},
{
name: "stable version filters beta tag",
currentVersion: v23,
currentVersion: "0.23.0",
tag: "v0.24.0-beta.2",
expectedFilter: true,
description: "When on stable release, beta tags should be filtered",
},
{
name: "stable version filters rc tag",
currentVersion: v23,
tag: v24RCTag,
currentVersion: "0.23.0",
tag: "v0.24.0-rc.1",
expectedFilter: true,
description: "When on stable release, rc tags should be filtered",
},
{
name: "stable version allows stable tag",
currentVersion: v23,
tag: v24Tag,
currentVersion: "0.23.0",
tag: "v0.24.0",
expectedFilter: false,
description: "When on stable release, stable tags should not be filtered",
},
{
name: "alpha version allows alpha tag",
currentVersion: v23Alpha1,
currentVersion: "0.23.0-alpha.1",
tag: "v0.24.0-alpha.2",
expectedFilter: false,
description: "When on alpha release, alpha tags should not be filtered",
},
{
name: "alpha version allows beta tag",
currentVersion: v23Alpha1,
currentVersion: "0.23.0-alpha.1",
tag: "v0.24.0-beta.1",
expectedFilter: false,
description: "When on alpha release, beta tags should not be filtered",
},
{
name: "alpha version allows rc tag",
currentVersion: v23Alpha1,
tag: v24RCTag,
currentVersion: "0.23.0-alpha.1",
tag: "v0.24.0-rc.1",
expectedFilter: false,
description: "When on alpha release, rc tags should not be filtered",
},
{
name: "alpha version allows stable tag",
currentVersion: v23Alpha1,
tag: v24Tag,
currentVersion: "0.23.0-alpha.1",
tag: "v0.24.0",
expectedFilter: false,
description: "When on alpha release, stable tags should not be filtered",
},
{
name: "beta version allows alpha tag",
currentVersion: v23Beta1,
tag: v24Alpha1Tag,
currentVersion: "0.23.0-beta.1",
tag: "v0.24.0-alpha.1",
expectedFilter: false,
description: "When on beta release, alpha tags should not be filtered",
},
@@ -97,28 +84,28 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
},
{
name: "beta version allows rc tag",
currentVersion: v23Beta1,
tag: v24RCTag,
currentVersion: "0.23.0-beta.1",
tag: "v0.24.0-rc.1",
expectedFilter: false,
description: "When on beta release, rc tags should not be filtered",
},
{
name: "beta version allows stable tag",
currentVersion: v23Beta1,
tag: v24Tag,
currentVersion: "0.23.0-beta.1",
tag: "v0.24.0",
expectedFilter: false,
description: "When on beta release, stable tags should not be filtered",
},
{
name: "rc version allows alpha tag",
currentVersion: v23RC1,
tag: v24Alpha1Tag,
currentVersion: "0.23.0-rc.1",
tag: "v0.24.0-alpha.1",
expectedFilter: false,
description: "When on rc release, alpha tags should not be filtered",
},
{
name: "rc version allows beta tag",
currentVersion: v23RC1,
currentVersion: "0.23.0-rc.1",
tag: "v0.24.0-beta.1",
expectedFilter: false,
description: "When on rc release, beta tags should not be filtered",
@@ -132,78 +119,78 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
},
{
name: "rc version allows stable tag",
currentVersion: v23RC1,
tag: v24Tag,
currentVersion: "0.23.0-rc.1",
tag: "v0.24.0",
expectedFilter: false,
description: "When on rc release, stable tags should not be filtered",
},
{
name: "stable version with patch filters alpha",
currentVersion: v231,
tag: v24Alpha1Tag,
currentVersion: "0.23.1",
tag: "v0.24.0-alpha.1",
expectedFilter: true,
description: "Stable version with patch number should filter alpha tags",
},
{
name: "stable version with patch allows stable",
currentVersion: v231,
tag: v24Tag,
currentVersion: "0.23.1",
tag: "v0.24.0",
expectedFilter: false,
description: "Stable version with patch number should allow stable tags",
},
{
name: "tag with alpha substring in version number",
currentVersion: v23,
currentVersion: "0.23.0",
tag: "v1.0.0-alpha.1",
expectedFilter: true,
description: "Tags with alpha in version string should be filtered on stable",
},
{
name: "tag with beta substring in version number",
currentVersion: v23,
currentVersion: "0.23.0",
tag: "v1.0.0-beta.1",
expectedFilter: true,
description: "Tags with beta in version string should be filtered on stable",
},
{
name: "tag with rc substring in version number",
currentVersion: v23,
currentVersion: "0.23.0",
tag: "v1.0.0-rc.1",
expectedFilter: true,
description: "Tags with rc in version string should be filtered on stable",
},
{
name: "empty tag on stable version",
currentVersion: v23,
currentVersion: "0.23.0",
tag: "",
expectedFilter: false,
description: "Empty tags should not be filtered",
},
{
name: "dev version allows all tags",
currentVersion: v23Dev,
tag: v24Alpha1Tag,
currentVersion: "0.23.0-dev",
tag: "v0.24.0-alpha.1",
expectedFilter: false,
description: "Dev versions should not filter any tags (pre-release allows all)",
},
{
name: "stable version filters dev tag",
currentVersion: v23,
currentVersion: "0.23.0",
tag: "v0.24.0-dev",
expectedFilter: true,
description: "When on stable release, dev tags should be filtered",
},
{
name: "dev version allows dev tag",
currentVersion: v23Dev,
currentVersion: "0.23.0-dev",
tag: "v0.24.0-dev.1",
expectedFilter: false,
description: "When on dev release, dev tags should not be filtered",
},
{
name: "dev version allows stable tag",
currentVersion: v23Dev,
tag: v24Tag,
currentVersion: "0.23.0-dev",
tag: "v0.24.0",
expectedFilter: false,
description: "When on dev release, stable tags should not be filtered",
},
@@ -213,8 +200,7 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
result := filterPreReleasesIfStable(func() string { return tt.currentVersion })(tt.tag)
if result != tt.expectedFilter {
t.Errorf(
"%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
t.Errorf("%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
tt.name,
result,
tt.expectedFilter,
@@ -236,25 +222,25 @@ func TestIsPreReleaseVersion(t *testing.T) {
}{
{
name: "stable version",
version: v23,
version: "0.23.0",
expected: false,
description: "Stable version should not be pre-release",
},
{
name: "alpha version",
version: v23Alpha1,
version: "0.23.0-alpha.1",
expected: true,
description: "Alpha version should be pre-release",
},
{
name: "beta version",
version: v23Beta1,
version: "0.23.0-beta.1",
expected: true,
description: "Beta version should be pre-release",
},
{
name: "rc version",
version: v23RC1,
version: "0.23.0-rc.1",
expected: true,
description: "RC version should be pre-release",
},
@@ -272,7 +258,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
},
{
name: "dev version",
version: v23Dev,
version: "0.23.0-dev",
expected: true,
description: "Dev version should be pre-release",
},
@@ -284,7 +270,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
},
{
name: "version with patch number",
version: v231,
version: "0.23.1",
expected: false,
description: "Stable version with patch should not be pre-release",
},
@@ -294,8 +280,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
result := isPreReleaseVersion(tt.version)
if result != tt.expected {
t.Errorf(
"%s: got %v, want %v\nDescription: %s\nVersion: %s",
t.Errorf("%s: got %v, want %v\nDescription: %s\nVersion: %s",
tt.name,
result,
tt.expected,
-23
View File
@@ -1,23 +0,0 @@
package cli
// Shared CLI vocabulary used across multiple command definitions in this
// package. Centralising the strings prevents goconst drift and ensures a
// typo in a subcommand name fails to compile rather than silently
// breaking the binding.
const (
// Subcommand verbs (cobra Use field).
cmdList = "list"
cmdShow = "show"
cmdNew = "new"
cmdDelete = "delete"
cmdExpire = "expire"
// Subcommand aliases.
aliasDel = "del"
aliasExp = "exp"
// Output table column headers and printOutput map keys.
colResult = "Result"
colCreated = "Created"
colExpiration = "Expiration"
)
+91 -74
View File
@@ -7,8 +7,11 @@ import (
"net/url"
"strconv"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
@@ -34,38 +37,13 @@ func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string, error) {
// Normalise unset/negative identifiers to 0 so the uint64
// conversion does not produce a bogus large value.
identifier = max(identifier, 0)
if identifier < 0 {
identifier = 0
}
return uint64(identifier), username, nil //nolint:gosec // identifier is clamped to >= 0 above
}
// resolveSingleUser resolves exactly one user from the --name/--id flags,
// returning the raw flag id and the matched user.
func resolveSingleUser(
ctx context.Context,
client *apiv1.Client,
cmd *cobra.Command,
) (uint64, *apiv1.User, error) {
id, username, err := usernameAndIDFromFlag(cmd)
if err != nil {
return 0, nil, err
}
resp, err := client.ListUsers(ctx, apiv1.ListUsersParams{
Name: optString(username),
ID: optUint64(id),
})
if err != nil {
return 0, nil, fmt.Errorf("listing users: %w", err)
}
if len(resp.Users) != 1 {
return 0, nil, errMultipleUsersMatch
}
return id, &resp.Users[0], nil
}
func init() {
rootCmd.AddCommand(userCmd)
userCmd.AddCommand(createUserCmd)
@@ -92,7 +70,7 @@ var userCmd = &cobra.Command{
var createUserCmd = &cobra.Command{
Use: "create NAME",
Short: "Creates a new user",
Aliases: []string{"c", cmdNew},
Aliases: []string{"c", "new"},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errMissingParameter
@@ -100,67 +78,90 @@ var createUserCmd = &cobra.Command{
return nil
},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
req := &apiv1.CreateUserReq{Name: apiv1.NewOptString(args[0])}
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
userName := args[0]
log.Trace().Interface(zf.Client, client).Msg("obtained gRPC client")
request := &v1.CreateUserRequest{Name: userName}
if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" {
req.DisplayName = apiv1.NewOptString(displayName)
request.DisplayName = displayName
}
if email, _ := cmd.Flags().GetString("email"); email != "" {
req.Email = apiv1.NewOptString(email)
request.Email = email
}
if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
_, err := url.Parse(pictureURL)
if err != nil {
if _, err := url.Parse(pictureURL); err != nil { //nolint:noinlineerr
return fmt.Errorf("invalid picture URL: %w", err)
}
req.PictureUrl = apiv1.NewOptString(pictureURL)
request.PictureUrl = pictureURL
}
resp, err := client.CreateUser(ctx, req)
log.Trace().Interface(zf.Request, request).Msg("sending CreateUser request")
response, err := client.CreateUser(ctx, request)
if err != nil {
return fmt.Errorf("creating user: %w", err)
}
return printOutput(cmd, resp.User.Value, "User created")
return printOutput(cmd, response.GetUser(), "User created")
}),
}
var destroyUserCmd = &cobra.Command{
Use: "destroy --identifier ID or --name NAME",
Short: "Destroys a user",
Aliases: []string{cmdDelete},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
_, user, err := resolveSingleUser(ctx, client, cmd)
Aliases: []string{"delete"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
id, username, err := usernameAndIDFromFlag(cmd)
if err != nil {
return err
}
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
user.Name.Value, user.ID.Value,
)) {
return printOutput(cmd, map[string]string{colResult: "User not destroyed"}, "User not destroyed")
request := &v1.ListUsersRequest{
Name: username,
Id: id,
}
err = client.DeleteUser(ctx, apiv1.DeleteUserParams{ID: user.ID.Value})
users, err := client.ListUsers(ctx, request)
if err != nil {
return fmt.Errorf("listing users: %w", err)
}
if len(users.GetUsers()) != 1 {
return errMultipleUsersMatch
}
user := users.GetUsers()[0]
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
user.GetName(), user.GetId(),
)) {
return printOutput(cmd, map[string]string{"Result": "User not destroyed"}, "User not destroyed")
}
deleteRequest := &v1.DeleteUserRequest{Id: user.GetId()}
response, err := client.DeleteUser(ctx, deleteRequest)
if err != nil {
return fmt.Errorf("destroying user: %w", err)
}
return printOutput(cmd, map[string]string{colResult: "User destroyed"}, "User destroyed")
return printOutput(cmd, response, "User destroyed")
}),
}
var listUsersCmd = &cobra.Command{
Use: cmdList,
Use: "list",
Short: "List all the users",
Aliases: []string{"ls", cmdShow},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
var params apiv1.ListUsersParams
Aliases: []string{"ls", "show"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
request := &v1.ListUsersRequest{}
id, _ := cmd.Flags().GetInt64("identifier")
username, _ := cmd.Flags().GetString("name")
@@ -169,34 +170,34 @@ var listUsersCmd = &cobra.Command{
// filter by one param at most
switch {
case id > 0:
params.ID = apiv1.NewOptUint64(uint64(id))
request.Id = uint64(id)
case username != "":
params.Name = apiv1.NewOptString(username)
request.Name = username
case email != "":
params.Email = apiv1.NewOptString(email)
request.Email = email
}
resp, err := client.ListUsers(ctx, params)
response, err := client.ListUsers(ctx, request)
if err != nil {
return fmt.Errorf("listing users: %w", err)
}
return printListOutput(cmd, resp.Users, func() error {
rows := make([][]string, 0, len(resp.Users))
for _, user := range resp.Users {
rows = append(
rows,
return printListOutput(cmd, response.GetUsers(), func() error {
tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}}
for _, user := range response.GetUsers() {
tableData = append(
tableData,
[]string{
strconv.FormatUint(user.ID.Value, util.Base10),
user.DisplayName.Value,
user.Name.Value,
user.Email.Value,
user.CreatedAt.Value.Format(HeadscaleDateTimeFormat),
strconv.FormatUint(user.GetId(), util.Base10),
user.GetDisplayName(),
user.GetName(),
user.GetEmail(),
user.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
},
)
}
return renderTable([]string{"ID", "Name", "Username", "Email", colCreated}, rows)
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
})
}),
}
@@ -205,22 +206,38 @@ var renameUserCmd = &cobra.Command{
Use: "rename",
Short: "Renames a user",
Aliases: []string{"mv"},
RunE: apiRunE(func(ctx context.Context, client *apiv1.Client, cmd *cobra.Command, args []string) error {
id, _, err := resolveSingleUser(ctx, client, cmd)
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
id, username, err := usernameAndIDFromFlag(cmd)
if err != nil {
return err
}
listReq := &v1.ListUsersRequest{
Name: username,
Id: id,
}
users, err := client.ListUsers(ctx, listReq)
if err != nil {
return fmt.Errorf("listing users: %w", err)
}
if len(users.GetUsers()) != 1 {
return errMultipleUsersMatch
}
newName, _ := cmd.Flags().GetString("new-name")
resp, err := client.RenameUser(ctx, apiv1.RenameUserParams{
OldID: id,
renameReq := &v1.RenameUserRequest{
OldId: id,
NewName: newName,
})
}
response, err := client.RenameUser(ctx, renameReq)
if err != nil {
return fmt.Errorf("renaming user: %w", err)
}
return printOutput(cmd, resp.User.Value, "User renamed")
return printOutput(cmd, response.GetUser(), "User renamed")
}),
}
+174 -59
View File
@@ -1,18 +1,26 @@
package cli
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"slices"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/pterm/pterm"
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
"github.com/prometheus/common/model"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/timestamppb"
"gopkg.in/yaml.v3"
)
@@ -59,37 +67,124 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
return app, nil
}
// addressableForJSON makes value-typed generated API structs (and slice
// elements) addressable so their pointer-receiver MarshalJSON is used. That
// method omits unset optional fields; stdlib reflection over a value instead
// calls Opt*.MarshalJSON directly, which returns empty for an unset field and
// fails with "unexpected end of JSON input".
func addressableForJSON(v any) any {
rv := reflect.ValueOf(v)
// grpcRunE wraps a cobra RunE func, injecting a ready gRPC client and
// context. Connection lifecycle is managed by the wrapper — callers
// never see the underlying conn or cancel func.
func grpcRunE(
fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error,
) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
switch rv.Kind() { //nolint:exhaustive // default handles all other kinds
case reflect.Slice, reflect.Array:
out := make([]any, rv.Len())
for i := range out {
out[i] = addressableForJSON(rv.Index(i).Interface())
return fn(ctx, client, cmd, args)
}
}
func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc, error) {
cfg, err := types.LoadCLIConfig()
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
}
log.Debug().
Dur("timeout", cfg.CLI.Timeout).
Msgf("Setting timeout")
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
grpcOptions := []grpc.DialOption{
grpc.WithBlock(), //nolint:staticcheck // SA1019: deprecated but supported in 1.x
}
address := cfg.CLI.Address
// If the address is not set, we assume that we are on the server hosting hscontrol.
if address == "" {
log.Debug().
Str("socket", cfg.UnixSocket).
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
address = cfg.UnixSocket
// Try to give the user better feedback if we cannot write to the headscale
// socket. Note: os.OpenFile on a Unix domain socket returns ENXIO on
// Linux which is expected — only permission errors are actionable here.
// The actual gRPC connection uses net.Dial which handles sockets properly.
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
if err != nil {
if os.IsPermission(err) {
cancel()
return nil, nil, nil, nil, fmt.Errorf(
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
cfg.UnixSocket,
err,
)
}
} else {
socket.Close()
}
return out
case reflect.Struct:
p := reflect.New(rv.Type())
p.Elem().Set(rv)
grpcOptions = append(
grpcOptions,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(util.GrpcSocketDialer),
)
} else {
// If we are not connecting to a local server, require an API key for authentication
apiKey := cfg.CLI.APIKey
if apiKey == "" {
cancel()
return p.Interface()
default:
return v
return nil, nil, nil, nil, errAPIKeyNotSet
}
grpcOptions = append(grpcOptions,
grpc.WithPerRPCCredentials(tokenAuth{
token: apiKey,
}),
)
if cfg.CLI.Insecure {
tlsConfig := &tls.Config{
// turn of gosec as we are intentionally setting
// insecure.
//nolint:gosec
InsecureSkipVerify: true,
}
grpcOptions = append(grpcOptions,
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
)
} else {
grpcOptions = append(grpcOptions,
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
)
}
}
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via gRPC")
conn, err := grpc.DialContext(ctx, address, grpcOptions...) //nolint:staticcheck // SA1019: deprecated but supported in 1.x
if err != nil {
cancel()
return nil, nil, nil, nil, fmt.Errorf("connecting to %s: %w", address, err)
}
client := v1.NewHeadscaleServiceClient(conn)
return ctx, client, conn, cancel, nil
}
// formatOutput serialises result into the requested format. For the
// default (empty) format the human-readable override string is returned.
func formatOutput(result any, override string, outputFormat string) (string, error) {
result = addressableForJSON(result)
switch outputFormat {
case outputFormatJSON:
b, err := json.MarshalIndent(result, "", "\t")
@@ -106,21 +201,7 @@ func formatOutput(result any, override string, outputFormat string) (string, err
return string(b), nil
case outputFormatYAML:
// Route through JSON so types with a custom MarshalJSON (the generated
// API types) serialise by their JSON shape, then convert to YAML.
j, err := json.Marshal(result)
if err != nil {
return "", fmt.Errorf("marshalling output: %w", err)
}
var generic any
err = yaml.Unmarshal(j, &generic)
if err != nil {
return "", fmt.Errorf("converting output to YAML: %w", err)
}
b, err := yaml.Marshal(generic)
b, err := yaml.Marshal(result)
if err != nil {
return "", fmt.Errorf("marshalling YAML output: %w", err)
}
@@ -146,6 +227,19 @@ func printOutput(cmd *cobra.Command, result any, override string) error {
return nil
}
// expirationFromFlag parses the --expiration flag as a Prometheus-style
// duration (e.g. "90d", "1h") and returns an absolute timestamp.
func expirationFromFlag(cmd *cobra.Command) (*timestamppb.Timestamp, error) {
durationStr, _ := cmd.Flags().GetString("expiration")
duration, err := model.ParseDuration(durationStr)
if err != nil {
return nil, fmt.Errorf("parsing duration: %w", err)
}
return timestamppb.New(time.Now().UTC().Add(time.Duration(duration))), nil
}
// confirmAction returns true when the user confirms a prompt, or when
// --force is set. Callers decide what to do when it returns false.
func confirmAction(cmd *cobra.Command, prompt string) bool {
@@ -157,19 +251,9 @@ func confirmAction(cmd *cobra.Command, prompt string) bool {
return util.YesNo(prompt)
}
// renderTable prints a human-readable pterm table with the given header row
// and data rows, using the shared header styling.
func renderTable(header []string, rows [][]string) error {
tableData := make(pterm.TableData, 0, 1+len(rows))
tableData = append(tableData, header)
tableData = append(tableData, rows...)
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
}
// printListOutput checks the --output flag: when a machine-readable format is
// requested it serialises data as JSON/YAML; otherwise it calls the render
// callback to produce the human-readable pterm table.
// requested it serialises data as JSON/YAML; otherwise it calls renderTable
// to produce the human-readable pterm table.
func printListOutput(
cmd *cobra.Command,
data any,
@@ -185,25 +269,56 @@ func printListOutput(
// printError writes err to stderr, formatting it as JSON/YAML when the
// --output flag requests machine-readable output. Used exclusively by
// [Execute] so that every error surfaces in the format the caller asked for.
// Execute() so that every error surfaces in the format the caller asked for.
func printError(err error, outputFormat string) {
type errOutput struct {
Error string `json:"error"`
}
if outputFormat == "" {
e := errOutput{Error: err.Error()}
var formatted []byte
switch outputFormat {
case outputFormatJSON:
formatted, _ = json.MarshalIndent(e, "", "\t") //nolint:errchkjson // errOutput contains only a string field
case outputFormatJSONLine:
formatted, _ = json.Marshal(e) //nolint:errchkjson // errOutput contains only a string field
case outputFormatYAML:
formatted, _ = yaml.Marshal(e)
default:
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}
// formatOutput cannot fail here: errOutput is a single string field.
out, _ := formatOutput(errOutput{Error: err.Error()}, "", outputFormat)
fmt.Fprintf(os.Stderr, "%s\n", out)
fmt.Fprintf(os.Stderr, "%s\n", formatted)
}
func hasMachineOutputFlag() bool {
return slices.ContainsFunc(os.Args, func(arg string) bool {
return arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML
})
for _, arg := range os.Args {
if arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML {
return true
}
}
return false
}
type tokenAuth struct {
token string
}
// Return value is mapped to request headers.
func (t tokenAuth) GetRequestMetadata(
ctx context.Context,
in ...string,
) (map[string]string, error) {
return map[string]string{
"authorization": "Bearer " + t.token,
}, nil
}
func (tokenAuth) RequireTransportSecurity() bool {
return true
}
-152
View File
@@ -1,152 +0,0 @@
package cli
import (
"encoding/json"
"strings"
"testing"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
"tailscale.com/types/key"
)
// sampleNode builds a fully-populated node with valid machine/node keys so the
// table renderer's UnmarshalText/ParseAddr paths exercise real data.
func sampleNode(t *testing.T) apiv1.Node {
t.Helper()
mkey, err := key.NewMachine().Public().MarshalText()
if err != nil {
t.Fatalf("machine key: %v", err)
}
nkey, err := key.NewNode().Public().MarshalText()
if err != nil {
t.Fatalf("node key: %v", err)
}
return apiv1.Node{
ID: apiv1.NewOptUint64(7),
Name: apiv1.NewOptString("host7"),
GivenName: apiv1.NewOptString("host7"),
MachineKey: apiv1.NewOptString(string(mkey)),
NodeKey: apiv1.NewOptString(string(nkey)),
User: apiv1.NewOptUser(apiv1.User{Name: apiv1.NewOptString("alice")}),
IpAddresses: []string{"100.64.0.7", "fd7a:115c:a1e0::7"},
Online: apiv1.NewOptBool(true),
// Expiry, LastSeen, Tags deliberately unset.
}
}
// TestFormatOutputValueWithUnsetOptional guards against a regression where the
// generated API types, passed by value with unset optional fields, fail to
// marshal because stdlib reflection calls Opt*.MarshalJSON directly.
func TestFormatOutputValueWithUnsetOptional(t *testing.T) {
cases := map[string]any{
"user": apiv1.User{ID: apiv1.NewOptUint64(1), Name: apiv1.NewOptString("alice")},
"node": sampleNode(t),
"preauthkey": apiv1.PreAuthKey{ID: apiv1.NewOptUint64(2), Key: apiv1.NewOptString("abc")},
"apikey": apiv1.ApiKey{ID: apiv1.NewOptUint64(3), Prefix: apiv1.NewOptString("pref")},
}
formats := []string{outputFormatJSON, outputFormatJSONLine, outputFormatYAML}
for name, val := range cases {
for _, format := range formats {
// Single value.
out, err := formatOutput(val, "", format)
if err != nil {
t.Fatalf("formatOutput(%s, %s) value: %v", name, format, err)
}
if strings.TrimSpace(out) == "" {
t.Errorf("formatOutput(%s, %s) value: empty output", name, format)
}
// Slice of values (the "list" commands).
sliceOut, err := formatOutput([]any{val}, "", format)
if err != nil {
t.Fatalf("formatOutput(%s, %s) slice: %v", name, format, err)
}
if strings.TrimSpace(sliceOut) == "" {
t.Errorf("formatOutput(%s, %s) slice: empty output", name, format)
}
}
}
}
// TestFormatOutputJSONShape pins that a value-typed object serialises by its
// JSON field names (not the Go struct shape) and omits unset optionals.
func TestFormatOutputJSONShape(t *testing.T) {
user := apiv1.User{ID: apiv1.NewOptUint64(1), Name: apiv1.NewOptString("alice")}
out, err := formatOutput(user, "", outputFormatJSON)
if err != nil {
t.Fatalf("formatOutput: %v", err)
}
var decoded map[string]any
err = json.Unmarshal([]byte(out), &decoded)
if err != nil {
t.Fatalf("output is not valid JSON: %v\n%s", err, out)
}
if decoded["name"] != "alice" || decoded["id"] != float64(1) {
t.Errorf("unexpected JSON shape: %s", out)
}
if _, present := decoded["displayName"]; present {
t.Errorf("unset optional should be omitted, got: %s", out)
}
}
// TestNodesToPtables exercises the default table renderer, whose manual
// MachineKey/NodeKey UnmarshalText and IP ParseAddr have no other coverage.
func TestNodesToPtables(t *testing.T) {
data, err := nodesToPtables([]apiv1.Node{sampleNode(t)})
if err != nil {
t.Fatalf("nodesToPtables: %v", err)
}
if len(data) != 2 {
t.Fatalf("want header + 1 row, got %d rows", len(data))
}
row := strings.Join(data[1], "|")
for _, want := range []string{"host7", "alice", "100.64.0.7", "fd7a:115c:a1e0::7"} {
if !strings.Contains(row, want) {
t.Errorf("row missing %q: %s", want, row)
}
}
}
// TestNodesToPtablesEmptyNodeKey documents that a missing NodeKey makes the
// table render fail loudly rather than silently — so a decode regression that
// drops NodeKey over HTTP is caught at the CLI, not just in -o json.
func TestNodesToPtablesEmptyNodeKey(t *testing.T) {
node := sampleNode(t)
node.NodeKey = apiv1.OptString{}
_, err := nodesToPtables([]apiv1.Node{node})
if err == nil {
t.Error("expected error for empty NodeKey, got nil")
}
}
// TestNodeRoutesToPtables renders the list-routes table, which has zero
// integration coverage.
func TestNodeRoutesToPtables(t *testing.T) {
node := sampleNode(t)
node.SubnetRoutes = []string{"10.0.0.0/24"}
node.ApprovedRoutes = []string{"10.0.0.0/24"}
data := nodeRoutesToPtables([]apiv1.Node{node})
if len(data) != 2 {
t.Fatalf("want header + 1 row, got %d rows", len(data))
}
if !strings.Contains(strings.Join(data[1], "|"), "10.0.0.0/24") {
t.Errorf("route row missing approved route: %v", data[1])
}
}
-124
View File
@@ -1,124 +0,0 @@
package cli
import (
"errors"
"testing"
"time"
"github.com/spf13/cobra"
)
func TestUsernameAndIDFromFlag(t *testing.T) {
newCmd := func() *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().StringP("name", "n", "", "")
cmd.Flags().Int64P("identifier", "i", -1, "")
return cmd
}
t.Run("neither flag is required", func(t *testing.T) {
_, _, err := usernameAndIDFromFlag(newCmd())
if !errors.Is(err, errFlagRequired) {
t.Fatalf("want errFlagRequired, got %v", err)
}
})
t.Run("name only", func(t *testing.T) {
cmd := newCmd()
_ = cmd.Flags().Set("name", "alice")
id, name, err := usernameAndIDFromFlag(cmd)
if err != nil || id != 0 || name != "alice" {
t.Fatalf("got id=%d name=%q err=%v", id, name, err)
}
})
t.Run("id only", func(t *testing.T) {
cmd := newCmd()
_ = cmd.Flags().Set("identifier", "7")
id, name, err := usernameAndIDFromFlag(cmd)
if err != nil || id != 7 || name != "" {
t.Fatalf("got id=%d name=%q err=%v", id, name, err)
}
})
}
func TestApiKeyIDOrPrefix(t *testing.T) {
newCmd := func() *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().Uint64P("id", "i", 0, "")
cmd.Flags().StringP("prefix", "p", "", "")
return cmd
}
t.Run("neither is an error", func(t *testing.T) {
_, _, err := apiKeyIDOrPrefix(newCmd())
if !errors.Is(err, errMissingParameter) {
t.Fatalf("want errMissingParameter, got %v", err)
}
})
t.Run("both is an error", func(t *testing.T) {
cmd := newCmd()
_ = cmd.Flags().Set("id", "1")
_ = cmd.Flags().Set("prefix", "abc")
_, _, err := apiKeyIDOrPrefix(cmd)
if !errors.Is(err, errMissingParameter) {
t.Fatalf("want errMissingParameter, got %v", err)
}
})
t.Run("id only", func(t *testing.T) {
cmd := newCmd()
_ = cmd.Flags().Set("id", "5")
id, prefix, err := apiKeyIDOrPrefix(cmd)
if err != nil || id != 5 || prefix != "" {
t.Fatalf("got id=%d prefix=%q err=%v", id, prefix, err)
}
})
}
func TestExpirationFromFlag(t *testing.T) {
newCmd := func() *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().StringP("expiration", "e", "", "")
return cmd
}
t.Run("empty is unset", func(t *testing.T) {
exp, err := expirationFromFlag(newCmd())
if err != nil || exp.Set {
t.Fatalf("want unset, got set=%v err=%v", exp.Set, err)
}
})
t.Run("invalid duration is an error", func(t *testing.T) {
cmd := newCmd()
_ = cmd.Flags().Set("expiration", "not-a-duration")
_, err := expirationFromFlag(cmd)
if err == nil {
t.Fatal("want error for invalid duration")
}
})
t.Run("valid duration is in the future", func(t *testing.T) {
cmd := newCmd()
_ = cmd.Flags().Set("expiration", "1h")
exp, err := expirationFromFlag(cmd)
if err != nil || !exp.Set {
t.Fatalf("want set, got set=%v err=%v", exp.Set, err)
}
if !exp.Value.After(time.Now()) {
t.Errorf("expiration not in the future: %v", exp.Value)
}
})
}
+15 -1
View File
@@ -11,7 +11,21 @@ import (
)
func main() {
colors := termcolor.SupportLevel(os.Stderr) != termcolor.LevelNone
var colors bool
switch l := termcolor.SupportLevel(os.Stderr); l {
case termcolor.Level16M:
colors = true
case termcolor.Level256:
colors = true
case termcolor.LevelBasic:
colors = true
case termcolor.LevelNone:
colors = false
default:
// no color, return text as is.
colors = false
}
// Adhere to no-color.org manifesto of allowing users to
// turn off color in cli/services
+41 -30
View File
@@ -71,8 +71,26 @@ func killTestContainers(ctx context.Context) error {
removed := 0
for _, cont := range containers {
if isTestContainerName(cont.Names) {
if killAndRemove(ctx, cli, cont) {
shouldRemove := false
for _, name := range cont.Names {
if strings.Contains(name, "headscale-test-suite") ||
strings.Contains(name, "hs-") ||
strings.Contains(name, "ts-") ||
strings.Contains(name, "derp-") {
shouldRemove = true
break
}
}
if shouldRemove {
// First kill the container if it's running
if cont.State == "running" {
_ = cli.ContainerKill(ctx, cont.ID, "KILL")
}
// Then remove the container with retry logic
if removeContainerWithRetry(ctx, cli, cont.ID) {
removed++
}
}
@@ -111,7 +129,13 @@ func killTestContainersByRunID(ctx context.Context, runID string) error {
removed := 0
for _, cont := range containers {
if killAndRemove(ctx, cli, cont) {
// Kill the container if it's running
if cont.State == "running" {
_ = cli.ContainerKill(ctx, cont.ID, "KILL")
}
// Remove the container with retry logic
if removeContainerWithRetry(ctx, cli, cont.ID) {
removed++
}
}
@@ -149,8 +173,20 @@ func cleanupStaleTestContainers(ctx context.Context) error {
for _, cont := range containers {
// Only remove containers that look like test containers
if isTestContainerName(cont.Names) {
if killAndRemove(ctx, cli, cont) {
shouldRemove := false
for _, name := range cont.Names {
if strings.Contains(name, "headscale-test-suite") ||
strings.Contains(name, "hs-") ||
strings.Contains(name, "ts-") ||
strings.Contains(name, "derp-") {
shouldRemove = true
break
}
}
if shouldRemove {
if removeContainerWithRetry(ctx, cli, cont.ID) {
removed++
}
}
@@ -187,31 +223,6 @@ func removeContainerWithRetry(ctx context.Context, cli *client.Client, container
return err == nil
}
// isTestContainerName reports whether any of the container names belong to an
// integration test container.
func isTestContainerName(names []string) bool {
for _, name := range names {
if strings.Contains(name, "headscale-test-suite") ||
strings.Contains(name, "hs-") ||
strings.Contains(name, "ts-") ||
strings.Contains(name, "derp-") {
return true
}
}
return false
}
// killAndRemove kills a running container then removes it with retry logic,
// reporting whether the removal succeeded.
func killAndRemove(ctx context.Context, cli *client.Client, cont container.Summary) bool {
if cont.State == "running" {
_ = cli.ContainerKill(ctx, cont.ID, "KILL")
}
return removeContainerWithRetry(ctx, cli, cont.ID)
}
// pruneDockerNetworks removes unused Docker networks.
func pruneDockerNetworks(ctx context.Context) error {
cli, err := createDockerClient(ctx)
+36 -47
View File
@@ -14,7 +14,6 @@ import (
"strings"
"time"
"github.com/cenkalti/backoff/v5"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
@@ -523,9 +522,9 @@ func checkImageAvailableLocally(ctx context.Context, cli *client.Client, imageNa
return true, nil
}
// ensureImageAvailable pulls imageName if missing, using Docker Hub
// credentials and retrying transient errors.
// ensureImageAvailable checks if the image is available locally first, then pulls if needed.
func ensureImageAvailable(ctx context.Context, cli *client.Client, imageName string, verbose bool) error {
// First check if image is available locally
available, err := checkImageAvailableLocally(ctx, cli, imageName)
if err != nil {
return fmt.Errorf("checking local image availability: %w", err)
@@ -539,64 +538,34 @@ func ensureImageAvailable(ctx context.Context, cli *client.Client, imageName str
return nil
}
// Image not available locally, try to pull it
if verbose {
log.Printf("Image %s not found locally, pulling...", imageName)
}
registryAuth, err := dockertestutil.RegistryAuth()
reader, err := cli.ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {
return fmt.Errorf("resolving registry auth: %w", err)
return fmt.Errorf("pulling image %s: %w", imageName, err)
}
defer reader.Close()
_, err = backoff.Retry(
ctx,
func() (struct{}, error) {
reader, pullErr := cli.ImagePull(ctx, imageName, image.PullOptions{RegistryAuth: registryAuth})
if pullErr != nil {
if isPermanentDockerPullError(pullErr) {
return struct{}{}, backoff.Permanent(pullErr)
}
if verbose {
_, err = io.Copy(os.Stdout, reader)
if err != nil {
return fmt.Errorf("reading pull output: %w", err)
}
} else {
_, err = io.Copy(io.Discard, reader)
if err != nil {
return fmt.Errorf("reading pull output: %w", err)
}
return struct{}{}, fmt.Errorf("pulling image %s: %w", imageName, pullErr)
}
defer reader.Close()
sink := io.Discard
if verbose {
sink = os.Stdout
}
_, copyErr := io.Copy(sink, reader)
if copyErr != nil {
return struct{}{}, fmt.Errorf("reading pull output: %w", copyErr)
}
return struct{}{}, nil
},
backoff.WithBackOff(backoff.NewExponentialBackOff()),
backoff.WithMaxElapsedTime(60*time.Second),
)
if err != nil {
return err
}
if !verbose {
log.Printf("Image %s pulled successfully", imageName)
}
return nil
}
func isPermanentDockerPullError(err error) bool {
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "manifest unknown") ||
strings.Contains(msg, "manifest not found") ||
strings.Contains(msg, "repository does not exist") ||
strings.Contains(msg, "name unknown") ||
strings.Contains(msg, "no such image")
}
// listControlFiles displays the headscale test artifacts created in the control logs directory.
func listControlFiles(logsDir string) {
entries, err := os.ReadDir(logsDir)
@@ -769,6 +738,17 @@ func extractContainerArtifacts(ctx context.Context, cli *client.Client, containe
return fmt.Errorf("extracting logs: %w", err)
}
// Extract tar files for headscale containers only
if strings.HasPrefix(containerName, "hs-") {
err := extractContainerFiles(ctx, cli, containerID, containerName, logsDir, verbose)
if err != nil {
if verbose {
log.Printf("Warning: failed to extract files from %s: %v", containerName, err)
}
// Don't fail the whole extraction if files are missing
}
}
return nil
}
@@ -816,3 +796,12 @@ func extractContainerLogs(ctx context.Context, cli *client.Client, containerID,
return nil
}
// extractContainerFiles extracts database file and directories from headscale containers.
// Note: The actual file extraction is now handled by the integration tests themselves
// via SaveProfile, SaveMapResponses, and SaveDatabase functions in hsic.go.
func extractContainerFiles(ctx context.Context, cli *client.Client, containerID, containerName, logsDir string, verbose bool) error {
// Files are now extracted directly by the integration tests
// This function is kept for potential future use or other file types
return nil
}
+175 -140
View File
@@ -5,23 +5,8 @@ import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/juanfont/headscale/integration/dockertestutil"
)
const (
statusPass = "PASS"
statusFail = "FAIL"
statusWarn = "WARN"
nameDockerDaemon = "Docker Daemon"
nameDockerContext = "Docker Context"
nameDockerSocket = "Docker Socket"
nameGolangImage = "Golang Image"
nameGoInstall = "Go Installation"
)
var ErrSystemChecksFailed = errors.New("system checks failed")
@@ -34,21 +19,6 @@ type DoctorResult struct {
Suggestions []string
}
// pass builds a passing DoctorResult.
func pass(name, message string) DoctorResult {
return DoctorResult{Name: name, Status: statusPass, Message: message}
}
// warn builds a warning DoctorResult with optional suggestions.
func warn(name, message string, suggestions ...string) DoctorResult {
return DoctorResult{Name: name, Status: statusWarn, Message: message, Suggestions: suggestions}
}
// fail builds a failing DoctorResult with optional suggestions.
func fail(name, message string, suggestions ...string) DoctorResult {
return DoctorResult{Name: name, Status: statusFail, Message: message, Suggestions: suggestions}
}
// runDoctorCheck performs comprehensive pre-flight checks for integration testing.
func runDoctorCheck(ctx context.Context) error {
results := []DoctorResult{}
@@ -61,10 +31,9 @@ func runDoctorCheck(ctx context.Context) error {
results = append(results, dockerResult)
// If Docker is available, run additional checks
if dockerResult.Status == statusPass {
if dockerResult.Status == "PASS" {
results = append(results, checkDockerContext(ctx))
results = append(results, checkDockerSocket(ctx))
results = append(results, checkDockerHubCredentials())
results = append(results, checkGolangImage(ctx))
}
@@ -82,7 +51,7 @@ func runDoctorCheck(ctx context.Context) error {
// Return error if any critical checks failed
for _, result := range results {
if result.Status == statusFail {
if result.Status == "FAIL" {
return fmt.Errorf("%w - see details above", ErrSystemChecksFailed)
}
}
@@ -96,115 +65,140 @@ func runDoctorCheck(ctx context.Context) error {
func checkDockerBinary() DoctorResult {
_, err := exec.LookPath("docker")
if err != nil {
return fail(
"Docker Binary",
"Docker binary not found in PATH",
"Install Docker: https://docs.docker.com/get-docker/",
"For macOS: consider using colima or Docker Desktop",
"Ensure docker is in your PATH",
)
return DoctorResult{
Name: "Docker Binary",
Status: "FAIL",
Message: "Docker binary not found in PATH",
Suggestions: []string{
"Install Docker: https://docs.docker.com/get-docker/",
"For macOS: consider using colima or Docker Desktop",
"Ensure docker is in your PATH",
},
}
}
return pass("Docker Binary", "Docker binary found")
return DoctorResult{
Name: "Docker Binary",
Status: "PASS",
Message: "Docker binary found",
}
}
// checkDockerDaemon verifies Docker daemon is running and accessible.
func checkDockerDaemon(ctx context.Context) DoctorResult {
cli, err := createDockerClient(ctx)
if err != nil {
return fail(
nameDockerDaemon,
fmt.Sprintf("Cannot create Docker client: %v", err),
"Start Docker daemon/service",
"Check Docker Desktop is running (if using Docker Desktop)",
"For colima: run 'colima start'",
"Verify DOCKER_HOST environment variable if set",
)
return DoctorResult{
Name: "Docker Daemon",
Status: "FAIL",
Message: fmt.Sprintf("Cannot create Docker client: %v", err),
Suggestions: []string{
"Start Docker daemon/service",
"Check Docker Desktop is running (if using Docker Desktop)",
"For colima: run 'colima start'",
"Verify DOCKER_HOST environment variable if set",
},
}
}
defer cli.Close()
_, err = cli.Ping(ctx)
if err != nil {
return fail(
nameDockerDaemon,
fmt.Sprintf("Cannot ping Docker daemon: %v", err),
"Ensure Docker daemon is running",
"Check Docker socket permissions",
"Try: docker info",
)
return DoctorResult{
Name: "Docker Daemon",
Status: "FAIL",
Message: fmt.Sprintf("Cannot ping Docker daemon: %v", err),
Suggestions: []string{
"Ensure Docker daemon is running",
"Check Docker socket permissions",
"Try: docker info",
},
}
}
return pass(nameDockerDaemon, "Docker daemon is running and accessible")
return DoctorResult{
Name: "Docker Daemon",
Status: "PASS",
Message: "Docker daemon is running and accessible",
}
}
// checkDockerContext verifies Docker context configuration.
func checkDockerContext(ctx context.Context) DoctorResult {
contextInfo, err := getCurrentDockerContext(ctx)
if err != nil {
return warn(
nameDockerContext,
"Could not detect Docker context, using default settings",
"Check: docker context ls",
"Consider setting up a specific context if needed",
)
return DoctorResult{
Name: "Docker Context",
Status: "WARN",
Message: "Could not detect Docker context, using default settings",
Suggestions: []string{
"Check: docker context ls",
"Consider setting up a specific context if needed",
},
}
}
if contextInfo == nil {
return pass(nameDockerContext, "Using default Docker context")
return DoctorResult{
Name: "Docker Context",
Status: "PASS",
Message: "Using default Docker context",
}
}
return pass(nameDockerContext, "Using Docker context: "+contextInfo.Name)
return DoctorResult{
Name: "Docker Context",
Status: "PASS",
Message: "Using Docker context: " + contextInfo.Name,
}
}
// checkDockerSocket verifies Docker socket accessibility.
func checkDockerSocket(ctx context.Context) DoctorResult {
cli, err := createDockerClient(ctx)
if err != nil {
return fail(
nameDockerSocket,
fmt.Sprintf("Cannot access Docker socket: %v", err),
"Check Docker socket permissions",
"Add user to docker group: sudo usermod -aG docker $USER",
"For colima: ensure socket is accessible",
)
return DoctorResult{
Name: "Docker Socket",
Status: "FAIL",
Message: fmt.Sprintf("Cannot access Docker socket: %v", err),
Suggestions: []string{
"Check Docker socket permissions",
"Add user to docker group: sudo usermod -aG docker $USER",
"For colima: ensure socket is accessible",
},
}
}
defer cli.Close()
info, err := cli.Info(ctx)
if err != nil {
return fail(
nameDockerSocket,
fmt.Sprintf("Cannot get Docker info: %v", err),
"Check Docker daemon status",
"Verify socket permissions",
)
return DoctorResult{
Name: "Docker Socket",
Status: "FAIL",
Message: fmt.Sprintf("Cannot get Docker info: %v", err),
Suggestions: []string{
"Check Docker daemon status",
"Verify socket permissions",
},
}
}
return pass(nameDockerSocket, fmt.Sprintf("Docker socket accessible (Server: %s)", info.ServerVersion))
}
// checkDockerHubCredentials warns when pulls would be anonymous and
// therefore rate-limited.
func checkDockerHubCredentials() DoctorResult {
_, _, source := dockertestutil.Credentials()
if source == dockertestutil.CredentialSourceAnonymous {
return warn(
"Docker Hub Credentials",
"No Docker Hub credentials found — pulls will be rate-limited (100/6h per IP)",
"Run: docker login",
"Or export DOCKERHUB_USERNAME and DOCKERHUB_TOKEN",
"In CI: ensure the docker/login-action step is configured with secrets",
)
return DoctorResult{
Name: "Docker Socket",
Status: "PASS",
Message: fmt.Sprintf("Docker socket accessible (Server: %s)", info.ServerVersion),
}
return pass("Docker Hub Credentials", fmt.Sprintf("Credentials available (source: %s)", source))
}
// checkGolangImage verifies the golang Docker image is available locally or can be pulled.
func checkGolangImage(ctx context.Context) DoctorResult {
cli, err := createDockerClient(ctx)
if err != nil {
return fail(nameGolangImage, "Cannot create Docker client for image check")
return DoctorResult{
Name: "Golang Image",
Status: "FAIL",
Message: "Cannot create Docker client for image check",
}
}
defer cli.Close()
@@ -214,56 +208,81 @@ func checkGolangImage(ctx context.Context) DoctorResult {
// First check if image is available locally
available, err := checkImageAvailableLocally(ctx, cli, imageName)
if err != nil {
return fail(
nameGolangImage,
fmt.Sprintf("Cannot check golang image %s: %v", imageName, err),
"Check Docker daemon status",
"Try: docker images | grep golang",
)
return DoctorResult{
Name: "Golang Image",
Status: "FAIL",
Message: fmt.Sprintf("Cannot check golang image %s: %v", imageName, err),
Suggestions: []string{
"Check Docker daemon status",
"Try: docker images | grep golang",
},
}
}
if available {
return pass(nameGolangImage, fmt.Sprintf("Golang image %s is available locally", imageName))
return DoctorResult{
Name: "Golang Image",
Status: "PASS",
Message: fmt.Sprintf("Golang image %s is available locally", imageName),
}
}
// Image not available locally, try to pull it
err = ensureImageAvailable(ctx, cli, imageName, false)
if err != nil {
return fail(
nameGolangImage,
fmt.Sprintf("Golang image %s not available locally and cannot pull: %v", imageName, err),
"Check internet connectivity",
"Verify Docker Hub access",
"Try: docker pull "+imageName,
"Or run tests offline if image was pulled previously",
)
return DoctorResult{
Name: "Golang Image",
Status: "FAIL",
Message: fmt.Sprintf("Golang image %s not available locally and cannot pull: %v", imageName, err),
Suggestions: []string{
"Check internet connectivity",
"Verify Docker Hub access",
"Try: docker pull " + imageName,
"Or run tests offline if image was pulled previously",
},
}
}
return pass(nameGolangImage, fmt.Sprintf("Golang image %s is now available", imageName))
return DoctorResult{
Name: "Golang Image",
Status: "PASS",
Message: fmt.Sprintf("Golang image %s is now available", imageName),
}
}
// checkGoInstallation verifies Go is installed and working.
func checkGoInstallation(ctx context.Context) DoctorResult {
_, err := exec.LookPath("go")
if err != nil {
return fail(
nameGoInstall,
"Go binary not found in PATH",
"Install Go: https://golang.org/dl/",
"Ensure go is in your PATH",
)
return DoctorResult{
Name: "Go Installation",
Status: "FAIL",
Message: "Go binary not found in PATH",
Suggestions: []string{
"Install Go: https://golang.org/dl/",
"Ensure go is in your PATH",
},
}
}
cmd := exec.CommandContext(ctx, "go", "version")
output, err := cmd.Output()
if err != nil {
return fail(nameGoInstall, fmt.Sprintf("Cannot get Go version: %v", err))
return DoctorResult{
Name: "Go Installation",
Status: "FAIL",
Message: fmt.Sprintf("Cannot get Go version: %v", err),
}
}
version := strings.TrimSpace(string(output))
return pass(nameGoInstall, version)
return DoctorResult{
Name: "Go Installation",
Status: "PASS",
Message: version,
}
}
// checkGitRepository verifies we're in a git repository.
@@ -272,19 +291,26 @@ func checkGitRepository(ctx context.Context) DoctorResult {
err := cmd.Run()
if err != nil {
return fail(
"Git Repository",
"Not in a Git repository",
"Run from within the headscale git repository",
"Clone the repository: git clone https://github.com/juanfont/headscale.git",
)
return DoctorResult{
Name: "Git Repository",
Status: "FAIL",
Message: "Not in a Git repository",
Suggestions: []string{
"Run from within the headscale git repository",
"Clone the repository: git clone https://github.com/juanfont/headscale.git",
},
}
}
return pass("Git Repository", "Running in Git repository")
return DoctorResult{
Name: "Git Repository",
Status: "PASS",
Message: "Running in Git repository",
}
}
// checkRequiredFiles verifies required files exist.
func checkRequiredFiles(_ context.Context) DoctorResult {
func checkRequiredFiles(ctx context.Context) DoctorResult {
requiredFiles := []string{
"go.mod",
"integration/",
@@ -294,23 +320,32 @@ func checkRequiredFiles(_ context.Context) DoctorResult {
var missingFiles []string
for _, file := range requiredFiles {
_, err := os.Stat(file)
cmd := exec.CommandContext(ctx, "test", "-e", file)
err := cmd.Run()
if err != nil {
missingFiles = append(missingFiles, file)
}
}
if len(missingFiles) > 0 {
return fail(
"Required Files",
"Missing required files: "+strings.Join(missingFiles, ", "),
"Ensure you're in the headscale project root directory",
"Check that integration/ directory exists",
"Verify this is a complete headscale repository",
)
return DoctorResult{
Name: "Required Files",
Status: "FAIL",
Message: "Missing required files: " + strings.Join(missingFiles, ", "),
Suggestions: []string{
"Ensure you're in the headscale project root directory",
"Check that integration/ directory exists",
"Verify this is a complete headscale repository",
},
}
}
return pass("Required Files", "All required files found")
return DoctorResult{
Name: "Required Files",
Status: "PASS",
Message: "All required files found",
}
}
// displayDoctorResults shows the results in a formatted way.
@@ -322,11 +357,11 @@ func displayDoctorResults(results []DoctorResult) {
var icon string
switch result.Status {
case statusPass:
case "PASS":
icon = "✅"
case statusWarn:
case "WARN":
icon = "⚠️"
case statusFail:
case "FAIL":
icon = "❌"
default:
icon = "❓"
-89
View File
@@ -1,89 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/creachadair/command"
"github.com/juanfont/headscale/hscontrol/capver"
)
var (
errUnknownSet = errors.New("unknown --set value (want must|all)")
errUnknownFormat = errors.New("unknown --format value (want space|newline|json)")
)
// ListVersionsConfig holds flags for the list-versions subcommand.
type ListVersionsConfig struct {
Set string `flag:"set,default=must,Version set: must|all"`
Exclude string `flag:"exclude,Comma-separated versions to exclude (e.g. head,unstable)"`
Format string `flag:"format,default=space,Output format: space|newline|json"`
}
var listVersionsConfig ListVersionsConfig
// listVersions prints the Tailscale versions used by integration tests
// in a format CI can shell out to. Mirrors integration/scenario.go
// AllVersions and MustTestVersions: "head" and "unstable" are bare
// tags, releases get a "v" prefix so each entry can be appended to
// "ghcr.io/tailscale/tailscale:" directly.
func listVersions(env *command.Env) error {
release := capver.TailscaleLatestMajorMinor(capver.SupportedMajorMinorVersions, true)
all := append([]string{"head", "unstable"}, release...)
must := append(append([]string{}, all[0:4]...), all[len(all)-2:]...)
var versions []string
switch listVersionsConfig.Set {
case "must":
versions = must
case "all":
versions = all
default:
return fmt.Errorf("%w: %q", errUnknownSet, listVersionsConfig.Set)
}
excluded := make(map[string]bool)
if listVersionsConfig.Exclude != "" {
for v := range strings.SplitSeq(listVersionsConfig.Exclude, ",") {
excluded[strings.TrimSpace(v)] = true
}
}
out := make([]string, 0, len(versions))
for _, v := range versions {
if excluded[v] {
continue
}
if v != "head" && v != "unstable" {
v = "v" + v
}
out = append(out, v)
}
switch listVersionsConfig.Format {
case "space":
fmt.Println(strings.Join(out, " "))
case "newline":
for _, v := range out {
fmt.Println(v)
}
case "json":
b, err := json.Marshal(out)
if err != nil {
return err
}
fmt.Println(string(b))
default:
return fmt.Errorf("%w: %q", errUnknownFormat, listVersionsConfig.Format)
}
return nil
}
+14 -18
View File
@@ -29,13 +29,6 @@ func main() {
return runDoctorCheck(env.Context())
},
},
{
Name: "list-versions",
Help: "Print Tailscale versions used by integration tests",
Usage: "list-versions [flags]",
SetFlags: command.Flags(flax.MustBind, &listVersionsConfig),
Run: listVersions,
},
{
Name: "clean",
Help: "Clean Docker resources",
@@ -86,17 +79,20 @@ func main() {
}
func cleanAll(ctx context.Context) error {
for _, step := range []func(context.Context) error{
killTestContainers,
pruneDockerNetworks,
cleanOldImages,
cleanCacheVolume,
} {
err := step(ctx)
if err != nil {
return err
}
err := killTestContainers(ctx)
if err != nil {
return err
}
return nil
err = pruneDockerNetworks(ctx)
if err != nil {
return err
}
err = cleanOldImages(ctx)
if err != nil {
return err
}
return cleanCacheVolume(ctx)
}
+53 -11
View File
@@ -6,7 +6,6 @@ import (
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/creachadair/command"
@@ -67,21 +66,64 @@ func runIntegrationTest(env *command.Env) error {
// detectGoVersion reads the Go version from go.mod file.
func detectGoVersion() string {
content, err := os.ReadFile("go.mod")
if err != nil {
content, err = os.ReadFile(filepath.Join("..", "..", "go.mod"))
if err != nil {
return "1.26.1"
}
goModPath := filepath.Join("..", "..", "go.mod")
if _, err := os.Stat("go.mod"); err == nil { //nolint:noinlineerr
goModPath = "go.mod"
} else if _, err := os.Stat("../../go.mod"); err == nil { //nolint:noinlineerr
goModPath = "../../go.mod"
}
for line := range strings.Lines(string(content)) {
if rest, ok := strings.CutPrefix(line, "go "); ok {
if f := strings.Fields(rest); len(f) > 0 {
return f[0]
content, err := os.ReadFile(goModPath)
if err != nil {
return "1.26.1"
}
lines := splitLines(string(content))
for _, line := range lines {
if len(line) > 3 && line[:3] == "go " {
version := line[3:]
if idx := indexOf(version, " "); idx != -1 {
version = version[:idx]
}
return version
}
}
return "1.26.1"
}
// splitLines splits a string into lines without using strings.Split.
func splitLines(s string) []string {
var (
lines []string
current string
)
for _, char := range s {
if char == '\n' {
lines = append(lines, current)
current = ""
} else {
current += string(char)
}
}
if current != "" {
lines = append(lines, current)
}
return lines
}
// indexOf finds the first occurrence of substr in s.
func indexOf(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
+22 -17
View File
@@ -1,13 +1,12 @@
package main
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"log"
"slices"
"sort"
"strings"
"sync"
"time"
@@ -161,7 +160,7 @@ func (sc *StatsCollector) monitorDockerEvents(ctx context.Context, runID string,
continue
}
// Convert to [types.Container] format for consistency
// Convert to types.Container format for consistency
cont := types.Container{ //nolint:staticcheck // SA1019: use container.Summary
ID: containerInfo.ID,
Names: []string{containerInfo.Name},
@@ -257,7 +256,7 @@ func (sc *StatsCollector) collectStatsForContainer(ctx context.Context, containe
err := decoder.Decode(&stats)
if err != nil {
// [io.EOF] is expected when container stops or stream ends
// EOF is expected when container stops or stream ends
if err.Error() != "EOF" && verbose {
log.Printf("Failed to decode stats for container %s: %v", containerID[:12], err)
}
@@ -313,7 +312,7 @@ func calculateCPUPercent(prevStats, stats *container.Stats) float64 { //nolint:s
// Calculate CPU percentage: (container CPU delta / system CPU delta) * number of CPUs * 100
numCPUs := float64(len(stats.CPUStats.CPUUsage.PercpuUsage))
if numCPUs == 0 {
// Fallback: if [PercpuUsage] is not available, assume 1 CPU
// Fallback: if PercpuUsage is not available, assume 1 CPU
numCPUs = 1.0
}
@@ -375,24 +374,24 @@ func (sc *StatsCollector) GetSummary() []ContainerStatsSummary {
SampleCount: len(stats),
}
extract := func(get func(StatsSample) float64) []float64 {
values := make([]float64, len(stats))
for i, sample := range stats {
values[i] = get(sample)
}
// Calculate CPU stats
cpuValues := make([]float64, len(stats))
memoryValues := make([]float64, len(stats))
return values
for i, sample := range stats {
cpuValues[i] = sample.CPUUsage
memoryValues[i] = sample.MemoryMB
}
summary.CPU = calculateStatsSummary(extract(func(s StatsSample) float64 { return s.CPUUsage }))
summary.Memory = calculateStatsSummary(extract(func(s StatsSample) float64 { return s.MemoryMB }))
summary.CPU = calculateStatsSummary(cpuValues)
summary.Memory = calculateStatsSummary(memoryValues)
summaries = append(summaries, summary)
}
// Sort by container name for consistent output
slices.SortFunc(summaries, func(a, b ContainerStatsSummary) int {
return cmp.Compare(a.ContainerName, b.ContainerName)
sort.Slice(summaries, func(i, j int) bool {
return summaries[i].ContainerName < summaries[j].ContainerName
})
return summaries
@@ -409,8 +408,14 @@ func calculateStatsSummary(values []float64) StatsSummary {
sum := 0.0
for _, value := range values {
minVal = min(minVal, value)
maxVal = max(maxVal, value)
if value < minVal {
minVal = value
}
if value > maxVal {
maxVal = value
}
sum += value
}
+3 -3
View File
@@ -12,7 +12,7 @@
// vendorhash check exit non-zero if flakehashes.json is stale
// vendorhash update recompute and rewrite flakehashes.json
//
// The JSON schema and [goModFingerprint] algorithm mirror upstream
// The JSON schema and goModFingerprint algorithm mirror upstream
// tailscale's tool/updateflakes so a future shared library extraction
// is straightforward.
package main
@@ -82,8 +82,8 @@ func usage() {
fmt.Fprintln(os.Stderr, "usage: vendorhash <check|update>")
}
// errStale signals to [main] that the check found a mismatch; it has
// already printed a remediation message, so [main] should exit 1
// errStale signals to main that the check found a mismatch; it has
// already printed a remediation message, so main should exit 1
// silently.
var errStale = errors.New("vendor hash stale")
+23 -13
View File
@@ -23,12 +23,21 @@ listen_addr: 127.0.0.1:8080
# Use an empty value to disable the metrics listener.
metrics_listen_addr: 127.0.0.1:9090
# CIDR(s) of reverse proxies (e.g. 127.0.0.1/32) whose
# True-Client-IP, X-Real-IP and X-Forwarded-For headers should
# be honoured. Empty (default) ignores those headers; setting
# this without a proxy in front lets clients spoof their logged
# source IP.
trusted_proxies: []
# Address to listen for gRPC.
# gRPC is used for controlling a headscale server
# remotely with the CLI
# Note: Remote access _only_ works if you have
# valid certificates.
#
# For production:
# grpc_listen_addr: 0.0.0.0:50443
grpc_listen_addr: 127.0.0.1:50443
# Allow the gRPC admin interface to run in INSECURE
# mode. This is not recommended as the traffic will
# be unencrypted. Only enable if you know what you
# are doing.
grpc_allow_insecure: false
# The Noise section includes specific configuration for the
# TS2021 Noise protocol
@@ -71,7 +80,8 @@ prefixes:
# connection cannot be established.
# https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
#
# Headscale needs a list of DERP servers that can be presented to the clients.
# headscale needs a list of DERP servers that can be presented
# to the clients.
derp:
server:
# If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
@@ -116,9 +126,9 @@ derp:
# Locally available DERP map files encoded in YAML
#
# This option is mostly interesting for people hosting their own DERP servers:
# This option is mostly interesting for people hosting
# their own DERP servers:
# https://tailscale.com/docs/reference/derp-servers/custom-derp-servers
# https://headscale.net/stable/ref/derp/
#
# paths:
# - /etc/headscale/derp-example.yaml
@@ -252,7 +262,7 @@ tls_letsencrypt_cache_dir: /var/lib/headscale/cache
# Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01
# See: https://headscale.net/stable/ref/tls/
# See: docs/ref/tls.md for more information
tls_letsencrypt_challenge_type: HTTP-01
# When HTTP-01 challenge is chosen, letsencrypt must set up a
# verification endpoint, and it will be listening on:
@@ -287,6 +297,7 @@ policy:
# headscale supports Tailscale's DNS configuration and MagicDNS.
# Please have a look to their docs to better understand the concepts:
#
# - https://tailscale.com/docs/features/access-control/acls
# - https://tailscale.com/docs/features/magicdns
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns
#
@@ -303,7 +314,7 @@ policy:
# If you want stop Headscale from managing the DNS configuration
# all the fields under `dns` should be set to empty values.
dns:
# Whether to use MagicDNS
# Whether to use [MagicDNS](https://tailscale.com/docs/features/magicdns).
magic_dns: true
# Defines the base domain to create the hostnames for MagicDNS.
@@ -344,7 +355,7 @@ dns:
# Extra DNS records
# so far only A and AAAA records are supported (on the tailscale side)
# See: https://headscale.net/stable/ref/dns/
# See: docs/ref/dns.md
extra_records: []
# - name: "grafana.myvpn.example.com"
# type: "A"
@@ -363,7 +374,6 @@ unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
# OpenID Connect
# https://headscale.net/stable/ref/oidc/
# oidc:
# # Block startup until the identity provider is available and healthy.
# only_start_if_oidc_is_available: true
+3 -3
View File
@@ -152,15 +152,15 @@ See also <https://tailscale.com/docs/concepts/device-visibility>.
Headscale checks if the policy is valid during startup and refuses to start if it detects an error. The error message
indicates which part of the policy is invalid. Follow these steps to fix your policy:
- Dump the policy to a file: `headscale policy get --bypass-server-and-access-database-directly > policy.json`
- Dump the policy to a file: `headscale policy get --bypass-grpc-and-access-database-directly > policy.json`
- Edit and fixup `policy.json`. Use the command `headscale policy check --file policy.json` to validate the policy.
- Load the modified policy: `headscale policy set --bypass-server-and-access-database-directly --file policy.json`
- Load the modified policy: `headscale policy set --bypass-grpc-and-access-database-directly --file policy.json`
- Start Headscale as usual.
!!! warning "Full server configuration required"
The above commands to get/set the policy require a complete server configuration file including database settings. A
minimal config to [control Headscale via remote CLI](../ref/api.md#remote-control) is not sufficient. You may use
minimal config to [control Headscale via remote CLI](../ref/api.md#grpc) is not sufficient. You may use
`headscale -c /path/to/config.yaml` to specify the path to an alternative configuration file.
## How can I migrate back to the recommended IP prefixes?
+1 -6
View File
@@ -13,9 +13,7 @@ provides on overview of Headscale's feature and compatibility with the Tailscale
- [x] [Global and restricted nameservers (split DNS)](https://tailscale.com/docs/reference/dns-in-tailscale#nameservers)
- [x] [search domains](https://tailscale.com/docs/reference/dns-in-tailscale#search-domains)
- [x] [Extra DNS records (Headscale only)](../ref/dns.md#setting-extra-dns-records)
- [x] File sharing
- [x] [Taildrive](https://tailscale.com/docs/features/taildrive)
- [x] [Taildrop](https://tailscale.com/docs/features/taildrop)
- [x] [Taildrop](https://tailscale.com/docs/features/taildrop)
- [x] [Tags](../ref/tags.md)
- [x] [Routes](../ref/routes.md)
- [x] [Subnet routers](../ref/routes.md#subnet-router)
@@ -33,9 +31,6 @@ provides on overview of Headscale's feature and compatibility with the Tailscale
routers](../ref/routes.md#automatically-approve-routes-of-a-subnet-router) and [exit
nodes](../ref/routes.md#automatically-approve-an-exit-node-with-auto-approvers)
- [x] [Tailscale SSH](https://tailscale.com/docs/features/tailscale-ssh)
- [x] [Node attributes](../ref/policy.md#node-attributes)
- [x] [Tests](https://tailscale.com/docs/reference/syntax/policy-file#tests) and
[sshTests](https://tailscale.com/docs/reference/syntax/policy-file#ssh-tests)
- [x] [Node registration using Single-Sign-On (OpenID Connect)](../ref/oidc.md) ([GitHub label "OIDC"](https://github.com/juanfont/headscale/labels/OIDC))
- [x] Basic registration
- [x] Update user profile from identity provider
+16 -13
View File
@@ -1,10 +1,10 @@
# API
Headscale provides a [HTTP REST API](#rest-api) which may be used to integrate a [web
interface](integration/web-ui.md), [remote control Headscale](#remote-control) or provide a base for custom
Headscale provides a [HTTP REST API](#rest-api) and a [gRPC interface](#grpc) which may be used to integrate a [web
interface](integration/web-ui.md), [remote control Headscale](#setup-remote-control) or provide a base for custom
integration and tooling.
The API requires a valid API key before use. To create an API key, log into your Headscale server and generate
Both interfaces require a valid API key before use. To create an API key, log into your Headscale server and generate
one with the default expiration of 90 days:
```shell
@@ -58,14 +58,15 @@ Headscale server at `/swagger` for details.
https://headscale.example.com/api/v1/auth/register
```
## Remote control
## gRPC
The `headscale` binary can control a Headscale instance from a remote machine over the HTTP API.
The gRPC interface can be used to control a Headscale instance from a remote machine with the `headscale` binary.
### Prerequisite
- A workstation to run `headscale` (any supported platform, e.g. Linux).
- The Headscale server reachable over HTTP(S).
- A Headscale server with gRPC enabled.
- Connections to the gRPC port (default: `50443`) are allowed.
- Remote access requires an encrypted connection via TLS.
- An [API key](#api) to authenticate with the Headscale server.
@@ -87,20 +88,19 @@ The `headscale` binary can control a Headscale instance from a remote machine ov
```yaml title="config.yaml"
cli:
address: <HEADSCALE_URL>
address: <HEADSCALE_ADDRESS>:<PORT>
api_key: <API_KEY>
```
=== "Environment variables"
```shell
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_URL>"
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_ADDRESS>:<PORT>"
export HEADSCALE_CLI_API_KEY="<API_KEY>"
```
This instructs the `headscale` binary to connect to a remote instance at `<HEADSCALE_URL>` (e.g.
`https://headscale.example.com`), instead of connecting to the local instance. A bare host without a scheme is
assumed to be `https`.
This instructs the `headscale` binary to connect to a remote instance at `<HEADSCALE_ADDRESS>:<PORT>`, instead of
connecting to the local instance.
1. Test the connection by listing all nodes:
@@ -113,12 +113,15 @@ The `headscale` binary can control a Headscale instance from a remote machine ov
### Behind a proxy
The remote CLI uses the same HTTP API as everything else, so it works through the reverse proxy already in front of
Headscale with no extra setup.
It's possible to run the gRPC remote endpoint behind a reverse proxy, like Nginx, and have it run on the _same_ port as Headscale.
While this is _not a supported_ feature, an example on how this can be set up on
[NixOS is shown here](https://github.com/kradalby/dotfiles/blob/4489cdbb19cddfbfae82cd70448a38fde5a76711/machines/headscale.oracldn/headscale.nix#L61-L91).
### Troubleshooting
- Make sure you have the _same_ Headscale version on your server and workstation.
- Ensure that connections to the gRPC port are allowed.
- Verify that your TLS certificate is valid and trusted.
- If you don't have access to a trusted certificate (e.g. from Let's Encrypt), either:
- Add your self-signed certificate to the trust store of your OS _or_
+95 -223
View File
@@ -1,183 +1,90 @@
# Running Headscale behind a reverse proxy
# Running headscale behind a reverse proxy
!!! warning "Community documentation"
This page is not actively maintained by the Headscale authors and is
written by community members. It is _not_ verified by Headscale developers.
This page is not actively maintained by the headscale authors and is
written by community members. It is _not_ verified by headscale developers.
**It might be outdated and it might miss necessary steps**.
Running Headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want
to reuse the same external IP and port - usually tcp/443 for HTTPS.
Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.
Please see [limitations](#limitations) for known issues and limitations.
### WebSockets
## Configuration
The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients.
The configuration depends on the set of Headscale features you intend to use. Please have a look at the
[requirements](../../setup/requirements.md) and especially the [ports in use](../../setup/requirements.md#ports-in-use)
section to learn what a Tailscale clients expects.
The configuration examples in this documentation are basic and cover only HTTP and HTTPS traffic. Other features such as
STUN for Headscale's [embedded DERP server](../derp.md) are expected to be exposed directly or to be only available on
localhost.
### WebSocket
Tailscale clients are using a custom protocol (Tailscale Control Protocol) to communicate with a control server such as
Headscale. The reverse proxy **must** be configured to support WebSockets in order to communicate with Tailscale clients
and it needs to handle two peculiarities of the Tailscale Control Protocol:
- The POST method is used to upgrade the WebSocket connection.
- The value for the `Upgrade` header is `tailscale-control-protocol`.
### TLS
Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration
values to your Headscale [configuration file](../configuration.md):
```yaml title="config.yaml" hl_lines="1"
server_url: https://<SERVER_NAME>
tls_cert_path: ""
tls_key_path: ""
```
Headscale logs `WRN listening without TLS but ServerURL does not start with http://` during startup. This is expected
and indicates that the reverse proxy is in charge of terminating TLS.
### Trusted proxies
Headscale ignores `True-Client-IP`, `X-Real-IP` and `X-Forwarded-For` headers unless the request's TCP peer matches the
`trusted_proxies` configuration option. Set this to the CIDR(s) your reverse proxy connects from so the real client IP
appears in access logs.
```yaml title="config.yaml"
trusted_proxies:
- 127.0.0.1/32
- ::1/128
```
The reverse proxy is responsible to replace any client-supplied `True-Client-IP`, `X-Real-IP`, `X-Forwarded-For` headers
on inbound requests with sanitized values. Headscale picks the first valid IP address supplied by headers in this order:
- `True-Client-IP`
- `X-Real-IP`
- `X-Forwarded-For`
## Limitations
- A reverse proxy adds another layer of complexity that needs to be able to handle the [Tailscale Control
Protocol](#websocket) properly. Be sure to test your setup without a reverse proxy before raising an issue.
- STUN (used along with the [embedded DERP server](../derp.md)) requires udp/3478 to be served publicly.
## Reverse proxy specific configuration
!!! warning "Third-party software and services"
This section of the documentation is specific for third-party software and services. We recommend users read the
third-party documentation for a secure configuration.
This following Headscale configuration may be used as base for the various reverse proxy examples below. The following
is [assumed](../../setup/requirements.md):
- Service for Tailscale clients is served via HTTPS on port 443.
- The reverse proxy redirects HTTP to HTTPS and is terminating TLS.
- Both Headscale and the reverse proxy are running on the same host.
- [Metrics](../debug.md#metrics-and-debug-endpoint) are not proxied, those are available via localhost.
```yaml title="config.yaml" hl_lines="1"
server_url: https://<SERVER_NAME>
listen_addr: 127.0.0.1:8080
metrics_listen_addr: 127.0.0.1:9090
trusted_proxies:
- 127.0.0.1/32
- ::1/128
tls_cert_path: ""
tls_key_path: ""
```
### Apache
The following basic Apache configuration works with the Headscale configuration [as shown
above](#reverse-proxy-specific-configuration). Substitute placeholders and adjust the configuration as needed:
- `<SERVER_NAME>`: The server name for your instance, e.g. `headscale.example.com`
- `<PATH_TO_TLS_CERT>`: Absolute path to your TLS certificate
- `<PATH_TO_TLS_KEY>`: Absolute path to your TLS private key
```apache title="apache.conf" hl_lines="2 7 11 14-15"
<VirtualHost *:80>
ServerName <SERVER_NAME>
# Tailscale captive portal detection
RedirectMatch 204 ^/generate_204$
RedirectMatch permanent "^/(.*)$" "https://<SERVER_NAME>/$1"
</VirtualHost>
<VirtualHost *:443>
ServerName <SERVER_NAME>
SSLEngine On
SSLCertificateFile <PATH_TO_TLS_CERT>
SSLCertificateKeyFile <PATH_TO_TLS_KEY>
RequestHeader set True-Client-IP "%{REMOTE_ADDR}s"
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8080/ upgrade=any
</VirtualHost>
```
Note that `upgrade=any` is required as a parameter for `ProxyPass` so that WebSocket traffic whose `Upgrade` header
value is not equal to `WebSocket` (i. e. Tailscale Control Protocol) is forwarded correctly. See the [Apache
docs](https://httpd.apache.org/docs/current/mod/mod_proxy.html#upgrade) for more information on this.
### Caddy
The following basic Caddyfile works with the Headscale configuration [as shown
above](#reverse-proxy-specific-configuration). Substitute placeholders and adjust the configuration as needed:
- `<SERVER_NAME>`: The server name for your instance, e.g. `headscale.example.com`
```none title="Caddyfile" hl_lines="1 12"
http://<SERVER_NAME> {
# Tailscale captive portal detection
handle /generate_204 {
respond 204
}
handle * {
redir https://{host}{uri}
}
}
<SERVER_NAME> {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}
```
Caddy will [automatically](https://caddyserver.com/docs/automatic-https) provision a certificate for your
domain/subdomain, force HTTPS, and proxy WebSocket connections.
WebSockets support is also required when using the Headscale [embedded DERP server](../derp.md). In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml).
### Cloudflare
Running Headscale behind a Cloudflare Proxy or Cloudflare Tunnel is not supported and will not work as Cloudflare does
not support [WebSocket POSTs as required by the Tailscale protocol](#websocket). See [issue
1468](https://github.com/juanfont/headscale/issues/1468) for more information.
Running headscale behind a cloudflare proxy or cloudflare tunnel is not supported and will not work as Cloudflare does not support WebSocket POSTs as required by the Tailscale protocol. See [this issue](https://github.com/juanfont/headscale/issues/1468)
### TLS
Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.
```yaml title="config.yaml"
server_url: https://<YOUR_SERVER_NAME> # This should be the FQDN at which headscale will be served
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
tls_cert_path: ""
tls_key_path: ""
```
## nginx
The following example configuration can be used in your nginx setup, substituting values as necessary. `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `http://localhost:8080`.
```nginx title="nginx.conf"
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name <YOUR_SERVER_NAME>;
ssl_certificate <PATH_TO_CERT>;
ssl_certificate_key <PATH_CERT_KEY>;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://<IP:PORT>;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
}
```
## istio/envoy
If you using [Istio](https://istio.io/) ingressgateway or [Envoy](https://www.envoyproxy.io/) as reverse proxy, there are some tips for you. If not set, you may see some debug log in proxy as below:
```log
Sending local reply with details upgrade_failed
```
### Envoy
You need to add a new upgrade_type named `tailscale-control-protocol`. [See
details](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig).
You need to add a new upgrade_type named `tailscale-control-protocol`. [see details](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig)
### Istio
Same as [envoy](#envoy), we can use `EnvoyFilter` to add a new upgrade_type named `tailscale-control-protocol`.
Same as envoy, we can use `EnvoyFilter` to add upgrade_type.
```yaml
apiVersion: networking.istio.io/v1alpha3
@@ -202,68 +109,33 @@ spec:
- upgrade_type: tailscale-control-protocol
```
### Nginx
## Caddy
The following basic Nginx configuration works with the Headscale configuration [as shown
above](#reverse-proxy-specific-configuration). Substitute placeholders and adjust the configuration as needed:
The following Caddyfile is all that is necessary to use Caddy as a reverse proxy for headscale, in combination with the `config.yaml` specifications above to disable headscale's built in TLS. Replace values as necessary - `<YOUR_SERVER_NAME>` should be the FQDN at which headscale will be served, and `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `localhost:8080`.
- `<SERVER_NAME>`: The server name for your instance, e.g. `headscale.example.com`
- `<PATH_TO_TLS_CERT>`: Absolute path to your TLS certificate
- `<PATH_TO_TLS_KEY>`: Absolute path to your TLS private key
```nginx title="nginx.conf" hl_lines="19 37 39-40"
# headscale
upstream headscale {
zone upstreams 64K;
server 127.0.0.1:8080 max_fails=1 fail_timeout=5s;
keepalive 2;
}
# websocket
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
# http
server {
listen 80;
listen [::]:80;
server_name <SERVER_NAME>;
# Tailscale captive portal detection
location = /generate_204 {
return 204;
}
location / {
return 301 https://$server_name$request_uri;
}
}
# https
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name <SERVER_NAME>;
ssl_certificate <PATH_TO_TLS_CERT>;
ssl_certificate_key <PATH_TO_TLS_KEY>;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header True-Client-IP $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_pass http://headscale;
}
```none title="Caddyfile"
<YOUR_SERVER_NAME> {
reverse_proxy <IP:PORT>
}
```
Caddy v2 will [automatically](https://caddyserver.com/docs/automatic-https) provision a certificate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary.
For a slightly more complex configuration which utilizes Docker containers to manage Caddy, headscale, and Headscale-UI, [Guru Computing's guide](https://blog.gurucomputing.com.au/smart-vpns-with-headscale/) is an excellent reference.
## Apache
The following minimal Apache config will proxy traffic to the headscale instance on `<IP:PORT>`. Note that `upgrade=any` is required as a parameter for `ProxyPass` so that WebSockets traffic whose `Upgrade` header value is not equal to `WebSocket` (i. e. Tailscale Control Protocol) is forwarded correctly. See the [Apache docs](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) for more information on this.
```apache title="apache.conf"
<VirtualHost *:443>
ServerName <YOUR_SERVER_NAME>
ProxyPreserveHost On
ProxyPass / http://<IP:PORT>/ upgrade=any
SSLEngine On
SSLCertificateFile <PATH_TO_CERT>
SSLCertificateKeyFile <PATH_CERT_KEY>
</VirtualHost>
```
-49
View File
@@ -196,54 +196,5 @@ Used in Tailscale SSH rules to allow access to any user except root. Can only be
This autogroup resolves to all IP addresses (`0.0.0.0/0` and `::/0`) which also includes all IP addresses outside the
standard Tailscale IP ranges. This autogroup can only be used as source.
## Node Attributes
[Node attributes](https://tailscale.com/docs/reference/syntax/policy-file#node-attributes) allow for device-specific
configuration and attributes. At least the following node attributes are currently supported by Headscale[^2]:
- `drive:access`, `drive:share`: [Taildrive support](https://tailscale.com/docs/features/taildrive).
- `nextdns:<profile>`, `nextdns:no-device-info`: [NextDNS integration](https://tailscale.com/docs/integrations/nextdns).
Be sure to set NextDNS as global resolver in the [configuration](configuration.md).
- `magicdns-aaaa`: Respond to AAAA queries on the local [MagicDNS](https://tailscale.com/docs/features/magicdns)
resolver at 100.100.100.100.
- `disable-ipv4`: Selectively disable IPv4 for specfic nodes. This is may be useful to workaround [CGNat
conflicts](https://tailscale.com/docs/reference/troubleshooting/network-configuration/cgnat-conflicts).
- `randomize-client-port`: Allocate a [random port for WireGuard
traffic](https://tailscale.com/docs/reference/syntax/policy-file#randomizeclientport) instead of the static default
port 41641.
- `disable-captive-portal-detection`: [Disable automatic captive portal
detection](https://tailscale.com/docs/integrations/captive-portals#disable-captive-portal-detection).
```json title="policy.json"
{
"nodeAttrs": [
{
// Enable MagicDNS AAAA records for all nodes
"target": ["*"]
"attr": ["magicdns-aaaa"]
}
]
}
```
## Network-wide policy options
The following options are applied for the entire tailnet. Consider [node attributes](#node-attributes) for a more
fine-grained configuration instead.
- `randomizeClientPort`: Allocate a [random port for WireGuard
traffic](https://tailscale.com/docs/reference/syntax/policy-file#randomizeclientport) instead of the static default
port 41641.
```json title="policy.json"
{
// Use a random WireGuard port for the entire tailnet
"randomizeClientPort": true
}
```
[^1]: Headscale also allows to store the policy in the database. This is typically only required in case a [web
interface](integration/web-ui.md) is used.
[^2]: Other key-only node attributes can be used as well. Find them in the client source code with `grep -E '^\s+NodeAttr\w+' tailcfg/tailcfg.go` or by using [GitHub code search (requires
login)](https://github.com/search?q=repo%3Atailscale%2Ftailscale%20language%3Ago%20path%3Atailcfg%2Ftailcfg.go%20symbol%3A%2FNodeAttr%5Cw%2B%2F&type=code).
+2 -2
View File
@@ -31,10 +31,10 @@ distributions are Ubuntu 22.04 or newer, Debian 12 or newer.
sudo nano /etc/headscale/config.yaml
```
1. Restart headscale to pick up configuration changes:
1. Enable and start the headscale service:
```shell
sudo systemctl restart headscale
sudo systemctl enable --now headscale
```
1. Verify that headscale is running as intended:
+3 -2
View File
@@ -7,8 +7,9 @@
**It might be outdated and it might miss necessary steps**.
Headscale can be built from source using the latest version of [Go](https://golang.org). See the [Contributing
section in the GitHub README](https://github.com/juanfont/headscale#contributing) for more information.
Headscale can be built from source using the latest version of [Go](https://golang.org) and [Buf](https://buf.build)
(Protobuf generator). See the [Contributing section in the GitHub
README](https://github.com/juanfont/headscale#contributing) for more information.
## OpenBSD
+3
View File
@@ -26,6 +26,9 @@ The ports in use vary with the intended scenario and enabled features. Some of t
- udp/3478
- Expose publicly: yes
- STUN, required if the [embedded DERP server](../ref/derp.md) is enabled
- tcp/50443
- Expose publicly: yes
- Only required if the gRPC interface is used to [remote-control Headscale](../ref/api.md#grpc).
- tcp/9090
- Expose publicly: no
- [Metrics and debug endpoint](../ref/debug.md#metrics-and-debug-endpoint)
-132
View File
@@ -1,132 +0,0 @@
# v1 API: behaviour changes (gRPC/grpc-gateway → ogen OpenAPI 3.0)
This file records every deliberate deviation of the new ogen-based v1 HTTP API
from the previous gRPC + grpc-gateway behaviour. Anything not listed here is
preserved: HTTP verbs and path templates, JSON property names (camelCase,
matching protojson), enum values (`SCREAMING_SNAKE`, e.g.
`REGISTER_METHOD_CLI`), RFC3339 timestamp strings, response envelopes
(`{"user": …}`, `{"nodes": […]}`, …), and the set of operations and their side
effects on the state layer.
Each entry: **what changed**, **why**, **client impact**.
## Wire format
### Integer IDs are JSON numbers, not strings
**What:** `id`, `nodeId`/`node_id`, `oldId`, `user`, and every other integer
identifier is now a JSON number (`"id": 42`). grpc-gateway emitted protojson's
int64-as-string form (`"id": "42"`).
**Why:** native integers are the idiomatic OpenAPI 3.0 / ogen representation,
type-safe end to end, and remove per-field string⇄int conversion. Headscale
identifiers are small and well within the JS safe-integer range.
**Client impact:** HTTP clients that read IDs as strings must read them as
numbers. `headscale … -o json/yaml` output changes accordingly (the CLI now
runs on the generated client).
### Error responses are RFC 7807 problem documents
**What:** errors are returned as `application/problem+json` with a body of
`{type?, title, status, detail, instance?}`. grpc-gateway returned an
`rpcStatus` envelope `{code, message, details[]}` as `application/json`, where
`code` was a gRPC status code and `details` was effectively always empty.
**Why:** RFC 7807 is the standard HTTP error shape; the gRPC `code`/`details`
fields were gRPC implementation leakage with no value over the HTTP status line.
**Client impact:** clients that parsed `{code, message}` must read
`{status, detail}` and the `application/problem+json` content type. The HTTP
status code itself is unchanged for equivalent conditions (e.g. unknown user →
404, invalid argument → 400, bad/My missing API key → 401).
## Behaviour
### Client errors return 4xx consistently
**What:** client mistakes now map to the appropriate 4xx status instead of 500.
Missing resources are `404` (e.g. `RenameUser`, `DeleteUser`, `GetNode`,
`DeleteNode`); invalid input is `400` (e.g. an unparseable route in
`SetApprovedRoutes`, a malformed registration key in `RegisterNode`, an invalid
tag in `SetTags`, an unconfirmed `BackfillNodeIPs`). Many of these gRPC handlers
returned a plain Go error, which grpc-gateway rendered as `500`.
**Why:** a missing resource or bad input is a client error, not a server error;
4xx is the correct, consistent status.
**Client impact:** clients that treated these as 500 should treat them as 400/404.
### Health on database failure
**What:** `GET /api/v1/health` returns `200 {"databaseConnectivity": true}` when
the database is reachable and `500` (problem document) when the ping fails. The
gRPC implementation returned the ping error, which grpc-gateway rendered as a
500; the `databaseConnectivity:false` body was never observable on failure.
**Why:** preserves the observable contract (200 healthy, 500 unhealthy) under
the new error shape.
**Client impact:** none beyond the problem-document error shape above.
## CLI
### Remote CLI connects to the HTTP API, not the gRPC port
**What:** with a configured `cli.address` (or `HEADSCALE_CLI_ADDRESS`), the CLI
now speaks HTTP to the headscale API URL rather than gRPC to `grpc_listen_addr`.
A bare `host:port` is assumed to be `https://host:port`. Locally (no address)
the CLI talks HTTP over the existing unix socket, unchanged in spirit — no API
key needed, filesystem permissions are the trust boundary.
**Why:** the gRPC service and its TCP listener are removed; the CLI runs on the
generated HTTP client.
**Client impact:** point `cli.address` at the headscale HTTP server (the same
URL `server_url` is reachable on) instead of the gRPC address. `cli.api_key`
and `cli.insecure` are unchanged.
### `delete`/`expire` commands print a result message
**What:** commands whose API operation has no response body (user/node/key
delete, key expire, auth approve/reject) print a small
`{"result": "..."}`-style object (or the human-readable message) instead of the
previous empty `{}`.
**Why:** the operations return no content; a result message is more useful than
an empty object.
**Client impact:** scripts parsing the empty `{}` should read the `result`
field (machine-readable output) or rely on the exit code.
### CLI database-bypass flag renamed
**What:** `--bypass-grpc-and-access-database-directly` is now
`--bypass-server-and-access-database-directly`.
**Why:** the gRPC server is gone; the flag bypasses the running server whatever
its transport, so the name no longer mentions gRPC.
**Client impact:** scripts using the old flag name must update it.
### Missing resources return a consistent `404`
**What:** renaming or expiring an unknown node, and expiring or deleting an
unknown pre-auth key, now return `404 Not Found`. Previously the node
operations surfaced as `500` and the pre-auth key operations reported success
without changing anything.
**Why:** a missing resource is a client error, not a server error, and an
expire or delete that matched no row should not report success.
**Client impact:** code that treated these as `500` or as a silent success
should handle `404`.
## Delivery note (not a shipped behaviour change)
The grpc-gateway HTTP facade is replaced wholesale at `/api/v1` by the ogen
server in the foundation commit, rather than path-by-path. During development,
endpoints whose resource group has not yet been migrated return `501` over HTTP;
all are implemented before the branch is complete. The CLI is unaffected during
this window because it still uses the gRPC servers until its own migration. No
intermediate state is released.
-147
View File
@@ -1,147 +0,0 @@
# Plan: Headscale v1 API — gRPC/Protobuf → ogen OpenAPI 3.0
This is the implementation plan for converting the Headscale v1 API from a
Protobuf gRPC service with a grpc-gateway REST facade to an OpenAPI 3.0 spec
driving ogen-generated server stubs and a Go client, backed unchanged by the
`hscontrol/state` layer. It is written so coding agents can execute the
remaining work autonomously; tests are the gate.
See [CHANGES.md](./CHANGES.md) for every deliberate behaviour deviation.
## Locked decisions
- **Integer IDs** are native JSON numbers (`type: integer, format: uint64`), not protojson strings.
- **Errors** are RFC 7807 problem documents (`application/problem+json`).
- **Local CLI** talks HTTP over the existing unix socket (no API key; socket permissions are the trust boundary). Remote stays HTTPS + bearer key.
- Preserved by default: HTTP verbs/paths, camelCase JSON keys, `SCREAMING_SNAKE` enums, RFC3339 timestamps, response envelopes, operation set and state side effects.
## Package layout (v1 self-contained; v2 drops in as a sibling)
| Concern | Path | Package |
| --------------------------- | --------------------------- | -------------------------------- |
| Spec (source of truth) | `openapi/v1/headscale.yaml` | — |
| Generated server+client | `gen/api/v1/` | `apiv1` (imported as `oas`) |
| Handlers / state adapter | `hscontrol/api/v1/` | `apiv1` |
| HTTP-parity tests + harness | `hscontrol/servertest/` | `servertest` / `servertest_test` |
v1 and v2 must not import each other or share generated types — they converge
only on `hscontrol/state`. Codegen: `go generate ./gen/api/v1/` (also run by
`make generate` via `go generate ./...`); CI freshness via
`.github/workflows/check-generated.yml`. Never hand-edit generated code; fix the
spec, never reach for `x-ogen` without recording in CHANGES.md why no spec
change works.
## Architecture
- **Routing.** chi is the top mux. The ogen server (`http.Handler`) is mounted at `/api/v1/*` in `app.go` `createRouter`; chi leaves `r.URL.Path` intact so ogen's router matches the full path. ogen owns `/api/v1` wholesale: un-migrated operations inherit `oas.UnimplementedHandler` (501) until their unit lands. Parallel endpoint agents therefore touch only their handler file + tests — never `app.go`.
- **Auth.** Spec declares `bearerAuth` (http/bearer) global security. `Server.HandleBearerAuth``state.ValidateAPIKey`; any failure → 401. (Unix-socket auth bypass is added in the CLI phase, when the socket serves HTTP.)
- **Errors.** `Server.NewError` (ogen's convenient-error hook for security/decoding) and `errorHandler` (`WithErrorHandler`, for plain handler errors) both route through `classify` in `hscontrol/api/v1/errors.go`: typed `*oas.ErrorStatusCode` pass through; ogen framework errors use their `Code()` (security→401, decode→400); state errors via `mapStateError` (not-found sentinels→404, `ErrPolicyUpdateIsDisabled`→400, else 500). Handlers build expected errors with `apiError`/`notFound`/`badRequest`/`mapStateError`.
- **Side effects.** Handlers replicate the exact `state.*` call sequence and `change(...)` notifications from `hscontrol/grpcv1.go` (policy reload on SetPolicy, route auto-approval on RegisterNode, change broadcasts on node mutations, etc.). The adapter holds `state *state.State`, `cfg *types.Config`, and `change func(...change.Change)` (wired to `Headscale.Change`); it does not import the parent `hscontrol` package (no import cycle).
- **Transitional gRPC.** The gRPC unix-socket and TCP servers keep running through the endpoint phase so the existing CLI works; they (and proto/gateway code) are removed in cutover.
## Operation inventory (backbone) → state mapping
All paths keep the current `google.api.http` templates. Source of truth for
behaviour: `hscontrol/grpcv1.go`.
**User**`hscontrol/api/v1/users.go`
- `POST /api/v1/user` CreateUser → `state.CreateUser`; `change(policyChanged)`.
- `GET /api/v1/user` ListUsers(`id`,`name`,`email` query) → `ListUsersWithFilter` (by name|email|id) else `ListAllUsers`; sort by id.
- `POST /api/v1/user/{old_id}/rename/{new_name}` RenameUser → `GetUserByID`,`RenameUser`,`change(c)`,`GetUserByName`.
- `DELETE /api/v1/user/{id}` DeleteUser → `GetUserByID`,`DeleteUser`,`change`.
**PreAuthKey**`preauthkeys.go`
- `POST /api/v1/preauthkey` CreatePreAuthKey → validate each `aclTags` (`validateTag`), optional `GetUserByID`, `CreatePreAuthKey(userID, reusable, ephemeral, &expiration, aclTags)`.
- `GET /api/v1/preauthkey` ListPreAuthKeys → `ListPreAuthKeys`; sort by id.
- `POST /api/v1/preauthkey/expire` ExpirePreAuthKey → `ExpirePreAuthKey(id)`.
- `DELETE /api/v1/preauthkey` DeletePreAuthKey(`id` query) → `DeletePreAuthKey(id)`.
**Node**`nodes.go`
- `GET /api/v1/node` ListNodes(`user` query) → `GetUserByName`+`ListNodesByUser` | `ListNodes`; `nodesToProto` (tagged→`TaggedDevices` user; `subnetRoutes` = primary+exit routes); sort by id.
- `POST /api/v1/node/register` RegisterNode(`user`,`key` query) → `AuthIDFromString`,`GetUserByName`,`HandleNodeFromAuthPath(...,RegisterMethodCLI)`,`AutoApproveRoutes`,`change(nodeChange,routeChange)`.
- `POST /api/v1/node/backfillips` BackfillNodeIPs(`confirmed` query) → require confirmed else error; `BackfillNodeIPs`.
- `GET /api/v1/node/{node_id}` GetNode → `GetNodeByID`; not found → 404.
- `DELETE /api/v1/node/{node_id}` DeleteNode → `GetNodeByID`(404),`DeleteNode`,`change`.
- `POST /api/v1/node/{node_id}/tags` SetTags → non-empty tags else 400; `validateTag` each; `GetNodeByID`(404); `SetNodeTags`(invalid→400); `change`.
- `POST /api/v1/node/{node_id}/approve_routes` SetApprovedRoutes → parse prefixes; exit-route expansion (AllIPv4/AllIPv6 → add both); sort+compact; `SetApprovedRoutes`(invalid→400); `change`; set `subnetRoutes` = `GetNodePrimaryRoutes`.
- `POST /api/v1/node/{node_id}/expire` ExpireNode(`expiry`,`disable_expiry` query) → both set → 400; disable → `SetNodeExpiry(nil)`; else `SetNodeExpiry(&expiry)` (default now); `change`.
- `POST /api/v1/node/{node_id}/rename/{new_name}` RenameNode → `RenameNode`,`change`.
- `POST /api/v1/debug/node` DebugCreateNode → `GetUserByName`,`StringToIPPrefix`,`AuthIDFromString`,`SetAuthCacheEntry`; echo synthetic node.
**Auth**`auth.go`
- `POST /api/v1/auth/register` AuthRegister → delegates to RegisterNode(`key`=authId,`user`).
- `POST /api/v1/auth/approve` AuthApprove → `AuthIDFromString`(400),`GetAuthCacheEntry`(404),`FinishAuth({})`.
- `POST /api/v1/auth/reject` AuthReject → `AuthIDFromString`(400),`GetAuthCacheEntry`(404),`FinishAuth({Err})`.
**ApiKey**`apikeys.go`
- `POST /api/v1/apikey` CreateApiKey → `CreateAPIKey(&expiration)`; returns raw `apiKey` string.
- `GET /api/v1/apikey` ListApiKeys → `ListAPIKeys`; sort by id.
- `POST /api/v1/apikey/expire` ExpireApiKey → `getAPIKey` (id xor prefix; neither/both→400),`ExpireAPIKey`.
- `DELETE /api/v1/apikey/{prefix}` DeleteApiKey(`id` query) → `getAPIKey`,`DestroyAPIKey`.
**Policy**`policy.go`
- `GET /api/v1/policy` GetPolicy → DB mode: `GetPolicy`; file mode: read `cfg.Policy.Path`.
- `PUT /api/v1/policy` SetPolicy → DB mode only (`ErrPolicyUpdateIsDisabled`→400); `ListNodes`,`SetPolicy`,`SSHPolicy(node0)`,`SetPolicyInDB`,`ReloadPolicy`,`change(cs...)`.
- `POST /api/v1/policy/check` CheckPolicy → `ListAllUsers`,`ListNodes`,`NewPolicyManager`+`SetPolicy` (no persist; invalid→400).
**Health**`health.go` (done) — `PingDB`; ok→200, fail→500.
## Handler authoring recipe (for endpoint agents)
For each resource group, in this order, then `/commit` (Go style: `api: <imperative>`):
1. **Tests first.** Add `hscontrol/servertest/apiv1_<resource>_test.go` (package `servertest_test`). Use `srv := servertest.NewServer(t)`, `client := srv.APIClient(t, srv.CreateAPIKey(t))`, seed state via `srv.State()` / existing `servertest` helpers. Assert: success payloads (status, envelope, field values), every error path (404/400/401 with `*apiv1.ErrorStatusCode` status), and state side effects (re-read via `srv.State()`). Cover real behaviour, not just happy paths. Where the new contract intentionally differs from the old, the assertion encodes the new value **and** there is a matching CHANGES.md entry.
1. **Implement** the group's handler methods on `*apiv1.Server` in `hscontrol/api/v1/<resource>.go`, mirroring the `grpcv1.go` logic and side effects exactly. Convert between ogen types (`*XxxReq`, `XxxParams`, `*XxxOK`, `oas.User`/`oas.Node`/…) and state types. Build `oas.OptXxx` for optional fields; map state results to the response envelope. Errors: `notFound`/`badRequest`/`mapStateError`/`apiError`.
1. **CLI tests** for the group's commands belong to the CLI phase (`integration/`); reference them in the unit's acceptance.
1. **CHANGES.md**: add an entry for any deviation; if a test had to encode a non-preserved behaviour, it must correspond to an entry.
1. Run `goimports -w`, `go test ./hscontrol/servertest/ -run TestAPIv1_<Resource>`, then `make fmt`/`make lint` on touched files. Commit.
A reusable proto→ogen conversion (`oas.User` from `types.User`, etc.) should live in `hscontrol/api/v1/convert.go`; the first agent to need a converter adds it there and others reuse it. Do not duplicate.
## Work units & dependency map
```
F1 spec → F2 codegen+CI → F3 server/adapter/auth/errors → F4 harness [DONE]
┌────────────────────────────────────────────────────┐
E1 User E2 PreAuthKey E3 Node E4 Auth E5 ApiKey E6 Policy E7 Health(done)
└────────────────────────────────────────────────────┘ (parallel; touch only own handler+tests)
F2 → C0 CLI client adapter → C1..C8 per-command (each needs its E-unit) (parallel)
all E* + all C* → X1 flip/auth → X2 remove proto/gRPC/gateway → X3 docs+served spec → X4 reconcile
```
- **E1E7**: each owns its handler file + `apiv1_<resource>_test.go`. Acceptance: that group's HTTP-parity tests green; behaviour matches `grpcv1.go` modulo CHANGES.md.
- **C0**: rework `cmd/headscale/cli/utils.go` dial point to build `apiv1.Client` (local HTTP-over-unix, no key; remote HTTPS+`HEADSCALE_CLI_API_KEY`+`cli.insecure`). Serve ogen over the unix socket; add socket auth-bypass. **C1C8**: convert each `cmd/headscale/cli/*.go` off the gRPC client; gate with `integration/` CLI tests. Acceptance: no `v1.HeadscaleServiceClient` usage remains.
- **X1**: ogen is already the sole `/api/v1` handler; ensure no chi auth duplication, remove any dead gateway wiring. **X2**: delete `proto/headscale/v1`, `gen/go`, `gen/openapiv2`, `hscontrol/grpcv1.go`(+test), gRPC servers, `buf.*`; drop grpc/grpc-gateway deps (verify Noise is independent). **X3**: serve `openapi/v1/headscale.yaml` at `/swagger`; update `swagger.go` embed + `docs/`. **X4**: reconcile PLAN.md/CHANGES.md to the built state; DoD checklist green.
## Status
- [x] **F1** spec — `openapi/v1/headscale.yaml` (all 27 ops, native int64, RFC7807 `Problem`, bearerAuth). ogen generates cleanly.
- [x] **F2** codegen — `gen/api/v1/generate.go` (`go generate`), ogen tool dep in `go.mod`, CI freshness covers `openapi/**`.
- [x] **F3** server/adapter/auth/errors — `hscontrol/api/v1/{server,errors,auth,health}.go`; mounted at `/api/v1`; gateway unmounted; gRPC servers retained.
- [x] **F4** harness — `servertest.APIClient`/`CreateAPIKey`; Health parity + 401 tests green.
- [x] **E1E7** endpoints — all 27 operations implemented in `hscontrol/api/v1/{users,apikeys,preauthkeys,nodes,auth,policy,health}.go` with HTTP-parity tests in `hscontrol/servertest/apiv1_*_test.go` (30 tests, all green).
- [x] **C0C8** CLI migration — server serves the API over the unix socket (auth bypassed via `apiv1.WithSocketAuth`); every `cmd/headscale/cli/*.go` command runs on the generated `apiv1.Client`; no gRPC client usage remains.
- [x] **X1/X2** proto/gRPC removal — `grpcv1.go`, the TCP gRPC server + interceptor, the `.Proto()`/`RegisterMethodToV1Enum` builders, `proto/`, `gen/go/`, `gen/openapiv2/`, and `buf.gen.yaml` are deleted; `convert.go` reads the state views directly; `integration/` decodes into `apiv1` types. `grep` finds zero proto references in any `.go` file. grpc/grpc-gateway are no longer direct go.mod deps. Proto lint/format targets removed from the Makefile.
- [x] **X3** served spec — `/swagger` serves the embedded `openapi/v1/headscale.yaml` (3.0).
- [x] **X4** reconcile — this document and CHANGES.md reflect the built state.
The conversion is complete. The full `go test ./...` suite is validatable here for
everything except the Docker-based `integration/` package, which is
compile-clean (`go vet ./integration/...`) and ported to the new wire format; its
end-to-end run is gated on Docker (`go run ./cmd/hi`).
## Definition of done
Every v1 op served by ogen over the state layer; HTTP + CLI tests cover every
endpoint and pass; CLI on the generated client (no gRPC client); proto/gRPC +
gateway + generated code removed; generated code matches committed spec
(CI green); docs + `/swagger` point at the 3.0 spec; v1 self-contained, no
cross-version coupling; current behaviour preserved except deliberate,
test-matched, CHANGES.md-recorded deviations.
Generated
+4 -4
View File
@@ -20,16 +20,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1781153106,
"narHash": "sha256-yzsroLCcuRG4KdGMxWt0eXKOrRSgQT8/xjYngeq9ujU=",
"lastModified": 1777270315,
"narHash": "sha256-yKB4G6cKsQsWN7M6rZGk6gkJPDNPIzT05y4qzRyCDlI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9ee75f111a06d7ab2b2f729698a8eff53d54e070",
"rev": "6368eda62c9775c38ef7f714b2555a741c20c72d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "staging-next-26.05",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
+50 -14
View File
@@ -2,12 +2,7 @@
description = "headscale - Open Source Tailscale Control server";
inputs = {
# Pinned to staging-next-26.05 for Go 1.26.4 (security fix GO-2026-5037/5039):
# nixpkgs-unstable still ships 1.26.3 — the bump is merged to nixpkgs staging
# but the large-rebuild staging->unstable pipeline lags. The 26.05 line is
# otherwise current (dev tools match unstable). Switch back to nixpkgs-unstable
# once it ships go_1_26 >= 1.26.4.
nixpkgs.url = "github:NixOS/nixpkgs/staging-next-26.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
@@ -31,7 +26,6 @@
overlays.default = _: prev:
let
pkgs = nixpkgs.legacyPackages.${prev.stdenv.hostPlatform.system};
# Go 1.26 builder; resolves to Go 1.26.4 from the pinned nixpkgs.
buildGo = pkgs.buildGo126Module;
vendorHash = (builtins.fromJSON (builtins.readFile ./flakehashes.json)).vendor.sri;
in
@@ -66,20 +60,53 @@
subPackages = [ "cmd/hi" ];
};
# Build golangci-lint with stock Go 1.26 (upstream uses hardcoded Go
# version); it does not build against the pinned 1.26.4.
protoc-gen-grpc-gateway = buildGo rec {
pname = "grpc-gateway";
version = "2.28.0";
src = pkgs.fetchFromGitHub {
owner = "grpc-ecosystem";
repo = "grpc-gateway";
rev = "v${version}";
sha256 = "sha256-93omvHb+b+S0w4D+FGEEwYYDjgumJFDAruc1P4elfvA=";
};
vendorHash = "sha256-jVP5zfFPfHeAEApKNJzZwuZLA+DjKgkL7m2DFG72UNs=";
nativeBuildInputs = [ pkgs.installShellFiles ];
subPackages = [ "protoc-gen-grpc-gateway" "protoc-gen-openapiv2" ];
};
protobuf-language-server = buildGo rec {
pname = "protobuf-language-server";
version = "ab4c128";
src = pkgs.fetchFromGitHub {
owner = "lasorda";
repo = "protobuf-language-server";
rev = "ab4c128f00774d51bd6d1f4cfa735f4b7c8619e3";
sha256 = "sha256-yF6kG+qTRxVO/qp2V9HgTyFBeOm5RQzeqdZFrdidwxM=";
};
vendorHash = "sha256-4nTpKBe7ekJsfQf+P6edT/9Vp2SBYbKz1ITawD3bhkI=";
subPackages = [ "." ];
};
# Build golangci-lint with Go 1.26 (upstream uses hardcoded Go version)
golangci-lint = buildGo rec {
pname = "golangci-lint";
version = "2.12.2";
version = "2.11.4";
src = pkgs.fetchFromGitHub {
owner = "golangci";
repo = "golangci-lint";
rev = "v${version}";
hash = "sha256-qR7fp1x2S+EwEAcplRHTvA3jWwLr/XSiYKSZtAwkrNU=";
hash = "sha256-B19aLvfNRY9TOYw/71f2vpNUuSIz8OI4dL0ijGezsas=";
};
vendorHash = "sha256-AG5wtLwWLz55bdp1oi3cW+9O3yj1W1P7MV9zxym7Pb4=";
vendorHash = "sha256-xuoj4+U4tB5gpABKq4Dbp2cxnljxdYoBbO8A7DqPM5E=";
subPackages = [ "cmd/golangci-lint" ];
@@ -161,8 +188,17 @@
# 'dot' is needed for pprof graphs
# go tool pprof -http=: <source>
graphviz
# Protobuf dependencies
protobuf
protoc-gen-go
protoc-gen-go-grpc
protoc-gen-grpc-gateway
buf
clang-tools # clang-format
protobuf-language-server
]
++ lib.optionals pkgs.stdenv.isLinux [ traceroute ];
++ lib.optional pkgs.stdenv.isLinux [ traceroute ];
# Add entry to build a docker image with headscale
# caveat: only works on Linux
@@ -193,7 +229,7 @@
(pkgs.writeShellScriptBin
"go-mod-update-all"
''
cat go.mod | ${pkgs.ripgrep}/bin/rg "\t" | ${pkgs.ripgrep}/bin/rg -v indirect | ${pkgs.gawk}/bin/awk '{print $1}' | ${pkgs.findutils}/bin/xargs go get -u
cat go.mod | ${pkgs.silver-searcher}/bin/ag "\t" | ${pkgs.silver-searcher}/bin/ag -v indirect | ${pkgs.gawk}/bin/awk '{print $1}' | ${pkgs.findutils}/bin/xargs go get -u
go mod tidy
'')
];
+2 -2
View File
@@ -1,6 +1,6 @@
{
"vendor": {
"goModSum": "sha256-pE9YS5dCh93GWjRWlbDTmPLhvXbHIoHEKU/6qiN2ex4=",
"sri": "sha256-KRgUvdDEVfkkwcEU6mxGWyCGXn/IMw9ZBVQmVGQZUcY="
"goModSum": "sha256-IE0n9cSqO4XNX4RN+CGBk9VC46iACiZKDFf/215iivk=",
"sri": "sha256-ijEIP9NSomhlWOgsVN7tPvSuvkTiLtnvXvhZmatIDLM="
}
}
-8
View File
@@ -1,8 +0,0 @@
// Package apiv1 contains the ogen-generated server and client for the
// Headscale v1 HTTP API. The code is generated from the committed OpenAPI 3.0
// spec at openapi/v1/headscale.yaml — the single source of truth. Do not edit
// generated files by hand; run `make generate` (or `go generate ./...`) after
// changing the spec.
//
//go:generate go run github.com/ogen-go/ogen/cmd/ogen --target . --package apiv1 --clean ../../../openapi/v1/headscale.yaml
package apiv1
-318
View File
@@ -1,318 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"net/http"
"strings"
ht "github.com/ogen-go/ogen/http"
"github.com/ogen-go/ogen/middleware"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/otelogen"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)
var (
// Allocate option closure once.
clientSpanKind = trace.WithSpanKind(trace.SpanKindClient)
// Allocate option closure once.
serverSpanKind = trace.WithSpanKind(trace.SpanKindServer)
)
type (
optionFunc[C any] func(*C)
otelOptionFunc func(*otelConfig)
)
type otelConfig struct {
TracerProvider trace.TracerProvider
Tracer trace.Tracer
MeterProvider metric.MeterProvider
Meter metric.Meter
Attributes []attribute.KeyValue
}
func (cfg *otelConfig) initOTEL() {
if cfg.TracerProvider == nil {
cfg.TracerProvider = otel.GetTracerProvider()
}
if cfg.MeterProvider == nil {
cfg.MeterProvider = otel.GetMeterProvider()
}
cfg.Tracer = cfg.TracerProvider.Tracer(otelogen.Name,
trace.WithInstrumentationVersion(otelogen.SemVersion()),
)
cfg.Meter = cfg.MeterProvider.Meter(otelogen.Name,
metric.WithInstrumentationVersion(otelogen.SemVersion()),
)
}
// ErrorHandler is error handler.
type ErrorHandler = ogenerrors.ErrorHandler
type serverConfig struct {
otelConfig
NotFound http.HandlerFunc
MethodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)
ErrorHandler ErrorHandler
Prefix string
Middleware Middleware
MaxMultipartMemory int64
}
// ServerOption is server config option.
type ServerOption interface {
applyServer(*serverConfig)
}
var _ ServerOption = (optionFunc[serverConfig])(nil)
func (o optionFunc[C]) applyServer(c *C) {
o(c)
}
var _ ServerOption = (otelOptionFunc)(nil)
func (o otelOptionFunc) applyServer(c *serverConfig) {
o(&c.otelConfig)
}
func newServerConfig(opts ...ServerOption) serverConfig {
cfg := serverConfig{
NotFound: http.NotFound,
MethodNotAllowed: nil,
ErrorHandler: ogenerrors.DefaultErrorHandler,
Middleware: nil,
MaxMultipartMemory: 32 << 20, // 32 MB
}
for _, opt := range opts {
opt.applyServer(&cfg)
}
cfg.initOTEL()
return cfg
}
type baseServer struct {
cfg serverConfig
requests metric.Int64Counter
errors metric.Int64Counter
duration metric.Float64Histogram
}
func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) {
s.cfg.NotFound(w, r)
}
type notAllowedParams struct {
allowedMethods string
allowedHeaders map[string]string
acceptPost string
acceptPatch string
}
func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, params notAllowedParams) {
h := w.Header()
isOptions := r.Method == "OPTIONS"
if isOptions {
h.Set("Access-Control-Allow-Methods", params.allowedMethods)
if params.allowedHeaders != nil {
m := r.Header.Get("Access-Control-Request-Method")
if m != "" {
allowedHeaders, ok := params.allowedHeaders[strings.ToUpper(m)]
if ok {
h.Set("Access-Control-Allow-Headers", allowedHeaders)
}
}
}
if params.acceptPost != "" {
h.Set("Accept-Post", params.acceptPost)
}
if params.acceptPatch != "" {
h.Set("Accept-Patch", params.acceptPatch)
}
}
if s.cfg.MethodNotAllowed != nil {
s.cfg.MethodNotAllowed(w, r, params.allowedMethods)
return
}
status := http.StatusNoContent
if !isOptions {
h.Set("Allow", params.allowedMethods)
status = http.StatusMethodNotAllowed
}
w.WriteHeader(status)
}
func (cfg serverConfig) baseServer() (s baseServer, err error) {
s = baseServer{cfg: cfg}
if s.requests, err = otelogen.ServerRequestCountCounter(s.cfg.Meter); err != nil {
return s, err
}
if s.errors, err = otelogen.ServerErrorsCountCounter(s.cfg.Meter); err != nil {
return s, err
}
if s.duration, err = otelogen.ServerDurationHistogram(s.cfg.Meter); err != nil {
return s, err
}
return s, nil
}
type clientConfig struct {
otelConfig
Client ht.Client
}
// ClientOption is client config option.
type ClientOption interface {
applyClient(*clientConfig)
}
var _ ClientOption = (optionFunc[clientConfig])(nil)
func (o optionFunc[C]) applyClient(c *C) {
o(c)
}
var _ ClientOption = (otelOptionFunc)(nil)
func (o otelOptionFunc) applyClient(c *clientConfig) {
o(&c.otelConfig)
}
func newClientConfig(opts ...ClientOption) clientConfig {
cfg := clientConfig{
Client: http.DefaultClient,
}
for _, opt := range opts {
opt.applyClient(&cfg)
}
cfg.initOTEL()
return cfg
}
type baseClient struct {
cfg clientConfig
requests metric.Int64Counter
errors metric.Int64Counter
duration metric.Float64Histogram
}
func (cfg clientConfig) baseClient() (c baseClient, err error) {
c = baseClient{cfg: cfg}
if c.requests, err = otelogen.ClientRequestCountCounter(c.cfg.Meter); err != nil {
return c, err
}
if c.errors, err = otelogen.ClientErrorsCountCounter(c.cfg.Meter); err != nil {
return c, err
}
if c.duration, err = otelogen.ClientDurationHistogram(c.cfg.Meter); err != nil {
return c, err
}
return c, nil
}
// Option is config option.
type Option interface {
ServerOption
ClientOption
}
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
//
// If none is specified, the global provider is used.
func WithTracerProvider(provider trace.TracerProvider) Option {
return otelOptionFunc(func(cfg *otelConfig) {
if provider != nil {
cfg.TracerProvider = provider
}
})
}
// WithMeterProvider specifies a meter provider to use for creating a meter.
//
// If none is specified, the otel.GetMeterProvider() is used.
func WithMeterProvider(provider metric.MeterProvider) Option {
return otelOptionFunc(func(cfg *otelConfig) {
if provider != nil {
cfg.MeterProvider = provider
}
})
}
// WithAttributes specifies default otel attributes.
func WithAttributes(attributes ...attribute.KeyValue) Option {
return otelOptionFunc(func(cfg *otelConfig) {
cfg.Attributes = attributes
})
}
// WithClient specifies http client to use.
func WithClient(client ht.Client) ClientOption {
return optionFunc[clientConfig](func(cfg *clientConfig) {
if client != nil {
cfg.Client = client
}
})
}
// WithNotFound specifies Not Found handler to use.
func WithNotFound(notFound http.HandlerFunc) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
if notFound != nil {
cfg.NotFound = notFound
}
})
}
// WithMethodNotAllowed specifies Method Not Allowed handler to use.
func WithMethodNotAllowed(methodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
if methodNotAllowed != nil {
cfg.MethodNotAllowed = methodNotAllowed
}
})
}
// WithErrorHandler specifies error handler to use.
func WithErrorHandler(h ErrorHandler) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
if h != nil {
cfg.ErrorHandler = h
}
})
}
// WithPathPrefix specifies server path prefix.
func WithPathPrefix(prefix string) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
cfg.Prefix = prefix
})
}
// WithMiddleware specifies middlewares to use.
func WithMiddleware(m ...Middleware) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
switch len(m) {
case 0:
cfg.Middleware = nil
case 1:
cfg.Middleware = m[0]
default:
cfg.Middleware = middleware.ChainMiddlewares(m...)
}
})
}
// WithMaxMultipartMemory specifies limit of memory for storing file parts.
// File parts which can't be stored in memory will be stored on disk in temporary files.
func WithMaxMultipartMemory(max int64) ServerOption {
return optionFunc[serverConfig](func(cfg *serverConfig) {
if max > 0 {
cfg.MaxMultipartMemory = max
}
})
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"context"
"go.opentelemetry.io/otel/attribute"
)
// Labeler is used to allow adding custom attributes to the server request metrics.
type Labeler struct {
attrs []attribute.KeyValue
}
// Add attributes to the Labeler.
func (l *Labeler) Add(attrs ...attribute.KeyValue) {
l.attrs = append(l.attrs, attrs...)
}
// AttributeSet returns the attributes added to the Labeler as an attribute.Set.
func (l *Labeler) AttributeSet() attribute.Set {
return attribute.NewSet(l.attrs...)
}
type labelerContextKey struct{}
// LabelerFromContext retrieves the Labeler from the provided context, if present.
//
// If no Labeler was found in the provided context a new, empty Labeler is returned and the second
// return value is false. In this case it is safe to use the Labeler but any attributes added to
// it will not be used.
func LabelerFromContext(ctx context.Context) (*Labeler, bool) {
if l, ok := ctx.Value(labelerContextKey{}).(*Labeler); ok {
return l, true
}
return &Labeler{}, false
}
func contextWithLabeler(ctx context.Context, l *Labeler) context.Context {
return context.WithValue(ctx, labelerContextKey{}, l)
}
-10
View File
@@ -1,10 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"github.com/ogen-go/ogen/middleware"
)
// Middleware is middleware type.
type Middleware = middleware.Middleware
-38
View File
@@ -1,38 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
// OperationName is the ogen operation name
type OperationName = string
const (
AuthApproveOperation OperationName = "AuthApprove"
AuthRegisterOperation OperationName = "AuthRegister"
AuthRejectOperation OperationName = "AuthReject"
BackfillNodeIPsOperation OperationName = "BackfillNodeIPs"
CheckPolicyOperation OperationName = "CheckPolicy"
CreateApiKeyOperation OperationName = "CreateApiKey"
CreatePreAuthKeyOperation OperationName = "CreatePreAuthKey"
CreateUserOperation OperationName = "CreateUser"
DebugCreateNodeOperation OperationName = "DebugCreateNode"
DeleteApiKeyOperation OperationName = "DeleteApiKey"
DeleteNodeOperation OperationName = "DeleteNode"
DeletePreAuthKeyOperation OperationName = "DeletePreAuthKey"
DeleteUserOperation OperationName = "DeleteUser"
ExpireApiKeyOperation OperationName = "ExpireApiKey"
ExpireNodeOperation OperationName = "ExpireNode"
ExpirePreAuthKeyOperation OperationName = "ExpirePreAuthKey"
GetNodeOperation OperationName = "GetNode"
GetPolicyOperation OperationName = "GetPolicy"
HealthOperation OperationName = "Health"
ListApiKeysOperation OperationName = "ListApiKeys"
ListNodesOperation OperationName = "ListNodes"
ListPreAuthKeysOperation OperationName = "ListPreAuthKeys"
ListUsersOperation OperationName = "ListUsers"
RegisterNodeOperation OperationName = "RegisterNode"
RenameNodeOperation OperationName = "RenameNode"
RenameUserOperation OperationName = "RenameUser"
SetApprovedRoutesOperation OperationName = "SetApprovedRoutes"
SetPolicyOperation OperationName = "SetPolicy"
SetTagsOperation OperationName = "SetTags"
)
File diff suppressed because it is too large Load Diff
-938
View File
@@ -1,938 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"bytes"
"io"
"mime"
"net/http"
"github.com/go-faster/errors"
"github.com/go-faster/jx"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/ogen-go/ogen/validate"
)
func (s *Server) decodeAuthApproveRequest(r *http.Request) (
req *AuthApproveReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request AuthApproveReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeAuthRegisterRequest(r *http.Request) (
req *AuthRegisterReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request AuthRegisterReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeAuthRejectRequest(r *http.Request) (
req *AuthRejectReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request AuthRejectReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCheckPolicyRequest(r *http.Request) (
req *CheckPolicyReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request CheckPolicyReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateApiKeyRequest(r *http.Request) (
req *CreateApiKeyReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request CreateApiKeyReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreatePreAuthKeyRequest(r *http.Request) (
req *CreatePreAuthKeyReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request CreatePreAuthKeyReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeCreateUserRequest(r *http.Request) (
req *CreateUserReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request CreateUserReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeDebugCreateNodeRequest(r *http.Request) (
req *DebugCreateNodeReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request DebugCreateNodeReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeExpireApiKeyRequest(r *http.Request) (
req *ExpireApiKeyReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request ExpireApiKeyReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeExpirePreAuthKeyRequest(r *http.Request) (
req *ExpirePreAuthKeyReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request ExpirePreAuthKeyReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeSetApprovedRoutesRequest(r *http.Request) (
req *SetApprovedRoutesReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request SetApprovedRoutesReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeSetPolicyRequest(r *http.Request) (
req *SetPolicyReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request SetPolicyReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeSetTagsRequest(r *http.Request) (
req *SetTagsReq,
rawBody []byte,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = errors.Join(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = errors.Join(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, rawBody, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
defer func() {
_ = r.Body.Close()
}()
if err != nil {
return req, rawBody, close, err
}
// Reset the body to allow for downstream reading.
r.Body = io.NopCloser(bytes.NewBuffer(buf))
if len(buf) == 0 {
return req, rawBody, close, validate.ErrBodyRequired
}
rawBody = append(rawBody, buf...)
d := jx.DecodeBytes(buf)
var request SetTagsReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, rawBody, close, err
}
return &request, rawBody, close, nil
default:
return req, rawBody, close, validate.InvalidContentType(ct)
}
}
-193
View File
@@ -1,193 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"bytes"
"net/http"
"github.com/go-faster/jx"
ht "github.com/ogen-go/ogen/http"
)
func encodeAuthApproveRequest(
req *AuthApproveReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeAuthRegisterRequest(
req *AuthRegisterReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeAuthRejectRequest(
req *AuthRejectReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeCheckPolicyRequest(
req *CheckPolicyReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeCreateApiKeyRequest(
req *CreateApiKeyReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeCreatePreAuthKeyRequest(
req *CreatePreAuthKeyReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeCreateUserRequest(
req *CreateUserReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeDebugCreateNodeRequest(
req *DebugCreateNodeReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeExpireApiKeyRequest(
req *ExpireApiKeyReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeExpirePreAuthKeyRequest(
req *ExpirePreAuthKeyReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeSetApprovedRoutesRequest(
req *SetApprovedRoutesReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeSetPolicyRequest(
req *SetPolicyReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeSetTagsRequest(
req *SetTagsReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
File diff suppressed because it is too large Load Diff
-415
View File
@@ -1,415 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"net/http"
"github.com/go-faster/errors"
"github.com/go-faster/jx"
ht "github.com/ogen-go/ogen/http"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func encodeAuthApproveResponse(response *AuthApproveOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeAuthRegisterResponse(response *AuthRegisterOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeAuthRejectResponse(response *AuthRejectOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeBackfillNodeIPsResponse(response *BackfillNodeIPsOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeCheckPolicyResponse(response *CheckPolicyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeCreateApiKeyResponse(response *CreateApiKeyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeCreatePreAuthKeyResponse(response *CreatePreAuthKeyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeCreateUserResponse(response *CreateUserOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeDebugCreateNodeResponse(response *DebugCreateNodeOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeDeleteApiKeyResponse(response *DeleteApiKeyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeDeleteNodeResponse(response *DeleteNodeOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeDeletePreAuthKeyResponse(response *DeletePreAuthKeyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeDeleteUserResponse(response *DeleteUserOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeExpireApiKeyResponse(response *ExpireApiKeyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeExpireNodeResponse(response *ExpireNodeOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeExpirePreAuthKeyResponse(response *ExpirePreAuthKeyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeGetNodeResponse(response *GetNodeOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeGetPolicyResponse(response *GetPolicyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeHealthResponse(response *HealthOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeListApiKeysResponse(response *ListApiKeysOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeListNodesResponse(response *ListNodesOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeListPreAuthKeysResponse(response *ListPreAuthKeysOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeListUsersResponse(response *ListUsersOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeRegisterNodeResponse(response *RegisterNodeOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeRenameNodeResponse(response *RenameNodeOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeRenameUserResponse(response *RenameUserOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeSetApprovedRoutesResponse(response *SetApprovedRoutesOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeSetPolicyResponse(response *SetPolicyOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeSetTagsResponse(response *SetTagsOK, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeErrorResponse(response *ErrorStatusCode, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/problem+json")
code := response.StatusCode
if code == 0 {
// Set default status code.
code = http.StatusOK
}
w.WriteHeader(code)
if code >= http.StatusInternalServerError {
span.SetStatus(codes.Error, http.StatusText(code))
}
e := new(jx.Encoder)
response.Response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
if code >= http.StatusInternalServerError {
return errors.Wrapf(ht.ErrInternalServerErrorResponse, "code: %d, message: %s", code, http.StatusText(code))
}
return nil
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-119
View File
@@ -1,119 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"context"
"net/http"
"strings"
"github.com/go-faster/errors"
"github.com/ogen-go/ogen/ogenerrors"
)
// SecurityHandler is handler for security parameters.
type SecurityHandler interface {
// HandleBearerAuth handles bearerAuth security.
HandleBearerAuth(ctx context.Context, operationName OperationName, t BearerAuth) (context.Context, error)
}
func findAuthorization(h http.Header, prefix string) (string, bool) {
v, ok := h["Authorization"]
if !ok {
return "", false
}
for _, vv := range v {
scheme, value, ok := strings.Cut(vv, " ")
if !ok || !strings.EqualFold(scheme, prefix) {
continue
}
return value, true
}
return "", false
}
// operationRolesBearerAuth is a private map storing roles per operation.
var operationRolesBearerAuth = map[string][]string{
AuthApproveOperation: []string{},
AuthRegisterOperation: []string{},
AuthRejectOperation: []string{},
BackfillNodeIPsOperation: []string{},
CheckPolicyOperation: []string{},
CreateApiKeyOperation: []string{},
CreatePreAuthKeyOperation: []string{},
CreateUserOperation: []string{},
DebugCreateNodeOperation: []string{},
DeleteApiKeyOperation: []string{},
DeleteNodeOperation: []string{},
DeletePreAuthKeyOperation: []string{},
DeleteUserOperation: []string{},
ExpireApiKeyOperation: []string{},
ExpireNodeOperation: []string{},
ExpirePreAuthKeyOperation: []string{},
GetNodeOperation: []string{},
GetPolicyOperation: []string{},
HealthOperation: []string{},
ListApiKeysOperation: []string{},
ListNodesOperation: []string{},
ListPreAuthKeysOperation: []string{},
ListUsersOperation: []string{},
RegisterNodeOperation: []string{},
RenameNodeOperation: []string{},
RenameUserOperation: []string{},
SetApprovedRoutesOperation: []string{},
SetPolicyOperation: []string{},
SetTagsOperation: []string{},
}
// GetRolesForBearerAuth returns the required roles for the given operation.
//
// This is useful for authorization scenarios where you need to know which roles
// are required for an operation.
//
// Example:
//
// requiredRoles := GetRolesForBearerAuth(AddPetOperation)
//
// Returns nil if the operation has no role requirements or if the operation is unknown.
func GetRolesForBearerAuth(operation string) []string {
roles, ok := operationRolesBearerAuth[operation]
if !ok {
return nil
}
// Return a copy to prevent external modification
result := make([]string, len(roles))
copy(result, roles)
return result
}
func (s *Server) securityBearerAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) {
var t BearerAuth
token, ok := findAuthorization(req.Header, "Bearer")
if !ok {
return ctx, false, nil
}
t.Token = token
t.Roles = operationRolesBearerAuth[operationName]
rctx, err := s.sec.HandleBearerAuth(ctx, operationName, t)
if errors.Is(err, ogenerrors.ErrSkipServerSecurity) {
return nil, false, nil
} else if err != nil {
return nil, false, err
}
return rctx, true, err
}
// SecuritySource is provider of security values (tokens, passwords, etc.).
type SecuritySource interface {
// BearerAuth provides bearerAuth security value.
BearerAuth(ctx context.Context, operationName OperationName) (BearerAuth, error)
}
func (s *Client) securityBearerAuth(ctx context.Context, operationName OperationName, req *http.Request) error {
t, err := s.sec.BearerAuth(ctx, operationName)
if err != nil {
return errors.Wrap(err, "security source \"BearerAuth\"")
}
req.Header.Set("Authorization", "Bearer "+t.Token)
return nil
}
-210
View File
@@ -1,210 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"context"
)
// Handler handles operations described by OpenAPI v3 specification.
type Handler interface {
// AuthApprove implements AuthApprove operation.
//
// Approve a pending auth session.
//
// POST /api/v1/auth/approve
AuthApprove(ctx context.Context, req *AuthApproveReq) error
// AuthRegister implements AuthRegister operation.
//
// Register a node via an auth id (alias of RegisterNode).
//
// POST /api/v1/auth/register
AuthRegister(ctx context.Context, req *AuthRegisterReq) (*AuthRegisterOK, error)
// AuthReject implements AuthReject operation.
//
// Reject a pending auth session.
//
// POST /api/v1/auth/reject
AuthReject(ctx context.Context, req *AuthRejectReq) error
// BackfillNodeIPs implements BackfillNodeIPs operation.
//
// Backfill missing IP addresses for all nodes.
//
// POST /api/v1/node/backfillips
BackfillNodeIPs(ctx context.Context, params BackfillNodeIPsParams) (*BackfillNodeIPsOK, error)
// CheckPolicy implements CheckPolicy operation.
//
// Validate a policy against live users and nodes without storing it.
//
// POST /api/v1/policy/check
CheckPolicy(ctx context.Context, req *CheckPolicyReq) error
// CreateApiKey implements CreateApiKey operation.
//
// Create an API key.
//
// POST /api/v1/apikey
CreateApiKey(ctx context.Context, req *CreateApiKeyReq) (*CreateApiKeyOK, error)
// CreatePreAuthKey implements CreatePreAuthKey operation.
//
// Create a pre-auth key.
//
// POST /api/v1/preauthkey
CreatePreAuthKey(ctx context.Context, req *CreatePreAuthKeyReq) (*CreatePreAuthKeyOK, error)
// CreateUser implements CreateUser operation.
//
// Create a user.
//
// POST /api/v1/user
CreateUser(ctx context.Context, req *CreateUserReq) (*CreateUserOK, error)
// DebugCreateNode implements DebugCreateNode operation.
//
// Create a debug node and cache its registration (testing only).
//
// POST /api/v1/debug/node
DebugCreateNode(ctx context.Context, req *DebugCreateNodeReq) (*DebugCreateNodeOK, error)
// DeleteApiKey implements DeleteApiKey operation.
//
// Delete an API key by prefix (or id).
//
// DELETE /api/v1/apikey/{prefix}
DeleteApiKey(ctx context.Context, params DeleteApiKeyParams) error
// DeleteNode implements DeleteNode operation.
//
// Delete a node.
//
// DELETE /api/v1/node/{node_id}
DeleteNode(ctx context.Context, params DeleteNodeParams) error
// DeletePreAuthKey implements DeletePreAuthKey operation.
//
// Delete a pre-auth key.
//
// DELETE /api/v1/preauthkey
DeletePreAuthKey(ctx context.Context, params DeletePreAuthKeyParams) error
// DeleteUser implements DeleteUser operation.
//
// Delete a user.
//
// DELETE /api/v1/user/{id}
DeleteUser(ctx context.Context, params DeleteUserParams) error
// ExpireApiKey implements ExpireApiKey operation.
//
// Expire an API key by id or prefix.
//
// POST /api/v1/apikey/expire
ExpireApiKey(ctx context.Context, req *ExpireApiKeyReq) error
// ExpireNode implements ExpireNode operation.
//
// Expire a node, or disable its expiry.
//
// POST /api/v1/node/{node_id}/expire
ExpireNode(ctx context.Context, params ExpireNodeParams) (*ExpireNodeOK, error)
// ExpirePreAuthKey implements ExpirePreAuthKey operation.
//
// Expire a pre-auth key.
//
// POST /api/v1/preauthkey/expire
ExpirePreAuthKey(ctx context.Context, req *ExpirePreAuthKeyReq) error
// GetNode implements GetNode operation.
//
// Get a node by id.
//
// GET /api/v1/node/{node_id}
GetNode(ctx context.Context, params GetNodeParams) (*GetNodeOK, error)
// GetPolicy implements GetPolicy operation.
//
// Get the current ACL policy.
//
// GET /api/v1/policy
GetPolicy(ctx context.Context) (*GetPolicyOK, error)
// Health implements Health operation.
//
// Report server health, including database connectivity.
//
// GET /api/v1/health
Health(ctx context.Context) (*HealthOK, error)
// ListApiKeys implements ListApiKeys operation.
//
// List all API keys.
//
// GET /api/v1/apikey
ListApiKeys(ctx context.Context) (*ListApiKeysOK, error)
// ListNodes implements ListNodes operation.
//
// List nodes, optionally filtered by user.
//
// GET /api/v1/node
ListNodes(ctx context.Context, params ListNodesParams) (*ListNodesOK, error)
// ListPreAuthKeys implements ListPreAuthKeys operation.
//
// List all pre-auth keys.
//
// GET /api/v1/preauthkey
ListPreAuthKeys(ctx context.Context) (*ListPreAuthKeysOK, error)
// ListUsers implements ListUsers operation.
//
// List users, optionally filtered by id, name, or email.
//
// GET /api/v1/user
ListUsers(ctx context.Context, params ListUsersParams) (*ListUsersOK, error)
// RegisterNode implements RegisterNode operation.
//
// Register a node to a user using a registration id.
//
// POST /api/v1/node/register
RegisterNode(ctx context.Context, params RegisterNodeParams) (*RegisterNodeOK, error)
// RenameNode implements RenameNode operation.
//
// Rename a node.
//
// POST /api/v1/node/{node_id}/rename/{new_name}
RenameNode(ctx context.Context, params RenameNodeParams) (*RenameNodeOK, error)
// RenameUser implements RenameUser operation.
//
// Rename a user.
//
// POST /api/v1/user/{old_id}/rename/{new_name}
RenameUser(ctx context.Context, params RenameUserParams) (*RenameUserOK, error)
// SetApprovedRoutes implements SetApprovedRoutes operation.
//
// Set the approved subnet routes of a node.
//
// POST /api/v1/node/{node_id}/approve_routes
SetApprovedRoutes(ctx context.Context, req *SetApprovedRoutesReq, params SetApprovedRoutesParams) (*SetApprovedRoutesOK, error)
// SetPolicy implements SetPolicy operation.
//
// Set the ACL policy (database policy mode only).
//
// PUT /api/v1/policy
SetPolicy(ctx context.Context, req *SetPolicyReq) (*SetPolicyOK, error)
// SetTags implements SetTags operation.
//
// Set the tags of a node (converts it to a tagged node).
//
// POST /api/v1/node/{node_id}/tags
SetTags(ctx context.Context, req *SetTagsReq, params SetTagsParams) (*SetTagsOK, error)
// NewError creates *ErrorStatusCode from error returned by handler.
//
// Used for common default response.
NewError(ctx context.Context, err error) *ErrorStatusCode
}
// Server implements http server based on OpenAPI v3 specification and
// calls Handler to handle requests.
type Server struct {
h Handler
sec SecurityHandler
baseServer
}
// NewServer creates new Server.
func NewServer(h Handler, sec SecurityHandler, opts ...ServerOption) (*Server, error) {
s, err := newServerConfig(opts...).baseServer()
if err != nil {
return nil, err
}
return &Server{
h: h,
sec: sec,
baseServer: s,
}, nil
}
-283
View File
@@ -1,283 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"context"
ht "github.com/ogen-go/ogen/http"
)
// UnimplementedHandler is no-op Handler which returns http.ErrNotImplemented.
type UnimplementedHandler struct{}
var _ Handler = UnimplementedHandler{}
// AuthApprove implements AuthApprove operation.
//
// Approve a pending auth session.
//
// POST /api/v1/auth/approve
func (UnimplementedHandler) AuthApprove(ctx context.Context, req *AuthApproveReq) error {
return ht.ErrNotImplemented
}
// AuthRegister implements AuthRegister operation.
//
// Register a node via an auth id (alias of RegisterNode).
//
// POST /api/v1/auth/register
func (UnimplementedHandler) AuthRegister(ctx context.Context, req *AuthRegisterReq) (r *AuthRegisterOK, _ error) {
return r, ht.ErrNotImplemented
}
// AuthReject implements AuthReject operation.
//
// Reject a pending auth session.
//
// POST /api/v1/auth/reject
func (UnimplementedHandler) AuthReject(ctx context.Context, req *AuthRejectReq) error {
return ht.ErrNotImplemented
}
// BackfillNodeIPs implements BackfillNodeIPs operation.
//
// Backfill missing IP addresses for all nodes.
//
// POST /api/v1/node/backfillips
func (UnimplementedHandler) BackfillNodeIPs(ctx context.Context, params BackfillNodeIPsParams) (r *BackfillNodeIPsOK, _ error) {
return r, ht.ErrNotImplemented
}
// CheckPolicy implements CheckPolicy operation.
//
// Validate a policy against live users and nodes without storing it.
//
// POST /api/v1/policy/check
func (UnimplementedHandler) CheckPolicy(ctx context.Context, req *CheckPolicyReq) error {
return ht.ErrNotImplemented
}
// CreateApiKey implements CreateApiKey operation.
//
// Create an API key.
//
// POST /api/v1/apikey
func (UnimplementedHandler) CreateApiKey(ctx context.Context, req *CreateApiKeyReq) (r *CreateApiKeyOK, _ error) {
return r, ht.ErrNotImplemented
}
// CreatePreAuthKey implements CreatePreAuthKey operation.
//
// Create a pre-auth key.
//
// POST /api/v1/preauthkey
func (UnimplementedHandler) CreatePreAuthKey(ctx context.Context, req *CreatePreAuthKeyReq) (r *CreatePreAuthKeyOK, _ error) {
return r, ht.ErrNotImplemented
}
// CreateUser implements CreateUser operation.
//
// Create a user.
//
// POST /api/v1/user
func (UnimplementedHandler) CreateUser(ctx context.Context, req *CreateUserReq) (r *CreateUserOK, _ error) {
return r, ht.ErrNotImplemented
}
// DebugCreateNode implements DebugCreateNode operation.
//
// Create a debug node and cache its registration (testing only).
//
// POST /api/v1/debug/node
func (UnimplementedHandler) DebugCreateNode(ctx context.Context, req *DebugCreateNodeReq) (r *DebugCreateNodeOK, _ error) {
return r, ht.ErrNotImplemented
}
// DeleteApiKey implements DeleteApiKey operation.
//
// Delete an API key by prefix (or id).
//
// DELETE /api/v1/apikey/{prefix}
func (UnimplementedHandler) DeleteApiKey(ctx context.Context, params DeleteApiKeyParams) error {
return ht.ErrNotImplemented
}
// DeleteNode implements DeleteNode operation.
//
// Delete a node.
//
// DELETE /api/v1/node/{node_id}
func (UnimplementedHandler) DeleteNode(ctx context.Context, params DeleteNodeParams) error {
return ht.ErrNotImplemented
}
// DeletePreAuthKey implements DeletePreAuthKey operation.
//
// Delete a pre-auth key.
//
// DELETE /api/v1/preauthkey
func (UnimplementedHandler) DeletePreAuthKey(ctx context.Context, params DeletePreAuthKeyParams) error {
return ht.ErrNotImplemented
}
// DeleteUser implements DeleteUser operation.
//
// Delete a user.
//
// DELETE /api/v1/user/{id}
func (UnimplementedHandler) DeleteUser(ctx context.Context, params DeleteUserParams) error {
return ht.ErrNotImplemented
}
// ExpireApiKey implements ExpireApiKey operation.
//
// Expire an API key by id or prefix.
//
// POST /api/v1/apikey/expire
func (UnimplementedHandler) ExpireApiKey(ctx context.Context, req *ExpireApiKeyReq) error {
return ht.ErrNotImplemented
}
// ExpireNode implements ExpireNode operation.
//
// Expire a node, or disable its expiry.
//
// POST /api/v1/node/{node_id}/expire
func (UnimplementedHandler) ExpireNode(ctx context.Context, params ExpireNodeParams) (r *ExpireNodeOK, _ error) {
return r, ht.ErrNotImplemented
}
// ExpirePreAuthKey implements ExpirePreAuthKey operation.
//
// Expire a pre-auth key.
//
// POST /api/v1/preauthkey/expire
func (UnimplementedHandler) ExpirePreAuthKey(ctx context.Context, req *ExpirePreAuthKeyReq) error {
return ht.ErrNotImplemented
}
// GetNode implements GetNode operation.
//
// Get a node by id.
//
// GET /api/v1/node/{node_id}
func (UnimplementedHandler) GetNode(ctx context.Context, params GetNodeParams) (r *GetNodeOK, _ error) {
return r, ht.ErrNotImplemented
}
// GetPolicy implements GetPolicy operation.
//
// Get the current ACL policy.
//
// GET /api/v1/policy
func (UnimplementedHandler) GetPolicy(ctx context.Context) (r *GetPolicyOK, _ error) {
return r, ht.ErrNotImplemented
}
// Health implements Health operation.
//
// Report server health, including database connectivity.
//
// GET /api/v1/health
func (UnimplementedHandler) Health(ctx context.Context) (r *HealthOK, _ error) {
return r, ht.ErrNotImplemented
}
// ListApiKeys implements ListApiKeys operation.
//
// List all API keys.
//
// GET /api/v1/apikey
func (UnimplementedHandler) ListApiKeys(ctx context.Context) (r *ListApiKeysOK, _ error) {
return r, ht.ErrNotImplemented
}
// ListNodes implements ListNodes operation.
//
// List nodes, optionally filtered by user.
//
// GET /api/v1/node
func (UnimplementedHandler) ListNodes(ctx context.Context, params ListNodesParams) (r *ListNodesOK, _ error) {
return r, ht.ErrNotImplemented
}
// ListPreAuthKeys implements ListPreAuthKeys operation.
//
// List all pre-auth keys.
//
// GET /api/v1/preauthkey
func (UnimplementedHandler) ListPreAuthKeys(ctx context.Context) (r *ListPreAuthKeysOK, _ error) {
return r, ht.ErrNotImplemented
}
// ListUsers implements ListUsers operation.
//
// List users, optionally filtered by id, name, or email.
//
// GET /api/v1/user
func (UnimplementedHandler) ListUsers(ctx context.Context, params ListUsersParams) (r *ListUsersOK, _ error) {
return r, ht.ErrNotImplemented
}
// RegisterNode implements RegisterNode operation.
//
// Register a node to a user using a registration id.
//
// POST /api/v1/node/register
func (UnimplementedHandler) RegisterNode(ctx context.Context, params RegisterNodeParams) (r *RegisterNodeOK, _ error) {
return r, ht.ErrNotImplemented
}
// RenameNode implements RenameNode operation.
//
// Rename a node.
//
// POST /api/v1/node/{node_id}/rename/{new_name}
func (UnimplementedHandler) RenameNode(ctx context.Context, params RenameNodeParams) (r *RenameNodeOK, _ error) {
return r, ht.ErrNotImplemented
}
// RenameUser implements RenameUser operation.
//
// Rename a user.
//
// POST /api/v1/user/{old_id}/rename/{new_name}
func (UnimplementedHandler) RenameUser(ctx context.Context, params RenameUserParams) (r *RenameUserOK, _ error) {
return r, ht.ErrNotImplemented
}
// SetApprovedRoutes implements SetApprovedRoutes operation.
//
// Set the approved subnet routes of a node.
//
// POST /api/v1/node/{node_id}/approve_routes
func (UnimplementedHandler) SetApprovedRoutes(ctx context.Context, req *SetApprovedRoutesReq, params SetApprovedRoutesParams) (r *SetApprovedRoutesOK, _ error) {
return r, ht.ErrNotImplemented
}
// SetPolicy implements SetPolicy operation.
//
// Set the ACL policy (database policy mode only).
//
// PUT /api/v1/policy
func (UnimplementedHandler) SetPolicy(ctx context.Context, req *SetPolicyReq) (r *SetPolicyOK, _ error) {
return r, ht.ErrNotImplemented
}
// SetTags implements SetTags operation.
//
// Set the tags of a node (converts it to a tagged node).
//
// POST /api/v1/node/{node_id}/tags
func (UnimplementedHandler) SetTags(ctx context.Context, req *SetTagsReq, params SetTagsParams) (r *SetTagsOK, _ error) {
return r, ht.ErrNotImplemented
}
// NewError creates *ErrorStatusCode from error returned by handler.
//
// Used for common default response.
func (UnimplementedHandler) NewError(ctx context.Context, err error) (r *ErrorStatusCode) {
r = new(ErrorStatusCode)
return r
}
-332
View File
@@ -1,332 +0,0 @@
// Code generated by ogen, DO NOT EDIT.
package apiv1
import (
"fmt"
"github.com/go-faster/errors"
"github.com/ogen-go/ogen/validate"
)
func (s *AuthRegisterOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.Node.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "node",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *DebugCreateNodeOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.Node.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "node",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *ExpireNodeOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.Node.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "node",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *GetNodeOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.Node.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "node",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *ListNodesOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
var failures []validate.FieldError
for i, elem := range s.Nodes {
if err := func() error {
if err := elem.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: fmt.Sprintf("[%d]", i),
Error: err,
})
}
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "nodes",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *Node) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.RegisterMethod.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "registerMethod",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s RegisterMethod) Validate() error {
switch s {
case "REGISTER_METHOD_UNSPECIFIED":
return nil
case "REGISTER_METHOD_AUTH_KEY":
return nil
case "REGISTER_METHOD_CLI":
return nil
case "REGISTER_METHOD_OIDC":
return nil
default:
return errors.Errorf("invalid value: %v", s)
}
}
func (s *RegisterNodeOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.Node.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "node",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *RenameNodeOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.Node.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "node",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *SetApprovedRoutesOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.Node.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "node",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *SetTagsOK) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if value, ok := s.Node.Get(); ok {
if err := func() error {
if err := value.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return err
}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "node",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
+537
View File
@@ -0,0 +1,537 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: headscale/v1/apikey.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ApiKey struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expiration,proto3" json:"expiration,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
LastSeen *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ApiKey) Reset() {
*x = ApiKey{}
mi := &file_headscale_v1_apikey_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ApiKey) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ApiKey) ProtoMessage() {}
func (x *ApiKey) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ApiKey.ProtoReflect.Descriptor instead.
func (*ApiKey) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{0}
}
func (x *ApiKey) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
func (x *ApiKey) GetPrefix() string {
if x != nil {
return x.Prefix
}
return ""
}
func (x *ApiKey) GetExpiration() *timestamppb.Timestamp {
if x != nil {
return x.Expiration
}
return nil
}
func (x *ApiKey) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *ApiKey) GetLastSeen() *timestamppb.Timestamp {
if x != nil {
return x.LastSeen
}
return nil
}
type CreateApiKeyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=expiration,proto3" json:"expiration,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateApiKeyRequest) Reset() {
*x = CreateApiKeyRequest{}
mi := &file_headscale_v1_apikey_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateApiKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateApiKeyRequest) ProtoMessage() {}
func (x *CreateApiKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateApiKeyRequest.ProtoReflect.Descriptor instead.
func (*CreateApiKeyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{1}
}
func (x *CreateApiKeyRequest) GetExpiration() *timestamppb.Timestamp {
if x != nil {
return x.Expiration
}
return nil
}
type CreateApiKeyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ApiKey string `protobuf:"bytes,1,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateApiKeyResponse) Reset() {
*x = CreateApiKeyResponse{}
mi := &file_headscale_v1_apikey_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateApiKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateApiKeyResponse) ProtoMessage() {}
func (x *CreateApiKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateApiKeyResponse.ProtoReflect.Descriptor instead.
func (*CreateApiKeyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{2}
}
func (x *CreateApiKeyResponse) GetApiKey() string {
if x != nil {
return x.ApiKey
}
return ""
}
type ExpireApiKeyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"`
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ExpireApiKeyRequest) Reset() {
*x = ExpireApiKeyRequest{}
mi := &file_headscale_v1_apikey_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ExpireApiKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExpireApiKeyRequest) ProtoMessage() {}
func (x *ExpireApiKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExpireApiKeyRequest.ProtoReflect.Descriptor instead.
func (*ExpireApiKeyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{3}
}
func (x *ExpireApiKeyRequest) GetPrefix() string {
if x != nil {
return x.Prefix
}
return ""
}
func (x *ExpireApiKeyRequest) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
type ExpireApiKeyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ExpireApiKeyResponse) Reset() {
*x = ExpireApiKeyResponse{}
mi := &file_headscale_v1_apikey_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ExpireApiKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExpireApiKeyResponse) ProtoMessage() {}
func (x *ExpireApiKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExpireApiKeyResponse.ProtoReflect.Descriptor instead.
func (*ExpireApiKeyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{4}
}
type ListApiKeysRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListApiKeysRequest) Reset() {
*x = ListApiKeysRequest{}
mi := &file_headscale_v1_apikey_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListApiKeysRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListApiKeysRequest) ProtoMessage() {}
func (x *ListApiKeysRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListApiKeysRequest.ProtoReflect.Descriptor instead.
func (*ListApiKeysRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{5}
}
type ListApiKeysResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ApiKeys []*ApiKey `protobuf:"bytes,1,rep,name=api_keys,json=apiKeys,proto3" json:"api_keys,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListApiKeysResponse) Reset() {
*x = ListApiKeysResponse{}
mi := &file_headscale_v1_apikey_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListApiKeysResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListApiKeysResponse) ProtoMessage() {}
func (x *ListApiKeysResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListApiKeysResponse.ProtoReflect.Descriptor instead.
func (*ListApiKeysResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{6}
}
func (x *ListApiKeysResponse) GetApiKeys() []*ApiKey {
if x != nil {
return x.ApiKeys
}
return nil
}
type DeleteApiKeyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"`
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteApiKeyRequest) Reset() {
*x = DeleteApiKeyRequest{}
mi := &file_headscale_v1_apikey_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteApiKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteApiKeyRequest) ProtoMessage() {}
func (x *DeleteApiKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteApiKeyRequest.ProtoReflect.Descriptor instead.
func (*DeleteApiKeyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{7}
}
func (x *DeleteApiKeyRequest) GetPrefix() string {
if x != nil {
return x.Prefix
}
return ""
}
func (x *DeleteApiKeyRequest) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
type DeleteApiKeyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteApiKeyResponse) Reset() {
*x = DeleteApiKeyResponse{}
mi := &file_headscale_v1_apikey_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteApiKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteApiKeyResponse) ProtoMessage() {}
func (x *DeleteApiKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_apikey_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteApiKeyResponse.ProtoReflect.Descriptor instead.
func (*DeleteApiKeyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_apikey_proto_rawDescGZIP(), []int{8}
}
var File_headscale_v1_apikey_proto protoreflect.FileDescriptor
const file_headscale_v1_apikey_proto_rawDesc = "" +
"\n" +
"\x19headscale/v1/apikey.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe0\x01\n" +
"\x06ApiKey\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\tR\x06prefix\x12:\n" +
"\n" +
"expiration\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
"expiration\x129\n" +
"\n" +
"created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x127\n" +
"\tlast_seen\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\"Q\n" +
"\x13CreateApiKeyRequest\x12:\n" +
"\n" +
"expiration\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
"expiration\"/\n" +
"\x14CreateApiKeyResponse\x12\x17\n" +
"\aapi_key\x18\x01 \x01(\tR\x06apiKey\"=\n" +
"\x13ExpireApiKeyRequest\x12\x16\n" +
"\x06prefix\x18\x01 \x01(\tR\x06prefix\x12\x0e\n" +
"\x02id\x18\x02 \x01(\x04R\x02id\"\x16\n" +
"\x14ExpireApiKeyResponse\"\x14\n" +
"\x12ListApiKeysRequest\"F\n" +
"\x13ListApiKeysResponse\x12/\n" +
"\bapi_keys\x18\x01 \x03(\v2\x14.headscale.v1.ApiKeyR\aapiKeys\"=\n" +
"\x13DeleteApiKeyRequest\x12\x16\n" +
"\x06prefix\x18\x01 \x01(\tR\x06prefix\x12\x0e\n" +
"\x02id\x18\x02 \x01(\x04R\x02id\"\x16\n" +
"\x14DeleteApiKeyResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
var (
file_headscale_v1_apikey_proto_rawDescOnce sync.Once
file_headscale_v1_apikey_proto_rawDescData []byte
)
func file_headscale_v1_apikey_proto_rawDescGZIP() []byte {
file_headscale_v1_apikey_proto_rawDescOnce.Do(func() {
file_headscale_v1_apikey_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_apikey_proto_rawDesc), len(file_headscale_v1_apikey_proto_rawDesc)))
})
return file_headscale_v1_apikey_proto_rawDescData
}
var file_headscale_v1_apikey_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_headscale_v1_apikey_proto_goTypes = []any{
(*ApiKey)(nil), // 0: headscale.v1.ApiKey
(*CreateApiKeyRequest)(nil), // 1: headscale.v1.CreateApiKeyRequest
(*CreateApiKeyResponse)(nil), // 2: headscale.v1.CreateApiKeyResponse
(*ExpireApiKeyRequest)(nil), // 3: headscale.v1.ExpireApiKeyRequest
(*ExpireApiKeyResponse)(nil), // 4: headscale.v1.ExpireApiKeyResponse
(*ListApiKeysRequest)(nil), // 5: headscale.v1.ListApiKeysRequest
(*ListApiKeysResponse)(nil), // 6: headscale.v1.ListApiKeysResponse
(*DeleteApiKeyRequest)(nil), // 7: headscale.v1.DeleteApiKeyRequest
(*DeleteApiKeyResponse)(nil), // 8: headscale.v1.DeleteApiKeyResponse
(*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
}
var file_headscale_v1_apikey_proto_depIdxs = []int32{
9, // 0: headscale.v1.ApiKey.expiration:type_name -> google.protobuf.Timestamp
9, // 1: headscale.v1.ApiKey.created_at:type_name -> google.protobuf.Timestamp
9, // 2: headscale.v1.ApiKey.last_seen:type_name -> google.protobuf.Timestamp
9, // 3: headscale.v1.CreateApiKeyRequest.expiration:type_name -> google.protobuf.Timestamp
0, // 4: headscale.v1.ListApiKeysResponse.api_keys:type_name -> headscale.v1.ApiKey
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_headscale_v1_apikey_proto_init() }
func file_headscale_v1_apikey_proto_init() {
if File_headscale_v1_apikey_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_apikey_proto_rawDesc), len(file_headscale_v1_apikey_proto_rawDesc)),
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_apikey_proto_goTypes,
DependencyIndexes: file_headscale_v1_apikey_proto_depIdxs,
MessageInfos: file_headscale_v1_apikey_proto_msgTypes,
}.Build()
File_headscale_v1_apikey_proto = out.File
file_headscale_v1_apikey_proto_goTypes = nil
file_headscale_v1_apikey_proto_depIdxs = nil
}
+351
View File
@@ -0,0 +1,351 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: headscale/v1/auth.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type AuthRegisterRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
AuthId string `protobuf:"bytes,2,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthRegisterRequest) Reset() {
*x = AuthRegisterRequest{}
mi := &file_headscale_v1_auth_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AuthRegisterRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthRegisterRequest) ProtoMessage() {}
func (x *AuthRegisterRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_auth_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AuthRegisterRequest.ProtoReflect.Descriptor instead.
func (*AuthRegisterRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{0}
}
func (x *AuthRegisterRequest) GetUser() string {
if x != nil {
return x.User
}
return ""
}
func (x *AuthRegisterRequest) GetAuthId() string {
if x != nil {
return x.AuthId
}
return ""
}
type AuthRegisterResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Node *Node `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthRegisterResponse) Reset() {
*x = AuthRegisterResponse{}
mi := &file_headscale_v1_auth_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AuthRegisterResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthRegisterResponse) ProtoMessage() {}
func (x *AuthRegisterResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_auth_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AuthRegisterResponse.ProtoReflect.Descriptor instead.
func (*AuthRegisterResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{1}
}
func (x *AuthRegisterResponse) GetNode() *Node {
if x != nil {
return x.Node
}
return nil
}
type AuthApproveRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
AuthId string `protobuf:"bytes,1,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthApproveRequest) Reset() {
*x = AuthApproveRequest{}
mi := &file_headscale_v1_auth_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AuthApproveRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthApproveRequest) ProtoMessage() {}
func (x *AuthApproveRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_auth_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AuthApproveRequest.ProtoReflect.Descriptor instead.
func (*AuthApproveRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{2}
}
func (x *AuthApproveRequest) GetAuthId() string {
if x != nil {
return x.AuthId
}
return ""
}
type AuthApproveResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthApproveResponse) Reset() {
*x = AuthApproveResponse{}
mi := &file_headscale_v1_auth_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AuthApproveResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthApproveResponse) ProtoMessage() {}
func (x *AuthApproveResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_auth_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AuthApproveResponse.ProtoReflect.Descriptor instead.
func (*AuthApproveResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{3}
}
type AuthRejectRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
AuthId string `protobuf:"bytes,1,opt,name=auth_id,json=authId,proto3" json:"auth_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthRejectRequest) Reset() {
*x = AuthRejectRequest{}
mi := &file_headscale_v1_auth_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AuthRejectRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthRejectRequest) ProtoMessage() {}
func (x *AuthRejectRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_auth_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AuthRejectRequest.ProtoReflect.Descriptor instead.
func (*AuthRejectRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{4}
}
func (x *AuthRejectRequest) GetAuthId() string {
if x != nil {
return x.AuthId
}
return ""
}
type AuthRejectResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthRejectResponse) Reset() {
*x = AuthRejectResponse{}
mi := &file_headscale_v1_auth_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AuthRejectResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthRejectResponse) ProtoMessage() {}
func (x *AuthRejectResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_auth_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AuthRejectResponse.ProtoReflect.Descriptor instead.
func (*AuthRejectResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_auth_proto_rawDescGZIP(), []int{5}
}
var File_headscale_v1_auth_proto protoreflect.FileDescriptor
const file_headscale_v1_auth_proto_rawDesc = "" +
"\n" +
"\x17headscale/v1/auth.proto\x12\fheadscale.v1\x1a\x17headscale/v1/node.proto\"B\n" +
"\x13AuthRegisterRequest\x12\x12\n" +
"\x04user\x18\x01 \x01(\tR\x04user\x12\x17\n" +
"\aauth_id\x18\x02 \x01(\tR\x06authId\">\n" +
"\x14AuthRegisterResponse\x12&\n" +
"\x04node\x18\x01 \x01(\v2\x12.headscale.v1.NodeR\x04node\"-\n" +
"\x12AuthApproveRequest\x12\x17\n" +
"\aauth_id\x18\x01 \x01(\tR\x06authId\"\x15\n" +
"\x13AuthApproveResponse\",\n" +
"\x11AuthRejectRequest\x12\x17\n" +
"\aauth_id\x18\x01 \x01(\tR\x06authId\"\x14\n" +
"\x12AuthRejectResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
var (
file_headscale_v1_auth_proto_rawDescOnce sync.Once
file_headscale_v1_auth_proto_rawDescData []byte
)
func file_headscale_v1_auth_proto_rawDescGZIP() []byte {
file_headscale_v1_auth_proto_rawDescOnce.Do(func() {
file_headscale_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_auth_proto_rawDesc), len(file_headscale_v1_auth_proto_rawDesc)))
})
return file_headscale_v1_auth_proto_rawDescData
}
var file_headscale_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_headscale_v1_auth_proto_goTypes = []any{
(*AuthRegisterRequest)(nil), // 0: headscale.v1.AuthRegisterRequest
(*AuthRegisterResponse)(nil), // 1: headscale.v1.AuthRegisterResponse
(*AuthApproveRequest)(nil), // 2: headscale.v1.AuthApproveRequest
(*AuthApproveResponse)(nil), // 3: headscale.v1.AuthApproveResponse
(*AuthRejectRequest)(nil), // 4: headscale.v1.AuthRejectRequest
(*AuthRejectResponse)(nil), // 5: headscale.v1.AuthRejectResponse
(*Node)(nil), // 6: headscale.v1.Node
}
var file_headscale_v1_auth_proto_depIdxs = []int32{
6, // 0: headscale.v1.AuthRegisterResponse.node:type_name -> headscale.v1.Node
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_headscale_v1_auth_proto_init() }
func file_headscale_v1_auth_proto_init() {
if File_headscale_v1_auth_proto != nil {
return
}
file_headscale_v1_node_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_auth_proto_rawDesc), len(file_headscale_v1_auth_proto_rawDesc)),
NumEnums: 0,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_auth_proto_goTypes,
DependencyIndexes: file_headscale_v1_auth_proto_depIdxs,
MessageInfos: file_headscale_v1_auth_proto_msgTypes,
}.Build()
File_headscale_v1_auth_proto = out.File
file_headscale_v1_auth_proto_goTypes = nil
file_headscale_v1_auth_proto_depIdxs = nil
}
+890
View File
@@ -0,0 +1,890 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: headscale/v1/device.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Latency struct {
state protoimpl.MessageState `protogen:"open.v1"`
LatencyMs float32 `protobuf:"fixed32,1,opt,name=latency_ms,json=latencyMs,proto3" json:"latency_ms,omitempty"`
Preferred bool `protobuf:"varint,2,opt,name=preferred,proto3" json:"preferred,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Latency) Reset() {
*x = Latency{}
mi := &file_headscale_v1_device_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Latency) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Latency) ProtoMessage() {}
func (x *Latency) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Latency.ProtoReflect.Descriptor instead.
func (*Latency) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{0}
}
func (x *Latency) GetLatencyMs() float32 {
if x != nil {
return x.LatencyMs
}
return 0
}
func (x *Latency) GetPreferred() bool {
if x != nil {
return x.Preferred
}
return false
}
type ClientSupports struct {
state protoimpl.MessageState `protogen:"open.v1"`
HairPinning bool `protobuf:"varint,1,opt,name=hair_pinning,json=hairPinning,proto3" json:"hair_pinning,omitempty"`
Ipv6 bool `protobuf:"varint,2,opt,name=ipv6,proto3" json:"ipv6,omitempty"`
Pcp bool `protobuf:"varint,3,opt,name=pcp,proto3" json:"pcp,omitempty"`
Pmp bool `protobuf:"varint,4,opt,name=pmp,proto3" json:"pmp,omitempty"`
Udp bool `protobuf:"varint,5,opt,name=udp,proto3" json:"udp,omitempty"`
Upnp bool `protobuf:"varint,6,opt,name=upnp,proto3" json:"upnp,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClientSupports) Reset() {
*x = ClientSupports{}
mi := &file_headscale_v1_device_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ClientSupports) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ClientSupports) ProtoMessage() {}
func (x *ClientSupports) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ClientSupports.ProtoReflect.Descriptor instead.
func (*ClientSupports) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{1}
}
func (x *ClientSupports) GetHairPinning() bool {
if x != nil {
return x.HairPinning
}
return false
}
func (x *ClientSupports) GetIpv6() bool {
if x != nil {
return x.Ipv6
}
return false
}
func (x *ClientSupports) GetPcp() bool {
if x != nil {
return x.Pcp
}
return false
}
func (x *ClientSupports) GetPmp() bool {
if x != nil {
return x.Pmp
}
return false
}
func (x *ClientSupports) GetUdp() bool {
if x != nil {
return x.Udp
}
return false
}
func (x *ClientSupports) GetUpnp() bool {
if x != nil {
return x.Upnp
}
return false
}
type ClientConnectivity struct {
state protoimpl.MessageState `protogen:"open.v1"`
Endpoints []string `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
Derp string `protobuf:"bytes,2,opt,name=derp,proto3" json:"derp,omitempty"`
MappingVariesByDestIp bool `protobuf:"varint,3,opt,name=mapping_varies_by_dest_ip,json=mappingVariesByDestIp,proto3" json:"mapping_varies_by_dest_ip,omitempty"`
Latency map[string]*Latency `protobuf:"bytes,4,rep,name=latency,proto3" json:"latency,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
ClientSupports *ClientSupports `protobuf:"bytes,5,opt,name=client_supports,json=clientSupports,proto3" json:"client_supports,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClientConnectivity) Reset() {
*x = ClientConnectivity{}
mi := &file_headscale_v1_device_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ClientConnectivity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ClientConnectivity) ProtoMessage() {}
func (x *ClientConnectivity) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ClientConnectivity.ProtoReflect.Descriptor instead.
func (*ClientConnectivity) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{2}
}
func (x *ClientConnectivity) GetEndpoints() []string {
if x != nil {
return x.Endpoints
}
return nil
}
func (x *ClientConnectivity) GetDerp() string {
if x != nil {
return x.Derp
}
return ""
}
func (x *ClientConnectivity) GetMappingVariesByDestIp() bool {
if x != nil {
return x.MappingVariesByDestIp
}
return false
}
func (x *ClientConnectivity) GetLatency() map[string]*Latency {
if x != nil {
return x.Latency
}
return nil
}
func (x *ClientConnectivity) GetClientSupports() *ClientSupports {
if x != nil {
return x.ClientSupports
}
return nil
}
type GetDeviceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetDeviceRequest) Reset() {
*x = GetDeviceRequest{}
mi := &file_headscale_v1_device_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetDeviceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetDeviceRequest) ProtoMessage() {}
func (x *GetDeviceRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetDeviceRequest.ProtoReflect.Descriptor instead.
func (*GetDeviceRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{3}
}
func (x *GetDeviceRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type GetDeviceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Addresses []string `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"`
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
User string `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
Hostname string `protobuf:"bytes,5,opt,name=hostname,proto3" json:"hostname,omitempty"`
ClientVersion string `protobuf:"bytes,6,opt,name=client_version,json=clientVersion,proto3" json:"client_version,omitempty"`
UpdateAvailable bool `protobuf:"varint,7,opt,name=update_available,json=updateAvailable,proto3" json:"update_available,omitempty"`
Os string `protobuf:"bytes,8,opt,name=os,proto3" json:"os,omitempty"`
Created *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created,proto3" json:"created,omitempty"`
LastSeen *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
KeyExpiryDisabled bool `protobuf:"varint,11,opt,name=key_expiry_disabled,json=keyExpiryDisabled,proto3" json:"key_expiry_disabled,omitempty"`
Expires *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=expires,proto3" json:"expires,omitempty"`
Authorized bool `protobuf:"varint,13,opt,name=authorized,proto3" json:"authorized,omitempty"`
IsExternal bool `protobuf:"varint,14,opt,name=is_external,json=isExternal,proto3" json:"is_external,omitempty"`
MachineKey string `protobuf:"bytes,15,opt,name=machine_key,json=machineKey,proto3" json:"machine_key,omitempty"`
NodeKey string `protobuf:"bytes,16,opt,name=node_key,json=nodeKey,proto3" json:"node_key,omitempty"`
BlocksIncomingConnections bool `protobuf:"varint,17,opt,name=blocks_incoming_connections,json=blocksIncomingConnections,proto3" json:"blocks_incoming_connections,omitempty"`
EnabledRoutes []string `protobuf:"bytes,18,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
AdvertisedRoutes []string `protobuf:"bytes,19,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
ClientConnectivity *ClientConnectivity `protobuf:"bytes,20,opt,name=client_connectivity,json=clientConnectivity,proto3" json:"client_connectivity,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetDeviceResponse) Reset() {
*x = GetDeviceResponse{}
mi := &file_headscale_v1_device_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetDeviceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetDeviceResponse) ProtoMessage() {}
func (x *GetDeviceResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetDeviceResponse.ProtoReflect.Descriptor instead.
func (*GetDeviceResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{4}
}
func (x *GetDeviceResponse) GetAddresses() []string {
if x != nil {
return x.Addresses
}
return nil
}
func (x *GetDeviceResponse) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *GetDeviceResponse) GetUser() string {
if x != nil {
return x.User
}
return ""
}
func (x *GetDeviceResponse) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *GetDeviceResponse) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
func (x *GetDeviceResponse) GetClientVersion() string {
if x != nil {
return x.ClientVersion
}
return ""
}
func (x *GetDeviceResponse) GetUpdateAvailable() bool {
if x != nil {
return x.UpdateAvailable
}
return false
}
func (x *GetDeviceResponse) GetOs() string {
if x != nil {
return x.Os
}
return ""
}
func (x *GetDeviceResponse) GetCreated() *timestamppb.Timestamp {
if x != nil {
return x.Created
}
return nil
}
func (x *GetDeviceResponse) GetLastSeen() *timestamppb.Timestamp {
if x != nil {
return x.LastSeen
}
return nil
}
func (x *GetDeviceResponse) GetKeyExpiryDisabled() bool {
if x != nil {
return x.KeyExpiryDisabled
}
return false
}
func (x *GetDeviceResponse) GetExpires() *timestamppb.Timestamp {
if x != nil {
return x.Expires
}
return nil
}
func (x *GetDeviceResponse) GetAuthorized() bool {
if x != nil {
return x.Authorized
}
return false
}
func (x *GetDeviceResponse) GetIsExternal() bool {
if x != nil {
return x.IsExternal
}
return false
}
func (x *GetDeviceResponse) GetMachineKey() string {
if x != nil {
return x.MachineKey
}
return ""
}
func (x *GetDeviceResponse) GetNodeKey() string {
if x != nil {
return x.NodeKey
}
return ""
}
func (x *GetDeviceResponse) GetBlocksIncomingConnections() bool {
if x != nil {
return x.BlocksIncomingConnections
}
return false
}
func (x *GetDeviceResponse) GetEnabledRoutes() []string {
if x != nil {
return x.EnabledRoutes
}
return nil
}
func (x *GetDeviceResponse) GetAdvertisedRoutes() []string {
if x != nil {
return x.AdvertisedRoutes
}
return nil
}
func (x *GetDeviceResponse) GetClientConnectivity() *ClientConnectivity {
if x != nil {
return x.ClientConnectivity
}
return nil
}
type DeleteDeviceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteDeviceRequest) Reset() {
*x = DeleteDeviceRequest{}
mi := &file_headscale_v1_device_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteDeviceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteDeviceRequest) ProtoMessage() {}
func (x *DeleteDeviceRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteDeviceRequest.ProtoReflect.Descriptor instead.
func (*DeleteDeviceRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{5}
}
func (x *DeleteDeviceRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type DeleteDeviceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteDeviceResponse) Reset() {
*x = DeleteDeviceResponse{}
mi := &file_headscale_v1_device_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteDeviceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteDeviceResponse) ProtoMessage() {}
func (x *DeleteDeviceResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteDeviceResponse.ProtoReflect.Descriptor instead.
func (*DeleteDeviceResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{6}
}
type GetDeviceRoutesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetDeviceRoutesRequest) Reset() {
*x = GetDeviceRoutesRequest{}
mi := &file_headscale_v1_device_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetDeviceRoutesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetDeviceRoutesRequest) ProtoMessage() {}
func (x *GetDeviceRoutesRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetDeviceRoutesRequest.ProtoReflect.Descriptor instead.
func (*GetDeviceRoutesRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{7}
}
func (x *GetDeviceRoutesRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type GetDeviceRoutesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
EnabledRoutes []string `protobuf:"bytes,1,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
AdvertisedRoutes []string `protobuf:"bytes,2,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetDeviceRoutesResponse) Reset() {
*x = GetDeviceRoutesResponse{}
mi := &file_headscale_v1_device_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetDeviceRoutesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetDeviceRoutesResponse) ProtoMessage() {}
func (x *GetDeviceRoutesResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetDeviceRoutesResponse.ProtoReflect.Descriptor instead.
func (*GetDeviceRoutesResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{8}
}
func (x *GetDeviceRoutesResponse) GetEnabledRoutes() []string {
if x != nil {
return x.EnabledRoutes
}
return nil
}
func (x *GetDeviceRoutesResponse) GetAdvertisedRoutes() []string {
if x != nil {
return x.AdvertisedRoutes
}
return nil
}
type EnableDeviceRoutesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Routes []string `protobuf:"bytes,2,rep,name=routes,proto3" json:"routes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EnableDeviceRoutesRequest) Reset() {
*x = EnableDeviceRoutesRequest{}
mi := &file_headscale_v1_device_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EnableDeviceRoutesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EnableDeviceRoutesRequest) ProtoMessage() {}
func (x *EnableDeviceRoutesRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EnableDeviceRoutesRequest.ProtoReflect.Descriptor instead.
func (*EnableDeviceRoutesRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{9}
}
func (x *EnableDeviceRoutesRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *EnableDeviceRoutesRequest) GetRoutes() []string {
if x != nil {
return x.Routes
}
return nil
}
type EnableDeviceRoutesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
EnabledRoutes []string `protobuf:"bytes,1,rep,name=enabled_routes,json=enabledRoutes,proto3" json:"enabled_routes,omitempty"`
AdvertisedRoutes []string `protobuf:"bytes,2,rep,name=advertised_routes,json=advertisedRoutes,proto3" json:"advertised_routes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EnableDeviceRoutesResponse) Reset() {
*x = EnableDeviceRoutesResponse{}
mi := &file_headscale_v1_device_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EnableDeviceRoutesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EnableDeviceRoutesResponse) ProtoMessage() {}
func (x *EnableDeviceRoutesResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_device_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EnableDeviceRoutesResponse.ProtoReflect.Descriptor instead.
func (*EnableDeviceRoutesResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_device_proto_rawDescGZIP(), []int{10}
}
func (x *EnableDeviceRoutesResponse) GetEnabledRoutes() []string {
if x != nil {
return x.EnabledRoutes
}
return nil
}
func (x *EnableDeviceRoutesResponse) GetAdvertisedRoutes() []string {
if x != nil {
return x.AdvertisedRoutes
}
return nil
}
var File_headscale_v1_device_proto protoreflect.FileDescriptor
const file_headscale_v1_device_proto_rawDesc = "" +
"\n" +
"\x19headscale/v1/device.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"F\n" +
"\aLatency\x12\x1d\n" +
"\n" +
"latency_ms\x18\x01 \x01(\x02R\tlatencyMs\x12\x1c\n" +
"\tpreferred\x18\x02 \x01(\bR\tpreferred\"\x91\x01\n" +
"\x0eClientSupports\x12!\n" +
"\fhair_pinning\x18\x01 \x01(\bR\vhairPinning\x12\x12\n" +
"\x04ipv6\x18\x02 \x01(\bR\x04ipv6\x12\x10\n" +
"\x03pcp\x18\x03 \x01(\bR\x03pcp\x12\x10\n" +
"\x03pmp\x18\x04 \x01(\bR\x03pmp\x12\x10\n" +
"\x03udp\x18\x05 \x01(\bR\x03udp\x12\x12\n" +
"\x04upnp\x18\x06 \x01(\bR\x04upnp\"\xe3\x02\n" +
"\x12ClientConnectivity\x12\x1c\n" +
"\tendpoints\x18\x01 \x03(\tR\tendpoints\x12\x12\n" +
"\x04derp\x18\x02 \x01(\tR\x04derp\x128\n" +
"\x19mapping_varies_by_dest_ip\x18\x03 \x01(\bR\x15mappingVariesByDestIp\x12G\n" +
"\alatency\x18\x04 \x03(\v2-.headscale.v1.ClientConnectivity.LatencyEntryR\alatency\x12E\n" +
"\x0fclient_supports\x18\x05 \x01(\v2\x1c.headscale.v1.ClientSupportsR\x0eclientSupports\x1aQ\n" +
"\fLatencyEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12+\n" +
"\x05value\x18\x02 \x01(\v2\x15.headscale.v1.LatencyR\x05value:\x028\x01\"\"\n" +
"\x10GetDeviceRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"\xa0\x06\n" +
"\x11GetDeviceResponse\x12\x1c\n" +
"\taddresses\x18\x01 \x03(\tR\taddresses\x12\x0e\n" +
"\x02id\x18\x02 \x01(\tR\x02id\x12\x12\n" +
"\x04user\x18\x03 \x01(\tR\x04user\x12\x12\n" +
"\x04name\x18\x04 \x01(\tR\x04name\x12\x1a\n" +
"\bhostname\x18\x05 \x01(\tR\bhostname\x12%\n" +
"\x0eclient_version\x18\x06 \x01(\tR\rclientVersion\x12)\n" +
"\x10update_available\x18\a \x01(\bR\x0fupdateAvailable\x12\x0e\n" +
"\x02os\x18\b \x01(\tR\x02os\x124\n" +
"\acreated\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\acreated\x127\n" +
"\tlast_seen\x18\n" +
" \x01(\v2\x1a.google.protobuf.TimestampR\blastSeen\x12.\n" +
"\x13key_expiry_disabled\x18\v \x01(\bR\x11keyExpiryDisabled\x124\n" +
"\aexpires\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\aexpires\x12\x1e\n" +
"\n" +
"authorized\x18\r \x01(\bR\n" +
"authorized\x12\x1f\n" +
"\vis_external\x18\x0e \x01(\bR\n" +
"isExternal\x12\x1f\n" +
"\vmachine_key\x18\x0f \x01(\tR\n" +
"machineKey\x12\x19\n" +
"\bnode_key\x18\x10 \x01(\tR\anodeKey\x12>\n" +
"\x1bblocks_incoming_connections\x18\x11 \x01(\bR\x19blocksIncomingConnections\x12%\n" +
"\x0eenabled_routes\x18\x12 \x03(\tR\renabledRoutes\x12+\n" +
"\x11advertised_routes\x18\x13 \x03(\tR\x10advertisedRoutes\x12Q\n" +
"\x13client_connectivity\x18\x14 \x01(\v2 .headscale.v1.ClientConnectivityR\x12clientConnectivity\"%\n" +
"\x13DeleteDeviceRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"\x16\n" +
"\x14DeleteDeviceResponse\"(\n" +
"\x16GetDeviceRoutesRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"m\n" +
"\x17GetDeviceRoutesResponse\x12%\n" +
"\x0eenabled_routes\x18\x01 \x03(\tR\renabledRoutes\x12+\n" +
"\x11advertised_routes\x18\x02 \x03(\tR\x10advertisedRoutes\"C\n" +
"\x19EnableDeviceRoutesRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" +
"\x06routes\x18\x02 \x03(\tR\x06routes\"p\n" +
"\x1aEnableDeviceRoutesResponse\x12%\n" +
"\x0eenabled_routes\x18\x01 \x03(\tR\renabledRoutes\x12+\n" +
"\x11advertised_routes\x18\x02 \x03(\tR\x10advertisedRoutesB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
var (
file_headscale_v1_device_proto_rawDescOnce sync.Once
file_headscale_v1_device_proto_rawDescData []byte
)
func file_headscale_v1_device_proto_rawDescGZIP() []byte {
file_headscale_v1_device_proto_rawDescOnce.Do(func() {
file_headscale_v1_device_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_device_proto_rawDesc), len(file_headscale_v1_device_proto_rawDesc)))
})
return file_headscale_v1_device_proto_rawDescData
}
var file_headscale_v1_device_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_headscale_v1_device_proto_goTypes = []any{
(*Latency)(nil), // 0: headscale.v1.Latency
(*ClientSupports)(nil), // 1: headscale.v1.ClientSupports
(*ClientConnectivity)(nil), // 2: headscale.v1.ClientConnectivity
(*GetDeviceRequest)(nil), // 3: headscale.v1.GetDeviceRequest
(*GetDeviceResponse)(nil), // 4: headscale.v1.GetDeviceResponse
(*DeleteDeviceRequest)(nil), // 5: headscale.v1.DeleteDeviceRequest
(*DeleteDeviceResponse)(nil), // 6: headscale.v1.DeleteDeviceResponse
(*GetDeviceRoutesRequest)(nil), // 7: headscale.v1.GetDeviceRoutesRequest
(*GetDeviceRoutesResponse)(nil), // 8: headscale.v1.GetDeviceRoutesResponse
(*EnableDeviceRoutesRequest)(nil), // 9: headscale.v1.EnableDeviceRoutesRequest
(*EnableDeviceRoutesResponse)(nil), // 10: headscale.v1.EnableDeviceRoutesResponse
nil, // 11: headscale.v1.ClientConnectivity.LatencyEntry
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
}
var file_headscale_v1_device_proto_depIdxs = []int32{
11, // 0: headscale.v1.ClientConnectivity.latency:type_name -> headscale.v1.ClientConnectivity.LatencyEntry
1, // 1: headscale.v1.ClientConnectivity.client_supports:type_name -> headscale.v1.ClientSupports
12, // 2: headscale.v1.GetDeviceResponse.created:type_name -> google.protobuf.Timestamp
12, // 3: headscale.v1.GetDeviceResponse.last_seen:type_name -> google.protobuf.Timestamp
12, // 4: headscale.v1.GetDeviceResponse.expires:type_name -> google.protobuf.Timestamp
2, // 5: headscale.v1.GetDeviceResponse.client_connectivity:type_name -> headscale.v1.ClientConnectivity
0, // 6: headscale.v1.ClientConnectivity.LatencyEntry.value:type_name -> headscale.v1.Latency
7, // [7:7] is the sub-list for method output_type
7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
}
func init() { file_headscale_v1_device_proto_init() }
func file_headscale_v1_device_proto_init() {
if File_headscale_v1_device_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_device_proto_rawDesc), len(file_headscale_v1_device_proto_rawDesc)),
NumEnums: 0,
NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_device_proto_goTypes,
DependencyIndexes: file_headscale_v1_device_proto_depIdxs,
MessageInfos: file_headscale_v1_device_proto_msgTypes,
}.Build()
File_headscale_v1_device_proto = out.File
file_headscale_v1_device_proto_goTypes = nil
file_headscale_v1_device_proto_depIdxs = nil
}
+318
View File
@@ -0,0 +1,318 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: headscale/v1/headscale.proto
package v1
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HealthRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HealthRequest) Reset() {
*x = HealthRequest{}
mi := &file_headscale_v1_headscale_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HealthRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HealthRequest) ProtoMessage() {}
func (x *HealthRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_headscale_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HealthRequest.ProtoReflect.Descriptor instead.
func (*HealthRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_headscale_proto_rawDescGZIP(), []int{0}
}
type HealthResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
DatabaseConnectivity bool `protobuf:"varint,1,opt,name=database_connectivity,json=databaseConnectivity,proto3" json:"database_connectivity,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HealthResponse) Reset() {
*x = HealthResponse{}
mi := &file_headscale_v1_headscale_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HealthResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HealthResponse) ProtoMessage() {}
func (x *HealthResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_headscale_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HealthResponse.ProtoReflect.Descriptor instead.
func (*HealthResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_headscale_proto_rawDescGZIP(), []int{1}
}
func (x *HealthResponse) GetDatabaseConnectivity() bool {
if x != nil {
return x.DatabaseConnectivity
}
return false
}
var File_headscale_v1_headscale_proto protoreflect.FileDescriptor
const file_headscale_v1_headscale_proto_rawDesc = "" +
"\n" +
"\x1cheadscale/v1/headscale.proto\x12\fheadscale.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17headscale/v1/user.proto\x1a\x1dheadscale/v1/preauthkey.proto\x1a\x17headscale/v1/node.proto\x1a\x19headscale/v1/apikey.proto\x1a\x17headscale/v1/auth.proto\x1a\x19headscale/v1/policy.proto\"\x0f\n" +
"\rHealthRequest\"E\n" +
"\x0eHealthResponse\x123\n" +
"\x15database_connectivity\x18\x01 \x01(\bR\x14databaseConnectivity2\xe0\x1a\n" +
"\x10HeadscaleService\x12h\n" +
"\n" +
"CreateUser\x12\x1f.headscale.v1.CreateUserRequest\x1a .headscale.v1.CreateUserResponse\"\x17\x82\xd3\xe4\x93\x02\x11:\x01*\"\f/api/v1/user\x12\x80\x01\n" +
"\n" +
"RenameUser\x12\x1f.headscale.v1.RenameUserRequest\x1a .headscale.v1.RenameUserResponse\"/\x82\xd3\xe4\x93\x02)\"'/api/v1/user/{old_id}/rename/{new_name}\x12j\n" +
"\n" +
"DeleteUser\x12\x1f.headscale.v1.DeleteUserRequest\x1a .headscale.v1.DeleteUserResponse\"\x19\x82\xd3\xe4\x93\x02\x13*\x11/api/v1/user/{id}\x12b\n" +
"\tListUsers\x12\x1e.headscale.v1.ListUsersRequest\x1a\x1f.headscale.v1.ListUsersResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/v1/user\x12\x80\x01\n" +
"\x10CreatePreAuthKey\x12%.headscale.v1.CreatePreAuthKeyRequest\x1a&.headscale.v1.CreatePreAuthKeyResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/preauthkey\x12\x87\x01\n" +
"\x10ExpirePreAuthKey\x12%.headscale.v1.ExpirePreAuthKeyRequest\x1a&.headscale.v1.ExpirePreAuthKeyResponse\"$\x82\xd3\xe4\x93\x02\x1e:\x01*\"\x19/api/v1/preauthkey/expire\x12}\n" +
"\x10DeletePreAuthKey\x12%.headscale.v1.DeletePreAuthKeyRequest\x1a&.headscale.v1.DeletePreAuthKeyResponse\"\x1a\x82\xd3\xe4\x93\x02\x14*\x12/api/v1/preauthkey\x12z\n" +
"\x0fListPreAuthKeys\x12$.headscale.v1.ListPreAuthKeysRequest\x1a%.headscale.v1.ListPreAuthKeysResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/preauthkey\x12}\n" +
"\x0fDebugCreateNode\x12$.headscale.v1.DebugCreateNodeRequest\x1a%.headscale.v1.DebugCreateNodeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17:\x01*\"\x12/api/v1/debug/node\x12f\n" +
"\aGetNode\x12\x1c.headscale.v1.GetNodeRequest\x1a\x1d.headscale.v1.GetNodeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/node/{node_id}\x12n\n" +
"\aSetTags\x12\x1c.headscale.v1.SetTagsRequest\x1a\x1d.headscale.v1.SetTagsResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/v1/node/{node_id}/tags\x12\x96\x01\n" +
"\x11SetApprovedRoutes\x12&.headscale.v1.SetApprovedRoutesRequest\x1a'.headscale.v1.SetApprovedRoutesResponse\"0\x82\xd3\xe4\x93\x02*:\x01*\"%/api/v1/node/{node_id}/approve_routes\x12t\n" +
"\fRegisterNode\x12!.headscale.v1.RegisterNodeRequest\x1a\".headscale.v1.RegisterNodeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x15/api/v1/node/register\x12o\n" +
"\n" +
"DeleteNode\x12\x1f.headscale.v1.DeleteNodeRequest\x1a .headscale.v1.DeleteNodeResponse\"\x1e\x82\xd3\xe4\x93\x02\x18*\x16/api/v1/node/{node_id}\x12v\n" +
"\n" +
"ExpireNode\x12\x1f.headscale.v1.ExpireNodeRequest\x1a .headscale.v1.ExpireNodeResponse\"%\x82\xd3\xe4\x93\x02\x1f\"\x1d/api/v1/node/{node_id}/expire\x12\x81\x01\n" +
"\n" +
"RenameNode\x12\x1f.headscale.v1.RenameNodeRequest\x1a .headscale.v1.RenameNodeResponse\"0\x82\xd3\xe4\x93\x02*\"(/api/v1/node/{node_id}/rename/{new_name}\x12b\n" +
"\tListNodes\x12\x1e.headscale.v1.ListNodesRequest\x1a\x1f.headscale.v1.ListNodesResponse\"\x14\x82\xd3\xe4\x93\x02\x0e\x12\f/api/v1/node\x12\x80\x01\n" +
"\x0fBackfillNodeIPs\x12$.headscale.v1.BackfillNodeIPsRequest\x1a%.headscale.v1.BackfillNodeIPsResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x18/api/v1/node/backfillips\x12w\n" +
"\fAuthRegister\x12!.headscale.v1.AuthRegisterRequest\x1a\".headscale.v1.AuthRegisterResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/register\x12s\n" +
"\vAuthApprove\x12 .headscale.v1.AuthApproveRequest\x1a!.headscale.v1.AuthApproveResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/auth/approve\x12o\n" +
"\n" +
"AuthReject\x12\x1f.headscale.v1.AuthRejectRequest\x1a .headscale.v1.AuthRejectResponse\"\x1e\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/api/v1/auth/reject\x12p\n" +
"\fCreateApiKey\x12!.headscale.v1.CreateApiKeyRequest\x1a\".headscale.v1.CreateApiKeyResponse\"\x19\x82\xd3\xe4\x93\x02\x13:\x01*\"\x0e/api/v1/apikey\x12w\n" +
"\fExpireApiKey\x12!.headscale.v1.ExpireApiKeyRequest\x1a\".headscale.v1.ExpireApiKeyResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/apikey/expire\x12j\n" +
"\vListApiKeys\x12 .headscale.v1.ListApiKeysRequest\x1a!.headscale.v1.ListApiKeysResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/apikey\x12v\n" +
"\fDeleteApiKey\x12!.headscale.v1.DeleteApiKeyRequest\x1a\".headscale.v1.DeleteApiKeyResponse\"\x1f\x82\xd3\xe4\x93\x02\x19*\x17/api/v1/apikey/{prefix}\x12d\n" +
"\tGetPolicy\x12\x1e.headscale.v1.GetPolicyRequest\x1a\x1f.headscale.v1.GetPolicyResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/policy\x12g\n" +
"\tSetPolicy\x12\x1e.headscale.v1.SetPolicyRequest\x1a\x1f.headscale.v1.SetPolicyResponse\"\x19\x82\xd3\xe4\x93\x02\x13:\x01*\x1a\x0e/api/v1/policy\x12s\n" +
"\vCheckPolicy\x12 .headscale.v1.CheckPolicyRequest\x1a!.headscale.v1.CheckPolicyResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/policy/check\x12[\n" +
"\x06Health\x12\x1b.headscale.v1.HealthRequest\x1a\x1c.headscale.v1.HealthResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/healthB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
var (
file_headscale_v1_headscale_proto_rawDescOnce sync.Once
file_headscale_v1_headscale_proto_rawDescData []byte
)
func file_headscale_v1_headscale_proto_rawDescGZIP() []byte {
file_headscale_v1_headscale_proto_rawDescOnce.Do(func() {
file_headscale_v1_headscale_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_headscale_proto_rawDesc), len(file_headscale_v1_headscale_proto_rawDesc)))
})
return file_headscale_v1_headscale_proto_rawDescData
}
var file_headscale_v1_headscale_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_headscale_v1_headscale_proto_goTypes = []any{
(*HealthRequest)(nil), // 0: headscale.v1.HealthRequest
(*HealthResponse)(nil), // 1: headscale.v1.HealthResponse
(*CreateUserRequest)(nil), // 2: headscale.v1.CreateUserRequest
(*RenameUserRequest)(nil), // 3: headscale.v1.RenameUserRequest
(*DeleteUserRequest)(nil), // 4: headscale.v1.DeleteUserRequest
(*ListUsersRequest)(nil), // 5: headscale.v1.ListUsersRequest
(*CreatePreAuthKeyRequest)(nil), // 6: headscale.v1.CreatePreAuthKeyRequest
(*ExpirePreAuthKeyRequest)(nil), // 7: headscale.v1.ExpirePreAuthKeyRequest
(*DeletePreAuthKeyRequest)(nil), // 8: headscale.v1.DeletePreAuthKeyRequest
(*ListPreAuthKeysRequest)(nil), // 9: headscale.v1.ListPreAuthKeysRequest
(*DebugCreateNodeRequest)(nil), // 10: headscale.v1.DebugCreateNodeRequest
(*GetNodeRequest)(nil), // 11: headscale.v1.GetNodeRequest
(*SetTagsRequest)(nil), // 12: headscale.v1.SetTagsRequest
(*SetApprovedRoutesRequest)(nil), // 13: headscale.v1.SetApprovedRoutesRequest
(*RegisterNodeRequest)(nil), // 14: headscale.v1.RegisterNodeRequest
(*DeleteNodeRequest)(nil), // 15: headscale.v1.DeleteNodeRequest
(*ExpireNodeRequest)(nil), // 16: headscale.v1.ExpireNodeRequest
(*RenameNodeRequest)(nil), // 17: headscale.v1.RenameNodeRequest
(*ListNodesRequest)(nil), // 18: headscale.v1.ListNodesRequest
(*BackfillNodeIPsRequest)(nil), // 19: headscale.v1.BackfillNodeIPsRequest
(*AuthRegisterRequest)(nil), // 20: headscale.v1.AuthRegisterRequest
(*AuthApproveRequest)(nil), // 21: headscale.v1.AuthApproveRequest
(*AuthRejectRequest)(nil), // 22: headscale.v1.AuthRejectRequest
(*CreateApiKeyRequest)(nil), // 23: headscale.v1.CreateApiKeyRequest
(*ExpireApiKeyRequest)(nil), // 24: headscale.v1.ExpireApiKeyRequest
(*ListApiKeysRequest)(nil), // 25: headscale.v1.ListApiKeysRequest
(*DeleteApiKeyRequest)(nil), // 26: headscale.v1.DeleteApiKeyRequest
(*GetPolicyRequest)(nil), // 27: headscale.v1.GetPolicyRequest
(*SetPolicyRequest)(nil), // 28: headscale.v1.SetPolicyRequest
(*CheckPolicyRequest)(nil), // 29: headscale.v1.CheckPolicyRequest
(*CreateUserResponse)(nil), // 30: headscale.v1.CreateUserResponse
(*RenameUserResponse)(nil), // 31: headscale.v1.RenameUserResponse
(*DeleteUserResponse)(nil), // 32: headscale.v1.DeleteUserResponse
(*ListUsersResponse)(nil), // 33: headscale.v1.ListUsersResponse
(*CreatePreAuthKeyResponse)(nil), // 34: headscale.v1.CreatePreAuthKeyResponse
(*ExpirePreAuthKeyResponse)(nil), // 35: headscale.v1.ExpirePreAuthKeyResponse
(*DeletePreAuthKeyResponse)(nil), // 36: headscale.v1.DeletePreAuthKeyResponse
(*ListPreAuthKeysResponse)(nil), // 37: headscale.v1.ListPreAuthKeysResponse
(*DebugCreateNodeResponse)(nil), // 38: headscale.v1.DebugCreateNodeResponse
(*GetNodeResponse)(nil), // 39: headscale.v1.GetNodeResponse
(*SetTagsResponse)(nil), // 40: headscale.v1.SetTagsResponse
(*SetApprovedRoutesResponse)(nil), // 41: headscale.v1.SetApprovedRoutesResponse
(*RegisterNodeResponse)(nil), // 42: headscale.v1.RegisterNodeResponse
(*DeleteNodeResponse)(nil), // 43: headscale.v1.DeleteNodeResponse
(*ExpireNodeResponse)(nil), // 44: headscale.v1.ExpireNodeResponse
(*RenameNodeResponse)(nil), // 45: headscale.v1.RenameNodeResponse
(*ListNodesResponse)(nil), // 46: headscale.v1.ListNodesResponse
(*BackfillNodeIPsResponse)(nil), // 47: headscale.v1.BackfillNodeIPsResponse
(*AuthRegisterResponse)(nil), // 48: headscale.v1.AuthRegisterResponse
(*AuthApproveResponse)(nil), // 49: headscale.v1.AuthApproveResponse
(*AuthRejectResponse)(nil), // 50: headscale.v1.AuthRejectResponse
(*CreateApiKeyResponse)(nil), // 51: headscale.v1.CreateApiKeyResponse
(*ExpireApiKeyResponse)(nil), // 52: headscale.v1.ExpireApiKeyResponse
(*ListApiKeysResponse)(nil), // 53: headscale.v1.ListApiKeysResponse
(*DeleteApiKeyResponse)(nil), // 54: headscale.v1.DeleteApiKeyResponse
(*GetPolicyResponse)(nil), // 55: headscale.v1.GetPolicyResponse
(*SetPolicyResponse)(nil), // 56: headscale.v1.SetPolicyResponse
(*CheckPolicyResponse)(nil), // 57: headscale.v1.CheckPolicyResponse
}
var file_headscale_v1_headscale_proto_depIdxs = []int32{
2, // 0: headscale.v1.HeadscaleService.CreateUser:input_type -> headscale.v1.CreateUserRequest
3, // 1: headscale.v1.HeadscaleService.RenameUser:input_type -> headscale.v1.RenameUserRequest
4, // 2: headscale.v1.HeadscaleService.DeleteUser:input_type -> headscale.v1.DeleteUserRequest
5, // 3: headscale.v1.HeadscaleService.ListUsers:input_type -> headscale.v1.ListUsersRequest
6, // 4: headscale.v1.HeadscaleService.CreatePreAuthKey:input_type -> headscale.v1.CreatePreAuthKeyRequest
7, // 5: headscale.v1.HeadscaleService.ExpirePreAuthKey:input_type -> headscale.v1.ExpirePreAuthKeyRequest
8, // 6: headscale.v1.HeadscaleService.DeletePreAuthKey:input_type -> headscale.v1.DeletePreAuthKeyRequest
9, // 7: headscale.v1.HeadscaleService.ListPreAuthKeys:input_type -> headscale.v1.ListPreAuthKeysRequest
10, // 8: headscale.v1.HeadscaleService.DebugCreateNode:input_type -> headscale.v1.DebugCreateNodeRequest
11, // 9: headscale.v1.HeadscaleService.GetNode:input_type -> headscale.v1.GetNodeRequest
12, // 10: headscale.v1.HeadscaleService.SetTags:input_type -> headscale.v1.SetTagsRequest
13, // 11: headscale.v1.HeadscaleService.SetApprovedRoutes:input_type -> headscale.v1.SetApprovedRoutesRequest
14, // 12: headscale.v1.HeadscaleService.RegisterNode:input_type -> headscale.v1.RegisterNodeRequest
15, // 13: headscale.v1.HeadscaleService.DeleteNode:input_type -> headscale.v1.DeleteNodeRequest
16, // 14: headscale.v1.HeadscaleService.ExpireNode:input_type -> headscale.v1.ExpireNodeRequest
17, // 15: headscale.v1.HeadscaleService.RenameNode:input_type -> headscale.v1.RenameNodeRequest
18, // 16: headscale.v1.HeadscaleService.ListNodes:input_type -> headscale.v1.ListNodesRequest
19, // 17: headscale.v1.HeadscaleService.BackfillNodeIPs:input_type -> headscale.v1.BackfillNodeIPsRequest
20, // 18: headscale.v1.HeadscaleService.AuthRegister:input_type -> headscale.v1.AuthRegisterRequest
21, // 19: headscale.v1.HeadscaleService.AuthApprove:input_type -> headscale.v1.AuthApproveRequest
22, // 20: headscale.v1.HeadscaleService.AuthReject:input_type -> headscale.v1.AuthRejectRequest
23, // 21: headscale.v1.HeadscaleService.CreateApiKey:input_type -> headscale.v1.CreateApiKeyRequest
24, // 22: headscale.v1.HeadscaleService.ExpireApiKey:input_type -> headscale.v1.ExpireApiKeyRequest
25, // 23: headscale.v1.HeadscaleService.ListApiKeys:input_type -> headscale.v1.ListApiKeysRequest
26, // 24: headscale.v1.HeadscaleService.DeleteApiKey:input_type -> headscale.v1.DeleteApiKeyRequest
27, // 25: headscale.v1.HeadscaleService.GetPolicy:input_type -> headscale.v1.GetPolicyRequest
28, // 26: headscale.v1.HeadscaleService.SetPolicy:input_type -> headscale.v1.SetPolicyRequest
29, // 27: headscale.v1.HeadscaleService.CheckPolicy:input_type -> headscale.v1.CheckPolicyRequest
0, // 28: headscale.v1.HeadscaleService.Health:input_type -> headscale.v1.HealthRequest
30, // 29: headscale.v1.HeadscaleService.CreateUser:output_type -> headscale.v1.CreateUserResponse
31, // 30: headscale.v1.HeadscaleService.RenameUser:output_type -> headscale.v1.RenameUserResponse
32, // 31: headscale.v1.HeadscaleService.DeleteUser:output_type -> headscale.v1.DeleteUserResponse
33, // 32: headscale.v1.HeadscaleService.ListUsers:output_type -> headscale.v1.ListUsersResponse
34, // 33: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
35, // 34: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
36, // 35: headscale.v1.HeadscaleService.DeletePreAuthKey:output_type -> headscale.v1.DeletePreAuthKeyResponse
37, // 36: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
38, // 37: headscale.v1.HeadscaleService.DebugCreateNode:output_type -> headscale.v1.DebugCreateNodeResponse
39, // 38: headscale.v1.HeadscaleService.GetNode:output_type -> headscale.v1.GetNodeResponse
40, // 39: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse
41, // 40: headscale.v1.HeadscaleService.SetApprovedRoutes:output_type -> headscale.v1.SetApprovedRoutesResponse
42, // 41: headscale.v1.HeadscaleService.RegisterNode:output_type -> headscale.v1.RegisterNodeResponse
43, // 42: headscale.v1.HeadscaleService.DeleteNode:output_type -> headscale.v1.DeleteNodeResponse
44, // 43: headscale.v1.HeadscaleService.ExpireNode:output_type -> headscale.v1.ExpireNodeResponse
45, // 44: headscale.v1.HeadscaleService.RenameNode:output_type -> headscale.v1.RenameNodeResponse
46, // 45: headscale.v1.HeadscaleService.ListNodes:output_type -> headscale.v1.ListNodesResponse
47, // 46: headscale.v1.HeadscaleService.BackfillNodeIPs:output_type -> headscale.v1.BackfillNodeIPsResponse
48, // 47: headscale.v1.HeadscaleService.AuthRegister:output_type -> headscale.v1.AuthRegisterResponse
49, // 48: headscale.v1.HeadscaleService.AuthApprove:output_type -> headscale.v1.AuthApproveResponse
50, // 49: headscale.v1.HeadscaleService.AuthReject:output_type -> headscale.v1.AuthRejectResponse
51, // 50: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
52, // 51: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
53, // 52: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
54, // 53: headscale.v1.HeadscaleService.DeleteApiKey:output_type -> headscale.v1.DeleteApiKeyResponse
55, // 54: headscale.v1.HeadscaleService.GetPolicy:output_type -> headscale.v1.GetPolicyResponse
56, // 55: headscale.v1.HeadscaleService.SetPolicy:output_type -> headscale.v1.SetPolicyResponse
57, // 56: headscale.v1.HeadscaleService.CheckPolicy:output_type -> headscale.v1.CheckPolicyResponse
1, // 57: headscale.v1.HeadscaleService.Health:output_type -> headscale.v1.HealthResponse
29, // [29:58] is the sub-list for method output_type
0, // [0:29] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_headscale_v1_headscale_proto_init() }
func file_headscale_v1_headscale_proto_init() {
if File_headscale_v1_headscale_proto != nil {
return
}
file_headscale_v1_user_proto_init()
file_headscale_v1_preauthkey_proto_init()
file_headscale_v1_node_proto_init()
file_headscale_v1_apikey_proto_init()
file_headscale_v1_auth_proto_init()
file_headscale_v1_policy_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_headscale_proto_rawDesc), len(file_headscale_v1_headscale_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_headscale_v1_headscale_proto_goTypes,
DependencyIndexes: file_headscale_v1_headscale_proto_depIdxs,
MessageInfos: file_headscale_v1_headscale_proto_msgTypes,
}.Build()
File_headscale_v1_headscale_proto = out.File
file_headscale_v1_headscale_proto_goTypes = nil
file_headscale_v1_headscale_proto_depIdxs = nil
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+363
View File
@@ -0,0 +1,363 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: headscale/v1/policy.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type SetPolicyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetPolicyRequest) Reset() {
*x = SetPolicyRequest{}
mi := &file_headscale_v1_policy_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetPolicyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetPolicyRequest) ProtoMessage() {}
func (x *SetPolicyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_policy_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetPolicyRequest.ProtoReflect.Descriptor instead.
func (*SetPolicyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{0}
}
func (x *SetPolicyRequest) GetPolicy() string {
if x != nil {
return x.Policy
}
return ""
}
type SetPolicyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetPolicyResponse) Reset() {
*x = SetPolicyResponse{}
mi := &file_headscale_v1_policy_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetPolicyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetPolicyResponse) ProtoMessage() {}
func (x *SetPolicyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_policy_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetPolicyResponse.ProtoReflect.Descriptor instead.
func (*SetPolicyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{1}
}
func (x *SetPolicyResponse) GetPolicy() string {
if x != nil {
return x.Policy
}
return ""
}
func (x *SetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
type GetPolicyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetPolicyRequest) Reset() {
*x = GetPolicyRequest{}
mi := &file_headscale_v1_policy_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetPolicyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetPolicyRequest) ProtoMessage() {}
func (x *GetPolicyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_policy_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetPolicyRequest.ProtoReflect.Descriptor instead.
func (*GetPolicyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{2}
}
type GetPolicyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetPolicyResponse) Reset() {
*x = GetPolicyResponse{}
mi := &file_headscale_v1_policy_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetPolicyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetPolicyResponse) ProtoMessage() {}
func (x *GetPolicyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_policy_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetPolicyResponse.ProtoReflect.Descriptor instead.
func (*GetPolicyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{3}
}
func (x *GetPolicyResponse) GetPolicy() string {
if x != nil {
return x.Policy
}
return ""
}
func (x *GetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
type CheckPolicyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Policy string `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CheckPolicyRequest) Reset() {
*x = CheckPolicyRequest{}
mi := &file_headscale_v1_policy_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CheckPolicyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CheckPolicyRequest) ProtoMessage() {}
func (x *CheckPolicyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_policy_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CheckPolicyRequest.ProtoReflect.Descriptor instead.
func (*CheckPolicyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{4}
}
func (x *CheckPolicyRequest) GetPolicy() string {
if x != nil {
return x.Policy
}
return ""
}
type CheckPolicyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CheckPolicyResponse) Reset() {
*x = CheckPolicyResponse{}
mi := &file_headscale_v1_policy_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CheckPolicyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CheckPolicyResponse) ProtoMessage() {}
func (x *CheckPolicyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_policy_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CheckPolicyResponse.ProtoReflect.Descriptor instead.
func (*CheckPolicyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_policy_proto_rawDescGZIP(), []int{5}
}
var File_headscale_v1_policy_proto protoreflect.FileDescriptor
const file_headscale_v1_policy_proto_rawDesc = "" +
"\n" +
"\x19headscale/v1/policy.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"*\n" +
"\x10SetPolicyRequest\x12\x16\n" +
"\x06policy\x18\x01 \x01(\tR\x06policy\"f\n" +
"\x11SetPolicyResponse\x12\x16\n" +
"\x06policy\x18\x01 \x01(\tR\x06policy\x129\n" +
"\n" +
"updated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x12\n" +
"\x10GetPolicyRequest\"f\n" +
"\x11GetPolicyResponse\x12\x16\n" +
"\x06policy\x18\x01 \x01(\tR\x06policy\x129\n" +
"\n" +
"updated_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\",\n" +
"\x12CheckPolicyRequest\x12\x16\n" +
"\x06policy\x18\x01 \x01(\tR\x06policy\"\x15\n" +
"\x13CheckPolicyResponseB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
var (
file_headscale_v1_policy_proto_rawDescOnce sync.Once
file_headscale_v1_policy_proto_rawDescData []byte
)
func file_headscale_v1_policy_proto_rawDescGZIP() []byte {
file_headscale_v1_policy_proto_rawDescOnce.Do(func() {
file_headscale_v1_policy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_policy_proto_rawDesc), len(file_headscale_v1_policy_proto_rawDesc)))
})
return file_headscale_v1_policy_proto_rawDescData
}
var file_headscale_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_headscale_v1_policy_proto_goTypes = []any{
(*SetPolicyRequest)(nil), // 0: headscale.v1.SetPolicyRequest
(*SetPolicyResponse)(nil), // 1: headscale.v1.SetPolicyResponse
(*GetPolicyRequest)(nil), // 2: headscale.v1.GetPolicyRequest
(*GetPolicyResponse)(nil), // 3: headscale.v1.GetPolicyResponse
(*CheckPolicyRequest)(nil), // 4: headscale.v1.CheckPolicyRequest
(*CheckPolicyResponse)(nil), // 5: headscale.v1.CheckPolicyResponse
(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
}
var file_headscale_v1_policy_proto_depIdxs = []int32{
6, // 0: headscale.v1.SetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp
6, // 1: headscale.v1.GetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_headscale_v1_policy_proto_init() }
func file_headscale_v1_policy_proto_init() {
if File_headscale_v1_policy_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_policy_proto_rawDesc), len(file_headscale_v1_policy_proto_rawDesc)),
NumEnums: 0,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_policy_proto_goTypes,
DependencyIndexes: file_headscale_v1_policy_proto_depIdxs,
MessageInfos: file_headscale_v1_policy_proto_msgTypes,
}.Build()
File_headscale_v1_policy_proto = out.File
file_headscale_v1_policy_proto_goTypes = nil
file_headscale_v1_policy_proto_depIdxs = nil
}
+596
View File
@@ -0,0 +1,596 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: headscale/v1/preauthkey.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type PreAuthKey struct {
state protoimpl.MessageState `protogen:"open.v1"`
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
Reusable bool `protobuf:"varint,4,opt,name=reusable,proto3" json:"reusable,omitempty"`
Ephemeral bool `protobuf:"varint,5,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
Used bool `protobuf:"varint,6,opt,name=used,proto3" json:"used,omitempty"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiration,proto3" json:"expiration,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
AclTags []string `protobuf:"bytes,9,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PreAuthKey) Reset() {
*x = PreAuthKey{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PreAuthKey) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PreAuthKey) ProtoMessage() {}
func (x *PreAuthKey) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PreAuthKey.ProtoReflect.Descriptor instead.
func (*PreAuthKey) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{0}
}
func (x *PreAuthKey) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
func (x *PreAuthKey) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
func (x *PreAuthKey) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *PreAuthKey) GetReusable() bool {
if x != nil {
return x.Reusable
}
return false
}
func (x *PreAuthKey) GetEphemeral() bool {
if x != nil {
return x.Ephemeral
}
return false
}
func (x *PreAuthKey) GetUsed() bool {
if x != nil {
return x.Used
}
return false
}
func (x *PreAuthKey) GetExpiration() *timestamppb.Timestamp {
if x != nil {
return x.Expiration
}
return nil
}
func (x *PreAuthKey) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *PreAuthKey) GetAclTags() []string {
if x != nil {
return x.AclTags
}
return nil
}
type CreatePreAuthKeyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
User uint64 `protobuf:"varint,1,opt,name=user,proto3" json:"user,omitempty"`
Reusable bool `protobuf:"varint,2,opt,name=reusable,proto3" json:"reusable,omitempty"`
Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"`
Expiration *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration,proto3" json:"expiration,omitempty"`
AclTags []string `protobuf:"bytes,5,rep,name=acl_tags,json=aclTags,proto3" json:"acl_tags,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreatePreAuthKeyRequest) Reset() {
*x = CreatePreAuthKeyRequest{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreatePreAuthKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreatePreAuthKeyRequest) ProtoMessage() {}
func (x *CreatePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreatePreAuthKeyRequest.ProtoReflect.Descriptor instead.
func (*CreatePreAuthKeyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{1}
}
func (x *CreatePreAuthKeyRequest) GetUser() uint64 {
if x != nil {
return x.User
}
return 0
}
func (x *CreatePreAuthKeyRequest) GetReusable() bool {
if x != nil {
return x.Reusable
}
return false
}
func (x *CreatePreAuthKeyRequest) GetEphemeral() bool {
if x != nil {
return x.Ephemeral
}
return false
}
func (x *CreatePreAuthKeyRequest) GetExpiration() *timestamppb.Timestamp {
if x != nil {
return x.Expiration
}
return nil
}
func (x *CreatePreAuthKeyRequest) GetAclTags() []string {
if x != nil {
return x.AclTags
}
return nil
}
type CreatePreAuthKeyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
PreAuthKey *PreAuthKey `protobuf:"bytes,1,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreatePreAuthKeyResponse) Reset() {
*x = CreatePreAuthKeyResponse{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreatePreAuthKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreatePreAuthKeyResponse) ProtoMessage() {}
func (x *CreatePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreatePreAuthKeyResponse.ProtoReflect.Descriptor instead.
func (*CreatePreAuthKeyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{2}
}
func (x *CreatePreAuthKeyResponse) GetPreAuthKey() *PreAuthKey {
if x != nil {
return x.PreAuthKey
}
return nil
}
type ExpirePreAuthKeyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ExpirePreAuthKeyRequest) Reset() {
*x = ExpirePreAuthKeyRequest{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ExpirePreAuthKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExpirePreAuthKeyRequest) ProtoMessage() {}
func (x *ExpirePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExpirePreAuthKeyRequest.ProtoReflect.Descriptor instead.
func (*ExpirePreAuthKeyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{3}
}
func (x *ExpirePreAuthKeyRequest) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
type ExpirePreAuthKeyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ExpirePreAuthKeyResponse) Reset() {
*x = ExpirePreAuthKeyResponse{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ExpirePreAuthKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExpirePreAuthKeyResponse) ProtoMessage() {}
func (x *ExpirePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExpirePreAuthKeyResponse.ProtoReflect.Descriptor instead.
func (*ExpirePreAuthKeyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{4}
}
type DeletePreAuthKeyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeletePreAuthKeyRequest) Reset() {
*x = DeletePreAuthKeyRequest{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeletePreAuthKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeletePreAuthKeyRequest) ProtoMessage() {}
func (x *DeletePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeletePreAuthKeyRequest.ProtoReflect.Descriptor instead.
func (*DeletePreAuthKeyRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{5}
}
func (x *DeletePreAuthKeyRequest) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
type DeletePreAuthKeyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeletePreAuthKeyResponse) Reset() {
*x = DeletePreAuthKeyResponse{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeletePreAuthKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeletePreAuthKeyResponse) ProtoMessage() {}
func (x *DeletePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeletePreAuthKeyResponse.ProtoReflect.Descriptor instead.
func (*DeletePreAuthKeyResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{6}
}
type ListPreAuthKeysRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListPreAuthKeysRequest) Reset() {
*x = ListPreAuthKeysRequest{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListPreAuthKeysRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListPreAuthKeysRequest) ProtoMessage() {}
func (x *ListPreAuthKeysRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListPreAuthKeysRequest.ProtoReflect.Descriptor instead.
func (*ListPreAuthKeysRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{7}
}
type ListPreAuthKeysResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
PreAuthKeys []*PreAuthKey `protobuf:"bytes,1,rep,name=pre_auth_keys,json=preAuthKeys,proto3" json:"pre_auth_keys,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListPreAuthKeysResponse) Reset() {
*x = ListPreAuthKeysResponse{}
mi := &file_headscale_v1_preauthkey_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListPreAuthKeysResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListPreAuthKeysResponse) ProtoMessage() {}
func (x *ListPreAuthKeysResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_preauthkey_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListPreAuthKeysResponse.ProtoReflect.Descriptor instead.
func (*ListPreAuthKeysResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_preauthkey_proto_rawDescGZIP(), []int{8}
}
func (x *ListPreAuthKeysResponse) GetPreAuthKeys() []*PreAuthKey {
if x != nil {
return x.PreAuthKeys
}
return nil
}
var File_headscale_v1_preauthkey_proto protoreflect.FileDescriptor
const file_headscale_v1_preauthkey_proto_rawDesc = "" +
"\n" +
"\x1dheadscale/v1/preauthkey.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17headscale/v1/user.proto\"\xb6\x02\n" +
"\n" +
"PreAuthKey\x12&\n" +
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\x12\x0e\n" +
"\x02id\x18\x02 \x01(\x04R\x02id\x12\x10\n" +
"\x03key\x18\x03 \x01(\tR\x03key\x12\x1a\n" +
"\breusable\x18\x04 \x01(\bR\breusable\x12\x1c\n" +
"\tephemeral\x18\x05 \x01(\bR\tephemeral\x12\x12\n" +
"\x04used\x18\x06 \x01(\bR\x04used\x12:\n" +
"\n" +
"expiration\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" +
"expiration\x129\n" +
"\n" +
"created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x19\n" +
"\bacl_tags\x18\t \x03(\tR\aaclTags\"\xbe\x01\n" +
"\x17CreatePreAuthKeyRequest\x12\x12\n" +
"\x04user\x18\x01 \x01(\x04R\x04user\x12\x1a\n" +
"\breusable\x18\x02 \x01(\bR\breusable\x12\x1c\n" +
"\tephemeral\x18\x03 \x01(\bR\tephemeral\x12:\n" +
"\n" +
"expiration\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\n" +
"expiration\x12\x19\n" +
"\bacl_tags\x18\x05 \x03(\tR\aaclTags\"V\n" +
"\x18CreatePreAuthKeyResponse\x12:\n" +
"\fpre_auth_key\x18\x01 \x01(\v2\x18.headscale.v1.PreAuthKeyR\n" +
"preAuthKey\")\n" +
"\x17ExpirePreAuthKeyRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\"\x1a\n" +
"\x18ExpirePreAuthKeyResponse\")\n" +
"\x17DeletePreAuthKeyRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\"\x1a\n" +
"\x18DeletePreAuthKeyResponse\"\x18\n" +
"\x16ListPreAuthKeysRequest\"W\n" +
"\x17ListPreAuthKeysResponse\x12<\n" +
"\rpre_auth_keys\x18\x01 \x03(\v2\x18.headscale.v1.PreAuthKeyR\vpreAuthKeysB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
var (
file_headscale_v1_preauthkey_proto_rawDescOnce sync.Once
file_headscale_v1_preauthkey_proto_rawDescData []byte
)
func file_headscale_v1_preauthkey_proto_rawDescGZIP() []byte {
file_headscale_v1_preauthkey_proto_rawDescOnce.Do(func() {
file_headscale_v1_preauthkey_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_preauthkey_proto_rawDesc), len(file_headscale_v1_preauthkey_proto_rawDesc)))
})
return file_headscale_v1_preauthkey_proto_rawDescData
}
var file_headscale_v1_preauthkey_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_headscale_v1_preauthkey_proto_goTypes = []any{
(*PreAuthKey)(nil), // 0: headscale.v1.PreAuthKey
(*CreatePreAuthKeyRequest)(nil), // 1: headscale.v1.CreatePreAuthKeyRequest
(*CreatePreAuthKeyResponse)(nil), // 2: headscale.v1.CreatePreAuthKeyResponse
(*ExpirePreAuthKeyRequest)(nil), // 3: headscale.v1.ExpirePreAuthKeyRequest
(*ExpirePreAuthKeyResponse)(nil), // 4: headscale.v1.ExpirePreAuthKeyResponse
(*DeletePreAuthKeyRequest)(nil), // 5: headscale.v1.DeletePreAuthKeyRequest
(*DeletePreAuthKeyResponse)(nil), // 6: headscale.v1.DeletePreAuthKeyResponse
(*ListPreAuthKeysRequest)(nil), // 7: headscale.v1.ListPreAuthKeysRequest
(*ListPreAuthKeysResponse)(nil), // 8: headscale.v1.ListPreAuthKeysResponse
(*User)(nil), // 9: headscale.v1.User
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
}
var file_headscale_v1_preauthkey_proto_depIdxs = []int32{
9, // 0: headscale.v1.PreAuthKey.user:type_name -> headscale.v1.User
10, // 1: headscale.v1.PreAuthKey.expiration:type_name -> google.protobuf.Timestamp
10, // 2: headscale.v1.PreAuthKey.created_at:type_name -> google.protobuf.Timestamp
10, // 3: headscale.v1.CreatePreAuthKeyRequest.expiration:type_name -> google.protobuf.Timestamp
0, // 4: headscale.v1.CreatePreAuthKeyResponse.pre_auth_key:type_name -> headscale.v1.PreAuthKey
0, // 5: headscale.v1.ListPreAuthKeysResponse.pre_auth_keys:type_name -> headscale.v1.PreAuthKey
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_headscale_v1_preauthkey_proto_init() }
func file_headscale_v1_preauthkey_proto_init() {
if File_headscale_v1_preauthkey_proto != nil {
return
}
file_headscale_v1_user_proto_init()
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_preauthkey_proto_rawDesc), len(file_headscale_v1_preauthkey_proto_rawDesc)),
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_preauthkey_proto_goTypes,
DependencyIndexes: file_headscale_v1_preauthkey_proto_depIdxs,
MessageInfos: file_headscale_v1_preauthkey_proto_msgTypes,
}.Build()
File_headscale_v1_preauthkey_proto = out.File
file_headscale_v1_preauthkey_proto_goTypes = nil
file_headscale_v1_preauthkey_proto_depIdxs = nil
}
+615
View File
@@ -0,0 +1,615 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: headscale/v1/user.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type User struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
ProviderId string `protobuf:"bytes,6,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"`
Provider string `protobuf:"bytes,7,opt,name=provider,proto3" json:"provider,omitempty"`
ProfilePicUrl string `protobuf:"bytes,8,opt,name=profile_pic_url,json=profilePicUrl,proto3" json:"profile_pic_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *User) Reset() {
*x = User{}
mi := &file_headscale_v1_user_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *User) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*User) ProtoMessage() {}
func (x *User) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use User.ProtoReflect.Descriptor instead.
func (*User) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{0}
}
func (x *User) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
func (x *User) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *User) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *User) GetDisplayName() string {
if x != nil {
return x.DisplayName
}
return ""
}
func (x *User) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
func (x *User) GetProviderId() string {
if x != nil {
return x.ProviderId
}
return ""
}
func (x *User) GetProvider() string {
if x != nil {
return x.Provider
}
return ""
}
func (x *User) GetProfilePicUrl() string {
if x != nil {
return x.ProfilePicUrl
}
return ""
}
type CreateUserRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
PictureUrl string `protobuf:"bytes,4,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateUserRequest) Reset() {
*x = CreateUserRequest{}
mi := &file_headscale_v1_user_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateUserRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateUserRequest) ProtoMessage() {}
func (x *CreateUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead.
func (*CreateUserRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{1}
}
func (x *CreateUserRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *CreateUserRequest) GetDisplayName() string {
if x != nil {
return x.DisplayName
}
return ""
}
func (x *CreateUserRequest) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
func (x *CreateUserRequest) GetPictureUrl() string {
if x != nil {
return x.PictureUrl
}
return ""
}
type CreateUserResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateUserResponse) Reset() {
*x = CreateUserResponse{}
mi := &file_headscale_v1_user_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateUserResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateUserResponse) ProtoMessage() {}
func (x *CreateUserResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateUserResponse.ProtoReflect.Descriptor instead.
func (*CreateUserResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{2}
}
func (x *CreateUserResponse) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
type RenameUserRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
OldId uint64 `protobuf:"varint,1,opt,name=old_id,json=oldId,proto3" json:"old_id,omitempty"`
NewName string `protobuf:"bytes,2,opt,name=new_name,json=newName,proto3" json:"new_name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RenameUserRequest) Reset() {
*x = RenameUserRequest{}
mi := &file_headscale_v1_user_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RenameUserRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RenameUserRequest) ProtoMessage() {}
func (x *RenameUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RenameUserRequest.ProtoReflect.Descriptor instead.
func (*RenameUserRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{3}
}
func (x *RenameUserRequest) GetOldId() uint64 {
if x != nil {
return x.OldId
}
return 0
}
func (x *RenameUserRequest) GetNewName() string {
if x != nil {
return x.NewName
}
return ""
}
type RenameUserResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RenameUserResponse) Reset() {
*x = RenameUserResponse{}
mi := &file_headscale_v1_user_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RenameUserResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RenameUserResponse) ProtoMessage() {}
func (x *RenameUserResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RenameUserResponse.ProtoReflect.Descriptor instead.
func (*RenameUserResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{4}
}
func (x *RenameUserResponse) GetUser() *User {
if x != nil {
return x.User
}
return nil
}
type DeleteUserRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteUserRequest) Reset() {
*x = DeleteUserRequest{}
mi := &file_headscale_v1_user_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteUserRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteUserRequest) ProtoMessage() {}
func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead.
func (*DeleteUserRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{5}
}
func (x *DeleteUserRequest) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
type DeleteUserResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteUserResponse) Reset() {
*x = DeleteUserResponse{}
mi := &file_headscale_v1_user_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteUserResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteUserResponse) ProtoMessage() {}
func (x *DeleteUserResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteUserResponse.ProtoReflect.Descriptor instead.
func (*DeleteUserResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{6}
}
type ListUsersRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListUsersRequest) Reset() {
*x = ListUsersRequest{}
mi := &file_headscale_v1_user_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListUsersRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListUsersRequest) ProtoMessage() {}
func (x *ListUsersRequest) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead.
func (*ListUsersRequest) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{7}
}
func (x *ListUsersRequest) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
func (x *ListUsersRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ListUsersRequest) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
type ListUsersResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListUsersResponse) Reset() {
*x = ListUsersResponse{}
mi := &file_headscale_v1_user_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListUsersResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListUsersResponse) ProtoMessage() {}
func (x *ListUsersResponse) ProtoReflect() protoreflect.Message {
mi := &file_headscale_v1_user_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead.
func (*ListUsersResponse) Descriptor() ([]byte, []int) {
return file_headscale_v1_user_proto_rawDescGZIP(), []int{8}
}
func (x *ListUsersResponse) GetUsers() []*User {
if x != nil {
return x.Users
}
return nil
}
var File_headscale_v1_user_proto protoreflect.FileDescriptor
const file_headscale_v1_user_proto_rawDesc = "" +
"\n" +
"\x17headscale/v1/user.proto\x12\fheadscale.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x83\x02\n" +
"\x04User\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x129\n" +
"\n" +
"created_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12!\n" +
"\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x14\n" +
"\x05email\x18\x05 \x01(\tR\x05email\x12\x1f\n" +
"\vprovider_id\x18\x06 \x01(\tR\n" +
"providerId\x12\x1a\n" +
"\bprovider\x18\a \x01(\tR\bprovider\x12&\n" +
"\x0fprofile_pic_url\x18\b \x01(\tR\rprofilePicUrl\"\x81\x01\n" +
"\x11CreateUserRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12!\n" +
"\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12\x14\n" +
"\x05email\x18\x03 \x01(\tR\x05email\x12\x1f\n" +
"\vpicture_url\x18\x04 \x01(\tR\n" +
"pictureUrl\"<\n" +
"\x12CreateUserResponse\x12&\n" +
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\"E\n" +
"\x11RenameUserRequest\x12\x15\n" +
"\x06old_id\x18\x01 \x01(\x04R\x05oldId\x12\x19\n" +
"\bnew_name\x18\x02 \x01(\tR\anewName\"<\n" +
"\x12RenameUserResponse\x12&\n" +
"\x04user\x18\x01 \x01(\v2\x12.headscale.v1.UserR\x04user\"#\n" +
"\x11DeleteUserRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\"\x14\n" +
"\x12DeleteUserResponse\"L\n" +
"\x10ListUsersRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
"\x05email\x18\x03 \x01(\tR\x05email\"=\n" +
"\x11ListUsersResponse\x12(\n" +
"\x05users\x18\x01 \x03(\v2\x12.headscale.v1.UserR\x05usersB)Z'github.com/juanfont/headscale/gen/go/v1b\x06proto3"
var (
file_headscale_v1_user_proto_rawDescOnce sync.Once
file_headscale_v1_user_proto_rawDescData []byte
)
func file_headscale_v1_user_proto_rawDescGZIP() []byte {
file_headscale_v1_user_proto_rawDescOnce.Do(func() {
file_headscale_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_headscale_v1_user_proto_rawDesc), len(file_headscale_v1_user_proto_rawDesc)))
})
return file_headscale_v1_user_proto_rawDescData
}
var file_headscale_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_headscale_v1_user_proto_goTypes = []any{
(*User)(nil), // 0: headscale.v1.User
(*CreateUserRequest)(nil), // 1: headscale.v1.CreateUserRequest
(*CreateUserResponse)(nil), // 2: headscale.v1.CreateUserResponse
(*RenameUserRequest)(nil), // 3: headscale.v1.RenameUserRequest
(*RenameUserResponse)(nil), // 4: headscale.v1.RenameUserResponse
(*DeleteUserRequest)(nil), // 5: headscale.v1.DeleteUserRequest
(*DeleteUserResponse)(nil), // 6: headscale.v1.DeleteUserResponse
(*ListUsersRequest)(nil), // 7: headscale.v1.ListUsersRequest
(*ListUsersResponse)(nil), // 8: headscale.v1.ListUsersResponse
(*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
}
var file_headscale_v1_user_proto_depIdxs = []int32{
9, // 0: headscale.v1.User.created_at:type_name -> google.protobuf.Timestamp
0, // 1: headscale.v1.CreateUserResponse.user:type_name -> headscale.v1.User
0, // 2: headscale.v1.RenameUserResponse.user:type_name -> headscale.v1.User
0, // 3: headscale.v1.ListUsersResponse.users:type_name -> headscale.v1.User
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_headscale_v1_user_proto_init() }
func file_headscale_v1_user_proto_init() {
if File_headscale_v1_user_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_headscale_v1_user_proto_rawDesc), len(file_headscale_v1_user_proto_rawDesc)),
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_headscale_v1_user_proto_goTypes,
DependencyIndexes: file_headscale_v1_user_proto_depIdxs,
MessageInfos: file_headscale_v1_user_proto_msgTypes,
}.Build()
File_headscale_v1_user_proto = out.File
file_headscale_v1_user_proto_goTypes = nil
file_headscale_v1_user_proto_depIdxs = nil
}
@@ -0,0 +1,44 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/apikey.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
@@ -0,0 +1,44 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/auth.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
@@ -0,0 +1,44 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/device.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/node.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
@@ -0,0 +1,44 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/policy.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
@@ -0,0 +1,44 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/preauthkey.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
@@ -0,0 +1,44 @@
{
"swagger": "2.0",
"info": {
"title": "headscale/v1/user.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"protobufAny": {
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
+63 -76
View File
@@ -1,6 +1,6 @@
module github.com/juanfont/headscale
go 1.26.4
go 1.26.2
require (
github.com/arl/statsviz v0.8.0
@@ -8,55 +8,54 @@ require (
github.com/chasefleming/elem-go v0.31.0
github.com/coder/websocket v1.8.14
github.com/coreos/go-oidc/v3 v3.18.0
github.com/creachadair/command v0.2.6
github.com/creachadair/flax v0.0.6
github.com/creachadair/command v0.2.2
github.com/creachadair/flax v0.0.5
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/docker/docker v28.5.2+incompatible
github.com/fsnotify/fsnotify v1.10.1
github.com/fsnotify/fsnotify v1.9.0
github.com/glebarez/sqlite v1.11.0
github.com/go-chi/chi/v5 v5.3.0
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/metrics v0.1.1
github.com/go-faster/errors v0.7.1
github.com/go-faster/jx v1.2.0
github.com/go-gormigrate/gormigrate/v2 v2.1.6
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6
github.com/go-gormigrate/gormigrate/v2 v2.1.5
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433
github.com/gofrs/uuid/v5 v5.4.0
github.com/google/go-cmp v0.7.0
github.com/gorilla/mux v1.8.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jagottsicher/termcolor v1.0.2
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
github.com/ogen-go/ogen v1.22.0
github.com/ory/dockertest/v3 v3.12.0
github.com/philip-bui/grpc-zerolog v1.0.1
github.com/pkg/profile v1.7.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.68.1
github.com/prometheus/common v0.67.5
github.com/pterm/pterm v0.12.83
github.com/puzpuzpuz/xsync/v4 v4.5.0
github.com/realclientip/realclientip-go v1.0.0
github.com/rs/zerolog v1.35.1
github.com/puzpuzpuz/xsync/v4 v4.4.0
github.com/rs/zerolog v1.35.0
github.com/samber/lo v1.53.0
github.com/sasha-s/go-deadlock v0.3.9
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
github.com/tailscale/squibble v0.0.0-20260411062017-141f5d618bc4
github.com/tailscale/tailsql v0.0.0-20260522170732-77aec5aabc76
github.com/tailscale/squibble v0.0.0-20260303070345-3ac5157f405e
github.com/tailscale/tailsql v0.0.0-20260322172246-3ab0c1744d9c
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.53.0
golang.org/x/exp v0.0.0-20260603202125-055de637280b
golang.org/x/net v0.56.0
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
golang.org/x/net v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.21.0
golang.org/x/sync v0.20.0
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
pgregory.net/rapid v1.3.0
tailscale.com v1.101.0-pre
pgregory.net/rapid v1.2.0
tailscale.com v1.97.0-pre.0.20260429005429-40088602c960
zombiezen.com/go/postgrestest v1.0.1
)
@@ -78,10 +77,10 @@ require (
// together, e.g:
// go get modernc.org/libc@v1.55.3 modernc.org/sqlite@v1.33.1
require (
modernc.org/libc v1.72.3 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.52.0
modernc.org/sqlite v1.48.2
)
// NOTE: gvisor must be updated in lockstep with
@@ -94,7 +93,7 @@ require gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect
require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.10 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
@@ -127,54 +126,49 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/containerd/continuity v0.5.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/creachadair/mds v0.29.0 // indirect
github.com/creachadair/mds v0.26.2 // indirect
github.com/creachadair/msync v0.8.2 // indirect
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/docker/cli v29.5.3+incompatible // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/cli v29.4.0+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gaissmai/bart v0.26.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-faster/yaml v0.4.6 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/pprof v0.0.0-20260604005048-7023385849c0 // indirect
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gookit/color v1.6.1 // indirect
github.com/gookit/color v1.6.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@@ -182,18 +176,18 @@ require (
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kamstrup/intmap v0.5.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/lib/pq v1.11.1 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/term v0.5.2 // indirect
@@ -203,19 +197,18 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runc v1.3.2 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterbourgon/ff/v3 v3.4.0 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/pires/go-proxyproto v0.9.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus-community/pro-bing v0.7.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/safchain/ethtool v0.7.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/spf13/afero v1.15.0 // indirect
@@ -225,46 +218,40 @@ require (
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/setec v0.0.0-20260310221408-dcd97e42f251 // indirect
github.com/tailscale/setec v0.0.0-20260115174028-19d190c5556d // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20251127225136-f19339b67368 // indirect
github.com/tailscale/wireguard-go v0.0.0-20260527010701-b48af7099cad // indirect
github.com/tailscale/wireguard-go v0.0.0-20260304043104-4184faf59e56 // indirect
github.com/toqueteos/webbrowser v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.8.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
golang.org/x/exp/typeparams v0.0.0-20260603202125-055de637280b // indirect
golang.org/x/image v0.41.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/image v0.27.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.46.0 // indirect
golang.org/x/tools v0.44.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
k8s.io/client-go v0.34.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect
)
tool (
github.com/ogen-go/ogen/cmd/ogen
golang.org/x/tools/cmd/stress
golang.org/x/tools/cmd/stringer
tailscale.com/cmd/tailscale

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