Compare commits

..

4 commits

Author SHA1 Message Date
ArcticLampyrid
47eb7f644c
Merge ff091dd56b into e7245856c5 2024-11-13 19:32:29 +00:00
ArcticLampyrid
ff091dd56b
tests: add integration test for DERP verify endpoint 2024-10-31 12:57:02 +08:00
117503445
63a9d16e79
docs: fix doc for integration test 2024-10-31 00:46:57 +08:00
117503445
ae0acf1084
feat: support client verify for derp 2024-10-31 00:46:57 +08:00
8 changed files with 93 additions and 87 deletions

View file

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

View file

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

View file

@ -64,41 +64,54 @@ func (h *Headscale) VerifyHandler(
) { ) {
if req.Method != http.MethodPost { if req.Method != http.MethodPost {
http.Error(writer, "Wrong method", http.StatusMethodNotAllowed) http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
return return
} }
log.Debug(). log.Debug().
Str("handler", "/verify"). Str("handler", "/verify").
Msg("verify client") Msg("verify client")
doVerify := func() (bool, error) {
body, err := io.ReadAll(req.Body) body, err := io.ReadAll(req.Body)
if err != nil { if err != nil {
return false, fmt.Errorf("cannot read request body: %w", err) log.Error().
Str("handler", "/verify").
Err(err).
Msg("Cannot read request body")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
} }
var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest var derpAdmitClientRequest tailcfg.DERPAdmitClientRequest
if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil { if err := json.Unmarshal(body, &derpAdmitClientRequest); err != nil {
return false, fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err) log.Error().
Caller().
Err(err).
Msg("Cannot parse derpAdmitClientRequest")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
} }
nodes, err := h.db.ListNodes() 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 { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
Err(err). Err(err).
Msg("Failed to verify client") Msg("Cannot list nodes")
http.Error(writer, "Internal error", http.StatusInternalServerError) 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
}
}
resp := tailcfg.DERPAdmitClientResponse{ resp := tailcfg.DERPAdmitClientResponse{
Allow: allow, Allow: allow,
} }

View file

@ -223,16 +223,6 @@ func (nodes Nodes) FilterByIP(ip netip.Addr) Nodes {
return found 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 { func (node *Node) Proto() *v1.Node {
nodeProto := &v1.Node{ nodeProto := &v1.Node{
Id: uint64(node.ID), Id: uint64(node.ID),

View file

@ -13,7 +13,6 @@ import (
"github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil" "github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic" "github.com/juanfont/headscale/integration/tsic"
"tailscale.com/tailcfg"
) )
func TestDERPVerifyEndpoint(t *testing.T) { func TestDERPVerifyEndpoint(t *testing.T) {
@ -45,34 +44,50 @@ func TestDERPVerifyEndpoint(t *testing.T) {
) )
assertNoErr(t, err) assertNoErr(t, err)
derpMap := tailcfg.DERPMap{ derpConfig := "regions:\n"
Regions: map[int]*tailcfg.DERPRegion{ derpConfig += " 900:\n"
900: { derpConfig += " regionid: 900\n"
RegionID: 900, derpConfig += " regioncode: test-derpverify\n"
RegionCode: "test-derpverify", derpConfig += " regionname: TestDerpVerify\n"
RegionName: "TestDerpVerify", derpConfig += " nodes:\n"
Nodes: []*tailcfg.DERPNode{ derpConfig += " - name: TestDerpVerify\n"
{ derpConfig += " regionid: 900\n"
Name: "TestDerpVerify", derpConfig += " hostname: " + derper.GetHostname() + "\n"
RegionID: 900, derpConfig += " stunport: " + derper.GetSTUNPort() + "\n"
HostName: derper.GetHostname(), derpConfig += " stunonly: false\n"
STUNPort: derper.GetSTUNPort(), derpConfig += " derpport: " + derper.GetDERPPort() + "\n"
STUNOnly: false,
DERPPort: derper.GetDERPPort(),
},
},
},
},
}
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithCACert(derper.GetCert())}, headscale, err := scenario.Headscale(
hsic.WithHostname(hostname), hsic.WithHostname(hostname),
hsic.WithPort(headscalePort), hsic.WithPort(headscalePort),
hsic.WithCustomTLS(certHeadscale, keyHeadscale), hsic.WithCustomTLS(certHeadscale, keyHeadscale),
hsic.WithHostnameAsServerURL(), hsic.WithHostnameAsServerURL(),
hsic.WithDERPConfig(derpMap)) hsic.WithCustomDERPServerOnly([]byte(derpConfig)),
)
assertNoErrHeadscaleEnv(t, err) 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() allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err) assertNoErrListClients(t, err)

View file

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

View file

@ -25,7 +25,6 @@ import (
"github.com/juanfont/headscale/integration/integrationutil" "github.com/juanfont/headscale/integration/integrationutil"
"github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker" "github.com/ory/dockertest/v3/docker"
"tailscale.com/tailcfg"
) )
const ( const (
@ -217,17 +216,10 @@ func WithEmbeddedDERPServerOnly() Option {
} }
} }
// WithDERPConfig configures Headscale use a custom // WithCustomDERPServerOnly configures Headscale use a custom
// DERP server only. // DERP server only.
func WithDERPConfig(derpMap tailcfg.DERPMap) Option { func WithCustomDERPServerOnly(contents []byte) Option {
return func(hsic *HeadscaleInContainer) { 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.env["HEADSCALE_DERP_PATHS"] = "/etc/headscale/derp.yml"
hsic.filesInContainer = append(hsic.filesInContainer, hsic.filesInContainer = append(hsic.filesInContainer,
fileInContainer{ 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) { func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error) {
if !util.TailscaleVersionNewerOrEqual("1.34", t.version) { if !util.TailscaleVersionNewerOrEqual("1.34", t.version) {
panic("tsic.DebugDERPRegion() called with unsupported version: " + t.version) panic(fmt.Sprintf("tsic.DebugDERPRegion() called with unsupported version: %s", t.version))
} }
command := []string{ command := []string{
@ -687,18 +687,17 @@ func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDE
result, stderr, err := t.Execute(command) result, stderr, err := t.Execute(command)
if err != nil { if err != nil {
fmt.Printf("stderr: %s\n", stderr) // nolint fmt.Printf("stderr: %s\n", stderr)
return nil, fmt.Errorf("failed to execute tailscale debug derp command: %w", err) return nil, fmt.Errorf("failed to execute tailscale debug derp command: %w", err)
} }
var report ipnstate.DebugDERPRegionReport var st ipnstate.DebugDERPRegionReport
err = json.Unmarshal([]byte(result), &report) err = json.Unmarshal([]byte(result), &st)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal tailscale derp region report: %w", err) return nil, fmt.Errorf("failed to unmarshal tailscale derp region report: %w", err)
} }
return &report, err return &st, err
} }
// Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance. // Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance.