Upgrade Go 1.21, Tailscale 1.50 and add Capability version support (#1563)
This commit is contained in:
parent
01b85e5232
commit
fb4ed95ff6
16 changed files with 277 additions and 132 deletions
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -36,6 +37,22 @@ const (
|
|||
var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(
|
||||
"machines registered with CLI does not support expire",
|
||||
)
|
||||
var ErrNoCapabilityVersion = errors.New("no capability version set")
|
||||
|
||||
func parseCabailityVersion(req *http.Request) (tailcfg.CapabilityVersion, error) {
|
||||
clientCapabilityStr := req.URL.Query().Get("v")
|
||||
|
||||
if clientCapabilityStr == "" {
|
||||
return 0, ErrNoCapabilityVersion
|
||||
}
|
||||
|
||||
clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse capability version: %w", err)
|
||||
}
|
||||
|
||||
return tailcfg.CapabilityVersion(clientCapabilityVersion), nil
|
||||
}
|
||||
|
||||
// KeyHandler provides the Headscale pub key
|
||||
// Listens in /key.
|
||||
|
@ -44,59 +61,79 @@ func (h *Headscale) KeyHandler(
|
|||
req *http.Request,
|
||||
) {
|
||||
// New Tailscale clients send a 'v' parameter to indicate the CurrentCapabilityVersion
|
||||
clientCapabilityStr := req.URL.Query().Get("v")
|
||||
if clientCapabilityStr != "" {
|
||||
log.Debug().
|
||||
Str("handler", "/key").
|
||||
Str("v", clientCapabilityStr).
|
||||
Msg("New noise client")
|
||||
clientCapabilityVersion, err := strconv.Atoi(clientCapabilityStr)
|
||||
if err != nil {
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, err := writer.Write([]byte("Wrong params"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TS2021 (Tailscale v2 protocol) requires to have a different key
|
||||
if clientCapabilityVersion >= NoiseCapabilityVersion {
|
||||
resp := tailcfg.OverTLSPublicKeyResponse{
|
||||
LegacyPublicKey: h.privateKey2019.Public(),
|
||||
PublicKey: h.noisePrivateKey.Public(),
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(writer).Encode(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Debug().
|
||||
Str("handler", "/key").
|
||||
Msg("New legacy client")
|
||||
|
||||
// Old clients don't send a 'v' parameter, so we send the legacy public key
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err := writer.Write([]byte(util.MachinePublicKeyStripPrefix(h.privateKey2019.Public())))
|
||||
capVer, err := parseCabailityVersion(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoCapabilityVersion) {
|
||||
log.Debug().
|
||||
Str("handler", "/key").
|
||||
Msg("New legacy client")
|
||||
// Old clients don't send a 'v' parameter, so we send the legacy public key
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err := writer.Write(
|
||||
[]byte(util.MachinePublicKeyStripPrefix(h.privateKey2019.Public())),
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
Msg("could not get capability version")
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("handler", "/key").
|
||||
Int("v", int(capVer)).
|
||||
Msg("New noise client")
|
||||
if err != nil {
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, err := writer.Write([]byte("Wrong params"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TS2021 (Tailscale v2 protocol) requires to have a different key
|
||||
if capVer >= NoiseCapabilityVersion {
|
||||
resp := tailcfg.OverTLSPublicKeyResponse{
|
||||
LegacyPublicKey: h.privateKey2019.Public(),
|
||||
PublicKey: h.noisePrivateKey.Public(),
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(writer).Encode(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ var debugDumpMapResponsePath = envknob.String("HEADSCALE_DEBUG_DUMP_MAPRESPONSE_
|
|||
type Mapper struct {
|
||||
privateKey2019 *key.MachinePrivate
|
||||
isNoise bool
|
||||
capVer tailcfg.CapabilityVersion
|
||||
|
||||
// Configuration
|
||||
// TODO(kradalby): figure out if this is the format we want this in
|
||||
|
@ -74,6 +75,7 @@ func NewMapper(
|
|||
peers types.Nodes,
|
||||
privateKey *key.MachinePrivate,
|
||||
isNoise bool,
|
||||
capVer tailcfg.CapabilityVersion,
|
||||
derpMap *tailcfg.DERPMap,
|
||||
baseDomain string,
|
||||
dnsCfg *tailcfg.DNSConfig,
|
||||
|
@ -91,6 +93,7 @@ func NewMapper(
|
|||
return &Mapper{
|
||||
privateKey2019: privateKey,
|
||||
isNoise: isNoise,
|
||||
capVer: capVer,
|
||||
|
||||
derpMap: derpMap,
|
||||
baseDomain: baseDomain,
|
||||
|
@ -221,10 +224,12 @@ func (m *Mapper) fullMapResponse(
|
|||
resp,
|
||||
pol,
|
||||
node,
|
||||
m.capVer,
|
||||
peers,
|
||||
peers,
|
||||
m.baseDomain,
|
||||
m.dnsCfg,
|
||||
m.randomClientPort,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -320,10 +325,12 @@ func (m *Mapper) PeerChangedResponse(
|
|||
&resp,
|
||||
pol,
|
||||
node,
|
||||
m.capVer,
|
||||
nodeMapToList(m.peers),
|
||||
changed,
|
||||
m.baseDomain,
|
||||
m.dnsCfg,
|
||||
m.randomClientPort,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -510,7 +517,7 @@ func (m *Mapper) baseWithConfigMapResponse(
|
|||
) (*tailcfg.MapResponse, error) {
|
||||
resp := m.baseMapResponse()
|
||||
|
||||
tailnode, err := tailNode(node, pol, m.dnsCfg, m.baseDomain)
|
||||
tailnode, err := tailNode(node, m.capVer, pol, m.dnsCfg, m.baseDomain, m.randomClientPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -527,8 +534,7 @@ func (m *Mapper) baseWithConfigMapResponse(
|
|||
resp.KeepAlive = false
|
||||
|
||||
resp.Debug = &tailcfg.Debug{
|
||||
DisableLogTail: !m.logtail,
|
||||
RandomizeClientPort: m.randomClientPort,
|
||||
DisableLogTail: !m.logtail,
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
|
@ -560,10 +566,12 @@ func appendPeerChanges(
|
|||
|
||||
pol *policy.ACLPolicy,
|
||||
node *types.Node,
|
||||
capVer tailcfg.CapabilityVersion,
|
||||
peers types.Nodes,
|
||||
changed types.Nodes,
|
||||
baseDomain string,
|
||||
dnsCfg *tailcfg.DNSConfig,
|
||||
randomClientPort bool,
|
||||
) error {
|
||||
fullChange := len(peers) == len(changed)
|
||||
|
||||
|
@ -594,7 +602,7 @@ func appendPeerChanges(
|
|||
peers,
|
||||
)
|
||||
|
||||
tailPeers, err := tailNodes(changed, pol, dnsCfg, baseDomain)
|
||||
tailPeers, err := tailNodes(changed, capVer, pol, dnsCfg, baseDomain, randomClientPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -234,12 +234,12 @@ func Test_fullMapResponse(t *testing.T) {
|
|||
PrimaryRoutes: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")},
|
||||
LastSeen: &lastSeen,
|
||||
Online: new(bool),
|
||||
KeepAlive: true,
|
||||
MachineAuthorized: true,
|
||||
Capabilities: []string{
|
||||
Capabilities: []tailcfg.NodeCapability{
|
||||
tailcfg.CapabilityFileSharing,
|
||||
tailcfg.CapabilityAdmin,
|
||||
tailcfg.CapabilitySSH,
|
||||
tailcfg.NodeAttrDisableUPnP,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -286,12 +286,12 @@ func Test_fullMapResponse(t *testing.T) {
|
|||
PrimaryRoutes: []netip.Prefix{},
|
||||
LastSeen: &lastSeen,
|
||||
Online: new(bool),
|
||||
KeepAlive: true,
|
||||
MachineAuthorized: true,
|
||||
Capabilities: []string{
|
||||
Capabilities: []tailcfg.NodeCapability{
|
||||
tailcfg.CapabilityFileSharing,
|
||||
tailcfg.CapabilityAdmin,
|
||||
tailcfg.CapabilitySSH,
|
||||
tailcfg.NodeAttrDisableUPnP,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -461,6 +461,7 @@ func Test_fullMapResponse(t *testing.T) {
|
|||
tt.peers,
|
||||
nil,
|
||||
false,
|
||||
0,
|
||||
tt.derpMap,
|
||||
tt.baseDomain,
|
||||
tt.dnsConfig,
|
||||
|
|
|
@ -15,18 +15,22 @@ import (
|
|||
|
||||
func tailNodes(
|
||||
nodes types.Nodes,
|
||||
capVer tailcfg.CapabilityVersion,
|
||||
pol *policy.ACLPolicy,
|
||||
dnsConfig *tailcfg.DNSConfig,
|
||||
baseDomain string,
|
||||
randomClientPort bool,
|
||||
) ([]*tailcfg.Node, error) {
|
||||
tNodes := make([]*tailcfg.Node, len(nodes))
|
||||
|
||||
for index, node := range nodes {
|
||||
node, err := tailNode(
|
||||
node,
|
||||
capVer,
|
||||
pol,
|
||||
dnsConfig,
|
||||
baseDomain,
|
||||
randomClientPort,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -42,9 +46,11 @@ func tailNodes(
|
|||
// as per the expected behaviour in the official SaaS.
|
||||
func tailNode(
|
||||
node *types.Node,
|
||||
capVer tailcfg.CapabilityVersion,
|
||||
pol *policy.ACLPolicy,
|
||||
dnsConfig *tailcfg.DNSConfig,
|
||||
baseDomain string,
|
||||
randomClientPort bool,
|
||||
) (*tailcfg.Node, error) {
|
||||
nodeKey, err := node.NodePublicKey()
|
||||
if err != nil {
|
||||
|
@ -133,14 +139,35 @@ func tailNode(
|
|||
|
||||
LastSeen: node.LastSeen,
|
||||
Online: &online,
|
||||
KeepAlive: true,
|
||||
MachineAuthorized: !node.IsExpired(),
|
||||
}
|
||||
|
||||
Capabilities: []string{
|
||||
// - 74: 2023-09-18: Client understands NodeCapMap
|
||||
if capVer >= 74 {
|
||||
tNode.CapMap = tailcfg.NodeCapMap{
|
||||
tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{},
|
||||
tailcfg.CapabilityAdmin: []tailcfg.RawMessage{},
|
||||
tailcfg.CapabilitySSH: []tailcfg.RawMessage{},
|
||||
}
|
||||
|
||||
if randomClientPort {
|
||||
tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
|
||||
}
|
||||
} else {
|
||||
tNode.Capabilities = []tailcfg.NodeCapability{
|
||||
tailcfg.CapabilityFileSharing,
|
||||
tailcfg.CapabilityAdmin,
|
||||
tailcfg.CapabilitySSH,
|
||||
},
|
||||
}
|
||||
|
||||
if randomClientPort {
|
||||
tNode.Capabilities = append(tNode.Capabilities, tailcfg.NodeAttrRandomizeClientPort)
|
||||
}
|
||||
}
|
||||
|
||||
// - 72: 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again
|
||||
if capVer < 72 {
|
||||
tNode.Capabilities = append(tNode.Capabilities, tailcfg.NodeAttrDisableUPnP)
|
||||
}
|
||||
|
||||
return &tNode, nil
|
||||
|
|
|
@ -146,13 +146,13 @@ func TestTailNode(t *testing.T) {
|
|||
|
||||
LastSeen: &lastSeen,
|
||||
Online: new(bool),
|
||||
KeepAlive: true,
|
||||
MachineAuthorized: true,
|
||||
|
||||
Capabilities: []string{
|
||||
Capabilities: []tailcfg.NodeCapability{
|
||||
tailcfg.CapabilityFileSharing,
|
||||
tailcfg.CapabilityAdmin,
|
||||
tailcfg.CapabilitySSH,
|
||||
tailcfg.NodeAttrDisableUPnP,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
|
@ -166,9 +166,11 @@ func TestTailNode(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tailNode(
|
||||
tt.node,
|
||||
0,
|
||||
tt.pol,
|
||||
tt.dnsConfig,
|
||||
tt.baseDomain,
|
||||
false,
|
||||
)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
|
|
|
@ -63,6 +63,7 @@ func (h *Headscale) handlePoll(
|
|||
node *types.Node,
|
||||
mapRequest tailcfg.MapRequest,
|
||||
isNoise bool,
|
||||
capVer tailcfg.CapabilityVersion,
|
||||
) {
|
||||
logInfo, logErr := logPollFunc(mapRequest, node, isNoise)
|
||||
|
||||
|
@ -130,7 +131,7 @@ func (h *Headscale) handlePoll(
|
|||
// The intended use is for clients to discover the DERP map at
|
||||
// start-up before their first real endpoint update.
|
||||
} else if mapRequest.OmitPeers && !mapRequest.Stream && mapRequest.ReadOnly {
|
||||
h.handleLiteRequest(writer, node, mapRequest, isNoise)
|
||||
h.handleLiteRequest(writer, node, mapRequest, isNoise, capVer)
|
||||
|
||||
return
|
||||
} else if mapRequest.OmitPeers && mapRequest.Stream {
|
||||
|
@ -163,6 +164,7 @@ func (h *Headscale) handlePoll(
|
|||
peers,
|
||||
h.privateKey2019,
|
||||
isNoise,
|
||||
capVer,
|
||||
h.DERPMap,
|
||||
h.cfg.BaseDomain,
|
||||
h.cfg.DNSConfig,
|
||||
|
@ -383,6 +385,7 @@ func (h *Headscale) handleLiteRequest(
|
|||
node *types.Node,
|
||||
mapRequest tailcfg.MapRequest,
|
||||
isNoise bool,
|
||||
capVer tailcfg.CapabilityVersion,
|
||||
) {
|
||||
logInfo, logErr := logPollFunc(mapRequest, node, isNoise)
|
||||
|
||||
|
@ -393,6 +396,7 @@ func (h *Headscale) handleLiteRequest(
|
|||
types.Nodes{},
|
||||
h.privateKey2019,
|
||||
isNoise,
|
||||
capVer,
|
||||
h.DERPMap,
|
||||
h.cfg.BaseDomain,
|
||||
h.cfg.DNSConfig,
|
||||
|
|
|
@ -93,5 +93,16 @@ func (h *Headscale) PollNetMapHandler(
|
|||
Str("node", node.Hostname).
|
||||
Msg("A node is sending a MapRequest via legacy protocol")
|
||||
|
||||
h.handlePoll(writer, req.Context(), node, mapRequest, false)
|
||||
capVer, err := parseCabailityVersion(req)
|
||||
if err != nil && !errors.Is(err, ErrNoCapabilityVersion) {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("failed to parse capVer")
|
||||
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.handlePoll(writer, req.Context(), node, mapRequest, false, capVer)
|
||||
}
|
||||
|
|
|
@ -75,5 +75,18 @@ func (ns *noiseServer) NoisePollNetMapHandler(
|
|||
Str("node", node.Hostname).
|
||||
Msg("A node sending a MapRequest with Noise protocol")
|
||||
|
||||
ns.headscale.handlePoll(writer, req.Context(), node, mapRequest, true)
|
||||
capVer, err := parseCabailityVersion(req)
|
||||
if err != nil && !errors.Is(err, ErrNoCapabilityVersion) {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("failed to parse capVer")
|
||||
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(kradalby): since we are now passing capVer, we could arguably stop passing
|
||||
// isNoise, and rather have a isNoise function that takes capVer
|
||||
ns.headscale.handlePoll(writer, req.Context(), node, mapRequest, true, capVer)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ func (n *User) TailscaleUser() *tailcfg.User {
|
|||
LoginName: n.Name,
|
||||
DisplayName: n.Name,
|
||||
ProfilePicURL: "",
|
||||
Domain: "headscale.net",
|
||||
Logins: []tailcfg.LoginID{},
|
||||
Created: time.Time{},
|
||||
}
|
||||
|
@ -40,7 +39,6 @@ func (n *User) TailscaleLogin() *tailcfg.Login {
|
|||
LoginName: n.Name,
|
||||
DisplayName: n.Name,
|
||||
ProfilePicURL: "",
|
||||
Domain: "headscale.net",
|
||||
}
|
||||
|
||||
return &login
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue