Socket access control: process ancestry check (#58)
* Socket access control: process ancestry check + file permissions Redesign socket control modes from (off, notifications, full) to (off, cmuxOnly, allowAll): - cmuxOnly (default): uses LOCAL_PEERPID + sysctl process tree walk to verify the connecting process is a descendant of cmux. External processes (SSH, other terminals) are rejected. - allowAll: hidden mode accessible only via CMUX_SOCKET_MODE=allowAll env var, skips ancestry check. Legacy "full"/"notifications" env values map here for backward compat. - off: disables socket entirely. Security hardening: - Server: chmod 0600 on socket after bind (owner-only access) - CLI: stat() ownership check before connect (reject fake sockets) Removes per-command allow-list (isCommandAllowed) — once a process passes the ancestry check, all commands are available. Includes migration for persisted UserDefaults values and env var aliases (cmux_only, cmux-only, allow_all, allow-all). * Add /sync-branch skill for submodule + main sync
This commit is contained in:
parent
60978d4d8b
commit
51a67e31fd
8 changed files with 577 additions and 85 deletions
|
|
@ -2,19 +2,24 @@ import Foundation
|
|||
|
||||
enum SocketControlMode: String, CaseIterable, Identifiable {
|
||||
case off
|
||||
case notifications
|
||||
case full
|
||||
case cmuxOnly
|
||||
/// Allow any local process to connect (no ancestry check).
|
||||
/// Only accessible via CMUX_SOCKET_MODE=allowAll env var — not shown in the UI.
|
||||
case allowAll
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
/// Cases shown in the Settings UI. `allowAll` is intentionally excluded.
|
||||
static var uiCases: [SocketControlMode] { [.off, .cmuxOnly] }
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .off:
|
||||
return "Off"
|
||||
case .notifications:
|
||||
return "Notifications only"
|
||||
case .full:
|
||||
return "Full control"
|
||||
case .cmuxOnly:
|
||||
return "cmux processes only"
|
||||
case .allowAll:
|
||||
return "Allow all processes"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -22,10 +27,10 @@ enum SocketControlMode: String, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
case .off:
|
||||
return "Disable the local control socket."
|
||||
case .notifications:
|
||||
return "Allow only notification commands over the local socket."
|
||||
case .full:
|
||||
return "Allow all socket commands, including tab and input control."
|
||||
case .cmuxOnly:
|
||||
return "Only processes started inside cmux terminals can send commands."
|
||||
case .allowAll:
|
||||
return "Allow any local process to connect (no ancestry check)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,12 +39,19 @@ struct SocketControlSettings {
|
|||
static let appStorageKey = "socketControlMode"
|
||||
static let legacyEnabledKey = "socketControlEnabled"
|
||||
|
||||
/// Map old persisted rawValues to the new enum.
|
||||
static func migrateMode(_ raw: String) -> SocketControlMode {
|
||||
switch raw {
|
||||
case "off": return .off
|
||||
case "cmuxOnly": return .cmuxOnly
|
||||
// Legacy values:
|
||||
case "notifications", "full": return .cmuxOnly
|
||||
default: return defaultMode
|
||||
}
|
||||
}
|
||||
|
||||
static var defaultMode: SocketControlMode {
|
||||
#if DEBUG
|
||||
return .full
|
||||
#else
|
||||
return .notifications
|
||||
#endif
|
||||
return .cmuxOnly
|
||||
}
|
||||
|
||||
static func socketPath() -> String {
|
||||
|
|
@ -72,7 +84,15 @@ struct SocketControlSettings {
|
|||
guard let raw = ProcessInfo.processInfo.environment["CMUX_SOCKET_MODE"], !raw.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return SocketControlMode(rawValue: raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased())
|
||||
let cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
switch cleaned {
|
||||
case "off": return .off
|
||||
case "cmuxonly", "cmux_only", "cmux-only": return .cmuxOnly
|
||||
case "allowall", "allow_all", "allow-all": return .allowAll
|
||||
// Legacy env var values — map to allowAll so existing test scripts keep working
|
||||
case "notifications", "full": return .allowAll
|
||||
default: return SocketControlMode(rawValue: cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
static func effectiveMode(userMode: SocketControlMode) -> SocketControlMode {
|
||||
|
|
@ -83,7 +103,7 @@ struct SocketControlSettings {
|
|||
if let overrideMode = envOverrideMode() {
|
||||
return overrideMode
|
||||
}
|
||||
return userMode == .off ? .notifications : userMode
|
||||
return userMode == .off ? .cmuxOnly : userMode
|
||||
}
|
||||
|
||||
if let overrideMode = envOverrideMode() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue