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:
parent
2dc2f3b3f0
commit
2cd8cab52a
@ -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.
|
||||||
#
|
#
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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 ®ion
|
||||||
|
})
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user