mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-11 07:59:08 +02:00
Refuse an ambiguous X display and settle the approval timeout race under one claim
Claude-Session: https://claude.ai/code/session_01QKDYfH4WKLbpNQHccpVo3P
This commit is contained in:
@@ -65,15 +65,32 @@ func detectX11Display() {
|
||||
return
|
||||
}
|
||||
|
||||
// Try /proc first (Linux), then ps fallback (FreeBSD and others).
|
||||
if detectX11FromProc() {
|
||||
// Try /proc first (Linux), then the socket scan (FreeBSD and others).
|
||||
switch detectX11FromProc() {
|
||||
case x11Detected:
|
||||
return
|
||||
}
|
||||
if detectX11FromSockets() {
|
||||
case x11Ambiguous:
|
||||
// /proc listed X servers and none of them could be shown to be the one
|
||||
// on the console. Falling through to the socket scan would only make
|
||||
// the same guess with less information, so stop here: no capture at all
|
||||
// beats capturing a session that may not be the caller's.
|
||||
return
|
||||
case x11NotFound:
|
||||
}
|
||||
detectX11FromSockets()
|
||||
}
|
||||
|
||||
// x11Detection is the outcome of one display-detection pass. Ambiguous is
|
||||
// deliberately distinct from NotFound: it means candidates existed and were
|
||||
// refused, which must not be retried by a less discriminating fallback.
|
||||
type x11Detection int
|
||||
|
||||
const (
|
||||
x11NotFound x11Detection = iota
|
||||
x11Detected
|
||||
x11Ambiguous
|
||||
)
|
||||
|
||||
// xorgCandidate is one X server found in /proc, with the pieces that decide
|
||||
// whether it is the one a remote user should be attached to.
|
||||
type xorgCandidate struct {
|
||||
@@ -94,10 +111,10 @@ type xorgCandidate struct {
|
||||
// delivering the remote user's input to it. /sys/class/tty/tty0/active names the
|
||||
// session actually on the console and X records its own VT in argv, so the two
|
||||
// can be matched.
|
||||
func detectX11FromProc() bool {
|
||||
func detectX11FromProc() x11Detection {
|
||||
entries, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return false
|
||||
return x11NotFound
|
||||
}
|
||||
|
||||
var candidates []xorgCandidate
|
||||
@@ -117,40 +134,69 @@ func detectX11FromProc() bool {
|
||||
candidates = append(candidates, xorgCandidate{display: display, auth: auth, vt: parseXorgVT(args)})
|
||||
}
|
||||
|
||||
best, ok := pickXorgCandidate(candidates, activeVT())
|
||||
if !ok {
|
||||
return false
|
||||
best, outcome := pickXorgCandidate(candidates, activeVT())
|
||||
if outcome != x11Detected {
|
||||
return outcome
|
||||
}
|
||||
setDisplayEnv(best.display, best.auth)
|
||||
return true
|
||||
return x11Detected
|
||||
}
|
||||
|
||||
// pickXorgCandidate chooses which X server to attach to: the one on the active
|
||||
// VT when that is known, and otherwise the lowest display number, so the choice
|
||||
// is at least stable across runs instead of following readdir order.
|
||||
func pickXorgCandidate(candidates []xorgCandidate, activeVT int) (xorgCandidate, bool) {
|
||||
// pickXorgCandidate chooses which X server to attach to.
|
||||
//
|
||||
// The one on the active VT wins outright. Failing that the choice has to be
|
||||
// unambiguous, because guessing wrong means handing a remote user another local
|
||||
// user's screen and keyboard: a single candidate is taken, and anything else is
|
||||
// refused. The lowest display number only breaks ties among servers that all
|
||||
// record no VT, where there is no active-session signal to go on at all.
|
||||
func pickXorgCandidate(candidates []xorgCandidate, activeVT int) (xorgCandidate, x11Detection) {
|
||||
if len(candidates) == 0 {
|
||||
return xorgCandidate{}, false
|
||||
return xorgCandidate{}, x11NotFound
|
||||
}
|
||||
if activeVT > 0 {
|
||||
for _, c := range candidates {
|
||||
if c.vt == activeVT {
|
||||
return c, true
|
||||
return c, x11Detected
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(candidates) == 1 {
|
||||
return candidates[0], x11Detected
|
||||
}
|
||||
|
||||
best := candidates[0]
|
||||
for _, c := range candidates[1:] {
|
||||
// Several servers and no way to tell which is on the console. One that
|
||||
// records a VT is on some seat other than the active one, so it is a
|
||||
// session this connection has no business showing.
|
||||
var vtless []xorgCandidate
|
||||
for _, c := range candidates {
|
||||
if c.vt < 0 {
|
||||
vtless = append(vtless, c)
|
||||
}
|
||||
}
|
||||
if len(vtless) == 0 {
|
||||
log.Warnf("found %d X servers, all on virtual terminals and none of them the active one (%s); not attaching to any",
|
||||
len(candidates), describeActiveVT(activeVT))
|
||||
return xorgCandidate{}, x11Ambiguous
|
||||
}
|
||||
|
||||
best := vtless[0]
|
||||
for _, c := range vtless[1:] {
|
||||
if displayNumber(c.display) < displayNumber(best.display) {
|
||||
best = c
|
||||
}
|
||||
}
|
||||
if len(candidates) > 1 {
|
||||
log.Warnf("found %d X servers and none on the active VT (%d); attaching to DISPLAY=%s",
|
||||
len(candidates), activeVT, best.display)
|
||||
log.Warnf("found %d X servers and none on the active VT (%s); attaching to the lowest VT-less display %s",
|
||||
len(candidates), describeActiveVT(activeVT), best.display)
|
||||
return best, x11Detected
|
||||
}
|
||||
|
||||
// describeActiveVT renders the active VT for a log line, keeping "we could not
|
||||
// read it" distinct from a VT number that simply matched nothing.
|
||||
func describeActiveVT(vt int) string {
|
||||
if vt <= 0 {
|
||||
return "unknown"
|
||||
}
|
||||
return best, true
|
||||
return "tty" + strconv.Itoa(vt)
|
||||
}
|
||||
|
||||
// activeVT reports the virtual terminal currently on the console, or -1 when
|
||||
|
||||
@@ -11,25 +11,26 @@ import (
|
||||
|
||||
// TestPickXorgCandidate covers which X server a service-mode capturer attaches
|
||||
// to when several are running. Picking the wrong one shows a remote user
|
||||
// another local user's screen, so the active VT wins whenever it is known.
|
||||
// another local user's screen and delivers their input to it, so the active VT
|
||||
// wins whenever it is known and an unresolvable choice is refused outright.
|
||||
func TestPickXorgCandidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
candidates []xorgCandidate
|
||||
activeVT int
|
||||
want string
|
||||
wantOK bool
|
||||
name string
|
||||
candidates []xorgCandidate
|
||||
activeVT int
|
||||
want string
|
||||
wantOutcome x11Detection
|
||||
}{
|
||||
{
|
||||
name: "no X server found",
|
||||
wantOK: false,
|
||||
name: "no X server found",
|
||||
wantOutcome: x11NotFound,
|
||||
},
|
||||
{
|
||||
name: "single server is used whatever its VT",
|
||||
candidates: []xorgCandidate{{display: ":3", vt: 9}},
|
||||
activeVT: 2,
|
||||
want: ":3",
|
||||
wantOK: true,
|
||||
name: "single server is used whatever its VT",
|
||||
candidates: []xorgCandidate{{display: ":3", vt: 9}},
|
||||
activeVT: 2,
|
||||
want: ":3",
|
||||
wantOutcome: x11Detected,
|
||||
},
|
||||
{
|
||||
name: "active VT wins over the lower display number",
|
||||
@@ -37,19 +38,37 @@ func TestPickXorgCandidate(t *testing.T) {
|
||||
{display: ":0", vt: 2},
|
||||
{display: ":1", vt: 7},
|
||||
},
|
||||
activeVT: 7,
|
||||
want: ":1",
|
||||
wantOK: true,
|
||||
activeVT: 7,
|
||||
want: ":1",
|
||||
wantOutcome: x11Detected,
|
||||
},
|
||||
{
|
||||
name: "unknown active VT falls back to the lowest display",
|
||||
name: "several servers on other VTs are refused, not guessed at",
|
||||
candidates: []xorgCandidate{
|
||||
{display: ":1", vt: 7},
|
||||
{display: ":0", vt: 2},
|
||||
{display: ":1", vt: 3},
|
||||
},
|
||||
activeVT: -1,
|
||||
want: ":0",
|
||||
wantOK: true,
|
||||
activeVT: 7,
|
||||
wantOutcome: x11Ambiguous,
|
||||
},
|
||||
{
|
||||
name: "unknown active VT does not license a guess between seats",
|
||||
candidates: []xorgCandidate{
|
||||
{display: ":0", vt: 2},
|
||||
{display: ":1", vt: 3},
|
||||
},
|
||||
activeVT: -1,
|
||||
wantOutcome: x11Ambiguous,
|
||||
},
|
||||
{
|
||||
name: "a VT-less server is preferred over one on an inactive VT",
|
||||
candidates: []xorgCandidate{
|
||||
{display: ":0", vt: 2},
|
||||
{display: ":99", vt: -1},
|
||||
},
|
||||
activeVT: 7,
|
||||
want: ":99",
|
||||
wantOutcome: x11Detected,
|
||||
},
|
||||
{
|
||||
name: "display numbers compare numerically, not lexically",
|
||||
@@ -57,9 +76,9 @@ func TestPickXorgCandidate(t *testing.T) {
|
||||
{display: ":10", vt: -1},
|
||||
{display: ":2", vt: -1},
|
||||
},
|
||||
activeVT: -1,
|
||||
want: ":2",
|
||||
wantOK: true,
|
||||
activeVT: -1,
|
||||
want: ":2",
|
||||
wantOutcome: x11Detected,
|
||||
},
|
||||
{
|
||||
name: "an unparseable display is only picked when it is alone",
|
||||
@@ -67,17 +86,18 @@ func TestPickXorgCandidate(t *testing.T) {
|
||||
{display: ":bogus", vt: -1},
|
||||
{display: ":4", vt: -1},
|
||||
},
|
||||
activeVT: -1,
|
||||
want: ":4",
|
||||
wantOK: true,
|
||||
activeVT: -1,
|
||||
want: ":4",
|
||||
wantOutcome: x11Detected,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, ok := pickXorgCandidate(tc.candidates, tc.activeVT)
|
||||
require.Equal(t, tc.wantOK, ok)
|
||||
if !tc.wantOK {
|
||||
got, outcome := pickXorgCandidate(tc.candidates, tc.activeVT)
|
||||
require.Equal(t, tc.wantOutcome, outcome)
|
||||
if tc.wantOutcome != x11Detected {
|
||||
assert.Empty(t, got.display, "a refused choice must not leak a display")
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tc.want, got.display)
|
||||
@@ -92,3 +112,11 @@ func TestParseXorgVT(t *testing.T) {
|
||||
assert.Equal(t, -1, parseXorgVT([]string{"/usr/bin/Xvfb", ":99", "-screen", "0", "1920x1080x24"}))
|
||||
assert.Equal(t, -1, parseXorgVT([]string{"/usr/lib/Xorg", ":0", "vtconsole"}))
|
||||
}
|
||||
|
||||
// TestDescribeActiveVT keeps "we could not read the active VT" legible in the
|
||||
// log line, rather than printing a -1 that reads like a real terminal.
|
||||
func TestDescribeActiveVT(t *testing.T) {
|
||||
assert.Equal(t, "unknown", describeActiveVT(-1))
|
||||
assert.Equal(t, "unknown", describeActiveVT(0))
|
||||
assert.Equal(t, "tty7", describeActiveVT(7))
|
||||
}
|
||||
|
||||
@@ -30,13 +30,17 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
|
||||
// cannot produce one, or the sprite failed to encode. Say which, once per
|
||||
// session, so the next report of a missing cursor names its own cause.
|
||||
if !supported || failed || composite {
|
||||
s.logCursorSkip("no cursor rect: client_requested=%v source_failed=%v compositing=%v",
|
||||
// The three flags form the key so a client that negotiates the encoding
|
||||
// mid-session, or a source that starts failing, still gets a line: eight
|
||||
// combinations at most.
|
||||
s.logCursorSkip(fmt.Sprintf("state:%t%t%t", supported, failed, composite),
|
||||
"no cursor rect: client_requested=%v source_failed=%v compositing=%v",
|
||||
supported, failed, composite)
|
||||
return nil
|
||||
}
|
||||
src, ok := s.capturer.(cursorSource)
|
||||
if !ok {
|
||||
s.logCursorSkip("no cursor rect: capturer %T reports no cursor source", s.capturer)
|
||||
s.logCursorSkip("no-source", "no cursor rect: capturer %T reports no cursor source", s.capturer)
|
||||
return nil
|
||||
}
|
||||
img, hotX, hotY, serial, err := src.Cursor()
|
||||
@@ -48,7 +52,7 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
|
||||
return nil
|
||||
}
|
||||
if img == nil {
|
||||
s.logCursorSkip("no cursor rect: capturer returned no sprite")
|
||||
s.logCursorSkip("no-sprite", "no cursor rect: capturer returned no sprite")
|
||||
return nil
|
||||
}
|
||||
if serial == lastSerial {
|
||||
@@ -57,7 +61,10 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
|
||||
buf := encodeCursorPseudoRect(img, hotX, hotY, pf)
|
||||
if buf == nil {
|
||||
b := img.Bounds()
|
||||
s.logCursorSkip("no cursor rect: sprite %dx%d stride=%d pix=%d could not be encoded",
|
||||
// Fixed key, varying detail: a source returning junk dimensions must not
|
||||
// be able to mint a new throttle entry per frame.
|
||||
s.logCursorSkip("encode-failed",
|
||||
"no cursor rect: sprite %dx%d stride=%d pix=%d could not be encoded",
|
||||
b.Dx(), b.Dy(), img.Stride, len(img.Pix))
|
||||
return nil
|
||||
}
|
||||
@@ -83,25 +90,28 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
|
||||
}
|
||||
|
||||
// logCursorSkip reports why this update carries no cursor rect, once per
|
||||
// distinct line. pendingCursorRect runs per framebuffer update, so an
|
||||
// unthrottled log would flood; throttling once per session instead would let
|
||||
// whichever reason came first hide every later one, and the reasons do change
|
||||
// as the client negotiates encodings and the cursor source starts failing.
|
||||
func (s *session) logCursorSkip(format string, args ...any) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
|
||||
// reason. pendingCursorRect runs per framebuffer update, so an unthrottled log
|
||||
// would flood; throttling once per session instead would let whichever reason
|
||||
// came first hide every later one, and the reasons do change as the client
|
||||
// negotiates encodings and the cursor source starts failing.
|
||||
//
|
||||
// reason keys the throttle and must come from a fixed set, never from the
|
||||
// message: a cursor source handing back varying junk dimensions would otherwise
|
||||
// mint a new key per frame, growing the map for the life of the session and
|
||||
// logging every one of them.
|
||||
func (s *session) logCursorSkip(reason, format string, args ...any) {
|
||||
s.cursorSkipMu.Lock()
|
||||
if s.cursorSkipSeen == nil {
|
||||
s.cursorSkipSeen = make(map[string]struct{})
|
||||
}
|
||||
_, seen := s.cursorSkipSeen[msg]
|
||||
s.cursorSkipSeen[msg] = struct{}{}
|
||||
_, seen := s.cursorSkipSeen[reason]
|
||||
s.cursorSkipSeen[reason] = struct{}{}
|
||||
s.cursorSkipMu.Unlock()
|
||||
|
||||
if seen {
|
||||
return
|
||||
}
|
||||
s.log.Debug(msg)
|
||||
s.log.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// maxCursorDim caps the cursor sprite size we'll encode. Real platform
|
||||
|
||||
Reference in New Issue
Block a user