Address CodeRabbit review and fix CI on embedded-vnc

This commit is contained in:
Viktor Liu
2026-05-23 19:44:21 +02:00
parent 7cb6388349
commit fa57eedaf5
13 changed files with 108 additions and 67 deletions
+1
View File
@@ -863,6 +863,7 @@ func TestAddConfig_AllFieldsCovered(t *testing.T) {
RosenpassPermissive: true, RosenpassPermissive: true,
ServerSSHAllowed: &bTrue, ServerSSHAllowed: &bTrue,
ServerVNCAllowed: &bTrue, ServerVNCAllowed: &bTrue,
DisableVNCApproval: &bTrue,
EnableSSHRoot: &bTrue, EnableSSHRoot: &bTrue,
EnableSSHSFTP: &bTrue, EnableSSHSFTP: &bTrue,
EnableSSHLocalPortForwarding: &bTrue, EnableSSHLocalPortForwarding: &bTrue,
+1 -1
View File
@@ -433,7 +433,7 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
updated = true updated = true
} }
} else if config.ServerVNCAllowed == nil { } else if config.ServerVNCAllowed == nil {
config.ServerVNCAllowed = util.True() config.ServerVNCAllowed = util.False()
updated = true updated = true
} }
+10 -2
View File
@@ -111,7 +111,7 @@ func (s *Server) StartCapture(req *proto.StartCaptureRequest, stream proto.Daemo
return status.Errorf(codes.Internal, "create capture session: %v", err) return status.Errorf(codes.Internal, "create capture session: %v", err)
} }
engine, err := s.claimCapture(sess) engine, err := s.claimCapture(sess, func() { pw.Close() })
if err != nil { if err != nil {
sess.Stop() sess.Stop()
pw.Close() pw.Close()
@@ -305,7 +305,7 @@ func (s *Server) cleanupBundleCapture() {
// capture is already running it is evicted: a previous streaming session // capture is already running it is evicted: a previous streaming session
// whose gRPC client died and never freed the slot stays stuck otherwise, // whose gRPC client died and never freed the slot stays stuck otherwise,
// and a bundle capture is just informational state. // and a bundle capture is just informational state.
func (s *Server) claimCapture(sess *capture.Session) (*internal.Engine, error) { func (s *Server) claimCapture(sess *capture.Session, cancel func()) (*internal.Engine, error) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
@@ -315,6 +315,7 @@ func (s *Server) claimCapture(sess *capture.Session) (*internal.Engine, error) {
return nil, err return nil, err
} }
s.activeCapture = sess s.activeCapture = sess
s.activeCaptureCancel = cancel
return engine, nil return engine, nil
} }
@@ -331,13 +332,18 @@ func (s *Server) evictActiveCaptureLocked() {
} }
log.Infof("evicting previous streaming capture to start a new one") log.Infof("evicting previous streaming capture to start a new one")
prev := s.activeCapture prev := s.activeCapture
cancel := s.activeCaptureCancel
if engine, err := s.getCaptureEngineLocked(); err == nil { if engine, err := s.getCaptureEngineLocked(); err == nil {
if err := engine.SetCapture(nil); err != nil { if err := engine.SetCapture(nil); err != nil {
log.Debugf("clear previous capture: %v", err) log.Debugf("clear previous capture: %v", err)
} }
} }
s.activeCapture = nil s.activeCapture = nil
s.activeCaptureCancel = nil
prev.Stop() prev.Stop()
if cancel != nil {
cancel()
}
} }
// releaseCapture clears the active-capture owner if it still matches sess. // releaseCapture clears the active-capture owner if it still matches sess.
@@ -346,6 +352,7 @@ func (s *Server) releaseCapture(sess *capture.Session) {
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.activeCapture == sess { if s.activeCapture == sess {
s.activeCapture = nil s.activeCapture = nil
s.activeCaptureCancel = nil
} }
} }
@@ -360,6 +367,7 @@ func (s *Server) clearCaptureIfOwner(sess *capture.Session, engine *internal.Eng
log.Debugf("clear capture: %v", err) log.Debugf("clear capture: %v", err)
} }
s.activeCapture = nil s.activeCapture = nil
s.activeCaptureCancel = nil
} }
func (s *Server) getCaptureEngineLocked() (*internal.Engine, error) { func (s *Server) getCaptureEngineLocked() (*internal.Engine, error) {
+6 -2
View File
@@ -93,8 +93,12 @@ type Server struct {
captureEnabled bool captureEnabled bool
bundleCapture *bundleCapture bundleCapture *bundleCapture
// activeCapture is the session currently installed on the engine; guarded by s.mutex. // activeCapture is the session currently installed on the engine; guarded by s.mutex.
activeCapture *capture.Session activeCapture *capture.Session
networksDisabled bool // activeCaptureCancel tears down the streaming pipe/cancel for the
// active streaming capture so eviction unblocks the StartCapture RPC
// handler. Nil for bundle captures (they own their own context).
activeCaptureCancel func()
networksDisabled bool
sleepHandler *sleephandler.SleepHandler sleepHandler *sleephandler.SleepHandler
+5 -3
View File
@@ -113,15 +113,17 @@ func (s *serviceClient) showApprovalUI(req approvalRequest) {
decide(outcome{}) decide(outcome{})
return return
} }
updateCountdown() fyne.Do(updateCountdown)
} }
}() }()
go func() { go func() {
o := <-decided o := <-decided
s.sendApprovalResponse(req.requestID, o.accept, o.viewOnly) s.sendApprovalResponse(req.requestID, o.accept, o.viewOnly)
w.Close() fyne.Do(func() {
s.app.Quit() w.Close()
s.app.Quit()
})
}() }()
w.Show() w.Show()
+4 -2
View File
@@ -47,8 +47,6 @@ var (
procWTSEnumerateSessionsW = wtsapi32.NewProc("WTSEnumerateSessionsW") procWTSEnumerateSessionsW = wtsapi32.NewProc("WTSEnumerateSessionsW")
procWTSFreeMemory = wtsapi32.NewProc("WTSFreeMemory") procWTSFreeMemory = wtsapi32.NewProc("WTSFreeMemory")
procWTSQuerySessionInformation = wtsapi32.NewProc("WTSQuerySessionInformationW") procWTSQuerySessionInformation = wtsapi32.NewProc("WTSQuerySessionInformationW")
iphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
) )
// GetCurrentSessionID returns the session ID of the current process. // GetCurrentSessionID returns the session ID of the current process.
@@ -514,6 +512,8 @@ func (m *sessionManager) reapExitedAgent() {
log.Debugf("close agent handle: %v", err) log.Debugf("close agent handle: %v", err)
} }
m.agentProc = 0 m.agentProc = 0
m.authToken = ""
m.socketPath = ""
} }
// scheduleNextSpawn applies an exponential backoff on fast crashes (<5s) and // scheduleNextSpawn applies an exponential backoff on fast crashes (<5s) and
@@ -586,6 +586,8 @@ func (m *sessionManager) killAgent() {
_ = windows.TerminateProcess(m.agentProc, 0) _ = windows.TerminateProcess(m.agentProc, 0)
_ = windows.CloseHandle(m.agentProc) _ = windows.CloseHandle(m.agentProc)
m.agentProc = 0 m.agentProc = 0
m.authToken = ""
m.socketPath = ""
log.Info("killed old agent") log.Info("killed old agent")
} }
+7 -16
View File
@@ -1,19 +1,10 @@
package server package server
import "errors" // interactiveUserError returns nil when a user is logged into the console
// (i.e. an Aqua session is active). At the loginwindow there is nobody to
// consoleHasInteractiveUser returns true when a user is logged into the // display an approval prompt to, so callers can decline without waiting on
// console (i.e. an Aqua session is active). At the loginwindow there is // the broker. Any error (including errNoConsoleUser) is treated as decline.
// nobody to display an approval prompt to, so callers can decline func interactiveUserError() error {
// without waiting on the broker. _, err := consoleUserID()
func consoleHasInteractiveUser() bool { return err
if _, err := consoleUserID(); err != nil {
if errors.Is(err, errNoConsoleUser) {
return false
}
// Unknown error: fail closed so a probe-time glitch does not
// silently let an unattended console accept VNC sessions.
return false
}
return true
} }
+3 -3
View File
@@ -2,6 +2,6 @@
package server package server
// consoleHasInteractiveUser is unused outside service mode (darwin/windows) // interactiveUserError is unused outside service mode (darwin/windows) but
// but the symbol must exist so gateApproval compiles on all platforms. // the symbol must exist so gateApproval compiles on all platforms.
func consoleHasInteractiveUser() bool { return true } func interactiveUserError() error { return nil }
+9 -7
View File
@@ -1,13 +1,15 @@
package server package server
// consoleHasInteractiveUser returns true when there is a logged-in user // interactiveUserError returns nil when there is a logged-in user session
// session on the box. At the lock/login screen WTSQueryUserName is empty, // on the box. At the lock/login screen WTSQueryUserName is empty, which
// which means there is nobody to display an approval prompt to. Callers // means there is nobody to display an approval prompt to.
// should decline without waiting on the broker in that case. func interactiveUserError() error {
func consoleHasInteractiveUser() bool {
sid := getActiveSessionID() sid := getActiveSessionID()
if sid == 0 { if sid == 0 {
return false return errNoConsoleUser
} }
return wtsSessionHasUser(sid) if !wtsSessionHasUser(sid) {
return errNoConsoleUser
}
return nil
} }
+12 -6
View File
@@ -431,10 +431,12 @@ func (s *Server) gateApproval(conn net.Conn, header *connectionHeader, connLog *
connLog.Warn("VNC connection rejected: approval required but no approver") connLog.Warn("VNC connection rejected: approval required but no approver")
return false, ApprovalDecision{} return false, ApprovalDecision{}
} }
if s.serviceMode && !consoleHasInteractiveUser() { if s.serviceMode {
rejectConnection(conn, codeMessage(RejectCodeNoConsoleUser, "no interactive user session")) if err := interactiveUserError(); err != nil {
connLog.Info("VNC connection rejected: no interactive user session to approve") rejectConnection(conn, codeMessage(RejectCodeNoConsoleUser, "no interactive user session"))
return false, ApprovalDecision{} connLog.Infof("VNC connection rejected: no interactive user session to approve: %v", err)
return false, ApprovalDecision{}
}
} }
info := ApprovalInfo{ info := ApprovalInfo{
SourceIP: sourceIPString(conn.RemoteAddr()), SourceIP: sourceIPString(conn.RemoteAddr()),
@@ -449,7 +451,7 @@ func (s *Server) gateApproval(conn net.Conn, header *connectionHeader, connLog *
} }
decision, err := s.approver.Request(s.ctx, info) decision, err := s.approver.Request(s.ctx, info)
if err != nil { if err != nil {
rejectConnection(conn, codeMessage(RejectCodeApprovalDenied, err.Error())) rejectConnection(conn, codeMessage(RejectCodeApprovalDenied, "approval denied"))
connLog.Infof("VNC connection rejected: approval %v", err) connLog.Infof("VNC connection rejected: approval %v", err)
return false, ApprovalDecision{} return false, ApprovalDecision{}
} }
@@ -737,9 +739,13 @@ func (s *Server) validateCapturer(capturer ScreenCapturer) error {
func (s *Server) isAllowedSource(addr net.Addr) bool { func (s *Server) isAllowedSource(addr net.Addr) bool {
// Unix-socket remotes (the agent path) are local IPC, gated by the // Unix-socket remotes (the agent path) are local IPC, gated by the
// token, not by overlay membership. // token, not by overlay membership.
if _, ok := addr.(*net.UnixAddr); ok {
return true
}
tcpAddr, ok := addr.(*net.TCPAddr) tcpAddr, ok := addr.(*net.TCPAddr)
if !ok { if !ok {
return true s.log.Warnf("connection rejected: unsupported remote address type %T", addr)
return false
} }
remoteIP, ok := netip.AddrFromSlice(tcpAddr.IP) remoteIP, ok := netip.AddrFromSlice(tcpAddr.IP)
-1
View File
@@ -269,4 +269,3 @@ func (s *Server) serviceAcceptLoop() {
}(conn) }(conn)
} }
} }
+9 -3
View File
@@ -545,6 +545,9 @@ func (s *session) handleFBUpdateRequest() error {
// in sync with the active session (e.g. username changes after login on // in sync with the active session (e.g. username changes after login on
// a virtual session). // a virtual session).
func (s *session) SendDesktopName(name string) error { func (s *session) SendDesktopName(name string) error {
if s.viewOnly {
name = ViewOnlyDesktopNamePrefix + name
}
s.encMu.RLock() s.encMu.RLock()
supported := s.clientSupportsDesktopName supported := s.clientSupportsDesktopName
s.encMu.RUnlock() s.encMu.RUnlock()
@@ -629,13 +632,13 @@ func (s *session) handlePointerEvent() error {
mask = (mask & 0x7f) | uint16(hi[0])<<7 mask = (mask & 0x7f) | uint16(hi[0])<<7
} }
if s.viewOnly {
return nil
}
s.pointerMu.Lock() s.pointerMu.Lock()
s.lastPointerX = x s.lastPointerX = x
s.lastPointerY = y s.lastPointerY = y
s.pointerMu.Unlock() s.pointerMu.Unlock()
if s.viewOnly {
return nil
}
s.injector.InjectPointer(mask, x, y, s.serverW, s.serverH) s.injector.InjectPointer(mask, x, y, s.serverW, s.serverH)
return nil return nil
} }
@@ -661,6 +664,9 @@ var stickyModifierKeysyms = [...]uint32{
// when the client disconnects mid-press. Mouse coordinates are reused // when the client disconnects mid-press. Mouse coordinates are reused
// from the last PointerEvent so we don't warp the cursor. // from the last PointerEvent so we don't warp the cursor.
func (s *session) releaseStickyInput() { func (s *session) releaseStickyInput() {
if s.viewOnly {
return
}
for _, ks := range stickyModifierKeysyms { for _, ks := range stickyModifierKeysyms {
s.injector.InjectKey(ks, false) s.injector.InjectKey(ks, false)
} }
@@ -475,6 +475,37 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request)
newPeer := &nbpeer.Peer{} newPeer := &nbpeer.Peer{}
newPeer.FromAPITemporaryAccessRequest(&req) newPeer.FromAPITemporaryAccessRequest(&req)
parsedRules := make([]struct {
raw string
protocol types.PolicyRuleProtocolType
portRange types.RulePortRange
}, 0, len(req.Rules))
needsVNCKey := false
for _, rule := range req.Rules {
protocol, portRange, err := types.ParseRuleString(rule)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
if protocol == types.PolicyRuleProtocolNetbirdVNC {
needsVNCKey = true
}
parsedRules = append(parsedRules, struct {
raw string
protocol types.PolicyRuleProtocolType
portRange types.RulePortRange
}{rule, protocol, portRange})
}
var vncSessionPubKey string
if needsVNCKey {
vncSessionPubKey, err = validateVNCSessionPubKey(req.SessionPubKey)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
}
targetPeer, err := h.accountManager.GetPeer(r.Context(), userAuth.AccountId, peerID, userAuth.UserId) targetPeer, err := h.accountManager.GetPeer(r.Context(), userAuth.AccountId, peerID, userAuth.UserId)
if err != nil { if err != nil {
util.WriteError(r.Context(), err, w) util.WriteError(r.Context(), err, w)
@@ -487,12 +518,7 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request)
return return
} }
for _, rule := range req.Rules { for _, pr := range parsedRules {
protocol, portRange, err := types.ParseRuleString(rule)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
policy := &types.Policy{ policy := &types.Policy{
AccountID: userAuth.AccountId, AccountID: userAuth.AccountId,
Description: "Temporary access policy for peer " + peer.Name, Description: "Temporary access policy for peer " + peer.Name,
@@ -512,34 +538,28 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request)
ID: targetPeer.ID, ID: targetPeer.ID,
}, },
Bidirectional: false, Bidirectional: false,
Protocol: protocol, Protocol: pr.protocol,
PortRanges: []types.RulePortRange{portRange}, PortRanges: []types.RulePortRange{pr.portRange},
}}, }},
} }
if protocol == types.PolicyRuleProtocolNetbirdSSH || protocol == types.PolicyRuleProtocolNetbirdVNC { if pr.protocol == types.PolicyRuleProtocolNetbirdSSH || pr.protocol == types.PolicyRuleProtocolNetbirdVNC {
policy.Rules[0].AuthorizedUser = userAuth.UserId policy.Rules[0].AuthorizedUser = userAuth.UserId
} }
if protocol == types.PolicyRuleProtocolNetbirdVNC { if pr.protocol == types.PolicyRuleProtocolNetbirdVNC {
pubKey, err := validateVNCSessionPubKey(req.SessionPubKey) policy.Rules[0].SessionPubKey = vncSessionPubKey
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
policy.Rules[0].SessionPubKey = pubKey
policy.Rules[0].SessionDisplayName = h.displayNameForUser(r.Context(), userAuth) policy.Rules[0].SessionDisplayName = h.displayNameForUser(r.Context(), userAuth)
} }
_, err = h.accountManager.SavePolicy(r.Context(), userAuth.AccountId, userAuth.UserId, policy, true) if _, err = h.accountManager.SavePolicy(r.Context(), userAuth.AccountId, userAuth.UserId, policy, true); err != nil {
if err != nil {
util.WriteError(r.Context(), err, w) util.WriteError(r.Context(), err, w)
return return
} }
} }
resp := &api.PeerTemporaryAccessResponse{ resp := &api.PeerTemporaryAccessResponse{
Id: peer.ID, Id: peer.ID,
Name: peer.Name, Name: peer.Name,
Rules: req.Rules, Rules: req.Rules,
TargetPubKey: targetPeer.Key, TargetPubKey: targetPeer.Key,
} }