mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-19 15:19:55 +00:00
Add DesktopSize, DesktopName, LastRect pseudo-encodings with resize detection
This commit is contained in:
57
client/vnc/server/pseudo_encodings_test.go
Normal file
57
client/vnc/server/pseudo_encodings_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package server
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncodeDesktopSizeBody(t *testing.T) {
|
||||
got := encodeDesktopSizeBody(1920, 1080)
|
||||
if len(got) != 12 {
|
||||
t.Fatalf("DesktopSize body length: want 12, got %d", len(got))
|
||||
}
|
||||
if got[0] != 0 || got[1] != 0 || got[2] != 0 || got[3] != 0 {
|
||||
t.Fatalf("DesktopSize: x and y must be zero; got % x", got[0:4])
|
||||
}
|
||||
if got[4] != 0x07 || got[5] != 0x80 {
|
||||
t.Fatalf("DesktopSize: width should be 1920 (0x0780); got % x", got[4:6])
|
||||
}
|
||||
if got[6] != 0x04 || got[7] != 0x38 {
|
||||
t.Fatalf("DesktopSize: height should be 1080 (0x0438); got % x", got[6:8])
|
||||
}
|
||||
// Encoding = -223 → 0xFFFFFF21 in two's complement big-endian.
|
||||
if got[8] != 0xFF || got[9] != 0xFF || got[10] != 0xFF || got[11] != 0x21 {
|
||||
t.Fatalf("DesktopSize: encoding bytes wrong: % x", got[8:12])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDesktopNameBody(t *testing.T) {
|
||||
name := "vma@debian3"
|
||||
got := encodeDesktopNameBody(name)
|
||||
if len(got) != 12+4+len(name) {
|
||||
t.Fatalf("DesktopName body length: want %d, got %d", 12+4+len(name), len(got))
|
||||
}
|
||||
// Encoding = -307 → 0xFFFFFECD.
|
||||
if got[8] != 0xFF || got[9] != 0xFF || got[10] != 0xFE || got[11] != 0xCD {
|
||||
t.Fatalf("DesktopName: encoding bytes wrong: % x", got[8:12])
|
||||
}
|
||||
if got[12] != 0 || got[13] != 0 || got[14] != 0 || got[15] != byte(len(name)) {
|
||||
t.Fatalf("DesktopName: name length prefix wrong: % x", got[12:16])
|
||||
}
|
||||
if string(got[16:]) != name {
|
||||
t.Fatalf("DesktopName: name body wrong: %q", got[16:])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeLastRectBody(t *testing.T) {
|
||||
got := encodeLastRectBody()
|
||||
if len(got) != 12 {
|
||||
t.Fatalf("LastRect body length: want 12, got %d", len(got))
|
||||
}
|
||||
for i := 0; i < 8; i++ {
|
||||
if got[i] != 0 {
|
||||
t.Fatalf("LastRect: header bytes 0..7 must be zero; got byte %d = 0x%02x", i, got[i])
|
||||
}
|
||||
}
|
||||
// Encoding = -224 → 0xFFFFFF20.
|
||||
if got[8] != 0xFF || got[9] != 0xFF || got[10] != 0xFF || got[11] != 0x20 {
|
||||
t.Fatalf("LastRect: encoding bytes wrong: % x", got[8:12])
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,14 @@ const (
|
||||
encZlib = 6
|
||||
encTight = 7
|
||||
|
||||
// Pseudo-encodings carried over wire as rects with a negative
|
||||
// encoding value. The client advertises supported optional protocol
|
||||
// extensions by listing these in SetEncodings.
|
||||
pseudoEncDesktopSize = -223
|
||||
pseudoEncLastRect = -224
|
||||
pseudoEncDesktopName = -307
|
||||
pseudoEncExtendedDesktopSize = -308
|
||||
|
||||
// Tight compression-control byte top nibble. Stream-reset bits 0-3
|
||||
// (one per zlib stream) are unused while we run a single stream.
|
||||
tightFillSubenc = 0x80
|
||||
@@ -157,6 +165,52 @@ func encodeCopyRectBody(srcX, srcY, dstX, dstY, w, h int) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
// encodeDesktopSizeBody emits a DesktopSize pseudo-encoded rectangle. The
|
||||
// "rect" carries no pixel data: x and y are zero, w and h are the new
|
||||
// framebuffer dimensions, and encoding=-223 signals to the client that the
|
||||
// framebuffer was resized. Clients reallocate their backing buffer and
|
||||
// expect a full update at the new size to follow.
|
||||
func encodeDesktopSizeBody(w, h int) []byte {
|
||||
buf := make([]byte, 12)
|
||||
binary.BigEndian.PutUint16(buf[0:2], 0)
|
||||
binary.BigEndian.PutUint16(buf[2:4], 0)
|
||||
binary.BigEndian.PutUint16(buf[4:6], uint16(w))
|
||||
binary.BigEndian.PutUint16(buf[6:8], uint16(h))
|
||||
enc := int32(pseudoEncDesktopSize)
|
||||
binary.BigEndian.PutUint32(buf[8:12], uint32(enc))
|
||||
return buf
|
||||
}
|
||||
|
||||
// encodeDesktopNameBody emits a DesktopName pseudo-encoded rectangle. The
|
||||
// rect header is all zeros and encoding=-307; the body is a 4-byte
|
||||
// big-endian length followed by the UTF-8 name. Clients update their
|
||||
// window title or label without reconnecting.
|
||||
func encodeDesktopNameBody(name string) []byte {
|
||||
nameBytes := []byte(name)
|
||||
buf := make([]byte, 12+4+len(nameBytes))
|
||||
binary.BigEndian.PutUint16(buf[0:2], 0)
|
||||
binary.BigEndian.PutUint16(buf[2:4], 0)
|
||||
binary.BigEndian.PutUint16(buf[4:6], 0)
|
||||
binary.BigEndian.PutUint16(buf[6:8], 0)
|
||||
enc := int32(pseudoEncDesktopName)
|
||||
binary.BigEndian.PutUint32(buf[8:12], uint32(enc))
|
||||
binary.BigEndian.PutUint32(buf[12:16], uint32(len(nameBytes)))
|
||||
copy(buf[16:], nameBytes)
|
||||
return buf
|
||||
}
|
||||
|
||||
// encodeLastRectBody emits a LastRect sentinel. When the server sets
|
||||
// numRects=0xFFFF in the FramebufferUpdate header, the client reads rects
|
||||
// until it sees one with this encoding. Lets us stream rects from a
|
||||
// goroutine without committing to a count up front.
|
||||
func encodeLastRectBody() []byte {
|
||||
buf := make([]byte, 12)
|
||||
// x, y, w, h all zero; encoding = -224.
|
||||
enc := int32(pseudoEncLastRect)
|
||||
binary.BigEndian.PutUint32(buf[8:12], uint32(enc))
|
||||
return buf
|
||||
}
|
||||
|
||||
// encodeRawRect encodes a framebuffer region as a raw RFB rectangle.
|
||||
// The returned buffer includes the FramebufferUpdate header (1 rectangle).
|
||||
func encodeRawRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte {
|
||||
|
||||
@@ -34,13 +34,14 @@ const (
|
||||
)
|
||||
|
||||
type session struct {
|
||||
conn net.Conn
|
||||
capturer ScreenCapturer
|
||||
injector InputInjector
|
||||
serverW int
|
||||
serverH int
|
||||
password string
|
||||
log *log.Entry
|
||||
conn net.Conn
|
||||
capturer ScreenCapturer
|
||||
injector InputInjector
|
||||
serverW int
|
||||
serverH int
|
||||
desktopName string
|
||||
password string
|
||||
log *log.Entry
|
||||
|
||||
writeMu sync.Mutex
|
||||
// encMu guards the negotiated pixel format and encoding state below.
|
||||
@@ -56,6 +57,12 @@ type session struct {
|
||||
zlib *zlibState
|
||||
tight *tightState
|
||||
copyRectDet *copyRectDetector
|
||||
// Pseudo-encodings the client advertised support for. Updated under
|
||||
// encMu by handleSetEncodings and read by the encoder goroutine.
|
||||
clientSupportsDesktopSize bool
|
||||
clientSupportsExtendedDesktopSize bool
|
||||
clientSupportsDesktopName bool
|
||||
clientSupportsLastRect bool
|
||||
// prevFrame, curFrame and idleFrames live on the encoder goroutine and
|
||||
// must not be touched elsewhere. curFrame holds a session-owned copy of
|
||||
// the capturer's latest frame so the encoder works on a stable buffer
|
||||
@@ -233,7 +240,11 @@ func (s *session) doVNCAuth() error {
|
||||
}
|
||||
|
||||
func (s *session) sendServerInit() error {
|
||||
name := []byte("NetBird VNC")
|
||||
desktop := s.desktopName
|
||||
if desktop == "" {
|
||||
desktop = "NetBird VNC"
|
||||
}
|
||||
name := []byte(desktop)
|
||||
buf := make([]byte, 0, 4+16+4+len(name))
|
||||
|
||||
// Framebuffer width and height.
|
||||
@@ -333,6 +344,18 @@ func (s *session) handleSetEncodings() error {
|
||||
s.copyRectDet = newCopyRectDetector(tileSize)
|
||||
}
|
||||
encs = append(encs, "copyrect")
|
||||
case pseudoEncDesktopSize:
|
||||
s.clientSupportsDesktopSize = true
|
||||
encs = append(encs, "desktop-size")
|
||||
case pseudoEncExtendedDesktopSize:
|
||||
s.clientSupportsExtendedDesktopSize = true
|
||||
encs = append(encs, "ext-desktop-size")
|
||||
case pseudoEncDesktopName:
|
||||
s.clientSupportsDesktopName = true
|
||||
encs = append(encs, "desktop-name")
|
||||
case pseudoEncLastRect:
|
||||
s.clientSupportsLastRect = true
|
||||
encs = append(encs, "last-rect")
|
||||
case encZlib:
|
||||
s.useZlib = true
|
||||
if s.zlib == nil {
|
||||
@@ -401,6 +424,15 @@ func (s *session) encoderLoop(done chan<- struct{}) {
|
||||
}
|
||||
|
||||
func (s *session) processFBRequest(req fbRequest) error {
|
||||
// Watch for resolution changes between cycles. When the capturer
|
||||
// reports a new size, tell the client via DesktopSize so it can
|
||||
// reallocate its backing buffer; the next full update will then fill
|
||||
// the new dimensions. Clients that didn't advertise support are stuck
|
||||
// with the original handshake size and just see clipping on resize.
|
||||
if err := s.handleResize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
img, err := s.captureFrame()
|
||||
if errors.Is(err, errFrameUnchanged) {
|
||||
// macOS hashes the raw capture bytes and short-circuits when the
|
||||
@@ -503,6 +535,90 @@ func (s *session) captureRecovered() {
|
||||
}
|
||||
}
|
||||
|
||||
// handleResize detects framebuffer-size changes between encode cycles and
|
||||
// notifies the client via the DesktopSize pseudo-encoding. Returns an
|
||||
// error only on write failure; capturers that don't expose Width/Height
|
||||
// yet (zero values during early startup) are silently ignored.
|
||||
func (s *session) handleResize() error {
|
||||
w, h := s.capturer.Width(), s.capturer.Height()
|
||||
if w <= 0 || h <= 0 {
|
||||
return nil
|
||||
}
|
||||
if w == s.serverW && h == s.serverH {
|
||||
return nil
|
||||
}
|
||||
s.log.Debugf("framebuffer resized: %dx%d -> %dx%d", s.serverW, s.serverH, w, h)
|
||||
s.serverW = w
|
||||
s.serverH = h
|
||||
// Drop the prev frame so the next encode produces a full update at
|
||||
// the new dimensions rather than diffing against a stale-sized buffer.
|
||||
s.prevFrame = nil
|
||||
s.curFrame = nil
|
||||
if s.copyRectDet != nil {
|
||||
// Tile geometry changed; let updateDirty rebuild from scratch on
|
||||
// the next pass instead of reusing stale hashes keyed on old
|
||||
// (cols, rows).
|
||||
s.copyRectDet.prevTiles = nil
|
||||
s.copyRectDet.tileHash = nil
|
||||
}
|
||||
if err := s.sendDesktopSize(w, h); err != nil {
|
||||
return fmt.Errorf("send desktop size: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendDesktopSize emits a single-rect FramebufferUpdate carrying the
|
||||
// DesktopSize pseudo-encoding. No-op if the client did not negotiate it,
|
||||
// in which case the client just sees the new dimensions on the next full
|
||||
// update and will likely clip or scale.
|
||||
func (s *session) sendDesktopSize(w, h int) error {
|
||||
s.encMu.RLock()
|
||||
supported := s.clientSupportsDesktopSize || s.clientSupportsExtendedDesktopSize
|
||||
s.encMu.RUnlock()
|
||||
if !supported {
|
||||
return nil
|
||||
}
|
||||
header := make([]byte, 4)
|
||||
header[0] = serverFramebufferUpdate
|
||||
binary.BigEndian.PutUint16(header[2:4], 1)
|
||||
|
||||
body := encodeDesktopSizeBody(w, h)
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
if _, err := s.conn.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.conn.Write(body)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendDesktopName pushes a DesktopName pseudo-encoded update to the
|
||||
// client if it advertised support. Used by the server to keep the
|
||||
// dashboard title in sync with the active session (e.g. username
|
||||
// changes after login on a virtual session).
|
||||
func (s *session) SendDesktopName(name string) error {
|
||||
s.encMu.RLock()
|
||||
supported := s.clientSupportsDesktopName
|
||||
s.encMu.RUnlock()
|
||||
if !supported {
|
||||
s.desktopName = name
|
||||
return nil
|
||||
}
|
||||
s.desktopName = name
|
||||
header := make([]byte, 4)
|
||||
header[0] = serverFramebufferUpdate
|
||||
binary.BigEndian.PutUint16(header[2:4], 1)
|
||||
|
||||
body := encodeDesktopNameBody(name)
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
if _, err := s.conn.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.conn.Write(body)
|
||||
return err
|
||||
}
|
||||
|
||||
// refreshCopyRectIndex does a full hash sweep of the just-swapped prevFrame.
|
||||
// Used after full-frame sends, where we don't have a per-tile dirty list to
|
||||
// drive an incremental update.
|
||||
|
||||
Reference in New Issue
Block a user