Files
netbird/client/vnc/server/capture_fb_linux.go

288 lines
8.6 KiB
Go

//go:build linux && !android
package server
import (
"encoding/binary"
"errors"
"fmt"
"image"
"sync"
"unsafe"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
// Linux framebuffer ioctls (linux/fb.h).
const (
fbioGetVScreenInfo = 0x4600
fbioGetFScreenInfo = 0x4602
)
func defaultFBPath() string { return "/dev/fb0" }
// fbVarScreenInfo mirrors the kernel's fb_var_screeninfo. Only the
// fields we use are mapped; the rest are absorbed into _padN.
type fbVarScreenInfo struct {
Xres, Yres uint32
XresVirtual, YresVirtual uint32
XOffset, YOffset uint32
BitsPerPixel uint32
Grayscale uint32
RedOffset, RedLen, RedMSBR uint32
GreenOffset, GreenLen, GreenMSBR uint32
BlueOffset, BlueLen, BlueMSBR uint32
TranspOffset, TranspLen, TranspM uint32
NonStd uint32
Activate uint32
Height, Width uint32
AccelFlags uint32
PixClock uint32
LeftMargin, RightMargin uint32
UpperMargin, LowerMargin uint32
HsyncLen, VsyncLen uint32
Sync uint32
Vmode uint32
Rotate uint32
Colorspace uint32
_pad [4]uint32
}
// fbFixScreenInfo mirrors fb_fix_screeninfo. We only need LineLength.
//
// SmemStart and MmioStart are `unsigned long` in the kernel header, so they are
// pointer-sized: uintptr, not uint64. Spelling them uint64 would shift every
// field after SmemStart by four bytes on a 32-bit build and make LineLength
// read the wrong part of the ioctl's answer.
type fbFixScreenInfo struct {
IDStr [16]byte
SmemStart uintptr
SmemLen uint32
Type uint32
TypeAux uint32
Visual uint32
XPanStep uint16
YPanStep uint16
YWrapStep uint16
_pad0 uint16
LineLength uint32
MmioStart uintptr
MmioLen uint32
Accel uint32
Capabilities uint16
_reserved [2]uint16
}
// FBCapturer reads pixels straight from the Linux framebuffer device.
// Used as a fallback when X11 isn't available, e.g. on a headless box at
// the kernel console or the display manager's pre-login screen on machines
// without an Xorg server. The framebuffer must be mmap()-able under our
// process privileges (typically the netbird service runs as root).
type FBCapturer struct {
mu sync.Mutex
path string
fd int
mmap []byte
w, h int
bpp int
stride int
rOff uint32
gOff uint32
bOff uint32
rLen uint32
gLen uint32
bLen uint32
closeOnce sync.Once
}
// NewFBCapturer opens the given framebuffer device (/dev/fbN) and
// queries its current geometry + pixel format.
func NewFBCapturer(path string) (*FBCapturer, error) {
if path == "" {
path = "/dev/fb0"
}
fd, err := unix.Open(path, unix.O_RDONLY, 0)
if err != nil {
return nil, fmt.Errorf("open %s: %w", path, err)
}
var vinfo fbVarScreenInfo
if _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), fbioGetVScreenInfo, uintptr(unsafe.Pointer(&vinfo))); e != 0 {
unix.Close(fd)
return nil, fmt.Errorf("FBIOGET_VSCREENINFO: %v", e)
}
var finfo fbFixScreenInfo
if _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), fbioGetFScreenInfo, uintptr(unsafe.Pointer(&finfo))); e != 0 {
unix.Close(fd)
return nil, fmt.Errorf("FBIOGET_FSCREENINFO: %v", e)
}
bpp := int(vinfo.BitsPerPixel)
if bpp != 16 && bpp != 24 && bpp != 32 {
unix.Close(fd)
return nil, fmt.Errorf("unsupported framebuffer bpp: %d", bpp)
}
if err := validateFBLayout(bpp, &vinfo); err != nil {
unix.Close(fd)
return nil, err
}
size := int(finfo.LineLength) * int(vinfo.Yres)
if size <= 0 {
unix.Close(fd)
return nil, fmt.Errorf("invalid framebuffer dimensions: stride=%d h=%d", finfo.LineLength, vinfo.Yres)
}
mm, err := unix.Mmap(fd, 0, size, unix.PROT_READ, unix.MAP_SHARED)
if err != nil {
unix.Close(fd)
return nil, fmt.Errorf("mmap %s: %w", path, err)
}
c := &FBCapturer{
path: path,
fd: fd,
mmap: mm,
w: int(vinfo.Xres),
h: int(vinfo.Yres),
bpp: bpp,
stride: int(finfo.LineLength),
rOff: vinfo.RedOffset,
gOff: vinfo.GreenOffset,
bOff: vinfo.BlueOffset,
rLen: vinfo.RedLen,
gLen: vinfo.GreenLen,
bLen: vinfo.BlueLen,
}
log.Infof("framebuffer capturer ready: %s %dx%d bpp=%d r=%d/%d g=%d/%d b=%d/%d",
path, c.w, c.h, c.bpp, c.rOff, c.rLen, c.gOff, c.gLen, c.bOff, c.bLen)
return c, nil
}
// validateFBLayout refuses a pixel layout the swizzlers do not implement.
//
// Each swizzler is written for one layout: 32bpp reads whole channels at the
// queried offsets, 24bpp reads packed B,G,R triplets, and 16bpp reads RGB565.
// A device reporting anything else (RGB888 at 24bpp, BGR565, a 10-bit channel)
// would be swizzled into wrong colours, which is worse than declining to
// capture at all, because nothing downstream can tell that it happened.
func validateFBLayout(bpp int, v *fbVarScreenInfo) error {
unsupported := func() error {
return fmt.Errorf("unsupported %dbpp framebuffer layout: r=%d/%d g=%d/%d b=%d/%d",
bpp, v.RedOffset, v.RedLen, v.GreenOffset, v.GreenLen, v.BlueOffset, v.BlueLen)
}
// msb_right marks a channel whose bits run the other way inside the pixel.
// Every swizzler reads them in normal order, so such a device would be
// decoded into mirrored channel values.
if v.RedMSBR != 0 || v.GreenMSBR != 0 || v.BlueMSBR != 0 {
return fmt.Errorf("unsupported %dbpp framebuffer layout: msb_right set (r=%d g=%d b=%d)",
bpp, v.RedMSBR, v.GreenMSBR, v.BlueMSBR)
}
switch bpp {
case 32:
// Offsets are honoured, channel widths are not.
if v.RedLen != 8 || v.GreenLen != 8 || v.BlueLen != 8 {
return unsupported()
}
case 24:
if v.RedLen != 8 || v.GreenLen != 8 || v.BlueLen != 8 ||
v.BlueOffset != 0 || v.GreenOffset != 8 || v.RedOffset != 16 {
return unsupported()
}
case 16:
if v.RedOffset != 11 || v.RedLen != 5 ||
v.GreenOffset != 5 || v.GreenLen != 6 ||
v.BlueOffset != 0 || v.BlueLen != 5 {
return unsupported()
}
}
return nil
}
// Width returns the framebuffer width in pixels.
func (c *FBCapturer) Width() int { return c.w }
// Height returns the framebuffer height in pixels.
func (c *FBCapturer) Height() int { return c.h }
// Capture allocates a fresh image and fills it with the current
// framebuffer contents.
func (c *FBCapturer) Capture() (*image.RGBA, error) {
img := image.NewRGBA(image.Rect(0, 0, c.w, c.h))
if err := c.CaptureInto(img); err != nil {
return nil, err
}
return img, nil
}
// CaptureInto reads the framebuffer directly into dst.Pix.
func (c *FBCapturer) CaptureInto(dst *image.RGBA) error {
c.mu.Lock()
defer c.mu.Unlock()
// Close unmaps but leaves w/h in place, so a capture arriving afterwards
// would pass the size check below and index into a nil mapping.
if c.mmap == nil {
return errors.New("framebuffer capturer is closed")
}
if dst.Rect.Dx() != c.w || dst.Rect.Dy() != c.h {
return fmt.Errorf("dst size mismatch: dst=%dx%d fb=%dx%d",
dst.Rect.Dx(), dst.Rect.Dy(), c.w, c.h)
}
switch c.bpp {
case 32:
swizzleFB32(dst.Pix, dst.Stride, c.mmap, c.stride, c.w, c.h, channelShifts{R: c.rOff, G: c.gOff, B: c.bOff})
case 24:
swizzleFB24(dst.Pix, dst.Stride, c.mmap, c.stride, c.w, c.h)
case 16:
swizzleFB16RGB565(dst.Pix, dst.Stride, c.mmap, c.stride, c.w, c.h)
}
return nil
}
// Close releases the framebuffer mmap and file descriptor. Serialized with
// CaptureInto via c.mu so an in-flight capture can't read freed memory.
func (c *FBCapturer) Close() {
c.closeOnce.Do(func() {
c.mu.Lock()
defer c.mu.Unlock()
if c.mmap != nil {
_ = unix.Munmap(c.mmap)
c.mmap = nil
}
if c.fd >= 0 {
_ = unix.Close(c.fd)
c.fd = -1
}
})
}
// channelShifts groups the bit offsets for the R/G/B channels in a packed
// uint32 framebuffer pixel. Bundling avoids drowning per-row callers in a
// 9-parameter signature.
type channelShifts struct {
R, G, B uint32
}
// swizzleFB32 handles 32-bit framebuffers with arbitrary R/G/B channel
// offsets. Pulls one pixel per uint32, then masks each channel into the
// destination RGBA byte order.
func swizzleFB32(dst []byte, dstStride int, src []byte, srcStride, w, h int, shifts channelShifts) {
for y := 0; y < h; y++ {
srcRow := src[y*srcStride : y*srcStride+w*4]
dstRow := dst[y*dstStride:]
for x := 0; x < w; x++ {
pix := binary.LittleEndian.Uint32(srcRow[x*4 : x*4+4])
dstRow[x*4+0] = byte(pix >> shifts.R)
dstRow[x*4+1] = byte(pix >> shifts.G)
dstRow[x*4+2] = byte(pix >> shifts.B)
dstRow[x*4+3] = 0xff
}
}
}