Compare commits

..

12 commits

Author SHA1 Message Date
ArcticLampyrid
73e038fa5c
Merge dfebd176df into e7245856c5 2024-11-15 03:12:32 +08:00
ArcticLampyrid
dfebd176df
refactor(verify-client): simplify error handling 2024-11-15 03:12:26 +08:00
ArcticLampyrid
bb79471139
tests(verify-client): perfer to use CreateHeadscaleEnv 2024-11-15 03:07:00 +08:00
ArcticLampyrid
28a40c0019
chore: cleanup 2024-11-15 02:59:05 +08:00
ArcticLampyrid
b0f074d18d
tests: fix derper failure 2024-11-15 02:50:02 +08:00
ArcticLampyrid
689c0983dc
ci: fix tests order 2024-11-15 02:34:58 +08:00
ArcticLampyrid
4f294c243c
tests(dsic): use string builder for cmd args 2024-11-15 02:33:48 +08:00
ArcticLampyrid
b3beb73f3f
refactor: introduce func ContainsNodeKey 2024-11-15 02:32:03 +08:00
ArcticLampyrid
b6adc84dc6
tests: use tailcfg.DERPMap instead of []byte 2024-11-15 02:31:36 +08:00
ArcticLampyrid
c726224e53
tests: add integration test for DERP verify endpoint 2024-11-15 02:19:52 +08:00
117503445
a5f27b2265
docs: fix doc for integration test 2024-11-15 02:19:52 +08:00
117503445
13454a3f2d
feat: support client verify for derp 2024-11-15 02:19:52 +08:00
8 changed files with 93 additions and 99 deletions

View file

@ -37,8 +37,8 @@ jobs:
- TestNodeRenameCommand
- TestNodeMoveCommand
- TestPolicyCommand
- TestDERPVerifyEndpoint
- TestPolicyBrokenConfigCommand
- TestDERPVerifyEndpoint
- TestResolveMagicDNS
- TestValidateResolvConf
- TestDERPServerScenario

View file

@ -1,6 +1,6 @@
# For testing purposes only
FROM golang:1.22-alpine AS build-env
FROM golang:alpine AS build-env
WORKDIR /go/src

View file

@ -64,52 +64,39 @@ func (h *Headscale) VerifyHandler(
) {
if req.Method != http.MethodPost {
http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
return
}
log.Debug().
Str("handler", "/verify").
Msg("verify client")
body, err := io.ReadAll(req.Body)
if err != nil {
log.Error().
Str("handler", "/verify").
Err(err).
Msg("Cannot read request body")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse derpAdmitClientRequest")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
nodes, err := h.db.ListNodes()
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot list nodes")
http.Error(writer, "Internal error", http.StatusInternalServerError)
}
for _, node := range nodes {
log.Debug().Str("node", node.NodeKey.String()).Msg("Node")
}
allow := false
// Check if the node is in the list of nodes
for _, node := range nodes {
if node.NodeKey == derpAdmitClientRequest.NodePublic {
allow = true
break
doVerify := func() (bool, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return false, fmt.Errorf("cannot read request body: %w", err)
}
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
return false, fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err)
}
nodes, err := h.db.ListNodes()
if err != nil {
return false, fmt.Errorf("cannot list nodes: %w", err)
}
return nodes.ContainsNodeKey(derpAdmitClientRequest.NodePublic), nil
}
allow, err := doVerify()
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to verify client")
http.Error(writer, "Internal error", http.StatusInternalServerError)
}
resp := tailcfg.DERPAdmitClientResponse{

View file

@ -223,6 +223,16 @@ func (nodes Nodes) FilterByIP(ip netip.Addr) Nodes {
return found
}
func (nodes Nodes) ContainsNodeKey(nodeKey key.NodePublic) bool {
for _, node := range nodes {
if node.NodeKey == nodeKey {
return true
}
}
return false
}
func (node *Node) Proto() *v1.Node {
nodeProto := &v1.Node{
Id: uint64(node.ID),

View file

@ -13,6 +13,7 @@ import (
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic"
"tailscale.com/tailcfg"
)
func TestDERPVerifyEndpoint(t *testing.T) {
@ -44,50 +45,34 @@ func TestDERPVerifyEndpoint(t *testing.T) {
)
assertNoErr(t, err)
derpConfig := "regions:\n"
derpConfig += " 900:\n"
derpConfig += " regionid: 900\n"
derpConfig += " regioncode: test-derpverify\n"
derpConfig += " regionname: TestDerpVerify\n"
derpConfig += " nodes:\n"
derpConfig += " - name: TestDerpVerify\n"
derpConfig += " regionid: 900\n"
derpConfig += " hostname: " + derper.GetHostname() + "\n"
derpConfig += " stunport: " + derper.GetSTUNPort() + "\n"
derpConfig += " stunonly: false\n"
derpConfig += " derpport: " + derper.GetDERPPort() + "\n"
derpMap := tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
900: {
RegionID: 900,
RegionCode: "test-derpverify",
RegionName: "TestDerpVerify",
Nodes: []*tailcfg.DERPNode{
{
Name: "TestDerpVerify",
RegionID: 900,
HostName: derper.GetHostname(),
STUNPort: derper.GetSTUNPort(),
STUNOnly: false,
DERPPort: derper.GetDERPPort(),
},
},
},
},
}
headscale, err := scenario.Headscale(
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithCACert(derper.GetCert())},
hsic.WithHostname(hostname),
hsic.WithPort(headscalePort),
hsic.WithCustomTLS(certHeadscale, keyHeadscale),
hsic.WithHostnameAsServerURL(),
hsic.WithCustomDERPServerOnly([]byte(derpConfig)),
)
hsic.WithDERPConfig(derpMap))
assertNoErrHeadscaleEnv(t, err)
for userName, clientCount := range spec {
err = scenario.CreateUser(userName)
if err != nil {
t.Fatalf("failed to create user %s: %s", userName, err)
}
err = scenario.CreateTailscaleNodesInUser(userName, "all", clientCount, tsic.WithCACert(derper.GetCert()))
if err != nil {
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
}
key, err := scenario.CreatePreAuthKey(userName, true, true)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
}
allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)

View file

@ -72,7 +72,7 @@ func WithOrCreateNetwork(network *dockertest.Network) Option {
network, err := dockertestutil.GetFirstOrCreateNetwork(
tsic.pool,
fmt.Sprintf("%s-network", tsic.hostname),
tsic.hostname+"-network",
)
if err != nil {
log.Fatalf("failed to create network: %s", err)
@ -135,14 +135,15 @@ func New(
opt(dsic)
}
cmdArgs := "--hostname=" + hostname
cmdArgs += " --certmode=manual"
cmdArgs += " --certdir=" + DERPerCertRoot
cmdArgs += " --a=:" + strconv.Itoa(dsic.derpPort)
cmdArgs += " --stun=true"
cmdArgs += " --stun-port=" + strconv.Itoa(dsic.stunPort)
var cmdArgs strings.Builder
fmt.Fprintf(&cmdArgs, "--hostname=%s", hostname)
fmt.Fprintf(&cmdArgs, " --certmode=manual")
fmt.Fprintf(&cmdArgs, " --certdir=%s", DERPerCertRoot)
fmt.Fprintf(&cmdArgs, " --a=:%d", dsic.derpPort)
fmt.Fprintf(&cmdArgs, " --stun=true")
fmt.Fprintf(&cmdArgs, " --stun-port=%d", dsic.stunPort)
if dsic.withVerifyClientURL != "" {
cmdArgs += " --verify-client-url=" + dsic.withVerifyClientURL
fmt.Fprintf(&cmdArgs, " --verify-client-url=%s", dsic.withVerifyClientURL)
}
runOptions := &dockertest.RunOptions{
@ -150,7 +151,7 @@ func New(
Networks: []*dockertest.Network{dsic.network},
ExtraHosts: dsic.withExtraHosts,
// we currently need to give us some time to inject the certificate further down.
Entrypoint: []string{"/bin/sh", "-c", "/bin/sleep 3 ; update-ca-certificates ; derper " + cmdArgs},
Entrypoint: []string{"/bin/sh", "-c", "/bin/sleep 3 ; update-ca-certificates ; derper " + cmdArgs.String()},
ExposedPorts: []string{
"80/tcp",
fmt.Sprintf("%d/tcp", dsic.derpPort),
@ -225,6 +226,7 @@ func New(
return nil, fmt.Errorf("failed to write TLS key to container: %w", err)
}
}
return dsic, nil
}
@ -238,6 +240,7 @@ func (t *DERPServerInContainer) Shutdown() error {
fmt.Errorf("failed to save log: %w", err),
)
}
return t.pool.Purge(t.container)
}
@ -267,18 +270,18 @@ func (t *DERPServerInContainer) GetHostname() string {
}
// GetSTUNPort returns the STUN port of the DERPer instance.
func (t *DERPServerInContainer) GetSTUNPort() string {
return strconv.Itoa(t.stunPort)
func (t *DERPServerInContainer) GetSTUNPort() int {
return t.stunPort
}
// GetDERPPort returns the DERP port of the DERPer instance.
func (t *DERPServerInContainer) GetDERPPort() string {
return strconv.Itoa(t.derpPort)
func (t *DERPServerInContainer) GetDERPPort() int {
return t.derpPort
}
// WaitForRunning blocks until the DERPer instance is ready to be used.
func (t *DERPServerInContainer) WaitForRunning() error {
url := "https://" + net.JoinHostPort(t.GetHostname(), t.GetDERPPort()) + "/"
url := "https://" + net.JoinHostPort(t.GetHostname(), strconv.Itoa(t.GetDERPPort())) + "/"
log.Printf("waiting for DERPer to be ready at %s", url)
insecureTransport := http.DefaultTransport.(*http.Transport).Clone() //nolint

View file

@ -25,6 +25,7 @@ import (
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"tailscale.com/tailcfg"
)
const (
@ -216,10 +217,17 @@ func WithEmbeddedDERPServerOnly() Option {
}
}
// WithCustomDERPServerOnly configures Headscale use a custom
// WithDERPConfig configures Headscale use a custom
// DERP server only.
func WithCustomDERPServerOnly(contents []byte) Option {
func WithDERPConfig(derpMap tailcfg.DERPMap) Option {
return func(hsic *HeadscaleInContainer) {
contents, err := json.Marshal(derpMap)
if err != nil {
log.Fatalf("failed to marshal DERP map: %s", err)
return
}
hsic.env["HEADSCALE_DERP_PATHS"] = "/etc/headscale/derp.yml"
hsic.filesInContainer = append(hsic.filesInContainer,
fileInContainer{

View file

@ -675,7 +675,7 @@ func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error
func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error) {
if !util.TailscaleVersionNewerOrEqual("1.34", t.version) {
panic(fmt.Sprintf("tsic.DebugDERPRegion() called with unsupported version: %s", t.version))
panic("tsic.DebugDERPRegion() called with unsupported version: " + t.version)
}
command := []string{
@ -687,17 +687,18 @@ func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDE
result, stderr, err := t.Execute(command)
if err != nil {
fmt.Printf("stderr: %s\n", stderr)
fmt.Printf("stderr: %s\n", stderr) // nolint
return nil, fmt.Errorf("failed to execute tailscale debug derp command: %w", err)
}
var st ipnstate.DebugDERPRegionReport
err = json.Unmarshal([]byte(result), &st)
var report ipnstate.DebugDERPRegionReport
err = json.Unmarshal([]byte(result), &report)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal tailscale derp region report: %w", err)
}
return &st, err
return &report, err
}
// Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance.