//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 // ── Security / antivirus / updates (from app_pool + common) ─────────────── "update-notifier": true, // Ubuntu update notifier (app_pool) "apport-gtk": true, // Ubuntu crash reporter (app_pool) "kgpg": true, // KDE GPG encryption (app_pool) "ksecretd": true, // KDE secrets daemon (app_pool) "clamtk": true, "clamav": true, "seahorse": true, // GNOME password/keys manager "kwalletd": true, "kwalletd5": true, "kwalletd6": true, "gufw": true, // UFW firewall GUI "firewalld": true, "pkexec": true, // Polkit privilege elevation prompts // ── 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) } } } } }