mirror of
https://github.com/juanfont/headscale.git
synced 2025-09-25 17:51:11 +02:00
Merge branch 'main' into registration_cache_fix
This commit is contained in:
commit
75f3b2f29b
15
CHANGELOG.md
15
CHANGELOG.md
@ -26,6 +26,7 @@ Please read the [PR description](https://github.com/juanfont/headscale/pull/2617
|
|||||||
for more technical details about the issues and solutions.
|
for more technical details about the issues and solutions.
|
||||||
|
|
||||||
**SQLite Database Backup Example:**
|
**SQLite Database Backup Example:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stop headscale
|
# Stop headscale
|
||||||
systemctl stop headscale
|
systemctl stop headscale
|
||||||
@ -41,6 +42,13 @@ cp /var/lib/headscale/db.sqlite-shm /var/lib/headscale/db.sqlite-shm.backup
|
|||||||
systemctl start headscale
|
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.
|
||||||
|
|
||||||
### BREAKING
|
### BREAKING
|
||||||
|
|
||||||
- Remove support for 32-bit binaries
|
- Remove support for 32-bit binaries
|
||||||
@ -55,6 +63,11 @@ systemctl start headscale
|
|||||||
- **IMPORTANT: Backup your SQLite database before upgrading**
|
- **IMPORTANT: Backup your SQLite database before upgrading**
|
||||||
- Introduces safer table renaming migration strategy
|
- Introduces safer table renaming migration strategy
|
||||||
- Addresses longstanding database integrity issues
|
- Addresses longstanding database integrity issues
|
||||||
|
- 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`
|
- Add support for `autogroup:member`, `autogroup:tagged`
|
||||||
[#2572](https://github.com/juanfont/headscale/pull/2572)
|
[#2572](https://github.com/juanfont/headscale/pull/2572)
|
||||||
- Remove policy v1 code [#2600](https://github.com/juanfont/headscale/pull/2600)
|
- Remove policy v1 code [#2600](https://github.com/juanfont/headscale/pull/2600)
|
||||||
@ -72,7 +85,7 @@ systemctl start headscale
|
|||||||
[#2643](https://github.com/juanfont/headscale/pull/2643)
|
[#2643](https://github.com/juanfont/headscale/pull/2643)
|
||||||
- OIDC: Use group claim from UserInfo
|
- OIDC: Use group claim from UserInfo
|
||||||
[#2663](https://github.com/juanfont/headscale/pull/2663)
|
[#2663](https://github.com/juanfont/headscale/pull/2663)
|
||||||
- OIDC: Update user with claims from UserInfo *before* comparing with allowed
|
- OIDC: Update user with claims from UserInfo _before_ comparing with allowed
|
||||||
groups, email and domain [#2663](https://github.com/juanfont/headscale/pull/2663)
|
groups, email and domain [#2663](https://github.com/juanfont/headscale/pull/2663)
|
||||||
|
|
||||||
## 0.26.1 (2025-06-06)
|
## 0.26.1 (2025-06-06)
|
||||||
|
@ -105,7 +105,7 @@ derp:
|
|||||||
|
|
||||||
# For better connection stability (especially when using an Exit-Node and DNS is not working),
|
# For better connection stability (especially when using an Exit-Node and DNS is not working),
|
||||||
# it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using:
|
# it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using:
|
||||||
ipv4: 1.2.3.4
|
ipv4: 198.51.100.1
|
||||||
ipv6: 2001:db8::1
|
ipv6: 2001:db8::1
|
||||||
|
|
||||||
# List of externally available DERP maps encoded in JSON
|
# List of externally available DERP maps encoded in JSON
|
||||||
@ -128,7 +128,7 @@ derp:
|
|||||||
auto_update_enabled: true
|
auto_update_enabled: true
|
||||||
|
|
||||||
# How often should we check for DERP updates?
|
# How often should we check for DERP updates?
|
||||||
update_frequency: 24h
|
update_frequency: 3h
|
||||||
|
|
||||||
# Disables the automatic check for headscale updates on startup
|
# Disables the automatic check for headscale updates on startup
|
||||||
disable_check_updates: false
|
disable_check_updates: false
|
||||||
@ -275,7 +275,7 @@ dns:
|
|||||||
# `hostname.base_domain` (e.g., _myhost.example.com_).
|
# `hostname.base_domain` (e.g., _myhost.example.com_).
|
||||||
base_domain: example.com
|
base_domain: example.com
|
||||||
|
|
||||||
# Whether to use the local DNS settings of a node or override the local DNS
|
# Whether to use the local DNS settings of a node or override the local DNS
|
||||||
# settings (default) and force the use of Headscale's DNS configuration.
|
# settings (default) and force the use of Headscale's DNS configuration.
|
||||||
override_local_dns: true
|
override_local_dns: true
|
||||||
|
|
||||||
@ -293,8 +293,7 @@ dns:
|
|||||||
|
|
||||||
# Split DNS (see https://tailscale.com/kb/1054/dns/),
|
# Split DNS (see https://tailscale.com/kb/1054/dns/),
|
||||||
# a map of domains and which DNS server to use for each.
|
# a map of domains and which DNS server to use for each.
|
||||||
split:
|
split: {}
|
||||||
{}
|
|
||||||
# foo.bar.com:
|
# foo.bar.com:
|
||||||
# - 1.1.1.1
|
# - 1.1.1.1
|
||||||
# darp.headscale.net:
|
# darp.headscale.net:
|
||||||
|
@ -7,9 +7,9 @@ regions:
|
|||||||
nodes:
|
nodes:
|
||||||
- name: 900a
|
- name: 900a
|
||||||
regionid: 900
|
regionid: 900
|
||||||
hostname: myderp.mydomain.no
|
hostname: myderp.example.com
|
||||||
ipv4: 123.123.123.123
|
ipv4: 198.51.100.1
|
||||||
ipv6: "2604:a880:400:d1::828:b001"
|
ipv6: 2001:db8::1
|
||||||
stunport: 0
|
stunport: 0
|
||||||
stunonly: false
|
stunonly: false
|
||||||
derpport: 0
|
derpport: 0
|
||||||
|
@ -19,7 +19,7 @@ provides on overview of Headscale's feature and compatibility with the Tailscale
|
|||||||
- [x] [Exit nodes](../ref/routes.md#exit-node)
|
- [x] [Exit nodes](../ref/routes.md#exit-node)
|
||||||
- [x] Dual stack (IPv4 and IPv6)
|
- [x] Dual stack (IPv4 and IPv6)
|
||||||
- [x] Ephemeral nodes
|
- [x] Ephemeral nodes
|
||||||
- [x] Embedded [DERP server](https://tailscale.com/kb/1232/derp-servers)
|
- [x] Embedded [DERP server](../ref/derp.md)
|
||||||
- [x] Access control lists ([GitHub label "policy"](https://github.com/juanfont/headscale/labels/policy%20%F0%9F%93%9D))
|
- [x] Access control lists ([GitHub label "policy"](https://github.com/juanfont/headscale/labels/policy%20%F0%9F%93%9D))
|
||||||
- [x] ACL management via API
|
- [x] ACL management via API
|
||||||
- [x] Some [Autogroups](https://tailscale.com/kb/1396/targets#autogroups), currently: `autogroup:internet`,
|
- [x] Some [Autogroups](https://tailscale.com/kb/1396/targets#autogroups), currently: `autogroup:internet`,
|
||||||
|
153
docs/ref/derp.md
Normal file
153
docs/ref/derp.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# DERP
|
||||||
|
|
||||||
|
A [DERP (Designated Encrypted Relay for Packets) server](https://tailscale.com/kb/1232/derp-servers) is mainly used to
|
||||||
|
relay traffic between two nodes in case a direct connection can't be established. Headscale provides an embedded DERP
|
||||||
|
server to ensure seamless connectivity between nodes.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
DERP related settings are configured within the `derp` section of the [configuration file](./configuration.md). The
|
||||||
|
following sections only use a few of the available settings, check the [example configuration](./configuration.md) for
|
||||||
|
all available configuration options.
|
||||||
|
|
||||||
|
### Enable embedded DERP
|
||||||
|
|
||||||
|
Headscale ships with an embedded DERP server which allows to run your own self-hosted DERP server easily. The embedded
|
||||||
|
DERP server is disabled by default and needs to be enabled. In addition, you should configure the public IPv4 and public
|
||||||
|
IPv6 address of your Headscale server for improved connection stability:
|
||||||
|
|
||||||
|
```yaml title="config.yaml" hl_lines="3-5"
|
||||||
|
derp:
|
||||||
|
server:
|
||||||
|
enabled: true
|
||||||
|
ipv4: 198.51.100.1
|
||||||
|
ipv6: 2001:db8::1
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep in mind that [additional ports are needed to run a DERP server](../setup/requirements.md#ports-in-use). Besides
|
||||||
|
relaying traffic, it also uses STUN (udp/3478) to help clients discover their public IP addresses and perform NAT
|
||||||
|
traversal. [Check DERP server connectivity](#check-derp-server-connectivity) to see if everything works.
|
||||||
|
|
||||||
|
### Remove Tailscale's DERP servers
|
||||||
|
|
||||||
|
Once enabled, Headscale's embedded DERP is added to the list of free-to-use [DERP
|
||||||
|
servers](https://tailscale.com/kb/1232/derp-servers) offered by Tailscale Inc. To only use Headscale's embedded DERP
|
||||||
|
server, disable the loading of the default DERP map:
|
||||||
|
|
||||||
|
```yaml title="config.yaml" hl_lines="6"
|
||||||
|
derp:
|
||||||
|
server:
|
||||||
|
enabled: true
|
||||||
|
ipv4: 198.51.100.1
|
||||||
|
ipv6: 2001:db8::1
|
||||||
|
urls: []
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "Single point of failure"
|
||||||
|
|
||||||
|
Removing Tailscale's DERP servers means that there is now just a single DERP server available for clients. This is a
|
||||||
|
single point of failure and could hamper connectivity.
|
||||||
|
|
||||||
|
[Check DERP server connectivity](#check-derp-server-connectivity) with your embedded DERP server before removing
|
||||||
|
Tailscale's DERP servers.
|
||||||
|
|
||||||
|
### Customize DERP map
|
||||||
|
|
||||||
|
The DERP map offered to clients can be customized with a [dedicated YAML-configuration
|
||||||
|
file](https://github.com/juanfont/headscale/blob/main/derp-example.yaml). Typical use-cases involve:
|
||||||
|
|
||||||
|
- Running a fleet of [custom DERP servers](https://tailscale.com/kb/1118/custom-derp-servers)
|
||||||
|
- Excluding or choosing specific regions from the Tailscale's list of free-to-use [DERP
|
||||||
|
servers](https://tailscale.com/kb/1232/derp-servers)
|
||||||
|
|
||||||
|
The following sample `derp.yaml` references two custom regions (`custom-east` with ID 900 and `custom-west` with ID 901)
|
||||||
|
with one custom DERP server in each region. Each DERP server offers DERP relay via HTTPS on tcp/443, support for captive
|
||||||
|
portal checks via HTTP on tcp/80 and STUN on udp/3478. See the definitions of
|
||||||
|
[DERPMap](https://pkg.go.dev/tailscale.com/tailcfg#DERPMap),
|
||||||
|
[DERPRegion](https://pkg.go.dev/tailscale.com/tailcfg#DERPRegion) and
|
||||||
|
[DERPNode](https://pkg.go.dev/tailscale.com/tailcfg#DERPNode) for all available options.
|
||||||
|
|
||||||
|
```yaml title="derp.yaml"
|
||||||
|
regions:
|
||||||
|
900:
|
||||||
|
regionid: 900
|
||||||
|
regioncode: custom-east
|
||||||
|
regionname: My region (east)
|
||||||
|
nodes:
|
||||||
|
- name: 900a
|
||||||
|
regionid: 900
|
||||||
|
hostname: derp900a.example.com
|
||||||
|
ipv4: 198.51.100.1
|
||||||
|
ipv6: 2001:db8::1
|
||||||
|
canport80: true
|
||||||
|
901:
|
||||||
|
regionid: 901
|
||||||
|
regioncode: custom-west
|
||||||
|
regionname: My Region (west)
|
||||||
|
nodes:
|
||||||
|
- name: 901a
|
||||||
|
regionid: 901
|
||||||
|
hostname: derp901a.example.com
|
||||||
|
ipv4: 198.51.100.2
|
||||||
|
ipv6: 2001:db8::2
|
||||||
|
canport80: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the following configuration to only serve the two DERP servers from the above `derp.yaml`:
|
||||||
|
|
||||||
|
```yaml title="config.yaml" hl_lines="5 6"
|
||||||
|
derp:
|
||||||
|
server:
|
||||||
|
enabled: false
|
||||||
|
urls: []
|
||||||
|
paths:
|
||||||
|
- /etc/headscale/derp.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
The embedded DERP server can also be enabled and is automatically added to the custom DERP map.
|
||||||
|
|
||||||
|
|
||||||
|
### Verify clients
|
||||||
|
|
||||||
|
Access to DERP serves can be restricted to nodes that are members of your Tailnet. Relay access is denied for unknown
|
||||||
|
clients.
|
||||||
|
|
||||||
|
=== "Embedded DERP"
|
||||||
|
|
||||||
|
Client verification is enabled by default.
|
||||||
|
|
||||||
|
```yaml title="config.yaml" hl_lines="3"
|
||||||
|
derp:
|
||||||
|
server:
|
||||||
|
verify_clients: true
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "3rd-party DERP"
|
||||||
|
|
||||||
|
Tailscale's `derper` provides two parameters to configure client verification:
|
||||||
|
|
||||||
|
- Use the `-verify-client-url` parameter of the `derper` and point it towards the `/verify` endpoint of your
|
||||||
|
Headscale server (e.g `https://headscale.example.com/verify`). The DERP server will query your Headscale instance
|
||||||
|
as soon as a client connects with it to ask whether access should be allowed or denied. Access is allowed if
|
||||||
|
Headscale knows about the connecting client and denied otherwise.
|
||||||
|
- The parameter `-verify-client-url-fail-open` controls what should happen when the DERP server can't reach the
|
||||||
|
Headscale instance. By default, it will allow access if Headscale is unreachable.
|
||||||
|
|
||||||
|
## Check DERP server connectivity
|
||||||
|
|
||||||
|
Any Tailscale client may be used to introspect the DERP map and to check for connectivity issues with DERP servers.
|
||||||
|
|
||||||
|
- Display DERP map: `tailscale debug derp-map`
|
||||||
|
- Check connectivity with the embedded DERP[^1]:`tailscale debug derp headscale`
|
||||||
|
|
||||||
|
Additional DERP related metrics and information is available via the [metrics and debug
|
||||||
|
endpoint](./debug.md#metrics-and-debug-endpoint).
|
||||||
|
|
||||||
|
[^1]:
|
||||||
|
This assumes that the default region code of the [configuration file](./configuration.md) is used.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- The embedded DERP server can't be used for Tailscale's captive portal checks as it doesn't support the `/generate_204`
|
||||||
|
endpoint via HTTP on port tcp/80.
|
||||||
|
- There are no speed or throughput optimisations, the main purpose is to assist in node connectivity.
|
@ -13,7 +13,7 @@ Running headscale behind a reverse proxy is useful when running multiple applica
|
|||||||
|
|
||||||
The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients.
|
The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients.
|
||||||
|
|
||||||
WebSockets support is also required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml).
|
WebSockets support is also required when using the Headscale [embedded DERP server](../derp.md). In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml).
|
||||||
|
|
||||||
### Cloudflare
|
### Cloudflare
|
||||||
|
|
||||||
|
@ -13,3 +13,4 @@ This page collects third-party tools, client libraries, and scripts related to h
|
|||||||
| headscalebacktosqlite | [Github](https://github.com/bigbozza/headscalebacktosqlite) | Migrate headscale from PostgreSQL back to SQLite |
|
| headscalebacktosqlite | [Github](https://github.com/bigbozza/headscalebacktosqlite) | Migrate headscale from PostgreSQL back to SQLite |
|
||||||
| headscale-pf | [Github](https://github.com/YouSysAdmin/headscale-pf) | Populates user groups based on user groups in Jumpcloud or Authentik |
|
| headscale-pf | [Github](https://github.com/YouSysAdmin/headscale-pf) | Populates user groups based on user groups in Jumpcloud or Authentik |
|
||||||
| headscale-client-go | [Github](https://github.com/hibare/headscale-client-go) | A Go client implementation for the Headscale HTTP API. |
|
| headscale-client-go | [Github](https://github.com/hibare/headscale-client-go) | A Go client implementation for the Headscale HTTP API. |
|
||||||
|
| headscale-zabbix | [Github](https://github.com/dblanque/headscale-zabbix) | A Zabbix Monitoring Template for the Headscale Service. |
|
||||||
|
@ -22,10 +22,10 @@ The ports in use vary with the intended scenario and enabled features. Some of t
|
|||||||
- tcp/443
|
- tcp/443
|
||||||
- Expose publicly: yes
|
- Expose publicly: yes
|
||||||
- HTTPS, required to make Headscale available to Tailscale clients[^1]
|
- HTTPS, required to make Headscale available to Tailscale clients[^1]
|
||||||
- Required if the built-in DERP server is enabled
|
- Required if the [embedded DERP server](../ref/derp.md) is enabled
|
||||||
- udp/3478
|
- udp/3478
|
||||||
- Expose publicly: yes
|
- Expose publicly: yes
|
||||||
- STUN, required if the built-in DERP server is enabled
|
- STUN, required if the [embedded DERP server](../ref/derp.md) is enabled
|
||||||
- tcp/50443
|
- tcp/50443
|
||||||
- Expose publicly: yes
|
- Expose publicly: yes
|
||||||
- Only required if the gRPC interface is used to [remote-control Headscale](../ref/remote-cli.md).
|
- Only required if the gRPC interface is used to [remote-control Headscale](../ref/remote-cli.md).
|
||||||
|
@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1752012998,
|
"lastModified": 1755829505,
|
||||||
"narHash": "sha256-Q82Ms+FQmgOBkdoSVm+FBpuFoeUAffNerR5yVV7SgT8=",
|
"narHash": "sha256-4/Jd+LkQ2ssw8luQVkqVs9spDBVE6h/u/hC/tzngsPo=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "2a2130494ad647f953593c4e84ea4df839fbd68c",
|
"rev": "f937f8ecd1c70efd7e9f90ba13dfb400cf559de4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v5"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
@ -284,11 +285,23 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
|
|||||||
|
|
||||||
case <-derpTickerChan:
|
case <-derpTickerChan:
|
||||||
log.Info().Msg("Fetching DERPMap updates")
|
log.Info().Msg("Fetching DERPMap updates")
|
||||||
derpMap := derp.GetDERPMap(h.cfg.DERP)
|
derpMap, err := backoff.Retry(ctx, func() (*tailcfg.DERPMap, error) {
|
||||||
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
derpMap, err := derp.GetDERPMap(h.cfg.DERP)
|
||||||
region, _ := h.DERPServer.GenerateRegion()
|
if err != nil {
|
||||||
derpMap.Regions[region.RegionID] = ®ion
|
return nil, err
|
||||||
|
}
|
||||||
|
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
||||||
|
region, _ := h.DERPServer.GenerateRegion()
|
||||||
|
derpMap.Regions[region.RegionID] = ®ion
|
||||||
|
}
|
||||||
|
|
||||||
|
return derpMap, nil
|
||||||
|
}, backoff.WithBackOff(backoff.NewExponentialBackOff()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to build new DERPMap, retrying later")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
h.state.SetDERPMap(derpMap)
|
||||||
|
|
||||||
h.Change(change.DERPSet)
|
h.Change(change.DERPSet)
|
||||||
|
|
||||||
@ -516,29 +529,31 @@ func (h *Headscale) Serve() error {
|
|||||||
h.mapBatcher.Start()
|
h.mapBatcher.Start()
|
||||||
defer h.mapBatcher.Close()
|
defer h.mapBatcher.Close()
|
||||||
|
|
||||||
// TODO(kradalby): fix state part.
|
|
||||||
if h.cfg.DERP.ServerEnabled {
|
if h.cfg.DERP.ServerEnabled {
|
||||||
// When embedded DERP is enabled we always need a STUN server
|
// When embedded DERP is enabled we always need a STUN server
|
||||||
if h.cfg.DERP.STUNAddr == "" {
|
if h.cfg.DERP.STUNAddr == "" {
|
||||||
return errSTUNAddressNotSet
|
return errSTUNAddressNotSet
|
||||||
}
|
}
|
||||||
|
|
||||||
region, err := h.DERPServer.GenerateRegion()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generating DERP region for embedded server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
|
||||||
h.state.DERPMap().Regions[region.RegionID] = ®ion
|
|
||||||
}
|
|
||||||
|
|
||||||
go h.DERPServer.ServeSTUN()
|
go h.DERPServer.ServeSTUN()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(h.state.DERPMap().Regions) == 0 {
|
derpMap, err := derp.GetDERPMap(h.cfg.DERP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get DERPMap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
||||||
|
region, _ := h.DERPServer.GenerateRegion()
|
||||||
|
derpMap.Regions[region.RegionID] = ®ion
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(derpMap.Regions) == 0 {
|
||||||
return errEmptyInitialDERPMap
|
return errEmptyInitialDERPMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.state.SetDERPMap(derpMap)
|
||||||
|
|
||||||
// Start ephemeral node garbage collector and schedule all nodes
|
// Start ephemeral node garbage collector and schedule all nodes
|
||||||
// that are already in the database and ephemeral. If they are still
|
// that are already in the database and ephemeral. If they are still
|
||||||
// around between restarts, they will reconnect and the GC will
|
// around between restarts, they will reconnect and the GC will
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Got from https://github.com/xdg-go/strum/blob/main/types.go
|
// Got from https://github.com/xdg-go/strum/blob/main/types.go
|
||||||
var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
var textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
||||||
|
|
||||||
func isTextUnmarshaler(rv reflect.Value) bool {
|
func isTextUnmarshaler(rv reflect.Value) bool {
|
||||||
return rv.Type().Implements(textUnmarshalerType)
|
return rv.Type().Implements(textUnmarshalerType)
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
package derp
|
package derp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"hash/crc64"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/spf13/viper"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
@ -76,56 +82,85 @@ func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
|
|||||||
maps.Copy(result.Regions, derpMap.Regions)
|
maps.Copy(result.Regions, derpMap.Regions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for id, region := range result.Regions {
|
||||||
|
if region == nil {
|
||||||
|
delete(result.Regions, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap {
|
func GetDERPMap(cfg types.DERPConfig) (*tailcfg.DERPMap, error) {
|
||||||
var derpMaps []*tailcfg.DERPMap
|
var derpMaps []*tailcfg.DERPMap
|
||||||
if cfg.DERPMap != nil {
|
if cfg.DERPMap != nil {
|
||||||
derpMaps = append(derpMaps, cfg.DERPMap)
|
derpMaps = append(derpMaps, cfg.DERPMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range cfg.Paths {
|
for _, addr := range cfg.URLs {
|
||||||
log.Debug().
|
derpMap, err := loadDERPMapFromURL(addr)
|
||||||
Str("func", "GetDERPMap").
|
|
||||||
Str("path", path).
|
|
||||||
Msg("Loading DERPMap from path")
|
|
||||||
derpMap, err := loadDERPMapFromPath(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
return nil, err
|
||||||
Str("func", "GetDERPMap").
|
|
||||||
Str("path", path).
|
|
||||||
Err(err).
|
|
||||||
Msg("Could not load DERP map from path")
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
derpMaps = append(derpMaps, derpMap)
|
derpMaps = append(derpMaps, derpMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, addr := range cfg.URLs {
|
for _, path := range cfg.Paths {
|
||||||
derpMap, err := loadDERPMapFromURL(addr)
|
derpMap, err := loadDERPMapFromPath(path)
|
||||||
log.Debug().
|
|
||||||
Str("func", "GetDERPMap").
|
|
||||||
Str("url", addr.String()).
|
|
||||||
Msg("Loading DERPMap from path")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
return nil, err
|
||||||
Str("func", "GetDERPMap").
|
|
||||||
Str("url", addr.String()).
|
|
||||||
Err(err).
|
|
||||||
Msg("Could not load DERP map from path")
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
derpMaps = append(derpMaps, derpMap)
|
derpMaps = append(derpMaps, derpMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
derpMap := mergeDERPMaps(derpMaps)
|
derpMap := mergeDERPMaps(derpMaps)
|
||||||
|
shuffleDERPMap(derpMap)
|
||||||
|
|
||||||
log.Trace().Interface("derpMap", derpMap).Msg("DERPMap loaded")
|
return derpMap, nil
|
||||||
|
}
|
||||||
return derpMap
|
|
||||||
|
func shuffleDERPMap(dm *tailcfg.DERPMap) {
|
||||||
|
if dm == nil || len(dm.Regions) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, region := range dm.Regions {
|
||||||
|
if len(region.Nodes) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dm.Regions[id] = shuffleRegionNoClone(region)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var crc64Table = crc64.MakeTable(crc64.ISO)
|
||||||
|
|
||||||
|
var (
|
||||||
|
derpRandomOnce sync.Once
|
||||||
|
derpRandomInst *rand.Rand
|
||||||
|
derpRandomMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func derpRandom() *rand.Rand {
|
||||||
|
derpRandomOnce.Do(func() {
|
||||||
|
seed := cmp.Or(viper.GetString("dns.base_domain"), time.Now().String())
|
||||||
|
rnd := rand.New(rand.NewSource(0))
|
||||||
|
rnd.Seed(int64(crc64.Checksum([]byte(seed), crc64Table)))
|
||||||
|
derpRandomInst = rnd
|
||||||
|
})
|
||||||
|
return derpRandomInst
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetDerpRandomForTesting() {
|
||||||
|
derpRandomMu.Lock()
|
||||||
|
defer derpRandomMu.Unlock()
|
||||||
|
derpRandomOnce = sync.Once{}
|
||||||
|
derpRandomInst = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shuffleRegionNoClone(r *tailcfg.DERPRegion) *tailcfg.DERPRegion {
|
||||||
|
derpRandom().Shuffle(len(r.Nodes), reflect.Swapper(r.Nodes))
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
284
hscontrol/derp/derp_test.go
Normal file
284
hscontrol/derp/derp_test.go
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
package derp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShuffleDERPMapDeterministic(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
baseDomain string
|
||||||
|
derpMap *tailcfg.DERPMap
|
||||||
|
expected *tailcfg.DERPMap
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single region with 4 nodes",
|
||||||
|
baseDomain: "test1.example.com",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "nyc",
|
||||||
|
RegionName: "New York City",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "1f", RegionID: 1, HostName: "derp1f.tailscale.com"},
|
||||||
|
{Name: "1g", RegionID: 1, HostName: "derp1g.tailscale.com"},
|
||||||
|
{Name: "1h", RegionID: 1, HostName: "derp1h.tailscale.com"},
|
||||||
|
{Name: "1i", RegionID: 1, HostName: "derp1i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "nyc",
|
||||||
|
RegionName: "New York City",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "1g", RegionID: 1, HostName: "derp1g.tailscale.com"},
|
||||||
|
{Name: "1f", RegionID: 1, HostName: "derp1f.tailscale.com"},
|
||||||
|
{Name: "1i", RegionID: 1, HostName: "derp1i.tailscale.com"},
|
||||||
|
{Name: "1h", RegionID: 1, HostName: "derp1h.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple regions with nodes",
|
||||||
|
baseDomain: "test2.example.com",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
10: {
|
||||||
|
RegionID: 10,
|
||||||
|
RegionCode: "sea",
|
||||||
|
RegionName: "Seattle",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "10b", RegionID: 10, HostName: "derp10b.tailscale.com"},
|
||||||
|
{Name: "10c", RegionID: 10, HostName: "derp10c.tailscale.com"},
|
||||||
|
{Name: "10d", RegionID: 10, HostName: "derp10d.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
RegionID: 2,
|
||||||
|
RegionCode: "sfo",
|
||||||
|
RegionName: "San Francisco",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "2d", RegionID: 2, HostName: "derp2d.tailscale.com"},
|
||||||
|
{Name: "2e", RegionID: 2, HostName: "derp2e.tailscale.com"},
|
||||||
|
{Name: "2f", RegionID: 2, HostName: "derp2f.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
10: {
|
||||||
|
RegionID: 10,
|
||||||
|
RegionCode: "sea",
|
||||||
|
RegionName: "Seattle",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "10b", RegionID: 10, HostName: "derp10b.tailscale.com"},
|
||||||
|
{Name: "10c", RegionID: 10, HostName: "derp10c.tailscale.com"},
|
||||||
|
{Name: "10d", RegionID: 10, HostName: "derp10d.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
RegionID: 2,
|
||||||
|
RegionCode: "sfo",
|
||||||
|
RegionName: "San Francisco",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "2f", RegionID: 2, HostName: "derp2f.tailscale.com"},
|
||||||
|
{Name: "2e", RegionID: 2, HostName: "derp2e.tailscale.com"},
|
||||||
|
{Name: "2d", RegionID: 2, HostName: "derp2d.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "large region with many nodes",
|
||||||
|
baseDomain: "test3.example.com",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
4: {
|
||||||
|
RegionID: 4,
|
||||||
|
RegionCode: "fra",
|
||||||
|
RegionName: "Frankfurt",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
4: {
|
||||||
|
RegionID: 4,
|
||||||
|
RegionCode: "fra",
|
||||||
|
RegionName: "Frankfurt",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same region different base domain",
|
||||||
|
baseDomain: "different.example.com",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
4: {
|
||||||
|
RegionID: 4,
|
||||||
|
RegionCode: "fra",
|
||||||
|
RegionName: "Frankfurt",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
4: {
|
||||||
|
RegionID: 4,
|
||||||
|
RegionCode: "fra",
|
||||||
|
RegionName: "Frankfurt",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
viper.Set("dns.base_domain", tt.baseDomain)
|
||||||
|
defer viper.Reset()
|
||||||
|
resetDerpRandomForTesting()
|
||||||
|
|
||||||
|
testMap := tt.derpMap.View().AsStruct()
|
||||||
|
shuffleDERPMap(testMap)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.expected, testMap); diff != "" {
|
||||||
|
t.Errorf("Shuffled DERP map doesn't match expected (-expected +actual):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuffleDERPMapEdgeCases(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
derpMap *tailcfg.DERPMap
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil derp map",
|
||||||
|
derpMap: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty derp map",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "region with no nodes",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "empty",
|
||||||
|
RegionName: "Empty Region",
|
||||||
|
Nodes: []*tailcfg.DERPNode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "region with single node",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "single",
|
||||||
|
RegionName: "Single Node Region",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "1a", RegionID: 1, HostName: "derp1a.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
shuffleDERPMap(tt.derpMap)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestShuffleDERPMapWithoutBaseDomain(t *testing.T) {
|
||||||
|
viper.Reset()
|
||||||
|
resetDerpRandomForTesting()
|
||||||
|
|
||||||
|
derpMap := &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "test",
|
||||||
|
RegionName: "Test Region",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "1a", RegionID: 1, HostName: "derp1a.test.com"},
|
||||||
|
{Name: "1b", RegionID: 1, HostName: "derp1b.test.com"},
|
||||||
|
{Name: "1c", RegionID: 1, HostName: "derp1c.test.com"},
|
||||||
|
{Name: "1d", RegionID: 1, HostName: "derp1d.test.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
original := derpMap.View().AsStruct()
|
||||||
|
shuffleDERPMap(derpMap)
|
||||||
|
|
||||||
|
if len(derpMap.Regions) != 1 || len(derpMap.Regions[1].Nodes) != 4 {
|
||||||
|
t.Error("Shuffle corrupted DERP map structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
originalNodes := make(map[string]bool)
|
||||||
|
for _, node := range original.Regions[1].Nodes {
|
||||||
|
originalNodes[node.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffledNodes := make(map[string]bool)
|
||||||
|
for _, node := range derpMap.Regions[1].Nodes {
|
||||||
|
shuffledNodes[node.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(originalNodes, shuffledNodes); diff != "" {
|
||||||
|
t.Errorf("Shuffle changed node set (-original +shuffled):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
@ -276,7 +276,7 @@ func DERPProbeHandler(
|
|||||||
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
|
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
|
||||||
// Coordination server is included automatically, since local DERP is using the same DNS Name in d.serverURL.
|
// Coordination server is included automatically, since local DERP is using the same DNS Name in d.serverURL.
|
||||||
func DERPBootstrapDNSHandler(
|
func DERPBootstrapDNSHandler(
|
||||||
derpMap *tailcfg.DERPMap,
|
derpMap tailcfg.DERPMapView,
|
||||||
) func(http.ResponseWriter, *http.Request) {
|
) func(http.ResponseWriter, *http.Request) {
|
||||||
return func(
|
return func(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
@ -287,18 +287,18 @@ func DERPBootstrapDNSHandler(
|
|||||||
resolvCtx, cancel := context.WithTimeout(req.Context(), time.Minute)
|
resolvCtx, cancel := context.WithTimeout(req.Context(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
var resolver net.Resolver
|
var resolver net.Resolver
|
||||||
for _, region := range derpMap.Regions {
|
for _, region := range derpMap.Regions().All() {
|
||||||
for _, node := range region.Nodes { // we don't care if we override some nodes
|
for _, node := range region.Nodes().All() { // we don't care if we override some nodes
|
||||||
addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName)
|
addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msgf("bootstrap DNS lookup failed %q", node.HostName)
|
Msgf("bootstrap DNS lookup failed %q", node.HostName())
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dnsEntries[node.HostName] = addrs
|
dnsEntries[node.HostName()] = addrs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writer.Header().Set("Content-Type", "application/json")
|
writer.Header().Set("Content-Type", "application/json")
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/db"
|
"github.com/juanfont/headscale/hscontrol/db"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/derp"
|
||||||
"github.com/juanfont/headscale/hscontrol/state"
|
"github.com/juanfont/headscale/hscontrol/state"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||||
@ -167,6 +168,12 @@ func setupBatcherWithTestData(t *testing.T, bf batcherFunc, userCount, nodesPerU
|
|||||||
t.Fatalf("Failed to create state: %v", err)
|
t.Fatalf("Failed to create state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
derpMap, err := derp.GetDERPMap(cfg.DERP)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, derpMap)
|
||||||
|
|
||||||
|
state.SetDERPMap(derpMap)
|
||||||
|
|
||||||
// Set up a permissive policy that allows all communication for testing
|
// Set up a permissive policy that allows all communication for testing
|
||||||
allowAllPolicy := `{
|
allowAllPolicy := `{
|
||||||
"acls": [
|
"acls": [
|
||||||
|
@ -79,7 +79,7 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
|
|||||||
|
|
||||||
// WithDERPMap adds the DERP map to the response
|
// WithDERPMap adds the DERP map to the response
|
||||||
func (b *MapResponseBuilder) WithDERPMap() *MapResponseBuilder {
|
func (b *MapResponseBuilder) WithDERPMap() *MapResponseBuilder {
|
||||||
b.resp.DERPMap = b.mapper.state.DERPMap()
|
b.resp.DERPMap = b.mapper.state.DERPMap().AsStruct()
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
hsdb "github.com/juanfont/headscale/hscontrol/db"
|
hsdb "github.com/juanfont/headscale/hscontrol/db"
|
||||||
"github.com/juanfont/headscale/hscontrol/derp"
|
|
||||||
"github.com/juanfont/headscale/hscontrol/policy"
|
"github.com/juanfont/headscale/hscontrol/policy"
|
||||||
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
||||||
"github.com/juanfont/headscale/hscontrol/routes"
|
"github.com/juanfont/headscale/hscontrol/routes"
|
||||||
@ -56,7 +56,7 @@ type State struct {
|
|||||||
// ipAlloc manages IP address allocation for nodes
|
// ipAlloc manages IP address allocation for nodes
|
||||||
ipAlloc *hsdb.IPAllocator
|
ipAlloc *hsdb.IPAllocator
|
||||||
// derpMap contains the current DERP relay configuration
|
// derpMap contains the current DERP relay configuration
|
||||||
derpMap *tailcfg.DERPMap
|
derpMap atomic.Pointer[tailcfg.DERPMap]
|
||||||
// polMan handles policy evaluation and management
|
// polMan handles policy evaluation and management
|
||||||
polMan policy.PolicyManager
|
polMan policy.PolicyManager
|
||||||
// registrationCache caches node registration data to reduce database load
|
// registrationCache caches node registration data to reduce database load
|
||||||
@ -87,8 +87,6 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||||||
return nil, fmt.Errorf("init ip allocatior: %w", err)
|
return nil, fmt.Errorf("init ip allocatior: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
derpMap := derp.GetDERPMap(cfg.DERP)
|
|
||||||
|
|
||||||
nodes, err := db.ListNodes()
|
nodes, err := db.ListNodes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading nodes: %w", err)
|
return nil, fmt.Errorf("loading nodes: %w", err)
|
||||||
@ -108,17 +106,17 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||||||
return nil, fmt.Errorf("init policy manager: %w", err)
|
return nil, fmt.Errorf("init policy manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &State{
|
s := &State{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
|
||||||
db: db,
|
db: db,
|
||||||
ipAlloc: ipAlloc,
|
ipAlloc: ipAlloc,
|
||||||
// TODO(kradalby): Update DERPMap
|
|
||||||
derpMap: derpMap,
|
|
||||||
polMan: polMan,
|
polMan: polMan,
|
||||||
registrationCache: registrationCache,
|
registrationCache: registrationCache,
|
||||||
primaryRoutes: routes.New(),
|
primaryRoutes: routes.New(),
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close gracefully shuts down the State instance and releases all resources.
|
// Close gracefully shuts down the State instance and releases all resources.
|
||||||
@ -171,9 +169,14 @@ func policyBytes(db *hsdb.HSDatabase, cfg *types.Config) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedPolicyMode, cfg.Policy.Mode)
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedPolicyMode, cfg.Policy.Mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDERPMap updates the DERP relay configuration.
|
||||||
|
func (s *State) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||||
|
s.derpMap.Store(dm)
|
||||||
|
}
|
||||||
|
|
||||||
// DERPMap returns the current DERP relay configuration for peer-to-peer connectivity.
|
// DERPMap returns the current DERP relay configuration for peer-to-peer connectivity.
|
||||||
func (s *State) DERPMap() *tailcfg.DERPMap {
|
func (s *State) DERPMap() tailcfg.DERPMapView {
|
||||||
return s.derpMap
|
return s.derpMap.Load().View()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadPolicy reloads the access control policy and triggers auto-approval if changed.
|
// ReloadPolicy reloads the access control policy and triggers auto-approval if changed.
|
||||||
@ -210,7 +213,6 @@ func (s *State) CreateUser(user types.User) (*types.User, bool, error) {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
|
||||||
if err := s.db.DB.Save(&user).Error; err != nil {
|
if err := s.db.DB.Save(&user).Error; err != nil {
|
||||||
return nil, false, fmt.Errorf("creating user: %w", err)
|
return nil, false, fmt.Errorf("creating user: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -300,6 +300,7 @@ func LoadConfig(path string, isFile bool) error {
|
|||||||
viper.SetDefault("derp.server.verify_clients", true)
|
viper.SetDefault("derp.server.verify_clients", true)
|
||||||
viper.SetDefault("derp.server.stun.enabled", true)
|
viper.SetDefault("derp.server.stun.enabled", true)
|
||||||
viper.SetDefault("derp.server.automatically_add_embedded_derp_region", true)
|
viper.SetDefault("derp.server.automatically_add_embedded_derp_region", true)
|
||||||
|
viper.SetDefault("derp.update_frequency", "3h")
|
||||||
|
|
||||||
viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock")
|
viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock")
|
||||||
viper.SetDefault("unix_socket_permission", "0o770")
|
viper.SetDefault("unix_socket_permission", "0o770")
|
||||||
|
@ -181,6 +181,7 @@ nav:
|
|||||||
- TLS: ref/tls.md
|
- TLS: ref/tls.md
|
||||||
- ACLs: ref/acls.md
|
- ACLs: ref/acls.md
|
||||||
- DNS: ref/dns.md
|
- DNS: ref/dns.md
|
||||||
|
- DERP: ref/derp.md
|
||||||
- Remote CLI: ref/remote-cli.md
|
- Remote CLI: ref/remote-cli.md
|
||||||
- Debug: ref/debug.md
|
- Debug: ref/debug.md
|
||||||
- Integration:
|
- Integration:
|
||||||
|
Loading…
Reference in New Issue
Block a user