mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-18 22:10:44 +09:00
Compare commits
277 Commits
tmp-prerework
...
vouch
| Author | SHA1 | Date | |
|---|---|---|---|
| 3566399f05 | |||
| 9d51a45093 | |||
| 048308511c | |||
| a84945f134 | |||
| fef80f3eb0 | |||
| 01ef350d5f | |||
| f4fba32dc6 | |||
| fc6f216b61 | |||
| de9db9c811 | |||
| 4946d1c88d | |||
| c497612c99 | |||
| 4e4512c4b7 | |||
| 08956d51a4 | |||
| d528686f14 | |||
| 6f317c7576 | |||
| 518b497be9 | |||
| 8d91e42a9f | |||
| 99fd62477d | |||
| c7b162509c | |||
| a4ec76605e | |||
| 1867213b69 | |||
| 622e08f5e6 | |||
| 79bf201ccb | |||
| af46fe8867 | |||
| b05f8875c3 | |||
| 84715e976c | |||
| ee95cf58d9 | |||
| 7b1776a87e | |||
| f6865fb40c | |||
| 6e0814a845 | |||
| 6e6ea3758d | |||
| 3846c3f148 | |||
| fe536dd799 | |||
| 5aeeff98d0 | |||
| 66937040f2 | |||
| 036760df02 | |||
| 83d3828ba1 | |||
| dd9f0a3f4f | |||
| 961b15313f | |||
| 475a5ead0c | |||
| ec0cb6f63a | |||
| a066a06bef | |||
| 33a65052c9 | |||
| 13ffd958ed | |||
| d5555c6b8c | |||
| 8898c69d0a | |||
| e2e9fe3081 | |||
| 3cddc07026 | |||
| b3472bb8dd | |||
| 6b413fe237 | |||
| ff7dd807fe | |||
| fe08d43978 | |||
| 2ff342764f | |||
| 3d811f29a6 | |||
| 4fa6af0102 | |||
| 96d736ec23 | |||
| 339cb392a9 | |||
| 24dfcf178f | |||
| 8f314797ce | |||
| 2632006f5f | |||
| 07b3a8ebd3 | |||
| 3c504af2be | |||
| 7af39a6798 | |||
| 49d6949639 | |||
| d7150755d4 | |||
| 88234d4046 | |||
| 15781ed6d8 | |||
| 72adc5ff2a | |||
| 560b6d81ad | |||
| 8efa5ad1fe | |||
| ba90048cfb | |||
| 1572ac551d | |||
| c9bbb5bdec | |||
| 97eff90ebe | |||
| a00de89c85 | |||
| f5198841bd | |||
| dfc25f06e1 | |||
| 74928c0241 | |||
| 9aba06ec7d | |||
| 00afce77b1 | |||
| 9f1886f587 | |||
| ea5165e325 | |||
| 368b9e7edd | |||
| abfd5c9236 | |||
| e4cd846075 | |||
| 4b693a1320 | |||
| 4f4e95fc80 | |||
| 27468f944b | |||
| ac725f27e4 | |||
| 60f0544b78 | |||
| ea5e52f662 | |||
| 722a2f1e18 | |||
| 75b09cb991 | |||
| 0320b56911 | |||
| 62869f01d2 | |||
| 19d5d9deeb | |||
| c3cfbf1cc0 | |||
| fdc7e26c8a | |||
| 45e6e90d11 | |||
| 468f70a9e5 | |||
| 8c10a96921 | |||
| 3979887d8d | |||
| 03e2d24c79 | |||
| e6ef1dda4d | |||
| 145672f0b6 | |||
| 77f289a627 | |||
| 55b9e3cfda | |||
| b56326427c | |||
| 0b7c8aa270 | |||
| b88a3cb7b2 | |||
| ed6596f9d7 | |||
| f49f27adf8 | |||
| 43ba20bebb | |||
| c8bd4a0947 | |||
| 84168fb90a | |||
| b0c221f3a1 | |||
| 68a6d3cf17 | |||
| 1689478485 | |||
| a1d3e98255 | |||
| b83bf3f993 | |||
| 96d2e6ed60 | |||
| 9b8949727d | |||
| bff216a184 | |||
| fd08b8fa8c | |||
| a73d38bb3f | |||
| e759d9fc90 | |||
| 0961e79e16 | |||
| a5ef3aff15 | |||
| 4da06925d0 | |||
| 21058d1142 | |||
| c5f3d5c28d | |||
| 40ed210521 | |||
| f4eeb94b1c | |||
| 7918187e7a | |||
| f497b4efd7 | |||
| 759381ad78 | |||
| 71a4ce3c9f | |||
| f585f8a94d | |||
| 88044f43ff | |||
| 5e05652a78 | |||
| 020560fc5f | |||
| fad8f2a729 | |||
| 0121083b53 | |||
| 9fc88e308f | |||
| c483bebba8 | |||
| 4914f9f2fd | |||
| 8237ac662a | |||
| cd1c208980 | |||
| 0fdff0c79b | |||
| eb57a3a62b | |||
| 56cd3eb24d | |||
| 8f75ee5647 | |||
| efdd9463e9 | |||
| a518a5076a | |||
| 2c9164b1c4 | |||
| 0e7b154617 | |||
| ad2693ff13 | |||
| 7d845ef65e | |||
| 9f0c74e73a | |||
| e413919810 | |||
| 0921972f96 | |||
| 99ad555d64 | |||
| 10696fa634 | |||
| 84c99023e5 | |||
| ba54349176 | |||
| 08f186f22a | |||
| 017162dac1 | |||
| bb06b90543 | |||
| 5a70a72988 | |||
| ec94573258 | |||
| 4c165ae5e7 | |||
| 06d6816dc9 | |||
| f61753e737 | |||
| 4f67300005 | |||
| 2e2401833b | |||
| 29f87e5eaa | |||
| f61d21b4a7 | |||
| b892b8f254 | |||
| 6777a82ee6 | |||
| 5228cb1a40 | |||
| cffdb77c8b | |||
| 7706552c99 | |||
| bceac495f9 | |||
| 171fd7a3c5 | |||
| ea8fc72570 | |||
| 77ba225cdb | |||
| 4483fd0cad | |||
| 66a5f99bfa | |||
| 2e49f3dc67 | |||
| 79562b9782 | |||
| 58a85b68b3 | |||
| eb23c125fa | |||
| 570735f204 | |||
| 78570c754f | |||
| 25adfaf341 | |||
| be90910d33 | |||
| 575d8ecbfd | |||
| e4e742c776 | |||
| 4cca63155d | |||
| 17236fd284 | |||
| 3e2aa5814e | |||
| f905d58292 | |||
| e00c899219 | |||
| 64c398f2c2 | |||
| 7f02210863 | |||
| e07b39108f | |||
| e285f3c932 | |||
| 355733342f | |||
| f3f84a5a63 | |||
| 4eb5899154 | |||
| e2f2f9211f | |||
| c7630b505b | |||
| de6be71a86 | |||
| fb8eecae25 | |||
| a345a22a3b | |||
| dfcc96d808 | |||
| 78fd6efb38 | |||
| eec3844f24 | |||
| 98e9ff4d36 | |||
| 4d3b567149 | |||
| 963daf8908 | |||
| c6c29c05e5 | |||
| 1f48ebb376 | |||
| b5b786f519 | |||
| 2cb914df59 | |||
| e5fcd01ee6 | |||
| af7e7a4560 | |||
| b1196baf6d | |||
| 7a20db9f49 | |||
| e78a24b892 | |||
| 574a61852a | |||
| 92a9accfcb | |||
| 26eebcea5a | |||
| 013dea4f40 | |||
| 6a0a297c7f | |||
| d600090f2c | |||
| 4ad200ab73 | |||
| 5d502bfb88 | |||
| 64d13f77e8 | |||
| 408f4022e4 | |||
| 8ea4cd3faa | |||
| 5ebc53c29e | |||
| b3f795f0b4 | |||
| 078b9e308f | |||
| 3f73ed5404 | |||
| 6fcff9e352 | |||
| a4f05b0962 | |||
| c4ab267c36 | |||
| 109bfc404c | |||
| 1a64d950fd | |||
| edb7ad0f81 | |||
| 892ffffc4a | |||
| e13f0458bb | |||
| 68b0014871 | |||
| 484462898b | |||
| 45b698dbac | |||
| 14ce7e9106 | |||
| 84c7f0d450 | |||
| c7f221dd0a | |||
| 163363a12a | |||
| f03d41ea9a | |||
| d5b2837231 | |||
| e4e209f919 | |||
| f172dba0e3 | |||
| c0774a739b | |||
| 7bc701179b | |||
| b29ae25356 | |||
| 56146de377 | |||
| c3df84e354 | |||
| 795a1efe9b | |||
| dc733767c4 | |||
| 542091e82b | |||
| 6cd919d411 | |||
| 2f907edf87 | |||
| 9621a97ebe | |||
| e470774f6a | |||
| 980622e9a5 |
+1
-10
@@ -1,10 +1 @@
|
||||
* @juanfont @kradalby
|
||||
|
||||
*.md @ohdearaugustin @nblock
|
||||
*.yml @ohdearaugustin @nblock
|
||||
*.yaml @ohdearaugustin @nblock
|
||||
Dockerfile* @ohdearaugustin @nblock
|
||||
.goreleaser.yaml @ohdearaugustin @nblock
|
||||
/docs/ @ohdearaugustin @nblock
|
||||
/.github/workflows/ @ohdearaugustin @nblock
|
||||
/.github/renovate.json @ohdearaugustin @nblock
|
||||
* @juanfont @kradalby @nblock
|
||||
@@ -0,0 +1,119 @@
|
||||
# Vouched (or actively denounced) users for this repository.
|
||||
#
|
||||
# Headscale gates pull requests to vouched contributors only. Issues
|
||||
# remain open to everyone — see CONTRIBUTING.md for the rationale and
|
||||
# the process for getting vouched.
|
||||
#
|
||||
# A denounced user (prefix `-`) is explicitly blocked from contributing.
|
||||
#
|
||||
# Syntax:
|
||||
# - One handle per line (without @). Sorted alphabetically.
|
||||
# - Optionally specify platform: `platform:username` (e.g., `github:nblock`).
|
||||
# - To denounce, prefix with minus: `-username` or `-platform:username`.
|
||||
# - Optionally, add a comment after a space following the handle.
|
||||
#
|
||||
# Maintainers can vouch for new contributors by commenting `!vouch` (or
|
||||
# `!vouch @username`) on any issue or PR by the author. `!denounce` blocks.
|
||||
|
||||
aalmenar
|
||||
adoolaard
|
||||
adriangoransson
|
||||
Aluxima
|
||||
AlynxZhou
|
||||
apollo13
|
||||
aradng
|
||||
ArcticLampyrid
|
||||
bobelev
|
||||
ChibangLW
|
||||
christian-heusel
|
||||
ckiee
|
||||
codethief
|
||||
cure
|
||||
dmeremyanin
|
||||
dotlambda
|
||||
dragetd
|
||||
e-zk
|
||||
edvardvb
|
||||
enoperm
|
||||
EtaoinWu
|
||||
evenh
|
||||
eyJhb
|
||||
felixscheinost
|
||||
FlorinPeter
|
||||
fredrikekre
|
||||
griffer
|
||||
GrigoriyMikhalkin
|
||||
gymnae
|
||||
huskyii
|
||||
igrikus
|
||||
ImpostorKeanu
|
||||
isaac-mcfadyen
|
||||
iSchluff
|
||||
ItalyPaleAle
|
||||
ItsShadowCone
|
||||
jdewinne
|
||||
joachimtingvold
|
||||
jonathanspw
|
||||
juanfont
|
||||
kazauwa
|
||||
korfuri
|
||||
kradalby
|
||||
linsomniac
|
||||
lyc8503
|
||||
madjam002
|
||||
majst01
|
||||
maprambo
|
||||
marcelmindemann
|
||||
maxpain
|
||||
mevansam
|
||||
mike-lloyd03
|
||||
motiejus
|
||||
mpldr
|
||||
nblock
|
||||
negbie
|
||||
Nemo157
|
||||
nicka101
|
||||
Niek
|
||||
nielscil
|
||||
NoelGisler
|
||||
noseshimself
|
||||
ohdearaugustin
|
||||
oplik0
|
||||
Orhideous
|
||||
pallabpain
|
||||
philippderdiedas
|
||||
Polsaker
|
||||
PowershellScripter
|
||||
ptman
|
||||
puzpuzpuz
|
||||
pvinis
|
||||
qbit
|
||||
QEDeD
|
||||
qoke
|
||||
Qup42
|
||||
redstonekasi
|
||||
restanrm
|
||||
reynico
|
||||
Riezebos
|
||||
samson4649
|
||||
SlackingVeteran
|
||||
spymobilfon
|
||||
stavros-k
|
||||
stblassitude
|
||||
SuperSandro2000
|
||||
SysAdminSmith
|
||||
sysvinit
|
||||
teleclimber
|
||||
tgrushka
|
||||
thebigbone
|
||||
tianon
|
||||
TotoTheDragon
|
||||
tsujamin
|
||||
unreality
|
||||
vbrandl
|
||||
vdovhanych
|
||||
victorhooi
|
||||
vsychov
|
||||
yaneony
|
||||
yaroslavkasatikov
|
||||
Zottelchen
|
||||
@@ -0,0 +1,25 @@
|
||||
Hi @{author}, thanks for your interest in headscale.
|
||||
|
||||
Headscale gates pull requests to vouched contributors only. Your account
|
||||
isn't on the list yet, so this PR has been auto-closed. This is **not** a
|
||||
judgment of you or this change.
|
||||
|
||||
Headscale is an "Open Source, acknowledged contribution" project: every
|
||||
contribution starts with a discussion on the issue tracker, **before** any
|
||||
code is written. See [`CONTRIBUTING.md`](https://github.com/juanfont/headscale/blob/main/CONTRIBUTING.md)
|
||||
for the full policy and the rationale.
|
||||
|
||||
To get vouched:
|
||||
|
||||
1. Open an issue describing the change you'd like to make (use case,
|
||||
implementation sketch, who will maintain it). For bug fixes, a clear
|
||||
reproduction is enough.
|
||||
2. Engage with maintainers on the issue. Once we've agreed on scope and
|
||||
approach, a maintainer will `!vouch @{author}`.
|
||||
3. You can then reopen this PR (or open a new one) and it will pass the
|
||||
gate.
|
||||
|
||||
Issues remain open to everyone — no vouching needed there, and that's the
|
||||
intended starting point for any contribution.
|
||||
|
||||
The vouched list: [`.github/VOUCHED.td`](https://github.com/juanfont/headscale/blob/main/.github/VOUCHED.td).
|
||||
@@ -19,4 +19,4 @@ If you find mistakes in the documentation, please submit a fix to the documentat
|
||||
- [ ] updated documentation if needed
|
||||
- [ ] updated CHANGELOG.md
|
||||
|
||||
<!-- If applicable, please reference the issue using `Fixes #XXX` and add tests to cover your new code. -->
|
||||
<!-- If applicable, please reference the issue using `Fixes #XXX` and add tests to cover your new code. -->
|
||||
+23
-28
@@ -10,6 +10,10 @@ concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
build-nix:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -29,33 +33,24 @@ jobs:
|
||||
- '**/*.go'
|
||||
- 'integration_test/'
|
||||
- 'config-example.yaml'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
|
||||
'**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Run nix build
|
||||
id: build
|
||||
- name: Check vendor hash
|
||||
id: vendorhash
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
nix build |& tee build-result
|
||||
BUILD_STATUS="${PIPESTATUS[0]}"
|
||||
go run ./cmd/vendorhash check | tee check-result
|
||||
{
|
||||
grep '^expected_sri=' check-result || true
|
||||
grep '^actual_sri=' check-result || true
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
OLD_HASH=$(cat build-result | grep specified: | awk -F ':' '{print $2}' | sed 's/ //g')
|
||||
NEW_HASH=$(cat build-result | grep got: | awk -F ':' '{print $2}' | sed 's/ //g')
|
||||
|
||||
echo "OLD_HASH=$OLD_HASH" >> $GITHUB_OUTPUT
|
||||
echo "NEW_HASH=$NEW_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
exit $BUILD_STATUS
|
||||
|
||||
- name: Nix gosum diverging
|
||||
- name: Vendor hash diverging
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
if: failure() && steps.build.outcome == 'failure'
|
||||
if: failure() && steps.vendorhash.outcome == 'failure'
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
@@ -63,9 +58,13 @@ jobs:
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Nix build failed with wrong gosum, please update "vendorSha256" (${{ steps.build.outputs.OLD_HASH }}) for the "headscale" package in flake.nix with the new SHA: ${{ steps.build.outputs.NEW_HASH }}'
|
||||
body: 'Vendor hash in `flakehashes.json` is stale (was `${{ steps.vendorhash.outputs.expected_sri }}`, should be `${{ steps.vendorhash.outputs.actual_sri }}`). Run `go run ./cmd/vendorhash update` and commit the result.'
|
||||
})
|
||||
|
||||
- name: Run nix build
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: nix build --fallback
|
||||
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
with:
|
||||
@@ -82,17 +81,13 @@ jobs:
|
||||
- "GOARCH=amd64 GOOS=darwin"
|
||||
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 }}
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: Run go cross compile
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: env ${{ matrix.env }} nix develop --command -- go build -o "headscale"
|
||||
run: env ${{ matrix.env }} go build -o "headscale"
|
||||
./cmd/headscale
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
|
||||
@@ -12,6 +12,10 @@ concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
check-generated:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -28,20 +32,15 @@ jobs:
|
||||
- '*.nix'
|
||||
- 'go.*'
|
||||
- '**/*.go'
|
||||
- '**/*.proto'
|
||||
- 'buf.gen.yaml'
|
||||
- 'tools/**'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Run make generate
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: nix develop --command -- make generate
|
||||
run: make generate
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
|
||||
@@ -6,6 +6,10 @@ concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
check-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -24,19 +28,15 @@ jobs:
|
||||
- '**/*.go'
|
||||
- 'integration_test/'
|
||||
- 'config-example.yaml'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
|
||||
'**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Generate and check integration tests
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
nix develop --command bash -c "cd .github/workflows && go generate"
|
||||
(cd .github/workflows && go generate)
|
||||
git diff --exit-code .github/workflows/test-integration.yaml
|
||||
|
||||
- name: Show missing tests
|
||||
|
||||
@@ -16,6 +16,10 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
container:
|
||||
if: github.repository == 'juanfont/headscale'
|
||||
@@ -40,12 +44,8 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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 }}
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: Set commit timestamp
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> $GITHUB_ENV
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
KO_DEFAULTBASEIMAGE: gcr.io/distroless/base-debian13
|
||||
CGO_ENABLED: "0"
|
||||
run: |
|
||||
nix develop --command -- ko build \
|
||||
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 --command -- ko build \
|
||||
ko build \
|
||||
--bare \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--tags=main-${GITHUB_SHA::7},development \
|
||||
@@ -92,19 +92,15 @@ jobs:
|
||||
- name: Checkout
|
||||
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 }}
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: Build binary
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: nix develop --command -- go build -o headscale ./cmd/headscale
|
||||
run: go build -o headscale ./cmd/headscale
|
||||
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Cache GC
|
||||
|
||||
# Garbage collection for the hestia binary cache. Must run on the default
|
||||
# branch: a PR job's cache scope is read-only towards the default branch and
|
||||
# dies with the PR, but the default-branch scope grows forever without GC.
|
||||
|
||||
concurrency:
|
||||
group: hestia-gc
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Daily, off-peak (UTC).
|
||||
- cron: "23 3 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry-run:
|
||||
description: Plan only; do not repack, touch, or delete anything.
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
gc:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# REST cache deletes need actions:write.
|
||||
actions: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: Run garbage collection
|
||||
run: '"${HESTIA_BIN}" gc ${{ inputs.dry-run && ''--dry-run'' || '''' }}'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -51,6 +51,11 @@ 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:
|
||||
@@ -67,28 +72,33 @@ jobs:
|
||||
with:
|
||||
name: postgres-image
|
||||
path: /tmp/artifacts
|
||||
- name: Pin Docker to v28 (avoid v29 breaking changes)
|
||||
- name: Force overlay2 storage driver
|
||||
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 mkdir -p /etc/docker
|
||||
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
docker version
|
||||
- name: Load br_netfilter for in-cluster service routing
|
||||
if: inputs.test == 'TestK8sOperator'
|
||||
# TestK8sOperator runs k3s in a container; without br_netfilter on the
|
||||
# host, bridged pod-to-pod traffic skips kube-proxy's ClusterIP DNAT and
|
||||
# in-cluster DNS (kube-dns) is unreachable. The module cannot be loaded
|
||||
# from inside the unprivileged-module rancher/k3s image, so load it here.
|
||||
run: sudo modprobe br_netfilter
|
||||
- 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
|
||||
@@ -105,6 +115,12 @@ 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 }}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
golangci-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
files:
|
||||
- '*.nix'
|
||||
- 'go.*'
|
||||
- '**/*.go'
|
||||
- 'integration_test/'
|
||||
- 'config-example.yaml'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
|
||||
'**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: golangci-lint
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
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
|
||||
--output.text.print-issued-lines
|
||||
--output.text.colors
|
||||
|
||||
prettier-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
files:
|
||||
- '*.nix'
|
||||
- '**/*.md'
|
||||
- '**/*.yml'
|
||||
- '**/*.yaml'
|
||||
- '**/*.ts'
|
||||
- '**/*.js'
|
||||
- '**/*.sass'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '**/*.html'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
|
||||
'**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Prettify code
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Nix Flake Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Each job only runs `nix build .#checks.<system>.<name>`; the check logic lives
|
||||
# in flake.nix via the flake-checks library. The fileset-filtered checks hit the
|
||||
# hestia cache when their inputs are unchanged, so no changed-files gating.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: build
|
||||
run: nix build -L .#checks.x86_64-linux.build
|
||||
|
||||
gotest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: gotest
|
||||
run: nix build -L .#checks.x86_64-linux.gotest
|
||||
|
||||
golangci-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: golangci-lint
|
||||
run: nix build -L .#checks.x86_64-linux.golangci-lint
|
||||
|
||||
formatting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- name: formatting
|
||||
run: nix build -L .#checks.x86_64-linux.formatting
|
||||
@@ -38,18 +38,14 @@ jobs:
|
||||
- 'cmd/**'
|
||||
- 'hscontrol/**'
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
|
||||
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
|
||||
'**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Run NixOS module tests
|
||||
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
|
||||
nix build .#checks.x86_64-linux.headscale -L --fallback
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
- "*" # triggers only if push new tag version
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
if: github.repository == 'juanfont/headscale'
|
||||
@@ -17,25 +21,6 @@ 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:
|
||||
@@ -49,14 +34,10 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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 }}
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: Run goreleaser
|
||||
run: nix develop --command -- goreleaser release --clean
|
||||
run: goreleaser release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Server Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# hscontrol/servertest is excluded from the sandboxed gotest flake check: it is
|
||||
# slow (10s+ convergence cases) and timing-sensitive (race/stress/HA property
|
||||
# tests), so it runs here in the devShell with a generous timeout instead.
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
servertest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
|
||||
- name: go test ./hscontrol/servertest
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
run: go test -timeout=20m ./hscontrol/servertest/...
|
||||
@@ -6,9 +6,15 @@ on: [pull_request]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
defaults:
|
||||
run:
|
||||
shell: nix develop --fallback --command bash -e {0}
|
||||
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:
|
||||
@@ -33,23 +39,18 @@ jobs:
|
||||
- '.github/workflows/test-integration.yaml'
|
||||
- '.github/workflows/integration-test-template.yml'
|
||||
- 'Dockerfile.*'
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
- name: Build binaries and warm Go cache
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
# Build all Go binaries in one nix shell to maximize cache reuse
|
||||
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
|
||||
go test -c ./integration -o /dev/null 2>/dev/null || true
|
||||
'
|
||||
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
|
||||
go test -c ./integration -o /dev/null 2>/dev/null || true
|
||||
- name: Upload hi binary
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
@@ -69,25 +70,26 @@ jobs:
|
||||
name: go-cache
|
||||
path: go-cache.tar.gz
|
||||
retention-days: 10
|
||||
- name: Pin Docker to v28 (avoid v29 breaking changes)
|
||||
- name: Force overlay2 storage driver
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
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.
|
||||
# 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.
|
||||
# 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 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: Build headscale image
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
run: |
|
||||
@@ -123,25 +125,24 @@ jobs:
|
||||
needs: build
|
||||
if: needs.build.outputs.files-changed == 'true'
|
||||
steps:
|
||||
- name: Pin Docker to v28 (avoid v29 breaking changes)
|
||||
- name: Force overlay2 storage driver
|
||||
shell: bash
|
||||
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 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: Pull and save postgres image
|
||||
shell: bash
|
||||
run: |
|
||||
docker pull postgres:latest
|
||||
docker tag postgres:latest postgres:${{ github.sha }}
|
||||
@@ -152,9 +153,68 @@ jobs:
|
||||
name: postgres-image
|
||||
path: postgres-image.tar.gz
|
||||
retention-days: 10
|
||||
sqlite:
|
||||
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: NixOS/nix-installer-action@6b8548fe06acfb0155a50ab5d561accb215764cc # main
|
||||
- uses: Mic92/hestia/action@ff07bb902a9968ac0c3d0e51d90a606662a375d8 # main
|
||||
- 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=$(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]
|
||||
if: needs.build.outputs.files-changed == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -181,7 +241,7 @@ jobs:
|
||||
- TestACLDynamicUnknownUserRemoval
|
||||
- TestAPIAuthenticationBypass
|
||||
- TestAPIAuthenticationBypassCurl
|
||||
- TestGRPCAuthenticationBypass
|
||||
- TestRemoteCLIAuthenticationBypass
|
||||
- TestCLIWithConfigAuthenticationBypass
|
||||
- TestAuthKeyLogoutAndReloginSameUser
|
||||
- TestAuthKeyLogoutAndReloginNewUser
|
||||
@@ -202,18 +262,34 @@ jobs:
|
||||
- TestAuthWebFlowAuthenticationPingAll
|
||||
- TestAuthWebFlowLogoutAndReloginSameUser
|
||||
- TestAuthWebFlowLogoutAndReloginNewUser
|
||||
- TestUserCommand
|
||||
- TestPreAuthKeyCommand
|
||||
- TestPreAuthKeyCommandWithoutExpiry
|
||||
- TestPreAuthKeyCommandReusableEphemeral
|
||||
- TestPreAuthKeyCorrectUserLoggedInCommand
|
||||
- TestTaggedNodesCLIOutput
|
||||
- TestApiKeyCommand
|
||||
- TestApiKeyCommandValidation
|
||||
- TestAuthCommandValidation
|
||||
- TestNodeCommand
|
||||
- TestNodeExpireCommand
|
||||
- TestNodeRenameCommand
|
||||
- TestPreAuthKeyCorrectUserLoggedInCommand
|
||||
- TestTaggedNodesCLIOutput
|
||||
- TestNodeExpireFlagsCommand
|
||||
- TestNodeCommandValidation
|
||||
- TestNodeTagCommand
|
||||
- TestNodeRouteCommands
|
||||
- TestNodeBackfillIPsCommand
|
||||
- TestOAuthClientCommand
|
||||
- TestOAuthClientCommandValidation
|
||||
- TestPolicyCheckCommand
|
||||
- TestSSHTestsRejectFailingPolicy
|
||||
- TestPolicyCommand
|
||||
- TestPolicyBrokenConfigCommand
|
||||
- TestPreAuthKeyCommand
|
||||
- TestPreAuthKeyCommandWithoutExpiry
|
||||
- TestPreAuthKeyCommandReusableEphemeral
|
||||
- TestPreAuthKeyDeleteCommand
|
||||
- TestPreAuthKeyCommandValidation
|
||||
- TestServerInfoCommands
|
||||
- TestUserCommand
|
||||
- TestUserCreateCommand
|
||||
- TestUserCommandValidation
|
||||
- TestDERPVerifyEndpoint
|
||||
- TestResolveMagicDNS
|
||||
- TestResolveMagicDNSExtraRecordsPath
|
||||
@@ -235,6 +311,7 @@ jobs:
|
||||
- Test2118DeletingOnlineNodePanics
|
||||
- TestGrantCapRelay
|
||||
- TestGrantCapDrive
|
||||
- TestK8sOperator
|
||||
- TestEnablingRoutes
|
||||
- TestHASubnetRouterFailover
|
||||
- TestSubnetRouteACL
|
||||
@@ -267,6 +344,7 @@ jobs:
|
||||
- TestSSHCheckModeUnapprovedTimeout
|
||||
- TestSSHCheckModeCheckPeriodCLI
|
||||
- TestSSHCheckModeAutoApprove
|
||||
- TestSSHCheckModeSessionLossReDelegates
|
||||
- TestSSHCheckModeNegativeCLI
|
||||
- TestSSHLocalpart
|
||||
- TestTagsAuthKeyWithTagRequestDifferentTag
|
||||
@@ -301,6 +379,8 @@ jobs:
|
||||
- TestTagsAuthKeyWithoutUserInheritsTags
|
||||
- TestTagsAuthKeyWithoutUserRejectsAdvertisedTags
|
||||
- TestTagsAuthKeyConvertToUserViaCLIRegister
|
||||
- TestTS2021WebSocketGET
|
||||
- TestTS2021WASMClientUnderNode
|
||||
- TestTailscaleRustAxum
|
||||
uses: ./.github/workflows/integration-test-template.yml
|
||||
secrets: inherit
|
||||
@@ -309,7 +389,7 @@ jobs:
|
||||
postgres_flag: "--postgres=0"
|
||||
database_name: "sqlite"
|
||||
postgres:
|
||||
needs: [build, build-postgres]
|
||||
needs: [build, build-postgres, build-tailscale-released]
|
||||
if: needs.build.outputs.files-changed == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
files:
|
||||
- '*.nix'
|
||||
- 'go.*'
|
||||
- '**/*.go'
|
||||
- 'integration_test/'
|
||||
- 'config-example.yaml'
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
|
||||
'**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Run tests
|
||||
if: steps.changed-files.outputs.files == 'true'
|
||||
env:
|
||||
# As of 2025-01-06, these env vars was not automatically
|
||||
# set anymore which breaks the initdb for postgres on
|
||||
# some of the database migration tests.
|
||||
LC_ALL: "en_US.UTF-8"
|
||||
LC_CTYPE: "en_US.UTF-8"
|
||||
run: nix develop --command -- gotestsum
|
||||
@@ -1,19 +0,0 @@
|
||||
name: update-flake-lock
|
||||
on:
|
||||
workflow_dispatch: # allows manual triggering
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # runs weekly on Sunday at 00:00
|
||||
|
||||
jobs:
|
||||
lockfile:
|
||||
if: github.repository == 'juanfont/headscale'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17
|
||||
- name: Update flake.lock
|
||||
uses: DeterminateSystems/update-flake-lock@428c2b58a4b7414dabd372acb6a03dba1084d3ab # v25
|
||||
with:
|
||||
pr-title: "Update flake.lock"
|
||||
@@ -0,0 +1,30 @@
|
||||
name: "Vouch - Check PR"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VOUCH_APP_ID }}
|
||||
private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
sparse-checkout: .github/pr-unvouched-message
|
||||
|
||||
- uses: mitchellh/vouch/action/check-pr@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
|
||||
with:
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
auto-close: true
|
||||
template-file: .github/pr-unvouched-message
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
@@ -0,0 +1,36 @@
|
||||
name: "Vouch - Manage by Issue"
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
concurrency:
|
||||
group: vouch-manage
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
manage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VOUCH_APP_ID }}
|
||||
private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: mitchellh/vouch/action/manage-by-issue@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
|
||||
with:
|
||||
repo: ${{ github.repository }}
|
||||
issue-id: ${{ github.event.issue.number }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
vouch-keyword: "!vouch"
|
||||
denounce-keyword: "!denounce"
|
||||
unvouch-keyword: "!unvouch"
|
||||
pull-request: "true"
|
||||
merge-immediately: "true"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
@@ -0,0 +1,32 @@
|
||||
name: "Vouch - Sync CODEOWNERS"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: vouch-manage
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VOUCH_APP_ID }}
|
||||
private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: mitchellh/vouch/action/sync-codeowners@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
|
||||
with:
|
||||
repo: ${{ github.repository }}
|
||||
pull-request: "true"
|
||||
merge-immediately: "true"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
@@ -46,6 +46,9 @@ result
|
||||
|
||||
integration_test/etc/config.dump.yaml
|
||||
|
||||
# OpenAPI spec is served live from the code and emitted on demand, not committed
|
||||
/openapi/v1/headscale.yaml
|
||||
|
||||
# MkDocs
|
||||
.cache
|
||||
/site
|
||||
|
||||
@@ -13,6 +13,7 @@ linters:
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- godox
|
||||
- gomodguard
|
||||
- interfacebloat
|
||||
- ireturn
|
||||
- lll
|
||||
@@ -30,6 +31,17 @@ 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
|
||||
|
||||
+12
-2
@@ -2,8 +2,9 @@
|
||||
# See: https://prek.j178.dev/quickstart/
|
||||
# See: https://prek.j178.dev/builtin/
|
||||
|
||||
# Global exclusions - ignore generated code
|
||||
exclude: ^gen/
|
||||
# Global exclusions - ignore generated code (proto output and emitted OpenAPI)
|
||||
# and recorded golden fixtures.
|
||||
exclude: ^(gen|openapi)/|^hscontrol/testdata/apiv1_golden/
|
||||
|
||||
repos:
|
||||
# Built-in hooks from pre-commit/pre-commit-hooks
|
||||
@@ -13,6 +14,7 @@ repos:
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
args: [--maxkb=1024]
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-json
|
||||
@@ -60,3 +62,11 @@ repos:
|
||||
language: system
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
|
||||
# vendor-hash keeps flakehashes.json in sync with go.mod/go.sum.
|
||||
- id: vendor-hash
|
||||
name: vendor-hash
|
||||
entry: nix develop --command -- go run ./cmd/vendorhash check
|
||||
language: system
|
||||
files: ^(go\.mod|go\.sum|flakehashes\.json)$
|
||||
pass_filenames: false
|
||||
|
||||
@@ -275,6 +275,11 @@ Key reminders:
|
||||
`e = e.Str("k", v)`. Forgetting to reassign silently drops the field.
|
||||
- **Tests**: prefer `hscontrol/servertest/` for server-level tests that
|
||||
don't need Docker — faster than full integration tests.
|
||||
- **View types in read paths**: response serializers must read through
|
||||
`NodeView`/`UserView`/`PreAuthKeyView` accessors. `AsStruct()` clones the
|
||||
whole record on every read — it is only for DB-write/merge clones and mutable
|
||||
working copies, never to build an API response. `grep AsStruct hscontrol/api`
|
||||
must come back empty.
|
||||
|
||||
## Gotchas
|
||||
|
||||
|
||||
+224
-39
@@ -1,8 +1,71 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 0.29.0 (202x-xx-xx)
|
||||
## 0.30.0 (202x-xx-xx)
|
||||
|
||||
**Minimum supported Tailscale client version: v1.76.0**
|
||||
**Minimum supported Tailscale client version: v1.xx.0**
|
||||
|
||||
### v1 REST API replaced; gRPC and Protobuf removed
|
||||
|
||||
The v1 REST API now provides an OpenAPI 3.1 specification at
|
||||
`/api/v1/openapi.yaml`, with interactive documentation at `/api/v1/docs`. This
|
||||
replaces the Swagger 2.0 document and the `/swagger` UI. The Protobuf, gRPC and
|
||||
grpc-gateway stack behind it is gone, and the `headscale` CLI now talks to the
|
||||
HTTP API directly.
|
||||
|
||||
[#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
|
||||
### OAuth clients and scopes for the v2 API
|
||||
|
||||
The v2 API now authenticates with OAuth 2.0 client-credentials, the way the
|
||||
Tailscale ecosystem does. An OAuth client mints short-lived access tokens whose
|
||||
scopes limit which operations they may perform and whose tags limit the devices
|
||||
they may create, so a credential can be issued with only the access it needs.
|
||||
The `headscale oauth-clients` command manages them. This lets the Tailscale
|
||||
Terraform provider and Kubernetes operator drive Headscale unchanged; admin API
|
||||
keys remain all-access.
|
||||
|
||||
[#3334](https://github.com/juanfont/headscale/pull/3334)
|
||||
|
||||
### BREAKING
|
||||
|
||||
#### API
|
||||
|
||||
- The gRPC API is removed; all programmatic access now goes through the HTTP API at `/api/v1` [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- API errors are now RFC 7807 `application/problem+json`, including authentication failures, instead of the previous gRPC-status JSON shape [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- Errors that previously returned HTTP 500 — unknown users or nodes, malformed input, duplicate names — now return the correct 404, 400 or 409 [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- The OpenAPI document is OpenAPI 3.1 at `/api/v1/openapi.yaml` (docs at `/api/v1/docs`), replacing Swagger 2.0 at `/swagger` [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
|
||||
#### CLI
|
||||
|
||||
- `--output json` / `--output yaml` now emit the API's shape — camelCase fields, string-encoded IDs, RFC3339 timestamps — instead of the old Protobuf encoding [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- `headscale policy` renames the database-bypass flag from `--bypass-grpc-and-access-database-directly` to `--bypass-server-and-access-database-directly` [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
|
||||
### Changes
|
||||
|
||||
- Expiring or deleting a non-existent pre-auth key now returns an error instead of silently succeeding [#3324](https://github.com/juanfont/headscale/pull/3324)
|
||||
- Improve systemd service file hardening [#3341](https://github.com/juanfont/headscale/pull/3341)
|
||||
|
||||
## 0.29.2 (2026-07-01)
|
||||
|
||||
**Minimum supported Tailscale client version: v1.80.0**
|
||||
|
||||
### Changes
|
||||
|
||||
- Fix map generation serializing on the policy lock, so a mass reconnect on `autogroup:self`, via or relay policies no longer stalls clients into `unexpected EOF` retry loops [#3358](https://github.com/juanfont/headscale/pull/3358)
|
||||
- Fix `/ts2021` rejecting the WebSocket `GET` upgrade with 405, which prevented Tailscale JS/WASM control clients from connecting [#3359](https://github.com/juanfont/headscale/pull/3359)
|
||||
- Gracefully handle nodes with an invalid FQDN (empty or too long) instead of failing map delivery; offending names are logged at startup with the fix command [#3349](https://github.com/juanfont/headscale/pull/3349)
|
||||
|
||||
## 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**
|
||||
|
||||
### Tailscale ACL compatibility improvements
|
||||
|
||||
@@ -27,6 +90,50 @@ A new `headscale auth` CLI command group supports the approval flow:
|
||||
[#1850](https://github.com/juanfont/headscale/pull/1850)
|
||||
[#3180](https://github.com/juanfont/headscale/pull/3180)
|
||||
|
||||
### Policy tests (beta)
|
||||
|
||||
Headscale now evaluates the `tests` block in a policy file. Tests assert reachability between
|
||||
named sources and destinations and cover the whole policy — both `acls` and `grants` rules
|
||||
contribute. They run on user-initiated writes via `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.
|
||||
|
||||
At boot a stored policy whose tests 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.
|
||||
|
||||
This feature is **beta** while behavioural coverage against Tailscale SaaS broadens.
|
||||
|
||||
[#3229](https://github.com/juanfont/headscale/pull/3229)
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
@@ -36,33 +143,82 @@ 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 intentionally scary: accepting traffic from the entire
|
||||
all IPs (see BREAKING below). The name is intentional: accepting traffic from the entire
|
||||
internet is a security-sensitive choice. `autogroup:danger-all` can only be used as a source.
|
||||
|
||||
### Hostname handling (cleanroom rewrite)
|
||||
### Node attributes (`nodeAttrs`)
|
||||
|
||||
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)).
|
||||
ACL policies now accept a `nodeAttrs` block. Each entry hands a list of
|
||||
Tailscale node capabilities to every node matching `target`. The accepted
|
||||
target forms are the same as `acls.src` and `grants.src`: users, groups,
|
||||
tags, hosts, prefixes, `autogroup:member`, `autogroup:tagged`, and `*`.
|
||||
|
||||
What changed:
|
||||
```jsonc
|
||||
{
|
||||
"randomizeClientPort": true,
|
||||
"nodeAttrs": [
|
||||
{ "target": ["autogroup:tagged"], "attr": ["disable-captive-portal-detection"] },
|
||||
{ "target": ["alice@example.com"], "attr": ["nextdns:abc123"] },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
- 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).
|
||||
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
|
||||
reaches clients unchanged.
|
||||
|
||||
`randomizeClientPort` also lands as a top-level policy field that toggles
|
||||
the default for every node, replacing the old server-config knob.
|
||||
|
||||
A new `auto_update.enabled` config option controls the tailnet-wide
|
||||
default for client auto-update. When true, every node's CapMap carries
|
||||
`default-auto-update: [true]` so fresh clients pick up the default
|
||||
unless they make a local opt-in / opt-out choice.
|
||||
|
||||
Policies that use the `funnel` cap, `ipPool` blocks, or
|
||||
`autogroup:admin` / `autogroup:owner` targets are rejected at load —
|
||||
those features depend on machinery headscale does not yet ship.
|
||||
|
||||
[#3251](https://github.com/juanfont/headscale/pull/3251)
|
||||
|
||||
### Taildrive
|
||||
|
||||
Taildrive ([file-sync between
|
||||
nodes](https://tailscale.com/docs/features/taildrive)) is now
|
||||
configurable through policy. Grant `drive:share` to the node that
|
||||
hosts files and `drive:access` to nodes that read or write them; pair
|
||||
with a `tailscale.com/cap/drive` grant to set the per-share access
|
||||
mode:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"nodeAttrs": [
|
||||
{ "target": ["tag:fileserver"], "attr": ["drive:share"] },
|
||||
{ "target": ["autogroup:member"], "attr": ["drive:access"] },
|
||||
],
|
||||
"grants": [
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:fileserver"],
|
||||
"app": {
|
||||
"tailscale.com/cap/drive": [{ "shares": ["*"], "access": "rw" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
A wildcard `nodeAttrs` (`"target": ["*"]`) hands the caps to every
|
||||
node when fine-grained control is not needed.
|
||||
|
||||
### Hostname sanitisation
|
||||
|
||||
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.
|
||||
|
||||
Examples that previously regressed and now work:
|
||||
|
||||
@@ -75,11 +231,24 @@ 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.
|
||||
- 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)
|
||||
|
||||
#### ACL Policy
|
||||
|
||||
@@ -88,8 +257,8 @@ Examples that previously regressed and now work:
|
||||
- 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
|
||||
@@ -101,21 +270,32 @@ Examples that previously regressed and now work:
|
||||
- Downgrading to a previous minor version is blocked
|
||||
- Patch version changes within the same minor are always allowed
|
||||
|
||||
#### Configuration
|
||||
|
||||
- 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)
|
||||
Headscale refuses to start when the old key is set. Move it to the
|
||||
policy file referenced by `policy.path`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"randomizeClientPort": true,
|
||||
}
|
||||
```
|
||||
|
||||
If you do not have a policy file yet, create one with that minimal
|
||||
content and point `policy.path` at it. The default carries over —
|
||||
empty / absent policy means `randomizeClientPort: false`, matching
|
||||
the previous behaviour for operators who never set the key. Per-node
|
||||
opt-in via `nodeAttrs` is also supported and stacks on top of the
|
||||
global default.
|
||||
|
||||
#### CLI
|
||||
|
||||
- `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
|
||||
@@ -134,6 +314,7 @@ connected" routers that maintain their control session but cannot route packets.
|
||||
- Fix exit node approval not triggering filter rule recalculation for peers [#2180](https://github.com/juanfont/headscale/pull/2180)
|
||||
- Policy validation error messages now include field context (e.g., `src=`, `dst=`) and are more descriptive [#2180](https://github.com/juanfont/headscale/pull/2180)
|
||||
- Reject policies whose `user@` tokens match multiple DB users; rename the duplicate via `headscale users rename` to load [#3160](https://github.com/juanfont/headscale/issues/3160)
|
||||
- Evaluate the policy `tests` block on user-initiated writes across both `acls` and `grants`; reject policies whose tests fail (beta) [#1803](https://github.com/juanfont/headscale/issues/1803)
|
||||
|
||||
#### Grants
|
||||
|
||||
@@ -155,7 +336,8 @@ 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)
|
||||
- **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)
|
||||
- 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
|
||||
|
||||
@@ -174,6 +356,7 @@ 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
|
||||
|
||||
@@ -185,8 +368,10 @@ 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)
|
||||
- **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
|
||||
- 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)
|
||||
|
||||
## 0.28.0 (2026-02-04)
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# For testing purposes only
|
||||
|
||||
FROM golang:1.26.2-alpine AS build-env
|
||||
FROM golang:1.26.4-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.22
|
||||
FROM alpine:3.23
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
|
||||
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
|
||||
@@ -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.1-trixie AS builder
|
||||
FROM docker.io/golang:1.26.4-trixie AS builder
|
||||
ARG VERSION=dev
|
||||
ENV GOPATH /go
|
||||
WORKDIR /go/src/headscale
|
||||
|
||||
@@ -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.2-alpine AS build-env
|
||||
FROM golang:1.26.4-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.22
|
||||
FROM alpine:3.23
|
||||
# Upstream: ca-certificates ip6tables iptables iproute2
|
||||
# Tests: curl python3 (traceroute via BusyBox)
|
||||
RUN apk add --no-cache ca-certificates curl ip6tables iptables iproute2 python3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.94-bookworm AS builder
|
||||
FROM rust:1.95-trixie 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:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# For integration testing only.
|
||||
#
|
||||
# Builds the Tailscale control client (integration/wasmic/wasmclient) for
|
||||
# GOOS=js/GOARCH=wasm and packages it with Go's wasm_exec Node runner. The
|
||||
# container idles; the integration test execs
|
||||
# node /app/wasm_exec_node.js /app/client.wasm <control-url>
|
||||
# to drive a real browser-style WebSocket GET against headscale's /ts2021,
|
||||
# guarding the regression in issue #3357.
|
||||
|
||||
FROM golang:1.26.4-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Only the module metadata and the wasm client package are needed to build the
|
||||
# js/wasm binary; its imports (tailscale.com/control/controlhttp, ...) resolve
|
||||
# from the module proxy.
|
||||
COPY go.mod go.sum ./
|
||||
COPY integration/wasmic/wasmclient ./integration/wasmic/wasmclient
|
||||
|
||||
RUN GOOS=js GOARCH=wasm go build -o /out/client.wasm ./integration/wasmic/wasmclient \
|
||||
&& cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" /out/wasm_exec.js \
|
||||
&& cp "$(go env GOROOT)/lib/wasm/wasm_exec_node.js" /out/wasm_exec_node.js
|
||||
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/ /app/
|
||||
|
||||
# Idle; the test execs the client on demand with the headscale control URL.
|
||||
ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -35,8 +34,6 @@ 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
|
||||
@@ -53,7 +50,7 @@ test: check-deps $(GO_SOURCES) go.mod go.sum
|
||||
|
||||
# Formatting targets
|
||||
.PHONY: fmt
|
||||
fmt: fmt-go fmt-mdformat fmt-prettier fmt-proto
|
||||
fmt: fmt-go fmt-mdformat fmt-prettier
|
||||
|
||||
.PHONY: fmt-go
|
||||
fmt-go: check-deps $(GO_SOURCES)
|
||||
@@ -71,35 +68,49 @@ 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-proto
|
||||
lint: lint-go
|
||||
|
||||
.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
|
||||
@echo "Generating code..."
|
||||
go generate ./...
|
||||
$(MAKE) client
|
||||
|
||||
# Emit the OpenAPI spec on demand. The server serves it live at /openapi.yaml;
|
||||
# this is for external consumers or inspection and is not committed.
|
||||
.PHONY: openapi
|
||||
openapi:
|
||||
@echo "Emitting OpenAPI spec from code..."
|
||||
go run ./cmd/gen-openapi
|
||||
|
||||
# Generate the strongly-typed Go HTTP clients (v1 and v2). The served specs are
|
||||
# OpenAPI 3.1, but oapi-codegen v2 does not yet read 3.1, so each client is
|
||||
# generated from a transient 3.0.3 downgrade of its document. Pinned so the
|
||||
# committed clients are reproducible.
|
||||
.PHONY: client
|
||||
client:
|
||||
@echo "Generating API clients..."
|
||||
@tmp=$$(mktemp -t headscale-openapi-3.0.XXXXXX.yaml); \
|
||||
go run ./cmd/gen-openapi -downgrade "$$tmp" && \
|
||||
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.7.1 \
|
||||
-generate types,client -package clientv1 -o gen/client/v1/client.gen.go "$$tmp" && \
|
||||
go run ./cmd/gen-openapi -api v2 -downgrade "$$tmp" && \
|
||||
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.7.1 \
|
||||
-generate types,client -package clientv2 -o gen/client/v2/client.gen.go "$$tmp"; \
|
||||
status=$$?; rm -f "$$tmp"; exit $$status
|
||||
|
||||
# Clean targets
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf headscale gen
|
||||
rm -rf headscale gen/client
|
||||
|
||||
# Development workflow
|
||||
.PHONY: dev
|
||||
@@ -119,9 +130,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, proto)"
|
||||
@echo " lint - Lint all code (Go, proto)"
|
||||
@echo " generate - Generate code from Protocol Buffers"
|
||||
@echo " fmt - Format all code (Go, docs, markup)"
|
||||
@echo " lint - Lint all code (Go)"
|
||||
@echo " generate - Generate code (go generate + client)"
|
||||
@echo " dev - Full development workflow (fmt + lint + test + build)"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo ""
|
||||
@@ -129,9 +140,7 @@ 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"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
+1
-2
@@ -42,8 +42,7 @@ 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) and the gRPC
|
||||
port is `port + 42363` (default 50443).
|
||||
The metrics/debug port is `port + 1010` (default 9090).
|
||||
|
||||
## What it does
|
||||
|
||||
|
||||
+9
-11
@@ -28,17 +28,14 @@ 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 both the derived
|
||||
// metrics port (port+1010) and gRPC port (port+42363) inside the valid
|
||||
// 1..65535 TCP range.
|
||||
const maxDevPort = 23172
|
||||
// maxDevPort is the highest --port value that keeps the derived metrics
|
||||
// port (port+1010) inside the valid 1..65535 TCP range.
|
||||
const maxDevPort = 64525
|
||||
|
||||
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
|
||||
@@ -83,7 +80,7 @@ func main() {
|
||||
|
||||
if *port < 1 || *port > maxDevPort {
|
||||
log.Fatalf(
|
||||
"--port must be in 1..%d (higher values overflow the derived gRPC port); got %d",
|
||||
"--port must be in 1..%d (higher values overflow the derived metrics port); got %d",
|
||||
maxDevPort, *port,
|
||||
)
|
||||
}
|
||||
@@ -101,7 +98,6 @@ func main() {
|
||||
|
||||
func run() error {
|
||||
metricsPort := *port + 1010 // default 9090
|
||||
grpcPort := *port + 42363 // default 50443
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "headscale-dev-")
|
||||
if err != nil {
|
||||
@@ -114,8 +110,9 @@ func run() error {
|
||||
|
||||
// Write config.
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
configContent := fmt.Sprintf(devConfig,
|
||||
*port, *port, metricsPort, grpcPort,
|
||||
configContent := fmt.Sprintf(
|
||||
devConfig,
|
||||
*port, *port, metricsPort,
|
||||
tmpDir, tmpDir, tmpDir,
|
||||
)
|
||||
|
||||
@@ -193,7 +190,8 @@ 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
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Command gen-openapi emits a Headscale OpenAPI document from the authoritative
|
||||
// Huma definitions in hscontrol/api/v1 and hscontrol/api/v2. The server also
|
||||
// serves each spec live (at /openapi.yaml and /api/v2/openapi); this tool emits
|
||||
// them on demand, and with -downgrade the 3.0.3 form used to generate the typed
|
||||
// client. The output is not committed.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/gen-openapi # write the v1 3.1 spec to its default path
|
||||
// go run ./cmd/gen-openapi -api v2 # write the v2 3.1 spec to its default path
|
||||
// go run ./cmd/gen-openapi -downgrade <path> # write the v1 3.0.3 downgrade (for client gen)
|
||||
// go run ./cmd/gen-openapi -api v2 -downgrade <path> # the v2 3.0.3 downgrade
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
apiv1 "github.com/juanfont/headscale/hscontrol/api/v1"
|
||||
apiv2 "github.com/juanfont/headscale/hscontrol/api/v2"
|
||||
)
|
||||
|
||||
// spec bundles a version's full (3.1) and downgraded (3.0.3) generators with the
|
||||
// committed output path. outPath is relative to the repository root.
|
||||
type spec struct {
|
||||
full func() ([]byte, error)
|
||||
down func() ([]byte, error)
|
||||
outPath string
|
||||
}
|
||||
|
||||
// specs maps the -api value to its generators.
|
||||
var specs = map[string]spec{
|
||||
"v1": {apiv1.Spec, apiv1.Spec30, "openapi/v1/headscale.yaml"},
|
||||
"v2": {apiv2.Spec, apiv2.Spec30, "openapi/v2/headscale.yaml"},
|
||||
}
|
||||
|
||||
func main() {
|
||||
api := flag.String("api", "v1", "which API spec to emit: v1 or v2")
|
||||
downgrade := flag.String("downgrade", "", "write the OpenAPI 3.0.3 downgrade to this path instead of the committed 3.1 spec")
|
||||
flag.Parse()
|
||||
|
||||
s, ok := specs[*api]
|
||||
if !ok {
|
||||
log.Fatalf("unknown -api %q (want v1 or v2)", *api)
|
||||
}
|
||||
|
||||
if *downgrade != "" {
|
||||
writeSpec(*downgrade, s.down)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writeSpec(s.outPath, s.full)
|
||||
}
|
||||
|
||||
func writeSpec(path string, gen func() ([]byte, error)) {
|
||||
spec, err := gen()
|
||||
if err != nil {
|
||||
log.Fatalf("generating OpenAPI spec: %v", err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(path), 0o755)
|
||||
if err != nil {
|
||||
log.Fatalf("creating output directory: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, spec, 0o600)
|
||||
if err != nil {
|
||||
log.Fatalf("writing %s: %v", path, err)
|
||||
}
|
||||
|
||||
log.Printf("wrote %s", path)
|
||||
}
|
||||
+103
-41
@@ -3,11 +3,11 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -41,36 +41,43 @@ var apiKeysCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var listAPIKeys = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List the Api keys for headscale",
|
||||
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{})
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.ListApiKeysWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing api keys: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetApiKeys(), func() error {
|
||||
tableData := pterm.TableData{
|
||||
{"ID", "Prefix", "Expiration", "Created"},
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
for _, key := range response.GetApiKeys() {
|
||||
apiKeys := resp.JSON200.ApiKeys
|
||||
|
||||
return printListOutput(cmd, apiKeys, func() error {
|
||||
rows := make([][]string, 0, len(apiKeys))
|
||||
for _, key := range apiKeys {
|
||||
expiration := "-"
|
||||
|
||||
if key.GetExpiration() != nil {
|
||||
expiration = ColourTime(key.GetExpiration().AsTime())
|
||||
if key.Expiration != nil {
|
||||
expiration = ColourTime(*key.Expiration)
|
||||
}
|
||||
|
||||
tableData = append(tableData, []string{
|
||||
strconv.FormatUint(key.GetId(), util.Base10),
|
||||
key.GetPrefix(),
|
||||
var created string
|
||||
if key.CreatedAt != nil {
|
||||
created = key.CreatedAt.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
rows = append(rows, []string{
|
||||
key.Id,
|
||||
key.Prefix,
|
||||
expiration,
|
||||
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
created,
|
||||
})
|
||||
}
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
return renderTable([]string{"ID", "Prefix", colExpiration, colCreated}, rows)
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -82,21 +89,25 @@ 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", "new"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
expiration, err := expirationFromFlag(cmd)
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
expiryTime, err := expirationFromFlag(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.CreateApiKey(ctx, &v1.CreateApiKeyRequest{
|
||||
Expiration: expiration,
|
||||
resp, err := client.CreateApiKeyWithResponse(ctx, clientv1.CreateApiKeyJSONRequestBody{
|
||||
Expiration: &expiryTime,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetApiKey(), response.GetApiKey())
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.ApiKey, resp.JSON200.ApiKey)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -117,45 +128,96 @@ func apiKeyIDOrPrefix(cmd *cobra.Command) (uint64, string, error) {
|
||||
}
|
||||
|
||||
var expireAPIKeyCmd = &cobra.Command{
|
||||
Use: "expire",
|
||||
Use: cmdExpire,
|
||||
Short: "Expire an ApiKey",
|
||||
Aliases: []string{"revoke", "exp", "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.ExpireApiKey(ctx, &v1.ExpireApiKeyRequest{
|
||||
Id: id,
|
||||
Prefix: prefix,
|
||||
})
|
||||
body := clientv1.ExpireApiKeyJSONRequestBody{}
|
||||
|
||||
if id != 0 {
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
body.Id = &idStr
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
body.Prefix = &prefix
|
||||
}
|
||||
|
||||
resp, err := client.ExpireApiKeyWithResponse(ctx, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key expired")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key expired")
|
||||
}),
|
||||
}
|
||||
|
||||
var deleteAPIKeyCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete an ApiKey",
|
||||
Aliases: []string{"remove", "del"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"remove", aliasDel},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.DeleteApiKey(ctx, &v1.DeleteApiKeyRequest{
|
||||
Id: id,
|
||||
Prefix: prefix,
|
||||
})
|
||||
// The DELETE route addresses the key by its prefix in the path. When the
|
||||
// user deletes by --id we resolve the id to its (masked) prefix first,
|
||||
// since the path segment is required and a query-only id cannot be routed.
|
||||
if prefix == "" {
|
||||
prefix, err = apiKeyPrefixForID(ctx, client, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.DeleteApiKeyWithResponse(ctx, prefix, &clientv1.DeleteApiKeyParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key deleted")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key deleted")
|
||||
}),
|
||||
}
|
||||
|
||||
// apiKeyPrefixForID resolves an API key id to its display prefix by listing the
|
||||
// keys. The DELETE endpoint addresses keys by prefix in the URL path, so a
|
||||
// delete by --id needs the prefix; the returned masked prefix is accepted by
|
||||
// the server's lookup.
|
||||
func apiKeyPrefixForID(
|
||||
ctx context.Context,
|
||||
client *clientv1.ClientWithResponses,
|
||||
id uint64,
|
||||
) (string, error) {
|
||||
resp, err := client.ListApiKeysWithResponse(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("listing api keys: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return "", apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
for _, key := range resp.JSON200.ApiKeys {
|
||||
if key.Id == idStr {
|
||||
return key.Prefix, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: api key %d not found", errMissingParameter, id)
|
||||
}
|
||||
|
||||
+30
-24
@@ -3,8 +3,9 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -33,61 +34,66 @@ var authCmd = &cobra.Command{
|
||||
var authRegisterCmd = &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a node to your network",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
request := &v1.AuthRegisterRequest{
|
||||
AuthId: authID,
|
||||
User: user,
|
||||
}
|
||||
|
||||
response, err := client.AuthRegister(ctx, request)
|
||||
resp, err := client.AuthRegisterWithResponse(ctx, clientv1.AuthRegisterJSONRequestBody{
|
||||
AuthId: &authID,
|
||||
User: &user,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("registering node: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
node := resp.JSON200.Node
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
response.GetNode(),
|
||||
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()))
|
||||
node,
|
||||
fmt.Sprintf("Node %s registered", node.GivenName),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
var authApproveCmd = &cobra.Command{
|
||||
Use: "approve",
|
||||
Short: "Approve a pending authentication request",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
request := &v1.AuthApproveRequest{
|
||||
AuthId: authID,
|
||||
}
|
||||
|
||||
response, err := client.AuthApprove(ctx, request)
|
||||
resp, err := client.AuthApproveWithResponse(ctx, clientv1.AuthApproveJSONRequestBody{AuthId: &authID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("approving auth request: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Auth request approved")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Auth request approved")
|
||||
}),
|
||||
}
|
||||
|
||||
var authRejectCmd = &cobra.Command{
|
||||
Use: "reject",
|
||||
Short: "Reject a pending authentication request",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
request := &v1.AuthRejectRequest{
|
||||
AuthId: authID,
|
||||
}
|
||||
|
||||
response, err := client.AuthReject(ctx, request)
|
||||
resp, err := client.AuthRejectWithResponse(ctx, clientv1.AuthRejectJSONRequestBody{AuthId: &authID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("rejecting auth request: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Auth request rejected")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Auth request rejected")
|
||||
}),
|
||||
}
|
||||
|
||||
+14
-11
@@ -3,8 +3,9 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -32,7 +33,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: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
registrationID, _ := cmd.Flags().GetString("key")
|
||||
@@ -44,18 +45,20 @@ var createNodeCmd = &cobra.Command{
|
||||
|
||||
routes, _ := cmd.Flags().GetStringSlice("route")
|
||||
|
||||
request := &v1.DebugCreateNodeRequest{
|
||||
Key: registrationID,
|
||||
Name: name,
|
||||
User: user,
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
response, err := client.DebugCreateNode(ctx, request)
|
||||
resp, err := client.DebugCreateNodeWithResponse(ctx, clientv1.DebugCreateNodeJSONRequestBody{
|
||||
Key: ®istrationID,
|
||||
Name: &name,
|
||||
User: &user,
|
||||
Routes: &routes,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node created")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node created")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -16,12 +17,16 @@ 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: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.Health(ctx, &v1.HealthRequest{})
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.HealthWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking health: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -16,19 +17,15 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
|
||||
type Error string
|
||||
|
||||
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 (
|
||||
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")
|
||||
)
|
||||
|
||||
const refreshTTL = 60 * time.Minute
|
||||
|
||||
var accessTTL = 2 * time.Minute
|
||||
|
||||
func init() {
|
||||
|
||||
+160
-159
@@ -3,17 +3,17 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/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"
|
||||
)
|
||||
|
||||
@@ -67,41 +67,59 @@ 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: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
registrationID, _ := cmd.Flags().GetString("key")
|
||||
|
||||
request := &v1.RegisterNodeRequest{
|
||||
Key: registrationID,
|
||||
User: user,
|
||||
params := &clientv1.RegisterNodeParams{
|
||||
User: &user,
|
||||
Key: ®istrationID,
|
||||
}
|
||||
|
||||
response, err := client.RegisterNode(ctx, request)
|
||||
resp, err := client.RegisterNodeWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("registering node: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
node := resp.JSON200.Node
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
response.GetNode(),
|
||||
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()))
|
||||
node,
|
||||
fmt.Sprintf("Node %s registered", node.GivenName),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
var listNodesCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List nodes",
|
||||
Aliases: []string{"ls", "show"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
|
||||
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{User: user})
|
||||
params := &clientv1.ListNodesParams{}
|
||||
if user != "" {
|
||||
params.User = &user
|
||||
}
|
||||
|
||||
resp, err := client.ListNodesWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing nodes: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetNodes(), func() error {
|
||||
tableData, err := nodesToPtables(user, response.GetNodes())
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
nodes := resp.JSON200.Nodes
|
||||
|
||||
return printListOutput(cmd, nodes, func() error {
|
||||
tableData, err := nodesToPtables(nodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting to table: %w", err)
|
||||
}
|
||||
@@ -115,27 +133,33 @@ var listNodeRoutesCmd = &cobra.Command{
|
||||
Use: "list-routes",
|
||||
Short: "List routes available on nodes",
|
||||
Aliases: []string{"lsr", "routes"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
|
||||
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{})
|
||||
resp, err := client.ListNodesWithResponse(ctx, &clientv1.ListNodesParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing nodes: %w", err)
|
||||
}
|
||||
|
||||
nodes := response.GetNodes()
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
nodes := resp.JSON200.Nodes
|
||||
|
||||
if identifier != 0 {
|
||||
for _, node := range response.GetNodes() {
|
||||
if node.GetId() == identifier {
|
||||
nodes = []*v1.Node{node}
|
||||
idStr := strconv.FormatUint(identifier, util.Base10)
|
||||
for _, node := range nodes {
|
||||
if node.Id == idStr {
|
||||
nodes = []clientv1.Node{node}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
nodes = lo.Filter(nodes, func(n clientv1.Node, _ int) bool {
|
||||
return len(n.SubnetRoutes) > 0 || len(n.ApprovedRoutes) > 0 || len(n.AvailableRoutes) > 0
|
||||
})
|
||||
|
||||
return printListOutput(cmd, nodes, func() error {
|
||||
@@ -145,29 +169,33 @@ var listNodeRoutesCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var expireNodeCmd = &cobra.Command{
|
||||
Use: "expire",
|
||||
Use: cmdExpire,
|
||||
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", "exp", "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"logout", aliasExp, "e"},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
disableExpiry, _ := cmd.Flags().GetBool("disable")
|
||||
nodeID := strconv.FormatUint(identifier, util.Base10)
|
||||
|
||||
// Handle disable expiry - node will never expire.
|
||||
if disableExpiry {
|
||||
request := &v1.ExpireNodeRequest{
|
||||
NodeId: identifier,
|
||||
DisableExpiry: true,
|
||||
}
|
||||
disable := true
|
||||
|
||||
response, err := client.ExpireNode(ctx, request)
|
||||
resp, err := client.ExpireNodeWithResponse(ctx, nodeID, clientv1.ExpireNodeJSONRequestBody{
|
||||
DisableExpiry: &disable,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("disabling node expiry: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node expiry disabled")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node expiry disabled")
|
||||
}
|
||||
|
||||
expiry, _ := cmd.Flags().GetString("expiry")
|
||||
@@ -185,28 +213,31 @@ Use --disable to disable key expiry (node will never expire).`,
|
||||
}
|
||||
}
|
||||
|
||||
request := &v1.ExpireNodeRequest{
|
||||
NodeId: identifier,
|
||||
Expiry: timestamppb.New(expiryTime),
|
||||
}
|
||||
|
||||
response, err := client.ExpireNode(ctx, request)
|
||||
resp, err := client.ExpireNodeWithResponse(ctx, nodeID, clientv1.ExpireNodeJSONRequestBody{
|
||||
Expiry: &expiryTime,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring node: %w", err)
|
||||
}
|
||||
|
||||
if now.Equal(expiryTime) || now.After(expiryTime) {
|
||||
return printOutput(cmd, response.GetNode(), "Node expired")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node expiration updated")
|
||||
node := resp.JSON200.Node
|
||||
|
||||
if now.Equal(expiryTime) || now.After(expiryTime) {
|
||||
return printOutput(cmd, node, "Node expired")
|
||||
}
|
||||
|
||||
return printOutput(cmd, node, "Node expiration updated")
|
||||
}),
|
||||
}
|
||||
|
||||
var renameNodeCmd = &cobra.Command{
|
||||
Use: "rename NEW_NAME",
|
||||
Short: "Renames a node in your network",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
|
||||
newName := ""
|
||||
@@ -214,55 +245,55 @@ var renameNodeCmd = &cobra.Command{
|
||||
newName = args[0]
|
||||
}
|
||||
|
||||
request := &v1.RenameNodeRequest{
|
||||
NodeId: identifier,
|
||||
NewName: newName,
|
||||
}
|
||||
|
||||
response, err := client.RenameNode(ctx, request)
|
||||
resp, err := client.RenameNodeWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), newName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetNode(), "Node renamed")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node renamed")
|
||||
}),
|
||||
}
|
||||
|
||||
var deleteNodeCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete a node",
|
||||
Aliases: []string{"del"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{aliasDel},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
nodeID := strconv.FormatUint(identifier, util.Base10)
|
||||
|
||||
getRequest := &v1.GetNodeRequest{
|
||||
NodeId: identifier,
|
||||
}
|
||||
|
||||
getResponse, err := client.GetNode(ctx, getRequest)
|
||||
getResponse, err := client.GetNodeWithResponse(ctx, nodeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting node: %w", err)
|
||||
}
|
||||
|
||||
deleteRequest := &v1.DeleteNodeRequest{
|
||||
NodeId: identifier,
|
||||
if getResponse.StatusCode() != http.StatusOK {
|
||||
return apiError(getResponse.StatusCode(), getResponse.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
if !confirmAction(cmd, fmt.Sprintf(
|
||||
"Do you want to remove the node %s?",
|
||||
getResponse.GetNode().GetName(),
|
||||
getResponse.JSON200.Node.Name,
|
||||
)) {
|
||||
return printOutput(cmd, map[string]string{"Result": "Node not deleted"}, "Node not deleted")
|
||||
return printOutput(cmd, map[string]string{colResult: "Node not deleted"}, "Node not deleted")
|
||||
}
|
||||
|
||||
_, err = client.DeleteNode(ctx, deleteRequest)
|
||||
deleteResponse, err := client.DeleteNodeWithResponse(ctx, nodeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting node: %w", err)
|
||||
}
|
||||
|
||||
if deleteResponse.StatusCode() != http.StatusOK {
|
||||
return apiError(deleteResponse.StatusCode(), deleteResponse.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
map[string]string{"Result": "Node deleted"},
|
||||
map[string]string{colResult: "Node deleted"},
|
||||
"Node deleted",
|
||||
)
|
||||
}),
|
||||
@@ -288,26 +319,26 @@ be assigned to nodes.`,
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
return withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
|
||||
confirmed := true
|
||||
|
||||
changes, err := client.BackfillNodeIPs(ctx, &v1.BackfillNodeIPsRequest{Confirmed: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("backfilling IPs: %w", err)
|
||||
}
|
||||
resp, err := client.BackfillNodeIPsWithResponse(ctx, &clientv1.BackfillNodeIPsParams{
|
||||
Confirmed: &confirmed,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("backfilling IPs: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, changes, "Node IPs backfilled successfully")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Node IPs backfilled successfully")
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
func nodesToPtables(
|
||||
currentUser string,
|
||||
nodes []*v1.Node,
|
||||
) (pterm.TableData, error) {
|
||||
func nodesToPtables(nodes []clientv1.Node) (pterm.TableData, error) {
|
||||
tableHeader := []string{
|
||||
"ID",
|
||||
"Hostname",
|
||||
@@ -319,88 +350,57 @@ func nodesToPtables(
|
||||
"IP addresses",
|
||||
"Ephemeral",
|
||||
"Last seen",
|
||||
"Expiration",
|
||||
colExpiration,
|
||||
"Connected",
|
||||
"Expired",
|
||||
}
|
||||
tableData := pterm.TableData{tableHeader}
|
||||
tableData := make(pterm.TableData, 1, 1+len(nodes))
|
||||
tableData[0] = tableHeader
|
||||
|
||||
for _, node := range nodes {
|
||||
var ephemeral bool
|
||||
if node.GetPreAuthKey() != nil && node.GetPreAuthKey().GetEphemeral() {
|
||||
ephemeral = true
|
||||
// An absent pre-auth key decodes into a zero NodePreAuthKey, so guard
|
||||
// on Id before reading its flags.
|
||||
ephemeral := node.PreAuthKey.Id != "" && node.PreAuthKey.Ephemeral
|
||||
|
||||
var lastSeenTime string
|
||||
if node.LastSeen != nil {
|
||||
lastSeenTime = node.LastSeen.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"
|
||||
expiryTime := "N/A"
|
||||
if node.Expiry != nil {
|
||||
expiryTime = node.Expiry.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
var machineKey key.MachinePublic
|
||||
|
||||
err := machineKey.UnmarshalText(
|
||||
[]byte(node.GetMachineKey()),
|
||||
)
|
||||
err := machineKey.UnmarshalText([]byte(node.MachineKey))
|
||||
if err != nil {
|
||||
machineKey = key.MachinePublic{}
|
||||
}
|
||||
|
||||
var nodeKey key.NodePublic
|
||||
|
||||
err = nodeKey.UnmarshalText(
|
||||
[]byte(node.GetNodeKey()),
|
||||
)
|
||||
err = nodeKey.UnmarshalText([]byte(node.NodeKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var online string
|
||||
if node.GetOnline() {
|
||||
online := pterm.LightRed("offline")
|
||||
if node.Online {
|
||||
online = pterm.LightGreen("online")
|
||||
} else {
|
||||
online = pterm.LightRed("offline")
|
||||
}
|
||||
|
||||
var expired string
|
||||
if node.GetExpiry() != nil && node.GetExpiry().AsTime().Before(time.Now()) {
|
||||
expired := pterm.LightGreen("no")
|
||||
if node.Expiry != nil && node.Expiry.Before(time.Now()) {
|
||||
expired = pterm.LightRed("yes")
|
||||
} else {
|
||||
expired = pterm.LightGreen("no")
|
||||
}
|
||||
|
||||
var tagsBuilder strings.Builder
|
||||
|
||||
for _, tag := range node.GetTags() {
|
||||
tagsBuilder.WriteString("\n" + tag)
|
||||
}
|
||||
|
||||
tags := strings.TrimLeft(tagsBuilder.String(), "\n")
|
||||
|
||||
var user string
|
||||
if node.GetUser() != nil {
|
||||
user = node.GetUser().GetName()
|
||||
}
|
||||
tags := strings.Join(node.Tags, "\n")
|
||||
|
||||
var ipBuilder strings.Builder
|
||||
|
||||
for _, addr := range node.GetIpAddresses() {
|
||||
for _, addr := range node.IpAddresses {
|
||||
ip, err := netip.ParseAddr(addr)
|
||||
if err == nil {
|
||||
if ipBuilder.Len() > 0 {
|
||||
@@ -414,12 +414,12 @@ func nodesToPtables(
|
||||
ipAddresses := ipBuilder.String()
|
||||
|
||||
nodeData := []string{
|
||||
strconv.FormatUint(node.GetId(), util.Base10),
|
||||
node.GetName(),
|
||||
node.GetGivenName(),
|
||||
node.Id,
|
||||
node.Name,
|
||||
node.GivenName,
|
||||
machineKey.ShortString(),
|
||||
nodeKey.ShortString(),
|
||||
user,
|
||||
node.User.Name,
|
||||
tags,
|
||||
ipAddresses,
|
||||
strconv.FormatBool(ephemeral),
|
||||
@@ -438,7 +438,7 @@ func nodesToPtables(
|
||||
}
|
||||
|
||||
func nodeRoutesToPtables(
|
||||
nodes []*v1.Node,
|
||||
nodes []clientv1.Node,
|
||||
) pterm.TableData {
|
||||
tableHeader := []string{
|
||||
"ID",
|
||||
@@ -447,15 +447,16 @@ func nodeRoutesToPtables(
|
||||
"Available",
|
||||
"Serving (Primary)",
|
||||
}
|
||||
tableData := pterm.TableData{tableHeader}
|
||||
tableData := make(pterm.TableData, 1, 1+len(nodes))
|
||||
tableData[0] = tableHeader
|
||||
|
||||
for _, node := range nodes {
|
||||
nodeData := []string{
|
||||
strconv.FormatUint(node.GetId(), util.Base10),
|
||||
node.GetGivenName(),
|
||||
strings.Join(node.GetApprovedRoutes(), "\n"),
|
||||
strings.Join(node.GetAvailableRoutes(), "\n"),
|
||||
strings.Join(node.GetSubnetRoutes(), "\n"),
|
||||
node.Id,
|
||||
node.GivenName,
|
||||
strings.Join(node.ApprovedRoutes, "\n"),
|
||||
strings.Join(node.AvailableRoutes, "\n"),
|
||||
strings.Join(node.SubnetRoutes, "\n"),
|
||||
}
|
||||
tableData = append(
|
||||
tableData,
|
||||
@@ -470,43 +471,43 @@ var tagCmd = &cobra.Command{
|
||||
Use: "tag",
|
||||
Short: "Manage the tags of a node",
|
||||
Aliases: []string{"tags", "t"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
tagsToSet, _ := cmd.Flags().GetStringSlice("tags")
|
||||
|
||||
// Sending tags to node
|
||||
request := &v1.SetTagsRequest{
|
||||
NodeId: identifier,
|
||||
Tags: tagsToSet,
|
||||
}
|
||||
|
||||
resp, err := client.SetTags(ctx, request)
|
||||
resp, err := client.SetTagsWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), clientv1.SetTagsJSONRequestBody{
|
||||
Tags: &tagsToSet,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting tags: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.GetNode(), "Node updated")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node updated")
|
||||
}),
|
||||
}
|
||||
|
||||
var approveRoutesCmd = &cobra.Command{
|
||||
Use: "approve-routes",
|
||||
Short: "Manage the approved routes of a node",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
identifier, _ := cmd.Flags().GetUint64("identifier")
|
||||
routes, _ := cmd.Flags().GetStringSlice("routes")
|
||||
|
||||
// Sending routes to node
|
||||
request := &v1.SetApprovedRoutesRequest{
|
||||
NodeId: identifier,
|
||||
Routes: routes,
|
||||
}
|
||||
|
||||
resp, err := client.SetApprovedRoutes(ctx, request)
|
||||
resp, err := client.SetApprovedRoutesWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), clientv1.SetApprovedRoutesJSONRequestBody{
|
||||
Routes: &routes,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting approved routes: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.GetNode(), "Node updated")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.Node, "Node updated")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
clientv2 "github.com/juanfont/headscale/gen/client/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// oauthTailnet is the single Headscale tailnet the v2 API addresses as "-".
|
||||
const oauthTailnet = "-"
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(oauthClientsCmd)
|
||||
|
||||
oauthClientsCmd.AddCommand(listOAuthClientsCmd)
|
||||
|
||||
createOAuthClientCmd.Flags().
|
||||
StringArrayP("scope", "s", nil, "Scope the client's tokens are granted (repeatable): auth_keys, oauth_keys, devices:core, devices:routes, policy_file, feature_settings (each with a :read variant), or all/all:read")
|
||||
createOAuthClientCmd.Flags().
|
||||
StringArrayP("tag", "t", nil, "Tag the client's tokens may assign to devices (repeatable), e.g. tag:k8s-operator")
|
||||
createOAuthClientCmd.Flags().StringP("description", "d", "", "Human-readable description")
|
||||
oauthClientsCmd.AddCommand(createOAuthClientCmd)
|
||||
|
||||
deleteOAuthClientCmd.Flags().StringP("id", "i", "", "OAuth client id")
|
||||
oauthClientsCmd.AddCommand(deleteOAuthClientCmd)
|
||||
}
|
||||
|
||||
var oauthClientsCmd = &cobra.Command{
|
||||
Use: "oauth-clients",
|
||||
Short: "Manage OAuth clients",
|
||||
Aliases: []string{"oauth-client", "oauthclients", "oauthclient", "oauth"},
|
||||
}
|
||||
|
||||
var createOAuthClientCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create an OAuth client",
|
||||
Long: `Create a general-purpose OAuth client. It authenticates with the OAuth 2.0
|
||||
client-credentials grant and mints short-lived, scope-limited access tokens.
|
||||
The wire format is compatible with Tailscale tooling (the Terraform provider,
|
||||
the Kubernetes operator, tscli, ...), so those can drive Headscale unchanged.
|
||||
|
||||
The client secret is shown ONCE on creation and cannot be retrieved again; if
|
||||
you lose it, delete the client and create a new one.
|
||||
|
||||
Scopes gate what the client's tokens may do; tags are the device tags those
|
||||
tokens may assign and are required when the scopes include devices:core or
|
||||
auth_keys.`,
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
scopes, _ := cmd.Flags().GetStringArray("scope")
|
||||
tags, _ := cmd.Flags().GetStringArray("tag")
|
||||
description, _ := cmd.Flags().GetString("description")
|
||||
|
||||
if len(scopes) == 0 {
|
||||
return fmt.Errorf("at least one --scope is required: %w", errMissingParameter)
|
||||
}
|
||||
|
||||
ctx, client, cancel, err := newV2Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
keyType := "client"
|
||||
|
||||
resp, err := client.CreateKeyWithResponse(ctx, oauthTailnet, clientv2.CreateKeyRequest{
|
||||
KeyType: &keyType,
|
||||
Scopes: &scopes,
|
||||
Tags: &tags,
|
||||
Description: &description,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating oauth client: %w", err)
|
||||
}
|
||||
|
||||
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := resp.JSON200
|
||||
|
||||
return printOutput(cmd, key,
|
||||
fmt.Sprintf("OAuth client %s created.\nSecret (shown once, store it now): %s", key.Id, ptrStr(key.Key)))
|
||||
},
|
||||
}
|
||||
|
||||
var listOAuthClientsCmd = &cobra.Command{
|
||||
Use: cmdList,
|
||||
Short: "List OAuth clients",
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx, client, cancel, err := newV2Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.ListKeysWithResponse(ctx, oauthTailnet, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing oauth clients: %w", err)
|
||||
}
|
||||
|
||||
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The keys endpoint is multiplexed; keep only OAuth clients.
|
||||
clients := make([]clientv2.Key, 0, len(resp.JSON200.Keys))
|
||||
|
||||
for _, k := range resp.JSON200.Keys {
|
||||
if k.KeyType == "client" {
|
||||
clients = append(clients, k)
|
||||
}
|
||||
}
|
||||
|
||||
return printListOutput(cmd, clients, func() error {
|
||||
rows := make([][]string, 0, len(clients))
|
||||
for _, c := range clients {
|
||||
rows = append(rows, []string{
|
||||
c.Id,
|
||||
strings.Join(ptrStrs(c.Scopes), ","),
|
||||
strings.Join(ptrStrs(c.Tags), ","),
|
||||
ptrStr(c.Description),
|
||||
c.Created.Format(HeadscaleDateTimeFormat),
|
||||
})
|
||||
}
|
||||
|
||||
return renderTable([]string{"ID", "Scopes", "Tags", "Description", colCreated}, rows)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var deleteOAuthClientCmd = &cobra.Command{
|
||||
Use: cmdDelete,
|
||||
Short: "Delete an OAuth client",
|
||||
Aliases: []string{"remove", aliasDel},
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
id, _ := cmd.Flags().GetString("id")
|
||||
if id == "" {
|
||||
return fmt.Errorf("--id is required: %w", errMissingParameter)
|
||||
}
|
||||
|
||||
ctx, client, cancel, err := newV2Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.DeleteKeyWithResponse(ctx, oauthTailnet, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting oauth client: %w", err)
|
||||
}
|
||||
|
||||
err = v2Error(resp.HTTPResponse.StatusCode, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printOutput(cmd, map[string]string{"id": id}, "OAuth client "+id+" deleted")
|
||||
},
|
||||
}
|
||||
|
||||
// newV2Client builds a generated v2 API client, selecting the transport the same
|
||||
// way the v1 client does: over the local unix socket it is unauthenticated
|
||||
// (local trust); a remote address injects the configured API key as a bearer
|
||||
// token.
|
||||
func newV2Client() (context.Context, *clientv2.ClientWithResponses, 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 == "" {
|
||||
socketPath := cfg.UnixSocket
|
||||
|
||||
httpClient := &http.Client{Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialHeadscaleSocket(ctx, socketPath)
|
||||
},
|
||||
}}
|
||||
|
||||
client, err := clientv2.NewClientWithResponses("http://local", clientv2.WithHTTPClient(httpClient))
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
if cfg.CLI.APIKey == "" {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, errAPIKeyNotSet
|
||||
}
|
||||
|
||||
transport := &http.Transport{}
|
||||
if cfg.CLI.Insecure {
|
||||
//nolint:gosec // intentionally honouring the insecure flag
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
apiKey := cfg.CLI.APIKey
|
||||
|
||||
client, err := clientv2.NewClientWithResponses(
|
||||
clientBaseURL(cfg.CLI.Address),
|
||||
clientv2.WithHTTPClient(&http.Client{Transport: transport}),
|
||||
clientv2.WithRequestEditorFn(func(_ context.Context, req *http.Request) error {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
// v2Error turns a non-2xx v2 response into an error. The v2 API emits the
|
||||
// Tailscale error body ({"message":...}) rather than RFC 7807, so it reads the
|
||||
// "message" field instead of the generated problem+json types.
|
||||
func v2Error(status int, body []byte) error {
|
||||
if status >= http.StatusOK && status < http.StatusMultipleChoices {
|
||||
return nil
|
||||
}
|
||||
|
||||
var e struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
if json.Unmarshal(body, &e) == nil && e.Message != "" {
|
||||
//nolint:err113 // surfacing the server's message
|
||||
return fmt.Errorf("api error (%d): %s", status, e.Message)
|
||||
}
|
||||
|
||||
//nolint:err113 // surfacing the server's body
|
||||
return fmt.Errorf("api error (%d): %s", status, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
func ptrStr(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return *s
|
||||
}
|
||||
|
||||
func ptrStrs(s *[]string) []string {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return *s
|
||||
}
|
||||
+103
-57
@@ -1,11 +1,13 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/policy"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
@@ -14,14 +16,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
bypassFlag = "bypass-grpc-and-access-database-directly" //nolint:gosec // not a credential
|
||||
bypassFlag = "bypass-server-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 gRPC server. The caller is responsible for closing the
|
||||
// returned database handle.
|
||||
// bypassDatabase opens the database directly, bypassing the running server.
|
||||
// The caller must close the returned handle.
|
||||
func bypassDatabase() (*db.HSDatabase, error) {
|
||||
cfg, err := types.LoadServerConfig()
|
||||
if err != nil {
|
||||
@@ -36,19 +37,29 @@ 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 gRPC and does not require the server to be running")
|
||||
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")
|
||||
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 gRPC and does not require the server to be running")
|
||||
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")
|
||||
mustMarkRequired(setPolicy, "file")
|
||||
policyCmd.AddCommand(setPolicy)
|
||||
|
||||
checkPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
|
||||
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running. Required to validate that user@ tokens resolve against the user database; without it, the check is syntax-only.")
|
||||
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.")
|
||||
mustMarkRequired(checkPolicy, "file")
|
||||
policyCmd.AddCommand(checkPolicy)
|
||||
}
|
||||
@@ -61,16 +72,12 @@ var policyCmd = &cobra.Command{
|
||||
var getPolicy = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Print the current ACL Policy",
|
||||
Aliases: []string{"show", "view", "fetch"},
|
||||
Aliases: []string{cmdShow, "view", "fetch"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var policyData string
|
||||
|
||||
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
|
||||
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()
|
||||
d, err := openBypassDB(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -83,19 +90,23 @@ var getPolicy = &cobra.Command{
|
||||
|
||||
policyData = pol.Data
|
||||
} else {
|
||||
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
err := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
|
||||
resp, err := client.GetPolicyWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading ACL policy: %w", err)
|
||||
}
|
||||
|
||||
response, err := client.GetPolicy(ctx, &v1.GetPolicyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading ACL policy: %w", err)
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
policyData = response.GetPolicy()
|
||||
policyData = resp.JSON200.Policy
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// This does not pass output format as we don't support yaml, json or
|
||||
@@ -122,17 +133,13 @@ var setPolicy = &cobra.Command{
|
||||
}
|
||||
|
||||
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
|
||||
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()
|
||||
d, err := openBypassDB(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
users, err := d.ListUsers()
|
||||
users, err := d.ListUsers(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading users for policy validation: %w", err)
|
||||
}
|
||||
@@ -147,18 +154,24 @@ var setPolicy = &cobra.Command{
|
||||
return fmt.Errorf("setting ACL policy: %w", err)
|
||||
}
|
||||
} else {
|
||||
request := &v1.SetPolicyRequest{Policy: string(policyBytes)}
|
||||
policyStr := 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 := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
|
||||
resp, err := client.SetPolicyWithResponse(ctx, clientv1.SetPolicyJSONRequestBody{
|
||||
Policy: &policyStr,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting ACL policy: %w", err)
|
||||
}
|
||||
|
||||
_, err = client.SetPolicy(ctx, request)
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting ACL policy: %w", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +184,11 @@ var setPolicy = &cobra.Command{
|
||||
var checkPolicy = &cobra.Command{
|
||||
Use: "check",
|
||||
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
|
||||
open the database directly when headscale is not running.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
policyPath, _ := cmd.Flags().GetString("file")
|
||||
|
||||
@@ -179,36 +197,64 @@ var checkPolicy = &cobra.Command{
|
||||
return fmt.Errorf("reading policy file: %w", err)
|
||||
}
|
||||
|
||||
var users []types.User
|
||||
|
||||
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
|
||||
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()
|
||||
d, err := openBypassDB(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
users, err = d.ListUsers()
|
||||
users, err := d.ListUsers(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading users for policy validation: %w", err)
|
||||
return fmt.Errorf("loading users: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = policy.NewPolicyManager(policyBytes, users, views.Slice[types.NodeView]{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing policy file: %w", err)
|
||||
}
|
||||
nodes, err := d.ListNodes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading nodes: %w", err)
|
||||
}
|
||||
|
||||
// [policy.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
|
||||
// tests and sshTests blocks.
|
||||
pm, err := policy.NewPolicyManager(policyBytes, users, nodes.ViewSlice())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing policy file: %w", err)
|
||||
}
|
||||
|
||||
_, err = pm.SetPolicy(policyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if users == nil {
|
||||
fmt.Println("Policy syntax is valid (run with --" + bypassFlag + " to also validate user references against the database)")
|
||||
} else {
|
||||
fmt.Println("Policy is valid")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
policyStr := string(policyBytes)
|
||||
|
||||
err = withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
|
||||
resp, err := client.CheckPolicyWithResponse(ctx, clientv1.CheckPolicyJSONRequestBody{
|
||||
Policy: &policyStr,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Policy is valid")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -42,57 +42,57 @@ var preauthkeysCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var listPreAuthKeys = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List all preauthkeys",
|
||||
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{})
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.ListPreAuthKeysWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing preauthkeys: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetPreAuthKeys(), func() error {
|
||||
tableData := pterm.TableData{
|
||||
{
|
||||
"ID",
|
||||
"Key/Prefix",
|
||||
"Reusable",
|
||||
"Ephemeral",
|
||||
"Used",
|
||||
"Expiration",
|
||||
"Created",
|
||||
"Owner",
|
||||
},
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
for _, key := range response.GetPreAuthKeys() {
|
||||
expiration := "-"
|
||||
if key.GetExpiration() != nil {
|
||||
expiration = ColourTime(key.GetExpiration().AsTime())
|
||||
preAuthKeys := resp.JSON200.PreAuthKeys
|
||||
|
||||
return printListOutput(cmd, preAuthKeys, func() error {
|
||||
rows := make([][]string, 0, len(preAuthKeys))
|
||||
for _, key := range preAuthKeys {
|
||||
expiration := ColourTime(key.Expiration)
|
||||
|
||||
owner := "-"
|
||||
|
||||
switch {
|
||||
case len(key.AclTags) > 0:
|
||||
owner = strings.Join(key.AclTags, "\n")
|
||||
case key.User.Id != "":
|
||||
owner = key.User.Name
|
||||
}
|
||||
|
||||
var owner string
|
||||
if len(key.GetAclTags()) > 0 {
|
||||
owner = strings.Join(key.GetAclTags(), "\n")
|
||||
} else if key.GetUser() != nil {
|
||||
owner = key.GetUser().GetName()
|
||||
} else {
|
||||
owner = "-"
|
||||
}
|
||||
|
||||
tableData = append(tableData, []string{
|
||||
strconv.FormatUint(key.GetId(), util.Base10),
|
||||
key.GetKey(),
|
||||
strconv.FormatBool(key.GetReusable()),
|
||||
strconv.FormatBool(key.GetEphemeral()),
|
||||
strconv.FormatBool(key.GetUsed()),
|
||||
rows = append(rows, []string{
|
||||
key.Id,
|
||||
key.Key,
|
||||
strconv.FormatBool(key.Reusable),
|
||||
strconv.FormatBool(key.Ephemeral),
|
||||
strconv.FormatBool(key.Used),
|
||||
expiration,
|
||||
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
key.CreatedAt.Format(HeadscaleDateTimeFormat),
|
||||
owner,
|
||||
})
|
||||
}
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
return renderTable([]string{
|
||||
"ID",
|
||||
"Key/Prefix",
|
||||
"Reusable",
|
||||
"Ephemeral",
|
||||
"Used",
|
||||
colExpiration,
|
||||
colCreated,
|
||||
"Owner",
|
||||
}, rows)
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -100,79 +100,103 @@ var listPreAuthKeys = &cobra.Command{
|
||||
var createPreAuthKeyCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Creates a new preauthkey",
|
||||
Aliases: []string{"c", "new"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetUint64("user")
|
||||
reusable, _ := cmd.Flags().GetBool("reusable")
|
||||
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
||||
tags, _ := cmd.Flags().GetStringSlice("tags")
|
||||
|
||||
expiration, err := expirationFromFlag(cmd)
|
||||
expiryTime, err := expirationFromFlag(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.CreatePreAuthKeyRequest{
|
||||
User: user,
|
||||
Reusable: reusable,
|
||||
Ephemeral: ephemeral,
|
||||
AclTags: tags,
|
||||
Expiration: expiration,
|
||||
userStr := strconv.FormatUint(user, util.Base10)
|
||||
|
||||
request := clientv1.CreatePreAuthKeyJSONRequestBody{
|
||||
User: &userStr,
|
||||
Reusable: &reusable,
|
||||
Ephemeral: &ephemeral,
|
||||
AclTags: &tags,
|
||||
Expiration: &expiryTime,
|
||||
}
|
||||
|
||||
response, err := client.CreatePreAuthKey(ctx, request)
|
||||
resp, err := client.CreatePreAuthKeyWithResponse(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetPreAuthKey(), response.GetPreAuthKey().GetKey())
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
preAuthKey := resp.JSON200.PreAuthKey
|
||||
|
||||
return printOutput(cmd, preAuthKey, preAuthKey.Key)
|
||||
}),
|
||||
}
|
||||
|
||||
// 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: "expire",
|
||||
Use: cmdExpire,
|
||||
Short: "Expire a preauthkey",
|
||||
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)
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, err := preAuthKeyID(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.ExpirePreAuthKeyRequest{
|
||||
Id: id,
|
||||
}
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
|
||||
response, err := client.ExpirePreAuthKey(ctx, request)
|
||||
resp, err := client.ExpirePreAuthKeyWithResponse(ctx, clientv1.ExpirePreAuthKeyJSONRequestBody{
|
||||
Id: &idStr,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key expired")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key expired")
|
||||
}),
|
||||
}
|
||||
|
||||
var deletePreAuthKeyCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Use: cmdDelete,
|
||||
Short: "Delete a preauthkey",
|
||||
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)
|
||||
Aliases: []string{aliasDel, "rm", "d"},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, err := preAuthKeyID(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.DeletePreAuthKeyRequest{
|
||||
Id: id,
|
||||
}
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
|
||||
response, err := client.DeletePreAuthKey(ctx, request)
|
||||
resp, err := client.DeletePreAuthKeyWithResponse(ctx, &clientv1.DeletePreAuthKeyParams{
|
||||
Id: &idStr,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting preauthkey: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key deleted")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key deleted")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -22,11 +22,6 @@ func init() {
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(os.Args, "policy") && slices.Contains(os.Args, "check") {
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
return
|
||||
}
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.PersistentFlags().
|
||||
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
|
||||
@@ -36,7 +31,7 @@ func init() {
|
||||
Bool("force", false, "Disable prompts and forces the execution")
|
||||
|
||||
// Re-enable usage output only for flag-parsing errors; runtime errors
|
||||
// from RunE should never dump usage text.
|
||||
// from [cobra.Command.RunE] should never dump usage text.
|
||||
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
||||
cmd.SilenceUsage = false
|
||||
|
||||
@@ -101,13 +96,9 @@ func initConfig() {
|
||||
var prereleases = []string{"alpha", "beta", "rc", "dev"}
|
||||
|
||||
func isPreReleaseVersion(version string) bool {
|
||||
for _, unstable := range prereleases {
|
||||
if strings.Contains(version, unstable) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return slices.ContainsFunc(prereleases, func(unstable string) bool {
|
||||
return strings.Contains(version, unstable)
|
||||
})
|
||||
}
|
||||
|
||||
// filterPreReleasesIfStable returns a function that filters out
|
||||
@@ -126,13 +117,7 @@ func filterPreReleasesIfStable(versionFunc func() string) func(string) bool {
|
||||
}
|
||||
|
||||
// If we are on a stable release, filter out pre-releases.
|
||||
for _, ignore := range prereleases {
|
||||
if strings.Contains(tag, ignore) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return isPreReleaseVersion(tag)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,19 @@ 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
|
||||
@@ -14,64 +27,64 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "stable version filters alpha tag",
|
||||
currentVersion: "0.23.0",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v23,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: true,
|
||||
description: "When on stable release, alpha tags should be filtered",
|
||||
},
|
||||
{
|
||||
name: "stable version filters beta tag",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
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: "0.23.0",
|
||||
tag: "v0.24.0-rc.1",
|
||||
currentVersion: v23,
|
||||
tag: v24RCTag,
|
||||
expectedFilter: true,
|
||||
description: "When on stable release, rc tags should be filtered",
|
||||
},
|
||||
{
|
||||
name: "stable version allows stable tag",
|
||||
currentVersion: "0.23.0",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on stable release, stable tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "alpha version allows alpha tag",
|
||||
currentVersion: "0.23.0-alpha.1",
|
||||
currentVersion: v23Alpha1,
|
||||
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: "0.23.0-alpha.1",
|
||||
currentVersion: v23Alpha1,
|
||||
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: "0.23.0-alpha.1",
|
||||
tag: "v0.24.0-rc.1",
|
||||
currentVersion: v23Alpha1,
|
||||
tag: v24RCTag,
|
||||
expectedFilter: false,
|
||||
description: "When on alpha release, rc tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "alpha version allows stable tag",
|
||||
currentVersion: "0.23.0-alpha.1",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23Alpha1,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on alpha release, stable tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "beta version allows alpha tag",
|
||||
currentVersion: "0.23.0-beta.1",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v23Beta1,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on beta release, alpha tags should not be filtered",
|
||||
},
|
||||
@@ -84,28 +97,28 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "beta version allows rc tag",
|
||||
currentVersion: "0.23.0-beta.1",
|
||||
tag: "v0.24.0-rc.1",
|
||||
currentVersion: v23Beta1,
|
||||
tag: v24RCTag,
|
||||
expectedFilter: false,
|
||||
description: "When on beta release, rc tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "beta version allows stable tag",
|
||||
currentVersion: "0.23.0-beta.1",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23Beta1,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on beta release, stable tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "rc version allows alpha tag",
|
||||
currentVersion: "0.23.0-rc.1",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v23RC1,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on rc release, alpha tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "rc version allows beta tag",
|
||||
currentVersion: "0.23.0-rc.1",
|
||||
currentVersion: v23RC1,
|
||||
tag: "v0.24.0-beta.1",
|
||||
expectedFilter: false,
|
||||
description: "When on rc release, beta tags should not be filtered",
|
||||
@@ -119,78 +132,78 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "rc version allows stable tag",
|
||||
currentVersion: "0.23.0-rc.1",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23RC1,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on rc release, stable tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "stable version with patch filters alpha",
|
||||
currentVersion: "0.23.1",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v231,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: true,
|
||||
description: "Stable version with patch number should filter alpha tags",
|
||||
},
|
||||
{
|
||||
name: "stable version with patch allows stable",
|
||||
currentVersion: "0.23.1",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v231,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "Stable version with patch number should allow stable tags",
|
||||
},
|
||||
{
|
||||
name: "tag with alpha substring in version number",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
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: "0.23.0",
|
||||
currentVersion: v23,
|
||||
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: "0.23.0",
|
||||
currentVersion: v23,
|
||||
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: "0.23.0",
|
||||
currentVersion: v23,
|
||||
tag: "",
|
||||
expectedFilter: false,
|
||||
description: "Empty tags should not be filtered",
|
||||
},
|
||||
{
|
||||
name: "dev version allows all tags",
|
||||
currentVersion: "0.23.0-dev",
|
||||
tag: "v0.24.0-alpha.1",
|
||||
currentVersion: v23Dev,
|
||||
tag: v24Alpha1Tag,
|
||||
expectedFilter: false,
|
||||
description: "Dev versions should not filter any tags (pre-release allows all)",
|
||||
},
|
||||
{
|
||||
name: "stable version filters dev tag",
|
||||
currentVersion: "0.23.0",
|
||||
currentVersion: v23,
|
||||
tag: "v0.24.0-dev",
|
||||
expectedFilter: true,
|
||||
description: "When on stable release, dev tags should be filtered",
|
||||
},
|
||||
{
|
||||
name: "dev version allows dev tag",
|
||||
currentVersion: "0.23.0-dev",
|
||||
currentVersion: v23Dev,
|
||||
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: "0.23.0-dev",
|
||||
tag: "v0.24.0",
|
||||
currentVersion: v23Dev,
|
||||
tag: v24Tag,
|
||||
expectedFilter: false,
|
||||
description: "When on dev release, stable tags should not be filtered",
|
||||
},
|
||||
@@ -200,7 +213,8 @@ 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,
|
||||
@@ -222,25 +236,25 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "stable version",
|
||||
version: "0.23.0",
|
||||
version: v23,
|
||||
expected: false,
|
||||
description: "Stable version should not be pre-release",
|
||||
},
|
||||
{
|
||||
name: "alpha version",
|
||||
version: "0.23.0-alpha.1",
|
||||
version: v23Alpha1,
|
||||
expected: true,
|
||||
description: "Alpha version should be pre-release",
|
||||
},
|
||||
{
|
||||
name: "beta version",
|
||||
version: "0.23.0-beta.1",
|
||||
version: v23Beta1,
|
||||
expected: true,
|
||||
description: "Beta version should be pre-release",
|
||||
},
|
||||
{
|
||||
name: "rc version",
|
||||
version: "0.23.0-rc.1",
|
||||
version: v23RC1,
|
||||
expected: true,
|
||||
description: "RC version should be pre-release",
|
||||
},
|
||||
@@ -258,7 +272,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "dev version",
|
||||
version: "0.23.0-dev",
|
||||
version: v23Dev,
|
||||
expected: true,
|
||||
description: "Dev version should be pre-release",
|
||||
},
|
||||
@@ -270,7 +284,7 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "version with patch number",
|
||||
version: "0.23.1",
|
||||
version: v231,
|
||||
expected: false,
|
||||
description: "Stable version with patch should not be pre-release",
|
||||
},
|
||||
@@ -280,7 +294,8 @@ 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,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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"
|
||||
)
|
||||
+101
-82
@@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/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"
|
||||
)
|
||||
@@ -37,13 +37,50 @@ 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.
|
||||
if identifier < 0 {
|
||||
identifier = 0
|
||||
}
|
||||
identifier = max(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 *clientv1.ClientWithResponses,
|
||||
cmd *cobra.Command,
|
||||
) (uint64, *clientv1.User, error) {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
params := &clientv1.ListUsersParams{}
|
||||
if username != "" {
|
||||
params.Name = &username
|
||||
}
|
||||
|
||||
if id != 0 {
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
params.Id = &idStr
|
||||
}
|
||||
|
||||
resp, err := client.ListUsersWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return 0, nil, apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
users := resp.JSON200.Users
|
||||
if len(users) != 1 {
|
||||
return 0, nil, errMultipleUsersMatch
|
||||
}
|
||||
|
||||
return id, &users[0], nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(userCmd)
|
||||
userCmd.AddCommand(createUserCmd)
|
||||
@@ -70,7 +107,7 @@ var userCmd = &cobra.Command{
|
||||
var createUserCmd = &cobra.Command{
|
||||
Use: "create NAME",
|
||||
Short: "Creates a new user",
|
||||
Aliases: []string{"c", "new"},
|
||||
Aliases: []string{"c", cmdNew},
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errMissingParameter
|
||||
@@ -78,19 +115,19 @@ var createUserCmd = &cobra.Command{
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
userName := args[0]
|
||||
|
||||
log.Trace().Interface(zf.Client, client).Msg("obtained gRPC client")
|
||||
log.Trace().Interface(zf.Client, client).Msg("obtained API client")
|
||||
|
||||
request := &v1.CreateUserRequest{Name: userName}
|
||||
request := clientv1.CreateUserJSONRequestBody{Name: &userName}
|
||||
|
||||
if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" {
|
||||
request.DisplayName = displayName
|
||||
request.DisplayName = &displayName
|
||||
}
|
||||
|
||||
if email, _ := cmd.Flags().GetString("email"); email != "" {
|
||||
request.Email = email
|
||||
request.Email = &email
|
||||
}
|
||||
|
||||
if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
|
||||
@@ -98,70 +135,60 @@ var createUserCmd = &cobra.Command{
|
||||
return fmt.Errorf("invalid picture URL: %w", err)
|
||||
}
|
||||
|
||||
request.PictureUrl = pictureURL
|
||||
request.PictureUrl = &pictureURL
|
||||
}
|
||||
|
||||
log.Trace().Interface(zf.Request, request).Msg("sending CreateUser request")
|
||||
|
||||
response, err := client.CreateUser(ctx, request)
|
||||
resp, err := client.CreateUserWithResponse(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetUser(), "User created")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.User, "User created")
|
||||
}),
|
||||
}
|
||||
|
||||
var destroyUserCmd = &cobra.Command{
|
||||
Use: "destroy --identifier ID or --name NAME",
|
||||
Short: "Destroys a user",
|
||||
Aliases: []string{"delete"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
Aliases: []string{cmdDelete},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
_, user, err := resolveSingleUser(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &v1.ListUsersRequest{
|
||||
Name: username,
|
||||
Id: id,
|
||||
}
|
||||
|
||||
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(),
|
||||
"Do you want to remove the user %q (%s) and any associated preauthkeys?",
|
||||
user.Name, user.Id,
|
||||
)) {
|
||||
return printOutput(cmd, map[string]string{"Result": "User not destroyed"}, "User not destroyed")
|
||||
return printOutput(cmd, map[string]string{colResult: "User not destroyed"}, "User not destroyed")
|
||||
}
|
||||
|
||||
deleteRequest := &v1.DeleteUserRequest{Id: user.GetId()}
|
||||
|
||||
response, err := client.DeleteUser(ctx, deleteRequest)
|
||||
resp, err := client.DeleteUserWithResponse(ctx, user.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("destroying user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "User destroyed")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "User destroyed")
|
||||
}),
|
||||
}
|
||||
|
||||
var listUsersCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Use: cmdList,
|
||||
Short: "List all the users",
|
||||
Aliases: []string{"ls", "show"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
request := &v1.ListUsersRequest{}
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
params := &clientv1.ListUsersParams{}
|
||||
|
||||
id, _ := cmd.Flags().GetInt64("identifier")
|
||||
username, _ := cmd.Flags().GetString("name")
|
||||
@@ -170,34 +197,41 @@ var listUsersCmd = &cobra.Command{
|
||||
// filter by one param at most
|
||||
switch {
|
||||
case id > 0:
|
||||
request.Id = uint64(id)
|
||||
idStr := strconv.FormatInt(id, util.Base10)
|
||||
params.Id = &idStr
|
||||
case username != "":
|
||||
request.Name = username
|
||||
params.Name = &username
|
||||
case email != "":
|
||||
request.Email = email
|
||||
params.Email = &email
|
||||
}
|
||||
|
||||
response, err := client.ListUsers(ctx, request)
|
||||
resp, err := client.ListUsersWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetUsers(), func() error {
|
||||
tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}}
|
||||
for _, user := range response.GetUsers() {
|
||||
tableData = append(
|
||||
tableData,
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
users := resp.JSON200.Users
|
||||
|
||||
return printListOutput(cmd, users, func() error {
|
||||
rows := make([][]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
rows = append(
|
||||
rows,
|
||||
[]string{
|
||||
strconv.FormatUint(user.GetId(), util.Base10),
|
||||
user.GetDisplayName(),
|
||||
user.GetName(),
|
||||
user.GetEmail(),
|
||||
user.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
user.Id,
|
||||
user.DisplayName,
|
||||
user.Name,
|
||||
user.Email,
|
||||
user.CreatedAt.Format(HeadscaleDateTimeFormat),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
return renderTable([]string{"ID", "Name", "Username", "Email", colCreated}, rows)
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -206,38 +240,23 @@ var renameUserCmd = &cobra.Command{
|
||||
Use: "rename",
|
||||
Short: "Renames a user",
|
||||
Aliases: []string{"mv"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, _, err := resolveSingleUser(ctx, client, 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")
|
||||
|
||||
renameReq := &v1.RenameUserRequest{
|
||||
OldId: id,
|
||||
NewName: newName,
|
||||
}
|
||||
|
||||
response, err := client.RenameUser(ctx, renameReq)
|
||||
resp, err := client.RenameUserWithResponse(ctx, strconv.FormatUint(id, util.Base10), newName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetUser(), "User renamed")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.User, "User renamed")
|
||||
}),
|
||||
}
|
||||
|
||||
+199
-123
@@ -6,21 +6,23 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/pterm/pterm"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -36,11 +38,45 @@ const (
|
||||
var (
|
||||
errAPIKeyNotSet = errors.New("HEADSCALE_CLI_API_KEY environment variable needs to be set")
|
||||
errMissingParameter = errors.New("missing parameters")
|
||||
errResponseStatus = errors.New("unexpected response status")
|
||||
)
|
||||
|
||||
// mustMarkRequired marks the named flags as required on cmd, panicking
|
||||
// if any name does not match a registered flag. This is only called
|
||||
// from init() where a failure indicates a programming error.
|
||||
// apiError turns a non-2xx response into an error, surfacing the server's
|
||||
// RFC7807 problem detail. detail holds the operation context and errors[] the
|
||||
// wrapped cause (e.g. "name is too long"); both are joined so the server's
|
||||
// message text is not lost.
|
||||
func apiError(statusCode int, problem *clientv1.ErrorModel) error {
|
||||
if problem == nil {
|
||||
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
|
||||
}
|
||||
|
||||
parts := make([]string, 0, 2)
|
||||
|
||||
if problem.Detail != nil && *problem.Detail != "" {
|
||||
parts = append(parts, *problem.Detail)
|
||||
}
|
||||
|
||||
if problem.Errors != nil {
|
||||
for _, e := range *problem.Errors {
|
||||
if e.Message != nil && *e.Message != "" {
|
||||
parts = append(parts, *e.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) == 0 && problem.Title != nil && *problem.Title != "" {
|
||||
parts = append(parts, *problem.Title)
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s", errResponseStatus, strings.Join(parts, ": "))
|
||||
}
|
||||
|
||||
// mustMarkRequired marks the named flags as required, panicking on an unknown
|
||||
// flag. Only called from init(), where a failure is a programming error.
|
||||
func mustMarkRequired(cmd *cobra.Command, names ...string) {
|
||||
for _, n := range names {
|
||||
err := cmd.MarkFlagRequired(n)
|
||||
@@ -67,28 +103,47 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// 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,
|
||||
// clientRunE wraps a [cobra.Command.RunE] func, injecting a ready API client
|
||||
// and a context whose timeout/cancel the wrapper owns.
|
||||
func clientRunE(
|
||||
fn func(ctx context.Context, client *clientv1.ClientWithResponses, 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()
|
||||
ctx, client, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
return fn(ctx, client, cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc, error) {
|
||||
// withClient runs fn with an API client. For commands that branch on a flag
|
||||
// before talking to the server, where clientRunE's whole-RunE wrapping does
|
||||
// not fit.
|
||||
func withClient(
|
||||
fn func(ctx context.Context, client *clientv1.ClientWithResponses) error,
|
||||
) error {
|
||||
ctx, client, cancel, err := newHeadscaleCLIWithConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to headscale: %w", err)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
return fn(ctx, client)
|
||||
}
|
||||
|
||||
// newHeadscaleCLIWithConfig builds an HTTP client for the Headscale v1 API.
|
||||
//
|
||||
// When cfg.CLI.Address is unset the CLI is assumed to run on the server host
|
||||
// and talks to the unix socket over HTTP without authentication (local trust).
|
||||
// Otherwise it talks to the remote TCP address over HTTPS and injects the
|
||||
// configured API key as a bearer token.
|
||||
func newHeadscaleCLIWithConfig() (context.Context, *clientv1.ClientWithResponses, context.CancelFunc, error) {
|
||||
cfg, err := types.LoadCLIConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
|
||||
return nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
@@ -97,89 +152,131 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
|
||||
|
||||
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 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
|
||||
client, err := newSocketClient(cfg.UnixSocket)
|
||||
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()
|
||||
}
|
||||
|
||||
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 nil, nil, nil, nil, errAPIKeyNotSet
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
grpcOptions = append(grpcOptions,
|
||||
grpc.WithPerRPCCredentials(tokenAuth{
|
||||
token: apiKey,
|
||||
}),
|
||||
)
|
||||
log.Trace().Caller().Str(zf.Address, cfg.UnixSocket).Msg("connecting via unix socket")
|
||||
|
||||
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, "")),
|
||||
)
|
||||
}
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via gRPC")
|
||||
// Remote connections require an API key for authentication.
|
||||
apiKey := cfg.CLI.APIKey
|
||||
if apiKey == "" {
|
||||
cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, address, grpcOptions...) //nolint:staticcheck // SA1019: deprecated but supported in 1.x
|
||||
return nil, nil, nil, errAPIKeyNotSet
|
||||
}
|
||||
|
||||
client, err := newRemoteClient(address, apiKey, cfg.CLI.Insecure)
|
||||
if err != nil {
|
||||
cancel()
|
||||
|
||||
return nil, nil, nil, nil, fmt.Errorf("connecting to %s: %w", address, err)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
client := v1.NewHeadscaleServiceClient(conn)
|
||||
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via HTTPS")
|
||||
|
||||
return ctx, client, conn, cancel, nil
|
||||
return ctx, client, cancel, nil
|
||||
}
|
||||
|
||||
// newSocketClient builds an API client that dials the local unix socket. The
|
||||
// base-URL host is irrelevant; the custom dialer routes every request to the
|
||||
// socket.
|
||||
func newSocketClient(socketPath string) (*clientv1.ClientWithResponses, error) {
|
||||
// Probe for a clearer permission error up front. [os.OpenFile] on a unix
|
||||
// socket returns ENXIO on Linux (expected); only permission errors are
|
||||
// actionable. The real connection goes through [net.Dial].
|
||||
socket, err := os.OpenFile(socketPath, os.O_WRONLY, SocketWritePermissions) //nolint
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return nil, fmt.Errorf(
|
||||
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
|
||||
socketPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
socket.Close()
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialHeadscaleSocket(ctx, socketPath)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return clientv1.NewClientWithResponses(
|
||||
"http://local",
|
||||
clientv1.WithHTTPClient(httpClient),
|
||||
)
|
||||
}
|
||||
|
||||
// dialHeadscaleSocket connects to the unix socket, retrying until it appears or
|
||||
// ctx (the CLI timeout) expires. The socket is created late in startup (after
|
||||
// noise key, database, migrations), so a command run right after the server
|
||||
// starts can race its creation; retrying preserves the old gRPC client's
|
||||
// blocking-dial tolerance rather than failing on a not-yet-present socket.
|
||||
func dialHeadscaleSocket(ctx context.Context, socketPath string) (net.Conn, error) {
|
||||
b := backoff.NewExponentialBackOff()
|
||||
b.InitialInterval = 50 * time.Millisecond
|
||||
b.MaxInterval = 1 * time.Second
|
||||
|
||||
return backoff.Retry(ctx, func() (net.Conn, error) {
|
||||
return util.SocketDialer(ctx, socketPath)
|
||||
}, backoff.WithBackOff(b))
|
||||
}
|
||||
|
||||
// clientBaseURL turns a configured CLI address into a client base URL. A bare
|
||||
// host[:port] (the historical form) defaults to https; an address that already
|
||||
// carries a scheme is used as-is, so an explicit http:// or https:// is honoured
|
||||
// rather than doubled into https://https://...
|
||||
func clientBaseURL(address string) string {
|
||||
if strings.Contains(address, "://") {
|
||||
return address
|
||||
}
|
||||
|
||||
return "https://" + address
|
||||
}
|
||||
|
||||
// newRemoteClient builds an API client for a remote Headscale over HTTPS,
|
||||
// honouring insecure (skip TLS verification) and injecting the API key as a
|
||||
// bearer token on every request.
|
||||
func newRemoteClient(address, apiKey string, insecure bool) (*clientv1.ClientWithResponses, error) {
|
||||
transport := &http.Transport{}
|
||||
if insecure {
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
// turn off gosec as we are intentionally setting insecure.
|
||||
//nolint:gosec
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Transport: transport}
|
||||
|
||||
return clientv1.NewClientWithResponses(
|
||||
clientBaseURL(address),
|
||||
clientv1.WithHTTPClient(httpClient),
|
||||
clientv1.WithRequestEditorFn(func(_ context.Context, req *http.Request) error {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// formatOutput serialises result into the requested format. For the
|
||||
@@ -228,16 +325,16 @@ func printOutput(cmd *cobra.Command, result any, override string) error {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// duration (e.g. "90d", "1h") and returns an absolute time.
|
||||
func expirationFromFlag(cmd *cobra.Command) (time.Time, error) {
|
||||
durationStr, _ := cmd.Flags().GetString("expiration")
|
||||
|
||||
duration, err := model.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing duration: %w", err)
|
||||
return time.Time{}, fmt.Errorf("parsing duration: %w", err)
|
||||
}
|
||||
|
||||
return timestamppb.New(time.Now().UTC().Add(time.Duration(duration))), nil
|
||||
return time.Now().UTC().Add(time.Duration(duration)), nil
|
||||
}
|
||||
|
||||
// confirmAction returns true when the user confirms a prompt, or when
|
||||
@@ -251,9 +348,19 @@ 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 renderTable
|
||||
// to produce the human-readable pterm table.
|
||||
// requested it serialises data as JSON/YAML; otherwise it calls the render
|
||||
// callback to produce the human-readable pterm table.
|
||||
func printListOutput(
|
||||
cmd *cobra.Command,
|
||||
data any,
|
||||
@@ -269,56 +376,25 @@ 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"`
|
||||
}
|
||||
|
||||
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:
|
||||
if outputFormat == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s\n", formatted)
|
||||
// formatOutput cannot fail here: errOutput is a single string field.
|
||||
out, _ := formatOutput(errOutput{Error: err.Error()}, "", outputFormat)
|
||||
fmt.Fprintf(os.Stderr, "%s\n", out)
|
||||
}
|
||||
|
||||
func hasMachineOutputFlag() bool {
|
||||
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
|
||||
return slices.ContainsFunc(os.Args, func(arg string) bool {
|
||||
return arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDialHeadscaleSocketRetriesUntilPresent proves the CLI socket dialer
|
||||
// tolerates a not-yet-created socket (the server-still-starting race) by
|
||||
// retrying until it appears, rather than failing immediately like a bare dial.
|
||||
func TestDialHeadscaleSocketRetriesUntilPresent(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "headscale.sock")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
type result struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
|
||||
done := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := dialHeadscaleSocket(ctx, sock)
|
||||
done <- result{conn, err}
|
||||
}()
|
||||
|
||||
// Listen only after the dialer has begun, so its backoff must retry the
|
||||
// absent socket and connect once it exists.
|
||||
var lc net.ListenConfig
|
||||
|
||||
ln, err := lc.Listen(ctx, "unix", sock)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
if conn, _ := ln.Accept(); conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
res := <-done
|
||||
require.NoError(t, res.err)
|
||||
require.NotNil(t, res.conn)
|
||||
|
||||
res.conn.Close()
|
||||
}
|
||||
|
||||
// TestDialHeadscaleSocketRespectsDeadline proves the retry is bounded by the
|
||||
// context: when the socket never appears, the dialer returns an error around the
|
||||
// deadline instead of hanging.
|
||||
func TestDialHeadscaleSocketRespectsDeadline(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "absent.sock")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
conn, err := dialHeadscaleSocket(ctx, sock)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, conn)
|
||||
assert.Less(t, time.Since(start), 5*time.Second, "should stop near the deadline, not hang")
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cli
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClientBaseURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
address string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "bare host defaults to https",
|
||||
address: "headscale.example.com:50443",
|
||||
want: "https://headscale.example.com:50443",
|
||||
},
|
||||
{
|
||||
name: "explicit https scheme is kept",
|
||||
address: "https://headscale.example.com",
|
||||
want: "https://headscale.example.com",
|
||||
},
|
||||
{
|
||||
name: "explicit http scheme is kept",
|
||||
address: "http://localhost:8080",
|
||||
want: "http://localhost:8080",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := clientBaseURL(tt.address); got != tt.want {
|
||||
t.Errorf("clientBaseURL(%q) = %q, want %q", tt.address, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,21 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
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
|
||||
}
|
||||
colors := termcolor.SupportLevel(os.Stderr) != termcolor.LevelNone
|
||||
|
||||
// Adhere to no-color.org manifesto of allowing users to
|
||||
// turn off color in cli/services
|
||||
|
||||
@@ -73,5 +73,4 @@ func TestConfigLoading(t *testing.T) {
|
||||
assert.Equal(t, "HTTP-01", viper.GetString("tls_letsencrypt_challenge_type"))
|
||||
assert.Equal(t, fs.FileMode(0o770), util.GetFileMode("unix_socket_permission"))
|
||||
assert.False(t, viper.GetBool("logtail.enabled"))
|
||||
assert.False(t, viper.GetBool("randomize_client_port"))
|
||||
}
|
||||
|
||||
+45
-41
@@ -71,26 +71,8 @@ func killTestContainers(ctx context.Context) error {
|
||||
removed := 0
|
||||
|
||||
for _, cont := range containers {
|
||||
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) {
|
||||
if isTestContainerName(cont.Names) {
|
||||
if killAndRemove(ctx, cli, cont) {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
@@ -129,13 +111,7 @@ func killTestContainersByRunID(ctx context.Context, runID string) error {
|
||||
removed := 0
|
||||
|
||||
for _, cont := range containers {
|
||||
// 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) {
|
||||
if killAndRemove(ctx, cli, cont) {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
@@ -173,20 +149,8 @@ func cleanupStaleTestContainers(ctx context.Context) error {
|
||||
|
||||
for _, cont := range containers {
|
||||
// Only remove containers that look like test containers
|
||||
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) {
|
||||
if isTestContainerName(cont.Names) {
|
||||
if killAndRemove(ctx, cli, cont) {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
@@ -223,6 +187,46 @@ func removeContainerWithRetry(ctx context.Context, cli *client.Client, container
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// testContainerNamePrefixes are the name prefixes used by containers that the
|
||||
// integration test harness creates (headscale, tailscale, DERP, and k3s).
|
||||
var testContainerNamePrefixes = []string{"hs-", "ts-", "derp-", "k3s-"}
|
||||
|
||||
// matchesTestContainerPrefix reports whether name belongs to an integration
|
||||
// test container, ignoring any leading "/" that Docker prefixes names with.
|
||||
func matchesTestContainerPrefix(name string) bool {
|
||||
name = strings.TrimPrefix(name, "/")
|
||||
for _, prefix := range testContainerNamePrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 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") ||
|
||||
matchesTestContainerPrefix(name) {
|
||||
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)
|
||||
|
||||
+48
-37
@@ -14,6 +14,7 @@ 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"
|
||||
@@ -522,9 +523,9 @@ func checkImageAvailableLocally(ctx context.Context, cli *client.Client, imageNa
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ensureImageAvailable checks if the image is available locally first, then pulls if needed.
|
||||
// ensureImageAvailable pulls imageName if missing, using Docker Hub
|
||||
// credentials and retrying transient errors.
|
||||
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)
|
||||
@@ -538,34 +539,64 @@ 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)
|
||||
}
|
||||
|
||||
reader, err := cli.ImagePull(ctx, imageName, image.PullOptions{})
|
||||
registryAuth, err := dockertestutil.RegistryAuth()
|
||||
if err != nil {
|
||||
return fmt.Errorf("pulling image %s: %w", imageName, err)
|
||||
return fmt.Errorf("resolving registry auth: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
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)
|
||||
}
|
||||
_, 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)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -704,7 +735,7 @@ func getCurrentTestContainers(containers []container.Summary, testContainerID st
|
||||
for _, cont := range containers {
|
||||
for _, name := range cont.Names {
|
||||
containerName := strings.TrimPrefix(name, "/")
|
||||
if strings.HasPrefix(containerName, "hs-") || strings.HasPrefix(containerName, "ts-") {
|
||||
if matchesTestContainerPrefix(containerName) {
|
||||
// Check if container has matching run ID label
|
||||
if cont.Labels != nil && cont.Labels["hi.run-id"] == runID {
|
||||
testRunContainers = append(testRunContainers, testContainer{
|
||||
@@ -738,17 +769,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -796,12 +816,3 @@ 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
|
||||
}
|
||||
|
||||
+180
-174
@@ -5,8 +5,25 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/juanfont/headscale/integration/dockertestutil"
|
||||
"github.com/juanfont/headscale/integration/k3sic"
|
||||
)
|
||||
|
||||
const (
|
||||
statusPass = "PASS"
|
||||
statusFail = "FAIL"
|
||||
statusWarn = "WARN"
|
||||
|
||||
nameDockerDaemon = "Docker Daemon"
|
||||
nameDockerContext = "Docker Context"
|
||||
nameDockerSocket = "Docker Socket"
|
||||
nameGolangImage = "Golang Image"
|
||||
nameK3sImage = "K3s Image"
|
||||
nameGoInstall = "Go Installation"
|
||||
)
|
||||
|
||||
var ErrSystemChecksFailed = errors.New("system checks failed")
|
||||
@@ -19,6 +36,21 @@ 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{}
|
||||
@@ -31,10 +63,12 @@ func runDoctorCheck(ctx context.Context) error {
|
||||
results = append(results, dockerResult)
|
||||
|
||||
// If Docker is available, run additional checks
|
||||
if dockerResult.Status == "PASS" {
|
||||
if dockerResult.Status == statusPass {
|
||||
results = append(results, checkDockerContext(ctx))
|
||||
results = append(results, checkDockerSocket(ctx))
|
||||
results = append(results, checkDockerHubCredentials())
|
||||
results = append(results, checkGolangImage(ctx))
|
||||
results = append(results, checkK3sImage(ctx))
|
||||
}
|
||||
|
||||
// Check 3: Go installation
|
||||
@@ -51,7 +85,7 @@ func runDoctorCheck(ctx context.Context) error {
|
||||
|
||||
// Return error if any critical checks failed
|
||||
for _, result := range results {
|
||||
if result.Status == "FAIL" {
|
||||
if result.Status == statusFail {
|
||||
return fmt.Errorf("%w - see details above", ErrSystemChecksFailed)
|
||||
}
|
||||
}
|
||||
@@ -65,140 +99,115 @@ func runDoctorCheck(ctx context.Context) error {
|
||||
func checkDockerBinary() DoctorResult {
|
||||
_, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
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 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: "PASS",
|
||||
Message: "Docker binary found",
|
||||
}
|
||||
return pass("Docker Binary", "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 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",
|
||||
},
|
||||
}
|
||||
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",
|
||||
)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
_, err = cli.Ping(ctx)
|
||||
if err != nil {
|
||||
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 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: "PASS",
|
||||
Message: "Docker daemon is running and accessible",
|
||||
}
|
||||
return pass(nameDockerDaemon, "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 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",
|
||||
},
|
||||
}
|
||||
return warn(
|
||||
nameDockerContext,
|
||||
"Could not detect Docker context, using default settings",
|
||||
"Check: docker context ls",
|
||||
"Consider setting up a specific context if needed",
|
||||
)
|
||||
}
|
||||
|
||||
if contextInfo == nil {
|
||||
return DoctorResult{
|
||||
Name: "Docker Context",
|
||||
Status: "PASS",
|
||||
Message: "Using default Docker context",
|
||||
}
|
||||
return pass(nameDockerContext, "Using default Docker context")
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Docker Context",
|
||||
Status: "PASS",
|
||||
Message: "Using Docker context: " + contextInfo.Name,
|
||||
}
|
||||
return pass(nameDockerContext, "Using Docker context: "+contextInfo.Name)
|
||||
}
|
||||
|
||||
// checkDockerSocket verifies Docker socket accessibility.
|
||||
func checkDockerSocket(ctx context.Context) DoctorResult {
|
||||
cli, err := createDockerClient(ctx)
|
||||
if err != nil {
|
||||
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",
|
||||
},
|
||||
}
|
||||
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",
|
||||
)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
info, err := cli.Info(ctx)
|
||||
if err != nil {
|
||||
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 fail(
|
||||
nameDockerSocket,
|
||||
fmt.Sprintf("Cannot get Docker info: %v", err),
|
||||
"Check Docker daemon status",
|
||||
"Verify socket permissions",
|
||||
)
|
||||
}
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Docker Socket",
|
||||
Status: "PASS",
|
||||
Message: fmt.Sprintf("Docker socket accessible (Server: %s)", info.ServerVersion),
|
||||
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 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 DoctorResult{
|
||||
Name: "Golang Image",
|
||||
Status: "FAIL",
|
||||
Message: "Cannot create Docker client for image check",
|
||||
}
|
||||
return fail(nameGolangImage, "Cannot create Docker client for image check")
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
@@ -208,81 +217,94 @@ func checkGolangImage(ctx context.Context) DoctorResult {
|
||||
// First check if image is available locally
|
||||
available, err := checkImageAvailableLocally(ctx, cli, imageName)
|
||||
if err != nil {
|
||||
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",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameGolangImage,
|
||||
fmt.Sprintf("Cannot check golang image %s: %v", imageName, err),
|
||||
"Check Docker daemon status",
|
||||
"Try: docker images | grep golang",
|
||||
)
|
||||
}
|
||||
|
||||
if available {
|
||||
return DoctorResult{
|
||||
Name: "Golang Image",
|
||||
Status: "PASS",
|
||||
Message: fmt.Sprintf("Golang image %s is available locally", imageName),
|
||||
}
|
||||
return pass(nameGolangImage, 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 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 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: "PASS",
|
||||
Message: fmt.Sprintf("Golang image %s is now available", imageName),
|
||||
return pass(nameGolangImage, fmt.Sprintf("Golang image %s is now available", imageName))
|
||||
}
|
||||
|
||||
// checkK3sImage verifies the ghcr k3s image used by TestK8sOperator is available
|
||||
// locally or can be pulled. The image is pinned (see [k3sic.K3sImage]).
|
||||
func checkK3sImage(ctx context.Context) DoctorResult {
|
||||
cli, err := createDockerClient(ctx)
|
||||
if err != nil {
|
||||
return fail(nameK3sImage, "Cannot create Docker client for image check")
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
imageName := k3sic.K3sImage
|
||||
|
||||
available, err := checkImageAvailableLocally(ctx, cli, imageName)
|
||||
if err != nil {
|
||||
return fail(
|
||||
nameK3sImage,
|
||||
fmt.Sprintf("Cannot check k3s image %s: %v", imageName, err),
|
||||
"Check Docker daemon status",
|
||||
"Try: docker images | grep k3s",
|
||||
)
|
||||
}
|
||||
|
||||
if available {
|
||||
return pass(nameK3sImage, fmt.Sprintf("K3s image %s is available locally", imageName))
|
||||
}
|
||||
|
||||
err = ensureImageAvailable(ctx, cli, imageName, false)
|
||||
if err != nil {
|
||||
return warn(
|
||||
nameK3sImage,
|
||||
fmt.Sprintf("K3s image %s not available locally and could not pull: %v", imageName, err),
|
||||
"Only TestK8sOperator needs this image; other tests are unaffected",
|
||||
"Try: docker pull "+imageName,
|
||||
)
|
||||
}
|
||||
|
||||
return pass(nameK3sImage, fmt.Sprintf("K3s 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 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",
|
||||
},
|
||||
}
|
||||
return fail(
|
||||
nameGoInstall,
|
||||
"Go binary not found in PATH",
|
||||
"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 DoctorResult{
|
||||
Name: "Go Installation",
|
||||
Status: "FAIL",
|
||||
Message: fmt.Sprintf("Cannot get Go version: %v", err),
|
||||
}
|
||||
return fail(nameGoInstall, fmt.Sprintf("Cannot get Go version: %v", err))
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(string(output))
|
||||
|
||||
return DoctorResult{
|
||||
Name: "Go Installation",
|
||||
Status: "PASS",
|
||||
Message: version,
|
||||
}
|
||||
return pass(nameGoInstall, version)
|
||||
}
|
||||
|
||||
// checkGitRepository verifies we're in a git repository.
|
||||
@@ -291,26 +313,19 @@ func checkGitRepository(ctx context.Context) DoctorResult {
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
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 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: "PASS",
|
||||
Message: "Running in Git repository",
|
||||
}
|
||||
return pass("Git Repository", "Running in Git repository")
|
||||
}
|
||||
|
||||
// checkRequiredFiles verifies required files exist.
|
||||
func checkRequiredFiles(ctx context.Context) DoctorResult {
|
||||
func checkRequiredFiles(_ context.Context) DoctorResult {
|
||||
requiredFiles := []string{
|
||||
"go.mod",
|
||||
"integration/",
|
||||
@@ -320,32 +335,23 @@ func checkRequiredFiles(ctx context.Context) DoctorResult {
|
||||
var missingFiles []string
|
||||
|
||||
for _, file := range requiredFiles {
|
||||
cmd := exec.CommandContext(ctx, "test", "-e", file)
|
||||
|
||||
err := cmd.Run()
|
||||
_, err := os.Stat(file)
|
||||
if err != nil {
|
||||
missingFiles = append(missingFiles, file)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingFiles) > 0 {
|
||||
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 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: "PASS",
|
||||
Message: "All required files found",
|
||||
}
|
||||
return pass("Required Files", "All required files found")
|
||||
}
|
||||
|
||||
// displayDoctorResults shows the results in a formatted way.
|
||||
@@ -357,11 +363,11 @@ func displayDoctorResults(results []DoctorResult) {
|
||||
var icon string
|
||||
|
||||
switch result.Status {
|
||||
case "PASS":
|
||||
case statusPass:
|
||||
icon = "✅"
|
||||
case "WARN":
|
||||
case statusWarn:
|
||||
icon = "⚠️"
|
||||
case "FAIL":
|
||||
case statusFail:
|
||||
icon = "❌"
|
||||
default:
|
||||
icon = "❓"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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
|
||||
}
|
||||
+18
-14
@@ -29,6 +29,13 @@ 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",
|
||||
@@ -79,20 +86,17 @@ func main() {
|
||||
}
|
||||
|
||||
func cleanAll(ctx context.Context) error {
|
||||
err := killTestContainers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
for _, step := range []func(context.Context) error{
|
||||
killTestContainers,
|
||||
pruneDockerNetworks,
|
||||
cleanOldImages,
|
||||
cleanCacheVolume,
|
||||
} {
|
||||
err := step(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = pruneDockerNetworks(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cleanOldImages(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cleanCacheVolume(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
+10
-52
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/creachadair/command"
|
||||
@@ -66,64 +67,21 @@ func runIntegrationTest(env *command.Env) error {
|
||||
|
||||
// detectGoVersion reads the Go version from go.mod file.
|
||||
func detectGoVersion() string {
|
||||
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"
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(goModPath)
|
||||
content, err := os.ReadFile("go.mod")
|
||||
if err != nil {
|
||||
return "1.26.1"
|
||||
content, err = os.ReadFile(filepath.Join("..", "..", "go.mod"))
|
||||
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]
|
||||
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]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+17
-22
@@ -1,12 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -160,7 +161,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},
|
||||
@@ -256,7 +257,7 @@ func (sc *StatsCollector) collectStatsForContainer(ctx context.Context, containe
|
||||
|
||||
err := decoder.Decode(&stats)
|
||||
if err != nil {
|
||||
// EOF is expected when container stops or stream ends
|
||||
// [io.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)
|
||||
}
|
||||
@@ -312,7 +313,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
|
||||
}
|
||||
|
||||
@@ -374,24 +375,24 @@ func (sc *StatsCollector) GetSummary() []ContainerStatsSummary {
|
||||
SampleCount: len(stats),
|
||||
}
|
||||
|
||||
// Calculate CPU stats
|
||||
cpuValues := make([]float64, len(stats))
|
||||
memoryValues := make([]float64, len(stats))
|
||||
extract := func(get func(StatsSample) float64) []float64 {
|
||||
values := make([]float64, len(stats))
|
||||
for i, sample := range stats {
|
||||
values[i] = get(sample)
|
||||
}
|
||||
|
||||
for i, sample := range stats {
|
||||
cpuValues[i] = sample.CPUUsage
|
||||
memoryValues[i] = sample.MemoryMB
|
||||
return values
|
||||
}
|
||||
|
||||
summary.CPU = calculateStatsSummary(cpuValues)
|
||||
summary.Memory = calculateStatsSummary(memoryValues)
|
||||
summary.CPU = calculateStatsSummary(extract(func(s StatsSample) float64 { return s.CPUUsage }))
|
||||
summary.Memory = calculateStatsSummary(extract(func(s StatsSample) float64 { return s.MemoryMB }))
|
||||
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
|
||||
// Sort by container name for consistent output
|
||||
sort.Slice(summaries, func(i, j int) bool {
|
||||
return summaries[i].ContainerName < summaries[j].ContainerName
|
||||
slices.SortFunc(summaries, func(a, b ContainerStatsSummary) int {
|
||||
return cmp.Compare(a.ContainerName, b.ContainerName)
|
||||
})
|
||||
|
||||
return summaries
|
||||
@@ -408,14 +409,8 @@ func calculateStatsSummary(values []float64) StatsSummary {
|
||||
sum := 0.0
|
||||
|
||||
for _, value := range values {
|
||||
if value < minVal {
|
||||
minVal = value
|
||||
}
|
||||
|
||||
if value > maxVal {
|
||||
maxVal = value
|
||||
}
|
||||
|
||||
minVal = min(minVal, value)
|
||||
maxVal = max(maxVal, value)
|
||||
sum += value
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
// vendorhash maintains the Nix SRI hash for the Go module vendor tree
|
||||
// and stores it in flakehashes.json alongside a content fingerprint of
|
||||
// go.mod and go.sum.
|
||||
//
|
||||
// Each block records its input fingerprint (goModSum) so that re-runs
|
||||
// with no input change are essentially free: the fast path is just a
|
||||
// sha256 over two small files. The vendor tree is only re-walked when
|
||||
// the fingerprint actually drifts.
|
||||
//
|
||||
// Subcommands:
|
||||
//
|
||||
// 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
|
||||
// tailscale's tool/updateflakes so a future shared library extraction
|
||||
// is straightforward.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"tailscale.com/cmd/nardump/nardump"
|
||||
)
|
||||
|
||||
const (
|
||||
hashesFile = "flakehashes.json"
|
||||
goModFile = "go.mod"
|
||||
goSumFile = "go.sum"
|
||||
)
|
||||
|
||||
type FlakeHashes struct {
|
||||
Vendor VendorBlock `json:"vendor"`
|
||||
}
|
||||
|
||||
type VendorBlock struct {
|
||||
GoModSum string `json:"goModSum"`
|
||||
SRI string `json:"sri"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var err error
|
||||
|
||||
switch os.Args[1] {
|
||||
case "check":
|
||||
err = cmdCheck(ctx)
|
||||
case "update":
|
||||
err = cmdUpdate(ctx)
|
||||
case "-h", "--help", "help":
|
||||
usage()
|
||||
return
|
||||
default:
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, errStale) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "vendorhash:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// silently.
|
||||
var errStale = errors.New("vendor hash stale")
|
||||
|
||||
// cmdCheck verifies that flakehashes.json matches the current
|
||||
// go.mod/go.sum. The fast path (fingerprint unchanged) costs only
|
||||
// a sha256 over the two files. On mismatch, it computes the actual
|
||||
// SRI so the failure message gives the developer the value to paste
|
||||
// (or to run `vendorhash update`).
|
||||
func cmdCheck(ctx context.Context) error {
|
||||
hashes, err := loadHashes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curFP, err := goModFingerprint()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if curFP == hashes.Vendor.GoModSum {
|
||||
return nil
|
||||
}
|
||||
|
||||
curSRI, err := hashVendor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "vendor hash is stale.")
|
||||
fmt.Fprintf(os.Stderr, " expected goModSum: %s\n", hashes.Vendor.GoModSum)
|
||||
fmt.Fprintf(os.Stderr, " actual goModSum: %s\n", curFP)
|
||||
fmt.Fprintf(os.Stderr, " expected sri: %s\n", hashes.Vendor.SRI)
|
||||
fmt.Fprintf(os.Stderr, " actual sri: %s\n", curSRI)
|
||||
fmt.Fprintln(os.Stderr, "run: go run ./cmd/vendorhash update")
|
||||
// Also emit machine-parseable lines so CI can pick them up.
|
||||
fmt.Printf("expected_sri=%s\n", hashes.Vendor.SRI)
|
||||
fmt.Printf("actual_sri=%s\n", curSRI)
|
||||
|
||||
return errStale
|
||||
}
|
||||
|
||||
func cmdUpdate(ctx context.Context) error {
|
||||
fp, err := goModFingerprint()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sri, err := hashVendor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeHashes(FlakeHashes{
|
||||
Vendor: VendorBlock{
|
||||
GoModSum: fp,
|
||||
SRI: sri,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// goModFingerprint returns a content fingerprint of go.mod and go.sum
|
||||
// that changes whenever either file changes. The byte layout matches
|
||||
// upstream tailscale's tool/updateflakes.
|
||||
func goModFingerprint() (string, error) {
|
||||
h := sha256.New()
|
||||
|
||||
for _, f := range []string{goModFile, goSumFile} {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Fprintf(h, "%s %d\n", f, len(b))
|
||||
h.Write(b)
|
||||
}
|
||||
|
||||
return "sha256-" + base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// hashVendor runs `go mod vendor` into a temporary directory and
|
||||
// returns the Nix SRI hash of the resulting tree.
|
||||
func hashVendor(ctx context.Context) (string, error) {
|
||||
out, err := os.MkdirTemp("", "nar-vendor-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// `go mod vendor -o` requires the destination to not already exist.
|
||||
err = os.Remove(out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer os.RemoveAll(out)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "go", "mod", "vendor", "-o", out)
|
||||
|
||||
cmd.Env = append(os.Environ(), "GOWORK=off")
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("go mod vendor: %w", err)
|
||||
}
|
||||
|
||||
return nardump.SRI(os.DirFS(out))
|
||||
}
|
||||
|
||||
func loadHashes() (FlakeHashes, error) {
|
||||
var h FlakeHashes
|
||||
|
||||
b, err := os.ReadFile(hashesFile)
|
||||
if err != nil {
|
||||
return h, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &h)
|
||||
if err != nil {
|
||||
return h, fmt.Errorf("%s: %w", hashesFile, err)
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func writeHashes(h FlakeHashes) error {
|
||||
b, err := json.MarshalIndent(h, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b = append(b, '\n')
|
||||
|
||||
// flakehashes.json is committed source read by Nix during evaluation;
|
||||
// world-readable matches every other tracked file in the repo.
|
||||
return os.WriteFile(hashesFile, b, 0o644) //nolint:gosec
|
||||
}
|
||||
+42
-47
@@ -23,21 +23,12 @@ listen_addr: 127.0.0.1:8080
|
||||
# Use an empty value to disable the metrics listener.
|
||||
metrics_listen_addr: 127.0.0.1:9090
|
||||
|
||||
# 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
|
||||
# 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: []
|
||||
|
||||
# The Noise section includes specific configuration for the
|
||||
# TS2021 Noise protocol
|
||||
@@ -80,8 +71,7 @@ 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
|
||||
@@ -126,9 +116,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
|
||||
@@ -240,12 +230,12 @@ database:
|
||||
# ssl: false
|
||||
|
||||
### TLS configuration
|
||||
#
|
||||
## Let's encrypt / ACME
|
||||
#
|
||||
# headscale supports automatically requesting and setting up
|
||||
# See: https://headscale.net/stable/ref/tls/
|
||||
|
||||
## Let's Encrypt / ACME
|
||||
# Headscale supports automatically requesting and setting up
|
||||
# TLS for a domain with Let's Encrypt.
|
||||
#
|
||||
|
||||
# URL to ACME directory
|
||||
acme_url: https://acme-v02.api.letsencrypt.org/directory
|
||||
|
||||
@@ -255,15 +245,13 @@ acme_email: ""
|
||||
# Domain name to request a TLS certificate for:
|
||||
tls_letsencrypt_hostname: ""
|
||||
|
||||
# Path to store certificates and metadata needed by
|
||||
# letsencrypt
|
||||
# For production:
|
||||
# Path to store certificates and metadata needed by letsencrypt
|
||||
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
|
||||
|
||||
# Type of ACME challenge to use, currently supported types:
|
||||
# HTTP-01 or TLS-ALPN-01
|
||||
# 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:
|
||||
# :http = port 80
|
||||
@@ -281,23 +269,23 @@ log:
|
||||
format: text
|
||||
|
||||
## Policy
|
||||
# headscale supports Tailscale's ACL policies.
|
||||
# Please have a look to their KB to better
|
||||
# understand the concepts: https://tailscale.com/docs/features/access-control/acls
|
||||
# Headscale supports a wide range of Tailscale policy features such as ACLs and
|
||||
# Grants. Please have a look at their docs to better understand the concepts:
|
||||
# ACLs: https://tailscale.com/docs/features/access-control/acls
|
||||
# Grants: https://tailscale.com/docs/features/access-control/grants
|
||||
policy:
|
||||
# The mode can be "file" or "database" that defines
|
||||
# where the ACL policies are stored and read from.
|
||||
# where the policies are stored and read from.
|
||||
mode: file
|
||||
# If the mode is set to "file", the path to a
|
||||
# HuJSON file containing ACL policies.
|
||||
|
||||
# If the mode is set to "file", the path to a HuJSON file containing policies.
|
||||
path: ""
|
||||
|
||||
## DNS
|
||||
#
|
||||
# headscale supports Tailscale's DNS configuration and MagicDNS.
|
||||
# Please have a look to their KB to better understand the concepts:
|
||||
# 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
|
||||
#
|
||||
@@ -314,7 +302,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](https://tailscale.com/docs/features/magicdns).
|
||||
# Whether to use MagicDNS
|
||||
magic_dns: true
|
||||
|
||||
# Defines the base domain to create the hostnames for MagicDNS.
|
||||
@@ -355,7 +343,7 @@ dns:
|
||||
|
||||
# Extra DNS records
|
||||
# so far only A and AAAA records are supported (on the tailscale side)
|
||||
# See: docs/ref/dns.md
|
||||
# See: https://headscale.net/stable/ref/dns/
|
||||
extra_records: []
|
||||
# - name: "grafana.myvpn.example.com"
|
||||
# type: "A"
|
||||
@@ -374,6 +362,7 @@ 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
|
||||
@@ -452,19 +441,25 @@ logtail:
|
||||
# disabled by default. Enabling this will make your clients send logs to Tailscale Inc.
|
||||
enabled: false
|
||||
|
||||
# Enabling this option makes devices prefer a random port for WireGuard traffic over the
|
||||
# default static port 41641. This option is intended as a workaround for some buggy
|
||||
# firewall devices. See https://tailscale.com/docs/integrations/firewalls for more information.
|
||||
randomize_client_port: false
|
||||
|
||||
# Taildrop configuration
|
||||
# Taildrop is the file sharing feature of Tailscale, allowing nodes to send files to each other.
|
||||
# Taildrop is the file sharing feature of Tailscale, allowing nodes to
|
||||
# send files to each other.
|
||||
# https://tailscale.com/docs/features/taildrop
|
||||
taildrop:
|
||||
# Enable or disable Taildrop for all nodes.
|
||||
# When enabled, nodes can send files to other nodes owned by the same user.
|
||||
# Tagged devices and cross-user transfers are not permitted by Tailscale clients.
|
||||
# Enable or disable Taildrop tailnet-wide. When disabled, headscale
|
||||
# withholds `https://tailscale.com/cap/file-sharing` from every
|
||||
# node's CapMap.
|
||||
enabled: true
|
||||
|
||||
# Default node auto-update behaviour. When enabled, every node's
|
||||
# CapMap carries `default-auto-update: [true]` so clients that have
|
||||
# not made a local opt-in / opt-out choice run auto-updates by
|
||||
# default. Setting it back to false flips the default for future
|
||||
# clients; clients that already stored the value locally keep their
|
||||
# choice.
|
||||
auto_update:
|
||||
enabled: false
|
||||
|
||||
# Advanced performance tuning parameters.
|
||||
# The defaults are carefully chosen and should rarely need adjustment.
|
||||
# Only modify these if you have identified a specific performance issue.
|
||||
|
||||
+6
-6
@@ -134,7 +134,7 @@ help to the community.
|
||||
|
||||
Running headscale on a machine that is also in the tailnet can cause problems with subnet routers, traffic relay nodes, and MagicDNS. It might work, but it is not supported.
|
||||
|
||||
## Why do two nodes see each other in their status, even if an ACL allows traffic only in one direction?
|
||||
## Why do two nodes see each other in their status, even if a policy rule allows traffic only in one direction?
|
||||
|
||||
A frequent use case is to allow traffic only from one node to another, but not the other way around. For example, the
|
||||
workstation of an administrator should be able to connect to all nodes but the nodes themselves shouldn't be able to
|
||||
@@ -142,7 +142,7 @@ connect back to the administrator's node. Why do all nodes see the administrator
|
||||
`tailscale status`?
|
||||
|
||||
This is essentially how Tailscale works. If traffic is allowed to flow in one direction, then both nodes see each other
|
||||
in their output of `tailscale status`. Traffic is still filtered according to the ACL, with the exception of
|
||||
in their output of `tailscale status`. Traffic is still filtered according to the policy, with the exception of
|
||||
`tailscale ping` which is always allowed in either direction.
|
||||
|
||||
See also <https://tailscale.com/docs/concepts/device-visibility>.
|
||||
@@ -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-grpc-and-access-database-directly > policy.json`
|
||||
- Dump the policy to a file: `headscale policy get --bypass-server-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-grpc-and-access-database-directly --file policy.json`
|
||||
- Load the modified policy: `headscale policy set --bypass-server-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#grpc) is not sufficient. You may use
|
||||
minimal config to [control Headscale via remote CLI](../ref/api.md#remote-control) 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?
|
||||
@@ -191,7 +191,7 @@ following steps can be used to migrate from unsupported IP prefixes back to the
|
||||
SET ipv4=concat('100.64.', id/256, '.', id%256),
|
||||
ipv6=concat('fd7a:115c:a1e0::', format('%x', id));
|
||||
```
|
||||
- Update the [policy](../ref/acls.md) to reflect the IP address changes (if any)
|
||||
- Update the [policy](../ref/policy.md) to reflect the IP address changes (if any)
|
||||
- Start Headscale
|
||||
|
||||
Nodes should reconnect within a few seconds and pickup their newly assigned IP addresses.
|
||||
|
||||
+12
-6
@@ -13,23 +13,29 @@ 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] [Taildrop (File Sharing)](https://tailscale.com/docs/features/taildrop)
|
||||
- [x] File sharing
|
||||
- [x] [Taildrive](https://tailscale.com/docs/features/taildrive)
|
||||
- [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)
|
||||
- [x] [Exit nodes](../ref/routes.md#exit-node)
|
||||
- [x] [Route filtering with Via](https://tailscale.com/docs/features/access-control/grants/grants-via)
|
||||
- [x] Dual stack (IPv4 and IPv6)
|
||||
- [x] Ephemeral nodes
|
||||
- [x] Embedded [DERP server](../ref/derp.md)
|
||||
- [x] Access control lists ([GitHub label "policy"](https://github.com/juanfont/headscale/labels/policy%20%F0%9F%93%9D))
|
||||
- [x] ACL management via API
|
||||
- [x] Some [Autogroups](https://tailscale.com/docs/reference/targets-and-selectors#autogroups), currently:
|
||||
`autogroup:internet`, `autogroup:nonroot`, `autogroup:member`, `autogroup:tagged`, `autogroup:self`,
|
||||
`autogroup:danger-all`
|
||||
- [x] [Peer relays](https://tailscale.com/docs/features/peer-relay)
|
||||
- [x] [Policy](../ref/policy.md) ([GitHub label "policy"](https://github.com/juanfont/headscale/labels/policy%20%F0%9F%93%9D))
|
||||
- [x] ACLs
|
||||
- [x] Grants
|
||||
- [x] Some [Autogroups](../ref/policy.md#autogroups)
|
||||
- [x] [Auto approvers](https://tailscale.com/docs/reference/syntax/policy-file#auto-approvers) for [subnet
|
||||
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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
+1
-1
@@ -8,7 +8,7 @@ hide:
|
||||
|
||||
Headscale is an open source, self-hosted implementation of the Tailscale control server.
|
||||
|
||||
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](./about/faq.md).
|
||||
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](about/faq.md).
|
||||
|
||||
Join our [Discord server](https://discord.gg/c84AZQhmpx) for a chat and community support.
|
||||
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.
|
||||
|
||||
For instance, instead of referring to users when defining groups you must
|
||||
use users (which are the equivalent to user/logins in Tailscale.com).
|
||||
|
||||
Please check [manage permissions using ACLs](https://tailscale.com/docs/features/access-control/acls) for further
|
||||
information.
|
||||
|
||||
When using ACL's the User borders are no longer applied. All machines
|
||||
whichever the User have the ability to communicate with other hosts as
|
||||
long as the ACL's permits this exchange.
|
||||
|
||||
## ACL Setup
|
||||
|
||||
To enable and configure ACLs in Headscale, you need to specify the path to your ACL policy file in the `policy.path` key in `config.yaml`.
|
||||
|
||||
Your ACL policy file must be formatted using [huJSON](https://github.com/tailscale/hujson).
|
||||
|
||||
Info on how these policies are written can be found in [Tailscale's ACL
|
||||
documentation](https://tailscale.com/docs/features/access-control/acls).
|
||||
|
||||
Please reload or restart Headscale after updating the ACL file. Headscale may be reloaded either via its systemd service
|
||||
(`sudo systemctl reload headscale`) or by sending a SIGHUP signal (`sudo kill -HUP $(pidof headscale)`) to the main
|
||||
process. Headscale logs the result of ACL policy processing after each reload.
|
||||
|
||||
## Simple Examples
|
||||
|
||||
- [**Allow All**](https://tailscale.com/docs/reference/examples/acls#allow-all-default-acl): If you define an ACL file but completely omit the `"acls"` field from its content, Headscale will default to an "allow all" policy. This means all devices connected to your tailnet will be able to communicate freely with each other.
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
- [**Deny All**](https://tailscale.com/docs/reference/examples/acls#deny-all): To prevent all communication within your tailnet, you can include an empty array for the `"acls"` field in your policy file.
|
||||
|
||||
```json
|
||||
{
|
||||
"acls": []
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Example
|
||||
|
||||
Let's build a more complex example use case for a small business (It may be the place where
|
||||
ACL's are the most useful).
|
||||
|
||||
We have a small company with a boss, an admin, two developers and an intern.
|
||||
|
||||
The boss should have access to all servers but not to the user's hosts. Admin
|
||||
should also have access to all hosts except that their permissions should be
|
||||
limited to maintaining the hosts (for example purposes). The developers can do
|
||||
anything they want on dev hosts but only watch on productions hosts. Intern
|
||||
can only interact with the development servers.
|
||||
|
||||
There's an additional server that acts as a router, connecting the VPN users
|
||||
to an internal network `10.20.0.0/16`. Developers must have access to those
|
||||
internal resources.
|
||||
|
||||
Each user have at least a device connected to the network and we have some
|
||||
servers.
|
||||
|
||||
- database.prod
|
||||
- database.dev
|
||||
- app-server1.prod
|
||||
- app-server1.dev
|
||||
- billing.internal
|
||||
- router.internal
|
||||
|
||||

|
||||
|
||||
When [registering the servers](../usage/getting-started.md#register-a-node) we
|
||||
will need to add the flag `--advertise-tags=tag:<tag1>,tag:<tag2>`, and the user
|
||||
that is registering the server should be allowed to do it. Since anyone can add
|
||||
tags to a server they can register, the check of the tags is done on headscale
|
||||
server and only valid tags are applied. A tag is valid if the user that is
|
||||
registering it is allowed to do it.
|
||||
|
||||
Here are the ACL's to implement the same permissions as above:
|
||||
|
||||
```json title="acl.json"
|
||||
{
|
||||
// groups are collections of users having a common scope. A user can be in multiple groups
|
||||
// groups cannot be composed of groups
|
||||
"groups": {
|
||||
"group:boss": ["boss@"],
|
||||
"group:dev": ["dev1@", "dev2@"],
|
||||
"group:admin": ["admin1@"],
|
||||
"group:intern": ["intern1@"]
|
||||
},
|
||||
// tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server.
|
||||
// This is documented [here](https://tailscale.com/docs/features/tags)
|
||||
// and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/)
|
||||
"tagOwners": {
|
||||
// the administrators can add servers in production
|
||||
"tag:prod-databases": ["group:admin"],
|
||||
"tag:prod-app-servers": ["group:admin"],
|
||||
|
||||
// the boss can tag any server as internal
|
||||
"tag:internal": ["group:boss"],
|
||||
|
||||
// dev can add servers for dev purposes as well as admins
|
||||
"tag:dev-databases": ["group:admin", "group:dev"],
|
||||
"tag:dev-app-servers": ["group:admin", "group:dev"]
|
||||
|
||||
// interns cannot add servers
|
||||
},
|
||||
// hosts should be defined using its IP addresses and a subnet mask.
|
||||
// to define a single host, use a /32 mask. You cannot use DNS entries here,
|
||||
// as they're prone to be hijacked by replacing their IP addresses.
|
||||
// see https://github.com/tailscale/tailscale/issues/3800 for more information.
|
||||
"hosts": {
|
||||
"postgresql.internal": "10.20.0.2/32",
|
||||
"webservers.internal": "10.20.10.1/29"
|
||||
},
|
||||
"acls": [
|
||||
// boss have access to all servers
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["group:boss"],
|
||||
"dst": [
|
||||
"tag:prod-databases:*",
|
||||
"tag:prod-app-servers:*",
|
||||
"tag:internal:*",
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*"
|
||||
]
|
||||
},
|
||||
|
||||
// admin have only access to administrative ports of the servers, in tcp/22
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["group:admin"],
|
||||
"proto": "tcp",
|
||||
"dst": [
|
||||
"tag:prod-databases:22",
|
||||
"tag:prod-app-servers:22",
|
||||
"tag:internal:22",
|
||||
"tag:dev-databases:22",
|
||||
"tag:dev-app-servers:22"
|
||||
]
|
||||
},
|
||||
|
||||
// we also allow admin to ping the servers
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["group:admin"],
|
||||
"proto": "icmp",
|
||||
"dst": [
|
||||
"tag:prod-databases:*",
|
||||
"tag:prod-app-servers:*",
|
||||
"tag:internal:*",
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*"
|
||||
]
|
||||
},
|
||||
|
||||
// developers have access to databases servers and application servers on all ports
|
||||
// they can only view the applications servers in prod and have no access to databases servers in production
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["group:dev"],
|
||||
"dst": [
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*",
|
||||
"tag:prod-app-servers:80,443"
|
||||
]
|
||||
},
|
||||
// developers have access to the internal network through the router.
|
||||
// the internal network is composed of HTTPS endpoints and Postgresql
|
||||
// database servers.
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["group:dev"],
|
||||
"dst": ["10.20.0.0/16:443,5432"]
|
||||
},
|
||||
|
||||
// servers should be able to talk to database in tcp/5432. Database should not be able to initiate connections to
|
||||
// applications servers
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["tag:dev-app-servers"],
|
||||
"proto": "tcp",
|
||||
"dst": ["tag:dev-databases:5432"]
|
||||
},
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["tag:prod-app-servers"],
|
||||
"dst": ["tag:prod-databases:5432"]
|
||||
},
|
||||
|
||||
// interns have access to dev-app-servers only in reading mode
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["group:intern"],
|
||||
"dst": ["tag:dev-app-servers:80,443"]
|
||||
},
|
||||
|
||||
// Allow users to access their own devices using autogroup:self (see below for more details about performance impact)
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:self:*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Autogroups
|
||||
|
||||
Headscale supports several autogroups that automatically include users, destinations, or devices with specific properties. Autogroups provide a convenient way to write ACL rules without manually listing individual users or devices.
|
||||
|
||||
### `autogroup:internet`
|
||||
|
||||
Allows access to the internet through [exit nodes](routes.md#exit-node). Can only be used in ACL destinations.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["group:users"],
|
||||
"dst": ["autogroup:internet:*"]
|
||||
}
|
||||
```
|
||||
|
||||
### `autogroup:member`
|
||||
|
||||
Includes all [personal (untagged) devices](registration.md/#identity-model).
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:prod-app-servers:80,443"]
|
||||
}
|
||||
```
|
||||
|
||||
### `autogroup:tagged`
|
||||
|
||||
Includes all devices that [have at least one tag](registration.md/#identity-model).
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["autogroup:tagged"],
|
||||
"dst": ["tag:monitoring:9090"]
|
||||
}
|
||||
```
|
||||
|
||||
### `autogroup:self`
|
||||
|
||||
!!! warning "The current implementation of `autogroup:self` is inefficient"
|
||||
|
||||
Includes devices where the same user is authenticated on both the source and destination. Does not include tagged devices. Can only be used in ACL destinations.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:self:*"]
|
||||
}
|
||||
```
|
||||
|
||||
*Using `autogroup:self` may cause performance degradation on the Headscale coordinator server in large deployments, as filter rules must be compiled per-node rather than globally and the current implementation is not very efficient.*
|
||||
|
||||
If you experience performance issues, consider using more specific ACL rules or limiting the use of `autogroup:self`.
|
||||
|
||||
```json
|
||||
{
|
||||
// The following rules allow internal users to communicate with their
|
||||
// own nodes in case autogroup:self is causing performance issues.
|
||||
{ "action": "accept", "src": ["boss@"], "dst": ["boss@:*"] },
|
||||
{ "action": "accept", "src": ["dev1@"], "dst": ["dev1@:*"] },
|
||||
{ "action": "accept", "src": ["dev2@"], "dst": ["dev2@:*"] },
|
||||
{ "action": "accept", "src": ["admin1@"], "dst": ["admin1@:*"] },
|
||||
{ "action": "accept", "src": ["intern1@"], "dst": ["intern1@:*"] }
|
||||
}
|
||||
```
|
||||
|
||||
### `autogroup:nonroot`
|
||||
|
||||
Used in Tailscale SSH rules to allow access to any user except root. Can only be used in the `users` field of SSH rules.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:self"],
|
||||
"users": ["autogroup:nonroot"]
|
||||
}
|
||||
```
|
||||
|
||||
### `autogroup:danger-all`
|
||||
|
||||
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](https://tailscale.com/docs/reference/targets-and-selectors#autogroupdanger-all).
|
||||
+16
-20
@@ -1,10 +1,10 @@
|
||||
# API
|
||||
|
||||
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
|
||||
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
|
||||
integration and tooling.
|
||||
|
||||
Both interfaces require a valid API key before use. To create an API key, log into your Headscale server and generate
|
||||
The API requires 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
|
||||
@@ -29,12 +29,12 @@ headscale apikeys expire --prefix <PREFIX>
|
||||
## REST API
|
||||
|
||||
- API endpoint: `/api/v1`, e.g. `https://headscale.example.com/api/v1`
|
||||
- Documentation: `/swagger`, e.g. `https://headscale.example.com/swagger`
|
||||
- Documentation: `/api/v1/docs`, e.g. `https://headscale.example.com/api/v1/docs`
|
||||
- Headscale Version: `/version`, e.g. `https://headscale.example.com/version`
|
||||
- Authenticate using HTTP Bearer authentication by sending the [API key](#api) with the HTTP `Authorization: Bearer <API_KEY>` header.
|
||||
|
||||
Start by [creating an API key](#api) and test it with the examples below. Read the API documentation provided by your
|
||||
Headscale server at `/swagger` for details.
|
||||
Headscale server at `/api/v1/docs` for details.
|
||||
|
||||
=== "Get details for all users"
|
||||
|
||||
@@ -54,20 +54,18 @@ Headscale server at `/swagger` for details.
|
||||
|
||||
```console
|
||||
curl -H "Authorization: Bearer <API_KEY>" \
|
||||
--json '{"user": "<USER>", "authId": "AUTH_ID>"}' \
|
||||
--json '{"user": "<USER>", "authId": "<AUTH_ID>"}' \
|
||||
https://headscale.example.com/api/v1/auth/register
|
||||
```
|
||||
|
||||
## gRPC
|
||||
## Remote control
|
||||
|
||||
The gRPC interface can be used to control a Headscale instance from a remote machine with the `headscale` binary.
|
||||
The `headscale` binary can control a Headscale instance from a remote machine over the HTTP API.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
- A workstation to run `headscale` (any supported platform, e.g. Linux).
|
||||
- A Headscale server with gRPC enabled.
|
||||
- Connections to the gRPC port (default: `50443`) are allowed.
|
||||
- Remote access requires an encrypted connection via TLS.
|
||||
- The Headscale server reachable over HTTP(S).
|
||||
- An [API key](#api) to authenticate with the Headscale server.
|
||||
|
||||
### Setup remote control
|
||||
@@ -88,19 +86,20 @@ The gRPC interface can be used to control a Headscale instance from a remote mac
|
||||
|
||||
```yaml title="config.yaml"
|
||||
cli:
|
||||
address: <HEADSCALE_ADDRESS>:<PORT>
|
||||
address: <HEADSCALE_URL>
|
||||
api_key: <API_KEY>
|
||||
```
|
||||
|
||||
=== "Environment variables"
|
||||
|
||||
```shell
|
||||
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_ADDRESS>:<PORT>"
|
||||
export HEADSCALE_CLI_ADDRESS="<HEADSCALE_URL>"
|
||||
export HEADSCALE_CLI_API_KEY="<API_KEY>"
|
||||
```
|
||||
|
||||
This instructs the `headscale` binary to connect to a remote instance at `<HEADSCALE_ADDRESS>:<PORT>`, instead of
|
||||
connecting to the local instance.
|
||||
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`.
|
||||
|
||||
1. Test the connection by listing all nodes:
|
||||
|
||||
@@ -113,15 +112,12 @@ The gRPC interface can be used to control a Headscale instance from a remote mac
|
||||
|
||||
### Behind a proxy
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
### 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_
|
||||
|
||||
+3
-3
@@ -53,19 +53,19 @@ Headscale provides a metrics and debug endpoint. It allows to introspect differe
|
||||
|
||||
- Information about the Go runtime, memory usage and statistics
|
||||
- Connected nodes and pending registrations
|
||||
- Active ACLs, filters and SSH policy
|
||||
- Active policy, filters and SSH policy
|
||||
- Current DERPMap
|
||||
- Prometheus metrics
|
||||
|
||||
!!! warning "Keep the metrics and debug endpoint private"
|
||||
|
||||
The listen address and port can be configured with the `metrics_listen_addr` variable in the [configuration
|
||||
file](./configuration.md). By default it listens on localhost, port 9090.
|
||||
file](configuration.md). By default it listens on localhost, port 9090.
|
||||
|
||||
Keep the metrics and debug endpoint private to your internal network and don't expose it to the Internet.
|
||||
|
||||
The metrics and debug interface can be disabled completely by setting `metrics_listen_addr: null` in the
|
||||
[configuration file](./configuration.md).
|
||||
[configuration file](configuration.md).
|
||||
|
||||
Query metrics via <http://localhost:9090/metrics> and get an overview of available debug information via
|
||||
<http://localhost:9090/debug/>. Metrics may be queried from outside localhost but the debug interface is subject to
|
||||
|
||||
+4
-4
@@ -6,8 +6,8 @@ DERP server to ensure seamless connectivity between nodes.
|
||||
|
||||
## Configuration
|
||||
|
||||
DERP related settings are configured within the `derp` section of the [configuration file](./configuration.md). The
|
||||
following sections only use a few of the available settings, check the [example configuration](./configuration.md) for
|
||||
DERP related settings are configured within the `derp` section of the [configuration file](configuration.md). The
|
||||
following sections only use a few of the available settings, check the [example configuration](configuration.md) for
|
||||
all available configuration options.
|
||||
|
||||
### Enable embedded DERP
|
||||
@@ -163,7 +163,7 @@ Any Tailscale client may be used to introspect the DERP map and to check for con
|
||||
- Check connectivity with the embedded DERP[^1]:`tailscale debug derp headscale`
|
||||
|
||||
Additional DERP related metrics and information is available via the [metrics and debug
|
||||
endpoint](./debug.md#metrics-and-debug-endpoint).
|
||||
endpoint](debug.md#metrics-and-debug-endpoint).
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -171,4 +171,4 @@ endpoint](./debug.md#metrics-and-debug-endpoint).
|
||||
endpoint via HTTP on port tcp/80.
|
||||
- There are no speed or throughput optimisations, the main purpose is to assist in node connectivity.
|
||||
|
||||
[^1]: This assumes that the default region code of the [configuration file](./configuration.md) is used.
|
||||
[^1]: This assumes that the default region code of the [configuration file](configuration.md) is used.
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
# DNS
|
||||
|
||||
Headscale supports [most DNS features](../about/features.md) from Tailscale. DNS related settings can be configured
|
||||
within the `dns` section of the [configuration file](./configuration.md).
|
||||
within the `dns` section of the [configuration file](configuration.md).
|
||||
|
||||
## Setting extra DNS records
|
||||
|
||||
Headscale allows to set extra DNS records which are made available via
|
||||
[MagicDNS](https://tailscale.com/docs/features/magicdns). Extra DNS records can be configured either via static entries
|
||||
in the [configuration file](./configuration.md) or from a JSON file that Headscale continuously watches for changes:
|
||||
in the [configuration file](configuration.md) or from a JSON file that Headscale continuously watches for changes:
|
||||
|
||||
- Use the `dns.extra_records` option in the [configuration file](./configuration.md) for entries that are static and
|
||||
- Use the `dns.extra_records` option in the [configuration file](configuration.md) for entries that are static and
|
||||
don't change while Headscale is running. Those entries are processed when Headscale is starting up and changes to the
|
||||
configuration require a restart of Headscale.
|
||||
- For dynamic DNS records that may be added, updated or removed while Headscale is running or DNS records that are
|
||||
generated by scripts the option `dns.extra_records_path` in the [configuration file](./configuration.md) is useful.
|
||||
generated by scripts the option `dns.extra_records_path` in the [configuration file](configuration.md) is useful.
|
||||
Set it to the absolute path of the JSON file containing DNS records and Headscale processes this file as it detects
|
||||
changes.
|
||||
|
||||
@@ -66,7 +66,7 @@ hostname and port combination "http://hostname-in-magic-dns.myvpn.example.com:30
|
||||
|
||||
!!! tip "Good to know"
|
||||
|
||||
- The `dns.extra_records_path` option in the [configuration file](./configuration.md) needs to reference the
|
||||
- The `dns.extra_records_path` option in the [configuration file](configuration.md) needs to reference the
|
||||
JSON file containing extra DNS records.
|
||||
- Be sure to "sort keys" and produce a stable output in case you generate the JSON file with a script.
|
||||
Headscale uses a checksum to detect changes to the file and a stable output avoids unnecessary processing.
|
||||
|
||||
@@ -1,90 +1,183 @@
|
||||
# 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.
|
||||
|
||||
### WebSockets
|
||||
Please see [limitations](#limitations) for known issues and limitations.
|
||||
|
||||
The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients.
|
||||
## Configuration
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
### Cloudflare
|
||||
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.
|
||||
|
||||
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)
|
||||
### 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 config file.
|
||||
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"
|
||||
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
|
||||
```yaml title="config.yaml" hl_lines="1"
|
||||
server_url: https://<SERVER_NAME>
|
||||
tls_cert_path: ""
|
||||
tls_key_path: ""
|
||||
```
|
||||
|
||||
## nginx
|
||||
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.
|
||||
|
||||
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`.
|
||||
### Trusted proxies
|
||||
|
||||
```nginx title="nginx.conf"
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
<SERVER_NAME> {
|
||||
reverse_proxy 127.0.0.1:8080 {
|
||||
header_up True-Client-IP {remote_host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## istio/envoy
|
||||
Caddy will [automatically](https://caddyserver.com/docs/automatic-https) provision a certificate for your
|
||||
domain/subdomain, force HTTPS, and proxy WebSocket connections.
|
||||
|
||||
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:
|
||||
### Cloudflare
|
||||
|
||||
```log
|
||||
Sending local reply with details upgrade_failed
|
||||
```
|
||||
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.
|
||||
|
||||
### 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, we can use `EnvoyFilter` to add upgrade_type.
|
||||
Same as [envoy](#envoy), we can use `EnvoyFilter` to add a new upgrade_type named `tailscale-control-protocol`.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
@@ -109,33 +202,68 @@ spec:
|
||||
- upgrade_type: tailscale-control-protocol
|
||||
```
|
||||
|
||||
## Caddy
|
||||
### Nginx
|
||||
|
||||
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`.
|
||||
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:
|
||||
|
||||
```none title="Caddyfile"
|
||||
<YOUR_SERVER_NAME> {
|
||||
reverse_proxy <IP:PORT>
|
||||
- `<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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
@@ -19,9 +19,11 @@ Headscale doesn't provide a built-in web interface but users may pick one from t
|
||||
it offers Local (`docker exec`) and API Mode
|
||||
- [headscale-console](https://github.com/rickli-cloud/headscale-console) - WebAssembly-based client supporting SSH, VNC
|
||||
and RDP with optional self-service capabilities
|
||||
- [headscale-piying](https://github.com/wszgrcy/headscale-piying) - headscale web ui,support visual ACL configuration
|
||||
- [headscale-piying](https://github.com/wszgrcy/headscale-piying) - headscale web ui, support visual ACL configuration
|
||||
- [HeadControl](https://github.com/ahmadzip/HeadControl) - Minimal Headscale admin dashboard, built with Go and HTMX
|
||||
- [Headscale Manager](https://github.com/hkdone/headscalemanager) - Headscale UI for Android
|
||||
- [Headscale UI](https://github.com/MunMunMiao/headscale-ui) - Headscale UI online and Self-hosting
|
||||
- [Headscale Panel](https://github.com/headscale-panel/panel) - A modern Headscale management panel with a clean,
|
||||
network-operations-focused UI
|
||||
|
||||
You can ask for support on our [Discord server](https://discord.gg/c84AZQhmpx) in the "web-interfaces" channel.
|
||||
|
||||
+7
-7
@@ -214,14 +214,14 @@ You may refer to users in the Headscale policy via:
|
||||
{
|
||||
"groups": {
|
||||
"group:alice": [
|
||||
"https://soo.example.com/oauth2/openid/59ac9125-c31b-46c5-814e-06242908cf57@"
|
||||
"https://sso.example.com/oauth2/openid/59ac9125-c31b-46c5-814e-06242908cf57@"
|
||||
]
|
||||
},
|
||||
"acls": [
|
||||
"grants": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["group:alice"],
|
||||
"dst": ["*:*"]
|
||||
"dst": ["*"],
|
||||
"ip": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -246,7 +246,7 @@ endpoint.
|
||||
|
||||
- Support for OpenID Connect aims to be generic and vendor independent. It offers only limited support for quirks of
|
||||
specific identity providers.
|
||||
- OIDC groups cannot be used in ACLs.
|
||||
- OIDC groups cannot be used in policy rules.
|
||||
- The username provided by the identity provider needs to adhere to this pattern:
|
||||
- The username must be at least two characters long.
|
||||
- It must only contain letters, digits, hyphens, dots, underscores, and up to a single `@`.
|
||||
@@ -283,9 +283,9 @@ Authelia is fully supported by Headscale.
|
||||
|
||||
### Google OAuth
|
||||
|
||||
!!! warning "No username due to missing preferred_username"
|
||||
!!! warning "No username due to missing preferred_username claim"
|
||||
|
||||
Google OAuth does not send the `preferred_username` claim when the scope `profile` is requested. The username in
|
||||
Google OAuth does not send the `preferred_username` claim when the `profile` scope is requested. The username in
|
||||
Headscale will be blank/not set.
|
||||
|
||||
In order to integrate Headscale with Google, you'll need to have a [Google Cloud
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
# Policy
|
||||
|
||||
Headscale implements a large portion of Tailscale's [policy
|
||||
features](https://tailscale.com/docs/features/tailnet-policy-file), most notably access control based on
|
||||
[ACLs](https://tailscale.com/docs/features/access-control/acls) and
|
||||
[Grants](https://tailscale.com/docs/features/access-control/grants) or [Tailscale
|
||||
SSH](https://tailscale.com/docs/features/tailscale-ssh). See [limitations](#limitations) to learn about missing features
|
||||
and notable implementation differences between Headscale and Tailscale.
|
||||
|
||||
Headscale uses the same [huJSON](https://github.com/tailscale/hujson) based file format as Tailscale. By default, no
|
||||
policy is loaded which means that Headscale allows all traffic between nodes. To start using a policy file[^1], specify
|
||||
its path in the `policy.path` key in the [configuration file](configuration.md).
|
||||
|
||||
Headscale needs to be reloaded to pick up changes to the policy file. Either reload Headscale via its systemd service
|
||||
(`sudo systemctl reload headscale`) or by sending a SIGHUP signal (`sudo kill -HUP $(pidof headscale)`) to the main
|
||||
process. Headscale logs the result of policy processing after each reload.
|
||||
|
||||
Please have a look at Tailscale's policy related documentation to learn more:
|
||||
|
||||
- [Tailscale policy file](https://tailscale.com/docs/features/tailnet-policy-file): A description of supported sections
|
||||
within the policy file along with links to syntax references for each section.
|
||||
- [ACLs](https://tailscale.com/docs/features/access-control/acls): How to configure access control using ACLs.
|
||||
- [Grants](https://tailscale.com/docs/features/access-control/grants): Introduction to Grants with links to [syntax
|
||||
reference](https://tailscale.com/docs/reference/syntax/grants),
|
||||
[examples](https://tailscale.com/docs/reference/examples/grants) and a [migration guide from ACLs to
|
||||
Grants](https://tailscale.com/docs/reference/migrate-acls-grants).
|
||||
|
||||
## Getting started
|
||||
|
||||
Headscale supports both [ACLs](https://tailscale.com/docs/features/access-control/acls) and
|
||||
[Grants](https://tailscale.com/docs/features/access-control/grants) to write an access control policy. We recommend the
|
||||
use of Grants since ACLs are considered legacy and will not receive new features by Tailscale.
|
||||
|
||||
### Allow All
|
||||
|
||||
If you define a policy file but completely omit the `"acls"` or `"grants"` section, Headscale will default to an [allow
|
||||
all](https://tailscale.com/docs/reference/examples/acls#allow-all-default-acl) policy. This means all devices connected
|
||||
to your tailnet will be able to communicate freely with each other.
|
||||
|
||||
```json title="policy.json"
|
||||
{}
|
||||
```
|
||||
|
||||
### Deny All
|
||||
|
||||
To [prevent all communication within your tailnet](https://tailscale.com/docs/reference/examples/acls#deny-all), you can
|
||||
include an empty array for the `"grants"` section in your policy file.
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
"grants": []
|
||||
}
|
||||
```
|
||||
|
||||
### More examples
|
||||
|
||||
- See our documentation on [subnet routers](routes.md#subnet-router) and [exit nodes](routes.md#exit-node) to learn how
|
||||
to restrict their use or how to automatically approve them.
|
||||
- The Tailscale documentation provides a large collection of configuration examples:
|
||||
- [ACL examples](https://tailscale.com/docs/reference/examples/acls)
|
||||
- [Grants examples](https://tailscale.com/docs/reference/examples/grants)
|
||||
- [SSH configuration](https://tailscale.com/docs/features/tailscale-ssh#configure-tailscale-ssh)
|
||||
- [Define a tag](https://tailscale.com/docs/features/tags#define-a-tag)
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Limitations
|
||||
|
||||
- [Device postures](https://tailscale.com/docs/features/device-posture) and the related sections such as `postures` or
|
||||
`srcPosture` aren't supported.
|
||||
- [IP sets](https://tailscale.com/docs/features/tailnet-policy-file/ip-sets) aren't supported.
|
||||
- A subset of [Autogroups](#autogroups) are available.
|
||||
|
||||
## Autogroups
|
||||
|
||||
Headscale supports several [Autogroups](https://tailscale.com/docs/reference/targets-and-selectors#autogroups) that
|
||||
automatically include users, destinations, or devices with specific properties. Autogroups provide a convenient way to
|
||||
write policy rules without manually listing individual users or devices.
|
||||
|
||||
### [`autogroup:internet`](https://tailscale.com/docs/reference/targets-and-selectors#autogroupinternet)
|
||||
|
||||
Allows access to the internet through [exit nodes](routes.md#exit-node). Can only be used in policy destinations.
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
"grants": [
|
||||
{
|
||||
"src": ["alice@"],
|
||||
"dst": ["autogroup:internet"],
|
||||
"ip": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### [`autogroup:member`](https://tailscale.com/docs/reference/targets-and-selectors#autogrouprole)
|
||||
|
||||
Includes all [personal (untagged) devices](registration.md/#identity-model).
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
"grants": [
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["tag:prod-app-servers"],
|
||||
"ip": ["80,443"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### [`autogroup:tagged`](https://tailscale.com/docs/reference/targets-and-selectors#autogrouptagged)
|
||||
|
||||
Includes all devices that [have at least one tag](registration.md/#identity-model).
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
"grants": [
|
||||
{
|
||||
"src": ["autogroup:tagged"],
|
||||
"dst": ["tag:monitoring"],
|
||||
"ip": ["9090"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### [`autogroup:self`](https://tailscale.com/docs/reference/targets-and-selectors#autogroupself)
|
||||
|
||||
Includes devices where the same user is authenticated on both the source and destination. Does not include tagged
|
||||
devices. Can only be used in policy destinations.
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
"grants": [
|
||||
{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:self"],
|
||||
"ip": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning "The current implementation of `autogroup:self` is inefficient"
|
||||
|
||||
Using `autogroup:self` may cause performance degradation on the Headscale coordinator server in large deployments,
|
||||
as filter rules must be compiled per-node rather than globally and the current implementation is not very efficient.
|
||||
|
||||
If you experience performance issues, consider using more specific policy rules or limiting the use of
|
||||
`autogroup:self`.
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
"grants": [
|
||||
// The following rules allow internal users to communicate with their
|
||||
// own nodes in case autogroup:self is causing performance issues.
|
||||
{
|
||||
"src": ["boss@"],
|
||||
"dst": ["boss@"],
|
||||
"ip": ["*"]
|
||||
},
|
||||
{
|
||||
"src": ["dev1@"],
|
||||
"dst": ["dev1@"],
|
||||
"ip": ["*"]
|
||||
},
|
||||
{
|
||||
"src": ["intern1@"],
|
||||
"dst": ["intern1@"],
|
||||
"ip": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### [`autogroup:nonroot`](https://tailscale.com/docs/reference/targets-and-selectors#other-built-in-targets)
|
||||
|
||||
Used in Tailscale SSH rules to allow access to any user except root. Can only be used in the `users` field of SSH rules.
|
||||
|
||||
```json title="policy.json"
|
||||
{
|
||||
"ssh": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:self"],
|
||||
"users": ["autogroup:nonroot"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### [`autogroup:danger-all`](https://tailscale.com/docs/reference/targets-and-selectors#autogroupdanger-all)
|
||||
|
||||
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).
|
||||
@@ -61,8 +61,8 @@ headscale users create <USER>
|
||||
=== "Tagged devices"
|
||||
|
||||
Your Headscale user needs to be authorized to register tagged devices. This authorization is specified in the
|
||||
[`tagOwners`](https://tailscale.com/docs/reference/syntax/policy-file#tag-owners) section of the [ACL](acls.md). A
|
||||
simple example looks like this:
|
||||
[`tagOwners`](https://tailscale.com/docs/reference/syntax/policy-file#tag-owners) section of the
|
||||
[policy](policy.md). A simple example looks like this:
|
||||
|
||||
```json title="The user alice can register nodes tagged with tag:server"
|
||||
{
|
||||
|
||||
+38
-42
@@ -76,29 +76,29 @@ Please refer to the official [Tailscale
|
||||
documentation](https://tailscale.com/docs/features/subnet-routers#use-your-subnet-routes-from-other-devices) for how to
|
||||
use a subnet router on different operating systems.
|
||||
|
||||
### Restrict the use of a subnet router with ACL
|
||||
### Restrict the use of a subnet router with a policy
|
||||
|
||||
The routes announced by subnet routers are available to the nodes in a tailnet. By default, without an ACL enabled, all
|
||||
nodes can accept and use such routes. Configure an ACL to explicitly manage who can use routes.
|
||||
The routes announced by subnet routers are available to the nodes in a tailnet. By default, without a policy enabled,
|
||||
all nodes can accept and use such routes. Configure a policy to explicitly manage who can use routes.
|
||||
|
||||
The ACL snippet below defines three hosts, a subnet router `router`, a regular node `node` and `service.example.net` as
|
||||
internal service that can be reached via a route on the subnet router `router`. It allows the node `node` to access
|
||||
The policy snippet below defines three hosts, a subnet router `router`, a regular node `node` and `service.example.net`
|
||||
as internal service that can be reached via a route on the subnet router `router`. It allows the node `node` to access
|
||||
`service.example.net` on port 80 and 443 which is reachable via the subnet router. Access to the subnet router itself is
|
||||
denied.
|
||||
|
||||
```json title="Access the routes of a subnet router without the subnet router itself"
|
||||
{
|
||||
"hosts": {
|
||||
// the router is not referenced but announces 192.168.0.0/24"
|
||||
// the router is not referenced but announces 192.168.0.0/24
|
||||
"router": "100.64.0.1/32",
|
||||
"node": "100.64.0.2/32",
|
||||
"service.example.net": "192.168.0.1/32"
|
||||
},
|
||||
"acls": [
|
||||
"grants": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["node"],
|
||||
"dst": ["service.example.net:80,443"]
|
||||
"dst": ["service.example.net"],
|
||||
"ip": ["80,443"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -107,10 +107,10 @@ denied.
|
||||
### Automatically approve routes of a subnet router
|
||||
|
||||
The initial setup of a subnet router usually requires manual approval of their announced routes on the control server
|
||||
before they can be used by a node in a tailnet. Headscale supports the `autoApprovers` section of an ACL to automate the
|
||||
approval of routes served with a subnet router.
|
||||
before they can be used by a node in a tailnet. Headscale supports the `autoApprovers` section in a policy to automate
|
||||
the approval of routes served with a subnet router.
|
||||
|
||||
The ACL snippet below defines the tag `tag:router` owned by the user `alice`. This tag is used for `routes` in the
|
||||
The policy snippet below defines the tag `tag:router` owned by the user `alice`. This tag is used for `routes` in the
|
||||
`autoApprovers` section. The IPv4 route `192.168.0.0/24` is automatically approved once announced by a subnet router
|
||||
that advertises the tag `tag:router`.
|
||||
|
||||
@@ -124,7 +124,7 @@ that advertises the tag `tag:router`.
|
||||
"192.168.0.0/24": ["tag:router"]
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
"grants": [
|
||||
// more rules
|
||||
]
|
||||
}
|
||||
@@ -204,19 +204,19 @@ $ sudo tailscale set --exit-node myexit
|
||||
Please refer to the official [Tailscale documentation](https://tailscale.com/docs/features/exit-nodes#use-the-exit-node)
|
||||
for how to use an exit node on different operating systems.
|
||||
|
||||
### Restrict the use of an exit node with ACL
|
||||
### Restrict the use of an exit node with a policy
|
||||
|
||||
An exit node is offered to all nodes in a tailnet. By default, without an ACL enabled, all nodes in a tailnet can select
|
||||
and use an exit node. Configure `autogroup:internet` in an ACL rule to restrict who can use _any_ of the available exit
|
||||
nodes.
|
||||
An exit node is offered to all nodes in a tailnet. By default, without a policy enabled, all nodes in a tailnet can
|
||||
select and use an exit node. Configure `autogroup:internet` in a policy rule to restrict who can use _any_ of the
|
||||
available exit nodes.
|
||||
|
||||
```json title="Example use of autogroup:internet"
|
||||
{
|
||||
"acls": [
|
||||
"grants": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["..."],
|
||||
"dst": ["autogroup:internet:*"]
|
||||
"dst": ["autogroup:internet"],
|
||||
"ip": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -224,45 +224,41 @@ nodes.
|
||||
|
||||
### Restrict access to exit nodes per user or group
|
||||
|
||||
A user can use _any_ of the available exit nodes with `autogroup:internet`. Alternatively, the ACL snippet below assigns
|
||||
each user a specific exit node while hiding all other exit nodes. The user `alice` can only use exit node `exit1` while
|
||||
user `bob` can only use exit node `exit2`.
|
||||
A user can use _any_ of the available exit nodes with `autogroup:internet`. Alternatively, the policy snippet below
|
||||
assigns each user a specific exit node while hiding all other exit nodes. The user `alice` can only use an exit node
|
||||
tagged with `tag:exit1` while user `bob` can only use an exit node tagged with `tag:exit2`.
|
||||
|
||||
```json title="Assign each user a dedicated exit node"
|
||||
{
|
||||
"hosts": {
|
||||
"exit1": "100.64.0.1/32",
|
||||
"exit2": "100.64.0.2/32"
|
||||
"tagOwners": {
|
||||
"tag:exit1": ["alice@"],
|
||||
"tag:exit2": ["bob@"]
|
||||
},
|
||||
"acls": [
|
||||
"grants": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["alice@"],
|
||||
"dst": ["exit1:*"]
|
||||
"dst": ["autogroup:internet"],
|
||||
"via": ["tag:exit1"],
|
||||
"ip": ["*"]
|
||||
},
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["bob@"],
|
||||
"dst": ["exit2:*"]
|
||||
"dst": ["autogroup:internet"],
|
||||
"via": ["tag:exit2"],
|
||||
"ip": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
- The above implementation is Headscale specific and will likely be removed once [support for
|
||||
`via`](https://github.com/juanfont/headscale/issues/2409) is available.
|
||||
- Beware that a user can also connect to any port of the exit node itself.
|
||||
|
||||
### Automatically approve an exit node with auto approvers
|
||||
|
||||
The initial setup of an exit node usually requires manual approval on the control server before it can be used by a node
|
||||
in a tailnet. Headscale supports the `autoApprovers` section of an ACL to automate the approval of a new exit node as
|
||||
in a tailnet. Headscale supports the `autoApprovers` section in a policy to automate the approval of a new exit node as
|
||||
soon as it joins the tailnet.
|
||||
|
||||
The ACL snippet below defines the tag `tag:exit` owned by the user `alice`. This tag is used for `exitNode` in the
|
||||
`autoApprovers` section. A new exit node that advertises the tag `tag:exit` is automatically approved:
|
||||
The policy snippet below defines the tag `tag:exit` owned by the user `alice`. This tag is used for the `exitNode` entry
|
||||
in the `autoApprovers` section. A new exit node that advertises the tag `tag:exit` is automatically approved:
|
||||
|
||||
```json title="Exit nodes tagged with tag:exit are automatically approved"
|
||||
{
|
||||
@@ -272,7 +268,7 @@ The ACL snippet below defines the tag `tag:exit` owned by the user `alice`. This
|
||||
"autoApprovers": {
|
||||
"exitNode": ["tag:exit"]
|
||||
},
|
||||
"acls": [
|
||||
"grants": [
|
||||
// more rules
|
||||
]
|
||||
}
|
||||
@@ -295,7 +291,7 @@ to clients. Please see the official [Tailscale documentation on high
|
||||
availability](https://tailscale.com/docs/how-to/set-up-high-availability#subnet-router-high-availability) for details.
|
||||
|
||||
This feature is enabled by default when at least two nodes advertise the same prefix. See the configuration options
|
||||
`node.routes.ha` in the [configuration file](./configuration.md) for details.
|
||||
`node.routes.ha` in the [configuration file](configuration.md) for details.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Community packages
|
||||
|
||||
Several Linux distributions and community members provide packages for headscale. Those packages may be used instead of
|
||||
the [official releases](./official.md) provided by the headscale maintainers. Such packages offer improved integration
|
||||
the [official releases](official.md) provided by the headscale maintainers. Such packages offer improved integration
|
||||
for their targeted operating system and usually:
|
||||
|
||||
- setup a dedicated local user account to run headscale
|
||||
@@ -10,8 +10,8 @@ for their targeted operating system and usually:
|
||||
|
||||
!!! warning "Community packages might be outdated"
|
||||
|
||||
The packages mentioned on this page might be outdated or unmaintained. Use the [official releases](./official.md) to
|
||||
get the current stable version or to test pre-releases.
|
||||
The packages mentioned on this page might be outdated or unmaintained. Use the [official releases](official.md) to
|
||||
get the current stable version or to [test pre-releases](main.md).
|
||||
|
||||
[](https://repology.org/project/headscale/versions)
|
||||
|
||||
@@ -23,9 +23,6 @@ Arch Linux offers a package for headscale, install via:
|
||||
pacman -S headscale
|
||||
```
|
||||
|
||||
The [AUR package `headscale-git`](https://aur.archlinux.org/packages/headscale-git) can be used to build the current
|
||||
development version.
|
||||
|
||||
## Fedora, RHEL, CentOS
|
||||
|
||||
A third-party repository for various RPM based distributions is available at:
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
|
||||
**It might be outdated and it might miss necessary steps**.
|
||||
|
||||
This documentation has the goal of showing a user how-to set up and run headscale in a container. A container runtime
|
||||
such as [Docker](https://www.docker.com) or [Podman](https://podman.io) is required. The container image can be found on
|
||||
[Docker Hub](https://hub.docker.com/r/headscale/headscale) and [GitHub Container
|
||||
A container runtime such as [Docker](https://www.docker.com) or [Podman](https://podman.io) is required. The container
|
||||
image can be found on [Docker Hub](https://hub.docker.com/r/headscale/headscale) and [GitHub Container
|
||||
Registry](https://github.com/juanfont/headscale/pkgs/container/headscale). The container image URLs are:
|
||||
|
||||
- [Docker Hub](https://hub.docker.com/r/headscale/headscale): `docker.io/headscale/headscale:<VERSION>`
|
||||
@@ -18,7 +17,7 @@ Registry](https://github.com/juanfont/headscale/pkgs/container/headscale). The c
|
||||
|
||||
## Configure and run headscale
|
||||
|
||||
1. Create a directory on the container host to store headscale's [configuration](../../ref/configuration.md) and the [SQLite](https://www.sqlite.org/) database:
|
||||
1. Create a directory on the container host to store headscale's [configuration](../../ref/configuration.md) and the SQLite database:
|
||||
|
||||
```shell
|
||||
mkdir -p ./headscale/{config,lib}
|
||||
@@ -98,7 +97,7 @@ Continue on the [getting started page](../../usage/getting-started.md) to regist
|
||||
|
||||
## Debugging headscale running in Docker
|
||||
|
||||
The Headscale container image is based on a "distroless" image that does not contain a shell or any other debug tools. If you need to debug headscale running in the Docker container, you can use the `-debug` variant, for example `docker.io/headscale/headscale:x.x.x-debug`.
|
||||
The Headscale container image is based on a distroless image that does not contain a shell or any other debug tools. If you need to debug headscale running in the Docker container, you can use the `-debug` variant, for example `docker.io/headscale/headscale:x.x.x-debug`.
|
||||
|
||||
### Running the debug Docker container
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ docker run \
|
||||
serve
|
||||
```
|
||||
|
||||
See [Running headscale in a container](./container.md) for full container setup instructions.
|
||||
See [Running headscale in a container](container.md) for full container setup instructions.
|
||||
|
||||
## Binaries
|
||||
|
||||
@@ -54,5 +54,5 @@ via [nightly.link](https://nightly.link/juanfont/headscale/workflows/container-m
|
||||
| macOS | arm64 | [headscale-darwin-arm64](https://nightly.link/juanfont/headscale/workflows/container-main/main/headscale-darwin-arm64.zip) |
|
||||
|
||||
After downloading and extracting the archive, make the binary executable and follow the
|
||||
[standalone binary installation](./official.md#using-standalone-binaries-advanced)
|
||||
[standalone binary installation](official.md#using-standalone-binaries-advanced)
|
||||
instructions for setting up the service.
|
||||
|
||||
@@ -31,10 +31,10 @@ distributions are Ubuntu 22.04 or newer, Debian 12 or newer.
|
||||
sudo nano /etc/headscale/config.yaml
|
||||
```
|
||||
|
||||
1. Enable and start the headscale service:
|
||||
1. Restart headscale to pick up configuration changes:
|
||||
|
||||
```shell
|
||||
sudo systemctl enable --now headscale
|
||||
sudo systemctl restart headscale
|
||||
```
|
||||
|
||||
1. Verify that headscale is running as intended:
|
||||
@@ -51,7 +51,7 @@ Continue on the [getting started page](../../usage/getting-started.md) to regist
|
||||
|
||||
This installation method is considered advanced as one needs to take care of the local user and the systemd
|
||||
service themselves. If possible, use the [DEB packages](#using-packages-for-debianubuntu-recommended) or a
|
||||
[community package](./community.md) instead.
|
||||
[community package](community.md) instead.
|
||||
|
||||
This section describes the installation of headscale according to the [Requirements and
|
||||
assumptions](../requirements.md#assumptions). Headscale is run by a dedicated local user and the service itself is
|
||||
|
||||
@@ -26,9 +26,6 @@ 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)
|
||||
@@ -40,7 +37,7 @@ The headscale documentation and the provided examples are written with a few ass
|
||||
- Headscale is running as system service via a dedicated local user `headscale`.
|
||||
- The [configuration](../ref/configuration.md) is loaded from `/etc/headscale/config.yaml`.
|
||||
- SQLite is used as database.
|
||||
- The data directory for headscale (used for private keys, ACLs, SQLite database, …) is located in `/var/lib/headscale`.
|
||||
- The data directory for headscale (used for private keys, policy, SQLite database, …) is located in `/var/lib/headscale`.
|
||||
- URLs and values that need to be replaced by the user are either denoted as `<VALUE_TO_CHANGE>` or use placeholder
|
||||
values such as `headscale.example.com`.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
!!! tip "Required update path"
|
||||
|
||||
Its required to update from one stable version to the next (e.g. 0.26.0 → 0.27.1 → 0.28.0) without skipping minor
|
||||
It's required to update from one stable version to the next (e.g. 0.26.0 → 0.27.1 → 0.28.0) without skipping minor
|
||||
versions in between. You should always pick the latest available patch release.
|
||||
|
||||
Update an existing Headscale installation to a new version:
|
||||
@@ -23,7 +23,7 @@ upgrading. A full backup of Headscale depends on your individual setup, but belo
|
||||
|
||||
=== "Standard installation"
|
||||
|
||||
A installation that follows our [official releases](install/official.md) setup guide uses the following paths:
|
||||
An installation that follows our [official releases](install/official.md) setup guide uses the following paths:
|
||||
|
||||
- [Configuration file](../ref/configuration.md): `/etc/headscale/config.yaml`
|
||||
- Data directory: `/var/lib/headscale`
|
||||
@@ -37,7 +37,7 @@ upgrading. A full backup of Headscale depends on your individual setup, but belo
|
||||
|
||||
=== "Container"
|
||||
|
||||
A installation that follows our [container](install/container.md) setup guide uses a single source volume directory
|
||||
An installation that follows our [container](install/container.md) setup guide uses a single source volume directory
|
||||
that contains the configuration file, data directory and the SQLite database.
|
||||
|
||||
```console
|
||||
|
||||
Generated
+82
-5
@@ -1,5 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-checks": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781951072,
|
||||
"narHash": "sha256-hA9u6hB4QzpReP8hudxqiaa4vLhoRvtXi6YmUgJRGEs=",
|
||||
"owner": "kradalby",
|
||||
"repo": "flake-checks",
|
||||
"rev": "3d2882efec5cf10f8b5a8a035d93ba6ba9f21717",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "kradalby",
|
||||
"repo": "flake-checks",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@@ -18,25 +40,44 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775701739,
|
||||
"narHash": "sha256-2FWWY1rr/+pGUJK1npcVcsWNEblzmKs6VxD3VEvwJSs=",
|
||||
"lastModified": 1781153106,
|
||||
"narHash": "sha256-yzsroLCcuRG4KdGMxWt0eXKOrRSgQT8/xjYngeq9ujU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0f7663154ff2fec150f9dbf5f81ec2785dc1e0db",
|
||||
"rev": "9ee75f111a06d7ab2b2f729698a8eff53d54e070",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"ref": "staging-next-26.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"flake-checks": "flake-checks",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
@@ -54,6 +95,42 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"flake-checks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780220602,
|
||||
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -2,14 +2,24 @@
|
||||
description = "headscale - Open Source Tailscale Control server";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
# 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";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
# Reusable Go flake checks (build/test/lint/format); CI runs them via
|
||||
# `nix build .#checks.<system>.<name>` instead of bespoke per-tool steps.
|
||||
flake-checks.url = "github:kradalby/flake-checks";
|
||||
flake-checks.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self
|
||||
, nixpkgs
|
||||
, flake-utils
|
||||
, flake-checks
|
||||
, ...
|
||||
}:
|
||||
let
|
||||
@@ -26,8 +36,9 @@
|
||||
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 = "sha256-8vTEkPEMbJ6DSOjcoQrYRyKSYI8jjcllTmJ6RXmUV9w=";
|
||||
vendorHash = (builtins.fromJSON (builtins.readFile ./flakehashes.json)).vendor.sri;
|
||||
in
|
||||
{
|
||||
headscale = buildGo {
|
||||
@@ -38,8 +49,8 @@
|
||||
# Only run unit tests when testing a build
|
||||
checkFlags = [ "-short" ];
|
||||
|
||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
||||
# update this if you have a mismatch after doing a change to those files.
|
||||
# vendorHash is read from flakehashes.json; refresh via:
|
||||
# go run ./cmd/vendorhash update
|
||||
inherit vendorHash;
|
||||
|
||||
subPackages = [ "cmd/headscale" ];
|
||||
@@ -60,53 +71,20 @@
|
||||
subPackages = [ "cmd/hi" ];
|
||||
};
|
||||
|
||||
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)
|
||||
# Build golangci-lint with stock Go 1.26 (upstream uses hardcoded Go
|
||||
# version); it does not build against the pinned 1.26.4.
|
||||
golangci-lint = buildGo rec {
|
||||
pname = "golangci-lint";
|
||||
version = "2.11.4";
|
||||
version = "2.12.2";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "golangci";
|
||||
repo = "golangci-lint";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-B19aLvfNRY9TOYw/71f2vpNUuSIz8OI4dL0ijGezsas=";
|
||||
hash = "sha256-qR7fp1x2S+EwEAcplRHTvA3jWwLr/XSiYKSZtAwkrNU=";
|
||||
};
|
||||
|
||||
vendorHash = "sha256-xuoj4+U4tB5gpABKq4Dbp2cxnljxdYoBbO8A7DqPM5E=";
|
||||
vendorHash = "sha256-AG5wtLwWLz55bdp1oi3cW+9O3yj1W1P7MV9zxym7Pb4=";
|
||||
|
||||
subPackages = [ "cmd/golangci-lint" ];
|
||||
|
||||
@@ -179,6 +157,11 @@
|
||||
yq-go
|
||||
ripgrep
|
||||
postgresql
|
||||
|
||||
# External clients exercised by the Tailscale-compatible v2 API
|
||||
# roundtrip tests (TestAPIv2). Binaries: tofu, tscli.
|
||||
opentofu
|
||||
tscli
|
||||
python314Packages.mdformat
|
||||
python314Packages.mdformat-footnote
|
||||
python314Packages.mdformat-frontmatter
|
||||
@@ -188,17 +171,8 @@
|
||||
# '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.optional pkgs.stdenv.isLinux [ traceroute ];
|
||||
++ lib.optionals pkgs.stdenv.isLinux [ traceroute ];
|
||||
|
||||
# Add entry to build a docker image with headscale
|
||||
# caveat: only works on Linux
|
||||
@@ -212,6 +186,59 @@
|
||||
contents = [ pkgs.headscale ];
|
||||
config.Entrypoint = [ (pkgs.headscale + "/bin/headscale") ];
|
||||
};
|
||||
|
||||
# Go flake checks from the flake-checks library. CI gates on
|
||||
# `nix build .#checks.<system>.<name>`; the logic lives here, not in
|
||||
# bespoke workflow steps. Linux-only: parts of the tree are
|
||||
# Linux-specific and the pure unit subset is validated by CI.
|
||||
fc = flake-checks.lib;
|
||||
common = {
|
||||
inherit pkgs;
|
||||
root = ./.;
|
||||
pname = "headscale";
|
||||
version = headscaleVersion;
|
||||
vendorHash = (builtins.fromJSON (builtins.readFile ./flakehashes.json)).vendor.sri;
|
||||
goPkg = pkgs.go_1_26;
|
||||
# //go:embed targets and test-read files outside the default whitelist.
|
||||
embedDirs = [ ./hscontrol/assets ./hscontrol/db/schema.sql ./config-example.yaml ];
|
||||
extraSrc = [
|
||||
./hscontrol/testdata
|
||||
./hscontrol/types/testdata
|
||||
./hscontrol/db/testdata
|
||||
./hscontrol/policy/v2/testdata
|
||||
];
|
||||
};
|
||||
goChecks = {
|
||||
build = fc.goBuild (common // { subPackages = [ "cmd/headscale" ]; });
|
||||
|
||||
# The pure unit subset. ./integration (Docker) and
|
||||
# ./hscontrol/servertest (slow: 10s+ convergence plus race/stress/HA
|
||||
# property tests — run by the servertest workflow instead) are dropped
|
||||
# from the test set but kept in source so cmd/hi and friends still
|
||||
# compile; TestPostgres* needs a server (the SQLite equivalents still
|
||||
# run). CGO off matches the build.
|
||||
gotest = fc.goTest (common // {
|
||||
testExclude = [ "/integration" "/hscontrol/servertest" ];
|
||||
goSkip = [ "TestPostgres" ];
|
||||
testEnv = "export CGO_ENABLED=0";
|
||||
});
|
||||
|
||||
# Full-tree golangci-lint (golines, gofumpt, etc.); uses the overlay's
|
||||
# golangci-lint built against the pinned Go.
|
||||
golangci-lint = fc.goLint common;
|
||||
|
||||
# nixpkgs-fmt + prettier, excluding generated output. goFmt = "off":
|
||||
# Go formatting (golines, gofumpt) is enforced by the golangci-lint
|
||||
# check, not treefmt. prettierExts matches the old prettier-lint glob
|
||||
# (no json: testdata fixtures are hand-formatted).
|
||||
formatting = fc.goFormat (common // {
|
||||
goFmt = "off";
|
||||
prettier = true;
|
||||
prettierExts = [ "ts" "js" "md" "yaml" "yml" "sass" "css" "scss" "html" ];
|
||||
# Mirror .prettierignore (docs/ are mkdocs-flavoured; gen/ generated).
|
||||
fmtExclude = [ ./gen ./docs ];
|
||||
});
|
||||
};
|
||||
in
|
||||
{
|
||||
# `nix develop`
|
||||
@@ -223,19 +250,13 @@
|
||||
"nix-vendor-sri"
|
||||
''
|
||||
set -eu
|
||||
|
||||
OUT=$(mktemp -d -t nar-hash-XXXXXX)
|
||||
rm -rf "$OUT"
|
||||
|
||||
go mod vendor -o "$OUT"
|
||||
go run tailscale.com/cmd/nardump --sri "$OUT"
|
||||
rm -rf "$OUT"
|
||||
exec go run ./cmd/vendorhash update "$@"
|
||||
'')
|
||||
|
||||
(pkgs.writeShellScriptBin
|
||||
"go-mod-update-all"
|
||||
''
|
||||
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
|
||||
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
|
||||
go mod tidy
|
||||
'')
|
||||
];
|
||||
@@ -263,6 +284,9 @@
|
||||
|
||||
checks = {
|
||||
headscale = pkgs.testers.nixosTest (import ./nix/tests/headscale.nix);
|
||||
};
|
||||
}
|
||||
# The Go build/test checks are gated to Linux: parts of the tree are
|
||||
# Linux-specific and the pure unit subset is validated by CI.
|
||||
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux goChecks;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"vendor": {
|
||||
"goModSum": "sha256-SJml8RXGmb2p0g1nOsHn86FA1hwgd5ZffLSkUj5zek8=",
|
||||
"sri": "sha256-pjGNuVtgFFzWNq/2cK7a4iyF13AfcHz098nk92a9Ido="
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,537 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,890 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
// 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\xeb\x19\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\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
|
||||
(*CreateUserResponse)(nil), // 29: headscale.v1.CreateUserResponse
|
||||
(*RenameUserResponse)(nil), // 30: headscale.v1.RenameUserResponse
|
||||
(*DeleteUserResponse)(nil), // 31: headscale.v1.DeleteUserResponse
|
||||
(*ListUsersResponse)(nil), // 32: headscale.v1.ListUsersResponse
|
||||
(*CreatePreAuthKeyResponse)(nil), // 33: headscale.v1.CreatePreAuthKeyResponse
|
||||
(*ExpirePreAuthKeyResponse)(nil), // 34: headscale.v1.ExpirePreAuthKeyResponse
|
||||
(*DeletePreAuthKeyResponse)(nil), // 35: headscale.v1.DeletePreAuthKeyResponse
|
||||
(*ListPreAuthKeysResponse)(nil), // 36: headscale.v1.ListPreAuthKeysResponse
|
||||
(*DebugCreateNodeResponse)(nil), // 37: headscale.v1.DebugCreateNodeResponse
|
||||
(*GetNodeResponse)(nil), // 38: headscale.v1.GetNodeResponse
|
||||
(*SetTagsResponse)(nil), // 39: headscale.v1.SetTagsResponse
|
||||
(*SetApprovedRoutesResponse)(nil), // 40: headscale.v1.SetApprovedRoutesResponse
|
||||
(*RegisterNodeResponse)(nil), // 41: headscale.v1.RegisterNodeResponse
|
||||
(*DeleteNodeResponse)(nil), // 42: headscale.v1.DeleteNodeResponse
|
||||
(*ExpireNodeResponse)(nil), // 43: headscale.v1.ExpireNodeResponse
|
||||
(*RenameNodeResponse)(nil), // 44: headscale.v1.RenameNodeResponse
|
||||
(*ListNodesResponse)(nil), // 45: headscale.v1.ListNodesResponse
|
||||
(*BackfillNodeIPsResponse)(nil), // 46: headscale.v1.BackfillNodeIPsResponse
|
||||
(*AuthRegisterResponse)(nil), // 47: headscale.v1.AuthRegisterResponse
|
||||
(*AuthApproveResponse)(nil), // 48: headscale.v1.AuthApproveResponse
|
||||
(*AuthRejectResponse)(nil), // 49: headscale.v1.AuthRejectResponse
|
||||
(*CreateApiKeyResponse)(nil), // 50: headscale.v1.CreateApiKeyResponse
|
||||
(*ExpireApiKeyResponse)(nil), // 51: headscale.v1.ExpireApiKeyResponse
|
||||
(*ListApiKeysResponse)(nil), // 52: headscale.v1.ListApiKeysResponse
|
||||
(*DeleteApiKeyResponse)(nil), // 53: headscale.v1.DeleteApiKeyResponse
|
||||
(*GetPolicyResponse)(nil), // 54: headscale.v1.GetPolicyResponse
|
||||
(*SetPolicyResponse)(nil), // 55: headscale.v1.SetPolicyResponse
|
||||
}
|
||||
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
|
||||
0, // 27: headscale.v1.HeadscaleService.Health:input_type -> headscale.v1.HealthRequest
|
||||
29, // 28: headscale.v1.HeadscaleService.CreateUser:output_type -> headscale.v1.CreateUserResponse
|
||||
30, // 29: headscale.v1.HeadscaleService.RenameUser:output_type -> headscale.v1.RenameUserResponse
|
||||
31, // 30: headscale.v1.HeadscaleService.DeleteUser:output_type -> headscale.v1.DeleteUserResponse
|
||||
32, // 31: headscale.v1.HeadscaleService.ListUsers:output_type -> headscale.v1.ListUsersResponse
|
||||
33, // 32: headscale.v1.HeadscaleService.CreatePreAuthKey:output_type -> headscale.v1.CreatePreAuthKeyResponse
|
||||
34, // 33: headscale.v1.HeadscaleService.ExpirePreAuthKey:output_type -> headscale.v1.ExpirePreAuthKeyResponse
|
||||
35, // 34: headscale.v1.HeadscaleService.DeletePreAuthKey:output_type -> headscale.v1.DeletePreAuthKeyResponse
|
||||
36, // 35: headscale.v1.HeadscaleService.ListPreAuthKeys:output_type -> headscale.v1.ListPreAuthKeysResponse
|
||||
37, // 36: headscale.v1.HeadscaleService.DebugCreateNode:output_type -> headscale.v1.DebugCreateNodeResponse
|
||||
38, // 37: headscale.v1.HeadscaleService.GetNode:output_type -> headscale.v1.GetNodeResponse
|
||||
39, // 38: headscale.v1.HeadscaleService.SetTags:output_type -> headscale.v1.SetTagsResponse
|
||||
40, // 39: headscale.v1.HeadscaleService.SetApprovedRoutes:output_type -> headscale.v1.SetApprovedRoutesResponse
|
||||
41, // 40: headscale.v1.HeadscaleService.RegisterNode:output_type -> headscale.v1.RegisterNodeResponse
|
||||
42, // 41: headscale.v1.HeadscaleService.DeleteNode:output_type -> headscale.v1.DeleteNodeResponse
|
||||
43, // 42: headscale.v1.HeadscaleService.ExpireNode:output_type -> headscale.v1.ExpireNodeResponse
|
||||
44, // 43: headscale.v1.HeadscaleService.RenameNode:output_type -> headscale.v1.RenameNodeResponse
|
||||
45, // 44: headscale.v1.HeadscaleService.ListNodes:output_type -> headscale.v1.ListNodesResponse
|
||||
46, // 45: headscale.v1.HeadscaleService.BackfillNodeIPs:output_type -> headscale.v1.BackfillNodeIPsResponse
|
||||
47, // 46: headscale.v1.HeadscaleService.AuthRegister:output_type -> headscale.v1.AuthRegisterResponse
|
||||
48, // 47: headscale.v1.HeadscaleService.AuthApprove:output_type -> headscale.v1.AuthApproveResponse
|
||||
49, // 48: headscale.v1.HeadscaleService.AuthReject:output_type -> headscale.v1.AuthRejectResponse
|
||||
50, // 49: headscale.v1.HeadscaleService.CreateApiKey:output_type -> headscale.v1.CreateApiKeyResponse
|
||||
51, // 50: headscale.v1.HeadscaleService.ExpireApiKey:output_type -> headscale.v1.ExpireApiKeyResponse
|
||||
52, // 51: headscale.v1.HeadscaleService.ListApiKeys:output_type -> headscale.v1.ListApiKeysResponse
|
||||
53, // 52: headscale.v1.HeadscaleService.DeleteApiKey:output_type -> headscale.v1.DeleteApiKeyResponse
|
||||
54, // 53: headscale.v1.HeadscaleService.GetPolicy:output_type -> headscale.v1.GetPolicyResponse
|
||||
55, // 54: headscale.v1.HeadscaleService.SetPolicy:output_type -> headscale.v1.SetPolicyResponse
|
||||
1, // 55: headscale.v1.HeadscaleService.Health:output_type -> headscale.v1.HealthResponse
|
||||
28, // [28:56] is the sub-list for method output_type
|
||||
0, // [0:28] 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
@@ -1,278 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
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\tupdatedAtB)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, 4)
|
||||
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
|
||||
(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
|
||||
}
|
||||
var file_headscale_v1_policy_proto_depIdxs = []int32{
|
||||
4, // 0: headscale.v1.SetPolicyResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
4, // 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: 4,
|
||||
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
|
||||
}
|
||||
@@ -1,596 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user