mirror of
https://github.com/juanfont/headscale.git
synced 2025-01-19 02:10:04 +09:00
feat: tailscale serve support
This commit is contained in:
parent
ede4f97a16
commit
3453990452
5 changed files with 109 additions and 0 deletions
|
@ -242,6 +242,15 @@ policy:
|
||||||
# HuJSON file containing ACL policies.
|
# HuJSON file containing ACL policies.
|
||||||
path: ""
|
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
|
## DNS
|
||||||
#
|
#
|
||||||
# headscale supports Tailscale's DNS configuration and MagicDNS.
|
# headscale supports Tailscale's DNS configuration and MagicDNS.
|
||||||
|
|
|
@ -124,6 +124,13 @@ func generateDNSConfig(
|
||||||
|
|
||||||
addNextDNSMetadata(dnsConfig.Resolvers, node)
|
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
|
return dnsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,6 +120,10 @@ func tailNode(
|
||||||
tailcfg.CapabilitySSH: []tailcfg.RawMessage{},
|
tailcfg.CapabilitySSH: []tailcfg.RawMessage{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.CertificatesFeatureConfig.Enabled {
|
||||||
|
tNode.CapMap[tailcfg.CapabilityHTTPS] = []tailcfg.RawMessage{}
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.RandomizeClientPort {
|
if cfg.RandomizeClientPort {
|
||||||
tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
|
tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
@ -99,6 +101,7 @@ func (h *Headscale) NoiseUpgradeHandler(
|
||||||
router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler).
|
router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler).
|
||||||
Methods(http.MethodPost)
|
Methods(http.MethodPost)
|
||||||
router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler)
|
router.HandleFunc("/machine/map", noiseServer.NoisePollNetMapHandler)
|
||||||
|
router.HandleFunc("/machine/set-dns", noiseServer.SetDNSHandler).Methods(http.MethodPost)
|
||||||
|
|
||||||
noiseServer.httpBaseConfig = &http.Server{
|
noiseServer.httpBaseConfig = &http.Server{
|
||||||
Handler: router,
|
Handler: router,
|
||||||
|
@ -232,3 +235,70 @@ func (ns *noiseServer) NoisePollNetMapHandler(
|
||||||
sess.serveLongPoll()
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -84,6 +84,8 @@ type Config struct {
|
||||||
// it can be used directly when sending Netmaps to clients.
|
// it can be used directly when sending Netmaps to clients.
|
||||||
TailcfgDNSConfig *tailcfg.DNSConfig
|
TailcfgDNSConfig *tailcfg.DNSConfig
|
||||||
|
|
||||||
|
CertificatesFeatureConfig CertificatesFeatureConfig
|
||||||
|
|
||||||
UnixSocket string
|
UnixSocket string
|
||||||
UnixSocketPermission fs.FileMode
|
UnixSocketPermission fs.FileMode
|
||||||
|
|
||||||
|
@ -165,6 +167,11 @@ type LetsEncryptConfig struct {
|
||||||
ChallengeType string
|
ChallengeType string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CertificatesFeatureConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
SetDNSCommand string
|
||||||
|
}
|
||||||
|
|
||||||
type PKCEConfig struct {
|
type PKCEConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Method string
|
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_cache_dir", "/var/www/.cache")
|
||||||
viper.SetDefault("tls_letsencrypt_challenge_type", HTTP01ChallengeType)
|
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.level", "info")
|
||||||
viper.SetDefault("log.format", TextLogFormat)
|
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"
|
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://") &&
|
if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
|
||||||
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
|
!strings.HasPrefix(viper.GetString("server_url"), "https://") {
|
||||||
errorText += "Fatal config error: server_url must start with https:// or http://\n"
|
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"),
|
ACMEEmail: viper.GetString("acme_email"),
|
||||||
ACMEURL: viper.GetString("acme_url"),
|
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"),
|
UnixSocket: viper.GetString("unix_socket"),
|
||||||
UnixSocketPermission: util.GetFileMode("unix_socket_permission"),
|
UnixSocketPermission: util.GetFileMode("unix_socket_permission"),
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue