Upgrade Go 1.21, Tailscale 1.50 and add Capability version support (#1563)

This commit is contained in:
Kristoffer Dalby 2023-09-28 12:33:53 -07:00 committed by GitHub
parent 01b85e5232
commit fb4ed95ff6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 277 additions and 132 deletions

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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,

View file

@ -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

View file

@ -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 {

View file

@ -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,

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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