From 67c786b4f47e24d6a1ed4408ffcceb4e93f2554f Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Tue, 22 Sep 2026 19:53:43 +0200 Subject: [PATCH] Report the MDM VNC keys to the UI and bound the copyrect tile-hash map --- client/mdm/restrictions.go | 4 ++++ client/mdm/restrictions_test.go | 37 ++++++++++++++++++++++++++++++ client/vnc/server/copyrect.go | 18 +++++++++++++-- client/vnc/server/copyrect_test.go | 30 ++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 client/mdm/restrictions_test.go diff --git a/client/mdm/restrictions.go b/client/mdm/restrictions.go index d9fdbd430..706284985 100644 --- a/client/mdm/restrictions.go +++ b/client/mdm/restrictions.go @@ -64,9 +64,13 @@ func BuildRestrictions(policy *Policy) Restrictions { r.MDM.SplitTunnelMode = policy.HasKey(KeySplitTunnelMode) r.MDM.SplitTunnelApps = policy.HasKey(KeySplitTunnelApps) r.MDM.RemoteJobsAllowed = policy.HasKey(KeyRemoteJobsAllowed) + r.MDM.DisableVNCApproval = policy.HasKey(KeyDisableVNCApproval) if v, ok := policy.GetBool(KeyAllowServerSSH); ok { r.MDM.AllowServerSSH = &v } + if v, ok := policy.GetBool(KeyAllowServerVNC); ok { + r.MDM.AllowServerVNC = &v + } if v, ok := policy.GetBool(KeyDisableAdvancedView); ok { r.MDM.DisableAdvancedView = &v } diff --git a/client/mdm/restrictions_test.go b/client/mdm/restrictions_test.go new file mode 100644 index 000000000..ba73fc046 --- /dev/null +++ b/client/mdm/restrictions_test.go @@ -0,0 +1,37 @@ +package mdm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Every key the UI can be told about has to be mapped here. A field left out +// reports the key as unmanaged, so the desktop and mobile UIs offer a control +// the policy actually enforces, and the user's change is silently overridden. +func TestBuildRestrictions_MapsRemoteAccessKeys(t *testing.T) { + policy := NewPolicy(map[string]any{ + KeyAllowServerSSH: true, + KeyAllowServerVNC: true, + KeyDisableVNCApproval: true, + KeyRemoteJobsAllowed: true, + }) + + r := BuildRestrictions(policy) + + require.NotNil(t, r.MDM.AllowServerSSH, "allowServerSSH must be reported as managed") + assert.True(t, *r.MDM.AllowServerSSH) + require.NotNil(t, r.MDM.AllowServerVNC, "allowServerVNC must be reported as managed") + assert.True(t, *r.MDM.AllowServerVNC) + assert.True(t, r.MDM.DisableVNCApproval, "disableVNCApproval must be reported as managed") + assert.True(t, r.MDM.RemoteJobsAllowed) +} + +// An unmanaged key stays nil/false so the UI leaves the control editable. +func TestBuildRestrictions_UnmanagedVNCKeys(t *testing.T) { + r := BuildRestrictions(NewPolicy(map[string]any{KeyAllowServerSSH: false})) + + assert.Nil(t, r.MDM.AllowServerVNC, "an unset allowServerVNC must not read as managed") + assert.False(t, r.MDM.DisableVNCApproval) +} diff --git a/client/vnc/server/copyrect.go b/client/vnc/server/copyrect.go index 4e73e2a54..99eb48957 100644 --- a/client/vnc/server/copyrect.go +++ b/client/vnc/server/copyrect.go @@ -126,13 +126,27 @@ func (d *copyRectDetector) updateDirty(frame *image.RGBA, w, h int, dirty [][4]i if tx+ts > w || ty+ts > h { continue } + idx := (ty/ts)*d.cols + (tx / ts) + pos := [2]int{tx, ty} sum := d.hashTile(frame, tx, ty) - d.tileHash[(ty/ts)*d.cols+(tx/ts)] = sum + + // Retire the hash this tile used to carry. Without this the map keeps + // one entry per distinct hash the tile has ever had, so a long session + // over changing content grows it without bound. Only drop the entry + // while it still points here: another tile may have claimed that hash + // since, and its mapping is live. + if old := d.tileHash[idx]; old != sum { + if owner, ok := d.prevTiles[old]; ok && owner == pos { + delete(d.prevTiles, old) + } + } + + d.tileHash[idx] = sum // Latest-wins on collision: ensures the most recent owner of this // hash is the one we'll return on lookup. The previous owner's // entry, if any, gets shadowed; if its content has changed it's // stale anyway and findTileMatch's verification will skip it. - d.prevTiles[sum] = [2]int{tx, ty} + d.prevTiles[sum] = pos } } diff --git a/client/vnc/server/copyrect_test.go b/client/vnc/server/copyrect_test.go index 610a04aba..8e6a47814 100644 --- a/client/vnc/server/copyrect_test.go +++ b/client/vnc/server/copyrect_test.go @@ -5,6 +5,8 @@ package server import ( "image" "testing" + + "github.com/stretchr/testify/assert" ) // fillTile paints a tileSizeƗtileSize block of img at (x,y) with the colour @@ -223,3 +225,31 @@ func TestEncodeCopyRectBody_Layout(t *testing.T) { t.Fatalf("bad src bytes: % x", got[12:16]) } } + +// prevTiles maps a tile hash to the position that carries it, so it can hold +// at most one entry per tile. Rehashing the same tile with fresh content must +// retire the hash it used to carry: a session over changing content otherwise +// accumulates one dead entry per distinct hash it has ever seen, for as long +// as the session lives. +func TestCopyRectDetector_PrevTilesStaysBounded(t *testing.T) { + const w, h = 128, 128 // 2x2 tiles at 64px + const ts = 64 + const tiles = 4 + + frame := image.NewRGBA(image.Rect(0, 0, w, h)) + d := newCopyRectDetector(ts) + d.rebuild(frame, w, h) + + dirty := [][4]int{{0, 0, ts, ts}, {ts, 0, ts, ts}, {0, ts, ts, ts}, {ts, ts, ts, ts}} + for i := range 200 { + for ty := range 2 { + for tx := range 2 { + fillTile(frame, tx*ts, ty*ts, ts, byte(i), byte(i*3), byte(tx+ty)) + } + } + d.updateDirty(frame, w, h, dirty) + } + + assert.LessOrEqual(t, len(d.prevTiles), tiles, + "prevTiles must not grow past one entry per tile across repeated content changes") +}