Skip to content

Commit

Permalink
NetBird SSH (#361)
Browse files Browse the repository at this point in the history
This PR adds support for SSH access through the NetBird network
without managing SSH skeys.
NetBird client app has an embedded SSH server (Linux/Mac only) 
and a netbird ssh command.
  • Loading branch information
braginini authored Jun 23, 2022
1 parent f883a10 commit 06860c4
Show file tree
Hide file tree
Showing 32 changed files with 1,702 additions and 349 deletions.
1 change: 1 addition & 0 deletions client/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func init() {
rootCmd.AddCommand(statusCmd)
rootCmd.AddCommand(loginCmd)
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(sshCmd)
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
}
Expand Down
137 changes: 137 additions & 0 deletions client/cmd/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package cmd

import (
"context"
"errors"
"fmt"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
nbssh "github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/util"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"os"
"os/signal"
"strings"
"syscall"
)

var (
port int
user = "netbird"
host string
)

var sshCmd = &cobra.Command{
Use: "ssh",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires a host argument")
}

split := strings.Split(args[0], "@")
if len(split) == 2 {
user = split[0]
host = split[1]
} else {
host = args[0]
}

return nil
},
Short: "connect to a remote SSH server",
RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars()

cmd.SetOut(cmd.OutOrStdout())

err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}

if !util.IsAdmin() {
cmd.Printf("error: you must have Administrator privileges to run this command\n")
return nil
}

ctx := internal.CtxInitState(cmd.Context())

conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}

defer func() {
err := conn.Close()
if err != nil {
log.Warnf("failed closing dameon gRPC client connection %v", err)
return
}
}()
client := proto.NewDaemonServiceClient(conn)

status, err := client.Status(ctx, &proto.StatusRequest{})
if err != nil {
return fmt.Errorf("unable to get daemon status: %v", err)
}

if status.Status != string(internal.StatusConnected) {
// todo maybe automatically start it?
cmd.Printf("You are disconnected from the NetBird network. Please run the UP command first to connect: \n\n" +
" netbird up \n\n")
return nil
}

config, err := internal.ReadConfig("", "", configPath, nil)
if err != nil {
return err
}

sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
sshctx, cancel := context.WithCancel(ctx)

go func() {
if err := runSSH(sshctx, host, []byte(config.SSHKey)); err != nil {
log.Print(err)
}
cancel()
}()

select {
case <-sig:
cancel()
case <-sshctx.Done():
}

return nil
},
}

func runSSH(ctx context.Context, addr string, pemKey []byte) error {
c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), user, pemKey)
if err != nil {
return err
}
go func() {
<-ctx.Done()
err = c.Close()
if err != nil {
return
}
}()

err = c.OpenTerminal()
if err != nil {
return err
}

return nil
}

func init() {
sshCmd.PersistentFlags().IntVarP(&port, "port", "p", nbssh.DefaultSSHPort, "Sets remote SSH port. Defaults to "+fmt.Sprint(nbssh.DefaultSSHPort))
}
28 changes: 21 additions & 7 deletions client/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ package internal
import (
"context"
"fmt"
"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"
"net/url"
"os"

"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/util"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

var managementURLDefault *url.URL
Expand All @@ -38,12 +38,18 @@ type Config struct {
AdminURL *url.URL
WgIface string
IFaceBlackList []string
// SSHKey is a private SSH key in a PEM format
SSHKey string
}

// createNewConfig creates a new config generating a new Wireguard key and saving to file
func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) {
wgKey := generateKey()
config := &Config{PrivateKey: wgKey, WgIface: iface.WgInterfaceDefault, IFaceBlackList: []string{}}
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
if err != nil {
return nil, err
}
config := &Config{SSHKey: string(pem), PrivateKey: wgKey, WgIface: iface.WgInterfaceDefault, IFaceBlackList: []string{}}
if managementURL != "" {
URL, err := parseURL("Management URL", managementURL)
if err != nil {
Expand All @@ -61,7 +67,7 @@ func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (
config.IFaceBlackList = []string{iface.WgInterfaceDefault, "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
"Tailscale", "tailscale"}

err := util.WriteJson(configPath, config)
err = util.WriteJson(configPath, config)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -126,6 +132,14 @@ func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string
config.PreSharedKey = *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 refresh {
// since we have new management URL, we need to update config file
Expand Down
13 changes: 10 additions & 3 deletions client/internal/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package internal

import (
"context"
"github.com/netbirdio/netbird/client/ssh"
"time"

"github.com/netbirdio/netbird/client/system"
Expand Down Expand Up @@ -63,8 +64,13 @@ func RunClient(ctx context.Context, config *Config) error {
engineCtx, cancel := context.WithCancel(ctx)
defer cancel()

publicSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
if err != nil {
return err
}
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config
mgmClient, loginResp, err := connectToManagement(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
mgmClient, loginResp, err := connectToManagement(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled,
publicSSHKey)
if err != nil {
log.Debug(err)
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
Expand Down Expand Up @@ -147,6 +153,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
IFaceBlackList: config.IFaceBlackList,
WgPrivateKey: key,
WgPort: iface.DefaultWgPort,
SSHKey: []byte(config.SSHKey),
}

if config.PreSharedKey != "" {
Expand Down Expand Up @@ -179,7 +186,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig,
}

// connectToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
func connectToManagement(ctx context.Context, managementAddr string, ourPrivateKey wgtypes.Key, tlsEnabled bool) (*mgm.GrpcClient, *mgmProto.LoginResponse, error) {
func connectToManagement(ctx context.Context, managementAddr string, ourPrivateKey wgtypes.Key, tlsEnabled bool, pubSSHKey []byte) (*mgm.GrpcClient, *mgmProto.LoginResponse, error) {
log.Debugf("connecting to Management Service %s", managementAddr)
client, err := mgm.NewClient(ctx, managementAddr, ourPrivateKey, tlsEnabled)
if err != nil {
Expand All @@ -193,7 +200,7 @@ func connectToManagement(ctx context.Context, managementAddr string, ourPrivateK
}

sysInfo := system.GetInfo(ctx)
loginResp, err := client.Login(*serverPublicKey, sysInfo)
loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey)
if err != nil {
return nil, nil, err
}
Expand Down
Loading

0 comments on commit 06860c4

Please sign in to comment.