Skip to content

Commit

Permalink
feat: tailscale serve support
Browse files Browse the repository at this point in the history
  • Loading branch information
nom3ad committed Dec 20, 2024
1 parent 770f3dc commit b1e6bac
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 0 deletions.
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

0 comments on commit b1e6bac

Please sign in to comment.