mirror of
https://github.com/juanfont/headscale.git
synced 2025-12-17 00:42:16 +00:00
Compare commits
7 Commits
v0.27.0
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18fb3f00a8 | ||
|
|
1980ee6ca4 | ||
|
|
6bb7f4383e | ||
|
|
0a43aab8f5 | ||
|
|
4bd614a559 | ||
|
|
19a33394f6 | ||
|
|
84fe3de251 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
## 0.27.0 (2025-xx-xx)
|
## 0.27.0 (2025-10-27)
|
||||||
|
|
||||||
**Minimum supported Tailscale client version: v1.64.0**
|
**Minimum supported Tailscale client version: v1.64.0**
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ WORKDIR /go/src/tailscale
|
|||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
RUN GOARCH=$TARGETARCH go install -v ./cmd/derper
|
RUN GOARCH=$TARGETARCH go install -v ./cmd/derper
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.22
|
||||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
|
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
|
||||||
|
|
||||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
# and are in no way endorsed by Headscale's maintainers as an
|
# and are in no way endorsed by Headscale's maintainers as an
|
||||||
# official nor supported release or distribution.
|
# official nor supported release or distribution.
|
||||||
|
|
||||||
FROM docker.io/golang:1.25-bookworm
|
FROM docker.io/golang:1.25-trixie
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ENV GOPATH /go
|
ENV GOPATH /go
|
||||||
WORKDIR /go/src/headscale
|
WORKDIR /go/src/headscale
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get --update install --no-install-recommends --yes less jq sqlite3 dnsutils \
|
||||||
&& apt-get install --no-install-recommends --yes less jq sqlite3 dnsutils \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& apt-get clean
|
&& apt-get clean
|
||||||
RUN mkdir -p /var/run/headscale
|
RUN mkdir -p /var/run/headscale
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ RUN GOARCH=$TARGETARCH go install -tags="${BUILD_TAGS}" -ldflags="\
|
|||||||
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
|
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
|
||||||
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
|
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.22
|
||||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
|
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables curl
|
||||||
|
|
||||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Both are available on the [GitHub releases page](https://github.com/juanfont/hea
|
|||||||
|
|
||||||
It is recommended to use our DEB packages to install headscale on a Debian based system as those packages configure a
|
It is recommended to use our DEB packages to install headscale on a Debian based system as those packages configure a
|
||||||
local user to run headscale, provide a default configuration and ship with a systemd service file. Supported
|
local user to run headscale, provide a default configuration and ship with a systemd service file. Supported
|
||||||
distributions are Ubuntu 22.04 or newer, Debian 11 or newer.
|
distributions are Ubuntu 22.04 or newer, Debian 12 or newer.
|
||||||
|
|
||||||
1. Download the [latest headscale package](https://github.com/juanfont/headscale/releases/latest) for your platform (`.deb` for Ubuntu and Debian).
|
1. Download the [latest headscale package](https://github.com/juanfont/headscale/releases/latest) for your platform (`.deb` for Ubuntu and Debian).
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -126,7 +127,17 @@ func shuffleDERPMap(dm *tailcfg.DERPMap) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, region := range dm.Regions {
|
// Collect region IDs and sort them to ensure deterministic iteration order.
|
||||||
|
// Map iteration order is non-deterministic in Go, which would cause the
|
||||||
|
// shuffle to be non-deterministic even with a fixed seed.
|
||||||
|
ids := make([]int, 0, len(dm.Regions))
|
||||||
|
for id := range dm.Regions {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
slices.Sort(ids)
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
region := dm.Regions[id]
|
||||||
if len(region.Nodes) == 0 {
|
if len(region.Nodes) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ func TestShuffleDERPMapDeterministic(t *testing.T) {
|
|||||||
RegionCode: "sea",
|
RegionCode: "sea",
|
||||||
RegionName: "Seattle",
|
RegionName: "Seattle",
|
||||||
Nodes: []*tailcfg.DERPNode{
|
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"},
|
{Name: "10d", RegionID: 10, HostName: "derp10d.tailscale.com"},
|
||||||
|
{Name: "10c", RegionID: 10, HostName: "derp10c.tailscale.com"},
|
||||||
|
{Name: "10b", RegionID: 10, HostName: "derp10b.tailscale.com"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
@@ -93,9 +93,9 @@ func TestShuffleDERPMapDeterministic(t *testing.T) {
|
|||||||
RegionCode: "sfo",
|
RegionCode: "sfo",
|
||||||
RegionName: "San Francisco",
|
RegionName: "San Francisco",
|
||||||
Nodes: []*tailcfg.DERPNode{
|
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: "2d", RegionID: 2, HostName: "derp2d.tailscale.com"},
|
||||||
|
{Name: "2e", RegionID: 2, HostName: "derp2e.tailscale.com"},
|
||||||
|
{Name: "2f", RegionID: 2, HostName: "derp2f.tailscale.com"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -169,6 +169,74 @@ func TestShuffleDERPMapDeterministic(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "same dataset with another base domain",
|
||||||
|
baseDomain: "another.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: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same dataset with yet another base domain",
|
||||||
|
baseDomain: "yetanother.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: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -1232,26 +1232,26 @@ func (h *HeadscaleInContainer) writePolicy(pol *policyv2.Policy) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HeadscaleInContainer) PID() (int, error) {
|
func (h *HeadscaleInContainer) PID() (int, error) {
|
||||||
cmd := []string{"bash", "-c", `ps aux | grep headscale | grep -v grep | awk '{print $2}'`}
|
// Use pidof to find the headscale process, which is more reliable than grep
|
||||||
output, err := h.Execute(cmd)
|
// as it only looks for the actual binary name, not processes that contain
|
||||||
|
// "headscale" in their command line (like the dlv debugger).
|
||||||
|
output, err := h.Execute([]string{"pidof", "headscale"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to execute command: %w", err)
|
// pidof returns exit code 1 when no process is found
|
||||||
|
return 0, os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.TrimSpace(output)
|
// pidof returns space-separated PIDs on a single line
|
||||||
if lines == "" {
|
pidStrs := strings.Fields(strings.TrimSpace(output))
|
||||||
return 0, os.ErrNotExist // No output means no process found
|
if len(pidStrs) == 0 {
|
||||||
|
return 0, os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
pids := make([]int, 0, len(lines))
|
pids := make([]int, 0, len(pidStrs))
|
||||||
for _, line := range strings.Split(lines, "\n") {
|
for _, pidStr := range pidStrs {
|
||||||
line = strings.TrimSpace(line)
|
pidInt, err := strconv.Atoi(pidStr)
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pidInt, err := strconv.Atoi(line)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("parsing PID: %w", err)
|
return 0, fmt.Errorf("parsing PID %q: %w", pidStr, err)
|
||||||
}
|
}
|
||||||
// We dont care about the root pid for the container
|
// We dont care about the root pid for the container
|
||||||
if pidInt == 1 {
|
if pidInt == 1 {
|
||||||
@@ -1266,7 +1266,9 @@ func (h *HeadscaleInContainer) PID() (int, error) {
|
|||||||
case 1:
|
case 1:
|
||||||
return pids[0], nil
|
return pids[0], nil
|
||||||
default:
|
default:
|
||||||
return 0, errors.New("multiple headscale processes running")
|
// If we still have multiple PIDs, return the first one as a fallback
|
||||||
|
// This can happen in edge cases during startup/shutdown
|
||||||
|
return pids[0], nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
xmaps "golang.org/x/exp/maps"
|
xmaps "golang.org/x/exp/maps"
|
||||||
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@@ -2215,11 +2216,31 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we should run the full matrix of tests
|
||||||
|
// By default, we only run a minimal subset to avoid overwhelming Docker/disk
|
||||||
|
// Set HEADSCALE_INTEGRATION_FULL_MATRIX=1 to run all combinations
|
||||||
|
fullMatrix := envknob.Bool("HEADSCALE_INTEGRATION_FULL_MATRIX")
|
||||||
|
|
||||||
|
// Minimal test set: 3 tests covering all key dimensions
|
||||||
|
// - Both auth methods (authkey, webauth)
|
||||||
|
// - All 3 approver types (tag, user, group)
|
||||||
|
// - Both policy modes (database, file)
|
||||||
|
// - Both advertiseDuringUp values (true, false)
|
||||||
|
minimalTestSet := map[string]bool{
|
||||||
|
"authkey-tag-advertiseduringup-false-pol-database": true, // authkey + database + tag + false
|
||||||
|
"webauth-user-advertiseduringup-true-pol-file": true, // webauth + file + user + true
|
||||||
|
"authkey-group-advertiseduringup-false-pol-file": true, // authkey + file + group + false
|
||||||
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
for _, polMode := range []types.PolicyMode{types.PolicyModeDB, types.PolicyModeFile} {
|
for _, polMode := range []types.PolicyMode{types.PolicyModeDB, types.PolicyModeFile} {
|
||||||
for _, advertiseDuringUp := range []bool{false, true} {
|
for _, advertiseDuringUp := range []bool{false, true} {
|
||||||
name := fmt.Sprintf("%s-advertiseduringup-%t-pol-%s", tt.name, advertiseDuringUp, polMode)
|
name := fmt.Sprintf("%s-advertiseduringup-%t-pol-%s", tt.name, advertiseDuringUp, polMode)
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// Skip tests not in minimal set unless full matrix is enabled
|
||||||
|
if !fullMatrix && !minimalTestSet[name] {
|
||||||
|
t.Skip("Skipping to reduce test matrix size. Set HEADSCALE_INTEGRATION_FULL_MATRIX=1 to run all tests.")
|
||||||
|
}
|
||||||
scenario, err := NewScenario(tt.spec)
|
scenario, err := NewScenario(tt.spec)
|
||||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||||
defer scenario.ShutdownAssertNoPanics(t)
|
defer scenario.ShutdownAssertNoPanics(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user