Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tailscale serve support #2312

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions hscontrol/mapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions hscontrol/mapper/tail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
}
Expand Down
70 changes: 70 additions & 0 deletions hscontrol/noise.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/json"
"io"
"net/http"
"os"
"os/exec"

"github.com/gorilla/mux"
"github.com/juanfont/headscale/hscontrol/types"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
}

}
19 changes: 19 additions & 0 deletions hscontrol/types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -162,6 +164,11 @@ type LetsEncryptConfig struct {
ChallengeType string
}

type CertificatesFeatureConfig struct {
Enabled bool
SetDNSCommand string
}

type OIDCConfig struct {
OnlyStartIfOIDCIsAvailable bool
Issuer string
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"),

Expand Down