All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m15s
387 lines
12 KiB
Go
387 lines
12 KiB
Go
//go:build linux
|
|
|
|
package blocker
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var systemAllowed = map[string]bool{
|
|
// ── Display servers / Compositors ─────────────────────────────────────────
|
|
"gnome-shell": true,
|
|
"mutter": true,
|
|
"mutter-x11-fram": true, // prefix match too
|
|
"xwayland": true,
|
|
"xorg": true,
|
|
"Xorg": true,
|
|
"x11": true,
|
|
"kwin_wayland": true, // KDE compositor
|
|
"kwin_x11": true,
|
|
"plasmashell": true, // KDE Plasma shell
|
|
"hyprland": true, // Hyprland Wayland compositor
|
|
"sway": true, // Sway compositor
|
|
"wayfire": true, // Wayfire compositor
|
|
"river": true, // River compositor
|
|
"labwc": true, // LabWC compositor
|
|
"openbox": true,
|
|
"i3": true,
|
|
"i3bar": true,
|
|
"awesome": true,
|
|
"bspwm": true,
|
|
"xfwm4": true,
|
|
"marco": true, // MATE wm
|
|
"compiz": true,
|
|
"picom": true,
|
|
"compton": true,
|
|
|
|
// ── Wayland session tools ─────────────────────────────────────────────────
|
|
"wl-paste": true,
|
|
"wl-copy": true,
|
|
"wlr-randr": true,
|
|
"kanshi": true, // output manager
|
|
"wlsunset": true,
|
|
"gammastep": true,
|
|
"swaylock": true,
|
|
"swayidle": true,
|
|
"swaybg": true,
|
|
"swaync": true, // SwayNotificationCenter
|
|
"waybar": true, // Wayland statusbar
|
|
"eww": true, // ElKowar's wacky widgets
|
|
"ags": true, // Aylur's GTK Shell
|
|
"rofi": true, // app launcher (used in many WMs)
|
|
"wofi": true, // Wayland rofi
|
|
"fuzzel": true, // Wayland launcher
|
|
"tofi": true,
|
|
"dmenu": true,
|
|
"bemenu": true,
|
|
|
|
// ── Session / Login managers ──────────────────────────────────────────────
|
|
"gdm": true,
|
|
"gdm3": true,
|
|
"sddm": true,
|
|
"lightdm": true,
|
|
"lxdm": true,
|
|
"slim": true,
|
|
"greetd": true,
|
|
"gnome-session": true,
|
|
"gnome-session-bi": true, // prefix matches binary
|
|
"lxsession": true,
|
|
"startx": true,
|
|
"xinit": true,
|
|
|
|
// ── Terminals ─────────────────────────────────────────────────────────────
|
|
"xterm": true,
|
|
"gnome-terminal": true,
|
|
"gnome-terminal-": true, // prefix for server process
|
|
"ptyxis": true,
|
|
"konsole": true,
|
|
"kitty": true,
|
|
"alacritty": true,
|
|
"wezterm": true,
|
|
"wezterm-gui": true,
|
|
"foot": true,
|
|
"xfce4-terminal": true,
|
|
"tilix": true,
|
|
"terminator": true,
|
|
"urxvt": true,
|
|
"rxvt": true,
|
|
"sakura": true,
|
|
"st": true,
|
|
|
|
// ── Shells ────────────────────────────────────────────────────────────────
|
|
"bash": true,
|
|
"zsh": true,
|
|
"sh": true,
|
|
"fish": true,
|
|
"dash": true,
|
|
"ksh": true,
|
|
|
|
// ── Input methods (critical — killing these breaks Vietnamese typing) ─────
|
|
"ibus-daemon": true,
|
|
"ibus-x11": true,
|
|
"ibus-": true, // prefix: ibus-extension-, ibus-portal, ibus-engine-...
|
|
"fcitx5": true,
|
|
"fcitx": true,
|
|
"fcitx-": true, // prefix
|
|
"uim": true,
|
|
"scim": true,
|
|
"sogou-qimpanel": true,
|
|
"gcin": true,
|
|
"kimpanel": true,
|
|
|
|
// ── GNOME core services ───────────────────────────────────────────────────
|
|
"gjs": true,
|
|
"gsd-": true, // prefix: gsd-keyboard, gsd-media-keys, gsd-power, gsd-color...
|
|
"goa-daemon": true,
|
|
"goa-identity-ser": true,
|
|
"evolution-": true, // prefix: evolution-calendar, evolution-addressbook...
|
|
"gnome-keyring-d": true,
|
|
"gnome-keyring": true,
|
|
"polkit-gnome-au": true,
|
|
"polkitd": true,
|
|
"gnome-settings-d": true,
|
|
"gnome-initial-se": true,
|
|
"gnome-control-ce": true,
|
|
"gvfsd": true,
|
|
"gvfsd-": true, // prefix
|
|
"tracker-miner-": true, // prefix
|
|
"tracker3": true,
|
|
"zeitgeist": true,
|
|
"zeitgeist-": true,
|
|
"accounts-daemon": true,
|
|
"colord": true,
|
|
"power-profiles-": true,
|
|
"fprintd": true,
|
|
"fwupd": true,
|
|
"udisksd": true,
|
|
"upowerd": true,
|
|
"packagekitd": true,
|
|
"nm-dispatcher": true,
|
|
|
|
// ── D-Bus / XDG / AT-SPI ─────────────────────────────────────────────────
|
|
"xdg-": true, // prefix: xdg-desktop-portal, xdg-permission-store...
|
|
"at-spi": true, // prefix
|
|
"at-spi-bus-laun": true,
|
|
"at-spi2-registr": true,
|
|
"dbus-daemon": true,
|
|
"dbus-launch": true,
|
|
|
|
// ── System services (often have GTK tray icons) ───────────────────────────
|
|
"systemd": true, // prefix match handles systemd-*
|
|
"snapd-": true, // prefix
|
|
"snapd": true,
|
|
|
|
// ── Network/Bluetooth tray (critical for connectivity UI) ─────────────────
|
|
"nm-applet": true, // NetworkManager tray — if killed, students lose wifi UI
|
|
"nm-tray": true,
|
|
"network-manager-": true, // prefix
|
|
"blueman-applet": true, // Bluetooth tray — kills BT management
|
|
"blueman-tray": true,
|
|
"blueman-manager": true,
|
|
"kdeconnectd": true, // KDE Connect daemon
|
|
"kdeconnect-indi": true, // KDE Connect indicator
|
|
|
|
// ── Notification daemons ──────────────────────────────────────────────────
|
|
"dunst": true,
|
|
"mako": true,
|
|
"notify-osd": true,
|
|
"xfce4-notifyd": true,
|
|
"fnott": true,
|
|
|
|
// ── Polkit authentication agents ─────────────────────────────────────────
|
|
"lxqt-policykit-": true,
|
|
"xfce-polkit": true,
|
|
"mate-polkit": true,
|
|
"pkttyagent": true,
|
|
|
|
// ── Clipboard managers (killing breaks copy/paste) ────────────────────────
|
|
"copyq": true,
|
|
"clipit": true,
|
|
"xclip": true,
|
|
"xsel": true,
|
|
"clipman": true,
|
|
"greenclip": true,
|
|
|
|
// ── GTK/GNOME image helpers ───────────────────────────────────────────────
|
|
"glycin": true,
|
|
"zenity": true,
|
|
"yad": true,
|
|
"kdialog": true,
|
|
|
|
// ── Screensaver / Lock ────────────────────────────────────────────────────
|
|
"gnome-screensave": true,
|
|
"xscreensaver": true,
|
|
"xlock": true,
|
|
"i3lock": true,
|
|
|
|
// ── WebKit subprocesses (used by many GTK apps) ───────────────────────────
|
|
"webkit": true, // prefix
|
|
"webkit2gtk": true,
|
|
"WebKitWebProcess": true,
|
|
"WebKitNetworkPro": true, // prefix
|
|
|
|
// ── Remote support tools (safety) ────────────────────────────────────────
|
|
"rustdesk": true,
|
|
"rustdesk-bin": true,
|
|
"anydesk": true,
|
|
"teamviewer": true,
|
|
"remmina": true,
|
|
|
|
// ── Our app + dev tools ───────────────────────────────────────────────────
|
|
"wails": true,
|
|
"code": true,
|
|
"cursor": true,
|
|
"windsurf": true,
|
|
"goland": true,
|
|
"idea": true,
|
|
"client": true,
|
|
"simple_care_v1.0": true,
|
|
"simple_care_v1.1": true,
|
|
"simple_care": true,
|
|
}
|
|
|
|
func isSystemAllowed(name string) bool {
|
|
name = strings.ToLower(name)
|
|
if systemAllowed[name] {
|
|
return true
|
|
}
|
|
for pattern := range systemAllowed {
|
|
if strings.HasPrefix(name, pattern) && pattern != name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getPPID(pid uint32) uint32 {
|
|
statusBytes, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
lines := strings.Split(string(statusBytes), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "PPid:") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
if ppid, err := strconv.ParseUint(parts[1], 10, 32); err == nil {
|
|
return uint32(ppid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func isDescendantOf(pid, targetPid uint32) bool {
|
|
curr := pid
|
|
for i := 0; i < 10; i++ { // limits lookup to 10 ancestor levels
|
|
ppid := getPPID(curr)
|
|
if ppid == 0 {
|
|
return false
|
|
}
|
|
if ppid == targetPid {
|
|
return true
|
|
}
|
|
curr = ppid
|
|
}
|
|
return false
|
|
}
|
|
|
|
type ProcessInfo struct {
|
|
PID uint32
|
|
Name string
|
|
}
|
|
|
|
func getVisibleProcesses() (map[uint32]ProcessInfo, error) {
|
|
files, err := os.ReadDir("/proc")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
procs := make(map[uint32]ProcessInfo)
|
|
for _, f := range files {
|
|
if !f.IsDir() {
|
|
continue
|
|
}
|
|
pid, err := strconv.ParseUint(f.Name(), 10, 32)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
mapsPath := fmt.Sprintf("/proc/%d/maps", pid)
|
|
mapsBytes, err := os.ReadFile(mapsPath)
|
|
if err != nil {
|
|
// Skip processes we don't own (permission denied)
|
|
continue
|
|
}
|
|
|
|
mapsStr := string(mapsBytes)
|
|
isGUI := strings.Contains(mapsStr, "libgtk") ||
|
|
strings.Contains(mapsStr, "libQt") ||
|
|
strings.Contains(mapsStr, "libX11") ||
|
|
strings.Contains(mapsStr, "libwayland-client")
|
|
|
|
if !isGUI {
|
|
continue
|
|
}
|
|
|
|
// Read process name from /proc/PID/comm
|
|
commBytes, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
procName := strings.TrimSpace(string(commBytes))
|
|
|
|
if procName != "" {
|
|
procs[uint32(pid)] = ProcessInfo{
|
|
PID: uint32(pid),
|
|
Name: procName,
|
|
}
|
|
}
|
|
}
|
|
|
|
return procs, nil
|
|
}
|
|
|
|
func (b *Blocker) checkAndKill() {
|
|
b.mu.Lock()
|
|
keywords := make([]string, len(b.allowedKeywords))
|
|
copy(keywords, b.allowedKeywords)
|
|
b.mu.Unlock()
|
|
|
|
if len(keywords) == 0 {
|
|
return
|
|
}
|
|
|
|
currentExec := ""
|
|
if execPath, err := os.Executable(); err == nil {
|
|
currentExec = strings.ToLower(filepath.Base(execPath))
|
|
}
|
|
|
|
procs, err := getVisibleProcesses()
|
|
if err != nil {
|
|
log.Printf("[BLOCKER] Failed to get visible processes: %v", err)
|
|
return
|
|
}
|
|
|
|
myPid := uint32(os.Getpid())
|
|
for pid, info := range procs {
|
|
pNameLower := strings.ToLower(info.Name)
|
|
|
|
// 1. Always allow our app, our sub-processes, system/critical developer tools, or agent helpers
|
|
isOurSubprocess := pid == myPid || isDescendantOf(pid, myPid)
|
|
if isOurSubprocess || (currentExec != "" && pNameLower == currentExec) || isSystemAllowed(pNameLower) {
|
|
continue
|
|
}
|
|
|
|
// 2. Check if the process name contains any allowed keywords
|
|
allowed := false
|
|
for _, kw := range keywords {
|
|
if matchesAllowedKeyword(kw, pNameLower, pNameLower) {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// 3. If not allowed, kill the application
|
|
if !allowed {
|
|
if b.OnBlocked != nil {
|
|
b.OnBlocked(info.Name, info.Name)
|
|
}
|
|
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d)", info.Name, pid)
|
|
proc, err := os.FindProcess(int(pid))
|
|
if err == nil {
|
|
errKill := proc.Kill()
|
|
if errKill == nil && b.OnKill != nil {
|
|
b.OnKill(info.Name, info.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|