feat: tailscale serve support

This commit is contained in:
nom3ad 2024-12-20 19:28:35 +05:30
parent ede4f97a16
commit 3453990452
5 changed files with 109 additions and 0 deletions

View file

@ -242,6 +242,15 @@ policy:
# HuJSON file containing ACL policies.
path: ""
certificates:
enabled: false
# Path to an executable that will be called when dns01 challenge is raised by tailscale client.
# Command will be called with 3 arguments: <domain>,<type>,<value>
# Eg: /path/to/set-dns-command "_acme-challenge.node1.example.com" "TXT" "jYhsfThsdf_Lo3shgdBRY7hNxe"
set_dns_command: ""
## DNS
#
# headscale supports Tailscale's DNS configuration and MagicDNS.

View file

@ -124,6 +124,13 @@ func generateDNSConfig(
addNextDNSMetadata(dnsConfig.Resolvers, node)
hostname, err := node.GetFQDN(cfg.BaseDomain)
if err != nil {
log.Warn().Msgf("failed to get FQDN of node %s for certDomains: %s", node.ID, err)
} else {
dnsConfig.CertDomains = append(dnsConfig.CertDomains, hostname)
}
return dnsConfig
}

View file

@ -120,6 +120,10 @@ func tailNode(
tailcfg.CapabilitySSH: []tailcfg.RawMessage{},
}
if cfg.CertificatesFeatureConfig.Enabled {
tNode.CapMap[tailcfg.CapabilityHTTPS] = []tailcfg.RawMessage{}
}
if cfg.RandomizeClientPort {
tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
}

View file

@ -5,6 +5,8 @@ import (
"encoding/json"
"io"
"net/http"
"os"
"os/exec"
"github.com/gorilla/mux"
"github.com/juanfont/headscale/hscontrol/types"
@ -99,6 +101,7 @@ func (h *Headscale) NoiseUpgradeHandler(
router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler)
router.HandleFunc("/machine/set-dns", noiseServer.SetDNSHandler).Methods(http.MethodPost)
noiseServer.httpBaseConfig = &http.Server{
Handler: router,
@ -232,3 +235,70 @@ func (ns *noiseServer) NoisePollNetMapHandler(
sess.serveLongPoll()
}
}
func (ns *noiseServer) SetDNSHandler(
writer http.ResponseWriter,
req *http.Request,
) {
body, _ := io.ReadAll(req.Body)
setDnsRequest := tailcfg.SetDNSRequest{}
if err := json.Unmarshal(body, &setDnsRequest); err != nil {
log.Error().
Caller().
Err(err).
Msg("Cannot parse MapRequest")
http.Error(writer, "Internal error", http.StatusInternalServerError)
return
}
log.Info().
Caller().
Str("handler", "NoisePollNetMap").
Any("headers", req.Header).
Str("NodeKey", setDnsRequest.NodeKey.ShortString()).
Str("Name", setDnsRequest.Name).
Str("Type", setDnsRequest.Type).
Str("Value", setDnsRequest.Value).
Msg("SetDNSHandler called")
if !ns.headscale.cfg.CertificatesFeatureConfig.Enabled {
http.Error(writer, "certificates feature is not enabled in headscale", http.StatusForbidden)
return
}
cmd := exec.Command(ns.headscale.cfg.CertificatesFeatureConfig.SetDNSCommand, setDnsRequest.Name, setDnsRequest.Type, setDnsRequest.Value)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Error().AnErr("error", err).
Strs("args", cmd.Args).
Str("NodeKey", setDnsRequest.NodeKey.ShortString()).
Str("DnsName", setDnsRequest.Name).
Msg("Error running set_dns_command")
http.Error(writer, "Failed to execute SetDNSCommand", http.StatusInternalServerError)
return
}
resp := tailcfg.SetDNSResponse{}
respBody, err := json.Marshal(resp)
if err != nil {
log.Error().
Caller().
Msg("Cannot encode message")
http.Error(writer, "Internal server error", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
_, err = writer.Write(respBody)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}
}

View file

@ -84,6 +84,8 @@ type Config struct {
// it can be used directly when sending Netmaps to clients.
TailcfgDNSConfig *tailcfg.DNSConfig
CertificatesFeatureConfig CertificatesFeatureConfig
UnixSocket string
UnixSocketPermission fs.FileMode
@ -165,6 +167,11 @@ type LetsEncryptConfig struct {
ChallengeType string
}
type CertificatesFeatureConfig struct {
Enabled bool
SetDNSCommand string
}
type PKCEConfig struct {
Enabled bool
Method string
@ -273,6 +280,9 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache")
viper.SetDefault("tls_letsencrypt_challenge_type", HTTP01ChallengeType)
viper.SetDefault("certificates.enabled", false)
viper.SetDefault("certificates.set_dns_command", "")
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", TextLogFormat)
@ -407,6 +417,10 @@ func validateServerConfig() error {
errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
}
if (viper.GetBool("certificates.enabled") == true) && viper.GetString("certificates.set_dns_command") == "" {
errorText += "Fatal config error: certificates.enabled is set to true, but certificates.set_dns_command is not set\n"
}
if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
errorText += "Fatal config error: server_url must start with https:// or http://\n"
@ -917,6 +931,11 @@ func LoadServerConfig() (*Config, error) {
ACMEEmail: viper.GetString("acme_email"),
ACMEURL: viper.GetString("acme_url"),
CertificatesFeatureConfig: CertificatesFeatureConfig{
Enabled: viper.GetBool("certificates.enabled"),
SetDNSCommand: os.ExpandEnv(viper.GetString("certificates.set_dns_command")),
},
UnixSocket: viper.GetString("unix_socket"),
UnixSocketPermission: util.GetFileMode("unix_socket_permission"),