1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-08-14 13:51:01 +02:00
This commit is contained in:
1fexd 2025-07-28 10:28:52 +02:00 committed by GitHub
commit 67c5ce6c9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 188 additions and 7 deletions

View File

@ -274,6 +274,12 @@ dns:
# The FQDN of the hosts will be
# `hostname.base_domain` (e.g., _myhost.example.com_).
base_domain: example.com
tls_cert:
type: hetzner
hetzner:
api_token: ""
zone_id: ""
ttl: 0
# Whether to use the local DNS settings of a node (default) or override the
# local DNS settings and force the use of Headscale's DNS configuration.

2
go.mod
View File

@ -24,6 +24,8 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0
github.com/jagottsicher/termcolor v1.0.2
github.com/klauspost/compress v1.18.0
github.com/libdns/hetzner v1.0.0
github.com/libdns/libdns v1.1.0
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
github.com/ory/dockertest/v3 v3.12.0
github.com/philip-bui/grpc-zerolog v1.0.1

4
go.sum
View File

@ -318,6 +318,10 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/hetzner v1.0.0 h1:dFcgqTIfdiKQTqoqBBtgU9CewD8JSnB7p6BKxQ5kheM=
github.com/libdns/hetzner v1.0.0/go.mod h1:OmuTyXMHTfy2nCqbt9KYkf0KwQSvo0ZeFGxEQSl3r2w=
github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=

View File

@ -81,10 +81,11 @@ type Headscale struct {
DERPServer *derpServer.DERPServer
// Things that generate changes
extraRecordMan *dns.ExtraRecordsMan
mapper *mapper.Mapper
nodeNotifier *notifier.Notifier
authProvider AuthProvider
extraRecordMan *dns.ExtraRecordsMan
mapper *mapper.Mapper
nodeNotifier *notifier.Notifier
authProvider AuthProvider
tlsCertProvider TlsCertProvider
pollNetMapStreamWG sync.WaitGroup
}
@ -194,6 +195,10 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
}
}
if app.cfg.DNSConfig.TlsCert.Type == types.TlsCertHetzner {
app.tlsCertProvider = NewHetznerTlsCertProvider(app.cfg.DNSConfig.TlsCert.Hetzner)
}
if cfg.DERP.ServerEnabled {
derpServerKey, err := readOrCreatePrivateKey(cfg.DERP.ServerPrivateKeyPath)
if err != nil {

View File

@ -126,10 +126,21 @@ func generateDNSConfig(
dnsConfig := cfg.TailcfgDNSConfig.Clone()
addNextDNSMetadata(dnsConfig.Resolvers, node)
addHostNameToCertDomains(dnsConfig, node)
return dnsConfig
}
func addHostNameToCertDomains(dnsConfig *tailcfg.DNSConfig, node types.NodeView) {
hostName := strings.ToLower(node.Hostname())
for i, certDomain := range dnsConfig.CertDomains {
prependedDomain := fmt.Sprintf("%s.%s", hostName, certDomain)
if prependedDomain != certDomain {
dnsConfig.CertDomains[i] = prependedDomain
}
}
}
// If any nextdns DoH resolvers are present in the list of resolvers it will
// take metadata from the node metadata and instruct tailscale to add it
// to the requests. This makes it possible to identify from which device the

View File

@ -101,6 +101,8 @@ func (h *Headscale) NoiseUpgradeHandler(
router.HandleFunc("/machine/register", noiseServer.NoiseRegistrationHandler).
Methods(http.MethodPost)
router.HandleFunc("/machine/set-dns", noiseServer.NoiseSetDnsHandler).
Methods(http.MethodPost)
// Endpoints outside of the register endpoint must use getAndValidateNode to
// get the node to ensure that the MachineKey matches the Node setting up the
// connection.
@ -234,6 +236,55 @@ func regErr(err error) *tailcfg.RegisterResponse {
return &tailcfg.RegisterResponse{Error: err.Error()}
}
func (ns *noiseServer) NoiseSetDnsHandler(
writer http.ResponseWriter,
req *http.Request,
) {
if req.Method != http.MethodPost {
httpError(writer, errMethodNotAllowed)
return
}
setDnsRequest, setDnsResponse := func() (*tailcfg.SetDNSRequest, *tailcfg.SetDNSResponse) {
var resp *tailcfg.SetDNSResponse
body, err := io.ReadAll(req.Body)
if err != nil {
return &tailcfg.SetDNSRequest{}, &tailcfg.SetDNSResponse{}
}
var setDnsReq tailcfg.SetDNSRequest
if err := json.Unmarshal(body, &setDnsReq); err != nil {
return &setDnsReq, &tailcfg.SetDNSResponse{}
}
ns.nodeKey = setDnsReq.NodeKey
resp, err = ns.headscale.handleSetDns(req.Context(), setDnsReq)
if err != nil {
return &setDnsReq, &tailcfg.SetDNSResponse{}
}
return &setDnsReq, resp
}()
// Reject unsupported versions
if rejectUnsupported(writer, setDnsRequest.Version, ns.machineKey, setDnsRequest.NodeKey) {
return
}
respBody, err := json.Marshal(setDnsResponse)
if err != nil {
httpError(writer, err)
return
}
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.WriteHeader(http.StatusOK)
writer.Write(respBody)
}
// NoiseRegistrationHandler handles the actual registration process of a node.
func (ns *noiseServer) NoiseRegistrationHandler(
writer http.ResponseWriter,

66
hscontrol/tlscert.go Normal file
View File

@ -0,0 +1,66 @@
package hscontrol
import (
"context"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/libdns/hetzner"
"github.com/libdns/libdns"
"tailscale.com/tailcfg"
)
type TlsCertProvider interface {
GetRecordSetter() libdns.RecordSetter
GetZoneName() string
CreateRecord(request tailcfg.SetDNSRequest) libdns.Record
}
func NewHetznerTlsCertProvider(config types.HetznerTlsCertConfig) *TlsCertProviderHetzner {
return &TlsCertProviderHetzner{
Provider: hetzner.New(config.ApiToken),
ZoneId: config.ZoneId,
ZoneName: config.ZoneName,
Ttl: config.Ttl,
}
}
type TlsCertProviderHetzner struct {
Provider libdns.RecordSetter
ZoneId string
ZoneName string
Ttl int
}
func (p *TlsCertProviderHetzner) GetRecordSetter() libdns.RecordSetter {
return p.Provider
}
func (p *TlsCertProviderHetzner) GetZoneName() string {
return p.ZoneName
}
func (p *TlsCertProviderHetzner) CreateRecord(request tailcfg.SetDNSRequest) libdns.Record {
return &hetzner.Record{
ZoneID: p.ZoneId,
Type: request.Type,
Name: request.Name,
Value: request.Value,
TTL: p.Ttl,
}
}
func (h *Headscale) handleSetDns(
ctx context.Context,
setDnsReq tailcfg.SetDNSRequest,
) (*tailcfg.SetDNSResponse, error) {
zoneName := h.tlsCertProvider.GetZoneName()
recordSetter := h.tlsCertProvider.GetRecordSetter()
libDnsRecord := h.tlsCertProvider.CreateRecord(setDnsReq)
_, err := recordSetter.SetRecords(ctx, zoneName, []libdns.Record{libDnsRecord})
if err != nil {
return nil, err
}
return &tailcfg.SetDNSResponse{}, nil
}

View File

@ -17,6 +17,7 @@ const (
SelfUpdateIdentifier = "self-update"
DatabasePostgres = "postgres"
DatabaseSqlite = "sqlite3"
TlsCertHetzner = "hetzner"
)
var ErrCannotParsePrefix = errors.New("cannot parse prefix")

View File

@ -101,15 +101,29 @@ type Config struct {
}
type DNSConfig struct {
MagicDNS bool `mapstructure:"magic_dns"`
BaseDomain string `mapstructure:"base_domain"`
OverrideLocalDNS bool `mapstructure:"override_local_dns"`
MagicDNS bool `mapstructure:"magic_dns"`
BaseDomain string `mapstructure:"base_domain"`
TlsCert TlsCertConfig `mapstructure:"tls_cert"`
OverrideLocalDNS bool `mapstructure:"override_local_dns"`
Nameservers Nameservers
SearchDomains []string `mapstructure:"search_domains"`
ExtraRecords []tailcfg.DNSRecord `mapstructure:"extra_records"`
ExtraRecordsPath string `mapstructure:"extra_records_path"`
}
type TlsCertConfig struct {
// Type sets the tls cert type, one of "hetzner", ...
Type string `mapstructure:"type"`
Hetzner HetznerTlsCertConfig `mapstructure:"hetzner"`
}
type HetznerTlsCertConfig struct {
ApiToken string `mapstructure:"api_token"`
ZoneId string `mapstructure:"zone_id"`
ZoneName string `mapstructure:"zone_name"`
Ttl int `mapstructure:"ttl"`
}
type Nameservers struct {
Global []string
Split map[string][]string
@ -639,6 +653,26 @@ func dns() (DNSConfig, error) {
dns.SearchDomains = viper.GetStringSlice("dns.search_domains")
dns.ExtraRecordsPath = viper.GetString("dns.extra_records_path")
if viper.IsSet("dns.tls_cert") {
certType := viper.GetString("dns.tls_cert.type")
if certType == TlsCertHetzner {
zoneName := dns.BaseDomain
if viper.IsSet("dns.tls_cert.hetzner.zone_name") {
zoneName = viper.GetString("dns.tls_cert.hetzner.zone_name")
}
dns.TlsCert = TlsCertConfig{
Type: certType,
Hetzner: HetznerTlsCertConfig{
ApiToken: viper.GetString("dns.tls_cert.hetzner.api_token"),
ZoneId: viper.GetString("dns.tls_cert.hetzner.zone_id"),
ZoneName: zoneName,
Ttl: viper.GetInt("dns.tls_cert.hetzner.ttl"),
},
}
}
}
if viper.IsSet("dns.extra_records") {
var extraRecords []tailcfg.DNSRecord
@ -749,6 +783,7 @@ func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig {
cfg.Routes = routes
if dns.BaseDomain != "" {
cfg.Domains = []string{dns.BaseDomain}
cfg.CertDomains = cfg.Domains
}
cfg.Domains = append(cfg.Domains, dns.SearchDomains...)