mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-26 10:03:47 +01:00
64dbd5fbfc
avoid sending admin or management URLs on service start as it doesn't have an input Parse management and admin URL when needed Pass empty admin url on commands to prevent default overwrite
367 lines
12 KiB
Go
367 lines
12 KiB
Go
package internal
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
|
|
"github.com/netbirdio/netbird/client/ssh"
|
|
"github.com/netbirdio/netbird/iface"
|
|
mgm "github.com/netbirdio/netbird/management/client"
|
|
"github.com/netbirdio/netbird/util"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
const (
|
|
// ManagementLegacyPort is the port that was used before by the Management gRPC server.
|
|
// It is used for backward compatibility now.
|
|
// NB: hardcoded from github.com/netbirdio/netbird/management/cmd to avoid import
|
|
ManagementLegacyPort = 33073
|
|
// DefaultManagementURL points to the NetBird's cloud management endpoint
|
|
DefaultManagementURL = "https://api.wiretrustee.com:443"
|
|
// DefaultAdminURL points to NetBird's cloud management console
|
|
DefaultAdminURL = "https://app.netbird.io:443"
|
|
)
|
|
|
|
var defaultInterfaceBlacklist = []string{iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
|
|
"Tailscale", "tailscale", "docker", "veth", "br-"}
|
|
|
|
// ConfigInput carries configuration changes to the client
|
|
type ConfigInput struct {
|
|
ManagementURL string
|
|
AdminURL string
|
|
ConfigPath string
|
|
PreSharedKey *string
|
|
NATExternalIPs []string
|
|
CustomDNSAddress []byte
|
|
}
|
|
|
|
// Config Configuration type
|
|
type Config struct {
|
|
// Wireguard private key of local peer
|
|
PrivateKey string
|
|
PreSharedKey string
|
|
ManagementURL *url.URL
|
|
AdminURL *url.URL
|
|
WgIface string
|
|
WgPort int
|
|
IFaceBlackList []string
|
|
DisableIPv6Discovery bool
|
|
// SSHKey is a private SSH key in a PEM format
|
|
SSHKey string
|
|
|
|
// ExternalIP mappings, if different than the host interface IP
|
|
//
|
|
// External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP
|
|
// to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT
|
|
// mapping ExternalIP to host interface IP, or a NAT DMZ to host interface IP.
|
|
//
|
|
// A single mapping will take the form of: external[/internal]
|
|
// external (required): either the external IP address or "stun" to use STUN to determine the external IP address
|
|
// internal (optional): either the internal/interface IP address or an interface name
|
|
//
|
|
// examples:
|
|
// "12.34.56.78" => all interfaces IPs will be mapped to external IP of 12.34.56.78
|
|
// "12.34.56.78/eth0" => IPv4 assigned to interface eth0 will be mapped to external IP of 12.34.56.78
|
|
// "12.34.56.78/10.1.2.3" => interface IP 10.1.2.3 will be mapped to external IP of 12.34.56.78
|
|
|
|
NATExternalIPs []string
|
|
// CustomDNSAddress sets the DNS resolver listening address in format ip:port
|
|
CustomDNSAddress string
|
|
}
|
|
|
|
// createNewConfig creates a new config generating a new Wireguard key and saving to file
|
|
func createNewConfig(input ConfigInput) (*Config, error) {
|
|
wgKey := generateKey()
|
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config := &Config{
|
|
SSHKey: string(pem),
|
|
PrivateKey: wgKey,
|
|
WgIface: iface.WgInterfaceDefault,
|
|
WgPort: iface.DefaultWgPort,
|
|
IFaceBlackList: []string{},
|
|
DisableIPv6Discovery: false,
|
|
NATExternalIPs: input.NATExternalIPs,
|
|
CustomDNSAddress: string(input.CustomDNSAddress),
|
|
}
|
|
|
|
defaultManagementURL, err := ParseURL("Management URL", DefaultManagementURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config.ManagementURL = defaultManagementURL
|
|
if input.ManagementURL != "" {
|
|
URL, err := ParseURL("Management URL", input.ManagementURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.ManagementURL = URL
|
|
}
|
|
|
|
if input.PreSharedKey != nil {
|
|
config.PreSharedKey = *input.PreSharedKey
|
|
}
|
|
|
|
defaultAdminURL, err := ParseURL("Admin URL", DefaultAdminURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config.AdminURL = defaultAdminURL
|
|
if input.AdminURL != "" {
|
|
newURL, err := ParseURL("Admin Panel URL", input.AdminURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.AdminURL = newURL
|
|
}
|
|
|
|
config.IFaceBlackList = defaultInterfaceBlacklist
|
|
|
|
err = util.WriteJson(input.ConfigPath, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// ParseURL parses and validates a service URL
|
|
func ParseURL(serviceName, serviceURL string) (*url.URL, error) {
|
|
parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
|
|
if err != nil {
|
|
log.Errorf("failed parsing %s URL %s: [%s]", serviceName, serviceURL, err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
if parsedMgmtURL.Scheme != "https" && parsedMgmtURL.Scheme != "http" {
|
|
return nil, fmt.Errorf(
|
|
"invalid %s URL provided %s. Supported format [http|https]://[host]:[port]",
|
|
serviceName, serviceURL)
|
|
}
|
|
|
|
if parsedMgmtURL.Port() == "" {
|
|
switch parsedMgmtURL.Scheme {
|
|
case "https":
|
|
parsedMgmtURL.Host = parsedMgmtURL.Host + ":443"
|
|
case "http":
|
|
parsedMgmtURL.Host = parsedMgmtURL.Host + ":80"
|
|
default:
|
|
log.Infof("unable to determine a default port for schema %s in URL %s", parsedMgmtURL.Scheme, serviceURL)
|
|
}
|
|
}
|
|
|
|
return parsedMgmtURL, err
|
|
}
|
|
|
|
// ReadConfig reads existing configuration and update settings according to input configuration
|
|
func ReadConfig(input ConfigInput) (*Config, error) {
|
|
config := &Config{}
|
|
if _, err := os.Stat(input.ConfigPath); os.IsNotExist(err) {
|
|
return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
|
|
}
|
|
|
|
if _, err := util.ReadJson(input.ConfigPath, config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
refresh := false
|
|
|
|
if input.ManagementURL != "" && config.ManagementURL.String() != input.ManagementURL {
|
|
log.Infof("new Management URL provided, updated to %s (old value %s)",
|
|
input.ManagementURL, config.ManagementURL)
|
|
newURL, err := ParseURL("Management URL", input.ManagementURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.ManagementURL = newURL
|
|
refresh = true
|
|
}
|
|
|
|
if input.AdminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != input.AdminURL) {
|
|
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
|
|
input.AdminURL, config.AdminURL)
|
|
newURL, err := ParseURL("Admin Panel URL", input.AdminURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.AdminURL = newURL
|
|
refresh = true
|
|
}
|
|
|
|
if input.PreSharedKey != nil && config.PreSharedKey != *input.PreSharedKey {
|
|
log.Infof("new pre-shared key provided, updated to %s (old value %s)",
|
|
*input.PreSharedKey, config.PreSharedKey)
|
|
config.PreSharedKey = *input.PreSharedKey
|
|
refresh = true
|
|
}
|
|
|
|
if config.SSHKey == "" {
|
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.SSHKey = string(pem)
|
|
refresh = true
|
|
}
|
|
|
|
if config.WgPort == 0 {
|
|
config.WgPort = iface.DefaultWgPort
|
|
refresh = true
|
|
}
|
|
if input.NATExternalIPs != nil && len(config.NATExternalIPs) != len(input.NATExternalIPs) {
|
|
config.NATExternalIPs = input.NATExternalIPs
|
|
refresh = true
|
|
}
|
|
|
|
if input.CustomDNSAddress != nil {
|
|
config.CustomDNSAddress = string(input.CustomDNSAddress)
|
|
refresh = true
|
|
}
|
|
|
|
if refresh {
|
|
// since we have new management URL, we need to update config file
|
|
if err := util.WriteJson(input.ConfigPath, config); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// GetConfig reads existing config or generates a new one
|
|
func GetConfig(input ConfigInput) (*Config, error) {
|
|
if _, err := os.Stat(input.ConfigPath); os.IsNotExist(err) {
|
|
log.Infof("generating new config %s", input.ConfigPath)
|
|
return createNewConfig(input)
|
|
} else {
|
|
// don't overwrite pre-shared key if we receive asterisks from UI
|
|
if *input.PreSharedKey == "**********" {
|
|
input.PreSharedKey = nil
|
|
}
|
|
return ReadConfig(input)
|
|
}
|
|
}
|
|
|
|
// generateKey generates a new Wireguard private key
|
|
func generateKey() string {
|
|
key, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return key.String()
|
|
}
|
|
|
|
// DeviceAuthorizationFlow represents Device Authorization Flow information
|
|
type DeviceAuthorizationFlow struct {
|
|
Provider string
|
|
ProviderConfig ProviderConfig
|
|
}
|
|
|
|
// ProviderConfig has all attributes needed to initiate a device authorization flow
|
|
type ProviderConfig struct {
|
|
// ClientID An IDP application client id
|
|
ClientID string
|
|
// ClientSecret An IDP application client secret
|
|
ClientSecret string
|
|
// Domain An IDP API domain
|
|
// Deprecated. Use OIDCConfigEndpoint instead
|
|
Domain string
|
|
// Audience An Audience for to authorization validation
|
|
Audience string
|
|
// TokenEndpoint is the endpoint of an IDP manager where clients can obtain access token
|
|
TokenEndpoint string
|
|
// DeviceAuthEndpoint is the endpoint of an IDP manager where clients can obtain device authorization code
|
|
DeviceAuthEndpoint string
|
|
}
|
|
|
|
func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (DeviceAuthorizationFlow, error) {
|
|
// validate our peer's Wireguard PRIVATE key
|
|
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
|
if err != nil {
|
|
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
|
|
var mgmTlsEnabled bool
|
|
if config.ManagementURL.Scheme == "https" {
|
|
mgmTlsEnabled = true
|
|
}
|
|
|
|
log.Debugf("connecting to Management Service %s", config.ManagementURL.String())
|
|
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
|
if err != nil {
|
|
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
log.Debugf("connected to the Management service %s", config.ManagementURL.String())
|
|
defer func() {
|
|
err = mgmClient.Close()
|
|
if err != nil {
|
|
log.Warnf("failed to close the Management service client %v", err)
|
|
}
|
|
}()
|
|
|
|
serverKey, err := mgmClient.GetServerPublicKey()
|
|
if err != nil {
|
|
log.Errorf("failed while getting Management Service public key: %v", err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
|
|
protoDeviceAuthorizationFlow, err := mgmClient.GetDeviceAuthorizationFlow(*serverKey)
|
|
if err != nil {
|
|
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
|
log.Warnf("server couldn't find device flow, contact admin: %v", err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
} else {
|
|
log.Errorf("failed to retrieve device flow: %v", err)
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
}
|
|
|
|
deviceAuthorizationFlow := DeviceAuthorizationFlow{
|
|
Provider: protoDeviceAuthorizationFlow.Provider.String(),
|
|
|
|
ProviderConfig: ProviderConfig{
|
|
Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(),
|
|
ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(),
|
|
ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(),
|
|
Domain: protoDeviceAuthorizationFlow.GetProviderConfig().Domain,
|
|
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
|
|
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
|
|
},
|
|
}
|
|
|
|
err = isProviderConfigValid(deviceAuthorizationFlow.ProviderConfig)
|
|
if err != nil {
|
|
return DeviceAuthorizationFlow{}, err
|
|
}
|
|
|
|
return deviceAuthorizationFlow, nil
|
|
}
|
|
|
|
func isProviderConfigValid(config ProviderConfig) error {
|
|
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
|
|
if config.Audience == "" {
|
|
return fmt.Errorf(errorMSGFormat, "Audience")
|
|
}
|
|
if config.ClientID == "" {
|
|
return fmt.Errorf(errorMSGFormat, "Client ID")
|
|
}
|
|
if config.TokenEndpoint == "" {
|
|
return fmt.Errorf(errorMSGFormat, "Token Endpoint")
|
|
}
|
|
if config.DeviceAuthEndpoint == "" {
|
|
return fmt.Errorf(errorMSGFormat, "Device Auth Endpoint")
|
|
}
|
|
return nil
|
|
}
|