2021-04-28 16:15:45 +02:00
package cli
import (
2021-05-08 13:28:22 +02:00
"encoding/json"
2021-06-05 11:13:28 +02:00
"errors"
2021-05-08 13:28:22 +02:00
"fmt"
2021-04-28 16:15:45 +02:00
"io"
"os"
"path/filepath"
"strings"
2021-05-23 02:15:29 +02:00
"time"
2021-04-28 16:15:45 +02:00
"github.com/juanfont/headscale"
2021-08-05 19:26:49 +02:00
"github.com/rs/zerolog/log"
2021-04-28 16:15:45 +02:00
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
2021-08-02 21:06:26 +02:00
"inet.af/netaddr"
2021-04-28 16:15:45 +02:00
"tailscale.com/tailcfg"
)
2021-05-08 13:28:22 +02:00
type ErrorOutput struct {
Error string
}
2021-06-05 11:13:28 +02:00
func LoadConfig ( path string ) error {
viper . SetConfigName ( "config" )
if path == "" {
viper . AddConfigPath ( "/etc/headscale/" )
viper . AddConfigPath ( "$HOME/.headscale" )
viper . AddConfigPath ( "." )
} else {
// For testing
viper . AddConfigPath ( path )
}
viper . AutomaticEnv ( )
viper . SetDefault ( "tls_letsencrypt_cache_dir" , "/var/www/.cache" )
viper . SetDefault ( "tls_letsencrypt_challenge_type" , "HTTP-01" )
2021-08-02 21:06:26 +02:00
viper . SetDefault ( "ip_prefix" , "100.64.0.0/10" )
2021-08-05 20:19:25 +02:00
viper . SetDefault ( "log_level" , "debug" )
2021-06-05 11:13:28 +02:00
err := viper . ReadInConfig ( )
if err != nil {
return fmt . Errorf ( "Fatal error reading config file: %s \n" , err )
}
// 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"
}
if ( viper . GetString ( "tls_letsencrypt_hostname" ) != "" ) && ( viper . GetString ( "tls_letsencrypt_challenge_type" ) == "TLS-ALPN-01" ) && ( ! strings . HasSuffix ( viper . GetString ( "listen_addr" ) , ":443" ) ) {
2021-07-17 04:02:05 +02:00
// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
2021-08-05 19:26:49 +02:00
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" )
2021-06-05 11:13:28 +02:00
}
if ( viper . GetString ( "tls_letsencrypt_challenge_type" ) != "HTTP-01" ) && ( viper . GetString ( "tls_letsencrypt_challenge_type" ) != "TLS-ALPN-01" ) {
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"
}
if errorText != "" {
return errors . New ( strings . TrimSuffix ( errorText , "\n" ) )
} else {
return nil
}
}
2021-04-28 16:15:45 +02:00
func absPath ( path string ) string {
// If a relative path is provided, prefix it with the the directory where
// the config file was found.
2021-05-18 23:33:08 +02:00
if ( path != "" ) && ! strings . HasPrefix ( path , string ( os . PathSeparator ) ) {
2021-04-28 16:15:45 +02:00
dir , _ := filepath . Split ( viper . ConfigFileUsed ( ) )
if dir != "" {
2021-05-18 23:33:08 +02:00
path = filepath . Join ( dir , path )
2021-04-28 16:15:45 +02:00
}
}
return path
}
func getHeadscaleApp ( ) ( * headscale . Headscale , error ) {
2021-08-05 19:26:49 +02:00
derpPath := absPath ( viper . GetString ( "derp_map_path" ) )
derpMap , err := loadDerpMap ( derpPath )
2021-04-28 16:15:45 +02:00
if err != nil {
2021-08-05 19:26:49 +02:00
log . Error ( ) .
2021-08-05 21:57:47 +02:00
Str ( "path" , derpPath ) .
2021-08-05 19:26:49 +02:00
Err ( err ) .
Msg ( "Could not load DERP servers map file" )
2021-04-28 16:15:45 +02:00
}
2021-05-23 02:15:29 +02: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 {
err = fmt . Errorf ( "ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s\n" , viper . GetString ( "ephemeral_node_inactivity_timeout" ) , minInactivityTimeout )
return nil , err
}
2021-04-28 16:15:45 +02:00
cfg := headscale . Config {
ServerURL : viper . GetString ( "server_url" ) ,
Addr : viper . GetString ( "listen_addr" ) ,
PrivateKeyPath : absPath ( viper . GetString ( "private_key_path" ) ) ,
DerpMap : derpMap ,
2021-08-02 21:06:26 +02:00
IPPrefix : netaddr . MustParseIPPrefix ( viper . GetString ( "ip_prefix" ) ) ,
2021-04-28 16:15:45 +02:00
2021-05-23 02:15:29 +02:00
EphemeralNodeInactivityTimeout : viper . GetDuration ( "ephemeral_node_inactivity_timeout" ) ,
2021-05-15 14:32:26 +02:00
DBtype : viper . GetString ( "db_type" ) ,
2021-05-19 01:28:47 +02:00
DBpath : absPath ( viper . GetString ( "db_path" ) ) ,
2021-04-28 16:15:45 +02:00
DBhost : viper . GetString ( "db_host" ) ,
DBport : viper . GetInt ( "db_port" ) ,
DBname : viper . GetString ( "db_name" ) ,
DBuser : viper . GetString ( "db_user" ) ,
DBpass : viper . GetString ( "db_pass" ) ,
TLSLetsEncryptHostname : viper . GetString ( "tls_letsencrypt_hostname" ) ,
2021-07-24 00:12:01 +02:00
TLSLetsEncryptListen : viper . GetString ( "tls_letsencrypt_listen" ) ,
2021-04-28 16:15:45 +02:00
TLSLetsEncryptCacheDir : absPath ( viper . GetString ( "tls_letsencrypt_cache_dir" ) ) ,
TLSLetsEncryptChallengeType : viper . GetString ( "tls_letsencrypt_challenge_type" ) ,
TLSCertPath : absPath ( viper . GetString ( "tls_cert_path" ) ) ,
TLSKeyPath : absPath ( viper . GetString ( "tls_key_path" ) ) ,
}
h , err := headscale . NewHeadscale ( cfg )
if err != nil {
return nil , err
}
2021-07-04 13:24:05 +02:00
// We are doing this here, as in the future could be cool to have it also hot-reload
2021-07-11 15:10:11 +02:00
if viper . GetString ( "acl_policy_path" ) != "" {
2021-08-05 19:26:49 +02:00
aclPath := absPath ( viper . GetString ( "acl_policy_path" ) )
err = h . LoadACLPolicy ( aclPath )
2021-07-11 15:10:11 +02:00
if err != nil {
2021-08-05 19:26:49 +02:00
log . Error ( ) .
2021-08-05 21:57:47 +02:00
Str ( "path" , aclPath ) .
2021-08-05 19:26:49 +02:00
Err ( err ) .
Msg ( "Could not load the ACL policy" )
2021-07-11 15:10:11 +02:00
}
2021-07-04 13:24:05 +02:00
}
2021-04-28 16:15:45 +02:00
return h , nil
}
func loadDerpMap ( path string ) ( * tailcfg . DERPMap , error ) {
derpFile , err := os . Open ( path )
if err != nil {
return nil , err
}
defer derpFile . Close ( )
var derpMap tailcfg . DERPMap
b , err := io . ReadAll ( derpFile )
if err != nil {
return nil , err
}
err = yaml . Unmarshal ( b , & derpMap )
return & derpMap , err
}
2021-05-08 13:28:22 +02:00
2021-05-08 17:06:36 +02:00
func JsonOutput ( result interface { } , errResult error , outputFormat string ) {
2021-05-08 13:28:22 +02:00
var j [ ] byte
var err error
switch outputFormat {
case "json" :
if errResult != nil {
j , err = json . MarshalIndent ( ErrorOutput { errResult . Error ( ) } , "" , "\t" )
if err != nil {
2021-08-05 20:19:25 +02:00
log . Fatal ( ) . Err ( err )
2021-05-08 13:28:22 +02:00
}
} else {
j , err = json . MarshalIndent ( result , "" , "\t" )
if err != nil {
2021-08-05 20:19:25 +02:00
log . Fatal ( ) . Err ( err )
2021-05-08 13:28:22 +02:00
}
}
case "json-line" :
if errResult != nil {
j , err = json . Marshal ( ErrorOutput { errResult . Error ( ) } )
if err != nil {
2021-08-05 20:19:25 +02:00
log . Fatal ( ) . Err ( err )
2021-05-08 13:28:22 +02:00
}
} else {
j , err = json . Marshal ( result )
if err != nil {
2021-08-05 20:19:25 +02:00
log . Fatal ( ) . Err ( err )
2021-05-08 13:28:22 +02:00
}
}
}
fmt . Println ( string ( j ) )
}