mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-18 20:49:56 +00:00
440 lines
12 KiB
Go
440 lines
12 KiB
Go
//go:build !ios && !android
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/kardianos/service"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/grpc"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/shell"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
"github.com/netbirdio/netbird/client/server"
|
|
"github.com/netbirdio/netbird/client/system"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
func (p *program) Start(svc service.Service) error {
|
|
// Start should not block. Do the actual work async.
|
|
log.Info("starting NetBird service") //nolint
|
|
|
|
// Collect static system and platform information
|
|
system.UpdateStaticInfoAsync()
|
|
|
|
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
|
p.serv = grpc.NewServer()
|
|
|
|
split := strings.Split(daemonAddr, "://")
|
|
switch split[0] {
|
|
case "unix":
|
|
// cleanup failed close
|
|
stat, err := os.Stat(split[1])
|
|
if err == nil && !stat.IsDir() {
|
|
if err := os.Remove(split[1]); err != nil {
|
|
log.Debugf("remove socket file: %v", err)
|
|
}
|
|
}
|
|
case "tcp":
|
|
default:
|
|
return fmt.Errorf("unsupported daemon address protocol: %v", split[0])
|
|
}
|
|
|
|
listen, err := net.Listen(split[0], split[1])
|
|
if err != nil {
|
|
return fmt.Errorf("listen daemon interface: %w", err)
|
|
}
|
|
go func() {
|
|
defer listen.Close()
|
|
|
|
srvListener := listen
|
|
if split[0] == "unix" {
|
|
owner := effectiveSocketOwner()
|
|
switch {
|
|
case strictSocketDisabled:
|
|
// Opt-out (root-only, via service.json): leave it world-writable.
|
|
if err := os.Chmod(split[1], 0666); err != nil {
|
|
log.Errorf("failed setting daemon permissions: %v", split[1])
|
|
return
|
|
}
|
|
case owner != "":
|
|
// Seeded owner (flag, MDM, or persisted TOFU result): restrict
|
|
// before serving so there is no open window.
|
|
uid, err := lookupUser(owner)
|
|
if err != nil {
|
|
log.Errorf("lookup socket owner %q: %v", owner, err)
|
|
return
|
|
}
|
|
if err := restrictSocket(split[1], uid); err != nil {
|
|
log.Errorf("restrict socket to %q: %v", owner, err)
|
|
return
|
|
}
|
|
default:
|
|
// Trust-on-first-use: open the socket now; tofuListener locks it
|
|
// to the first caller's uid on the first connection.
|
|
if err := os.Chmod(split[1], 0666); err != nil {
|
|
log.Errorf("failed setting daemon permissions: %v", split[1])
|
|
return
|
|
}
|
|
srvListener = &tofuListener{Listener: listen, path: split[1], owner: -1}
|
|
}
|
|
}
|
|
|
|
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, captureEnabled, networksDisabled)
|
|
if err := serverInstance.Start(); err != nil {
|
|
log.Fatalf("failed to start daemon: %v", err)
|
|
}
|
|
proto.RegisterDaemonServiceServer(p.serv, serverInstance)
|
|
|
|
p.serverInstanceMu.Lock()
|
|
p.serverInstance = serverInstance
|
|
p.serverInstanceMu.Unlock()
|
|
|
|
log.Printf("started daemon server: %v", split[1])
|
|
if err := p.serv.Serve(srvListener); err != nil {
|
|
log.Errorf("failed to serve daemon requests: %v", err)
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func lookupUser(username string) (int, error) {
|
|
u, err := shell.LookupWithGetent(username)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("lookup user %s: %w", username, err)
|
|
}
|
|
uid, err := strconv.Atoi(u.Uid)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("parse uid %s: %w", u.Uid, err)
|
|
}
|
|
return uid, nil
|
|
}
|
|
|
|
// addGroup creates a system group if it doesn't already exist and returns the gid.
|
|
// Must run as root.
|
|
func addGroup(name string) (int, error) {
|
|
group, err := shell.LookupGroupWithGetent(name)
|
|
if err == nil {
|
|
gid, err := strconv.ParseInt(group.Gid, 10, 64)
|
|
return int(gid), err
|
|
}
|
|
|
|
// looup failed, create the group
|
|
groupadd, err := exec.LookPath("groupadd")
|
|
if err != nil {
|
|
// Fallback for Alpine/BusyBox systems.
|
|
if groupadd, err = exec.LookPath("addgroup"); err != nil {
|
|
return -1, errors.New("neither groupadd nor addgroup found")
|
|
}
|
|
}
|
|
|
|
// Use --system for a service/daemon group (no login, low GID).
|
|
out, err := exec.Command(groupadd, "--system", name).CombinedOutput()
|
|
if err != nil {
|
|
return -1, fmt.Errorf("create group %q: %w: %s", name, err, out)
|
|
}
|
|
if group, err := shell.LookupWithGetent(name); err == nil {
|
|
gid, err := strconv.ParseInt(group.Gid, 10, 64)
|
|
return int(gid), err
|
|
}
|
|
return -1, fmt.Errorf("lookup group %q: %w", name, err)
|
|
}
|
|
|
|
// restrictSocket locks the unix socket down to the owner uid plus the netbird
|
|
// group (0660). If the group cannot be created or applied, it fails closed to
|
|
// owner-only 0600 — it never leaves the socket world-writable.
|
|
func restrictSocket(path string, uid int) error {
|
|
// TODO: introduce flag to configure this (LDAP/AD usecase)
|
|
gid, err := addGroup("netbird")
|
|
if err != nil {
|
|
log.Errorf("create netbird group, failing closed to owner-only 0600: %v", err)
|
|
return chownChmod(path, uid, -1, 0600)
|
|
}
|
|
if err := chownChmod(path, uid, gid, 0660); err != nil {
|
|
log.Errorf("apply netbird group to socket, failing closed to owner-only 0600: %v", err)
|
|
return chownChmod(path, uid, -1, 0600)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// chownChmod sets ownership and mode on the socket. A gid of -1 leaves the
|
|
// group unchanged.
|
|
func chownChmod(path string, uid, gid int, mode os.FileMode) error {
|
|
if err := os.Chown(path, uid, gid); err != nil {
|
|
return fmt.Errorf("chown socket %s: %w", path, err)
|
|
}
|
|
if err := os.Chmod(path, mode); err != nil {
|
|
return fmt.Errorf("chmod socket %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// tofuListener implements trust-on-first-use for the daemon control socket.
|
|
// The socket starts world-writable; the first caller's uid (read via the
|
|
// platform peer-credential mechanism) becomes the owner. On that first
|
|
// connection the socket is restricted (see restrictSocket) and the owner is
|
|
// persisted so the open window never reopens on later starts. Connections that
|
|
// raced in during the open window and are neither the owner nor root are
|
|
// dropped. Changing the socket mode does not disturb the already-open
|
|
// connection, so the first caller's request is served normally.
|
|
type tofuListener struct {
|
|
net.Listener
|
|
path string
|
|
mu sync.Mutex
|
|
owner int // -1 until claimed
|
|
}
|
|
|
|
func (l *tofuListener) Accept() (net.Conn, error) {
|
|
for {
|
|
c, err := l.Listener.Accept()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uid, err := peerUID(c)
|
|
if err != nil {
|
|
log.Errorf("read peer credentials, dropping connection: %v", err)
|
|
_ = c.Close()
|
|
continue
|
|
}
|
|
|
|
l.mu.Lock()
|
|
if l.owner == -1 {
|
|
if err := restrictSocket(l.path, uid); err != nil {
|
|
l.mu.Unlock()
|
|
_ = c.Close()
|
|
// Refuse to serve on a socket we could not lock down.
|
|
return nil, fmt.Errorf("restrict socket on first connection: %w", err)
|
|
}
|
|
l.owner = uid
|
|
persistSocketOwner(uid)
|
|
log.Infof("control socket restricted to first caller (uid %d)", uid)
|
|
l.mu.Unlock()
|
|
return c, nil
|
|
}
|
|
owner := l.owner
|
|
l.mu.Unlock()
|
|
|
|
// New connects are already gated by the 0660 perms set above; this only
|
|
// drops anything that slipped in during the brief open window.
|
|
if uid != owner && uid != 0 {
|
|
log.Warnf("dropping non-owner connection (uid %d) during socket bootstrap", uid)
|
|
_ = c.Close()
|
|
continue
|
|
}
|
|
return c, nil
|
|
}
|
|
}
|
|
|
|
// effectiveSocketOwner returns the configured socket owner: the --socket-owner
|
|
// flag when set, otherwise the owner persisted by a previous TOFU migration.
|
|
func effectiveSocketOwner() string {
|
|
if socketOwner != "" {
|
|
return socketOwner
|
|
}
|
|
params, err := loadServiceParams()
|
|
if err != nil {
|
|
log.Errorf("load service params for socket owner: %v", err)
|
|
return ""
|
|
}
|
|
if params != nil {
|
|
return params.SocketOwner
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// persistSocketOwner records the TOFU-selected owner (by username) so the next
|
|
// daemon start restricts the socket immediately, with no open window.
|
|
func persistSocketOwner(uid int) {
|
|
u, err := user.LookupId(strconv.Itoa(uid))
|
|
if err != nil {
|
|
log.Errorf("resolve uid %d to username for persistence: %v", uid, err)
|
|
return
|
|
}
|
|
params, err := loadServiceParams()
|
|
if err != nil {
|
|
log.Errorf("load service params to persist socket owner: %v", err)
|
|
return
|
|
}
|
|
if params == nil {
|
|
params = currentServiceParams()
|
|
}
|
|
params.SocketOwner = u.Username
|
|
if err := saveServiceParams(params); err != nil {
|
|
log.Errorf("persist socket owner: %v", err)
|
|
}
|
|
}
|
|
|
|
func (p *program) Stop(srv service.Service) error {
|
|
p.serverInstanceMu.Lock()
|
|
if p.serverInstance != nil {
|
|
in := new(proto.DownRequest)
|
|
_, err := p.serverInstance.Down(p.ctx, in)
|
|
if err != nil {
|
|
log.Errorf("failed to stop daemon: %v", err)
|
|
}
|
|
}
|
|
p.serverInstanceMu.Unlock()
|
|
|
|
p.cancel()
|
|
|
|
if p.serv != nil {
|
|
p.serv.Stop()
|
|
}
|
|
|
|
time.Sleep(time.Second * 2)
|
|
log.Info("stopped NetBird service") //nolint
|
|
return nil
|
|
}
|
|
|
|
// Common setup for service control commands
|
|
func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc, consoleLog bool) (service.Service, error) {
|
|
// rootCmd env vars are already applied by PersistentPreRunE.
|
|
SetFlagsFromEnvVars(serviceCmd)
|
|
|
|
cmd.SetOut(cmd.OutOrStdout())
|
|
|
|
if err := handleRebrand(cmd); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if consoleLog {
|
|
if err := util.InitLog(logLevel, util.LogConsole); err != nil {
|
|
return nil, fmt.Errorf("init log: %w", err)
|
|
}
|
|
} else {
|
|
if err := util.InitLog(logLevel, logFiles...); err != nil {
|
|
return nil, fmt.Errorf("init log: %w", err)
|
|
}
|
|
}
|
|
|
|
cfg, err := newSVCConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create service config: %w", err)
|
|
}
|
|
|
|
s, err := newSVC(newProgram(ctx, cancel), cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
var runCmd = &cobra.Command{
|
|
Use: "run",
|
|
Short: "runs NetBird as service",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
|
|
SetupCloseHandler(ctx, cancel)
|
|
SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))
|
|
|
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.Run()
|
|
},
|
|
}
|
|
|
|
var startCmd = &cobra.Command{
|
|
Use: "start",
|
|
Short: "starts NetBird service",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.Start(); err != nil {
|
|
return fmt.Errorf("start service: %w", err)
|
|
}
|
|
cmd.Println("NetBird service has been started")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var stopCmd = &cobra.Command{
|
|
Use: "stop",
|
|
Short: "stops NetBird service",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.Stop(); err != nil {
|
|
return fmt.Errorf("stop service: %w", err)
|
|
}
|
|
cmd.Println("NetBird service has been stopped")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var restartCmd = &cobra.Command{
|
|
Use: "restart",
|
|
Short: "restarts NetBird service",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.Restart(); err != nil {
|
|
return fmt.Errorf("restart service: %w", err)
|
|
}
|
|
cmd.Println("NetBird service has been restarted")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var svcStatusCmd = &cobra.Command{
|
|
Use: "status",
|
|
Short: "shows NetBird service status",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
s, err := setupServiceControlCommand(cmd, ctx, cancel, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
status, err := s.Status()
|
|
if err != nil {
|
|
return fmt.Errorf("get service status: %w", err)
|
|
}
|
|
|
|
var statusText string
|
|
switch status {
|
|
case service.StatusRunning:
|
|
statusText = "Running"
|
|
case service.StatusStopped:
|
|
statusText = "Stopped"
|
|
case service.StatusUnknown:
|
|
statusText = "Unknown"
|
|
default:
|
|
statusText = fmt.Sprintf("Unknown (%d)", status)
|
|
}
|
|
|
|
cmd.Printf("NetBird service status: %s\n", statusText)
|
|
return nil
|
|
},
|
|
}
|