diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d989a84ce3..02e4742507 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,19 +6,24 @@ labels: ["bug"] assignees: "" --- - + + +## Bug description -**To Reproduce** - - - -**Context info** +## Environment + +- OS: +- Headscale version: +- Tailscale version: + + + +- [ ] Headscale is behind a (reverse) proxy +- [ ] Headscale runs in a container + +## To Reproduce + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index a9428c0036..92c51b8f73 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,12 +6,21 @@ labels: ["enhancement"] assignees: "" --- - + - +## Why - + +## Description + + diff --git a/.github/ISSUE_TEMPLATE/other_issue.md b/.github/ISSUE_TEMPLATE/other_issue.md deleted file mode 100644 index 3004a97ccd..0000000000 --- a/.github/ISSUE_TEMPLATE/other_issue.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: "Other issue" -about: "Report a different issue" -title: "" -labels: ["bug"] -assignees: "" ---- - - - - - -**Issue description** - - - -**To Reproduce** - - - -**Context info** - - diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 88289dab45..d4e4f4f9a5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,15 @@ + + - [ ] read the [CONTRIBUTING guidelines](README.md#contributing) diff --git a/.github/renovate.json b/.github/renovate.json index ce38ba9694..7b3ef57c7b 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -6,31 +6,27 @@ "onboarding": false, "extends": ["config:base", ":rebaseStalePrs"], "ignorePresets": [":prHourlyLimit2"], - "enabledManagers": ["dockerfile", "gomod", "github-actions","regex" ], + "enabledManagers": ["dockerfile", "gomod", "github-actions", "regex"], "includeForks": true, "repositories": ["juanfont/headscale"], "platform": "github", "packageRules": [ { - "matchDatasources": ["go"], - "groupName": "Go modules", - "groupSlug": "gomod", - "separateMajorMinor": false + "matchDatasources": ["go"], + "groupName": "Go modules", + "groupSlug": "gomod", + "separateMajorMinor": false }, { - "matchDatasources": ["docker"], - "groupName": "Dockerfiles", - "groupSlug": "dockerfiles" - } + "matchDatasources": ["docker"], + "groupName": "Dockerfiles", + "groupSlug": "dockerfiles" + } ], "regexManagers": [ { - "fileMatch": [ - ".github/workflows/.*.yml$" - ], - "matchStrings": [ - "\\s*go-version:\\s*\"?(?.*?)\"?\\n" - ], + "fileMatch": [".github/workflows/.*.yml$"], + "matchStrings": ["\\s*go-version:\\s*\"?(?.*?)\"?\\n"], "datasourceTemplate": "golang-version", "depNameTemplate": "actions/go-version" } diff --git a/.github/workflows/test-integration-cli.yml b/.github/workflows/test-integration-cli.yml deleted file mode 100644 index 72cf31aac2..0000000000 --- a/.github/workflows/test-integration-cli.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Integration Test CLI - -on: [pull_request] - -jobs: - integration-test-cli: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 10 - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@v34 - with: - files: | - *.nix - go.* - **/*.go - integration_test/ - config-example.yaml - - - uses: cachix/install-nix-action@v16 - if: steps.changed-files.outputs.any_changed == 'true' - - - name: Run CLI integration tests - if: steps.changed-files.outputs.any_changed == 'true' - run: nix develop --command -- make test_integration_cli diff --git a/.github/workflows/test-integration-v2-TestACLAllowStarDst.yaml b/.github/workflows/test-integration-v2-TestACLAllowStarDst.yaml index 47a1b60d55..1a0487b0d4 100644 --- a/.github/workflows/test-integration-v2-TestACLAllowStarDst.yaml +++ b/.github/workflows/test-integration-v2-TestACLAllowStarDst.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestACLAllowUser80Dst.yaml b/.github/workflows/test-integration-v2-TestACLAllowUser80Dst.yaml index 2e6c3c2efd..06e48b83f7 100644 --- a/.github/workflows/test-integration-v2-TestACLAllowUser80Dst.yaml +++ b/.github/workflows/test-integration-v2-TestACLAllowUser80Dst.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestACLAllowUserDst.yaml b/.github/workflows/test-integration-v2-TestACLAllowUserDst.yaml index d745e87175..e78049b218 100644 --- a/.github/workflows/test-integration-v2-TestACLAllowUserDst.yaml +++ b/.github/workflows/test-integration-v2-TestACLAllowUserDst.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestACLDenyAllPort80.yaml b/.github/workflows/test-integration-v2-TestACLDenyAllPort80.yaml index 767aa213e2..79df71d519 100644 --- a/.github/workflows/test-integration-v2-TestACLDenyAllPort80.yaml +++ b/.github/workflows/test-integration-v2-TestACLDenyAllPort80.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml b/.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml index d25f774f05..8a36a2d253 100644 --- a/.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml +++ b/.github/workflows/test-integration-v2-TestACLDevice1CanAccessDevice2.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestACLHostsInNetMapTable.yaml b/.github/workflows/test-integration-v2-TestACLHostsInNetMapTable.yaml index 70b7b94384..c20cdf688b 100644 --- a/.github/workflows/test-integration-v2-TestACLHostsInNetMapTable.yaml +++ b/.github/workflows/test-integration-v2-TestACLHostsInNetMapTable.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml b/.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml index 2d62422832..d1e4c9d164 100644 --- a/.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml +++ b/.github/workflows/test-integration-v2-TestACLNamedHostsCanReach.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml b/.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml index 039512392f..344ae51866 100644 --- a/.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml +++ b/.github/workflows/test-integration-v2-TestACLNamedHostsCanReachBySubnet.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestAuthKeyLogoutAndRelogin.yaml b/.github/workflows/test-integration-v2-TestAuthKeyLogoutAndRelogin.yaml index 8df6f22a72..675f7dd027 100644 --- a/.github/workflows/test-integration-v2-TestAuthKeyLogoutAndRelogin.yaml +++ b/.github/workflows/test-integration-v2-TestAuthKeyLogoutAndRelogin.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestAuthWebFlowAuthenticationPingAll.yaml b/.github/workflows/test-integration-v2-TestAuthWebFlowAuthenticationPingAll.yaml index acf473d795..984eb6500c 100644 --- a/.github/workflows/test-integration-v2-TestAuthWebFlowAuthenticationPingAll.yaml +++ b/.github/workflows/test-integration-v2-TestAuthWebFlowAuthenticationPingAll.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestAuthWebFlowLogoutAndRelogin.yaml b/.github/workflows/test-integration-v2-TestAuthWebFlowLogoutAndRelogin.yaml index 6d70750c5c..47177d49d1 100644 --- a/.github/workflows/test-integration-v2-TestAuthWebFlowLogoutAndRelogin.yaml +++ b/.github/workflows/test-integration-v2-TestAuthWebFlowLogoutAndRelogin.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestCreateTailscale.yaml b/.github/workflows/test-integration-v2-TestCreateTailscale.yaml index 0ab32477f1..47a5157d9c 100644 --- a/.github/workflows/test-integration-v2-TestCreateTailscale.yaml +++ b/.github/workflows/test-integration-v2-TestCreateTailscale.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestDERPServerScenario.yaml b/.github/workflows/test-integration-v2-TestDERPServerScenario.yaml index 610794e8e1..0dfaeb75d7 100644 --- a/.github/workflows/test-integration-v2-TestDERPServerScenario.yaml +++ b/.github/workflows/test-integration-v2-TestDERPServerScenario.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestEnablingRoutes.yaml b/.github/workflows/test-integration-v2-TestEnablingRoutes.yaml index 6063741563..aeb8642828 100644 --- a/.github/workflows/test-integration-v2-TestEnablingRoutes.yaml +++ b/.github/workflows/test-integration-v2-TestEnablingRoutes.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestEphemeral.yaml b/.github/workflows/test-integration-v2-TestEphemeral.yaml index 068c028d7a..e81e0937e9 100644 --- a/.github/workflows/test-integration-v2-TestEphemeral.yaml +++ b/.github/workflows/test-integration-v2-TestEphemeral.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestExpireNode.yaml b/.github/workflows/test-integration-v2-TestExpireNode.yaml index bfa4faa730..80af2608f1 100644 --- a/.github/workflows/test-integration-v2-TestExpireNode.yaml +++ b/.github/workflows/test-integration-v2-TestExpireNode.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestHeadscale.yaml b/.github/workflows/test-integration-v2-TestHeadscale.yaml index b7849ae4ad..52b562dfec 100644 --- a/.github/workflows/test-integration-v2-TestHeadscale.yaml +++ b/.github/workflows/test-integration-v2-TestHeadscale.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestOIDCAuthenticationPingAll.yaml b/.github/workflows/test-integration-v2-TestOIDCAuthenticationPingAll.yaml index 86dcd8f6a0..e9a4e1cac1 100644 --- a/.github/workflows/test-integration-v2-TestOIDCAuthenticationPingAll.yaml +++ b/.github/workflows/test-integration-v2-TestOIDCAuthenticationPingAll.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml b/.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml index 7380c4e875..f90688888c 100644 --- a/.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml +++ b/.github/workflows/test-integration-v2-TestOIDCExpireNodesBasedOnTokenExpiry.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestPingAllByHostname.yaml b/.github/workflows/test-integration-v2-TestPingAllByHostname.yaml index 691b306c4f..60e9fbafe6 100644 --- a/.github/workflows/test-integration-v2-TestPingAllByHostname.yaml +++ b/.github/workflows/test-integration-v2-TestPingAllByHostname.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestPingAllByIP.yaml b/.github/workflows/test-integration-v2-TestPingAllByIP.yaml index 96172998f9..05413e86bc 100644 --- a/.github/workflows/test-integration-v2-TestPingAllByIP.yaml +++ b/.github/workflows/test-integration-v2-TestPingAllByIP.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestPreAuthKeyCommand.yaml b/.github/workflows/test-integration-v2-TestPreAuthKeyCommand.yaml index 32843253c3..f828b51db8 100644 --- a/.github/workflows/test-integration-v2-TestPreAuthKeyCommand.yaml +++ b/.github/workflows/test-integration-v2-TestPreAuthKeyCommand.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestPreAuthKeyCommandReusableEphemeral.yaml b/.github/workflows/test-integration-v2-TestPreAuthKeyCommandReusableEphemeral.yaml index bbcb3f67d4..2114bd898e 100644 --- a/.github/workflows/test-integration-v2-TestPreAuthKeyCommandReusableEphemeral.yaml +++ b/.github/workflows/test-integration-v2-TestPreAuthKeyCommandReusableEphemeral.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestPreAuthKeyCommandWithoutExpiry.yaml b/.github/workflows/test-integration-v2-TestPreAuthKeyCommandWithoutExpiry.yaml index a3dac5c514..53cb3957d8 100644 --- a/.github/workflows/test-integration-v2-TestPreAuthKeyCommandWithoutExpiry.yaml +++ b/.github/workflows/test-integration-v2-TestPreAuthKeyCommandWithoutExpiry.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestResolveMagicDNS.yaml b/.github/workflows/test-integration-v2-TestResolveMagicDNS.yaml index 98c0563d90..ac2b0b9238 100644 --- a/.github/workflows/test-integration-v2-TestResolveMagicDNS.yaml +++ b/.github/workflows/test-integration-v2-TestResolveMagicDNS.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestSSHIsBlockedInACL.yaml b/.github/workflows/test-integration-v2-TestSSHIsBlockedInACL.yaml index 509e45ea03..5ee979b410 100644 --- a/.github/workflows/test-integration-v2-TestSSHIsBlockedInACL.yaml +++ b/.github/workflows/test-integration-v2-TestSSHIsBlockedInACL.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml b/.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml index 71d5533b54..bcc88863fe 100644 --- a/.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml +++ b/.github/workflows/test-integration-v2-TestSSHMultipleUsersAllToAll.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestSSHNoSSHConfigured.yaml b/.github/workflows/test-integration-v2-TestSSHNoSSHConfigured.yaml index 0707fdadee..bf641a0e6d 100644 --- a/.github/workflows/test-integration-v2-TestSSHNoSSHConfigured.yaml +++ b/.github/workflows/test-integration-v2-TestSSHNoSSHConfigured.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestSSHOneUserAllToAll.yaml b/.github/workflows/test-integration-v2-TestSSHOneUserAllToAll.yaml index 6ab4f083ae..d61378dee0 100644 --- a/.github/workflows/test-integration-v2-TestSSHOneUserAllToAll.yaml +++ b/.github/workflows/test-integration-v2-TestSSHOneUserAllToAll.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestSSUserOnlyIsolation.yaml b/.github/workflows/test-integration-v2-TestSSUserOnlyIsolation.yaml index b3ddf2511a..a40a3af090 100644 --- a/.github/workflows/test-integration-v2-TestSSUserOnlyIsolation.yaml +++ b/.github/workflows/test-integration-v2-TestSSUserOnlyIsolation.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestTaildrop.yaml b/.github/workflows/test-integration-v2-TestTaildrop.yaml index b10723daca..1d6c2430e1 100644 --- a/.github/workflows/test-integration-v2-TestTaildrop.yaml +++ b/.github/workflows/test-integration-v2-TestTaildrop.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestTailscaleNodesJoiningHeadcale.yaml b/.github/workflows/test-integration-v2-TestTailscaleNodesJoiningHeadcale.yaml index fc685836c6..941c2311bf 100644 --- a/.github/workflows/test-integration-v2-TestTailscaleNodesJoiningHeadcale.yaml +++ b/.github/workflows/test-integration-v2-TestTailscaleNodesJoiningHeadcale.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.github/workflows/test-integration-v2-TestUserCommand.yaml b/.github/workflows/test-integration-v2-TestUserCommand.yaml index 7a4c911927..449512bfa7 100644 --- a/.github/workflows/test-integration-v2-TestUserCommand.yaml +++ b/.github/workflows/test-integration-v2-TestUserCommand.yaml @@ -43,7 +43,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -55,3 +55,9 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" diff --git a/.goreleaser.yml b/.goreleaser.yml index 5e64ac83cf..07efe6f7bf 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -71,7 +71,7 @@ nfpms: file_info: mode: 0644 - src: ./docs/packaging/headscale.systemd.service - dst: /etc/systemd/system/headscale.service + dst: /usr/lib/systemd/system/headscale.service - dst: /var/lib/headscale type: dir - dst: /var/run/headscale diff --git a/CHANGELOG.md b/CHANGELOG.md index 0345ec9081..1c2976e9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,35 @@ ## 0.23.0 (2023-XX-XX) +### BREAKING + +- Code reorganisation, a lot of code has moved, please review the following PRs accordingly [#1444](https://github.com/juanfont/headscale/pull/1444) + +### Changes + +## 0.22.3 (2023-05-12) + ### Changes +- Added missing ca-certificates in Docker image [#1463](https://github.com/juanfont/headscale/pull/1463) + +## 0.22.2 (2023-05-10) + +### Changes + +- Add environment flags to enable pprof (profiling) [#1382](https://github.com/juanfont/headscale/pull/1382) + - Profiles are continously generated in our integration tests. +- Fix systemd service file location in `.deb` packages [#1391](https://github.com/juanfont/headscale/pull/1391) +- Improvements on Noise implementation [#1379](https://github.com/juanfont/headscale/pull/1379) +- Replace node filter logic, ensuring nodes with access can see eachother [#1381](https://github.com/juanfont/headscale/pull/1381) +- Disable (or delete) both exit routes at the same time [#1428](https://github.com/juanfont/headscale/pull/1428) +- Ditch distroless for Docker image, create default socket dir in `/var/run/headscale` [#1450](https://github.com/juanfont/headscale/pull/1450) + ## 0.22.1 (2023-04-20) ### Changes -- Fix issue where SystemD could not bind to port 80 [#1365](https://github.com/juanfont/headscale/pull/1365) +- Fix issue where systemd could not bind to port 80 [#1365](https://github.com/juanfont/headscale/pull/1365) ## 0.22.0 (2023-04-20) diff --git a/Dockerfile b/Dockerfile index a85c1220cf..b1ec3317e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,10 +14,17 @@ RUN strip /go/bin/headscale RUN test -e /go/bin/headscale # Production image -FROM gcr.io/distroless/base-debian11 +FROM docker.io/debian:bullseye-slim + +RUN apt-get update \ + && apt-get install -y ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean COPY --from=build /go/bin/headscale /bin/headscale ENV TZ UTC +RUN mkdir -p /var/run/headscale + EXPOSE 8080/tcp CMD ["headscale"] diff --git a/Dockerfile.debug b/Dockerfile.debug index 3a751ecd17..7cd609cfe2 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -18,6 +18,8 @@ FROM docker.io/golang:1.20.0-bullseye COPY --from=build /go/bin/headscale /bin/headscale ENV TZ UTC +RUN mkdir -p /var/run/headscale + # Need to reset the entrypoint or everything will run as a busybox script ENTRYPOINT [] EXPOSE 8080/tcp diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale index fc02aeaa02..adf37ed039 100644 --- a/Dockerfile.tailscale +++ b/Dockerfile.tailscale @@ -1,19 +1,16 @@ -FROM ubuntu:latest +FROM ubuntu:22.04 ARG TAILSCALE_VERSION=* ARG TAILSCALE_CHANNEL=stable RUN apt-get update \ - && apt-get install -y gnupg curl ssh \ - && curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \ + && apt-get install -y gnupg curl ssh dnsutils ca-certificates \ + && adduser --shell=/bin/bash ssh-it-user + +# Tailscale is deliberately split into a second stage so we can cash utils as a seperate layer. +RUN curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \ && curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ && apt-get update \ - && apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \ + && apt-get install -y tailscale=${TAILSCALE_VERSION} \ + && apt-get clean \ && rm -rf /var/lib/apt/lists/* - -RUN adduser --shell=/bin/bash ssh-it-user - -ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/ -RUN chmod 644 /usr/local/share/ca-certificates/server.crt - -RUN update-ca-certificates diff --git a/Dockerfile.tailscale-HEAD b/Dockerfile.tailscale-HEAD index c9a041893f..2a3aac769c 100644 --- a/Dockerfile.tailscale-HEAD +++ b/Dockerfile.tailscale-HEAD @@ -1,7 +1,7 @@ FROM golang:latest RUN apt-get update \ - && apt-get install -y ca-certificates dnsutils git iptables ssh \ + && apt-get install -y dnsutils git iptables ssh ca-certificates \ && rm -rf /var/lib/apt/lists/* RUN useradd --shell=/bin/bash --create-home ssh-it-user @@ -10,15 +10,8 @@ RUN git clone https://github.com/tailscale/tailscale.git WORKDIR /go/tailscale -RUN git checkout main - -RUN sh build_dist.sh tailscale.com/cmd/tailscale -RUN sh build_dist.sh tailscale.com/cmd/tailscaled - -RUN cp tailscale /usr/local/bin/ -RUN cp tailscaled /usr/local/bin/ - -ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/ -RUN chmod 644 /usr/local/share/ca-certificates/server.crt - -RUN update-ca-certificates +RUN git checkout main \ + && sh build_dist.sh tailscale.com/cmd/tailscale \ + && sh build_dist.sh tailscale.com/cmd/tailscaled \ + && cp tailscale /usr/local/bin/ \ + && cp tailscaled /usr/local/bin/ diff --git a/Makefile b/Makefile index a4b0f7da17..4fdf418e06 100644 --- a/Makefile +++ b/Makefile @@ -24,21 +24,9 @@ build: dev: lint test build test: - @go test $(TAGS) -short -coverprofile=coverage.out ./... + gotestsum -- $(TAGS) -short -coverprofile=coverage.out ./... -test_integration: test_integration_cli test_integration_derp test_integration_v2_general - -test_integration_cli: - docker network rm $$(docker network ls --filter name=headscale --quiet) || true - docker network create headscale-test || true - docker run -t --rm \ - --network headscale-test \ - -v ~/.cache/hs-integration-go:/go \ - -v $$PWD:$$PWD -w $$PWD \ - -v /var/run/docker.sock:/var/run/docker.sock golang:1 \ - go run gotest.tools/gotestsum@latest -- $(TAGS) -failfast -timeout 30m -count=1 -run IntegrationCLI ./... - -test_integration_v2_general: +test_integration: docker run \ -t --rm \ -v ~/.cache/hs-integration-go:/go \ diff --git a/README.md b/README.md index 408f6997e6..200c8cde84 100644 --- a/README.md +++ b/README.md @@ -32,22 +32,18 @@ organisation. ## Design goal -`headscale` aims to implement a self-hosted, open source alternative to the Tailscale -control server. `headscale` has a narrower scope and an instance of `headscale` -implements a _single_ Tailnet, which is typically what a single organisation, or -home/personal setup would use. +Headscale aims to implement a self-hosted, open source alternative to the Tailscale +control server. +Headscale's goal is to provide self-hosters and hobbyists with an open-source +server they can use for their projects and labs. +It implements a narrow scope, a single Tailnet, suitable for a personal use, or a small +open-source organisation. -`headscale` uses terms that maps to Tailscale's control server, consult the -[glossary](./docs/glossary.md) for explainations. - -## Support +## Supporting Headscale If you like `headscale` and find it useful, there is a sponsorship and donation buttons available in the repo. -If you would like to sponsor features, bugs or prioritisation, reach out to -one of the maintainers. - ## Features - Full "base" support of Tailscale's features @@ -79,16 +75,10 @@ one of the maintainers. ## Running headscale -Please have a look at the documentation under [`docs/`](docs/). - -## Graphical Control Panels +**Please note that we do not support nor encourage the use of reverse proxies +and container to run Headscale.** -Headscale provides an API for complete management of your Tailnet. -These are community projects not directly affiliated with the Headscale project. - -| Name | Repository Link | Description | Status | -| --------------- | ---------------------------------------------------- | ------------------------------------------------------ | ------ | -| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha | +Please have a look at the [`documentation`](https://headscale.net/). ## Talks @@ -97,11 +87,23 @@ These are community projects not directly affiliated with the Headscale project. ## Disclaimer -1. We have nothing to do with Tailscale, or Tailscale Inc. +1. This project is not associated with Tailscale Inc. 2. The purpose of Headscale is maintaining a working, self-hosted Tailscale control panel. ## Contributing +Headscale is "Open Source, acknowledged contribution", this means that any +contribution will have to be discussed with the Maintainers before being submitted. + +This model has been chosen to reduce the risk of burnout by limiting the +maintenance overhead of reviewing and validating third-party code. + +Headscale is open to code contributions for bug fixes without discussion. + +If you find mistakes in the documentation, please submit a fix to the documentation. + +### Requirements + To contribute to headscale you would need the lastest version of [Go](https://golang.org) and [Buf](https://buf.build)(Protobuf generator). @@ -109,8 +111,6 @@ We recommend using [Nix](https://nixos.org/) to setup a development environment. be done with `nix develop`, which will install the tools and give you a shell. This guarantees that you will have the same dev env as `headscale` maintainers. -PRs and suggestions are welcome. - ### Code style To ensure we have some consistency with a growing number of contributions, diff --git a/cmd/build-docker-img/main.go b/cmd/build-docker-img/main.go new file mode 100644 index 0000000000..a33ce19f93 --- /dev/null +++ b/cmd/build-docker-img/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "log" + + "github.com/juanfont/headscale/integration" + "github.com/juanfont/headscale/integration/tsic" + "github.com/ory/dockertest/v3" +) + +func main() { + log.Printf("creating docker pool") + pool, err := dockertest.NewPool("") + if err != nil { + log.Fatalf("could not connect to docker: %s", err) + } + + log.Printf("creating docker network") + network, err := pool.CreateNetwork("docker-integration-net") + if err != nil { + log.Fatalf("failed to create or get network: %s", err) + } + + for _, version := range integration.TailscaleVersions { + log.Printf("creating container image for Tailscale (%s)", version) + + tsClient, err := tsic.New( + pool, + version, + network, + ) + if err != nil { + log.Fatalf("failed to create tailscale node: %s", err) + } + + err = tsClient.Shutdown() + if err != nil { + log.Fatalf("failed to shut down container: %s", err) + } + } + + network.Close() + err = pool.RemoveNetwork(network) + if err != nil { + log.Fatalf("failed to remove network: %s", err) + } +} diff --git a/cmd/gh-action-integration-generator/main.go b/cmd/gh-action-integration-generator/main.go index 1ea69dae62..0b363b0cdf 100644 --- a/cmd/gh-action-integration-generator/main.go +++ b/cmd/gh-action-integration-generator/main.go @@ -64,7 +64,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume $PWD/control_logs:/tmp/control \ golang:1 \ - go test ./... \ + go run gotest.tools/gotestsum@latest -- ./... \ -tags ts2019 \ -failfast \ -timeout 120m \ @@ -76,6 +76,12 @@ jobs: with: name: logs path: "control_logs/*.log" + + - uses: actions/upload-artifact@v3 + if: always() && steps.changed-files.outputs.any_changed == 'true' + with: + name: pprof + path: "control_logs/*.pprof.tar" `), ) ) diff --git a/cmd/headscale/cli/api_key.go b/cmd/headscale/cli/api_key.go index 5756db48b1..f7c7e3a264 100644 --- a/cmd/headscale/cli/api_key.go +++ b/cmd/headscale/cli/api_key.go @@ -5,8 +5,8 @@ import ( "strconv" "time" - "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" "github.com/prometheus/common/model" "github.com/pterm/pterm" "github.com/rs/zerolog/log" @@ -83,7 +83,7 @@ var listAPIKeys = &cobra.Command{ } tableData = append(tableData, []string{ - strconv.FormatUint(key.GetId(), headscale.Base10), + strconv.FormatUint(key.GetId(), hscontrol.Base10), key.GetPrefix(), expiration, key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat), diff --git a/cmd/headscale/cli/debug.go b/cmd/headscale/cli/debug.go index 383ed13c1b..f2c8028f25 100644 --- a/cmd/headscale/cli/debug.go +++ b/cmd/headscale/cli/debug.go @@ -3,8 +3,8 @@ package cli import ( "fmt" - "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "google.golang.org/grpc/status" @@ -93,7 +93,7 @@ var createNodeCmd = &cobra.Command{ return } - if !headscale.NodePublicKeyRegex.Match([]byte(machineKey)) { + if !hscontrol.NodePublicKeyRegex.Match([]byte(machineKey)) { err = errPreAuthKeyMalformed ErrorOutput( err, diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 5d8babd8cf..772b428e82 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -9,8 +9,8 @@ import ( "time" survey "github.com/AlecAivazis/survey/v2" - "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" "github.com/pterm/pterm" "github.com/spf13/cobra" "google.golang.org/grpc/status" @@ -529,7 +529,7 @@ func nodesToPtables( var machineKey key.MachinePublic err := machineKey.UnmarshalText( - []byte(headscale.MachinePublicKeyEnsurePrefix(machine.MachineKey)), + []byte(hscontrol.MachinePublicKeyEnsurePrefix(machine.MachineKey)), ) if err != nil { machineKey = key.MachinePublic{} @@ -537,7 +537,7 @@ func nodesToPtables( var nodeKey key.NodePublic err = nodeKey.UnmarshalText( - []byte(headscale.NodePublicKeyEnsurePrefix(machine.NodeKey)), + []byte(hscontrol.NodePublicKeyEnsurePrefix(machine.NodeKey)), ) if err != nil { return nil, err @@ -596,7 +596,7 @@ func nodesToPtables( } nodeData := []string{ - strconv.FormatUint(machine.Id, headscale.Base10), + strconv.FormatUint(machine.Id, hscontrol.Base10), machine.Name, machine.GetGivenName(), machineKey.ShortString(), diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index cf173f5662..ab76fff599 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -5,7 +5,7 @@ import ( "os" "runtime" - "github.com/juanfont/headscale" + "github.com/juanfont/headscale/hscontrol" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -38,18 +38,18 @@ func initConfig() { cfgFile = os.Getenv("HEADSCALE_CONFIG") } if cfgFile != "" { - err := headscale.LoadConfig(cfgFile, true) + err := hscontrol.LoadConfig(cfgFile, true) if err != nil { log.Fatal().Caller().Err(err).Msgf("Error loading config file %s", cfgFile) } } else { - err := headscale.LoadConfig("", false) + err := hscontrol.LoadConfig("", false) if err != nil { log.Fatal().Caller().Err(err).Msgf("Error loading config") } } - cfg, err := headscale.GetHeadscaleConfig() + cfg, err := hscontrol.GetHeadscaleConfig() if err != nil { log.Fatal().Caller().Err(err) } @@ -64,7 +64,7 @@ func initConfig() { zerolog.SetGlobalLevel(zerolog.Disabled) } - if cfg.Log.Format == headscale.JSONLogFormat { + if cfg.Log.Format == hscontrol.JSONLogFormat { log.Logger = log.Output(os.Stdout) } diff --git a/cmd/headscale/cli/routes.go b/cmd/headscale/cli/routes.go index 55f009e9c5..206209d976 100644 --- a/cmd/headscale/cli/routes.go +++ b/cmd/headscale/cli/routes.go @@ -6,8 +6,8 @@ import ( "net/netip" "strconv" - "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" "github.com/pterm/pterm" "github.com/spf13/cobra" "google.golang.org/grpc/status" @@ -277,7 +277,7 @@ func routesToPtables(routes []*v1.Route) pterm.TableData { continue } - if prefix == headscale.ExitRouteV4 || prefix == headscale.ExitRouteV6 { + if prefix == hscontrol.ExitRouteV4 || prefix == hscontrol.ExitRouteV6 { isPrimaryStr = "-" } else { isPrimaryStr = strconv.FormatBool(route.IsPrimary) diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index 653ab537a8..3724fe98e4 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -4,8 +4,8 @@ import ( "fmt" survey "github.com/AlecAivazis/survey/v2" - "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" "github.com/pterm/pterm" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -21,7 +21,7 @@ func init() { } const ( - errMissingParameter = headscale.Error("missing parameters") + errMissingParameter = hscontrol.Error("missing parameters") ) var userCmd = &cobra.Command{ diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 3b3ac2153f..a2a5d59251 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -8,8 +8,8 @@ import ( "os" "reflect" - "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -22,8 +22,8 @@ const ( SocketWritePermissions = 0o666 ) -func getHeadscaleApp() (*headscale.Headscale, error) { - cfg, err := headscale.GetHeadscaleConfig() +func getHeadscaleApp() (*hscontrol.Headscale, error) { + cfg, err := hscontrol.GetHeadscaleConfig() if err != nil { return nil, fmt.Errorf( "failed to load configuration while creating headscale instance: %w", @@ -31,7 +31,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { ) } - app, err := headscale.NewHeadscale(cfg) + app, err := hscontrol.NewHeadscale(cfg) if err != nil { return nil, err } @@ -39,8 +39,8 @@ func getHeadscaleApp() (*headscale.Headscale, error) { // We are doing this here, as in the future could be cool to have it also hot-reload if cfg.ACL.PolicyPath != "" { - aclPath := headscale.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath) - err = app.LoadACLPolicy(aclPath) + aclPath := hscontrol.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath) + err = app.LoadACLPolicyFromPath(aclPath) if err != nil { log.Fatal(). Str("path", aclPath). @@ -53,7 +53,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) { } func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) { - cfg, err := headscale.GetHeadscaleConfig() + cfg, err := hscontrol.GetHeadscaleConfig() if err != nil { log.Fatal(). Err(err). @@ -74,7 +74,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc. address := cfg.CLI.Address - // If the address is not set, we assume that we are on the server hosting headscale. + // If the address is not set, we assume that we are on the server hosting hscontrol. if address == "" { log.Debug(). Str("socket", cfg.UnixSocket). @@ -98,7 +98,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc. grpcOptions = append( grpcOptions, grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(headscale.GrpcSocketDialer), + grpc.WithContextDialer(hscontrol.GrpcSocketDialer), ) } else { // If we are not connecting to a local server, require an API key for authentication diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index 40772a9312..26055f05e0 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -6,11 +6,25 @@ import ( "github.com/efekarakus/termcolor" "github.com/juanfont/headscale/cmd/headscale/cli" + "github.com/pkg/profile" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) func main() { + if _, enableProfile := os.LookupEnv("HEADSCALE_PROFILING_ENABLED"); enableProfile { + if profilePath, ok := os.LookupEnv("HEADSCALE_PROFILING_PATH"); ok { + err := os.MkdirAll(profilePath, os.ModePerm) + if err != nil { + log.Fatal().Err(err).Msg("failed to create profiling directory") + } + + defer profile.Start(profile.ProfilePath(profilePath)).Stop() + } else { + defer profile.Start().Stop() + } + } + var colors bool switch l := termcolor.SupportLevel(os.Stderr); l { case termcolor.Level16M: diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index c7b332aac0..1b987313bb 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/juanfont/headscale" + "github.com/juanfont/headscale/hscontrol" "github.com/spf13/viper" "gopkg.in/check.v1" ) @@ -50,7 +50,7 @@ func (*Suite) TestConfigFileLoading(c *check.C) { } // Load example config, it should load without validation errors - err = headscale.LoadConfig(cfgFile, true) + err = hscontrol.LoadConfig(cfgFile, true) c.Assert(err, check.IsNil) // Test that config file was interpreted correctly @@ -64,7 +64,7 @@ func (*Suite) TestConfigFileLoading(c *check.C) { c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert( - headscale.GetFileMode("unix_socket_permission"), + hscontrol.GetFileMode("unix_socket_permission"), check.Equals, fs.FileMode(0o770), ) @@ -93,7 +93,7 @@ func (*Suite) TestConfigLoading(c *check.C) { } // Load example config, it should load without validation errors - err = headscale.LoadConfig(tmpDir, false) + err = hscontrol.LoadConfig(tmpDir, false) c.Assert(err, check.IsNil) // Test that config file was interpreted correctly @@ -107,7 +107,7 @@ func (*Suite) TestConfigLoading(c *check.C) { c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert( - headscale.GetFileMode("unix_socket_permission"), + hscontrol.GetFileMode("unix_socket_permission"), check.Equals, fs.FileMode(0o770), ) @@ -137,10 +137,10 @@ func (*Suite) TestDNSConfigLoading(c *check.C) { } // Load example config, it should load without validation errors - err = headscale.LoadConfig(tmpDir, false) + err = hscontrol.LoadConfig(tmpDir, false) c.Assert(err, check.IsNil) - dnsConfig, baseDomain := headscale.GetDNSConfig() + dnsConfig, baseDomain := hscontrol.GetDNSConfig() c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1") c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1") @@ -172,7 +172,7 @@ noise: writeConfig(c, tmpDir, configYaml) // Check configuration validation errors (1) - err = headscale.LoadConfig(tmpDir, false) + err = hscontrol.LoadConfig(tmpDir, false) c.Assert(err, check.NotNil) // check.Matches can not handle multiline strings tmp := strings.ReplaceAll(err.Error(), "\n", "***") @@ -201,6 +201,6 @@ tls_letsencrypt_hostname: example.com tls_letsencrypt_challenge_type: TLS-ALPN-01 `) writeConfig(c, tmpDir, configYaml) - err = headscale.LoadConfig(tmpDir, false) + err = hscontrol.LoadConfig(tmpDir, false) c.Assert(err, check.IsNil) } diff --git a/config-example.yaml b/config-example.yaml index f2f997c6d4..baf108d0ef 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -58,11 +58,12 @@ noise: # List of IP prefixes to allocate tailaddresses from. # Each prefix consists of either an IPv4 or IPv6 address, # and the associated prefix length, delimited by a slash. -# While this looks like it can take arbitrary values, it -# needs to be within IP ranges supported by the Tailscale -# client. +# It must be within IP ranges supported by the Tailscale +# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. +# See below: # IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71 # IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33 +# Any other range is NOT supported, and it will cause unexpected issues. ip_prefixes: - fd7a:115c:a1e0::/48 - 100.64.0.0/10 diff --git a/docs/exit-node.md b/docs/exit-node.md index 47eaed1d1c..898b7811d5 100644 --- a/docs/exit-node.md +++ b/docs/exit-node.md @@ -14,6 +14,8 @@ If the node is already registered, it can advertise exit capabilities like this: $ sudo tailscale set --advertise-exit-node ``` +To use a node as an exit node, IP forwarding must be enabled on the node. Check the official [Tailscale documentation](https://tailscale.com/kb/1019/subnets/?tab=linux#enable-ip-forwarding) for how to enable IP fowarding. + ## On the control server ```console diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..6331c54a1c --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,53 @@ +--- +hide: + - navigation +--- + +# Frequently Asked Questions + +## What is the design goal of headscale? + +`headscale` aims to implement a self-hosted, open source alternative to the [Tailscale](https://tailscale.com/) +control server. +`headscale`'s goal is to provide self-hosters and hobbyists with an open-source +server they can use for their projects and labs. +It implements a narrow scope, a _single_ Tailnet, suitable for a personal use, or a small +open-source organisation. + +## How can I contribute? + +Headscale is "Open Source, acknowledged contribution", this means that any +contribution will have to be discussed with the Maintainers before being submitted. + +Headscale is open to code contributions for bug fixes without discussion. + +If you find mistakes in the documentation, please also submit a fix to the documentation. + +## Why is 'acknowledged contribution' the chosen model? + +Both maintainers have full-time jobs and families, and we want to avoid burnout. We also want to avoid frustration from contributors when their PRs are not accepted. + +We are more than happy to exchange emails, or to have dedicated calls before a PR is submitted. + +## When/Why is Feature X going to be implemented? + +We don't know. We might be working on it. If you want to help, please send us a PR. + +Please be aware that there are a number of reasons why we might not accept specific contributions: + +- It is not possible to implement the feature in a way that makes sense in a self-hosted environment. +- Given that we are reverse-engineering Tailscale to satify our own curiosity, we might be interested in implementing the feature ourselves. +- You are not sending unit and integration tests with it. + +## Do you support Y method of deploying Headscale? + +We currently support deploying `headscale` using our binaries and the DEB packages. Both can be found in the +[GitHub releases page](https://github.com/juanfont/headscale/releases). + +In addition to that, there are semi-official RPM packages by the Fedora infra team https://copr.fedorainfracloud.org/coprs/jonathanspw/headscale/ + +For convenience, we also build Docker images with `headscale`. But **please be aware that we don't officially support deploying `headscale` using Docker**. We have a [Discord channel](https://discord.com/channels/896711691637780480/1070619770942148618) where you can ask for Docker-specific help to the community. + +## Why is my reverse proxy not working with Headscale? + +We don't know. We don't use reverse proxies with `headscale` ourselves, so we don't have any experience with them. We have [community documentation](https://headscale.net/reverse-proxy/) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community. diff --git a/docs/index.md b/docs/index.md index 2130f7a5b3..d13339d81e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,9 +4,40 @@ hide: - toc --- -# headscale documentation +# headscale -This site contains the official and community contributed documentation for `headscale`. +`headscale` is an open source, self-hosted implementation of the Tailscale control server. -If you are having trouble with following the documentation or get unexpected results, -please ask on [Discord](https://discord.gg/c84AZQhmpx) instead of opening an Issue. +This page contains the documentation for the latest version of headscale. Please also check our [FAQ](/faq/). + +Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support. + +## Design goal + +Headscale aims to implement a self-hosted, open source alternative to the Tailscale +control server. +Headscale's goal is to provide self-hosters and hobbyists with an open-source +server they can use for their projects and labs. +It implements a narrower scope, a single Tailnet, suitable for a personal use, or a small +open-source organisation. + +## Supporting headscale + +If you like `headscale` and find it useful, there is a sponsorship and donation +buttons available in the repo. + +## Contributing + +Headscale is "Open Source, acknowledged contribution", this means that any +contribution will have to be discussed with the Maintainers before being submitted. + +This model has been chosen to reduce the risk of burnout by limiting the +maintenance overhead of reviewing and validating third-party code. + +Headscale is open to code contributions for bug fixes without discussion. + +If you find mistakes in the documentation, please submit a fix to the documentation. + +## About + +`headscale` is maintained by [Kristoffer Dalby](https://kradalby.no/) and [Juan Font](https://font.eu). diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index 511adbb192..66ccc3d35e 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -20,7 +20,7 @@ configuration (`/etc/headscale/config.yaml`). ## Installation -1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page](): +1. Download the lastest Headscale package for your platform (`.deb` for Ubuntu and Debian) from [Headscale's releases page](https://github.com/juanfont/headscale/releases): ```shell wget --output-document=headscale.deb \ diff --git a/docs/web-ui.md b/docs/web-ui.md new file mode 100644 index 0000000000..d018666e6d --- /dev/null +++ b/docs/web-ui.md @@ -0,0 +1,14 @@ +# Headscale web interface + +!!! warning "Community contributions" + + This page contains community contributions. The projects listed here are not + maintained by the Headscale authors and are written by community members. + +| Name | Repository Link | Description | Status | +| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- | ------ | +| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha | +| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server | Alpha | +| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required | Alpha | + +You can ask for support on our dedicated [Discord channel](https://discord.com/channels/896711691637780480/1105842846386356294). diff --git a/flake.nix b/flake.nix index 2986fa7d99..0363238b40 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorSha256 = "sha256-lqMmTPLlnm4dGs3O+hS0LmR5LTWP5ICqMSkNnB+XXLI="; + vendorSha256 = "sha256-IOkbbFtE6+tNKnglE/8ZuNxhPSnloqM2sLgTvagMmnc="; ldflags = [ "-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" ]; }; @@ -99,6 +99,11 @@ goreleaser nfpm gotestsum + gotests + + # 'dot' is needed for pprof graphs + # go tool pprof -http=: + graphviz # Protobuf dependencies protobuf diff --git a/gen/go/headscale/v1/apikey.pb.go b/gen/go/headscale/v1/apikey.pb.go index 4e29272cc6..cebca90d16 100644 --- a/gen/go/headscale/v1/apikey.pb.go +++ b/gen/go/headscale/v1/apikey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.29.1 // protoc (unknown) // source: headscale/v1/apikey.proto diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go index dc8f4980c4..6d626c9d35 100644 --- a/gen/go/headscale/v1/device.pb.go +++ b/gen/go/headscale/v1/device.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.29.1 // protoc (unknown) // source: headscale/v1/device.proto diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go index 338364029d..d29ab04b10 100644 --- a/gen/go/headscale/v1/headscale.pb.go +++ b/gen/go/headscale/v1/headscale.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.29.1 // protoc (unknown) // source: headscale/v1/headscale.proto diff --git a/gen/go/headscale/v1/headscale_grpc.pb.go b/gen/go/headscale/v1/headscale_grpc.pb.go index c1839ed79a..b5f67c7faa 100644 --- a/gen/go/headscale/v1/headscale_grpc.pb.go +++ b/gen/go/headscale/v1/headscale_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.2.0 +// - protoc-gen-go-grpc v1.3.0 // - protoc (unknown) // source: headscale/v1/headscale.proto @@ -18,6 +18,34 @@ import ( // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 +const ( + HeadscaleService_GetUser_FullMethodName = "/headscale.v1.HeadscaleService/GetUser" + HeadscaleService_CreateUser_FullMethodName = "/headscale.v1.HeadscaleService/CreateUser" + HeadscaleService_RenameUser_FullMethodName = "/headscale.v1.HeadscaleService/RenameUser" + HeadscaleService_DeleteUser_FullMethodName = "/headscale.v1.HeadscaleService/DeleteUser" + HeadscaleService_ListUsers_FullMethodName = "/headscale.v1.HeadscaleService/ListUsers" + HeadscaleService_CreatePreAuthKey_FullMethodName = "/headscale.v1.HeadscaleService/CreatePreAuthKey" + HeadscaleService_ExpirePreAuthKey_FullMethodName = "/headscale.v1.HeadscaleService/ExpirePreAuthKey" + HeadscaleService_ListPreAuthKeys_FullMethodName = "/headscale.v1.HeadscaleService/ListPreAuthKeys" + HeadscaleService_DebugCreateMachine_FullMethodName = "/headscale.v1.HeadscaleService/DebugCreateMachine" + HeadscaleService_GetMachine_FullMethodName = "/headscale.v1.HeadscaleService/GetMachine" + HeadscaleService_SetTags_FullMethodName = "/headscale.v1.HeadscaleService/SetTags" + HeadscaleService_RegisterMachine_FullMethodName = "/headscale.v1.HeadscaleService/RegisterMachine" + HeadscaleService_DeleteMachine_FullMethodName = "/headscale.v1.HeadscaleService/DeleteMachine" + HeadscaleService_ExpireMachine_FullMethodName = "/headscale.v1.HeadscaleService/ExpireMachine" + HeadscaleService_RenameMachine_FullMethodName = "/headscale.v1.HeadscaleService/RenameMachine" + HeadscaleService_ListMachines_FullMethodName = "/headscale.v1.HeadscaleService/ListMachines" + HeadscaleService_MoveMachine_FullMethodName = "/headscale.v1.HeadscaleService/MoveMachine" + HeadscaleService_GetRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetRoutes" + HeadscaleService_EnableRoute_FullMethodName = "/headscale.v1.HeadscaleService/EnableRoute" + HeadscaleService_DisableRoute_FullMethodName = "/headscale.v1.HeadscaleService/DisableRoute" + HeadscaleService_GetMachineRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetMachineRoutes" + HeadscaleService_DeleteRoute_FullMethodName = "/headscale.v1.HeadscaleService/DeleteRoute" + HeadscaleService_CreateApiKey_FullMethodName = "/headscale.v1.HeadscaleService/CreateApiKey" + HeadscaleService_ExpireApiKey_FullMethodName = "/headscale.v1.HeadscaleService/ExpireApiKey" + HeadscaleService_ListApiKeys_FullMethodName = "/headscale.v1.HeadscaleService/ListApiKeys" +) + // HeadscaleServiceClient is the client API for HeadscaleService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. @@ -64,7 +92,7 @@ func NewHeadscaleServiceClient(cc grpc.ClientConnInterface) HeadscaleServiceClie func (c *headscaleServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) { out := new(GetUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -73,7 +101,7 @@ func (c *headscaleServiceClient) GetUser(ctx context.Context, in *GetUserRequest func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { out := new(CreateUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreateUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -82,7 +110,7 @@ func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *CreateUserR func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *RenameUserRequest, opts ...grpc.CallOption) (*RenameUserResponse, error) { out := new(RenameUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RenameUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -91,7 +119,7 @@ func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *RenameUserR func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*DeleteUserResponse, error) { out := new(DeleteUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -100,7 +128,7 @@ func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *DeleteUserR func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { out := new(ListUsersResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListUsers", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListUsers_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -109,7 +137,7 @@ func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *ListUsersReq func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *CreatePreAuthKeyRequest, opts ...grpc.CallOption) (*CreatePreAuthKeyResponse, error) { out := new(CreatePreAuthKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreatePreAuthKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreatePreAuthKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -118,7 +146,7 @@ func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *Creat func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *ExpirePreAuthKeyRequest, opts ...grpc.CallOption) (*ExpirePreAuthKeyResponse, error) { out := new(ExpirePreAuthKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpirePreAuthKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpirePreAuthKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -127,7 +155,7 @@ func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *Expir func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPreAuthKeysRequest, opts ...grpc.CallOption) (*ListPreAuthKeysResponse, error) { out := new(ListPreAuthKeysResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListPreAuthKeys", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListPreAuthKeys_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -136,7 +164,7 @@ func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPr func (c *headscaleServiceClient) DebugCreateMachine(ctx context.Context, in *DebugCreateMachineRequest, opts ...grpc.CallOption) (*DebugCreateMachineResponse, error) { out := new(DebugCreateMachineResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DebugCreateMachine", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DebugCreateMachine_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -145,7 +173,7 @@ func (c *headscaleServiceClient) DebugCreateMachine(ctx context.Context, in *Deb func (c *headscaleServiceClient) GetMachine(ctx context.Context, in *GetMachineRequest, opts ...grpc.CallOption) (*GetMachineResponse, error) { out := new(GetMachineResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetMachine", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetMachine_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -154,7 +182,7 @@ func (c *headscaleServiceClient) GetMachine(ctx context.Context, in *GetMachineR func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest, opts ...grpc.CallOption) (*SetTagsResponse, error) { out := new(SetTagsResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/SetTags", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_SetTags_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -163,7 +191,7 @@ func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest func (c *headscaleServiceClient) RegisterMachine(ctx context.Context, in *RegisterMachineRequest, opts ...grpc.CallOption) (*RegisterMachineResponse, error) { out := new(RegisterMachineResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RegisterMachine", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RegisterMachine_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -172,7 +200,7 @@ func (c *headscaleServiceClient) RegisterMachine(ctx context.Context, in *Regist func (c *headscaleServiceClient) DeleteMachine(ctx context.Context, in *DeleteMachineRequest, opts ...grpc.CallOption) (*DeleteMachineResponse, error) { out := new(DeleteMachineResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteMachine", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteMachine_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -181,7 +209,7 @@ func (c *headscaleServiceClient) DeleteMachine(ctx context.Context, in *DeleteMa func (c *headscaleServiceClient) ExpireMachine(ctx context.Context, in *ExpireMachineRequest, opts ...grpc.CallOption) (*ExpireMachineResponse, error) { out := new(ExpireMachineResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireMachine", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpireMachine_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -190,7 +218,7 @@ func (c *headscaleServiceClient) ExpireMachine(ctx context.Context, in *ExpireMa func (c *headscaleServiceClient) RenameMachine(ctx context.Context, in *RenameMachineRequest, opts ...grpc.CallOption) (*RenameMachineResponse, error) { out := new(RenameMachineResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameMachine", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RenameMachine_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -199,7 +227,7 @@ func (c *headscaleServiceClient) RenameMachine(ctx context.Context, in *RenameMa func (c *headscaleServiceClient) ListMachines(ctx context.Context, in *ListMachinesRequest, opts ...grpc.CallOption) (*ListMachinesResponse, error) { out := new(ListMachinesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListMachines", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListMachines_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -208,7 +236,7 @@ func (c *headscaleServiceClient) ListMachines(ctx context.Context, in *ListMachi func (c *headscaleServiceClient) MoveMachine(ctx context.Context, in *MoveMachineRequest, opts ...grpc.CallOption) (*MoveMachineResponse, error) { out := new(MoveMachineResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/MoveMachine", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_MoveMachine_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -217,7 +245,7 @@ func (c *headscaleServiceClient) MoveMachine(ctx context.Context, in *MoveMachin func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesRequest, opts ...grpc.CallOption) (*GetRoutesResponse, error) { out := new(GetRoutesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetRoutes", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetRoutes_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -226,7 +254,7 @@ func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesReq func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRouteRequest, opts ...grpc.CallOption) (*EnableRouteResponse, error) { out := new(EnableRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/EnableRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_EnableRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -235,7 +263,7 @@ func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRout func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRouteRequest, opts ...grpc.CallOption) (*DisableRouteResponse, error) { out := new(DisableRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DisableRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DisableRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -244,7 +272,7 @@ func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRo func (c *headscaleServiceClient) GetMachineRoutes(ctx context.Context, in *GetMachineRoutesRequest, opts ...grpc.CallOption) (*GetMachineRoutesResponse, error) { out := new(GetMachineRoutesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetMachineRoutes", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetMachineRoutes_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -253,7 +281,7 @@ func (c *headscaleServiceClient) GetMachineRoutes(ctx context.Context, in *GetMa func (c *headscaleServiceClient) DeleteRoute(ctx context.Context, in *DeleteRouteRequest, opts ...grpc.CallOption) (*DeleteRouteResponse, error) { out := new(DeleteRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -262,7 +290,7 @@ func (c *headscaleServiceClient) DeleteRoute(ctx context.Context, in *DeleteRout func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in *CreateApiKeyRequest, opts ...grpc.CallOption) (*CreateApiKeyResponse, error) { out := new(CreateApiKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateApiKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreateApiKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -271,7 +299,7 @@ func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in *CreateApi func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in *ExpireApiKeyRequest, opts ...grpc.CallOption) (*ExpireApiKeyResponse, error) { out := new(ExpireApiKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireApiKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpireApiKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -280,7 +308,7 @@ func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in *ExpireApi func (c *headscaleServiceClient) ListApiKeys(ctx context.Context, in *ListApiKeysRequest, opts ...grpc.CallOption) (*ListApiKeysResponse, error) { out := new(ListApiKeysResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListApiKeys", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListApiKeys_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -426,7 +454,7 @@ func _HeadscaleService_GetUser_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetUser", + FullMethod: HeadscaleService_GetUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetUser(ctx, req.(*GetUserRequest)) @@ -444,7 +472,7 @@ func _HeadscaleService_CreateUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreateUser", + FullMethod: HeadscaleService_CreateUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) @@ -462,7 +490,7 @@ func _HeadscaleService_RenameUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RenameUser", + FullMethod: HeadscaleService_RenameUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RenameUser(ctx, req.(*RenameUserRequest)) @@ -480,7 +508,7 @@ func _HeadscaleService_DeleteUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteUser", + FullMethod: HeadscaleService_DeleteUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteUser(ctx, req.(*DeleteUserRequest)) @@ -498,7 +526,7 @@ func _HeadscaleService_ListUsers_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListUsers", + FullMethod: HeadscaleService_ListUsers_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListUsers(ctx, req.(*ListUsersRequest)) @@ -516,7 +544,7 @@ func _HeadscaleService_CreatePreAuthKey_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreatePreAuthKey", + FullMethod: HeadscaleService_CreatePreAuthKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreatePreAuthKey(ctx, req.(*CreatePreAuthKeyRequest)) @@ -534,7 +562,7 @@ func _HeadscaleService_ExpirePreAuthKey_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpirePreAuthKey", + FullMethod: HeadscaleService_ExpirePreAuthKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpirePreAuthKey(ctx, req.(*ExpirePreAuthKeyRequest)) @@ -552,7 +580,7 @@ func _HeadscaleService_ListPreAuthKeys_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListPreAuthKeys", + FullMethod: HeadscaleService_ListPreAuthKeys_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListPreAuthKeys(ctx, req.(*ListPreAuthKeysRequest)) @@ -570,7 +598,7 @@ func _HeadscaleService_DebugCreateMachine_Handler(srv interface{}, ctx context.C } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DebugCreateMachine", + FullMethod: HeadscaleService_DebugCreateMachine_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DebugCreateMachine(ctx, req.(*DebugCreateMachineRequest)) @@ -588,7 +616,7 @@ func _HeadscaleService_GetMachine_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetMachine", + FullMethod: HeadscaleService_GetMachine_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetMachine(ctx, req.(*GetMachineRequest)) @@ -606,7 +634,7 @@ func _HeadscaleService_SetTags_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/SetTags", + FullMethod: HeadscaleService_SetTags_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).SetTags(ctx, req.(*SetTagsRequest)) @@ -624,7 +652,7 @@ func _HeadscaleService_RegisterMachine_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RegisterMachine", + FullMethod: HeadscaleService_RegisterMachine_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RegisterMachine(ctx, req.(*RegisterMachineRequest)) @@ -642,7 +670,7 @@ func _HeadscaleService_DeleteMachine_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteMachine", + FullMethod: HeadscaleService_DeleteMachine_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteMachine(ctx, req.(*DeleteMachineRequest)) @@ -660,7 +688,7 @@ func _HeadscaleService_ExpireMachine_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpireMachine", + FullMethod: HeadscaleService_ExpireMachine_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpireMachine(ctx, req.(*ExpireMachineRequest)) @@ -678,7 +706,7 @@ func _HeadscaleService_RenameMachine_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RenameMachine", + FullMethod: HeadscaleService_RenameMachine_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RenameMachine(ctx, req.(*RenameMachineRequest)) @@ -696,7 +724,7 @@ func _HeadscaleService_ListMachines_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListMachines", + FullMethod: HeadscaleService_ListMachines_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListMachines(ctx, req.(*ListMachinesRequest)) @@ -714,7 +742,7 @@ func _HeadscaleService_MoveMachine_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/MoveMachine", + FullMethod: HeadscaleService_MoveMachine_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).MoveMachine(ctx, req.(*MoveMachineRequest)) @@ -732,7 +760,7 @@ func _HeadscaleService_GetRoutes_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetRoutes", + FullMethod: HeadscaleService_GetRoutes_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetRoutes(ctx, req.(*GetRoutesRequest)) @@ -750,7 +778,7 @@ func _HeadscaleService_EnableRoute_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/EnableRoute", + FullMethod: HeadscaleService_EnableRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).EnableRoute(ctx, req.(*EnableRouteRequest)) @@ -768,7 +796,7 @@ func _HeadscaleService_DisableRoute_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DisableRoute", + FullMethod: HeadscaleService_DisableRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DisableRoute(ctx, req.(*DisableRouteRequest)) @@ -786,7 +814,7 @@ func _HeadscaleService_GetMachineRoutes_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetMachineRoutes", + FullMethod: HeadscaleService_GetMachineRoutes_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetMachineRoutes(ctx, req.(*GetMachineRoutesRequest)) @@ -804,7 +832,7 @@ func _HeadscaleService_DeleteRoute_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteRoute", + FullMethod: HeadscaleService_DeleteRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteRoute(ctx, req.(*DeleteRouteRequest)) @@ -822,7 +850,7 @@ func _HeadscaleService_CreateApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreateApiKey", + FullMethod: HeadscaleService_CreateApiKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreateApiKey(ctx, req.(*CreateApiKeyRequest)) @@ -840,7 +868,7 @@ func _HeadscaleService_ExpireApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpireApiKey", + FullMethod: HeadscaleService_ExpireApiKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpireApiKey(ctx, req.(*ExpireApiKeyRequest)) @@ -858,7 +886,7 @@ func _HeadscaleService_ListApiKeys_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListApiKeys", + FullMethod: HeadscaleService_ListApiKeys_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListApiKeys(ctx, req.(*ListApiKeysRequest)) diff --git a/gen/go/headscale/v1/machine.pb.go b/gen/go/headscale/v1/machine.pb.go index 616ef2f172..c6b70dfb82 100644 --- a/gen/go/headscale/v1/machine.pb.go +++ b/gen/go/headscale/v1/machine.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.29.1 // protoc (unknown) // source: headscale/v1/machine.proto diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go index 4b03209f07..0698b34b3d 100644 --- a/gen/go/headscale/v1/preauthkey.pb.go +++ b/gen/go/headscale/v1/preauthkey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.29.1 // protoc (unknown) // source: headscale/v1/preauthkey.proto diff --git a/gen/go/headscale/v1/routes.pb.go b/gen/go/headscale/v1/routes.pb.go index 4da8250a7d..154f78bb46 100644 --- a/gen/go/headscale/v1/routes.pb.go +++ b/gen/go/headscale/v1/routes.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.29.1 // protoc (unknown) // source: headscale/v1/routes.proto diff --git a/gen/go/headscale/v1/user.pb.go b/gen/go/headscale/v1/user.pb.go index 905afc8664..46c3740dad 100644 --- a/gen/go/headscale/v1/user.pb.go +++ b/gen/go/headscale/v1/user.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.29.1 // protoc (unknown) // source: headscale/v1/user.proto diff --git a/go.mod b/go.mod index e3c60072e0..e26e61abd9 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.20 require ( github.com/AlecAivazis/survey/v2 v2.3.6 - github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 github.com/cenkalti/backoff/v4 v4.2.0 github.com/coreos/go-oidc/v3 v3.5.0 github.com/davecgh/go-spew v1.1.1 @@ -12,6 +11,7 @@ require ( github.com/efekarakus/termcolor v1.0.1 github.com/glebarez/sqlite v1.7.0 github.com/gofrs/uuid/v5 v5.0.0 + github.com/google/go-cmp v0.5.9 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 @@ -20,6 +20,7 @@ require ( github.com/ory/dockertest/v3 v3.9.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/philip-bui/grpc-zerolog v1.0.1 + github.com/pkg/profile v1.7.0 github.com/prometheus/client_golang v1.14.0 github.com/prometheus/common v0.42.0 github.com/pterm/pterm v0.12.58 @@ -64,6 +65,7 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/fgprof v0.9.3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/glebarez/go-sqlite v1.20.3 // indirect @@ -72,9 +74,9 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gookit/color v1.5.3 // indirect @@ -141,6 +143,7 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gotest.tools/v3 v3.4.0 // indirect modernc.org/libc v1.22.2 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect diff --git a/go.sum b/go.sum index 89115e47a3..cf73d72375 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,6 @@ github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkU github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 h1:POmUHfxXdeyM8Aomg4tKDcwATCFuW+cYLkj6pwsw9pc= -github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029/go.mod h1:Rpr5n9cGHYdM3S3IK8ROSUUUYjQOu+MSUCZDcJbYWi8= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -129,6 +127,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -238,7 +238,9 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -272,6 +274,7 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -384,6 +387,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -669,6 +674,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -894,7 +900,8 @@ gorm.io/driver/postgres v1.4.8/go.mod h1:O9MruWGNLUBUWVYfWuBClpf3HeGjOoybY0SNmCs gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s= gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/acls.go b/hscontrol/acls.go similarity index 62% rename from acls.go rename to hscontrol/acls.go index 2073ee8483..449c7ffd3d 100644 --- a/acls.go +++ b/hscontrol/acls.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "encoding/json" @@ -13,7 +13,6 @@ import ( "time" "github.com/rs/zerolog/log" - "github.com/samber/lo" "github.com/tailscale/hujson" "go4.org/netipx" "gopkg.in/yaml.v3" @@ -60,8 +59,8 @@ const ( var featureEnableSSH = envknob.RegisterBool("HEADSCALE_EXPERIMENTAL_FEATURE_SSH") -// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules. -func (h *Headscale) LoadACLPolicy(path string) error { +// LoadACLPolicyFromPath loads the ACL policy from the specify path, and generates the ACL rules. +func (h *Headscale) LoadACLPolicyFromPath(path string) error { log.Debug(). Str("func", "LoadACLPolicy"). Str("path", path). @@ -73,37 +72,42 @@ func (h *Headscale) LoadACLPolicy(path string) error { } defer policyFile.Close() - var policy ACLPolicy policyBytes, err := io.ReadAll(policyFile) if err != nil { return err } + log.Debug(). + Str("path", path). + Bytes("file", policyBytes). + Msg("Loading ACLs") + switch filepath.Ext(path) { case ".yml", ".yaml": - log.Debug(). - Str("path", path). - Bytes("file", policyBytes). - Msg("Loading ACLs from YAML") + return h.LoadACLPolicyFromBytes(policyBytes, "yaml") + } - err := yaml.Unmarshal(policyBytes, &policy) + return h.LoadACLPolicyFromBytes(policyBytes, "hujson") +} + +func (h *Headscale) LoadACLPolicyFromBytes(acl []byte, format string) error { + var policy ACLPolicy + switch format { + case "yaml": + err := yaml.Unmarshal(acl, &policy) if err != nil { return err } - log.Trace(). - Interface("policy", policy). - Msg("Loaded policy from YAML") - default: - ast, err := hujson.Parse(policyBytes) + ast, err := hujson.Parse(acl) if err != nil { return err } ast.Standardize() - policyBytes = ast.Pack() - err = json.Unmarshal(policyBytes, &policy) + acl = ast.Pack() + err = json.Unmarshal(acl, &policy) if err != nil { return err } @@ -128,21 +132,14 @@ func (h *Headscale) UpdateACLRules() error { return errEmptyPolicy } - rules, err := generateACLRules(machines, *h.aclPolicy, h.cfg.OIDC.StripEmaildomain) + rules, err := h.aclPolicy.generateFilterRules(machines, h.cfg.OIDC.StripEmaildomain) if err != nil { return err } + log.Trace().Interface("ACL", rules).Msg("ACL rules generated") h.aclRules = rules - // Precompute a map of which sources can reach each destination, this is - // to provide quicker lookup when we calculate the peerlist for the map - // response to nodes. - aclPeerCacheMap := generateACLPeerCacheMap(rules) - h.aclPeerCacheMapRW.Lock() - h.aclPeerCacheMap = aclPeerCacheMap - h.aclPeerCacheMapRW.Unlock() - if featureEnableSSH() { sshRules, err := h.generateSSHRules() if err != nil { @@ -160,91 +157,28 @@ func (h *Headscale) UpdateACLRules() error { return nil } -// generateACLPeerCacheMap takes a list of Tailscale filter rules and generates a map -// of which Sources ("*" and IPs) can access destinations. This is to speed up the -// process of generating MapResponses when deciding which Peers to inform nodes about. -func generateACLPeerCacheMap(rules []tailcfg.FilterRule) map[string]map[string]struct{} { - aclCachePeerMap := make(map[string]map[string]struct{}) - for _, rule := range rules { - for _, srcIP := range rule.SrcIPs { - for _, ip := range expandACLPeerAddr(srcIP) { - if data, ok := aclCachePeerMap[ip]; ok { - for _, dstPort := range rule.DstPorts { - for _, dstIP := range expandACLPeerAddr(dstPort.IP) { - data[dstIP] = struct{}{} - } - } - } else { - dstPortsMap := make(map[string]struct{}, len(rule.DstPorts)) - for _, dstPort := range rule.DstPorts { - for _, dstIP := range expandACLPeerAddr(dstPort.IP) { - dstPortsMap[dstIP] = struct{}{} - } - } - aclCachePeerMap[ip] = dstPortsMap - } - } - } - } - - log.Trace().Interface("ACL Cache Map", aclCachePeerMap).Msg("ACL Peer Cache Map generated") - - return aclCachePeerMap -} - -// expandACLPeerAddr takes a "tailcfg.FilterRule" "IP" and expands it into -// something our cache logic can look up, which is "*" or single IP addresses. -// This is probably quite inefficient, but it is a result of -// "make it work, then make it fast", and a lot of the ACL stuff does not -// work, but people have tried to make it fast. -func expandACLPeerAddr(srcIP string) []string { - if ip, err := netip.ParseAddr(srcIP); err == nil { - return []string{ip.String()} - } - - if cidr, err := netip.ParsePrefix(srcIP); err == nil { - addrs := []string{} - - ipRange := netipx.RangeOfPrefix(cidr) - - from := ipRange.From() - too := ipRange.To() - - if from == too { - return []string{from.String()} - } - - for from != too && from.Less(too) { - addrs = append(addrs, from.String()) - from = from.Next() - } - addrs = append(addrs, too.String()) // Add the last IP address in the range - - return addrs - } - - // probably "*" or other string based "IP" - return []string{srcIP} -} - -func generateACLRules( +// generateFilterRules takes a set of machines and an ACLPolicy and generates a +// set of Tailscale compatible FilterRules used to allow traffic on clients. +func (pol *ACLPolicy) generateFilterRules( machines []Machine, - aclPolicy ACLPolicy, - stripEmaildomain bool, + stripEmailDomain bool, ) ([]tailcfg.FilterRule, error) { rules := []tailcfg.FilterRule{} - for index, acl := range aclPolicy.ACLs { + for index, acl := range pol.ACLs { if acl.Action != "accept" { return nil, errInvalidAction } srcIPs := []string{} - for innerIndex, src := range acl.Sources { - srcs, err := generateACLPolicySrc(machines, aclPolicy, src, stripEmaildomain) + for srcIndex, src := range acl.Sources { + srcs, err := pol.getIPsFromSource(src, machines, stripEmailDomain) if err != nil { log.Error(). - Msgf("Error parsing ACL %d, Source %d", index, innerIndex) + Interface("src", src). + Int("ACL index", index). + Int("Src index", srcIndex). + Msgf("Error parsing ACL") return nil, err } @@ -260,17 +194,19 @@ func generateACLRules( } destPorts := []tailcfg.NetPortRange{} - for innerIndex, dest := range acl.Destinations { - dests, err := generateACLPolicyDest( - machines, - aclPolicy, + for destIndex, dest := range acl.Destinations { + dests, err := pol.getNetPortRangeFromDestination( dest, + machines, needsWildcard, - stripEmaildomain, + stripEmailDomain, ) if err != nil { log.Error(). - Msgf("Error parsing ACL %d, Destination %d", index, innerIndex) + Interface("dest", dest). + Int("ACL index", index). + Int("dest index", destIndex). + Msgf("Error parsing ACL") return nil, err } @@ -341,22 +277,41 @@ func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) { principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources)) for innerIndex, rawSrc := range sshACL.Sources { - expandedSrcs, err := expandAlias( - machines, - *h.aclPolicy, - rawSrc, - h.cfg.OIDC.StripEmaildomain, - ) - if err != nil { - log.Error(). - Msgf("Error parsing SSH %d, Source %d", index, innerIndex) - - return nil, err - } - for _, expandedSrc := range expandedSrcs { + if isWildcard(rawSrc) { principals = append(principals, &tailcfg.SSHPrincipal{ - NodeIP: expandedSrc, + Any: true, }) + } else if isGroup(rawSrc) { + users, err := h.aclPolicy.getUsersInGroup(rawSrc, h.cfg.OIDC.StripEmaildomain) + if err != nil { + log.Error(). + Msgf("Error parsing SSH %d, Source %d", index, innerIndex) + + return nil, err + } + + for _, user := range users { + principals = append(principals, &tailcfg.SSHPrincipal{ + UserLogin: user, + }) + } + } else { + expandedSrcs, err := h.aclPolicy.expandAlias( + machines, + rawSrc, + h.cfg.OIDC.StripEmaildomain, + ) + if err != nil { + log.Error(). + Msgf("Error parsing SSH %d, Source %d", index, innerIndex) + + return nil, err + } + for _, expandedSrc := range expandedSrcs.Prefixes() { + principals = append(principals, &tailcfg.SSHPrincipal{ + NodeIP: expandedSrc.Addr().String(), + }) + } } } @@ -365,10 +320,9 @@ func (h *Headscale) generateSSHRules() ([]*tailcfg.SSHRule, error) { userMap[user] = "=" } rules = append(rules, &tailcfg.SSHRule{ - RuleExpires: nil, - Principals: principals, - SSHUsers: userMap, - Action: &action, + Principals: principals, + SSHUsers: userMap, + Action: &action, }) } @@ -392,19 +346,32 @@ func sshCheckAction(duration string) (*tailcfg.SSHAction, error) { }, nil } -func generateACLPolicySrc( - machines []Machine, - aclPolicy ACLPolicy, +// getIPsFromSource returns a set of Source IPs that would be associated +// with the given src alias. +func (pol *ACLPolicy) getIPsFromSource( src string, + machines []Machine, stripEmaildomain bool, ) ([]string, error) { - return expandAlias(machines, aclPolicy, src, stripEmaildomain) + ipSet, err := pol.expandAlias(machines, src, stripEmaildomain) + if err != nil { + return []string{}, err + } + + prefixes := []string{} + + for _, prefix := range ipSet.Prefixes() { + prefixes = append(prefixes, prefix.String()) + } + + return prefixes, nil } -func generateACLPolicyDest( - machines []Machine, - aclPolicy ACLPolicy, +// getNetPortRangeFromDestination returns a set of tailcfg.NetPortRange +// which are associated with the dest alias. +func (pol *ACLPolicy) getNetPortRangeFromDestination( dest string, + machines []Machine, needsWildcard bool, stripEmaildomain bool, ) ([]tailcfg.NetPortRange, error) { @@ -451,9 +418,8 @@ func generateACLPolicyDest( alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1]) } - expanded, err := expandAlias( + expanded, err := pol.expandAlias( machines, - aclPolicy, alias, stripEmaildomain, ) @@ -466,11 +432,11 @@ func generateACLPolicyDest( } dests := []tailcfg.NetPortRange{} - for _, d := range expanded { - for _, p := range *ports { + for _, dest := range expanded.Prefixes() { + for _, port := range *ports { pr := tailcfg.NetPortRange{ - IP: d, - Ports: p, + IP: dest.String(), + Ports: port, } dests = append(dests, pr) } @@ -537,135 +503,64 @@ func parseProtocol(protocol string) ([]int, bool, error) { // - an ip // - a cidr // and transform these in IPAddresses. -func expandAlias( +func (pol *ACLPolicy) expandAlias( machines Machines, - aclPolicy ACLPolicy, alias string, stripEmailDomain bool, -) ([]string, error) { - ips := []string{} - if alias == "*" { - return []string{"*"}, nil +) (*netipx.IPSet, error) { + if isWildcard(alias) { + return parseIPSet("*", nil) } + build := netipx.IPSetBuilder{} + log.Debug(). Str("alias", alias). Msg("Expanding") - if strings.HasPrefix(alias, "group:") { - users, err := expandGroup(aclPolicy, alias, stripEmailDomain) - if err != nil { - return ips, err - } - for _, n := range users { - nodes := filterMachinesByUser(machines, n) - for _, node := range nodes { - ips = append(ips, node.IPAddresses.ToStringSlice()...) - } - } - - return ips, nil + // if alias is a group + if isGroup(alias) { + return pol.getIPsFromGroup(alias, machines, stripEmailDomain) } - if strings.HasPrefix(alias, "tag:") { - // check for forced tags - for _, machine := range machines { - if contains(machine.ForcedTags, alias) { - ips = append(ips, machine.IPAddresses.ToStringSlice()...) - } - } - - // find tag owners - owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain) - if err != nil { - if errors.Is(err, errInvalidTag) { - if len(ips) == 0 { - return ips, fmt.Errorf( - "%w. %v isn't owned by a TagOwner and no forced tags are defined", - errInvalidTag, - alias, - ) - } - - return ips, nil - } else { - return ips, err - } - } - - // filter out machines per tag owner - for _, user := range owners { - machines := filterMachinesByUser(machines, user) - for _, machine := range machines { - hi := machine.GetHostInfo() - if contains(hi.RequestTags, alias) { - ips = append(ips, machine.IPAddresses.ToStringSlice()...) - } - } - } - - return ips, nil + // if alias is a tag + if isTag(alias) { + return pol.getIPsFromTag(alias, machines, stripEmailDomain) } // if alias is a user - nodes := filterMachinesByUser(machines, alias) - nodes = excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias, stripEmailDomain) - - for _, n := range nodes { - ips = append(ips, n.IPAddresses.ToStringSlice()...) - } - if len(ips) > 0 { - return ips, nil + if ips, err := pol.getIPsForUser(alias, machines, stripEmailDomain); ips != nil { + return ips, err } // if alias is an host - if h, ok := aclPolicy.Hosts[alias]; ok { + // Note, this is recursive. + if h, ok := pol.Hosts[alias]; ok { log.Trace().Str("host", h.String()).Msg("expandAlias got hosts entry") - return expandAlias(machines, aclPolicy, h.String(), stripEmailDomain) + return pol.expandAlias(machines, h.String(), stripEmailDomain) } // if alias is an IP if ip, err := netip.ParseAddr(alias); err == nil { - log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip") - ips := []string{ip.String()} - matches := machines.FilterByIP(ip) - - for _, machine := range matches { - ips = append(ips, machine.IPAddresses.ToStringSlice()...) - } - - return lo.Uniq(ips), nil + return pol.getIPsFromSingleIP(ip, machines) } - if cidr, err := netip.ParsePrefix(alias); err == nil { - log.Trace().Str("cidr", cidr.String()).Msg("expandAlias got cidr") - val := []string{cidr.String()} - // This is suboptimal and quite expensive, but if we only add the cidr, we will miss all the relevant IPv6 - // addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers. - for _, machine := range machines { - for _, ip := range machine.IPAddresses { - // log.Trace(). - // Msgf("checking if machine ip (%s) is part of cidr (%s): %v, is single ip cidr (%v), addr: %s", ip.String(), cidr.String(), cidr.Contains(ip), cidr.IsSingleIP(), cidr.Addr().String()) - if cidr.Contains(ip) { - val = append(val, machine.IPAddresses.ToStringSlice()...) - } - } - } - - return lo.Uniq(val), nil + // if alias is an IP Prefix (CIDR) + if prefix, err := netip.ParsePrefix(alias); err == nil { + return pol.getIPsFromIPPrefix(prefix, machines) } log.Warn().Msgf("No IPs found with the alias %v", alias) - return ips, nil + return build.IPSet() } // excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones // that are correctly tagged since they should not be listed as being in the user // we assume in this function that we only have nodes from 1 user. func excludeCorrectlyTaggedNodes( - aclPolicy ACLPolicy, + aclPolicy *ACLPolicy, nodes []Machine, user string, stripEmailDomain bool, @@ -673,7 +568,7 @@ func excludeCorrectlyTaggedNodes( out := []Machine{} tags := []string{} for tag := range aclPolicy.TagOwners { - owners, _ := expandTagOwners(aclPolicy, user, stripEmailDomain) + owners, _ := getTagOwners(aclPolicy, user, stripEmailDomain) ns := append(owners, user) if contains(ns, user) { tags = append(tags, tag) @@ -703,7 +598,7 @@ func excludeCorrectlyTaggedNodes( } func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) { - if portsStr == "*" { + if isWildcard(portsStr) { return &[]tailcfg.PortRange{ {First: portRangeBegin, Last: portRangeEnd}, }, nil @@ -761,15 +656,15 @@ func filterMachinesByUser(machines []Machine, user string) []Machine { return out } -// expandTagOwners will return a list of user. An owner can be either a user or a group +// getTagOwners will return a list of user. An owner can be either a user or a group // a group cannot be composed of groups. -func expandTagOwners( - aclPolicy ACLPolicy, +func getTagOwners( + pol *ACLPolicy, tag string, stripEmailDomain bool, ) ([]string, error) { var owners []string - ows, ok := aclPolicy.TagOwners[tag] + ows, ok := pol.TagOwners[tag] if !ok { return []string{}, fmt.Errorf( "%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners", @@ -778,8 +673,8 @@ func expandTagOwners( ) } for _, owner := range ows { - if strings.HasPrefix(owner, "group:") { - gs, err := expandGroup(aclPolicy, owner, stripEmailDomain) + if isGroup(owner) { + gs, err := pol.getUsersInGroup(owner, stripEmailDomain) if err != nil { return []string{}, err } @@ -792,15 +687,15 @@ func expandTagOwners( return owners, nil } -// expandGroup will return the list of user inside the group +// getUsersInGroup will return the list of user inside the group // after some validation. -func expandGroup( - aclPolicy ACLPolicy, +func (pol *ACLPolicy) getUsersInGroup( group string, stripEmailDomain bool, ) ([]string, error) { - outGroups := []string{} - aclGroups, ok := aclPolicy.Groups[group] + users := []string{} + log.Trace().Caller().Interface("pol", pol).Msg("test") + aclGroups, ok := pol.Groups[group] if !ok { return []string{}, fmt.Errorf( "group %v isn't registered. %w", @@ -809,7 +704,7 @@ func expandGroup( ) } for _, group := range aclGroups { - if strings.HasPrefix(group, "group:") { + if isGroup(group) { return []string{}, fmt.Errorf( "%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups", errInvalidGroup, @@ -823,8 +718,151 @@ func expandGroup( errInvalidGroup, ) } - outGroups = append(outGroups, grp) + users = append(users, grp) + } + + return users, nil +} + +func (pol *ACLPolicy) getIPsFromGroup( + group string, + machines Machines, + stripEmailDomain bool, +) (*netipx.IPSet, error) { + build := netipx.IPSetBuilder{} + + users, err := pol.getUsersInGroup(group, stripEmailDomain) + if err != nil { + return &netipx.IPSet{}, err + } + for _, user := range users { + filteredMachines := filterMachinesByUser(machines, user) + for _, machine := range filteredMachines { + machine.IPAddresses.AppendToIPSet(&build) + } + } + + return build.IPSet() +} + +func (pol *ACLPolicy) getIPsFromTag( + alias string, + machines Machines, + stripEmailDomain bool, +) (*netipx.IPSet, error) { + build := netipx.IPSetBuilder{} + + // check for forced tags + for _, machine := range machines { + if contains(machine.ForcedTags, alias) { + machine.IPAddresses.AppendToIPSet(&build) + } + } + + // find tag owners + owners, err := getTagOwners(pol, alias, stripEmailDomain) + if err != nil { + if errors.Is(err, errInvalidTag) { + ipSet, _ := build.IPSet() + if len(ipSet.Prefixes()) == 0 { + return ipSet, fmt.Errorf( + "%w. %v isn't owned by a TagOwner and no forced tags are defined", + errInvalidTag, + alias, + ) + } + + return build.IPSet() + } else { + return nil, err + } + } + + // filter out machines per tag owner + for _, user := range owners { + machines := filterMachinesByUser(machines, user) + for _, machine := range machines { + hi := machine.GetHostInfo() + if contains(hi.RequestTags, alias) { + machine.IPAddresses.AppendToIPSet(&build) + } + } + } + + return build.IPSet() +} + +func (pol *ACLPolicy) getIPsForUser( + user string, + machines Machines, + stripEmailDomain bool, +) (*netipx.IPSet, error) { + build := netipx.IPSetBuilder{} + + filteredMachines := filterMachinesByUser(machines, user) + filteredMachines = excludeCorrectlyTaggedNodes(pol, filteredMachines, user, stripEmailDomain) + + // shortcurcuit if we have no machines to get ips from. + if len(filteredMachines) == 0 { + return nil, nil //nolint + } + + for _, machine := range filteredMachines { + machine.IPAddresses.AppendToIPSet(&build) + } + + return build.IPSet() +} + +func (pol *ACLPolicy) getIPsFromSingleIP( + ip netip.Addr, + machines Machines, +) (*netipx.IPSet, error) { + log.Trace().Str("ip", ip.String()).Msg("expandAlias got ip") + + matches := machines.FilterByIP(ip) + + build := netipx.IPSetBuilder{} + build.Add(ip) + + for _, machine := range matches { + machine.IPAddresses.AppendToIPSet(&build) + } + + return build.IPSet() +} + +func (pol *ACLPolicy) getIPsFromIPPrefix( + prefix netip.Prefix, + machines Machines, +) (*netipx.IPSet, error) { + log.Trace().Str("prefix", prefix.String()).Msg("expandAlias got prefix") + build := netipx.IPSetBuilder{} + build.AddPrefix(prefix) + + // This is suboptimal and quite expensive, but if we only add the prefix, we will miss all the relevant IPv6 + // addresses for the hosts that belong to tailscale. This doesnt really affect stuff like subnet routers. + for _, machine := range machines { + for _, ip := range machine.IPAddresses { + // log.Trace(). + // Msgf("checking if machine ip (%s) is part of prefix (%s): %v, is single ip prefix (%v), addr: %s", ip.String(), prefix.String(), prefix.Contains(ip), prefix.IsSingleIP(), prefix.Addr().String()) + if prefix.Contains(ip) { + machine.IPAddresses.AppendToIPSet(&build) + } + } } - return outGroups, nil + return build.IPSet() +} + +func isWildcard(str string) bool { + return str == "*" +} + +func isGroup(str string) bool { + return strings.HasPrefix(str, "group:") +} + +func isTag(str string) bool { + return strings.HasPrefix(str, "tag:") } diff --git a/acls_test.go b/hscontrol/acls_test.go similarity index 73% rename from acls_test.go rename to hscontrol/acls_test.go index eaac2a7cf3..095597f273 100644 --- a/acls_test.go +++ b/hscontrol/acls_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "errors" @@ -6,23 +6,35 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" + "github.com/rs/zerolog/log" + "go4.org/netipx" "gopkg.in/check.v1" "tailscale.com/envknob" "tailscale.com/tailcfg" ) func (s *Suite) TestWrongPath(c *check.C) { - err := app.LoadACLPolicy("asdfg") + err := app.LoadACLPolicyFromPath("asdfg") c.Assert(err, check.NotNil) } func (s *Suite) TestBrokenHuJson(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/broken.hujson") + acl := []byte(` +{ + `) + err := app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.NotNil) } func (s *Suite) TestInvalidPolicyHuson(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/invalid.hujson") + acl := []byte(` +{ + "valid_json": true, + "but_a_policy_though": false +} + `) + err := app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.NotNil) c.Assert(err, check.Equals, errEmptyPolicy) } @@ -46,15 +58,164 @@ func (s *Suite) TestParseInvalidCIDR(c *check.C) { } func (s *Suite) TestRuleInvalidGeneration(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/acl_policy_invalid.hujson") + acl := []byte(` +{ + // Declare static groups of users beyond those in the identity service. + "groups": { + "group:example": [ + "user1@example.com", + "user2@example.com", + ], + }, + // Declare hostname aliases to use in place of IP addresses or subnets. + "hosts": { + "example-host-1": "100.100.100.100", + "example-host-2": "100.100.101.100/24", + }, + // Define who is allowed to use which tags. + "tagOwners": { + // Everyone in the montreal-admins or global-admins group are + // allowed to tag servers as montreal-webserver. + "tag:montreal-webserver": [ + "group:montreal-admins", + "group:global-admins", + ], + // Only a few admins are allowed to create API servers. + "tag:api-server": [ + "group:global-admins", + "example-host-1", + ], + }, + // Access control lists. + "acls": [ + // Engineering users, plus the president, can access port 22 (ssh) + // and port 3389 (remote desktop protocol) on all servers, and all + // ports on git-server or ci-server. + { + "action": "accept", + "src": [ + "group:engineering", + "president@example.com" + ], + "dst": [ + "*:22,3389", + "git-server:*", + "ci-server:*" + ], + }, + // Allow engineer users to access any port on a device tagged with + // tag:production. + { + "action": "accept", + "src": [ + "group:engineers" + ], + "dst": [ + "tag:production:*" + ], + }, + // Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts + // on both networks. + { + "action": "accept", + "src": [ + "my-subnet", + "192.168.1.0/24" + ], + "dst": [ + "my-subnet:*", + "192.168.1.0/24:*" + ], + }, + // Allow every user of your network to access anything on the network. + // Comment out this section if you want to define specific ACL + // restrictions above. + { + "action": "accept", + "src": [ + "*" + ], + "dst": [ + "*:*" + ], + }, + // All users in Montreal are allowed to access the Montreal web + // servers. + { + "action": "accept", + "src": [ + "group:montreal-users" + ], + "dst": [ + "tag:montreal-webserver:80,443" + ], + }, + // Montreal web servers are allowed to make outgoing connections to + // the API servers, but only on https port 443. + // In contrast, this doesn't grant API servers the right to initiate + // any connections. + { + "action": "accept", + "src": [ + "tag:montreal-webserver" + ], + "dst": [ + "tag:api-server:443" + ], + }, + ], + // Declare tests to check functionality of ACL rules + "tests": [ + { + "src": "user1@example.com", + "accept": [ + "example-host-1:22", + "example-host-2:80" + ], + "deny": [ + "exapmle-host-2:100" + ], + }, + { + "src": "user2@example.com", + "accept": [ + "100.60.3.4:22" + ], + }, + ], +} + `) + err := app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.NotNil) } func (s *Suite) TestBasicRule(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_1.hujson") + acl := []byte(` +{ + "hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "acls": [ + { + "action": "accept", + "src": [ + "subnet-1", + "192.168.1.0/24" + ], + "dst": [ + "*:22,3389", + "host-1:*", + ], + }, + ], +} + `) + err := app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.IsNil) - rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) + rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) c.Assert(err, check.IsNil) c.Assert(rules, check.NotNil) } @@ -142,7 +303,7 @@ func (s *Suite) TestSshRules(c *check.C) { c.Assert(app.sshPolicy.Rules, check.HasLen, 2) c.Assert(app.sshPolicy.Rules[0].SSHUsers, check.HasLen, 1) c.Assert(app.sshPolicy.Rules[0].Principals, check.HasLen, 1) - c.Assert(app.sshPolicy.Rules[0].Principals[0].NodeIP, check.Matches, "100.64.0.1") + c.Assert(app.sshPolicy.Rules[0].Principals[0].UserLogin, check.Matches, "user1") c.Assert(app.sshPolicy.Rules[1].SSHUsers, check.HasLen, 1) c.Assert(app.sshPolicy.Rules[1].Principals, check.HasLen, 1) @@ -230,7 +391,7 @@ func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) { c.Assert(err, check.IsNil) c.Assert(app.aclRules, check.HasLen, 1) c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1) - c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1") + c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1/32") } // this test should validate that we can expand a group in a TagOWner section and @@ -280,7 +441,7 @@ func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) { c.Assert(err, check.IsNil) c.Assert(app.aclRules, check.HasLen, 1) c.Assert(app.aclRules[0].DstPorts, check.HasLen, 1) - c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1") + c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1/32") } // need a test with: @@ -329,7 +490,7 @@ func (s *Suite) TestInvalidTagValidUser(c *check.C) { c.Assert(err, check.IsNil) c.Assert(app.aclRules, check.HasLen, 1) c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1) - c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1") + c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1/32") } // tag on a host is owned by a tag owner, the tag is valid. @@ -397,21 +558,41 @@ func (s *Suite) TestValidTagInvalidUser(c *check.C) { c.Assert(err, check.IsNil) c.Assert(app.aclRules, check.HasLen, 1) c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1) - c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.2") + c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.2/32") c.Assert(app.aclRules[0].DstPorts, check.HasLen, 2) c.Assert(app.aclRules[0].DstPorts[0].Ports.First, check.Equals, uint16(80)) c.Assert(app.aclRules[0].DstPorts[0].Ports.Last, check.Equals, uint16(80)) - c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1") + c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1/32") c.Assert(app.aclRules[0].DstPorts[1].Ports.First, check.Equals, uint16(443)) c.Assert(app.aclRules[0].DstPorts[1].Ports.Last, check.Equals, uint16(443)) - c.Assert(app.aclRules[0].DstPorts[1].IP, check.Equals, "100.64.0.1") + c.Assert(app.aclRules[0].DstPorts[1].IP, check.Equals, "100.64.0.1/32") } func (s *Suite) TestPortRange(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson") + acl := []byte(` +{ + "hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "acls": [ + { + "action": "accept", + "src": [ + "subnet-1", + ], + "dst": [ + "host-1:5400-5500", + ], + }, + ], +} + `) + err := app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.IsNil) - rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) + rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) c.Assert(err, check.IsNil) c.Assert(rules, check.NotNil) @@ -422,10 +603,51 @@ func (s *Suite) TestPortRange(c *check.C) { } func (s *Suite) TestProtocolParsing(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_protocols.hujson") + acl := []byte(` +{ + "hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "acls": [ + { + "Action": "accept", + "src": [ + "*", + ], + "proto": "tcp", + "dst": [ + "host-1:*", + ], + }, + { + "Action": "accept", + "src": [ + "*", + ], + "proto": "udp", + "dst": [ + "host-1:53", + ], + }, + { + "Action": "accept", + "src": [ + "*", + ], + "proto": "icmp", + "dst": [ + "host-1:*", + ], + }, + ], +} + `) + err := app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.IsNil) - rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) + rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) c.Assert(err, check.IsNil) c.Assert(rules, check.NotNil) @@ -436,10 +658,30 @@ func (s *Suite) TestProtocolParsing(c *check.C) { } func (s *Suite) TestPortWildcard(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson") + acl := []byte(` +{ + "hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "acls": [ + { + "Action": "accept", + "src": [ + "*", + ], + "dst": [ + "host-1:*", + ], + }, + ], +} + `) + err := app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.IsNil) - rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) + rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) c.Assert(err, check.IsNil) c.Assert(rules, check.NotNil) @@ -447,15 +689,27 @@ func (s *Suite) TestPortWildcard(c *check.C) { c.Assert(rules[0].DstPorts, check.HasLen, 1) c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) - c.Assert(rules[0].SrcIPs, check.HasLen, 1) - c.Assert(rules[0].SrcIPs[0], check.Equals, "*") + c.Assert(rules[0].SrcIPs, check.HasLen, 2) + c.Assert(rules[0].SrcIPs[0], check.Equals, "0.0.0.0/0") } func (s *Suite) TestPortWildcardYAML(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.yaml") + acl := []byte(` +--- +hosts: + host-1: 100.100.100.100/32 + subnet-1: 100.100.101.100/24 +acls: + - action: accept + src: + - "*" + dst: + - host-1:* +`) + err := app.LoadACLPolicyFromBytes(acl, "yaml") c.Assert(err, check.IsNil) - rules, err := generateACLRules([]Machine{}, *app.aclPolicy, false) + rules, err := app.aclPolicy.generateFilterRules([]Machine{}, false) c.Assert(err, check.IsNil) c.Assert(rules, check.NotNil) @@ -463,8 +717,8 @@ func (s *Suite) TestPortWildcardYAML(c *check.C) { c.Assert(rules[0].DstPorts, check.HasLen, 1) c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) - c.Assert(rules[0].SrcIPs, check.HasLen, 1) - c.Assert(rules[0].SrcIPs[0], check.Equals, "*") + c.Assert(rules[0].SrcIPs, check.HasLen, 2) + c.Assert(rules[0].SrcIPs[0], check.Equals, "0.0.0.0/0") } func (s *Suite) TestPortUser(c *check.C) { @@ -490,15 +744,33 @@ func (s *Suite) TestPortUser(c *check.C) { } app.db.Save(&machine) - err = app.LoadACLPolicy( - "./tests/acls/acl_policy_basic_user_as_user.hujson", - ) + acl := []byte(` +{ + "hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "acls": [ + { + "action": "accept", + "src": [ + "testuser", + ], + "dst": [ + "host-1:*", + ], + }, + ], +} + `) + err = app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.IsNil) machines, err := app.ListMachines() c.Assert(err, check.IsNil) - rules, err := generateACLRules(machines, *app.aclPolicy, false) + rules, err := app.aclPolicy.generateFilterRules(machines, false) c.Assert(err, check.IsNil) c.Assert(rules, check.NotNil) @@ -509,7 +781,7 @@ func (s *Suite) TestPortUser(c *check.C) { c.Assert(rules[0].SrcIPs, check.HasLen, 1) c.Assert(rules[0].SrcIPs[0], check.Not(check.Equals), "not an ip") c.Assert(len(ips), check.Equals, 1) - c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()) + c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()+"/32") } func (s *Suite) TestPortGroup(c *check.C) { @@ -535,13 +807,39 @@ func (s *Suite) TestPortGroup(c *check.C) { } app.db.Save(&machine) - err = app.LoadACLPolicy("./tests/acls/acl_policy_basic_groups.hujson") + acl := []byte(` +{ + "groups": { + "group:example": [ + "testuser", + ], + }, + + "hosts": { + "host-1": "100.100.100.100", + "subnet-1": "100.100.101.100/24", + }, + + "acls": [ + { + "action": "accept", + "src": [ + "group:example", + ], + "dst": [ + "host-1:*", + ], + }, + ], +} + `) + err = app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.IsNil) machines, err := app.ListMachines() c.Assert(err, check.IsNil) - rules, err := generateACLRules(machines, *app.aclPolicy, false) + rules, err := app.aclPolicy.generateFilterRules(machines, false) c.Assert(err, check.IsNil) c.Assert(rules, check.NotNil) @@ -552,30 +850,35 @@ func (s *Suite) TestPortGroup(c *check.C) { c.Assert(rules[0].SrcIPs, check.HasLen, 1) c.Assert(rules[0].SrcIPs[0], check.Not(check.Equals), "not an ip") c.Assert(len(ips), check.Equals, 1) - c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()) + c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()+"/32") } func Test_expandGroup(t *testing.T) { + type field struct { + pol ACLPolicy + } type args struct { - aclPolicy ACLPolicy group string stripEmailDomain bool } tests := []struct { name string + field field args args want []string wantErr bool }{ { name: "simple test", - args: args{ - aclPolicy: ACLPolicy{ + field: field{ + pol: ACLPolicy{ Groups: Groups{ "group:test": []string{"user1", "user2", "user3"}, "group:foo": []string{"user2", "user3"}, }, }, + }, + args: args{ group: "group:test", stripEmailDomain: true, }, @@ -584,13 +887,15 @@ func Test_expandGroup(t *testing.T) { }, { name: "InexistantGroup", - args: args{ - aclPolicy: ACLPolicy{ + field: field{ + pol: ACLPolicy{ Groups: Groups{ "group:test": []string{"user1", "user2", "user3"}, "group:foo": []string{"user2", "user3"}, }, }, + }, + args: args{ group: "group:undefined", stripEmailDomain: true, }, @@ -599,8 +904,8 @@ func Test_expandGroup(t *testing.T) { }, { name: "Expand emails in group", - args: args{ - aclPolicy: ACLPolicy{ + field: field{ + pol: ACLPolicy{ Groups: Groups{ "group:admin": []string{ "joe.bar@gmail.com", @@ -608,6 +913,8 @@ func Test_expandGroup(t *testing.T) { }, }, }, + }, + args: args{ group: "group:admin", stripEmailDomain: true, }, @@ -616,8 +923,8 @@ func Test_expandGroup(t *testing.T) { }, { name: "Expand emails in group", - args: args{ - aclPolicy: ACLPolicy{ + field: field{ + pol: ACLPolicy{ Groups: Groups{ "group:admin": []string{ "joe.bar@gmail.com", @@ -625,6 +932,8 @@ func Test_expandGroup(t *testing.T) { }, }, }, + }, + args: args{ group: "group:admin", stripEmailDomain: false, }, @@ -634,8 +943,7 @@ func Test_expandGroup(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := expandGroup( - test.args.aclPolicy, + got, err := test.field.pol.getUsersInGroup( test.args.group, test.args.stripEmailDomain, ) @@ -653,7 +961,7 @@ func Test_expandGroup(t *testing.T) { func Test_expandTagOwners(t *testing.T) { type args struct { - aclPolicy ACLPolicy + aclPolicy *ACLPolicy tag string stripEmailDomain bool } @@ -666,7 +974,7 @@ func Test_expandTagOwners(t *testing.T) { { name: "simple tag expansion", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ TagOwners: TagOwners{"tag:test": []string{"user1"}}, }, tag: "tag:test", @@ -678,7 +986,7 @@ func Test_expandTagOwners(t *testing.T) { { name: "expand with tag and group", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ Groups: Groups{"group:foo": []string{"user1", "user2"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo"}}, }, @@ -691,7 +999,7 @@ func Test_expandTagOwners(t *testing.T) { { name: "expand with user and group", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ Groups: Groups{"group:foo": []string{"user1", "user2"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}}, }, @@ -704,7 +1012,7 @@ func Test_expandTagOwners(t *testing.T) { { name: "invalid tag", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}}, }, tag: "tag:test", @@ -716,7 +1024,7 @@ func Test_expandTagOwners(t *testing.T) { { name: "invalid group", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ Groups: Groups{"group:bar": []string{"user1", "user2"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}}, }, @@ -729,7 +1037,7 @@ func Test_expandTagOwners(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := expandTagOwners( + got, err := getTagOwners( test.args.aclPolicy, test.args.tag, test.args.stripEmailDomain, @@ -908,6 +1216,25 @@ func Test_listMachinesInUser(t *testing.T) { } func Test_expandAlias(t *testing.T) { + set := func(ips []string, prefixes []string) *netipx.IPSet { + var builder netipx.IPSetBuilder + + for _, ip := range ips { + builder.Add(netip.MustParseAddr(ip)) + } + + for _, pre := range prefixes { + builder.AddPrefix(netip.MustParsePrefix(pre)) + } + + s, _ := builder.IPSet() + + return s + } + + type field struct { + pol ACLPolicy + } type args struct { machines []Machine aclPolicy ACLPolicy @@ -916,12 +1243,16 @@ func Test_expandAlias(t *testing.T) { } tests := []struct { name string + field field args args - want []string + want *netipx.IPSet wantErr bool }{ { name: "wildcard", + field: field{ + pol: ACLPolicy{}, + }, args: args{ alias: "*", machines: []Machine{ @@ -932,14 +1263,21 @@ func Test_expandAlias(t *testing.T) { }, }, }, - aclPolicy: ACLPolicy{}, stripEmailDomain: true, }, - want: []string{"*"}, + want: set([]string{}, []string{ + "0.0.0.0/0", + "::/0", + }), wantErr: false, }, { name: "simple group", + field: field{ + pol: ACLPolicy{ + Groups: Groups{"group:accountant": []string{"joe", "marc"}}, + }, + }, args: args{ alias: "group:accountant", machines: []Machine{ @@ -968,16 +1306,20 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "mickael"}, }, }, - aclPolicy: ACLPolicy{ - Groups: Groups{"group:accountant": []string{"joe", "marc"}}, - }, stripEmailDomain: true, }, - want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, + want: set([]string{ + "100.64.0.1", "100.64.0.2", "100.64.0.3", + }, []string{}), wantErr: false, }, { name: "wrong group", + field: field{ + pol: ACLPolicy{ + Groups: Groups{"group:accountant": []string{"joe", "marc"}}, + }, + }, args: args{ alias: "group:hr", machines: []Machine{ @@ -1006,38 +1348,46 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "mickael"}, }, }, - aclPolicy: ACLPolicy{ - Groups: Groups{"group:accountant": []string{"joe", "marc"}}, - }, stripEmailDomain: true, }, - want: []string{}, + want: set([]string{}, []string{}), wantErr: true, }, { name: "simple ipaddress", + field: field{ + pol: ACLPolicy{}, + }, args: args{ alias: "10.0.0.3", machines: []Machine{}, - aclPolicy: ACLPolicy{}, stripEmailDomain: true, }, - want: []string{"10.0.0.3"}, + want: set([]string{ + "10.0.0.3", + }, []string{}), wantErr: false, }, { name: "simple host by ip passed through", + field: field{ + pol: ACLPolicy{}, + }, args: args{ alias: "10.0.0.1", machines: []Machine{}, - aclPolicy: ACLPolicy{}, stripEmailDomain: true, }, - want: []string{"10.0.0.1"}, + want: set([]string{ + "10.0.0.1", + }, []string{}), wantErr: false, }, { name: "simple host by ipv4 single ipv4", + field: field{ + pol: ACLPolicy{}, + }, args: args{ alias: "10.0.0.1", machines: []Machine{ @@ -1048,14 +1398,18 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "mickael"}, }, }, - aclPolicy: ACLPolicy{}, stripEmailDomain: true, }, - want: []string{"10.0.0.1"}, + want: set([]string{ + "10.0.0.1", + }, []string{}), wantErr: false, }, { name: "simple host by ipv4 single dual stack", + field: field{ + pol: ACLPolicy{}, + }, args: args{ alias: "10.0.0.1", machines: []Machine{ @@ -1067,14 +1421,18 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "mickael"}, }, }, - aclPolicy: ACLPolicy{}, stripEmailDomain: true, }, - want: []string{"10.0.0.1", "fd7a:115c:a1e0:ab12:4843:2222:6273:2222"}, + want: set([]string{ + "10.0.0.1", "fd7a:115c:a1e0:ab12:4843:2222:6273:2222", + }, []string{}), wantErr: false, }, { name: "simple host by ipv6 single dual stack", + field: field{ + pol: ACLPolicy{}, + }, args: args{ alias: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222", machines: []Machine{ @@ -1086,55 +1444,68 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "mickael"}, }, }, - aclPolicy: ACLPolicy{}, stripEmailDomain: true, }, - want: []string{"fd7a:115c:a1e0:ab12:4843:2222:6273:2222", "10.0.0.1"}, + want: set([]string{ + "fd7a:115c:a1e0:ab12:4843:2222:6273:2222", "10.0.0.1", + }, []string{}), wantErr: false, }, { name: "simple host by hostname alias", - args: args{ - alias: "testy", - machines: []Machine{}, - aclPolicy: ACLPolicy{ + field: field{ + pol: ACLPolicy{ Hosts: Hosts{ "testy": netip.MustParsePrefix("10.0.0.132/32"), }, }, + }, + args: args{ + alias: "testy", + machines: []Machine{}, stripEmailDomain: true, }, - want: []string{"10.0.0.132/32"}, + want: set([]string{}, []string{"10.0.0.132/32"}), wantErr: false, }, { name: "private network", - args: args{ - alias: "homeNetwork", - machines: []Machine{}, - aclPolicy: ACLPolicy{ + field: field{ + pol: ACLPolicy{ Hosts: Hosts{ "homeNetwork": netip.MustParsePrefix("192.168.1.0/24"), }, }, + }, + args: args{ + alias: "homeNetwork", + machines: []Machine{}, stripEmailDomain: true, }, - want: []string{"192.168.1.0/24"}, + want: set([]string{}, []string{"192.168.1.0/24"}), wantErr: false, }, { name: "simple CIDR", + field: field{ + pol: ACLPolicy{}, + }, args: args{ alias: "10.0.0.0/16", machines: []Machine{}, aclPolicy: ACLPolicy{}, stripEmailDomain: true, }, - want: []string{"10.0.0.0/16"}, + want: set([]string{}, []string{"10.0.0.0/16"}), wantErr: false, }, { name: "simple tag", + field: field{ + pol: ACLPolicy{ + TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}}, + }, + }, args: args{ alias: "tag:hr-webserver", machines: []Machine{ @@ -1173,16 +1544,23 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "joe"}, }, }, - aclPolicy: ACLPolicy{ - TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}}, - }, stripEmailDomain: true, }, - want: []string{"100.64.0.1", "100.64.0.2"}, + want: set([]string{ + "100.64.0.1", "100.64.0.2", + }, []string{}), wantErr: false, }, { name: "No tag defined", + field: field{ + pol: ACLPolicy{ + Groups: Groups{"group:accountant": []string{"joe", "marc"}}, + TagOwners: TagOwners{ + "tag:accountant-webserver": []string{"group:accountant"}, + }, + }, + }, args: args{ alias: "tag:hr-webserver", machines: []Machine{ @@ -1211,19 +1589,16 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "mickael"}, }, }, - aclPolicy: ACLPolicy{ - Groups: Groups{"group:accountant": []string{"joe", "marc"}}, - TagOwners: TagOwners{ - "tag:accountant-webserver": []string{"group:accountant"}, - }, - }, stripEmailDomain: true, }, - want: []string{}, + want: set([]string{}, []string{}), wantErr: true, }, { name: "Forced tag defined", + field: field{ + pol: ACLPolicy{}, + }, args: args{ alias: "tag:hr-webserver", machines: []Machine{ @@ -1254,14 +1629,20 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "mickael"}, }, }, - aclPolicy: ACLPolicy{}, stripEmailDomain: true, }, - want: []string{"100.64.0.1", "100.64.0.2"}, + want: set([]string{"100.64.0.1", "100.64.0.2"}, []string{}), wantErr: false, }, { name: "Forced tag with legitimate tagOwner", + field: field{ + pol: ACLPolicy{ + TagOwners: TagOwners{ + "tag:hr-webserver": []string{"joe"}, + }, + }, + }, args: args{ alias: "tag:hr-webserver", machines: []Machine{ @@ -1296,18 +1677,18 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "mickael"}, }, }, - aclPolicy: ACLPolicy{ - TagOwners: TagOwners{ - "tag:hr-webserver": []string{"joe"}, - }, - }, stripEmailDomain: true, }, - want: []string{"100.64.0.1", "100.64.0.2"}, + want: set([]string{"100.64.0.1", "100.64.0.2"}, []string{}), wantErr: false, }, { name: "list host in user without correctly tagged servers", + field: field{ + pol: ACLPolicy{ + TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, + }, + }, args: args{ alias: "joe", machines: []Machine{ @@ -1346,20 +1727,16 @@ func Test_expandAlias(t *testing.T) { User: User{Name: "joe"}, }, }, - aclPolicy: ACLPolicy{ - TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, - }, stripEmailDomain: true, }, - want: []string{"100.64.0.4"}, + want: set([]string{"100.64.0.4"}, []string{}), wantErr: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := expandAlias( + got, err := test.field.pol.expandAlias( test.args.machines, - test.args.aclPolicy, test.args.alias, test.args.stripEmailDomain, ) @@ -1368,7 +1745,7 @@ func Test_expandAlias(t *testing.T) { return } - if !reflect.DeepEqual(got, test.want) { + if diff := cmp.Diff(test.want, got); diff != "" { t.Errorf("expandAlias() = %v, want %v", got, test.want) } }) @@ -1377,7 +1754,7 @@ func Test_expandAlias(t *testing.T) { func Test_excludeCorrectlyTaggedNodes(t *testing.T) { type args struct { - aclPolicy ACLPolicy + aclPolicy *ACLPolicy nodes []Machine user string stripEmailDomain bool @@ -1391,7 +1768,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { { name: "exclude nodes with valid tags", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, }, nodes: []Machine{ @@ -1437,7 +1814,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { { name: "exclude nodes with valid tags, and owner is in a group", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ Groups: Groups{ "group:accountant": []string{"joe", "bar"}, }, @@ -1488,7 +1865,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { { name: "exclude nodes with valid tags and with forced tags", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, }, nodes: []Machine{ @@ -1530,7 +1907,7 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { { name: "all nodes have invalid tags, don't exclude them", args: args{ - aclPolicy: ACLPolicy{ + aclPolicy: &ACLPolicy{ TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, }, nodes: []Machine{ @@ -1613,136 +1990,139 @@ func Test_excludeCorrectlyTaggedNodes(t *testing.T) { } } -func Test_expandACLPeerAddr(t *testing.T) { - type args struct { - srcIP string +func TestACLPolicy_generateFilterRules(t *testing.T) { + type field struct { + pol ACLPolicy } - tests := []struct { - name string - args args - want []string - }{ - { - name: "asterix", - args: args{ - srcIP: "*", - }, - want: []string{"*"}, - }, - { - name: "ip", - args: args{ - srcIP: "10.0.0.1", - }, - want: []string{"10.0.0.1"}, - }, - { - name: "ip/32", - args: args{ - srcIP: "10.0.0.1/32", - }, - want: []string{"10.0.0.1"}, - }, - { - name: "ip/30", - args: args{ - srcIP: "10.0.0.1/30", - }, - want: []string{ - "10.0.0.0", - "10.0.0.1", - "10.0.0.2", - "10.0.0.3", - }, - }, - { - name: "ip/28", - args: args{ - srcIP: "192.168.0.128/28", - }, - want: []string{ - "192.168.0.128", "192.168.0.129", "192.168.0.130", - "192.168.0.131", "192.168.0.132", "192.168.0.133", - "192.168.0.134", "192.168.0.135", "192.168.0.136", - "192.168.0.137", "192.168.0.138", "192.168.0.139", - "192.168.0.140", "192.168.0.141", "192.168.0.142", - "192.168.0.143", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := expandACLPeerAddr(tt.args.srcIP); !reflect.DeepEqual(got, tt.want) { - t.Errorf("expandACLPeerAddr() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_expandACLPeerAddrV6(t *testing.T) { type args struct { - srcIP string + machines []Machine + stripEmailDomain bool } tests := []struct { - name string - args args - want []string + name string + field field + args args + want []tailcfg.FilterRule + wantErr bool }{ { - name: "asterix", - args: args{ - srcIP: "*", - }, - want: []string{"*"}, + name: "no-policy", + field: field{}, + args: args{}, + want: []tailcfg.FilterRule{}, + wantErr: false, }, { - name: "ipfull", - args: args{ - srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c:3166", + name: "allow-all", + field: field{ + pol: ACLPolicy{ + ACLs: []ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + }, }, - want: []string{"fd7a:115c:a1e0:ab12:4943:cd96:624c:3166"}, - }, - { - name: "ipzerocompression", args: args{ - srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c::", + machines: []Machine{}, + stripEmailDomain: true, }, - want: []string{"fd7a:115c:a1e0:ab12:4943:cd96:624c:0"}, - }, - { - name: "ip/128", - args: args{ - srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c:3166/128", + want: []tailcfg.FilterRule{ + { + SrcIPs: []string{"0.0.0.0/0", "::/0"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "0.0.0.0/0", + Ports: tailcfg.PortRange{ + First: 0, + Last: 65535, + }, + }, + { + IP: "::/0", + Ports: tailcfg.PortRange{ + First: 0, + Last: 65535, + }, + }, + }, + }, }, - want: []string{"fd7a:115c:a1e0:ab12:4943:cd96:624c:3166"}, + wantErr: false, }, { - name: "ip/127", - args: args{ - srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c:0000/127", - }, - want: []string{ - "fd7a:115c:a1e0:ab12:4943:cd96:624c:0", - "fd7a:115c:a1e0:ab12:4943:cd96:624c:1", + name: "host1-can-reach-host2", + field: field{ + pol: ACLPolicy{ + ACLs: []ACL{ + { + Action: "accept", + Sources: []string{"100.64.0.1"}, + Destinations: []string{"100.64.0.2:*"}, + }, + }, + }, }, - }, - { - name: "ip/126", args: args{ - srcIP: "fd7a:115c:a1e0:ab12:4943:cd96:624c:0000/126", + machines: []Machine{ + { + IPAddresses: MachineAddresses{ + netip.MustParseAddr("100.64.0.1"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"), + }, + User: User{Name: "mickael"}, + }, + { + IPAddresses: MachineAddresses{ + netip.MustParseAddr("100.64.0.2"), + netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"), + }, + User: User{Name: "mickael"}, + }, + }, + stripEmailDomain: true, }, - want: []string{ - "fd7a:115c:a1e0:ab12:4943:cd96:624c:0", - "fd7a:115c:a1e0:ab12:4943:cd96:624c:1", - "fd7a:115c:a1e0:ab12:4943:cd96:624c:2", - "fd7a:115c:a1e0:ab12:4943:cd96:624c:3", + want: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.0.1/32", "fd7a:115c:a1e0:ab12:4843:2222:6273:2221/128"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "100.64.0.2/32", + Ports: tailcfg.PortRange{ + First: 0, + Last: 65535, + }, + }, + { + IP: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222/128", + Ports: tailcfg.PortRange{ + First: 0, + Last: 65535, + }, + }, + }, + }, }, + wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := expandACLPeerAddr(tt.args.srcIP); !reflect.DeepEqual(got, tt.want) { - t.Errorf("expandACLPeerAddr() = %v, want %v", got, tt.want) + got, err := tt.field.pol.generateFilterRules( + tt.args.machines, + tt.args.stripEmailDomain, + ) + if (err != nil) != tt.wantErr { + t.Errorf("ACLPolicy.generateFilterRules() error = %v, wantErr %v", err, tt.wantErr) + + return + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + log.Trace().Interface("got", got).Msg("result") + t.Errorf("ACLPolicy.generateFilterRules() = %v, want %v", got, tt.want) } }) } diff --git a/acls_types.go b/hscontrol/acls_types.go similarity index 96% rename from acls_types.go rename to hscontrol/acls_types.go index 4f318ddc88..0e55351503 100644 --- a/acls_types.go +++ b/hscontrol/acls_types.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "encoding/json" @@ -111,8 +111,8 @@ func (hosts *Hosts) UnmarshalYAML(data []byte) error { } // IsZero is perhaps a bit naive here. -func (policy ACLPolicy) IsZero() bool { - if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 { +func (pol ACLPolicy) IsZero() bool { + if len(pol.Groups) == 0 && len(pol.Hosts) == 0 && len(pol.ACLs) == 0 { return true } diff --git a/api.go b/hscontrol/api.go similarity index 99% rename from api.go rename to hscontrol/api.go index 308bc28196..f8b1496f64 100644 --- a/api.go +++ b/hscontrol/api.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "bytes" diff --git a/api_common.go b/hscontrol/api_common.go similarity index 99% rename from api_common.go rename to hscontrol/api_common.go index 7905c29dd1..3dd65ac6fa 100644 --- a/api_common.go +++ b/hscontrol/api_common.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "time" diff --git a/api_key.go b/hscontrol/api_key.go similarity index 99% rename from api_key.go rename to hscontrol/api_key.go index ae19eb9ca5..6382a33193 100644 --- a/api_key.go +++ b/hscontrol/api_key.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "fmt" diff --git a/api_key_test.go b/hscontrol/api_key_test.go similarity index 99% rename from api_key_test.go rename to hscontrol/api_key_test.go index 2ddbbbc0c2..fd4fa00db9 100644 --- a/api_key_test.go +++ b/hscontrol/api_key_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "time" diff --git a/app.go b/hscontrol/app.go similarity index 98% rename from app.go rename to hscontrol/app.go index 26a8e23bdd..b8dceba8ae 100644 --- a/app.go +++ b/hscontrol/app.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "context" @@ -21,6 +21,7 @@ import ( "github.com/gorilla/mux" grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/patrickmn/go-cache" zerolog "github.com/philip-bui/grpc-zerolog" @@ -84,11 +85,9 @@ type Headscale struct { DERPMap *tailcfg.DERPMap DERPServer *DERPServer - aclPolicy *ACLPolicy - aclRules []tailcfg.FilterRule - aclPeerCacheMapRW sync.RWMutex - aclPeerCacheMap map[string]map[string]struct{} - sshPolicy *tailcfg.SSHPolicy + aclPolicy *ACLPolicy + aclRules []tailcfg.FilterRule + sshPolicy *tailcfg.SSHPolicy lastStateChange *xsync.MapOf[string, time.Time] @@ -509,8 +508,10 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *mux.Router { router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet) router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig). Methods(http.MethodGet) - router.HandleFunc("/swagger", SwaggerUI).Methods(http.MethodGet) - router.HandleFunc("/swagger/v1/openapiv2.json", SwaggerAPIv1). + + // TODO(kristoffer): move swagger into a package + router.HandleFunc("/swagger", headscale.SwaggerUI).Methods(http.MethodGet) + router.HandleFunc("/swagger/v1/openapiv2.json", headscale.SwaggerAPIv1). Methods(http.MethodGet) if h.cfg.DERP.ServerEnabled { @@ -760,7 +761,7 @@ func (h *Headscale) Serve() error { if h.cfg.ACL.PolicyPath != "" { aclPath := AbsolutePathFromConfigPath(h.cfg.ACL.PolicyPath) - err := h.LoadACLPolicy(aclPath) + err := h.LoadACLPolicyFromPath(aclPath) if err != nil { log.Error().Err(err).Msg("Failed to reload ACL policy") } @@ -820,7 +821,6 @@ func (h *Headscale) Serve() error { // And we're done: cancel() - os.Exit(0) } } } diff --git a/app_test.go b/hscontrol/app_test.go similarity index 97% rename from app_test.go rename to hscontrol/app_test.go index 5f23fd2b02..7d3907d3f8 100644 --- a/app_test.go +++ b/hscontrol/app_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "net/netip" diff --git a/config.go b/hscontrol/config.go similarity index 95% rename from config.go rename to hscontrol/config.go index f0e739e051..5eb55a8d7d 100644 --- a/config.go +++ b/hscontrol/config.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "errors" @@ -16,6 +16,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/viper" "go4.org/netipx" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/dnstype" ) @@ -175,7 +176,7 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("derp.server.enabled", false) viper.SetDefault("derp.server.stun.enabled", true) - viper.SetDefault("unix_socket", "/var/run/headscale.sock") + viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock") viper.SetDefault("unix_socket_permission", "0o770") viper.SetDefault("grpc_listen_addr", ":50443") @@ -517,6 +518,29 @@ func GetHeadscaleConfig() (*Config, error) { if err != nil { panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err)) } + + if prefix.Addr().Is4() { + builder := netipx.IPSetBuilder{} + builder.AddPrefix(tsaddr.CGNATRange()) + ipSet, _ := builder.IPSet() + if !ipSet.ContainsPrefix(prefix) { + log.Warn(). + Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.", + prefixInConfig, tsaddr.CGNATRange()) + } + } + + if prefix.Addr().Is6() { + builder := netipx.IPSetBuilder{} + builder.AddPrefix(tsaddr.TailscaleULARange()) + ipSet, _ := builder.IPSet() + if !ipSet.ContainsPrefix(prefix) { + log.Warn(). + Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.", + prefixInConfig, tsaddr.TailscaleULARange()) + } + } + parsedPrefixes = append(parsedPrefixes, prefix) } diff --git a/db.go b/hscontrol/db.go similarity index 99% rename from db.go rename to hscontrol/db.go index fb5c5c35b5..14df4b3bf1 100644 --- a/db.go +++ b/hscontrol/db.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "context" diff --git a/derp.go b/hscontrol/derp.go similarity index 99% rename from derp.go rename to hscontrol/derp.go index a447da78e5..fbc366a983 100644 --- a/derp.go +++ b/hscontrol/derp.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "context" diff --git a/derp_server.go b/hscontrol/derp_server.go similarity index 99% rename from derp_server.go rename to hscontrol/derp_server.go index c4e0d65f28..9ca6eee5ef 100644 --- a/derp_server.go +++ b/hscontrol/derp_server.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "context" diff --git a/dns.go b/hscontrol/dns.go similarity index 99% rename from dns.go rename to hscontrol/dns.go index da1a95d0f7..72c5b03c0b 100644 --- a/dns.go +++ b/hscontrol/dns.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "fmt" diff --git a/dns_test.go b/hscontrol/dns_test.go similarity index 99% rename from dns_test.go rename to hscontrol/dns_test.go index eb96bbe996..b825721913 100644 --- a/dns_test.go +++ b/hscontrol/dns_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "fmt" diff --git a/grpcv1.go b/hscontrol/grpcv1.go similarity index 99% rename from grpcv1.go rename to hscontrol/grpcv1.go index 2be047bb20..a65a380503 100644 --- a/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -1,5 +1,5 @@ // nolint -package headscale +package hscontrol import ( "context" diff --git a/grpcv1_test.go b/hscontrol/grpcv1_test.go similarity index 97% rename from grpcv1_test.go rename to hscontrol/grpcv1_test.go index e48ae1efa8..1d87bfe052 100644 --- a/grpcv1_test.go +++ b/hscontrol/grpcv1_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import "testing" diff --git a/handler_legacy.go b/hscontrol/handler_legacy.go similarity index 94% rename from handler_legacy.go rename to hscontrol/handler_legacy.go index 8911d430c7..bb94b1e584 100644 --- a/handler_legacy.go +++ b/hscontrol/handler_legacy.go @@ -1,6 +1,6 @@ //go:build ts2019 -package headscale +package hscontrol import ( "net/http" diff --git a/handler_placeholder.go b/hscontrol/handler_placeholder.go similarity index 86% rename from handler_placeholder.go rename to hscontrol/handler_placeholder.go index 25fe9c6530..73d17c4988 100644 --- a/handler_placeholder.go +++ b/hscontrol/handler_placeholder.go @@ -1,6 +1,6 @@ //go:build !ts2019 -package headscale +package hscontrol import "github.com/gorilla/mux" diff --git a/machine.go b/hscontrol/machine.go similarity index 91% rename from machine.go rename to hscontrol/machine.go index 6dfa9501af..9f04d8ce30 100644 --- a/machine.go +++ b/hscontrol/machine.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "database/sql/driver" @@ -8,12 +8,12 @@ import ( "sort" "strconv" "strings" - "sync" "time" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/rs/zerolog/log" "github.com/samber/lo" + "go4.org/netipx" "google.golang.org/protobuf/types/known/timestamppb" "gorm.io/gorm" "tailscale.com/tailcfg" @@ -98,6 +98,14 @@ func (ma MachineAddresses) ToStringSlice() []string { return strSlice } +// AppendToIPSet adds the individual ips in MachineAddresses to a +// given netipx.IPSetBuilder. +func (ma MachineAddresses) AppendToIPSet(build *netipx.IPSetBuilder) { + for _, ip := range ma { + build.Add(ip) + } +} + func (ma *MachineAddresses) Scan(destination interface{}) error { switch value := destination.(type) { case string: @@ -161,125 +169,48 @@ func (machine *Machine) isEphemeral() bool { return machine.AuthKey != nil && machine.AuthKey.Ephemeral } +func (machine *Machine) canAccess(filter []tailcfg.FilterRule, machine2 *Machine) bool { + for _, rule := range filter { + // TODO(kradalby): Cache or pregen this + matcher := MatchFromFilterRule(rule) + + if !matcher.SrcsContainsIPs([]netip.Addr(machine.IPAddresses)) { + continue + } + + if matcher.DestsContainsIP([]netip.Addr(machine2.IPAddresses)) { + return true + } + } + + return false +} + // filterMachinesByACL wrapper function to not have devs pass around locks and maps // related to the application outside of tests. func (h *Headscale) filterMachinesByACL(currentMachine *Machine, peers Machines) Machines { - return filterMachinesByACL(currentMachine, peers, &h.aclPeerCacheMapRW, h.aclPeerCacheMap) + return filterMachinesByACL(currentMachine, peers, h.aclRules) } // filterMachinesByACL returns the list of peers authorized to be accessed from a given machine. func filterMachinesByACL( machine *Machine, machines Machines, - lock *sync.RWMutex, - aclPeerCacheMap map[string]map[string]struct{}, + filter []tailcfg.FilterRule, ) Machines { - log.Trace(). - Caller(). - Str("self", machine.Hostname). - Str("input", machines.String()). - Msg("Finding peers filtered by ACLs") - - peers := make(map[uint64]Machine) - // Aclfilter peers here. We are itering through machines in all users and search through the computed aclRules - // for match between rule SrcIPs and DstPorts. If the rule is a match we allow the machine to be viewable. - machineIPs := machine.IPAddresses.ToStringSlice() + result := Machines{} - // TODO(kradalby): Remove this lock, I suspect its not a good idea, and might not be necessary, - // we only set this at startup atm (reading ACLs) and it might become a bottleneck. - lock.RLock() - - for _, peer := range machines { + for index, peer := range machines { if peer.ID == machine.ID { continue } - peerIPs := peer.IPAddresses.ToStringSlice() - - if dstMap, ok := aclPeerCacheMap["*"]; ok { - // match source and all destination - if _, dstOk := dstMap["*"]; dstOk { - peers[peer.ID] = peer - - continue - } - - // match source and all destination - for _, peerIP := range peerIPs { - if _, dstOk := dstMap[peerIP]; dstOk { - peers[peer.ID] = peer - - continue - } - } - // match all sources and source - for _, machineIP := range machineIPs { - if _, dstOk := dstMap[machineIP]; dstOk { - peers[peer.ID] = peer - - continue - } - } - } - - for _, machineIP := range machineIPs { - if dstMap, ok := aclPeerCacheMap[machineIP]; ok { - // match source and all destination - if _, dstOk := dstMap["*"]; dstOk { - peers[peer.ID] = peer - - continue - } - - // match source and destination - for _, peerIP := range peerIPs { - if _, dstOk := dstMap[peerIP]; dstOk { - peers[peer.ID] = peer - - continue - } - } - } - } - - for _, peerIP := range peerIPs { - if dstMap, ok := aclPeerCacheMap[peerIP]; ok { - // match source and all destination - if _, dstOk := dstMap["*"]; dstOk { - peers[peer.ID] = peer - - continue - } - // match return path - for _, machineIP := range machineIPs { - if _, dstOk := dstMap[machineIP]; dstOk { - peers[peer.ID] = peer - - continue - } - } - } + if machine.canAccess(filter, &machines[index]) || peer.canAccess(filter, machine) { + result = append(result, peer) } } - lock.RUnlock() - - authorizedPeers := make(Machines, 0, len(peers)) - for _, m := range peers { - authorizedPeers = append(authorizedPeers, m) - } - sort.Slice( - authorizedPeers, - func(i, j int) bool { return authorizedPeers[i].ID < authorizedPeers[j].ID }, - ) - - log.Trace(). - Caller(). - Str("self", machine.Hostname). - Str("peers", authorizedPeers.String()). - Msg("Authorized peers") - - return authorizedPeers + return result } func (h *Headscale) ListPeers(machine *Machine) (Machines, error) { @@ -868,7 +799,7 @@ func getTags( validTagMap := make(map[string]bool) invalidTagMap := make(map[string]bool) for _, tag := range machine.HostInfo.RequestTags { - owners, err := expandTagOwners(*aclPolicy, tag, stripEmailDomain) + owners, err := getTagOwners(aclPolicy, tag, stripEmailDomain) if errors.Is(err, errInvalidTag) { invalidTagMap[tag] = true @@ -1182,7 +1113,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { if approvedAlias == machine.User.Name { approvedRoutes = append(approvedRoutes, advertisedRoute) } else { - approvedIps, err := expandAlias([]Machine{*machine}, *h.aclPolicy, approvedAlias, h.cfg.OIDC.StripEmaildomain) + approvedIps, err := h.aclPolicy.expandAlias([]Machine{*machine}, approvedAlias, h.cfg.OIDC.StripEmaildomain) if err != nil { log.Err(err). Str("alias", approvedAlias). @@ -1192,7 +1123,7 @@ func (h *Headscale) EnableAutoApprovedRoutes(machine *Machine) error { } // approvedIPs should contain all of machine's IPs if it matches the rule, so check for first - if contains(approvedIps, machine.IPAddresses[0].String()) { + if approvedIps.Contains(machine.IPAddresses[0]) { approvedRoutes = append(approvedRoutes, advertisedRoute) } } diff --git a/machine_test.go b/hscontrol/machine_test.go similarity index 91% rename from machine_test.go rename to hscontrol/machine_test.go index c25f32dac9..3f11da4b2b 100644 --- a/machine_test.go +++ b/hscontrol/machine_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "fmt" @@ -6,7 +6,6 @@ import ( "reflect" "regexp" "strconv" - "sync" "testing" "time" @@ -1041,16 +1040,12 @@ func Test_getFilteredByACLPeers(t *testing.T) { }, }, } - var lock sync.RWMutex for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - aclRulesMap := generateACLPeerCacheMap(tt.args.rules) - got := filterMachinesByACL( tt.args.machine, tt.args.machines, - &lock, - aclRulesMap, + tt.args.rules, ) if !reflect.DeepEqual(got, tt.want) { t.Errorf("filterMachinesByACL() = %v, want %v", got, tt.want) @@ -1217,7 +1212,31 @@ func TestHeadscale_generateGivenName(t *testing.T) { } func (s *Suite) TestAutoApproveRoutes(c *check.C) { - err := app.LoadACLPolicy("./tests/acls/acl_policy_autoapprovers.hujson") + acl := []byte(` +{ + "tagOwners": { + "tag:exit": ["test"], + }, + + "groups": { + "group:test": ["test"] + }, + + "acls": [ + {"action": "accept", "users": ["*"], "ports": ["*:*"]}, + ], + + "autoApprovers": { + "exitNode": ["tag:exit"], + "routes": { + "10.10.0.0/16": ["group:test"], + "10.11.0.0/16": ["test"], + } + } +} + `) + + err := app.LoadACLPolicyFromBytes(acl, "hujson") c.Assert(err, check.IsNil) user, err := app.CreateUser("test") @@ -1264,3 +1283,131 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { c.Assert(err, check.IsNil) c.Assert(enabledRoutes, check.HasLen, 3) } + +func TestMachine_canAccess(t *testing.T) { + type args struct { + filter []tailcfg.FilterRule + machine2 *Machine + } + tests := []struct { + name string + machine Machine + args args + want bool + }{ + { + name: "no-rules", + machine: Machine{ + IPAddresses: MachineAddresses{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + args: args{ + filter: []tailcfg.FilterRule{}, + machine2: &Machine{ + IPAddresses: MachineAddresses{ + netip.MustParseAddr("10.0.0.2"), + }, + }, + }, + want: false, + }, + { + name: "wildcard", + machine: Machine{ + IPAddresses: MachineAddresses{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + args: args{ + filter: []tailcfg.FilterRule{ + { + SrcIPs: []string{"*"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "*", + Ports: tailcfg.PortRange{ + First: 0, + Last: 65535, + }, + }, + }, + }, + }, + machine2: &Machine{ + IPAddresses: MachineAddresses{ + netip.MustParseAddr("10.0.0.2"), + }, + }, + }, + want: true, + }, + { + name: "explicit-m1-to-m2", + machine: Machine{ + IPAddresses: MachineAddresses{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + args: args{ + filter: []tailcfg.FilterRule{ + { + SrcIPs: []string{"10.0.0.1"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "10.0.0.2", + Ports: tailcfg.PortRange{ + First: 0, + Last: 65535, + }, + }, + }, + }, + }, + machine2: &Machine{ + IPAddresses: MachineAddresses{ + netip.MustParseAddr("10.0.0.2"), + }, + }, + }, + want: true, + }, + { + name: "explicit-m2-to-m1", + machine: Machine{ + IPAddresses: MachineAddresses{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + args: args{ + filter: []tailcfg.FilterRule{ + { + SrcIPs: []string{"10.0.0.2"}, + DstPorts: []tailcfg.NetPortRange{ + { + IP: "10.0.0.1", + Ports: tailcfg.PortRange{ + First: 0, + Last: 65535, + }, + }, + }, + }, + }, + machine2: &Machine{ + IPAddresses: MachineAddresses{ + netip.MustParseAddr("10.0.0.2"), + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.machine.canAccess(tt.args.filter, tt.args.machine2); got != tt.want { + t.Errorf("Machine.canAccess() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/hscontrol/matcher.go b/hscontrol/matcher.go new file mode 100644 index 0000000000..3b4670e8d4 --- /dev/null +++ b/hscontrol/matcher.go @@ -0,0 +1,142 @@ +package hscontrol + +import ( + "fmt" + "net/netip" + "strings" + + "go4.org/netipx" + "tailscale.com/tailcfg" +) + +// This is borrowed from, and updated to use IPSet +// https://github.com/tailscale/tailscale/blob/71029cea2ddf82007b80f465b256d027eab0f02d/wgengine/filter/tailcfg.go#L97-L162 +// TODO(kradalby): contribute upstream and make public. +var ( + zeroIP4 = netip.AddrFrom4([4]byte{}) + zeroIP6 = netip.AddrFrom16([16]byte{}) +) + +// parseIPSet parses arg as one: +// +// - an IP address (IPv4 or IPv6) +// - the string "*" to match everything (both IPv4 & IPv6) +// - a CIDR (e.g. "192.168.0.0/16") +// - a range of two IPs, inclusive, separated by hyphen ("2eff::1-2eff::0800") +// +// bits, if non-nil, is the legacy SrcBits CIDR length to make a IP +// address (without a slash) treated as a CIDR of *bits length. +// nolint +func parseIPSet(arg string, bits *int) (*netipx.IPSet, error) { + var ipSet netipx.IPSetBuilder + if arg == "*" { + ipSet.AddPrefix(netip.PrefixFrom(zeroIP4, 0)) + ipSet.AddPrefix(netip.PrefixFrom(zeroIP6, 0)) + + return ipSet.IPSet() + } + if strings.Contains(arg, "/") { + pfx, err := netip.ParsePrefix(arg) + if err != nil { + return nil, err + } + if pfx != pfx.Masked() { + return nil, fmt.Errorf("%v contains non-network bits set", pfx) + } + + ipSet.AddPrefix(pfx) + + return ipSet.IPSet() + } + if strings.Count(arg, "-") == 1 { + ip1s, ip2s, _ := strings.Cut(arg, "-") + + ip1, err := netip.ParseAddr(ip1s) + if err != nil { + return nil, err + } + + ip2, err := netip.ParseAddr(ip2s) + if err != nil { + return nil, err + } + + r := netipx.IPRangeFrom(ip1, ip2) + if !r.IsValid() { + return nil, fmt.Errorf("invalid IP range %q", arg) + } + + for _, prefix := range r.Prefixes() { + ipSet.AddPrefix(prefix) + } + + return ipSet.IPSet() + } + ip, err := netip.ParseAddr(arg) + if err != nil { + return nil, fmt.Errorf("invalid IP address %q", arg) + } + bits8 := uint8(ip.BitLen()) + if bits != nil { + if *bits < 0 || *bits > int(bits8) { + return nil, fmt.Errorf("invalid CIDR size %d for IP %q", *bits, arg) + } + bits8 = uint8(*bits) + } + + ipSet.AddPrefix(netip.PrefixFrom(ip, int(bits8))) + + return ipSet.IPSet() +} + +type Match struct { + Srcs *netipx.IPSet + Dests *netipx.IPSet +} + +func MatchFromFilterRule(rule tailcfg.FilterRule) Match { + srcs := new(netipx.IPSetBuilder) + dests := new(netipx.IPSetBuilder) + + for _, srcIP := range rule.SrcIPs { + set, _ := parseIPSet(srcIP, nil) + + srcs.AddSet(set) + } + + for _, dest := range rule.DstPorts { + set, _ := parseIPSet(dest.IP, nil) + + dests.AddSet(set) + } + + srcsSet, _ := srcs.IPSet() + destsSet, _ := dests.IPSet() + + match := Match{ + Srcs: srcsSet, + Dests: destsSet, + } + + return match +} + +func (m *Match) SrcsContainsIPs(ips []netip.Addr) bool { + for _, ip := range ips { + if m.Srcs.Contains(ip) { + return true + } + } + + return false +} + +func (m *Match) DestsContainsIP(ips []netip.Addr) bool { + for _, ip := range ips { + if m.Dests.Contains(ip) { + return true + } + } + + return false +} diff --git a/hscontrol/matcher_test.go b/hscontrol/matcher_test.go new file mode 100644 index 0000000000..fb0e9b076c --- /dev/null +++ b/hscontrol/matcher_test.go @@ -0,0 +1,119 @@ +package hscontrol + +import ( + "net/netip" + "reflect" + "testing" + + "go4.org/netipx" +) + +func Test_parseIPSet(t *testing.T) { + set := func(ips []string, prefixes []string) *netipx.IPSet { + var builder netipx.IPSetBuilder + + for _, ip := range ips { + builder.Add(netip.MustParseAddr(ip)) + } + + for _, pre := range prefixes { + builder.AddPrefix(netip.MustParsePrefix(pre)) + } + + s, _ := builder.IPSet() + + return s + } + + type args struct { + arg string + bits *int + } + tests := []struct { + name string + args args + want *netipx.IPSet + wantErr bool + }{ + { + name: "simple ip4", + args: args{ + arg: "10.0.0.1", + bits: nil, + }, + want: set([]string{ + "10.0.0.1", + }, []string{}), + wantErr: false, + }, + { + name: "simple ip6", + args: args{ + arg: "2001:db8:abcd:1234::2", + bits: nil, + }, + want: set([]string{ + "2001:db8:abcd:1234::2", + }, []string{}), + wantErr: false, + }, + { + name: "wildcard", + args: args{ + arg: "*", + bits: nil, + }, + want: set([]string{}, []string{ + "0.0.0.0/0", + "::/0", + }), + wantErr: false, + }, + { + name: "prefix4", + args: args{ + arg: "192.168.0.0/16", + bits: nil, + }, + want: set([]string{}, []string{ + "192.168.0.0/16", + }), + wantErr: false, + }, + { + name: "prefix6", + args: args{ + arg: "2001:db8:abcd:1234::/64", + bits: nil, + }, + want: set([]string{}, []string{ + "2001:db8:abcd:1234::/64", + }), + wantErr: false, + }, + { + name: "range4", + args: args{ + arg: "192.168.0.0-192.168.255.255", + bits: nil, + }, + want: set([]string{}, []string{ + "192.168.0.0/16", + }), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseIPSet(tt.args.arg, tt.args.bits) + if (err != nil) != tt.wantErr { + t.Errorf("parseIPSet() error = %v, wantErr %v", err, tt.wantErr) + + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseIPSet() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/metrics.go b/hscontrol/metrics.go similarity index 98% rename from metrics.go rename to hscontrol/metrics.go index f1f86909ae..087ce3028b 100644 --- a/metrics.go +++ b/hscontrol/metrics.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "github.com/prometheus/client_golang/prometheus" diff --git a/hscontrol/noise.go b/hscontrol/noise.go new file mode 100644 index 0000000000..f938dfe5df --- /dev/null +++ b/hscontrol/noise.go @@ -0,0 +1,164 @@ +package hscontrol + +import ( + "encoding/binary" + "encoding/json" + "io" + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "tailscale.com/control/controlbase" + "tailscale.com/control/controlhttp" + "tailscale.com/tailcfg" + "tailscale.com/types/key" +) + +const ( + // ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade. + ts2021UpgradePath = "/ts2021" + + // The first 9 bytes from the server to client over Noise are either an HTTP/2 + // settings frame (a normal HTTP/2 setup) or, as Tailscale added later, an "early payload" + // header that's also 9 bytes long: 5 bytes (earlyPayloadMagic) followed by 4 bytes + // of length. Then that many bytes of JSON-encoded tailcfg.EarlyNoise. + // The early payload is optional. Some servers may not send it... But we do! + earlyPayloadMagic = "\xff\xff\xffTS" + + // EarlyNoise was added in protocol version 49. + earlyNoiseCapabilityVersion = 49 +) + +type noiseServer struct { + headscale *Headscale + + httpBaseConfig *http.Server + http2Server *http2.Server + conn *controlbase.Conn + machineKey key.MachinePublic + nodeKey key.NodePublic + + // EarlyNoise-related stuff + challenge key.ChallengePrivate + protocolVersion int +} + +// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn +// in order to use the Noise-based TS2021 protocol. Listens in /ts2021. +func (h *Headscale) NoiseUpgradeHandler( + writer http.ResponseWriter, + req *http.Request, +) { + log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr) + + upgrade := req.Header.Get("Upgrade") + if upgrade == "" { + // This probably means that the user is running Headscale behind an + // improperly configured reverse proxy. TS2021 requires WebSockets to + // be passed to Headscale. Let's give them a hint. + log.Warn(). + Caller(). + Msg("No Upgrade header in TS2021 request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through.") + http.Error(writer, "Internal error", http.StatusInternalServerError) + + return + } + + noiseServer := noiseServer{ + headscale: h, + challenge: key.NewChallenge(), + } + + noiseConn, err := controlhttp.AcceptHTTP( + req.Context(), + writer, + req, + *h.noisePrivateKey, + noiseServer.earlyNoise, + ) + if err != nil { + log.Error().Err(err).Msg("noise upgrade failed") + http.Error(writer, err.Error(), http.StatusInternalServerError) + + return + } + + noiseServer.conn = noiseConn + noiseServer.machineKey = noiseServer.conn.Peer() + noiseServer.protocolVersion = noiseServer.conn.ProtocolVersion() + + // This router is served only over the Noise connection, and exposes only the new API. + // + // The HTTP2 server that exposes this router is created for + // a single hijacked connection from /ts2021, using netutil.NewOneConnListener + router := mux.NewRouter() + + router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler). + Methods(http.MethodPost) + router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler) + + server := http.Server{ + ReadTimeout: HTTPReadTimeout, + } + + noiseServer.httpBaseConfig = &http.Server{ + Handler: router, + ReadHeaderTimeout: HTTPReadTimeout, + } + noiseServer.http2Server = &http2.Server{} + + server.Handler = h2c.NewHandler(router, noiseServer.http2Server) + + noiseServer.http2Server.ServeConn( + noiseConn, + &http2.ServeConnOpts{ + BaseConfig: noiseServer.httpBaseConfig, + }, + ) +} + +func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error { + log.Trace(). + Caller(). + Int("protocol_version", protocolVersion). + Str("challenge", ns.challenge.Public().String()). + Msg("earlyNoise called") + + if protocolVersion < earlyNoiseCapabilityVersion { + log.Trace(). + Caller(). + Msgf("protocol version %d does not support early noise", protocolVersion) + + return nil + } + + earlyJSON, err := json.Marshal(&tailcfg.EarlyNoise{ + NodeKeyChallenge: ns.challenge.Public(), + }) + if err != nil { + return err + } + + // 5 bytes that won't be mistaken for an HTTP/2 frame: + // https://httpwg.org/specs/rfc7540.html#rfc.section.4.1 (Especially not + // an HTTP/2 settings frame, which isn't of type 'T') + var notH2Frame [5]byte + copy(notH2Frame[:], earlyPayloadMagic) + var lenBuf [4]byte + binary.BigEndian.PutUint32(lenBuf[:], uint32(len(earlyJSON))) + // These writes are all buffered by caller, so fine to do them + // separately: + if _, err := writer.Write(notH2Frame[:]); err != nil { + return err + } + if _, err := writer.Write(lenBuf[:]); err != nil { + return err + } + if _, err := writer.Write(earlyJSON); err != nil { + return err + } + + return nil +} diff --git a/oidc.go b/hscontrol/oidc.go similarity index 99% rename from oidc.go rename to hscontrol/oidc.go index 53063a2cc0..634eda2dca 100644 --- a/oidc.go +++ b/hscontrol/oidc.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "bytes" diff --git a/platform_config.go b/hscontrol/platform_config.go similarity index 99% rename from platform_config.go rename to hscontrol/platform_config.go index b23133e759..0404f5465e 100644 --- a/platform_config.go +++ b/hscontrol/platform_config.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "bytes" diff --git a/preauth_keys.go b/hscontrol/preauth_keys.go similarity index 99% rename from preauth_keys.go rename to hscontrol/preauth_keys.go index 3222c71d1b..6cff90b001 100644 --- a/preauth_keys.go +++ b/hscontrol/preauth_keys.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "crypto/rand" diff --git a/preauth_keys_test.go b/hscontrol/preauth_keys_test.go similarity index 99% rename from preauth_keys_test.go rename to hscontrol/preauth_keys_test.go index 697144fe0c..bd383cfd25 100644 --- a/preauth_keys_test.go +++ b/hscontrol/preauth_keys_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "time" diff --git a/protocol_common.go b/hscontrol/protocol_common.go similarity index 99% rename from protocol_common.go rename to hscontrol/protocol_common.go index 2a30046f8e..97da464bb9 100644 --- a/protocol_common.go +++ b/hscontrol/protocol_common.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "encoding/json" diff --git a/protocol_common_poll.go b/hscontrol/protocol_common_poll.go similarity index 99% rename from protocol_common_poll.go rename to hscontrol/protocol_common_poll.go index 09df63094b..f267c9999a 100644 --- a/protocol_common_poll.go +++ b/hscontrol/protocol_common_poll.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "context" diff --git a/protocol_common_utils.go b/hscontrol/protocol_common_utils.go similarity index 99% rename from protocol_common_utils.go rename to hscontrol/protocol_common_utils.go index 96c236d09d..e05b04a24a 100644 --- a/protocol_common_utils.go +++ b/hscontrol/protocol_common_utils.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "encoding/binary" diff --git a/protocol_legacy.go b/hscontrol/protocol_legacy.go similarity index 98% rename from protocol_legacy.go rename to hscontrol/protocol_legacy.go index 99f68e5e34..6712828631 100644 --- a/protocol_legacy.go +++ b/hscontrol/protocol_legacy.go @@ -1,6 +1,6 @@ //go:build ts2019 -package headscale +package hscontrol import ( "io" diff --git a/protocol_legacy_poll.go b/hscontrol/protocol_legacy_poll.go similarity index 99% rename from protocol_legacy_poll.go rename to hscontrol/protocol_legacy_poll.go index a8d9343a65..0121bf3f1d 100644 --- a/protocol_legacy_poll.go +++ b/hscontrol/protocol_legacy_poll.go @@ -1,6 +1,6 @@ //go:build ts2019 -package headscale +package hscontrol import ( "errors" diff --git a/protocol_noise.go b/hscontrol/protocol_noise.go similarity index 76% rename from protocol_noise.go rename to hscontrol/protocol_noise.go index 1d1b9c8a91..dfbad73b74 100644 --- a/protocol_noise.go +++ b/hscontrol/protocol_noise.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "encoding/json" @@ -10,7 +10,7 @@ import ( ) // // NoiseRegistrationHandler handles the actual registration process of a machine. -func (t *ts2021App) NoiseRegistrationHandler( +func (ns *noiseServer) NoiseRegistrationHandler( writer http.ResponseWriter, req *http.Request, ) { @@ -20,6 +20,11 @@ func (t *ts2021App) NoiseRegistrationHandler( return } + + log.Trace(). + Any("headers", req.Header). + Msg("Headers") + body, _ := io.ReadAll(req.Body) registerRequest := tailcfg.RegisterRequest{} if err := json.Unmarshal(body, ®isterRequest); err != nil { @@ -33,5 +38,7 @@ func (t *ts2021App) NoiseRegistrationHandler( return } - t.headscale.handleRegisterCommon(writer, req, registerRequest, t.conn.Peer(), true) + ns.nodeKey = registerRequest.NodeKey + + ns.headscale.handleRegisterCommon(writer, req, registerRequest, ns.conn.Peer(), true) } diff --git a/protocol_noise_poll.go b/hscontrol/protocol_noise_poll.go similarity index 83% rename from protocol_noise_poll.go rename to hscontrol/protocol_noise_poll.go index 2c29869323..38f2b1c7e9 100644 --- a/protocol_noise_poll.go +++ b/hscontrol/protocol_noise_poll.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "encoding/json" @@ -21,13 +21,18 @@ import ( // only after their first request (marked with the ReadOnly field). // // At this moment the updates are sent in a quite horrendous way, but they kinda work. -func (t *ts2021App) NoisePollNetMapHandler( +func (ns *noiseServer) NoisePollNetMapHandler( writer http.ResponseWriter, req *http.Request, ) { log.Trace(). Str("handler", "NoisePollNetMap"). Msg("PollNetMapHandler called") + + log.Trace(). + Any("headers", req.Header). + Msg("Headers") + body, _ := io.ReadAll(req.Body) mapRequest := tailcfg.MapRequest{} @@ -41,7 +46,9 @@ func (t *ts2021App) NoisePollNetMapHandler( return } - machine, err := t.headscale.GetMachineByAnyKey(t.conn.Peer(), mapRequest.NodeKey, key.NodePublic{}) + ns.nodeKey = mapRequest.NodeKey + + machine, err := ns.headscale.GetMachineByAnyKey(ns.conn.Peer(), mapRequest.NodeKey, key.NodePublic{}) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { log.Warn(). @@ -63,5 +70,5 @@ func (t *ts2021App) NoisePollNetMapHandler( Str("machine", machine.Hostname). Msg("A machine is entering polling via the Noise protocol") - t.headscale.handlePollCommon(writer, req.Context(), machine, mapRequest, true) + ns.headscale.handlePollCommon(writer, req.Context(), machine, mapRequest, true) } diff --git a/routes.go b/hscontrol/routes.go similarity index 87% rename from routes.go rename to hscontrol/routes.go index bab35ea897..89f9a6941b 100644 --- a/routes.go +++ b/hscontrol/routes.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "errors" @@ -106,13 +106,36 @@ func (h *Headscale) DisableRoute(id uint64) error { return err } - route.Enabled = false - route.IsPrimary = false - err = h.db.Save(route).Error + // Tailscale requires both IPv4 and IPv6 exit routes to + // be enabled at the same time, as per + // https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002 + if !route.isExitRoute() { + route.Enabled = false + route.IsPrimary = false + err = h.db.Save(route).Error + if err != nil { + return err + } + + return h.handlePrimarySubnetFailover() + } + + routes, err := h.GetMachineRoutes(&route.Machine) if err != nil { return err } + for i := range routes { + if routes[i].isExitRoute() { + routes[i].Enabled = false + routes[i].IsPrimary = false + err = h.db.Save(&routes[i]).Error + if err != nil { + return err + } + } + } + return h.handlePrimarySubnetFailover() } @@ -122,7 +145,30 @@ func (h *Headscale) DeleteRoute(id uint64) error { return err } - if err := h.db.Unscoped().Delete(&route).Error; err != nil { + // Tailscale requires both IPv4 and IPv6 exit routes to + // be enabled at the same time, as per + // https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002 + if !route.isExitRoute() { + if err := h.db.Unscoped().Delete(&route).Error; err != nil { + return err + } + + return h.handlePrimarySubnetFailover() + } + + routes, err := h.GetMachineRoutes(&route.Machine) + if err != nil { + return err + } + + routesToDelete := []Route{} + for _, r := range routes { + if r.isExitRoute() { + routesToDelete = append(routesToDelete, r) + } + } + + if err := h.db.Unscoped().Delete(&routesToDelete).Error; err != nil { return err } diff --git a/routes_test.go b/hscontrol/routes_test.go similarity index 93% rename from routes_test.go rename to hscontrol/routes_test.go index b67b3ee937..1e5e2bbf7b 100644 --- a/routes_test.go +++ b/hscontrol/routes_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "net/netip" @@ -457,6 +457,37 @@ func (s *Suite) TestAllowedIPRoutes(c *check.C) { c.Assert(foundExitNodeV4, check.Equals, true) c.Assert(foundExitNodeV6, check.Equals, true) + + // Now we disable only one of the exit routes + // and we see if both are disabled + var exitRouteV4 Route + for _, route := range routes { + if route.isExitRoute() && netip.Prefix(route.Prefix) == prefixExitNodeV4 { + exitRouteV4 = route + + break + } + } + + err = app.DisableRoute(uint64(exitRouteV4.ID)) + c.Assert(err, check.IsNil) + + enabledRoutes1, err = app.GetEnabledRoutes(&machine1) + c.Assert(err, check.IsNil) + c.Assert(len(enabledRoutes1), check.Equals, 1) + + // and now we delete only one of the exit routes + // and we check if both are deleted + routes, err = app.GetMachineRoutes(&machine1) + c.Assert(err, check.IsNil) + c.Assert(len(routes), check.Equals, 4) + + err = app.DeleteRoute(uint64(exitRouteV4.ID)) + c.Assert(err, check.IsNil) + + routes, err = app.GetMachineRoutes(&machine1) + c.Assert(err, check.IsNil) + c.Assert(len(routes), check.Equals, 2) } func (s *Suite) TestDeleteRoutes(c *check.C) { diff --git a/templates/apple.html b/hscontrol/templates/apple.html similarity index 100% rename from templates/apple.html rename to hscontrol/templates/apple.html diff --git a/templates/windows.html b/hscontrol/templates/windows.html similarity index 100% rename from templates/windows.html rename to hscontrol/templates/windows.html diff --git a/users.go b/hscontrol/users.go similarity index 99% rename from users.go rename to hscontrol/users.go index c32213aa52..8782a8908b 100644 --- a/users.go +++ b/hscontrol/users.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "errors" diff --git a/users_test.go b/hscontrol/users_test.go similarity index 99% rename from users_test.go rename to hscontrol/users_test.go index 144c333770..12aa9880d4 100644 --- a/users_test.go +++ b/hscontrol/users_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "net/netip" diff --git a/utils.go b/hscontrol/utils.go similarity index 99% rename from utils.go rename to hscontrol/utils.go index 8bdb2b3f0f..9cfbf0cab9 100644 --- a/utils.go +++ b/hscontrol/utils.go @@ -3,7 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package headscale +package hscontrol import ( "context" diff --git a/utils_test.go b/hscontrol/utils_test.go similarity index 99% rename from utils_test.go rename to hscontrol/utils_test.go index fd1d25e7e0..436df8ac88 100644 --- a/utils_test.go +++ b/hscontrol/utils_test.go @@ -1,4 +1,4 @@ -package headscale +package hscontrol import ( "net/netip" diff --git a/integration/acl_test.go b/integration/acl_test.go index f8aeceb13c..e85e28cd4a 100644 --- a/integration/acl_test.go +++ b/integration/acl_test.go @@ -6,13 +6,46 @@ import ( "strings" "testing" - "github.com/juanfont/headscale" + "github.com/juanfont/headscale/hscontrol" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" ) -func aclScenario(t *testing.T, policy *headscale.ACLPolicy, clientsPerUser int) *Scenario { +var veryLargeDestination = []string{ + "0.0.0.0/5:*", + "8.0.0.0/7:*", + "11.0.0.0/8:*", + "12.0.0.0/6:*", + "16.0.0.0/4:*", + "32.0.0.0/3:*", + "64.0.0.0/2:*", + "128.0.0.0/3:*", + "160.0.0.0/5:*", + "168.0.0.0/6:*", + "172.0.0.0/12:*", + "172.32.0.0/11:*", + "172.64.0.0/10:*", + "172.128.0.0/9:*", + "173.0.0.0/8:*", + "174.0.0.0/7:*", + "176.0.0.0/4:*", + "192.0.0.0/9:*", + "192.128.0.0/11:*", + "192.160.0.0/13:*", + "192.169.0.0/16:*", + "192.170.0.0/15:*", + "192.172.0.0/14:*", + "192.176.0.0/12:*", + "192.192.0.0/10:*", + "193.0.0.0/8:*", + "194.0.0.0/7:*", + "196.0.0.0/6:*", + "200.0.0.0/5:*", + "208.0.0.0/4:*", +} + +func aclScenario(t *testing.T, policy *hscontrol.ACLPolicy, clientsPerUser int) *Scenario { t.Helper() scenario, err := NewScenario() assert.NoError(t, err) @@ -59,7 +92,7 @@ func TestACLHostsInNetMapTable(t *testing.T) { // they can access minus one (them self). tests := map[string]struct { users map[string]int - policy headscale.ACLPolicy + policy hscontrol.ACLPolicy want map[string]int }{ // Test that when we have no ACL, each client netmap has @@ -69,8 +102,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { "user1": 2, "user2": 2, }, - policy: headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + policy: hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"*"}, @@ -90,8 +123,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { "user1": 2, "user2": 2, }, - policy: headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + policy: hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"user1"}, @@ -116,8 +149,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { "user1": 2, "user2": 2, }, - policy: headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + policy: hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"user1"}, @@ -153,8 +186,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { "user1": 2, "user2": 2, }, - policy: headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + policy: hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"user1"}, @@ -176,6 +209,34 @@ func TestACLHostsInNetMapTable(t *testing.T) { "user2": 3, // ns1 + ns2 (return path) }, }, + "very-large-destination-prefix-1372": { + users: map[string]int{ + "user1": 2, + "user2": 2, + }, + policy: hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ + { + Action: "accept", + Sources: []string{"user1"}, + Destinations: append([]string{"user1:*"}, veryLargeDestination...), + }, + { + Action: "accept", + Sources: []string{"user2"}, + Destinations: append([]string{"user2:*"}, veryLargeDestination...), + }, + { + Action: "accept", + Sources: []string{"user1"}, + Destinations: append([]string{"user2:*"}, veryLargeDestination...), + }, + }, + }, want: map[string]int{ + "user1": 3, // ns1 + ns2 + "user2": 3, // ns1 + ns2 (return path) + }, + }, } for name, testCase := range tests { @@ -188,7 +249,6 @@ func TestACLHostsInNetMapTable(t *testing.T) { err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithACLPolicy(&testCase.policy), - // hsic.WithTestName(fmt.Sprintf("aclinnetmap%s", name)), ) assert.NoError(t, err) @@ -198,9 +258,6 @@ func TestACLHostsInNetMapTable(t *testing.T) { err = scenario.WaitForTailscaleSync() assert.NoError(t, err) - // allHostnames, err := scenario.ListTailscaleClientsFQDNs() - // assert.NoError(t, err) - for _, client := range allClients { status, err := client.Status() assert.NoError(t, err) @@ -225,8 +282,8 @@ func TestACLAllowUser80Dst(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + &hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"user1"}, @@ -281,11 +338,11 @@ func TestACLDenyAllPort80(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &headscale.ACLPolicy{ + &hscontrol.ACLPolicy{ Groups: map[string][]string{ "group:integration-acl-test": {"user1", "user2"}, }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"group:integration-acl-test"}, @@ -330,8 +387,8 @@ func TestACLAllowUserDst(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + &hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"user1"}, @@ -388,8 +445,8 @@ func TestACLAllowStarDst(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + &hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"user1"}, @@ -447,11 +504,11 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &headscale.ACLPolicy{ - Hosts: headscale.Hosts{ + &hscontrol.ACLPolicy{ + Hosts: hscontrol.Hosts{ "all": netip.MustParsePrefix("100.64.0.0/24"), }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ // Everyone can curl test3 { Action: "accept", @@ -546,16 +603,16 @@ func TestACLNamedHostsCanReach(t *testing.T) { IntegrationSkip(t) tests := map[string]struct { - policy headscale.ACLPolicy + policy hscontrol.ACLPolicy }{ "ipv4": { - policy: headscale.ACLPolicy{ - Hosts: headscale.Hosts{ + policy: hscontrol.ACLPolicy{ + Hosts: hscontrol.Hosts{ "test1": netip.MustParsePrefix("100.64.0.1/32"), "test2": netip.MustParsePrefix("100.64.0.2/32"), "test3": netip.MustParsePrefix("100.64.0.3/32"), }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ // Everyone can curl test3 { Action: "accept", @@ -572,13 +629,13 @@ func TestACLNamedHostsCanReach(t *testing.T) { }, }, "ipv6": { - policy: headscale.ACLPolicy{ - Hosts: headscale.Hosts{ + policy: hscontrol.ACLPolicy{ + Hosts: hscontrol.Hosts{ "test1": netip.MustParsePrefix("fd7a:115c:a1e0::1/128"), "test2": netip.MustParsePrefix("fd7a:115c:a1e0::2/128"), "test3": netip.MustParsePrefix("fd7a:115c:a1e0::3/128"), }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ // Everyone can curl test3 { Action: "accept", @@ -797,11 +854,11 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) { IntegrationSkip(t) tests := map[string]struct { - policy headscale.ACLPolicy + policy hscontrol.ACLPolicy }{ "ipv4": { - policy: headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + policy: hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"100.64.0.1"}, @@ -811,8 +868,8 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) { }, }, "ipv6": { - policy: headscale.ACLPolicy{ - ACLs: []headscale.ACL{ + policy: hscontrol.ACLPolicy{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"fd7a:115c:a1e0::1"}, @@ -822,12 +879,12 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) { }, }, "hostv4cidr": { - policy: headscale.ACLPolicy{ - Hosts: headscale.Hosts{ + policy: hscontrol.ACLPolicy{ + Hosts: hscontrol.Hosts{ "test1": netip.MustParsePrefix("100.64.0.1/32"), "test2": netip.MustParsePrefix("100.64.0.2/32"), }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"test1"}, @@ -837,12 +894,12 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) { }, }, "hostv6cidr": { - policy: headscale.ACLPolicy{ - Hosts: headscale.Hosts{ + policy: hscontrol.ACLPolicy{ + Hosts: hscontrol.Hosts{ "test1": netip.MustParsePrefix("fd7a:115c:a1e0::1/128"), "test2": netip.MustParsePrefix("fd7a:115c:a1e0::2/128"), }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"test1"}, @@ -852,12 +909,12 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) { }, }, "group": { - policy: headscale.ACLPolicy{ + policy: hscontrol.ACLPolicy{ Groups: map[string][]string{ "group:one": {"user1"}, "group:two": {"user2"}, }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"group:one"}, diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index f04e5a37a4..d3cad212b9 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -15,7 +15,7 @@ import ( "testing" "time" - "github.com/juanfont/headscale" + "github.com/juanfont/headscale/hscontrol" "github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/hsic" "github.com/ory/dockertest/v3" @@ -396,7 +396,7 @@ func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration, users string) (* } portNotation := fmt.Sprintf("%d/tcp", port) - hash, _ := headscale.GenerateRandomStringDNSSafe(hsicOIDCMockHashLength) + hash, _ := hscontrol.GenerateRandomStringDNSSafe(hsicOIDCMockHashLength) hostname := fmt.Sprintf("hs-oidcmock-%s", hash) @@ -464,7 +464,7 @@ func (s *AuthOIDCScenario) runMockOIDC(accessTTL time.Duration, users string) (* log.Printf("headscale mock oidc is ready for tests at %s", hostEndpoint) - return &headscale.OIDCConfig{ + return &hscontrol.OIDCConfig{ Issuer: fmt.Sprintf( "http://%s/oidc", net.JoinHostPort(s.mockOIDC.GetIPInNetwork(s.network), strconv.Itoa(port)), diff --git a/integration/cli_test.go b/integration/cli_test.go index ff90fd9a68..039b065ad1 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -532,3 +532,989 @@ func TestEnablingRoutes(t *testing.T) { } } } + +func TestApiKeyCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + count := 5 + + scenario, err := NewScenario() + assert.NoError(t, err) + + spec := map[string]int{ + "user1": 0, + "user2": 0, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) + assert.NoError(t, err) + + headscale, err := scenario.Headscale() + assert.NoError(t, err) + + keys := make([]string, count) + + for idx := 0; idx < count; idx++ { + apiResult, err := headscale.Execute( + []string{ + "headscale", + "apikeys", + "create", + "--expiration", + "24h", + "--output", + "json", + }, + ) + assert.Nil(t, err) + assert.NotEmpty(t, apiResult) + + keys[idx] = apiResult + } + + assert.Len(t, keys, 5) + + var listedAPIKeys []v1.ApiKey + err = executeAndUnmarshal(headscale, + []string{ + "headscale", + "apikeys", + "list", + "--output", + "json", + }, + &listedAPIKeys, + ) + assert.Nil(t, err) + + assert.Len(t, listedAPIKeys, 5) + + assert.Equal(t, uint64(1), listedAPIKeys[0].Id) + assert.Equal(t, uint64(2), listedAPIKeys[1].Id) + assert.Equal(t, uint64(3), listedAPIKeys[2].Id) + assert.Equal(t, uint64(4), listedAPIKeys[3].Id) + assert.Equal(t, uint64(5), listedAPIKeys[4].Id) + + assert.NotEmpty(t, listedAPIKeys[0].Prefix) + assert.NotEmpty(t, listedAPIKeys[1].Prefix) + assert.NotEmpty(t, listedAPIKeys[2].Prefix) + assert.NotEmpty(t, listedAPIKeys[3].Prefix) + assert.NotEmpty(t, listedAPIKeys[4].Prefix) + + assert.True(t, listedAPIKeys[0].Expiration.AsTime().After(time.Now())) + assert.True(t, listedAPIKeys[1].Expiration.AsTime().After(time.Now())) + assert.True(t, listedAPIKeys[2].Expiration.AsTime().After(time.Now())) + assert.True(t, listedAPIKeys[3].Expiration.AsTime().After(time.Now())) + assert.True(t, listedAPIKeys[4].Expiration.AsTime().After(time.Now())) + + assert.True( + t, + listedAPIKeys[0].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), + ) + assert.True( + t, + listedAPIKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), + ) + assert.True( + t, + listedAPIKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), + ) + assert.True( + t, + listedAPIKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), + ) + assert.True( + t, + listedAPIKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), + ) + + expiredPrefixes := make(map[string]bool) + + // Expire three keys + for idx := 0; idx < 3; idx++ { + _, err := headscale.Execute( + []string{ + "headscale", + "apikeys", + "expire", + "--prefix", + listedAPIKeys[idx].Prefix, + }, + ) + assert.Nil(t, err) + + expiredPrefixes[listedAPIKeys[idx].Prefix] = true + } + + var listedAfterExpireAPIKeys []v1.ApiKey + err = executeAndUnmarshal(headscale, + []string{ + "headscale", + "apikeys", + "list", + "--output", + "json", + }, + &listedAfterExpireAPIKeys, + ) + assert.Nil(t, err) + + for index := range listedAfterExpireAPIKeys { + if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].Prefix]; ok { + // Expired + assert.True( + t, + listedAfterExpireAPIKeys[index].Expiration.AsTime().Before(time.Now()), + ) + } else { + // Not expired + assert.False( + t, + listedAfterExpireAPIKeys[index].Expiration.AsTime().Before(time.Now()), + ) + } + } + + err = scenario.Shutdown() + assert.NoError(t, err) +} + +func TestNodeTagCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario() + assert.NoError(t, err) + + spec := map[string]int{ + "user1": 0, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) + assert.NoError(t, err) + + headscale, err := scenario.Headscale() + assert.NoError(t, err) + + machineKeys := []string{ + "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", + } + machines := make([]*v1.Machine, len(machineKeys)) + assert.Nil(t, err) + + for index, machineKey := range machineKeys { + _, err := headscale.Execute( + []string{ + "headscale", + "debug", + "create-node", + "--name", + fmt.Sprintf("machine-%d", index+1), + "--user", + "user1", + "--key", + machineKey, + "--output", + "json", + }, + ) + assert.Nil(t, err) + + var machine v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "--user", + "user1", + "register", + "--key", + machineKey, + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + machines[index] = &machine + } + assert.Len(t, machines, len(machineKeys)) + + var machine v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "tag", + "-i", "1", + "-t", "tag:test", + "--output", "json", + }, + &machine, + ) + assert.Nil(t, err) + + assert.Equal(t, []string{"tag:test"}, machine.ForcedTags) + + // try to set a wrong tag and retrieve the error + type errOutput struct { + Error string `json:"error"` + } + var errorOutput errOutput + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "tag", + "-i", "2", + "-t", "wrong-tag", + "--output", "json", + }, + &errorOutput, + ) + assert.Nil(t, err) + assert.Contains(t, errorOutput.Error, "tag must start with the string 'tag:'") + + // Test list all nodes after added seconds + resultMachines := make([]*v1.Machine, len(machineKeys)) + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", "json", + }, + &resultMachines, + ) + assert.Nil(t, err) + found := false + for _, machine := range resultMachines { + if machine.ForcedTags != nil { + for _, tag := range machine.ForcedTags { + if tag == "tag:test" { + found = true + } + } + } + } + assert.Equal( + t, + true, + found, + "should find a machine with the tag 'tag:test' in the list of machines", + ) + + err = scenario.Shutdown() + assert.NoError(t, err) +} + +func TestNodeCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario() + assert.NoError(t, err) + + spec := map[string]int{ + "machine-user": 0, + "other-user": 0, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) + assert.NoError(t, err) + + headscale, err := scenario.Headscale() + assert.NoError(t, err) + + // Randomly generated machine keys + machineKeys := []string{ + "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", + "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", + "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + } + machines := make([]*v1.Machine, len(machineKeys)) + assert.Nil(t, err) + + for index, machineKey := range machineKeys { + _, err := headscale.Execute( + []string{ + "headscale", + "debug", + "create-node", + "--name", + fmt.Sprintf("machine-%d", index+1), + "--user", + "machine-user", + "--key", + machineKey, + "--output", + "json", + }, + ) + assert.Nil(t, err) + + var machine v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "--user", + "machine-user", + "register", + "--key", + machineKey, + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + machines[index] = &machine + } + + assert.Len(t, machines, len(machineKeys)) + + // Test list all nodes after added seconds + var listAll []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &listAll, + ) + assert.Nil(t, err) + + assert.Len(t, listAll, 5) + + assert.Equal(t, uint64(1), listAll[0].Id) + assert.Equal(t, uint64(2), listAll[1].Id) + assert.Equal(t, uint64(3), listAll[2].Id) + assert.Equal(t, uint64(4), listAll[3].Id) + assert.Equal(t, uint64(5), listAll[4].Id) + + assert.Equal(t, "machine-1", listAll[0].Name) + assert.Equal(t, "machine-2", listAll[1].Name) + assert.Equal(t, "machine-3", listAll[2].Name) + assert.Equal(t, "machine-4", listAll[3].Name) + assert.Equal(t, "machine-5", listAll[4].Name) + + otherUserMachineKeys := []string{ + "nodekey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e", + "nodekey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584", + } + otherUserMachines := make([]*v1.Machine, len(otherUserMachineKeys)) + assert.Nil(t, err) + + for index, machineKey := range otherUserMachineKeys { + _, err := headscale.Execute( + []string{ + "headscale", + "debug", + "create-node", + "--name", + fmt.Sprintf("otherUser-machine-%d", index+1), + "--user", + "other-user", + "--key", + machineKey, + "--output", + "json", + }, + ) + assert.Nil(t, err) + + var machine v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "--user", + "other-user", + "register", + "--key", + machineKey, + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + otherUserMachines[index] = &machine + } + + assert.Len(t, otherUserMachines, len(otherUserMachineKeys)) + + // Test list all nodes after added otherUser + var listAllWithotherUser []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &listAllWithotherUser, + ) + assert.Nil(t, err) + + // All nodes, machines + otherUser + assert.Len(t, listAllWithotherUser, 7) + + assert.Equal(t, uint64(6), listAllWithotherUser[5].Id) + assert.Equal(t, uint64(7), listAllWithotherUser[6].Id) + + assert.Equal(t, "otherUser-machine-1", listAllWithotherUser[5].Name) + assert.Equal(t, "otherUser-machine-2", listAllWithotherUser[6].Name) + + // Test list all nodes after added otherUser + var listOnlyotherUserMachineUser []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--user", + "other-user", + "--output", + "json", + }, + &listOnlyotherUserMachineUser, + ) + assert.Nil(t, err) + + assert.Len(t, listOnlyotherUserMachineUser, 2) + + assert.Equal(t, uint64(6), listOnlyotherUserMachineUser[0].Id) + assert.Equal(t, uint64(7), listOnlyotherUserMachineUser[1].Id) + + assert.Equal( + t, + "otherUser-machine-1", + listOnlyotherUserMachineUser[0].Name, + ) + assert.Equal( + t, + "otherUser-machine-2", + listOnlyotherUserMachineUser[1].Name, + ) + + // Delete a machines + _, err = headscale.Execute( + []string{ + "headscale", + "nodes", + "delete", + "--identifier", + // Delete the last added machine + "4", + "--output", + "json", + "--force", + }, + ) + assert.Nil(t, err) + + // Test: list main user after machine is deleted + var listOnlyMachineUserAfterDelete []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--user", + "machine-user", + "--output", + "json", + }, + &listOnlyMachineUserAfterDelete, + ) + assert.Nil(t, err) + + assert.Len(t, listOnlyMachineUserAfterDelete, 4) + + err = scenario.Shutdown() + assert.NoError(t, err) +} + +func TestNodeExpireCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario() + assert.NoError(t, err) + + spec := map[string]int{ + "machine-expire-user": 0, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) + assert.NoError(t, err) + + headscale, err := scenario.Headscale() + assert.NoError(t, err) + + // Randomly generated machine keys + machineKeys := []string{ + "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", + "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", + "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + } + machines := make([]*v1.Machine, len(machineKeys)) + + for index, machineKey := range machineKeys { + _, err := headscale.Execute( + []string{ + "headscale", + "debug", + "create-node", + "--name", + fmt.Sprintf("machine-%d", index+1), + "--user", + "machine-expire-user", + "--key", + machineKey, + "--output", + "json", + }, + ) + assert.Nil(t, err) + + var machine v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "--user", + "machine-expire-user", + "register", + "--key", + machineKey, + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + machines[index] = &machine + } + + assert.Len(t, machines, len(machineKeys)) + + var listAll []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &listAll, + ) + assert.Nil(t, err) + + assert.Len(t, listAll, 5) + + assert.True(t, listAll[0].Expiry.AsTime().IsZero()) + assert.True(t, listAll[1].Expiry.AsTime().IsZero()) + assert.True(t, listAll[2].Expiry.AsTime().IsZero()) + assert.True(t, listAll[3].Expiry.AsTime().IsZero()) + assert.True(t, listAll[4].Expiry.AsTime().IsZero()) + + for idx := 0; idx < 3; idx++ { + _, err := headscale.Execute( + []string{ + "headscale", + "nodes", + "expire", + "--identifier", + fmt.Sprintf("%d", listAll[idx].Id), + }, + ) + assert.Nil(t, err) + } + + var listAllAfterExpiry []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &listAllAfterExpiry, + ) + assert.Nil(t, err) + + assert.Len(t, listAllAfterExpiry, 5) + + assert.True(t, listAllAfterExpiry[0].Expiry.AsTime().Before(time.Now())) + assert.True(t, listAllAfterExpiry[1].Expiry.AsTime().Before(time.Now())) + assert.True(t, listAllAfterExpiry[2].Expiry.AsTime().Before(time.Now())) + assert.True(t, listAllAfterExpiry[3].Expiry.AsTime().IsZero()) + assert.True(t, listAllAfterExpiry[4].Expiry.AsTime().IsZero()) + + err = scenario.Shutdown() + assert.NoError(t, err) +} + +func TestNodeRenameCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario() + assert.NoError(t, err) + + spec := map[string]int{ + "machine-rename-command": 0, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) + assert.NoError(t, err) + + headscale, err := scenario.Headscale() + assert.NoError(t, err) + + // Randomly generated machine keys + machineKeys := []string{ + "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", + "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", + "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + } + machines := make([]*v1.Machine, len(machineKeys)) + assert.Nil(t, err) + + for index, machineKey := range machineKeys { + _, err := headscale.Execute( + []string{ + "headscale", + "debug", + "create-node", + "--name", + fmt.Sprintf("machine-%d", index+1), + "--user", + "machine-rename-command", + "--key", + machineKey, + "--output", + "json", + }, + ) + assert.Nil(t, err) + + var machine v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "--user", + "machine-rename-command", + "register", + "--key", + machineKey, + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + machines[index] = &machine + } + + assert.Len(t, machines, len(machineKeys)) + + var listAll []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &listAll, + ) + assert.Nil(t, err) + + assert.Len(t, listAll, 5) + + assert.Contains(t, listAll[0].GetGivenName(), "machine-1") + assert.Contains(t, listAll[1].GetGivenName(), "machine-2") + assert.Contains(t, listAll[2].GetGivenName(), "machine-3") + assert.Contains(t, listAll[3].GetGivenName(), "machine-4") + assert.Contains(t, listAll[4].GetGivenName(), "machine-5") + + for idx := 0; idx < 3; idx++ { + _, err := headscale.Execute( + []string{ + "headscale", + "nodes", + "rename", + "--identifier", + fmt.Sprintf("%d", listAll[idx].Id), + fmt.Sprintf("newmachine-%d", idx+1), + }, + ) + assert.Nil(t, err) + } + + var listAllAfterRename []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &listAllAfterRename, + ) + assert.Nil(t, err) + + assert.Len(t, listAllAfterRename, 5) + + assert.Equal(t, "newmachine-1", listAllAfterRename[0].GetGivenName()) + assert.Equal(t, "newmachine-2", listAllAfterRename[1].GetGivenName()) + assert.Equal(t, "newmachine-3", listAllAfterRename[2].GetGivenName()) + assert.Contains(t, listAllAfterRename[3].GetGivenName(), "machine-4") + assert.Contains(t, listAllAfterRename[4].GetGivenName(), "machine-5") + + // Test failure for too long names + result, err := headscale.Execute( + []string{ + "headscale", + "nodes", + "rename", + "--identifier", + fmt.Sprintf("%d", listAll[4].Id), + "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890", + }, + ) + assert.Nil(t, err) + assert.Contains(t, result, "not be over 63 chars") + + var listAllAfterRenameAttempt []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &listAllAfterRenameAttempt, + ) + assert.Nil(t, err) + + assert.Len(t, listAllAfterRenameAttempt, 5) + + assert.Equal(t, "newmachine-1", listAllAfterRenameAttempt[0].GetGivenName()) + assert.Equal(t, "newmachine-2", listAllAfterRenameAttempt[1].GetGivenName()) + assert.Equal(t, "newmachine-3", listAllAfterRenameAttempt[2].GetGivenName()) + assert.Contains(t, listAllAfterRenameAttempt[3].GetGivenName(), "machine-4") + assert.Contains(t, listAllAfterRenameAttempt[4].GetGivenName(), "machine-5") + + err = scenario.Shutdown() + assert.NoError(t, err) +} + +func TestNodeMoveCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario() + assert.NoError(t, err) + + spec := map[string]int{ + "old-user": 0, + "new-user": 0, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) + assert.NoError(t, err) + + headscale, err := scenario.Headscale() + assert.NoError(t, err) + + // Randomly generated machine key + machineKey := "nodekey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa" + + _, err = headscale.Execute( + []string{ + "headscale", + "debug", + "create-node", + "--name", + "nomad-machine", + "--user", + "old-user", + "--key", + machineKey, + "--output", + "json", + }, + ) + assert.Nil(t, err) + + var machine v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "--user", + "old-user", + "register", + "--key", + machineKey, + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + assert.Equal(t, uint64(1), machine.Id) + assert.Equal(t, "nomad-machine", machine.Name) + assert.Equal(t, machine.User.Name, "old-user") + + machineID := fmt.Sprintf("%d", machine.Id) + + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "move", + "--identifier", + machineID, + "--user", + "new-user", + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + assert.Equal(t, machine.User.Name, "new-user") + + var allNodes []v1.Machine + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &allNodes, + ) + assert.Nil(t, err) + + assert.Len(t, allNodes, 1) + + assert.Equal(t, allNodes[0].Id, machine.Id) + assert.Equal(t, allNodes[0].User, machine.User) + assert.Equal(t, allNodes[0].User.Name, "new-user") + + moveToNonExistingNSResult, err := headscale.Execute( + []string{ + "headscale", + "nodes", + "move", + "--identifier", + machineID, + "--user", + "non-existing-user", + "--output", + "json", + }, + ) + assert.Nil(t, err) + + assert.Contains( + t, + moveToNonExistingNSResult, + "User not found", + ) + assert.Equal(t, machine.User.Name, "new-user") + + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "move", + "--identifier", + machineID, + "--user", + "old-user", + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + assert.Equal(t, machine.User.Name, "old-user") + + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "move", + "--identifier", + machineID, + "--user", + "old-user", + "--output", + "json", + }, + &machine, + ) + assert.Nil(t, err) + + assert.Equal(t, machine.User.Name, "old-user") + + err = scenario.Shutdown() + assert.NoError(t, err) +} diff --git a/integration/control.go b/integration/control.go index 02d2750d1d..65432b3ff2 100644 --- a/integration/control.go +++ b/integration/control.go @@ -8,6 +8,7 @@ import ( type ControlServer interface { Shutdown() error SaveLog(string) error + SaveProfile(string) error Execute(command []string) (string, error) ConnectToNetwork(network *dockertest.Network) error GetHealthEndpoint() string diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index 5f831bf47f..be128087d5 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -6,7 +6,7 @@ import ( "net/url" "testing" - "github.com/juanfont/headscale" + "github.com/juanfont/headscale/hscontrol" "github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" @@ -110,7 +110,7 @@ func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( return err } - hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength) + hash, err := hscontrol.GenerateRandomStringDNSSafe(scenarioHashLength) if err != nil { return err } diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 6517b74546..6b1652b084 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -16,11 +16,14 @@ import ( "net" "net/http" "net/url" + "os" + "path" + "strings" "time" "github.com/davecgh/go-spew/spew" - "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" "github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/integrationutil" "github.com/ory/dockertest/v3" @@ -56,7 +59,7 @@ type HeadscaleInContainer struct { port int extraPorts []string hostPortBindings map[string][]string - aclPolicy *headscale.ACLPolicy + aclPolicy *hscontrol.ACLPolicy env map[string]string tlsCert []byte tlsKey []byte @@ -67,9 +70,9 @@ type HeadscaleInContainer struct { // Headscale instance. type Option = func(c *HeadscaleInContainer) -// WithACLPolicy adds a headscale.ACLPolicy policy to the +// WithACLPolicy adds a hscontrol.ACLPolicy policy to the // HeadscaleInContainer instance. -func WithACLPolicy(acl *headscale.ACLPolicy) Option { +func WithACLPolicy(acl *hscontrol.ACLPolicy) Option { return func(hsic *HeadscaleInContainer) { // TODO(kradalby): Move somewhere appropriate hsic.env["HEADSCALE_ACL_POLICY_PATH"] = aclPolicyPath @@ -129,7 +132,7 @@ func WithHostPortBindings(bindings map[string][]string) Option { // in the Docker container name. func WithTestName(testName string) Option { return func(hsic *HeadscaleInContainer) { - hash, _ := headscale.GenerateRandomStringDNSSafe(hsicHashLength) + hash, _ := hscontrol.GenerateRandomStringDNSSafe(hsicHashLength) hostname := fmt.Sprintf("hs-%s-%s", testName, hash) hsic.hostname = hostname @@ -164,7 +167,7 @@ func New( network *dockertest.Network, opts ...Option, ) (*HeadscaleInContainer, error) { - hash, err := headscale.GenerateRandomStringDNSSafe(hsicHashLength) + hash, err := hscontrol.GenerateRandomStringDNSSafe(hsicHashLength) if err != nil { return nil, err } @@ -205,7 +208,10 @@ func New( ContextDir: dockerContextPath, } - env := []string{} + env := []string{ + "HEADSCALE_PROFILING_ENABLED=1", + "HEADSCALE_PROFILING_PATH=/tmp/profile", + } for key, value := range hsic.env { env = append(env, fmt.Sprintf("%s=%s", key, value)) } @@ -219,7 +225,7 @@ func New( // Cmd: []string{"headscale", "serve"}, // TODO(kradalby): Get rid of this hack, we currently need to give us some // to inject the headscale configuration further down. - Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve"}, + Entrypoint: []string{"/bin/bash", "-c", "/bin/sleep 3 ; headscale serve ; /bin/sleep 30"}, Env: env, } @@ -305,6 +311,33 @@ func (t *HeadscaleInContainer) hasTLS() bool { // Shutdown stops and cleans up the Headscale container. func (t *HeadscaleInContainer) Shutdown() error { + err := t.SaveLog("/tmp/control") + if err != nil { + log.Printf( + "Failed to save log from control: %s", + fmt.Errorf("failed to save log from control: %w", err), + ) + } + + // Send a interrupt signal to the "headscale" process inside the container + // allowing it to shut down gracefully and flush the profile to disk. + // The container will live for a bit longer due to the sleep at the end. + err = t.SendInterrupt() + if err != nil { + log.Printf( + "Failed to send graceful interrupt to control: %s", + fmt.Errorf("failed to send graceful interrupt to control: %w", err), + ) + } + + err = t.SaveProfile("/tmp/control") + if err != nil { + log.Printf( + "Failed to save profile from control: %s", + fmt.Errorf("failed to save profile from control: %w", err), + ) + } + return t.pool.Purge(t.container) } @@ -314,6 +347,24 @@ func (t *HeadscaleInContainer) SaveLog(path string) error { return dockertestutil.SaveLog(t.pool, t.container, path) } +func (t *HeadscaleInContainer) SaveProfile(savePath string) error { + tarFile, err := t.FetchPath("/tmp/profile") + if err != nil { + return err + } + + err = os.WriteFile( + path.Join(savePath, t.hostname+".pprof.tar"), + tarFile, + os.ModePerm, + ) + if err != nil { + return err + } + + return nil +} + // Execute runs a command inside the Headscale container and returns the // result of stdout as a string. func (t *HeadscaleInContainer) Execute( @@ -498,6 +549,26 @@ func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error { return integrationutil.WriteFileToContainer(t.pool, t.container, path, data) } +// FetchPath gets a path from inside the Headscale container and returns a tar +// file as byte array. +func (t *HeadscaleInContainer) FetchPath(path string) ([]byte, error) { + return integrationutil.FetchPathFromContainer(t.pool, t.container, path) +} + +func (t *HeadscaleInContainer) SendInterrupt() error { + pid, err := t.Execute([]string{"pidof", "headscale"}) + if err != nil { + return err + } + + _, err = t.Execute([]string{"kill", "-2", strings.Trim(pid, "'\n")}) + if err != nil { + return err + } + + return nil +} + // nolint func createCertificate(hostname string) ([]byte, []byte, error) { // From: diff --git a/integration/integrationutil/util.go b/integration/integrationutil/util.go index 640613b718..59eeeb17b4 100644 --- a/integration/integrationutil/util.go +++ b/integration/integrationutil/util.go @@ -72,3 +72,24 @@ func WriteFileToContainer( return nil } + +func FetchPathFromContainer( + pool *dockertest.Pool, + container *dockertest.Resource, + path string, +) ([]byte, error) { + buf := bytes.NewBuffer([]byte{}) + + err := pool.Client.DownloadFromContainer( + container.Container.ID, + docker.DownloadFromContainerOptions{ + OutputStream: buf, + Path: path, + }, + ) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/integration/scenario.go b/integration/scenario.go index 7efd08b09b..58005482a6 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -9,8 +9,8 @@ import ( "sync" "time" - "github.com/juanfont/headscale" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" + "github.com/juanfont/headscale/hscontrol" "github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" @@ -33,6 +33,7 @@ var ( tailscaleVersions2021 = []string{ "head", "unstable", + "1.40.0", "1.38.4", "1.36.2", "1.34.2", @@ -104,7 +105,7 @@ type Scenario struct { // NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with // a set of Users and TailscaleClients. func NewScenario() (*Scenario, error) { - hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength) + hash, err := hscontrol.GenerateRandomStringDNSSafe(scenarioHashLength) if err != nil { return nil, err } @@ -149,15 +150,7 @@ func NewScenario() (*Scenario, error) { // environment running the tests. func (s *Scenario) Shutdown() error { s.controlServers.Range(func(_ string, control ControlServer) bool { - err := control.SaveLog("/tmp/control") - if err != nil { - log.Printf( - "Failed to save log from control: %s", - fmt.Errorf("failed to save log from control: %w", err), - ) - } - - err = control.Shutdown() + err := control.Shutdown() if err != nil { log.Printf( "Failed to shut down control: %s", @@ -287,7 +280,7 @@ func (s *Scenario) CreateTailscaleNodesInUser( headscale, err := s.Headscale() if err != nil { - return fmt.Errorf("failed to create tailscale node: %w", err) + return fmt.Errorf("failed to create tailscale node (version: %s): %w", version, err) } cert := headscale.GetCert() diff --git a/integration/ssh_test.go b/integration/ssh_test.go index 66f9f815a6..922ced622d 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/juanfont/headscale" + "github.com/juanfont/headscale/hscontrol" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" @@ -57,18 +57,18 @@ func TestSSHOneUserAllToAll(t *testing.T) { err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithSSH()}, hsic.WithACLPolicy( - &headscale.ACLPolicy{ + &hscontrol.ACLPolicy{ Groups: map[string][]string{ "group:integration-test": {"user1"}, }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"*"}, Destinations: []string{"*:*"}, }, }, - SSHs: []headscale.SSH{ + SSHs: []hscontrol.SSH{ { Action: "accept", Sources: []string{"group:integration-test"}, @@ -134,18 +134,18 @@ func TestSSHMultipleUsersAllToAll(t *testing.T) { err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithSSH()}, hsic.WithACLPolicy( - &headscale.ACLPolicy{ + &hscontrol.ACLPolicy{ Groups: map[string][]string{ "group:integration-test": {"user1", "user2"}, }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"*"}, Destinations: []string{"*:*"}, }, }, - SSHs: []headscale.SSH{ + SSHs: []hscontrol.SSH{ { Action: "accept", Sources: []string{"group:integration-test"}, @@ -216,18 +216,18 @@ func TestSSHNoSSHConfigured(t *testing.T) { err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithSSH()}, hsic.WithACLPolicy( - &headscale.ACLPolicy{ + &hscontrol.ACLPolicy{ Groups: map[string][]string{ "group:integration-test": {"user1"}, }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"*"}, Destinations: []string{"*:*"}, }, }, - SSHs: []headscale.SSH{}, + SSHs: []hscontrol.SSH{}, }, ), hsic.WithTestName("sshnoneconfigured"), @@ -286,18 +286,18 @@ func TestSSHIsBlockedInACL(t *testing.T) { err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithSSH()}, hsic.WithACLPolicy( - &headscale.ACLPolicy{ + &hscontrol.ACLPolicy{ Groups: map[string][]string{ "group:integration-test": {"user1"}, }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"*"}, Destinations: []string{"*:80"}, }, }, - SSHs: []headscale.SSH{ + SSHs: []hscontrol.SSH{ { Action: "accept", Sources: []string{"group:integration-test"}, @@ -364,19 +364,19 @@ func TestSSUserOnlyIsolation(t *testing.T) { err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithSSH()}, hsic.WithACLPolicy( - &headscale.ACLPolicy{ + &hscontrol.ACLPolicy{ Groups: map[string][]string{ "group:ssh1": {"useracl1"}, "group:ssh2": {"useracl2"}, }, - ACLs: []headscale.ACL{ + ACLs: []hscontrol.ACL{ { Action: "accept", Sources: []string{"*"}, Destinations: []string{"*:*"}, }, }, - SSHs: []headscale.SSH{ + SSHs: []hscontrol.SSH{ { Action: "accept", Sources: []string{"group:ssh1"}, @@ -424,7 +424,7 @@ func TestSSUserOnlyIsolation(t *testing.T) { // TODO(kradalby,evenh): ACLs do currently not cover reject // cases properly, and currently will accept all incomming connections // as long as a rule is present. - // + // for _, client := range ssh1Clients { // for _, peer := range ssh2Clients { // if client.Hostname() == peer.Hostname() { diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index 52a3ff386a..cc285f3bb0 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -12,7 +12,7 @@ import ( "time" "github.com/cenkalti/backoff/v4" - "github.com/juanfont/headscale" + "github.com/juanfont/headscale/hscontrol" "github.com/juanfont/headscale/integration/dockertestutil" "github.com/juanfont/headscale/integration/integrationutil" "github.com/ory/dockertest/v3" @@ -150,7 +150,7 @@ func New( network *dockertest.Network, opts ...Option, ) (*TailscaleInContainer, error) { - hash, err := headscale.GenerateRandomStringDNSSafe(tsicHashLength) + hash, err := hscontrol.GenerateRandomStringDNSSafe(tsicHashLength) if err != nil { return nil, err } @@ -212,7 +212,11 @@ func New( dockertestutil.DockerAllowNetworkAdministration, ) if err != nil { - return nil, fmt.Errorf("could not start tailscale container: %w", err) + return nil, fmt.Errorf( + "could not start tailscale container (version: %s): %w", + version, + err, + ) } log.Printf("Created %s container\n", hostname) diff --git a/integration_cli_test.go b/integration_cli_test.go deleted file mode 100644 index d6d32569d5..0000000000 --- a/integration_cli_test.go +++ /dev/null @@ -1,1635 +0,0 @@ -// nolint -package headscale - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "os" - "testing" - "time" - - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type IntegrationCLITestSuite struct { - suite.Suite - stats *suite.SuiteInformation - - pool dockertest.Pool - network dockertest.Network - headscale dockertest.Resource -} - -func TestIntegrationCLITestSuite(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration tests due to short flag") - } - - s := new(IntegrationCLITestSuite) - - suite.Run(t, s) -} - -func (s *IntegrationCLITestSuite) SetupTest() { - var err error - - if ppool, err := dockertest.NewPool(""); err == nil { - s.pool = *ppool - } else { - s.FailNow(fmt.Sprintf("Could not connect to docker: %s", err), "") - } - - network, err := GetFirstOrCreateNetwork(&s.pool, headscaleNetwork) - if err != nil { - s.FailNow(fmt.Sprintf("Failed to create or get network: %s", err), "") - } - s.network = network - - headscaleBuildOptions := &dockertest.BuildOptions{ - Dockerfile: "Dockerfile", - ContextDir: ".", - } - - currentPath, err := os.Getwd() - if err != nil { - s.FailNow(fmt.Sprintf("Could not determine current path: %s", err), "") - } - - headscaleOptions := &dockertest.RunOptions{ - Name: "headscale-cli", - Mounts: []string{ - fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath), - }, - Cmd: []string{"headscale", "serve"}, - Networks: []*dockertest.Network{&s.network}, - ExposedPorts: []string{"8080/tcp"}, - PortBindings: map[docker.Port][]docker.PortBinding{ - "8080/tcp": {{HostPort: "8080"}}, - }, - } - - err = s.pool.RemoveContainerByName(headscaleHostname) - if err != nil { - s.FailNow( - fmt.Sprintf( - "Could not remove existing container before building test: %s", - err, - ), - "", - ) - } - - fmt.Println("Creating headscale container for CLI tests") - if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil { - s.headscale = *pheadscale - } else { - s.FailNow(fmt.Sprintf("Could not start headscale container: %s", err), "") - } - fmt.Println("Created headscale container for CLI tests") - - fmt.Println("Waiting for headscale to be ready for CLI tests") - hostEndpoint := fmt.Sprintf("%s:%s", - s.headscale.GetIPInNetwork(&s.network), - s.headscale.GetPort("8080/tcp")) - - if err := s.pool.Retry(func() error { - url := fmt.Sprintf("http://%s/health", hostEndpoint) - resp, err := http.Get(url) - if err != nil { - fmt.Printf("headscale for CLI test is not ready: %s\n", err) - return err - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("status code not OK") - } - - return nil - }); err != nil { - // TODO(kradalby): If we cannot access headscale, or any other fatal error during - // test setup, we need to abort and tear down. However, testify does not seem to - // support that at the moment: - // https://github.com/stretchr/testify/issues/849 - return // fmt.Errorf("Could not connect to headscale: %s", err) - } - fmt.Println("headscale container is ready for CLI tests") -} - -func (s *IntegrationCLITestSuite) TearDownTest() { - if err := s.pool.Purge(&s.headscale); err != nil { - log.Printf("Could not purge resource: %s\n", err) - } - - if err := s.network.Close(); err != nil { - log.Printf("Could not close network: %s\n", err) - } -} - -func (s *IntegrationCLITestSuite) HandleStats( - suiteName string, - stats *suite.SuiteInformation, -) { - s.stats = stats -} - -func (s *IntegrationCLITestSuite) createUser(name string) (*v1.User, error) { - result, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "users", - "create", - name, - "--output", - "json", - }, - []string{}, - ) - if err != nil { - return nil, err - } - - var user v1.User - err = json.Unmarshal([]byte(result), &user) - if err != nil { - return nil, err - } - - return &user, nil -} - -func (s *IntegrationCLITestSuite) TestUserCommand() { - names := []string{"user1", "otherspace", "tasty"} - users := make([]*v1.User, len(names)) - - for index, userName := range names { - user, err := s.createUser(userName) - assert.Nil(s.T(), err) - - users[index] = user - } - - assert.Len(s.T(), users, len(names)) - - assert.Equal(s.T(), names[0], users[0].Name) - assert.Equal(s.T(), names[1], users[1].Name) - assert.Equal(s.T(), names[2], users[2].Name) - - // Test list users - listResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listedUsers []v1.User - err = json.Unmarshal([]byte(listResult), &listedUsers) - assert.Nil(s.T(), err) - - assert.Equal(s.T(), names[0], listedUsers[0].Name) - assert.Equal(s.T(), names[1], listedUsers[1].Name) - assert.Equal(s.T(), names[2], listedUsers[2].Name) - - // Test rename user - renameResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "users", - "rename", - "--output", - "json", - "tasty", - "newname", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var renamedUser v1.User - err = json.Unmarshal([]byte(renameResult), &renamedUser) - assert.Nil(s.T(), err) - - assert.Equal(s.T(), renamedUser.Name, "newname") - - // Test list after rename users - listAfterRenameResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "users", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listedAfterRenameUsers []v1.User - err = json.Unmarshal([]byte(listAfterRenameResult), &listedAfterRenameUsers) - assert.Nil(s.T(), err) - - assert.Equal(s.T(), names[0], listedAfterRenameUsers[0].Name) - assert.Equal(s.T(), names[1], listedAfterRenameUsers[1].Name) - assert.Equal(s.T(), "newname", listedAfterRenameUsers[2].Name) -} - -func (s *IntegrationCLITestSuite) TestPreAuthKeyCommand() { - count := 5 - - user, err := s.createUser("pre-auth-key-user") - - keys := make([]*v1.PreAuthKey, count) - assert.Nil(s.T(), err) - - for i := 0; i < count; i++ { - preAuthResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "create", - "--reusable", - "--expiration", - "24h", - "--output", - "json", - "--tags", - "tag:test1,tag:test2", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var preAuthKey v1.PreAuthKey - err = json.Unmarshal([]byte(preAuthResult), &preAuthKey) - assert.Nil(s.T(), err) - - keys[i] = &preAuthKey - } - - assert.Len(s.T(), keys, 5) - - // Test list of keys - listResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listedPreAuthKeys []v1.PreAuthKey - err = json.Unmarshal([]byte(listResult), &listedPreAuthKeys) - assert.Nil(s.T(), err) - - assert.Equal(s.T(), "1", listedPreAuthKeys[0].Id) - assert.Equal(s.T(), "2", listedPreAuthKeys[1].Id) - assert.Equal(s.T(), "3", listedPreAuthKeys[2].Id) - assert.Equal(s.T(), "4", listedPreAuthKeys[3].Id) - assert.Equal(s.T(), "5", listedPreAuthKeys[4].Id) - - assert.NotEmpty(s.T(), listedPreAuthKeys[0].Key) - assert.NotEmpty(s.T(), listedPreAuthKeys[1].Key) - assert.NotEmpty(s.T(), listedPreAuthKeys[2].Key) - assert.NotEmpty(s.T(), listedPreAuthKeys[3].Key) - assert.NotEmpty(s.T(), listedPreAuthKeys[4].Key) - - assert.True(s.T(), listedPreAuthKeys[0].Expiration.AsTime().After(time.Now())) - assert.True(s.T(), listedPreAuthKeys[1].Expiration.AsTime().After(time.Now())) - assert.True(s.T(), listedPreAuthKeys[2].Expiration.AsTime().After(time.Now())) - assert.True(s.T(), listedPreAuthKeys[3].Expiration.AsTime().After(time.Now())) - assert.True(s.T(), listedPreAuthKeys[4].Expiration.AsTime().After(time.Now())) - - assert.True( - s.T(), - listedPreAuthKeys[0].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - assert.True( - s.T(), - listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - assert.True( - s.T(), - listedPreAuthKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - assert.True( - s.T(), - listedPreAuthKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - assert.True( - s.T(), - listedPreAuthKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - - // Test that tags are present - for i := 0; i < count; i++ { - assert.Equal(s.T(), listedPreAuthKeys[i].AclTags, []string{"tag:test1", "tag:test2"}) - } - - // Expire three keys - for i := 0; i < 3; i++ { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "expire", - listedPreAuthKeys[i].Key, - }, - []string{}, - ) - assert.Nil(s.T(), err) - } - - // Test list pre auth keys after expire - listAfterExpireResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listedAfterExpirePreAuthKeys []v1.PreAuthKey - err = json.Unmarshal([]byte(listAfterExpireResult), &listedAfterExpirePreAuthKeys) - assert.Nil(s.T(), err) - - assert.True( - s.T(), - listedAfterExpirePreAuthKeys[0].Expiration.AsTime().Before(time.Now()), - ) - assert.True( - s.T(), - listedAfterExpirePreAuthKeys[1].Expiration.AsTime().Before(time.Now()), - ) - assert.True( - s.T(), - listedAfterExpirePreAuthKeys[2].Expiration.AsTime().Before(time.Now()), - ) - assert.True( - s.T(), - listedAfterExpirePreAuthKeys[3].Expiration.AsTime().After(time.Now()), - ) - assert.True( - s.T(), - listedAfterExpirePreAuthKeys[4].Expiration.AsTime().After(time.Now()), - ) -} - -func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandWithoutExpiry() { - user, err := s.createUser("pre-auth-key-without-exp-user") - assert.Nil(s.T(), err) - - preAuthResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "create", - "--reusable", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var preAuthKey v1.PreAuthKey - err = json.Unmarshal([]byte(preAuthResult), &preAuthKey) - assert.Nil(s.T(), err) - - // Test list of keys - listResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listedPreAuthKeys []v1.PreAuthKey - err = json.Unmarshal([]byte(listResult), &listedPreAuthKeys) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listedPreAuthKeys, 1) - - assert.True(s.T(), listedPreAuthKeys[0].Expiration.AsTime().After(time.Now())) - assert.True( - s.T(), - listedPreAuthKeys[0].Expiration.AsTime().Before(time.Now().Add(time.Minute*70)), - ) -} - -func (s *IntegrationCLITestSuite) TestPreAuthKeyCommandReusableEphemeral() { - user, err := s.createUser("pre-auth-key-reus-ephm-user") - assert.Nil(s.T(), err) - - preAuthReusableResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "create", - "--reusable=true", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var preAuthReusableKey v1.PreAuthKey - err = json.Unmarshal([]byte(preAuthReusableResult), &preAuthReusableKey) - assert.Nil(s.T(), err) - - assert.True(s.T(), preAuthReusableKey.GetReusable()) - assert.False(s.T(), preAuthReusableKey.GetEphemeral()) - - preAuthEphemeralResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "create", - "--ephemeral=true", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var preAuthEphemeralKey v1.PreAuthKey - err = json.Unmarshal([]byte(preAuthEphemeralResult), &preAuthEphemeralKey) - assert.Nil(s.T(), err) - - assert.True(s.T(), preAuthEphemeralKey.GetEphemeral()) - assert.False(s.T(), preAuthEphemeralKey.GetReusable()) - - // TODO(kradalby): Evaluate if we need a case to test for reusable and ephemeral - // preAuthReusableAndEphemeralResult, err := ExecuteCommand( - // &s.headscale, - // []string{ - // "headscale", - // "preauthkeys", - // "--user", - // user.Name, - // "create", - // "--ephemeral", - // "--reusable", - // "--output", - // "json", - // }, - // []string{}, - // ) - // assert.NotNil(s.T(), err) - - // Test list of keys - listResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "preauthkeys", - "--user", - user.Name, - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listedPreAuthKeys []v1.PreAuthKey - err = json.Unmarshal([]byte(listResult), &listedPreAuthKeys) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listedPreAuthKeys, 2) -} - -func (s *IntegrationCLITestSuite) TestNodeTagCommand() { - user, err := s.createUser("machine-user") - assert.Nil(s.T(), err) - - machineKeys := []string{ - "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", - "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", - } - machines := make([]*v1.Machine, len(machineKeys)) - assert.Nil(s.T(), err) - - for index, machineKey := range machineKeys { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "debug", - "create-node", - "--name", - fmt.Sprintf("machine-%d", index+1), - "--user", - user.Name, - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - machineResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "--user", - user.Name, - "register", - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var machine v1.Machine - err = json.Unmarshal([]byte(machineResult), &machine) - assert.Nil(s.T(), err) - - machines[index] = &machine - } - assert.Len(s.T(), machines, len(machineKeys)) - - addTagResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "tag", - "-i", "1", - "-t", "tag:test", - "--output", "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var machine v1.Machine - err = json.Unmarshal([]byte(addTagResult), &machine) - assert.Nil(s.T(), err) - assert.Equal(s.T(), []string{"tag:test"}, machine.ForcedTags) - - // try to set a wrong tag and retrieve the error - wrongTagResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "tag", - "-i", "2", - "-t", "wrong-tag", - "--output", "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - type errOutput struct { - Error string `json:"error"` - } - var errorOutput errOutput - err = json.Unmarshal([]byte(wrongTagResult), &errorOutput) - assert.Nil(s.T(), err) - assert.Contains(s.T(), errorOutput.Error, "tag must start with the string 'tag:'") - - // Test list all nodes after added seconds - listAllResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", "json", - }, - []string{}, - ) - resultMachines := make([]*v1.Machine, len(machineKeys)) - assert.Nil(s.T(), err) - json.Unmarshal([]byte(listAllResult), &resultMachines) - found := false - for _, machine := range resultMachines { - if machine.ForcedTags != nil { - for _, tag := range machine.ForcedTags { - if tag == "tag:test" { - found = true - } - } - } - } - assert.Equal( - s.T(), - true, - found, - "should find a machine with the tag 'tag:test' in the list of machines", - ) -} - -func (s *IntegrationCLITestSuite) TestNodeCommand() { - user, err := s.createUser("machine-user") - assert.Nil(s.T(), err) - - secondUser, err := s.createUser("other-user") - assert.Nil(s.T(), err) - - // Randomly generated machine keys - machineKeys := []string{ - "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", - "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", - "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", - "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", - "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", - } - machines := make([]*v1.Machine, len(machineKeys)) - assert.Nil(s.T(), err) - - for index, machineKey := range machineKeys { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "debug", - "create-node", - "--name", - fmt.Sprintf("machine-%d", index+1), - "--user", - user.Name, - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - machineResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "--user", - user.Name, - "register", - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var machine v1.Machine - err = json.Unmarshal([]byte(machineResult), &machine) - assert.Nil(s.T(), err) - - machines[index] = &machine - } - - assert.Len(s.T(), machines, len(machineKeys)) - - // Test list all nodes after added seconds - listAllResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listAll []v1.Machine - err = json.Unmarshal([]byte(listAllResult), &listAll) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listAll, 5) - - assert.Equal(s.T(), uint64(1), listAll[0].Id) - assert.Equal(s.T(), uint64(2), listAll[1].Id) - assert.Equal(s.T(), uint64(3), listAll[2].Id) - assert.Equal(s.T(), uint64(4), listAll[3].Id) - assert.Equal(s.T(), uint64(5), listAll[4].Id) - - assert.Equal(s.T(), "machine-1", listAll[0].Name) - assert.Equal(s.T(), "machine-2", listAll[1].Name) - assert.Equal(s.T(), "machine-3", listAll[2].Name) - assert.Equal(s.T(), "machine-4", listAll[3].Name) - assert.Equal(s.T(), "machine-5", listAll[4].Name) - - otherUserMachineKeys := []string{ - "nodekey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e", - "nodekey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584", - } - otherUserMachines := make([]*v1.Machine, len(otherUserMachineKeys)) - assert.Nil(s.T(), err) - - for index, machineKey := range otherUserMachineKeys { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "debug", - "create-node", - "--name", - fmt.Sprintf("otherUser-machine-%d", index+1), - "--user", - secondUser.Name, - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - machineResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "--user", - secondUser.Name, - "register", - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var machine v1.Machine - err = json.Unmarshal([]byte(machineResult), &machine) - assert.Nil(s.T(), err) - - otherUserMachines[index] = &machine - } - - assert.Len(s.T(), otherUserMachines, len(otherUserMachineKeys)) - - // Test list all nodes after added otherUser - listAllWithotherUserResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listAllWithotherUser []v1.Machine - err = json.Unmarshal( - []byte(listAllWithotherUserResult), - &listAllWithotherUser, - ) - assert.Nil(s.T(), err) - - // All nodes, machines + otherUser - assert.Len(s.T(), listAllWithotherUser, 7) - - assert.Equal(s.T(), uint64(6), listAllWithotherUser[5].Id) - assert.Equal(s.T(), uint64(7), listAllWithotherUser[6].Id) - - assert.Equal(s.T(), "otherUser-machine-1", listAllWithotherUser[5].Name) - assert.Equal(s.T(), "otherUser-machine-2", listAllWithotherUser[6].Name) - - // Test list all nodes after added otherUser - listOnlyotherUserMachineUserResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--user", - secondUser.Name, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listOnlyotherUserMachineUser []v1.Machine - err = json.Unmarshal( - []byte(listOnlyotherUserMachineUserResult), - &listOnlyotherUserMachineUser, - ) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listOnlyotherUserMachineUser, 2) - - assert.Equal(s.T(), uint64(6), listOnlyotherUserMachineUser[0].Id) - assert.Equal(s.T(), uint64(7), listOnlyotherUserMachineUser[1].Id) - - assert.Equal( - s.T(), - "otherUser-machine-1", - listOnlyotherUserMachineUser[0].Name, - ) - assert.Equal( - s.T(), - "otherUser-machine-2", - listOnlyotherUserMachineUser[1].Name, - ) - - // Delete a machines - _, _, err = ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "delete", - "--identifier", - // Delete the last added machine - "4", - "--output", - "json", - "--force", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - // Test: list main user after machine is deleted - listOnlyMachineUserAfterDeleteResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--user", - user.Name, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listOnlyMachineUserAfterDelete []v1.Machine - err = json.Unmarshal( - []byte(listOnlyMachineUserAfterDeleteResult), - &listOnlyMachineUserAfterDelete, - ) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listOnlyMachineUserAfterDelete, 4) -} - -func (s *IntegrationCLITestSuite) TestNodeExpireCommand() { - user, err := s.createUser("machine-expire-user") - assert.Nil(s.T(), err) - - // Randomly generated machine keys - machineKeys := []string{ - "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", - "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", - "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", - "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", - "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", - } - machines := make([]*v1.Machine, len(machineKeys)) - assert.Nil(s.T(), err) - - for index, machineKey := range machineKeys { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "debug", - "create-node", - "--name", - fmt.Sprintf("machine-%d", index+1), - "--user", - user.Name, - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - machineResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "--user", - user.Name, - "register", - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var machine v1.Machine - err = json.Unmarshal([]byte(machineResult), &machine) - assert.Nil(s.T(), err) - - machines[index] = &machine - } - - assert.Len(s.T(), machines, len(machineKeys)) - - listAllResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listAll []v1.Machine - err = json.Unmarshal([]byte(listAllResult), &listAll) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listAll, 5) - - assert.True(s.T(), listAll[0].Expiry.AsTime().IsZero()) - assert.True(s.T(), listAll[1].Expiry.AsTime().IsZero()) - assert.True(s.T(), listAll[2].Expiry.AsTime().IsZero()) - assert.True(s.T(), listAll[3].Expiry.AsTime().IsZero()) - assert.True(s.T(), listAll[4].Expiry.AsTime().IsZero()) - - for i := 0; i < 3; i++ { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "expire", - "--identifier", - fmt.Sprintf("%d", listAll[i].Id), - }, - []string{}, - ) - assert.Nil(s.T(), err) - } - - listAllAfterExpiryResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listAllAfterExpiry []v1.Machine - err = json.Unmarshal([]byte(listAllAfterExpiryResult), &listAllAfterExpiry) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listAllAfterExpiry, 5) - - assert.True(s.T(), listAllAfterExpiry[0].Expiry.AsTime().Before(time.Now())) - assert.True(s.T(), listAllAfterExpiry[1].Expiry.AsTime().Before(time.Now())) - assert.True(s.T(), listAllAfterExpiry[2].Expiry.AsTime().Before(time.Now())) - assert.True(s.T(), listAllAfterExpiry[3].Expiry.AsTime().IsZero()) - assert.True(s.T(), listAllAfterExpiry[4].Expiry.AsTime().IsZero()) -} - -func (s *IntegrationCLITestSuite) TestNodeRenameCommand() { - user, err := s.createUser("machine-rename-command") - assert.Nil(s.T(), err) - - // Randomly generated machine keys - machineKeys := []string{ - "nodekey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", - "nodekey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", - "nodekey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", - "nodekey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", - "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", - } - machines := make([]*v1.Machine, len(machineKeys)) - assert.Nil(s.T(), err) - - for index, machineKey := range machineKeys { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "debug", - "create-node", - "--name", - fmt.Sprintf("machine-%d", index+1), - "--user", - user.Name, - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - machineResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "--user", - user.Name, - "register", - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var machine v1.Machine - err = json.Unmarshal([]byte(machineResult), &machine) - assert.Nil(s.T(), err) - - machines[index] = &machine - } - - assert.Len(s.T(), machines, len(machineKeys)) - - listAllResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listAll []v1.Machine - err = json.Unmarshal([]byte(listAllResult), &listAll) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listAll, 5) - - assert.Contains(s.T(), listAll[0].GetGivenName(), "machine-1") - assert.Contains(s.T(), listAll[1].GetGivenName(), "machine-2") - assert.Contains(s.T(), listAll[2].GetGivenName(), "machine-3") - assert.Contains(s.T(), listAll[3].GetGivenName(), "machine-4") - assert.Contains(s.T(), listAll[4].GetGivenName(), "machine-5") - - for i := 0; i < 3; i++ { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "rename", - "--identifier", - fmt.Sprintf("%d", listAll[i].Id), - fmt.Sprintf("newmachine-%d", i+1), - }, - []string{}, - ) - assert.Nil(s.T(), err) - } - - listAllAfterRenameResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listAllAfterRename []v1.Machine - err = json.Unmarshal([]byte(listAllAfterRenameResult), &listAllAfterRename) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listAllAfterRename, 5) - - assert.Equal(s.T(), "newmachine-1", listAllAfterRename[0].GetGivenName()) - assert.Equal(s.T(), "newmachine-2", listAllAfterRename[1].GetGivenName()) - assert.Equal(s.T(), "newmachine-3", listAllAfterRename[2].GetGivenName()) - assert.Contains(s.T(), listAllAfterRename[3].GetGivenName(), "machine-4") - assert.Contains(s.T(), listAllAfterRename[4].GetGivenName(), "machine-5") - - // Test failure for too long names - result, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "rename", - "--identifier", - fmt.Sprintf("%d", listAll[4].Id), - "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890", - }, - []string{}, - ) - assert.Nil(s.T(), err) - assert.Contains(s.T(), result, "not be over 63 chars") - - listAllAfterRenameAttemptResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listAllAfterRenameAttempt []v1.Machine - err = json.Unmarshal( - []byte(listAllAfterRenameAttemptResult), - &listAllAfterRenameAttempt, - ) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listAllAfterRenameAttempt, 5) - - assert.Equal(s.T(), "newmachine-1", listAllAfterRenameAttempt[0].GetGivenName()) - assert.Equal(s.T(), "newmachine-2", listAllAfterRenameAttempt[1].GetGivenName()) - assert.Equal(s.T(), "newmachine-3", listAllAfterRenameAttempt[2].GetGivenName()) - assert.Contains(s.T(), listAllAfterRenameAttempt[3].GetGivenName(), "machine-4") - assert.Contains(s.T(), listAllAfterRenameAttempt[4].GetGivenName(), "machine-5") -} - -func (s *IntegrationCLITestSuite) TestApiKeyCommand() { - count := 5 - - keys := make([]string, count) - - for i := 0; i < count; i++ { - apiResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "apikeys", - "create", - "--expiration", - "24h", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - assert.NotEmpty(s.T(), apiResult) - - // var apiKey v1.ApiKey - // err = json.Unmarshal([]byte(apiResult), &apiKey) - // assert.Nil(s.T(), err) - - keys[i] = apiResult - } - - assert.Len(s.T(), keys, 5) - - // Test list of keys - listResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "apikeys", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listedApiKeys []v1.ApiKey - err = json.Unmarshal([]byte(listResult), &listedApiKeys) - assert.Nil(s.T(), err) - - assert.Len(s.T(), listedApiKeys, 5) - - assert.Equal(s.T(), uint64(1), listedApiKeys[0].Id) - assert.Equal(s.T(), uint64(2), listedApiKeys[1].Id) - assert.Equal(s.T(), uint64(3), listedApiKeys[2].Id) - assert.Equal(s.T(), uint64(4), listedApiKeys[3].Id) - assert.Equal(s.T(), uint64(5), listedApiKeys[4].Id) - - assert.NotEmpty(s.T(), listedApiKeys[0].Prefix) - assert.NotEmpty(s.T(), listedApiKeys[1].Prefix) - assert.NotEmpty(s.T(), listedApiKeys[2].Prefix) - assert.NotEmpty(s.T(), listedApiKeys[3].Prefix) - assert.NotEmpty(s.T(), listedApiKeys[4].Prefix) - - assert.True(s.T(), listedApiKeys[0].Expiration.AsTime().After(time.Now())) - assert.True(s.T(), listedApiKeys[1].Expiration.AsTime().After(time.Now())) - assert.True(s.T(), listedApiKeys[2].Expiration.AsTime().After(time.Now())) - assert.True(s.T(), listedApiKeys[3].Expiration.AsTime().After(time.Now())) - assert.True(s.T(), listedApiKeys[4].Expiration.AsTime().After(time.Now())) - - assert.True( - s.T(), - listedApiKeys[0].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - assert.True( - s.T(), - listedApiKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - assert.True( - s.T(), - listedApiKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - assert.True( - s.T(), - listedApiKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - assert.True( - s.T(), - listedApiKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), - ) - - expiredPrefixes := make(map[string]bool) - - // Expire three keys - for i := 0; i < 3; i++ { - _, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "apikeys", - "expire", - "--prefix", - listedApiKeys[i].Prefix, - }, - []string{}, - ) - assert.Nil(s.T(), err) - - expiredPrefixes[listedApiKeys[i].Prefix] = true - } - - // Test list pre auth keys after expire - listAfterExpireResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "apikeys", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var listedAfterExpireApiKeys []v1.ApiKey - err = json.Unmarshal([]byte(listAfterExpireResult), &listedAfterExpireApiKeys) - assert.Nil(s.T(), err) - - for index := range listedAfterExpireApiKeys { - if _, ok := expiredPrefixes[listedAfterExpireApiKeys[index].Prefix]; ok { - // Expired - assert.True( - s.T(), - listedAfterExpireApiKeys[index].Expiration.AsTime().Before(time.Now()), - ) - } else { - // Not expired - assert.False( - s.T(), - listedAfterExpireApiKeys[index].Expiration.AsTime().Before(time.Now()), - ) - } - } -} - -func (s *IntegrationCLITestSuite) TestNodeMoveCommand() { - oldUser, err := s.createUser("old-user") - assert.Nil(s.T(), err) - newUser, err := s.createUser("new-user") - assert.Nil(s.T(), err) - - // Randomly generated machine key - machineKey := "nodekey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa" - - _, _, err = ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "debug", - "create-node", - "--name", - "nomad-machine", - "--user", - oldUser.Name, - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - machineResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "--user", - oldUser.Name, - "register", - "--key", - machineKey, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var machine v1.Machine - err = json.Unmarshal([]byte(machineResult), &machine) - assert.Nil(s.T(), err) - - assert.Equal(s.T(), uint64(1), machine.Id) - assert.Equal(s.T(), "nomad-machine", machine.Name) - assert.Equal(s.T(), machine.User.Name, oldUser.Name) - - machineId := fmt.Sprintf("%d", machine.Id) - - moveToNewNSResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "move", - "--identifier", - machineId, - "--user", - newUser.Name, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - err = json.Unmarshal([]byte(moveToNewNSResult), &machine) - assert.Nil(s.T(), err) - - assert.Equal(s.T(), machine.User, newUser) - - listAllNodesResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - var allNodes []v1.Machine - err = json.Unmarshal([]byte(listAllNodesResult), &allNodes) - assert.Nil(s.T(), err) - - assert.Len(s.T(), allNodes, 1) - - assert.Equal(s.T(), allNodes[0].Id, machine.Id) - assert.Equal(s.T(), allNodes[0].User, machine.User) - assert.Equal(s.T(), allNodes[0].User, newUser) - - moveToNonExistingNSResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "move", - "--identifier", - machineId, - "--user", - "non-existing-user", - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - assert.Contains( - s.T(), - string(moveToNonExistingNSResult), - "User not found", - ) - assert.Equal(s.T(), machine.User, newUser) - - moveToOldNSResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "move", - "--identifier", - machineId, - "--user", - oldUser.Name, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - err = json.Unmarshal([]byte(moveToOldNSResult), &machine) - assert.Nil(s.T(), err) - - assert.Equal(s.T(), machine.User, oldUser) - - moveToSameNSResult, _, err := ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "nodes", - "move", - "--identifier", - machineId, - "--user", - oldUser.Name, - "--output", - "json", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - err = json.Unmarshal([]byte(moveToSameNSResult), &machine) - assert.Nil(s.T(), err) - - assert.Equal(s.T(), machine.User, oldUser) -} - -func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() { - // TODO: make sure defaultConfig is not same as altConfig - defaultConfig, err := os.ReadFile("integration_test/etc/config.dump.gold.yaml") - assert.Nil(s.T(), err) - altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml") - assert.Nil(s.T(), err) - altEnvConfig, err := os.ReadFile("integration_test/etc/alt-env-config.dump.gold.yaml") - assert.Nil(s.T(), err) - - _, _, err = ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "dumpConfig", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - defaultDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml") - assert.Nil(s.T(), err) - - assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig)) - - _, _, err = ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "-c", - "/etc/headscale/alt-config.yaml", - "dumpConfig", - }, - []string{}, - ) - assert.Nil(s.T(), err) - - altDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml") - assert.Nil(s.T(), err) - - assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig)) - - _, _, err = ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "dumpConfig", - }, - []string{ - "HEADSCALE_CONFIG=/etc/headscale/alt-env-config.yaml", - }, - ) - assert.Nil(s.T(), err) - - altEnvDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml") - assert.Nil(s.T(), err) - - assert.YAMLEq(s.T(), string(altEnvConfig), string(altEnvDumpConfig)) - - _, _, err = ExecuteCommand( - &s.headscale, - []string{ - "headscale", - "-c", - "/etc/headscale/alt-config.yaml", - "dumpConfig", - }, - []string{ - "HEADSCALE_CONFIG=/etc/headscale/alt-env-config.yaml", - }, - ) - assert.Nil(s.T(), err) - - altDumpConfig, err = os.ReadFile("integration_test/etc/config.dump.yaml") - assert.Nil(s.T(), err) - - assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig)) -} diff --git a/integration_common_test.go b/integration_common_test.go deleted file mode 100644 index edc0280a5c..0000000000 --- a/integration_common_test.go +++ /dev/null @@ -1,271 +0,0 @@ -// nolint -package headscale - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "net/netip" - "os" - "strconv" - "time" - - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" -) - -const ( - headscaleNetwork = "headscale-test" - headscaleHostname = "headscale" - DOCKER_EXECUTE_TIMEOUT = 10 * time.Second -) - -var ( - errEnvVarEmpty = errors.New("getenv: environment variable empty") - - IpPrefix4 = netip.MustParsePrefix("100.64.0.0/10") - IpPrefix6 = netip.MustParsePrefix("fd7a:115c:a1e0::/48") - - tailscaleVersions = []string{ - "head", - "unstable", - "1.38.4", - "1.36.2", - "1.34.2", - "1.32.3", - "1.30.2", - "1.28.0", - "1.26.2", - "1.24.2", - "1.22.2", - "1.20.4", - "1.18.2", - "1.16.2", - "1.14.3", - "1.12.3", - } -) - -type ExecuteCommandConfig struct { - timeout time.Duration -} - -type ExecuteCommandOption func(*ExecuteCommandConfig) error - -func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption { - return ExecuteCommandOption(func(conf *ExecuteCommandConfig) error { - conf.timeout = timeout - return nil - }) -} - -func ExecuteCommand( - resource *dockertest.Resource, - cmd []string, - env []string, - options ...ExecuteCommandOption, -) (string, string, error) { - var stdout bytes.Buffer - var stderr bytes.Buffer - - execConfig := ExecuteCommandConfig{ - timeout: DOCKER_EXECUTE_TIMEOUT, - } - - for _, opt := range options { - if err := opt(&execConfig); err != nil { - return "", "", fmt.Errorf("execute-command/options: %w", err) - } - } - - type result struct { - exitCode int - err error - } - - resultChan := make(chan result, 1) - - // Run your long running function in it's own goroutine and pass back it's - // response into our channel. - go func() { - exitCode, err := resource.Exec( - cmd, - dockertest.ExecOptions{ - Env: append(env, "HEADSCALE_LOG_LEVEL=disabled"), - StdOut: &stdout, - StdErr: &stderr, - }, - ) - resultChan <- result{exitCode, err} - }() - - // Listen on our channel AND a timeout channel - which ever happens first. - select { - case res := <-resultChan: - if res.err != nil { - return stdout.String(), stderr.String(), res.err - } - - if res.exitCode != 0 { - fmt.Println("Command: ", cmd) - fmt.Println("stdout: ", stdout.String()) - fmt.Println("stderr: ", stderr.String()) - - return stdout.String(), stderr.String(), fmt.Errorf( - "command failed with: %s", - stderr.String(), - ) - } - - return stdout.String(), stderr.String(), nil - case <-time.After(execConfig.timeout): - - return stdout.String(), stderr.String(), fmt.Errorf( - "command timed out after %s", - execConfig.timeout, - ) - } -} - -func DockerRestartPolicy(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself on error *immediately*. - // when set to false, containers remain until the end of the integration test. - config.AutoRemove = false - config.RestartPolicy = docker.RestartPolicy{ - Name: "no", - } -} - -func DockerAllowLocalIPv6(config *docker.HostConfig) { - if config.Sysctls == nil { - config.Sysctls = make(map[string]string, 1) - } - config.Sysctls["net.ipv6.conf.all.disable_ipv6"] = "0" -} - -func DockerAllowNetworkAdministration(config *docker.HostConfig) { - config.CapAdd = append(config.CapAdd, "NET_ADMIN") - config.Mounts = append(config.Mounts, docker.HostMount{ - Type: "bind", - Source: "/dev/net/tun", - Target: "/dev/net/tun", - }) -} - -func getDockerBuildOptions(version string) *dockertest.BuildOptions { - var tailscaleBuildOptions *dockertest.BuildOptions - switch version { - case "head": - tailscaleBuildOptions = &dockertest.BuildOptions{ - Dockerfile: "Dockerfile.tailscale-HEAD", - ContextDir: ".", - BuildArgs: []docker.BuildArg{}, - } - case "unstable": - tailscaleBuildOptions = &dockertest.BuildOptions{ - Dockerfile: "Dockerfile.tailscale", - ContextDir: ".", - BuildArgs: []docker.BuildArg{ - { - Name: "TAILSCALE_VERSION", - Value: "*", // Installs the latest version https://askubuntu.com/a/824926 - }, - { - Name: "TAILSCALE_CHANNEL", - Value: "unstable", - }, - }, - } - default: - tailscaleBuildOptions = &dockertest.BuildOptions{ - Dockerfile: "Dockerfile.tailscale", - ContextDir: ".", - BuildArgs: []docker.BuildArg{ - { - Name: "TAILSCALE_VERSION", - Value: version, - }, - { - Name: "TAILSCALE_CHANNEL", - Value: "stable", - }, - }, - } - } - return tailscaleBuildOptions -} - -func getDNSNames( - headscale *dockertest.Resource, -) ([]string, error) { - listAllResult, _, err := ExecuteCommand( - headscale, - []string{ - "headscale", - "nodes", - "list", - "--output", - "json", - }, - []string{}, - ) - if err != nil { - return nil, err - } - - var listAll []v1.Machine - err = json.Unmarshal([]byte(listAllResult), &listAll) - if err != nil { - return nil, err - } - - hostnames := make([]string, len(listAll)) - - for index := range listAll { - hostnames[index] = listAll[index].GetGivenName() - } - - return hostnames, nil -} - -func GetEnvStr(key string) (string, error) { - v := os.Getenv(key) - if v == "" { - return v, errEnvVarEmpty - } - - return v, nil -} - -func GetEnvBool(key string) (bool, error) { - s, err := GetEnvStr(key) - if err != nil { - return false, err - } - v, err := strconv.ParseBool(s) - if err != nil { - return false, err - } - - return v, nil -} - -func GetFirstOrCreateNetwork(pool *dockertest.Pool, name string) (dockertest.Network, error) { - networks, err := pool.NetworksByName(name) - if err != nil || len(networks) == 0 { - if _, err := pool.CreateNetwork(name); err == nil { - // Create does not give us an updated version of the resource, so we need to - // get it again. - networks, err := pool.NetworksByName(name) - if err != nil { - return dockertest.Network{}, err - } - - return networks[0], nil - } - } - - return networks[0], nil -} diff --git a/integration_test/.gitignore b/integration_test/.gitignore deleted file mode 100644 index 4e9cb7a149..0000000000 --- a/integration_test/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -derp.yaml -*.sqlite -*.sqlite3 diff --git a/integration_test/etc/alt-config.dump.gold.yaml b/integration_test/etc/alt-config.dump.gold.yaml deleted file mode 100644 index aeb33a4390..0000000000 --- a/integration_test/etc/alt-config.dump.gold.yaml +++ /dev/null @@ -1,57 +0,0 @@ -acl_policy_path: "" -cli: - insecure: false - timeout: 5s -db_path: /tmp/integration_test_db.sqlite3 -db_ssl: false -db_type: sqlite3 -derp: - auto_update_enabled: false - server: - enabled: false - stun: - enabled: true - update_frequency: 1m - urls: - - https://controlplane.tailscale.com/derpmap/default -dns_config: - override_local_dns: true - base_domain: headscale.net - domains: [] - magic_dns: true - nameservers: - - 127.0.0.11 - - 1.1.1.1 -ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 10s -grpc_allow_insecure: false -grpc_listen_addr: :50443 -ip_prefixes: - - fd7a:115c:a1e0::/48 - - 100.64.0.0/10 -listen_addr: 0.0.0.0:18080 -log: - level: disabled - format: text -logtail: - enabled: false -metrics_listen_addr: 127.0.0.1:19090 -oidc: - expiry: 180d - only_start_if_oidc_is_available: true - scope: - - openid - - profile - - email - strip_email_domain: true - use_username_claim: false - use_expiry_from_token: false -private_key_path: private.key -noise: - private_key_path: noise_private.key -server_url: http://headscale:18080 -tls_letsencrypt_cache_dir: /var/www/.cache -tls_letsencrypt_challenge_type: HTTP-01 -unix_socket: /var/run/headscale.sock -unix_socket_permission: "0o770" -randomize_client_port: false diff --git a/integration_test/etc/alt-config.yaml b/integration_test/etc/alt-config.yaml deleted file mode 100644 index 555678614a..0000000000 --- a/integration_test/etc/alt-config.yaml +++ /dev/null @@ -1,31 +0,0 @@ -log: - level: trace -acl_policy_path: "" -db_type: sqlite3 -ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 10s -ip_prefixes: - - fd7a:115c:a1e0::/48 - - 100.64.0.0/10 -dns_config: - override_local_dns: true - base_domain: headscale.net - magic_dns: true - domains: [] - nameservers: - - 127.0.0.11 - - 1.1.1.1 -db_path: /tmp/integration_test_db.sqlite3 -db_ssl: false -private_key_path: private.key -noise: - private_key_path: noise_private.key -listen_addr: 0.0.0.0:18080 -metrics_listen_addr: 127.0.0.1:19090 -server_url: http://headscale:18080 - -derp: - urls: - - https://controlplane.tailscale.com/derpmap/default - auto_update_enabled: false - update_frequency: 1m diff --git a/integration_test/etc/alt-env-config.dump.gold.yaml b/integration_test/etc/alt-env-config.dump.gold.yaml deleted file mode 100644 index b839660479..0000000000 --- a/integration_test/etc/alt-env-config.dump.gold.yaml +++ /dev/null @@ -1,56 +0,0 @@ -acl_policy_path: "" -cli: - insecure: false - timeout: 5s -db_path: /tmp/integration_test_db.sqlite3 -db_ssl: false -db_type: sqlite3 -derp: - auto_update_enabled: false - server: - enabled: false - stun: - enabled: true - update_frequency: 1m - urls: - - https://controlplane.tailscale.com/derpmap/default -dns_config: - override_local_dns: true - base_domain: headscale.net - domains: [] - magic_dns: true - nameservers: - - 1.1.1.1 -ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 30s -grpc_allow_insecure: false -grpc_listen_addr: :50443 -ip_prefixes: - - fd7a:115c:a1e0::/48 - - 100.64.0.0/10 -listen_addr: 0.0.0.0:18080 -log: - level: disabled - format: text -logtail: - enabled: false -metrics_listen_addr: 127.0.0.1:19090 -oidc: - expiry: 180d - only_start_if_oidc_is_available: true - scope: - - openid - - profile - - email - strip_email_domain: true - use_username_claim: false - use_expiry_from_token: false -private_key_path: private.key -noise: - private_key_path: noise_private.key -server_url: http://headscale:18080 -tls_letsencrypt_cache_dir: /var/www/.cache -tls_letsencrypt_challenge_type: HTTP-01 -unix_socket: /var/run/headscale.sock -unix_socket_permission: "0o770" -randomize_client_port: false diff --git a/integration_test/etc/alt-env-config.yaml b/integration_test/etc/alt-env-config.yaml deleted file mode 100644 index 2410ca64f6..0000000000 --- a/integration_test/etc/alt-env-config.yaml +++ /dev/null @@ -1,30 +0,0 @@ -log: - level: trace -acl_policy_path: "" -db_type: sqlite3 -ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 30s -ip_prefixes: - - fd7a:115c:a1e0::/48 - - 100.64.0.0/10 -dns_config: - override_local_dns: true - base_domain: headscale.net - magic_dns: true - domains: [] - nameservers: - - 1.1.1.1 -db_path: /tmp/integration_test_db.sqlite3 -db_ssl: false -private_key_path: private.key -noise: - private_key_path: noise_private.key -listen_addr: 0.0.0.0:18080 -metrics_listen_addr: 127.0.0.1:19090 -server_url: http://headscale:18080 - -derp: - urls: - - https://controlplane.tailscale.com/derpmap/default - auto_update_enabled: false - update_frequency: 1m diff --git a/integration_test/etc/config.dump.gold.yaml b/integration_test/etc/config.dump.gold.yaml deleted file mode 100644 index 683ed0aca9..0000000000 --- a/integration_test/etc/config.dump.gold.yaml +++ /dev/null @@ -1,57 +0,0 @@ -acl_policy_path: "" -cli: - insecure: false - timeout: 5s -db_path: /tmp/integration_test_db.sqlite3 -db_ssl: false -db_type: sqlite3 -derp: - auto_update_enabled: false - server: - enabled: false - stun: - enabled: true - update_frequency: 1m - urls: - - https://controlplane.tailscale.com/derpmap/default -dns_config: - override_local_dns: true - base_domain: headscale.net - domains: [] - magic_dns: true - nameservers: - - 127.0.0.11 - - 1.1.1.1 -ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 10s -grpc_allow_insecure: false -grpc_listen_addr: :50443 -ip_prefixes: - - fd7a:115c:a1e0::/48 - - 100.64.0.0/10 -listen_addr: 0.0.0.0:8080 -log: - format: text - level: disabled -logtail: - enabled: false -metrics_listen_addr: 127.0.0.1:9090 -oidc: - expiry: 180d - only_start_if_oidc_is_available: true - scope: - - openid - - profile - - email - strip_email_domain: true - use_username_claim: false - use_expiry_from_token: false -private_key_path: private.key -noise: - private_key_path: noise_private.key -server_url: http://headscale:8080 -tls_letsencrypt_cache_dir: /var/www/.cache -tls_letsencrypt_challenge_type: HTTP-01 -unix_socket: /var/run/headscale.sock -unix_socket_permission: "0o770" -randomize_client_port: false diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml deleted file mode 100644 index efe75fec1f..0000000000 --- a/integration_test/etc/config.yaml +++ /dev/null @@ -1,30 +0,0 @@ -log: - level: trace -acl_policy_path: "" -db_type: sqlite3 -ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 10s -ip_prefixes: - - fd7a:115c:a1e0::/48 - - 100.64.0.0/10 -dns_config: - override_local_dns: true - base_domain: headscale.net - magic_dns: true - domains: [] - nameservers: - - 127.0.0.11 - - 1.1.1.1 -db_path: /tmp/integration_test_db.sqlite3 -private_key_path: private.key -noise: - private_key_path: noise_private.key -listen_addr: 0.0.0.0:8080 -metrics_listen_addr: 127.0.0.1:9090 -server_url: http://headscale:8080 - -derp: - urls: - - https://controlplane.tailscale.com/derpmap/default - auto_update_enabled: false - update_frequency: 1m diff --git a/integration_test/etc_embedded_derp/config.yaml b/integration_test/etc_embedded_derp/config.yaml deleted file mode 100644 index ed4d51a075..0000000000 --- a/integration_test/etc_embedded_derp/config.yaml +++ /dev/null @@ -1,31 +0,0 @@ -log_level: trace -acl_policy_path: "" -db_type: sqlite3 -ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 10s -ip_prefixes: - - fd7a:115c:a1e0::/48 - - 100.64.0.0/10 -dns_config: - base_domain: headscale.net - magic_dns: true - domains: [] - nameservers: - - 1.1.1.1 -db_path: /tmp/integration_test_db.sqlite3 -private_key_path: private.key -noise: - private_key_path: noise_private.key -listen_addr: 0.0.0.0:443 -server_url: https://headscale:443 -tls_cert_path: "/etc/headscale/tls/server.crt" -tls_key_path: "/etc/headscale/tls/server.key" -tls_client_auth_mode: disabled -derp: - server: - enabled: true - region_id: 999 - region_code: "headscale" - region_name: "Headscale Embedded DERP" - - stun_listen_addr: "0.0.0.0:3478" diff --git a/integration_test/etc_embedded_derp/tls/server.crt b/integration_test/etc_embedded_derp/tls/server.crt deleted file mode 100644 index 9555649571..0000000000 --- a/integration_test/etc_embedded_derp/tls/server.crt +++ /dev/null @@ -1,22 +0,0 @@ - ------BEGIN CERTIFICATE----- -MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx -MTA0MTY0ODAzWjAUMRIwEAYDVQQDDAloZWFkc2NhbGUwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDqcfpToLZUF0rlNwXkkt3lbyw4Cl4TJdx36o2PKaOK -U+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1WATuQJlMeg+2UJXGaTGRKkkbPMy3 -5m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6sXmNeETJvBixpBev9yKJuVXgqHNS4 -NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH14rav8Uimonl8UTNVXufMzyUOuoaQ -TGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3uQJXy0m8I6OrIoXLNxnqYMfFls79 -9SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJRG1E3ZiLAgMBAAGjOjA4MBQGA1Ud -EQQNMAuCCWhlYWRzY2FsZTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH -AwEwDQYJKoZIhvcNAQELBQADggEBANGlVN7NCsJaKz0k0nhlRGK+tcxn2p1PXN/i -Iy+JX8ahixPC4ocRwOhrXgb390ZXLLwq08HrWYRB/Wi1VUzCp5d8dVxvrR43dJ+v -L2EOBiIKgcu2C3pWW1qRR46/EoXUU9kSH2VNBvIhNufi32kEOidoDzxtQf6qVCoF -guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt -B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl -w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M= ------END CERTIFICATE----- - -(Expires on Nov 4 16:48:03 2521 GMT) - diff --git a/integration_test/etc_embedded_derp/tls/server.key b/integration_test/etc_embedded_derp/tls/server.key deleted file mode 100644 index 8a2df34be5..0000000000 --- a/integration_test/etc_embedded_derp/tls/server.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDqcfpToLZUF0rl -NwXkkt3lbyw4Cl4TJdx36o2PKaOKU+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1 -WATuQJlMeg+2UJXGaTGRKkkbPMy35m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6s -XmNeETJvBixpBev9yKJuVXgqHNS4NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH1 -4rav8Uimonl8UTNVXufMzyUOuoaQTGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3 -uQJXy0m8I6OrIoXLNxnqYMfFls799SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJ -RG1E3ZiLAgMBAAECggEBALu1Ni/u5Qy++YA8ZcN0s6UXNdhItLmv/q0kZuLQ+9et -CT8VZfFInLndTdsaXenDKLHdryunviFA8SV+q7P2lMbek+Xs735EiyMnMBFWxLIZ -FWNGOeQERGL19QCmLEOmEi2b+iWJQHlKaMWpbPXL3w11a+lKjIBNO4ALfoJ5QveZ -cGMKsJdm/mpqBvLeNeh2eAFk3Gp6sT1g80Ge8NkgyzFBNIqnut0eerM15kPTc6Qz -12JLaOXUuV3PrcB4PN4nOwrTDg88GDNOQtc1Pc9r4nOHyLfr8X7QEtj1wXSwmOuK -d6ynMnAmoxVA9wEnupLbil1bzohRzpsTpkmDruYaBEECgYEA/Z09I8D6mt2NVqIE -KyvLjBK39ijSV9r3/lvB2Ple2OOL5YQEd+yTrIFy+3zdUnDgD1zmNnXjmjvHZ9Lc -IFf2o06AF84QLNB5gLPdDQkGNFdDqUxljBrfAfE3oANmPS/B0SijMGOOOiDO2FtO -xl1nfRr78mswuRs9awoUWCdNRKUCgYEA7KaTYKIQW/FEjw9lshp74q5vbn6zoXF5 -7N8VkwI+bBVNvRbM9XZ8qhfgRdu9eXs5oL/N4mSYY54I8fA//pJ0Z2vpmureMm1V -mL5WBUmSD9DIbAchoK+sRiQhVmNMBQC6cHMABA7RfXvBeGvWrm9pKCS6ZLgLjkjp -PsmAcaXQcW8CgYEA2inAxljjOwUK6FNGsrxhxIT1qtNC3kCGxE+6WSNq67gSR8Vg -8qiX//T7LEslOB3RIGYRwxd2St7RkgZZRZllmOWWWuPwFhzf6E7RAL2akLvggGov -kG4tGEagSw2hjVDfsUT73ExHtMk0Jfmlsg33UC8+PDLpHtLH6qQpDAwC8+ECgYEA -o+AqOIWhvHmT11l7O915Ip1WzvZwYADbxLsrDnVEUsZh4epTHjvh0kvcY6PqTqCV -ZIrOANNWb811Nkz/k8NJVoD08PFp0xPBbZeIq/qpachTsfMyRzq/mobUiyUR9Hjv -ooUQYr78NOApNsG+lWbTNBhS9wI4BlzZIECbcJe5g4MCgYEAndRoy8S+S0Hx/S8a -O3hzXeDmivmgWqn8NVD4AKOovpkz4PaIVVQbAQkiNfAx8/DavPvjEKAbDezJ4ECV -j7IsOWtDVI7pd6eF9fTcECwisrda8aUoiOap8AQb48153Vx+g2N4Vy3uH0xJs4cz -TDALZPOBg8VlV+HEFDP43sp9Bf0= ------END PRIVATE KEY----- diff --git a/integration_test/etc_oidc/base_config.yaml b/integration_test/etc_oidc/base_config.yaml deleted file mode 100644 index cbfac529cf..0000000000 --- a/integration_test/etc_oidc/base_config.yaml +++ /dev/null @@ -1,21 +0,0 @@ -log_level: trace -acl_policy_path: "" -db_type: sqlite3 -ephemeral_node_inactivity_timeout: 30m -node_update_check_interval: 10s -ip_prefixes: - - fd7a:115c:a1e0::/48 - - 100.64.0.0/10 -db_path: /tmp/integration_test_db.sqlite3 -private_key_path: private.key -noise: - private_key_path: noise_private.key -listen_addr: 0.0.0.0:8443 -server_url: https://headscale-oidc:8443 -tls_cert_path: "/etc/headscale/tls/server.crt" -tls_key_path: "/etc/headscale/tls/server.key" -derp: - urls: - - https://controlplane.tailscale.com/derpmap/default - auto_update_enabled: true - update_frequency: 1m diff --git a/integration_test/etc_oidc/tls/server.crt b/integration_test/etc_oidc/tls/server.crt deleted file mode 100644 index 9555649571..0000000000 --- a/integration_test/etc_oidc/tls/server.crt +++ /dev/null @@ -1,22 +0,0 @@ - ------BEGIN CERTIFICATE----- -MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx -MTA0MTY0ODAzWjAUMRIwEAYDVQQDDAloZWFkc2NhbGUwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDqcfpToLZUF0rlNwXkkt3lbyw4Cl4TJdx36o2PKaOK -U+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1WATuQJlMeg+2UJXGaTGRKkkbPMy3 -5m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6sXmNeETJvBixpBev9yKJuVXgqHNS4 -NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH14rav8Uimonl8UTNVXufMzyUOuoaQ -TGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3uQJXy0m8I6OrIoXLNxnqYMfFls79 -9SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJRG1E3ZiLAgMBAAGjOjA4MBQGA1Ud -EQQNMAuCCWhlYWRzY2FsZTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH -AwEwDQYJKoZIhvcNAQELBQADggEBANGlVN7NCsJaKz0k0nhlRGK+tcxn2p1PXN/i -Iy+JX8ahixPC4ocRwOhrXgb390ZXLLwq08HrWYRB/Wi1VUzCp5d8dVxvrR43dJ+v -L2EOBiIKgcu2C3pWW1qRR46/EoXUU9kSH2VNBvIhNufi32kEOidoDzxtQf6qVCoF -guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt -B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl -w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M= ------END CERTIFICATE----- - -(Expires on Nov 4 16:48:03 2521 GMT) - diff --git a/integration_test/etc_oidc/tls/server.key b/integration_test/etc_oidc/tls/server.key deleted file mode 100644 index 8a2df34be5..0000000000 --- a/integration_test/etc_oidc/tls/server.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDqcfpToLZUF0rl -NwXkkt3lbyw4Cl4TJdx36o2PKaOKU+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1 -WATuQJlMeg+2UJXGaTGRKkkbPMy35m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6s -XmNeETJvBixpBev9yKJuVXgqHNS4NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH1 -4rav8Uimonl8UTNVXufMzyUOuoaQTGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3 -uQJXy0m8I6OrIoXLNxnqYMfFls799SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJ -RG1E3ZiLAgMBAAECggEBALu1Ni/u5Qy++YA8ZcN0s6UXNdhItLmv/q0kZuLQ+9et -CT8VZfFInLndTdsaXenDKLHdryunviFA8SV+q7P2lMbek+Xs735EiyMnMBFWxLIZ -FWNGOeQERGL19QCmLEOmEi2b+iWJQHlKaMWpbPXL3w11a+lKjIBNO4ALfoJ5QveZ -cGMKsJdm/mpqBvLeNeh2eAFk3Gp6sT1g80Ge8NkgyzFBNIqnut0eerM15kPTc6Qz -12JLaOXUuV3PrcB4PN4nOwrTDg88GDNOQtc1Pc9r4nOHyLfr8X7QEtj1wXSwmOuK -d6ynMnAmoxVA9wEnupLbil1bzohRzpsTpkmDruYaBEECgYEA/Z09I8D6mt2NVqIE -KyvLjBK39ijSV9r3/lvB2Ple2OOL5YQEd+yTrIFy+3zdUnDgD1zmNnXjmjvHZ9Lc -IFf2o06AF84QLNB5gLPdDQkGNFdDqUxljBrfAfE3oANmPS/B0SijMGOOOiDO2FtO -xl1nfRr78mswuRs9awoUWCdNRKUCgYEA7KaTYKIQW/FEjw9lshp74q5vbn6zoXF5 -7N8VkwI+bBVNvRbM9XZ8qhfgRdu9eXs5oL/N4mSYY54I8fA//pJ0Z2vpmureMm1V -mL5WBUmSD9DIbAchoK+sRiQhVmNMBQC6cHMABA7RfXvBeGvWrm9pKCS6ZLgLjkjp -PsmAcaXQcW8CgYEA2inAxljjOwUK6FNGsrxhxIT1qtNC3kCGxE+6WSNq67gSR8Vg -8qiX//T7LEslOB3RIGYRwxd2St7RkgZZRZllmOWWWuPwFhzf6E7RAL2akLvggGov -kG4tGEagSw2hjVDfsUT73ExHtMk0Jfmlsg33UC8+PDLpHtLH6qQpDAwC8+ECgYEA -o+AqOIWhvHmT11l7O915Ip1WzvZwYADbxLsrDnVEUsZh4epTHjvh0kvcY6PqTqCV -ZIrOANNWb811Nkz/k8NJVoD08PFp0xPBbZeIq/qpachTsfMyRzq/mobUiyUR9Hjv -ooUQYr78NOApNsG+lWbTNBhS9wI4BlzZIECbcJe5g4MCgYEAndRoy8S+S0Hx/S8a -O3hzXeDmivmgWqn8NVD4AKOovpkz4PaIVVQbAQkiNfAx8/DavPvjEKAbDezJ4ECV -j7IsOWtDVI7pd6eF9fTcECwisrda8aUoiOap8AQb48153Vx+g2N4Vy3uH0xJs4cz -TDALZPOBg8VlV+HEFDP43sp9Bf0= ------END PRIVATE KEY----- diff --git a/mkdocs.yml b/mkdocs.yml index 0bfb2f9388..75abcddbd7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,6 @@ site_name: Headscale site_url: https://juanfont.github.io/headscale +edit_uri: blob/main/docs/ # Change the master branch to main as we are using main as a main branch site_author: Headscale authors site_description: >- An open source, self-hosted implementation of the Tailscale control server. @@ -121,12 +122,14 @@ markdown_extensions: # Page tree nav: - Home: index.md + - FAQ: faq.md - Getting started: - Installation: - Linux: running-headscale-linux.md - OpenBSD: running-headscale-openbsd.md - Container: running-headscale-container.md - Configuration: + - Web UI: web-ui.md - OIDC authentication: oidc.md - Exit node: exit-node.md - Reverse proxy: reverse-proxy.md diff --git a/noise.go b/noise.go deleted file mode 100644 index 569671481e..0000000000 --- a/noise.go +++ /dev/null @@ -1,78 +0,0 @@ -package headscale - -import ( - "net/http" - - "github.com/gorilla/mux" - "github.com/rs/zerolog/log" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" - "tailscale.com/control/controlbase" - "tailscale.com/control/controlhttp" - "tailscale.com/net/netutil" -) - -const ( - // ts2021UpgradePath is the path that the server listens on for the WebSockets upgrade. - ts2021UpgradePath = "/ts2021" -) - -type ts2021App struct { - headscale *Headscale - - conn *controlbase.Conn -} - -// NoiseUpgradeHandler is to upgrade the connection and hijack the net.Conn -// in order to use the Noise-based TS2021 protocol. Listens in /ts2021. -func (h *Headscale) NoiseUpgradeHandler( - writer http.ResponseWriter, - req *http.Request, -) { - log.Trace().Caller().Msgf("Noise upgrade handler for client %s", req.RemoteAddr) - - upgrade := req.Header.Get("Upgrade") - if upgrade == "" { - // This probably means that the user is running Headscale behind an - // improperly configured reverse proxy. TS2021 requires WebSockets to - // be passed to Headscale. Let's give them a hint. - log.Warn(). - Caller(). - Msg("No Upgrade header in TS2021 request. If headscale is behind a reverse proxy, make sure it is configured to pass WebSockets through.") - http.Error(writer, "Internal error", http.StatusInternalServerError) - - return - } - - noiseConn, err := controlhttp.AcceptHTTP(req.Context(), writer, req, *h.noisePrivateKey, nil) - if err != nil { - log.Error().Err(err).Msg("noise upgrade failed") - http.Error(writer, err.Error(), http.StatusInternalServerError) - - return - } - - ts2021App := ts2021App{ - headscale: h, - conn: noiseConn, - } - - // This router is served only over the Noise connection, and exposes only the new API. - // - // The HTTP2 server that exposes this router is created for - // a single hijacked connection from /ts2021, using netutil.NewOneConnListener - router := mux.NewRouter() - - router.HandleFunc("/machine/register", ts2021App.NoiseRegistrationHandler). - Methods(http.MethodPost) - router.HandleFunc("/machine/map", ts2021App.NoisePollNetMapHandler) - - server := http.Server{ - ReadTimeout: HTTPReadTimeout, - } - server.Handler = h2c.NewHandler(router, &http2.Server{}) - err = server.Serve(netutil.NewOneConnListener(noiseConn, nil)) - if err != nil { - log.Info().Err(err).Msg("The HTTP2 server was closed") - } -} diff --git a/tests/acls/acl_policy_1.hujson b/tests/acls/acl_policy_1.hujson deleted file mode 100644 index dba403f15a..0000000000 --- a/tests/acls/acl_policy_1.hujson +++ /dev/null @@ -1,127 +0,0 @@ -{ - // Declare static groups of users beyond those in the identity service. - "groups": { - "group:example": [ - "user1@example.com", - "user2@example.com", - ], - "group:example2": [ - "user1@example.com", - "user2@example.com", - ], - }, - // Declare hostname aliases to use in place of IP addresses or subnets. - "hosts": { - "example-host-1": "100.100.100.100", - "example-host-2": "100.100.101.100/24", - }, - // Define who is allowed to use which tags. - "tagOwners": { - // Everyone in the montreal-admins or global-admins group are - // allowed to tag servers as montreal-webserver. - "tag:montreal-webserver": [ - "group:example", - ], - // Only a few admins are allowed to create API servers. - "tag:production": [ - "group:example", - "president@example.com", - ], - }, - // Access control lists. - "acls": [ - // Engineering users, plus the president, can access port 22 (ssh) - // and port 3389 (remote desktop protocol) on all servers, and all - // ports on git-server or ci-server. - { - "action": "accept", - "src": [ - "group:example2", - "192.168.1.0/24" - ], - "dst": [ - "*:22,3389", - "git-server:*", - "ci-server:*" - ], - }, - // Allow engineer users to access any port on a device tagged with - // tag:production. - { - "action": "accept", - "src": [ - "group:example" - ], - "dst": [ - "tag:production:*" - ], - }, - // Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts - // on both networks. - { - "action": "accept", - "src": [ - "example-host-2", - ], - "dst": [ - "example-host-1:*", - "192.168.1.0/24:*" - ], - }, - // Allow every user of your network to access anything on the network. - // Comment out this section if you want to define specific ACL - // restrictions above. - { - "action": "accept", - "src": [ - "*" - ], - "dst": [ - "*:*" - ], - }, - // All users in Montreal are allowed to access the Montreal web - // servers. - { - "action": "accept", - "src": [ - "example-host-1" - ], - "dst": [ - "tag:montreal-webserver:80,443" - ], - }, - // Montreal web servers are allowed to make outgoing connections to - // the API servers, but only on https port 443. - // In contrast, this doesn't grant API servers the right to initiate - // any connections. - { - "action": "accept", - "src": [ - "tag:montreal-webserver" - ], - "dst": [ - "tag:api-server:443" - ], - }, - ], - // Declare tests to check functionality of ACL rules - "tests": [ - { - "src": "user1@example.com", - "accept": [ - "example-host-1:22", - "example-host-2:80" - ], - "deny": [ - "exapmle-host-2:100" - ], - }, - { - "src": "user2@example.com", - "accept": [ - "100.60.3.4:22" - ], - }, - ], -} \ No newline at end of file diff --git a/tests/acls/acl_policy_autoapprovers.hujson b/tests/acls/acl_policy_autoapprovers.hujson deleted file mode 100644 index bf564d88f7..0000000000 --- a/tests/acls/acl_policy_autoapprovers.hujson +++ /dev/null @@ -1,24 +0,0 @@ -// This ACL validates autoApprovers support for -// exit nodes and advertised routes - -{ - "tagOwners": { - "tag:exit": ["test"], - }, - - "groups": { - "group:test": ["test"] - }, - - "acls": [ - {"action": "accept", "users": ["*"], "ports": ["*:*"]}, - ], - - "autoApprovers": { - "exitNode": ["tag:exit"], - "routes": { - "10.10.0.0/16": ["group:test"], - "10.11.0.0/16": ["test"], - } - } -} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_1.hujson b/tests/acls/acl_policy_basic_1.hujson deleted file mode 100644 index db78ea9c72..0000000000 --- a/tests/acls/acl_policy_basic_1.hujson +++ /dev/null @@ -1,24 +0,0 @@ -// This ACL is a very basic example to validate the -// expansion of hosts - - -{ - "hosts": { - "host-1": "100.100.100.100", - "subnet-1": "100.100.101.100/24", - }, - - "acls": [ - { - "action": "accept", - "src": [ - "subnet-1", - "192.168.1.0/24" - ], - "dst": [ - "*:22,3389", - "host-1:*", - ], - }, - ], -} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_groups.hujson b/tests/acls/acl_policy_basic_groups.hujson deleted file mode 100644 index a99568ad98..0000000000 --- a/tests/acls/acl_policy_basic_groups.hujson +++ /dev/null @@ -1,26 +0,0 @@ -// This ACL is used to test group expansion - -{ - "groups": { - "group:example": [ - "testuser", - ], - }, - - "hosts": { - "host-1": "100.100.100.100", - "subnet-1": "100.100.101.100/24", - }, - - "acls": [ - { - "action": "accept", - "src": [ - "group:example", - ], - "dst": [ - "host-1:*", - ], - }, - ], -} diff --git a/tests/acls/acl_policy_basic_protocols.hujson b/tests/acls/acl_policy_basic_protocols.hujson deleted file mode 100644 index 6772c5643f..0000000000 --- a/tests/acls/acl_policy_basic_protocols.hujson +++ /dev/null @@ -1,41 +0,0 @@ -// This ACL is used to test wildcards - -{ - "hosts": { - "host-1": "100.100.100.100", - "subnet-1": "100.100.101.100/24", - }, - - "acls": [ - { - "Action": "accept", - "src": [ - "*", - ], - "proto": "tcp", - "dst": [ - "host-1:*", - ], - }, - { - "Action": "accept", - "src": [ - "*", - ], - "proto": "udp", - "dst": [ - "host-1:53", - ], - }, - { - "Action": "accept", - "src": [ - "*", - ], - "proto": "icmp", - "dst": [ - "host-1:*", - ], - }, - ], -} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_range.hujson b/tests/acls/acl_policy_basic_range.hujson deleted file mode 100644 index 2a4208fb3a..0000000000 --- a/tests/acls/acl_policy_basic_range.hujson +++ /dev/null @@ -1,20 +0,0 @@ -// This ACL is used to test the port range expansion - -{ - "hosts": { - "host-1": "100.100.100.100", - "subnet-1": "100.100.101.100/24", - }, - - "acls": [ - { - "action": "accept", - "src": [ - "subnet-1", - ], - "dst": [ - "host-1:5400-5500", - ], - }, - ], -} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_user_as_user.hujson b/tests/acls/acl_policy_basic_user_as_user.hujson deleted file mode 100644 index 0009364c75..0000000000 --- a/tests/acls/acl_policy_basic_user_as_user.hujson +++ /dev/null @@ -1,20 +0,0 @@ -// This ACL is used to test namespace expansion - -{ - "hosts": { - "host-1": "100.100.100.100", - "subnet-1": "100.100.101.100/24", - }, - - "acls": [ - { - "action": "accept", - "src": [ - "testuser", - ], - "dst": [ - "host-1:*", - ], - }, - ], -} diff --git a/tests/acls/acl_policy_basic_wildcards.hujson b/tests/acls/acl_policy_basic_wildcards.hujson deleted file mode 100644 index e1a1f7148f..0000000000 --- a/tests/acls/acl_policy_basic_wildcards.hujson +++ /dev/null @@ -1,20 +0,0 @@ -// This ACL is used to test wildcards - -{ - "hosts": { - "host-1": "100.100.100.100", - "subnet-1": "100.100.101.100/24", - }, - - "acls": [ - { - "Action": "accept", - "src": [ - "*", - ], - "dst": [ - "host-1:*", - ], - }, - ], -} \ No newline at end of file diff --git a/tests/acls/acl_policy_basic_wildcards.yaml b/tests/acls/acl_policy_basic_wildcards.yaml deleted file mode 100644 index 4318fcd240..0000000000 --- a/tests/acls/acl_policy_basic_wildcards.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -hosts: - host-1: 100.100.100.100/32 - subnet-1: 100.100.101.100/24 -acls: - - action: accept - src: - - "*" - dst: - - host-1:* diff --git a/tests/acls/acl_policy_invalid.hujson b/tests/acls/acl_policy_invalid.hujson deleted file mode 100644 index 3684b1f163..0000000000 --- a/tests/acls/acl_policy_invalid.hujson +++ /dev/null @@ -1,125 +0,0 @@ -{ - // Declare static groups of users beyond those in the identity service. - "groups": { - "group:example": [ - "user1@example.com", - "user2@example.com", - ], - }, - // Declare hostname aliases to use in place of IP addresses or subnets. - "hosts": { - "example-host-1": "100.100.100.100", - "example-host-2": "100.100.101.100/24", - }, - // Define who is allowed to use which tags. - "tagOwners": { - // Everyone in the montreal-admins or global-admins group are - // allowed to tag servers as montreal-webserver. - "tag:montreal-webserver": [ - "group:montreal-admins", - "group:global-admins", - ], - // Only a few admins are allowed to create API servers. - "tag:api-server": [ - "group:global-admins", - "example-host-1", - ], - }, - // Access control lists. - "acls": [ - // Engineering users, plus the president, can access port 22 (ssh) - // and port 3389 (remote desktop protocol) on all servers, and all - // ports on git-server or ci-server. - { - "action": "accept", - "src": [ - "group:engineering", - "president@example.com" - ], - "dst": [ - "*:22,3389", - "git-server:*", - "ci-server:*" - ], - }, - // Allow engineer users to access any port on a device tagged with - // tag:production. - { - "action": "accept", - "src": [ - "group:engineers" - ], - "dst": [ - "tag:production:*" - ], - }, - // Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts - // on both networks. - { - "action": "accept", - "src": [ - "my-subnet", - "192.168.1.0/24" - ], - "dst": [ - "my-subnet:*", - "192.168.1.0/24:*" - ], - }, - // Allow every user of your network to access anything on the network. - // Comment out this section if you want to define specific ACL - // restrictions above. - { - "action": "accept", - "src": [ - "*" - ], - "dst": [ - "*:*" - ], - }, - // All users in Montreal are allowed to access the Montreal web - // servers. - { - "action": "accept", - "src": [ - "group:montreal-users" - ], - "dst": [ - "tag:montreal-webserver:80,443" - ], - }, - // Montreal web servers are allowed to make outgoing connections to - // the API servers, but only on https port 443. - // In contrast, this doesn't grant API servers the right to initiate - // any connections. - { - "action": "accept", - "src": [ - "tag:montreal-webserver" - ], - "dst": [ - "tag:api-server:443" - ], - }, - ], - // Declare tests to check functionality of ACL rules - "tests": [ - { - "src": "user1@example.com", - "accept": [ - "example-host-1:22", - "example-host-2:80" - ], - "deny": [ - "exapmle-host-2:100" - ], - }, - { - "src": "user2@example.com", - "accept": [ - "100.60.3.4:22" - ], - }, - ], -} \ No newline at end of file diff --git a/tests/acls/broken.hujson b/tests/acls/broken.hujson deleted file mode 100644 index 98232c64fc..0000000000 --- a/tests/acls/broken.hujson +++ /dev/null @@ -1 +0,0 @@ -{ diff --git a/tests/acls/invalid.hujson b/tests/acls/invalid.hujson deleted file mode 100644 index 733f6924d3..0000000000 --- a/tests/acls/invalid.hujson +++ /dev/null @@ -1,4 +0,0 @@ -{ - "valid_json": true, - "but_a_policy_though": false -} \ No newline at end of file