2021-04-28 16:15:45 +02:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
2021-10-29 19:09:06 +02:00
|
|
|
"context"
|
2022-02-13 09:41:49 +01:00
|
|
|
"crypto/tls"
|
2021-05-08 13:28:22 +02:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2021-04-28 16:15:45 +02:00
|
|
|
"os"
|
2022-04-25 21:50:40 +02:00
|
|
|
"reflect"
|
2021-04-28 16:15:45 +02:00
|
|
|
|
|
|
|
"github.com/juanfont/headscale"
|
2021-11-04 23:31:47 +01:00
|
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
2021-08-05 19:26:49 +02:00
|
|
|
"github.com/rs/zerolog/log"
|
2021-10-29 19:09:06 +02:00
|
|
|
"google.golang.org/grpc"
|
2022-02-12 20:08:33 +01:00
|
|
|
"google.golang.org/grpc/credentials"
|
|
|
|
"google.golang.org/grpc/credentials/insecure"
|
2021-11-04 23:31:47 +01:00
|
|
|
"gopkg.in/yaml.v2"
|
2021-04-28 16:15:45 +02:00
|
|
|
)
|
|
|
|
|
2022-01-28 19:58:22 +01:00
|
|
|
const (
|
2022-01-25 23:11:15 +01:00
|
|
|
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
|
2022-10-31 10:41:34 +01:00
|
|
|
SocketWritePermissions = 0o666
|
2022-01-28 19:58:22 +01:00
|
|
|
)
|
|
|
|
|
2021-10-29 19:09:06 +02:00
|
|
|
func getHeadscaleApp() (*headscale.Headscale, error) {
|
2022-06-05 17:47:12 +02:00
|
|
|
cfg, err := headscale.GetHeadscaleConfig()
|
|
|
|
if err != nil {
|
2022-08-04 10:47:00 +02:00
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"failed to load configuration while creating headscale instance: %w",
|
|
|
|
err,
|
|
|
|
)
|
2022-06-05 17:47:12 +02:00
|
|
|
}
|
|
|
|
|
2021-11-14 20:32:03 +01:00
|
|
|
app, err := headscale.NewHeadscale(cfg)
|
2021-04-28 16:15:45 +02:00
|
|
|
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
|
|
|
|
2022-05-31 12:50:43 +02:00
|
|
|
if cfg.ACL.PolicyPath != "" {
|
2022-05-31 12:57:48 +02:00
|
|
|
aclPath := headscale.AbsolutePathFromConfigPath(cfg.ACL.PolicyPath)
|
2021-11-14 20:32:03 +01:00
|
|
|
err = app.LoadACLPolicy(aclPath)
|
2021-07-11 15:10:11 +02:00
|
|
|
if err != nil {
|
2022-04-06 16:05:08 +02:00
|
|
|
log.Fatal().
|
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-11-14 20:32:03 +01:00
|
|
|
return app, nil
|
2021-04-28 16:15:45 +02:00
|
|
|
}
|
|
|
|
|
2021-11-07 10:41:14 +01:00
|
|
|
func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) {
|
2022-06-05 17:47:12 +02:00
|
|
|
cfg, err := headscale.GetHeadscaleConfig()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().
|
|
|
|
Err(err).
|
|
|
|
Caller().
|
|
|
|
Msgf("Failed to load configuration")
|
2022-06-26 09:52:04 +02:00
|
|
|
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
|
2022-06-05 17:47:12 +02:00
|
|
|
}
|
2021-11-07 10:41:14 +01:00
|
|
|
|
|
|
|
log.Debug().
|
|
|
|
Dur("timeout", cfg.CLI.Timeout).
|
|
|
|
Msgf("Setting timeout")
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
|
|
|
|
|
2021-10-29 19:15:52 +02:00
|
|
|
grpcOptions := []grpc.DialOption{
|
|
|
|
grpc.WithBlock(),
|
|
|
|
}
|
|
|
|
|
2021-11-07 10:41:14 +01:00
|
|
|
address := cfg.CLI.Address
|
2021-10-29 19:15:52 +02:00
|
|
|
|
|
|
|
// If the address is not set, we assume that we are on the server hosting headscale.
|
|
|
|
if address == "" {
|
2021-10-30 16:08:16 +02:00
|
|
|
log.Debug().
|
|
|
|
Str("socket", cfg.UnixSocket).
|
2021-11-07 10:41:14 +01:00
|
|
|
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
|
2021-10-29 19:15:52 +02:00
|
|
|
|
2021-10-30 16:08:16 +02:00
|
|
|
address = cfg.UnixSocket
|
2021-10-29 19:15:52 +02:00
|
|
|
|
2022-10-31 10:37:42 +01:00
|
|
|
// Try to give the user better feedback if we cannot write to the headscale
|
|
|
|
// socket.
|
2022-10-31 10:41:34 +01:00
|
|
|
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions)
|
2022-10-31 10:37:42 +01:00
|
|
|
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()
|
|
|
|
|
2021-10-30 16:08:16 +02:00
|
|
|
grpcOptions = append(
|
|
|
|
grpcOptions,
|
2022-02-12 20:08:33 +01:00
|
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
2021-10-30 16:29:03 +02:00
|
|
|
grpc.WithContextDialer(headscale.GrpcSocketDialer),
|
2021-10-30 16:08:16 +02:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
// If we are not connecting to a local server, require an API key for authentication
|
2021-11-07 10:41:14 +01:00
|
|
|
apiKey := cfg.CLI.APIKey
|
2021-10-29 19:15:52 +02:00
|
|
|
if apiKey == "" {
|
2022-01-25 23:11:15 +01:00
|
|
|
log.Fatal().Caller().Msgf("HEADSCALE_CLI_API_KEY environment variable needs to be set.")
|
2021-10-29 19:15:52 +02:00
|
|
|
}
|
|
|
|
grpcOptions = append(grpcOptions,
|
|
|
|
grpc.WithPerRPCCredentials(tokenAuth{
|
|
|
|
token: apiKey,
|
|
|
|
}),
|
|
|
|
)
|
2022-02-13 09:41:49 +01:00
|
|
|
|
|
|
|
if cfg.CLI.Insecure {
|
|
|
|
tlsConfig := &tls.Config{
|
2022-02-13 09:46:35 +01:00
|
|
|
// turn of gosec as we are intentionally setting
|
|
|
|
// insecure.
|
|
|
|
//nolint:gosec
|
2022-02-13 09:41:49 +01:00
|
|
|
InsecureSkipVerify: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
grpcOptions = append(grpcOptions,
|
|
|
|
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
grpcOptions = append(grpcOptions,
|
|
|
|
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
|
|
|
|
)
|
|
|
|
}
|
2021-10-29 19:15:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
log.Trace().Caller().Str("address", address).Msg("Connecting via gRPC")
|
2021-10-29 19:36:11 +02:00
|
|
|
conn, err := grpc.DialContext(ctx, address, grpcOptions...)
|
2021-10-29 19:15:52 +02:00
|
|
|
if err != nil {
|
2022-01-25 23:11:15 +01:00
|
|
|
log.Fatal().Caller().Err(err).Msgf("Could not connect: %v", err)
|
2022-06-26 09:52:11 +02:00
|
|
|
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
|
2021-10-29 19:15:52 +02:00
|
|
|
}
|
|
|
|
|
2021-11-04 23:31:47 +01:00
|
|
|
client := v1.NewHeadscaleServiceClient(conn)
|
2021-10-29 19:15:52 +02:00
|
|
|
|
2021-11-07 10:41:14 +01:00
|
|
|
return ctx, client, conn, cancel
|
2021-10-29 19:15:52 +02:00
|
|
|
}
|
|
|
|
|
2021-11-04 23:31:47 +01:00
|
|
|
func SuccessOutput(result interface{}, override string, outputFormat string) {
|
2022-02-12 20:08:41 +01:00
|
|
|
var jsonBytes []byte
|
2021-05-08 13:28:22 +02:00
|
|
|
var err error
|
|
|
|
switch outputFormat {
|
|
|
|
case "json":
|
2022-02-12 20:08:41 +01:00
|
|
|
jsonBytes, err = json.MarshalIndent(result, "", "\t")
|
2021-11-04 23:31:47 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err)
|
2021-05-08 13:28:22 +02:00
|
|
|
}
|
|
|
|
case "json-line":
|
2022-02-12 20:08:41 +01:00
|
|
|
jsonBytes, err = json.Marshal(result)
|
2021-11-04 23:31:47 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err)
|
|
|
|
}
|
|
|
|
case "yaml":
|
2022-02-12 20:08:41 +01:00
|
|
|
jsonBytes, err = yaml.Marshal(result)
|
2021-11-04 23:31:47 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err)
|
2021-05-08 13:28:22 +02:00
|
|
|
}
|
2021-11-04 23:31:47 +01:00
|
|
|
default:
|
2021-11-15 19:36:02 +01:00
|
|
|
//nolint
|
2021-11-04 23:31:47 +01:00
|
|
|
fmt.Println(override)
|
2021-11-14 16:46:09 +01:00
|
|
|
|
2021-11-04 23:31:47 +01:00
|
|
|
return
|
2021-05-08 13:28:22 +02:00
|
|
|
}
|
2021-11-04 23:31:47 +01:00
|
|
|
|
2021-11-15 19:36:02 +01:00
|
|
|
//nolint
|
2022-02-12 20:08:41 +01:00
|
|
|
fmt.Println(string(jsonBytes))
|
2021-05-08 13:28:22 +02:00
|
|
|
}
|
2021-10-13 00:18:55 +02:00
|
|
|
|
2021-11-04 23:31:47 +01:00
|
|
|
func ErrorOutput(errResult error, override string, outputFormat string) {
|
|
|
|
type errOutput struct {
|
|
|
|
Error string `json:"error"`
|
|
|
|
}
|
|
|
|
|
|
|
|
SuccessOutput(errOutput{errResult.Error()}, override, outputFormat)
|
|
|
|
}
|
|
|
|
|
|
|
|
func HasMachineOutputFlag() bool {
|
2021-10-13 00:18:55 +02:00
|
|
|
for _, arg := range os.Args {
|
2021-11-04 23:31:47 +01:00
|
|
|
if arg == "json" || arg == "json-line" || arg == "yaml" {
|
2021-10-13 00:18:55 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2021-11-14 16:46:09 +01:00
|
|
|
|
2021-10-13 00:18:55 +02:00
|
|
|
return false
|
|
|
|
}
|
2021-10-29 19:08:21 +02:00
|
|
|
|
|
|
|
type tokenAuth struct {
|
|
|
|
token string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return value is mapped to request headers.
|
2021-11-13 09:36:45 +01:00
|
|
|
func (t tokenAuth) GetRequestMetadata(
|
|
|
|
ctx context.Context,
|
|
|
|
in ...string,
|
|
|
|
) (map[string]string, error) {
|
2021-10-29 19:08:21 +02:00
|
|
|
return map[string]string{
|
|
|
|
"authorization": "Bearer " + t.token,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tokenAuth) RequireTransportSecurity() bool {
|
2022-02-12 20:35:55 +01:00
|
|
|
return true
|
2021-10-29 19:08:21 +02:00
|
|
|
}
|
2021-10-31 10:40:43 +01:00
|
|
|
|
2022-04-25 22:33:53 +02:00
|
|
|
func contains[T string](ts []T, t T) bool {
|
2022-04-25 21:50:40 +02:00
|
|
|
for _, v := range ts {
|
2022-04-25 22:27:44 +02:00
|
|
|
if reflect.DeepEqual(v, t) {
|
2022-04-21 23:43:20 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|