Full Code of juanfont/headscale for AI

main 568baf3d021b cached
407 files
4.5 MB
1.2M tokens
3826 symbols
1 requests
Download .txt
Showing preview only (4,791K chars total). Download the full file or copy to clipboard to get everything.
Repository: juanfont/headscale
Branch: main
Commit: 568baf3d021b
Files: 407
Total size: 4.5 MB

Directory structure:
gitextract_td0jbyuq/

├── .dockerignore
├── .editorconfig
├── .envrc
├── .github/
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   ├── config.yml
│   │   └── feature_request.yaml
│   ├── label-response/
│   │   ├── needs-more-info.md
│   │   └── support-request.md
│   ├── pull_request_template.md
│   ├── renovate.json
│   └── workflows/
│       ├── build.yml
│       ├── check-generated.yml
│       ├── check-tests.yaml
│       ├── docs-deploy.yml
│       ├── docs-test.yml
│       ├── gh-action-integration-generator.go
│       ├── gh-actions-updater.yaml
│       ├── integration-test-template.yml
│       ├── lint.yml
│       ├── needs-more-info-comment.yml
│       ├── needs-more-info-timer.yml
│       ├── nix-module-test.yml
│       ├── release.yml
│       ├── stale.yml
│       ├── support-request.yml
│       ├── test-integration.yaml
│       ├── test.yml
│       └── update-flake.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yml
├── .mcp.json
├── .mdformat.toml
├── .pre-commit-config.yaml
├── .prettierignore
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile.derper
├── Dockerfile.integration
├── Dockerfile.integration-ci
├── Dockerfile.tailscale-HEAD
├── LICENSE
├── Makefile
├── README.md
├── buf.gen.yaml
├── cmd/
│   ├── headscale/
│   │   ├── cli/
│   │   │   ├── api_key.go
│   │   │   ├── auth.go
│   │   │   ├── configtest.go
│   │   │   ├── debug.go
│   │   │   ├── dump_config.go
│   │   │   ├── generate.go
│   │   │   ├── health.go
│   │   │   ├── mockoidc.go
│   │   │   ├── nodes.go
│   │   │   ├── policy.go
│   │   │   ├── preauthkeys.go
│   │   │   ├── pterm_style.go
│   │   │   ├── root.go
│   │   │   ├── root_test.go
│   │   │   ├── serve.go
│   │   │   ├── users.go
│   │   │   ├── utils.go
│   │   │   └── version.go
│   │   ├── headscale.go
│   │   └── headscale_test.go
│   ├── hi/
│   │   ├── README.md
│   │   ├── cleanup.go
│   │   ├── docker.go
│   │   ├── doctor.go
│   │   ├── main.go
│   │   ├── run.go
│   │   └── stats.go
│   └── mapresponses/
│       └── main.go
├── config-example.yaml
├── derp-example.yaml
├── docs/
│   ├── about/
│   │   ├── clients.md
│   │   ├── contributing.md
│   │   ├── faq.md
│   │   ├── features.md
│   │   ├── help.md
│   │   ├── releases.md
│   │   └── sponsor.md
│   ├── index.md
│   ├── ref/
│   │   ├── acls.md
│   │   ├── api.md
│   │   ├── configuration.md
│   │   ├── debug.md
│   │   ├── derp.md
│   │   ├── dns.md
│   │   ├── integration/
│   │   │   ├── reverse-proxy.md
│   │   │   ├── tools.md
│   │   │   └── web-ui.md
│   │   ├── oidc.md
│   │   ├── registration.md
│   │   ├── routes.md
│   │   ├── tags.md
│   │   └── tls.md
│   ├── requirements.txt
│   ├── setup/
│   │   ├── install/
│   │   │   ├── community.md
│   │   │   ├── container.md
│   │   │   ├── official.md
│   │   │   └── source.md
│   │   ├── requirements.md
│   │   └── upgrade.md
│   └── usage/
│       ├── connect/
│       │   ├── android.md
│       │   ├── apple.md
│       │   └── windows.md
│       └── getting-started.md
├── flake.nix
├── gen/
│   ├── go/
│   │   └── headscale/
│   │       └── v1/
│   │           ├── apikey.pb.go
│   │           ├── auth.pb.go
│   │           ├── device.pb.go
│   │           ├── headscale.pb.go
│   │           ├── headscale.pb.gw.go
│   │           ├── headscale_grpc.pb.go
│   │           ├── node.pb.go
│   │           ├── policy.pb.go
│   │           ├── preauthkey.pb.go
│   │           └── user.pb.go
│   └── openapiv2/
│       └── headscale/
│           └── v1/
│               ├── apikey.swagger.json
│               ├── auth.swagger.json
│               ├── device.swagger.json
│               ├── headscale.swagger.json
│               ├── node.swagger.json
│               ├── policy.swagger.json
│               ├── preauthkey.swagger.json
│               └── user.swagger.json
├── go.mod
├── go.sum
├── hscontrol/
│   ├── app.go
│   ├── assets/
│   │   ├── assets.go
│   │   └── style.css
│   ├── auth.go
│   ├── auth_tags_test.go
│   ├── auth_test.go
│   ├── capver/
│   │   ├── capver.go
│   │   ├── capver_generated.go
│   │   ├── capver_test.go
│   │   └── capver_test_data.go
│   ├── db/
│   │   ├── api_key.go
│   │   ├── api_key_test.go
│   │   ├── db.go
│   │   ├── db_test.go
│   │   ├── ephemeral_garbage_collector_test.go
│   │   ├── ip.go
│   │   ├── ip_test.go
│   │   ├── main_test.go
│   │   ├── node.go
│   │   ├── node_test.go
│   │   ├── policy.go
│   │   ├── preauth_keys.go
│   │   ├── preauth_keys_test.go
│   │   ├── schema.sql
│   │   ├── sqliteconfig/
│   │   │   ├── config.go
│   │   │   ├── config_test.go
│   │   │   └── integration_test.go
│   │   ├── suite_test.go
│   │   ├── testdata/
│   │   │   └── sqlite/
│   │   │       ├── failing-node-preauth-constraint_dump.sql
│   │   │       ├── headscale_0.26.0-beta.1_dump.sql
│   │   │       ├── headscale_0.26.0-beta.2_dump.sql
│   │   │       ├── headscale_0.26.0_dump.sql
│   │   │       ├── headscale_0.26.1_dump-litestream.sql
│   │   │       ├── headscale_0.26.1_dump.sql
│   │   │       ├── headscale_0.26.1_dump_schema-to-0.27.0-old-table-cleanup.sql
│   │   │       └── request_tags_migration_test.sql
│   │   ├── text_serialiser.go
│   │   ├── user_update_test.go
│   │   ├── users.go
│   │   ├── users_test.go
│   │   ├── versioncheck.go
│   │   └── versioncheck_test.go
│   ├── debug.go
│   ├── derp/
│   │   ├── derp.go
│   │   ├── derp_test.go
│   │   └── server/
│   │       └── derp_server.go
│   ├── dns/
│   │   └── extrarecords.go
│   ├── grpcv1.go
│   ├── grpcv1_test.go
│   ├── handlers.go
│   ├── mapper/
│   │   ├── batcher.go
│   │   ├── batcher_bench_test.go
│   │   ├── batcher_concurrency_test.go
│   │   ├── batcher_scale_bench_test.go
│   │   ├── batcher_test.go
│   │   ├── batcher_unit_test.go
│   │   ├── builder.go
│   │   ├── builder_test.go
│   │   ├── mapper.go
│   │   ├── mapper_test.go
│   │   ├── node_conn.go
│   │   └── tail_test.go
│   ├── metrics.go
│   ├── noise.go
│   ├── noise_test.go
│   ├── oidc.go
│   ├── oidc_template_test.go
│   ├── oidc_test.go
│   ├── platform_config.go
│   ├── policy/
│   │   ├── matcher/
│   │   │   ├── matcher.go
│   │   │   └── matcher_test.go
│   │   ├── pm.go
│   │   ├── policy.go
│   │   ├── policy_autoapprove_test.go
│   │   ├── policy_route_approval_test.go
│   │   ├── policy_test.go
│   │   ├── policyutil/
│   │   │   ├── reduce.go
│   │   │   └── reduce_test.go
│   │   ├── route_approval_test.go
│   │   └── v2/
│   │       ├── filter.go
│   │       ├── filter_test.go
│   │       ├── main_test.go
│   │       ├── policy.go
│   │       ├── policy_test.go
│   │       ├── tailscale_compat_test.go
│   │       ├── tailscale_routes_compat_test.go
│   │       ├── tailscale_ssh_data_compat_test.go
│   │       ├── testdata/
│   │       │   └── ssh_results/
│   │       │       ├── SSH-A1.json
│   │       │       ├── SSH-A2.json
│   │       │       ├── SSH-A3.json
│   │       │       ├── SSH-A4.json
│   │       │       ├── SSH-A5.json
│   │       │       ├── SSH-A6.json
│   │       │       ├── SSH-A7.json
│   │       │       ├── SSH-A8.json
│   │       │       ├── SSH-B1.json
│   │       │       ├── SSH-B2.json
│   │       │       ├── SSH-B3.json
│   │       │       ├── SSH-B5.json
│   │       │       ├── SSH-B6.json
│   │       │       ├── SSH-C1.json
│   │       │       ├── SSH-C2.json
│   │       │       ├── SSH-C3.json
│   │       │       ├── SSH-C4.json
│   │       │       ├── SSH-D10.json
│   │       │       ├── SSH-D11.json
│   │       │       ├── SSH-D12.json
│   │       │       ├── SSH-D2.json
│   │       │       ├── SSH-D3.json
│   │       │       ├── SSH-D4.json
│   │       │       ├── SSH-D5.json
│   │       │       ├── SSH-D6.json
│   │       │       ├── SSH-D7.json
│   │       │       ├── SSH-D8.json
│   │       │       ├── SSH-D9.json
│   │       │       ├── SSH-E3.json
│   │       │       ├── SSH-E4.json
│   │       │       ├── SSH-E5.json
│   │       │       ├── SSH-E6.json
│   │       │       ├── SSH-F1.json
│   │       │       ├── SSH-F2.json
│   │       │       ├── SSH-F3.json
│   │       │       ├── SSH-F4.json
│   │       │       ├── SSH-F5.json
│   │       │       ├── SSH-G1.json
│   │       │       └── SSH-G2.json
│   │       ├── types.go
│   │       ├── types_test.go
│   │       ├── utils.go
│   │       └── utils_test.go
│   ├── poll.go
│   ├── poll_test.go
│   ├── routes/
│   │   ├── primary.go
│   │   └── primary_test.go
│   ├── servertest/
│   │   ├── assertions.go
│   │   ├── client.go
│   │   ├── consistency_test.go
│   │   ├── content_test.go
│   │   ├── ephemeral_test.go
│   │   ├── harness.go
│   │   ├── issues_test.go
│   │   ├── lifecycle_test.go
│   │   ├── policy_test.go
│   │   ├── poll_race_test.go
│   │   ├── race_test.go
│   │   ├── routes_test.go
│   │   ├── server.go
│   │   ├── stress_test.go
│   │   └── weather_test.go
│   ├── state/
│   │   ├── debug.go
│   │   ├── debug_test.go
│   │   ├── endpoint_test.go
│   │   ├── ephemeral_test.go
│   │   ├── maprequest.go
│   │   ├── maprequest_test.go
│   │   ├── node_store.go
│   │   ├── node_store_test.go
│   │   ├── ssh_check_test.go
│   │   ├── state.go
│   │   ├── tags.go
│   │   └── test_helpers.go
│   ├── tailsql.go
│   ├── templates/
│   │   ├── apple.go
│   │   ├── auth_success.go
│   │   ├── auth_web.go
│   │   ├── design.go
│   │   ├── general.go
│   │   └── windows.go
│   ├── templates_consistency_test.go
│   ├── types/
│   │   ├── api_key.go
│   │   ├── change/
│   │   │   ├── change.go
│   │   │   └── change_test.go
│   │   ├── common.go
│   │   ├── common_test.go
│   │   ├── config.go
│   │   ├── config_test.go
│   │   ├── const.go
│   │   ├── main_test.go
│   │   ├── node.go
│   │   ├── node_benchmark_test.go
│   │   ├── node_tags_test.go
│   │   ├── node_test.go
│   │   ├── policy.go
│   │   ├── preauth_key.go
│   │   ├── preauth_key_test.go
│   │   ├── routes.go
│   │   ├── testdata/
│   │   │   ├── base-domain-in-server-url.yaml
│   │   │   ├── base-domain-not-in-server-url.yaml
│   │   │   ├── dns-override-true-error.yaml
│   │   │   ├── dns-override-true.yaml
│   │   │   ├── dns_full.yaml
│   │   │   ├── dns_full_no_magic.yaml
│   │   │   ├── minimal.yaml
│   │   │   └── policy-path-is-loaded.yaml
│   │   ├── types_clone.go
│   │   ├── types_view.go
│   │   ├── users.go
│   │   ├── users_test.go
│   │   └── version.go
│   └── util/
│       ├── addr.go
│       ├── addr_test.go
│       ├── const.go
│       ├── dns.go
│       ├── dns_test.go
│       ├── file.go
│       ├── key.go
│       ├── log.go
│       ├── net.go
│       ├── norace.go
│       ├── prompt.go
│       ├── prompt_test.go
│       ├── race.go
│       ├── string.go
│       ├── string_test.go
│       ├── test.go
│       ├── util.go
│       ├── util_test.go
│       └── zlog/
│           ├── fields.go
│           ├── hostinfo.go
│           ├── maprequest.go
│           ├── zf/
│           │   └── fields.go
│           └── zlog_test.go
├── integration/
│   ├── README.md
│   ├── acl_test.go
│   ├── api_auth_test.go
│   ├── auth_key_test.go
│   ├── auth_oidc_test.go
│   ├── auth_web_flow_test.go
│   ├── cli_test.go
│   ├── control.go
│   ├── derp_verify_endpoint_test.go
│   ├── dns_test.go
│   ├── dockertestutil/
│   │   ├── build.go
│   │   ├── config.go
│   │   ├── execute.go
│   │   ├── logs.go
│   │   └── network.go
│   ├── dsic/
│   │   └── dsic.go
│   ├── embedded_derp_test.go
│   ├── general_test.go
│   ├── helpers.go
│   ├── hsic/
│   │   ├── config.go
│   │   └── hsic.go
│   ├── integrationutil/
│   │   └── util.go
│   ├── route_test.go
│   ├── run.sh
│   ├── scenario.go
│   ├── scenario_test.go
│   ├── ssh_test.go
│   ├── tags_test.go
│   ├── tailscale.go
│   └── tsic/
│       └── tsic.go
├── mkdocs.yml
├── nix/
│   ├── README.md
│   ├── example-configuration.nix
│   ├── module.nix
│   └── tests/
│       └── headscale.nix
├── packaging/
│   ├── README.md
│   ├── deb/
│   │   ├── postinst
│   │   ├── postrm
│   │   └── prerm
│   └── systemd/
│       └── headscale.service
├── proto/
│   ├── buf.yaml
│   └── headscale/
│       └── v1/
│           ├── apikey.proto
│           ├── auth.proto
│           ├── device.proto
│           ├── headscale.proto
│           ├── node.proto
│           ├── policy.proto
│           ├── preauthkey.proto
│           └── user.proto
├── swagger.go
└── tools/
    └── capver/
        └── main.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
// integration tests are not needed in docker
// ignoring it let us speed up the integration test
// development
integration_test.go
integration_test/
!integration_test/etc_embedded_derp/tls/server.crt

Dockerfile*
docker-compose*
.dockerignore
.goreleaser.yml
.git
.github
.gitignore
README.md
LICENSE
.vscode

*.sock

node_modules/
package-lock.json
package.json


================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120

[*.go]
indent_style = tab

[Makefile]
indent_style = tab


================================================
FILE: .envrc
================================================
use flake


================================================
FILE: .github/CODEOWNERS
================================================
* @juanfont @kradalby

*.md @ohdearaugustin @nblock
*.yml @ohdearaugustin @nblock
*.yaml @ohdearaugustin @nblock
Dockerfile* @ohdearaugustin @nblock
.goreleaser.yaml @ohdearaugustin @nblock
/docs/ @ohdearaugustin @nblock
/.github/workflows/ @ohdearaugustin @nblock
/.github/renovate.json @ohdearaugustin @nblock


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

ko_fi: headscale


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: 🐞 Bug
description: File a bug/issue
title: "[Bug] <title>"
labels: ["bug", "needs triage"]
body:
  - type: checkboxes
    attributes:
      label: Is this a support request?
      description: This issue tracker is for bugs and feature requests only. If you need
        help, please use ask in our Discord community
      options:
        - label: This is not a support request
          required: true
  - type: checkboxes
    attributes:
      label: Is there an existing issue for this?
      description: Please search to see if an issue already exists for the bug you
        encountered.
      options:
        - label: I have searched the existing issues
          required: true
  - type: textarea
    attributes:
      label: Current Behavior
      description: A concise description of what you're experiencing.
    validations:
      required: true
  - type: textarea
    attributes:
      label: Expected Behavior
      description: A concise description of what you expected to happen.
    validations:
      required: true
  - type: textarea
    attributes:
      label: Steps To Reproduce
      description: Steps to reproduce the behavior.
      placeholder: |
        1. In this environment...
        1. With this config...
        1. Run '...'
        1. See error...
    validations:
      required: true
  - type: textarea
    attributes:
      label: Environment
      description: |
        Please provide information about your environment.
        If you are using a container, always provide the headscale version and not only the Docker image version.
        Please do not put "latest".

        Describe your "headscale network". Is there a lot of nodes, are the nodes all interconnected, are some subnet routers?

        If you are experiencing a problem during an upgrade, please provide the versions of the old and new versions of Headscale and Tailscale.

        examples:
          - **OS**: Ubuntu 24.04
          - **Headscale version**: 0.24.3
          - **Tailscale version**: 1.80.0
          - **Number of nodes**: 20
      value: |
        - OS:
        - Headscale version:
        - Tailscale version:
      render: markdown
    validations:
      required: true
  - type: checkboxes
    attributes:
      label: Runtime environment
      options:
        - label: Headscale is behind a (reverse) proxy
          required: false
        - label: Headscale runs in a container
          required: false
  - type: textarea
    attributes:
      label: Debug information
      description: |
        Please have a look at our [Debugging and troubleshooting
        guide](https://headscale.net/development/ref/debug/) to learn about
        common debugging techniques.

        Links? References? Anything that will give us more context about the issue you are encountering.
        If **any** of these are omitted we will likely close your issue, do **not** ignore them.

        - Client netmap dump (see below)
        - Policy configuration
        - Headscale configuration
        - Headscale log (with `trace` enabled)

        Dump the netmap of tailscale clients:
        `tailscale debug netmap > DESCRIPTIVE_NAME.json`

        Dump the status of tailscale clients:
        `tailscale status --json > DESCRIPTIVE_NAME.json`

        Get the logs of a Tailscale client that is not working as expected.
        `tailscale debug daemon-logs`

        Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
        **Ensure** you use formatting for files you attach.
        Do **not** paste in long files.
    validations:
      required: true


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
# Issues must have some content
blank_issues_enabled: false

# Contact links
contact_links:
  - name: "headscale Discord community"
    url: "https://discord.gg/c84AZQhmpx"
    about: "Please ask and answer questions about usage of headscale here."
  - name: "headscale usage documentation"
    url: "https://headscale.net/"
    about: "Find documentation about how to configure and run headscale."


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yaml
================================================
name: 🚀 Feature Request
description: Suggest an idea for Headscale
title: "[Feature] <title>"
labels: [enhancement]
body:
  - type: textarea
    attributes:
      label: Use case
      description: Please describe the use case for this feature.
      placeholder: |
        <!-- Include the reason, why you would need the feature. E.g. what problem
          does it solve? Or which workflow is currently frustrating and will be improved by
          this? -->
    validations:
      required: true
  - type: textarea
    attributes:
      label: Description
      description: A clear and precise description of what new or changed feature you want.
    validations:
      required: true
  - type: checkboxes
    attributes:
      label: Contribution
      description: Are you willing to contribute to the implementation of this feature?
      options:
        - label: I can write the design doc for this feature
          required: false
        - label: I can contribute this feature
          required: false
  - type: textarea
    attributes:
      label: How can it be implemented?
      description: Free text for your ideas on how this feature could be implemented.
    validations:
      required: false


================================================
FILE: .github/label-response/needs-more-info.md
================================================
Thank you for taking the time to report this issue.

To help us investigate and resolve this, we need more information. Please provide the following:

> [!TIP]
> Most issues turn out to be configuration errors rather than bugs. We encourage you to discuss your problem in our [Discord community](https://discord.gg/c84AZQhmpx) **before** opening an issue. The community can often help identify misconfigurations quickly, saving everyone time.

## Required Information

### Environment Details

- **Headscale version**: (run `headscale version`)
- **Tailscale client version**: (run `tailscale version`)
- **Operating System**: (e.g., Ubuntu 24.04, macOS 14, Windows 11)
- **Deployment method**: (binary, Docker, Kubernetes, etc.)
- **Reverse proxy**: (if applicable: nginx, Traefik, Caddy, etc. - include configuration)

### Debug Information

Please follow our [Debugging and Troubleshooting Guide](https://headscale.net/stable/ref/debug/) and provide:

1. **Client netmap dump** (from affected Tailscale client):

   ```bash
   tailscale debug netmap > netmap.json
   ```

2. **Client status dump** (from affected Tailscale client):

   ```bash
   tailscale status --json > status.json
   ```

3. **Tailscale client logs** (if experiencing client issues):

   ```bash
   tailscale debug daemon-logs
   ```

   > [!IMPORTANT]
   > We need logs from **multiple nodes** to understand the full picture:
   >
   > - The node(s) initiating connections
   > - The node(s) being connected to
   >
   > Without logs from both sides, we cannot diagnose connectivity issues.

4. **Headscale server logs** with `log.level: trace` enabled

5. **Headscale configuration** (with sensitive values redacted - see rules below)

6. **ACL/Policy configuration** (if using ACLs)

7. **Proxy/Docker configuration** (if applicable - nginx.conf, docker-compose.yml, Traefik config, etc.)

## Formatting Requirements

- **Attach long files** - Do not paste large logs or configurations inline. Use GitHub file attachments or GitHub Gists.
- **Use proper Markdown** - Format code blocks, logs, and configurations with appropriate syntax highlighting.
- **Structure your response** - Use the headings above to organize your information clearly.

## Redaction Rules

> [!CAUTION]
> **Replace, do not remove.** Removing information makes debugging impossible.

When redacting sensitive information:

- ✅ **Replace consistently** - If you change `alice@company.com` to `user1@example.com`, use `user1@example.com` everywhere (logs, config, policy, etc.)
- ✅ **Use meaningful placeholders** - `user1@example.com`, `bob@example.com`, `my-secret-key` are acceptable
- ❌ **Never remove information** - Gaps in data prevent us from correlating events across logs
- ❌ **Never redact IP addresses** - We need the actual IPs to trace network paths and identify issues

**If redaction rules are not followed, we will be unable to debug the issue and will have to close it.**

---

**Note:** This issue will be automatically closed in 3 days if no additional information is provided. Once you reply with the requested information, the `needs-more-info` label will be removed automatically.

If you need help gathering this information, please visit our [Discord community](https://discord.gg/c84AZQhmpx).


================================================
FILE: .github/label-response/support-request.md
================================================
Thank you for reaching out.

This issue tracker is used for **bug reports and feature requests** only. Your question appears to be a support or configuration question rather than a bug report.

For help with setup, configuration, or general questions, please visit our [Discord community](https://discord.gg/c84AZQhmpx) where the community and maintainers can assist you in real-time.

**Before posting in Discord, please check:**

- [Documentation](https://headscale.net/)
- [FAQ](https://headscale.net/stable/faq/)
- [Debugging and Troubleshooting Guide](https://headscale.net/stable/ref/debug/)

If after troubleshooting you determine this is actually a bug, please open a new issue with the required debug information from the troubleshooting guide.

This issue has been automatically closed.


================================================
FILE: .github/pull_request_template.md
================================================
<!--
Headscale is "Open Source, acknowledged contribution", this means that any
contribution will have to be discussed with the Maintainers before being submitted.

This model has been chosen to reduce the risk of burnout by limiting the
maintenance overhead of reviewing and validating third-party code.

Headscale is open to code contributions for bug fixes without discussion.

If you find mistakes in the documentation, please submit a fix to the documentation.
-->

<!-- Please tick if the following things apply. You… -->

- [ ] have read the [CONTRIBUTING.md](./CONTRIBUTING.md) file
- [ ] raised a GitHub issue or discussed it on the projects chat beforehand
- [ ] added unit tests
- [ ] added integration tests
- [ ] updated documentation if needed
- [ ] updated CHANGELOG.md

<!-- If applicable, please reference the issue using `Fixes #XXX` and add tests to cover your new code. -->


================================================
FILE: .github/renovate.json
================================================
{
  "baseBranches": ["main"],
  "username": "renovate-release",
  "gitAuthor": "Renovate Bot <bot@renovateapp.com>",
  "branchPrefix": "renovateaction/",
  "onboarding": false,
  "extends": ["config:base", ":rebaseStalePrs"],
  "ignorePresets": [":prHourlyLimit2"],
  "enabledManagers": ["dockerfile", "gomod", "github-actions", "regex"],
  "includeForks": true,
  "repositories": ["juanfont/headscale"],
  "platform": "github",
  "packageRules": [
    {
      "matchDatasources": ["go"],
      "groupName": "Go modules",
      "groupSlug": "gomod",
      "separateMajorMinor": false
    },
    {
      "matchDatasources": ["docker"],
      "groupName": "Dockerfiles",
      "groupSlug": "dockerfiles"
    }
  ],
  "regexManagers": [
    {
      "fileMatch": [".github/workflows/.*.yml$"],
      "matchStrings": ["\\s*go-version:\\s*\"?(?<currentValue>.*?)\"?\\n"],
      "datasourceTemplate": "golang-version",
      "depNameTemplate": "actions/go-version"
    }
  ]
}


================================================
FILE: .github/workflows/build.yml
================================================
name: Build

on:
  push:
    branches:
      - main
  pull_request:

concurrency:
  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  build-nix:
    runs-on: ubuntu-latest
    permissions: write-all
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2
      - name: Get changed files
        id: changed-files
        uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
        with:
          filters: |
            files:
              - '*.nix'
              - 'go.*'
              - '**/*.go'
              - 'integration_test/'
              - 'config-example.yaml'
      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
        if: steps.changed-files.outputs.files == 'true'
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        if: steps.changed-files.outputs.files == 'true'
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Run nix build
        id: build
        if: steps.changed-files.outputs.files == 'true'
        run: |
          nix build |& tee build-result
          BUILD_STATUS="${PIPESTATUS[0]}"

          OLD_HASH=$(cat build-result | grep specified: | awk -F ':' '{print $2}' | sed 's/ //g')
          NEW_HASH=$(cat build-result | grep got: | awk -F ':' '{print $2}' | sed 's/ //g')

          echo "OLD_HASH=$OLD_HASH" >> $GITHUB_OUTPUT
          echo "NEW_HASH=$NEW_HASH" >> $GITHUB_OUTPUT

          exit $BUILD_STATUS

      - name: Nix gosum diverging
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        if: failure() && steps.build.outcome == 'failure'
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          script: |
            github.rest.pulls.createReviewComment({
              pull_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'Nix build failed with wrong gosum, please update "vendorSha256" (${{ steps.build.outputs.OLD_HASH }}) for the "headscale" package in flake.nix with the new SHA: ${{ steps.build.outputs.NEW_HASH }}'
            })

      - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        if: steps.changed-files.outputs.files == 'true'
        with:
          name: headscale-linux
          path: result/bin/headscale
  build-cross:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        env:
          - "GOARCH=arm64 GOOS=linux"
          - "GOARCH=amd64 GOOS=linux"
          - "GOARCH=arm64 GOOS=darwin"
          - "GOARCH=amd64 GOOS=darwin"
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Run go cross compile
        env:
          CGO_ENABLED: 0
        run: env ${{ matrix.env }} nix develop --command -- go build -o "headscale"
          ./cmd/headscale
      - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        with:
          name: "headscale-${{ matrix.env }}"
          path: "headscale"


================================================
FILE: .github/workflows/check-generated.yml
================================================
name: Check Generated Files

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  check-generated:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2
      - name: Get changed files
        id: changed-files
        uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
        with:
          filters: |
            files:
              - '*.nix'
              - 'go.*'
              - '**/*.go'
              - '**/*.proto'
              - 'buf.gen.yaml'
              - 'tools/**'
      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
        if: steps.changed-files.outputs.files == 'true'
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        if: steps.changed-files.outputs.files == 'true'
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Run make generate
        if: steps.changed-files.outputs.files == 'true'
        run: nix develop --command -- make generate

      - name: Check for uncommitted changes
        if: steps.changed-files.outputs.files == 'true'
        run: |
          if ! git diff --exit-code; then
            echo "❌ Generated files are not up to date!"
            echo "Please run 'make generate' and commit the changes."
            exit 1
          else
            echo "✅ All generated files are up to date."
          fi


================================================
FILE: .github/workflows/check-tests.yaml
================================================
name: Check integration tests workflow

on: [pull_request]

concurrency:
  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  check-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2
      - name: Get changed files
        id: changed-files
        uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
        with:
          filters: |
            files:
              - '*.nix'
              - 'go.*'
              - '**/*.go'
              - 'integration_test/'
              - 'config-example.yaml'
      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
        if: steps.changed-files.outputs.files == 'true'
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        if: steps.changed-files.outputs.files == 'true'
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Generate and check integration tests
        if: steps.changed-files.outputs.files == 'true'
        run: |
          nix develop --command bash -c "cd .github/workflows && go generate"
          git diff --exit-code .github/workflows/test-integration.yaml

      - name: Show missing tests
        if: failure()
        run: |
          git diff .github/workflows/test-integration.yaml


================================================
FILE: .github/workflows/docs-deploy.yml
================================================
name: Deploy docs

on:
  push:
    branches:
      # Main branch for development docs
      - main

      # Doc maintenance branches
      - doc/[0-9]+.[0-9]+.[0-9]+
    tags:
      # Stable release tags
      - v[0-9]+.[0-9]+.[0-9]+
    paths:
      - "docs/**"
      - "mkdocs.yml"
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0
      - name: Install python
        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
        with:
          python-version: 3.x
      - name: Setup cache
        uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
        with:
          key: ${{ github.ref }}
          path: .cache
      - name: Setup dependencies
        run: pip install -r docs/requirements.txt
      - name: Configure git
        run: |
          git config user.name github-actions
          git config user.email github-actions@github.com
      - name: Deploy development docs
        if: github.ref == 'refs/heads/main'
        run: mike deploy --push development unstable
      - name: Deploy stable docs from doc branches
        if: startsWith(github.ref, 'refs/heads/doc/')
        run: mike deploy --push ${GITHUB_REF_NAME##*/}
      - name: Deploy stable docs from tag
        if: startsWith(github.ref, 'refs/tags/v')
        # This assumes that only newer tags are pushed
        run: mike deploy --push --update-aliases ${GITHUB_REF_NAME#v} stable latest


================================================
FILE: .github/workflows/docs-test.yml
================================================
name: Test documentation build

on: [pull_request]

concurrency:
  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
      - name: Install python
        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
        with:
          python-version: 3.x
      - name: Setup cache
        uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
        with:
          key: ${{ github.ref }}
          path: .cache
      - name: Setup dependencies
        run: pip install -r docs/requirements.txt
      - name: Build docs
        run: mkdocs build --strict


================================================
FILE: .github/workflows/gh-action-integration-generator.go
================================================
package main

//go:generate go run ./gh-action-integration-generator.go

import (
	"bytes"
	"fmt"
	"log"
	"os/exec"
	"strings"
)

// testsToSplit defines tests that should be split into multiple CI jobs.
// Key is the test function name, value is a list of subtest prefixes.
// Each prefix becomes a separate CI job as "TestName/prefix".
//
// Example: TestAutoApproveMultiNetwork has subtests like:
//   - TestAutoApproveMultiNetwork/authkey-tag-advertiseduringup-false-pol-database
//   - TestAutoApproveMultiNetwork/webauth-user-advertiseduringup-true-pol-file
//
// Splitting by approver type (tag, user, group) creates 6 CI jobs with 4 tests each:
//   - TestAutoApproveMultiNetwork/authkey-tag.* (4 tests)
//   - TestAutoApproveMultiNetwork/authkey-user.* (4 tests)
//   - TestAutoApproveMultiNetwork/authkey-group.* (4 tests)
//   - TestAutoApproveMultiNetwork/webauth-tag.* (4 tests)
//   - TestAutoApproveMultiNetwork/webauth-user.* (4 tests)
//   - TestAutoApproveMultiNetwork/webauth-group.* (4 tests)
//
// This reduces load per CI job (4 tests instead of 12) to avoid infrastructure
// flakiness when running many sequential Docker-based integration tests.
var testsToSplit = map[string][]string{
	"TestAutoApproveMultiNetwork": {
		"authkey-tag",
		"authkey-user",
		"authkey-group",
		"webauth-tag",
		"webauth-user",
		"webauth-group",
	},
}

// expandTests takes a list of test names and expands any that need splitting
// into multiple subtest patterns.
func expandTests(tests []string) []string {
	var expanded []string
	for _, test := range tests {
		if prefixes, ok := testsToSplit[test]; ok {
			// This test should be split into multiple jobs.
			// We append ".*" to each prefix because the CI runner wraps patterns
			// with ^...$ anchors. Without ".*", a pattern like "authkey$" wouldn't
			// match "authkey-tag-advertiseduringup-false-pol-database".
			for _, prefix := range prefixes {
				expanded = append(expanded, fmt.Sprintf("%s/%s.*", test, prefix))
			}
		} else {
			expanded = append(expanded, test)
		}
	}
	return expanded
}

func findTests() []string {
	rgBin, err := exec.LookPath("rg")
	if err != nil {
		log.Fatalf("failed to find rg (ripgrep) binary")
	}

	args := []string{
		"--regexp", "func (Test.+)\\(.*",
		"../../integration/",
		"--replace", "$1",
		"--sort", "path",
		"--no-line-number",
		"--no-filename",
		"--no-heading",
	}

	cmd := exec.Command(rgBin, args...)
	var out bytes.Buffer
	cmd.Stdout = &out
	err = cmd.Run()
	if err != nil {
		log.Fatalf("failed to run command: %s", err)
	}

	tests := strings.Split(strings.TrimSpace(out.String()), "\n")
	return tests
}

func updateYAML(tests []string, jobName string, testPath string) {
	testsForYq := fmt.Sprintf("[%s]", strings.Join(tests, ", "))

	yqCommand := fmt.Sprintf(
		"yq eval '.jobs.%s.strategy.matrix.test = %s' %s -i",
		jobName,
		testsForYq,
		testPath,
	)
	cmd := exec.Command("bash", "-c", yqCommand)

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	err := cmd.Run()
	if err != nil {
		log.Printf("stdout: %s", stdout.String())
		log.Printf("stderr: %s", stderr.String())
		log.Fatalf("failed to run yq command: %s", err)
	}

	fmt.Printf("YAML file (%s) job %s updated successfully\n", testPath, jobName)
}

func main() {
	tests := findTests()

	// Expand tests that should be split into multiple jobs
	expandedTests := expandTests(tests)

	quotedTests := make([]string, len(expandedTests))
	for i, test := range expandedTests {
		quotedTests[i] = fmt.Sprintf("\"%s\"", test)
	}

	// Define selected tests for PostgreSQL
	postgresTestNames := []string{
		"TestACLAllowUserDst",
		"TestPingAllByIP",
		"TestEphemeral2006DeletedTooQuickly",
		"TestPingAllByIPManyUpDown",
		"TestSubnetRouterMultiNetwork",
	}

	quotedPostgresTests := make([]string, len(postgresTestNames))
	for i, test := range postgresTestNames {
		quotedPostgresTests[i] = fmt.Sprintf("\"%s\"", test)
	}

	// Update both SQLite and PostgreSQL job matrices
	updateYAML(quotedTests, "sqlite", "./test-integration.yaml")
	updateYAML(quotedPostgresTests, "postgres", "./test-integration.yaml")
}


================================================
FILE: .github/workflows/gh-actions-updater.yaml
================================================
name: GitHub Actions Version Updater

on:
  schedule:
    # Automatically run on every Sunday
    - cron: "0 0 * * 0"

jobs:
  build:
    if: github.repository == 'juanfont/headscale'
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          # [Required] Access token with `workflow` scope.
          token: ${{ secrets.WORKFLOW_SECRET }}

      - name: Run GitHub Actions Version Updater
        uses: saadmk11/github-actions-version-updater@d8781caf11d11168579c8e5e94f62b068038f442 # v0.9.0
        with:
          # [Required] Access token with `workflow` scope.
          token: ${{ secrets.WORKFLOW_SECRET }}


================================================
FILE: .github/workflows/integration-test-template.yml
================================================
name: Integration Test Template

on:
  workflow_call:
    inputs:
      test:
        required: true
        type: string
      postgres_flag:
        required: false
        type: string
        default: ""
      database_name:
        required: true
        type: string

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      # Github does not allow us to access secrets in pull requests,
      # so this env var is used to check if we have the secret or not.
      # If we have the secrets, meaning we are running on push in a fork,
      # there might be secrets available for more debugging.
      # If TS_OAUTH_CLIENT_ID and TS_OAUTH_SECRET is set, then the job
      # will join a debug tailscale network, set up SSH and a tmux session.
      # The SSH will be configured to use the SSH key of the Github user
      # that triggered the build.
      HAS_TAILSCALE_SECRET: ${{ secrets.TS_OAUTH_CLIENT_ID }}
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2
      - name: Tailscale
        if: ${{ env.HAS_TAILSCALE_SECRET }}
        uses: tailscale/github-action@a392da0a182bba0e9613b6243ebd69529b1878aa # v4.1.0
        with:
          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
          tags: tag:gh
      - name: Setup SSH server for Actor
        if: ${{ env.HAS_TAILSCALE_SECRET }}
        uses: alexellis/setup-sshd-actor@master
      - name: Download headscale image
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
        with:
          name: headscale-image
          path: /tmp/artifacts
      - name: Download tailscale HEAD image
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
        with:
          name: tailscale-head-image
          path: /tmp/artifacts
      - name: Download hi binary
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
        with:
          name: hi-binary
          path: /tmp/artifacts
      - name: Download Go cache
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
        with:
          name: go-cache
          path: /tmp/artifacts
      - name: Download postgres image
        if: ${{ inputs.postgres_flag == '--postgres=1' }}
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
        with:
          name: postgres-image
          path: /tmp/artifacts
      - name: Pin Docker to v28 (avoid v29 breaking changes)
        run: |
          # Docker 29 breaks docker build via Go client libraries and
          # docker load/save with certain tarball formats.
          # Pin to Docker 28.x until our tooling is updated.
          # https://github.com/actions/runner-images/issues/13474
          sudo install -m 0755 -d /etc/apt/keyrings
          curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
            | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
            https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
            | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
          sudo apt-get update -qq
          VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
          sudo apt-get install -y --allow-downgrades \
            "docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
          sudo systemctl restart docker
          docker version
      - name: Load Docker images, Go cache, and prepare binary
        run: |
          gunzip -c /tmp/artifacts/headscale-image.tar.gz | docker load
          gunzip -c /tmp/artifacts/tailscale-head-image.tar.gz | docker load
          if [ -f /tmp/artifacts/postgres-image.tar.gz ]; then
            gunzip -c /tmp/artifacts/postgres-image.tar.gz | docker load
          fi
          chmod +x /tmp/artifacts/hi
          docker images
          # Extract Go cache to host directories for bind mounting
          mkdir -p /tmp/go-cache
          tar -xzf /tmp/artifacts/go-cache.tar.gz -C /tmp/go-cache
          ls -la /tmp/go-cache/ /tmp/go-cache/.cache/
      - name: Run Integration Test
        env:
          HEADSCALE_INTEGRATION_HEADSCALE_IMAGE: headscale:${{ github.sha }}
          HEADSCALE_INTEGRATION_TAILSCALE_IMAGE: tailscale-head:${{ github.sha }}
          HEADSCALE_INTEGRATION_POSTGRES_IMAGE: ${{ inputs.postgres_flag == '--postgres=1' && format('postgres:{0}', github.sha) || '' }}
          HEADSCALE_INTEGRATION_GO_CACHE: /tmp/go-cache/go
          HEADSCALE_INTEGRATION_GO_BUILD_CACHE: /tmp/go-cache/.cache/go-build
        run: /tmp/artifacts/hi run --stats --ts-memory-limit=300 --hs-memory-limit=1500 "^${{ inputs.test }}$" \
          --timeout=120m \
          ${{ inputs.postgres_flag }}
      # Sanitize test name for artifact upload (replace invalid characters: " : < > | * ? \ / with -)
      - name: Sanitize test name for artifacts
        if: always()
        id: sanitize
        run: echo "name=${TEST_NAME//[\":<>|*?\\\/]/-}" >> $GITHUB_OUTPUT
        env:
          TEST_NAME: ${{ inputs.test }}
      - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        if: always()
        with:
          name: ${{ inputs.database_name }}-${{ steps.sanitize.outputs.name }}-logs
          path: "control_logs/*/*.log"
      - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        if: always()
        with:
          name: ${{ inputs.database_name }}-${{ steps.sanitize.outputs.name }}-artifacts
          path: control_logs/
      - name: Setup a blocking tmux session
        if: ${{ env.HAS_TAILSCALE_SECRET }}
        uses: alexellis/block-with-tmux-action@master


================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint

on: [pull_request]

concurrency:
  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  golangci-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2
      - name: Get changed files
        id: changed-files
        uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
        with:
          filters: |
            files:
              - '*.nix'
              - 'go.*'
              - '**/*.go'
              - 'integration_test/'
              - 'config-example.yaml'
      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
        if: steps.changed-files.outputs.files == 'true'
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        if: steps.changed-files.outputs.files == 'true'
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: golangci-lint
        if: steps.changed-files.outputs.files == 'true'
        run: nix develop --command -- golangci-lint run
          --new-from-rev=${{github.event.pull_request.base.sha}}
          --output.text.path=stdout
          --output.text.print-linter-name
          --output.text.print-issued-lines
          --output.text.colors

  prettier-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2
      - name: Get changed files
        id: changed-files
        uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
        with:
          filters: |
            files:
              - '*.nix'
              - '**/*.md'
              - '**/*.yml'
              - '**/*.yaml'
              - '**/*.ts'
              - '**/*.js'
              - '**/*.sass'
              - '**/*.css'
              - '**/*.scss'
              - '**/*.html'
      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
        if: steps.changed-files.outputs.files == 'true'
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        if: steps.changed-files.outputs.files == 'true'
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Prettify code
        if: steps.changed-files.outputs.files == 'true'
        run: nix develop --command -- prettier --no-error-on-unmatched-pattern
          --ignore-unknown --check **/*.{ts,js,md,yaml,yml,sass,css,scss,html}

  proto-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Buf lint
        run: nix develop --command -- buf lint proto


================================================
FILE: .github/workflows/needs-more-info-comment.yml
================================================
name: Needs More Info - Post Comment

on:
  issues:
    types: [labeled]

jobs:
  post-comment:
    if: >-
      github.event.label.name == 'needs-more-info' &&
      github.repository == 'juanfont/headscale'
    runs-on: ubuntu-latest
    permissions:
      issues: write
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          sparse-checkout: .github/label-response/needs-more-info.md
          sparse-checkout-cone-mode: false

      - name: Post instruction comment
        run: gh issue comment "$NUMBER" --body-file .github/label-response/needs-more-info.md
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GH_REPO: ${{ github.repository }}
          NUMBER: ${{ github.event.issue.number }}


================================================
FILE: .github/workflows/needs-more-info-timer.yml
================================================
name: Needs More Info - Timer

on:
  schedule:
    - cron: "0 0 * * *" # Daily at midnight UTC
  issue_comment:
    types: [created]
  workflow_dispatch:

jobs:
  # When a non-bot user comments on a needs-more-info issue, remove the label.
  remove-label-on-response:
    if: >-
      github.repository == 'juanfont/headscale' &&
      github.event_name == 'issue_comment' &&
      github.event.comment.user.type != 'Bot' &&
      contains(github.event.issue.labels.*.name, 'needs-more-info')
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - name: Remove needs-more-info label
        run: gh issue edit "$NUMBER" --remove-label needs-more-info
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GH_REPO: ${{ github.repository }}
          NUMBER: ${{ github.event.issue.number }}

  # On schedule, close issues that have had no human response for 3 days.
  close-stale:
    if: >-
      github.repository == 'juanfont/headscale' &&
      github.event_name != 'issue_comment'
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - uses: hustcer/setup-nu@920172d92eb04671776f3ba69d605d3b09351c30 # v3.22
        with:
          version: "*"

      - name: Close stale needs-more-info issues
        shell: nu {0}
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GH_REPO: ${{ github.repository }}
        run: |
          let issues = (gh issue list
            --repo $env.GH_REPO
            --label "needs-more-info"
            --state open
            --json number
            | from json)

          for issue in $issues {
            let number = $issue.number
            print $"Checking issue #($number)"

            # Find when needs-more-info was last added
            let events = (gh api $"repos/($env.GH_REPO)/issues/($number)/events"
              --paginate | from json | flatten)
            let label_event = ($events
              | where event == "labeled" and label.name == "needs-more-info"
              | last)
            let label_added_at = ($label_event.created_at | into datetime)

            # Check for non-bot comments after the label was added
            let comments = (gh api $"repos/($env.GH_REPO)/issues/($number)/comments"
              --paginate | from json | flatten)
            let human_responses = ($comments
              | where user.type != "Bot"
              | where { ($in.created_at | into datetime) > $label_added_at })

            if ($human_responses | length) > 0 {
              print $"  Human responded, removing label"
              gh issue edit $number --repo $env.GH_REPO --remove-label needs-more-info
              continue
            }

            # Check if 3 days have passed
            let elapsed = (date now) - $label_added_at
            if $elapsed < 3day {
              print $"  Only ($elapsed | format duration day) elapsed, skipping"
              continue
            }

            print $"  No response for ($elapsed | format duration day), closing"
            let message = [
              "This issue has been automatically closed because no additional information was provided within 3 days."
              ""
              "If you have the requested information, please open a new issue and include the debug information requested above."
              ""
              "Thank you for your understanding."
            ] | str join "\n"
            gh issue comment $number --repo $env.GH_REPO --body $message
            gh issue close $number --repo $env.GH_REPO --reason "not planned"
            gh issue edit $number --repo $env.GH_REPO --remove-label needs-more-info
          }


================================================
FILE: .github/workflows/nix-module-test.yml
================================================
name: NixOS Module Tests

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  nix-module-check:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2

      - name: Get changed files
        id: changed-files
        uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
        with:
          filters: |
            nix:
              - 'nix/**'
              - 'flake.nix'
              - 'flake.lock'
            go:
              - 'go.*'
              - '**/*.go'
              - 'cmd/**'
              - 'hscontrol/**'

      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
        if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'

      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Run NixOS module tests
        if: steps.changed-files.outputs.nix == 'true' || steps.changed-files.outputs.go == 'true'
        run: |
          echo "Running NixOS module integration test..."
          nix build .#checks.x86_64-linux.headscale -L


================================================
FILE: .github/workflows/release.yml
================================================
---
name: Release

on:
  push:
    tags:
      - "*" # triggers only if push new tag version
  workflow_dispatch:

jobs:
  goreleaser:
    if: github.repository == 'juanfont/headscale'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0

      - name: Pin Docker to v28 (avoid v29 breaking changes)
        run: |
          # Docker 29 breaks docker build via Go client libraries and
          # docker load/save with certain tarball formats.
          # Pin to Docker 28.x until our tooling is updated.
          # https://github.com/actions/runner-images/issues/13474
          sudo install -m 0755 -d /etc/apt/keyrings
          curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
            | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
            https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
            | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
          sudo apt-get update -qq
          VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
          sudo apt-get install -y --allow-downgrades \
            "docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
          sudo systemctl restart docker
          docker version

      - name: Login to DockerHub
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GHCR
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Run goreleaser
        run: nix develop --command -- goreleaser release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/stale.yml
================================================
name: Close inactive issues

on:
  schedule:
    - cron: "30 1 * * *"

jobs:
  close-issues:
    if: github.repository == 'juanfont/headscale'
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
    steps:
      - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
        with:
          days-before-issue-stale: 90
          days-before-issue-close: 7
          stale-issue-label: "stale"
          stale-issue-message: "This issue is stale because it has been open for 90 days with no
            activity."
          close-issue-message: "This issue was closed because it has been inactive for 14 days
            since being marked as stale."
          days-before-pr-stale: -1
          days-before-pr-close: -1
          exempt-issue-labels: "no-stale-bot,needs-more-info"
          repo-token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/support-request.yml
================================================
name: Support Request - Close Issue

on:
  issues:
    types: [labeled]

jobs:
  close-support-request:
    if: >-
      github.event.label.name == 'support-request' &&
      github.repository == 'juanfont/headscale'
    runs-on: ubuntu-latest
    permissions:
      issues: write
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          sparse-checkout: .github/label-response/support-request.md
          sparse-checkout-cone-mode: false

      - name: Post comment and close issue
        run: |
          gh issue comment "$NUMBER" --body-file .github/label-response/support-request.md
          gh issue close "$NUMBER" --reason "not planned"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GH_REPO: ${{ github.repository }}
          NUMBER: ${{ github.event.issue.number }}


================================================
FILE: .github/workflows/test-integration.yaml
================================================
name: integration
# To debug locally on a branch, and when needing secrets
# change this to include `push` so the build is ran on
# the main repository.
on: [pull_request]
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true
jobs:
  # build: Builds binaries and Docker images once, uploads as artifacts for reuse.
  # build-postgres: Pulls postgres image separately to avoid Docker Hub rate limits.
  # sqlite: Runs all integration tests with SQLite backend.
  # postgres: Runs a subset of tests with PostgreSQL to verify database compatibility.
  build:
    runs-on: ubuntu-latest
    outputs:
      files-changed: ${{ steps.changed-files.outputs.files }}
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2
      - name: Get changed files
        id: changed-files
        uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
        with:
          filters: |
            files:
              - '*.nix'
              - 'go.*'
              - '**/*.go'
              - 'integration/**'
              - 'config-example.yaml'
              - '.github/workflows/test-integration.yaml'
              - '.github/workflows/integration-test-template.yml'
              - 'Dockerfile.*'
      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
        if: steps.changed-files.outputs.files == 'true'
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        if: steps.changed-files.outputs.files == 'true'
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
      - name: Build binaries and warm Go cache
        if: steps.changed-files.outputs.files == 'true'
        run: |
          # Build all Go binaries in one nix shell to maximize cache reuse
          nix develop --command -- bash -c '
            go build -o hi ./cmd/hi
            CGO_ENABLED=0 GOOS=linux go build -o headscale ./cmd/headscale
            # Build integration test binary to warm the cache with all dependencies
            go test -c ./integration -o /dev/null 2>/dev/null || true
          '
      - name: Upload hi binary
        if: steps.changed-files.outputs.files == 'true'
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        with:
          name: hi-binary
          path: hi
          retention-days: 10
      - name: Package Go cache
        if: steps.changed-files.outputs.files == 'true'
        run: |
          # Package Go module cache and build cache
          tar -czf go-cache.tar.gz -C ~ go .cache/go-build
      - name: Upload Go cache
        if: steps.changed-files.outputs.files == 'true'
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        with:
          name: go-cache
          path: go-cache.tar.gz
          retention-days: 10
      - name: Pin Docker to v28 (avoid v29 breaking changes)
        if: steps.changed-files.outputs.files == 'true'
        run: |
          # Docker 29 breaks docker build via Go client libraries and
          # docker load/save with certain tarball formats.
          # Pin to Docker 28.x until our tooling is updated.
          # https://github.com/actions/runner-images/issues/13474
          sudo install -m 0755 -d /etc/apt/keyrings
          curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
            | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
            https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
            | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
          sudo apt-get update -qq
          VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
          sudo apt-get install -y --allow-downgrades \
            "docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
          sudo systemctl restart docker
          docker version
      - name: Build headscale image
        if: steps.changed-files.outputs.files == 'true'
        run: |
          docker build \
            --file Dockerfile.integration-ci \
            --tag headscale:${{ github.sha }} \
            .
          docker save headscale:${{ github.sha }} | gzip > headscale-image.tar.gz
      - name: Build tailscale HEAD image
        if: steps.changed-files.outputs.files == 'true'
        run: |
          docker build \
            --file Dockerfile.tailscale-HEAD \
            --tag tailscale-head:${{ github.sha }} \
            .
          docker save tailscale-head:${{ github.sha }} | gzip > tailscale-head-image.tar.gz
      - name: Upload headscale image
        if: steps.changed-files.outputs.files == 'true'
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        with:
          name: headscale-image
          path: headscale-image.tar.gz
          retention-days: 10
      - name: Upload tailscale HEAD image
        if: steps.changed-files.outputs.files == 'true'
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        with:
          name: tailscale-head-image
          path: tailscale-head-image.tar.gz
          retention-days: 10
  build-postgres:
    runs-on: ubuntu-latest
    needs: build
    if: needs.build.outputs.files-changed == 'true'
    steps:
      - name: Pin Docker to v28 (avoid v29 breaking changes)
        run: |
          # Docker 29 breaks docker build via Go client libraries and
          # docker load/save with certain tarball formats.
          # Pin to Docker 28.x until our tooling is updated.
          # https://github.com/actions/runner-images/issues/13474
          sudo install -m 0755 -d /etc/apt/keyrings
          curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
            | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
          echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
            https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
            | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
          sudo apt-get update -qq
          VERSION=$(apt-cache madison docker-ce | grep '28\.5' | head -1 | awk '{print $3}')
          sudo apt-get install -y --allow-downgrades \
            "docker-ce=${VERSION}" "docker-ce-cli=${VERSION}"
          sudo systemctl restart docker
          docker version
      - name: Pull and save postgres image
        run: |
          docker pull postgres:latest
          docker tag postgres:latest postgres:${{ github.sha }}
          docker save postgres:${{ github.sha }} | gzip > postgres-image.tar.gz
      - name: Upload postgres image
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
        with:
          name: postgres-image
          path: postgres-image.tar.gz
          retention-days: 10
  sqlite:
    needs: build
    if: needs.build.outputs.files-changed == 'true'
    strategy:
      fail-fast: false
      matrix:
        test:
          - TestACLHostsInNetMapTable
          - TestACLAllowUser80Dst
          - TestACLDenyAllPort80
          - TestACLAllowUserDst
          - TestACLAllowStarDst
          - TestACLNamedHostsCanReachBySubnet
          - TestACLNamedHostsCanReach
          - TestACLDevice1CanAccessDevice2
          - TestPolicyUpdateWhileRunningWithCLIInDatabase
          - TestACLAutogroupMember
          - TestACLAutogroupTagged
          - TestACLAutogroupSelf
          - TestACLPolicyPropagationOverTime
          - TestACLTagPropagation
          - TestACLTagPropagationPortSpecific
          - TestACLGroupWithUnknownUser
          - TestACLGroupAfterUserDeletion
          - TestACLGroupDeletionExactReproduction
          - TestACLDynamicUnknownUserAddition
          - TestACLDynamicUnknownUserRemoval
          - TestAPIAuthenticationBypass
          - TestAPIAuthenticationBypassCurl
          - TestGRPCAuthenticationBypass
          - TestCLIWithConfigAuthenticationBypass
          - TestAuthKeyLogoutAndReloginSameUser
          - TestAuthKeyLogoutAndReloginNewUser
          - TestAuthKeyLogoutAndReloginSameUserExpiredKey
          - TestAuthKeyDeleteKey
          - TestAuthKeyLogoutAndReloginRoutesPreserved
          - TestOIDCAuthenticationPingAll
          - TestOIDCExpireNodesBasedOnTokenExpiry
          - TestOIDC024UserCreation
          - TestOIDCAuthenticationWithPKCE
          - TestOIDCReloginSameNodeNewUser
          - TestOIDCFollowUpUrl
          - TestOIDCMultipleOpenedLoginUrls
          - TestOIDCReloginSameNodeSameUser
          - TestOIDCExpiryAfterRestart
          - TestOIDCACLPolicyOnJoin
          - TestOIDCReloginSameUserRoutesPreserved
          - TestAuthWebFlowAuthenticationPingAll
          - TestAuthWebFlowLogoutAndReloginSameUser
          - TestAuthWebFlowLogoutAndReloginNewUser
          - TestUserCommand
          - TestPreAuthKeyCommand
          - TestPreAuthKeyCommandWithoutExpiry
          - TestPreAuthKeyCommandReusableEphemeral
          - TestPreAuthKeyCorrectUserLoggedInCommand
          - TestTaggedNodesCLIOutput
          - TestApiKeyCommand
          - TestNodeCommand
          - TestNodeExpireCommand
          - TestNodeRenameCommand
          - TestPolicyCommand
          - TestPolicyBrokenConfigCommand
          - TestDERPVerifyEndpoint
          - TestResolveMagicDNS
          - TestResolveMagicDNSExtraRecordsPath
          - TestDERPServerScenario
          - TestDERPServerWebsocketScenario
          - TestPingAllByIP
          - TestPingAllByIPPublicDERP
          - TestEphemeral
          - TestEphemeralInAlternateTimezone
          - TestEphemeral2006DeletedTooQuickly
          - TestPingAllByHostname
          - TestTaildrop
          - TestUpdateHostnameFromClient
          - TestExpireNode
          - TestSetNodeExpiryInFuture
          - TestDisableNodeExpiry
          - TestNodeOnlineStatus
          - TestPingAllByIPManyUpDown
          - Test2118DeletingOnlineNodePanics
          - TestEnablingRoutes
          - TestHASubnetRouterFailover
          - TestSubnetRouteACL
          - TestEnablingExitRoutes
          - TestSubnetRouterMultiNetwork
          - TestSubnetRouterMultiNetworkExitNode
          - TestAutoApproveMultiNetwork/authkey-tag.*
          - TestAutoApproveMultiNetwork/authkey-user.*
          - TestAutoApproveMultiNetwork/authkey-group.*
          - TestAutoApproveMultiNetwork/webauth-tag.*
          - TestAutoApproveMultiNetwork/webauth-user.*
          - TestAutoApproveMultiNetwork/webauth-group.*
          - TestSubnetRouteACLFiltering
          - TestHeadscale
          - TestTailscaleNodesJoiningHeadcale
          - TestSSHOneUserToAll
          - TestSSHMultipleUsersAllToAll
          - TestSSHNoSSHConfigured
          - TestSSHIsBlockedInACL
          - TestSSHUserOnlyIsolation
          - TestSSHAutogroupSelf
          - TestSSHOneUserToOneCheckModeCLI
          - TestSSHOneUserToOneCheckModeOIDC
          - TestSSHCheckModeUnapprovedTimeout
          - TestSSHCheckModeCheckPeriodCLI
          - TestSSHCheckModeAutoApprove
          - TestSSHCheckModeNegativeCLI
          - TestSSHLocalpart
          - TestTagsAuthKeyWithTagRequestDifferentTag
          - TestTagsAuthKeyWithTagNoAdvertiseFlag
          - TestTagsAuthKeyWithTagCannotAddViaCLI
          - TestTagsAuthKeyWithTagCannotChangeViaCLI
          - TestTagsAuthKeyWithTagAdminOverrideReauthPreserves
          - TestTagsAuthKeyWithTagCLICannotModifyAdminTags
          - TestTagsAuthKeyWithoutTagCannotRequestTags
          - TestTagsAuthKeyWithoutTagRegisterNoTags
          - TestTagsAuthKeyWithoutTagCannotAddViaCLI
          - TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset
          - TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise
          - TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag
          - TestTagsUserLoginOwnedTagAtRegistration
          - TestTagsUserLoginNonExistentTagAtRegistration
          - TestTagsUserLoginUnownedTagAtRegistration
          - TestTagsUserLoginAddTagViaCLIReauth
          - TestTagsUserLoginRemoveTagViaCLIReauth
          - TestTagsUserLoginCLINoOpAfterAdminAssignment
          - TestTagsUserLoginCLICannotRemoveAdminTags
          - TestTagsAuthKeyWithTagRequestNonExistentTag
          - TestTagsAuthKeyWithTagRequestUnownedTag
          - TestTagsAuthKeyWithoutTagRequestNonExistentTag
          - TestTagsAuthKeyWithoutTagRequestUnownedTag
          - TestTagsAdminAPICannotSetNonExistentTag
          - TestTagsAdminAPICanSetUnownedTag
          - TestTagsAdminAPICannotRemoveAllTags
          - TestTagsIssue2978ReproTagReplacement
          - TestTagsAdminAPICannotSetInvalidFormat
          - TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags
          - TestTagsAuthKeyWithoutUserInheritsTags
          - TestTagsAuthKeyWithoutUserRejectsAdvertisedTags
          - TestTagsAuthKeyConvertToUserViaCLIRegister
    uses: ./.github/workflows/integration-test-template.yml
    secrets: inherit
    with:
      test: ${{ matrix.test }}
      postgres_flag: "--postgres=0"
      database_name: "sqlite"
  postgres:
    needs: [build, build-postgres]
    if: needs.build.outputs.files-changed == 'true'
    strategy:
      fail-fast: false
      matrix:
        test:
          - TestACLAllowUserDst
          - TestPingAllByIP
          - TestEphemeral2006DeletedTooQuickly
          - TestPingAllByIPManyUpDown
          - TestSubnetRouterMultiNetwork
    uses: ./.github/workflows/integration-test-template.yml
    secrets: inherit
    with:
      test: ${{ matrix.test }}
      postgres_flag: "--postgres=1"
      database_name: "postgres"


================================================
FILE: .github/workflows/test.yml
================================================
name: Tests

on: [push, pull_request]

concurrency:
  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 2

      - name: Get changed files
        id: changed-files
        uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
        with:
          filters: |
            files:
              - '*.nix'
              - 'go.*'
              - '**/*.go'
              - 'integration_test/'
              - 'config-example.yaml'

      - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
        if: steps.changed-files.outputs.files == 'true'
      - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
        if: steps.changed-files.outputs.files == 'true'
        with:
          primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix',
            '**/flake.lock') }}
          restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}

      - name: Run tests
        if: steps.changed-files.outputs.files == 'true'
        env:
          # As of 2025-01-06, these env vars was not automatically
          # set anymore which breaks the initdb for postgres on
          # some of the database migration tests.
          LC_ALL: "en_US.UTF-8"
          LC_CTYPE: "en_US.UTF-8"
        run: nix develop --command -- gotestsum


================================================
FILE: .github/workflows/update-flake.yml
================================================
name: update-flake-lock
on:
  workflow_dispatch: # allows manual triggering
  schedule:
    - cron: "0 0 * * 0" # runs weekly on Sunday at 00:00

jobs:
  lockfile:
    if: github.repository == 'juanfont/headscale'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - name: Install Nix
        uses: DeterminateSystems/nix-installer-action@21a544727d0c62386e78b4befe52d19ad12692e3 # v17
      - name: Update flake.lock
        uses: DeterminateSystems/update-flake-lock@428c2b58a4b7414dabd372acb6a03dba1084d3ab # v25
        with:
          pr-title: "Update flake.lock"


================================================
FILE: .gitignore
================================================
ignored/
tailscale/
.vscode/
.claude/
logs/

*.prof

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
vendor/

dist/
/headscale
config.yaml
config*.yaml
!config-example.yaml
derp.yaml
*.hujson
*.key
/db.sqlite
*.sqlite3

# Exclude Jetbrains Editors
.idea

test_output/
control_logs/

# Nix build output
result
.direnv/

integration_test/etc/config.dump.yaml

# MkDocs
.cache
/site

__debug_bin

node_modules/
package-lock.json
package.json


================================================
FILE: .golangci.yaml
================================================
---
version: "2"
linters:
  default: all
  disable:
    - cyclop
    - depguard
    - dupl
    - exhaustruct
    - funcorder
    - funlen
    - gochecknoglobals
    - gochecknoinits
    - gocognit
    - godox
    - interfacebloat
    - ireturn
    - lll
    - maintidx
    - makezero
    - mnd
    - musttag
    - nestif
    - nolintlint
    - paralleltest
    - revive
    - tagliatelle
    - testpackage
    - varnamelen
    - wrapcheck
    - wsl
  settings:
    forbidigo:
      forbid:
        # Forbid time.Sleep everywhere with context-appropriate alternatives
        - pattern: 'time\.Sleep'
          msg: >-
            time.Sleep is forbidden.
            In tests: use assert.EventuallyWithT for polling/waiting patterns.
            In production code: use a backoff strategy (e.g., cenkalti/backoff) or proper synchronization primitives.
        # Forbid inline string literals in zerolog field methods - use zf.* constants
        - pattern: '\.(Str|Int|Int8|Int16|Int32|Int64|Uint|Uint8|Uint16|Uint32|Uint64|Float32|Float64|Bool|Dur|Time|TimeDiff|Strs|Ints|Uints|Floats|Bools|Any|Interface)\("[^"]+"'
          msg: >-
            Use zf.* constants for zerolog field names instead of string literals.
            Import "github.com/juanfont/headscale/hscontrol/util/zlog/zf" and use
            constants like zf.NodeID, zf.UserName, etc. Add new constants to
            hscontrol/util/zlog/zf/fields.go if needed.
        # Forbid ptr.To - use Go 1.26 new(expr) instead
        - pattern: 'ptr\.To\('
          msg: >-
            ptr.To is forbidden. Use Go 1.26's new(expr) syntax instead.
            Example: ptr.To(value) → new(value)
        # Forbid tsaddr.SortPrefixes - use slices.SortFunc with netip.Prefix.Compare
        - pattern: 'tsaddr\.SortPrefixes'
          msg: >-
            tsaddr.SortPrefixes is forbidden. Use Go 1.26's netip.Prefix.Compare instead.
            Example: slices.SortFunc(prefixes, netip.Prefix.Compare)
      analyze-types: true
    gocritic:
      disabled-checks:
        - appendAssign
        - ifElseChain
    nlreturn:
      block-size: 4
    varnamelen:
      ignore-names:
        - err
        - db
        - id
        - ip
        - ok
        - c
        - tt
        - tx
        - rx
        - sb
        - wg
        - pr
        - p
        - p2
      ignore-type-assert-ok: true
      ignore-map-index-ok: true
  exclusions:
    generated: lax
    presets:
      - comments
      - common-false-positives
      - legacy
      - std-error-handling
    paths:
      - third_party$
      - builtin$
      - examples$
      - gen

formatters:
  enable:
    - gci
    - gofmt
    - gofumpt
    - goimports
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$
      - gen


================================================
FILE: .goreleaser.yml
================================================
---
version: 2
before:
  hooks:
    - go mod tidy -compat=1.26
    - go mod vendor

release:
  prerelease: auto
  draft: true
  header: |
    ## Upgrade

    Please follow the steps outlined in the [upgrade guide](https://headscale.net/stable/setup/upgrade/) to update your existing Headscale installation.

builds:
  - id: headscale
    main: ./cmd/headscale
    mod_timestamp: "{{ .CommitTimestamp }}"
    env:
      - CGO_ENABLED=0
    targets:
      - darwin_amd64
      - darwin_arm64
      - freebsd_amd64
      - linux_amd64
      - linux_arm64
    flags:
      - -mod=readonly
    tags:
      - ts2019

archives:
  - id: golang-cross
    name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
    formats:
      - binary

source:
  enabled: true
  name_template: "{{ .ProjectName }}_{{ .Version }}"
  format: tar.gz
  files:
    - "vendor/"

nfpms:
  # Configure nFPM for .deb and .rpm releases
  #
  # See https://nfpm.goreleaser.com/configuration/
  # and https://goreleaser.com/customization/nfpm/
  #
  # Useful tools for debugging .debs:
  # List file contents: dpkg -c dist/headscale...deb
  # Package metadata: dpkg --info dist/headscale....deb
  #
  - ids:
      - headscale
    package_name: headscale
    priority: optional
    vendor: headscale
    maintainer: Kristoffer Dalby <kristoffer@dalby.cc>
    homepage: https://github.com/juanfont/headscale
    description: |-
      Open source implementation of the Tailscale control server.
      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 Tailscale network (tailnet),
      suitable for a personal use, or a small open-source organisation.
    bindir: /usr/bin
    section: net
    formats:
      - deb
    contents:
      - src: ./config-example.yaml
        dst: /etc/headscale/config.yaml
        type: config|noreplace
        file_info:
          mode: 0644
      - src: ./packaging/systemd/headscale.service
        dst: /usr/lib/systemd/system/headscale.service
      - dst: /var/lib/headscale
        type: dir
      - src: LICENSE
        dst: /usr/share/doc/headscale/copyright
    scripts:
      postinstall: ./packaging/deb/postinst
      postremove: ./packaging/deb/postrm
      preremove: ./packaging/deb/prerm
    deb:
      lintian_overrides:
        - no-changelog # Our CHANGELOG.md uses a different formatting
        - no-manual-page
        - statically-linked-binary

kos:
  - id: ghcr
    repositories:
      - ghcr.io/juanfont/headscale
      - headscale/headscale

    # bare tells KO to only use the repository
    # for tagging and naming the container.
    bare: true
    base_image: gcr.io/distroless/base-debian13
    build: headscale
    main: ./cmd/headscale
    env:
      - CGO_ENABLED=0
    platforms:
      - linux/amd64
      - linux/arm64
    tags:
      - "{{ if not .Prerelease }}latest{{ end }}"
      - "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}.{{ .Patch }}{{ end }}"
      - "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}{{ end }}"
      - "{{ if not .Prerelease }}{{ .Major }}{{ end }}"
      - "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}{{ end }}"
      - "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}{{ end }}"
      - "{{ if not .Prerelease }}v{{ .Major }}{{ end }}"
      - "{{ if not .Prerelease }}stable{{ else }}unstable{{ end }}"
      - "{{ .Tag }}"
      - '{{ trimprefix .Tag "v" }}'
      - "sha-{{ .ShortCommit }}"
    creation_time: "{{.CommitTimestamp}}"
    ko_data_creation_time: "{{.CommitTimestamp}}"

  - id: ghcr-debug
    repositories:
      - ghcr.io/juanfont/headscale
      - headscale/headscale

    bare: true
    base_image: gcr.io/distroless/base-debian13:debug
    build: headscale
    main: ./cmd/headscale
    env:
      - CGO_ENABLED=0
    platforms:
      - linux/amd64
      - linux/arm64
    tags:
      - "{{ if not .Prerelease }}latest-debug{{ end }}"
      - "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
      - "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}-debug{{ end }}"
      - "{{ if not .Prerelease }}{{ .Major }}-debug{{ end }}"
      - "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}.{{ .Patch }}-debug{{ end }}"
      - "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}-debug{{ end }}"
      - "{{ if not .Prerelease }}v{{ .Major }}-debug{{ end }}"
      - "{{ if not .Prerelease }}stable-debug{{ else }}unstable-debug{{ end }}"
      - "{{ .Tag }}-debug"
      - '{{ trimprefix .Tag "v" }}-debug'
      - "sha-{{ .ShortCommit }}-debug"

checksum:
  name_template: "checksums.txt"
snapshot:
  version_template: "{{ .Tag }}-next"
changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"


================================================
FILE: .mcp.json
================================================
{
  "mcpServers": {
    "claude-code-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@steipete/claude-code-mcp@latest"],
      "env": {}
    },
    "sequential-thinking": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
      "env": {}
    },
    "nixos": {
      "type": "stdio",
      "command": "uvx",
      "args": ["mcp-nixos"],
      "env": {}
    },
    "context7": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@upstash/context7-mcp"],
      "env": {}
    },
    "git": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@cyanheads/git-mcp-server"],
      "env": {}
    }
  }
}


================================================
FILE: .mdformat.toml
================================================
[plugin.mkdocs]
align_semantic_breaks_in_lists = true


================================================
FILE: .pre-commit-config.yaml
================================================
# prek/pre-commit configuration for headscale
# See: https://prek.j178.dev/quickstart/
# See: https://prek.j178.dev/builtin/

# Global exclusions - ignore generated code
exclude: ^gen/

repos:
  # Built-in hooks from pre-commit/pre-commit-hooks
  # prek will use fast-path optimized versions automatically
  # See: https://prek.j178.dev/builtin/
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-added-large-files
      - id: check-case-conflict
      - id: check-executables-have-shebangs
      - id: check-json
      - id: check-merge-conflict
      - id: check-symlinks
      - id: check-toml
      - id: check-xml
      - id: check-yaml
      - id: detect-private-key
      - id: end-of-file-fixer
      - id: fix-byte-order-marker
      - id: mixed-line-ending
      - id: trailing-whitespace

  # Local hooks for project-specific tooling
  - repo: local
    hooks:
      # nixpkgs-fmt for Nix files
      - id: nixpkgs-fmt
        name: nixpkgs-fmt
        entry: nixpkgs-fmt
        language: system
        files: \.nix$

      # Prettier for formatting
      - id: prettier
        name: prettier
        entry: prettier --write --list-different
        language: system
        exclude: ^docs/
        types_or: [javascript, jsx, ts, tsx, yaml, json, toml, html, css, scss, sass, markdown]

      # mdformat for docs
      - id: mdformat
        name: mdformat
        entry: mdformat
        language: system
        types_or: [markdown]
        files: ^docs/

      # golangci-lint for Go code quality
      - id: golangci-lint
        name: golangci-lint
        entry: nix develop --command -- golangci-lint run --new-from-rev=HEAD~1 --timeout=5m --fix
        language: system
        types: [go]
        pass_filenames: false


================================================
FILE: .prettierignore
================================================
.github/workflows/test-integration-v2*
docs/


================================================
FILE: AGENTS.md
================================================
# AGENTS.md

This file provides guidance to AI agents when working with code in this repository.

## Overview

Headscale is an open-source implementation of the Tailscale control server written in Go. It provides self-hosted coordination for Tailscale networks (tailnets), managing node registration, IP allocation, policy enforcement, and DERP routing.

## Development Commands

### Quick Setup

```bash
# Recommended: Use Nix for dependency management
nix develop

# Full development workflow
make dev  # runs fmt + lint + test + build
```

### Essential Commands

```bash
# Build headscale binary
make build

# Run tests
make test
go test ./...                    # All unit tests
go test -race ./...              # With race detection

# Run specific integration test
go run ./cmd/hi run "TestName" --postgres

# Code formatting and linting
make fmt         # Format all code (Go, docs, proto)
make lint        # Lint all code (Go, proto)
make fmt-go      # Format Go code only
make lint-go     # Lint Go code only

# Protocol buffer generation (after modifying proto/)
make generate

# Clean build artifacts
make clean
```

### Integration Testing

```bash
# Use the hi (Headscale Integration) test runner
go run ./cmd/hi doctor                    # Check system requirements
go run ./cmd/hi run "TestPattern"         # Run specific test
go run ./cmd/hi run "TestPattern" --postgres  # With PostgreSQL backend

# Test artifacts are saved to control_logs/ with logs and debug data
```

## Pre-Commit Quality Checks

### **MANDATORY: Automated Pre-Commit Hooks with prek**

**CRITICAL REQUIREMENT**: This repository uses [prek](https://prek.j178.dev/) for automated pre-commit hooks. All commits are automatically validated for code quality, formatting, and common issues.

### Initial Setup

When you first clone the repository or enter the nix shell, install the git hooks:

```bash
# Enter nix development environment
nix develop

# Install prek git hooks (one-time setup)
prek install
```

This installs the pre-commit hook at `.git/hooks/pre-commit` which automatically runs all configured checks before each commit.

### Configured Hooks

The repository uses `.pre-commit-config.yaml` with the following hooks:

**Built-in Checks** (optimized fast-path execution):

- `check-added-large-files` - Prevents accidentally committing large files
- `check-case-conflict` - Checks for files that would conflict in case-insensitive filesystems
- `check-executables-have-shebangs` - Ensures executables have proper shebangs
- `check-json` - Validates JSON syntax
- `check-merge-conflict` - Prevents committing files with merge conflict markers
- `check-symlinks` - Checks for broken symlinks
- `check-toml` - Validates TOML syntax
- `check-xml` - Validates XML syntax
- `check-yaml` - Validates YAML syntax
- `detect-private-key` - Detects accidentally committed private keys
- `end-of-file-fixer` - Ensures files end with a newline
- `fix-byte-order-marker` - Removes UTF-8 byte order markers
- `mixed-line-ending` - Prevents mixed line endings
- `trailing-whitespace` - Removes trailing whitespace

**Project-Specific Hooks**:

- `nixpkgs-fmt` - Formats Nix files
- `prettier` - Formats markdown, YAML, JSON, and TOML files
- `golangci-lint` - Runs Go linter with auto-fix on changed files only

### Manual Hook Execution

Run hooks manually without making a commit:

```bash
# Run hooks on staged files only
prek run

# Run hooks on all files in the repository
prek run --all-files

# Run a specific hook
prek run golangci-lint

# Run hooks on specific files
prek run --files path/to/file1.go path/to/file2.go
```

### Workflow Pattern

With prek installed, your normal workflow becomes:

```bash
# 1. Make your code changes
vim hscontrol/state/state.go

# 2. Stage your changes
git add .

# 3. Commit - hooks run automatically
git commit -m "feat: add new feature"

# If hooks fail, they will show which checks failed
# Fix the issues and try committing again
```

### Manual golangci-lint

While golangci-lint runs automatically via prek, you can also run it manually:

```bash
# If you have upstream remote configured (recommended)
golangci-lint run --new-from-rev=upstream/main --timeout=5m --fix

# If you only have origin remote
golangci-lint run --new-from-rev=main --timeout=5m --fix
```

**Important**: Always use `--new-from-rev` to only lint changed files. This prevents formatting the entire repository and keeps changes focused on your actual modifications.

### Skipping Hooks (Not Recommended)

In rare cases where you need to skip hooks (e.g., work-in-progress commits), use:

```bash
git commit --no-verify -m "WIP: work in progress"
```

**WARNING**: Only use `--no-verify` for temporary WIP commits on feature branches. All commits to main must pass all hooks.

### Troubleshooting

**Hook installation issues**:

```bash
# Check if hooks are installed
ls -la .git/hooks/pre-commit

# Reinstall hooks
prek install
```

**Hooks running slow**:

```bash
# prek uses optimized fast-path for built-in hooks
# If running slow, check which hook is taking time with verbose output
prek run -v
```

**Update hook configuration**:

```bash
# After modifying .pre-commit-config.yaml, hooks will automatically use new config
# No reinstallation needed
```

## Project Structure & Architecture

### Top-Level Organization

```
headscale/
├── cmd/                    # Command-line applications
│   ├── headscale/         # Main headscale server binary
│   └── hi/               # Headscale Integration test runner
├── hscontrol/            # Core control plane logic
├── integration/          # End-to-end Docker-based tests
├── proto/               # Protocol buffer definitions
├── gen/                 # Generated code (protobuf)
├── docs/                # Documentation
└── packaging/           # Distribution packaging
```

### Core Packages (`hscontrol/`)

**Main Server (`hscontrol/`)**

- `app.go`: Application setup, dependency injection, server lifecycle
- `handlers.go`: HTTP/gRPC API endpoints for management operations
- `grpcv1.go`: gRPC service implementation for headscale API
- `poll.go`: **Critical** - Handles Tailscale MapRequest/MapResponse protocol
- `noise.go`: Noise protocol implementation for secure client communication
- `auth.go`: Authentication flows (web, OIDC, command-line)
- `oidc.go`: OpenID Connect integration for user authentication

**State Management (`hscontrol/state/`)**

- `state.go`: Central coordinator for all subsystems (database, policy, IP allocation, DERP)
- `node_store.go`: **Performance-critical** - In-memory cache with copy-on-write semantics
- Thread-safe operations with deadlock detection
- Coordinates between database persistence and real-time operations

**Database Layer (`hscontrol/db/`)**

- `db.go`: Database abstraction, GORM setup, migration management
- `node.go`: Node lifecycle, registration, expiration, IP assignment
- `users.go`: User management, namespace isolation
- `api_key.go`: API authentication tokens
- `preauth_keys.go`: Pre-authentication keys for automated node registration
- `ip.go`: IP address allocation and management
- `policy.go`: Policy storage and retrieval
- Schema migrations in `schema.sql` with extensive test data coverage

**CRITICAL DATABASE MIGRATION RULES**:

1. **NEVER reorder existing migrations** - Migration order is immutable once committed
2. **ONLY add new migrations to the END** of the migrations array
3. **NEVER disable foreign keys** in new migrations - no new migrations should be added to `migrationsRequiringFKDisabled`
4. **Migration ID format**: `YYYYMMDDHHSS-short-description` (timestamp + descriptive suffix)
   - Example: `202511131500-add-user-roles`
   - The timestamp must be chronologically ordered
5. **New migrations go after the comment** "As of 2025-07-02, no new IDs should be added here"
6. If you need to rename a column that other migrations depend on:
   - Accept that the old column name will exist in intermediate migration states
   - Update code to work with the new column name
   - Let AutoMigrate create the new column if needed
   - Do NOT try to rename columns that later migrations reference

**Policy Engine (`hscontrol/policy/`)**

- `policy.go`: Core ACL evaluation logic, HuJSON parsing
- `v2/`: Next-generation policy system with improved filtering
- `matcher/`: ACL rule matching and evaluation engine
- Determines peer visibility, route approval, and network access rules
- Supports both file-based and database-stored policies

**Network Management (`hscontrol/`)**

- `derp/`: DERP (Designated Encrypted Relay for Packets) server implementation
  - NAT traversal when direct connections fail
  - Fallback relay for firewall-restricted environments
- `mapper/`: Converts internal Headscale state to Tailscale's wire protocol format
  - `tail.go`: Tailscale-specific data structure generation
- `routes/`: Subnet route management and primary route selection
- `dns/`: DNS record management and MagicDNS implementation

**Utilities & Support (`hscontrol/`)**

- `types/`: Core data structures, configuration, validation
- `util/`: Helper functions for networking, DNS, key management
- `templates/`: Client configuration templates (Apple, Windows, etc.)
- `notifier/`: Event notification system for real-time updates
- `metrics.go`: Prometheus metrics collection
- `capver/`: Tailscale capability version management

### Key Subsystem Interactions

**Node Registration Flow**

1. **Client Connection**: `noise.go` handles secure protocol handshake
2. **Authentication**: `auth.go` validates credentials (web/OIDC/preauth)
3. **State Creation**: `state.go` coordinates IP allocation via `db/ip.go`
4. **Storage**: `db/node.go` persists node, `NodeStore` caches in memory
5. **Network Setup**: `mapper/` generates initial Tailscale network map

**Ongoing Operations**

1. **Poll Requests**: `poll.go` receives periodic client updates
2. **State Updates**: `NodeStore` maintains real-time node information
3. **Policy Application**: `policy/` evaluates ACL rules for peer relationships
4. **Map Distribution**: `mapper/` sends network topology to all affected clients

**Route Management**

1. **Advertisement**: Clients announce routes via `poll.go` Hostinfo updates
2. **Storage**: `db/` persists routes, `NodeStore` caches for performance
3. **Approval**: `policy/` auto-approves routes based on ACL rules
4. **Distribution**: `routes/` selects primary routes, `mapper/` distributes to peers

### Command-Line Tools (`cmd/`)

**Main Server (`cmd/headscale/`)**

- `headscale.go`: CLI parsing, configuration loading, server startup
- Supports daemon mode, CLI operations (user/node management), database operations

**Integration Test Runner (`cmd/hi/`)**

- `main.go`: Test execution framework with Docker orchestration
- `run.go`: Individual test execution with artifact collection
- `doctor.go`: System requirements validation
- `docker.go`: Container lifecycle management
- Essential for validating changes against real Tailscale clients

### Generated & External Code

**Protocol Buffers (`proto/` → `gen/`)**

- Defines gRPC API for headscale management operations
- Client libraries can generate from these definitions
- Run `make generate` after modifying `.proto` files

**Integration Testing (`integration/`)**

- `scenario.go`: Docker test environment setup
- `tailscale.go`: Tailscale client container management
- Individual test files for specific functionality areas
- Real end-to-end validation with network isolation

### Critical Performance Paths

**High-Frequency Operations**

1. **MapRequest Processing** (`poll.go`): Every 15-60 seconds per client
2. **NodeStore Reads** (`node_store.go`): Every operation requiring node data
3. **Policy Evaluation** (`policy/`): On every peer relationship calculation
4. **Route Lookups** (`routes/`): During network map generation

**Database Write Patterns**

- **Frequent**: Node heartbeats, endpoint updates, route changes
- **Moderate**: User operations, policy updates, API key management
- **Rare**: Schema migrations, bulk operations

### Configuration & Deployment

**Configuration** (`hscontrol/types/config.go`)\*\*

- Database connection settings (SQLite/PostgreSQL)
- Network configuration (IP ranges, DNS settings)
- Policy mode (file vs database)
- DERP relay configuration
- OIDC provider settings

**Key Dependencies**

- **GORM**: Database ORM with migration support
- **Tailscale Libraries**: Core networking and protocol code
- **Zerolog**: Structured logging throughout the application
- **Buf**: Protocol buffer toolchain for code generation

### Development Workflow Integration

The architecture supports incremental development:

- **Unit Tests**: Focus on individual packages (`*_test.go` files)
- **Integration Tests**: Validate cross-component interactions
- **Database Tests**: Extensive migration and data integrity validation
- **Policy Tests**: ACL rule evaluation and edge cases
- **Performance Tests**: NodeStore and high-frequency operation validation

## Integration Testing System

### Overview

Headscale uses Docker-based integration tests with real Tailscale clients to validate end-to-end functionality. The integration test system is complex and requires specialized knowledge for effective execution and debugging.

### **MANDATORY: Use the headscale-integration-tester Agent**

**CRITICAL REQUIREMENT**: For ANY integration test execution, analysis, troubleshooting, or validation, you MUST use the `headscale-integration-tester` agent. This agent contains specialized knowledge about:

- Test execution strategies and timing requirements
- Infrastructure vs code issue distinction (99% vs 1% failure patterns)
- Security-critical debugging rules and forbidden practices
- Comprehensive artifact analysis workflows
- Real-world failure patterns from HA debugging experiences

### Quick Reference Commands

```bash
# Check system requirements (always run first)
go run ./cmd/hi doctor

# Run single test (recommended for development)
go run ./cmd/hi run "TestName"

# Use PostgreSQL for database-heavy tests
go run ./cmd/hi run "TestName" --postgres

# Pattern matching for related tests
go run ./cmd/hi run "TestPattern*"

# Run multiple tests concurrently (each gets isolated run ID)
go run ./cmd/hi run "TestPingAllByIP" &
go run ./cmd/hi run "TestACLAllowUserDst" &
go run ./cmd/hi run "TestOIDCAuthenticationPingAll" &
```

**Concurrent Execution Support**:

The test runner supports running multiple tests concurrently on the same Docker daemon:

- Each test run gets a **unique Run ID** (format: `YYYYMMDD-HHMMSS-{6-char-hash}`)
- All containers are labeled with `hi.run-id` for isolation
- Container names include the run ID for easy identification (e.g., `ts-{runID}-1-74-{hash}`)
- Dynamic port allocation prevents port conflicts between concurrent runs
- Cleanup only affects containers belonging to the specific run ID
- Log directories are isolated per run: `control_logs/{runID}/`

**Critical Notes**:

- Tests generate ~100MB of logs per run in `control_logs/`
- Running many tests concurrently may cause resource contention (CPU/memory)
- Clean stale containers periodically: `docker system prune -f`

### Test Artifacts Location

All test runs save comprehensive debugging artifacts to `control_logs/TIMESTAMP-ID/` including server logs, client logs, database dumps, MapResponse protocol data, and Prometheus metrics.

**For all integration test work, use the headscale-integration-tester agent - it contains the complete knowledge needed for effective testing and debugging.**

## NodeStore Implementation Details

**Key Insight from Recent Work**: The NodeStore is a critical performance optimization that caches node data in memory while ensuring consistency with the database. When working with route advertisements or node state changes:

1. **Timing Considerations**: Route advertisements need time to propagate from clients to server. Use `require.EventuallyWithT()` patterns in tests instead of immediate assertions.

2. **Synchronization Points**: NodeStore updates happen at specific points like `poll.go:420` after Hostinfo changes. Ensure these are maintained when modifying the polling logic.

3. **Peer Visibility**: The NodeStore's `peersFunc` determines which nodes are visible to each other. Policy-based filtering is separate from monitoring visibility - expired nodes should remain visible for debugging but marked as expired.

## Testing Guidelines

### Integration Test Patterns

#### **CRITICAL: EventuallyWithT Pattern for External Calls**

**All external calls in integration tests MUST be wrapped in EventuallyWithT blocks** to handle eventual consistency in distributed systems. External calls include:

- `client.Status()` - Getting Tailscale client status
- `client.Curl()` - Making HTTP requests through clients
- `client.Traceroute()` - Running network diagnostics
- `headscale.ListNodes()` - Querying headscale server state
- Any other calls that interact with external systems or network operations

**Key Rules**:

1. **Never use bare `require.NoError(t, err)` with external calls** - Always wrap in EventuallyWithT
2. **Keep related assertions together** - If multiple assertions depend on the same external call, keep them in the same EventuallyWithT block
3. **Split unrelated external calls** - Different external calls should be in separate EventuallyWithT blocks
4. **Never nest EventuallyWithT calls** - Each EventuallyWithT should be at the same level
5. **Declare shared variables at function scope** - Variables used across multiple EventuallyWithT blocks must be declared before first use

**Examples**:

```go
// CORRECT: External call wrapped in EventuallyWithT
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    status, err := client.Status()
    assert.NoError(c, err)

    // Related assertions using the same status call
    for _, peerKey := range status.Peers() {
        peerStatus := status.Peer[peerKey]
        assert.NotNil(c, peerStatus.PrimaryRoutes)
        requirePeerSubnetRoutesWithCollect(c, peerStatus, expectedRoutes)
    }
}, 5*time.Second, 200*time.Millisecond, "Verifying client status and routes")

// INCORRECT: Bare external call without EventuallyWithT
status, err := client.Status()  // ❌ Will fail intermittently
require.NoError(t, err)

// CORRECT: Separate EventuallyWithT for different external calls
// First external call - headscale.ListNodes()
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    nodes, err := headscale.ListNodes()
    assert.NoError(c, err)
    assert.Len(c, nodes, 2)
    requireNodeRouteCountWithCollect(c, nodes[0], 2, 2, 2)
}, 10*time.Second, 500*time.Millisecond, "route state changes should propagate to nodes")

// Second external call - client.Status()
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    status, err := client.Status()
    assert.NoError(c, err)

    for _, peerKey := range status.Peers() {
        peerStatus := status.Peer[peerKey]
        requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()})
    }
}, 10*time.Second, 500*time.Millisecond, "routes should be visible to client")

// INCORRECT: Multiple unrelated external calls in same EventuallyWithT
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    nodes, err := headscale.ListNodes()  // ❌ First external call
    assert.NoError(c, err)

    status, err := client.Status()  // ❌ Different external call - should be separate
    assert.NoError(c, err)
}, 10*time.Second, 500*time.Millisecond, "mixed calls")

// CORRECT: Variable scoping for shared data
var (
    srs1, srs2, srs3       *ipnstate.Status
    clientStatus           *ipnstate.Status
    srs1PeerStatus         *ipnstate.PeerStatus
)

assert.EventuallyWithT(t, func(c *assert.CollectT) {
    srs1 = subRouter1.MustStatus()  // = not :=
    srs2 = subRouter2.MustStatus()
    clientStatus = client.MustStatus()

    srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
    // assertions...
}, 5*time.Second, 200*time.Millisecond, "checking router status")

// CORRECT: Wrapping client operations
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    result, err := client.Curl(weburl)
    assert.NoError(c, err)
    assert.Len(c, result, 13)
}, 5*time.Second, 200*time.Millisecond, "Verifying HTTP connectivity")

assert.EventuallyWithT(t, func(c *assert.CollectT) {
    tr, err := client.Traceroute(webip)
    assert.NoError(c, err)
    assertTracerouteViaIPWithCollect(c, tr, expectedRouter.MustIPv4())
}, 5*time.Second, 200*time.Millisecond, "Verifying network path")
```

**Helper Functions**:

- Use `requirePeerSubnetRoutesWithCollect` instead of `requirePeerSubnetRoutes` inside EventuallyWithT
- Use `requireNodeRouteCountWithCollect` instead of `requireNodeRouteCount` inside EventuallyWithT
- Use `assertTracerouteViaIPWithCollect` instead of `assertTracerouteViaIP` inside EventuallyWithT

```go
// Node route checking by actual node properties, not array position
var routeNode *v1.Node
for _, node := range nodes {
    if nodeIDStr := fmt.Sprintf("%d", node.GetId()); expectedRoutes[nodeIDStr] != "" {
        routeNode = node
        break
    }
}
```

### Running Problematic Tests

- Some tests require significant time (e.g., `TestNodeOnlineStatus` runs for 12 minutes)
- Infrastructure issues like disk space can cause test failures unrelated to code changes
- Use `--postgres` flag when testing database-heavy scenarios

## Quality Assurance and Testing Requirements

### **MANDATORY: Always Use Specialized Testing Agents**

**CRITICAL REQUIREMENT**: For ANY task involving testing, quality assurance, review, or validation, you MUST use the appropriate specialized agent at the END of your task list. This ensures comprehensive quality validation and prevents regressions.

**Required Agents for Different Task Types**:

1. **Integration Testing**: Use `headscale-integration-tester` agent for:
   - Running integration tests with `cmd/hi`
   - Analyzing test failures and artifacts
   - Troubleshooting Docker-based test infrastructure
   - Validating end-to-end functionality changes

2. **Quality Control**: Use `quality-control-enforcer` agent for:
   - Code review and validation
   - Ensuring best practices compliance
   - Preventing common pitfalls and anti-patterns
   - Validating architectural decisions

**Agent Usage Pattern**: Always add the appropriate agent as the FINAL step in any task list to ensure quality validation occurs after all work is complete.

### Integration Test Debugging Reference

Test artifacts are preserved in `control_logs/TIMESTAMP-ID/` including:

- Headscale server logs (stderr/stdout)
- Tailscale client logs and status
- Database dumps and network captures
- MapResponse JSON files for protocol debugging

**For integration test issues, ALWAYS use the headscale-integration-tester agent - do not attempt manual debugging.**

## EventuallyWithT Pattern for Integration Tests

### Overview

EventuallyWithT is a testing pattern used to handle eventual consistency in distributed systems. In Headscale integration tests, many operations are asynchronous - clients advertise routes, the server processes them, updates propagate through the network. EventuallyWithT allows tests to wait for these operations to complete while making assertions.

### External Calls That Must Be Wrapped

The following operations are **external calls** that interact with the headscale server or tailscale clients and MUST be wrapped in EventuallyWithT:

- `headscale.ListNodes()` - Queries server state
- `client.Status()` - Gets client network status
- `client.Curl()` - Makes HTTP requests through the network
- `client.Traceroute()` - Performs network diagnostics
- `client.Execute()` when running commands that query state
- Any operation that reads from the headscale server or tailscale client

### Operations That Must NOT Be Wrapped

The following are **blocking operations** that modify state and should NOT be wrapped in EventuallyWithT:

- `tailscale set` commands (e.g., `--advertise-routes`, `--exit-node`)
- Any command that changes configuration or state
- Use `client.MustStatus()` instead of `client.Status()` when you just need the ID for a blocking operation

### Five Key Rules for EventuallyWithT

1. **One External Call Per EventuallyWithT Block**
   - Each EventuallyWithT should make ONE external call (e.g., ListNodes OR Status)
   - Related assertions based on that single call can be grouped together
   - Unrelated external calls must be in separate EventuallyWithT blocks

2. **Variable Scoping**
   - Declare variables that need to be shared across EventuallyWithT blocks at function scope
   - Use `=` for assignment inside EventuallyWithT, not `:=` (unless the variable is only used within that block)
   - Variables declared with `:=` inside EventuallyWithT are not accessible outside

3. **No Nested EventuallyWithT**
   - NEVER put an EventuallyWithT inside another EventuallyWithT
   - This is a critical anti-pattern that must be avoided

4. **Use CollectT for Assertions**
   - Inside EventuallyWithT, use `assert` methods with the CollectT parameter
   - Helper functions called within EventuallyWithT must accept `*assert.CollectT`

5. **Descriptive Messages**
   - Always provide a descriptive message as the last parameter
   - Message should explain what condition is being waited for

### Correct Pattern Examples

```go
// CORRECT: Blocking operation NOT wrapped
for _, client := range allClients {
    status := client.MustStatus()
    command := []string{
        "tailscale",
        "set",
        "--advertise-routes=" + expectedRoutes[string(status.Self.ID)],
    }
    _, _, err = client.Execute(command)
    require.NoErrorf(t, err, "failed to advertise route: %s", err)
}

// CORRECT: Single external call with related assertions
var nodes []*v1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    nodes, err = headscale.ListNodes()
    assert.NoError(c, err)
    assert.Len(c, nodes, 2)
    requireNodeRouteCountWithCollect(c, nodes[0], 2, 2, 2)
}, 10*time.Second, 500*time.Millisecond, "nodes should have expected route counts")

// CORRECT: Separate EventuallyWithT for different external call
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    status, err := client.Status()
    assert.NoError(c, err)
    for _, peerKey := range status.Peers() {
        peerStatus := status.Peer[peerKey]
        requirePeerSubnetRoutesWithCollect(c, peerStatus, expectedPrefixes)
    }
}, 10*time.Second, 500*time.Millisecond, "client should see expected routes")
```

### Incorrect Patterns to Avoid

```go
// INCORRECT: Blocking operation wrapped in EventuallyWithT
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    status, err := client.Status()
    assert.NoError(c, err)

    // This is a blocking operation - should NOT be in EventuallyWithT!
    command := []string{
        "tailscale",
        "set",
        "--advertise-routes=" + expectedRoutes[string(status.Self.ID)],
    }
    _, _, err = client.Execute(command)
    assert.NoError(c, err)
}, 5*time.Second, 200*time.Millisecond, "wrong pattern")

// INCORRECT: Multiple unrelated external calls in same EventuallyWithT
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    // First external call
    nodes, err := headscale.ListNodes()
    assert.NoError(c, err)
    assert.Len(c, nodes, 2)

    // Second unrelated external call - WRONG!
    status, err := client.Status()
    assert.NoError(c, err)
    assert.NotNil(c, status)
}, 10*time.Second, 500*time.Millisecond, "mixed operations")
```

## Tags-as-Identity Architecture

### Overview

Headscale implements a **tags-as-identity** model where tags and user ownership are mutually exclusive ways to identify nodes. This is a fundamental architectural principle that affects node registration, ownership, ACL evaluation, and API behavior.

### Core Principle: Tags XOR User Ownership

Every node in Headscale is **either** tagged **or** user-owned, never both:

- **Tagged Nodes**: Ownership is defined by tags (e.g., `tag:server`, `tag:database`)
  - Tags are set during registration via tagged PreAuthKey
  - Tags are immutable after registration (cannot be changed via API)
  - May have `UserID` set for "created by" tracking, but ownership is via tags
  - Identified by: `node.IsTagged()` returns `true`

- **User-Owned Nodes**: Ownership is defined by user assignment
  - Registered via OIDC, web auth, or untagged PreAuthKey
  - Node belongs to a specific user's namespace
  - No tags (empty tags array)
  - Identified by: `node.UserID().Valid() && !node.IsTagged()`

### Critical Implementation Details

#### Node Identification Methods

```go
// Primary methods for determining node ownership
node.IsTagged()      // Returns true if node has tags OR AuthKey.Tags
node.HasTag(tag)     // Returns true if node has specific tag
node.IsUserOwned()   // Returns true if UserID set AND not tagged

// IMPORTANT: UserID can be set on tagged nodes for tracking!
// Always use IsTagged() to determine actual ownership, not just UserID.Valid()
```

#### UserID Field Semantics

**Critical distinction**: `UserID` has different meanings depending on node type:

- **Tagged nodes**: `UserID` is optional "created by" tracking
  - Indicates which user created the tagged PreAuthKey
  - Does NOT define ownership (tags define ownership)
  - Example: User "alice" creates tagged PreAuthKey with `tag:server`, node gets `UserID=alice.ID` + `Tags=["tag:server"]`

- **User-owned nodes**: `UserID` defines ownership
  - Required field for non-tagged nodes
  - Defines which user namespace the node belongs to
  - Example: User "bob" registers via OIDC, node gets `UserID=bob.ID` + `Tags=[]`

#### Mapper Behavior (mapper/tail.go)

The mapper converts internal nodes to Tailscale protocol format, handling the TaggedDevices special user:

```go
// From mapper/tail.go:102-116
User: func() tailcfg.UserID {
    // IMPORTANT: Tags-as-identity model
    // Tagged nodes ALWAYS use TaggedDevices user, even if UserID is set
    if node.IsTagged() {
        return tailcfg.UserID(int64(types.TaggedDevices.ID))
    }
    // User-owned nodes: use the actual user ID
    return tailcfg.UserID(int64(node.UserID().Get()))
}()
```

**TaggedDevices constant** (`types.TaggedDevices.ID = 2147455555`): Special user ID for all tagged nodes in MapResponse protocol.

#### Registration Flow

**Tagged Node Registration** (via tagged PreAuthKey):

1. User creates PreAuthKey with tags: `pak.Tags = ["tag:server"]`
2. Node registers with PreAuthKey
3. Node gets: `Tags = ["tag:server"]`, `UserID = user.ID` (optional tracking), `AuthKeyID = pak.ID`
4. `IsTagged()` returns `true` (ownership via tags)
5. MapResponse sends `User = TaggedDevices.ID`

**User-Owned Node Registration** (via OIDC/web/untagged PreAuthKey):

1. User authenticates or uses untagged PreAuthKey
2. Node registers
3. Node gets: `Tags = []`, `UserID = user.ID` (required)
4. `IsTagged()` returns `false` (ownership via user)
5. MapResponse sends `User = user.ID`

#### API Validation (SetTags)

The SetTags gRPC API enforces tags-as-identity rules:

```go
// From grpcv1.go:340-347
// User-owned nodes are nodes with UserID that are NOT tagged
isUserOwned := nodeView.UserID().Valid() && !nodeView.IsTagged()
if isUserOwned && len(request.GetTags()) > 0 {
    return error("cannot set tags on user-owned nodes")
}
```

**Key validation rules**:

- ✅ Can call SetTags on tagged nodes (tags already define ownership)
- ❌ Cannot set tags on user-owned nodes (would violate XOR rule)
- ❌ Cannot remove all tags from tagged nodes (would orphan the node)

#### Database Layer (db/node.go)

**Tag storage**: Tags are stored in PostgreSQL ARRAY column and SQLite JSON column:

```sql
-- From schema.sql
tags TEXT[] DEFAULT '{}' NOT NULL,  -- PostgreSQL
tags TEXT DEFAULT '[]' NOT NULL,    -- SQLite (JSON array)
```

**Validation** (`state/tags.go`):

- `validateNodeOwnership()`: Enforces tags XOR user rule
- `validateAndNormalizeTags()`: Validates tag format (`tag:name`) and uniqueness

#### Policy Layer

**Tag Ownership** (policy/v2/policy.go):

```go
func NodeCanHaveTag(node types.NodeView, tag string) bool {
    // Checks if node's IP is in the tagOwnerMap IP set
    // This is IP-based authorization, not UserID-based
    if ips, ok := pm.tagOwnerMap[Tag(tag)]; ok {
        if slices.ContainsFunc(node.IPs(), ips.Contains) {
            return true
        }
    }
    return false
}
```

**Important**: Tag authorization is based on IP ranges in ACL, not UserID. Tags define identity, ACL authorizes that identity.

### Testing Tags-as-Identity

**Unit Tests** (`hscontrol/types/node_tags_test.go`):

- `TestNodeIsTagged`: Validates IsTagged() for various scenarios
- `TestNodeOwnershipModel`: Tests tags XOR user ownership
- `TestUserTypedID`: Helper method validation

**API Tests** (`hscontrol/grpcv1_test.go`):

- `TestSetTags_UserXORTags`: Validates rejection of setting tags on user-owned nodes
- `TestSetTags_TaggedNode`: Validates that tagged nodes (even with UserID) are not rejected

**Auth Tests** (`hscontrol/auth_test.go:890-928`):

- Tests node registration with tagged PreAuthKey
- Validates tags are applied during registration

### Common Pitfalls

1. **Don't check only `UserID.Valid()` to determine user ownership**
   - ❌ Wrong: `if node.UserID().Valid() { /* user-owned */ }`
   - ✅ Correct: `if node.UserID().Valid() && !node.IsTagged() { /* user-owned */ }`

2. **Don't assume tagged nodes never have UserID set**
   - Tagged nodes MAY have UserID for "created by" tracking
   - Always use `IsTagged()` to determine ownership type

3. **Don't allow setting tags on user-owned nodes**
   - This violates the tags XOR user principle
   - Use API validation to prevent this

4. **Don't forget TaggedDevices in mapper**
   - All tagged nodes MUST use `TaggedDevices.ID` in MapResponse
   - User ID is only for actual user-owned nodes

### Migration Considerations

When nodes transition between ownership models:

- **No automatic migration**: Tags-as-identity is set at registration and immutable
- **Re-registration required**: To change from user-owned to tagged (or vice versa), node must be deleted and re-registered
- **UserID persistence**: UserID on tagged nodes is informational and not cleared

### Architecture Benefits

The tags-as-identity model provides:

1. **Clear ownership semantics**: No ambiguity about who/what owns a node
2. **ACL simplicity**: Tag-based access control without user conflicts
3. **API safety**: Validation prevents invalid ownership states
4. **Protocol compatibility**: TaggedDevices special user aligns with Tailscale's model

## Logging Patterns

### Incremental Log Event Building

When building log statements with multiple fields, especially with conditional fields, use the **incremental log event pattern** instead of long single-line chains. This improves readability and allows conditional field addition.

**Pattern:**

```go
// GOOD: Incremental building with conditional fields
logEvent := log.Debug().
    Str("node", node.Hostname).
    Str("machine_key", node.MachineKey.ShortString()).
    Str("node_key", node.NodeKey.ShortString())

if node.User != nil {
    logEvent = logEvent.Str("user", node.User.Username())
} else if node.UserID != nil {
    logEvent = logEvent.Uint("user_id", *node.UserID)
} else {
    logEvent = logEvent.Str("user", "none")
}

logEvent.Msg("Registering node")
```

**Key rules:**

1. **Assign chained calls back to the variable**: `logEvent = logEvent.Str(...)` - zerolog methods return a new event, so you must capture the return value
2. **Use for conditional fields**: When fields depend on runtime conditions, build incrementally
3. **Use for long log lines**: When a log line exceeds ~100 characters, split it for readability
4. **Call `.Msg()` at the end**: The final `.Msg()` or `.Msgf()` sends the log event

**Anti-pattern to avoid:**

```go
// BAD: Long single-line chains are hard to read and can't have conditional fields
log.Debug().Caller().Str("node", node.Hostname).Str("machine_key", node.MachineKey.ShortString()).Str("node_key", node.NodeKey.ShortString()).Str("user", node.User.Username()).Msg("Registering node")

// BAD: Forgetting to assign the return value (field is lost!)
logEvent := log.Debug().Str("node", node.Hostname)
logEvent.Str("user", username)  // This field is LOST - not assigned back
logEvent.Msg("message")         // Only has "node" field
```

**When to use this pattern:**

- Log statements with 4+ fields
- Any log with conditional fields
- Complex logging in loops or error handling
- When you need to add context incrementally

**Example from codebase** (`hscontrol/db/node.go`):

```go
logEvent := log.Debug().
    Str("node", node.Hostname).
    Str("machine_key", node.MachineKey.ShortString()).
    Str("node_key", node.NodeKey.ShortString())

if node.User != nil {
    logEvent = logEvent.Str("user", node.User.Username())
} else if node.UserID != nil {
    logEvent = logEvent.Uint("user_id", *node.UserID)
} else {
    logEvent = logEvent.Str("user", "none")
}

logEvent.Msg("Registering test node")
```

### Avoiding Log Helper Functions

Prefer the incremental log event pattern over creating helper functions that return multiple logging closures. Helper functions like `logPollFunc` create unnecessary indirection and allocate closures.

**Instead of:**

```go
// AVOID: Helper function returning closures
func logPollFunc(req tailcfg.MapRequest, node *types.Node) (
    func(string, ...any),  // warnf
    func(string, ...any),  // infof
    func(string, ...any),  // tracef
    func(error, string, ...any),  // errf
) {
    return func(msg string, a ...any) {
        log.Warn().
            Caller().
            Bool("omitPeers", req.OmitPeers).
            Bool("stream", req.Stream).
            Uint64("node.id", node.ID.Uint64()).
            Str("node.name", node.Hostname).
            Msgf(msg, a...)
    },
    // ... more closures
}
```

**Prefer:**

```go
// BETTER: Build log events inline with shared context
func (m *mapSession) logTrace(msg string) {
    log.Trace().
        Caller().
        Bool("omitPeers", m.req.OmitPeers).
        Bool("stream", m.req.Stream).
        Uint64("node.id", m.node.ID.Uint64()).
        Str("node.name", m.node.Hostname).
        Msg(msg)
}

// Or use incremental building for complex cases
logEvent := log.Trace().
    Caller().
    Bool("omitPeers", m.req.OmitPeers).
    Bool("stream", m.req.Stream).
    Uint64("node.id", m.node.ID.Uint64()).
    Str("node.name", m.node.Hostname)

if additionalContext {
    logEvent = logEvent.Str("extra", value)
}

logEvent.Msg("Operation completed")
```

## Important Notes

- **Dependencies**: Use `nix develop` for consistent toolchain (Go, buf, protobuf tools, linting)
- **Protocol Buffers**: Changes to `proto/` require `make generate` and should be committed separately
- **Code Style**: Enforced via golangci-lint with golines (width 88) and gofumpt formatting
- **Linting**: ALL code must pass `golangci-lint run --new-from-rev=upstream/main --timeout=5m --fix` before commit
- **Database**: Supports both SQLite (development) and PostgreSQL (production/testing)
- **Integration Tests**: Require Docker and can consume significant disk space - use headscale-integration-tester agent
- **Performance**: NodeStore optimizations are critical for scale - be careful with changes to state management
- **Quality Assurance**: Always use appropriate specialized agents for testing and validation tasks
- **Tags-as-Identity**: Tags and user ownership are mutually exclusive - always use `IsTagged()` to determine ownership


================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG

## 0.29.0 (202x-xx-xx)

**Minimum supported Tailscale client version: v1.76.0**

### Tailscale ACL compatibility improvements

Extensive test cases were systematically generated using Tailscale clients and the official SaaS
to understand how the packet filter should be generated. We discovered a few differences, but
overall our implementation was very close.
[#3036](https://github.com/juanfont/headscale/pull/3036)

### SSH check action

SSH rules with `"action": "check"` are now supported. When a client initiates a SSH connection to a node
with a `check` action policy, the user is prompted to authenticate via OIDC or CLI approval before access
is granted.

A new `headscale auth` CLI command group supports the approval flow:

- `headscale auth approve --auth-id <id>` approves a pending authentication request (SSH check or web auth)
- `headscale auth reject --auth-id <id>` rejects a pending authentication request
- `headscale auth register --auth-id <id> --user <user>` registers a node (replaces deprecated `headscale nodes register`)

[#1850](https://github.com/juanfont/headscale/pull/1850)

### BREAKING

- **ACL Policy**: Wildcard (`*`) in ACL sources and destinations now resolves to Tailscale's CGNAT range (`100.64.0.0/10`) and ULA range (`fd7a:115c:a1e0::/48`) instead of all IPs (`0.0.0.0/0` and `::/0`) [#3036](https://github.com/juanfont/headscale/pull/3036)
  - This better matches Tailscale's security model where `*` means "any node in the tailnet" rather than "any IP address"
  - Policies relying on wildcard to match non-Tailscale IPs will need to use explicit CIDR ranges instead
  - **Note**: Users with non-standard IP ranges configured in `prefixes.ipv4` or `prefixes.ipv6` (which is unsupported and produces a warning) will need to explicitly specify their CIDR ranges in ACL rules instead of using `*`
- **ACL Policy**: Validate autogroup:self source restrictions matching Tailscale behavior - tags, hosts, and IPs are rejected as sources for autogroup:self destinations [#3036](https://github.com/juanfont/headscale/pull/3036)
  - Policies using tags, hosts, or IP addresses as sources for autogroup:self destinations will now fail validation
- **Upgrade path**: Headscale now enforces a strict version upgrade path [#3083](https://github.com/juanfont/headscale/pull/3083)
  - Skipping minor versions (e.g. 0.27 → 0.29) is blocked; upgrade one minor version at a time
  - Downgrading to a previous minor version is blocked
  - Patch version changes within the same minor are always allowed
- **ACL Policy**: The `proto:icmp` protocol name now only includes ICMPv4 (protocol 1), matching Tailscale behavior [#3036](https://github.com/juanfont/headscale/pull/3036)
  - Previously, `proto:icmp` included both ICMPv4 and ICMPv6
  - Use `proto:ipv6-icmp` or protocol number `58` explicitly for ICMPv6
- **CLI**: `headscale nodes register` is deprecated in favour of `headscale auth register --auth-id <id> --user <user>` [#1850](https://github.com/juanfont/headscale/pull/1850)
  - The old command continues to work but will be removed in a future release

### Changes

- **SSH Policy**: Add support for `localpart:*@<domain>` in SSH rule `users` field, mapping each matching user's email local-part as their OS username [#3091](https://github.com/juanfont/headscale/pull/3091)
- **ACL Policy**: Add ICMP and IPv6-ICMP protocols to default filter rules when no protocol is specified [#3036](https://github.com/juanfont/headscale/pull/3036)
- **ACL Policy**: Fix autogroup:self handling for tagged nodes - tagged nodes no longer incorrectly receive autogroup:self filter rules [#3036](https://github.com/juanfont/headscale/pull/3036)
- **ACL Policy**: Use CIDR format for autogroup:self destination IPs matching Tailscale behavior [#3036](https://github.com/juanfont/headscale/pull/3036)
- **ACL Policy**: Merge filter rules with identical SrcIPs and IPProto matching Tailscale behavior - multiple ACL rules with the same source now produce a single FilterRule with combined DstPorts [#3036](https://github.com/juanfont/headscale/pull/3036)
- Remove deprecated `--namespace` flag from `nodes list`, `nodes register`, and `debug create-node` commands (use `--user` instead) [#3093](https://github.com/juanfont/headscale/pull/3093)
- Remove deprecated `namespace`/`ns` command aliases for `users` and `machine`/`machines` aliases for `nodes` [#3093](https://github.com/juanfont/headscale/pull/3093)
- Add SSH `check` action support with OIDC and CLI-based approval flows [#1850](https://github.com/juanfont/headscale/pull/1850)
- Add `headscale auth register`, `headscale auth approve`, and `headscale auth reject` CLI commands [#1850](https://github.com/juanfont/headscale/pull/1850)
- Add `auth` related routes to the API. The `auth/register` endpoint now expects data as JSON [#1850](https://github.com/juanfont/headscale/pull/1850)
- Deprecate `headscale nodes register --key` in favour of `headscale auth register --auth-id` [#1850](https://github.com/juanfont/headscale/pull/1850)
- Generalise auth templates into reusable `AuthSuccess` and `AuthWeb` components [#1850](https://github.com/juanfont/headscale/pull/1850)
- Unify auth pipeline with `AuthVerdict` type, supporting registration, reauthentication, and SSH checks [#1850](https://github.com/juanfont/headscale/pull/1850)

## 0.28.0 (2026-02-04)

**Minimum supported Tailscale client version: v1.74.0**

### Tags as identity

Tags are now implemented following the Tailscale model where tags and user ownership are mutually exclusive. Devices can be either
user-owned (authenticated via web/OIDC) or tagged (authenticated via tagged PreAuthKeys). Tagged devices receive their identity from
tags rather than users, making them suitable for servers and infrastructure. Applying a tag to a device removes user-based
ownership. See the [Tailscale tags documentation](https://tailscale.com/kb/1068/tags) for details on how tags work.

User-owned nodes can now request tags during registration using `--advertise-tags`. Tags are validated against the `tagOwners` policy
and applied at registration time. Tags can be managed via the CLI or API after registration. Tagged nodes can return to user-owned
by re-authenticating with `tailscale up --advertise-tags= --force-reauth`.

A one-time migration will validate and migrate any `RequestTags` (stored in hostinfo) to the tags column. Tags are validated against
your policy's `tagOwners` rules during migration. [#3011](https://github.com/juanfont/headscale/pull/3011)

### Smarter map updates

The map update system has been rewritten to send smaller, partial updates instead of full network maps whenever possible. This reduces bandwidth usage and improves performance, especially for large networks. The system now properly tracks peer
changes and can send removal notifications when nodes are removed due to policy changes.
[#2856](https://github.com/juanfont/headscale/pull/2856) [#2961](https://github.com/juanfont/headscale/pull/2961)

### Pre-authentication key security improvements

Pre-authentication keys now use bcrypt hashing for improved security [#2853](https://github.com/juanfont/headscale/pull/2853). Keys
are stored as a prefix and bcrypt hash instead of plaintext. The full key is only displayed once at creation time. When listing keys,
only the prefix is shown (e.g., `hskey-auth-{prefix}-***`). All new keys use the format `hskey-auth-{prefix}-{secret}`. Legacy plaintext keys in the format `{secret}` will continue to work for backwards compatibility.

### Web registration templates redesign

The OIDC callback and device registration web pages have been updated to use the Material for MkDocs design system from the official
documentation. The templates now use consistent typography, spacing, and colours across all registration flows.

### Database migration support removed for pre-0.25.0 databases

Headscale no longer supports direct upgrades from databases created before version 0.25.0. Users on older versions must upgrade
sequentially through each stable release, selecting the latest patch version available for each minor release.

### BREAKING

- **API**: The Node message in the gRPC/REST API has been simplified - the `ForcedTags`, `InvalidTags`, and `ValidTags` fields have been removed and replaced with a single `Tags` field that contains the node's applied tags [#2993](https://github.com/juanfont/headscale/pull/2993)
  - API clients should use the `Tags` field instead of `ValidTags`
  - The `headscale nodes list` CLI command now always shows a Tags column and the `--tags` flag has been removed
- **PreAuthKey CLI**: Commands now use ID-based operations instead of user+key combinations [#2992](https://github.com/juanfont/headscale/pull/2992)
  - `headscale preauthkeys create` no longer requires `--user` flag (optional for tracking creation)
  - `headscale preauthkeys list` lists all keys (no longer filtered by user)
  - `headscale preauthkeys expire --id <ID>` replaces `--user <USER> <KEY>`
  - `headscale preauthkeys delete --id <ID>` replaces `--user <USER> <KEY>`

  **Before:**

  ```bash
  headscale preauthkeys create --user 1 --reusable --tags tag:server
  headscale preauthkeys list --user 1
  headscale preauthkeys expire --user 1 <KEY>
  headscale preauthkeys delete --user 1 <KEY>
  ```

  **After:**

  ```bash
  headscale preauthkeys create --reusable --tags tag:server
  headscale preauthkeys list
  headscale preauthkeys expire --id 123
  headscale preauthkeys delete --id 123
  ```

- **Tags**: The gRPC `SetTags` endpoint now allows converting user-owned nodes to tagged nodes by setting tags. [#2885](https://github.com/juanfont/headscale/pull/2885)
- **Tags**: Tags are now resolved from the node's stored Tags field only [#2931](https://github.com/juanfont/headscale/pull/2931)
  - `--advertise-tags` is processed during registration, not on every policy evaluation
  - PreAuthKey tagged devices ignore `--advertise-tags` from clients
  - User-owned nodes can use `--advertise-tags` if authorized by `tagOwners` policy
  - Tags can be managed via CLI (`headscale nodes tag`) or the SetTags API after registration
- Database migration support removed for pre-0.25.0 databases [#2883](https://github.com/juanfont/headscale/pull/2883)
  - If you are running a version older than 0.25.0, you must upgrade to 0.25.1 first, then upgrade to this release
  - See the [upgrade path documentation](https://headscale.net/stable/about/faq/#what-is-the-recommended-update-path-can-i-skip-multiple-versions-while-updating) for detailed guidance
  - In version 0.29, all migrations before 0.28.0 will also be removed
- Remove ability to move nodes between users [#2922](https://github.com/juanfont/headscale/pull/2922)
  - The `headscale nodes move` CLI command has been removed
  - The `MoveNode` API endpoint has been removed
  - Nodes are permanently associated with their user or tag at registration time
- Add `oidc.email_verified_required` config option to control email verification requirement [#2860](https://github.com/juanfont/headscale/pull/2860)
  - When `true` (default), only verified emails can authenticate via OIDC in conjunction with `oidc.allowed_domains` or
    `oidc.allowed_users`. Previous versions allowed to authenticate with an unverified email but did not store the email
    address in the user profile. This is now rejected during authentication with an `unverified email` error.
  - When `false`, unverified emails are allowed for OIDC authentication and the email address is stored in the user
    profile regardless of its verification state.
- **SSH Policy**: Wildcard (`*`) is no longer supported as an SSH destination [#3009](https://github.com/juanfont/headscale/issues/3009)
  - Use `autogroup:member` for user-owned devices
  - Use `autogroup:tagged` for tagged devices
  - Use specific tags (e.g., `tag:server`) for targeted access

  **Before:**

  ```json
  { "action": "accept", "src": ["group:admins"], "dst": ["*"], "users": ["root"] }
  ```

  **After:**

  ```json
  { "action": "accept", "src": ["group:admins"], "dst": ["autogroup:member", "autogroup:tagged"], "users": ["root"] }
  ```

- **SSH Policy**: SSH source/destination validation now enforces Tailscale's security model [#3010](https://github.com/juanfont/headscale/issues/3010)

  Per [Tailscale SSH documentation](https://tailscale.com/kb/1193/tailscale-ssh), the following rules are now enforced:
  1. **Tags cannot SSH to user-owned devices**: SSH rules with `tag:*` or `autogroup:tagged` as source cannot have username destinations (e.g., `alice@`) or `autogroup:member`/`autogroup:self` as destination
  2. **Username destinations require same-user source**: If destination is a specific username (e.g., `alice@`), the source must be that exact same user only. Use `autogroup:self` for same-user SSH access instead

  **Invalid policies now rejected at load time:**

  ```json
  // INVALID: tag source to user destination
  {"src": ["tag:server"], "dst": ["alice@"], ...}

  // INVALID: autogroup:tagged to autogroup:member
  {"src": ["autogroup:tagged"], "dst": ["autogroup:member"], ...}

  // INVALID: group to specific user (use autogroup:self instead)
  {"src": ["group:admins"], "dst": ["alice@"], ...}
  ```

  **Valid patterns:**

  ```json
  // Users/groups can SSH to their own devices via autogroup:self
  {"src": ["group:admins"], "dst": ["autogroup:self"], ...}

  // Users/groups can SSH to tagged devices
  {"src": ["group:admins"], "dst": ["autogroup:tagged"], ...}

  // Tagged devices can SSH to other tagged devices
  {"src": ["autogroup:tagged"], "dst": ["autogroup:tagged"], ...}

  // Same user can SSH to their own devices
  {"src": ["alice@"], "dst": ["alice@"], ...}
  ```

### Changes

- Smarter change notifications send partial map updates and node removals instead of full maps [#2961](https://github.com/juanfont/headscale/pull/2961)
  - Send lightweight endpoint and DERP region updates instead of full maps [#2856](https://github.com/juanfont/headscale/pull/2856)
- Add NixOS module in repository for faster iteration [#2857](https://github.com/juanfont/headscale/pull/2857)
- Add favicon to webpages [#2858](https://github.com/juanfont/headscale/pull/2858)
- Redesign OIDC callback and registration web templates [#2832](https://github.com/juanfont/headscale/pull/2832)
- Reclaim IPs from the IP allocator when nodes are deleted [#2831](https://github.com/juanfont/headscale/pull/2831)
- Add bcrypt hashing for pre-authentication keys [#2853](https://github.com/juanfont/headscale/pull/2853)
- Add prefix to API keys (`hskey-api-{prefix}-{secret}`) [#2853](https://github.com/juanfont/headscale/pull/2853)
- Add prefix to registration keys for web authentication tracking (`hskey-reg-{random}`) [#2853](https://github.com/juanfont/headscale/pull/2853)
- Tags can now be tagOwner of other tags [#2930](https://github.com/juanfont/headscale/pull/2930)
- Add `taildrop.enabled` configuration option to enable/disable Taildrop file sharing [#2955](https://github.com/juanfont/headscale/pull/2955)
- Allow disabling the metrics server by setting empty `metrics_listen_addr` [#2914](https://github.com/juanfont/headscale/pull/2914)
- Log ACME/autocert errors for easier debugging [#2933](https://github.com/juanfont/headscale/pull/2933)
- Improve CLI list output formatting [#2951](https://github.com/juanfont/headscale/pull/2951)
- Use Debian 13 distroless base images for containers [#2944](https://github.com/juanfont/headscale/pull/2944)
- Fix ACL policy not applied to new OIDC nodes until client restart [#2890](https://github.com/juanfont/headscale/pull/2890)
- Fix autogroup:self preventing visibility of nodes matched by other ACL rules [#2882](https://github.com/juanfont/headscale/pull/2882)
- Fix nodes being rejected after pre-authentication key expiration [#2917](https://github.com/juanfont/headscale/pull/2917)
- Fix list-routes command respecting identifier filter with JSON output [#2927](https://github.com/juanfont/headscale/pull/2927)
- Add `--id` flag to expire/delete commands as alternative to `--prefix` for API Keys [#3016](https://github.com/juanfont/headscale/pull/3016)

## 0.27.1 (2025-11-11)

**Minimum supported Tailscale client version: v1.64.0**

### Changes

- Expire nodes with a custom timestamp [#2828](https://github.com/juanfont/headscale/pull/2828)
- Fix issue where node expiry was reset when tailscaled restarts [#2875](https://github.com/juanfont/headscale/pull/2875)
- Fix OIDC authentication when multiple login URLs are opened [#2861](https://github.com/juanfont/headscale/pull/2861)
- Fix node re-registration failing with expired auth keys [#2859](https://github.com/juanfont/headscale/pull/2859)
- Remove old unused database tables and indices [#2844](https://github.com/juanfont/headscale/pull/2844) [#2872](https://github.com/juanfont/headscale/pull/2872)
- Ignore litestream tables during database validation [#2843](https://github.com/juanfont/headscale/pull/2843)
- Fix exit node visibility to respect ACL rules [#2855](https://github.com/juanfont/headscale/pull/2855)
- Fix SSH policy becoming empty when unknown user is referenced [#2874](https://github.com/juanfont/headscale/pull/2874)
- Fix policy validation when using bypass-grpc mode [#2854](https://github.com/juanfont/headscale/pull/2854)
- Fix autogroup:self interaction with other ACL rules [#2842](https://github.com/juanfont/headscale/pull/2842)
- Fix flaky DERP map shuffle test [#2848](https://github.com/juanfont/headscale/pull/2848)
- Use current stable base images for Debian and Alpine containers [#2827](https://github.com/juanfont/headscale/pull/2827)

## 0.27.0 (2025-10-27)

**Minimum supported Tailscale client version: v1.64.0**

### Database integrity improvements

This release includes a significant database migration that addresses
longstanding issues with the database schema and data integrity that has
accumulated over the years. The migration introduces a `schema.sql` file as the
source of truth for the expected database schema to ensure new migrations that
will cause divergence does not occur again.

These issues arose from a combination of factors discovered over time: SQLite
foreign keys not being enforced for many early versions, all migrations being
run in one large function until version 0.23.0, and inconsistent use of GORM's
AutoMigrate feature. Moving forward, all new migrations will be explicit SQL
operations rather than relying on GORM AutoMigrate, and foreign keys will be
enforced throughout the migration process.

We are only improving SQLite databases with this change - PostgreSQL databases
are not affected.

Please read the
[PR description](https://github.com/juanfont/headscale/pull/2617) for more
technical details about the issues and solutions.

**SQLite Database Backup Example:**

```bash
# Stop headscale
systemctl stop headscale

# Backup sqlite database
cp /var/lib/headscale/db.sqlite /var/lib/headscale/db.sqlite.backup

# Backup sqlite WAL/SHM files (if they exist)
cp /var/lib/headscale/db.sqlite-wal /var/lib/headscale/db.sqlite-wal.backup
cp /var/lib/headscale/db.sqlite-shm /var/lib/headscale/db.sqlite-shm.backup

# Start headscale (migration will run automatically)
systemctl start headscale
```

### DERPMap update frequency

The default DERPMap update frequency has been changed from 24 hours to 3 hours.
If you set the `derp.update_frequency` configuration option, it is recommended
to change it to `3h` to ensure that the headscale instance gets the latest
DERPMap updates when upstream is changed.

### Autogroups

This release adds support for the three missing autogroups: `self`
(experimental), `member`, and `tagged`. Please refer to the
[documentation](https://tailscale.com/kb/1018/autogroups/) for a detailed
explanation.

`autogroup:self` is marked as experimental and should be used with caution, but
we need help testing it. Experimental here means two things; first, generating
the packet filter from policies that use `autogroup:self` is very expensive, and
it might perform, or straight up not work on Headscale installations with a
large number of nodes. Second, the implementation might have bugs or edge cases
we are not aware of, meaning that nodes or users might gain _more_ access than
expected. Please report bugs.

### Node store (in memory database)

Under the hood, we have added a new datastructure to store nodes in memory. This
datastructure is called `NodeStore` and aims to reduce the reading and writing
of nodes to the database layer. We have not benchmarked it, but expect it to
improve performance for read heavy workloads. We think of it as, "worst case" we
have moved the bottle neck somewhere else, and "best case" we should see a good
improvement in compute resource usage at the expense of memory usage. We are
quite excited for this change and think it will make it easier for us to improve
the code base over time and make it more correct and efficient.

### BREAKING

- Remove support for 32-bit binaries [#2692](https://github.com/juanfont/headscale/pull/2692)
- Policy: Zero or empty destination port is no longer allowed [#2606](https://github.com/juanfont/headscale/pull/2606)
- Stricter hostname validation [#2383](https://github.com/juanfont/headscale/pull/2383)
  - Hostnames must be valid DNS labels (2-63 characters, alphanumeric and
    hyphens only, cannot start/end with hyphen)
  - **Client Registration (New Nodes)**: Invalid hostnames are automatically
    renamed to `invalid-XXXXXX` format
    - `my-laptop` → accepted as-is
    - `My-Laptop` → `my-laptop` (lowercased)
    - `my_laptop` → `invalid-a1b2c3` (underscore not allowed)
    - `test@host` → `invalid-d4e5f6` (@ not allowed)
    - `laptop-🚀` → `invalid-j1k2l3` (emoji not allowed)
  - **Hostinfo Updates / CLI**: Invalid hostnames are rejected with an error
    - Valid names are accepted or lowercased
    - Names with invalid characters, too short (<2), too long (>63), or
      starting/ending with hyphen are rejected

### Changes

- **Database schema migration improvements for SQLite** [#2617](https://github.com/juanfont/headscale/pull/2617)
  - **IMPORTANT: Backup your SQLite database before upgrading**
  - Introduces safer table renaming migration strategy
  - Addresses longstanding database integrity issues
- Add flag to directly manipulate the policy in the database [#2765](https://github.com/juanfont/headscale/pull/2765)
- DERPmap update frequency default changed from 24h to 3h [#2741](https://github.com/juanfont/headscale/pull/2741)
- DERPmap update mechanism has been improved with retry, and is now failing
  conservatively, preserving the old map upon failure.
  [#2741](https://github.com/juanfont/headscale/pull/2741)
- Add support for `autogroup:member`, `autogroup:tagged` [#2572](https://github.com/juanfont/headscale/pull/2572)
- Fix bug where return routes were being removed by policy [#2767](https://github.com/juanfont/headscale/pull/2767)
- Remove policy v1 code [#2600](https://github.com/juanfont/headscale/pull/2600)
- Refactor Debian/Ubuntu packaging and drop support for Ubuntu 20.04. [#2614](https://github.com/juanfont/headscale/pull/2614)
- Remove redundant check regarding `noise` config [#2658](https://github.com/juanfont/headscale/pull/2658)
- Refactor OpenID Connect documentation [#2625](https://github.com/juanfont/headscale/pull/2625)
- Don't crash if config file is missing [#2656](https://github.com/juanfont/headscale/pull/2656)
- Adds `/robots.txt` endpoint to avoid crawlers [#2643](https://github.com/juanfont/headscale/pull/2643)
- OIDC: Use group claim from UserInfo [#2663](https://github.com/juanfont/headscale/pull/2663)
- OIDC: Update user with claims from UserInfo _before_ comparing with allowed
  groups, email and domain
  [#2663](https://github.com/juanfont/headscale/pull/2663)
- Policy will now reject invalid fields, making it easier to spot spelling
  errors [#2764](https://github.com/juanfont/headscale/pull/2764)
- Add FAQ entry on how to recover from an invalid policy in the database [#2776](https://github.com/juanfont/headscale/pull/2776)
- EXPERIMENTAL: Add support for `autogroup:self` [#2789](https://github.com/juanfont/headscale/pull/2789)
- Add healthcheck command [#2659](https://github.com/juanfont/headscale/pull/2659)

## 0.26.1 (2025-06-06)

### Changes

- Ensure nodes are matching both node key and machine key when connecting. [#2642](https://github.com/juanfont/headscale/pull/2642)

## 0.26.0 (2025-05-14)

### BREAKING

#### Routes

Route internals have been rewritten, removing the dedicated route table in the
database. This was done to simplify the codebase, which had grown unnecessarily
complex after the routes were split into separate tables. The overhead of having
to go via the database and keeping the state in sync made the code very hard to
reason about and prone to errors. The majority of the route state is only
relevant when headscale is running, and is now only kept in memory. As part of
this, the CLI and API has been simplified to reflect the changes;

```console
$ headscale nodes list-routes
ID | Hostname           | Approved | Available       | Serving (Primary)
1  | ts-head-ruqsg8     |          | 0.0.0.0/0, ::/0 |
2  | ts-unstable-fq7ob4 |          | 0.0.0.0/0, ::/0 |

$ headscale nodes approve-routes --identifier 1 --routes 0.0.0.0/0,::/0
Node updated

$ headscale nodes list-routes
ID | Hostname           | Approved        | Available       | Serving (Primary)
1  | ts-head-ruqsg8     | 0.0.0.0/0, ::/0 | 0.0.0.0/0, ::/0 | 0.0.0.0/0, ::/0
2  | ts-unstable-fq7ob4 |                 | 0.0.0.0/0, ::/0 |
```

Note that if an exit route is approved (0.0.0.0/0 or ::/0), both IPv4 and IPv6
will be approved.

- Route API and CLI has been removed [#2422](https://github.com/juanfont/headscale/pull/2422)
- Routes are now managed via the Node API [#2422](https://github.com/juanfont/headscale/pull/2422)
- Only routes accessible to the node will be sent to the node [#2561](https://github.com/juanfont/headscale/pull/2561)

#### Policy v2

This release introduces a new policy implementation. The new policy is a
complete rewrite, and it introduces some significant quality and consistency
improvements. In principle, there are not really any new features, but some long
standing bugs should have been resolved, or be easier to fix in the future. The
new policy code passes all of our tests.

**Changes**

- The policy is validated and "resolved" when loading, providing errors for
  invalid rules and conditions.
  - Previously this was done as a mix between load and runtime (when it was
    applied to a node).
  - This means that when you convert the first time, what was previously a
    policy that loaded, but failed at runtime, will now fail at load time.
- Error messages should be more descriptive and informative.
  - There is still work to be here, but it is already improved with "typing"
    (e.g. only Users can be put in Groups)
- All users in the policy must contain an `@` character.
  - If your user naturally contains and `@`, like an email, this will just work.
  - If its based on usernames, or other identifiers not containing an `@`, an
    `@` should be appended at the end. For example, if your user is `john`, it
    must be written as `john@` in the policy.

<details>

<summary>Migration notes when the policy is stored in the database.</summary>

This section **only** applies if the policy is stored in the database and
Headscale 0.26 doesn't start due to a policy error
(`failed to load ACL policy`).

- Start Headscale 0.26 with the environment variable `HEADSCALE_POLICY_V1=1`
  set. You can check that Headscale picked up the environment variable by
  observing this message during startup: `Using policy manager version: 1`
- Dump the policy to a file: `headscale policy get > policy.json`
- Edit `policy.json` and migrate to policy V2. Use the command
  `headscale policy check --file policy.json` to check for policy errors.
- Load the modified policy: `headscale policy set --file policy.json`
- Restart Headscale **without** the environment variable `HEADSCALE_POLICY_V1`.
  Headscale should now print the message `Using policy manager version: 2` and
  startup successfully.

</details>

**SSH**

The SSH policy has been reworked to be more consistent with the rest of the
policy. In addition, several inconsistencies between our implementation and
Tailscale's upstream has been closed and this might be a breaking change for
some users. Please refer to the
[upstream documentation](https://tailscale.com/kb/1337/acl-syntax#tailscale-ssh)
for more information on which types are allowed in `src`, `dst` and `users`.

There is one large inconsistency left, we allow `*` as a destination as we
currently do not support `autogroup:self`, `autogroup:member` and
`autogroup:tagged`. The support for `*` will be removed when we have support for
the autogroups.

**Current state**

The new policy is passing all tests, both integration and unit tests. This does
not mean it is perfect, but it is a good start. Corner cases that is currently
working in v1 and not tested might be broken in v2 (and vice versa).

**We do need help testing this code**

#### Other breaking changes

- Disallow `server_url` and `base_domain` to be equal [#2544](https://github.com/juanfont/headscale/pull/2544)
- Return full user in API for pre auth keys instead of string [#2542](https://github.com/juanfont/headscale/pull/2542)
- Pre auth key API/CLI now uses ID over username [#2542](https://github.com/juanfont/headscale/pull/2542)
- A non-empty list of global nameservers needs to be specified via
  `dns.nameservers.global` if the configuration option `dns.override_local_dns`
  is enabled or is not specified in the configuration file. This aligns with
  behaviour of tailscale.com.
  [#2438](https://github.com/juanfont/headscale/pull/2438)

### Changes

- Use Go 1.24 [#2427](https://github.com/juanfont/headscale/pull/2427)
- Add `headscale policy check` command to check policy [#2553](https://github.com/juanfont/headscale/pull/2553)
- `oidc.map_legacy_users` and `oidc.strip_email_domain` has been removed [#2411](https://github.com/juanfont/headscale/pull/2411)
- Add more information to `/debug` endpoint [#2420](https://github.com/juanfont/headscale/pull/2420)
  - It is now possible to inspect running goroutines and take profiles
  - View of config, policy, filter, ssh policy per node, connected nodes and
    DERPmap
- OIDC: Fetch UserInfo to get EmailVerified if necessary [#2493](https://github.com/juanfont/headscale/pull/2493)
  - If a OIDC provider doesn't include the `email_verified` claim in its ID
    tokens, Headscale will attempt to get it from the UserInfo endpoint.
- OIDC: Try to populate name, email and username from UserInfo [#2545](https://github.com/juanfont/headscale/pull/2545)
- Improve performance by only querying relevant nodes from the database for node
  updates [#2509](https://github.com/juanfont/headscale/pull/2509)
- node FQDNs in the netmap will now contain a dot (".") at the end. This aligns
  with behaviour of tailscale.com
  [#2503](https://github.com/juanfont/headscale/pull/2503)
- Restore support for "Override local DNS" [#2438](https://github.com/juanfont/headscale/pull/2438)
- Add documentation for routes [#2496](https://github.com/juanfont/headscale/pull/2496)

## 0.25.1 (2025-02-25)

### Changes

- Fix issue where registration errors are sent correctly [#2435](https://github.com/juanfont/headscale/pull/2435)
- Fix issue where routes passed on registration were not saved [#2444](https://github.com/juanfont/headscale/pull/2444)
- Fix issue where registration page was displayed twice [#2445](https://github.com/juanfont/headscale/pull/2445)

## 0.25.0 (2025-02-11)

### BREAKING

- Authentication flow has been rewritten [#2374](https://github.com/juanfont/headscale/pull/2374) This change should be
  transparent to users with the exception of some buxfixes that has been
  discovered and was fixed as part of the rewrite.
  - When a node is registered with _a new user_, it will be registered as a new
    node ([#2327](https://github.com/juanfont/headscale/issues/2327) and
    [#1310](https://github.com/juanfont/headscale/issues/1310)).
  - A logged out node logging in with the same user will replace the existing
    node.
- Remove support for Tailscale clients older than 1.62 (Capability version 87) [#2405](https://github.com/juanfont/headscale/pull/2405)

### Changes

- `oidc.map_legacy_users` is now `false` by default [#2350](https://github.com/juanfont/headscale/pull/2350)
- Print Tailscale version instead of capability versions for outdated nodes [#2391](https://github.com/juanfont/headscale/pull/2391)
- Do not allow renaming of users from OIDC [#2393](https://github.com/juanfont/headscale/pull/2393)
- Change minimum hostname length to 2 [#2393](https://github.com/juanfont/headscale/pull/2393)
- Fix migration error caused by nodes having invalid auth keys [#2412](https://github.com/juanfont/headscale/pull/2412)
- Pre auth keys belonging to a user are no longer deleted with the user [#2396](https://github.com/juanfont/headscale/pull/2396)
- Pre auth keys that are used by a node can no longer be deleted [#2396](https://github.com/juanfont/headscale/pull/2396)
- Rehaul HTTP errors, return better status code and errors to users [#2398](https://github.com/juanfont/headscale/pull/2398)
- Print headscale version and commit on server startup [#2415](https://github.com/juanfont/headscale/pull/2415)

## 0.24.3 (2025-02-07)

### Changes

- Fix migration error caused by nodes having invalid auth keys [#2412](https://github.com/juanfont/headscale/pull/2412)
- Pre auth keys belonging to a user are no longer deleted with the user [#2396](https://github.com/juanfont/headscale/pull/2396)
- Pre auth keys that are used by a node can no longer be deleted [#2396](https://github.com/juanfont/headscale/pull/2396)

## 0.24.2 (2025-01-30)

### Changes

- Fix issue where email and username being equal fails to match in Policy [#2388](https://github.com/juanfont/headscale/pull/2388)
- Delete invalid routes before adding a NOT NULL constraint on node_id [#2386](https://github.com/juanfont/headscale/pull/2386)

## 0.24.1 (2025-01-23)

### Changes

- Fix migration issue with user table for PostgreSQL [#2367](https://github.com/juanfont/headscale/pull/2367)
- Relax username validation to allow emails [#2364](https://github.com/juanfont/headscale/pull/2364)
- Remove invalid routes and add stronger constraints for routes to avoid API
  panic [#2371](https://github.com/juanfont/headscale/pull/2371)
- Fix panic when `derp.update_frequency` is 0 [#2368](https://github.com/juanfont/headscale/pull/2368)

## 0.24.0 (2025-01-17)

### Security fix: OIDC changes in Headscale 0.24.0

The following issue _only_ affects Headscale installations which authenticate
with OIDC.

_Headscale v0.23.0 and earlier_ identified OIDC users by the "username" part of
their email address (when `strip_email_domain: true`, the default) or whole
email address (when `strip_email_domain: false`).

Depending on how Headscale and your Identity Provider (IdP) were configured,
only using the `email` claim could allow a malicious user with an IdP account to
take over another Headscale user's account, even when
`strip_email_domain: false`.

This would also cause a user to lose access to their Headscale account if they
changed their email address.

_Headscale v0.24.0_ now identifies OIDC users by the `iss` and `sub` claims.
[These are guaranteed by the OIDC specification to be stable and unique](https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability),
even if a user changes email address. A well-designed IdP will typically set
`sub` to an opaque identifier like a UUID or numeric ID, which has no relation
to the user's name or email address.

Headscale v0.24.0 and later will also automatically update profile fields with
OIDC data on login. This means that users can change those details in your IdP,
and have it populate to Headscale automatically the next time they log in.
However, this may affect the way you reference users in policies.

Headscale v0.23.0 and earlier never recorded the `iss` and `sub` fields, so all
legacy (existing) OIDC accounts _need to be migrated_ to be properly secured.

#### What do I need to do to migrate?

Headscale v0.24.0 has an automatic migration feature, which is enabled by
default (`map_legacy_users: true`). **This will be disabled by default in a
future version of Headscale – any unmigrated users will get new accounts.**

The migration will mostly be done automatically, with one exception. If your
OIDC does not provide an `email_verified` claim, Headscale will ignore the
`email`. This means that either the administrator will have to mark the user
emails as verified, or ensure the users verify their emails. Any unverified
emails will be ignored, meaning that the users will get new accounts instead of
being migrated.

After this exception is ensured, make all users log into Headscale with their
account, and Headscale will automatically update the account record. This will
be transparent to the users.

When all users have logged in, you can disable the automatic migration by
setting `map_legacy_users: false` in your configuration file.

Please note that `map_legacy_users` will be set to `false` by default in v0.25.0
and the migration mechanism will be removed in v0.26.0.

<details>

<summary>What does automatic migration do?</summary>

##### What does automatic migration do?

When automatic migration is enabled (`map_legacy_users: true`), Headscale will
first match an OIDC account to a Headscale account by `iss` and `sub`, and then
fall back to matching OIDC users similarly to how Headscale v0.23.0 did:

- If `strip_email_domain: true` (the default): the Headscale username matches
  the "username" part of their email address.
- If `strip_email_domain: false`: the Headscale username matches the _whole_
  email address.

On migration, Headscale will change the account's username to their
`preferred_username`. **This could break any ACLs or policies which are
configured to match by username.**

Like with Headscale v0.23.0 and earlier, this migration only works for users who
haven't changed their email address since their last Headscale login.

A _successful_ automated migration should otherwise be transparent to users.

Once a Headscale account has been migrated, it will be _unavailable_ to be
matched by the legacy process. An OIDC login with a matching username, but
_non-matching_ `iss` and `sub` will instead get a _new_ Headscale account.

Because of the way OIDC works, Headscale's automated migration process can
_only_ work when a user tries to log in after the update.

Legacy account migration should have no effect on new installations where all
users have a recorded `sub` and `iss`.

</details>

<details>

<summary>What happens when automatic migration is disabled?</summary>

##### What happens when automatic migration is disabled?

When automatic migration is disabled (`map_legacy_users: false`), Headscale will
only try to match an OIDC account to a Headscale account by `iss` and `sub`.

If there is no match, it will get a _new_ Headscale account – even if there was
a legacy account which _could_ have matched and migrated.

We recommend new Headscale users explicitly disable automatic migration – but it
should otherwise have no effect if every account has a recorded `iss` and `sub`.

When automatic migration is disabled, the `strip_email_domain` setting will have
no effect.

</details>

Special thanks to @micolous for reviewing, proposing and working with us on
these changes.

#### Other OIDC changes

Headscale now uses
[the standard OIDC claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims)
to populate and update user information every time they log in:

| Headscale profile field | OIDC claim           | Notes / examples                                                                                          |
| ----------------------- | -------------------- | --------------------------------------------------------------------------------------------------------- |
| email address           | `email`              | Only used when `"email_verified": true`                                                                   |
| display name            | `name`               | eg: `Sam Smith`                                                                                           |
| username                | `preferred_username` | Varies depending on IdP and configuration, eg: `ssmith`, `ssmith@idp.example.com`, `\\example.com\ssmith` |
| profile picture         | `picture`            | URL to a profile picture or avatar                                                                        |

These should show up nicely in the Tailscale client.

This will also affect the way you
[reference users in policies](https://github.com/juanfont/headscale/pull/2205).

### BREAKING

- Remove `dns.use_username_in_magic_dns` configuration option [#2020](https://github.com/juanfont/headscale/pull/2020),
  [#2279](https://github.com/juanfont/headscale/pull/2279)
  - Having usernames in magic DNS is no longer possible.
- Remove versions older than 1.56 [#2149](https://github.com/juanfont/headscale/pull/2149)
  - Clean up old code required by old versions
- User gRPC/API [#2261](https://github.com/juanfont/headscale/pull/2261):
  - If you depend on a Headscale Web UI, you should wait with this update until
    the UI have been updated to match the new API.
  - `GET /api/v1/user/{name}` and `GetUser` have been removed in favour of
    `ListUsers` with an ID parameter
  - `RenameUser` and `DeleteUser` now require an ID instead of a name.

### Changes

- Improved compatibility of built-in DERP server with clients connecting over
  WebSocket [#2132](https://github.com/juanfont/headscale/pull/2132)
- Allow nodes to use SSH agent forwarding [#2145](https://github.com/juanfont/headscale/pull/2145)
- Fixed processing of fields in post request in MoveNode rpc [#2179](https://github.com/juanfont/headscale/pull/2179)
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules
  applied [#2198](https://github.com/juanfont/headscale/pull/2198)
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)
- Fixed missing `stable-debug` container tag [#2232](https://github.com/juanfont/headscale/pull/2232)
- Loosened up `server_url` and `base_domain` check. It was overly strict in some
  cases. [#2248](https://github.com/juanfont/headscale/pull/2248)
- CLI for managing users now accepts `--identifier` in addition to `--name`,
  usage of `--identifier` is recommended
  [#2261](https://github.com/juanfont/headscale/pull/2261)
- Add `dns.extra_records_path` configuration option [#2262](https://github.com/juanfont/headscale/issues/2262)
- Support client verify for DERP [#2046](https://github.com/juanfont/headscale/pull/2046)
- Add PKCE Verifier for OIDC [#2314](https://github.com/juanfont/headscale/pull/2314)

## 0.23.0 (2024-09-18)

This release was intended to be mainly a code reorganisation and refactoring,
significantly improving the maintainability of the codebase. This should allow
us to improve further and make it easier for the maintainers to keep on top of
the project. However, as you all have noticed, it turned out to become a much
larger, much longer release cycle than anticipated. It has ended up to be a
release with a lot of rewrites and changes to the code base and functionality of
Headscale, cleaning up a lot of technical debt and introducing a lot of
improvements. This does come with some breaking changes,

**Please remember to always back up your database between versions**

#### Here is a short summary of the broad topics of changes:

Code has been organised into modules, reducing use of global variables/objects,
isolating concerns and “putting the right things in the logical place”.

The new
[policy](https://github.com/juanfont/headscale/tree/main/hscontrol/policy) and
[mapper](https://github.com/juanfont/headscale/tree/main/hscontrol/mapper)
package, containing the ACL/Policy logic and the logic for creating the data
served to clients (the network “map”) has been rewritten and improved. This
change has allowed us to finish SSH support and add additional tests throughout
the code to ensure correctness.

The
[“poller”, or streaming logic](https://github.com/juanfont/headscale/blob/main/hscontrol/poll.go)
has been rewritten and instead of keeping track of the latest updates, checking
at a fixed interval, it now uses go channels, implemented in our new
[notifier](https://github.com/juanfont/headscale/tree/main/hscontrol/notifier)
package and it allows us to send updates to connected clients immediately. This
should both improve performance and potential latency before a client picks up
an update.

Headscale now supports sending “delta” updates, thanks to the new mapper and
poller logic, allowing us to only inform nodes about new nodes, changed nodes
and removed nodes. Previously we sent the entire state of the network every time
an update was due.

While we have a pretty good
[test harness](https://github.com/search?q=repo%3Ajuanfont%2Fheadscale+path%3A_test.go&type=code)
for validating our changes, the changes came down to
[284 changed files with 32,316 additions and 24,245 deletions](https://github.com/juanfont/headscale/compare/b01f1f1867136d9b2d7b1392776eb363b482c525...ed78ecd)
and bugs are expected. We need help testing this release. In addition, while we
think the performance should in general be better, there might be regressions in
parts of the platform, particularly where we prioritised correctness over speed.

There are also several bugfixes that has been encountered and fixed as part of
implementing these changes, particularly after improving the test harness as
part of adopting [#1460](https://github.com/juanfont/headscale/pull/1460).

### BREAKING

- Code reorganisation, a lot of code has moved, please review the following PRs
  accordingly [#1473](https://github.com/juanfont/headscale/pull/1473)
- Change the structure of database configuration, see
  [config-example.yaml](./config-example.yaml) for the new structure.
  [#1700](https://github.com/juanfont/headscale/pull/1700)
  - Old structure has been remove and the configuration _must_ be converted.
  - Adds additional configuration for PostgreSQL for setting max open, idle
    connection and idle connection lifetime.
- API: Machine is now Node [#1553](https://github.com/juanfont/headscale/pull/1553)
- Remove support for older Tailscale clients [#1611](https://github.com/juanfont/headscale/pull/1611)
  - The oldest supported client is 1.42
- Headscale checks that _at least_ one DERP is defined at start [#1564](https://github.com/juanfont/headscale/pull/1564)
  - If no DERP is configured, the server will fail to start, this can be because
    it cannot load the DERPMap from file or url.
- Embedded DERP server requires a private key [#1611](https://github.com/juanfont/headscale/pull/1611)
  - Add a filepath entry to
    [`derp.server.private_key_path`](https://github.com/juanfont/headscale/blob/b35993981297e18393706b2c963d6db882bba6aa/config-example.yaml#L95)
- Docker images are now built with goreleaser (ko) [#1716](https://github.com/juanfont/headscale/pull/1716)
  [#1763](https://github.com/juanfont/headscale/pull/1763)
  - Entrypoint of container image has changed from shell to headscale, require
    change from `headscale serve` to `serve`
  - `/var/lib/headscale` and `/var/run/headscale` is no longer created
    automatically, see [container docs](./docs/setup/install/container.md)
- Prefixes are now defined per v4 and v6 range. [#1756](https://github.com/juanfont/headscale/pull/1756)
  - `ip_prefixes` option is now `prefixes.v4` and `prefixes.v6`
  - `prefixes.allocation` can be set to assign IPs at `sequential` or `random`.
    [#1869](https://github.com/juanfont/headscale/pull/1869)
- MagicDNS domains no longer contain usernames []()
  - This is in preparation to fix Headscales implementation of tags which
    currently does not correctly remove the link between a tagged device and a
    user. As tagged devices will not have a user, this will require a change to
    the DNS generation, removing the username, see
    [#1369](https://github.com/juanfont/headscale/issues/1369) for more
    information.
  - `use_username_in_magic_dns` can be used to turn this behaviour on again, but
    note that this option _will be removed_ when tags are fixed.
    - dns.base_domain can no longer be the same as (or part of) server_url.
    - This option brings Headscales behaviour in line with Tailscale.
- YAML files are no longer supported for headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792)
  - HuJSON is now the only supported format for policy.
- DNS configuration has been restructured [#2034](https://github.com/juanfont/headscale/pull/2034)
  - Please review the new [config-example.yaml](./config-example.yaml) for the
    new structure.

### Changes

- Use versioned migrations [#1644](https://github.com/juanfont/headscale/pull/1644)
- Make the OIDC callback page better [#1484](https://github.com/juanfont/headscale/pull/1484)
- SSH support [#1487](https://github.com/juanfont/headscale/pull/1487)
- State management has been improved [#1492](https://github.com/juanfont/headscale/pull/1492)
- Use error group handling to ensure tests actually pass [#1535](https://github.com/juanfont/headscale/pull/1535) based on
  [#1460](https://github.com/juanfont/headscale/pull/1460)
- Fix hang on SIGTERM [#1492](https://github.com/juanfont/headscale/pull/1492)
  taken from [#1480](https://github.com/juanfont/headscale/pull/1480)
- Send logs to stderr by default [#1524](https://github.com/juanfont/headscale/pull/1524)
- Fix [TS-2023-006](https://tailscale.com/security-bulletins/#ts-2023-006)
  security UPnP issue [#1563](https://github.com/juanfont/headscale/pull/1563)
- Turn off gRPC logging [#1640](https://github.com/juanfont/headscale/pull/1640)
  fixes [#1259](https://github.com/juanfont/headscale/issues/1259)
- Added the possibility to manually create a DERP-map entry which can be
  customized, instead of automatically creating it.
  [#1565](https://github.com/juanfont/headscale/pull/1565)
- Add support for deleting api keys [#1702](https://github.com/juanfont/headscale/pull/1702)
- Add command to backfill IP addresses for nodes missing IPs from configured
  prefixes. [#1869](https://github.com/juanfont/headscale/pull/1869)
- Log available update as warning [#1877](https://github.com/juanfont/headscale/pull/1877)
- Add `autogroup:internet` to Policy [#1917](https://github.com/juanfont/headscale/pull/1917)
- Restore foreign keys and add constraints [#1562](https://github.com/juanfont/headscale/pull/1562)
- Make registration page easier to use on mobile devices
- Make write-ahead-log default on and configurable for SQLite [#1985](https://github.com/juanfont/headscale/pull/1985)
- Add APIs for managing headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792)
- Fix for registering nodes using preauthkeys when running on a postgres
  database in a non-UTC timezone.
  [#764](https://github.com/juanfont/headscale/issues/764)
- Make sure integration tests cover postgres for all scenarios
- CLI commands (all except `serve`) only requires minimal configuration, no more
  errors or warnings from unset settings
  [#2109](https://github.com/juanfont/headscale/pull/2109)
- CLI results are now concistently sent to stdout and errors to stderr [#2109](https://github.com/juanfont/headscale/pull/2109)
- Fix issue where shutting down headscale would hang [#2113](https://github.com/juanfont/headscale/pull/2113)

## 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 continuously 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 each other [#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)

## 0.22.0 (2023-04-20)

### Changes

- Add `.deb` packages to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
- Update and simplify the documentation to use new `.deb` packages [#1349](https://github.com/juanfont/headscale/pull/1349)
- Add 32-bit Arm platforms to release process [#1297](https://github.com/juanfont/headscale/pull/1297)
- Fix longstanding bug that would prevent "\*" from working properly in ACLs
  (issue [#699](https://github.com/juanfont/headscale/issues/699))
  [#1279](https://github.com/juanfont/headscale/pull/1279)
- Fix issue where IPv6 could not be used in, or while using ACLs (part of [#809](https://github.com/juanfont/headscale/issues/809))
  [#1339](https://github.com/juanfont/headscale/pull/1339)
- Target Go 1.20 and Tailscale 1.38 for Headscale [#1323](https://github.com/juanfont/headscale/pull/1323)

## 0.21.0 (2023-03-20)

### Changes

- Adding "configtest" CLI command. [#1230](https://github.com/juanfont/headscale/pull/1230)
- Add documentation on connecting with iOS to `/apple` [#1261](https://github.com/juanfont/headscale/pull/1261)
- Update iOS compatibility and added documentation for iOS [#1264](https://github.com/juanfont/headscale/pull/1264)
- Allow to delete routes [#1244](https://github.com/juanfont/headscale/pull/1244)

## 0.20.0 (2023-02-03)

### Changes

- Fix wrong behaviour in exit nodes [#1159](https://github.com/juanfont/headscale/pull/1159)
- Align behaviour of `dns_config.restricted_nameservers` to tailscale [#1162](https://github.com/juanfont/headscale/pull/1162)
- Make OpenID Connect authenticated client expiry time configurable [#1191](https://github.com/juanfont/headscale/pull/1191)
  - defaults to 180 days like Tailscale SaaS
  - adds option to use the expiry time from the OpenID token for the node (see
    config-example.yaml)
- Set ControlTime in Map info sent to nodes [#1195](https://github.com/juanfont/headscale/pull/1195)
- Populate Tags field on Node updates sent [#1195](https://github.com/juanfont/headscale/pull/1195)

## 0.19.0 (2023-01-29)

### BREAKING

- Rename Namespace to User [#1144](https://github.com/juanfont/headscale/pull/1144)
  - **BACKUP your database before upgrading**
- Command line flags previously taking `--namespace` or `-n` will now require
  `--user` or `-u`

## 0.18.0 (2023-01-14)

### Changes

- Reworked routing and added support for subnet router failover [#1024](https://github.com/juanfont/headscale/pull/1024)
- Added an OIDC AllowGroups Configuration options and authorization check [#1041](https://github.com/juanfont/headscale/pull/1041)
- Set `db_ssl` to false by default [#1052](https://github.com/juanfont/headscale/pull/1052)
- Fix duplicate nodes due to incorrect implementation of the protocol [#1058](https://github.com/juanfont/headscale/pull/1058)
- Report if a machine is online in CLI more accurately [#1062](https://github.com/juanfont/headscale/pull/1062)
- Added config option for custom DNS records [#1035](https://github.com/juanfont/headscale/pull/1035)
- Expire nodes based on OIDC token expiry [#1067](https://github.com/juanfont/headscale/pull/1067)
- Remove ephemeral nodes on logout [#1098](https://github.com/juanfont/headscale/pull/1098)
- Performance improvements in ACLs [#1129](https://github.com/juanfont/headscale/pull/1129)
- OIDC client secret can be passed via a file [#1127](https://github.com/juanfont/headscale/pull/1127)

## 0.17.1 (2022-12-05)

### Changes

- Correct typo on macOS standalone profile link [#1028](https://github.com/juanfont/headscale/pull/1028)
- Update platform docs with Fast User Switching [#1016](https://github.com/juanfont/headscale/pull/1016)

## 0.17.0 (2022-11-26)

### BREAKING

- `noise.private_key_path` has been added and is required for the new noise
  protocol.
- Log level option `log_level` was moved to a distinct `log` config section and
  renamed to `level` [#768](https://github.com/juanfont/headscale/pull/768)
- Removed Alpine Linux container image [#962](https://github.com/juanfont/headscale/pull/962)

### Important Changes

- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738)
- Add experimental support for
  [SSH ACL](https://tailscale.com/kb/1018/acls/#tailscale-ssh) (see docs for
  limitations) [#847](https://github.com/juanfont/headscale/pull/847)
  - Please note that this support should be considered _partially_ implemented
  - SSH ACLs status:
    - Support `accept` and `check` (SSH can be enabled and used for connecting
      and authentication)
    - Rejecting connections **are not supported**, meaning that if you enable
      SSH, then assume that _all_ `ssh` connections **will be allowed**.
    - If you decided to try this feature, please carefully managed permissions
      by blocking port `22` with regular ACLs or do _not_ set `--ssh` on your
      clients.
    - We are currently improving our testing of the SSH ACLs, help us get an
      overview by testing and giving feedback.
  - This feature should be considered dangerous and it is disabled by default.
    Enable by setting `HEADSCALE_EXPERIMENTAL_FEATURE_SSH=1`.

### Changes

- Add ability to specify config location via env var `HEADSCALE_CONFIG` [#674](https://github.com/juanfont/headscale/issues/674)
- Target Go 1.19 for Headscale [#778](https://github.com/juanfont/headscale/pull/778)
- Target Tailscale v1.30.0 to build Headscale [#780](https://github.com/juanfont/headscale/pull/780)
- Give a warning when running Headscale with reverse proxy improperly configured
  for WebSockets [#788](https://github.com/juanfont/headscale/pull/788)
- Fix subnet routers with Primary Routes [#811](https://github.com/juanfont/headscale/pull/811)
- Added support for JSON logs [#653](https://github.com/juanfont/headscale/issues/653)
- Sanitise the node key passed to registration url [#823](https://github.com/juanfont/headscale/pull/823)
- Add support for generating pre-auth keys with tags [#767](https://github.com/juanfont/headscale/pull/767)
- Add support for evaluating `autoApprovers` ACL entries when a machine is
  registered [#763](https://github.com/juanfont/headscale/pull/763)
- Add config flag to allow Headscale to start if OIDC provider is down [#829](https://github.com/juanfont/headscale/pull/829)
- Fix prefix length comparison bug in AutoApprovers route evaluation [#862](https://github.com/juanfont/headscale/pull/862)
- Random node DNS suffix only applied if names collide in namespace. [#766](https://github.com/juanfont/headscale/issues/766)
- Remove `ip_prefix` configuration option and warning [#899](https://github.com/juanfont/headscale/pull/899)
- Add `dns_config.override_local_dns` option [#905](https://github.com/juanfont/headscale/pull/905)
- Fix some DNS config issues [#660](https://github.com/juanfont/headscale/issues/660)
- Make it possible to disable TS2019 with build flag [#928](https://github.com/juanfont/headscale/pull/928)
- Fix OIDC registration issues [#960](https://github.com/juanfont/headscale/pull/960) and
  [#971](https://github.com/juanfont/headscale/pull/971)
- Add support for specifying NextDNS DNS-over-HTTPS resolver [#940](https://github.com/juanfont/headscale/pull/940)
- Make more sslmode available for postgresql connection [#927](https://github.com/juanfont/headscale/pull/927)

## 0.16.4 (2022-08-21)

### Changes

- Add ability to connect to PostgreSQL over TLS/SSL [#745](https://github.com/juanfont/headscale/pull/745)
- Fix CLI registration of expired machines [#754](https://github.com/juanfont/headscale/pull/754)

## 0.16.3 (2022-08-17)

### Changes

- Fix issue with OIDC authentication [#747](https://github.com/juanfont/headscale/pull/747)

## 0.16.2 (2022-08-14)

### Changes

- Fixed bugs in the client registration process after migration to NodeKey [#735](https://github.com/juanfont/headscale/pull/735)

## 0.16.1 (2022-08-12)

### Changes

- Updated dependencies (including the library that lacked armhf support) [#722](https://github.com/juanfont/headscale/pull/722)
- Fix missing group expansion in function `excludeCorrectlyTaggedNodes` [#563](https://github.com/juanfont/headscale/issues/563)
- Improve registration protocol implementation and switch to NodeKey as main
  identifier [#725](https://github.com/juanfont/headscale/pull/725)
- Add ability to connect to PostgreSQL via unix socket [#734](https://github.com/juanfont/headscale/pull/734)

## 0.16.0 (2022-07-25)

**Note:** Take a backup of your database before upgrading.

### BREAKING

- Old ACL syntax is no longer supported ("users" & "ports" -> "src" & "dst").
  Please check [the new syntax](https://tailscale.com/kb/1018/acls/).

### Changes

- **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609)
- Headscale fails to serve if the ACL policy file cannot be parsed [#537](https://github.com/juanfont/headscale/pull/537)
- Fix labels cardinality error when registering unknown pre-auth key [#519](https://github.com/juanfont/headscale/pull/519)
- Fix send on closed channel crash in polling [#542](https://github.com/juanfont/headscale/pull/542)
- Fixed spurious calls to setLastStateChangeToNow from ephemeral nodes [#566](https://github.com/juanfont/headscale/pull/566)
- Add command for moving nodes between namespaces [#362](https://github.com/juanfont/headscale/issues/362)
- Added more configuration parameters for OpenID Connect (scopes, free-form
  parameters, domain and user allowlist)
- Add command to set tags on a node [#525](https://github.com/juanfont/headscale/issues/525)
- Add command to view tags of nodes [#356](https://github.com/juanfont/headscale/issues/356)
- Add --all (-a) flag to enable routes command [#360](https://github.com/juanfont/headscale/issues/360)
- Fix issue where nodes was not updated across namespaces [#560](https://github.com/juanfont/headscale/pull/560)
- Add the ability to rename a nodes name [#560](https://github.com/juanfont/headscale/pull/560)
  - Node DNS names are now unique, a random suffix will be added when a node
    joins
  - This change contains database changes, remember to **backup** your database
    before upgrading
- Add option to enable/disable logtail (Tailscale's logging infrastructure) [#596](https://github.com/juanfont/headscale/pull/596)
  - This change disables the logs by default
- Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and
  years (`y`) [#598](https://github.com/juanfont/headscale/pull/598)
- Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601)
- Use new ACL syntax [#618](https://github.com/juanfont/headscale/pull/618)
- Add -c option to specify config file from command line [#285](https://github.com/juanfont/headscale/issues/285)
  [#612](https://github.com/juanfont/headscale/pull/601)
- Add configuration option to allow Tailscale clients to use a random WireGuard
  port. [kb/1181/firewalls](https://tailscale.com/kb/1181/firewalls)
  [#624](https://github.com/juanfont/headscale/pull/624)
- Improve obtuse UX regarding missing configuration
  (`ephemeral_node_inactivity_timeout` not set)
  [#639](https://github.com/juanfont/headscale/pull/639)
- Fix nodes being shown as 'offline' in `tailscale status` [#648](https://github.com/juanfont/headscale/pull/648)
- Improve shutdown behaviour [#651](https://github.com/juanfont/headscale/pull/651)
- Drop Gin as web framework in Headscale
  [648](https://github.com/juanfont/headscale/pull/648)
  [677](https://github.com/juanfont/headscale/pull/677)
- Make tailnet node updates check interval configurable [#675](https://github.com/juanfont/headscale/pull/675)
- Fix regression with HTTP API [#684](https://github.com/juanfont/headscale/pull/684)
- nodes ls now print both Hostname and Name(Issue [#647](https://github.com/juanfont/headscale/issues/647) PR
  [#687](https://github.com/juanfont/headscale/pull/687))

## 0.15.0 (2022-03-20)

**Note:** Take a backup of your database before upgrading.

### BREAKING

- Boundaries between Namespaces has been removed and all nodes can communicate
  by default [#357](https://github.com/juanfont/headscale/pull/357)
  - To limit access between nodes, use [ACLs](./docs/ref/acls.md).
- `/metrics` is now a configurable host:port endpoint: [#344](https://github.com/juanfont/headscale/pull/344). You must update your
  `config.yaml` file to include:
  ```yaml
  metrics_listen_addr: 127.0.0.1:9090
  ```

### Features

- Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359)
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
- Add shorthand aliases for commands and subcommands [#376](https://github.com/juanfont/headscale/pull/376)
- Add `/windows` endpoint for Windows configuration instructions + registry file
  download [#392](https://github.com/juanfont/headscale/pull/392)
- Added embedded DERP (and STUN) server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)

### Changes

- Fix a bug were the same IP could be assigned to multiple hosts if joined in
  quick succession [#346](https://github.com/juanfont/headscale/pull/346)
- Simplify the code behind registration of machines [#366](https://github.com/juanfont/headscale/pull/366)
  - Nodes are now only written to database if they are registered successfully
- Fix a limitation in the ACLs that prevented users to write rules with `*` as
  source [#374](https://github.com/juanfont/headscale/issues/374)
- Reduce the overhead of marshal/unmarshal for Hostinfo, routes and endpoints by
  using specific types in Machine
  [#371](https://github.com/juanfont/headscale/pull/371)
- Apply normalization function to FQDN on hostnames when hosts registers and
  retrieve information [#363](https://github.com/juanfont/headscale/issues/363)
- Fix a bug that prevented the use of `tailscale logout` with OIDC [#508](https://github.com/juanfont/headscale/issues/508)
- Added Tailscale repo HEAD and unstable releases channel to the integration
  tests targets [#513](https://github.com/juanfont/headscale/pull/513)

## 0.14.0 (2022-02-24)

**UPCOMING ### BREAKING From the **next\*\* version (`0.15.0`), all machines
will be able to communicate regardless of if they are in the same namespace.
This means that the behaviour currently limited to ACLs will become default.
From version `0.15.0`, all limitation of communications must be done with ACLs.

This is a part of aligning `headscale`'s behaviour with Tailscale's upstream
behaviour.

### BREAKING

- ACLs have been rewritten to align with the bevaviour Tailscale Control Panel
  provides. **NOTE:** This is only active if you use ACLs
  - Namespaces are now treated as Users
  - All machines can communicate with all machines by default
  - Tags should now work correctly and adding a host to Headscale should now
    reload the rules.
  - The documentation have a [fictional example](./docs/ref/acls.md) that should
    cover some use cases of the ACLs features

### Features

- Add support for configurable mTLS [docs](./docs/ref/tls.md) [#297](https://github.com/juanfont/headscale/pull/297)

### Changes

- Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346)

**0.13.0 (2022-02-18):**

### Features

- Add IPv6 support to the prefix assigned to namespaces
- Add API Key support
  - Enable remote control of `headscale` via CLI
    [docs](./docs/ref/api.md#grpc)
  - Enable HTTP API (beta, subject to change)
- OpenID Connect users will be mapped per namespaces
  - Each user will get its own namespace, created if it does not exist
  - `oidc.domain_map` option has been removed
  - `strip_email_domain` option has been added (see
    [config-example.yaml](./config-example.yaml))

### Changes

- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208)
- Upgrade `tailscale` (1.20.4) and other dependencies to latest [#314](https://github.com/juanfont/headscale/pull/314)
- fix swapped machine<->namespace labels in `/metrics` [#312](https://github.com/juanfont/headscale/pull/312)
- remove key-value based update mechanism for namespace changes [#316](https://github.com/juanfont/headscale/pull/316)

**0.12.4 (2022-01-29):**

### Changes

- Make gRPC Unix Socket permissions configurable [#292](https://github.com/juanfont/headscale/pull/292)
- Trim whitespace before reading Private Key from file [#289](https://github.com/juanfont/headscale/pull/289)
- Add new command to generate a private key for `headscale` [#290](https://github.com/juanfont/headscale/pull/290)
- Fixed issue where hosts deleted from control server may be written back to the
  database, as long as they are connected to the control server
  [#278](https://github.com/juanfont/headscale/pull/278)

## 0.12.3 (2022-01-13)

### Changes

- Added Alpine container [#270](https://github.com/juanfont/headscale/pull/270)
- Minor updates in dependencies [#271](https://github.com/juanfont/headscale/pull/271)

## 0.12.2 (2022-01-11)

Happy New Year!

### Changes

- Fix Docker release [#258](https://github.com/juanfont/headscale/pull/258)
- Rewrite main docs [#262](https://github.com/juanfont/headscale/pull/262)
- Improve Docker docs [#263](https://github.com/juanfont/headscale/pull/263)

## 0.12.1 (2021-12-24)

(We are skipping 0.12.0 to correct a mishap done weeks ago with the version
tagging)

### BREAKING

- Upgrade to Tailscale 1.18 [#229](https://github.com/juanfont/headscale/pull/229)
  - This change requires a new format for private key, private keys are now
    generated automatically:
    1. Delete your current key
    2. Restart `headscale`, a new key will be generated.
    3. Restart all Tailscale clients to fetch the new key

### Changes

- Unify configuration example [#197](https://github.com/juanfont/headscale/pull/197)
- Add stricter linting and formatting [#223](https://github.com/juanfont/headscale/pull/223)

### Features

- Add gRPC and HTTP API (HTTP API is currently disabled) [#204](https://github.com/juanfont/headscale/pull/204)
- Use gRPC between the CLI and the server [#206](https://github.com/juanfont/headscale/pull/206),
  [#212](https://github.com/juanfont/headscale/pull/212)
- Beta OpenID Connect support [#126](https://github.com/juanfont/headscale/pull/126),
  [#227](https://github.com/juanfont/headscale/pull/227)

## 0.11.0 (2021-10-25)

### BREAKING

- Make headscale fetch DERP map from URL and file [#196](https://github.com/juanfont/headscale/pull/196)


================================================
FILE: CLAUDE.md
================================================
@AGENTS.md


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation
in our community a harassment-free experience for everyone, regardless
of age, body size, visible or invisible disability, ethnicity, sex
characteristics, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.

We pledge to act and interact in ways that contribute to an open,
welcoming, diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for
our community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our
  mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or
  advances of any kind
- Trolling, insulting or derogatory comments, and personal or
  political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
  address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in
  a professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our
standards of acceptable behavior and will take appropriate and fair
corrective action in response to any behavior that they deem
inappropriate, threatening, offensive, or harmful.

Community leaders have the right and responsibility to remove, edit,
or reject comments, commits, code, wiki edits, issues, and other
contributions that are not aligned to this Code of Conduct, and will
communicate reasons for moderation decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also
applies when an individual is officially representing the community in
public spaces. Examples of representing our community include using an
official e-mail address, posting via an official social media account,
or acting as an appointed representative at an online or offline
event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported to the community leaders responsible for enforcement
on our [Discord server](https://discord.gg/c84AZQhmpx). All complaints
will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and
security of the reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in
determining the consequences for any action they deem in violation of
this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior
deemed unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders,
providing clarity around the nature of the violation and an
explanation of why the behavior was inappropriate. A public apology
may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued
behavior. No interaction with the people involved, including
unsolicited interaction with those enforcing the Code of Conduct, for
a specified period of time. This includes avoiding interactions in
community spaces as well as external channels like social
media. Violating these terms may lead to a temporary or permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards,
including sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or
public communication with the community for a specified period of
time. No public or private interaction with the people involved,
including unsolicited interaction with those enforcing the Code of
Conduct, is allowed during this period. Violating these terms may lead
to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of
community standards, including sustained inappropriate behavior,
harassment of an individual, or aggression toward or disparagement of
classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction
within the community.

## Attribution

This Code of Conduct is adapted from the [Contributor
Covenant][homepage], version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of
conduct enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the
FAQ at https://www.contributor-covenant.org/faq. Translations are
available at https://www.contributor-covenant.org/translations.


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

Headscale is "Open Source, acknowledged contribution", this means that any contribution will have to be discussed with the maintainers before being added to the project.
This model has been chosen to reduce the risk of burnout by limiting the maintenance overhead of reviewing and validating third-party code.

## Why do we have this model?

Headscale has a small maintainer team that tries to balance working on the project, fixing bugs and reviewing contributions.

When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops.

Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly.

The review and day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added.

This means that when someone contributes, we are mostly happy about it, but we do have to run it through a series of checks to establish if we actually can maintain this feature.

## What do we require?

A general description is provided here and an explicit list is provided in our pull request template.

All new features have to start out with a design document, which should be discussed on the issue tracker (not discord). It should include a use case for the feature, how it can be implemented, who will implement it and a plan for maintaining it.

All features have to be end-to-end tested (integration tests) and have good unit test coverage to ensure that they work as expected. This will also ensure that the feature continues to work as expected over time. If a change cannot be tested, a strong case for why this is not possible needs to be presented.

The contributor should help to maintain the feature over time. In case the feature is not maintained probably, the maintainers reserve themselves the right to remove features they redeem as unmaintainable. This should help to improve the quality of the software and keep it in a maintainable state.

## Bug fixes

Headscale is open to code contributions for bug fixes without discussion.

## Documentation

If you find mistakes in the documentation, please submit a fix to the documentation.


================================================
FILE: Dockerfile.derper
================================================
# For testing purposes only

FROM golang:1.26.1-alpine AS build-env

WORKDIR /go/src

RUN apk add --no-cache git
ARG VERSION_BRANCH=main
RUN git clone https://github.com/tailscale/tailscale.git --branch=$VERSION_BRANCH --depth=1
WORKDIR /go/src/tailscale

ARG TARGETARCH
RUN GOARCH=$TARGETARCH go install -v ./cmd/derper

FROM alpine:3.22
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl

COPY --from=build-env /go/bin/* /usr/local/bin/
ENTRYPOINT [ "/usr/local/bin/derper" ]


================================================
FILE: Dockerfile.integration
================================================
# This Dockerfile and the images produced are for testing headscale,
# and are in no way endorsed by Headscale's maintainers as an
# official nor supported release or distribution.

FROM docker.io/golang:1.26.1-trixie AS builder
ARG VERSION=dev
ENV GOPATH /go
WORKDIR /go/src/headscale

# Install delve debugger first - rarely changes, good cache candidate
RUN go insta
Download .txt
gitextract_td0jbyuq/

├── .dockerignore
├── .editorconfig
├── .envrc
├── .github/
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   ├── config.yml
│   │   └── feature_request.yaml
│   ├── label-response/
│   │   ├── needs-more-info.md
│   │   └── support-request.md
│   ├── pull_request_template.md
│   ├── renovate.json
│   └── workflows/
│       ├── build.yml
│       ├── check-generated.yml
│       ├── check-tests.yaml
│       ├── docs-deploy.yml
│       ├── docs-test.yml
│       ├── gh-action-integration-generator.go
│       ├── gh-actions-updater.yaml
│       ├── integration-test-template.yml
│       ├── lint.yml
│       ├── needs-more-info-comment.yml
│       ├── needs-more-info-timer.yml
│       ├── nix-module-test.yml
│       ├── release.yml
│       ├── stale.yml
│       ├── support-request.yml
│       ├── test-integration.yaml
│       ├── test.yml
│       └── update-flake.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yml
├── .mcp.json
├── .mdformat.toml
├── .pre-commit-config.yaml
├── .prettierignore
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile.derper
├── Dockerfile.integration
├── Dockerfile.integration-ci
├── Dockerfile.tailscale-HEAD
├── LICENSE
├── Makefile
├── README.md
├── buf.gen.yaml
├── cmd/
│   ├── headscale/
│   │   ├── cli/
│   │   │   ├── api_key.go
│   │   │   ├── auth.go
│   │   │   ├── configtest.go
│   │   │   ├── debug.go
│   │   │   ├── dump_config.go
│   │   │   ├── generate.go
│   │   │   ├── health.go
│   │   │   ├── mockoidc.go
│   │   │   ├── nodes.go
│   │   │   ├── policy.go
│   │   │   ├── preauthkeys.go
│   │   │   ├── pterm_style.go
│   │   │   ├── root.go
│   │   │   ├── root_test.go
│   │   │   ├── serve.go
│   │   │   ├── users.go
│   │   │   ├── utils.go
│   │   │   └── version.go
│   │   ├── headscale.go
│   │   └── headscale_test.go
│   ├── hi/
│   │   ├── README.md
│   │   ├── cleanup.go
│   │   ├── docker.go
│   │   ├── doctor.go
│   │   ├── main.go
│   │   ├── run.go
│   │   └── stats.go
│   └── mapresponses/
│       └── main.go
├── config-example.yaml
├── derp-example.yaml
├── docs/
│   ├── about/
│   │   ├── clients.md
│   │   ├── contributing.md
│   │   ├── faq.md
│   │   ├── features.md
│   │   ├── help.md
│   │   ├── releases.md
│   │   └── sponsor.md
│   ├── index.md
│   ├── ref/
│   │   ├── acls.md
│   │   ├── api.md
│   │   ├── configuration.md
│   │   ├── debug.md
│   │   ├── derp.md
│   │   ├── dns.md
│   │   ├── integration/
│   │   │   ├── reverse-proxy.md
│   │   │   ├── tools.md
│   │   │   └── web-ui.md
│   │   ├── oidc.md
│   │   ├── registration.md
│   │   ├── routes.md
│   │   ├── tags.md
│   │   └── tls.md
│   ├── requirements.txt
│   ├── setup/
│   │   ├── install/
│   │   │   ├── community.md
│   │   │   ├── container.md
│   │   │   ├── official.md
│   │   │   └── source.md
│   │   ├── requirements.md
│   │   └── upgrade.md
│   └── usage/
│       ├── connect/
│       │   ├── android.md
│       │   ├── apple.md
│       │   └── windows.md
│       └── getting-started.md
├── flake.nix
├── gen/
│   ├── go/
│   │   └── headscale/
│   │       └── v1/
│   │           ├── apikey.pb.go
│   │           ├── auth.pb.go
│   │           ├── device.pb.go
│   │           ├── headscale.pb.go
│   │           ├── headscale.pb.gw.go
│   │           ├── headscale_grpc.pb.go
│   │           ├── node.pb.go
│   │           ├── policy.pb.go
│   │           ├── preauthkey.pb.go
│   │           └── user.pb.go
│   └── openapiv2/
│       └── headscale/
│           └── v1/
│               ├── apikey.swagger.json
│               ├── auth.swagger.json
│               ├── device.swagger.json
│               ├── headscale.swagger.json
│               ├── node.swagger.json
│               ├── policy.swagger.json
│               ├── preauthkey.swagger.json
│               └── user.swagger.json
├── go.mod
├── go.sum
├── hscontrol/
│   ├── app.go
│   ├── assets/
│   │   ├── assets.go
│   │   └── style.css
│   ├── auth.go
│   ├── auth_tags_test.go
│   ├── auth_test.go
│   ├── capver/
│   │   ├── capver.go
│   │   ├── capver_generated.go
│   │   ├── capver_test.go
│   │   └── capver_test_data.go
│   ├── db/
│   │   ├── api_key.go
│   │   ├── api_key_test.go
│   │   ├── db.go
│   │   ├── db_test.go
│   │   ├── ephemeral_garbage_collector_test.go
│   │   ├── ip.go
│   │   ├── ip_test.go
│   │   ├── main_test.go
│   │   ├── node.go
│   │   ├── node_test.go
│   │   ├── policy.go
│   │   ├── preauth_keys.go
│   │   ├── preauth_keys_test.go
│   │   ├── schema.sql
│   │   ├── sqliteconfig/
│   │   │   ├── config.go
│   │   │   ├── config_test.go
│   │   │   └── integration_test.go
│   │   ├── suite_test.go
│   │   ├── testdata/
│   │   │   └── sqlite/
│   │   │       ├── failing-node-preauth-constraint_dump.sql
│   │   │       ├── headscale_0.26.0-beta.1_dump.sql
│   │   │       ├── headscale_0.26.0-beta.2_dump.sql
│   │   │       ├── headscale_0.26.0_dump.sql
│   │   │       ├── headscale_0.26.1_dump-litestream.sql
│   │   │       ├── headscale_0.26.1_dump.sql
│   │   │       ├── headscale_0.26.1_dump_schema-to-0.27.0-old-table-cleanup.sql
│   │   │       └── request_tags_migration_test.sql
│   │   ├── text_serialiser.go
│   │   ├── user_update_test.go
│   │   ├── users.go
│   │   ├── users_test.go
│   │   ├── versioncheck.go
│   │   └── versioncheck_test.go
│   ├── debug.go
│   ├── derp/
│   │   ├── derp.go
│   │   ├── derp_test.go
│   │   └── server/
│   │       └── derp_server.go
│   ├── dns/
│   │   └── extrarecords.go
│   ├── grpcv1.go
│   ├── grpcv1_test.go
│   ├── handlers.go
│   ├── mapper/
│   │   ├── batcher.go
│   │   ├── batcher_bench_test.go
│   │   ├── batcher_concurrency_test.go
│   │   ├── batcher_scale_bench_test.go
│   │   ├── batcher_test.go
│   │   ├── batcher_unit_test.go
│   │   ├── builder.go
│   │   ├── builder_test.go
│   │   ├── mapper.go
│   │   ├── mapper_test.go
│   │   ├── node_conn.go
│   │   └── tail_test.go
│   ├── metrics.go
│   ├── noise.go
│   ├── noise_test.go
│   ├── oidc.go
│   ├── oidc_template_test.go
│   ├── oidc_test.go
│   ├── platform_config.go
│   ├── policy/
│   │   ├── matcher/
│   │   │   ├── matcher.go
│   │   │   └── matcher_test.go
│   │   ├── pm.go
│   │   ├── policy.go
│   │   ├── policy_autoapprove_test.go
│   │   ├── policy_route_approval_test.go
│   │   ├── policy_test.go
│   │   ├── policyutil/
│   │   │   ├── reduce.go
│   │   │   └── reduce_test.go
│   │   ├── route_approval_test.go
│   │   └── v2/
│   │       ├── filter.go
│   │       ├── filter_test.go
│   │       ├── main_test.go
│   │       ├── policy.go
│   │       ├── policy_test.go
│   │       ├── tailscale_compat_test.go
│   │       ├── tailscale_routes_compat_test.go
│   │       ├── tailscale_ssh_data_compat_test.go
│   │       ├── testdata/
│   │       │   └── ssh_results/
│   │       │       ├── SSH-A1.json
│   │       │       ├── SSH-A2.json
│   │       │       ├── SSH-A3.json
│   │       │       ├── SSH-A4.json
│   │       │       ├── SSH-A5.json
│   │       │       ├── SSH-A6.json
│   │       │       ├── SSH-A7.json
│   │       │       ├── SSH-A8.json
│   │       │       ├── SSH-B1.json
│   │       │       ├── SSH-B2.json
│   │       │       ├── SSH-B3.json
│   │       │       ├── SSH-B5.json
│   │       │       ├── SSH-B6.json
│   │       │       ├── SSH-C1.json
│   │       │       ├── SSH-C2.json
│   │       │       ├── SSH-C3.json
│   │       │       ├── SSH-C4.json
│   │       │       ├── SSH-D10.json
│   │       │       ├── SSH-D11.json
│   │       │       ├── SSH-D12.json
│   │       │       ├── SSH-D2.json
│   │       │       ├── SSH-D3.json
│   │       │       ├── SSH-D4.json
│   │       │       ├── SSH-D5.json
│   │       │       ├── SSH-D6.json
│   │       │       ├── SSH-D7.json
│   │       │       ├── SSH-D8.json
│   │       │       ├── SSH-D9.json
│   │       │       ├── SSH-E3.json
│   │       │       ├── SSH-E4.json
│   │       │       ├── SSH-E5.json
│   │       │       ├── SSH-E6.json
│   │       │       ├── SSH-F1.json
│   │       │       ├── SSH-F2.json
│   │       │       ├── SSH-F3.json
│   │       │       ├── SSH-F4.json
│   │       │       ├── SSH-F5.json
│   │       │       ├── SSH-G1.json
│   │       │       └── SSH-G2.json
│   │       ├── types.go
│   │       ├── types_test.go
│   │       ├── utils.go
│   │       └── utils_test.go
│   ├── poll.go
│   ├── poll_test.go
│   ├── routes/
│   │   ├── primary.go
│   │   └── primary_test.go
│   ├── servertest/
│   │   ├── assertions.go
│   │   ├── client.go
│   │   ├── consistency_test.go
│   │   ├── content_test.go
│   │   ├── ephemeral_test.go
│   │   ├── harness.go
│   │   ├── issues_test.go
│   │   ├── lifecycle_test.go
│   │   ├── policy_test.go
│   │   ├── poll_race_test.go
│   │   ├── race_test.go
│   │   ├── routes_test.go
│   │   ├── server.go
│   │   ├── stress_test.go
│   │   └── weather_test.go
│   ├── state/
│   │   ├── debug.go
│   │   ├── debug_test.go
│   │   ├── endpoint_test.go
│   │   ├── ephemeral_test.go
│   │   ├── maprequest.go
│   │   ├── maprequest_test.go
│   │   ├── node_store.go
│   │   ├── node_store_test.go
│   │   ├── ssh_check_test.go
│   │   ├── state.go
│   │   ├── tags.go
│   │   └── test_helpers.go
│   ├── tailsql.go
│   ├── templates/
│   │   ├── apple.go
│   │   ├── auth_success.go
│   │   ├── auth_web.go
│   │   ├── design.go
│   │   ├── general.go
│   │   └── windows.go
│   ├── templates_consistency_test.go
│   ├── types/
│   │   ├── api_key.go
│   │   ├── change/
│   │   │   ├── change.go
│   │   │   └── change_test.go
│   │   ├── common.go
│   │   ├── common_test.go
│   │   ├── config.go
│   │   ├── config_test.go
│   │   ├── const.go
│   │   ├── main_test.go
│   │   ├── node.go
│   │   ├── node_benchmark_test.go
│   │   ├── node_tags_test.go
│   │   ├── node_test.go
│   │   ├── policy.go
│   │   ├── preauth_key.go
│   │   ├── preauth_key_test.go
│   │   ├── routes.go
│   │   ├── testdata/
│   │   │   ├── base-domain-in-server-url.yaml
│   │   │   ├── base-domain-not-in-server-url.yaml
│   │   │   ├── dns-override-true-error.yaml
│   │   │   ├── dns-override-true.yaml
│   │   │   ├── dns_full.yaml
│   │   │   ├── dns_full_no_magic.yaml
│   │   │   ├── minimal.yaml
│   │   │   └── policy-path-is-loaded.yaml
│   │   ├── types_clone.go
│   │   ├── types_view.go
│   │   ├── users.go
│   │   ├── users_test.go
│   │   └── version.go
│   └── util/
│       ├── addr.go
│       ├── addr_test.go
│       ├── const.go
│       ├── dns.go
│       ├── dns_test.go
│       ├── file.go
│       ├── key.go
│       ├── log.go
│       ├── net.go
│       ├── norace.go
│       ├── prompt.go
│       ├── prompt_test.go
│       ├── race.go
│       ├── string.go
│       ├── string_test.go
│       ├── test.go
│       ├── util.go
│       ├── util_test.go
│       └── zlog/
│           ├── fields.go
│           ├── hostinfo.go
│           ├── maprequest.go
│           ├── zf/
│           │   └── fields.go
│           └── zlog_test.go
├── integration/
│   ├── README.md
│   ├── acl_test.go
│   ├── api_auth_test.go
│   ├── auth_key_test.go
│   ├── auth_oidc_test.go
│   ├── auth_web_flow_test.go
│   ├── cli_test.go
│   ├── control.go
│   ├── derp_verify_endpoint_test.go
│   ├── dns_test.go
│   ├── dockertestutil/
│   │   ├── build.go
│   │   ├── config.go
│   │   ├── execute.go
│   │   ├── logs.go
│   │   └── network.go
│   ├── dsic/
│   │   └── dsic.go
│   ├── embedded_derp_test.go
│   ├── general_test.go
│   ├── helpers.go
│   ├── hsic/
│   │   ├── config.go
│   │   └── hsic.go
│   ├── integrationutil/
│   │   └── util.go
│   ├── route_test.go
│   ├── run.sh
│   ├── scenario.go
│   ├── scenario_test.go
│   ├── ssh_test.go
│   ├── tags_test.go
│   ├── tailscale.go
│   └── tsic/
│       └── tsic.go
├── mkdocs.yml
├── nix/
│   ├── README.md
│   ├── example-configuration.nix
│   ├── module.nix
│   └── tests/
│       └── headscale.nix
├── packaging/
│   ├── README.md
│   ├── deb/
│   │   ├── postinst
│   │   ├── postrm
│   │   └── prerm
│   └── systemd/
│       └── headscale.service
├── proto/
│   ├── buf.yaml
│   └── headscale/
│       └── v1/
│           ├── apikey.proto
│           ├── auth.proto
│           ├── device.proto
│           ├── headscale.proto
│           ├── node.proto
│           ├── policy.proto
│           ├── preauthkey.proto
│           └── user.proto
├── swagger.go
└── tools/
    └── capver/
        └── main.go
Download .txt
Showing preview only (360K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (3826 symbols across 236 files)

FILE: .github/workflows/gh-action-integration-generator.go
  function expandTests (line 44) | func expandTests(tests []string) []string {
  function findTests (line 62) | func findTests() []string {
  function updateYAML (line 90) | func updateYAML(tests []string, jobName string, testPath string) {
  function main (line 115) | func main() {

FILE: cmd/headscale/cli/api_key.go
  constant DefaultAPIKeyExpiry (line 16) | DefaultAPIKeyExpiry = "90d"
  function init (line 19) | func init() {
  function apiKeyIDOrPrefix (line 105) | func apiKeyIDOrPrefix(cmd *cobra.Command) (uint64, string, error) {

FILE: cmd/headscale/cli/auth.go
  function init (line 11) | func init() {

FILE: cmd/headscale/cli/configtest.go
  function init (line 9) | func init() {

FILE: cmd/headscale/cli/debug.go
  function init (line 12) | func init() {

FILE: cmd/headscale/cli/dump_config.go
  function init (line 10) | func init() {

FILE: cmd/headscale/cli/generate.go
  function init (line 10) | func init() {

FILE: cmd/headscale/cli/health.go
  function init (line 11) | func init() {

FILE: cmd/headscale/cli/mockoidc.go
  type Error (line 20) | type Error
    method Error (line 22) | func (e Error) Error() string { return string(e) }
  constant errMockOidcClientIDNotDefined (line 25) | errMockOidcClientIDNotDefined     = Error("MOCKOIDC_CLIENT_ID not defined")
  constant errMockOidcClientSecretNotDefined (line 26) | errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not de...
  constant errMockOidcPortNotDefined (line 27) | errMockOidcPortNotDefined         = Error("MOCKOIDC_PORT not defined")
  constant errMockOidcUsersNotDefined (line 28) | errMockOidcUsersNotDefined        = Error("MOCKOIDC_USERS not defined")
  constant refreshTTL (line 29) | refreshTTL                        = 60 * time.Minute
  function init (line 34) | func init() {
  function mockOIDC (line 52) | func mockOIDC() error {
  function getMockOIDC (line 128) | func getMockOIDC(clientID string, clientSecret string, users []mockoidc....

FILE: cmd/headscale/cli/nodes.go
  function init (line 20) | func init() {
  function nodesToPtables (line 305) | func nodesToPtables(
  function nodeRoutesToPtables (line 437) | func nodeRoutesToPtables(

FILE: cmd/headscale/cli/policy.go
  constant bypassFlag (line 17) | bypassFlag = "bypass-grpc-and-access-database-directly"
  function bypassDatabase (line 25) | func bypassDatabase() (*db.HSDatabase, error) {
  function init (line 39) | func init() {

FILE: cmd/headscale/cli/preauthkeys.go
  constant DefaultPreAuthKeyExpiry (line 16) | DefaultPreAuthKeyExpiry = "1h"
  function init (line 19) | func init() {

FILE: cmd/headscale/cli/pterm_style.go
  function ColourTime (line 9) | func ColourTime(date time.Time) string {

FILE: cmd/headscale/cli/root.go
  function init (line 19) | func init() {
  function initConfig (line 47) | func initConfig() {
  function isPreReleaseVersion (line 103) | func isPreReleaseVersion(version string) bool {
  function filterPreReleasesIfStable (line 118) | func filterPreReleasesIfStable(versionFunc func() string) func(string) b...
  function Execute (line 150) | func Execute() {

FILE: cmd/headscale/cli/root_test.go
  function TestFilterPreReleasesIfStable (line 7) | func TestFilterPreReleasesIfStable(t *testing.T) {
  function TestIsPreReleaseVersion (line 216) | func TestIsPreReleaseVersion(t *testing.T) {

FILE: cmd/headscale/cli/serve.go
  function init (line 12) | func init() {

FILE: cmd/headscale/cli/users.go
  function usernameAndIDFlag (line 24) | func usernameAndIDFlag(cmd *cobra.Command) {
  function usernameAndIDFromFlag (line 30) | func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string, error) {
  function init (line 47) | func init() {

FILE: cmd/headscale/cli/utils.go
  constant HeadscaleDateTimeFormat (line 28) | HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
  constant SocketWritePermissions (line 29) | SocketWritePermissions  = 0o666
  constant outputFormatJSON (line 31) | outputFormatJSON     = "json"
  constant outputFormatJSONLine (line 32) | outputFormatJSONLine = "json-line"
  constant outputFormatYAML (line 33) | outputFormatYAML     = "yaml"
  function mustMarkRequired (line 44) | func mustMarkRequired(cmd *cobra.Command, names ...string) {
  function newHeadscaleServerWithConfig (line 53) | func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
  function grpcRunE (line 73) | func grpcRunE(
  function newHeadscaleCLIWithConfig (line 88) | func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceCl...
  function formatOutput (line 187) | func formatOutput(result any, override string, outputFormat string) (str...
  function printOutput (line 217) | func printOutput(cmd *cobra.Command, result any, override string) error {
  function expirationFromFlag (line 232) | func expirationFromFlag(cmd *cobra.Command) (*timestamppb.Timestamp, err...
  function confirmAction (line 245) | func confirmAction(cmd *cobra.Command, prompt string) bool {
  function printListOutput (line 257) | func printListOutput(
  function printError (line 273) | func printError(err error, outputFormat string) {
  function hasMachineOutputFlag (line 298) | func hasMachineOutputFlag() bool {
  type tokenAuth (line 308) | type tokenAuth struct
    method GetRequestMetadata (line 313) | func (t tokenAuth) GetRequestMetadata(
    method RequireTransportSecurity (line 322) | func (tokenAuth) RequireTransportSecurity() bool {

FILE: cmd/headscale/cli/version.go
  function init (line 8) | func init() {

FILE: cmd/headscale/headscale.go
  function main (line 13) | func main() {

FILE: cmd/headscale/headscale_test.go
  function TestConfigFileLoading (line 16) | func TestConfigFileLoading(t *testing.T) {
  function TestConfigLoading (line 48) | func TestConfigLoading(t *testing.T) {

FILE: cmd/hi/cleanup.go
  function cleanupBeforeTest (line 22) | func cleanupBeforeTest(ctx context.Context) error {
  function cleanupAfterTest (line 36) | func cleanupAfterTest(ctx context.Context, cli *client.Client, container...
  function killTestContainers (line 57) | func killTestContainers(ctx context.Context) error {
  function killTestContainersByRunID (line 111) | func killTestContainersByRunID(ctx context.Context, runID string) error {
  function cleanupStaleTestContainers (line 153) | func cleanupStaleTestContainers(ctx context.Context) error {
  constant containerRemoveInitialInterval (line 203) | containerRemoveInitialInterval = 100 * time.Millisecond
  constant containerRemoveMaxElapsedTime (line 204) | containerRemoveMaxElapsedTime  = 2 * time.Second
  function removeContainerWithRetry (line 208) | func removeContainerWithRetry(ctx context.Context, cli *client.Client, c...
  function pruneDockerNetworks (line 227) | func pruneDockerNetworks(ctx context.Context) error {
  function cleanOldImages (line 249) | func cleanOldImages(ctx context.Context) error {
  function cleanCacheVolume (line 301) | func cleanCacheVolume(ctx context.Context) error {
  function cleanupSuccessfulTestArtifacts (line 335) | func cleanupSuccessfulTestArtifacts(logsDir string, verbose bool) error {
  function getDirSize (line 415) | func getDirSize(path string) (int64, error) {

FILE: cmd/hi/docker.go
  constant defaultDirPerm (line 25) | defaultDirPerm = 0o755
  function runTestContainer (line 37) | func runTestContainer(ctx context.Context, config *RunConfig) error {
  function buildGoTestCommand (line 205) | func buildGoTestCommand(config *RunConfig) []string {
  function createGoTestContainer (line 223) | func createGoTestContainer(ctx context.Context, cli *client.Client, conf...
  function streamAndWait (line 321) | func streamAndWait(ctx context.Context, cli *client.Client, containerID ...
  function waitForContainerFinalization (line 350) | func waitForContainerFinalization(ctx context.Context, cli *client.Clien...
  function isContainerFinalized (line 412) | func isContainerFinalized(state *container.State) bool {
  function findProjectRoot (line 418) | func findProjectRoot(startPath string) string {
  function boolToInt (line 435) | func boolToInt(b bool) int {
  type DockerContext (line 444) | type DockerContext struct
  function createDockerClient (line 452) | func createDockerClient(ctx context.Context) (*client.Client, error) {
  function getCurrentDockerContext (line 484) | func getCurrentDockerContext(ctx context.Context) (*DockerContext, error) {
  function getDockerSocketPath (line 505) | func getDockerSocketPath() string {
  function checkImageAvailableLocally (line 512) | func checkImageAvailableLocally(ctx context.Context, cli *client.Client,...
  function ensureImageAvailable (line 526) | func ensureImageAvailable(ctx context.Context, cli *client.Client, image...
  function listControlFiles (line 570) | func listControlFiles(logsDir string) {
  function extractArtifactsFromContainers (line 630) | func extractArtifactsFromContainers(ctx context.Context, testContainerID...
  type testContainer (line 672) | type testContainer struct
  function getCurrentTestContainers (line 678) | func getCurrentTestContainers(containers []container.Summary, testContai...
  function extractContainerArtifacts (line 728) | func extractContainerArtifacts(ctx context.Context, cli *client.Client, ...
  function extractContainerLogs (line 756) | func extractContainerLogs(ctx context.Context, cli *client.Client, conta...
  function extractContainerFiles (line 803) | func extractContainerFiles(ctx context.Context, cli *client.Client, cont...

FILE: cmd/hi/doctor.go
  type DoctorResult (line 15) | type DoctorResult struct
  function runDoctorCheck (line 23) | func runDoctorCheck(ctx context.Context) error {
  function checkDockerBinary (line 65) | func checkDockerBinary() DoctorResult {
  function checkDockerDaemon (line 88) | func checkDockerDaemon(ctx context.Context) DoctorResult {
  function checkDockerContext (line 127) | func checkDockerContext(ctx context.Context) DoctorResult {
  function checkDockerSocket (line 157) | func checkDockerSocket(ctx context.Context) DoctorResult {
  function checkGolangImage (line 194) | func checkGolangImage(ctx context.Context) DoctorResult {
  function checkGoInstallation (line 254) | func checkGoInstallation(ctx context.Context) DoctorResult {
  function checkGitRepository (line 289) | func checkGitRepository(ctx context.Context) DoctorResult {
  function checkRequiredFiles (line 313) | func checkRequiredFiles(ctx context.Context) DoctorResult {
  function displayDoctorResults (line 352) | func displayDoctorResults(results []DoctorResult) {

FILE: cmd/hi/main.go
  function main (line 13) | func main() {
  function cleanAll (line 81) | func cleanAll(ctx context.Context) error {

FILE: cmd/hi/run.go
  type RunConfig (line 16) | type RunConfig struct
  function runIntegrationTest (line 33) | func runIntegrationTest(env *command.Env) error {
  function detectGoVersion (line 68) | func detectGoVersion() string {
  function splitLines (line 98) | func splitLines(s string) []string {
  function indexOf (line 121) | func indexOf(s, substr string) int {

FILE: cmd/hi/stats.go
  type ContainerStats (line 25) | type ContainerStats struct
  type StatsSample (line 33) | type StatsSample struct
  type StatsCollector (line 40) | type StatsCollector struct
    method StartCollection (line 64) | func (sc *StatsCollector) StartCollection(ctx context.Context, runID s...
    method StopCollection (line 92) | func (sc *StatsCollector) StopCollection() {
    method monitorExistingContainers (line 116) | func (sc *StatsCollector) monitorExistingContainers(ctx context.Contex...
    method monitorDockerEvents (line 136) | func (sc *StatsCollector) monitorDockerEvents(ctx context.Context, run...
    method shouldMonitorContainer (line 185) | func (sc *StatsCollector) shouldMonitorContainer(cont types.Container,...
    method startStatsForContainer (line 203) | func (sc *StatsCollector) startStatsForContainer(ctx context.Context, ...
    method collectStatsForContainer (line 230) | func (sc *StatsCollector) collectStatsForContainer(ctx context.Context...
    method GetSummary (line 348) | func (sc *StatsCollector) GetSummary() []ContainerStatsSummary {
    method PrintSummary (line 430) | func (sc *StatsCollector) PrintSummary() {
    method CheckMemoryLimits (line 452) | func (sc *StatsCollector) CheckMemoryLimits(hsLimitMB, tsLimitMB float...
    method PrintSummaryAndCheckLimits (line 484) | func (sc *StatsCollector) PrintSummaryAndCheckLimits(hsLimitMB, tsLimi...
    method Close (line 490) | func (sc *StatsCollector) Close() error {
  function NewStatsCollector (line 50) | func NewStatsCollector(ctx context.Context) (*StatsCollector, error) {
  function calculateCPUPercent (line 306) | func calculateCPUPercent(prevStats, stats *container.Stats) float64 { //...
  type ContainerStatsSummary (line 326) | type ContainerStatsSummary struct
  type MemoryViolation (line 334) | type MemoryViolation struct
  type StatsSummary (line 341) | type StatsSummary struct
  function calculateStatsSummary (line 401) | func calculateStatsSummary(values []float64) StatsSummary {

FILE: cmd/mapresponses/main.go
  type MapConfig (line 15) | type MapConfig struct
  function main (line 24) | func main() {
  function runOnline (line 45) | func runOnline(env *command.Env) error {

FILE: gen/go/headscale/v1/apikey.pb.go
  constant _ (line 20) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 22) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type ApiKey (line 25) | type ApiKey struct
    method Reset (line 36) | func (x *ApiKey) Reset() {
    method String (line 43) | func (x *ApiKey) String() string {
    method ProtoMessage (line 47) | func (*ApiKey) ProtoMessage() {}
    method ProtoReflect (line 49) | func (x *ApiKey) ProtoReflect() protoreflect.Message {
    method Descriptor (line 62) | func (*ApiKey) Descriptor() ([]byte, []int) {
    method GetId (line 66) | func (x *ApiKey) GetId() uint64 {
    method GetPrefix (line 73) | func (x *ApiKey) GetPrefix() string {
    method GetExpiration (line 80) | func (x *ApiKey) GetExpiration() *timestamppb.Timestamp {
    method GetCreatedAt (line 87) | func (x *ApiKey) GetCreatedAt() *timestamppb.Timestamp {
    method GetLastSeen (line 94) | func (x *ApiKey) GetLastSeen() *timestamppb.Timestamp {
  type CreateApiKeyRequest (line 101) | type CreateApiKeyRequest struct
    method Reset (line 108) | func (x *CreateApiKeyRequest) Reset() {
    method String (line 115) | func (x *CreateApiKeyRequest) String() string {
    method ProtoMessage (line 119) | func (*CreateApiKeyRequest) ProtoMessage() {}
    method ProtoReflect (line 121) | func (x *CreateApiKeyRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 134) | func (*CreateApiKeyRequest) Descriptor() ([]byte, []int) {
    method GetExpiration (line 138) | func (x *CreateApiKeyRequest) GetExpiration() *timestamppb.Timestamp {
  type CreateApiKeyResponse (line 145) | type CreateApiKeyResponse struct
    method Reset (line 152) | func (x *CreateApiKeyResponse) Reset() {
    method String (line 159) | func (x *CreateApiKeyResponse) String() string {
    method ProtoMessage (line 163) | func (*CreateApiKeyResponse) ProtoMessage() {}
    method ProtoReflect (line 165) | func (x *CreateApiKeyResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 178) | func (*CreateApiKeyResponse) Descriptor() ([]byte, []int) {
    method GetApiKey (line 182) | func (x *CreateApiKeyResponse) GetApiKey() string {
  type ExpireApiKeyRequest (line 189) | type ExpireApiKeyRequest struct
    method Reset (line 197) | func (x *ExpireApiKeyRequest) Reset() {
    method String (line 204) | func (x *ExpireApiKeyRequest) String() string {
    method ProtoMessage (line 208) | func (*ExpireApiKeyRequest) ProtoMessage() {}
    method ProtoReflect (line 210) | func (x *ExpireApiKeyRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 223) | func (*ExpireApiKeyRequest) Descriptor() ([]byte, []int) {
    method GetPrefix (line 227) | func (x *ExpireApiKeyRequest) GetPrefix() string {
    method GetId (line 234) | func (x *ExpireApiKeyRequest) GetId() uint64 {
  type ExpireApiKeyResponse (line 241) | type ExpireApiKeyResponse struct
    method Reset (line 247) | func (x *ExpireApiKeyResponse) Reset() {
    method String (line 254) | func (x *ExpireApiKeyResponse) String() string {
    method ProtoMessage (line 258) | func (*ExpireApiKeyResponse) ProtoMessage() {}
    method ProtoReflect (line 260) | func (x *ExpireApiKeyResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 273) | func (*ExpireApiKeyResponse) Descriptor() ([]byte, []int) {
  type ListApiKeysRequest (line 277) | type ListApiKeysRequest struct
    method Reset (line 283) | func (x *ListApiKeysRequest) Reset() {
    method String (line 290) | func (x *ListApiKeysRequest) String() string {
    method ProtoMessage (line 294) | func (*ListApiKeysRequest) ProtoMessage() {}
    method ProtoReflect (line 296) | func (x *ListApiKeysRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 309) | func (*ListApiKeysRequest) Descriptor() ([]byte, []int) {
  type ListApiKeysResponse (line 313) | type ListApiKeysResponse struct
    method Reset (line 320) | func (x *ListApiKeysResponse) Reset() {
    method String (line 327) | func (x *ListApiKeysResponse) String() string {
    method ProtoMessage (line 331) | func (*ListApiKeysResponse) ProtoMessage() {}
    method ProtoReflect (line 333) | func (x *ListApiKeysResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 346) | func (*ListApiKeysResponse) Descriptor() ([]byte, []int) {
    method GetApiKeys (line 350) | func (x *ListApiKeysResponse) GetApiKeys() []*ApiKey {
  type DeleteApiKeyRequest (line 357) | type DeleteApiKeyRequest struct
    method Reset (line 365) | func (x *DeleteApiKeyRequest) Reset() {
    method String (line 372) | func (x *DeleteApiKeyRequest) String() string {
    method ProtoMessage (line 376) | func (*DeleteApiKeyRequest) ProtoMessage() {}
    method ProtoReflect (line 378) | func (x *DeleteApiKeyRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 391) | func (*DeleteApiKeyRequest) Descriptor() ([]byte, []int) {
    method GetPrefix (line 395) | func (x *DeleteApiKeyRequest) GetPrefix() string {
    method GetId (line 402) | func (x *DeleteApiKeyRequest) GetId() uint64 {
  type DeleteApiKeyResponse (line 409) | type DeleteApiKeyResponse struct
    method Reset (line 415) | func (x *DeleteApiKeyResponse) Reset() {
    method String (line 422) | func (x *DeleteApiKeyResponse) String() string {
    method ProtoMessage (line 426) | func (*DeleteApiKeyResponse) ProtoMessage() {}
    method ProtoReflect (line 428) | func (x *DeleteApiKeyResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 441) | func (*DeleteApiKeyResponse) Descriptor() ([]byte, []int) {
  constant file_headscale_v1_apikey_proto_rawDesc (line 447) | file_headscale_v1_apikey_proto_rawDesc = "" +
  function file_headscale_v1_apikey_proto_rawDescGZIP (line 482) | func file_headscale_v1_apikey_proto_rawDescGZIP() []byte {
  function init (line 515) | func init() { file_headscale_v1_apikey_proto_init() }
  function file_headscale_v1_apikey_proto_init (line 516) | func file_headscale_v1_apikey_proto_init() {

FILE: gen/go/headscale/v1/auth.pb.go
  constant _ (line 19) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 21) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type AuthRegisterRequest (line 24) | type AuthRegisterRequest struct
    method Reset (line 32) | func (x *AuthRegisterRequest) Reset() {
    method String (line 39) | func (x *AuthRegisterRequest) String() string {
    method ProtoMessage (line 43) | func (*AuthRegisterRequest) ProtoMessage() {}
    method ProtoReflect (line 45) | func (x *AuthRegisterRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 58) | func (*AuthRegisterRequest) Descriptor() ([]byte, []int) {
    method GetUser (line 62) | func (x *AuthRegisterRequest) GetUser() string {
    method GetAuthId (line 69) | func (x *AuthRegisterRequest) GetAuthId() string {
  type AuthRegisterResponse (line 76) | type AuthRegisterResponse struct
    method Reset (line 83) | func (x *AuthRegisterResponse) Reset() {
    method String (line 90) | func (x *AuthRegisterResponse) String() string {
    method ProtoMessage (line 94) | func (*AuthRegisterResponse) ProtoMessage() {}
    method ProtoReflect (line 96) | func (x *AuthRegisterResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 109) | func (*AuthRegisterResponse) Descriptor() ([]byte, []int) {
    method GetNode (line 113) | func (x *AuthRegisterResponse) GetNode() *Node {
  type AuthApproveRequest (line 120) | type AuthApproveRequest struct
    method Reset (line 127) | func (x *AuthApproveRequest) Reset() {
    method String (line 134) | func (x *AuthApproveRequest) String() string {
    method ProtoMessage (line 138) | func (*AuthApproveRequest) ProtoMessage() {}
    method ProtoReflect (line 140) | func (x *AuthApproveRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 153) | func (*AuthApproveRequest) Descriptor() ([]byte, []int) {
    method GetAuthId (line 157) | func (x *AuthApproveRequest) GetAuthId() string {
  type AuthApproveResponse (line 164) | type AuthApproveResponse struct
    method Reset (line 170) | func (x *AuthApproveResponse) Reset() {
    method String (line 177) | func (x *AuthApproveResponse) String() string {
    method ProtoMessage (line 181) | func (*AuthApproveResponse) ProtoMessage() {}
    method ProtoReflect (line 183) | func (x *AuthApproveResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 196) | func (*AuthApproveResponse) Descriptor() ([]byte, []int) {
  type AuthRejectRequest (line 200) | type AuthRejectRequest struct
    method Reset (line 207) | func (x *AuthRejectRequest) Reset() {
    method String (line 214) | func (x *AuthRejectRequest) String() string {
    method ProtoMessage (line 218) | func (*AuthRejectRequest) ProtoMessage() {}
    method ProtoReflect (line 220) | func (x *AuthRejectRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 233) | func (*AuthRejectRequest) Descriptor() ([]byte, []int) {
    method GetAuthId (line 237) | func (x *AuthRejectRequest) GetAuthId() string {
  type AuthRejectResponse (line 244) | type AuthRejectResponse struct
    method Reset (line 250) | func (x *AuthRejectResponse) Reset() {
    method String (line 257) | func (x *AuthRejectResponse) String() string {
    method ProtoMessage (line 261) | func (*AuthRejectResponse) ProtoMessage() {}
    method ProtoReflect (line 263) | func (x *AuthRejectResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 276) | func (*AuthRejectResponse) Descriptor() ([]byte, []int) {
  constant file_headscale_v1_auth_proto_rawDesc (line 282) | file_headscale_v1_auth_proto_rawDesc = "" +
  function file_headscale_v1_auth_proto_rawDescGZIP (line 302) | func file_headscale_v1_auth_proto_rawDescGZIP() []byte {
  function init (line 328) | func init() { file_headscale_v1_auth_proto_init() }
  function file_headscale_v1_auth_proto_init (line 329) | func file_headscale_v1_auth_proto_init() {

FILE: gen/go/headscale/v1/device.pb.go
  constant _ (line 20) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 22) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type Latency (line 25) | type Latency struct
    method Reset (line 33) | func (x *Latency) Reset() {
    method String (line 40) | func (x *Latency) String() string {
    method ProtoMessage (line 44) | func (*Latency) ProtoMessage() {}
    method ProtoReflect (line 46) | func (x *Latency) ProtoReflect() protoreflect.Message {
    method Descriptor (line 59) | func (*Latency) Descriptor() ([]byte, []int) {
    method GetLatencyMs (line 63) | func (x *Latency) GetLatencyMs() float32 {
    method GetPreferred (line 70) | func (x *Latency) GetPreferred() bool {
  type ClientSupports (line 77) | type ClientSupports struct
    method Reset (line 89) | func (x *ClientSupports) Reset() {
    method String (line 96) | func (x *ClientSupports) String() string {
    method ProtoMessage (line 100) | func (*ClientSupports) ProtoMessage() {}
    method ProtoReflect (line 102) | func (x *ClientSupports) ProtoReflect() protoreflect.Message {
    method Descriptor (line 115) | func (*ClientSupports) Descriptor() ([]byte, []int) {
    method GetHairPinning (line 119) | func (x *ClientSupports) GetHairPinning() bool {
    method GetIpv6 (line 126) | func (x *ClientSupports) GetIpv6() bool {
    method GetPcp (line 133) | func (x *ClientSupports) GetPcp() bool {
    method GetPmp (line 140) | func (x *ClientSupports) GetPmp() bool {
    method GetUdp (line 147) | func (x *ClientSupports) GetUdp() bool {
    method GetUpnp (line 154) | func (x *ClientSupports) GetUpnp() bool {
  type ClientConnectivity (line 161) | type ClientConnectivity struct
    method Reset (line 172) | func (x *ClientConnectivity) Reset() {
    method String (line 179) | func (x *ClientConnectivity) String() string {
    method ProtoMessage (line 183) | func (*ClientConnectivity) ProtoMessage() {}
    method ProtoReflect (line 185) | func (x *ClientConnectivity) ProtoReflect() protoreflect.Message {
    method Descriptor (line 198) | func (*ClientConnectivity) Descriptor() ([]byte, []int) {
    method GetEndpoints (line 202) | func (x *ClientConnectivity) GetEndpoints() []string {
    method GetDerp (line 209) | func (x *ClientConnectivity) GetDerp() string {
    method GetMappingVariesByDestIp (line 216) | func (x *ClientConnectivity) GetMappingVariesByDestIp() bool {
    method GetLatency (line 223) | func (x *ClientConnectivity) GetLatency() map[string]*Latency {
    method GetClientSupports (line 230) | func (x *ClientConnectivity) GetClientSupports() *ClientSupports {
  type GetDeviceRequest (line 237) | type GetDeviceRequest struct
    method Reset (line 244) | func (x *GetDeviceRequest) Reset() {
    method String (line 251) | func (x *GetDeviceRequest) String() string {
    method ProtoMessage (line 255) | func (*GetDeviceRequest) ProtoMessage() {}
    method ProtoReflect (line 257) | func (x *GetDeviceRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 270) | func (*GetDeviceRequest) Descriptor() ([]byte, []int) {
    method GetId (line 274) | func (x *GetDeviceRequest) GetId() string {
  type GetDeviceResponse (line 281) | type GetDeviceResponse struct
    method Reset (line 307) | func (x *GetDeviceResponse) Reset() {
    method String (line 314) | func (x *GetDeviceResponse) String() string {
    method ProtoMessage (line 318) | func (*GetDeviceResponse) ProtoMessage() {}
    method ProtoReflect (line 320) | func (x *GetDeviceResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 333) | func (*GetDeviceResponse) Descriptor() ([]byte, []int) {
    method GetAddresses (line 337) | func (x *GetDeviceResponse) GetAddresses() []string {
    method GetId (line 344) | func (x *GetDeviceResponse) GetId() string {
    method GetUser (line 351) | func (x *GetDeviceResponse) GetUser() string {
    method GetName (line 358) | func (x *GetDeviceResponse) GetName() string {
    method GetHostname (line 365) | func (x *GetDeviceResponse) GetHostname() string {
    method GetClientVersion (line 372) | func (x *GetDeviceResponse) GetClientVersion() string {
    method GetUpdateAvailable (line 379) | func (x *GetDeviceResponse) GetUpdateAvailable() bool {
    method GetOs (line 386) | func (x *GetDeviceResponse) GetOs() string {
    method GetCreated (line 393) | func (x *GetDeviceResponse) GetCreated() *timestamppb.Timestamp {
    method GetLastSeen (line 400) | func (x *GetDeviceResponse) GetLastSeen() *timestamppb.Timestamp {
    method GetKeyExpiryDisabled (line 407) | func (x *GetDeviceResponse) GetKeyExpiryDisabled() bool {
    method GetExpires (line 414) | func (x *GetDeviceResponse) GetExpires() *timestamppb.Timestamp {
    method GetAuthorized (line 421) | func (x *GetDeviceResponse) GetAuthorized() bool {
    method GetIsExternal (line 428) | func (x *GetDeviceResponse) GetIsExternal() bool {
    method GetMachineKey (line 435) | func (x *GetDeviceResponse) GetMachineKey() string {
    method GetNodeKey (line 442) | func (x *GetDeviceResponse) GetNodeKey() string {
    method GetBlocksIncomingConnections (line 449) | func (x *GetDeviceResponse) GetBlocksIncomingConnections() bool {
    method GetEnabledRoutes (line 456) | func (x *GetDeviceResponse) GetEnabledRoutes() []string {
    method GetAdvertisedRoutes (line 463) | func (x *GetDeviceResponse) GetAdvertisedRoutes() []string {
    method GetClientConnectivity (line 470) | func (x *GetDeviceResponse) GetClientConnectivity() *ClientConnectivity {
  type DeleteDeviceRequest (line 477) | type DeleteDeviceRequest struct
    method Reset (line 484) | func (x *DeleteDeviceRequest) Reset() {
    method String (line 491) | func (x *DeleteDeviceRequest) String() string {
    method ProtoMessage (line 495) | func (*DeleteDeviceRequest) ProtoMessage() {}
    method ProtoReflect (line 497) | func (x *DeleteDeviceRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 510) | func (*DeleteDeviceRequest) Descriptor() ([]byte, []int) {
    method GetId (line 514) | func (x *DeleteDeviceRequest) GetId() string {
  type DeleteDeviceResponse (line 521) | type DeleteDeviceResponse struct
    method Reset (line 527) | func (x *DeleteDeviceResponse) Reset() {
    method String (line 534) | func (x *DeleteDeviceResponse) String() string {
    method ProtoMessage (line 538) | func (*DeleteDeviceResponse) ProtoMessage() {}
    method ProtoReflect (line 540) | func (x *DeleteDeviceResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 553) | func (*DeleteDeviceResponse) Descriptor() ([]byte, []int) {
  type GetDeviceRoutesRequest (line 557) | type GetDeviceRoutesRequest struct
    method Reset (line 564) | func (x *GetDeviceRoutesRequest) Reset() {
    method String (line 571) | func (x *GetDeviceRoutesRequest) String() string {
    method ProtoMessage (line 575) | func (*GetDeviceRoutesRequest) ProtoMessage() {}
    method ProtoReflect (line 577) | func (x *GetDeviceRoutesRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 590) | func (*GetDeviceRoutesRequest) Descriptor() ([]byte, []int) {
    method GetId (line 594) | func (x *GetDeviceRoutesRequest) GetId() string {
  type GetDeviceRoutesResponse (line 601) | type GetDeviceRoutesResponse struct
    method Reset (line 609) | func (x *GetDeviceRoutesResponse) Reset() {
    method String (line 616) | func (x *GetDeviceRoutesResponse) String() string {
    method ProtoMessage (line 620) | func (*GetDeviceRoutesResponse) ProtoMessage() {}
    method ProtoReflect (line 622) | func (x *GetDeviceRoutesResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 635) | func (*GetDeviceRoutesResponse) Descriptor() ([]byte, []int) {
    method GetEnabledRoutes (line 639) | func (x *GetDeviceRoutesResponse) GetEnabledRoutes() []string {
    method GetAdvertisedRoutes (line 646) | func (x *GetDeviceRoutesResponse) GetAdvertisedRoutes() []string {
  type EnableDeviceRoutesRequest (line 653) | type EnableDeviceRoutesRequest struct
    method Reset (line 661) | func (x *EnableDeviceRoutesRequest) Reset() {
    method String (line 668) | func (x *EnableDeviceRoutesRequest) String() string {
    method ProtoMessage (line 672) | func (*EnableDeviceRoutesRequest) ProtoMessage() {}
    method ProtoReflect (line 674) | func (x *EnableDeviceRoutesRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 687) | func (*EnableDeviceRoutesRequest) Descriptor() ([]byte, []int) {
    method GetId (line 691) | func (x *EnableDeviceRoutesRequest) GetId() string {
    method GetRoutes (line 698) | func (x *EnableDeviceRoutesRequest) GetRoutes() []string {
  type EnableDeviceRoutesResponse (line 705) | type EnableDeviceRoutesResponse struct
    method Reset (line 713) | func (x *EnableDeviceRoutesResponse) Reset() {
    method String (line 720) | func (x *EnableDeviceRoutesResponse) String() string {
    method ProtoMessage (line 724) | func (*EnableDeviceRoutesResponse) ProtoMessage() {}
    method ProtoReflect (line 726) | func (x *EnableDeviceRoutesResponse) ProtoReflect() protoreflect.Messa...
    method Descriptor (line 739) | func (*EnableDeviceRoutesResponse) Descriptor() ([]byte, []int) {
    method GetEnabledRoutes (line 743) | func (x *EnableDeviceRoutesResponse) GetEnabledRoutes() []string {
    method GetAdvertisedRoutes (line 750) | func (x *EnableDeviceRoutesResponse) GetAdvertisedRoutes() []string {
  constant file_headscale_v1_device_proto_rawDesc (line 759) | file_headscale_v1_device_proto_rawDesc = "" +
  function file_headscale_v1_device_proto_rawDescGZIP (line 830) | func file_headscale_v1_device_proto_rawDescGZIP() []byte {
  function init (line 868) | func init() { file_headscale_v1_device_proto_init() }
  function file_headscale_v1_device_proto_init (line 869) | func file_headscale_v1_device_proto_init() {

FILE: gen/go/headscale/v1/headscale.pb.go
  constant _ (line 20) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 22) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type HealthRequest (line 25) | type HealthRequest struct
    method Reset (line 31) | func (x *HealthRequest) Reset() {
    method String (line 38) | func (x *HealthRequest) String() string {
    method ProtoMessage (line 42) | func (*HealthRequest) ProtoMessage() {}
    method ProtoReflect (line 44) | func (x *HealthRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 57) | func (*HealthRequest) Descriptor() ([]byte, []int) {
  type HealthResponse (line 61) | type HealthResponse struct
    method Reset (line 68) | func (x *HealthResponse) Reset() {
    method String (line 75) | func (x *HealthResponse) String() string {
    method ProtoMessage (line 79) | func (*HealthResponse) ProtoMessage() {}
    method ProtoReflect (line 81) | func (x *HealthResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 94) | func (*HealthResponse) Descriptor() ([]byte, []int) {
    method GetDatabaseConnectivity (line 98) | func (x *HealthResponse) GetDatabaseConnectivity() bool {
  constant file_headscale_v1_headscale_proto_rawDesc (line 107) | file_headscale_v1_headscale_proto_rawDesc = "" +
  function file_headscale_v1_headscale_proto_rawDescGZIP (line 155) | func file_headscale_v1_headscale_proto_rawDescGZIP() []byte {
  function init (line 285) | func init() { file_headscale_v1_headscale_proto_init() }
  function file_headscale_v1_headscale_proto_init (line 286) | func file_headscale_v1_headscale_proto_init() {

FILE: gen/go/headscale/v1/headscale.pb.gw.go
  function request_HeadscaleService_CreateUser_0 (line 38) | func request_HeadscaleService_CreateUser_0(ctx context.Context, marshale...
  function local_request_HeadscaleService_CreateUser_0 (line 53) | func local_request_HeadscaleService_CreateUser_0(ctx context.Context, ma...
  function request_HeadscaleService_RenameUser_0 (line 65) | func request_HeadscaleService_RenameUser_0(ctx context.Context, marshale...
  function local_request_HeadscaleService_RenameUser_0 (line 94) | func local_request_HeadscaleService_RenameUser_0(ctx context.Context, ma...
  function request_HeadscaleService_DeleteUser_0 (line 120) | func request_HeadscaleService_DeleteUser_0(ctx context.Context, marshale...
  function local_request_HeadscaleService_DeleteUser_0 (line 141) | func local_request_HeadscaleService_DeleteUser_0(ctx context.Context, ma...
  function request_HeadscaleService_ListUsers_0 (line 161) | func request_HeadscaleService_ListUsers_0(ctx context.Context, marshaler...
  function local_request_HeadscaleService_ListUsers_0 (line 179) | func local_request_HeadscaleService_ListUsers_0(ctx context.Context, mar...
  function request_HeadscaleService_CreatePreAuthKey_0 (line 194) | func request_HeadscaleService_CreatePreAuthKey_0(ctx context.Context, ma...
  function local_request_HeadscaleService_CreatePreAuthKey_0 (line 209) | func local_request_HeadscaleService_CreatePreAuthKey_0(ctx context.Conte...
  function request_HeadscaleService_ExpirePreAuthKey_0 (line 221) | func request_HeadscaleService_ExpirePreAuthKey_0(ctx context.Context, ma...
  function local_request_HeadscaleService_ExpirePreAuthKey_0 (line 236) | func local_request_HeadscaleService_ExpirePreAuthKey_0(ctx context.Conte...
  function request_HeadscaleService_DeletePreAuthKey_0 (line 250) | func request_HeadscaleService_DeletePreAuthKey_0(ctx context.Context, ma...
  function local_request_HeadscaleService_DeletePreAuthKey_0 (line 268) | func local_request_HeadscaleService_DeletePreAuthKey_0(ctx context.Conte...
  function request_HeadscaleService_ListPreAuthKeys_0 (line 283) | func request_HeadscaleService_ListPreAuthKeys_0(ctx context.Context, mar...
  function local_request_HeadscaleService_ListPreAuthKeys_0 (line 295) | func local_request_HeadscaleService_ListPreAuthKeys_0(ctx context.Contex...
  function request_HeadscaleService_DebugCreateNode_0 (line 304) | func request_HeadscaleService_DebugCreateNode_0(ctx context.Context, mar...
  function local_request_HeadscaleService_DebugCreateNode_0 (line 319) | func local_request_HeadscaleService_DebugCreateNode_0(ctx context.Contex...
  function request_HeadscaleService_GetNode_0 (line 331) | func request_HeadscaleService_GetNode_0(ctx context.Context, marshaler r...
  function local_request_HeadscaleService_GetNode_0 (line 352) | func local_request_HeadscaleService_GetNode_0(ctx context.Context, marsh...
  function request_HeadscaleService_SetTags_0 (line 370) | func request_HeadscaleService_SetTags_0(ctx context.Context, marshaler r...
  function local_request_HeadscaleService_SetTags_0 (line 394) | func local_request_HeadscaleService_SetTags_0(ctx context.Context, marsh...
  function request_HeadscaleService_SetApprovedRoutes_0 (line 415) | func request_HeadscaleService_SetApprovedRoutes_0(ctx context.Context, m...
  function local_request_HeadscaleService_SetApprovedRoutes_0 (line 439) | func local_request_HeadscaleService_SetApprovedRoutes_0(ctx context.Cont...
  function request_HeadscaleService_RegisterNode_0 (line 462) | func request_HeadscaleService_RegisterNode_0(ctx context.Context, marsha...
  function local_request_HeadscaleService_RegisterNode_0 (line 480) | func local_request_HeadscaleService_RegisterNode_0(ctx context.Context, ...
  function request_HeadscaleService_DeleteNode_0 (line 495) | func request_HeadscaleService_DeleteNode_0(ctx context.Context, marshale...
  function local_request_HeadscaleService_DeleteNode_0 (line 516) | func local_request_HeadscaleService_DeleteNode_0(ctx context.Context, ma...
  function request_HeadscaleService_ExpireNode_0 (line 536) | func request_HeadscaleService_ExpireNode_0(ctx context.Context, marshale...
  function local_request_HeadscaleService_ExpireNode_0 (line 563) | func local_request_HeadscaleService_ExpireNode_0(ctx context.Context, ma...
  function request_HeadscaleService_RenameNode_0 (line 587) | func request_HeadscaleService_RenameNode_0(ctx context.Context, marshale...
  function local_request_HeadscaleService_RenameNode_0 (line 616) | func local_request_HeadscaleService_RenameNode_0(ctx context.Context, ma...
  function request_HeadscaleService_ListNodes_0 (line 644) | func request_HeadscaleService_ListNodes_0(ctx context.Context, marshaler...
  function local_request_HeadscaleService_ListNodes_0 (line 662) | func local_request_HeadscaleService_ListNodes_0(ctx context.Context, mar...
  function request_HeadscaleService_BackfillNodeIPs_0 (line 679) | func request_HeadscaleService_BackfillNodeIPs_0(ctx context.Context, mar...
  function local_request_HeadscaleService_BackfillNodeIPs_0 (line 697) | func local_request_HeadscaleService_BackfillNodeIPs_0(ctx context.Contex...
  function request_HeadscaleService_AuthRegister_0 (line 712) | func request_HeadscaleService_AuthRegister_0(ctx context.Context, marsha...
  function local_request_HeadscaleService_AuthRegister_0 (line 727) | func local_request_HeadscaleService_AuthRegister_0(ctx context.Context, ...
  function request_HeadscaleService_AuthApprove_0 (line 739) | func request_HeadscaleService_AuthApprove_0(ctx context.Context, marshal...
  function local_request_HeadscaleService_AuthApprove_0 (line 754) | func local_request_HeadscaleService_AuthApprove_0(ctx context.Context, m...
  function request_HeadscaleService_AuthReject_0 (line 766) | func request_HeadscaleService_AuthReject_0(ctx context.Context, marshale...
  function local_request_HeadscaleService_AuthReject_0 (line 781) | func local_request_HeadscaleService_AuthReject_0(ctx context.Context, ma...
  function request_HeadscaleService_CreateApiKey_0 (line 793) | func request_HeadscaleService_CreateApiKey_0(ctx context.Context, marsha...
  function local_request_HeadscaleService_CreateApiKey_0 (line 808) | func local_request_HeadscaleService_CreateApiKey_0(ctx context.Context, ...
  function request_HeadscaleService_ExpireApiKey_0 (line 820) | func request_HeadscaleService_ExpireApiKey_0(ctx context.Context, marsha...
  function local_request_HeadscaleService_ExpireApiKey_0 (line 835) | func local_request_HeadscaleService_ExpireApiKey_0(ctx context.Context, ...
  function request_HeadscaleService_ListApiKeys_0 (line 847) | func request_HeadscaleService_ListApiKeys_0(ctx context.Context, marshal...
  function local_request_HeadscaleService_ListApiKeys_0 (line 859) | func local_request_HeadscaleService_ListApiKeys_0(ctx context.Context, m...
  function request_HeadscaleService_DeleteApiKey_0 (line 870) | func request_HeadscaleService_DeleteApiKey_0(ctx context.Context, marsha...
  function local_request_HeadscaleService_DeleteApiKey_0 (line 897) | func local_request_HeadscaleService_DeleteApiKey_0(ctx context.Context, ...
  function request_HeadscaleService_GetPolicy_0 (line 921) | func request_HeadscaleService_GetPolicy_0(ctx context.Context, marshaler...
  function local_request_HeadscaleService_GetPolicy_0 (line 933) | func local_request_HeadscaleService_GetPolicy_0(ctx context.Context, mar...
  function request_HeadscaleService_SetPolicy_0 (line 942) | func request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler...
  function local_request_HeadscaleService_SetPolicy_0 (line 957) | func local_request_HeadscaleService_SetPolicy_0(ctx context.Context, mar...
  function request_HeadscaleService_Health_0 (line 969) | func request_HeadscaleService_Health_0(ctx context.Context, marshaler ru...
  function local_request_HeadscaleService_Health_0 (line 981) | func local_request_HeadscaleService_Health_0(ctx context.Context, marsha...
  function RegisterHeadscaleServiceHandlerServer (line 995) | func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *run...
  function RegisterHeadscaleServiceHandlerFromEndpoint (line 1562) | func RegisterHeadscaleServiceHandlerFromEndpoint(ctx context.Context, mu...
  function RegisterHeadscaleServiceHandler (line 1586) | func RegisterHeadscaleServiceHandler(ctx context.Context, mux *runtime.S...
  function RegisterHeadscaleServiceHandlerClient (line 1595) | func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *run...

FILE: gen/go/headscale/v1/headscale_grpc.pb.go
  constant _ (line 19) | _ = grpc.SupportPackageIsVersion9
  constant HeadscaleService_CreateUser_FullMethodName (line 22) | HeadscaleService_CreateUser_FullMethodName        = "/headscale.v1.Heads...
  constant HeadscaleService_RenameUser_FullMethodName (line 23) | HeadscaleService_RenameUser_FullMethodName        = "/headscale.v1.Heads...
  constant HeadscaleService_DeleteUser_FullMethodName (line 24) | HeadscaleService_DeleteUser_FullMethodName        = "/headscale.v1.Heads...
  constant HeadscaleService_ListUsers_FullMethodName (line 25) | HeadscaleService_ListUsers_FullMethodName         = "/headscale.v1.Heads...
  constant HeadscaleService_CreatePreAuthKey_FullMethodName (line 26) | HeadscaleService_CreatePreAuthKey_FullMethodName  = "/headscale.v1.Heads...
  constant HeadscaleService_ExpirePreAuthKey_FullMethodName (line 27) | HeadscaleService_ExpirePreAuthKey_FullMethodName  = "/headscale.v1.Heads...
  constant HeadscaleService_DeletePreAuthKey_FullMethodName (line 28) | HeadscaleService_DeletePreAuthKey_FullMethodName  = "/headscale.v1.Heads...
  constant HeadscaleService_ListPreAuthKeys_FullMethodName (line 29) | HeadscaleService_ListPreAuthKeys_FullMethodName   = "/headscale.v1.Heads...
  constant HeadscaleService_DebugCreateNode_FullMethodName (line 30) | HeadscaleService_DebugCreateNode_FullMethodName   = "/headscale.v1.Heads...
  constant HeadscaleService_GetNode_FullMethodName (line 31) | HeadscaleService_GetNode_FullMethodName           = "/headscale.v1.Heads...
  constant HeadscaleService_SetTags_FullMethodName (line 32) | HeadscaleService_SetTags_FullMethodName           = "/headscale.v1.Heads...
  constant HeadscaleService_SetApprovedRoutes_FullMethodName (line 33) | HeadscaleService_SetApprovedRoutes_FullMethodName = "/headscale.v1.Heads...
  constant HeadscaleService_RegisterNode_FullMethodName (line 34) | HeadscaleService_RegisterNode_FullMethodName      = "/headscale.v1.Heads...
  constant HeadscaleService_DeleteNode_FullMethodName (line 35) | HeadscaleService_DeleteNode_FullMethodName        = "/headscale.v1.Heads...
  constant HeadscaleService_ExpireNode_FullMethodName (line 36) | HeadscaleService_ExpireNode_FullMethodName        = "/headscale.v1.Heads...
  constant HeadscaleService_RenameNode_FullMethodName (line 37) | HeadscaleService_RenameNode_FullMethodName        = "/headscale.v1.Heads...
  constant HeadscaleService_ListNodes_FullMethodName (line 38) | HeadscaleService_ListNodes_FullMethodName         = "/headscale.v1.Heads...
  constant HeadscaleService_BackfillNodeIPs_FullMethodName (line 39) | HeadscaleService_BackfillNodeIPs_FullMethodName   = "/headscale.v1.Heads...
  constant HeadscaleService_AuthRegister_FullMethodName (line 40) | HeadscaleService_AuthRegister_FullMethodName      = "/headscale.v1.Heads...
  constant HeadscaleService_AuthApprove_FullMethodName (line 41) | HeadscaleService_AuthApprove_FullMethodName       = "/headscale.v1.Heads...
  constant HeadscaleService_AuthReject_FullMethodName (line 42) | HeadscaleService_AuthReject_FullMethodName        = "/headscale.v1.Heads...
  constant HeadscaleService_CreateApiKey_FullMethodName (line 43) | HeadscaleService_CreateApiKey_FullMethodName      = "/headscale.v1.Heads...
  constant HeadscaleService_ExpireApiKey_FullMethodName (line 44) | HeadscaleService_ExpireApiKey_FullMethodName      = "/headscale.v1.Heads...
  constant HeadscaleService_ListApiKeys_FullMethodName (line 45) | HeadscaleService_ListApiKeys_FullMethodName       = "/headscale.v1.Heads...
  constant HeadscaleService_DeleteApiKey_FullMethodName (line 46) | HeadscaleService_DeleteApiKey_FullMethodName      = "/headscale.v1.Heads...
  constant HeadscaleService_GetPolicy_FullMethodName (line 47) | HeadscaleService_GetPolicy_FullMethodName         = "/headscale.v1.Heads...
  constant HeadscaleService_SetPolicy_FullMethodName (line 48) | HeadscaleService_SetPolicy_FullMethodName         = "/headscale.v1.Heads...
  constant HeadscaleService_Health_FullMethodName (line 49) | HeadscaleService_Health_FullMethodName            = "/headscale.v1.Heads...
  type HeadscaleServiceClient (line 55) | type HeadscaleServiceClient interface
  type headscaleServiceClient (line 93) | type headscaleServiceClient struct
    method CreateUser (line 101) | func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *C...
    method RenameUser (line 111) | func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *R...
    method DeleteUser (line 121) | func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *D...
    method ListUsers (line 131) | func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *Li...
    method CreatePreAuthKey (line 141) | func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context,...
    method ExpirePreAuthKey (line 151) | func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context,...
    method DeletePreAuthKey (line 161) | func (c *headscaleServiceClient) DeletePreAuthKey(ctx context.Context,...
    method ListPreAuthKeys (line 171) | func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, ...
    method DebugCreateNode (line 181) | func (c *headscaleServiceClient) DebugCreateNode(ctx context.Context, ...
    method GetNode (line 191) | func (c *headscaleServiceClient) GetNode(ctx context.Context, in *GetN...
    method SetTags (line 201) | func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetT...
    method SetApprovedRoutes (line 211) | func (c *headscaleServiceClient) SetApprovedRoutes(ctx context.Context...
    method RegisterNode (line 221) | func (c *headscaleServiceClient) RegisterNode(ctx context.Context, in ...
    method DeleteNode (line 231) | func (c *headscaleServiceClient) DeleteNode(ctx context.Context, in *D...
    method ExpireNode (line 241) | func (c *headscaleServiceClient) ExpireNode(ctx context.Context, in *E...
    method RenameNode (line 251) | func (c *headscaleServiceClient) RenameNode(ctx context.Context, in *R...
    method ListNodes (line 261) | func (c *headscaleServiceClient) ListNodes(ctx context.Context, in *Li...
    method BackfillNodeIPs (line 271) | func (c *headscaleServiceClient) BackfillNodeIPs(ctx context.Context, ...
    method AuthRegister (line 281) | func (c *headscaleServiceClient) AuthRegister(ctx context.Context, in ...
    method AuthApprove (line 291) | func (c *headscaleServiceClient) AuthApprove(ctx context.Context, in *...
    method AuthReject (line 301) | func (c *headscaleServiceClient) AuthReject(ctx context.Context, in *A...
    method CreateApiKey (line 311) | func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in ...
    method ExpireApiKey (line 321) | func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in ...
    method ListApiKeys (line 331) | func (c *headscaleServiceClient) ListApiKeys(ctx context.Context, in *...
    method DeleteApiKey (line 341) | func (c *headscaleServiceClient) DeleteApiKey(ctx context.Context, in ...
    method GetPolicy (line 351) | func (c *headscaleServiceClient) GetPolicy(ctx context.Context, in *Ge...
    method SetPolicy (line 361) | func (c *headscaleServiceClient) SetPolicy(ctx context.Context, in *Se...
    method Health (line 371) | func (c *headscaleServiceClient) Health(ctx context.Context, in *Healt...
  function NewHeadscaleServiceClient (line 97) | func NewHeadscaleServiceClient(cc grpc.ClientConnInterface) HeadscaleSer...
  type HeadscaleServiceServer (line 384) | type HeadscaleServiceServer interface
  type UnimplementedHeadscaleServiceServer (line 428) | type UnimplementedHeadscaleServiceServer struct
    method CreateUser (line 430) | func (UnimplementedHeadscaleServiceServer) CreateUser(context.Context,...
    method RenameUser (line 433) | func (UnimplementedHeadscaleServiceServer) RenameUser(context.Context,...
    method DeleteUser (line 436) | func (UnimplementedHeadscaleServiceServer) DeleteUser(context.Context,...
    method ListUsers (line 439) | func (UnimplementedHeadscaleServiceServer) ListUsers(context.Context, ...
    method CreatePreAuthKey (line 442) | func (UnimplementedHeadscaleServiceServer) CreatePreAuthKey(context.Co...
    method ExpirePreAuthKey (line 445) | func (UnimplementedHeadscaleServiceServer) ExpirePreAuthKey(context.Co...
    method DeletePreAuthKey (line 448) | func (UnimplementedHeadscaleServiceServer) DeletePreAuthKey(context.Co...
    method ListPreAuthKeys (line 451) | func (UnimplementedHeadscaleServiceServer) ListPreAuthKeys(context.Con...
    method DebugCreateNode (line 454) | func (UnimplementedHeadscaleServiceServer) DebugCreateNode(context.Con...
    method GetNode (line 457) | func (UnimplementedHeadscaleServiceServer) GetNode(context.Context, *G...
    method SetTags (line 460) | func (UnimplementedHeadscaleServiceServer) SetTags(context.Context, *S...
    method SetApprovedRoutes (line 463) | func (UnimplementedHeadscaleServiceServer) SetApprovedRoutes(context.C...
    method RegisterNode (line 466) | func (UnimplementedHeadscaleServiceServer) RegisterNode(context.Contex...
    method DeleteNode (line 469) | func (UnimplementedHeadscaleServiceServer) DeleteNode(context.Context,...
    method ExpireNode (line 472) | func (UnimplementedHeadscaleServiceServer) ExpireNode(context.Context,...
    method RenameNode (line 475) | func (UnimplementedHeadscaleServiceServer) RenameNode(context.Context,...
    method ListNodes (line 478) | func (UnimplementedHeadscaleServiceServer) ListNodes(context.Context, ...
    method BackfillNodeIPs (line 481) | func (UnimplementedHeadscaleServiceServer) BackfillNodeIPs(context.Con...
    method AuthRegister (line 484) | func (UnimplementedHeadscaleServiceServer) AuthRegister(context.Contex...
    method AuthApprove (line 487) | func (UnimplementedHeadscaleServiceServer) AuthApprove(context.Context...
    method AuthReject (line 490) | func (UnimplementedHeadscaleServiceServer) AuthReject(context.Context,...
    method CreateApiKey (line 493) | func (UnimplementedHeadscaleServiceServer) CreateApiKey(context.Contex...
    method ExpireApiKey (line 496) | func (UnimplementedHeadscaleServiceServer) ExpireApiKey(context.Contex...
    method ListApiKeys (line 499) | func (UnimplementedHeadscaleServiceServer) ListApiKeys(context.Context...
    method DeleteApiKey (line 502) | func (UnimplementedHeadscaleServiceServer) DeleteApiKey(context.Contex...
    method GetPolicy (line 505) | func (UnimplementedHeadscaleServiceServer) GetPolicy(context.Context, ...
    method SetPolicy (line 508) | func (UnimplementedHeadscaleServiceServer) SetPolicy(context.Context, ...
    method Health (line 511) | func (UnimplementedHeadscaleServiceServer) Health(context.Context, *He...
    method mustEmbedUnimplementedHeadscaleServiceServer (line 514) | func (UnimplementedHeadscaleServiceServer) mustEmbedUnimplementedHeads...
    method testEmbeddedByValue (line 515) | func (UnimplementedHeadscaleServiceServer) testEmbeddedByValue()      ...
  type UnsafeHeadscaleServiceServer (line 520) | type UnsafeHeadscaleServiceServer interface
  function RegisterHeadscaleServiceServer (line 524) | func RegisterHeadscaleServiceServer(s grpc.ServiceRegistrar, srv Headsca...
  function _HeadscaleService_CreateUser_Handler (line 535) | func _HeadscaleService_CreateUser_Handler(srv interface{}, ctx context.C...
  function _HeadscaleService_RenameUser_Handler (line 553) | func _HeadscaleService_RenameUser_Handler(srv interface{}, ctx context.C...
  function _HeadscaleService_DeleteUser_Handler (line 571) | func _HeadscaleService_DeleteUser_Handler(srv interface{}, ctx context.C...
  function _HeadscaleService_ListUsers_Handler (line 589) | func _HeadscaleService_ListUsers_Handler(srv interface{}, ctx context.Co...
  function _HeadscaleService_CreatePreAuthKey_Handler (line 607) | func _HeadscaleService_CreatePreAuthKey_Handler(srv interface{}, ctx con...
  function _HeadscaleService_ExpirePreAuthKey_Handler (line 625) | func _HeadscaleService_ExpirePreAuthKey_Handler(srv interface{}, ctx con...
  function _HeadscaleService_DeletePreAuthKey_Handler (line 643) | func _HeadscaleService_DeletePreAuthKey_Handler(srv interface{}, ctx con...
  function _HeadscaleService_ListPreAuthKeys_Handler (line 661) | func _HeadscaleService_ListPreAuthKeys_Handler(srv interface{}, ctx cont...
  function _HeadscaleService_DebugCreateNode_Handler (line 679) | func _HeadscaleService_DebugCreateNode_Handler(srv interface{}, ctx cont...
  function _HeadscaleService_GetNode_Handler (line 697) | func _HeadscaleService_GetNode_Handler(srv interface{}, ctx context.Cont...
  function _HeadscaleService_SetTags_Handler (line 715) | func _HeadscaleService_SetTags_Handler(srv interface{}, ctx context.Cont...
  function _HeadscaleService_SetApprovedRoutes_Handler (line 733) | func _HeadscaleService_SetApprovedRoutes_Handler(srv interface{}, ctx co...
  function _HeadscaleService_RegisterNode_Handler (line 751) | func _HeadscaleService_RegisterNode_Handler(srv interface{}, ctx context...
  function _HeadscaleService_DeleteNode_Handler (line 769) | func _HeadscaleService_DeleteNode_Handler(srv interface{}, ctx context.C...
  function _HeadscaleService_ExpireNode_Handler (line 787) | func _HeadscaleService_ExpireNode_Handler(srv interface{}, ctx context.C...
  function _HeadscaleService_RenameNode_Handler (line 805) | func _HeadscaleService_RenameNode_Handler(srv interface{}, ctx context.C...
  function _HeadscaleService_ListNodes_Handler (line 823) | func _HeadscaleService_ListNodes_Handler(srv interface{}, ctx context.Co...
  function _HeadscaleService_BackfillNodeIPs_Handler (line 841) | func _HeadscaleService_BackfillNodeIPs_Handler(srv interface{}, ctx cont...
  function _HeadscaleService_AuthRegister_Handler (line 859) | func _HeadscaleService_AuthRegister_Handler(srv interface{}, ctx context...
  function _HeadscaleService_AuthApprove_Handler (line 877) | func _HeadscaleService_AuthApprove_Handler(srv interface{}, ctx context....
  function _HeadscaleService_AuthReject_Handler (line 895) | func _HeadscaleService_AuthReject_Handler(srv interface{}, ctx context.C...
  function _HeadscaleService_CreateApiKey_Handler (line 913) | func _HeadscaleService_CreateApiKey_Handler(srv interface{}, ctx context...
  function _HeadscaleService_ExpireApiKey_Handler (line 931) | func _HeadscaleService_ExpireApiKey_Handler(srv interface{}, ctx context...
  function _HeadscaleService_ListApiKeys_Handler (line 949) | func _HeadscaleService_ListApiKeys_Handler(srv interface{}, ctx context....
  function _HeadscaleService_DeleteApiKey_Handler (line 967) | func _HeadscaleService_DeleteApiKey_Handler(srv interface{}, ctx context...
  function _HeadscaleService_GetPolicy_Handler (line 985) | func _HeadscaleService_GetPolicy_Handler(srv interface{}, ctx context.Co...
  function _HeadscaleService_SetPolicy_Handler (line 1003) | func _HeadscaleService_SetPolicy_Handler(srv interface{}, ctx context.Co...
  function _HeadscaleService_Health_Handler (line 1021) | func _HeadscaleService_Health_Handler(srv interface{}, ctx context.Conte...

FILE: gen/go/headscale/v1/node.pb.go
  constant _ (line 20) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 22) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type RegisterMethod (line 25) | type RegisterMethod
    method Enum (line 50) | func (x RegisterMethod) Enum() *RegisterMethod {
    method String (line 56) | func (x RegisterMethod) String() string {
    method Descriptor (line 60) | func (RegisterMethod) Descriptor() protoreflect.EnumDescriptor {
    method Type (line 64) | func (RegisterMethod) Type() protoreflect.EnumType {
    method Number (line 68) | func (x RegisterMethod) Number() protoreflect.EnumNumber {
    method EnumDescriptor (line 73) | func (RegisterMethod) EnumDescriptor() ([]byte, []int) {
  constant RegisterMethod_REGISTER_METHOD_UNSPECIFIED (line 28) | RegisterMethod_REGISTER_METHOD_UNSPECIFIED RegisterMethod = 0
  constant RegisterMethod_REGISTER_METHOD_AUTH_KEY (line 29) | RegisterMethod_REGISTER_METHOD_AUTH_KEY    RegisterMethod = 1
  constant RegisterMethod_REGISTER_METHOD_CLI (line 30) | RegisterMethod_REGISTER_METHOD_CLI         RegisterMethod = 2
  constant RegisterMethod_REGISTER_METHOD_OIDC (line 31) | RegisterMethod_REGISTER_METHOD_OIDC        RegisterMethod = 3
  type Node (line 77) | type Node struct
    method Reset (line 105) | func (x *Node) Reset() {
    method String (line 112) | func (x *Node) String() string {
    method ProtoMessage (line 116) | func (*Node) ProtoMessage() {}
    method ProtoReflect (line 118) | func (x *Node) ProtoReflect() protoreflect.Message {
    method Descriptor (line 131) | func (*Node) Descriptor() ([]byte, []int) {
    method GetId (line 135) | func (x *Node) GetId() uint64 {
    method GetMachineKey (line 142) | func (x *Node) GetMachineKey() string {
    method GetNodeKey (line 149) | func (x *Node) GetNodeKey() string {
    method GetDiscoKey (line 156) | func (x *Node) GetDiscoKey() string {
    method GetIpAddresses (line 163) | func (x *Node) GetIpAddresses() []string {
    method GetName (line 170) | func (x *Node) GetName() string {
    method GetUser (line 177) | func (x *Node) GetUser() *User {
    method GetLastSeen (line 184) | func (x *Node) GetLastSeen() *timestamppb.Timestamp {
    method GetExpiry (line 191) | func (x *Node) GetExpiry() *timestamppb.Timestamp {
    method GetPreAuthKey (line 198) | func (x *Node) GetPreAuthKey() *PreAuthKey {
    method GetCreatedAt (line 205) | func (x *Node) GetCreatedAt() *timestamppb.Timestamp {
    method GetRegisterMethod (line 212) | func (x *Node) GetRegisterMethod() RegisterMethod {
    method GetGivenName (line 219) | func (x *Node) GetGivenName() string {
    method GetOnline (line 226) | func (x *Node) GetOnline() bool {
    method GetApprovedRoutes (line 233) | func (x *Node) GetApprovedRoutes() []string {
    method GetAvailableRoutes (line 240) | func (x *Node) GetAvailableRoutes() []string {
    method GetSubnetRoutes (line 247) | func (x *Node) GetSubnetRoutes() []string {
    method GetTags (line 254) | func (x *Node) GetTags() []string {
  type RegisterNodeRequest (line 261) | type RegisterNodeRequest struct
    method Reset (line 269) | func (x *RegisterNodeRequest) Reset() {
    method String (line 276) | func (x *RegisterNodeRequest) String() string {
    method ProtoMessage (line 280) | func (*RegisterNodeRequest) ProtoMessage() {}
    method ProtoReflect (line 282) | func (x *RegisterNodeRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 295) | func (*RegisterNodeRequest) Descriptor() ([]byte, []int) {
    method GetUser (line 299) | func (x *RegisterNodeRequest) GetUser() string {
    method GetKey (line 306) | func (x *RegisterNodeRequest) GetKey() string {
  type RegisterNodeResponse (line 313) | type RegisterNodeResponse struct
    method Reset (line 320) | func (x *RegisterNodeResponse) Reset() {
    method String (line 327) | func (x *RegisterNodeResponse) String() string {
    method ProtoMessage (line 331) | func (*RegisterNodeResponse) ProtoMessage() {}
    method ProtoReflect (line 333) | func (x *RegisterNodeResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 346) | func (*RegisterNodeResponse) Descriptor() ([]byte, []int) {
    method GetNode (line 350) | func (x *RegisterNodeResponse) GetNode() *Node {
  type GetNodeRequest (line 357) | type GetNodeRequest struct
    method Reset (line 364) | func (x *GetNodeRequest) Reset() {
    method String (line 371) | func (x *GetNodeRequest) String() string {
    method ProtoMessage (line 375) | func (*GetNodeRequest) ProtoMessage() {}
    method ProtoReflect (line 377) | func (x *GetNodeRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 390) | func (*GetNodeRequest) Descriptor() ([]byte, []int) {
    method GetNodeId (line 394) | func (x *GetNodeRequest) GetNodeId() uint64 {
  type GetNodeResponse (line 401) | type GetNodeResponse struct
    method Reset (line 408) | func (x *GetNodeResponse) Reset() {
    method String (line 415) | func (x *GetNodeResponse) String() string {
    method ProtoMessage (line 419) | func (*GetNodeResponse) ProtoMessage() {}
    method ProtoReflect (line 421) | func (x *GetNodeResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 434) | func (*GetNodeResponse) Descriptor() ([]byte, []int) {
    method GetNode (line 438) | func (x *GetNodeResponse) GetNode() *Node {
  type SetTagsRequest (line 445) | type SetTagsRequest struct
    method Reset (line 453) | func (x *SetTagsRequest) Reset() {
    method String (line 460) | func (x *SetTagsRequest) String() string {
    method ProtoMessage (line 464) | func (*SetTagsRequest) ProtoMessage() {}
    method ProtoReflect (line 466) | func (x *SetTagsRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 479) | func (*SetTagsRequest) Descriptor() ([]byte, []int) {
    method GetNodeId (line 483) | func (x *SetTagsRequest) GetNodeId() uint64 {
    method GetTags (line 490) | func (x *SetTagsRequest) GetTags() []string {
  type SetTagsResponse (line 497) | type SetTagsResponse struct
    method Reset (line 504) | func (x *SetTagsResponse) Reset() {
    method String (line 511) | func (x *SetTagsResponse) String() string {
    method ProtoMessage (line 515) | func (*SetTagsResponse) ProtoMessage() {}
    method ProtoReflect (line 517) | func (x *SetTagsResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 530) | func (*SetTagsResponse) Descriptor() ([]byte, []int) {
    method GetNode (line 534) | func (x *SetTagsResponse) GetNode() *Node {
  type SetApprovedRoutesRequest (line 541) | type SetApprovedRoutesRequest struct
    method Reset (line 549) | func (x *SetApprovedRoutesRequest) Reset() {
    method String (line 556) | func (x *SetApprovedRoutesRequest) String() string {
    method ProtoMessage (line 560) | func (*SetApprovedRoutesRequest) ProtoMessage() {}
    method ProtoReflect (line 562) | func (x *SetApprovedRoutesRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 575) | func (*SetApprovedRoutesRequest) Descriptor() ([]byte, []int) {
    method GetNodeId (line 579) | func (x *SetApprovedRoutesRequest) GetNodeId() uint64 {
    method GetRoutes (line 586) | func (x *SetApprovedRoutesRequest) GetRoutes() []string {
  type SetApprovedRoutesResponse (line 593) | type SetApprovedRoutesResponse struct
    method Reset (line 600) | func (x *SetApprovedRoutesResponse) Reset() {
    method String (line 607) | func (x *SetApprovedRoutesResponse) String() string {
    method ProtoMessage (line 611) | func (*SetApprovedRoutesResponse) ProtoMessage() {}
    method ProtoReflect (line 613) | func (x *SetApprovedRoutesResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 626) | func (*SetApprovedRoutesResponse) Descriptor() ([]byte, []int) {
    method GetNode (line 630) | func (x *SetApprovedRoutesResponse) GetNode() *Node {
  type DeleteNodeRequest (line 637) | type DeleteNodeRequest struct
    method Reset (line 644) | func (x *DeleteNodeRequest) Reset() {
    method String (line 651) | func (x *DeleteNodeRequest) String() string {
    method ProtoMessage (line 655) | func (*DeleteNodeRequest) ProtoMessage() {}
    method ProtoReflect (line 657) | func (x *DeleteNodeRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 670) | func (*DeleteNodeRequest) Descriptor() ([]byte, []int) {
    method GetNodeId (line 674) | func (x *DeleteNodeRequest) GetNodeId() uint64 {
  type DeleteNodeResponse (line 681) | type DeleteNodeResponse struct
    method Reset (line 687) | func (x *DeleteNodeResponse) Reset() {
    method String (line 694) | func (x *DeleteNodeResponse) String() string {
    method ProtoMessage (line 698) | func (*DeleteNodeResponse) ProtoMessage() {}
    method ProtoReflect (line 700) | func (x *DeleteNodeResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 713) | func (*DeleteNodeResponse) Descriptor() ([]byte, []int) {
  type ExpireNodeRequest (line 717) | type ExpireNodeRequest struct
    method Reset (line 727) | func (x *ExpireNodeRequest) Reset() {
    method String (line 734) | func (x *ExpireNodeRequest) String() string {
    method ProtoMessage (line 738) | func (*ExpireNodeRequest) ProtoMessage() {}
    method ProtoReflect (line 740) | func (x *ExpireNodeRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 753) | func (*ExpireNodeRequest) Descriptor() ([]byte, []int) {
    method GetNodeId (line 757) | func (x *ExpireNodeRequest) GetNodeId() uint64 {
    method GetExpiry (line 764) | func (x *ExpireNodeRequest) GetExpiry() *timestamppb.Timestamp {
    method GetDisableExpiry (line 771) | func (x *ExpireNodeRequest) GetDisableExpiry() bool {
  type ExpireNodeResponse (line 778) | type ExpireNodeResponse struct
    method Reset (line 785) | func (x *ExpireNodeResponse) Reset() {
    method String (line 792) | func (x *ExpireNodeResponse) String() string {
    method ProtoMessage (line 796) | func (*ExpireNodeResponse) ProtoMessage() {}
    method ProtoReflect (line 798) | func (x *ExpireNodeResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 811) | func (*ExpireNodeResponse) Descriptor() ([]byte, []int) {
    method GetNode (line 815) | func (x *ExpireNodeResponse) GetNode() *Node {
  type RenameNodeRequest (line 822) | type RenameNodeRequest struct
    method Reset (line 830) | func (x *RenameNodeRequest) Reset() {
    method String (line 837) | func (x *RenameNodeRequest) String() string {
    method ProtoMessage (line 841) | func (*RenameNodeRequest) ProtoMessage() {}
    method ProtoReflect (line 843) | func (x *RenameNodeRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 856) | func (*RenameNodeRequest) Descriptor() ([]byte, []int) {
    method GetNodeId (line 860) | func (x *RenameNodeRequest) GetNodeId() uint64 {
    method GetNewName (line 867) | func (x *RenameNodeRequest) GetNewName() string {
  type RenameNodeResponse (line 874) | type RenameNodeResponse struct
    method Reset (line 881) | func (x *RenameNodeResponse) Reset() {
    method String (line 888) | func (x *RenameNodeResponse) String() string {
    method ProtoMessage (line 892) | func (*RenameNodeResponse) ProtoMessage() {}
    method ProtoReflect (line 894) | func (x *RenameNodeResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 907) | func (*RenameNodeResponse) Descriptor() ([]byte, []int) {
    method GetNode (line 911) | func (x *RenameNodeResponse) GetNode() *Node {
  type ListNodesRequest (line 918) | type ListNodesRequest struct
    method Reset (line 925) | func (x *ListNodesRequest) Reset() {
    method String (line 932) | func (x *ListNodesRequest) String() string {
    method ProtoMessage (line 936) | func (*ListNodesRequest) ProtoMessage() {}
    method ProtoReflect (line 938) | func (x *ListNodesRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 951) | func (*ListNodesRequest) Descriptor() ([]byte, []int) {
    method GetUser (line 955) | func (x *ListNodesRequest) GetUser() string {
  type ListNodesResponse (line 962) | type ListNodesResponse struct
    method Reset (line 969) | func (x *ListNodesResponse) Reset() {
    method String (line 976) | func (x *ListNodesResponse) String() string {
    method ProtoMessage (line 980) | func (*ListNodesResponse) ProtoMessage() {}
    method ProtoReflect (line 982) | func (x *ListNodesResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 995) | func (*ListNodesResponse) Descriptor() ([]byte, []int) {
    method GetNodes (line 999) | func (x *ListNodesResponse) GetNodes() []*Node {
  type DebugCreateNodeRequest (line 1006) | type DebugCreateNodeRequest struct
    method Reset (line 1016) | func (x *DebugCreateNodeRequest) Reset() {
    method String (line 1023) | func (x *DebugCreateNodeRequest) String() string {
    method ProtoMessage (line 1027) | func (*DebugCreateNodeRequest) ProtoMessage() {}
    method ProtoReflect (line 1029) | func (x *DebugCreateNodeRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 1042) | func (*DebugCreateNodeRequest) Descriptor() ([]byte, []int) {
    method GetUser (line 1046) | func (x *DebugCreateNodeRequest) GetUser() string {
    method GetKey (line 1053) | func (x *DebugCreateNodeRequest) GetKey() string {
    method GetName (line 1060) | func (x *DebugCreateNodeRequest) GetName() string {
    method GetRoutes (line 1067) | func (x *DebugCreateNodeRequest) GetRoutes() []string {
  type DebugCreateNodeResponse (line 1074) | type DebugCreateNodeResponse struct
    method Reset (line 1081) | func (x *DebugCreateNodeResponse) Reset() {
    method String (line 1088) | func (x *DebugCreateNodeResponse) String() string {
    method ProtoMessage (line 1092) | func (*DebugCreateNodeResponse) ProtoMessage() {}
    method ProtoReflect (line 1094) | func (x *DebugCreateNodeResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 1107) | func (*DebugCreateNodeResponse) Descriptor() ([]byte, []int) {
    method GetNode (line 1111) | func (x *DebugCreateNodeResponse) GetNode() *Node {
  type BackfillNodeIPsRequest (line 1118) | type BackfillNodeIPsRequest struct
    method Reset (line 1125) | func (x *BackfillNodeIPsRequest) Reset() {
    method String (line 1132) | func (x *BackfillNodeIPsRequest) String() string {
    method ProtoMessage (line 1136) | func (*BackfillNodeIPsRequest) ProtoMessage() {}
    method ProtoReflect (line 1138) | func (x *BackfillNodeIPsRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 1151) | func (*BackfillNodeIPsRequest) Descriptor() ([]byte, []int) {
    method GetConfirmed (line 1155) | func (x *BackfillNodeIPsRequest) GetConfirmed() bool {
  type BackfillNodeIPsResponse (line 1162) | type BackfillNodeIPsResponse struct
    method Reset (line 1169) | func (x *BackfillNodeIPsResponse) Reset() {
    method String (line 1176) | func (x *BackfillNodeIPsResponse) String() string {
    method ProtoMessage (line 1180) | func (*BackfillNodeIPsResponse) ProtoMessage() {}
    method ProtoReflect (line 1182) | func (x *BackfillNodeIPsResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 1195) | func (*BackfillNodeIPsResponse) Descriptor() ([]byte, []int) {
    method GetChanges (line 1199) | func (x *BackfillNodeIPsResponse) GetChanges() []string {
  constant file_headscale_v1_node_proto_rawDesc (line 1208) | file_headscale_v1_node_proto_rawDesc = "" +
  function file_headscale_v1_node_proto_rawDescGZIP (line 1295) | func file_headscale_v1_node_proto_rawDescGZIP() []byte {
  function init (line 1354) | func init() { file_headscale_v1_node_proto_init() }
  function file_headscale_v1_node_proto_init (line 1355) | func file_headscale_v1_node_proto_init() {

FILE: gen/go/headscale/v1/policy.pb.go
  constant _ (line 20) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 22) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type SetPolicyRequest (line 25) | type SetPolicyRequest struct
    method Reset (line 32) | func (x *SetPolicyRequest) Reset() {
    method String (line 39) | func (x *SetPolicyRequest) String() string {
    method ProtoMessage (line 43) | func (*SetPolicyRequest) ProtoMessage() {}
    method ProtoReflect (line 45) | func (x *SetPolicyRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 58) | func (*SetPolicyRequest) Descriptor() ([]byte, []int) {
    method GetPolicy (line 62) | func (x *SetPolicyRequest) GetPolicy() string {
  type SetPolicyResponse (line 69) | type SetPolicyResponse struct
    method Reset (line 77) | func (x *SetPolicyResponse) Reset() {
    method String (line 84) | func (x *SetPolicyResponse) String() string {
    method ProtoMessage (line 88) | func (*SetPolicyResponse) ProtoMessage() {}
    method ProtoReflect (line 90) | func (x *SetPolicyResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 103) | func (*SetPolicyResponse) Descriptor() ([]byte, []int) {
    method GetPolicy (line 107) | func (x *SetPolicyResponse) GetPolicy() string {
    method GetUpdatedAt (line 114) | func (x *SetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
  type GetPolicyRequest (line 121) | type GetPolicyRequest struct
    method Reset (line 127) | func (x *GetPolicyRequest) Reset() {
    method String (line 134) | func (x *GetPolicyRequest) String() string {
    method ProtoMessage (line 138) | func (*GetPolicyRequest) ProtoMessage() {}
    method ProtoReflect (line 140) | func (x *GetPolicyRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 153) | func (*GetPolicyRequest) Descriptor() ([]byte, []int) {
  type GetPolicyResponse (line 157) | type GetPolicyResponse struct
    method Reset (line 165) | func (x *GetPolicyResponse) Reset() {
    method String (line 172) | func (x *GetPolicyResponse) String() string {
    method ProtoMessage (line 176) | func (*GetPolicyResponse) ProtoMessage() {}
    method ProtoReflect (line 178) | func (x *GetPolicyResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 191) | func (*GetPolicyResponse) Descriptor() ([]byte, []int) {
    method GetPolicy (line 195) | func (x *GetPolicyResponse) GetPolicy() string {
    method GetUpdatedAt (line 202) | func (x *GetPolicyResponse) GetUpdatedAt() *timestamppb.Timestamp {
  constant file_headscale_v1_policy_proto_rawDesc (line 211) | file_headscale_v1_policy_proto_rawDesc = "" +
  function file_headscale_v1_policy_proto_rawDescGZIP (line 231) | func file_headscale_v1_policy_proto_rawDescGZIP() []byte {
  function init (line 256) | func init() { file_headscale_v1_policy_proto_init() }
  function file_headscale_v1_policy_proto_init (line 257) | func file_headscale_v1_policy_proto_init() {

FILE: gen/go/headscale/v1/preauthkey.pb.go
  constant _ (line 20) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 22) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type PreAuthKey (line 25) | type PreAuthKey struct
    method Reset (line 40) | func (x *PreAuthKey) Reset() {
    method String (line 47) | func (x *PreAuthKey) String() string {
    method ProtoMessage (line 51) | func (*PreAuthKey) ProtoMessage() {}
    method ProtoReflect (line 53) | func (x *PreAuthKey) ProtoReflect() protoreflect.Message {
    method Descriptor (line 66) | func (*PreAuthKey) Descriptor() ([]byte, []int) {
    method GetUser (line 70) | func (x *PreAuthKey) GetUser() *User {
    method GetId (line 77) | func (x *PreAuthKey) GetId() uint64 {
    method GetKey (line 84) | func (x *PreAuthKey) GetKey() string {
    method GetReusable (line 91) | func (x *PreAuthKey) GetReusable() bool {
    method GetEphemeral (line 98) | func (x *PreAuthKey) GetEphemeral() bool {
    method GetUsed (line 105) | func (x *PreAuthKey) GetUsed() bool {
    method GetExpiration (line 112) | func (x *PreAuthKey) GetExpiration() *timestamppb.Timestamp {
    method GetCreatedAt (line 119) | func (x *PreAuthKey) GetCreatedAt() *timestamppb.Timestamp {
    method GetAclTags (line 126) | func (x *PreAuthKey) GetAclTags() []string {
  type CreatePreAuthKeyRequest (line 133) | type CreatePreAuthKeyRequest struct
    method Reset (line 144) | func (x *CreatePreAuthKeyRequest) Reset() {
    method String (line 151) | func (x *CreatePreAuthKeyRequest) String() string {
    method ProtoMessage (line 155) | func (*CreatePreAuthKeyRequest) ProtoMessage() {}
    method ProtoReflect (line 157) | func (x *CreatePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 170) | func (*CreatePreAuthKeyRequest) Descriptor() ([]byte, []int) {
    method GetUser (line 174) | func (x *CreatePreAuthKeyRequest) GetUser() uint64 {
    method GetReusable (line 181) | func (x *CreatePreAuthKeyRequest) GetReusable() bool {
    method GetEphemeral (line 188) | func (x *CreatePreAuthKeyRequest) GetEphemeral() bool {
    method GetExpiration (line 195) | func (x *CreatePreAuthKeyRequest) GetExpiration() *timestamppb.Timesta...
    method GetAclTags (line 202) | func (x *CreatePreAuthKeyRequest) GetAclTags() []string {
  type CreatePreAuthKeyResponse (line 209) | type CreatePreAuthKeyResponse struct
    method Reset (line 216) | func (x *CreatePreAuthKeyResponse) Reset() {
    method String (line 223) | func (x *CreatePreAuthKeyResponse) String() string {
    method ProtoMessage (line 227) | func (*CreatePreAuthKeyResponse) ProtoMessage() {}
    method ProtoReflect (line 229) | func (x *CreatePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 242) | func (*CreatePreAuthKeyResponse) Descriptor() ([]byte, []int) {
    method GetPreAuthKey (line 246) | func (x *CreatePreAuthKeyResponse) GetPreAuthKey() *PreAuthKey {
  type ExpirePreAuthKeyRequest (line 253) | type ExpirePreAuthKeyRequest struct
    method Reset (line 260) | func (x *ExpirePreAuthKeyRequest) Reset() {
    method String (line 267) | func (x *ExpirePreAuthKeyRequest) String() string {
    method ProtoMessage (line 271) | func (*ExpirePreAuthKeyRequest) ProtoMessage() {}
    method ProtoReflect (line 273) | func (x *ExpirePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 286) | func (*ExpirePreAuthKeyRequest) Descriptor() ([]byte, []int) {
    method GetId (line 290) | func (x *ExpirePreAuthKeyRequest) GetId() uint64 {
  type ExpirePreAuthKeyResponse (line 297) | type ExpirePreAuthKeyResponse struct
    method Reset (line 303) | func (x *ExpirePreAuthKeyResponse) Reset() {
    method String (line 310) | func (x *ExpirePreAuthKeyResponse) String() string {
    method ProtoMessage (line 314) | func (*ExpirePreAuthKeyResponse) ProtoMessage() {}
    method ProtoReflect (line 316) | func (x *ExpirePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 329) | func (*ExpirePreAuthKeyResponse) Descriptor() ([]byte, []int) {
  type DeletePreAuthKeyRequest (line 333) | type DeletePreAuthKeyRequest struct
    method Reset (line 340) | func (x *DeletePreAuthKeyRequest) Reset() {
    method String (line 347) | func (x *DeletePreAuthKeyRequest) String() string {
    method ProtoMessage (line 351) | func (*DeletePreAuthKeyRequest) ProtoMessage() {}
    method ProtoReflect (line 353) | func (x *DeletePreAuthKeyRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 366) | func (*DeletePreAuthKeyRequest) Descriptor() ([]byte, []int) {
    method GetId (line 370) | func (x *DeletePreAuthKeyRequest) GetId() uint64 {
  type DeletePreAuthKeyResponse (line 377) | type DeletePreAuthKeyResponse struct
    method Reset (line 383) | func (x *DeletePreAuthKeyResponse) Reset() {
    method String (line 390) | func (x *DeletePreAuthKeyResponse) String() string {
    method ProtoMessage (line 394) | func (*DeletePreAuthKeyResponse) ProtoMessage() {}
    method ProtoReflect (line 396) | func (x *DeletePreAuthKeyResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 409) | func (*DeletePreAuthKeyResponse) Descriptor() ([]byte, []int) {
  type ListPreAuthKeysRequest (line 413) | type ListPreAuthKeysRequest struct
    method Reset (line 419) | func (x *ListPreAuthKeysRequest) Reset() {
    method String (line 426) | func (x *ListPreAuthKeysRequest) String() string {
    method ProtoMessage (line 430) | func (*ListPreAuthKeysRequest) ProtoMessage() {}
    method ProtoReflect (line 432) | func (x *ListPreAuthKeysRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 445) | func (*ListPreAuthKeysRequest) Descriptor() ([]byte, []int) {
  type ListPreAuthKeysResponse (line 449) | type ListPreAuthKeysResponse struct
    method Reset (line 456) | func (x *ListPreAuthKeysResponse) Reset() {
    method String (line 463) | func (x *ListPreAuthKeysResponse) String() string {
    method ProtoMessage (line 467) | func (*ListPreAuthKeysResponse) ProtoMessage() {}
    method ProtoReflect (line 469) | func (x *ListPreAuthKeysResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 482) | func (*ListPreAuthKeysResponse) Descriptor() ([]byte, []int) {
    method GetPreAuthKeys (line 486) | func (x *ListPreAuthKeysResponse) GetPreAuthKeys() []*PreAuthKey {
  constant file_headscale_v1_preauthkey_proto_rawDesc (line 495) | file_headscale_v1_preauthkey_proto_rawDesc = "" +
  function file_headscale_v1_preauthkey_proto_rawDescGZIP (line 538) | func file_headscale_v1_preauthkey_proto_rawDescGZIP() []byte {
  function init (line 573) | func init() { file_headscale_v1_preauthkey_proto_init() }
  function file_headscale_v1_preauthkey_proto_init (line 574) | func file_headscale_v1_preauthkey_proto_init() {

FILE: gen/go/headscale/v1/user.pb.go
  constant _ (line 20) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 22) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type User (line 25) | type User struct
    method Reset (line 39) | func (x *User) Reset() {
    method String (line 46) | func (x *User) String() string {
    method ProtoMessage (line 50) | func (*User) ProtoMessage() {}
    method ProtoReflect (line 52) | func (x *User) ProtoReflect() protoreflect.Message {
    method Descriptor (line 65) | func (*User) Descriptor() ([]byte, []int) {
    method GetId (line 69) | func (x *User) GetId() uint64 {
    method GetName (line 76) | func (x *User) GetName() string {
    method GetCreatedAt (line 83) | func (x *User) GetCreatedAt() *timestamppb.Timestamp {
    method GetDisplayName (line 90) | func (x *User) GetDisplayName() string {
    method GetEmail (line 97) | func (x *User) GetEmail() string {
    method GetProviderId (line 104) | func (x *User) GetProviderId() string {
    method GetProvider (line 111) | func (x *User) GetProvider() string {
    method GetProfilePicUrl (line 118) | func (x *User) GetProfilePicUrl() string {
  type CreateUserRequest (line 125) | type CreateUserRequest struct
    method Reset (line 135) | func (x *CreateUserRequest) Reset() {
    method String (line 142) | func (x *CreateUserRequest) String() string {
    method ProtoMessage (line 146) | func (*CreateUserRequest) ProtoMessage() {}
    method ProtoReflect (line 148) | func (x *CreateUserRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 161) | func (*CreateUserRequest) Descriptor() ([]byte, []int) {
    method GetName (line 165) | func (x *CreateUserRequest) GetName() string {
    method GetDisplayName (line 172) | func (x *CreateUserRequest) GetDisplayName() string {
    method GetEmail (line 179) | func (x *CreateUserRequest) GetEmail() string {
    method GetPictureUrl (line 186) | func (x *CreateUserRequest) GetPictureUrl() string {
  type CreateUserResponse (line 193) | type CreateUserResponse struct
    method Reset (line 200) | func (x *CreateUserResponse) Reset() {
    method String (line 207) | func (x *CreateUserResponse) String() string {
    method ProtoMessage (line 211) | func (*CreateUserResponse) ProtoMessage() {}
    method ProtoReflect (line 213) | func (x *CreateUserResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 226) | func (*CreateUserResponse) Descriptor() ([]byte, []int) {
    method GetUser (line 230) | func (x *CreateUserResponse) GetUser() *User {
  type RenameUserRequest (line 237) | type RenameUserRequest struct
    method Reset (line 245) | func (x *RenameUserRequest) Reset() {
    method String (line 252) | func (x *RenameUserRequest) String() string {
    method ProtoMessage (line 256) | func (*RenameUserRequest) ProtoMessage() {}
    method ProtoReflect (line 258) | func (x *RenameUserRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 271) | func (*RenameUserRequest) Descriptor() ([]byte, []int) {
    method GetOldId (line 275) | func (x *RenameUserRequest) GetOldId() uint64 {
    method GetNewName (line 282) | func (x *RenameUserRequest) GetNewName() string {
  type RenameUserResponse (line 289) | type RenameUserResponse struct
    method Reset (line 296) | func (x *RenameUserResponse) Reset() {
    method String (line 303) | func (x *RenameUserResponse) String() string {
    method ProtoMessage (line 307) | func (*RenameUserResponse) ProtoMessage() {}
    method ProtoReflect (line 309) | func (x *RenameUserResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 322) | func (*RenameUserResponse) Descriptor() ([]byte, []int) {
    method GetUser (line 326) | func (x *RenameUserResponse) GetUser() *User {
  type DeleteUserRequest (line 333) | type DeleteUserRequest struct
    method Reset (line 340) | func (x *DeleteUserRequest) Reset() {
    method String (line 347) | func (x *DeleteUserRequest) String() string {
    method ProtoMessage (line 351) | func (*DeleteUserRequest) ProtoMessage() {}
    method ProtoReflect (line 353) | func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 366) | func (*DeleteUserRequest) Descriptor() ([]byte, []int) {
    method GetId (line 370) | func (x *DeleteUserRequest) GetId() uint64 {
  type DeleteUserResponse (line 377) | type DeleteUserResponse struct
    method Reset (line 383) | func (x *DeleteUserResponse) Reset() {
    method String (line 390) | func (x *DeleteUserResponse) String() string {
    method ProtoMessage (line 394) | func (*DeleteUserResponse) ProtoMessage() {}
    method ProtoReflect (line 396) | func (x *DeleteUserResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 409) | func (*DeleteUserResponse) Descriptor() ([]byte, []int) {
  type ListUsersRequest (line 413) | type ListUsersRequest struct
    method Reset (line 422) | func (x *ListUsersRequest) Reset() {
    method String (line 429) | func (x *ListUsersRequest) String() string {
    method ProtoMessage (line 433) | func (*ListUsersRequest) ProtoMessage() {}
    method ProtoReflect (line 435) | func (x *ListUsersRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 448) | func (*ListUsersRequest) Descriptor() ([]byte, []int) {
    method GetId (line 452) | func (x *ListUsersRequest) GetId() uint64 {
    method GetName (line 459) | func (x *ListUsersRequest) GetName() string {
    method GetEmail (line 466) | func (x *ListUsersRequest) GetEmail() string {
  type ListUsersResponse (line 473) | type ListUsersResponse struct
    method Reset (line 480) | func (x *ListUsersResponse) Reset() {
    method String (line 487) | func (x *ListUsersResponse) String() string {
    method ProtoMessage (line 491) | func (*ListUsersResponse) ProtoMessage() {}
    method ProtoReflect (line 493) | func (x *ListUsersResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 506) | func (*ListUsersResponse) Descriptor() ([]byte, []int) {
    method GetUsers (line 510) | func (x *ListUsersResponse) GetUsers() []*User {
  constant file_headscale_v1_user_proto_rawDesc (line 519) | file_headscale_v1_user_proto_rawDesc = "" +
  function file_headscale_v1_user_proto_rawDescGZIP (line 561) | func file_headscale_v1_user_proto_rawDescGZIP() []byte {
  function init (line 593) | func init() { file_headscale_v1_user_proto_init() }
  function file_headscale_v1_user_proto_init (line 594) | func file_headscale_v1_user_proto_init() {

FILE: hscontrol/app.go
  function init (line 78) | func init() {
  constant AuthPrefix (line 87) | AuthPrefix         = "Bearer "
  constant updateInterval (line 88) | updateInterval     = 5 * time.Second
  constant privateKeyFileMode (line 89) | privateKeyFileMode = 0o600
  constant headscaleDirPerm (line 90) | headscaleDirPerm   = 0o700
  type Headscale (line 94) | type Headscale struct
    method redirect (line 252) | func (h *Headscale) redirect(w http.ResponseWriter, req *http.Request) {
    method scheduledTasks (line 257) | func (h *Headscale) scheduledTasks(ctx context.Context) {
    method grpcAuthenticationInterceptor (line 339) | func (h *Headscale) grpcAuthenticationInterceptor(ctx context.Context,
    method httpAuthenticationMiddleware (line 397) | func (h *Headscale) httpAuthenticationMiddleware(next http.Handler) ht...
    method ensureUnixSocketIsAbsent (line 454) | func (h *Headscale) ensureUnixSocketIsAbsent() error {
    method createRouter (line 463) | func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *chi.M...
    method Serve (line 520) | func (h *Headscale) Serve() error {
    method getTLSSettings (line 943) | func (h *Headscale) getTLSSettings() (*tls.Config, error) {
    method Change (line 1069) | func (h *Headscale) Change(cs ...change.Change) {
    method HTTPHandler (line 1076) | func (h *Headscale) HTTPHandler() http.Handler {
    method NoisePublicKey (line 1081) | func (h *Headscale) NoisePublicKey() key.MachinePublic {
    method GetState (line 1087) | func (h *Headscale) GetState() *state.State {
    method SetServerURLForTest (line 1095) | func (h *Headscale) SetServerURLForTest(tb testing.TB, url string) {
    method StartBatcherForTest (line 1104) | func (h *Headscale) StartBatcherForTest(tb testing.TB) {
    method StartEphemeralGCForTest (line 1115) | func (h *Headscale) StartEphemeralGCForTest(tb testing.TB) {
  function NewHeadscale (line 119) | func NewHeadscale(cfg *types.Config) (*Headscale, error) {
  function readOrCreatePrivateKey (line 1020) | func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
  type acmeLogger (line 1125) | type acmeLogger struct
    method RoundTrip (line 1131) | func (l *acmeLogger) RoundTrip(req *http.Request) (*http.Response, err...
  type zerologRequestLogger (line 1150) | type zerologRequestLogger struct
    method NewLogEntry (line 1152) | func (z *zerologRequestLogger) NewLogEntry(
  type zerologLogEntry (line 1163) | type zerologLogEntry struct
    method Write (line 1170) | func (e *zerologLogEntry) Write(
    method Panic (line 1187) | func (e *zerologLogEntry) Panic(

FILE: hscontrol/auth.go
  type AuthProvider (line 21) | type AuthProvider interface
  method handleRegister (line 28) | func (h *Headscale) handleRegister(
  method handleLogout (line 137) | func (h *Headscale) handleLogout(
  function isAuthKey (line 231) | func isAuthKey(req tailcfg.RegisterRequest) bool {
  function nodeToRegisterResponse (line 235) | func nodeToRegisterResponse(node types.NodeView) *tailcfg.RegisterRespon...
  method waitForFollowup (line 258) | func (h *Headscale) waitForFollowup(
  method reqToNewRegisterResponse (line 296) | func (h *Headscale) reqToNewRegisterResponse(
  method handleRegisterWithAuthKey (line 338) | func (h *Headscale) handleRegisterWithAuthKey(
  method handleRegisterInteractive (line 402) | func (h *Headscale) handleRegisterInteractive(

FILE: hscontrol/auth_tags_test.go
  function TestTaggedPreAuthKeyCreatesTaggedNode (line 19) | func TestTaggedPreAuthKeyCreatesTaggedNode(t *testing.T) {
  function TestReAuthDoesNotReapplyTags (line 72) | func TestReAuthDoesNotReapplyTags(t *testing.T) {
  function TestCannotRemoveAllTags (line 144) | func TestCannotRemoveAllTags(t *testing.T) {
  function TestUserOwnedNodeCreatedWithUntaggedPreAuthKey (line 191) | func TestUserOwnedNodeCreatedWithUntaggedPreAuthKey(t *testing.T) {
  function TestMultipleNodesWithSameReusableTaggedPreAuthKey (line 236) | func TestMultipleNodesWithSameReusableTaggedPreAuthKey(t *testing.T) {
  function TestNonReusableTaggedPreAuthKey (line 308) | func TestNonReusableTaggedPreAuthKey(t *testing.T) {
  function TestExpiredTaggedPreAuthKey (line 369) | func TestExpiredTaggedPreAuthKey(t *testing.T) {
  function TestSingleVsMultipleTags (line 406) | func TestSingleVsMultipleTags(t *testing.T) {
  function TestTaggedPreAuthKeyDisablesKeyExpiry (line 476) | func TestTaggedPreAuthKeyDisablesKeyExpiry(t *testing.T) {
  function TestUntaggedPreAuthKeyPreservesKeyExpiry (line 520) | func TestUntaggedPreAuthKeyPreservesKeyExpiry(t *testing.T) {
  function TestTaggedNodeReauthPreservesDisabledExpiry (line 566) | func TestTaggedNodeReauthPreservesDisabledExpiry(t *testing.T) {
  function TestExpiryDuringPersonalToTaggedConversion (line 632) | func TestExpiryDuringPersonalToTaggedConversion(t *testing.T) {
  function TestExpiryDuringTaggedToPersonalConversion (line 705) | func TestExpiryDuringTaggedToPersonalConversion(t *testing.T) {
  function TestReAuthWithDifferentMachineKey (line 777) | func TestReAuthWithDifferentMachineKey(t *testing.T) {

FILE: hscontrol/auth_test.go
  constant stepTypeInitialRequest (line 22) | stepTypeInitialRequest  = "initial_request"
  constant stepTypeAuthCompletion (line 23) | stepTypeAuthCompletion  = "auth_completion"
  constant stepTypeFollowupRequest (line 24) | stepTypeFollowupRequest = "followup_request"
  type interactiveStep (line 30) | type interactiveStep struct
  function TestAuthenticationFlows (line 38) | func TestAuthenticationFlows(t *testing.T) {
  function runInteractiveWorkflowTest (line 2562) | func runInteractiveWorkflowTest(t *testing.T, tt struct {
  function extractRegistrationIDFromAuthURL (line 2703) | func extractRegistrationIDFromAuthURL(authURL string) (types.AuthID, err...
  function validateCompleteRegistrationResponse (line 2718) | func validateCompleteRegistrationResponse(t *testing.T, resp *tailcfg.Re...
  function TestNodeStoreLookup (line 2733) | func TestNodeStoreLookup(t *testing.T) {
  function TestPreAuthKeyLogoutAndReloginDifferentUser (line 2783) | func TestPreAuthKeyLogoutAndReloginDifferentUser(t *testing.T) {
  function TestWebFlowReauthDifferentUser (line 2957) | func TestWebFlowReauthDifferentUser(t *testing.T) {
  function createTestApp (line 3119) | func createTestApp(t *testing.T) *Headscale {
  function TestGitHubIssue2830_NodeRestartWithUsedPreAuthKey (line 3174) | func TestGitHubIssue2830_NodeRestartWithUsedPreAuthKey(t *testing.T) {
  function TestNodeReregistrationWithReusablePreAuthKey (line 3270) | func TestNodeReregistrationWithReusablePreAuthKey(t *testing.T) {
  function TestNodeReregistrationWithExpiredPreAuthKey (line 3325) | func TestNodeReregistrationWithExpiredPreAuthKey(t *testing.T) {
  function TestIssue2830_ExistingNodeReregistersWithExpiredKey (line 3359) | func TestIssue2830_ExistingNodeReregistersWithExpiredKey(t *testing.T) {
  function TestGitHubIssue2830_ExistingNodeCanReregisterWithUsedPreAuthKey (line 3457) | func TestGitHubIssue2830_ExistingNodeCanReregisterWithUsedPreAuthKey(t *...
  function TestWebAuthRejectsUnauthorizedRequestTags (line 3560) | func TestWebAuthRejectsUnauthorizedRequestTags(t *testing.T) {
  function TestWebAuthReauthWithEmptyTagsRemovesAllTags (line 3607) | func TestWebAuthReauthWithEmptyTagsRemovesAllTags(t *testing.T) {
  function TestAuthKeyTaggedToUserOwnedViaReauth (line 3703) | func TestAuthKeyTaggedToUserOwnedViaReauth(t *testing.T) {
  function TestDeletedPreAuthKeyNotRecreatedOnNodeUpdate (line 3800) | func TestDeletedPreAuthKeyNotRecreatedOnNodeUpdate(t *testing.T) {
  function TestTaggedNodeWithoutUserToDifferentUser (line 3898) | func TestTaggedNodeWithoutUserToDifferentUser(t *testing.T) {

FILE: hscontrol/capver/capver.go
  constant minVersionParts (line 17) | minVersionParts = 2
  constant legacyDERPCapVer (line 20) | legacyDERPCapVer = 111
  function CanOldCodeBeCleanedUp (line 29) | func CanOldCodeBeCleanedUp() {
  function tailscaleVersSorted (line 35) | func tailscaleVersSorted() []string {
  function capVersSorted (line 42) | func capVersSorted() []tailcfg.CapabilityVersion {
  function TailscaleVersion (line 50) | func TailscaleVersion(ver tailcfg.CapabilityVersion) string {
  function CapabilityVersion (line 56) | func CapabilityVersion(ver string) tailcfg.CapabilityVersion {
  function TailscaleLatest (line 77) | func TailscaleLatest(n int) []string {
  function TailscaleLatestMajorMinor (line 92) | func TailscaleLatestMajorMinor(n int, stripV bool) []string {
  function CapVerLatest (line 119) | func CapVerLatest(n int) []tailcfg.CapabilityVersion {

FILE: hscontrol/capver/capver_generated.go
  constant SupportedMajorMinorVersions (line 81) | SupportedMajorMinorVersions = 10
  constant MinSupportedCapabilityVersion (line 85) | MinSupportedCapabilityVersion tailcfg.CapabilityVersion = 106

FILE: hscontrol/capver/capver_test.go
  function TestTailscaleLatestMajorMinor (line 9) | func TestTailscaleLatestMajorMinor(t *testing.T) {
  function TestCapVerMinimumTailscaleVersion (line 20) | func TestCapVerMinimumTailscaleVersion(t *testing.T) {

FILE: hscontrol/db/api_key.go
  constant apiKeyPrefix (line 16) | apiKeyPrefix       = "hskey-api-"
  constant apiKeyPrefixLength (line 17) | apiKeyPrefixLength = 12
  constant apiKeyHashLength (line 18) | apiKeyHashLength   = 64
  constant legacyAPIPrefixLength (line 21) | legacyAPIPrefixLength = 7
  constant legacyAPIKeyLength (line 22) | legacyAPIKeyLength    = 32
  method CreateAPIKey (line 32) | func (hsdb *HSDatabase) CreateAPIKey(
  method ListAPIKeys (line 88) | func (hsdb *HSDatabase) ListAPIKeys() ([]types.APIKey, error) {
  method GetAPIKey (line 100) | func (hsdb *HSDatabase) GetAPIKey(prefix string) (*types.APIKey, error) {
  method GetAPIKeyByID (line 110) | func (hsdb *HSDatabase) GetAPIKeyByID(id uint64) (*types.APIKey, error) {
  method DestroyAPIKey (line 121) | func (hsdb *HSDatabase) DestroyAPIKey(key types.APIKey) error {
  method ExpireAPIKey (line 130) | func (hsdb *HSDatabase) ExpireAPIKey(key *types.APIKey) error {
  method ValidateAPIKey (line 139) | func (hsdb *HSDatabase) ValidateAPIKey(keyStr string) (bool, error) {
  function ParseAPIKeyPrefix (line 155) | func ParseAPIKeyPrefix(displayPrefix string) (string, error) {
  function validateAPIKey (line 190) | func validateAPIKey(db *gorm.DB, keyStr string) (*types.APIKey, error) {
  function validateLegacyAPIKey (line 274) | func validateLegacyAPIKey(db *gorm.DB, keyStr string) (*types.APIKey, er...

FILE: hscontrol/db/api_key_test.go
  function TestCreateAPIKey (line 14) | func TestCreateAPIKey(t *testing.T) {
  function TestAPIKeyDoesNotExist (line 35) | func TestAPIKeyDoesNotExist(t *testing.T) {
  function TestValidateAPIKeyOk (line 44) | func TestValidateAPIKeyOk(t *testing.T) {
  function TestValidateAPIKeyNotOk (line 58) | func TestValidateAPIKeyNotOk(t *testing.T) {
  function TestExpireAPIKey (line 89) | func TestExpireAPIKey(t *testing.T) {
  function TestAPIKeyWithPrefix (line 111) | func TestAPIKeyWithPrefix(t *testing.T) {
  function TestGetAPIKeyByID (line 250) | func TestGetAPIKeyByID(t *testing.T) {
  function TestGetAPIKeyByIDNotFound (line 267) | func TestGetAPIKeyByIDNotFound(t *testing.T) {

FILE: hscontrol/db/db.go
  function init (line 33) | func init() {
  constant maxIdleConns (line 42) | maxIdleConns       = 100
  constant maxOpenConns (line 43) | maxOpenConns       = 100
  constant contextTimeoutSecs (line 44) | contextTimeoutSecs = 10
  type HSDatabase (line 47) | type HSDatabase struct
    method PingDB (line 1080) | func (hsdb *HSDatabase) PingDB(ctx context.Context) error {
    method Close (line 1092) | func (hsdb *HSDatabase) Close() error {
    method Read (line 1105) | func (hsdb *HSDatabase) Read(fn func(rx *gorm.DB) error) error {
    method Write (line 1125) | func (hsdb *HSDatabase) Write(fn func(tx *gorm.DB) error) error {
  function NewHeadscaleDatabase (line 57) | func NewHeadscaleDatabase(
  function openDB (line 849) | func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {
  function runMigrations (line 955) | func runMigrations(cfg types.DatabaseConfig, dbConn *gorm.DB, migrations...
  function Read (line 1112) | func Read[T any](db *gorm.DB, fn func(rx *gorm.DB) (T, error)) (T, error) {
  function Write (line 1137) | func Write[T any](db *gorm.DB, fn func(tx *gorm.DB) (T, error)) (T, erro...

FILE: hscontrol/db/db_test.go
  function TestSQLiteMigrationAndDataValidation (line 23) | func TestSQLiteMigrationAndDataValidation(t *testing.T) {
  function emptyCache (line 165) | func emptyCache() *zcache.Cache[types.AuthID, types.AuthRequest] {
  function createSQLiteFromSQLFile (line 169) | func createSQLiteFromSQLFile(sqlFilePath, dbPath string) error {
  function requireConstraintFailed (line 188) | func requireConstraintFailed(t *testing.T, err error) {
  function TestConstraints (line 197) | func TestConstraints(t *testing.T) {
  function TestPostgresMigrationAndDataValidation (line 309) | func TestPostgresMigrationAndDataValidation(t *testing.T) {
  function dbForTest (line 347) | func dbForTest(t *testing.T) *HSDatabase {
  function dbForTestWithPath (line 352) | func dbForTestWithPath(t *testing.T, sqlFilePath string) *HSDatabase {
  function TestSQLiteAllTestdataMigrations (line 405) | func TestSQLiteAllTestdataMigrations(t *testing.T) {

FILE: hscontrol/db/ephemeral_garbage_collector_test.go
  constant fiveHundred (line 15) | fiveHundred = 500 * time.Millisecond
  constant oneHundred (line 16) | oneHundred  = 100 * time.Millisecond
  constant fifty (line 17) | fifty       = 50 * time.Millisecond
  function TestEphemeralGarbageCollectorGoRoutineLeak (line 24) | func TestEphemeralGarbageCollectorGoRoutineLeak(t *testing.T) {
  function TestEphemeralGarbageCollectorReschedule (line 95) | func TestEphemeralGarbageCollectorReschedule(t *testing.T) {
  function TestEphemeralGarbageCollectorCancelAndReschedule (line 151) | func TestEphemeralGarbageCollectorCancelAndReschedule(t *testing.T) {
  function TestEphemeralGarbageCollectorCloseBeforeTimerFires (line 219) | func TestEphemeralGarbageCollectorCloseBeforeTimerFires(t *testing.T) {
  function TestEphemeralGarbageCollectorScheduleAfterClose (line 269) | func TestEphemeralGarbageCollectorScheduleAfterClose(t *testing.T) {
  function TestEphemeralGarbageCollectorConcurrentScheduleAndClose (line 343) | func TestEphemeralGarbageCollectorConcurrentScheduleAndClose(t *testing....

FILE: hscontrol/db/ip.go
  type IPAllocator (line 31) | type IPAllocator struct
    method Next (line 140) | func (i *IPAllocator) Next() (*netip.Addr, *netip.Addr, error) {
    method nextLocked (line 173) | func (i *IPAllocator) nextLocked(prev netip.Addr, prefix *netip.Prefix...
    method next (line 180) | func (i *IPAllocator) next(prev netip.Addr, prefix *netip.Prefix) (*ne...
    method FreeIPs (line 363) | func (i *IPAllocator) FreeIPs(ips []netip.Addr) {
  function NewIPAllocator (line 57) | func NewIPAllocator(
  function randomNext (line 230) | func randomNext(pfx netip.Prefix) (netip.Addr, error) {
  function isTailscaleReservedIP (line 268) | func isTailscaleReservedIP(ip netip.Addr) bool {
  method BackfillNodeIPs (line 283) | func (db *HSDatabase) BackfillNodeIPs(i *IPAllocator) ([]string, error) {

FILE: hscontrol/db/ip_test.go
  function TestIPAllocatorSequential (line 30) | func TestIPAllocatorSequential(t *testing.T) {
  function TestIPAllocatorRandom (line 189) | func TestIPAllocatorRandom(t *testing.T) {
  function TestBackfillIPAddresses (line 287) | func TestBackfillIPAddresses(t *testing.T) {
  function TestIPAllocatorNextNoReservedIPs (line 485) | func TestIPAllocatorNextNoReservedIPs(t *testing.T) {

FILE: hscontrol/db/main_test.go
  function TestMain (line 13) | func TestMain(m *testing.M) {

FILE: hscontrol/db/node.go
  constant NodeGivenNameHashLength (line 27) | NodeGivenNameHashLength = 8
  constant NodeGivenNameTrimSize (line 28) | NodeGivenNameTrimSize   = 2
  constant defaultTestNodePrefix (line 31) | defaultTestNodePrefix = "testnode"
  method ListPeers (line 51) | func (hsdb *HSDatabase) ListPeers(nodeID types.NodeID, peerIDs ...types....
  function ListPeers (line 58) | func ListPeers(tx *gorm.DB, nodeID types.NodeID, peerIDs ...types.NodeID...
  method ListNodes (line 78) | func (hsdb *HSDatabase) ListNodes(nodeIDs ...types.NodeID) (types.Nodes,...
  function ListNodes (line 84) | func ListNodes(tx *gorm.DB, nodeIDs ...types.NodeID) (types.Nodes, error) {
  method ListEphemeralNodes (line 99) | func (hsdb *HSDatabase) ListEphemeralNodes() (types.Nodes, error) {
  method getNode (line 112) | func (hsdb *HSDatabase) getNode(uid types.UserID, name string) (*types.N...
  function getNode (line 119) | func getNode(tx *gorm.DB, uid types.UserID, name string) (*types.Node, e...
  method GetNodeByID (line 134) | func (hsdb *HSDatabase) GetNodeByID(id types.NodeID) (*types.Node, error) {
  function GetNodeByID (line 139) | func GetNodeByID(tx *gorm.DB, id types.NodeID) (*types.Node, error) {
  method GetNodeByMachineKey (line 152) | func (hsdb *HSDatabase) GetNodeByMachineKey(machineKey key.MachinePublic...
  function GetNodeByMachineKey (line 157) | func GetNodeByMachineKey(
  method GetNodeByNodeKey (line 173) | func (hsdb *HSDatabase) GetNodeByNodeKey(nodeKey key.NodePublic) (*types...
  function GetNodeByNodeKey (line 178) | func GetNodeByNodeKey(
  method SetTags (line 194) | func (hsdb *HSDatabase) SetTags(
  function SetTags (line 205) | func SetTags(
  function SetApprovedRoutes (line 237) | func SetApprovedRoutes(
  method SetLastSeen (line 277) | func (hsdb *HSDatabase) SetLastSeen(nodeID types.NodeID, lastSeen time.T...
  function SetLastSeen (line 285) | func SetLastSeen(tx *gorm.DB, nodeID types.NodeID, lastSeen time.Time) e...
  function RenameNode (line 291) | func RenameNode(tx *gorm.DB,
  method NodeSetExpiry (line 318) | func (hsdb *HSDatabase) NodeSetExpiry(nodeID types.NodeID, expiry *time....
  function NodeSetExpiry (line 326) | func NodeSetExpiry(tx *gorm.DB, nodeID types.NodeID, expiry *time.Time) ...
  method DeleteNode (line 330) | func (hsdb *HSDatabase) DeleteNode(node *types.Node) error {
  function DeleteNode (line 338) | func DeleteNode(tx *gorm.DB,
  method DeleteEphemeralNode (line 353) | func (hsdb *HSDatabase) DeleteEphemeralNode(
  function RegisterNodeForTest (line 368) | func RegisterNodeForTest(tx *gorm.DB, node types.Node, ipv4 *netip.Addr,...
  function NodeSetNodeKey (line 461) | func NodeSetNodeKey(tx *gorm.DB, node *types.Node, nodeKey key.NodePubli...
  method NodeSetMachineKey (line 467) | func (hsdb *HSDatabase) NodeSetMachineKey(
  function NodeSetMachineKey (line 477) | func NodeSetMachineKey(
  function generateGivenName (line 487) | func generateGivenName(suppliedName string, randomSuffix bool) (string, ...
  function isUniqueName (line 514) | func isUniqueName(tx *gorm.DB, name string) (bool, error) {
  function EnsureUniqueGivenName (line 527) | func EnsureUniqueGivenName(
  type EphemeralGarbageCollector (line 557) | type EphemeralGarbageCollector struct
    method Close (line 579) | func (e *EphemeralGarbageCollector) Close() {
    method Schedule (line 594) | func (e *EphemeralGarbageCollector) Schedule(nodeID types.NodeID, expi...
    method Cancel (line 638) | func (e *EphemeralGarbageCollector) Cancel(nodeID types.NodeID) {
    method Start (line 649) | func (e *EphemeralGarbageCollector) Start() {
  function NewEphemeralGarbageCollector (line 569) | func NewEphemeralGarbageCollector(deleteFunc func(types.NodeID)) *Epheme...
  method CreateNodeForTest (line 664) | func (hsdb *HSDatabase) CreateNodeForTest(user *types.User, hostname ......
  method CreateRegisteredNodeForTest (line 707) | func (hsdb *HSDatabase) CreateRegisteredNodeForTest(user *types.User, ho...
  method CreateNodesForTest (line 737) | func (hsdb *HSDatabase) CreateNodesForTest(user *types.User, count int, ...
  method CreateRegisteredNodesForTest (line 760) | func (hsdb *HSDatabase) CreateRegisteredNodesForTest(user *types.User, c...
  method allocateTestIPs (line 784) | func (hsdb *HSDatabase) allocateTestIPs(nodeID types.NodeID) (*netip.Add...

FILE: hscontrol/db/node_test.go
  function TestGetNode (line 27) | func TestGetNode(t *testing.T) {
  function TestGetNodeByID (line 43) | func TestGetNodeByID(t *testing.T) {
  function TestHardDeleteNode (line 59) | func TestHardDeleteNode(t *testing.T) {
  function TestListPeersManyNodes (line 73) | func TestListPeersManyNodes(t *testing.T) {
  function TestExpireNode (line 94) | func TestExpireNode(t *testing.T) {
  function TestDisableNodeExpiry (line 140) | func TestDisableNodeExpiry(t *testing.T) {
  function TestSetTags (line 182) | func TestSetTags(t *testing.T) {
  function TestHeadscale_generateGivenName (line 230) | func TestHeadscale_generateGivenName(t *testing.T) {
  function TestAutoApproveRoutes (line 347) | func TestAutoApproveRoutes(t *testing.T) {
  function TestEphemeralGarbageCollectorOrder (line 568) | func TestEphemeralGarbageCollectorOrder(t *testing.T) {
  function TestEphemeralGarbageCollectorLoads (line 628) | func TestEphemeralGarbageCollectorLoads(t *testing.T) {
  function generateRandomNumber (line 673) | func generateRandomNumber(t *testing.T, maxVal int64) int64 {
  function TestListEphemeralNodes (line 686) | func TestListEphemeralNodes(t *testing.T) {
  function TestRenameNodeComprehensive (line 902) | func TestRenameNodeComprehensive(t *testing.T) {
  function TestListPeers (line 1021) | func TestListPeers(t *testing.T) {
  function TestListNodes (line 1107) | func TestListNodes(t *testing.T) {

FILE: hscontrol/db/policy.go
  method SetPolicy (line 14) | func (hsdb *HSDatabase) SetPolicy(policy string) (*types.Policy, error) {
  method GetPolicy (line 29) | func (hsdb *HSDatabase) GetPolicy() (*types.Policy, error) {
  function GetPolicy (line 36) | func GetPolicy(tx *gorm.DB) (*types.Policy, error) {
  function PolicyBytes (line 60) | func PolicyBytes(tx *gorm.DB, cfg *types.Config) ([]byte, error) {

FILE: hscontrol/db/preauth_keys.go
  method CreatePreAuthKey (line 25) | func (hsdb *HSDatabase) CreatePreAuthKey(
  constant authKeyPrefix (line 38) | authKeyPrefix       = "hskey-auth-"
  constant authKeyPrefixLength (line 39) | authKeyPrefixLength = 12
  constant authKeyLength (line 40) | authKeyLength       = 64
  function CreatePreAuthKey (line 47) | func CreatePreAuthKey(
  method ListPreAuthKeys (line 157) | func (hsdb *HSDatabase) ListPreAuthKeys() ([]types.PreAuthKey, error) {
  function ListPreAuthKeys (line 162) | func ListPreAuthKeys(tx *gorm.DB) ([]types.PreAuthKey, error) {
  function findAuthKey (line 178) | func findAuthKey(tx *gorm.DB, keyStr string) (*types.PreAuthKey, error) {
  function isValidBase64URLSafe (line 267) | func isValidBase64URLSafe(s string) bool {
  method GetPreAuthKey (line 277) | func (hsdb *HSDatabase) GetPreAuthKey(key string) (*types.PreAuthKey, er...
  function GetPreAuthKey (line 283) | func GetPreAuthKey(tx *gorm.DB, key string) (*types.PreAuthKey, error) {
  function DestroyPreAuthKey (line 290) | func DestroyPreAuthKey(tx *gorm.DB, id uint64) error {
  method ExpirePreAuthKey (line 310) | func (hsdb *HSDatabase) ExpirePreAuthKey(id uint64) error {
  method DeletePreAuthKey (line 316) | func (hsdb *HSDatabase) DeletePreAuthKey(id uint64) error {
  function UsePreAuthKey (line 323) | func UsePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
  function ExpirePreAuthKey (line 335) | func ExpirePreAuthKey(tx *gorm.DB, id uint64) error {

FILE: hscontrol/db/preauth_keys_test.go
  function TestCreatePreAuthKey (line 16) | func TestCreatePreAuthKey(t *testing.T) {
  function TestPreAuthKeyACLTags (line 63) | func TestPreAuthKeyACLTags(t *testing.T) {
  function TestCannotDeleteAssignedPreAuthKey (line 115) | func TestCannotDeleteAssignedPreAuthKey(t *testing.T) {
  function TestPreAuthKeyAuthentication (line 137) | func TestPreAuthKeyAuthentication(t *testing.T) {
  function TestMultipleLegacyKeysAllowed (line 378) | func TestMultipleLegacyKeysAllowed(t *testing.T) {

FILE: hscontrol/db/schema.sql
  type migrations (line 5) | CREATE TABLE migrations(id text,PRIMARY KEY(id))
  type users (line 7) | CREATE TABLE users(
  type idx_users_deleted_at (line 20) | CREATE INDEX idx_users_deleted_at ON users(deleted_at)
  type idx_provider_identifier (line 37) | CREATE UNIQUE INDEX idx_provider_identifier ON users(provider_identifier...
  type idx_name_provider_identifier (line 38) | CREATE UNIQUE INDEX idx_name_provider_identifier ON users(name, provider...
  type idx_name_no_provider_identifier (line 39) | CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users(name) WHERE...
  type pre_auth_keys (line 41) | CREATE TABLE pre_auth_keys(
  type idx_pre_auth_keys_prefix (line 57) | CREATE UNIQUE INDEX idx_pre_auth_keys_prefix ON pre_auth_keys(prefix) WH...
  type api_keys (line 59) | CREATE TABLE api_keys(
  type idx_api_keys_prefix (line 68) | CREATE UNIQUE INDEX idx_api_keys_prefix ON api_keys(prefix)
  type nodes (line 70) | CREATE TABLE nodes(
  type policies (line 100) | CREATE TABLE policies(
  type idx_policies_deleted_at (line 108) | CREATE INDEX idx_policies_deleted_at ON policies(deleted_at)
  type database_versions (line 110) | CREATE TABLE database_versions(

FILE: hscontrol/db/sqliteconfig/config.go
  constant DefaultBusyTimeout (line 24) | DefaultBusyTimeout = 10000
  type JournalMode (line 65) | type JournalMode
    method IsValid (line 94) | func (j JournalMode) IsValid() bool {
    method String (line 105) | func (j JournalMode) String() string {
  constant JournalModeWAL (line 70) | JournalModeWAL JournalMode = "WAL"
  constant JournalModeDelete (line 74) | JournalModeDelete JournalMode = "DELETE"
  constant JournalModeTruncate (line 78) | JournalModeTruncate JournalMode = "TRUNCATE"
  constant JournalModePersist (line 82) | JournalModePersist JournalMode = "PERSIST"
  constant JournalModeMemory (line 86) | JournalModeMemory JournalMode = "MEMORY"
  constant JournalModeOff (line 90) | JournalModeOff JournalMode = "OFF"
  type AutoVacuum (line 134) | type AutoVacuum
    method IsValid (line 151) | func (a AutoVacuum) IsValid() bool {
    method String (line 161) | func (a AutoVacuum) String() string {
  constant AutoVacuumNone (line 139) | AutoVacuumNone AutoVacuum = "NONE"
  constant AutoVacuumFull (line 143) | AutoVacuumFull AutoVacuum = "FULL"
  constant AutoVacuumIncremental (line 147) | AutoVacuumIncremental AutoVacuum = "INCREMENTAL"
  type Synchronous (line 194) | type Synchronous
    method IsValid (line 215) | func (s Synchronous) IsValid() bool {
    method String (line 225) | func (s Synchronous) String() string {
  constant SynchronousOff (line 199) | SynchronousOff Synchronous = "OFF"
  constant SynchronousNormal (line 203) | SynchronousNormal Synchronous = "NORMAL"
  constant SynchronousFull (line 207) | SynchronousFull Synchronous = "FULL"
  constant SynchronousExtra (line 211) | SynchronousExtra Synchronous = "EXTRA"
  type TxLock (line 253) | type TxLock
    method IsValid (line 271) | func (t TxLock) IsValid() bool {
    method String (line 281) | func (t TxLock) String() string {
  constant TxLockDeferred (line 258) | TxLockDeferred TxLock = "deferred"
  constant TxLockImmediate (line 263) | TxLockImmediate TxLock = "immediate"
  constant TxLockExclusive (line 267) | TxLockExclusive TxLock = "exclusive"
  type Config (line 288) | type Config struct
    method Validate (line 330) | func (c *Config) Validate() error {
    method ToURL (line 364) | func (c *Config) ToURL() (string, error) {
  function Default (line 307) | func Default(path string) *Config {
  function Memory (line 321) | func Memory() *Config {

FILE: hscontrol/db/sqliteconfig/config_test.go
  function TestJournalMode (line 7) | func TestJournalMode(t *testing.T) {
  function TestAutoVacuum (line 31) | func TestAutoVacuum(t *testing.T) {
  function TestSynchronous (line 52) | func TestSynchronous(t *testing.T) {
  function TestTxLock (line 74) | func TestTxLock(t *testing.T) {
  function TestTxLockString (line 101) | func TestTxLockString(t *testing.T) {
  function TestConfigValidate (line 120) | func TestConfigValidate(t *testing.T) {
  function TestConfigToURL (line 180) | func TestConfigToURL(t *testing.T) {
  function TestConfigToURLInvalid (line 305) | func TestConfigToURLInvalid(t *testing.T) {
  function TestDefaultConfigHasTxLockImmediate (line 317) | func TestDefaultConfigHasTxLockImmediate(t *testing.T) {

FILE: hscontrol/db/sqliteconfig/integration_test.go
  constant memoryDBPath (line 13) | memoryDBPath = ":memory:"
  function TestSQLiteDriverPragmaIntegration (line 18) | func TestSQLiteDriverPragmaIntegration(t *testing.T) {
  function TestForeignKeyConstraintEnforcement (line 155) | func TestForeignKeyConstraintEnforcement(t *testing.T) {
  function TestJournalModeValidation (line 228) | func TestJournalModeValidation(t *testing.T) {
  function contains (line 278) | func contains(str, substr string) bool {

FILE: hscontrol/db/suite_test.go
  function newSQLiteTestDB (line 16) | func newSQLiteTestDB() (*HSDatabase, error) {
  function newPostgresTestDB (line 46) | func newPostgresTestDB(t *testing.T) *HSDatabase {
  function newPostgresDBForTest (line 52) | func newPostgresDBForTest(t *testing.T) *url.URL {
  function newHeadscaleDBFromPostgresURL (line 75) | func newHeadscaleDBFromPostgresURL(t *testing.T, pu *url.URL) *HSDatabase {

FILE: hscontrol/db/testdata/sqlite/failing-node-preauth-constraint_dump.sql
  type "api_keys" (line 3) | CREATE TABLE IF NOT EXISTS "api_keys" (`id` integer,`prefix` text UNIQUE...
  type `migrations` (line 6) | CREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`))
  type `policies` (line 17) | CREATE TABLE `policies` (`id` integer PRIMARY KEY AUTOINCREMENT,`created...
  type "pre_auth_keys" (line 18) | CREATE TABLE IF NOT EXISTS "pre_auth_keys"  (`id` integer,`key` text,`us...
  type "nodes" (line 19) | CREATE TABLE IF NOT EXISTS "nodes"  (`id` integer,`machine_key` text,`no...
  type "routes" (line 26) | CREATE TABLE IF NOT EXISTS "routes"  (`id` integer,`created_at` datetime...
  type "users" (line 27) | CREATE TABLE IF NOT EXISTS "users"  (`id` integer,`created_at` datetime,...
  type `idx_api_keys_prefix` (line 30) | CREATE UNIQUE INDEX `idx_api_keys_prefix` ON `api_keys`(`prefix`)
  type `idx_policies_deleted_at` (line 31) | CREATE INDEX `idx_policies_deleted_at` ON `policies`(`deleted_at`)
  type `idx_routes_deleted_at` (line 32) | CREATE INDEX `idx_routes_deleted_at` ON `routes`(`deleted_at`)
  type `idx_users_deleted_at` (line 33) | CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`)

FILE: hscontrol/db/testdata/sqlite/headscale_0.26.0-beta.1_dump.sql
  type `migrations` (line 3) | CREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`))
  type `users` (line 17) | CREATE TABLE `users` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at...
  type `pre_auth_keys` (line 18) | CREATE TABLE `pre_auth_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`ke...
  type `api_keys` (line 19) | CREATE TABLE `api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`prefix`...
  type `policies` (line 20) | CREATE TABLE `policies` (`id` integer PRIMARY KEY AUTOINCREMENT,`created...
  type "nodes" (line 21) | CREATE TABLE IF NOT EXISTS "nodes"  (`id` integer PRIMARY KEY AUTOINCREM...
  type `idx_users_deleted_at` (line 24) | CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`)
  type `idx_api_keys_prefix` (line 25) | CREATE UNIQUE INDEX `idx_api_keys_prefix` ON `api_keys`(`prefix`)
  type `idx_policies_deleted_at` (line 26) | CREATE INDEX `idx_policies_deleted_at` ON `policies`(`deleted_at`)
  type idx_provider_identifier (line 27) | CREATE UNIQUE INDEX idx_provider_identifier ON users (provider_identifie...
  type idx_name_provider_identifier (line 28) | CREATE UNIQUE INDEX idx_name_provider_identifier ON users (name,provider...
  type idx_name_no_provider_identifier (line 29) | CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users (name) WHER...

FILE: hscontrol/db/testdata/sqlite/headscale_0.26.0-beta.2_dump.sql
  type `migrations` (line 3) | CREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`))
  type `users` (line 18) | CREATE TABLE `users` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at...
  type `pre_auth_keys` (line 19) | CREATE TABLE `pre_auth_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`ke...
  type `api_keys` (line 20) | CREATE TABLE `api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`prefix`...
  type "nodes" (line 21) | CREATE TABLE IF NOT EXISTS "nodes"  (`id` integer PRIMARY KEY AUTOINCREM...
  type `policies` (line 22) | CREATE TABLE `policies` (`id` integer PRIMARY KEY AUTOINCREMENT,`created...
  type `idx_users_deleted_at` (line 25) | CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`)
  type `idx_api_keys_prefix` (line 26) | CREATE UNIQUE INDEX `idx_api_keys_prefix` ON `api_keys`(`prefix`)
  type `idx_policies_deleted_at` (line 27) | CREATE INDEX `idx_policies_deleted_at` ON `policies`(`deleted_at`)
  type idx_provider_identifier (line 28) | CREATE UNIQUE INDEX idx_provider_identifier ON users (provider_identifie...
  type idx_name_provider_identifier (line 29) | CREATE UNIQUE INDEX idx_name_provider_identifier ON users (name,provider...
  type idx_name_no_provider_identifier (line 30) | CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users (name) WHER...

FILE: hscontrol/db/testdata/sqlite/headscale_0.26.0_dump.sql
  type `migrations` (line 3) | CREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`))
  type `users` (line 19) | CREATE TABLE `users` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at...
  type `pre_auth_keys` (line 20) | CREATE TABLE `pre_auth_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`ke...
  type `api_keys` (line 21) | CREATE TABLE `api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`prefix`...
  type "nodes" (line 22) | CREATE TABLE IF NOT EXISTS "nodes"  (`id` integer PRIMARY KEY AUTOINCREM...
  type `policies` (line 23) | CREATE TABLE `policies` (`id` integer PRIMARY KEY AUTOINCREMENT,`created...
  type `idx_users_deleted_at` (line 26) | CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`)
  type `idx_api_keys_prefix` (line 27) | CREATE UNIQUE INDEX `idx_api_keys_prefix` ON `api_keys`(`prefix`)
  type `idx_policies_deleted_at` (line 28) | CREATE INDEX `idx_policies_deleted_at` ON `policies`(`deleted_at`)
  type idx_provider_identifier (line 29) | CREATE UNIQUE INDEX idx_provider_identifier ON users (provider_identifie...
  type idx_name_provider_identifier (line 30) | CREATE UNIQUE INDEX idx_name_provider_identifier ON users (name,provider...
  type idx_name_no_provider_identifier (line 31) | CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users (name) WHER...

FILE: hscontrol/db/testdata/sqlite/headscale_0.26.1_dump-litestream.sql
  type `migrations` (line 3) | CREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`))
  type `users` (line 19) | CREATE TABLE `users` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at...
  type `pre_auth_keys` (line 20) | CREATE TABLE `pre_auth_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`ke...
  type `api_keys` (line 21) | CREATE TABLE `api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`prefix`...
  type "nodes" (line 22) | CREATE TABLE IF NOT EXISTS "nodes"  (`id` integer PRIMARY KEY AUTOINCREM...
  type `policies` (line 23) | CREATE TABLE `policies` (`id` integer PRIMARY KEY AUTOINCREMENT,`created...
  type `idx_users_deleted_at` (line 26) | CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`)
  type `idx_api_keys_prefix` (line 27) | CREATE UNIQUE INDEX `idx_api_keys_prefix` ON `api_keys`(`prefix`)
  type `idx_policies_deleted_at` (line 28) | CREATE INDEX `idx_policies_deleted_at` ON `policies`(`deleted_at`)
  type idx_provider_identifier (line 29) | CREATE UNIQUE INDEX idx_provider_identifier ON users (provider_identifie...
  type idx_name_provider_identifier (line 30) | CREATE UNIQUE INDEX idx_name_provider_identifier ON users (name,provider...
  type idx_name_no_provider_identifier (line 31) | CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users (name) WHER...
  type _litestream_seq (line 32) | CREATE TABLE _litestream_seq (id INTEGER PRIMARY KEY, seq INTEGER)
  type _litestream_lock (line 33) | CREATE TABLE _litestream_lock (id INTEGER)

FILE: hscontrol/db/testdata/sqlite/headscale_0.26.1_dump.sql
  type `migrations` (line 3) | CREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`))
  type `users` (line 19) | CREATE TABLE `users` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at...
  type `pre_auth_keys` (line 20) | CREATE TABLE `pre_auth_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`ke...
  type `api_keys` (line 21) | CREATE TABLE `api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`prefix`...
  type "nodes" (line 22) | CREATE TABLE IF NOT EXISTS "nodes"  (`id` integer PRIMARY KEY AUTOINCREM...
  type `policies` (line 23) | CREATE TABLE `policies` (`id` integer PRIMARY KEY AUTOINCREMENT,`created...
  type `idx_users_deleted_at` (line 26) | CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`)
  type `idx_api_keys_prefix` (line 27) | CREATE UNIQUE INDEX `idx_api_keys_prefix` ON `api_keys`(`prefix`)
  type `idx_policies_deleted_at` (line 28) | CREATE INDEX `idx_policies_deleted_at` ON `policies`(`deleted_at`)
  type idx_provider_identifier (line 29) | CREATE UNIQUE INDEX idx_provider_identifier ON users (provider_identifie...
  type idx_name_provider_identifier (line 30) | CREATE UNIQUE INDEX idx_name_provider_identifier ON users (name,provider...
  type idx_name_no_provider_identifier (line 31) | CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users (name) WHER...

FILE: hscontrol/db/testdata/sqlite/headscale_0.26.1_dump_schema-to-0.27.0-old-table-cleanup.sql
  type `migrations` (line 3) | CREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`))
  type `users` (line 19) | CREATE TABLE `users` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at...
  type `pre_auth_keys` (line 20) | CREATE TABLE `pre_auth_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`ke...
  type `api_keys` (line 21) | CREATE TABLE `api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`prefix`...
  type "nodes" (line 22) | CREATE TABLE IF NOT EXISTS "nodes"  (`id` integer PRIMARY KEY AUTOINCREM...
  type `policies` (line 23) | CREATE TABLE `policies` (`id` integer PRIMARY KEY AUTOINCREMENT,`created...
  type `idx_users_deleted_at` (line 26) | CREATE INDEX `idx_users_deleted_at` ON `users`(`deleted_at`)
  type `idx_api_keys_prefix` (line 27) | CREATE UNIQUE INDEX `idx_api_keys_prefix` ON `api_keys`(`prefix`)
  type `idx_policies_deleted_at` (line 28) | CREATE INDEX `idx_policies_deleted_at` ON `policies`(`deleted_at`)
  type idx_provider_identifier (line 29) | CREATE UNIQUE INDEX idx_provider_identifier ON users (provider_identifie...
  type idx_name_provider_identifier (line 30) | CREATE UNIQUE INDEX idx_name_provider_identifier ON users (name,provider...
  type idx_name_no_provider_identifier (line 31) | CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users (name) WHER...
  type `namespaces` (line 34) | CREATE TABLE `namespaces` (`id` text,`deleted_at` datetime,PRIMARY KEY (...
  type `machines` (line 35) | CREATE TABLE `machines` (`id` text,PRIMARY KEY (`id`))
  type `kvs` (line 36) | CREATE TABLE `kvs` (`id` text,PRIMARY KEY (`id`))
  type `shared_machines` (line 37) | CREATE TABLE `shared_machines` (`id` text,`deleted_at` datetime,PRIMARY ...
  type `pre_auth_key_acl_tags` (line 38) | CREATE TABLE `pre_auth_key_acl_tags` (`id` text,PRIMARY KEY (`id`))
  type `routes` (line 39) | CREATE TABLE `routes` (`id` text,`deleted_at` datetime,PRIMARY KEY (`id`))
  type `idx_routes_deleted_at` (line 41) | CREATE INDEX `idx_routes_deleted_at` ON `routes`(`deleted_at`)
  type `idx_namespaces_deleted_at` (line 42) | CREATE INDEX `idx_namespaces_deleted_at` ON `namespaces`(`deleted_at`)
  type `idx_shared_machines_deleted_at` (line 43) | CREATE INDEX `idx_shared_machines_deleted_at` ON `shared_machines`(`dele...

FILE: hscontrol/db/testdata/sqlite/request_tags_migration_test.sql
  type `migrations` (line 20) | CREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`))
  type `users` (line 46) | CREATE TABLE `users` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at...
  type `pre_auth_keys` (line 52) | CREATE TABLE `pre_auth_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`ke...
  type `api_keys` (line 55) | CREATE TABLE `api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT,`prefix`...
  type "nodes" (line 58) | CREATE TABLE IF NOT EXISTS "nodes" (`id` integer PRIMARY KEY AUTOINCREME...
  type `policies` (line 91) | CREATE TABLE `policies` (`id` integer PRIMARY KEY AUTOINCREMENT,`created...
  type idx_users_deleted_at (line 111) | CREATE INDEX idx_users_deleted_at ON users(deleted_at)
  type idx_api_keys_prefix (line 112) | CREATE UNIQUE INDEX idx_api_keys_prefix ON api_keys(prefix)
  type idx_policies_deleted_at (line 113) | CREATE INDEX idx_policies_deleted_at ON policies(deleted_at)
  type idx_provider_identifier (line 114) | CREATE UNIQUE INDEX idx_provider_identifier ON users(provider_identifier...
  type idx_name_provider_identifier (line 115) | CREATE UNIQUE INDEX idx_name_provider_identifier ON users(name, provider...
  type idx_name_no_provider_identifier (line 116) | CREATE UNIQUE INDEX idx_name_no_provider_identifier ON users(name) WHERE...
  type idx_pre_auth_keys_prefix (line 117) | CREATE UNIQUE INDEX IF NOT EXISTS idx_pre_auth_keys_prefix ON pre_auth_k...

FILE: hscontrol/db/text_serialiser.go
  function isTextUnmarshaler (line 22) | func isTextUnmarshaler(rv reflect.Value) bool {
  function maybeInstantiatePtr (line 26) | func maybeInstantiatePtr(rv reflect.Value) {
  function decodingError (line 33) | func decodingError(name string, err error) error {
  type TextSerialiser (line 39) | type TextSerialiser struct
    method Scan (line 41) | func (TextSerialiser) Scan(ctx context.Context, field *schema.Field, d...
    method Value (line 94) | func (TextSerialiser) Value(ctx context.Context, field *schema.Field, ...

FILE: hscontrol/db/user_update_test.go
  function TestUserUpdatePreservesUnchangedFields (line 16) | func TestUserUpdatePreservesUnchangedFields(t *testing.T) {
  function TestUserUpdateWithUpdatesMethod (line 82) | func TestUserUpdateWithUpdatesMethod(t *testing.T) {

FILE: hscontrol/db/users.go
  method CreateUser (line 22) | func (hsdb *HSDatabase) CreateUser(user types.User) (*types.User, error) {
  function CreateUser (line 30) | func CreateUser(tx *gorm.DB, user types.User) (*types.User, error) {
  method DestroyUser (line 44) | func (hsdb *HSDatabase) DestroyUser(uid types.UserID) error {
  function DestroyUser (line 53) | func DestroyUser(tx *gorm.DB, uid types.UserID) error {
  method RenameUser (line 87) | func (hsdb *HSDatabase) RenameUser(uid types.UserID, newName string) err...
  function RenameUser (line 97) | func RenameUser(tx *gorm.DB, uid types.UserID, newName string) error {
  method GetUserByID (line 123) | func (hsdb *HSDatabase) GetUserByID(uid types.UserID) (*types.User, erro...
  function GetUserByID (line 127) | func GetUserByID(tx *gorm.DB, uid types.UserID) (*types.User, error) {
  method GetUserByOIDCIdentifier (line 139) | func (hsdb *HSDatabase) GetUserByOIDCIdentifier(id string) (*types.User,...
  function GetUserByOIDCIdentifier (line 145) | func GetUserByOIDCIdentifier(tx *gorm.DB, id string) (*types.User, error) {
  method ListUsers (line 157) | func (hsdb *HSDatabase) ListUsers(where ...*types.User) ([]types.User, e...
  function ListUsers (line 162) | func ListUsers(tx *gorm.DB, where ...*types.User) ([]types.User, error) {
  method GetUserByName (line 184) | func (hsdb *HSDatabase) GetUserByName(name string) (*types.User, error) {
  function ListNodesByUser (line 202) | func ListNodesByUser(tx *gorm.DB, uid types.UserID) (types.Nodes, error) {
  method CreateUserForTest (line 215) | func (hsdb *HSDatabase) CreateUserForTest(name ...string) *types.User {
  method CreateUsersForTest (line 233) | func (hsdb *HSDatabase) CreateUsersForTest(count int, namePrefix ...stri...

FILE: hscontrol/db/users_test.go
  function TestCreateAndDestroyUser (line 13) | func TestCreateAndDestroyUser(t *testing.T) {
  function TestDestroyUserErrors (line 31) | func TestDestroyUserErrors(t *testing.T) {
  function TestRenameUser (line 175) | func TestRenameUser(t *testing.T) {

FILE: hscontrol/db/versioncheck.go
  type DatabaseVersion (line 30) | type DatabaseVersion struct
  type semver (line 37) | type semver struct
    method String (line 43) | func (s semver) String() string {
  function parseVersion (line 50) | func parseVersion(s string) (semver, error) {
  function ensureDatabaseVersionTable (line 90) | func ensureDatabaseVersionTable(db *gorm.DB) error {
  function getDatabaseVersion (line 101) | func getDatabaseVersion(db *gorm.DB) (string, error) {
  function setDatabaseVersion (line 117) | func setDatabaseVersion(db *gorm.DB, version string) error {
  function isDev (line 144) | func isDev(version string) bool {
  function checkVersionUpgradePath (line 158) | func checkVersionUpgradePath(db *gorm.DB) error {

FILE: hscontrol/db/versioncheck_test.go
  function TestParseVersion (line 13) | func TestParseVersion(t *testing.T) {
  function TestSemverString (line 54) | func TestSemverString(t *testing.T) {
  function TestIsDev (line 59) | func TestIsDev(t *testing.T) {
  function versionTestDB (line 69) | func versionTestDB(t *testing.T) *gorm.DB {
  function TestSetAndGetDatabaseVersion (line 81) | func TestSetAndGetDatabaseVersion(t *testing.T) {
  function TestEnsureDatabaseVersionTableIdempotent (line 106) | func TestEnsureDatabaseVersionTableIdempotent(t *testing.T) {
  function TestCheckVersionUpgradePathDirect (line 121) | func TestCheckVersionUpgradePathDirect(t *testing.T) {
  function checkVersionUpgradePathFromVersions (line 270) | func checkVersionUpgradePathFromVersions(db *gorm.DB, currentVersion str...

FILE: hscontrol/debug.go
  method debugHTTPServer (line 15) | func (h *Headscale) debugHTTPServer() *http.Server {
  method debugBatcher (line 315) | func (h *Headscale) debugBatcher() string {
  type DebugBatcherInfo (line 374) | type DebugBatcherInfo struct
  type DebugBatcherNodeInfo (line 380) | type DebugBatcherNodeInfo struct
  method debugBatcherJSON (line 386) | func (h *Headscale) debugBatcherJSON() DebugBatcherInfo {

FILE: hscontrol/derp/derp.go
  function loadDERPMapFromPath (line 25) | func loadDERPMapFromPath(path string) (*tailcfg.DERPMap, error) {
  function loadDERPMapFromURL (line 44) | func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
  function mergeDERPMaps (line 81) | func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
  function GetDERPMap (line 100) | func GetDERPMap(cfg types.DERPConfig) (*tailcfg.DERPMap, error) {
  function shuffleDERPMap (line 130) | func shuffleDERPMap(dm *tailcfg.DERPMap) {
  function derpRandom (line 163) | func derpRandom() *rand.Rand {
  function resetDerpRandomForTesting (line 177) | func resetDerpRandomForTesting() {
  function shuffleRegionNoClone (line 185) | func shuffleRegionNoClone(r *tailcfg.DERPRegion) *tailcfg.DERPRegion {

FILE: hscontrol/derp/derp_test.go
  function TestShuffleDERPMapDeterministic (line 11) | func TestShuffleDERPMapDeterministic(t *testing.T) {
  function TestShuffleDERPMapEdgeCases (line 260) | func TestShuffleDERPMapEdgeCases(t *testing.T) {
  function TestShuffleDERPMapWithoutBaseDomain (line 312) | func TestShuffleDERPMapWithoutBaseDomain(t *testing.T) {

FILE: hscontrol/derp/server/derp_server.go
  constant fastStartHeader (line 36) | fastStartHeader  = "Derp-Fast-Start"
  constant DerpVerifyScheme (line 37) | DerpVerifyScheme = "headscale-derp-verify"
  type DERPServer (line 45) | type DERPServer struct
    method GenerateRegion (line 73) | func (d *DERPServer) GenerateRegion() (tailcfg.DERPRegion, error) {
    method DERPHandler (line 150) | func (d *DERPServer) DERPHandler(
    method serveWebsocket (line 185) | func (d *DERPServer) serveWebsocket(writer http.ResponseWriter, req *h...
    method servePlain (line 228) | func (d *DERPServer) servePlain(writer http.ResponseWriter, req *http....
    method ServeSTUN (line 357) | func (d *DERPServer) ServeSTUN() {
  function NewDERPServer (line 52) | func NewDERPServer(
  function DERPProbeHandler (line 284) | func DERPProbeHandler(
  function DERPBootstrapDNSHandler (line 313) | func DERPBootstrapDNSHandler(
  function serverSTUNListener (line 373) | func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
  function NewDERPVerifyTransport (line 428) | func NewDERPVerifyTransport(handleVerifyRequest func(*http.Request, io.W...
  type DERPVerifyTransport (line 434) | type DERPVerifyTransport struct
    method RoundTrip (line 438) | func (t *DERPVerifyTransport) RoundTrip(req *http.Request) (*http.Resp...

FILE: hscontrol/dns/extrarecords.go
  type ExtraRecordsMan (line 22) | type ExtraRecordsMan struct
    method Records (line 75) | func (e *ExtraRecordsMan) Records() []tailcfg.DNSRecord {
    method Run (line 82) | func (e *ExtraRecordsMan) Run() {
    method Close (line 139) | func (e *ExtraRecordsMan) Close() {
    method UpdateCh (line 144) | func (e *ExtraRecordsMan) UpdateCh() <-chan []tailcfg.DNSRecord {
    method updateRecords (line 148) | func (e *ExtraRecordsMan) updateRecords() {
  function NewExtraRecordsManager (line 34) | func NewExtraRecordsManager(path string) (*ExtraRecordsMan, error) {
  function readExtraRecordsFromPath (line 182) | func readExtraRecordsFromPath(path string) ([]tailcfg.DNSRecord, [32]byt...

FILE: hscontrol/grpcv1.go
  type headscaleV1APIServer (line 35) | type headscaleV1APIServer struct
    method CreateUser (line 46) | func (api headscaleV1APIServer) CreateUser(
    method RenameUser (line 68) | func (api headscaleV1APIServer) RenameUser(
    method DeleteUser (line 93) | func (api headscaleV1APIServer) DeleteUser(
    method ListUsers (line 113) | func (api headscaleV1APIServer) ListUsers(
    method CreatePreAuthKey (line 146) | func (api headscaleV1APIServer) CreatePreAuthKey(
    method ExpirePreAuthKey (line 187) | func (api headscaleV1APIServer) ExpirePreAuthKey(
    method DeletePreAuthKey (line 199) | func (api headscaleV1APIServer) DeletePreAuthKey(
    method ListPreAuthKeys (line 211) | func (api headscaleV1APIServer) ListPreAuthKeys(
    method RegisterNode (line 232) | func (api headscaleV1APIServer) RegisterNode(
    method GetNode (line 301) | func (api headscaleV1APIServer) GetNode(
    method SetTags (line 315) | func (api headscaleV1APIServer) SetTags(
    method SetApprovedRoutes (line 365) | func (api headscaleV1APIServer) SetApprovedRoutes(
    method DeleteNode (line 431) | func (api headscaleV1APIServer) DeleteNode(
    method ExpireNode (line 450) | func (api headscaleV1APIServer) ExpireNode(
    method RenameNode (line 504) | func (api headscaleV1APIServer) RenameNode(
    method ListNodes (line 525) | func (api headscaleV1APIServer) ListNodes(
    method BackfillNodeIPs (line 573) | func (api headscaleV1APIServer) BackfillNodeIPs(
    method CreateApiKey (line 591) | func (api headscaleV1APIServer) CreateApiKey(
    method getAPIKey (line 616) | func (api headscaleV1APIServer) getAPIKey(req apiKeyIdentifier) (*type...
    method ExpireApiKey (line 632) | func (api headscaleV1APIServer) ExpireApiKey(
    method ListApiKeys (line 649) | func (api headscaleV1APIServer) ListApiKeys(
    method DeleteApiKey (line 670) | func (api headscaleV1APIServer) DeleteApiKey(
    method GetPolicy (line 686) | func (api headscaleV1APIServer) GetPolicy(
    method SetPolicy (line 722) | func (api headscaleV1APIServer) SetPolicy(
    method DebugCreateNode (line 785) | func (api headscaleV1APIServer) DebugCreateNode(
    method Health (line 839) | func (api headscaleV1APIServer) Health(
    method AuthRegister (line 859) | func (api headscaleV1APIServer) AuthRegister(
    method AuthApprove (line 874) | func (api headscaleV1APIServer) AuthApprove(
    method AuthReject (line 893) | func (api headscaleV1APIServer) AuthReject(
    method mustEmbedUnimplementedHeadscaleServiceServer (line 914) | func (api headscaleV1APIServer) mustEmbedUnimplementedHeadscaleService...
  function newHeadscaleV1APIServer (line 40) | func newHeadscaleV1APIServer(h *Headscale) v1.HeadscaleServiceServer {
  function validateTag (line 418) | func validateTag(tag string) error {
  function nodesToProto (line 551) | func nodesToProto(state *state.State, nodes views.Slice[types.NodeView])...
  type apiKeyIdentifier (line 609) | type apiKeyIdentifier interface

FILE: hscontrol/grpcv1_test.go
  function Test_validateTag (line 18) | func Test_validateTag(t *testing.T) {
  function TestSetTags_Conversion (line 62) | func TestSetTags_Conversion(t *testing.T) {
  function TestSetTags_TaggedNode (line 161) | func TestSetTags_TaggedNode(t *testing.T) {
  function TestSetTags_CannotRemoveAllTags (line 218) | func TestSetTags_CannotRemoveAllTags(t *testing.T) {
  function TestDeleteUser_ReturnsProperChangeSignal (line 270) | func TestDeleteUser_ReturnsProperChangeSignal(t *testing.T) {
  function TestDeleteUser_TaggedNodeSurvives (line 291) | func TestDeleteUser_TaggedNodeSurvives(t *testing.T) {
  function TestExpireApiKey_ByID (line 362) | func TestExpireApiKey_ByID(t *testing.T) {
  function TestExpireApiKey_ByPrefix (line 394) | func TestExpireApiKey_ByPrefix(t *testing.T) {
  function TestDeleteApiKey_ByID (line 420) | func TestDeleteApiKey_ByID(t *testing.T) {
  function TestDeleteApiKey_ByPrefix (line 451) | func TestDeleteApiKey_ByPrefix(t *testing.T) {
  function TestExpireApiKey_NoIdentifier (line 482) | func TestExpireApiKey_NoIdentifier(t *testing.T) {
  function TestDeleteApiKey_NoIdentifier (line 497) | func TestDeleteApiKey_NoIdentifier(t *testing.T) {
  function TestExpireApiKey_BothIdentifiers (line 512) | func TestExpireApiKey_BothIdentifiers(t *testing.T) {
  function TestDeleteApiKey_BothIdentifiers (line 530) | func TestDeleteApiKey_BothIdentifiers(t *testing.T) {

FILE: hscontrol/handlers.go
  constant NoiseCapabilityVersion (line 31) | NoiseCapabilityVersion = 39
  constant reservedResponseHeaderSize (line 33) | reservedResponseHeaderSize = 4
  function httpError (line 37) | func httpError(w http.ResponseWriter, err error) {
  type HTTPError (line 48) | type HTTPError struct
    method Error (line 54) | func (e HTTPError) Error() string { return fmt.Sprintf("http error[%d]...
    method Unwrap (line 55) | func (e HTTPError) Unwrap() error { return e.Err }
  function NewHTTPError (line 58) | func NewHTTPError(code int, msg string, err error) HTTPError {
  function parseCapabilityVersion (line 68) | func parseCapabilityVersion(req *http.Request) (tailcfg.CapabilityVersio...
  method handleVerifyRequest (line 83) | func (h *Headscale) handleVerifyRequest(
  method VerifyHandler (line 118) | func (h *Headscale) VerifyHandler(
  method KeyHandler (line 138) | func (h *Headscale) KeyHandler(
  method HealthHandler (line 166) | func (h *Headscale) HealthHandler(
  method RobotsHandler (line 201) | func (h *Headscale) RobotsHandler(
  method VersionHandler (line 219) | func (h *Headscale) VersionHandler(
  type AuthProviderWeb (line 237) | type AuthProviderWeb struct
    method RegisterURL (line 247) | func (a *AuthProviderWeb) RegisterURL(authID types.AuthID) string {
    method AuthURL (line 254) | func (a *AuthProviderWeb) AuthURL(authID types.AuthID) string {
    method AuthHandler (line 261) | func (a *AuthProviderWeb) AuthHandler(
    method RegisterHandler (line 306) | func (a *AuthProviderWeb) RegisterHandler(
  function NewAuthProviderWeb (line 241) | func NewAuthProviderWeb(serverURL string) *AuthProviderWeb {
  function authIDFromRequest (line 284) | func authIDFromRequest(req *http.Request) (types.AuthID, error) {
  function FaviconHandler (line 329) | func FaviconHandler(writer http.ResponseWriter, req *http.Request) {
  function BlankHandler (line 335) | func BlankHandler(writer http.ResponseWriter, res *http.Request) {

FILE: hscontrol/mapper/batcher.go
  constant offlineNodeCleanupThreshold (line 31) | offlineNodeCleanupThreshold = 15 * time.Minute
  function NewBatcher (line 39) | func NewBatcher(batchTime time.Duration, workers int, mapper *mapper) *B...
  function NewBatcherAndMapper (line 53) | func NewBatcherAndMapper(cfg *types.Config, state *state.State) *Batcher {
  type nodeConnection (line 62) | type nodeConnection interface
  function generateMapResponse (line 73) | func generateMapResponse(nc nodeConnection, mapper *mapper, r change.Cha...
  function handleNodeChange (line 135) | func handleNodeChange(nc nodeConnection, mapper *mapper, r change.Change...
  type workResult (line 167) | type workResult struct
  type work (line 177) | type work struct
  type Batcher (line 197) | type Batcher struct
    method AddNode (line 226) | func (b *Batcher) AddNode(
    method RemoveNode (line 308) | func (b *Batcher) RemoveNode(id types.NodeID, c chan<- *tailcfg.MapRes...
    method AddWork (line 341) | func (b *Batcher) AddWork(r ...change.Change) {
    method Start (line 345) | func (b *Batcher) Start() {
    method Close (line 355) | func (b *Batcher) Close() {
    method doWork (line 385) | func (b *Batcher) doWork() {
    method worker (line 413) | func (b *Batcher) worker(workerID int) {
    method queueWork (line 500) | func (b *Batcher) queueWork(w work) {
    method addToBatch (line 513) | func (b *Batcher) addToBatch(changes ...change.Change) {
    method processBatchedChanges (line 586) | func (b *Batcher) processBatchedChanges() {
    method cleanupOfflineNodes (line 609) | func (b *Batcher) cleanupOfflineNodes() {
    method IsConnected (line 661) | func (b *Batcher) IsConnected(id types.NodeID) bool {
    method ConnectedMap (line 672) | func (b *Batcher) ConnectedMap() *xsync.Map[types.NodeID, bool] {
    method MapResponseFromChange (line 688) | func (b *Batcher) MapResponseFromChange(id types.NodeID, ch change.Cha...
    method Debug (line 710) | func (b *Batcher) Debug() map[types.NodeID]DebugNodeInfo {
    method DebugMapResponses (line 729) | func (b *Batcher) DebugMapResponses() (map[types.NodeID][]tailcfg.MapR...
    method WorkErrors (line 735) | func (b *Batcher) WorkErrors() int64 {
  type DebugNodeInfo (line 704) | type DebugNodeInfo struct

FILE: hscontrol/mapper/batcher_bench_test.go
  function BenchmarkConnectionEntry_Send (line 31) | func BenchmarkConnectionEntry_Send(b *testing.B) {
  function BenchmarkMultiChannelSend (line 44) | func BenchmarkMultiChannelSend(b *testing.B) {
  function BenchmarkComputePeerDiff (line 67) | func BenchmarkComputePeerDiff(b *testing.B) {
  function BenchmarkUpdateSentPeers (line 95) | func BenchmarkUpdateSentPeers(b *testing.B) {
  function benchBatcher (line 151) | func benchBatcher(nodeCount, bufferSize int) (*Batcher, map[types.NodeID...
  function BenchmarkAddToBatch_Broadcast (line 184) | func BenchmarkAddToBatch_Broadcast(b *testing.B) {
  function BenchmarkAddToBatch_Targeted (line 215) | func BenchmarkAddToBatch_Targeted(b *testing.B) {
  function BenchmarkAddToBatch_FullUpdate (line 253) | func BenchmarkAddToBatch_FullUpdate(b *testing.B) {
  function BenchmarkProcessBatchedChanges (line 277) | func BenchmarkProcessBatchedChanges(b *testing.B) {
  function BenchmarkBroadcastToN (line 313) | func BenchmarkBroadcastToN(b *testing.B) {
  function BenchmarkMultiChannelBroadcast (line 341) | func BenchmarkMultiChannelBroadcast(b *testing.B) {
  function BenchmarkConcurrentAddToBatch (line 389) | func BenchmarkConcurrentAddToBatch(b *testing.B) {
  function BenchmarkIsConnected (line 442) | func BenchmarkIsConnected(b *testing.B) {
  function BenchmarkConnectedMap (line 466) | func BenchmarkConnectedMap(b *testing.B) {
  function BenchmarkConnectionChurn (line 501) | func BenchmarkConnectionChurn(b *testing.B) {
  function BenchmarkConcurrentSendAndChurn (line 547) | func BenchmarkConcurrentSendAndChurn(b *testing.B) {
  function BenchmarkAddNode (line 618) | func BenchmarkAddNode(b *testing.B) {
  function BenchmarkFullPipeline (line 681) | func BenchmarkFullPipeline(b *testing.B) {
  function BenchmarkMapResponseFromChange (line 736) | func BenchmarkMapResponseFromChange(b *testing.B) {

FILE: hscontrol/mapper/batcher_concurrency_test.go
  type lightweightBatcher (line 37) | type lightweightBatcher struct
    method cleanup (line 79) | func (lb *lightweightBatcher) cleanup() {
  function setupLightweightBatcher (line 46) | func setupLightweightBatcher(t *testing.T, nodeCount, bufferSize int) *l...
  function countTotalPending (line 87) | func countTotalPending(b *Batcher) int {
  function countNodesPending (line 102) | func countNodesPending(b *Batcher) int {
  function getPendingForNode (line 121) | func getPendingForNode(b *Batcher, id types.NodeID) []change.Change {
  function runConcurrently (line 137) | func runConcurrently(t *testing.T, n int, fn func(i int)) int {
  function runConcurrentlyWithTimeout (line 168) | func runConcurrentlyWithTimeout(t *testing.T, n int, timeout time.Durati...
  function TestAddToBatch_ConcurrentTargeted_NoDataLoss (line 198) | func TestAddToBatch_ConcurrentTargeted_NoDataLoss(t *testing.T) {
  function TestAddToBatch_ConcurrentBroadcast (line 233) | func TestAddToBatch_ConcurrentBroadcast(t *testing.T) {
  function TestAddToBatch_FullUpdateOverrides (line 254) | func TestAddToBatch_FullUpdateOverrides(t *testing.T) {
  function TestAddToBatch_NodeRemovalCleanup (line 284) | func TestAddToBatch_NodeRemovalCleanup(t *testing.T) {
  function TestProcessBatchedChanges_QueuesWork (line 316) | func TestProcessBatchedChanges_QueuesWork(t *testing.T) {
  function TestProcessBatchedChanges_ConcurrentAdd_NoDataLoss (line 346) | func TestProcessBatchedChanges_ConcurrentAdd_NoDataLoss(t *testing.T) {
  function TestProcessBatchedChanges_EmptyPending (line 416) | func TestProcessBatchedChanges_EmptyPending(t *testing.T) {
  function TestProcessBatchedChanges_BundlesChangesPerNode (line 430) | func TestProcessBatchedChanges_BundlesChangesPerNode(t *testing.T) {
  function TestWorkMu_PreventsInterTickRace (line 475) | func TestWorkMu_PreventsInterTickRace(t *testing.T) {
  function TestCleanupOfflineNodes_RemovesOld (line 540) | func TestCleanupOfflineNodes_RemovesOld(t *testing.T) {
  function TestCleanupOfflineNodes_KeepsRecent (line 565) | func TestCleanupOfflineNodes_KeepsRecent(t *testing.T) {
  function TestCleanupOfflineNodes_KeepsActive (line 586) | func TestCleanupOfflineNodes_KeepsActive(t *testing.T) {
  function TestBatcher_CloseStopsWorkers (line 610) | func TestBatcher_CloseStopsWorkers(t *testing.T) {
  function TestBatcher_CloseMultipleTimes_DoubleClosePanic (line 646) | func TestBatcher_CloseMultipleTimes_DoubleClosePanic(t *testing.T) {
  function TestBatcher_MapResponseDuringShutdown (line 665) | func TestBatcher_MapResponseDuringShutdown(t *testing.T) {
  function TestBatcher_IsConnectedReflectsState (line 677) | func TestBatcher_IsConnectedReflectsState(t *testing.T) {
  function TestBatcher_ConnectedMapConsistency (line 706) | func TestBatcher_ConnectedMapConsistency(t *testing.T) {
  function TestBug3_CleanupOfflineNodes_TOCTOU (line 752) | func TestBug3_CleanupOfflineNodes_TOCTOU(t *testing.T) {
  function TestBug5_WorkerPanicKillsWorkerPermanently (line 850) | func TestBug5_WorkerPanicKillsWorkerPermanently(t *testing.T) {
  function TestBug6_StartCalledMultipleTimes_GoroutineLeak (line 928) | func TestBug6_StartCalledMultipleTimes_GoroutineLeak(t *testing.T) {
  function TestBug7_CleanupOfflineNodes_PendingChangesCleanedStructurally (line 991) | func TestBug7_CleanupOfflineNodes_PendingChangesCleanedStructurally(t *t...
  function TestBug8_SerialTimeoutUnderWriteLock (line 1043) | func TestBug8_SerialTimeoutUnderWriteLock(t *testing.T) {
  function TestBug1_BroadcastNoDataLoss (line 1100) | func TestBug1_BroadcastNoDataLoss(t *testing.T) {
  function TestScale1000_AddToBatch_Broadcast (line 1145) | func TestScale1000_AddToBatch_Broadcast(t *testing.T) {
  function TestScale1000_ProcessBatchedWithConcurrentAdd (line 1178) | func TestScale1000_ProcessBatchedWithConcurrentAdd(t *testing.T) {
  function TestScale1000_MultiChannelBroadcast (line 1233) | func TestScale1000_MultiChannelBroadcast(t *testing.T) {
  function TestScale1000_ConnectionChurn (line 1336) | func TestScale1000_ConnectionChurn(t *testing.T) {
  function TestScale1000_ConcurrentAddRemove (line 1438) | func TestScale1000_ConcurrentAddRemove(t *testing.T) {
  function TestScale1000_IsConnectedConsistency (line 1482) | func TestScale1000_IsConnectedConsistency(t *testing.T) {
  function TestScale1000_BroadcastDuringNodeChurn (line 1551) | func TestScale1000_BroadcastDuringNodeChurn(t *testing.T) {
  function TestScale1000_WorkChannelSaturation (line 1638) | func TestScale1000_WorkChannelSaturation(t *testing.T) {
  function TestScale1000_FullUpdate_AllNodesGetPending (line 1716) | func TestScale1000_FullUpdate_AllNodesGetPending(t *testing.T) {
  function TestScale1000_AllToAll_FullPipeline (line 1750) | func TestScale1000_AllToAll_FullPipeline(t *testing.T) {

FILE: hscontrol/mapper/batcher_scale_bench_test.go
  function BenchmarkScale_IsConnected (line 42) | func BenchmarkScale_IsConnected(b *testing.B) {
  function BenchmarkScale_AddToBatch_Targeted (line 67) | func BenchmarkScale_AddToBatch_Targeted(b *testing.B) {
  function BenchmarkScale_ConnectionChurn (line 106) | func BenchmarkScale_ConnectionChurn(b *testing.B) {
  function BenchmarkScale_AddToBatch_Broadcast (line 154) | func BenchmarkScale_AddToBatch_Broadcast(b *testing.B) {
  function BenchmarkScale_AddToBatch_FullUpdate (line 184) | func BenchmarkScale_AddToBatch_FullUpdate(b *testing.B) {
  function BenchmarkScale_ProcessBatchedChanges (line 207) | func BenchmarkScale_ProcessBatchedChanges(b *testing.B) {
  function BenchmarkScale_BroadcastToN (line 240) | func BenchmarkScale_BroadcastToN(b *testing.B) {
  function BenchmarkScale_SendToAll (line 269) | func BenchmarkScale_SendToAll(b *testing.B) {
  function BenchmarkScale_ConnectedMap (line 302) | func BenchmarkScale_ConnectedMap(b *testing.B) {
  function BenchmarkScale_ComputePeerDiff (line 337) | func BenchmarkScale_ComputePeerDiff(b *testing.B) {
  function BenchmarkScale_UpdateSentPeers_Full (line 368) | func BenchmarkScale_UpdateSentPeers_Full(b *testing.B) {
  function BenchmarkScale_UpdateSentPeers_Incremental (line 393) | func BenchmarkScale_UpdateSentPeers_Incremental(b *testing.B) {
  function BenchmarkScale_MultiChannelBroadcast (line 430) | func BenchmarkScale_MultiChannelBroadcast(b *testing.B) {
  function BenchmarkScale_ConcurrentAddToBatch (line 482) | func BenchmarkScale_ConcurrentAddToBatch(b *testing.B) {
  function BenchmarkScale_ConcurrentSendAndChurn (line 531) | func BenchmarkScale_ConcurrentSendAndChurn(b *testing.B) {
  function BenchmarkScale_MixedWorkload (line 604) | func BenchmarkScale_MixedWorkload(b *testing.B) {
  function BenchmarkScale_AddAllNodes (line 720) | func BenchmarkScale_AddAllNodes(b *testing.B) {
  function BenchmarkScale_SingleAddNode (line 783) | func BenchmarkScale_SingleAddNode(b *testing.B) {
  function BenchmarkScale_MapResponse_DERPMap (line 849) | func BenchmarkScale_MapResponse_DERPMap(b *testing.B) {
  function BenchmarkScale_MapResponse_FullUpdate (line 901) | func BenchmarkScale_MapResponse_FullUpdate(b *testing.B) {

FILE: hscontrol/mapper/batcher_test.go
  type batcherFunc (line 28) | type batcherFunc
  type batcherTestCase (line 31) | type batcherTestCase struct
  type testBatcherWrapper (line 38) | type testBatcherWrapper struct
    method AddNode (line 48) | func (t *testBatcherWrapper) AddNode(id types.NodeID, c chan<- *tailcf...
    method RemoveNode (line 76) | func (t *testBatcherWrapper) RemoveNode(id types.NodeID, c chan<- *tai...
  function wrapBatcherForTest (line 103) | func wrapBatcherForTest(b *Batcher, state *state.State) *testBatcherWrap...
  function emptyCache (line 113) | func emptyCache() *zcache.Cache[types.AuthID, types.AuthRequest] {
  constant testUserCount (line 120) | testUserCount    = 3
  constant testNodesPerUser (line 121) | testNodesPerUser = 2
  constant testTimeout (line 124) | testTimeout     = 120 * time.Second
  constant updateTimeout (line 125) | updateTimeout   = 5 * time.Second
  constant deadlockTimeout (line 126) | deadlockTimeout = 30 * time.Second
  constant normalBufferSize (line 129) | normalBufferSize = 50
  constant smallBufferSize (line 130) | smallBufferSize  = 3
  constant tinyBufferSize (line 131) | tinyBufferSize   = 1
  constant largeBufferSize (line 132) | largeBufferSize  = 200
  type TestData (line 136) | type TestData struct
  type node (line 145) | type node struct
    method start (line 401) | func (n *node) start() {
    method cleanup (line 471) | func (n *node) cleanup() NodeStats {
  function setupBatcherWithTestData (line 167) | func setupBatcherWithTestData(
  type UpdateStats (line 286) | type UpdateStats struct
  type updateTracker (line 293) | type updateTracker struct
    method recordUpdate (line 306) | func (ut *updateTracker) recordUpdate(nodeID types.NodeID, updateSize ...
    method getStats (line 323) | func (ut *updateTracker) getStats(nodeID types.NodeID) UpdateStats {
    method getAllStats (line 340) | func (ut *updateTracker) getAllStats() map[types.NodeID]UpdateStats {
  function newUpdateTracker (line 299) | func newUpdateTracker() *updateTracker {
  function assertDERPMapResponse (line 356) | func assertDERPMapResponse(t *testing.T, resp *tailcfg.MapResponse) {
  function assertOnlineMapResponse (line 364) | func assertOnlineMapResponse(t *testing.T, resp *tailcfg.MapResponse, ex...
  type UpdateInfo (line 381) | type UpdateInfo struct
  function parseUpdateAndAnalyze (line 390) | func parseUpdateAndAnalyze(resp *tailcfg.MapResponse) UpdateInfo {
  type NodeStats (line 462) | type NodeStats struct
  function validateUpdateContent (line 487) | func validateUpdateContent(resp *tailcfg.MapResponse) (bool, string) {
  function TestEnhancedNodeTracking (line 497) | func TestEnhancedNodeTracking(t *testing.T) {
  function TestEnhancedTrackingWithBatcher (line 535) | func TestEnhancedTrackingWithBatcher(t *testing.T) {
  function TestBatcherScalabilityAllToAll (line 585) | func TestBatcherScalabilityAllToAll(t *testing.T) {
  function TestBatcherBasicOperations (line 817) | func TestBatcherBasicOperations(t *testing.T) {
  function drainChannelTimeout (line 916) | func drainChannelTimeout(ch <-chan *tailcfg.MapResponse, timeout time.Du...
  function TestBatcherWorkQueueBatching (line 1040) | func TestBatcherWorkQueueBatching(t *testing.T) {
  function TestBatcherWorkerChannelSafety (line 1131) | func TestBatcherWorkerChannelSafety(t *testing.T) {
  function TestBatcherConcurrentClients (line 1261) | func TestBatcherConcurrentClients(t *testing.T) {
  function TestBatcherFullPeerUpdates (line 1538) | func TestBatcherFullPeerUpdates(t *testing.T) {
  function TestBatcherRapidReconnection (line 1687) | func TestBatcherRapidReconnection(t *testing.T) {
  function TestBatcherMultiConnection (line 1816) | func TestBatcherMultiConnection(t *testing.T) {
  function TestNodeDeletedWhileChangesPending (line 2047) | func TestNodeDeletedWhileChangesPending(t *testing.T) {
  function TestRemoveNodeChannelAlreadyRemoved (line 2162) | func TestRemoveNodeChannelAlreadyRemoved(t *testing.T) {
  function unwrapBatcher (line 2226) | func unwrapBatcher(b *testBatcherWrapper) *Batcher {

FILE: hscontrol/mapper/batcher_unit_test.go
  type mockNodeConnection (line 30) | type mockNodeConnection struct
    method withSendError (line 55) | func (m *mockNodeConnection) withSendError(err error) *mockNodeConnect...
    method nodeID (line 60) | func (m *mockNodeConnection) nodeID() types.NodeID               { ret...
    method version (line 61) | func (m *mockNodeConnection) version() tailcfg.CapabilityVersion { ret...
    method send (line 63) | func (m *mockNodeConnection) send(data *tailcfg.MapResponse) error {
    method computePeerDiff (line 75) | func (m *mockNodeConnection) computePeerDiff(currentPeers []tailcfg.No...
    method updateSentPeers (line 94) | func (m *mockNodeConnection) updateSentPeers(resp *tailcfg.MapResponse) {
    method getSent (line 117) | func (m *mockNodeConnection) getSent() []*tailcfg.MapResponse {
  function newMockNodeConnection (line 46) | func newMockNodeConnection(id types.NodeID) *mockNodeConnection {
  function testMapResponse (line 129) | func testMapResponse() *tailcfg.MapResponse {
  function testMapResponseWithPeers (line 138) | func testMapResponseWithPeers(peerIDs ...tailcfg.NodeID) *tailcfg.MapRes...
  function ids (line 150) | func ids(nodeIDs ...tailcfg.NodeID) []tailcfg.NodeID {
  function expectReceive (line 155) | func expectReceive(t *testing.T, ch <-chan *tailcfg.MapResponse, msg str...
  function expectNoReceive (line 170) | func expectNoReceive(t *testing.T, ch <-chan *tailcfg.MapResponse, timeo...
  function makeConnectionEntry (line 182) | func makeConnectionEntry(id string, ch chan<- *tailcfg.MapResponse) *con...
  function TestConnectionEntry_SendSuccess (line 198) | func TestConnectionEntry_SendSuccess(t *testing.T) {
  function TestConnectionEntry_SendNilData (line 215) | func TestConnectionEntry_SendNilData(t *testing.T) {
  function TestConnectionEntry_SendTimeout (line 225) | func TestConnectionEntry_SendTimeout(t *testing.T) {
  function TestConnectionEntry_SendClosed (line 240) | func TestConnectionEntry_SendClosed(t *testing.T) {
  function TestConnectionEntry_SendUpdatesLastUsed (line 254) | func TestConnectionEntry_SendUpdatesLastUsed(t *testing.T) {
  function TestMultiChannelSend_AllSuccess (line 273) | func TestMultiChannelSend_AllSuccess(t *testing.T) {
  function TestMultiChannelSend_PartialFailure (line 298) | func TestMultiChannelSend_PartialFailure(t *testing.T) {
  function TestMultiChannelSend_AllFail (line 321) | func TestMultiChannelSend_AllFail(t *testing.T) {
  function TestMultiChannelSend_ZeroConnections (line 337) | func TestMultiChannelSend_ZeroConnections(t *testing.T) {
  function TestMultiChannelSend_NilData (line 346) | func TestMultiChannelSend_NilData(t *testing.T) {
  function TestMultiChannelSend_FailedConnectionRemoved (line 357) | func TestMultiChannelSend_FailedConnectionRemoved(t *testing.T) {
  function TestMultiChannelSend_UpdateCount (line 379) | func TestMultiChannelSend_UpdateCount(t *testing.T) {
  function TestMultiChannelClose_MarksEntriesClosed (line 397) | func TestMultiChannelClose_MarksEntriesClosed(t *testing.T) {
  function TestMultiChannelClose_PreventsSendPanic (line 415) | func TestMultiChannelClose_PreventsSendPanic(t *testing.T) {
  function TestMultiChannelNodeConn_AddRemoveConnections (line 434) | func TestMultiChannelNodeConn_AddRemoveConnections(t *testing.T) {
  function TestMultiChannelNodeConn_Version (line 466) | func TestMultiChannelNodeConn_Version(t *testing.T) {
  function TestComputePeerDiff (line 485) | func TestComputePeerDiff(t *testing.T) {
  function TestUpdateSentPeers (line 563) | func TestUpdateSentPeers(t *testing.T) {
  function TestGenerateMapResponse_EmptyChange (line 678) | func TestGenerateMapResponse_EmptyChange(t *testing.T) {
  function TestGenerateMapResponse_InvalidNodeID (line 687) | func TestGenerateMapResponse_InvalidNodeID(t *testing.T) {
  function TestGenerateMapResponse_NilMapper (line 696) | func TestGenerateMapResponse_NilMapper(t *testing.T) {
  function TestGenerateMapResponse_SelfOnlyOtherNode (line 705) | func TestGenerateMapResponse_SelfOnlyOtherNode(t *testing.T) {
  function TestGenerateMapResponse_SelfOnlySameNode (line 717) | func TestGenerateMapResponse_SelfOnlySameNode(t *testing.T) {
  function TestHandleNodeChange_NilConnection (line 733) | func TestHandleNodeChange_NilConnection(t *testing.T) {
  function TestHandleNodeChange_EmptyChange (line 739) | func TestHandleNodeChange_EmptyChange(t *testing.T) {
  function TestHandleNodeChange_SendError (line 750) | func TestHandleNodeChange_SendError(t *testing.T) {
  function TestHandleNodeChange_NilDataNoSend (line 762) | func TestHandleNodeChange_NilDataNoSend(t *testing.T) {
  function TestConnectionEntry_ConcurrentSends (line 777) | func TestConnectionEntry_ConcurrentSends(t *testing.T) {
  function TestConnectionEntry_ConcurrentSendAndClose (line 814) | func TestConnectionEntry_ConcurrentSendAndClose(t *testing.T) {
  function TestMultiChannelSend_ConcurrentAddAndSend (line 856) | func TestMultiChannelSend_ConcurrentAddAndSend(t *testing.T) {
  function TestMultiChannelSend_ConcurrentRemoveAndSend (line 903) | func TestMultiChannelSend_ConcurrentRemoveAndSend(t *testing.T) {
  function TestConnectionEntry_SendFastPath_TimerStopped (line 963) | func TestConnectionEntry_SendFastPath_TimerStopped(t *testing.T) {
  function TestBatcher_CloseWaitsForWorkers (line 1003) | func TestBatcher_CloseWaitsForWorkers(t *testing.T) {
  function TestBatcher_CloseThenStartIsNoop (line 1033) | func TestBatcher_CloseThenStartIsNoop(t *testing.T) {
  function TestBatcher_CloseStopsTicker (line 1056) | func TestBatcher_CloseStopsTicker(t *testing.T) {
  function TestBatcher_CloseBeforeStart_DoesNotHang (line 1081) | func TestBatcher_CloseBeforeStart_DoesNotHang(t *testing.T) {
  function TestBatcher_QueueWorkAfterClose_DoesNotHang (line 1103) | func TestBatcher_QueueWorkAfterClose_DoesNotHang(t *testing.T) {
  function TestIsConnected_FalseAfterAddNodeFailure (line 1127) | func TestIsConnected_FalseAfterAddNodeFailure(t *testing.T) {
  function TestRemoveConnectionAtIndex_NilsTrailingSlot (line 1156) | func TestRemoveConnectionAtIndex_NilsTrailingSlot(t *testing.T) {

FILE: hscontrol/mapper/builder.go
  type MapResponseBuilder (line 16) | type MapResponseBuilder struct
    method addError (line 51) | func (b *MapResponseBuilder) addError(err error) {
    method hasErrors (line 58) | func (b *MapResponseBuilder) hasErrors() bool {
    method WithCapabilityVersion (line 63) | func (b *MapResponseBuilder) WithCapabilityVersion(capVer tailcfg.Capa...
    method WithSelfNode (line 69) | func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
    method WithDebugType (line 94) | func (b *MapResponseBuilder) WithDebugType(t debugType) *MapResponseBu...
    method WithDERPMap (line 103) | func (b *MapResponseBuilder) WithDERPMap() *MapResponseBuilder {
    method WithDomain (line 109) | func (b *MapResponseBuilder) WithDomain() *MapResponseBuilder {
    method WithCollectServicesDisabled (line 115) | func (b *MapResponseBuilder) WithCollectServicesDisabled() *MapRespons...
    method WithDebugConfig (line 122) | func (b *MapResponseBuilder) WithDebugConfig() *MapResponseBuilder {
    method WithSSHPolicy (line 131) | func (b *MapResponseBuilder) WithSSHPolicy() *MapResponseBuilder {
    method WithDNSConfig (line 150) | func (b *MapResponseBuilder) WithDNSConfig() *MapResponseBuilder {
    method WithUserProfiles (line 163) | func (b *MapResponseBuilder) WithUserProfiles(peers views.Slice[types....
    method WithPacketFilters (line 176) | func (b *MapResponseBuilder) WithPacketFilters() *MapResponseBuilder {
    method WithPeers (line 204) | func (b *MapResponseBuilder) WithPeers(peers views.Slice[types.NodeVie...
    method WithPeerChanges (line 217) | func (b *MapResponseBuilder) WithPeerChanges(peers views.Slice[types.N...
    method buildTailPeers (line 230) | func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.No...
    method WithPeerChangedPatch (line 273) | func (b *MapResponseBuilder) WithPeerChangedPatch(changes []*tailcfg.P...
    method WithPeersRemoved (line 279) | func (b *MapResponseBuilder) WithPeersRemoved(removedIDs ...types.Node...
    method Build (line 291) | func (b *MapResponseBuilder) Build() (*tailcfg.MapResponse, error) {
  type debugType (line 26) | type debugType
  constant fullResponseDebug (line 29) | fullResponseDebug   debugType = "full"
  constant selfResponseDebug (line 30) | selfResponseDebug   debugType = "self"
  constant changeResponseDebug (line 31) | changeResponseDebug debugType = "change"
  constant policyResponseDebug (line 32) | policyResponseDebug debugType = "policy"
  method NewMapResponseBuilder (line 36) | func (m *mapper) NewMapResponseBuilder(nodeID types.NodeID) *MapResponse...

FILE: hscontrol/mapper/builder_test.go
  function TestMapResponseBuilder_Basic (line 14) | func TestMapResponseBuilder_Basic(t *testing.T) {
  function TestMapResponseBuilder_WithCapabilityVersion (line 41) | func TestMapResponseBuilder_WithCapabilityVersion(t *testing.T) {
  function TestMapResponseBuilder_WithDomain (line 59) | func TestMapResponseBuilder_WithDomain(t *testing.T) {
  function TestMapResponseBuilder_WithCollectServicesDisabled (line 81) | func TestMapResponseBuilder_WithCollectServicesDisabled(t *testing.T) {
  function TestMapResponseBuilder_WithDebugConfig (line 100) | func TestMapResponseBuilder_WithDebugConfig(t *testing.T) {
  function TestMapResponseBuilder_WithPeerChangedPatch (line 143) | func TestMapResponseBuilder_WithPeerChangedPatch(t *testing.T) {
  function TestMapResponseBuilder_WithPeersRemoved (line 170) | func TestMapResponseBuilder_WithPeersRemoved(t *testing.T) {
  function TestMapResponseBuilder_ErrorHandling (line 193) | func TestMapResponseBuilder_ErrorHandling(t *testing.T) {
  function TestMapResponseBuilder_ChainedCalls (line 223) | func TestMapResponseBuilder_ChainedCalls(t *testing.T) {
  function TestMapResponseBuilder_MultipleWithPeersRemoved (line 259) | func TestMapResponseBuilder_MultipleWithPeersRemoved(t *testing.T) {
  function TestMapResponseBuilder_EmptyPeerChangedPatch (line 282) | func TestMapResponseBuilder_EmptyPeerChangedPatch(t *testing.T) {
  function TestMapResponseBuilder_NilPeerChangedPatch (line 299) | func TestMapResponseBuilder_NilPeerChangedPatch(t *testing.T) {
  function TestMapResponseBuilder_MultipleErrors (line 316) | func TestMapResponseBuilder_MultipleErrors(t *testing.T) {

FILE: hscontrol/mapper/mapper.go
  constant nextDNSDoHPrefix (line 26) | nextDNSDoHPrefix     = "https://dns.nextdns.io"
  constant debugMapResponsePerm (line 27) | debugMapResponsePerm = 0o755
  type mapper (line 43) | type mapper struct
    method fullMapResponse (line 159) | func (m *mapper) fullMapResponse(
    method selfMapResponse (line 181) | func (m *mapper) selfMapResponse(
    method policyChangeResponse (line 213) | func (m *mapper) policyChangeResponse(
    method buildFromChange (line 251) | func (m *mapper) buildFromChange(
    method debugMapResponses (line 347) | func (m *mapper) debugMapResponses() (map[types.NodeID][]tailcfg.MapRe...
  type patch (line 53) | type patch struct
  function newMapper (line 58) | func newMapper(
  function generateUserProfiles (line 72) | func generateUserProfiles(
  function generateDNSConfig (line 117) | func generateDNSConfig(
  function addNextDNSMetadata (line 139) | func addNextDNSMetadata(resolvers []*dnstype.Resolver, node types.NodeVi...
  function writeDebugMapResponse (line 314) | func writeDebugMapResponse(
  function ReadMapResponsesFromDirectory (line 355) | func ReadMapResponsesFromDirectory(dir string) (map[types.NodeID][]tailc...

FILE: hscontrol/mapper/mapper_test.go
  function TestDNSConfigMapResponse (line 20) | func TestDNSConfigMapResponse(t *testing.T) {

FILE: hscontrol/mapper/node_conn.go
  type connectionEntry (line 20) | type connectionEntry struct
    method send (line 333) | func (entry *connectionEntry) send(data *tailcfg.MapResponse) error {
  type multiChannelNodeConn (line 31) | type multiChannelNodeConn struct
    method close (line 88) | func (mc *multiChannelNodeConn) close() {
    method stopConnection (line 101) | func (mc *multiChannelNodeConn) stopConnection(conn *connectionEntry) {
    method removeConnectionAtIndexLocked (line 112) | func (mc *multiChannelNodeConn) removeConnectionAtIndexLocked(i int, s...
    method addConnection (line 126) | func (mc *multiChannelNodeConn) addConnection(entry *connectionEntry) {
    method removeConnectionByChannel (line 137) | func (mc *multiChannelNodeConn) removeConnectionByChannel(c chan<- *ta...
    method hasActiveConnections (line 156) | func (mc *multiChannelNodeConn) hasActiveConnections() bool {
    method getActiveConnectionCount (line 164) | func (mc *multiChannelNodeConn) getActiveConnectionCount() int {
    method markConnected (line 173) | func (mc *multiChannelNodeConn) markConnected() {
    method markDisconnected (line 180) | func (mc *multiChannelNodeConn) markDisconnected() {
    method isConnected (line 187) | func (mc *multiChannelNodeConn) isConnected() bool {
    method offlineDuration (line 197) | func (mc *multiChannelNodeConn) offlineDuration() time.Duration {
    method appendPending (line 208) | func (mc *multiChannelNodeConn) appendPending(changes ...change.Change) {
    method drainPending (line 216) | func (mc *multiChannelNodeConn) drainPending() []change.Change {
    method send (line 233) | func (mc *multiChannelNodeConn) send(data *tailcfg.MapResponse) error {
    method nodeID (line 368) | func (mc *multiChannelNodeConn) nodeID() types.NodeID {
    method version (line 374) | func (mc *multiChannelNodeConn) version() tailcfg.CapabilityVersion {
    method updateSentPeers (line 388) | func (mc *multiChannelNodeConn) updateSentPeers(resp *tailcfg.MapRespo...
    method computePeerDiff (line 415) | func (mc *multiChannelNodeConn) computePeerDiff(currentPeers []tailcfg...
    method change (line 436) | func (mc *multiChannelNodeConn) change(r change.Change) error {
  function generateConnectionID (line 74) | func generateConnectionID() string {
  function newMultiChannelNodeConn (line 79) | func newMultiChannelNodeConn(id types.NodeID, mapper *mapper) *multiChan...

FILE: hscontrol/mapper/tail_test.go
  function TestTailNode (line 18) | func TestTailNode(t *testing.T) {
  function TestNodeExpiry (line 238) | func TestNodeExpiry(t *testing.T) {

FILE: hscontrol/metrics.go
  function init (line 17) | func init() {
  constant prometheusNamespace (line 27) | prometheusNamespace = "headscale"
  function prometheusMiddleware (line 59) | func prometheusMiddleware(next http.Handler) http.Handler {
  type respWriterProm (line 81) | type respWriterProm struct
    method WriteHeader (line 89) | func (r *respWriterProm) WriteHeader(code int) {
    method Write (line 95) | func (r *respWriterProm) Write(b []byte) (int, error) {

FILE: hscontrol/noise.go
  constant ts2021UpgradePath (line 41) | ts2021UpgradePath = "/ts2021"
  constant earlyPayloadMagic (line 48) | earlyPayloadMagic = "\xff\xff\xffTS"
  constant noiseBodyLimit (line 54) | noiseBodyLimit int64 = 1048576
  type noiseServer (line 57) | type noiseServer struct
    method earlyNoise (line 204) | func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Write...
    method NotImplementedHandler (line 270) | func (ns *noiseServer) NotImplementedHandler(writer http.ResponseWrite...
    method SSHActionHandler (line 314) | func (ns *noiseServer) SSHActionHandler(
    method sshAction (line 386) | func (ns *noiseServer) sshAction(
    method sshActionHoldAndDelegate (line 434) | func (ns *noiseServer) sshActionHoldAndDelegate(
    method sshActionFollowUp (line 488) | func (ns *noiseServer) sshActionFollowUp(
    method PollNetMapHandler (line 550) | func (ns *noiseServer) PollNetMapHandler(
    method RegistrationHandler (line 590) | func (ns *noiseServer) RegistrationHandler(
    method getAndValidateNode (line 650) | func (ns *noiseServer) getAndValidateNode(mapRequest tailcfg.MapReques...
  method NoiseUpgradeHandler (line 73) | func (h *Headscale) NoiseUpgradeHandler(
  function unsupportedClientError (line 200) | func unsupportedClientError(version tailcfg.CapabilityVersion) error {
  function isSupportedVersion (line 241) | func isSupportedVersion(version tailcfg.CapabilityVersion) bool {
  function rejectUnsupported (line 245) | func rejectUnsupported(
  function urlParam (line 275) | func urlParam[T any](req *http.Request, key string) (T, error) {
  function regErr (line 585) | func regErr(err error) *tailcfg.RegisterResponse {

FILE: hscontrol/noise_test.go
  function newNoiseRouterWithBodyLimit (line 22) | func newNoiseRouterWithBodyLimit(readBody *[]byte, readErr *error) http....
  function TestNoiseBodyLimit_MapEndpoint (line 48) | func TestNoiseBodyLimit_MapEndpoint(t *testing.T) {
  function TestNoiseBodyLimit_RegisterEndpoint (line 93) | func TestNoiseBodyLimit_RegisterEndpoint(t *testing.T) {
  function TestNoiseBodyLimit_AtExactLimit (line 138) | func TestNoiseBodyLimit_AtExactLimit(t *testing.T) {
  function TestPollNetMapHandler_OversizedBody (line 160) | func TestPollNetMapHandler_OversizedBody(t *testing.T) {
  function TestRegistrationHandler_OversizedBody (line 179) | func TestRegistrationHandler_OversizedBody(t *testing.T) {

FILE: hscontrol/oidc.go
  constant randomByteSize (line 26) | randomByteSize           = 16
  constant defaultOAuthOptionsCount (line 27) | defaultOAuthOptionsCount = 3
  constant authCacheExpiration (line 28) | authCacheExpiration      = time.Minute * 15
  constant authCacheCleanup (line 29) | authCacheCleanup         = time.Minute * 20
  type AuthInfo (line 47) | type AuthInfo struct
  type AuthProviderOIDC (line 53) | type AuthProviderOIDC struct
    method AuthURL (line 103) | func (a *AuthProviderOIDC) AuthURL(authID types.AuthID) string {
    method AuthHandler (line 110) | func (a *AuthProviderOIDC) AuthHandler(
    method RegisterURL (line 117) | func (a *AuthProviderOIDC) RegisterURL(authID types.AuthID) string {
    method RegisterHandler (line 127) | func (a *AuthProviderOIDC) RegisterHandler(
    method authHandler (line 136) | func (a *AuthProviderOIDC) authHandler(
    method OIDCCallbackHandler (line 204) | func (a *AuthProviderOIDC) OIDCCallbackHandler(
    method determineNodeExpiry (line 386) | func (a *AuthProviderOIDC) determineNodeExpiry(idTokenExpiration time....
    method getOauth2Token (line 408) | func (a *AuthProviderOIDC) getOauth2Token(
    method extractIDToken (line 435) | func (a *AuthProviderOIDC) extractIDToken(
    method getAuthInfoFromState (line 550) | func (a *AuthProviderOIDC) getAuthInfoFromState(state string) *AuthInfo {
    method createOrUpdateUserFromClaim (line 559) | func (a *AuthProviderOIDC) createOrUpdateUserFromClaim(
    method handleRegistration (line 602) | func (a *AuthProviderOIDC) handleRegistration(
  function NewAuthProviderOIDC (line 66) | func NewAuthProviderOIDC(
  function extractCodeAndStateParamFromRequest (line 394) | func extractCodeAndStateParamFromRequest(
  function validateOIDCAllowedDomains (line 456) | func validateOIDCAllowedDomains(
  function validateOIDCAllowedGroups (line 474) | func validateOIDCAllowedGroups(
  function validateOIDCAllowedUsers (line 489) | func validateOIDCAllowedUsers(
  function doOIDCAuthorization (line 514) | func doOIDCAuthorization(
  function renderRegistrationSuccessTemplate (line 639) | func renderRegistrationSuccessTemplate(
  function renderAuthSuccessTemplate (line 659) | func renderAuthSuccessTemplate(
  function getCookieName (line 674) | func getCookieName(baseName, value string) string {
  function setCSRFCookie (line 678) | func setCSRFCookie(w http.ResponseWriter, r *http.Request, name string) ...

FILE: hscontrol/oidc_template_test.go
  function TestAuthSuccessTemplate (line 10) | func TestAuthSuccessTemplate(t *testing.T) {

FILE: hscontrol/oidc_test.go
  function TestDoOIDCAuthorization (line 9) | func TestDoOIDCAuthorization(t *testing.T) {

FILE: hscontrol/platform_config.go
  method WindowsConfigMessage (line 16) | func (h *Headscale) WindowsConfigMessage(
  method AppleConfigMessage (line 26) | func (h *Headscale) AppleConfigMessage(
  method ApplePlatformConfig (line 35) | func (h *Headscale) ApplePlatformConfig(
  type AppleMobileConfig (line 108) | type AppleMobileConfig struct
  type AppleMobilePlatformConfig (line 114) | type AppleMobilePlatformConfig struct

FILE: hscontrol/policy/matcher/matcher.go
  type Match (line 14) | type Match struct
    method DebugString (line 19) | func (m *Match) DebugString() string {
    method SrcsContainsIPs (line 83) | func (m *Match) SrcsContainsIPs(ips ...netip.Addr) bool {
    method DestsContainsIP (line 87) | func (m *Match) DestsContainsIP(ips ...netip.Addr) bool {
    method SrcsOverlapsPrefixes (line 91) | func (m *Match) SrcsOverlapsPrefixes(prefixes ...netip.Prefix) bool {
    method DestsOverlapsPrefixes (line 95) | func (m *Match) DestsOverlapsPrefixes(prefixes ...netip.Prefix) bool {
    method DestsIsTheInternet (line 104) | func (m *Match) DestsIsTheInternet() bool {
  function MatchesFromFilterRules (line 38) | func MatchesFromFilterRules(rules []tailcfg.FilterRule) []Match {
  function MatchFromFilterRule (line 47) | func MatchFromFilterRule(rule tailcfg.FilterRule) Match {
  function MatchFromStrings (line 56) | func MatchFromStrings(sources, destinations []string) Match {

FILE: hscontrol/policy/pm.go
  type PolicyManager (line 14) | type PolicyManager interface
  function NewPolicyManager (line 44) | func NewPolicyManager(pol []byte, users []types.User, nodes views.Slice[...
  function PolicyManagersForTest (line 61) | func PolicyManagersForTest(pol []byte, users []types.User, nodes views.S...
  function PolicyManagerFuncsForTest (line 76) | func PolicyManagerFuncsForTest(pol []byte) []func([]types.User, views.Sl...

FILE: hscontrol/policy/policy.go
  function ReduceNodes (line 16) | func ReduceNodes(
  function ReduceRoutes (line 37) | func ReduceRoutes(
  function BuildPeerMap (line 54) | func BuildPeerMap(
  function ApproveRoutesWithPolicy (line 89) | func ApproveRoutesWithPolicy(pm PolicyManager, nv types.NodeView, curren...

FILE: hscontrol/policy/policy_autoapprove_test.go
  function TestApproveRoutesWithPolicy_NeverRemovesApprovedRoutes (line 19) | func TestApproveRoutesWithPolicy_NeverRemovesApprovedRoutes(t *testing.T) {
  function TestApproveRoutesWithPolicy_NilAndEmptyCases (line 209) | func TestApproveRoutesWithPolicy_NilAndEmptyCases(t *testing.T) {

FILE: hscontrol/policy/policy_route_approval_test.go
  function TestApproveRoutesWithPolicy_NeverRemovesRoutes (line 18) | func TestApproveRoutesWithPolicy_NeverRemovesRoutes(t *testing.T) {
  function TestApproveRoutesWithPolicy_EdgeCases (line 221) | func TestApproveRoutesWithPolicy_EdgeCases(t *testing.T) {
  function TestApproveRoutesWithPolicy_NilPolicyManagerCase (line 327) | func TestApproveRoutesWithPolicy_NilPolicyManagerCase(t *testing.T) {

FILE: hscontrol/policy/policy_test.go
  function TestReduceNodes (line 29) | func TestReduceNodes(t *testing.T) {
  function TestReduceNodesFromPolicy (line 799) | func TestReduceNodesFromPolicy(t *testing.T) {
  function TestSSHPolicyRules (line 1075) | func TestSSHPolicyRules(t *testing.T) {
  function TestReduceRoutes (line 1610) | func TestReduceRoutes(t *testing.T) {

FILE: hscontrol/policy/policyutil/reduce.go
  function ReduceFilterRules (line 15) | func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) ...

FILE: hscontrol/policy/policyutil/reduce_test.go
  function TestTheInternet (line 86) | func TestTheInternet(t *testing.T) {
  function TestReduceFilterRules (line 110) | func TestReduceFilterRules(t *testing.T) {

FILE: hscontrol/policy/route_approval_test.go
  function TestNodeCanApproveRoute (line 15) | func TestNodeCanApproveRoute(t *testing.T) {

FILE: hscontrol/policy/v2/filter.go
  method compileFilterRules (line 27) | func (pol *Policy) compileFilterRules(
  method compileFilterRulesForNode (line 112) | func (pol *Policy) compileFilterRulesForNode(
  method compileACLWithAutogroupSelf (line 151) | func (pol *Policy) compileACLWithAutogroupSelf(
  function checkPeriodFromRule (line 333) | func checkPeriodFromRule(rule SSH) time.Duration {
  function sshCheck (line 344) | func sshCheck(baseURL string, duration time.Duration) tailcfg.SSHAction {
  method compileSSHPolicy (line 365) | func (pol *Policy) compileSSHPolicy(
  function ipSetToPrincipals (line 572) | func ipSetToPrincipals(ipSet *netipx.IPSet) []*tailcfg.SSHPrincipal {
  function resolveLocalparts (line 591) | func resolveLocalparts(
  function groupSourcesByUser (line 636) | func groupSourcesByUser(
  function ipSetToPrefixStringList (line 702) | func ipSetToPrefixStringList(ips *netipx.IPSet) []string {
  function filterRuleKey (line 717) | func filterRuleKey(rule tailcfg.FilterRule) string {
  function mergeFilterRules (line 730) | func mergeFilterRules(rules []tailcfg.FilterRule) []tailcfg.FilterRule {

FILE: hscontrol/policy/v2/filter_test.go
  function aliasWithPorts (line 21) | func aliasWithPorts(alias Alias, ports ...tailcfg.PortRange) AliasWithPo...
  function TestParsing (line 28) | func TestParsing(t *testing.T) {
  function TestCompileSSHPolicy_UserMapping (line 401) | func TestCompileSSHPolicy_UserMapping(t *testing.T) {
  function TestCompileSSHPolicy_LocalpartMapping (line 649) | func TestCompileSSHPolicy_LocalpartMapping(t *testing.T) {
  function TestCompileSSHPolicy_CheckAction (line 1016) | func TestCompileSSHPolicy_CheckAction(t *testing.T) {
  function TestCompileSSHPolicy_CheckBeforeAcceptOrdering (line 1088) | func TestCompileSSHPolicy_CheckBeforeAcceptOrdering(t *testing.T) {
  function TestSSHIntegrationReproduction (line 1163) | func TestSSHIntegrationReproduction(t *testing.T) {
  function TestSSHJSONSerialization (line 1229) | func TestSSHJSONSerialization(t *testing.T) {
  function TestCompileFilterRulesForNodeWithAutogroupSelf (line 1289) | func TestCompileFilterRulesForNodeWithAutogroupSelf(t *testing.T) {
  function TestTagUserMutualExclusivity (line 1423) | func TestTagUserMutualExclusivity(t *testing.T) {
  function TestAutogroupTagged (line 1539) | func TestAutogroupTagged(t *testing.T) {
  function TestAutogroupSelfInSourceIsRejected (line 1667) | func TestAutogroupSelfInSourceIsRejected(t *testing.T) {
  function TestAutogroupSelfWithSpecificUserSource (line 1694) | func TestAutogroupSelfWithSpecificUserSource(t *testing.T) {
  function TestAutogroupSelfWithGroupSource (line 1761) | func TestAutogroupSelfWithGroupSource(t *testing.T) {
  function createAddr (line 1823) | func createAddr(ip string) *netip.Addr {
  function TestSSHWithAutogroupSelfInDestination (line 1830) | func TestSSHWithAutogroupSelfInDestination(t *testing.T) {
  function TestSSHWithAutogroupSelfAndSpecificUser (line 1912) | func TestSSHWithAutogroupSelfAndSpecificUser(t *testing.T) {
  function TestSSHWithAutogroupSelfAndGroup (line 1967) | func TestSSHWithAutogroupSelfAndGroup(t *testing.T) {
  function TestSSHWithAutogroupSelfExcludesTaggedDevices (line 2028) | func TestSSHWithAutogroupSelfExcludesTaggedDevices(t *testing.T) {
  function TestSSHWithAutogroupSelfAndMixedDestinations (line 2089) | func TestSSHWithAutogroupSelfAndMixedDestinations(t *testing.T) {
  function TestAutogroupSelfWithNonExistentUserInGroup (line 2162) | func TestAutogroupSelfWithNonExistentUserInGroup(t *testing.T) {
  function TestMergeFilterRules (line 2330) | func TestMergeFilterRules(t *testing.T) {
  function TestCompileSSHPolicy_CheckPeriodVariants (line 2539) | func TestCompileSSHPolicy_CheckPeriodVariants(t *testing.T) {
  function TestIPSetToPrincipals (line 2612) | func TestIPSetToPrincipals(t *testing.T) {
  function TestSSHCheckParams (line 2695) | func TestSSHCheckParams(t *testing.T) {
  function TestResolveLocalparts (line 2876) | func TestResolveLocalparts(t *testing.T) {
  function TestGroupSourcesByUser (line 2954) | func TestGroupSourcesByUser(t *testing.T) {

FILE: hscontrol/policy/v2/main_test.go
  function TestMain (line 13) | func TestMain(m *testing.M) {

FILE: hscontrol/policy/v2/policy.go
  type PolicyManager (line 28) | type PolicyManager struct
    method updateLocked (line 93) | func (pm *PolicyManager) updateLocked() (bool, error) {
    method SSHPolicy (line 226) | func (pm *PolicyManager) SSHPolicy(baseURL string, node types.NodeView...
    method SSHCheckParams (line 249) | func (pm *PolicyManager) SSHCheckParams(
    method SetPolicy (line 322) | func (pm *PolicyManager) SetPolicy(polB []byte) (bool, error) {
    method Filter (line 351) | func (pm *PolicyManager) Filter() ([]tailcfg.FilterRule, []matcher.Mat...
    method BuildPeerMap (line 366) | func (pm *PolicyManager) BuildPeerMap(nodes views.Slice[types.NodeView...
    method compileFilterRulesForNodeLocked (line 454) | func (pm *PolicyManager) compileFilterRulesForNodeLocked(node types.No...
    method filterForNodeLocked (line 480) | func (pm *PolicyManager) filterForNodeLocked(node types.NodeView) ([]t...
    method FilterForNode (line 525) | func (pm *PolicyManager) FilterForNode(node types.NodeView) ([]tailcfg...
    method MatchersForNode (line 542) | func (pm *PolicyManager) MatchersForNode(node types.NodeView) ([]match...
    method SetUsers (line 566) | func (pm *PolicyManager) SetUsers(users []types.User) (bool, error) {
    method SetNodes (line 596) | func (pm *PolicyManager) SetNodes(nodes views.Slice[types.NodeView]) (...
    method nodesHavePolicyAffectingChanges (line 644) | func (pm *PolicyManager) nodesHavePolicyAffectingChanges(newNodes view...
    method NodeCanHaveTag (line 674) | func (pm *PolicyManager) NodeCanHaveTag(node types.NodeView, tag strin...
    method userMatchesOwner (line 714) | func (pm *PolicyManager) userMatchesOwner(user types.UserView, owner O...
    method TagExists (line 757) | func (pm *PolicyManager) TagExists(tag string) bool {
    method NodeCanApproveRoute (line 770) | func (pm *PolicyManager) NodeCanApproveRoute(node types.NodeView, rout...
    method Version (line 824) | func (pm *PolicyManager) Version() int {
    method DebugString (line 828) | func (pm *PolicyManager) DebugString() string {
    method invalidateAutogroupSelfCache (line 904) | func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNod...
    method invalidateNodeCache (line 1051) | func (pm *PolicyManager) invalidateNodeCache(newNodes views.Slice[type...
    method invalidateGlobalPolicyCache (line 1065) | func (pm *PolicyManager) invalidateGlobalPolicyCache(newNodes views.Sl...
  type filterAndPolicy (line 59) | type filterAndPolicy struct
  function NewPolicyManager (line 67) | func NewPolicyManager(b []byte, users []types.User, nodes views.Slice[ty...
  function flattenTags (line 1099) | func flattenTags(tagOwners TagOwners, tag Tag, visiting map[Tag]bool, ch...
  function flattenTagOwners (line 1150) | func flattenTagOwners(tagOwners TagOwners) (TagOwners, error) {
  function resolveTagOwners (line 1173) | func resolveTagOwners(p *Policy, users types.Users, nodes views.Slice[ty...

FILE: hscontrol/policy/v2/policy_test.go
  function node (line 16) | func node(name, ipv4, ipv6 string, user types.User) *types.Node {
  function TestPolicyManager (line 27) | func TestPolicyManager(t *testing.T) {
  function TestInvalidateAutogroupSelfCache (line 72) | func TestInvalidateAutogroupSelfCache(t *testing.T) {
  function TestInvalidateGlobalPolicyCache (line 215) | func TestInvalidateGlobalPolicyCache(t *testing.T) {
  function TestAutogroupSelfReducedVsUnreducedRules (line 377) | func TestAutogroupSelfReducedVsUnreducedRules(t *testing.T) {
  function TestAutogroupSelfWithOtherRules (line 453) | func TestAutogroupSelfWithOtherRules(t *testing.T) {
  function TestAutogroupSelfPolicyUpdateTriggersMapResponse (line 534) | func TestAutogroupSelfPolicyUpdateTriggersMapResponse(t *testing.T) {
  function TestTagPropagationToPeerMap (line 618) | func TestTagPropagationToPeerMap(t *testing.T) {
  function TestAutogroupSelfWithAdminOverride (line 745) | func TestAutogroupSelfWithAdminOverride(t *testing.T) {
  function TestAutogroupSelfSymmetricVisibility (line 828) | func TestAutogroupSelfSymmetricVisibility(t *testing.T) {
  function TestAutogroupSelfDoesNotBreakOtherUsersAccess (line 911) | func TestAutogroupSelfDoesNotBreakOtherUsersAccess(t *testing.T) {
  function TestEmptyFilterNodesStillVisible (line 1081) | func TestEmptyFilterNodesStillVisible(t *testing.T) {
  function TestAutogroupSelfCombinedWithTags (line 1149) | func TestAutogroupSelfCombinedWithTags(t *testing.T) {
  function TestIssue2990SameUserTaggedDevice (line 1245) | func TestIssue2990SameUserTaggedDevice(t *testing.T) {

FILE: hscontrol/policy/v2/tailscale_compat_test.go
  function ptrAddr (line 32) | func ptrAddr(s string) *netip.Addr {
  function setupTailscaleCompatUsers (line 38) | func setupTailscaleCompatUsers() types.Users {
  function setupTailscaleCompatNodes (line 48) | func setupTailscaleCompatNodes(users types.Users) types.Nodes {
  function findNodeByGivenName (line 110) | func findNodeByGivenName(nodes types.Nodes, name string) *types.Node {
  type tailscaleCompatTest (line 121) | type tailscaleCompatTest struct
  constant basePolicyPrefix (line 129) | basePolicyPrefix = `{
  constant basePolicySuffix (line 149) | basePolicySuffix = `
  function makePolicy (line 154) | func makePolicy(aclRules string) string {
  function cmpOptions (line 160) | func cmpOptions() []cmp.Option {
  function TestTailscaleCompatWildcardACLs (line 203) | func TestTailscaleCompatWildcardACLs(t *testing.T) {
  function TestTailscaleCompatBasicTags (line 511) | func TestTailscaleCompatBasicTags(t *testing.T) {
  function TestTailscaleCompatUsersGroups (line 788) | func TestTailscaleCompatUsersGroups(t *testing.T) {
  function TestTailscaleCompatAutogroups (line 1059) | func TestTailscaleCompatAutogroups(t *testing.T) {
  function TestTailscaleCompatHosts (line 1506) | func TestTailscaleCompatHosts(t *testing.T) {
  function TestTailscaleCompatProtocolsPorts (line 1715) | func TestTailscaleCompatProtocolsPorts(t *testing.T) {
  function TestTailscaleCompatMixedSources (line 1917) | func TestTailscaleCompatMixedSources(t *testing.T) {
  function TestTailscaleCompatComplexScenarios (line 2229) | func TestTailscaleCompatComplexScenarios(t *testing.T) {
  function TestTailscaleCompatErrorCases (line 9634) | func TestTailscaleCompatErrorCases(t *testing.T) {
  function TestTailscaleCompatErrorCasesHeadscaleDiffers (line 9733) | func TestTailscaleCompatErrorCasesHeadscaleDiffers(t *testing.T) {

FILE: hscontrol/policy/v2/tailscale_routes_compat_test.go
  function setupRouteCompatUsers (line 47) | func setupRouteCompatUsers() types.Users {
  function setupRouteCompatNodes (line 62) | func setupRouteCompatNodes(users types.Users) types.Nodes {
  constant routesPolicyPrefix (line 212) | routesPolicyPrefix = `{
  constant routesPolicySuffix (line 228) | routesPolicySuffix = `
  function makeRoutesPolicy (line 233) | func makeRoutesPolicy(aclRules string) string {
  type routesCompatTest (line 238) | type routesCompatTest struct
  function TestTailscaleRoutesCompatSubnetBasics (line 248) | func TestTailscaleRoutesCompatSubnetBasics(t *testing.T) {
  function TestTailscaleRoutesCompatExitNodes (line 960) | func TestTailscaleRoutesCompatExitNodes(t *testing.T) {
  function TestTailscaleRoutesCompatHARouters (line 1584) | func TestTailscaleRoutesCompatHARouters(t *testing.T) {
  function TestTailscaleRoutesCompatFilterPlacement (line 2066) | func TestTailscaleRoutesCompatFilterPlacement(t *testing.T) {
  function runRoutesCompatTests (line 3255) | func runRoutesCompatTests(t *testing.T, users types.Users, nodes types.N...
  function TestTailscaleRoutesCompatRouteCoverage (line 3294) | func TestTailscaleRoutesCompatRouteCoverage(t *testing.T) {
  function TestTailscaleRoutesCompatOverlapping (line 3544) | func TestTailscaleRoutesCompatOverlapping(t *testing.T) {
  function TestTailscaleRoutesCompatTagResolution (line 3814) | func TestTailscaleRoutesCompatTagResolution(t *testing.T) {
  function TestTailscaleRoutesCompatProtocolPort (line 4072) | func TestTailscaleRoutesCompatProtocolPort(t *testing.T) {
  function TestTailscaleRoutesCompatIPv6 (line 4372) | func TestTailscaleRoutesCompatIPv6(t *testing.T) {
  function TestTailscaleRoutesCompatEdgeCases (line 4726) | func TestTailscaleRoutesCompatEdgeCases(t *testing.T) {
  function TestTailscaleRoutesCompatAdditionalR (line 5431) | func TestTailscaleRoutesCompatAdditionalR(t *testing.T) {
  function TestTailscaleRoutesCompatAdditionalO (line 5710) | func TestTailscaleRoutesCompatAdditionalO(t *testing.T) {
  function TestTailscaleRoutesCompatAdditionalG (line 6157) | func TestTailscaleRoutesCompatAdditionalG(t *testing.T) {
  function TestTailscaleRoutesCompatAdditionalT (line 6315) | func TestTailscaleRoutesCompatAdditionalT(t *testing.T) {
  function TestTailscaleRoutesCompatA
Condensed preview — 407 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,271K chars).
[
  {
    "path": ".dockerignore",
    "chars": 365,
    "preview": "// integration tests are not needed in docker\n// ignoring it let us speed up the integration test\n// development\nintegra"
  },
  {
    "path": ".editorconfig",
    "chars": 227,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".envrc",
    "chars": 10,
    "preview": "use flake\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "chars": 312,
    "preview": "* @juanfont @kradalby\n\n*.md @ohdearaugustin @nblock\n*.yml @ohdearaugustin @nblock\n*.yaml @ohdearaugustin @nblock\nDockerf"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 64,
    "preview": "# These are supported funding model platforms\n\nko_fi: headscale\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "chars": 3654,
    "preview": "name: 🐞 Bug\ndescription: File a bug/issue\ntitle: \"[Bug] <title>\"\nlabels: [\"bug\", \"needs triage\"]\nbody:\n  - type: checkbo"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 399,
    "preview": "# Issues must have some content\nblank_issues_enabled: false\n\n# Contact links\ncontact_links:\n  - name: \"headscale Discord"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "chars": 1215,
    "preview": "name: 🚀 Feature Request\ndescription: Suggest an idea for Headscale\ntitle: \"[Feature] <title>\"\nlabels: [enhancement]\nbody"
  },
  {
    "path": ".github/label-response/needs-more-info.md",
    "chars": 3267,
    "preview": "Thank you for taking the time to report this issue.\n\nTo help us investigate and resolve this, we need more information. "
  },
  {
    "path": ".github/label-response/support-request.md",
    "chars": 797,
    "preview": "Thank you for reaching out.\n\nThis issue tracker is used for **bug reports and feature requests** only. Your question app"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 894,
    "preview": "<!--\nHeadscale is \"Open Source, acknowledged contribution\", this means that any\ncontribution will have to be discussed w"
  },
  {
    "path": ".github/renovate.json",
    "chars": 970,
    "preview": "{\n  \"baseBranches\": [\"main\"],\n  \"username\": \"renovate-release\",\n  \"gitAuthor\": \"Renovate Bot <bot@renovateapp.com>\",\n  \""
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 3776,
    "preview": "name: Build\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\nconcurrency:\n  group: ${{ github.workflow }}-$${{ g"
  },
  {
    "path": ".github/workflows/check-generated.yml",
    "chars": 1799,
    "preview": "name: Check Generated Files\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurre"
  },
  {
    "path": ".github/workflows/check-tests.yaml",
    "chars": 1614,
    "preview": "name: Check integration tests workflow\n\non: [pull_request]\n\nconcurrency:\n  group: ${{ github.workflow }}-$${{ github.hea"
  },
  {
    "path": ".github/workflows/docs-deploy.yml",
    "chars": 1602,
    "preview": "name: Deploy docs\n\non:\n  push:\n    branches:\n      # Main branch for development docs\n      - main\n\n      # Doc maintena"
  },
  {
    "path": ".github/workflows/docs-test.yml",
    "chars": 808,
    "preview": "name: Test documentation build\n\non: [pull_request]\n\nconcurrency:\n  group: ${{ github.workflow }}-$${{ github.head_ref ||"
  },
  {
    "path": ".github/workflows/gh-action-integration-generator.go",
    "chars": 4131,
    "preview": "package main\n\n//go:generate go run ./gh-action-integration-generator.go\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log\"\n\t\"os/exec\"\n\t\"st"
  },
  {
    "path": ".github/workflows/gh-actions-updater.yaml",
    "chars": 700,
    "preview": "name: GitHub Actions Version Updater\n\non:\n  schedule:\n    # Automatically run on every Sunday\n    - cron: \"0 0 * * 0\"\n\nj"
  },
  {
    "path": ".github/workflows/integration-test-template.yml",
    "chars": 5930,
    "preview": "name: Integration Test Template\n\non:\n  workflow_call:\n    inputs:\n      test:\n        required: true\n        type: strin"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 3559,
    "preview": "name: Lint\n\non: [pull_request]\n\nconcurrency:\n  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}\n  "
  },
  {
    "path": ".github/workflows/needs-more-info-comment.yml",
    "chars": 836,
    "preview": "name: Needs More Info - Post Comment\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  post-comment:\n    if: >-\n      github."
  },
  {
    "path": ".github/workflows/needs-more-info-timer.yml",
    "chars": 3682,
    "preview": "name: Needs More Info - Timer\n\non:\n  schedule:\n    - cron: \"0 0 * * *\" # Daily at midnight UTC\n  issue_comment:\n    type"
  },
  {
    "path": ".github/workflows/nix-module-test.yml",
    "chars": 1710,
    "preview": "name: NixOS Module Tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2530,
    "preview": "---\nname: Release\n\non:\n  push:\n    tags:\n      - \"*\" # triggers only if push new tag version\n  workflow_dispatch:\n\njobs:"
  },
  {
    "path": ".github/workflows/stale.yml",
    "chars": 889,
    "preview": "name: Close inactive issues\n\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n\njobs:\n  close-issues:\n    if: github.repository ="
  },
  {
    "path": ".github/workflows/support-request.yml",
    "chars": 918,
    "preview": "name: Support Request - Close Issue\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  close-support-request:\n    if: >-\n     "
  },
  {
    "path": ".github/workflows/test-integration.yaml",
    "chars": 14026,
    "preview": "name: integration\n# To debug locally on a branch, and when needing secrets\n# change this to include `push` so the build "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1576,
    "preview": "name: Tests\n\non: [push, pull_request]\n\nconcurrency:\n  group: ${{ github.workflow }}-$${{ github.head_ref || github.run_i"
  },
  {
    "path": ".github/workflows/update-flake.yml",
    "chars": 678,
    "preview": "name: update-flake-lock\non:\n  workflow_dispatch: # allows manual triggering\n  schedule:\n    - cron: \"0 0 * * 0\" # runs w"
  },
  {
    "path": ".gitignore",
    "chars": 650,
    "preview": "ignored/\ntailscale/\n.vscode/\n.claude/\nlogs/\n\n*.prof\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib"
  },
  {
    "path": ".golangci.yaml",
    "chars": 2790,
    "preview": "---\nversion: \"2\"\nlinters:\n  default: all\n  disable:\n    - cyclop\n    - depguard\n    - dupl\n    - exhaustruct\n    - funco"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 5029,
    "preview": "---\nversion: 2\nbefore:\n  hooks:\n    - go mod tidy -compat=1.26\n    - go mod vendor\n\nrelease:\n  prerelease: auto\n  draft:"
  },
  {
    "path": ".mcp.json",
    "chars": 734,
    "preview": "{\n  \"mcpServers\": {\n    \"claude-code-mcp\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@steip"
  },
  {
    "path": ".mdformat.toml",
    "chars": 54,
    "preview": "[plugin.mkdocs]\nalign_semantic_breaks_in_lists = true\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1794,
    "preview": "# prek/pre-commit configuration for headscale\n# See: https://prek.j178.dev/quickstart/\n# See: https://prek.j178.dev/buil"
  },
  {
    "path": ".prettierignore",
    "chars": 45,
    "preview": ".github/workflows/test-integration-v2*\ndocs/\n"
  },
  {
    "path": "AGENTS.md",
    "chars": 39667,
    "preview": "# AGENTS.md\n\nThis file provides guidance to AI agents when working with code in this repository.\n\n## Overview\n\nHeadscale"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 71555,
    "preview": "# CHANGELOG\n\n## 0.29.0 (202x-xx-xx)\n\n**Minimum supported Tailscale client version: v1.76.0**\n\n### Tailscale ACL compatib"
  },
  {
    "path": "CLAUDE.md",
    "chars": 11,
    "preview": "@AGENTS.md\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5254,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2740,
    "preview": "# Contributing\n\nHeadscale is \"Open Source, acknowledged contribution\", this means that any contribution will have to be "
  },
  {
    "path": "Dockerfile.derper",
    "chars": 499,
    "preview": "# For testing purposes only\n\nFROM golang:1.26.1-alpine AS build-env\n\nWORKDIR /go/src\n\nRUN apk add --no-cache git\nARG VER"
  },
  {
    "path": "Dockerfile.integration",
    "chars": 1545,
    "preview": "# This Dockerfile and the images produced are for testing headscale,\n# and are in no way endorsed by Headscale's maintai"
  },
  {
    "path": "Dockerfile.integration-ci",
    "chars": 547,
    "preview": "# Minimal CI image - expects pre-built headscale binary in build context\n# For local development with delve debugging, u"
  },
  {
    "path": "Dockerfile.tailscale-HEAD",
    "chars": 1695,
    "preview": "# Copyright (c) Tailscale Inc & AUTHORS\n# SPDX-License-Identifier: BSD-3-Clause\n\n# This Dockerfile is more or less lifte"
  },
  {
    "path": "LICENSE",
    "chars": 1517,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2020, Juan Font\nAll rights reserved.\n\nRedistribution and use in source and binary fo"
  },
  {
    "path": "Makefile",
    "chars": 3955,
    "preview": "# Headscale Makefile\n# Modern Makefile following best practices\n\n# Version calculation\nVERSION ?= $(shell git describe -"
  },
  {
    "path": "README.md",
    "chars": 6238,
    "preview": "![headscale logo](./docs/assets/logo/headscale3_header_stacked_left.png)\n\n![ci](https://github.com/juanfont/headscale/ac"
  },
  {
    "path": "buf.gen.yaml",
    "chars": 425,
    "preview": "version: v1\nplugins:\n  - name: go\n    out: gen/go\n    opt:\n      - paths=source_relative\n  - name: go-grpc\n    out: gen/"
  },
  {
    "path": "cmd/headscale/cli/api_key.go",
    "chars": 4477,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\tv1 \"github.com/juanfont/headscale/gen/go/headscale/v1\"\n\t\"github.com"
  },
  {
    "path": "cmd/headscale/cli/auth.go",
    "chars": 2474,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\tv1 \"github.com/juanfont/headscale/gen/go/headscale/v1\"\n\t\"github.com/spf13/cobr"
  },
  {
    "path": "cmd/headscale/cli/configtest.go",
    "chars": 450,
    "preview": "package cli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\trootCmd.AddCommand(configTestCmd)\n}\n\nvar confi"
  },
  {
    "path": "cmd/headscale/cli/debug.go",
    "chars": 1666,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\tv1 \"github.com/juanfont/headscale/gen/go/headscale/v1\"\n\t\"github.com/juanfont/h"
  },
  {
    "path": "cmd/headscale/cli/dump_config.go",
    "chars": 510,
    "preview": "package cli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\nfunc init() {\n\trootCmd.AddCommand(d"
  },
  {
    "path": "cmd/headscale/cli/generate.go",
    "chars": 767,
    "preview": "package cli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\t\"tailscale.com/types/key\"\n)\n\nfunc init() {\n\trootCmd.AddCommand("
  },
  {
    "path": "cmd/headscale/cli/health.go",
    "chars": 714,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\tv1 \"github.com/juanfont/headscale/gen/go/headscale/v1\"\n\t\"github.com/spf13/cobr"
  },
  {
    "path": "cmd/headscale/cli/mockoidc.go",
    "chars": 3868,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/juanf"
  },
  {
    "path": "cmd/headscale/cli/nodes.go",
    "chars": 13530,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tv1 \"github.com/juanfont/headscale/g"
  },
  {
    "path": "cmd/headscale/cli/policy.go",
    "chars": 5284,
    "preview": "package cli\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\tv1 \"github.com/juanfont/headscale/gen/go/headscale/v1\"\n\t\"github.com/juanf"
  },
  {
    "path": "cmd/headscale/cli/preauthkeys.go",
    "chars": 5020,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\tv1 \"github.com/juanfont/headscale/gen/go/headscale/v1\"\n\t"
  },
  {
    "path": "cmd/headscale/cli/pterm_style.go",
    "chars": 286,
    "preview": "package cli\n\nimport (\n\t\"time\"\n\n\t\"github.com/pterm/pterm\"\n)\n\nfunc ColourTime(date time.Time) string {\n\tdateStr := date.Fo"
  },
  {
    "path": "cmd/headscale/cli/root.go",
    "chars": 4135,
    "preview": "package cli\n\nimport (\n\t\"os\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.c"
  },
  {
    "path": "cmd/headscale/cli/root_test.go",
    "chars": 8584,
    "preview": "package cli\n\nimport (\n\t\"testing\"\n)\n\nfunc TestFilterPreReleasesIfStable(t *testing.T) {\n\ttests := []struct {\n\t\tname      "
  },
  {
    "path": "cmd/headscale/cli/serve.go",
    "chars": 774,
    "preview": "package cli\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/tailscale/squibble\"\n)\n\nfunc i"
  },
  {
    "path": "cmd/headscale/cli/users.go",
    "chars": 6629,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strconv\"\n\n\tv1 \"github.com/juanfont/headscale/gen/go/heads"
  },
  {
    "path": "cmd/headscale/cli/utils.go",
    "chars": 8939,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\tv1 \"github.com/juanfont"
  },
  {
    "path": "cmd/headscale/cli/version.go",
    "chars": 531,
    "preview": "package cli\n\nimport (\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\trootC"
  },
  {
    "path": "cmd/headscale/headscale.go",
    "chars": 878,
    "preview": "package main\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/jagottsicher/termcolor\"\n\t\"github.com/juanfont/headscale/cmd/headscale"
  },
  {
    "path": "cmd/headscale/headscale_test.go",
    "chars": 2721,
    "preview": "package main\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"gi"
  },
  {
    "path": "cmd/hi/README.md",
    "chars": 251,
    "preview": "# hi\n\nhi (headscale integration runner) is an entirely \"vibe coded\" wrapper around our\n[integration test suite](../integ"
  },
  {
    "path": "cmd/hi/cleanup.go",
    "chars": 11134,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff"
  },
  {
    "path": "cmd/hi/docker.go",
    "chars": 23303,
    "preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepa"
  },
  {
    "path": "cmd/hi/doctor.go",
    "chars": 9428,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nvar ErrSystemChecksFailed = errors.Ne"
  },
  {
    "path": "cmd/hi/main.go",
    "chars": 2010,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/creachadair/command\"\n\t\"github.com/creachadair/flax\"\n)\n\nvar runConf"
  },
  {
    "path": "cmd/hi/run.go",
    "chars": 3667,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/creachadair/command\"\n)\n\nvar "
  },
  {
    "path": "cmd/hi/stats.go",
    "chars": 13325,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github."
  },
  {
    "path": "cmd/mapresponses/main.go",
    "chars": 1507,
    "preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/creachadair/command\"\n\t\"github.com/creachada"
  },
  {
    "path": "config-example.yaml",
    "chars": 16653,
    "preview": "---\n# headscale will look for a configuration file named `config.yaml` (or `config.json`) in the following order:\n#\n# - "
  },
  {
    "path": "derp-example.yaml",
    "chars": 457,
    "preview": "# If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-ser"
  },
  {
    "path": "docs/about/clients.md",
    "chars": 1439,
    "preview": "# Client and operating system support\n\nWe aim to support the [**last 10 releases** of the Tailscale client](https://tail"
  },
  {
    "path": "docs/about/contributing.md",
    "chars": 47,
    "preview": "{%\ninclude-markdown \"../../CONTRIBUTING.md\"\n%}\n"
  },
  {
    "path": "docs/about/faq.md",
    "chars": 11716,
    "preview": "# Frequently Asked Questions\n\n## What is the design goal of headscale?\n\nHeadscale aims to implement a self-hosted, open "
  },
  {
    "path": "docs/about/features.md",
    "chars": 2641,
    "preview": "# Features\n\nHeadscale aims to implement a self-hosted, open source alternative to the Tailscale control server. Headscal"
  },
  {
    "path": "docs/about/help.md",
    "chars": 200,
    "preview": "# Getting help\n\nJoin our [Discord server](https://discord.gg/c84AZQhmpx) for announcements and community support.\n\nPleas"
  },
  {
    "path": "docs/about/releases.md",
    "chars": 687,
    "preview": "# Releases\n\nAll headscale releases are available on the [GitHub release page](https://github.com/juanfont/headscale/rele"
  },
  {
    "path": "docs/about/sponsor.md",
    "chars": 158,
    "preview": "# Sponsor\n\nIf you like to support the development of headscale, please consider a donation via\n[ko-fi.com/headscale](htt"
  },
  {
    "path": "docs/index.md",
    "chars": 1218,
    "preview": "---\nhide:\n  - navigation\n  - toc\n---\n\n# Welcome to headscale\n\nHeadscale is an open source, self-hosted implementation of"
  },
  {
    "path": "docs/ref/acls.md",
    "chars": 9610,
    "preview": "Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.\n\nFor instance, inste"
  },
  {
    "path": "docs/ref/api.md",
    "chars": 4646,
    "preview": "# API\n\nHeadscale provides a [HTTP REST API](#rest-api) and a [gRPC interface](#grpc) which may be used to integrate a [w"
  },
  {
    "path": "docs/ref/configuration.md",
    "chars": 1787,
    "preview": "# Configuration\n\n- Headscale loads its configuration from a YAML file\n- It searches for `config.yaml` in the following p"
  },
  {
    "path": "docs/ref/debug.md",
    "chars": 4567,
    "preview": "# Debugging and troubleshooting\n\nHeadscale and Tailscale provide debug and introspection capabilities that can be helpfu"
  },
  {
    "path": "docs/ref/derp.md",
    "chars": 6764,
    "preview": "# DERP\n\nA [DERP (Designated Encrypted Relay for Packets) server](https://tailscale.com/kb/1232/derp-servers) is mainly u"
  },
  {
    "path": "docs/ref/dns.md",
    "chars": 4113,
    "preview": "# DNS\n\nHeadscale supports [most DNS features](../about/features.md) from Tailscale. DNS related settings can be configur"
  },
  {
    "path": "docs/ref/integration/reverse-proxy.md",
    "chars": 5701,
    "preview": "# Running headscale behind a reverse proxy\n\n!!! warning \"Community documentation\"\n\n    This page is not actively maintai"
  },
  {
    "path": "docs/ref/integration/tools.md",
    "chars": 1252,
    "preview": "# Tools related to headscale\n\n!!! warning \"Community contributions\"\n\n    This page contains community contributions. The"
  },
  {
    "path": "docs/ref/integration/web-ui.md",
    "chars": 1765,
    "preview": "# Web interfaces for headscale\n\n!!! warning \"Community contributions\"\n\n    This page contains community contributions. T"
  },
  {
    "path": "docs/ref/oidc.md",
    "chars": 17302,
    "preview": "# OpenID Connect\n\nHeadscale supports authentication via external identity providers using OpenID Connect (OIDC). It feat"
  },
  {
    "path": "docs/ref/registration.md",
    "chars": 5998,
    "preview": "# Registration methods\n\nHeadscale supports multiple ways to register a node. The preferred registration method depends o"
  },
  {
    "path": "docs/ref/routes.md",
    "chars": 10908,
    "preview": "# Routes\n\nHeadscale supports route advertising and can be used to manage [subnet routers](https://tailscale.com/kb/1019/"
  },
  {
    "path": "docs/ref/tags.md",
    "chars": 1850,
    "preview": "# Tags\n\nHeadscale supports Tailscale tags. Please read [Tailscale's tag documentation](https://tailscale.com/kb/1068/tag"
  },
  {
    "path": "docs/ref/tls.md",
    "chars": 4701,
    "preview": "# Running the service via TLS (optional)\n\n## Bring your own certificate\n\nHeadscale can be configured to expose its web s"
  },
  {
    "path": "docs/requirements.txt",
    "chars": 150,
    "preview": "mike~=2.1\nmkdocs-include-markdown-plugin~=7.1\nmkdocs-macros-plugin~=1.3\nmkdocs-material[imaging]~=9.5\nmkdocs-minify-plug"
  },
  {
    "path": "docs/setup/install/community.md",
    "chars": 1812,
    "preview": "# Community packages\n\nSeveral Linux distributions and community members provide packages for headscale. Those packages m"
  },
  {
    "path": "docs/setup/install/container.md",
    "chars": 4680,
    "preview": "# Running headscale in a container\n\n!!! warning \"Community documentation\"\n\n    This page is not actively maintained by t"
  },
  {
    "path": "docs/setup/install/official.md",
    "chars": 4177,
    "preview": "# Official releases\n\nOfficial releases for headscale are available as binaries for various platforms and DEB packages fo"
  },
  {
    "path": "docs/setup/install/source.md",
    "chars": 1939,
    "preview": "# Build from source\n\n!!! warning \"Community documentation\"\n\n    This page is not actively maintained by the headscale au"
  },
  {
    "path": "docs/setup/requirements.md",
    "chars": 2544,
    "preview": "# Requirements\n\nHeadscale should just work as long as the following requirements are met:\n\n- A server with a public IP a"
  },
  {
    "path": "docs/setup/upgrade.md",
    "chars": 2104,
    "preview": "# Upgrade an existing installation\n\n!!! tip \"Required update path\"\n\n    Its required to update from one stable version t"
  },
  {
    "path": "docs/usage/connect/android.md",
    "chars": 1546,
    "preview": "# Connecting an Android client\n\nThis documentation has the goal of showing how a user can use the official Android [Tail"
  },
  {
    "path": "docs/usage/connect/apple.md",
    "chars": 2214,
    "preview": "# Connecting an Apple client\n\nThis documentation has the goal of showing how a user can use the official iOS and macOS ["
  },
  {
    "path": "docs/usage/connect/windows.md",
    "chars": 2097,
    "preview": "# Connecting a Windows client\n\nThis documentation has the goal of showing how a user can use the official Windows [Tails"
  },
  {
    "path": "docs/usage/getting-started.md",
    "chars": 4631,
    "preview": "# Getting started\n\nThis page helps you get started with headscale and provides a few usage examples for the headscale co"
  },
  {
    "path": "flake.nix",
    "chars": 7821,
    "preview": "{\n  description = \"headscale - Open Source Tailscale Control server\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixp"
  },
  {
    "path": "gen/go/headscale/v1/apikey.pb.go",
    "chars": 16440,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// "
  },
  {
    "path": "gen/go/headscale/v1/auth.pb.go",
    "chars": 10679,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// "
  },
  {
    "path": "gen/go/headscale/v1/device.pb.go",
    "chars": 28762,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// "
  },
  {
    "path": "gen/go/headscale/v1/headscale.pb.go",
    "chars": 20209,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// "
  },
  {
    "path": "gen/go/headscale/v1/headscale.pb.gw.go",
    "chars": 114856,
    "preview": "// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.\n// source: headscale/v1/headscale.proto\n\n/*\nPackage v1 is a r"
  },
  {
    "path": "gen/go/headscale/v1/headscale_grpc.pb.go",
    "chars": 50123,
    "preview": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.0\n// - protoc           "
  },
  {
    "path": "gen/go/headscale/v1/node.pb.go",
    "chars": 42065,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// "
  },
  {
    "path": "gen/go/headscale/v1/policy.pb.go",
    "chars": 8748,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// "
  },
  {
    "path": "gen/go/headscale/v1/preauthkey.pb.go",
    "chars": 18980,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// "
  },
  {
    "path": "gen/go/headscale/v1/user.pb.go",
    "chars": 18274,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// "
  },
  {
    "path": "gen/openapiv2/headscale/v1/apikey.swagger.json",
    "chars": 807,
    "preview": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"headscale/v1/apikey.proto\",\n    \"version\": \"version not set\"\n  },\n  \"con"
  },
  {
    "path": "gen/openapiv2/headscale/v1/auth.swagger.json",
    "chars": 805,
    "preview": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"headscale/v1/auth.proto\",\n    \"version\": \"version not set\"\n  },\n  \"consu"
  },
  {
    "path": "gen/openapiv2/headscale/v1/device.swagger.json",
    "chars": 807,
    "preview": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"headscale/v1/device.proto\",\n    \"version\": \"version not set\"\n  },\n  \"con"
  },
  {
    "path": "gen/openapiv2/headscale/v1/headscale.swagger.json",
    "chars": 35099,
    "preview": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"headscale/v1/headscale.proto\",\n    \"version\": \"version not set\"\n  },\n  \""
  },
  {
    "path": "gen/openapiv2/headscale/v1/node.swagger.json",
    "chars": 805,
    "preview": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"headscale/v1/node.proto\",\n    \"version\": \"version not set\"\n  },\n  \"consu"
  },
  {
    "path": "gen/openapiv2/headscale/v1/policy.swagger.json",
    "chars": 807,
    "preview": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"headscale/v1/policy.proto\",\n    \"version\": \"version not set\"\n  },\n  \"con"
  },
  {
    "path": "gen/openapiv2/headscale/v1/preauthkey.swagger.json",
    "chars": 811,
    "preview": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"headscale/v1/preauthkey.proto\",\n    \"version\": \"version not set\"\n  },\n  "
  },
  {
    "path": "gen/openapiv2/headscale/v1/user.swagger.json",
    "chars": 805,
    "preview": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"headscale/v1/user.proto\",\n    \"version\": \"version not set\"\n  },\n  \"consu"
  },
  {
    "path": "go.mod",
    "chars": 11526,
    "preview": "module github.com/juanfont/headscale\n\ngo 1.26.1\n\nrequire (\n\tgithub.com/arl/statsviz v0.8.0\n\tgithub.com/cenkalti/backoff/"
  },
  {
    "path": "go.sum",
    "chars": 65009,
    "preview": "9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=\n9fans.net/go v0.0.8-0."
  },
  {
    "path": "hscontrol/app.go",
    "chars": 31798,
    "preview": "package hscontrol\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t_ \"net/http/pprof\" // no"
  },
  {
    "path": "hscontrol/assets/assets.go",
    "chars": 583,
    "preview": "// Package assets provides embedded static assets for Headscale.\n// All static files (favicon, CSS, SVG) are embedded he"
  },
  {
    "path": "hscontrol/assets/style.css",
    "chars": 2956,
    "preview": "/* CSS Variables from Material for MkDocs */\n:root {\n  --md-default-fg-color: rgba(0, 0, 0, 0.87);\n  --md-default-fg-col"
  },
  {
    "path": "hscontrol/auth.go",
    "chars": 14640,
    "preview": "package hscontrol\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/j"
  },
  {
    "path": "hscontrol/auth_tags_test.go",
    "chars": 28481,
    "preview": "package hscontrol\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/stretchr/t"
  },
  {
    "path": "hscontrol/auth_test.go",
    "chars": 154366,
    "preview": "package hscontrol\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/juanfont/"
  },
  {
    "path": "hscontrol/capver/capver.go",
    "chars": 2953,
    "preview": "package capver\n\n//go:generate go run ../../tools/capver/main.go\n\nimport (\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\txmaps \"golang.o"
  },
  {
    "path": "hscontrol/capver/capver_generated.go",
    "chars": 1529,
    "preview": "package capver\n\n// Generated DO NOT EDIT\n\nimport \"tailscale.com/tailcfg\"\n\nvar tailscaleToCapVer = map[string]tailcfg.Cap"
  },
  {
    "path": "hscontrol/capver/capver_test.go",
    "chars": 783,
    "preview": "package capver\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestTailscaleLatestMajorMinor(t *testing.T)"
  },
  {
    "path": "hscontrol/capver/capver_test_data.go",
    "chars": 733,
    "preview": "package capver\n\n// Generated DO NOT EDIT\n\nimport \"tailscale.com/tailcfg\"\n\nvar tailscaleLatestMajorMinorTests = []struct "
  },
  {
    "path": "hscontrol/db/api_key.go",
    "chars": 8356,
    "preview": "package db\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/"
  },
  {
    "path": "hscontrol/db/api_key_test.go",
    "chars": 6864,
    "preview": "package db\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/stretc"
  },
  {
    "path": "hscontrol/db/db.go",
    "chars": 35144,
    "preview": "package db\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"s"
  },
  {
    "path": "hscontrol/db/db_test.go",
    "chars": 13955,
    "preview": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"githu"
  },
  {
    "path": "hscontrol/db/ephemeral_garbage_collector_test.go",
    "chars": 12535,
    "preview": "package db\n\nimport (\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/typ"
  },
  {
    "path": "hscontrol/db/ip.go",
    "chars": 9292,
    "preview": "package db\n\nimport (\n\t\"crypto/rand\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net/netip\"\n\t\"sync\"\n\n\t\"github.com/juan"
  },
  {
    "path": "hscontrol/db/ip_test.go",
    "chars": 9638,
    "preview": "package db\n\nimport (\n\t\"fmt\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-c"
  },
  {
    "path": "hscontrol/db/main_test.go",
    "chars": 585,
    "preview": "package db\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n)\n\n// TestMain ensures the working directory is set to"
  },
  {
    "path": "hscontrol/db/node.go",
    "chars": 22356,
    "preview": "package db\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"regexp\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\""
  },
  {
    "path": "hscontrol/db/node_test.go",
    "chars": 30542,
    "preview": "package db\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net/netip\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testin"
  },
  {
    "path": "hscontrol/db/policy.go",
    "chars": 2020,
    "preview": "package db\n\nimport (\n\t\"errors\"\n\t\"os\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/juanfont/headscale/h"
  },
  {
    "path": "hscontrol/db/preauth_keys.go",
    "chars": 9270,
    "preview": "package db\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"g"
  },
  {
    "path": "hscontrol/db/preauth_keys_test.go",
    "chars": 12647,
    "preview": "package db\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\""
  },
  {
    "path": "hscontrol/db/schema.sql",
    "chars": 3648,
    "preview": "-- This file is the representation of the SQLite schema of Headscale.\n-- It is the \"source of truth\" and is used to vali"
  },
  {
    "path": "hscontrol/db/sqliteconfig/config.go",
    "chars": 14730,
    "preview": "// Package sqliteconfig provides type-safe configuration for SQLite databases\n// with proper enum validation and URL gen"
  },
  {
    "path": "hscontrol/db/sqliteconfig/config_test.go",
    "chars": 7556,
    "preview": "package sqliteconfig\n\nimport (\n\t\"testing\"\n)\n\nfunc TestJournalMode(t *testing.T) {\n\ttests := []struct {\n\t\tmode  JournalMo"
  },
  {
    "path": "hscontrol/db/sqliteconfig/integration_test.go",
    "chars": 7611,
    "preview": "package sqliteconfig\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t_ \"modernc.org/sqlite"
  },
  {
    "path": "hscontrol/db/suite_test.go",
    "chars": 1899,
    "preview": "package db\n\nimport (\n\t\"log\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/juanfont/headscale/hscontrol"
  },
  {
    "path": "hscontrol/db/testdata/sqlite/failing-node-preauth-constraint_dump.sql",
    "chars": 6391,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE IF NOT EXISTS \"api_keys\" (`id` integer,`prefix` text UNIQUE,`ha"
  },
  {
    "path": "hscontrol/db/testdata/sqlite/headscale_0.26.0-beta.1_dump.sql",
    "chars": 2784,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`));\nINSERT INTO migrat"
  },
  {
    "path": "hscontrol/db/testdata/sqlite/headscale_0.26.0-beta.2_dump.sql",
    "chars": 2852,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`));\nINSERT INTO migrat"
  },
  {
    "path": "hscontrol/db/testdata/sqlite/headscale_0.26.0_dump.sql",
    "chars": 2899,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`));\nINSERT INTO migrat"
  },
  {
    "path": "hscontrol/db/testdata/sqlite/headscale_0.26.1_dump-litestream.sql",
    "chars": 3011,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`));\nINSERT INTO migrat"
  },
  {
    "path": "hscontrol/db/testdata/sqlite/headscale_0.26.1_dump.sql",
    "chars": 2899,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`));\nINSERT INTO migrat"
  },
  {
    "path": "hscontrol/db/testdata/sqlite/headscale_0.26.1_dump_schema-to-0.27.0-old-table-cleanup.sql",
    "chars": 3608,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE `migrations` (`id` text,PRIMARY KEY (`id`));\nINSERT INTO migrat"
  },
  {
    "path": "hscontrol/db/testdata/sqlite/request_tags_migration_test.sql",
    "chars": 10155,
    "preview": "-- Test SQL dump for RequestTags migration (202601121700-migrate-hostinfo-request-tags)\n-- and forced_tags->tags rename "
  },
  {
    "path": "hscontrol/db/text_serialiser.go",
    "chars": 3075,
    "preview": "package db\n\nimport (\n\t\"context\"\n\t\"encoding\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\n\t\"gorm.io/gorm/schema\"\n)\n\nvar (\n\terrUnmarshalTe"
  },
  {
    "path": "hscontrol/db/user_update_test.go",
    "chars": 4194,
    "preview": "package db\n\nimport (\n\t\"database/sql\"\n\t\"testing\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/stretchr/"
  },
  {
    "path": "hscontrol/db/users.go",
    "chars": 5772,
    "preview": "package db\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.c"
  },
  {
    "path": "hscontrol/db/users_test.go",
    "chars": 6021,
    "preview": "package db\n\nimport (\n\t\"testing\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\"github.com/juanfont/headscale/hscont"
  },
  {
    "path": "hscontrol/db/versioncheck.go",
    "chars": 7182,
    "preview": "package db\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/types\"\n\t\""
  },
  {
    "path": "hscontrol/db/versioncheck_test.go",
    "chars": 7519,
    "preview": "package db\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/glebarez/sqlite\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com"
  },
  {
    "path": "hscontrol/debug.go",
    "chars": 11155,
    "preview": "package hscontrol\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/arl/statsviz\"\n\t\"github.com/juan"
  },
  {
    "path": "hscontrol/derp/derp.go",
    "chars": 3930,
    "preview": "package derp\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"hash/crc64\"\n\t\"io\"\n\t\"maps\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url"
  },
  {
    "path": "hscontrol/derp/derp_test.go",
    "chars": 9975,
    "preview": "package derp\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/spf13/viper\"\n\t\"tailscale.com/tailcfg\"\n)\n"
  },
  {
    "path": "hscontrol/derp/server/derp_server.go",
    "chars": 12125,
    "preview": "package server\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"ne"
  },
  {
    "path": "hscontrol/dns/extrarecords.go",
    "chars": 5209,
    "preview": "package dns\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/cenkalti"
  },
  {
    "path": "hscontrol/grpcv1.go",
    "chars": 24326,
    "preview": "//go:generate buf generate --template ../buf.gen.yaml -o .. ../proto\n\n// nolint\npackage hscontrol\n\nimport (\n\t\"context\"\n\t"
  },
  {
    "path": "hscontrol/grpcv1_test.go",
    "chars": 16854,
    "preview": "package hscontrol\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\tv1 \"github.com/juanfont/headscale/gen/go/headscale/v1\"\n\t\"git"
  },
  {
    "path": "hscontrol/handlers.go",
    "chars": 9657,
    "preview": "package hscontrol\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n"
  },
  {
    "path": "hscontrol/mapper/batcher.go",
    "chars": 22693,
    "preview": "package mapper\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/stat"
  },
  {
    "path": "hscontrol/mapper/batcher_bench_test.go",
    "chars": 20604,
    "preview": "package mapper\n\n// Benchmarks for batcher components and full pipeline.\n//\n// Organized into three tiers:\n// - Component"
  },
  {
    "path": "hscontrol/mapper/batcher_concurrency_test.go",
    "chars": 55428,
    "preview": "package mapper\n\n// Concurrency, lifecycle, and scale tests for the batcher.\n// Tests in this file exercise:\n// - addToBa"
  },
  {
    "path": "hscontrol/mapper/batcher_scale_bench_test.go",
    "chars": 23813,
    "preview": "package mapper\n\n// Scale benchmarks for the batcher system.\n//\n// These benchmarks systematically increase node counts t"
  },
  {
    "path": "hscontrol/mapper/batcher_test.go",
    "chars": 69889,
    "preview": "package mapper\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n"
  },
  {
    "path": "hscontrol/mapper/batcher_unit_test.go",
    "chars": 34832,
    "preview": "package mapper\n\n// Unit tests for batcher components that do NOT require database setup.\n// These tests exercise connect"
  },
  {
    "path": "hscontrol/mapper/builder.go",
    "chars": 8074,
    "preview": "package mapper\n\nimport (\n\t\"net/netip\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/policy\"\n\t\"github.com/ju"
  },
  {
    "path": "hscontrol/mapper/builder_test.go",
    "chars": 7939,
    "preview": "package mapper\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/state\"\n\t\"github.com/juanfont/head"
  },
  {
    "path": "hscontrol/mapper/mapper.go",
    "chars": 10177,
    "preview": "package mapper\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"tim"
  },
  {
    "path": "hscontrol/mapper/mapper_test.go",
    "chars": 1636,
    "preview": "package mapper\n\nimport (\n\t\"fmt\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/"
  },
  {
    "path": "hscontrol/mapper/node_conn.go",
    "chars": 13371,
    "preview": "package mapper\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/juanfont/headscale/hscontrol/typ"
  },
  {
    "path": "hscontrol/mapper/tail_test.go",
    "chars": 7770,
    "preview": "package mapper\n\nimport (\n\t\"encoding/json\"\n\t\"net/netip\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/"
  },
  {
    "path": "hscontrol/metrics.go",
    "chars": 3025,
    "preview": "package hscontrol\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/prometheus/client_golang/prom"
  },
  {
    "path": "hscontrol/noise.go",
    "chars": 19378,
    "preview": "package hscontrol\n\nimport (\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t"
  },
  {
    "path": "hscontrol/noise_test.go",
    "chars": 5717,
    "preview": "package hscontrol\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"gi"
  },
  {
    "path": "hscontrol/oidc.go",
    "chars": 20303,
    "preview": "package hscontrol\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gith"
  }
]

// ... and 207 more files (download for full content)

About this extraction

This page contains the full source code of the juanfont/headscale GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 407 files (4.5 MB), approximately 1.2M tokens, and a symbol index with 3826 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!