mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			220 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			220 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package cli
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/tls"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"reflect"
 | 
						|
 | 
						|
	"github.com/juanfont/headscale"
 | 
						|
	v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
 | 
						|
	"github.com/rs/zerolog/log"
 | 
						|
	"google.golang.org/grpc"
 | 
						|
	"google.golang.org/grpc/credentials"
 | 
						|
	"google.golang.org/grpc/credentials/insecure"
 | 
						|
	"gopkg.in/yaml.v2"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
 | 
						|
	SocketWritePermissions  = 0o666
 | 
						|
)
 | 
						|
 | 
						|
func getHeadscaleApp() (*headscale.Headscale, error) {
 | 
						|
	cfg, err := headscale.GetHeadscaleConfig()
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf(
 | 
						|
			"failed to load configuration while creating headscale instance: %w",
 | 
						|
			err,
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	app, err := headscale.NewHeadscale(cfg)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// We are doing this here, as in the future could be cool to have it also hot-reload
 | 
						|
 | 
						|
	if cfg.ACL.PolicyPath != "" {
 | 
						|
		aclPath := headscale.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath)
 | 
						|
		err = app.LoadACLPolicy(aclPath)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal().
 | 
						|
				Str("path", aclPath).
 | 
						|
				Err(err).
 | 
						|
				Msg("Could not load the ACL policy")
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return app, nil
 | 
						|
}
 | 
						|
 | 
						|
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
 | 
						|
	cfg, err := headscale.GetHeadscaleConfig()
 | 
						|
	if err != nil {
 | 
						|
		log.Fatal().
 | 
						|
			Err(err).
 | 
						|
			Caller().
 | 
						|
			Msgf("Failed to load configuration")
 | 
						|
		os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
 | 
						|
	}
 | 
						|
 | 
						|
	log.Debug().
 | 
						|
		Dur("timeout", cfg.CLI.Timeout).
 | 
						|
		Msgf("Setting timeout")
 | 
						|
 | 
						|
	ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
 | 
						|
 | 
						|
	grpcOptions := []grpc.DialOption{
 | 
						|
		grpc.WithBlock(),
 | 
						|
	}
 | 
						|
 | 
						|
	address := cfg.CLI.Address
 | 
						|
 | 
						|
	// If the address is not set, we assume that we are on the server hosting headscale.
 | 
						|
	if address == "" {
 | 
						|
		log.Debug().
 | 
						|
			Str("socket", cfg.UnixSocket).
 | 
						|
			Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
 | 
						|
 | 
						|
		address = cfg.UnixSocket
 | 
						|
 | 
						|
		// Try to give the user better feedback if we cannot write to the headscale
 | 
						|
		// socket.
 | 
						|
		socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
 | 
						|
		if err != nil {
 | 
						|
			if os.IsPermission(err) {
 | 
						|
				log.Fatal().
 | 
						|
					Err(err).
 | 
						|
					Str("socket", cfg.UnixSocket).
 | 
						|
					Msgf("Unable to read/write to headscale socket, do you have the correct permissions?")
 | 
						|
			}
 | 
						|
		}
 | 
						|
		socket.Close()
 | 
						|
 | 
						|
		grpcOptions = append(
 | 
						|
			grpcOptions,
 | 
						|
			grpc.WithTransportCredentials(insecure.NewCredentials()),
 | 
						|
			grpc.WithContextDialer(headscale.GrpcSocketDialer),
 | 
						|
		)
 | 
						|
	} else {
 | 
						|
		// If we are not connecting to a local server, require an API key for authentication
 | 
						|
		apiKey := cfg.CLI.APIKey
 | 
						|
		if apiKey == "" {
 | 
						|
			log.Fatal().Caller().Msgf("HEADSCALE_CLI_API_KEY environment variable needs to be set.")
 | 
						|
		}
 | 
						|
		grpcOptions = append(grpcOptions,
 | 
						|
			grpc.WithPerRPCCredentials(tokenAuth{
 | 
						|
				token: apiKey,
 | 
						|
			}),
 | 
						|
		)
 | 
						|
 | 
						|
		if cfg.CLI.Insecure {
 | 
						|
			tlsConfig := &tls.Config{
 | 
						|
				// turn of gosec as we are intentionally setting
 | 
						|
				// insecure.
 | 
						|
				//nolint:gosec
 | 
						|
				InsecureSkipVerify: true,
 | 
						|
			}
 | 
						|
 | 
						|
			grpcOptions = append(grpcOptions,
 | 
						|
				grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
 | 
						|
			)
 | 
						|
		} else {
 | 
						|
			grpcOptions = append(grpcOptions,
 | 
						|
				grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
 | 
						|
			)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	log.Trace().Caller().Str("address", address).Msg("Connecting via gRPC")
 | 
						|
	conn, err := grpc.DialContext(ctx, address, grpcOptions...)
 | 
						|
	if err != nil {
 | 
						|
		log.Fatal().Caller().Err(err).Msgf("Could not connect: %v", err)
 | 
						|
		os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
 | 
						|
	}
 | 
						|
 | 
						|
	client := v1.NewHeadscaleServiceClient(conn)
 | 
						|
 | 
						|
	return ctx, client, conn, cancel
 | 
						|
}
 | 
						|
 | 
						|
func SuccessOutput(result interface{}, override string, outputFormat string) {
 | 
						|
	var jsonBytes []byte
 | 
						|
	var err error
 | 
						|
	switch outputFormat {
 | 
						|
	case "json":
 | 
						|
		jsonBytes, err = json.MarshalIndent(result, "", "\t")
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal().Err(err)
 | 
						|
		}
 | 
						|
	case "json-line":
 | 
						|
		jsonBytes, err = json.Marshal(result)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal().Err(err)
 | 
						|
		}
 | 
						|
	case "yaml":
 | 
						|
		jsonBytes, err = yaml.Marshal(result)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal().Err(err)
 | 
						|
		}
 | 
						|
	default:
 | 
						|
		//nolint
 | 
						|
		fmt.Println(override)
 | 
						|
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	//nolint
 | 
						|
	fmt.Println(string(jsonBytes))
 | 
						|
}
 | 
						|
 | 
						|
func ErrorOutput(errResult error, override string, outputFormat string) {
 | 
						|
	type errOutput struct {
 | 
						|
		Error string `json:"error"`
 | 
						|
	}
 | 
						|
 | 
						|
	SuccessOutput(errOutput{errResult.Error()}, override, outputFormat)
 | 
						|
}
 | 
						|
 | 
						|
func HasMachineOutputFlag() bool {
 | 
						|
	for _, arg := range os.Args {
 | 
						|
		if arg == "json" || arg == "json-line" || arg == "yaml" {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
type tokenAuth struct {
 | 
						|
	token string
 | 
						|
}
 | 
						|
 | 
						|
// Return value is mapped to request headers.
 | 
						|
func (t tokenAuth) GetRequestMetadata(
 | 
						|
	ctx context.Context,
 | 
						|
	in ...string,
 | 
						|
) (map[string]string, error) {
 | 
						|
	return map[string]string{
 | 
						|
		"authorization": "Bearer " + t.token,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (tokenAuth) RequireTransportSecurity() bool {
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
func contains[T string](ts []T, t T) bool {
 | 
						|
	for _, v := range ts {
 | 
						|
		if reflect.DeepEqual(v, t) {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return false
 | 
						|
}
 |