From 0f2b2413eab0f18ef1c6a238c2181c0ada32fced Mon Sep 17 00:00:00 2001 From: shw Date: Mon, 22 May 2017 00:25:03 +0000 Subject: [PATCH] Added per-process (ephemeral) rule support. (proc coroner now has support for multiple callbacks) --- fw-prompt/dbus.go | 3 +- fw-prompt/fw-prompt.go | 6 ++-- fw-settings/definitions/Dialog.ui | 30 ++++++++++++++++-- fw-settings/definitions/dialog.go | 31 +++++++++++++++++-- fw-settings/main.go | 31 +++++++++++++++++-- fw-settings/rules.go | 8 ++++- gnome-shell/firewall@subgraph.com/dialog.js | 19 +++++++++--- .../firewall@subgraph.com/extension.js | 2 +- proc-coroner/pcoroner.go | 18 ++++++++++- sgfw/const.go | 11 +++++-- sgfw/dbus.go | 30 ++++++++++++++++++ sgfw/dns.go | 15 +-------- sgfw/policy.go | 1 + sgfw/prompt.go | 5 +++ sgfw/rules.go | 7 ++++- sgfw/sgfw.go | 2 ++ 16 files changed, 182 insertions(+), 37 deletions(-) diff --git a/fw-prompt/dbus.go b/fw-prompt/dbus.go index 9989bd2..933cb66 100644 --- a/fw-prompt/dbus.go +++ b/fw-prompt/dbus.go @@ -65,8 +65,7 @@ func newDbusServer() (*dbusServer, error) { func (ds *dbusServer) RequestPrompt(application, icon, path, address string, port int32, ip, origin, proto string, uid, gid int32, username, groupname string, pid int32, optstring string, expanded, expert bool, action int32) (int32, string, *dbus.Error) { - log.Printf("REALLY GOT IT!") - log.Printf("app = %s, icon = %s, path = %s, address = %s, action = %v\n", application, icon, path, address, action) + log.Printf("request prompt: app = %s, icon = %s, path = %s, address = %s, action = %v\n", application, icon, path, address, action) decision := addRequest(nil, path, proto, int(pid), ip, address, int(port), int(uid), int(gid), origin, optstring) log.Print("Waiting on decision...") decision.Cond.L.Lock() diff --git a/fw-prompt/fw-prompt.go b/fw-prompt/fw-prompt.go index 0ad75ad..b39b646 100644 --- a/fw-prompt/fw-prompt.go +++ b/fw-prompt/fw-prompt.go @@ -513,7 +513,7 @@ func createCurrentRule() (ruleColumns, error) { var err error = nil if radioProcess.GetActive() { - return rule, errors.New("Process scope is unsupported at the moment") + rule.Scope = int(sgfw.APPLY_PROCESS) } else if radioParent.GetActive() { return rule, errors.New("Parent process scope is unsupported at the moment") } else if radioSession.GetActive() { @@ -612,7 +612,6 @@ func getSelectedRule() (ruleColumns, int, error) { } rows := sel.GetSelectedRows(globalLS) - fmt.Println("RETURNED ROWS: ", rows.Length()) if rows.Length() <= 0 { return rule, -1, errors.New("No selection was made") @@ -814,7 +813,6 @@ func main() { radioParent = get_radiobutton(radioOnce, "Parent Process", false) radioSession = get_radiobutton(radioOnce, "Session", false) radioPermanent = get_radiobutton(radioOnce, "Permanent", false) - radioProcess.SetSensitive(false) radioParent.SetSensitive(false) hbox.PackStart(lbl, false, false, 10) hbox.PackStart(radioOnce, false, false, 5) @@ -948,6 +946,7 @@ func main() { editPort.SetText(strconv.Itoa(seldata.Port)) radioOnce.SetActive(true) radioProcess.SetActive(false) + radioProcess.SetSensitive(seldata.Pid > 0) radioParent.SetActive(false) radioSession.SetActive(false) radioPermanent.SetActive(false) @@ -971,6 +970,7 @@ func main() { chkUser.SetActive(false) chkGroup.SetActive(false) + return }) diff --git a/fw-settings/definitions/Dialog.ui b/fw-settings/definitions/Dialog.ui index 18876f0..f71815e 100644 --- a/fw-settings/definitions/Dialog.ui +++ b/fw-settings/definitions/Dialog.ui @@ -76,7 +76,7 @@ - + True True True @@ -93,7 +93,7 @@ True False - System + Process 2 @@ -101,6 +101,32 @@ False + + + True + True + True + True + never + in + + + 3 + True + + + + + True + False + System + + + 3 + True + False + + page0 diff --git a/fw-settings/definitions/dialog.go b/fw-settings/definitions/dialog.go index d34e2f6..85a1f25 100644 --- a/fw-settings/definitions/dialog.go +++ b/fw-settings/definitions/dialog.go @@ -86,7 +86,7 @@ func (*defDialog) String() string { - + True True True @@ -103,7 +103,7 @@ func (*defDialog) String() string { True False - System + Process 2 @@ -111,6 +111,32 @@ func (*defDialog) String() string { False + + + True + True + True + True + never + in + + + 3 + True + + + + + True + False + System + + + 3 + True + False + + page0 @@ -272,6 +298,7 @@ func (*defDialog) String() string { Forever Session + Process Once diff --git a/fw-settings/main.go b/fw-settings/main.go index 9e1136f..05fe3b8 100644 --- a/fw-settings/main.go +++ b/fw-settings/main.go @@ -15,6 +15,7 @@ var fwswin *gtk.Window = nil var fwsbuilder *builder = nil var swRulesPermanent *gtk.ScrolledWindow = nil var swRulesSession *gtk.ScrolledWindow = nil +var swRulesProcess *gtk.ScrolledWindow = nil var swRulesSystem *gtk.ScrolledWindow = nil func failDialog(parent *gtk.Window, format string, args ...interface{}) { @@ -38,6 +39,7 @@ var repopMutex = &sync.Mutex{} func repopulateWin() { fmt.Println("Refreshing firewall rule list.") repopMutex.Lock() + defer repopMutex.Unlock() win := fwswin dbus, err := newDbusObject() @@ -57,9 +59,15 @@ func repopulateWin() { } swRulesSession.Remove(child) + child, err = swRulesProcess.GetChild() + if err != nil { + failDialog(win, "Unable to clear out process rules list display: %v", err) + } + swRulesProcess.Remove(child) + child, err = swRulesSystem.GetChild() if err != nil { - failDialog(win, "Unable to clear out session rules list display: %v", err) + failDialog(win, "Unable to clear out system rules list display: %v", err) } swRulesSystem.Remove(child) @@ -69,10 +77,12 @@ func repopulateWin() { boxSession, _ := gtk.ListBoxNew() swRulesSession.Add(boxSession) + boxProcess, _ := gtk.ListBoxNew() + swRulesProcess.Add(boxProcess) + boxSystem, _ := gtk.ListBoxNew() swRulesSystem.Add(boxSystem) - rlPermanent := newRuleList(dbus, win, boxPermanent) if _, err := dbus.isEnabled(); err != nil { failDialog(win, "Unable is connect to firewall daemon. Is it running?") @@ -85,6 +95,12 @@ func repopulateWin() { } rlSession.loadRules(sgfw.RULE_MODE_SESSION) + rlProcess := newRuleList(dbus, win, boxProcess) + if _, err := dbus.isEnabled(); err != nil { + failDialog(win, "Unable is connect to firewall daemon. Is it running?") + } + rlProcess.loadRules(sgfw.RULE_MODE_PROCESS) + rlSystem := newRuleList(dbus, win, boxSystem) if _, err := dbus.isEnabled(); err != nil { failDialog(win, "Unable is connect to firewall daemon. Is it running?") @@ -94,7 +110,6 @@ func repopulateWin() { loadConfig(win, fwsbuilder, dbus) // app.AddWindow(win) win.ShowAll() - repopMutex.Unlock() } func populateWin(app *gtk.Application, win *gtk.Window) { @@ -104,6 +119,7 @@ func populateWin(app *gtk.Application, win *gtk.Window) { "window", &win, "swRulesPermanent", &swRulesPermanent, "swRulesSession", &swRulesSession, + "swRulesProcess", &swRulesProcess, "swRulesSystem", &swRulesSystem, ) //win.SetIconName("security-high-symbolic") @@ -115,6 +131,9 @@ func populateWin(app *gtk.Application, win *gtk.Window) { boxSession, _ := gtk.ListBoxNew() swRulesSession.Add(boxSession) + boxProcess, _ := gtk.ListBoxNew() + swRulesProcess.Add(boxProcess) + boxSystem, _ := gtk.ListBoxNew() swRulesSystem.Add(boxSystem) @@ -135,6 +154,12 @@ func populateWin(app *gtk.Application, win *gtk.Window) { } rlSession.loadRules(sgfw.RULE_MODE_SESSION) + rlProcess := newRuleList(dbus, win, boxProcess) + if _, err := dbus.isEnabled(); err != nil { + failDialog(win, "Unable is connect to firewall daemon. Is it running?") + } + rlProcess.loadRules(sgfw.RULE_MODE_PROCESS) + rlSystem := newRuleList(dbus, win, boxSystem) if _, err := dbus.isEnabled(); err != nil { failDialog(win, "Unable is connect to firewall daemon. Is it running?") diff --git a/fw-settings/rules.go b/fw-settings/rules.go index c0b308e..38f2397 100644 --- a/fw-settings/rules.go +++ b/fw-settings/rules.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strings" + "strconv" "github.com/subgraph/fw-daemon/sgfw" @@ -113,7 +114,12 @@ func createWidget(rule *sgfw.DbusRule) *ruleRow { } func (rr *ruleRow) update() { - rr.gtkLabelApp.SetText(rr.rule.App) + if rr.rule.Mode == uint16(sgfw.RULE_MODE_PROCESS) { + appstr := "(" + strconv.Itoa(int(rr.rule.Pid)) + ") " + rr.rule.App + rr.gtkLabelApp.SetText(appstr) + } else { + rr.gtkLabelApp.SetText(rr.rule.App) + } rr.gtkLabelApp.SetTooltipText(rr.rule.Path) rr.gtkLabelVerb.SetText(getVerbText(rr.rule)) if (rr.rule.Proto == "tcp") { diff --git a/gnome-shell/firewall@subgraph.com/dialog.js b/gnome-shell/firewall@subgraph.com/dialog.js index 26612e8..427cad7 100644 --- a/gnome-shell/firewall@subgraph.com/dialog.js +++ b/gnome-shell/firewall@subgraph.com/dialog.js @@ -12,7 +12,8 @@ const Tweener = imports.ui.tweener; const RuleScope = { APPLY_ONCE: 0, APPLY_SESSION: 1, - APPLY_FOREVER: 2, + APPLY_PROCESS: 2, + APPLY_FOREVER: 3, }; const DetailSection = new Lang.Class({ @@ -129,9 +130,13 @@ Signals.addSignalMethods(OptionListItem.prototype); const OptionList = new Lang.Class({ Name: 'OptionList', - _init: function() { + _init: function(pid_known) { this.actor = new St.BoxLayout({vertical: true, style_class: 'fw-option-list'}); - this.buttonGroup = new ButtonGroup("Forever", "Session", "Once"); + if (pid_known) { + this.buttonGroup = new ButtonGroup("Forever", "Session", "Once", "PID"); + } else { + this.buttonGroup = new ButtonGroup("Forever", "Session", "Once"); + } this.actor.add_child(this.buttonGroup.actor); this.items = []; this._selected; @@ -188,6 +193,8 @@ const OptionList = new Lang.Class({ return RuleScope.APPLY_SESSION; case 2: return RuleScope.APPLY_ONCE; + case 3: + return RuleScope.APPLY_PROCESS; default: log("unexpected scope value "+ this.buttonGroup._selected); return RuleScope.APPLY_SESSION; @@ -196,6 +203,8 @@ const OptionList = new Lang.Class({ scopeToIdx: function(scope) { switch (scope) { + case RuleScope.APPLY_PROCESS: + return 3; case RuleScope.APPLY_ONCE: return 2; case RuleScope.APPLY_SESSION: @@ -413,7 +422,7 @@ const PromptDialog = new Lang.Class({ Name: 'PromptDialog', Extends: ModalDialog.ModalDialog, - _init: function(invocation) { + _init: function(invocation, pid_known) { this.parent({ styleClass: 'fw-prompt-dialog' }); this._invocation = invocation; this.header = new PromptDialogHeader(); @@ -426,7 +435,7 @@ const PromptDialog = new Lang.Class({ this.info = new DetailSection(); box.add_child(this.info.actor); - this.optionList = new OptionList(); + this.optionList = new OptionList(pid_known); box.add_child(this.optionList.actor); this.optionList.addOptions([ "Only PORT AND ADDRESS", diff --git a/gnome-shell/firewall@subgraph.com/extension.js b/gnome-shell/firewall@subgraph.com/extension.js index ff3b0c1..3499a6d 100644 --- a/gnome-shell/firewall@subgraph.com/extension.js +++ b/gnome-shell/firewall@subgraph.com/extension.js @@ -94,7 +94,7 @@ const FirewallPromptHandler = new Lang.Class({ RequestPromptAsync: function(params, invocation) { let [app, icon, path, address, port, ip, origin, proto, uid, gid, user, group, pid, optstring, expanded, expert, action] = params; // this._closeDialog(); - this._dialog = new Dialog.PromptDialog(invocation); + this._dialog = new Dialog.PromptDialog(invocation, (pid >= 0)); this._invocation = invocation; this._dialog.update(app, icon, path, address, port, ip, origin, uid, gid, user, group, pid, proto, optstring, expanded, expert, action); this._dialog.open(); diff --git a/proc-coroner/pcoroner.go b/proc-coroner/pcoroner.go index a012af5..23e7a7d 100644 --- a/proc-coroner/pcoroner.go +++ b/proc-coroner/pcoroner.go @@ -18,9 +18,17 @@ type WatchProcess struct { Stime int } +type CallbackEntry struct { + fn procCB + param interface{} +} + type procCB func(int, interface{}) +var Callbacks []CallbackEntry + + var pmutex = &sync.Mutex{} var pidMap map[int]WatchProcess = make(map[int]WatchProcess) @@ -53,6 +61,11 @@ func UnmonitorProcess(pid int) { return } +func AddCallback(cbfunc procCB, param interface{}) { + cbe := CallbackEntry{cbfunc, param} + Callbacks = append(Callbacks, cbe) +} + func MonitorThread(cbfunc procCB, param interface{}) { for { /* if len(pidMap) == 0 { @@ -71,12 +84,15 @@ func MonitorThread(cbfunc procCB, param interface{}) { if cbfunc != nil { cbfunc(pkey, param) } + for i := 0; i < len(Callbacks); i++ { + Callbacks[i].fn(pkey, Callbacks[i].param) + } continue } } - time.Sleep(2 * time.Second) + time.Sleep(1 * time.Second) } } diff --git a/sgfw/const.go b/sgfw/const.go index 2e25532..3bb2078 100644 --- a/sgfw/const.go +++ b/sgfw/const.go @@ -31,18 +31,21 @@ var RuleActionValue = map[string]RuleAction{ type RuleMode uint16 const ( RULE_MODE_SESSION RuleMode = iota + RULE_MODE_PROCESS RULE_MODE_PERMANENT RULE_MODE_SYSTEM ) // RuleModeString is used to get a rule mode string from its id var RuleModeString = map[RuleMode]string{ RULE_MODE_SESSION: "SESSION", + RULE_MODE_PROCESS: "PROCESS", RULE_MODE_PERMANENT: "PERMANENT", RULE_MODE_SYSTEM: "SYSTEM", } // RuleModeValue converts a mode string to its id var RuleModeValue = map[string]RuleMode{ RuleModeString[RULE_MODE_SESSION]: RULE_MODE_SESSION, + RuleModeString[RULE_MODE_PROCESS]: RULE_MODE_PROCESS, RuleModeString[RULE_MODE_PERMANENT]: RULE_MODE_PERMANENT, RuleModeString[RULE_MODE_SYSTEM]: RULE_MODE_SYSTEM, } @@ -52,18 +55,21 @@ type FilterScope uint16 const ( APPLY_ONCE FilterScope = iota APPLY_SESSION + APPLY_PROCESS APPLY_FOREVER ) // FilterScopeString converts a filter scope ID to its string var FilterScopeString = map[FilterScope]string{ APPLY_ONCE: "ONCE", APPLY_SESSION: "SESSION", + APPLY_PROCESS: "PROCESS", APPLY_FOREVER: "FOREVER", } // FilterScopeString converts a filter scope string to its ID var FilterScopeValue = map[string]FilterScope{ FilterScopeString[APPLY_ONCE]: APPLY_ONCE, FilterScopeString[APPLY_SESSION]: APPLY_SESSION, + FilterScopeString[APPLY_PROCESS]: APPLY_PROCESS, FilterScopeString[APPLY_FOREVER]: APPLY_FOREVER, } // GetFilterScopeString is used to safely return a filter scope string @@ -108,6 +114,7 @@ type DbusRule struct { Net string Origin string Proto string + Pid uint32 Privs string App string Path string @@ -116,8 +123,8 @@ type DbusRule struct { Mode uint16 } -const ( +/*const ( OZ_FWRULE_WHITELIST = iota OZ_FWRULE_BLACKLIST OZ_FWRULE_NONE -) +) */ diff --git a/sgfw/dbus.go b/sgfw/dbus.go index 87e316a..d445c01 100644 --- a/sgfw/dbus.go +++ b/sgfw/dbus.go @@ -8,6 +8,7 @@ import ( "github.com/godbus/dbus" "github.com/godbus/dbus/introspect" "github.com/op/go-logging" + "github.com/subgraph/fw-daemon/proc-coroner" ) const introspectXML = ` @@ -68,6 +69,31 @@ type dbusServer struct { prompter *prompter } +func DbusProcDeathCB(pid int, param interface{}) { + ds := param.(*dbusServer) + ds.fw.lock.Lock() + defer ds.fw.lock.Unlock() + done, updated := false, false + for !done { + done = true + for _, p := range ds.fw.policies { + for r := 0; r < len(p.rules); r++ { + if p.rules[r].pid == pid && p.rules[r].mode == RULE_MODE_PROCESS { + p.rules = append(p.rules[:r], p.rules[r+1:]...) + done = false + updated = true + log.Notice("Removed per-process firewall rule for PID: ", pid) + break + } + } + } + } + + if updated { + dbusp.alertRule("Firewall removed on process death") + } +} + func newDbusServer() (*dbusServer, error) { conn, err := dbus.SystemBus() if err != nil { @@ -92,6 +118,7 @@ func newDbusServer() (*dbusServer, error) { ds.conn = conn ds.prompter = newPrompter(conn) + pcoroner.AddCallback(DbusProcDeathCB, ds) return ds, nil } @@ -132,6 +159,7 @@ func createDbusRule(r *Rule) DbusRule { Net: netstr, Origin: ostr, Proto: r.proto, + Pid: uint32(r.pid), Privs: pstr, App: path.Base(r.policy.path), Path: r.policy.path, @@ -191,6 +219,8 @@ func (ds *dbusServer) UpdateRule(rule DbusRule) *dbus.Error { r.rtype = RuleAction(rule.Verb) } r.hostname = tmp.hostname + r.proto = tmp.proto + r.pid = tmp.pid r.addr = tmp.addr r.port = tmp.port r.mode = RuleMode(rule.Mode) diff --git a/sgfw/dns.go b/sgfw/dns.go index 42eec7d..aaa1355 100644 --- a/sgfw/dns.go +++ b/sgfw/dns.go @@ -14,9 +14,6 @@ import ( "github.com/subgraph/fw-daemon/proc-coroner" ) -var monitoring = false -var mlock = sync.Mutex{} - type dnsEntry struct { name string ttl uint32 @@ -96,8 +93,7 @@ func (dc *dnsCache) processDNS(pkt *nfqueue.NFQPacket) { } } */ -func procDeathCallback(pid int, param interface{}) { -// log.Warning("XXX: IN CALLBACK for pid: ", pid, " / param = ", param) +func procDeathCallbackDNS(pid int, param interface{}) { if pid != 0 { cache := param.(*dnsCache) @@ -148,15 +144,6 @@ func (dc *dnsCache) processRecordAddress(name string, answers []dnsRR, pid int) if pid > 0 { log.Warning("Adding process to be monitored by DNS cache: ", pid) - if !monitoring { - mlock.Lock() - if !monitoring { - monitoring = true -// go checker(dc) - go pcoroner.MonitorThread(procDeathCallback, dc) - } - mlock.Unlock() - } pcoroner.MonitorProcess(pid) } if !FirewallConfig.LogRedact { diff --git a/sgfw/policy.go b/sgfw/policy.go index dfeacbb..33e3730 100644 --- a/sgfw/policy.go +++ b/sgfw/policy.go @@ -298,6 +298,7 @@ func (p *Policy) processNewRule(r *Rule, scope FilterScope) bool { func (p *Policy) parseRule(s string, add bool) (*Rule, error) { log.Noticef("XXX: attempt to parse rule: |%s|\n", s) r := new(Rule) + r.pid = -1 r.mode = RULE_MODE_PERMANENT r.policy = p if !r.parse(s) { diff --git a/sgfw/prompt.go b/sgfw/prompt.go index fcd154e..010dac0 100644 --- a/sgfw/prompt.go +++ b/sgfw/prompt.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/godbus/dbus" + "github.com/subgraph/fw-daemon/proc-coroner" ) @@ -177,6 +178,10 @@ func (p *prompter) processConnection(pc pendingConnection) { fscope := FilterScope(scope) if fscope == APPLY_SESSION { r.mode = RULE_MODE_SESSION + } else if fscope == APPLY_PROCESS { + r.mode = RULE_MODE_PROCESS + r.pid = pc.procInfo().Pid + pcoroner.MonitorProcess(r.pid) } if !policy.processNewRule(r, fscope) { p.lock.Lock() diff --git a/sgfw/rules.go b/sgfw/rules.go index 8d15f41..487105e 100644 --- a/sgfw/rules.go +++ b/sgfw/rules.go @@ -27,6 +27,7 @@ type Rule struct { mode RuleMode rtype RuleAction proto string + pid int hostname string network *net.IPNet addr net.IP @@ -146,7 +147,7 @@ func (rl *RuleList) filter(pkt *nfqueue.NFQPacket, src, dst net.IP, dstPort uint result := FILTER_PROMPT sandboxed := strings.HasPrefix(optstr, "Sandbox") for _, r := range *rl { -log.Notice("------------ trying match of src ", src, " against: ", r, " | ", r.saddr, " / optstr = ", optstr) +log.Notice("------------ trying match of src ", src, " against: ", r, " | ", r.saddr, " / optstr = ", optstr, "; pid ", pinfo.Pid, " vs rule pid ", r.pid) if r.saddr == nil && src != nil && sandboxed { log.Notice("! Skipping comparison against incompatible rule types: rule src = ", r.saddr, " / packet src = ", src) continue @@ -154,6 +155,10 @@ log.Notice("! Skipping comparison against incompatible rule types: rule src = ", log.Notice("! Skipping comparison of mismatching source ips") continue } + if r.pid >= 0 && r.pid != pinfo.Pid { +//log.Notice("! Skipping comparison of mismatching PIDs") + continue + } if r.match(src, dst, dstPort, hostname, getNFQProto(pkt), pinfo.UID, pinfo.GID, uidToUser(pinfo.UID), gidToGroup(pinfo.GID)) { log.Notice("+ MATCH SUCCEEDED") dstStr := dst.String() diff --git a/sgfw/sgfw.go b/sgfw/sgfw.go index 97ad4ba..95f5db3 100644 --- a/sgfw/sgfw.go +++ b/sgfw/sgfw.go @@ -16,6 +16,7 @@ import ( nfqueue "github.com/subgraph/go-nfnetlink/nfqueue" // "github.com/subgraph/go-nfnetlink" "github.com/subgraph/go-procsnitch" + "github.com/subgraph/fw-daemon/proc-coroner" "github.com/google/gopacket" "github.com/google/gopacket/layers" ) @@ -227,6 +228,7 @@ func Main() { stopChan: make(chan bool, 0), } ds.fw = fw + go pcoroner.MonitorThread(procDeathCallbackDNS, fw.dns) fw.loadRules()