mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-01 10:18:42 -04:00
356 lines
11 KiB
Go
356 lines
11 KiB
Go
//go:build windows
|
|
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
var (
|
|
sasDLL = windows.NewLazySystemDLL("sas.dll")
|
|
procSendSAS = sasDLL.NewProc("SendSAS")
|
|
|
|
procConvertStringSecurityDescriptorToSecurityDescriptor = advapi32.NewProc("ConvertStringSecurityDescriptorToSecurityDescriptorW")
|
|
)
|
|
|
|
// sasSecurityAttributes builds a SECURITY_ATTRIBUTES granting access to the
|
|
// SYSTEM account only, so no unprivileged local process can trigger the Secure
|
|
// Attention Sequence. The caller owns the returned descriptor and must release
|
|
// it with freeSecurityDescriptor once the object it secures exists.
|
|
//
|
|
// SYSTEM alone is enough for both ends: the daemon creates and waits on the
|
|
// event as LocalSystem, and the only legitimate signaller is the session agent,
|
|
// which spawnAgentInSession starts with a SYSTEM token and which never drops
|
|
// privileges on Windows. Granting EVENT_MODIFY_STATE to interactive users would
|
|
// let any logged-in local user drive SendSAS.
|
|
func sasSecurityAttributes() (*windows.SecurityAttributes, error) {
|
|
sddl, err := windows.UTF16PtrFromString("D:(A;;GA;;;SY)")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var sd uintptr
|
|
r, _, lerr := procConvertStringSecurityDescriptorToSecurityDescriptor.Call(
|
|
uintptr(unsafe.Pointer(sddl)),
|
|
1, // SDDL_REVISION_1
|
|
uintptr(unsafe.Pointer(&sd)),
|
|
0,
|
|
)
|
|
if r == 0 {
|
|
return nil, lerr
|
|
}
|
|
return &windows.SecurityAttributes{
|
|
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
|
|
SecurityDescriptor: (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(sd)),
|
|
InheritHandle: 0,
|
|
}, nil
|
|
}
|
|
|
|
// freeSecurityDescriptor releases the descriptor
|
|
// ConvertStringSecurityDescriptorToSecurityDescriptorW allocated. Safe on a nil
|
|
// SecurityAttributes so callers can defer it before checking for an error.
|
|
func freeSecurityDescriptor(sa *windows.SecurityAttributes) {
|
|
if sa == nil || sa.SecurityDescriptor == nil {
|
|
return
|
|
}
|
|
if _, err := windows.LocalFree(windows.Handle(unsafe.Pointer(sa.SecurityDescriptor))); err != nil {
|
|
log.Debugf("free SAS security descriptor: %v", err)
|
|
}
|
|
sa.SecurityDescriptor = nil
|
|
}
|
|
|
|
// sasOriginalState tracks the SoftwareSASGeneration value present before we
|
|
// changed it, so disableSoftwareSAS can restore the machine to its prior
|
|
// state on shutdown instead of leaving the policy enabled.
|
|
type sasOriginalState struct {
|
|
had bool // true if the value existed before we wrote
|
|
value uint32 // its prior DWORD value, if had == true
|
|
// captured stays true once we have read the genuine pre-enable state
|
|
// for the first time, so a second enableSoftwareSAS call (e.g. after
|
|
// a daemon restart with no intervening disable) cannot overwrite the
|
|
// snapshot with our own forced value.
|
|
captured bool
|
|
}
|
|
|
|
var (
|
|
// sasStateMu guards savedSASState: enable and disable run from a Server's
|
|
// platformInit / platformShutdown, and nothing stops two Servers (or a
|
|
// restart overlapping a shutdown) from reaching it at once.
|
|
sasStateMu sync.Mutex
|
|
savedSASState sasOriginalState
|
|
)
|
|
|
|
// enableSoftwareSAS sets the SoftwareSASGeneration registry key to allow
|
|
// services to trigger the Secure Attention Sequence via SendSAS. Without this,
|
|
// SendSAS silently does nothing on most Windows editions. The original value
|
|
// is snapshotted so disableSoftwareSAS can put the system back as it was.
|
|
func enableSoftwareSAS() {
|
|
key, _, err := registry.CreateKey(
|
|
registry.LOCAL_MACHINE,
|
|
`SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System`,
|
|
registry.SET_VALUE|registry.QUERY_VALUE,
|
|
)
|
|
if err != nil {
|
|
log.Warnf("open SoftwareSASGeneration registry key: %v", err)
|
|
return
|
|
}
|
|
defer key.Close()
|
|
|
|
// Held across the read and the write: releasing in between would let a
|
|
// concurrent restore clear the snapshot we are about to force a value over,
|
|
// leaving the machine with our value and nothing recorded to put back.
|
|
sasStateMu.Lock()
|
|
defer sasStateMu.Unlock()
|
|
|
|
if !savedSASState.captured {
|
|
prev, _, err := key.GetIntegerValue("SoftwareSASGeneration")
|
|
switch {
|
|
case err == nil && prev > math.MaxUint32:
|
|
// A DWORD is what the policy takes, so a wider value is not ours to
|
|
// narrow and restore.
|
|
log.Warnf("SoftwareSASGeneration is %d, wider than a DWORD, leaving it alone", prev)
|
|
return
|
|
case err == nil:
|
|
savedSASState = sasOriginalState{had: true, value: uint32(prev), captured: true}
|
|
case errors.Is(err, registry.ErrNotExist):
|
|
savedSASState = sasOriginalState{had: false, captured: true}
|
|
default:
|
|
// Unreadable is not absent. Recording it as absent would have
|
|
// disableSoftwareSAS delete a value the administrator set, so leave
|
|
// the policy untouched instead.
|
|
log.Warnf("read SoftwareSASGeneration, leaving it alone: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := key.SetDWordValue("SoftwareSASGeneration", 1); err != nil {
|
|
log.Warnf("set SoftwareSASGeneration: %v", err)
|
|
return
|
|
}
|
|
log.Debug("SoftwareSASGeneration registry key set to 1 (services allowed)")
|
|
}
|
|
|
|
// disableSoftwareSAS restores the SoftwareSASGeneration value to its
|
|
// pre-enable state. Idempotent; safe to call when enableSoftwareSAS never ran.
|
|
func disableSoftwareSAS() {
|
|
key, err := registry.OpenKey(
|
|
registry.LOCAL_MACHINE,
|
|
`SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System`,
|
|
registry.SET_VALUE,
|
|
)
|
|
if err != nil {
|
|
log.Debugf("open SoftwareSASGeneration for restore: %v", err)
|
|
return
|
|
}
|
|
defer key.Close()
|
|
|
|
sasStateMu.Lock()
|
|
defer sasStateMu.Unlock()
|
|
|
|
if !savedSASState.captured {
|
|
return
|
|
}
|
|
|
|
if savedSASState.had {
|
|
if err := key.SetDWordValue("SoftwareSASGeneration", savedSASState.value); err != nil {
|
|
// Keep the snapshot: the machine still holds our forced value, and a
|
|
// later attempt is the only chance to put the original one back.
|
|
log.Warnf("restore SoftwareSASGeneration to %d: %v", savedSASState.value, err)
|
|
return
|
|
}
|
|
savedSASState = sasOriginalState{}
|
|
return
|
|
}
|
|
|
|
err = key.DeleteValue("SoftwareSASGeneration")
|
|
if err != nil && !errors.Is(err, registry.ErrNotExist) {
|
|
log.Debugf("delete SoftwareSASGeneration: %v", err)
|
|
return
|
|
}
|
|
savedSASState = sasOriginalState{}
|
|
}
|
|
|
|
// startSASListener creates a named event with a restricted DACL and waits for
|
|
// the VNC input injector to signal it. When signaled, it calls SendSAS(FALSE)
|
|
// from Session 0 to trigger the Secure Attention Sequence (Ctrl+Alt+Del).
|
|
// Only SYSTEM processes can open the event.
|
|
//
|
|
// sas.dll / SendSAS is part of the Desktop Experience feature: present on
|
|
// client SKUs (Win10/11) and Server SKUs with Desktop Experience installed,
|
|
// missing on Server Core. We probe for the symbol at startup; if absent we
|
|
// don't register the listener and the agent will silently drop SAS keysyms,
|
|
// rather than panicking the entire service every time the user clicks
|
|
// Ctrl+Alt+Del.
|
|
func startSASListener(ctx context.Context) {
|
|
ev, ok := createSASEvent()
|
|
if !ok {
|
|
return
|
|
}
|
|
log.Info("SAS listener ready (Session 0)")
|
|
go runSASListenerLoop(ctx, ev)
|
|
}
|
|
|
|
// createSASEvent prepares the named event handle on which the SAS listener
|
|
// waits for client signals. Returns ok=false (with the failure already
|
|
// logged) when the platform doesn't support SAS or the event cannot be
|
|
// created; the caller must not spawn the listener goroutine in that case.
|
|
func createSASEvent() (windows.Handle, bool) {
|
|
if err := procSendSAS.Find(); err != nil {
|
|
log.Warnf("SAS unavailable on this Windows SKU (sas.dll/SendSAS not present): %v", err)
|
|
return 0, false
|
|
}
|
|
enableSoftwareSAS()
|
|
namePtr, err := windows.UTF16PtrFromString(sasEventName)
|
|
if err != nil {
|
|
log.Warnf("SAS listener UTF16: %v", err)
|
|
return 0, false
|
|
}
|
|
sa, err := sasSecurityAttributes()
|
|
if err != nil {
|
|
log.Warnf("build SAS security descriptor: %v", err)
|
|
return 0, false
|
|
}
|
|
// Only after CreateEvent: the descriptor has to outlive the call that
|
|
// copies it into the kernel object.
|
|
defer freeSecurityDescriptor(sa)
|
|
|
|
ev, err := windows.CreateEvent(sa, 0, 0, namePtr)
|
|
if err != nil {
|
|
log.Warnf("SAS CreateEvent: %v", err)
|
|
return 0, false
|
|
}
|
|
return ev, true
|
|
}
|
|
|
|
// runSASListenerLoop blocks on ev and invokes SendSAS each time it is
|
|
// signalled, until ctx is cancelled. Recovers from panics inside SendSAS so
|
|
// a future ABI surprise doesn't tear down the service.
|
|
func runSASListenerLoop(ctx context.Context, ev windows.Handle) {
|
|
defer func() { _ = windows.CloseHandle(ev) }()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Warnf("SAS listener recovered from panic: %v", r)
|
|
}
|
|
}()
|
|
const pollMillis = 500
|
|
for {
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
ret, _ := windows.WaitForSingleObject(ev, pollMillis)
|
|
if ret != windows.WAIT_OBJECT_0 {
|
|
continue
|
|
}
|
|
r, _, sasErr := procSendSAS.Call(0) // FALSE = not from service desktop
|
|
if r == 0 {
|
|
log.Warnf("SendSAS: %v", sasErr)
|
|
continue
|
|
}
|
|
log.Info("SendSAS called from Session 0")
|
|
}
|
|
}
|
|
|
|
// enablePrivilege enables a named privilege on the current process token.
|
|
func enablePrivilege(name string) error {
|
|
var token windows.Token
|
|
if err := windows.OpenProcessToken(windows.CurrentProcess(),
|
|
windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token); err != nil {
|
|
return err
|
|
}
|
|
defer token.Close()
|
|
|
|
var luid windows.LUID
|
|
namePtr, err := windows.UTF16PtrFromString(name)
|
|
if err != nil {
|
|
return fmt.Errorf("UTF16 privilege name: %w", err)
|
|
}
|
|
if err := windows.LookupPrivilegeValue(nil, namePtr, &luid); err != nil {
|
|
return err
|
|
}
|
|
tp := windows.Tokenprivileges{PrivilegeCount: 1}
|
|
tp.Privileges[0].Luid = luid
|
|
tp.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED
|
|
return windows.AdjustTokenPrivileges(token, false, &tp, 0, nil, nil)
|
|
}
|
|
|
|
func (s *Server) platformSessionManager() virtualSessionManager {
|
|
return nil
|
|
}
|
|
|
|
// platformShutdown restores any machine state mutated by platformInit.
|
|
func (s *Server) platformShutdown() {
|
|
disableSoftwareSAS()
|
|
}
|
|
|
|
// platformInit starts the SAS listener and enables privileges needed for
|
|
// Session 0 operations (agent spawning, SendSAS).
|
|
func (s *Server) platformInit() {
|
|
for _, priv := range []string{"SeTcbPrivilege", "SeAssignPrimaryTokenPrivilege"} {
|
|
if err := enablePrivilege(priv); err != nil {
|
|
log.Debugf("enable %s: %v", priv, err)
|
|
}
|
|
}
|
|
startSASListener(s.ctx)
|
|
}
|
|
|
|
// serviceAcceptLoop runs in Session 0. It validates the source IP and
|
|
// hands accepted connections to handleServiceConnection, which runs the
|
|
// Noise_IK handshake before proxying to the user-session agent.
|
|
func (s *Server) serviceAcceptLoop(ln net.Listener) {
|
|
if ln == nil {
|
|
return
|
|
}
|
|
|
|
sm := s.serviceAgent()
|
|
if sm == nil {
|
|
s.log.Error("service mode: no agent manager available")
|
|
return
|
|
}
|
|
|
|
log.Info("service mode, proxying connections to agent over Unix socket")
|
|
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
s.log.Debugf("accept VNC connection: %v", err)
|
|
continue
|
|
}
|
|
|
|
if !s.tryAcquireConnSlot() {
|
|
s.log.Warnf("rejecting VNC connection from %s: %d concurrent connections in flight", conn.RemoteAddr(), maxConcurrentVNCConns)
|
|
_ = conn.Close()
|
|
continue
|
|
}
|
|
enableTCPKeepAlive(conn, s.log)
|
|
conn = newMetricsConn(conn, s.sessionRecorder)
|
|
s.trackConn(conn)
|
|
go func(c net.Conn) {
|
|
defer s.releaseConnSlot()
|
|
defer s.untrackConn(c)
|
|
s.handleServiceConnection(c, sm)
|
|
}(conn)
|
|
}
|
|
}
|
|
|
|
// newServiceAgentManager starts the session manager that owns the console
|
|
// agent. Called once per server via Server.serviceAgent.
|
|
func (s *Server) newServiceAgentManager() (sessionAgent, func()) {
|
|
sm := newSessionManager()
|
|
go sm.run()
|
|
return sm, sm.Stop
|
|
}
|