1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-06-05 01:20:21 +02:00

chore: merge upstream changes

This commit is contained in:
seiuneko 2025-05-15 00:27:50 +08:00
parent 2dc2f3b3f0
commit 2cd8cab52a
No known key found for this signature in database
GPG Key ID: A5A75952899A0179
9 changed files with 181 additions and 50 deletions

View File

@ -85,6 +85,9 @@ derp:
region_code: "headscale" region_code: "headscale"
region_name: "Headscale Embedded DERP" region_name: "Headscale Embedded DERP"
# Verify clients to this DERP server using the Headscale node list
verify_clients: true
# Listens over UDP at the configured address for STUN connections - to help with NAT traversal. # Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
# When the embedded DERP server is enabled stun_listen_addr MUST be defined. # When the embedded DERP server is enabled stun_listen_addr MUST be defined.
# #

View File

@ -226,6 +226,14 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
) )
} }
if cfg.DERP.ServerVerifyClients {
t := http.DefaultTransport.(*http.Transport)
t.RegisterProtocol(
derpServer.DerpVerifyScheme,
derpServer.NewDERPVerifyTransport(app.handleVerifyRequest),
)
}
embeddedDERPServer, err := derpServer.NewDERPServer( embeddedDERPServer, err := derpServer.NewDERPServer(
cfg.ServerURL, cfg.ServerURL,
key.NodePrivate(*derpServerKey), key.NodePrivate(*derpServerKey),

View File

@ -2,9 +2,11 @@ package server
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
@ -28,7 +30,10 @@ import (
// server that the DERP HTTP client does not want the HTTP 101 response // server that the DERP HTTP client does not want the HTTP 101 response
// headers and it will begin writing & reading the DERP protocol immediately // headers and it will begin writing & reading the DERP protocol immediately
// following its HTTP request. // following its HTTP request.
const fastStartHeader = "Derp-Fast-Start" const (
fastStartHeader = "Derp-Fast-Start"
DerpVerifyScheme = "headscale-derp-verify"
)
type DERPServer struct { type DERPServer struct {
serverURL string serverURL string
@ -45,6 +50,11 @@ func NewDERPServer(
log.Trace().Caller().Msg("Creating new embedded DERP server") log.Trace().Caller().Msg("Creating new embedded DERP server")
server := derp.NewServer(derpKey, util.TSLogfWrapper()) // nolint // zerolinter complains server := derp.NewServer(derpKey, util.TSLogfWrapper()) // nolint // zerolinter complains
if cfg.ServerVerifyClients {
server.SetVerifyClientURL(DerpVerifyScheme + "://verify")
server.SetVerifyClientURLFailOpen(false)
}
return &DERPServer{ return &DERPServer{
serverURL: serverURL, serverURL: serverURL,
key: derpKey, key: derpKey,
@ -360,3 +370,32 @@ func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
} }
} }
} }
func NewDERPVerifyTransport(handleVerifyRequest func(*http.Request, io.Writer) error) *DERPVerifyTransport {
return &DERPVerifyTransport{
handleVerifyRequest: handleVerifyRequest,
}
}
type DERPVerifyTransport struct {
handleVerifyRequest func(*http.Request, io.Writer) error
}
func (t *DERPVerifyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
buf := new(bytes.Buffer)
if err := t.handleVerifyRequest(req, buf); err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to handle verify request")
return nil, err
}
resp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(buf),
}
return resp, nil
}

View File

@ -81,28 +81,33 @@ func parseCabailityVersion(req *http.Request) (tailcfg.CapabilityVersion, error)
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
} }
func (h *Headscale) derpRequestIsAllowed( func (h *Headscale) handleVerifyRequest(
req *http.Request, req *http.Request,
) (bool, error) { writer io.Writer,
) 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) return fmt.Errorf("cannot read request body: %w", err)
} }
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) return fmt.Errorf("cannot parse derpAdmitClientRequest: %w", err)
} }
nodes, err := h.db.ListNodes() nodes, err := h.db.ListNodes()
if err != nil { if err != nil {
return false, fmt.Errorf("cannot list nodes: %w", err) return fmt.Errorf("cannot list nodes: %w", err)
} }
return nodes.ContainsNodeKey(derpAdmitClientRequest.NodePublic), nil resp := &tailcfg.DERPAdmitClientResponse{
Allow: nodes.ContainsNodeKey(derpAdmitClientRequest.NodePublic),
}
return json.NewEncoder(writer).Encode(resp)
} }
// see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159, Derp use verifyClientsURL to verify whether a client is allowed to connect to the DERP server. // VerifyHandler see https://github.com/tailscale/tailscale/blob/964282d34f06ecc06ce644769c66b0b31d118340/derp/derp_server.go#L1159
// DERP use verifyClientsURL to verify whether a client is allowed to connect to the DERP server.
func (h *Headscale) VerifyHandler( func (h *Headscale) VerifyHandler(
writer http.ResponseWriter, writer http.ResponseWriter,
req *http.Request, req *http.Request,
@ -112,18 +117,12 @@ func (h *Headscale) VerifyHandler(
return return
} }
allow, err := h.derpRequestIsAllowed(req) err := h.handleVerifyRequest(req, writer)
if err != nil { if err != nil {
httpError(writer, err) httpError(writer, err)
return return
} }
resp := tailcfg.DERPAdmitClientResponse{
Allow: allow,
}
writer.Header().Set("Content-Type", "application/json") writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(writer).Encode(resp)
} }
// KeyHandler provides the Headscale pub key // KeyHandler provides the Headscale pub key

View File

@ -194,6 +194,7 @@ type DERPConfig struct {
ServerRegionCode string ServerRegionCode string
ServerRegionName string ServerRegionName string
ServerPrivateKeyPath string ServerPrivateKeyPath string
ServerVerifyClients bool
STUNAddr string STUNAddr string
URLs []url.URL URLs []url.URL
Paths []string Paths []string
@ -458,6 +459,7 @@ func derpConfig() DERPConfig {
serverRegionID := viper.GetInt("derp.server.region_id") serverRegionID := viper.GetInt("derp.server.region_id")
serverRegionCode := viper.GetString("derp.server.region_code") serverRegionCode := viper.GetString("derp.server.region_code")
serverRegionName := viper.GetString("derp.server.region_name") serverRegionName := viper.GetString("derp.server.region_name")
serverVerifyClients := viper.GetBool("derp.server.verify_clients")
stunAddr := viper.GetString("derp.server.stun_listen_addr") stunAddr := viper.GetString("derp.server.stun_listen_addr")
privateKeyPath := util.AbsolutePathFromConfigPath( privateKeyPath := util.AbsolutePathFromConfigPath(
viper.GetString("derp.server.private_key_path"), viper.GetString("derp.server.private_key_path"),
@ -502,6 +504,7 @@ func derpConfig() DERPConfig {
ServerRegionID: serverRegionID, ServerRegionID: serverRegionID,
ServerRegionCode: serverRegionCode, ServerRegionCode: serverRegionCode,
ServerRegionName: serverRegionName, ServerRegionName: serverRegionName,
ServerVerifyClients: serverVerifyClients,
ServerPrivateKeyPath: privateKeyPath, ServerPrivateKeyPath: privateKeyPath,
STUNAddr: stunAddr, STUNAddr: stunAddr,
URLs: urls, URLs: urls,

View File

@ -1,11 +1,10 @@
package integration package integration
import ( import (
"encoding/json" "context"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
"strings"
"testing" "testing"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
@ -13,7 +12,11 @@ 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/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/net/netmon"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key"
) )
func TestDERPVerifyEndpoint(t *testing.T) { func TestDERPVerifyEndpoint(t *testing.T) {
@ -46,10 +49,7 @@ func TestDERPVerifyEndpoint(t *testing.T) {
) )
assertNoErr(t, err) assertNoErr(t, err)
derpMap := tailcfg.DERPMap{ derpRegion := tailcfg.DERPRegion{
Regions: map[int]*tailcfg.DERPRegion{
900: {
RegionID: 900,
RegionCode: "test-derpverify", RegionCode: "test-derpverify",
RegionName: "TestDerpVerify", RegionName: "TestDerpVerify",
Nodes: []*tailcfg.DERPNode{ Nodes: []*tailcfg.DERPNode{
@ -60,9 +60,13 @@ func TestDERPVerifyEndpoint(t *testing.T) {
STUNPort: derper.GetSTUNPort(), STUNPort: derper.GetSTUNPort(),
STUNOnly: false, STUNOnly: false,
DERPPort: derper.GetDERPPort(), DERPPort: derper.GetDERPPort(),
InsecureForTests: true,
}, },
}, },
}, }
derpMap := tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
900: &derpRegion,
}, },
} }
@ -76,21 +80,40 @@ func TestDERPVerifyEndpoint(t *testing.T) {
allClients, err := scenario.ListTailscaleClients() allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err) assertNoErrListClients(t, err)
for _, client := range allClients { fakeKey := key.NewNode()
report, err := client.DebugDERPRegion("test-derpverify") DERPVerify(t, fakeKey, derpRegion, false)
assertNoErr(t, err)
successful := false
for _, line := range report.Info {
if strings.Contains(line, "Successfully established a DERP connection with node") {
successful = true
break for _, client := range allClients {
} nodeKey, err := client.GetNodePrivateKey()
}
if !successful {
stJSON, err := json.Marshal(report)
assertNoErr(t, err) assertNoErr(t, err)
t.Errorf("Client %s could not establish a DERP connection: %s", client.Hostname(), string(stJSON)) DERPVerify(t, *nodeKey, derpRegion, true)
} }
}
func DERPVerify(
t *testing.T,
nodeKey key.NodePrivate,
region tailcfg.DERPRegion,
expectSuccess bool,
) {
t.Helper()
c := derphttp.NewRegionClient(nodeKey, t.Logf, netmon.NewStatic(), func() *tailcfg.DERPRegion {
return &region
})
var result error
if err := c.Connect(context.Background()); err != nil {
result = fmt.Errorf("client Connect: %w", err)
}
if m, err := c.Recv(); err != nil {
result = fmt.Errorf("client first Recv: %w", err)
} else if v, ok := m.(derp.ServerInfoMessage); !ok {
result = fmt.Errorf("client first Recv was unexpected type %T", v)
}
if expectSuccess && result != nil {
t.Fatalf("DERP verify failed unexpectedly for client %s. Expected success but got error: %v", nodeKey.Public(), result)
} else if !expectSuccess && result == nil {
t.Fatalf("DERP verify succeeded unexpectedly for client %s. Expected failure but it succeeded.", nodeKey.Public())
} }
} }

View File

@ -2,6 +2,8 @@ package integration
import ( import (
"strings" "strings"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"testing" "testing"
"time" "time"
@ -39,6 +41,28 @@ func TestDERPServerScenario(t *testing.T) {
t.Fail() t.Fail()
} }
} }
hsServer, err := scenario.Headscale()
assertNoErrGetHeadscale(t, err)
derpRegion := tailcfg.DERPRegion{
RegionCode: "test-derpverify",
RegionName: "TestDerpVerify",
Nodes: []*tailcfg.DERPNode{
{
Name: "TestDerpVerify",
RegionID: 900,
HostName: hsServer.GetHostname(),
STUNPort: 3478,
STUNOnly: false,
DERPPort: 443,
InsecureForTests: true,
},
},
}
fakeKey := key.NewNode()
DERPVerify(t, fakeKey, derpRegion, false)
}) })
} }
@ -102,6 +126,7 @@ func derpServerScenario(
"HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "true", "HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "true",
"HEADSCALE_DERP_UPDATE_FREQUENCY": "10s", "HEADSCALE_DERP_UPDATE_FREQUENCY": "10s",
"HEADSCALE_LISTEN_ADDR": "0.0.0.0:443", "HEADSCALE_LISTEN_ADDR": "0.0.0.0:443",
"HEADSCALE_DERP_SERVER_VERIFY_CLIENTS": "true",
}), }),
) )
assertNoErrHeadscaleEnv(t, err) assertNoErrHeadscaleEnv(t, err)

View File

@ -11,6 +11,7 @@ import (
"github.com/juanfont/headscale/integration/tsic" "github.com/juanfont/headscale/integration/tsic"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/net/netcheck" "tailscale.com/net/netcheck"
"tailscale.com/types/key"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
) )
@ -37,6 +38,7 @@ type TailscaleClient interface {
MustStatus() *ipnstate.Status MustStatus() *ipnstate.Status
Netmap() (*netmap.NetworkMap, error) Netmap() (*netmap.NetworkMap, error)
DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error) DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error)
GetNodePrivateKey() (*key.NodePrivate, error)
Netcheck() (*netcheck.Report, error) Netcheck() (*netcheck.Report, error)
WaitForNeedsLogin() error WaitForNeedsLogin() error
WaitForRunning() error WaitForRunning() error

View File

@ -26,7 +26,10 @@ import (
"github.com/ory/dockertest/v3/docker" "github.com/ory/dockertest/v3/docker"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/netcheck" "tailscale.com/net/netcheck"
"tailscale.com/paths"
"tailscale.com/types/key"
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
) )
@ -1228,3 +1231,29 @@ func (t *TailscaleInContainer) ReadFile(path string) ([]byte, error) {
return out.Bytes(), nil return out.Bytes(), nil
} }
func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
state, err := t.ReadFile(paths.DefaultTailscaledStateFile())
if err != nil {
return nil, fmt.Errorf("failed to read state file: %w", err)
}
store := &mem.Store{}
if err = store.LoadFromJSON(state); err != nil {
return nil, fmt.Errorf("failed to unmarshal state file: %w", err)
}
currentProfileKey, err := store.ReadState(ipn.CurrentProfileStateKey)
if err != nil {
return nil, fmt.Errorf("failed to read current profile state key: %w", err)
}
currentProfile, err := store.ReadState(ipn.StateKey(currentProfileKey))
if err != nil {
return nil, fmt.Errorf("failed to read current profile state: %w", err)
}
p := &ipn.Prefs{}
if err = json.Unmarshal(currentProfile, &p); err != nil {
return nil, fmt.Errorf("failed to unmarshal current profile state: %w", err)
}
return &p.Persist.PrivateNodeKey, nil
}