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
- TestNodeMoveCommand
- TestPolicyCommand
- TestPolicyBrokenConfigCommand
- TestDERPVerifyEndpoint
- TestPolicyBrokenConfigCommand
- TestResolveMagicDNS
- TestValidateResolvConf
- TestDERPServerScenario

View file

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

View file

@ -64,41 +64,54 @@ 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")
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
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
}
allow, err := doVerify()
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("Failed to verify client")
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
}
}
resp := tailcfg.DERPAdmitClientResponse{
Allow: allow,
}

View file

@ -223,16 +223,6 @@ 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,7 +13,6 @@ 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) {
@ -45,34 +44,50 @@ func TestDERPVerifyEndpoint(t *testing.T) {
)
assertNoErr(t, err)
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(),
},
},
},
},
}
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"
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithCACert(derper.GetCert())},
headscale, err := scenario.Headscale(
hsic.WithHostname(hostname),
hsic.WithPort(headscalePort),
hsic.WithCustomTLS(certHeadscale, keyHeadscale),
hsic.WithHostnameAsServerURL(),
hsic.WithDERPConfig(derpMap))
hsic.WithCustomDERPServerOnly([]byte(derpConfig)),
)
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,
tsic.hostname+"-network",
fmt.Sprintf("%s-network", tsic.hostname),
)
if err != nil {
log.Fatalf("failed to create network: %s", err)
@ -135,15 +135,14 @@ func New(
opt(dsic)
}
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)
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)
if dsic.withVerifyClientURL != "" {
fmt.Fprintf(&cmdArgs, " --verify-client-url=%s", dsic.withVerifyClientURL)
cmdArgs += " --verify-client-url=" + dsic.withVerifyClientURL
}
runOptions := &dockertest.RunOptions{
@ -151,7 +150,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.String()},
Entrypoint: []string{"/bin/sh", "-c", "/bin/sleep 3 ; update-ca-certificates ; derper " + cmdArgs},
ExposedPorts: []string{
"80/tcp",
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 dsic, nil
}
@ -240,7 +238,6 @@ func (t *DERPServerInContainer) Shutdown() error {
fmt.Errorf("failed to save log: %w", err),
)
}
return t.pool.Purge(t.container)
}
@ -270,18 +267,18 @@ func (t *DERPServerInContainer) GetHostname() string {
}
// GetSTUNPort returns the STUN port of the DERPer instance.
func (t *DERPServerInContainer) GetSTUNPort() int {
return t.stunPort
func (t *DERPServerInContainer) GetSTUNPort() string {
return strconv.Itoa(t.stunPort)
}
// GetDERPPort returns the DERP port of the DERPer instance.
func (t *DERPServerInContainer) GetDERPPort() int {
return t.derpPort
func (t *DERPServerInContainer) GetDERPPort() string {
return strconv.Itoa(t.derpPort)
}
// WaitForRunning blocks until the DERPer instance is ready to be used.
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)
insecureTransport := http.DefaultTransport.(*http.Transport).Clone() //nolint

View file

@ -25,7 +25,6 @@ import (
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"tailscale.com/tailcfg"
)
const (
@ -217,17 +216,10 @@ func WithEmbeddedDERPServerOnly() Option {
}
}
// WithDERPConfig configures Headscale use a custom
// WithCustomDERPServerOnly configures Headscale use a custom
// DERP server only.
func WithDERPConfig(derpMap tailcfg.DERPMap) Option {
func WithCustomDERPServerOnly(contents []byte) 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("tsic.DebugDERPRegion() called with unsupported version: " + t.version)
panic(fmt.Sprintf("tsic.DebugDERPRegion() called with unsupported version: %s", t.version))
}
command := []string{
@ -687,18 +687,17 @@ func (t *TailscaleInContainer) DebugDERPRegion(region string) (*ipnstate.DebugDE
result, stderr, err := t.Execute(command)
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)
}
var report ipnstate.DebugDERPRegionReport
err = json.Unmarshal([]byte(result), &report)
var st ipnstate.DebugDERPRegionReport
err = json.Unmarshal([]byte(result), &st)
if err != nil {
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.