package main // tray.go — the parts of the tray that are not Windows. // // The systray plumbing has to live behind //go:build windows (tray_windows.go // explains why in full: the library needs cgo on darwin, which would end the // single-container CGO_ENABLED=0 build for all six release targets). But the // SHUTDOWN channel and the menu's wording are ordinary logic, and logic behind a // build tag is logic that is never tested, because the suite runs on Linux. import ( "fmt" "os" "os/exec" "sync" "time" ) // trayRefresh is how often the icon re-reads the machine's state. The heartbeat // is 60s and the liveness probe is a 400ms TCP connect per pairing, so five // seconds is responsive without being a busy loop on a laptop battery. // // It lives HERE, untagged, rather than beside the systray plumbing, because it // is also the rate at which an approval notification decides whether to fire — // and the rules in notify.go are stated in ticks of this. A cadence the test // suite cannot see is a cadence the rules cannot be tested against. const trayRefresh = 5 * time.Second // shutdownCh is closed when the user asks the daemon to stop from somewhere // other than a signal — today, "Stop the bastion" in the tray menu. It exists on // every platform so cmdRun's select needs no build tag; off Windows nothing // closes it and that case is simply never chosen. var ( shutdownOnce sync.Once shutdownCh = make(chan struct{}) ) // requestShutdown asks cmdRun to stop, exactly as Ctrl-C or SIGTERM does. Once // only — closing a closed channel panics, and a menu item can be clicked twice. func requestShutdown() { shutdownOnce.Do(func() { close(shutdownCh) }) } // openLocalUI is what every "take me to the page" affordance does: the tray's // menu item, and a click on an approval notification (notify_windows.go). // // One function because they must behave identically — both mint a one-use ticket // against the RUNNING daemon through localUIEntryURL (ui.go:591) rather than // building a URL, so neither can drift into handing out a link that does not // authenticate. `from` only names the caller in the log line. // // It is untagged so that a build off Windows still compiles it; nothing off // Windows calls it today. func openLocalUI(from string) { entry, _, err := localUIEntryURL() if err != nil { uilogf("%s: %v\n", from, err) return } if err := openBrowser(entry); err != nil { uilogf("%s: %v\n", from, err) } } // trayStateLine is the disabled label at the top of the tray menu: the one // sentence a person wants when they click the icon. // // "Not anchored" outranks the count on purpose. A networked box with no anchor // serves NOTHING (anchor.go), so reporting "Serving 3 hosts" there would be // true about pairings and a lie about the machine — and it is the exact state // that is otherwise invisible until some op fails far away. func trayStateLine(s uiStatus) string { switch { case len(s.Pairings) == 0: return "No host paired yet" case s.AnchorRequired: return "Not anchored — answering nothing" default: n := 0 for _, p := range s.Pairings { if p.Listening { n++ } } if n == len(s.Pairings) { return fmt.Sprintf("Serving %d host(s)", n) } return fmt.Sprintf("Serving %d of %d hosts", n, len(s.Pairings)) } } // openContextSurface starts the context surface and opens it (contextui.go). // // A SEPARATE PROCESS, DELIBERATELY, even though the tray is already running as // this person. contextui.go's whole security argument is that the walk is done by a // process running as the user, so the only files it can draw are the ones the // kernel already lets them open. Serving it from inside the daemon would work // on a one-person Windows desktop and would be wrong everywhere else — on a // networked box that daemon is root, and the same code would then be rendering // somebody's files under an authority they never granted. One path, right on // both machines. // // The child gets no console (hideConsole) and opens the browser itself, so the // person who clicked a tray item never sees a window they did not ask for. func openContextSurface(from string) { exe, err := os.Executable() if err != nil { uilogf("%s: %v\n", from, err) return } cmd := exec.Command(exe, "context") hideConsole(cmd) if err := cmd.Start(); err != nil { uilogf("%s: could not start the context surface: %v\n", from, err) return } // NOT Wait(): this runs on the tray's goroutine, and waiting here would // freeze the menu for as long as the person had their files open. The child // is a server with its own lifetime — it prints its address, opens a // browser, and exits when they close it. go func() { _ = cmd.Wait() }() } // trayKind is which tray, if any, a process puts up. type trayKind int const ( // trayNone: no icon from this process. trayNone trayKind = iota // trayInProcess: the daemon's own icon (tray_windows.go), for a bastion // that runs as the signed-in person. trayInProcess // trayPipe: the signed-in person's icon beside a system service // (tray_pipe_windows.go), which reads the service over the approvals pipe. trayPipe ) // trayFor is THE START RULE, decided here so the suite checks it off Windows. // // - `run` as the service: none. The service runs in session 0, which has no // desktop, so an icon there would be on a screen nobody sees. // - `run` beside an installed service: none. That is a second bastion the // guard refuses for an ordinary account (sysservice.go); an administrator // who runs it anyway gets no second icon. // - `run` with no service: the daemon's own icon, as before. // - `tray` with a service installed, not as the service: the person's icon, // over the pipe. This is what the logon entry starts for every person who // signs in (trayRunValue). // - `tray` with no service: none; the daemon's own icon is the tray there. // // Only Windows has a tray at all (tray_windows.go says why). func trayFor(goos string, asService, serviceInstalled bool, cmd string) trayKind { if goos != "windows" || asService { return trayNone } switch cmd { case "run": if serviceInstalled { return trayNone } return trayInProcess case "tray": if serviceInstalled { return trayPipe } } return trayNone } // trayRunKey and trayRunName are the machine-wide logon entry `service install` // writes: Windows starts it in every person's session at their sign-in, as // that person and unelevated, which is exactly who the tray must run as. const ( trayRunKey = `SOFTWARE\Microsoft\Windows\CurrentVersion\Run` trayRunName = "PrivataeBastionTray" ) // trayRunValue is that entry's command line: the service's own program, which // lives under Program Files where only administrators can change it, and the // tray verb. Quoted, because Program Files has a space in it. func trayRunValue(exe string) string { return `"` + exe + `" tray` } // trayApprovalsLine is the tray menu's approvals entry: the count of what is // waiting on the person, or that nothing is. Untagged so the words are tested // on the platform the suite runs on. func trayApprovalsLine(n int) string { switch n { case 0: return "No approvals waiting" case 1: return "1 approval waiting — open…" } return fmt.Sprintf("%d approvals waiting — open…", n) }