Compare commits

...

7 commits

Author SHA1 Message Date
Kedas
ca9938f4e9 fix: config aliases (spf13/viper#689) 2024-07-14 07:01:01 +00:00
Kedas
1fc14790f9
Merge branch 'juanfont:main' into main 2024-07-13 13:11:13 -07:00
Kedas
6d9e5b0022 feat: move configs (untested) 2024-07-13 10:31:05 +00:00
Kedas
9163edcf50 feat: add WithGrpcTLS shim 2024-05-25 05:35:00 +00:00
Mia
50a7315226
feat: move grpc config 2024-05-24 18:16:33 -07:00
Kedas
bbe1327785
Merge branch 'juanfont:main' into main 2024-05-24 14:44:25 -07:00
Kedas
e72bd1cc8c feat: add config to overwrite grpc certificate 2024-04-13 04:49:25 +00:00
8 changed files with 170 additions and 76 deletions

View file

@ -60,9 +60,9 @@ func (*Suite) TestConfigFileLoading(c *check.C) {
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("database.type"), check.Equals, "sqlite") c.Assert(viper.GetString("database.type"), check.Equals, "sqlite")
c.Assert(viper.GetString("database.sqlite.path"), check.Equals, "/var/lib/headscale/db.sqlite") c.Assert(viper.GetString("database.sqlite.path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls.letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls.letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetString("tls.letsencrypt_challenge_type"), check.Equals, "HTTP-01")
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
c.Assert( c.Assert(
util.GetFileMode("unix_socket_permission"), util.GetFileMode("unix_socket_permission"),
@ -103,9 +103,9 @@ func (*Suite) TestConfigLoading(c *check.C) {
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090") c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
c.Assert(viper.GetString("database.type"), check.Equals, "sqlite") c.Assert(viper.GetString("database.type"), check.Equals, "sqlite")
c.Assert(viper.GetString("database.sqlite.path"), check.Equals, "/var/lib/headscale/db.sqlite") c.Assert(viper.GetString("database.sqlite.path"), check.Equals, "/var/lib/headscale/db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls.letsencrypt_hostname"), check.Equals, "")
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls.letsencrypt_listen"), check.Equals, ":http")
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") c.Assert(viper.GetString("tls.letsencrypt_challenge_type"), check.Equals, "HTTP-01")
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
c.Assert( c.Assert(
util.GetFileMode("unix_socket_permission"), util.GetFileMode("unix_socket_permission"),
@ -180,12 +180,12 @@ noise:
c.Assert( c.Assert(
tmp, tmp,
check.Matches, check.Matches,
".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*", ".*Fatal config error: set either tls.letsencrypt_hostname or tls.cert_path/tls.key_path, not both.*",
) )
c.Assert( c.Assert(
tmp, tmp,
check.Matches, check.Matches,
".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*", ".*Fatal config error: the only supported values for tls.letsencrypt_challenge_type are.*",
) )
c.Assert( c.Assert(
tmp, tmp,
@ -193,6 +193,8 @@ noise:
".*Fatal config error: server_url must start with https:// or http://.*", ".*Fatal config error: server_url must start with https:// or http://.*",
) )
viper.Reset()
// Check configuration validation errors (2) // Check configuration validation errors (2)
configYaml = []byte(`--- configYaml = []byte(`---
noise: noise:

View file

@ -24,21 +24,22 @@ listen_addr: 127.0.0.1:8080
# #
metrics_listen_addr: 127.0.0.1:9090 metrics_listen_addr: 127.0.0.1:9090
# Address to listen for gRPC.
# gRPC is used for controlling a headscale server # gRPC is used for controlling a headscale server
# remotely with the CLI # remotely with the CLI
grpc:
# Address to listen for gRPC.
# Note: Remote access _only_ works if you have # Note: Remote access _only_ works if you have
# valid certificates. # valid certificates.
# #
# For production: # For production:
# grpc_listen_addr: 0.0.0.0:50443 # listen_addr: 0.0.0.0:50443
grpc_listen_addr: 127.0.0.1:50443 listen_addr: 127.0.0.1:50443
# Allow the gRPC admin interface to run in INSECURE # Allow the gRPC admin interface to run in INSECURE
# mode. This is not recommended as the traffic will # mode. This is not recommended as the traffic will
# be unencrypted. Only enable if you know what you # be unencrypted. Only enable if you know what you
# are doing. # are doing.
grpc_allow_insecure: false allow_insecure: false
# The Noise section includes specific configuration for the # The Noise section includes specific configuration for the
# TS2021 Noise protocol # TS2021 Noise protocol
@ -165,7 +166,7 @@ database:
# ssl: false # ssl: false
### TLS configuration ### TLS configuration
# tls:
## Let's encrypt / ACME ## Let's encrypt / ACME
# #
# headscale supports automatically requesting and setting up # headscale supports automatically requesting and setting up
@ -178,25 +179,31 @@ acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: "" acme_email: ""
# Domain name to request a TLS certificate for: # Domain name to request a TLS certificate for:
tls_letsencrypt_hostname: "" letsencrypt_hostname: ""
# Path to store certificates and metadata needed by # Path to store certificates and metadata needed by
# letsencrypt # letsencrypt
# For production: # For production:
tls_letsencrypt_cache_dir: /var/lib/headscale/cache letsencrypt_cache_dir: /var/lib/headscale/cache
# Type of ACME challenge to use, currently supported types: # Type of ACME challenge to use, currently supported types:
# HTTP-01 or TLS-ALPN-01 # HTTP-01 or TLS-ALPN-01
# See [docs/tls.md](docs/tls.md) for more information # See [docs/tls.md](docs/tls.md) for more information
tls_letsencrypt_challenge_type: HTTP-01 letsencrypt_challenge_type: HTTP-01
# When HTTP-01 challenge is chosen, letsencrypt must set up a # When HTTP-01 challenge is chosen, letsencrypt must set up a
# verification endpoint, and it will be listening on: # verification endpoint, and it will be listening on:
# :http = port 80 # :http = port 80
tls_letsencrypt_listen: ":http" letsencrypt_listen: ":http"
## Use already defined certificates: ## Use already defined certificates:
tls_cert_path: "" cert_path: ""
tls_key_path: "" key_path: ""
# Use a separate x509 certificate for gRPC, this is used
# instead of the global certificate.
grpc:
cert_path: ""
key_path: ""
log: log:
# Output formatting for logs: text or json # Output formatting for logs: text or json

View file

@ -7,7 +7,7 @@
- Access to create API keys (local access to the `headscale` server) - Access to create API keys (local access to the `headscale` server)
- `headscale` _must_ be served over TLS/HTTPS - `headscale` _must_ be served over TLS/HTTPS
- Remote access does _not_ support unencrypted traffic. - Remote access does _not_ support unencrypted traffic.
- Port `50443` must be open in the firewall (or port overridden by `grpc_listen_addr` option) - Port `50443` must be open in the firewall (or port overridden by `grpc.listen_addr` option)
## Goal ## Goal

View file

@ -653,9 +653,27 @@ func (h *Headscale) Serve() error {
// https://github.com/soheilhy/cmux/issues/68 // https://github.com/soheilhy/cmux/issues/68
// https://github.com/soheilhy/cmux/issues/91 // https://github.com/soheilhy/cmux/issues/91
grpcTlsConfig := &tls.Config{
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS12,
}
if h.cfg.TLS.GRPCCertPath == "" && h.cfg.TLS.GRPCKeyPath == "" {
grpcTlsConfig = tlsConfig
} else {
grpcTlsConfig.Certificates[0], err = tls.LoadX509KeyPair(h.cfg.TLS.GRPCCertPath, h.cfg.TLS.GRPCKeyPath)
if err != nil {
log.Error().Err(err).Msg("Failed to set up gRPC TLS configuration")
return err
}
}
var grpcServer *grpc.Server var grpcServer *grpc.Server
var grpcListener net.Listener var grpcListener net.Listener
if tlsConfig != nil || h.cfg.GRPCAllowInsecure { if grpcTlsConfig != nil || h.cfg.GRPCAllowInsecure {
log.Info().Msgf("Enabling remote gRPC at %s", h.cfg.GRPCAddr) log.Info().Msgf("Enabling remote gRPC at %s", h.cfg.GRPCAddr)
grpcOptions := []grpc.ServerOption{ grpcOptions := []grpc.ServerOption{
@ -668,9 +686,9 @@ func (h *Headscale) Serve() error {
), ),
} }
if tlsConfig != nil { if grpcTlsConfig != nil {
grpcOptions = append(grpcOptions, grpcOptions = append(grpcOptions,
grpc.Creds(credentials.NewTLS(tlsConfig)), grpc.Creds(credentials.NewTLS(grpcTlsConfig)),
) )
} else { } else {
log.Warn().Msg("gRPC is running without security") log.Warn().Msg("gRPC is running without security")

View file

@ -110,6 +110,8 @@ type DatabaseConfig struct {
type TLSConfig struct { type TLSConfig struct {
CertPath string CertPath string
KeyPath string KeyPath string
GRPCCertPath string
GRPCKeyPath string
LetsEncrypt LetsEncryptConfig LetsEncrypt LetsEncryptConfig
} }
@ -178,6 +180,13 @@ type Tuning struct {
NodeMapSessionBufferedChanSize int NodeMapSessionBufferedChanSize int
} }
func RegisterDeprecatedAlias(old, new string) {
if viper.IsSet(old) {
log.Warn().Msgf("%s is deprecated and may be removed in future versions, please use %s instead.", old, new)
viper.Set(new, viper.GetString(old))
}
}
func LoadConfig(path string, isFile bool) error { func LoadConfig(path string, isFile bool) error {
if isFile { if isFile {
viper.SetConfigFile(path) viper.SetConfigFile(path)
@ -197,8 +206,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv() viper.AutomaticEnv()
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("log.level", "info") viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", TextLogFormat) viper.SetDefault("log.format", TextLogFormat)
@ -214,8 +223,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock") viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock")
viper.SetDefault("unix_socket_permission", "0o770") viper.SetDefault("unix_socket_permission", "0o770")
viper.SetDefault("grpc_listen_addr", ":50443") viper.SetDefault("grpc.listen_addr", ":50443")
viper.SetDefault("grpc_allow_insecure", false) viper.SetDefault("grpc.allow_insecure", false)
viper.SetDefault("cli.timeout", "5s") viper.SetDefault("cli.timeout", "5s")
viper.SetDefault("cli.insecure", false) viper.SetDefault("cli.insecure", false)
@ -254,28 +263,39 @@ func LoadConfig(path string, isFile bool) error {
return fmt.Errorf("fatal error reading config file: %w", err) return fmt.Errorf("fatal error reading config file: %w", err)
} }
RegisterDeprecatedAlias("grpc_listen_addr", "grpc.listen_addr")
RegisterDeprecatedAlias("grpc_allow_insecure", "grpc.allow_insecure")
RegisterDeprecatedAlias("acme_url", "tls.acme_url")
RegisterDeprecatedAlias("acme_email", "tls.acme_email")
RegisterDeprecatedAlias("tls_letsencrypt_hostname", "tls.letsencrypt_hostname")
RegisterDeprecatedAlias("tls_letsencrypt_cache_dir", "tls.letsencrypt_cache_dir")
RegisterDeprecatedAlias("tls_letsencrypt_challenge_type", "tls.letsencrypt_challenge_type")
RegisterDeprecatedAlias("tls_letsencrypt_listen", "tls.letsencrypt_listen")
RegisterDeprecatedAlias("tls_cert_path", "tls.cert_path")
RegisterDeprecatedAlias("tls_key_path", "tls.key_path")
// Collect any validation errors and return them all at once // Collect any validation errors and return them all at once
var errorText string var errorText string
if (viper.GetString("tls_letsencrypt_hostname") != "") && if (viper.GetString("tls.letsencrypt_hostname") != "") &&
((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) { ((viper.GetString("tls.cert_path") != "") || (viper.GetString("tls.key_path") != "")) {
errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n" errorText += "Fatal config error: set either tls.letsencrypt_hostname or tls.cert_path/tls.key_path, not both\n"
} }
if !viper.IsSet("noise") || viper.GetString("noise.private_key_path") == "" { if !viper.IsSet("noise") || viper.GetString("noise.private_key_path") == "" {
errorText += "Fatal config error: headscale now requires a new `noise.private_key_path` field in the config file for the Tailscale v2 protocol\n" errorText += "Fatal config error: headscale now requires a new `noise.private_key_path` field in the config file for the Tailscale v2 protocol\n"
} }
if (viper.GetString("tls_letsencrypt_hostname") != "") && if (viper.GetString("tls.letsencrypt_hostname") != "") &&
(viper.GetString("tls_letsencrypt_challenge_type") == TLSALPN01ChallengeType) && (viper.GetString("tls.letsencrypt_challenge_type") == TLSALPN01ChallengeType) &&
(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) { (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) {
// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule) // this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
log.Warn(). log.Warn().
Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443") Msg("Warning: when using tls.letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443")
} }
if (viper.GetString("tls_letsencrypt_challenge_type") != HTTP01ChallengeType) && if (viper.GetString("tls.letsencrypt_challenge_type") != HTTP01ChallengeType) &&
(viper.GetString("tls_letsencrypt_challenge_type") != TLSALPN01ChallengeType) { (viper.GetString("tls.letsencrypt_challenge_type") != TLSALPN01ChallengeType) {
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 !strings.HasPrefix(viper.GetString("server_url"), "http://") && if !strings.HasPrefix(viper.GetString("server_url"), "http://") &&
@ -305,18 +325,24 @@ func LoadConfig(path string, isFile bool) error {
func GetTLSConfig() TLSConfig { func GetTLSConfig() TLSConfig {
return TLSConfig{ return TLSConfig{
LetsEncrypt: LetsEncryptConfig{ LetsEncrypt: LetsEncryptConfig{
Hostname: viper.GetString("tls_letsencrypt_hostname"), Hostname: viper.GetString("tls.letsencrypt_hostname"),
Listen: viper.GetString("tls_letsencrypt_listen"), Listen: viper.GetString("tls.letsencrypt_listen"),
CacheDir: util.AbsolutePathFromConfigPath( CacheDir: util.AbsolutePathFromConfigPath(
viper.GetString("tls_letsencrypt_cache_dir"), viper.GetString("tls.letsencrypt_cache_dir"),
), ),
ChallengeType: viper.GetString("tls_letsencrypt_challenge_type"), ChallengeType: viper.GetString("tls.letsencrypt_challenge_type"),
}, },
CertPath: util.AbsolutePathFromConfigPath( CertPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls_cert_path"), viper.GetString("tls.cert_path"),
), ),
KeyPath: util.AbsolutePathFromConfigPath( KeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls_key_path"), viper.GetString("tls.key_path"),
),
GRPCCertPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls.grpc.cert_path"),
),
GRPCKeyPath: util.AbsolutePathFromConfigPath(
viper.GetString("tls.grpc.key_path"),
), ),
} }
} }
@ -698,8 +724,8 @@ func GetHeadscaleConfig() (*Config, error) {
ServerURL: viper.GetString("server_url"), ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"), Addr: viper.GetString("listen_addr"),
MetricsAddr: viper.GetString("metrics_listen_addr"), MetricsAddr: viper.GetString("metrics_listen_addr"),
GRPCAddr: viper.GetString("grpc_listen_addr"), GRPCAddr: viper.GetString("grpc.listen_addr"),
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"), GRPCAllowInsecure: viper.GetBool("grpc.allow_insecure"),
DisableUpdateCheck: viper.GetBool("disable_check_updates"), DisableUpdateCheck: viper.GetBool("disable_check_updates"),
PrefixV4: prefix4, PrefixV4: prefix4,
@ -724,8 +750,8 @@ func GetHeadscaleConfig() (*Config, error) {
DNSConfig: dnsConfig, DNSConfig: dnsConfig,
DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"), DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"),
ACMEEmail: viper.GetString("acme_email"), ACMEEmail: viper.GetString("tls.acme_email"),
ACMEURL: viper.GetString("acme_url"), ACMEURL: viper.GetString("tls.acme_url"),
UnixSocket: viper.GetString("unix_socket"), UnixSocket: viper.GetString("unix_socket"),
UnixSocketPermission: util.GetFileMode("unix_socket_permission"), UnixSocketPermission: util.GetFileMode("unix_socket_permission"),

View file

@ -43,6 +43,7 @@ func TestDERPServerScenario(t *testing.T) {
hsic.WithExtraPorts([]string{"3478/udp"}), hsic.WithExtraPorts([]string{"3478/udp"}),
hsic.WithEmbeddedDERPServerOnly(), hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(), hsic.WithTLS(),
hsic.WithGrpcTLS(),
hsic.WithHostnameAsServerURL(), hsic.WithHostnameAsServerURL(),
) )
assertNoErrHeadscaleEnv(t, err) assertNoErrHeadscaleEnv(t, err)

View file

@ -41,6 +41,7 @@ func TestPingAllByIP(t *testing.T) {
hsic.WithTestName("pingallbyip"), hsic.WithTestName("pingallbyip"),
hsic.WithEmbeddedDERPServerOnly(), hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(), hsic.WithTLS(),
hsic.WithGrpcTLS(),
hsic.WithHostnameAsServerURL(), hsic.WithHostnameAsServerURL(),
hsic.WithIPAllocationStrategy(types.IPAllocationStrategyRandom), hsic.WithIPAllocationStrategy(types.IPAllocationStrategyRandom),
) )
@ -836,6 +837,7 @@ func TestPingAllByIPManyUpDown(t *testing.T) {
hsic.WithTestName("pingallbyipmany"), hsic.WithTestName("pingallbyipmany"),
hsic.WithEmbeddedDERPServerOnly(), hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(), hsic.WithTLS(),
hsic.WithGrpcTLS(),
hsic.WithHostnameAsServerURL(), hsic.WithHostnameAsServerURL(),
) )
assertNoErrHeadscaleEnv(t, err) assertNoErrHeadscaleEnv(t, err)

View file

@ -40,6 +40,8 @@ const (
aclPolicyPath = "/etc/headscale/acl.hujson" aclPolicyPath = "/etc/headscale/acl.hujson"
tlsCertPath = "/etc/headscale/tls.cert" tlsCertPath = "/etc/headscale/tls.cert"
tlsKeyPath = "/etc/headscale/tls.key" tlsKeyPath = "/etc/headscale/tls.key"
grpcTlsCertPath = "/etc/headscale/grpc_tls.cert"
grpcTlsKeyPath = "/etc/headscale/grpc_tls.key"
headscaleDefaultPort = 8080 headscaleDefaultPort = 8080
) )
@ -69,6 +71,8 @@ type HeadscaleInContainer struct {
env map[string]string env map[string]string
tlsCert []byte tlsCert []byte
tlsKey []byte tlsKey []byte
grpcTlsCert []byte
grpcTlsKey []byte
filesInContainer []fileInContainer filesInContainer []fileInContainer
postgres bool postgres bool
} }
@ -105,6 +109,19 @@ func WithTLS() Option {
} }
} }
// WithGrpcTLS creates gRPC certificates and enables them.
func WithGrpcTLS() Option {
return func(hsic *HeadscaleInContainer) {
cert, key, err := createCertificate(hsic.hostname)
if err != nil {
log.Fatalf("failed to create grpc certificates for headscale test: %s", err)
}
hsic.grpcTlsCert = cert
hsic.grpcTlsKey = key
}
}
// WithConfigEnv takes a map of environment variables that // WithConfigEnv takes a map of environment variables that
// can be used to override Headscale configuration. // can be used to override Headscale configuration.
func WithConfigEnv(configEnv map[string]string) Option { func WithConfigEnv(configEnv map[string]string) Option {
@ -253,6 +270,11 @@ func New(
hsic.env["HEADSCALE_SERVER_URL"] = serverURL.String() hsic.env["HEADSCALE_SERVER_URL"] = serverURL.String()
} }
if hsic.hasGrpcTLS() {
hsic.env["HEADSCALE_GRPC_TLS_CERT_PATH"] = grpcTlsCertPath
hsic.env["HEADSCALE_GRPC_TLS_KEY_PATH"] = grpcTlsKeyPath
}
headscaleBuildOptions := &dockertest.BuildOptions{ headscaleBuildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.debug", Dockerfile: "Dockerfile.debug",
ContextDir: dockerContextPath, ContextDir: dockerContextPath,
@ -374,6 +396,18 @@ func New(
} }
} }
if hsic.hasGrpcTLS() {
err = hsic.WriteFile(grpcTlsCertPath, hsic.grpcTlsCert)
if err != nil {
return nil, fmt.Errorf("failed to write TLS certificate to container: %w", err)
}
err = hsic.WriteFile(grpcTlsKeyPath, hsic.grpcTlsKey)
if err != nil {
return nil, fmt.Errorf("failed to write TLS key to container: %w", err)
}
}
for _, f := range hsic.filesInContainer { for _, f := range hsic.filesInContainer {
if err := hsic.WriteFile(f.path, f.contents); err != nil { if err := hsic.WriteFile(f.path, f.contents); err != nil {
return nil, fmt.Errorf("failed to write %q: %w", f.path, err) return nil, fmt.Errorf("failed to write %q: %w", f.path, err)
@ -391,6 +425,10 @@ func (t *HeadscaleInContainer) hasTLS() bool {
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0 return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
} }
func (t *HeadscaleInContainer) hasGrpcTLS() bool {
return len(t.grpcTlsCert) != 0 && len(t.grpcTlsKey) != 0
}
// Shutdown stops and cleans up the Headscale container. // Shutdown stops and cleans up the Headscale container.
func (t *HeadscaleInContainer) Shutdown() error { func (t *HeadscaleInContainer) Shutdown() error {
err := t.SaveLog("/tmp/control") err := t.SaveLog("/tmp/control")