2023-06-06 02:23:39 -06:00
package types
2022-06-03 01:05:41 -06:00
import (
2022-06-03 01:26:36 -06:00
"errors"
"fmt"
2022-06-03 01:05:41 -06:00
"io/fs"
2022-09-04 03:32:29 -06:00
"net/netip"
2022-06-03 01:05:41 -06:00
"net/url"
2023-01-10 04:46:42 -07:00
"os"
2022-06-03 01:26:36 -06:00
"strings"
2022-06-03 01:05:41 -06:00
"time"
2022-06-03 01:26:36 -06:00
"github.com/coreos/go-oidc/v3/oidc"
2024-07-22 00:56:00 -06:00
"github.com/juanfont/headscale/hscontrol/util"
2023-01-31 04:40:38 -07:00
"github.com/prometheus/common/model"
2022-06-03 02:37:45 -06:00
"github.com/rs/zerolog"
2022-06-03 01:26:36 -06:00
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
2022-09-01 16:04:04 -06:00
"go4.org/netipx"
2023-05-06 03:30:15 -06:00
"tailscale.com/net/tsaddr"
2022-06-03 01:05:41 -06:00
"tailscale.com/tailcfg"
2022-06-03 01:26:36 -06:00
"tailscale.com/types/dnstype"
2024-08-19 03:41:05 -06:00
"tailscale.com/util/set"
2022-06-03 01:05:41 -06:00
)
2022-07-11 12:33:24 -06:00
const (
2023-01-31 04:40:38 -07:00
defaultOIDCExpiryTime = 180 * 24 * time . Hour // 180 Days
maxDuration time . Duration = 1 << 63 - 1
2022-07-11 12:33:24 -06:00
)
2023-01-31 04:40:38 -07:00
var errOidcMutuallyExclusive = errors . New (
"oidc_client_secret and oidc_client_secret_path are mutually exclusive" ,
)
2023-01-10 04:46:42 -07:00
2024-04-16 23:03:06 -06:00
type IPAllocationStrategy string
const (
IPAllocationStrategySequential IPAllocationStrategy = "sequential"
IPAllocationStrategyRandom IPAllocationStrategy = "random"
)
2024-07-17 23:38:25 -06:00
type PolicyMode string
const (
PolicyModeDB = "database"
PolicyModeFile = "file"
)
2022-06-03 01:05:41 -06:00
// Config contains the initial Headscale configuration.
type Config struct {
ServerURL string
Addr string
MetricsAddr string
GRPCAddr string
GRPCAllowInsecure bool
EphemeralNodeInactivityTimeout time . Duration
2024-02-18 11:31:29 -07:00
PrefixV4 * netip . Prefix
PrefixV6 * netip . Prefix
2024-04-16 23:03:06 -06:00
IPAllocation IPAllocationStrategy
2022-08-13 03:14:38 -06:00
NoisePrivateKeyPath string
2022-06-03 01:05:41 -06:00
BaseDomain string
2022-09-11 13:37:23 -06:00
Log LogConfig
2022-06-03 02:37:45 -06:00
DisableUpdateCheck bool
2022-06-03 01:05:41 -06:00
2024-02-08 23:27:00 -07:00
Database DatabaseConfig
2022-06-03 01:05:41 -06:00
2024-02-08 23:27:00 -07:00
DERP DERPConfig
2022-06-03 01:05:41 -06:00
2022-06-03 02:14:14 -06:00
TLS TLSConfig
2022-06-03 01:05:41 -06:00
ACMEURL string
ACMEEmail string
2024-06-26 05:44:40 -06:00
DNSConfig * tailcfg . DNSConfig
DNSUserNameInMagicDNS bool
2022-06-03 01:05:41 -06:00
UnixSocket string
UnixSocketPermission fs . FileMode
OIDC OIDCConfig
2022-06-09 13:20:11 -06:00
LogTail LogTailConfig
RandomizeClientPort bool
2022-06-03 01:05:41 -06:00
CLI CLIConfig
2024-07-17 23:38:25 -06:00
Policy PolicyConfig
2024-02-23 02:59:24 -07:00
Tuning Tuning
2022-06-03 01:05:41 -06:00
}
2024-08-19 03:41:05 -06:00
type DNSConfig struct {
MagicDNS bool ` mapstructure:"magic_dns" `
BaseDomain string ` mapstructure:"base_domain" `
Nameservers Nameservers
SearchDomains [ ] string ` mapstructure:"search_domains" `
ExtraRecords [ ] tailcfg . DNSRecord ` mapstructure:"extra_records" `
UserNameInMagicDNS bool ` mapstructure:"use_username_in_magic_dns" `
}
type Nameservers struct {
Global [ ] string
Split map [ string ] [ ] string
}
2024-02-08 23:27:00 -07:00
type SqliteConfig struct {
2024-06-23 14:06:59 -06:00
Path string
WriteAheadLog bool
2024-02-08 23:27:00 -07:00
}
type PostgresConfig struct {
2024-02-09 09:34:28 -07:00
Host string
Port int
Name string
User string
Pass string
Ssl string
MaxOpenConnections int
MaxIdleConnections int
ConnMaxIdleTimeSecs int
2024-02-08 23:27:00 -07:00
}
2024-08-19 03:47:52 -06:00
type GormConfig struct {
Debug bool
SlowThreshold time . Duration
SkipErrRecordNotFound bool
ParameterizedQueries bool
PrepareStmt bool
}
2024-02-08 23:27:00 -07:00
type DatabaseConfig struct {
// Type sets the database type, either "sqlite3" or "postgres"
Type string
Debug bool
2024-08-19 03:47:52 -06:00
// Type sets the gorm configuration
Gorm GormConfig
2024-02-08 23:27:00 -07:00
Sqlite SqliteConfig
Postgres PostgresConfig
}
2022-06-03 02:14:14 -06:00
type TLSConfig struct {
2022-11-19 03:33:15 -07:00
CertPath string
KeyPath string
2022-06-03 02:14:14 -06:00
LetsEncrypt LetsEncryptConfig
}
type LetsEncryptConfig struct {
Listen string
Hostname string
CacheDir string
ChallengeType string
}
2022-06-03 01:05:41 -06:00
type OIDCConfig struct {
2022-09-26 01:57:28 -06:00
OnlyStartIfOIDCIsAvailable bool
Issuer string
ClientID string
ClientSecret string
Scope [ ] string
ExtraParams map [ string ] string
2024-05-09 08:42:39 -06:00
ClaimsMap OIDCClaimsMap
Allowed OIDCAllowedConfig
2024-05-09 07:15:57 -06:00
Expiry OIDCExpireConfig
2024-05-09 08:42:39 -06:00
Misc OIDCMiscConfig
2024-05-09 07:15:57 -06:00
}
type OIDCExpireConfig struct {
2024-05-09 08:42:39 -06:00
FromToken bool
FixedTime time . Duration
2024-05-09 07:15:57 -06:00
}
type OIDCAllowedConfig struct {
2024-05-09 08:42:39 -06:00
Domains [ ] string
Users [ ] string
Groups [ ] string
2024-05-09 07:15:57 -06:00
}
type OIDCClaimsMap struct {
2024-05-09 08:42:39 -06:00
Name string
Username string
Email string
Groups string
2024-05-09 07:15:57 -06:00
}
type OIDCMiscConfig struct {
2024-05-09 08:42:39 -06:00
StripEmaildomain bool
FlattenGroups bool
FlattenSplter string
2022-06-03 01:05:41 -06:00
}
type DERPConfig struct {
2024-01-16 08:04:03 -07:00
ServerEnabled bool
AutomaticallyAddEmbeddedDerpRegion bool
ServerRegionID int
ServerRegionCode string
ServerRegionName string
ServerPrivateKeyPath string
STUNAddr string
URLs [ ] url . URL
Paths [ ] string
AutoUpdate bool
UpdateFrequency time . Duration
IPv4 string
IPv6 string
2022-06-03 01:05:41 -06:00
}
type LogTailConfig struct {
Enabled bool
}
type CLIConfig struct {
Address string
APIKey string
Timeout time . Duration
Insecure bool
}
2024-07-17 23:38:25 -06:00
type PolicyConfig struct {
Path string
Mode PolicyMode
2022-06-03 01:05:41 -06:00
}
2022-06-03 01:26:36 -06:00
2022-09-11 13:37:23 -06:00
type LogConfig struct {
Format string
Level zerolog . Level
}
2024-02-23 02:59:24 -07:00
type Tuning struct {
2024-05-24 02:15:34 -06:00
NotifierSendTimeout time . Duration
2024-02-23 02:59:24 -07:00
BatchChangeDelay time . Duration
NodeMapSessionBufferedChanSize int
}
2022-06-07 08:24:35 -06:00
func LoadConfig ( path string , isFile bool ) error {
if isFile {
viper . SetConfigFile ( path )
2022-06-03 01:26:36 -06:00
} else {
2022-06-07 08:24:35 -06:00
viper . SetConfigName ( "config" )
if path == "" {
viper . AddConfigPath ( "/etc/headscale/" )
viper . AddConfigPath ( "$HOME/.headscale" )
viper . AddConfigPath ( "." )
} else {
// For testing
viper . AddConfigPath ( path )
}
2022-06-03 01:26:36 -06:00
}
2024-08-19 03:41:05 -06:00
envPrefix := "headscale"
viper . SetEnvPrefix ( envPrefix )
2022-06-03 01:26:36 -06:00
viper . SetEnvKeyReplacer ( strings . NewReplacer ( "." , "_" ) )
viper . AutomaticEnv ( )
2024-07-17 23:38:25 -06:00
viper . SetDefault ( "policy.mode" , "file" )
2022-06-03 01:26:36 -06:00
viper . SetDefault ( "tls_letsencrypt_cache_dir" , "/var/www/.cache" )
2023-06-06 03:12:36 -06:00
viper . SetDefault ( "tls_letsencrypt_challenge_type" , HTTP01ChallengeType )
2022-06-03 01:26:36 -06:00
2022-09-11 13:37:23 -06:00
viper . SetDefault ( "log.level" , "info" )
viper . SetDefault ( "log.format" , TextLogFormat )
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
viper . SetDefault ( "dns.magic_dns" , true )
viper . SetDefault ( "dns.base_domain" , "" )
viper . SetDefault ( "dns.nameservers.global" , [ ] string { } )
viper . SetDefault ( "dns.nameservers.split" , map [ string ] string { } )
viper . SetDefault ( "dns.search_domains" , [ ] string { } )
viper . SetDefault ( "dns.extra_records" , [ ] tailcfg . DNSRecord { } )
2022-06-03 01:26:36 -06:00
viper . SetDefault ( "derp.server.enabled" , false )
viper . SetDefault ( "derp.server.stun.enabled" , true )
2024-01-16 08:04:03 -07:00
viper . SetDefault ( "derp.server.automatically_add_embedded_derp_region" , true )
2022-06-03 01:26:36 -06:00
2023-05-10 08:32:15 -06:00
viper . SetDefault ( "unix_socket" , "/var/run/headscale/headscale.sock" )
2022-06-03 01:26:36 -06:00
viper . SetDefault ( "unix_socket_permission" , "0o770" )
viper . SetDefault ( "grpc_listen_addr" , ":50443" )
viper . SetDefault ( "grpc_allow_insecure" , false )
viper . SetDefault ( "cli.timeout" , "5s" )
viper . SetDefault ( "cli.insecure" , false )
2024-02-08 23:27:00 -07:00
viper . SetDefault ( "database.postgres.ssl" , false )
2024-02-09 09:34:28 -07:00
viper . SetDefault ( "database.postgres.max_open_conns" , 10 )
viper . SetDefault ( "database.postgres.max_idle_conns" , 10 )
viper . SetDefault ( "database.postgres.conn_max_idle_time_secs" , 3600 )
2022-12-07 01:37:45 -07:00
2024-06-23 14:06:59 -06:00
viper . SetDefault ( "database.sqlite.write_ahead_log" , true )
2022-06-03 01:26:36 -06:00
viper . SetDefault ( "oidc.scope" , [ ] string { oidc . ScopeOpenID , "profile" , "email" } )
2022-09-26 01:57:28 -06:00
viper . SetDefault ( "oidc.only_start_if_oidc_is_available" , true )
2024-05-09 07:15:57 -06:00
// expiry
viper . SetDefault ( "oidc.expiry.fixed_time" , "180d" )
viper . SetDefault ( "oidc.expiry.from_token" , false )
// claims_map
viper . SetDefault ( "oidc.claims_map.name" , "name" )
viper . SetDefault ( "oidc.claims_map.username" , "preferred_username" )
viper . SetDefault ( "oidc.claims_map.email" , "email" )
viper . SetDefault ( "oidc.claims_map.groups" , "groups" )
// misc
2024-05-09 08:42:39 -06:00
viper . SetDefault ( "oidc.misc.strip_email_domain" , false )
viper . SetDefault ( "oidc.misc.flatten_groups" , false )
viper . SetDefault ( "oidc.misc.flatten_splitter" , "/" )
2022-06-03 01:26:36 -06:00
viper . SetDefault ( "logtail.enabled" , false )
2022-06-09 13:20:11 -06:00
viper . SetDefault ( "randomize_client_port" , false )
2022-06-03 01:26:36 -06:00
2022-06-12 07:12:53 -06:00
viper . SetDefault ( "ephemeral_node_inactivity_timeout" , "120s" )
2024-05-24 02:15:34 -06:00
viper . SetDefault ( "tuning.notifier_send_timeout" , "800ms" )
2024-02-23 02:59:24 -07:00
viper . SetDefault ( "tuning.batch_change_delay" , "800ms" )
viper . SetDefault ( "tuning.node_mapsession_buffered_chan_size" , 30 )
2024-04-17 03:09:22 -06:00
viper . SetDefault ( "prefixes.allocation" , string ( IPAllocationStrategySequential ) )
2024-04-16 23:03:06 -06:00
2022-11-18 10:02:34 -07:00
if IsCLIConfigured ( ) {
return nil
}
2022-06-03 01:26:36 -06:00
if err := viper . ReadInConfig ( ) ; err != nil {
return fmt . Errorf ( "fatal error reading config file: %w" , err )
}
2024-08-19 03:41:05 -06:00
depr := deprecator {
warns : make ( set . Set [ string ] ) ,
fatals : make ( set . Set [ string ] ) ,
}
2024-07-17 23:38:25 -06:00
// Register aliases for backward compatibility
// Has to be called _after_ viper.ReadInConfig()
// https://github.com/spf13/viper/issues/560
// Alias the old ACL Policy path with the new configuration option.
2024-08-19 05:03:01 -06:00
depr . fatalIfNewKeyIsNotUsed ( "policy.path" , "acl_policy_path" )
2024-08-19 03:41:05 -06:00
// Move dns_config -> dns
depr . warn ( "dns_config.override_local_dns" )
depr . fatalIfNewKeyIsNotUsed ( "dns.magic_dns" , "dns_config.magic_dns" )
depr . fatalIfNewKeyIsNotUsed ( "dns.base_domain" , "dns_config.base_domain" )
depr . fatalIfNewKeyIsNotUsed ( "dns.nameservers.global" , "dns_config.nameservers" )
depr . fatalIfNewKeyIsNotUsed ( "dns.nameservers.split" , "dns_config.restricted_nameservers" )
depr . fatalIfNewKeyIsNotUsed ( "dns.search_domains" , "dns_config.domains" )
depr . fatalIfNewKeyIsNotUsed ( "dns.extra_records" , "dns_config.extra_records" )
depr . warn ( "dns_config.use_username_in_magic_dns" )
depr . warn ( "dns.use_username_in_magic_dns" )
depr . Log ( )
2024-07-17 23:38:25 -06:00
2022-06-03 01:26:36 -06:00
// Collect any validation errors and return them all at once
var errorText string
if ( viper . GetString ( "tls_letsencrypt_hostname" ) != "" ) &&
( ( 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"
}
2022-08-21 02:42:23 -06:00
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"
2022-08-14 04:35:14 -06:00
}
2022-06-03 01:26:36 -06:00
if ( viper . GetString ( "tls_letsencrypt_hostname" ) != "" ) &&
2023-06-06 03:12:36 -06:00
( viper . GetString ( "tls_letsencrypt_challenge_type" ) == TLSALPN01ChallengeType ) &&
2022-06-03 01:26:36 -06:00
( ! 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)
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" )
}
2023-06-06 03:12:36 -06:00
if ( viper . GetString ( "tls_letsencrypt_challenge_type" ) != HTTP01ChallengeType ) &&
( viper . GetString ( "tls_letsencrypt_challenge_type" ) != TLSALPN01ChallengeType ) {
2022-06-03 01:26:36 -06:00
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://" ) &&
! strings . HasPrefix ( viper . GetString ( "server_url" ) , "https://" ) {
errorText += "Fatal config error: server_url must start with https:// or http://\n"
}
2022-06-12 07:12:43 -06:00
// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
// to avoid races
minInactivityTimeout , _ := time . ParseDuration ( "65s" )
if viper . GetDuration ( "ephemeral_node_inactivity_timeout" ) <= minInactivityTimeout {
errorText += fmt . Sprintf (
"Fatal config error: ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s" ,
viper . GetString ( "ephemeral_node_inactivity_timeout" ) ,
minInactivityTimeout ,
)
}
2022-06-03 01:26:36 -06:00
if errorText != "" {
2024-02-09 09:34:28 -07:00
// nolint
2022-06-03 01:26:36 -06:00
return errors . New ( strings . TrimSuffix ( errorText , "\n" ) )
} else {
return nil
}
}
2022-06-03 02:14:14 -06:00
func GetTLSConfig ( ) TLSConfig {
return TLSConfig {
LetsEncrypt : LetsEncryptConfig {
Hostname : viper . GetString ( "tls_letsencrypt_hostname" ) ,
Listen : viper . GetString ( "tls_letsencrypt_listen" ) ,
2023-05-11 01:09:18 -06:00
CacheDir : util . AbsolutePathFromConfigPath (
2022-06-03 02:14:14 -06:00
viper . GetString ( "tls_letsencrypt_cache_dir" ) ,
) ,
ChallengeType : viper . GetString ( "tls_letsencrypt_challenge_type" ) ,
} ,
2023-05-11 01:09:18 -06:00
CertPath : util . AbsolutePathFromConfigPath (
2022-06-03 02:14:14 -06:00
viper . GetString ( "tls_cert_path" ) ,
) ,
2023-05-11 01:09:18 -06:00
KeyPath : util . AbsolutePathFromConfigPath (
2022-06-03 02:14:14 -06:00
viper . GetString ( "tls_key_path" ) ,
) ,
}
}
2022-06-03 01:26:36 -06:00
func GetDERPConfig ( ) DERPConfig {
serverEnabled := viper . GetBool ( "derp.server.enabled" )
serverRegionID := viper . GetInt ( "derp.server.region_id" )
serverRegionCode := viper . GetString ( "derp.server.region_code" )
serverRegionName := viper . GetString ( "derp.server.region_name" )
stunAddr := viper . GetString ( "derp.server.stun_listen_addr" )
2024-01-16 08:04:03 -07:00
privateKeyPath := util . AbsolutePathFromConfigPath (
viper . GetString ( "derp.server.private_key_path" ) ,
)
ipv4 := viper . GetString ( "derp.server.ipv4" )
ipv6 := viper . GetString ( "derp.server.ipv6" )
automaticallyAddEmbeddedDerpRegion := viper . GetBool (
"derp.server.automatically_add_embedded_derp_region" ,
)
2022-06-03 01:26:36 -06:00
if serverEnabled && stunAddr == "" {
log . Fatal ( ) .
Msg ( "derp.server.stun_listen_addr must be set if derp.server.enabled is true" )
}
urlStrs := viper . GetStringSlice ( "derp.urls" )
urls := make ( [ ] url . URL , len ( urlStrs ) )
for index , urlStr := range urlStrs {
urlAddr , err := url . Parse ( urlStr )
if err != nil {
log . Error ( ) .
Str ( "url" , urlStr ) .
Err ( err ) .
Msg ( "Failed to parse url, ignoring..." )
}
urls [ index ] = * urlAddr
}
paths := viper . GetStringSlice ( "derp.paths" )
2024-01-16 08:04:03 -07:00
if serverEnabled && ! automaticallyAddEmbeddedDerpRegion && len ( paths ) == 0 {
log . Fatal ( ) .
Msg ( "Disabling derp.server.automatically_add_embedded_derp_region requires to configure the derp server in derp.paths" )
}
2022-06-03 01:26:36 -06:00
autoUpdate := viper . GetBool ( "derp.auto_update_enabled" )
updateFrequency := viper . GetDuration ( "derp.update_frequency" )
return DERPConfig {
2024-01-16 08:04:03 -07:00
ServerEnabled : serverEnabled ,
ServerRegionID : serverRegionID ,
ServerRegionCode : serverRegionCode ,
ServerRegionName : serverRegionName ,
ServerPrivateKeyPath : privateKeyPath ,
STUNAddr : stunAddr ,
URLs : urls ,
Paths : paths ,
AutoUpdate : autoUpdate ,
UpdateFrequency : updateFrequency ,
IPv4 : ipv4 ,
IPv6 : ipv6 ,
AutomaticallyAddEmbeddedDerpRegion : automaticallyAddEmbeddedDerpRegion ,
2022-06-03 01:26:36 -06:00
}
}
func GetLogTailConfig ( ) LogTailConfig {
enabled := viper . GetBool ( "logtail.enabled" )
return LogTailConfig {
Enabled : enabled ,
}
}
2024-07-17 23:38:25 -06:00
func GetPolicyConfig ( ) PolicyConfig {
policyPath := viper . GetString ( "policy.path" )
policyMode := viper . GetString ( "policy.mode" )
2022-06-03 01:26:36 -06:00
2024-07-17 23:38:25 -06:00
return PolicyConfig {
Path : policyPath ,
Mode : PolicyMode ( policyMode ) ,
2022-06-03 01:26:36 -06:00
}
}
2022-09-11 13:37:23 -06:00
func GetLogConfig ( ) LogConfig {
logLevelStr := viper . GetString ( "log.level" )
logLevel , err := zerolog . ParseLevel ( logLevelStr )
if err != nil {
logLevel = zerolog . DebugLevel
}
logFormatOpt := viper . GetString ( "log.format" )
var logFormat string
switch logFormatOpt {
case "json" :
logFormat = JSONLogFormat
case "text" :
logFormat = TextLogFormat
case "" :
logFormat = TextLogFormat
default :
log . Error ( ) .
Str ( "func" , "GetLogConfig" ) .
Msgf ( "Could not parse log format: %s. Valid choices are 'json' or 'text'" , logFormatOpt )
}
return LogConfig {
Format : logFormat ,
Level : logLevel ,
}
}
2024-02-08 23:27:00 -07:00
func GetDatabaseConfig ( ) DatabaseConfig {
debug := viper . GetBool ( "database.debug" )
type_ := viper . GetString ( "database.type" )
2024-08-19 03:47:52 -06:00
skipErrRecordNotFound := viper . GetBool ( "database.gorm.skip_err_record_not_found" )
slowThreshold := viper . GetDuration ( "database.gorm.slow_threshold" ) * time . Millisecond
parameterizedQueries := viper . GetBool ( "database.gorm.parameterized_queries" )
prepareStmt := viper . GetBool ( "database.gorm.prepare_stmt" )
2024-02-08 23:27:00 -07:00
switch type_ {
case DatabaseSqlite , DatabasePostgres :
break
case "sqlite" :
type_ = "sqlite3"
default :
2024-02-09 09:34:28 -07:00
log . Fatal ( ) .
Msgf ( "invalid database type %q, must be sqlite, sqlite3 or postgres" , type_ )
2024-02-08 23:27:00 -07:00
}
return DatabaseConfig {
Type : type_ ,
Debug : debug ,
2024-08-19 03:47:52 -06:00
Gorm : GormConfig {
Debug : debug ,
SkipErrRecordNotFound : skipErrRecordNotFound ,
SlowThreshold : slowThreshold ,
ParameterizedQueries : parameterizedQueries ,
PrepareStmt : prepareStmt ,
} ,
2024-02-08 23:27:00 -07:00
Sqlite : SqliteConfig {
2024-02-09 09:34:28 -07:00
Path : util . AbsolutePathFromConfigPath (
viper . GetString ( "database.sqlite.path" ) ,
) ,
2024-06-23 14:06:59 -06:00
WriteAheadLog : viper . GetBool ( "database.sqlite.write_ahead_log" ) ,
2024-02-08 23:27:00 -07:00
} ,
Postgres : PostgresConfig {
2024-02-09 09:34:28 -07:00
Host : viper . GetString ( "database.postgres.host" ) ,
Port : viper . GetInt ( "database.postgres.port" ) ,
Name : viper . GetString ( "database.postgres.name" ) ,
User : viper . GetString ( "database.postgres.user" ) ,
Pass : viper . GetString ( "database.postgres.pass" ) ,
Ssl : viper . GetString ( "database.postgres.ssl" ) ,
MaxOpenConnections : viper . GetInt ( "database.postgres.max_open_conns" ) ,
MaxIdleConnections : viper . GetInt ( "database.postgres.max_idle_conns" ) ,
ConnMaxIdleTimeSecs : viper . GetInt (
"database.postgres.conn_max_idle_time_secs" ,
) ,
2024-02-08 23:27:00 -07:00
} ,
}
}
2024-08-19 03:41:05 -06:00
func DNS ( ) ( DNSConfig , error ) {
var dns DNSConfig
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
// TODO: Use this instead of manually getting settings when
// UnmarshalKey is compatible with Environment Variables.
// err := viper.UnmarshalKey("dns", &dns)
// if err != nil {
// return DNSConfig{}, fmt.Errorf("unmarshaling dns config: %w", err)
// }
2022-10-31 09:26:18 -06:00
2024-08-19 03:41:05 -06:00
dns . MagicDNS = viper . GetBool ( "dns.magic_dns" )
dns . BaseDomain = viper . GetString ( "dns.base_domain" )
dns . Nameservers . Global = viper . GetStringSlice ( "dns.nameservers.global" )
dns . Nameservers . Split = viper . GetStringMapStringSlice ( "dns.nameservers.split" )
dns . SearchDomains = viper . GetStringSlice ( "dns.search_domains" )
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
if viper . IsSet ( "dns.extra_records" ) {
var extraRecords [ ] tailcfg . DNSRecord
2022-11-07 13:10:06 -07:00
2024-08-19 03:41:05 -06:00
err := viper . UnmarshalKey ( "dns.extra_records" , & extraRecords )
if err != nil {
return DNSConfig { } , fmt . Errorf ( "unmarshaling dns extra records: %w" , err )
}
2022-11-07 13:10:06 -07:00
2024-08-19 03:41:05 -06:00
dns . ExtraRecords = extraRecords
}
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
dns . UserNameInMagicDNS = viper . GetBool ( "dns.use_username_in_magic_dns" )
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
return dns , nil
}
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
// GlobalResolvers returns the global DNS resolvers
// defined in the config file.
// If a nameserver is a valid IP, it will be used as a regular resolver.
// If a nameserver is a valid URL, it will be used as a DoH resolver.
// If a nameserver is neither a valid URL nor a valid IP, it will be ignored.
func ( d * DNSConfig ) GlobalResolvers ( ) [ ] * dnstype . Resolver {
var resolvers [ ] * dnstype . Resolver
for _ , nsStr := range d . Nameservers . Global {
warn := ""
if _ , err := netip . ParseAddr ( nsStr ) ; err == nil {
resolvers = append ( resolvers , & dnstype . Resolver {
Addr : nsStr ,
} )
continue
} else {
warn = fmt . Sprintf ( "Invalid global nameserver %q. Parsing error: %s ignoring" , nsStr , err )
}
2022-10-31 09:26:18 -06:00
2024-08-19 03:41:05 -06:00
if _ , err := url . Parse ( nsStr ) ; err == nil {
resolvers = append ( resolvers , & dnstype . Resolver {
Addr : nsStr ,
} )
2024-08-23 09:17:37 -06:00
continue
2024-08-19 03:41:05 -06:00
} else {
warn = fmt . Sprintf ( "Invalid global nameserver %q. Parsing error: %s ignoring" , nsStr , err )
2022-06-03 01:26:36 -06:00
}
2024-08-19 03:41:05 -06:00
if warn != "" {
log . Warn ( ) . Msg ( warn )
2022-06-03 01:26:36 -06:00
}
2024-08-19 03:41:05 -06:00
}
return resolvers
}
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
// SplitResolvers returns a map of domain to DNS resolvers.
// If a nameserver is a valid IP, it will be used as a regular resolver.
// If a nameserver is a valid URL, it will be used as a DoH resolver.
// If a nameserver is neither a valid URL nor a valid IP, it will be ignored.
func ( d * DNSConfig ) SplitResolvers ( ) map [ string ] [ ] * dnstype . Resolver {
routes := make ( map [ string ] [ ] * dnstype . Resolver )
for domain , nameservers := range d . Nameservers . Split {
var resolvers [ ] * dnstype . Resolver
for _ , nsStr := range nameservers {
warn := ""
if _ , err := netip . ParseAddr ( nsStr ) ; err == nil {
resolvers = append ( resolvers , & dnstype . Resolver {
Addr : nsStr ,
} )
2022-12-01 18:03:26 -07:00
2024-08-19 03:41:05 -06:00
continue
} else {
warn = fmt . Sprintf ( "Invalid split dns nameserver %q. Parsing error: %s ignoring" , nsStr , err )
2022-12-01 18:03:26 -07:00
}
2024-08-19 03:41:05 -06:00
if _ , err := url . Parse ( nsStr ) ; err == nil {
resolvers = append ( resolvers , & dnstype . Resolver {
Addr : nsStr ,
} )
2024-08-23 09:17:37 -06:00
continue
2024-08-19 03:41:05 -06:00
} else {
warn = fmt . Sprintf ( "Invalid split dns nameserver %q. Parsing error: %s ignoring" , nsStr , err )
}
2022-12-01 18:03:26 -07:00
2024-08-19 03:41:05 -06:00
if warn != "" {
log . Warn ( ) . Msg ( warn )
}
2022-10-31 08:59:50 -06:00
}
2024-08-19 03:41:05 -06:00
routes [ domain ] = resolvers
}
2022-10-31 08:59:50 -06:00
2024-08-19 03:41:05 -06:00
return routes
}
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
func DNSToTailcfgDNS ( dns DNSConfig ) * tailcfg . DNSConfig {
cfg := tailcfg . DNSConfig { }
2024-06-26 05:44:40 -06:00
2024-08-19 03:41:05 -06:00
if dns . BaseDomain == "" && dns . MagicDNS {
log . Fatal ( ) . Msg ( "dns.base_domain must be set when using MagicDNS (dns.magic_dns)" )
}
2023-08-31 10:37:18 -06:00
2024-08-19 03:41:05 -06:00
cfg . Proxied = dns . MagicDNS
cfg . ExtraRecords = dns . ExtraRecords
cfg . Resolvers = dns . GlobalResolvers ( )
routes := dns . SplitResolvers ( )
cfg . Routes = routes
if dns . BaseDomain != "" {
cfg . Domains = [ ] string { dns . BaseDomain }
2022-06-03 01:26:36 -06:00
}
2024-08-19 03:41:05 -06:00
cfg . Domains = append ( cfg . Domains , dns . SearchDomains ... )
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
return & cfg
2022-06-03 01:26:36 -06:00
}
2024-04-16 23:03:06 -06:00
func PrefixV4 ( ) ( * netip . Prefix , error ) {
2024-02-18 11:31:29 -07:00
prefixV4Str := viper . GetString ( "prefixes.v4" )
2024-04-16 23:03:06 -06:00
if prefixV4Str == "" {
return nil , nil
2024-02-18 11:31:29 -07:00
}
2024-04-16 23:03:06 -06:00
prefixV4 , err := netip . ParsePrefix ( prefixV4Str )
2024-02-18 11:31:29 -07:00
if err != nil {
2024-04-16 23:03:06 -06:00
return nil , fmt . Errorf ( "parsing IPv4 prefix from config: %w" , err )
2024-02-18 11:31:29 -07:00
}
builder := netipx . IPSetBuilder { }
builder . AddPrefix ( tsaddr . CGNATRange ( ) )
ipSet , _ := builder . IPSet ( )
if ! ipSet . ContainsPrefix ( prefixV4 ) {
log . Warn ( ) .
Msgf ( "Prefix %s is not in the %s range. This is an unsupported configuration." ,
prefixV4Str , tsaddr . CGNATRange ( ) )
}
2024-04-16 23:03:06 -06:00
return & prefixV4 , nil
}
func PrefixV6 ( ) ( * netip . Prefix , error ) {
prefixV6Str := viper . GetString ( "prefixes.v6" )
if prefixV6Str == "" {
return nil , nil
}
prefixV6 , err := netip . ParsePrefix ( prefixV6Str )
if err != nil {
return nil , fmt . Errorf ( "parsing IPv6 prefix from config: %w" , err )
}
builder := netipx . IPSetBuilder { }
builder . AddPrefix ( tsaddr . TailscaleULARange ( ) )
ipSet , _ := builder . IPSet ( )
2024-02-18 11:31:29 -07:00
if ! ipSet . ContainsPrefix ( prefixV6 ) {
log . Warn ( ) .
Msgf ( "Prefix %s is not in the %s range. This is an unsupported configuration." ,
prefixV6Str , tsaddr . TailscaleULARange ( ) )
}
2024-04-16 23:03:06 -06:00
return & prefixV6 , nil
2024-02-18 11:31:29 -07:00
}
2024-05-09 07:15:57 -06:00
func GetOIDCConfig ( ) ( OIDCConfig , error ) {
// get expiry config
2024-05-09 08:02:23 -06:00
oidcExpireConfig := OIDCExpireConfig {
2024-05-09 07:15:57 -06:00
FromToken : viper . GetBool ( "oidc.expiry.from_token" ) ,
FixedTime : func ( ) time . Duration {
// if set to 0, we assume no expiry
if value := viper . GetString ( "oidc.expiry.fixed_time" ) ; value == "0" {
return maxDuration
} else {
expiry , err := model . ParseDuration ( value )
if err != nil {
log . Warn ( ) . Msg ( "failed to parse oidc.expiry.fixed_time, defaulting back to 180 days" )
return defaultOIDCExpiryTime
}
return time . Duration ( expiry )
}
} ( ) ,
}
// get allowed config
2024-05-09 08:02:23 -06:00
oidcAllowed := OIDCAllowedConfig {
2024-05-09 07:15:57 -06:00
Domains : viper . GetStringSlice ( "oidc.allowed.domains" ) ,
Users : viper . GetStringSlice ( "oidc.allowed.users" ) ,
Groups : viper . GetStringSlice ( "oidc.allowed.groups" ) ,
}
// get claims map
2024-05-09 08:02:23 -06:00
oidcClaimsMap := OIDCClaimsMap {
2024-05-09 07:15:57 -06:00
Name : viper . GetString ( "oidc.claims_map.name" ) ,
Username : viper . GetString ( "oidc.claims_map.username" ) ,
Email : viper . GetString ( "oidc.claims_map.email" ) ,
Groups : viper . GetString ( "oidc.claims_map.groups" ) ,
}
// get misc config
2024-05-09 08:02:23 -06:00
oidcMiscConfig := OIDCMiscConfig {
2024-05-09 08:42:39 -06:00
StripEmaildomain : viper . GetBool ( "oidc.misc.strip_email_domain" ) ,
FlattenGroups : viper . GetBool ( "oidc.misc.flatten_groups" ) ,
FlattenSplter : viper . GetString ( "oidc.misc.flatten_splitter" ) ,
2024-05-09 07:15:57 -06:00
}
// get client secret
oidcClientSecret := viper . GetString ( "oidc.client_secret" )
oidcClientSecretPath := viper . GetString ( "oidc.client_secret_path" )
if oidcClientSecretPath != "" && oidcClientSecret != "" {
return OIDCConfig { } , errOidcMutuallyExclusive
}
if oidcClientSecretPath != "" {
secretBytes , err := os . ReadFile ( os . ExpandEnv ( oidcClientSecretPath ) )
if err != nil {
return OIDCConfig { } , err
}
oidcClientSecret = strings . TrimSpace ( string ( secretBytes ) )
}
OIDC := OIDCConfig {
OnlyStartIfOIDCIsAvailable : viper . GetBool (
"oidc.only_start_if_oidc_is_available" ,
) ,
2024-05-09 08:42:39 -06:00
Issuer : viper . GetString ( "oidc.issuer" ) ,
ClientID : viper . GetString ( "oidc.client_id" ) ,
ClientSecret : oidcClientSecret ,
Scope : viper . GetStringSlice ( "oidc.scope" ) ,
ExtraParams : viper . GetStringMapString ( "oidc.extra_params" ) ,
Allowed : oidcAllowed ,
ClaimsMap : oidcClaimsMap ,
Expiry : oidcExpireConfig ,
Misc : oidcMiscConfig ,
2024-05-09 07:15:57 -06:00
}
return OIDC , nil
}
2022-06-05 09:47:12 -06:00
func GetHeadscaleConfig ( ) ( * Config , error ) {
2022-11-18 10:02:34 -07:00
if IsCLIConfigured ( ) {
return & Config {
CLI : CLIConfig {
Address : viper . GetString ( "cli.address" ) ,
APIKey : viper . GetString ( "cli.api_key" ) ,
Timeout : viper . GetDuration ( "cli.timeout" ) ,
Insecure : viper . GetBool ( "cli.insecure" ) ,
} ,
} , nil
}
2024-05-24 02:15:34 -06:00
logConfig := GetLogConfig ( )
2024-05-15 18:40:30 -06:00
zerolog . SetGlobalLevel ( logConfig . Level )
2024-04-16 23:03:06 -06:00
prefix4 , err := PrefixV4 ( )
if err != nil {
return nil , err
}
prefix6 , err := PrefixV6 ( )
2024-02-18 11:31:29 -07:00
if err != nil {
return nil , err
}
2024-04-30 03:11:29 -06:00
if prefix4 == nil && prefix6 == nil {
return nil , fmt . Errorf ( "no IPv4 or IPv6 prefix configured, minimum one prefix is required" )
}
2024-04-16 23:03:06 -06:00
allocStr := viper . GetString ( "prefixes.allocation" )
var alloc IPAllocationStrategy
switch allocStr {
case string ( IPAllocationStrategySequential ) :
alloc = IPAllocationStrategySequential
case string ( IPAllocationStrategyRandom ) :
alloc = IPAllocationStrategyRandom
default :
2024-04-30 03:11:29 -06:00
return nil , fmt . Errorf ( "config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s" , allocStr , IPAllocationStrategySequential , IPAllocationStrategyRandom )
2024-04-16 23:03:06 -06:00
}
2024-08-19 03:41:05 -06:00
dnsConfig , err := DNS ( )
if err != nil {
return nil , err
}
2022-06-03 01:26:36 -06:00
derpConfig := GetDERPConfig ( )
2024-05-15 18:40:30 -06:00
logTailConfig := GetLogTailConfig ( )
2022-06-09 13:20:11 -06:00
randomizeClientPort := viper . GetBool ( "randomize_client_port" )
2022-06-03 01:26:36 -06:00
2024-05-09 07:15:57 -06:00
oidcConfig , err := GetOIDCConfig ( )
if err != nil {
return nil , err
2023-01-10 04:46:42 -07:00
}
2024-08-19 03:41:05 -06:00
serverURL := viper . GetString ( "server_url" )
// BaseDomain cannot be the same as the server URL.
// This is because Tailscale takes over the domain in BaseDomain,
// causing the headscale server and DERP to be unreachable.
// For Tailscale upstream, the following is true:
// - DERP run on their own domains
// - Control plane runs on login.tailscale.com/controlplane.tailscale.com
// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net)
//
// TODO(kradalby): remove dnsConfig.UserNameInMagicDNS check when removed.
if ! dnsConfig . UserNameInMagicDNS && dnsConfig . BaseDomain != "" && strings . Contains ( serverURL , dnsConfig . BaseDomain ) {
return nil , errors . New ( "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node." )
}
2022-06-05 09:47:12 -06:00
return & Config {
2024-08-19 03:41:05 -06:00
ServerURL : serverURL ,
2022-06-03 02:37:45 -06:00
Addr : viper . GetString ( "listen_addr" ) ,
MetricsAddr : viper . GetString ( "metrics_listen_addr" ) ,
GRPCAddr : viper . GetString ( "grpc_listen_addr" ) ,
GRPCAllowInsecure : viper . GetBool ( "grpc_allow_insecure" ) ,
DisableUpdateCheck : viper . GetBool ( "disable_check_updates" ) ,
2022-06-03 01:26:36 -06:00
2024-04-16 23:03:06 -06:00
PrefixV4 : prefix4 ,
PrefixV6 : prefix6 ,
IPAllocation : IPAllocationStrategy ( alloc ) ,
2024-02-18 11:31:29 -07:00
2023-05-11 01:09:18 -06:00
NoisePrivateKeyPath : util . AbsolutePathFromConfigPath (
2022-08-21 02:42:23 -06:00
viper . GetString ( "noise.private_key_path" ) ,
2022-08-13 03:14:38 -06:00
) ,
2024-08-19 03:41:05 -06:00
BaseDomain : dnsConfig . BaseDomain ,
2022-06-03 01:26:36 -06:00
DERP : derpConfig ,
EphemeralNodeInactivityTimeout : viper . GetDuration (
"ephemeral_node_inactivity_timeout" ,
) ,
2024-02-08 23:27:00 -07:00
Database : GetDatabaseConfig ( ) ,
2022-06-03 01:26:36 -06:00
2022-06-03 02:14:14 -06:00
TLS : GetTLSConfig ( ) ,
2022-06-03 01:26:36 -06:00
2024-08-19 03:41:05 -06:00
DNSConfig : DNSToTailcfgDNS ( dnsConfig ) ,
DNSUserNameInMagicDNS : dnsConfig . UserNameInMagicDNS ,
2022-06-03 01:26:36 -06:00
ACMEEmail : viper . GetString ( "acme_email" ) ,
ACMEURL : viper . GetString ( "acme_url" ) ,
UnixSocket : viper . GetString ( "unix_socket" ) ,
2023-05-11 01:09:18 -06:00
UnixSocketPermission : util . GetFileMode ( "unix_socket_permission" ) ,
2024-05-09 08:42:39 -06:00
OIDC : oidcConfig ,
LogTail : logConfig ,
RandomizeClientPort : randomizeClientPort ,
2022-06-03 01:26:36 -06:00
2024-07-17 23:38:25 -06:00
Policy : GetPolicyConfig ( ) ,
2022-09-11 13:37:23 -06:00
2022-11-18 10:48:34 -07:00
CLI : CLIConfig {
Address : viper . GetString ( "cli.address" ) ,
APIKey : viper . GetString ( "cli.api_key" ) ,
Timeout : viper . GetDuration ( "cli.timeout" ) ,
Insecure : viper . GetBool ( "cli.insecure" ) ,
} ,
2024-05-15 18:40:30 -06:00
Log : logConfig ,
2024-02-23 02:59:24 -07:00
// TODO(kradalby): Document these settings when more stable
Tuning : Tuning {
2024-05-24 02:15:34 -06:00
NotifierSendTimeout : viper . GetDuration ( "tuning.notifier_send_timeout" ) ,
2024-02-23 02:59:24 -07:00
BatchChangeDelay : viper . GetDuration ( "tuning.batch_change_delay" ) ,
NodeMapSessionBufferedChanSize : viper . GetInt ( "tuning.node_mapsession_buffered_chan_size" ) ,
} ,
2022-06-05 09:47:12 -06:00
} , nil
2022-06-03 01:26:36 -06:00
}
2022-11-18 10:02:34 -07:00
func IsCLIConfigured ( ) bool {
return viper . GetString ( "cli.address" ) != "" && viper . GetString ( "cli.api_key" ) != ""
}
2024-07-17 23:38:25 -06:00
2024-08-19 03:41:05 -06:00
type deprecator struct {
warns set . Set [ string ]
fatals set . Set [ string ]
}
// warnWithAlias will register an alias between the newKey and the oldKey,
2024-07-17 23:38:25 -06:00
// and log a deprecation warning if the oldKey is set.
2024-08-19 03:41:05 -06:00
func ( d * deprecator ) warnWithAlias ( newKey , oldKey string ) {
2024-07-17 23:38:25 -06:00
// NOTE: RegisterAlias is called with NEW KEY -> OLD KEY
viper . RegisterAlias ( newKey , oldKey )
if viper . IsSet ( oldKey ) {
2024-08-19 03:41:05 -06:00
d . warns . Add ( fmt . Sprintf ( "The %q configuration key is deprecated. Please use %q instead. %q will be removed in the future." , oldKey , newKey , oldKey ) )
2024-07-17 23:38:25 -06:00
}
}
2024-08-19 03:41:05 -06:00
// fatal deprecates and adds an entry to the fatal list of options if the oldKey is set.
func ( d * deprecator ) fatal ( newKey , oldKey string ) {
2024-07-17 23:38:25 -06:00
if viper . IsSet ( oldKey ) {
2024-08-19 03:41:05 -06:00
d . fatals . Add ( fmt . Sprintf ( "The %q configuration key is deprecated. Please use %q instead. %q has been removed." , oldKey , newKey , oldKey ) )
}
}
// fatalIfNewKeyIsNotUsed deprecates and adds an entry to the fatal list of options if the oldKey is set and the new key is _not_ set.
// If the new key is set, a warning is emitted instead.
func ( d * deprecator ) fatalIfNewKeyIsNotUsed ( newKey , oldKey string ) {
if viper . IsSet ( oldKey ) && ! viper . IsSet ( newKey ) {
d . fatals . Add ( fmt . Sprintf ( "The %q configuration key is deprecated. Please use %q instead. %q has been removed." , oldKey , newKey , oldKey ) )
} else if viper . IsSet ( oldKey ) {
d . warns . Add ( fmt . Sprintf ( "The %q configuration key is deprecated. Please use %q instead. %q has been removed." , oldKey , newKey , oldKey ) )
}
}
// warn deprecates and adds an option to log a warning if the oldKey is set.
func ( d * deprecator ) warnNoAlias ( newKey , oldKey string ) {
if viper . IsSet ( oldKey ) {
d . warns . Add ( fmt . Sprintf ( "The %q configuration key is deprecated. Please use %q instead. %q has been removed." , oldKey , newKey , oldKey ) )
}
}
// warn deprecates and adds an entry to the warn list of options if the oldKey is set.
func ( d * deprecator ) warn ( oldKey string ) {
if viper . IsSet ( oldKey ) {
d . warns . Add ( fmt . Sprintf ( "The %q configuration key is deprecated and has been removed. Please see the changelog for more details." , oldKey ) )
}
}
func ( d * deprecator ) String ( ) string {
var b strings . Builder
for _ , w := range d . warns . Slice ( ) {
fmt . Fprintf ( & b , "WARN: %s\n" , w )
}
for _ , f := range d . fatals . Slice ( ) {
fmt . Fprintf ( & b , "FATAL: %s\n" , f )
}
return b . String ( )
}
func ( d * deprecator ) Log ( ) {
if len ( d . fatals ) > 0 {
log . Fatal ( ) . Msg ( "\n" + d . String ( ) )
} else if len ( d . warns ) > 0 {
log . Warn ( ) . Msg ( "\n" + d . String ( ) )
2024-07-17 23:38:25 -06:00
}
}