diff --git a/config-example.yaml b/config-example.yaml index cb7bf4da0a..1f45fa327e 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -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: ,, + # 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. diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index e18276ad6f..8b64945b7a 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -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 } diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index 4082df2b45..b2d0368a96 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -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{} } diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 393b9608f3..351501c22d 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -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") + } + +} diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index f6c5c48a29..a3a2820a7a 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -81,6 +81,8 @@ type Config struct { // it can be used directly when sending Netmaps to clients. TailcfgDNSConfig *tailcfg.DNSConfig + CertificatesFeatureConfig CertificatesFeatureConfig + UnixSocket string UnixSocketPermission fs.FileMode @@ -162,6 +164,11 @@ type LetsEncryptConfig struct { ChallengeType string } +type CertificatesFeatureConfig struct { + Enabled bool + SetDNSCommand string +} + type OIDCConfig struct { OnlyStartIfOIDCIsAvailable bool Issuer string @@ -257,6 +264,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) @@ -383,6 +393,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" @@ -893,6 +907,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"),