-
Notifications
You must be signed in to change notification settings - Fork 340
[UNMAINTAINED] feat(vnc): add VNC server with H.264 #1447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
dec25e1
video: refcount the capture pipeline
mcuelenaere 389b9fb
internal/rfb: protocol primitives
mcuelenaere b7ca78a
internal/rfb: keysym to HID Usage ID mapping
mcuelenaere 53bee10
internal/rfb: placeholder framebuffer for non-H.264 clients
mcuelenaere 54319ea
vnc: scaffold (config, JSON-RPC, logger)
mcuelenaere 88058f2
vnc: server, frame fan-out, HID forwarding
mcuelenaere b5522bc
ui: VNC settings panel
mcuelenaere 776a5f6
vnc: drop VncAllowOverWAN, always allow non-loopback bind
mcuelenaere 1d41410
vnc: pin H.264 codec when VNC starts the capture pipeline
mcuelenaere ccc66b3
vnc: cycle codec to H.264 when VNC is enabled mid-session
mcuelenaere 6dcd42c
vnc: fix deadlock + linter + bugbot review issues
mcuelenaere a816f53
internal/rfb: move splitAnnexB into the rfb package
mcuelenaere cb65bcf
vnc: fix RFB->HID button bit swap and vncServer data race
mcuelenaere ad1c334
vnc: trace every KeyEvent for keymap debugging
mcuelenaere 3508879
vnc: trace every PointerEvent, document side-button situation
mcuelenaere 9a05352
vnc: reduce H.264 scroll artifacts (TCP_NODELAY, larger buffer, drop …
mcuelenaere 588f70d
vnc: drop the per-rect emission trace logs
mcuelenaere 5367ff7
vnc: add macOS-client keymap to fix TightVNC modifier mismapping
mcuelenaere ba5cd0a
vnc: implement Extended Mouse Buttons (RFB encoding -316)
mcuelenaere c259a9b
vnc: tighten TCP write path + skip duplicate SPS/PPS prepend
mcuelenaere 83ca5c2
vnc: shorten the broken-decoder window after a frame drop
mcuelenaere File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| package rfb | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "encoding/binary" | ||
| "io" | ||
| "net" | ||
| "time" | ||
| ) | ||
|
|
||
| // Conn wraps a net.Conn with framing buffers for the RFB protocol. | ||
| // | ||
| // The Conn assumes a single-writer model: at most one goroutine | ||
| // invokes write methods on the same Conn at a time. In typical use | ||
| // the handshake runs sequentially on one goroutine and is then | ||
| // followed by a per-connection dispatcher (also single goroutine) | ||
| // while a separate reader goroutine only calls read methods. Do not | ||
| // share writes between goroutines without external synchronization. | ||
| type Conn struct { | ||
| nc net.Conn | ||
| r *bufio.Reader | ||
| w *bufio.Writer | ||
|
|
||
| // extendedMouseButtons indicates that the Extended Mouse Buttons | ||
| // extension (encoding -316) has been negotiated. The PointerEvent | ||
| // reader uses it to pick the legacy (6-byte) or extended (7-byte) | ||
| // wire format. Set by SetExtendedMouseButtons after the server | ||
| // has sent the announce rectangle. Single-writer model also | ||
| // applies to this flag. | ||
| extendedMouseButtons bool | ||
| } | ||
|
|
||
| // SetExtendedMouseButtons enables decoding of the Extended Mouse | ||
| // Buttons extension on the next PointerEvent message. Call once | ||
| // after the announce rectangle has been written to the client. | ||
| func (c *Conn) SetExtendedMouseButtons(enabled bool) { c.extendedMouseButtons = enabled } | ||
|
|
||
| // writeBufferSize is the bufio.Writer buffer for outbound bytes. | ||
| // Picked to comfortably hold one typical 1080p H.264 P-frame so a | ||
| // FramebufferUpdate with the rect headers + length/flags + payload | ||
| // goes out in a single Write to the underlying TCP socket — the | ||
| // default 4 KB caused ~12 syscalls per frame and as many TCP segments | ||
| // when NoDelay forced an immediate flush. | ||
| const writeBufferSize = 64 * 1024 | ||
|
|
||
| // NewConn wraps a net.Conn with read/write buffers. | ||
| func NewConn(nc net.Conn) *Conn { | ||
| return &Conn{ | ||
| nc: nc, | ||
| r: bufio.NewReader(nc), | ||
| w: bufio.NewWriterSize(nc, writeBufferSize), | ||
| } | ||
| } | ||
|
|
||
| // Close terminates the underlying connection. | ||
| func (c *Conn) Close() error { return c.nc.Close() } | ||
|
|
||
| // RemoteAddr returns the peer's network address. | ||
| func (c *Conn) RemoteAddr() net.Addr { return c.nc.RemoteAddr() } | ||
|
|
||
| // SetReadDeadline forwards to the underlying net.Conn. | ||
| func (c *Conn) SetReadDeadline(t time.Time) error { return c.nc.SetReadDeadline(t) } | ||
|
|
||
| // SetWriteDeadline forwards to the underlying net.Conn. | ||
| func (c *Conn) SetWriteDeadline(t time.Time) error { return c.nc.SetWriteDeadline(t) } | ||
|
|
||
| // Flush flushes the write buffer to the underlying connection. | ||
| func (c *Conn) Flush() error { return c.w.Flush() } | ||
|
|
||
| // readByte reads one byte. | ||
| func (c *Conn) readByte() (byte, error) { return c.r.ReadByte() } | ||
|
|
||
| // readFull fills buf entirely. | ||
| func (c *Conn) readFull(buf []byte) error { | ||
| _, err := io.ReadFull(c.r, buf) | ||
| return err | ||
| } | ||
|
|
||
| // readU16 reads a big-endian uint16. | ||
| func (c *Conn) readU16() (uint16, error) { | ||
| var b [2]byte | ||
| if err := c.readFull(b[:]); err != nil { | ||
| return 0, err | ||
| } | ||
| return binary.BigEndian.Uint16(b[:]), nil | ||
| } | ||
|
|
||
| // readU32 reads a big-endian uint32. | ||
| func (c *Conn) readU32() (uint32, error) { | ||
| var b [4]byte | ||
| if err := c.readFull(b[:]); err != nil { | ||
| return 0, err | ||
| } | ||
| return binary.BigEndian.Uint32(b[:]), nil | ||
| } | ||
|
|
||
| // readS32 reads a big-endian int32. | ||
| func (c *Conn) readS32() (int32, error) { | ||
| v, err := c.readU32() | ||
| return int32(v), err | ||
| } | ||
|
|
||
| // writeRaw writes buf verbatim to the buffered writer. | ||
| func (c *Conn) writeRaw(buf []byte) error { | ||
| _, err := c.w.Write(buf) | ||
| return err | ||
| } | ||
|
|
||
| // writeByte writes a single byte. | ||
| func (c *Conn) writeByte(b byte) error { return c.w.WriteByte(b) } | ||
|
|
||
| // writeU16 writes a big-endian uint16. | ||
| func (c *Conn) writeU16(v uint16) error { | ||
| var b [2]byte | ||
| binary.BigEndian.PutUint16(b[:], v) | ||
| return c.writeRaw(b[:]) | ||
| } | ||
|
|
||
| // writeU32 writes a big-endian uint32. | ||
| func (c *Conn) writeU32(v uint32) error { | ||
| var b [4]byte | ||
| binary.BigEndian.PutUint32(b[:], v) | ||
| return c.writeRaw(b[:]) | ||
| } | ||
|
|
||
| // writeS32 writes a big-endian int32. | ||
| func (c *Conn) writeS32(v int32) error { return c.writeU32(uint32(v)) } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| package rfb | ||
|
|
||
| import "fmt" | ||
|
|
||
| // Rect describes a rectangle that will be sent inside a | ||
| // FramebufferUpdate message header. The encoding-specific payload | ||
| // follows separately. | ||
| type Rect struct { | ||
| X, Y, W, H uint16 | ||
| Encoding EncodingType | ||
| } | ||
|
|
||
| // BeginFramebufferUpdate writes the FramebufferUpdate message header | ||
| // (RFC 6143 §7.6.1) for `n` rectangles. The caller must follow up | ||
| // with `n` WriteRectHeader+payload pairs and a final Conn.Flush. | ||
| // The Conn type assumes a single-writer model — see Conn's doc. | ||
| func (c *Conn) BeginFramebufferUpdate(n uint16) error { | ||
| if err := c.writeByte(byte(ServerMsgFramebufferUpdate)); err != nil { | ||
| return err | ||
| } | ||
| if err := c.writeByte(0); err != nil { // padding | ||
| return err | ||
| } | ||
| return c.writeU16(n) | ||
| } | ||
|
|
||
| // WriteRectHeader writes a 12-byte rectangle header (x, y, w, h, | ||
| // encoding). The caller is responsible for writing the | ||
| // encoding-specific payload immediately after. | ||
| func (c *Conn) WriteRectHeader(r Rect) error { | ||
| if err := c.writeU16(r.X); err != nil { | ||
| return err | ||
| } | ||
| if err := c.writeU16(r.Y); err != nil { | ||
| return err | ||
| } | ||
| if err := c.writeU16(r.W); err != nil { | ||
| return err | ||
| } | ||
| if err := c.writeU16(r.H); err != nil { | ||
| return err | ||
| } | ||
| return c.writeS32(int32(r.Encoding)) | ||
| } | ||
|
|
||
| // WriteRawRect writes a Raw-encoded rectangle. `pixels` must be | ||
| // width*height*(bitsPerPixel/8) bytes laid out in row-major order | ||
| // using the format previously requested by the client (or the | ||
| // server's default if SetPixelFormat was never sent). | ||
| func (c *Conn) WriteRawRect(r Rect, pixels []byte) error { | ||
| if r.Encoding != EncodingRaw { | ||
| return fmt.Errorf("rfb: WriteRawRect called with encoding %d", r.Encoding) | ||
| } | ||
| if err := c.WriteRectHeader(r); err != nil { | ||
| return err | ||
| } | ||
| return c.writeRaw(pixels) | ||
| } | ||
|
|
||
| // WriteOpenH264Rect writes an "Open H.264 Encoding" rectangle | ||
| // (pseudo-encoding 50). The H.264 payload must be a valid Annex-B | ||
| // bytestream — typically one or more NAL units concatenated with | ||
| // 00 00 00 01 (or 00 00 01) start codes. | ||
| // | ||
| // `flags` is the U32 flags field; commonly OpenH264FlagResetContext | ||
| // when the rectangle should reset the client's decoder state (e.g. | ||
| // on first frame, after a resolution change, or after any non-H.264 | ||
| // fallback rectangle). | ||
| func (c *Conn) WriteOpenH264Rect(r Rect, flags uint32, h264 []byte) error { | ||
| if r.Encoding != EncodingOpenH264 { | ||
| return fmt.Errorf("rfb: WriteOpenH264Rect called with encoding %d", r.Encoding) | ||
| } | ||
| if err := c.WriteRectHeader(r); err != nil { | ||
| return err | ||
| } | ||
| if err := c.writeU32(uint32(len(h264))); err != nil { | ||
| return err | ||
| } | ||
| if err := c.writeU32(flags); err != nil { | ||
| return err | ||
| } | ||
| return c.writeRaw(h264) | ||
| } | ||
|
|
||
| // WriteDesktopSizeRect emits a DesktopSize pseudo-encoding rectangle | ||
| // (-223). The framebuffer's new dimensions are encoded in the rect's | ||
| // W/H fields; X/Y are zero. There is no payload. | ||
| func (c *Conn) WriteDesktopSizeRect(width, height uint16) error { | ||
| r := Rect{X: 0, Y: 0, W: width, H: height, Encoding: EncodingDesktopSize} | ||
| return c.WriteRectHeader(r) | ||
| } | ||
|
|
||
| // WriteExtendedMouseButtonsAnnounceRect emits the fake 0×0 rectangle | ||
| // that confirms server-side support for the Extended Mouse Buttons | ||
| // extension (encoding -316). After writing it, callers should | ||
| // transition the connection's reader to extended mode via | ||
| // SetExtendedMouseButtons(true). | ||
| func (c *Conn) WriteExtendedMouseButtonsAnnounceRect() error { | ||
| r := Rect{X: 0, Y: 0, W: 0, H: 0, Encoding: EncodingExtendedMouseButtons} | ||
| return c.WriteRectHeader(r) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Data race on
extendedMouseButtonsbetween goroutinesMedium Severity
The
extendedMouseButtonsfield onrfb.Connis a plainboolwritten by the dispatch goroutine viaSetExtendedMouseButtons(true)and read concurrently by the read goroutine inReadClientMessage. This is a data race under Go's memory model — no synchronization (mutex, atomic, or channel) ensures the reader sees the updated value. If the reader sees a stalefalse, it will parse an extended 7-bytePointerEventas the legacy 6-byte format, consuming one byte too few and permanently misaligning all subsequent message parsing on that connection.Additional Locations (2)
internal/rfb/messages.go#L191-L192vnc_conn.go#L288-L289Reviewed by Cursor Bugbot for commit ba5cd0a. Configure here.