mirror of https://github.com/subgraph/fw-daemon
commit
2d8afe1d60
@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.idea/*.iml
|
||||
.idea/*.iml
|
||||
.idea/*.iml
|
||||
.idea/*.iml
|
||||
.idea/*.iml
|
||||
.idea/*.iml
|
||||
.idea/*.iml
|
||||
.idea/
|
@ -0,0 +1,4 @@
|
||||
# Subgraph Firewall
|
||||
|
||||
A desktop application firewall for Subgraph OS.
|
||||
|
@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/godbus/dbus"
|
||||
"github.com/godbus/dbus/introspect"
|
||||
)
|
||||
|
||||
const introspectXml = `
|
||||
<node>
|
||||
<interface name="com.subgraph.Firewall">
|
||||
<method name="SetEnabled">
|
||||
<arg name="enabled" direction="in" type="b" />
|
||||
</method>
|
||||
</interface>` +
|
||||
introspect.IntrospectDataString +
|
||||
`</node>`
|
||||
|
||||
const busName = "com.subgraph.Firewall"
|
||||
const objectPath = "/com/subgraph/Firewall"
|
||||
const interfaceName = "com.subgraph.Firewall"
|
||||
|
||||
type dbusServer struct {
|
||||
conn *dbus.Conn
|
||||
prompter *prompter
|
||||
}
|
||||
|
||||
func dbusConnect() (*dbus.Conn, error) {
|
||||
// https://github.com/golang/go/issues/1435
|
||||
runtime.LockOSThread()
|
||||
syscall.Setresuid(-1, 1000, 0)
|
||||
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
syscall.Setresuid(0, 0, -1)
|
||||
runtime.UnlockOSThread()
|
||||
|
||||
if os.Geteuid() != 0 || os.Getuid() != 0 {
|
||||
log.Warning("Not root as expected")
|
||||
os.Exit(0)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func newDbusServer(conn *dbus.Conn) (*dbusServer, error) {
|
||||
reply, err := conn.RequestName(busName, dbus.NameFlagDoNotQueue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
return nil, errors.New("Bus name is already owned")
|
||||
}
|
||||
ds := &dbusServer{}
|
||||
|
||||
if err := conn.Export(ds, objectPath, interfaceName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ps := strings.Split(objectPath, "/")
|
||||
path := "/"
|
||||
for _, p := range ps {
|
||||
if len(path) > 1 {
|
||||
path += "/"
|
||||
}
|
||||
path += p
|
||||
|
||||
if err := conn.Export(ds, dbus.ObjectPath(path), "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ds.conn = conn
|
||||
ds.prompter = newPrompter(conn)
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func (ds *dbusServer) Introspect(msg dbus.Message) (string, *dbus.Error) {
|
||||
path := string(msg.Headers[dbus.FieldPath].Value().(dbus.ObjectPath))
|
||||
if path == objectPath {
|
||||
return introspectXml, nil
|
||||
}
|
||||
parts := strings.Split(objectPath, "/")
|
||||
current := "/"
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
if len(current) > 1 {
|
||||
current += "/"
|
||||
}
|
||||
current += parts[i]
|
||||
if path == current {
|
||||
next := parts[i+1]
|
||||
return fmt.Sprintf("<node><node name=\"%s\"/></node>", next), nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (ds *dbusServer) SetEnabled(flag bool) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *dbusServer) prompt(p *Policy) {
|
||||
log.Info("prompting...")
|
||||
ds.prompter.prompt(p)
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/subgraph/fw-daemon/nfqueue"
|
||||
)
|
||||
|
||||
type dnsCache struct {
|
||||
ipMap map[string]string
|
||||
lock sync.Mutex
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewDnsCache() *dnsCache {
|
||||
return &dnsCache{
|
||||
ipMap: make(map[string]string),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *dnsCache) processDNS(pkt *nfqueue.Packet) {
|
||||
dns := &dnsMsg{}
|
||||
if !dns.Unpack(pkt.Payload) {
|
||||
log.Warning("Failed to Unpack DNS message")
|
||||
return
|
||||
}
|
||||
if !dns.response {
|
||||
return
|
||||
}
|
||||
if len(dns.question) != 1 {
|
||||
log.Warning("Length of DNS Question section is not 1 as expected: %d", len(dns.question))
|
||||
return
|
||||
}
|
||||
q := dns.question[0]
|
||||
if q.Qtype == dnsTypeA {
|
||||
dc.processRecordA(q.Name, dns.answer)
|
||||
return
|
||||
}
|
||||
log.Info("Unhandled DNS message: %v", dns)
|
||||
|
||||
}
|
||||
|
||||
func (dc *dnsCache) processRecordA(name string, answers []dnsRR) {
|
||||
dc.lock.Lock()
|
||||
defer dc.lock.Unlock()
|
||||
for _, rr := range answers {
|
||||
switch rec := rr.(type) {
|
||||
case *dnsRR_A:
|
||||
ip := net.IPv4(byte(rec.A>>24), byte(rec.A>>16), byte(rec.A>>8), byte(rec.A)).String()
|
||||
if strings.HasSuffix(name, ".") {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
dc.ipMap[ip] = name
|
||||
log.Info("Adding %s: %s", name, ip)
|
||||
default:
|
||||
log.Warning("Unexpected RR type in answer section of A response: %v", rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *dnsCache) Lookup(ip net.IP) string {
|
||||
dc.lock.Lock()
|
||||
defer dc.lock.Unlock()
|
||||
return dc.ipMap[ip.String()]
|
||||
}
|
@ -0,0 +1,451 @@
|
||||
const Clutter = imports.gi.Clutter;
|
||||
const GLib = imports.gi.GLib;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
const Lang = imports.lang;
|
||||
const Pango = imports.gi.Pango;
|
||||
const Signals = imports.signals;
|
||||
const St = imports.gi.St;
|
||||
|
||||
const ModalDialog = imports.ui.modalDialog;
|
||||
const Tweener = imports.ui.tweener;
|
||||
|
||||
const RuleScope = {
|
||||
APPLY_ONCE: 0,
|
||||
APPLY_SESSION: 1,
|
||||
APPLY_FOREVER: 2,
|
||||
};
|
||||
|
||||
const DetailSection = new Lang.Class({
|
||||
Name: 'DetailSection',
|
||||
|
||||
_init: function() {
|
||||
this.actor = new St.BoxLayout({ style_class: 'fw-details-section' });
|
||||
this._left = new St.BoxLayout({ vertical: true, style_class: 'fw-details-left'});
|
||||
this._right = new St.BoxLayout({ vertical: true });
|
||||
this.actor.add_child(this._left);
|
||||
this.actor.add_child(this._right);
|
||||
|
||||
this.ipAddr = this._addDetails("IP Address:");
|
||||
this.path = this._addDetails("Path:");
|
||||
this.pid = this._addDetails("Process ID:");
|
||||
this.user = this._addDetails("User:");
|
||||
},
|
||||
|
||||
_addDetails: function(text) {
|
||||
let title = new St.Label({ style_class: 'fw-detail-title', text: text});
|
||||
let msg = new St.Label({ style_class: 'fw-detail-message' });
|
||||
this._left.add(title, { expand: true, x_fill: false, x_align: St.Align.END});
|
||||
this._right.add(msg);
|
||||
return msg;
|
||||
},
|
||||
|
||||
setDetails: function(ip, path, pid, user) {
|
||||
this.ipAddr.text = ip;
|
||||
this.path.text = path;
|
||||
this.pid.text = pid.toString();
|
||||
this.user.text = user;
|
||||
}
|
||||
});
|
||||
|
||||
const OptionListItem = new Lang.Class({
|
||||
Name: 'OptionListItem',
|
||||
|
||||
_init: function(text, idx) {
|
||||
this.actor = new St.BoxLayout({ style_class: 'fw-option-item', reactive: true, can_focus: true });
|
||||
this._selectedIcon = new St.Icon({style_class: 'fw-option-item-icon', icon_name: 'object-select-symbolic'});
|
||||
this._selectedIcon.opacity = 0;
|
||||
|
||||
this._label = new St.Label({text: text});
|
||||
let spacer = new St.Bin();
|
||||
this.actor.add_child(this._label);
|
||||
this.actor.add(spacer, {expand: true});
|
||||
this.actor.add_child(this._selectedIcon);
|
||||
this.idx = idx;
|
||||
|
||||
let action = new Clutter.ClickAction();
|
||||
action.connect('clicked', Lang.bind(this, function() {
|
||||
this.actor.grab_key_focus();
|
||||
this.emit('selected');
|
||||
}));
|
||||
this.actor.add_action(action);
|
||||
|
||||
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
||||
},
|
||||
|
||||
setText: function(text) {
|
||||
this._label.text = text;
|
||||
},
|
||||
|
||||
setSelected: function(isSelected) {
|
||||
this._selectedIcon.opacity = isSelected ? 255 : 0;
|
||||
},
|
||||
|
||||
_onKeyPressEvent: function(actor, event) {
|
||||
let symbol = event.get_key_symbol();
|
||||
if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
|
||||
this.emit('selected');
|
||||
}
|
||||
}
|
||||
});
|
||||
Signals.addSignalMethods(OptionListItem.prototype);
|
||||
|
||||
const OptionList = new Lang.Class({
|
||||
Name: 'OptionList',
|
||||
|
||||
_init: function() {
|
||||
this.actor = new St.BoxLayout({vertical: true, style_class: 'fw-option-list'});
|
||||
this.buttonGroup = new ButtonGroup("Forever", "Session", "Once");
|
||||
this.actor.add_child(this.buttonGroup.actor);
|
||||
this.items = [];
|
||||
this._selected;
|
||||
},
|
||||
|
||||
setOptionText: function(idx, text) {
|
||||
if(this.items.length <= idx) {
|
||||
log("attempt to setOptionText with idx = "+ idx + " when this.items.length = "+ this.items.length)
|
||||
return;
|
||||
}
|
||||
this.items[idx].setText(text);
|
||||
},
|
||||
|
||||
addOptions: function(options) {
|
||||
for(let i = 0; i < options.length; i++) {
|
||||
this._addOption(options[i], i)
|
||||
}
|
||||
if(this.items.length) {
|
||||
this._optionSelected(this.items[0])
|
||||
}
|
||||
},
|
||||
|
||||
_addOption: function(text, idx) {
|
||||
let item = new OptionListItem(text, idx);
|
||||
item.connect('selected', Lang.bind(this, function() {
|
||||
this._optionSelected(item);
|
||||
}));
|
||||
this.actor.add_child(item.actor);
|
||||
this.items.push(item);
|
||||
},
|
||||
|
||||
_optionSelected: function(item) {
|
||||
if (item == this._selected) {
|
||||
return;
|
||||
}
|
||||
if(this._selected) {
|
||||
this._selected.actor.remove_style_pseudo_class('selected');
|
||||
this._selected.setSelected(false);
|
||||
}
|
||||
item.setSelected(true);
|
||||
this._selected = item;
|
||||
this._selected.actor.add_style_pseudo_class('selected');
|
||||
},
|
||||
|
||||
selectedIdx: function() {
|
||||
return this._selected.idx;
|
||||
},
|
||||
|
||||
selectedScope: function() {
|
||||
switch(this.buttonGroup._checked) {
|
||||
case 0:
|
||||
return RuleScope.APPLY_FOREVER;
|
||||
case 1:
|
||||
return RuleScope.APPLY_SESSION;
|
||||
case 2:
|
||||
return RuleScope.APPLY_ONCE;
|
||||
default:
|
||||
log("unexpected scope value "+ this.buttonGroup._selected);
|
||||
return RuleScope.APPLY_SESSION;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const ButtonGroup = new Lang.Class({
|
||||
Name: 'ButtonGroup',
|
||||
|
||||
_init: function() {
|
||||
this.actor = new St.BoxLayout({ style_class: 'fw-button-group'});
|
||||
this._checked = -1;
|
||||
this._buttons= [];
|
||||
for(let i = 0; i < arguments.length; i++) {
|
||||
let idx = i;
|
||||
this._buttons[i] = new St.Button({ style_class: 'fw-group-button button',
|
||||
label: arguments[i],
|
||||
can_focus: true,
|
||||
x_expand: true });
|
||||
this._buttons[i].connect('clicked', Lang.bind(this, function(actor) {
|
||||
this._setChecked(idx);
|
||||
}));
|
||||
this.actor.add_child(this._buttons[i]);
|
||||
}
|
||||
this._setChecked(0);
|
||||
},
|
||||
|
||||
_setChecked: function(idx) {
|
||||
|
||||
if(idx == this._checked) {
|
||||
return;
|
||||
}
|
||||
this._buttons[idx].add_style_pseudo_class('checked');
|
||||
if(this._checked >= 0) {
|
||||
this._buttons[this._checked].remove_style_pseudo_class('checked');
|
||||
}
|
||||
this._checked = idx;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const ExpandingSection = new Lang.Class({
|
||||
Name: 'ExpandingSection',
|
||||
|
||||
_init: function(text, content) {
|
||||
this.actor = new St.BoxLayout({vertical: true});
|
||||
this._createHeader(this.actor, text);
|
||||
this.scroll = new St.ScrollView({hscrollbar_policy: Gtk.PolicyType.NEVER,
|
||||
vscrollbar_policy: Gtk.PolicyType.NEVER });
|
||||
this.actor.add_child(this.scroll);
|
||||
this.isOpen = false;
|
||||
},
|
||||
|
||||
_createHeader: function(parent, text) {
|
||||
this.header = new St.BoxLayout({ style_class: 'fw-expanding-section-header', reactive: true, track_hover: true, can_focus: true});
|
||||
this.label = new St.Label({ style_class: 'fw-expanding-section-label', text: text, y_expand: true, y_align: Clutter.ActorAlign.CENTER });
|
||||
this.header.add_child(this.label);
|
||||
let spacer = new St.Bin({ style_class: 'fw-expanding-section-spacer'});
|
||||
this.header.add(spacer, {expand: true});
|
||||
|
||||
this._triangle = new St.Icon({ style_class: 'popup-menu-arrow',
|
||||
icon_name: 'pan-end-symbolic',
|
||||
y_expand: true,
|
||||
y_align: Clutter.ActorAlign.CENTER});
|
||||
this._triangle.pivot_point = new Clutter.Point({ x: 0.5, y: 0.6 });
|
||||
|
||||
this._triangleBin = new St.Widget({ y_expand: true, y_align: Clutter.ActorAlign.CENTER});
|
||||
this._triangleBin.add_child(this._triangle);
|
||||
|
||||
this.header.add_child(this._triangleBin);
|
||||
this.header.connect('button-press-event', Lang.bind(this, this._onButtonPressEvent));
|
||||
this.header.connect('button-release-event', Lang.bind(this, this._onButtonReleaseEvent));
|
||||
this.header.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
||||
parent.add_child(this.header);
|
||||
},
|
||||
|
||||
_onButtonPressEvent: function (actor, event) {
|
||||
this.actor.add_style_pseudo_class('active');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
},
|
||||
|
||||
_onButtonReleaseEvent: function (actor, event) {
|
||||
this.actor.remove_style_pseudo_class('active');
|
||||
this.activate(event);
|
||||
return Clutter.EVENT_STOP;
|
||||
},
|
||||
|
||||
_onKeyPressEvent: function(actor, event) {
|
||||
let symbol = event.get_key_symbol();
|
||||
if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
|
||||
this.activate(event);
|
||||
}
|
||||
},
|
||||
|
||||
activate: function(event) {
|
||||
if(!this.isOpen) {
|
||||
this.open();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
set_child: function(child) {
|
||||
if(this.child) {
|
||||
this.child.destroy();
|
||||
}
|
||||
this.scroll.add_actor(child);
|
||||
this.child = child;
|
||||
let [min, nat] = this.child.get_preferred_width(-1);
|
||||
this.scroll.width = nat;
|
||||
this.scroll.show();
|
||||
this.scroll.height = 0;
|
||||
this.child.hide();
|
||||
},
|
||||
|
||||
open: function() {
|
||||
if(this.isOpen) {
|
||||
return;
|
||||
}
|
||||
if(!this.child) {
|
||||
return;
|
||||
}
|
||||
this.isOpen = true;
|
||||
this.scroll.show();
|
||||
this.child.show();
|
||||
let targetAngle = 90;
|
||||
let [minHeight, naturalHeight] = this.child.get_preferred_height(-1);
|
||||
this.scroll.height = 0;
|
||||
this.scroll._arrowRotation = this._triangle.rotation_angle_z;
|
||||
Tweener.addTween(this.scroll,
|
||||
{ _arrowRotation: targetAngle,
|
||||
height: naturalHeight,
|
||||
time: 0.5,
|
||||
onUpdateScope: this,
|
||||
onUpdate: function() {
|
||||
this._triangle.rotation_angle_z = this.scroll._arrowRotation;
|
||||
},
|
||||
onCompleteScope: this,
|
||||
onComplete: function() {
|
||||
this.scroll.set_height(-1);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
close: function() {
|
||||
if(!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.isOpen = false;
|
||||
this.scroll._arrowRotation = this._triangle.rotation_angle_z;
|
||||
Tweener.addTween(this.scroll,
|
||||
{ _arrowRotation: 0,
|
||||
height: 0,
|
||||
time: 0.5,
|
||||
onUpdateScope: this,
|
||||
onUpdate: function() {
|
||||
this._triangle.rotation_angle_z = this.scroll._arrowRotation;
|
||||
},
|
||||
onCompleteScope: this,
|
||||
onComplete: function() {
|
||||
this.child.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const PromptDialogHeader = new Lang.Class({
|
||||
Name: 'PromptDialogHeader',
|
||||
|
||||
_init: function() {
|
||||
this.actor = new St.BoxLayout();
|
||||
let inner = new St.BoxLayout({ vertical: true });
|
||||
this.icon = new St.Icon({style_class: 'fw-prompt-icon'})
|
||||
this.title = new St.Label({style_class: 'fw-prompt-title'})
|
||||
this.message = new St.Label({style_class: 'fw-prompt-message'});
|
||||
this.message.clutter_text.line_wrap = true;
|
||||
this.message.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
||||
inner.add_child(this.title);
|
||||
inner.add_child(this.message);
|
||||
this.actor.add_child(this.icon);
|
||||
this.actor.add_child(inner);
|
||||
},
|
||||
|
||||
setTitle: function(text) {
|
||||
if(!text) {
|
||||
text = "Unknown";
|
||||
}
|
||||
this.title.text = text;
|
||||
},
|
||||
|
||||
setMessage: function(text) {
|
||||
this.message.text = text;
|
||||
},
|
||||
|
||||
setIcon: function(name) {
|
||||
this.icon.icon_name = name;
|
||||
},
|
||||
|
||||
setIconDefault: function() {
|
||||
this.setIcon('security-high-symbolic');
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const PromptDialog = new Lang.Class({
|
||||
Name: 'PromptDialog',
|
||||
Extends: ModalDialog.ModalDialog,
|
||||
|
||||
_init: function(invocation) {
|
||||
this.parent({ styleClass: 'fw-prompt-dialog' });
|
||||
this._invocation = invocation;
|
||||
this.header = new PromptDialogHeader();
|
||||
this.contentLayout.add_child(this.header.actor);
|
||||
|
||||
this.details = new ExpandingSection("Details");
|
||||
this.contentLayout.add(this.details.actor, {y_fill: false, x_fill: true});
|
||||
let box = new St.BoxLayout({ vertical: true });
|
||||
this.details.set_child(box);
|
||||
this.info = new DetailSection();
|
||||
box.add_child(this.info.actor);
|
||||
|
||||
this.optionList = new OptionList();
|
||||
box.add_child(this.optionList.actor);
|
||||
this.optionList.addOptions([
|
||||
"Any Connection",
|
||||
"Only PORT",
|
||||
"Only ADDRESS",
|
||||
"Only PORT AND ADDRESS"]);
|
||||
|
||||
|
||||
this.setButtons([
|
||||
{ label: "Allow", action: Lang.bind(this, this.onAllow) },
|
||||
{ label: "Deny", action: Lang.bind(this, this.onDeny) }]);
|
||||
|
||||
},
|
||||
|
||||
onAllow: function() {
|
||||
this.close();
|
||||
this.sendReturnValue(true);
|
||||
},
|
||||
|
||||
onDeny: function() {
|
||||
this.close();
|
||||
this.sendReturnValue(false);
|
||||
},
|
||||
|
||||
sendReturnValue: function(allow) {
|
||||
if(!this._invocation) {
|
||||
return;
|
||||
}
|
||||
let verb = "DENY";
|
||||
if(allow) {
|
||||
verb = "ALLOW";
|
||||
}
|
||||
let rule = verb + "|" + this.ruleTarget();
|
||||
let scope = this.optionList.selectedScope()
|
||||
this._invocation.return_value(GLib.Variant.new('(is)', [scope, rule]));
|
||||
this._invocation = null;
|
||||
},
|
||||
|
||||
ruleTarget: function() {
|
||||
switch(this.optionList.selectedIdx()) {
|
||||
case 0:
|
||||
return "*:*";
|
||||
case 1:
|
||||
return "*:" + this._port;
|
||||
case 2:
|
||||
return this._address + ":*";
|
||||
case 3:
|
||||
return this._address + ":" + this._port;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
update: function(application, icon, path, address, port, ip, user, pid) {
|
||||
this._address = address;
|
||||
this._port = port;
|
||||
|
||||
let port_str = "TCP Port "+ port;
|
||||
|
||||
this.header.setTitle(application);
|
||||
this.header.setMessage("Wants to connect to "+ address + " on " + port_str);
|
||||
|
||||
if(icon) {
|
||||
this.header.setIcon(icon);
|
||||
} else {
|
||||
this.header.setIconDefault();
|
||||
}
|
||||
|
||||
this.optionList.setOptionText(1, "Only "+ port_str);
|
||||
this.optionList.setOptionText(2, "Only "+ address);
|
||||
this.optionList.setOptionText(3, "Only "+ address + " on "+ port_str);
|
||||
this.info.setDetails(ip, path, pid, user);
|
||||
},
|
||||
});
|
@ -0,0 +1,97 @@
|
||||
const Lang = imports.lang;
|
||||
const Gio = imports.gi.Gio;
|
||||
|
||||
const Extension = imports.misc.extensionUtils.getCurrentExtension();
|
||||
const Dialog = Extension.imports.dialog;
|
||||
|
||||
function init() {
|
||||
return new FirewallSupport();
|
||||
}
|
||||
|
||||
const FirewallSupport = new Lang.Class({
|
||||
Name: 'FirewallSupport',
|
||||
|
||||
_init: function() {
|
||||
this.handler = null;
|
||||
},
|
||||
|
||||
_destroyHandler: function() {
|
||||
if(this.handler) {
|
||||
this.handler.destroy();
|
||||
this.handler = null;
|
||||
}
|
||||
},
|
||||
enable: function() {
|
||||
this._destroyHandler();
|
||||
this.handler = new FirewallPromptHandler();
|
||||
},
|
||||
disable: function() {
|
||||
this._destroyHandler();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// $ busctl --user call com.subgraph.FirewallPrompt /com/subgraph/FirewallPrompt com.subgraph.FirewallPrompt TestPrompt
|
||||
const FirewallPromptInterface = '<node> \
|
||||
<interface name="com.subgraph.FirewallPrompt"> \
|
||||
<method name="RequestPrompt"> \
|
||||
<arg type="s" direction="in" name="application" /> \
|
||||
<arg type="s" direction="in" name="icon" /> \
|
||||
<arg type="s" direction="in" name="path" /> \
|
||||
<arg type="s" direction="in" name="address" /> \
|
||||
<arg type="i" direction="in" name="port" /> \
|
||||
<arg type="s" direction="in" name="ip" /> \
|
||||
<arg type="s" direction="in" name="user" /> \
|
||||
<arg type="i" direction="in" name="pid" /> \
|
||||
<arg type="i" direction="out" name="scope" /> \
|
||||
<arg type="s" direction="out" name="rule" /> \
|
||||
</method> \
|
||||
<method name="ClosePrompt"/> \
|
||||
<method name="TestPrompt"/> \
|
||||
</interface> \
|
||||
</node>';
|
||||
|
||||
const FirewallPromptHandler = new Lang.Class({
|
||||
Name: 'FirewallPromptHandler',
|
||||
|
||||
_init: function() {
|
||||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(FirewallPromptInterface, this);
|
||||
this._dbusImpl.export(Gio.DBus.session, '/com/subgraph/FirewallPrompt');
|
||||
Gio.bus_own_name_on_connection(Gio.DBus.session, 'com.subgraph.FirewallPrompt', Gio.BusNameOwnerFlags.REPLACE, null, null);
|
||||
this._dialog = null;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this._closeDialog();
|
||||
this._dbusImpl.unexport();
|
||||
},
|
||||
|
||||
_closeDialog: function() {
|
||||
if (this._dialog) {
|
||||
this._dialog.close();
|
||||
this._dialog = null;
|
||||
}
|
||||
},
|
||||
|
||||
RequestPromptAsync: function(params, invocation) {
|
||||
let [app, icon, path, address, port, ip, user, pid] = params;
|
||||
this._closeDialog();
|
||||
this._dialog = new Dialog.PromptDialog(invocation);
|
||||
this._invocation = invocation;
|
||||
this._dialog.update(app, icon, path, address, port, ip, user, pid);
|
||||
this._dialog.open();
|
||||
|
||||
},
|
||||
|
||||
CloseAsync: function(params, invocation) {
|
||||
this._closeDialog();
|
||||
},
|
||||
|
||||
TestPrompt: function(params, invocation) {
|
||||
this._closeDialog();
|
||||
this._dialog = new Dialog.PromptDialog(nil);
|
||||
this._dialog.update("Firefox", "firefox", "/usr/bin/firefox", "242.12.111.18", "443", "linux", "2342");
|
||||
this._dialog.open();
|
||||
}
|
||||
});
|
||||
|
@ -0,0 +1 @@
|
||||
{"description": "Firewall Extension", "shell-version": ["3.18"], "uuid": "firewall@subgraph.com", "name": "Firewall Extension"}
|
@ -0,0 +1,71 @@
|
||||
.fw-prompt-dialog {
|
||||
min-width: 450px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.fw-group-button:checked {
|
||||
color: white;
|
||||
border-color: rgba(0,0,0,0.7);
|
||||
background-color: #222728;
|
||||
box-shadow: inset 0 0 black;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.fw-button-group {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.fw-option-list {
|
||||
border: 2px solid rgba(238, 238, 236, 0.5);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.fw-option-item{
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.fw-option-item:focus {
|
||||
background-color: #215d9c;
|
||||
}
|
||||
|
||||
.fw-option-item:selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fw-option-item-icon {
|
||||
icon-size: 16px;
|
||||
}
|
||||
|
||||
.fw-prompt-title {
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fw-prompt-icon {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.fw-detail-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fw-expanding-section-header {
|
||||
border-width: 1px;
|
||||
border-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.fw-expanding-section-header:focus {
|
||||
border-color: #215d9c;
|
||||
}
|
||||
|
||||
.fw-expanding-section-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fw-details-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.fw-details-left {
|
||||
padding-right: 10px;
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DesktopEntry struct {
|
||||
icon string
|
||||
name string
|
||||
}
|
||||
|
||||
var entryMap = map[string]*DesktopEntry{}
|
||||
var initialized = false
|
||||
|
||||
func entryForPath(p string) *DesktopEntry {
|
||||
if !initialized {
|
||||
initIcons()
|
||||
}
|
||||
entry, ok := entryMap[path.Base(p)]
|
||||
if ok {
|
||||
return entry
|
||||
}
|
||||
return entryMap[p]
|
||||
}
|
||||
|
||||
func initIcons() {
|
||||
if initialized {
|
||||
return
|
||||
}
|
||||
path := "/usr/share/applications"
|
||||
dir, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Warning("Failed to open %s for reading: %v", path, err)
|
||||
return
|
||||
}
|
||||
names, err := dir.Readdirnames(0)
|
||||
if err != nil {
|
||||
log.Warning("Could not read directory %s: %v", path, err)
|
||||
return
|
||||
}
|
||||
for _, n := range names {
|
||||
if strings.HasSuffix(n, ".desktop") {
|
||||
loadDesktopFile(fmt.Sprintf("%s/%s", path, n))
|
||||
}
|
||||
}
|
||||
initialized = true
|
||||
}
|
||||
|
||||
func loadDesktopFile(path string) {
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Warning("Error reading %s: %v", path, err)
|
||||
return
|
||||
}
|
||||
exec := ""
|
||||
icon := ""
|
||||
name := ""
|
||||
inDE := false
|
||||
|
||||
for _, line := range strings.Split(string(bs), "\n") {
|
||||
if strings.Contains(line, "[Desktop Entry]") {
|
||||
inDE = true
|
||||
} else if len(line) > 0 && line[0] == '[' {
|
||||
inDE = false
|
||||
}
|
||||
if inDE && strings.HasPrefix(line, "Exec=") {
|
||||
exec = strings.Fields(line[5:])[0]
|
||||
}
|
||||
if inDE && strings.HasPrefix(line, "Icon=") {
|
||||
icon = line[5:]
|
||||
}
|
||||
if inDE && strings.HasPrefix(line, "Name=") {
|
||||
name = line[5:]
|
||||
}
|
||||
}
|
||||
if exec != "" && icon != "" {
|
||||
entryMap[exec] = &DesktopEntry{
|
||||
icon: icon,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const iptablesRule = "-t mangle -%c OUTPUT -m conntrack --ctstate NEW -j NFQUEUE --queue-num 0 --queue-bypass"
|
||||
const dnsRule = "-%c INPUT --protocol udp -m multiport --source-ports 53 -j NFQUEUE --queue-num 0 --queue-bypass"
|
||||
|
||||
func setupIPTables() {
|
||||
removeIPTRules(dnsRule, iptablesRule)
|
||||
addIPTRules(iptablesRule, dnsRule)
|
||||
}
|
||||
|
||||
func removeIPTRules(rules ...string) {
|
||||
for _, r := range rules {
|
||||
iptables('D', r)
|
||||
}
|
||||
}
|
||||
|
||||
func addIPTRules(rules ...string) {
|
||||
for _, r := range rules {
|
||||
iptables('I', r)
|
||||
}
|
||||
}
|
||||
|
||||
func iptables(verb rune, rule string) {
|
||||
|
||||
iptablesPath, err := exec.LookPath("iptables")
|
||||
if err != nil {
|
||||
log.Warning("Could not find iptables binary in path")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
argLine := fmt.Sprintf(rule, verb)
|
||||
args := strings.Fields(argLine)
|
||||
fmt.Println(iptablesPath, argLine)
|
||||
cmd := exec.Command(iptablesPath, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
fmt.Fprintf(os.Stderr, string(out))
|
||||
_, exitErr := err.(*exec.ExitError)
|
||||
if err != nil && !exitErr {
|
||||
log.Warning("Error reading output: %v", err)
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// _ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
"github.com/subgraph/fw-daemon/nfqueue"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("sgfw")
|
||||
var format = logging.MustStringFormatter(
|
||||
"%{color}%{time:15:04:05} â–¶ %{level:.4s} %{id:03x}%{color:reset} %{message}",
|
||||
)
|
||||
|
||||
func init() {
|
||||
backend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
formatter := logging.NewBackendFormatter(backend, format)
|
||||
leveler := logging.AddModuleLevel(formatter)
|
||||
log.SetBackend(leveler)
|
||||
}
|
||||
|
||||
type Firewall struct {
|
||||
dbus *dbusServer
|
||||
dns *dnsCache
|
||||
|
||||
lock sync.Mutex
|
||||
policyMap map[string]*Policy
|
||||
policies []*Policy
|
||||
}
|
||||
|
||||
func (fw *Firewall) runFilter() {
|
||||
q := nfqueue.NewNFQueue(0)
|
||||
defer q.Destroy()
|
||||
|
||||
q.DefaultVerdict = nfqueue.DROP
|
||||
q.Timeout = 5 * time.Minute
|
||||
packets := q.Process()
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, os.Interrupt, os.Kill)
|
||||
|
||||
for {
|
||||
select {
|
||||
case pkt := <-packets:
|
||||
fw.filterPacket(pkt)
|
||||
case <-sigs:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
log.Error("Must be run as root")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupIPTables()
|
||||
|
||||
dbus, err := dbusConnect()
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
ds, err := newDbusServer(dbus)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fw := &Firewall{
|
||||
dbus: ds,
|
||||
dns: NewDnsCache(),
|
||||
policyMap: make(map[string]*Policy),
|
||||
}
|
||||
|
||||
fw.loadRules()
|
||||
|
||||
/*
|
||||
go func() {
|
||||
http.ListenAndServe("localhost:6060", nil)
|
||||
}()
|
||||
*/
|
||||
|
||||
fw.runFilter()
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -0,0 +1,42 @@
|
||||
Go-NFQueue
|
||||
==========
|
||||
Go Wrapper For Creating IPTables' NFQueue clients in Go
|
||||
|
||||
Usage
|
||||
------
|
||||
Check the `examples/main.go` file
|
||||
|
||||
```bash
|
||||
cd $GOPATH/github.com/OneOfOne/go-nfqueue/examples
|
||||
go build -race && sudo ./examples
|
||||
```
|
||||
* Open another terminal :
|
||||
```bash
|
||||
sudo iptables -I INPUT 1 -m conntrack --ctstate NEW -j NFQUEUE --queue-num 0
|
||||
#or
|
||||
sudo iptables -I INPUT -i eth0 -m conntrack --ctstate NEW -j NFQUEUE --queue-num 0
|
||||
curl --head localhost
|
||||
ping localhost
|
||||
sudo iptables -D INPUT -m conntrack --ctstate NEW -j NFQUEUE --queue-num 0
|
||||
```
|
||||
Then you can `ctrl+c` the program to exit.
|
||||
|
||||
* If you have recent enough iptables/nfqueue you could also use a balanced (multithreaded queue).
|
||||
* check the example in `examples/mq/multiqueue.go`
|
||||
|
||||
```bash
|
||||
iptables -I INPUT 1 -m conntrack --ctstate NEW -j NFQUEUE --queue-balance 0:5 --queue-cpu-fanout
|
||||
```
|
||||
Notes
|
||||
-----
|
||||
|
||||
You must run the executable as root.
|
||||
This is *WIP*, but all patches are welcome.
|
||||
|
||||
License
|
||||
-------
|
||||
go-nfqueue is under the Apache v2 license, check the included license file.
|
||||
Copyright © [Ahmed W.](http://www.limitlessfx.com/)
|
||||
See the included `LICENSE` file.
|
||||
|
||||
> Copyright (c) 2014 Ahmed W.
|
@ -0,0 +1,41 @@
|
||||
package nfqueue
|
||||
|
||||
import "sync"
|
||||
|
||||
type multiQueue struct {
|
||||
qs []*nfQueue
|
||||
}
|
||||
|
||||
func NewMultiQueue(min, max uint16) (mq *multiQueue) {
|
||||
mq = &multiQueue{make([]*nfQueue, 0, max-min)}
|
||||
for i := min; i < max; i++ {
|
||||
mq.qs = append(mq.qs, NewNFQueue(i))
|
||||
}
|
||||
return mq
|
||||
}
|
||||
|
||||
func (mq *multiQueue) Process() <-chan *Packet {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
out = make(chan *Packet, len(mq.qs))
|
||||
)
|
||||
for _, q := range mq.qs {
|
||||
wg.Add(1)
|
||||
go func(ch <-chan *Packet) {
|
||||
for pkt := range ch {
|
||||
out <- pkt
|
||||
}
|
||||
wg.Done()
|
||||
}(q.Process())
|
||||
}
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(out)
|
||||
}()
|
||||
return out
|
||||
}
|
||||
func (mq *multiQueue) Destroy() {
|
||||
for _, q := range mq.qs {
|
||||
q.Destroy()
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
#include "nfqueue.h"
|
||||
#include "_cgo_export.h"
|
||||
|
||||
|
||||
int nfqueue_cb_new(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfa, void *data) {
|
||||
|
||||
struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfa);
|
||||
|
||||
if(ph == NULL) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int id = ntohl(ph->packet_id);
|
||||
|
||||
unsigned char * payload;
|
||||
unsigned char * saddr, * daddr;
|
||||
uint16_t sport = 0, dport = 0, checksum = 0;
|
||||
uint32_t mark = nfq_get_nfmark(nfa);
|
||||
|
||||
int len = nfq_get_payload(nfa, &payload);
|
||||
|
||||
if(len < sizeof(struct iphdr)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct iphdr * ip = (struct iphdr *) payload;
|
||||
|
||||
if(ip->version == 4) {
|
||||
uint32_t ipsz = (ip->ihl << 2);
|
||||
if(len < ipsz) {
|
||||
return 0;
|
||||
}
|
||||
len -= ipsz;
|
||||
payload += ipsz;
|
||||
|
||||
saddr = (unsigned char *)&ip->saddr;
|
||||
daddr = (unsigned char *)&ip->daddr;
|
||||
|
||||
if(ip->protocol == IPPROTO_TCP) {
|
||||
if(len < sizeof(struct tcphdr)) {
|
||||
return 0;
|
||||
}
|
||||
struct tcphdr *tcp = (struct tcphdr *) payload;
|
||||
uint32_t tcpsz = (tcp->doff << 2);
|
||||
if(len < tcpsz) {
|
||||
return 0;
|
||||
}
|
||||
len -= tcpsz;
|
||||
payload += tcpsz;
|
||||
|
||||
sport = ntohs(tcp->source);
|
||||
dport = ntohs(tcp->dest);
|
||||
checksum = ntohs(tcp->check);
|
||||
} else if(ip->protocol == IPPROTO_UDP) {
|
||||
if(len < sizeof(struct udphdr)) {
|
||||
return 0;
|
||||
}
|
||||
struct udphdr *u = (struct udphdr *) payload;
|
||||
len -= sizeof(struct udphdr);
|
||||
payload += sizeof(struct udphdr);
|
||||
|
||||
sport = ntohs(u->source);
|
||||
dport = ntohs(u->dest);
|
||||
checksum = ntohs(u->check);
|
||||
}
|
||||
} else {
|
||||
struct ipv6hdr *ip6 = (struct ipv6hdr*) payload;
|
||||
saddr = (unsigned char *)&ip6->saddr;
|
||||
daddr = (unsigned char *)&ip6->daddr;
|
||||
//ipv6
|
||||
}
|
||||
//pass everything we can and let Go handle it, I'm not a big fan of C
|
||||
uint32_t verdict = go_nfq_callback(id, ntohs(ph->hw_protocol), ph->hook, &mark, ip->version, ip->protocol,
|
||||
ip->tos, ip->ttl, saddr, daddr, sport, dport, checksum, len, payload, data);
|
||||
return nfq_set_verdict2(qh, id, verdict, mark, 0, NULL);
|
||||
}
|
||||
|
||||
void loop_for_packets(struct nfq_handle *h) {
|
||||
int fd = nfq_fd(h);
|
||||
char buf[4096] __attribute__ ((aligned));
|
||||
int rv;
|
||||
while ((rv = recv(fd, buf, sizeof(buf), 0)) && rv >= 0) {
|
||||
nfq_handle_packet(h, buf, rv);
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
package nfqueue
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lnetfilter_queue
|
||||
#cgo CFLAGS: -Wall
|
||||
#include "nfqueue.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type nfQueue struct {
|
||||
DefaultVerdict Verdict
|
||||
Timeout time.Duration
|
||||
qid uint16
|
||||
h *C.struct_nfq_handle
|
||||
//qh *C.struct_q_handle
|
||||
qh *C.struct_nfq_q_handle
|
||||
fd int
|
||||
lk sync.Mutex
|
||||
|
||||
pktch chan *Packet
|
||||
}
|
||||
|
||||
func NewNFQueue(qid uint16) (nfq *nfQueue) {
|
||||
if os.Geteuid() != 0 {
|
||||
|
||||
}
|
||||
if os.Geteuid() != 0 {
|
||||
panic("Must be ran by root.")
|
||||
}
|
||||
nfq = &nfQueue{DefaultVerdict: ACCEPT, Timeout: time.Microsecond * 5, qid: qid}
|
||||
return nfq
|
||||
}
|
||||
|
||||
/*
|
||||
This returns a channel that will recieve packets,
|
||||
the user then must call pkt.Accept() or pkt.Drop()
|
||||
*/
|
||||
func (this *nfQueue) Process() <-chan *Packet {
|
||||
if this.h != nil {
|
||||
return this.pktch
|
||||
}
|
||||
this.init()
|
||||
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
C.loop_for_packets(this.h)
|
||||
}()
|
||||
|
||||
return this.pktch
|
||||
}
|
||||
|
||||
func (this *nfQueue) init() {
|
||||
var err error
|
||||
if this.h, err = C.nfq_open(); err != nil || this.h == nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//if this.qh, err = C.nfq_create_queue(this.h, qid, C.get_cb(), unsafe.Pointer(nfq)); err != nil || this.qh == nil {
|
||||
|
||||
this.pktch = make(chan *Packet, 1)
|
||||
|
||||
if C.nfq_unbind_pf(this.h, C.AF_INET) < 0 {
|
||||
this.Destroy()
|
||||
panic("nfq_unbind_pf(AF_INET) failed, are you running root?.")
|
||||
}
|
||||
if C.nfq_unbind_pf(this.h, C.AF_INET6) < 0 {
|
||||
this.Destroy()
|
||||
panic("nfq_unbind_pf(AF_INET6) failed.")
|
||||
}
|
||||
|
||||
if C.nfq_bind_pf(this.h, C.AF_INET) < 0 {
|
||||
this.Destroy()
|
||||
panic("nfq_bind_pf(AF_INET) failed.")
|
||||
}
|
||||
|
||||
if C.nfq_bind_pf(this.h, C.AF_INET6) < 0 {
|
||||
this.Destroy()
|
||||
panic("nfq_bind_pf(AF_INET6) failed.")
|
||||
}
|
||||
|
||||
if this.qh, err = C.create_queue(this.h, C.uint16_t(this.qid), unsafe.Pointer(this)); err != nil || this.qh == nil {
|
||||
C.nfq_close(this.h)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
this.fd = int(C.nfq_fd(this.h))
|
||||
|
||||
if C.nfq_set_mode(this.qh, C.NFQNL_COPY_PACKET, 0xffff) < 0 {
|
||||
this.Destroy()
|
||||
panic("nfq_set_mode(NFQNL_COPY_PACKET) failed.")
|
||||
}
|
||||
if C.nfq_set_queue_maxlen(this.qh, 1024*8) < 0 {
|
||||
this.Destroy()
|
||||
panic("nfq_set_queue_maxlen(1024 * 8) failed.")
|
||||
}
|
||||
}
|
||||
|
||||
func (this *nfQueue) Destroy() {
|
||||
this.lk.Lock()
|
||||
defer this.lk.Unlock()
|
||||
|
||||
if this.fd != 0 && this.Valid() {
|
||||
syscall.Close(this.fd)
|
||||
}
|
||||
if this.qh != nil {
|
||||
C.nfq_destroy_queue(this.qh)
|
||||
this.qh = nil
|
||||
}
|
||||
if this.h != nil {
|
||||
C.nfq_close(this.h)
|
||||
this.h = nil
|
||||
}
|
||||
|
||||
if this.pktch != nil {
|
||||
close(this.pktch)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *nfQueue) Valid() bool {
|
||||
return this.h != nil && this.qh != nil
|
||||
}
|
||||
|
||||
//export go_nfq_callback
|
||||
func go_nfq_callback(id uint32, hwproto uint16, hook uint8, mark *uint32,
|
||||
version, protocol, tos, ttl uint8, saddr, daddr unsafe.Pointer,
|
||||
sport, dport, checksum uint16, payload_len uint32, payload, nfqptr unsafe.Pointer) (v uint32) {
|
||||
|
||||
var (
|
||||
nfq = (*nfQueue)(nfqptr)
|
||||
ipver = IPVersion(version)
|
||||
ipsz = C.int(ipver.Size())
|
||||
)
|
||||
bs := C.GoBytes(payload, (C.int)(payload_len))
|
||||
|
||||
verdict := make(chan uint32, 1)
|
||||
pkt := Packet{
|
||||
QueueId: nfq.qid,
|
||||
Id: id,
|
||||
HWProtocol: hwproto,
|
||||
Hook: hook,
|
||||
Mark: *mark,
|
||||
Payload: bs,
|
||||
IPHeader: &IPHeader{
|
||||
Version: ipver,
|
||||
Protocol: IPProtocol(protocol),
|
||||
Tos: tos,
|
||||
TTL: ttl,
|
||||
Src: net.IP(C.GoBytes(saddr, ipsz)),
|
||||
Dst: net.IP(C.GoBytes(daddr, ipsz)),
|
||||
},
|
||||
|
||||
TCPUDPHeader: &TCPUDPHeader{
|
||||
SrcPort: sport,
|
||||
DstPort: dport,
|
||||
Checksum: checksum,
|
||||
},
|
||||
|
||||
verdict: verdict,
|
||||
}
|
||||
nfq.pktch <- &pkt
|
||||
|
||||
select {
|
||||
case v = <-pkt.verdict:
|
||||
*mark = pkt.Mark
|
||||
case <-time.After(nfq.Timeout):
|
||||
v = uint32(nfq.DefaultVerdict)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
// #define _BSD_SOURCE
|
||||
// #define __BSD_SOURCE
|
||||
|
||||
// #define __FAVOR_BSD // Just Using _BSD_SOURCE didn't work on my system for some reason
|
||||
// #define __USE_BSD
|
||||
#include <stdlib.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <linux/ip.h>
|
||||
#include <linux/tcp.h>
|
||||
#include <linux/udp.h>
|
||||
#include <linux/ipv6.h>
|
||||
#include <linux/netfilter.h>
|
||||
#include <libnetfilter_queue/libnetfilter_queue.h>
|
||||
|
||||
// extern int nfq_callback(uint8_t version, uint8_t protocol, unsigned char *saddr, unsigned char *daddr,
|
||||
// uint16_t sport, uint16_t dport, unsigned char * extra, void* data);
|
||||
|
||||
int nfqueue_cb_new(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfa, void *data);
|
||||
void loop_for_packets(struct nfq_handle *h);
|
||||
|
||||
static inline struct nfq_q_handle * create_queue(struct nfq_handle *h, uint16_t num, void *data) {
|
||||
//we use this because it's more convient to pass the callback in C
|
||||
return nfq_create_queue(h, num, &nfqueue_cb_new, data);
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package nfqueue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type (
|
||||
IPVersion uint8
|
||||
IPProtocol uint8
|
||||
Verdict uint8
|
||||
)
|
||||
|
||||
const (
|
||||
IPv4 = IPVersion(4)
|
||||
IPv6 = IPVersion(6)
|
||||
|
||||
//convience really
|
||||
IGMP = IPProtocol(syscall.IPPROTO_IGMP)
|
||||
RAW = IPProtocol(syscall.IPPROTO_RAW)
|
||||
TCP = IPProtocol(syscall.IPPROTO_TCP)
|
||||
UDP = IPProtocol(syscall.IPPROTO_UDP)
|
||||
ICMP = IPProtocol(syscall.IPPROTO_ICMP)
|
||||
ICMPv6 = IPProtocol(syscall.IPPROTO_ICMPV6)
|
||||
)
|
||||
|
||||
const (
|
||||
DROP Verdict = iota
|
||||
ACCEPT
|
||||
STOLEN
|
||||
QUEUE
|
||||
REPEAT
|
||||
STOP
|
||||
)
|
||||
|
||||
var (
|
||||
ErrVerdictSentOrTimedOut error = fmt.Errorf("The verdict was already sent or timed out.")
|
||||
)
|
||||
|
||||
func (v IPVersion) String() string {
|
||||
switch v {
|
||||
case IPv4:
|
||||
return "IPv4"
|
||||
case IPv6:
|
||||
return "IPv6"
|
||||
}
|
||||
return fmt.Sprintf("<unknown ip version, %d>", uint8(v))
|
||||
}
|
||||
|
||||
// Returns the byte size of the ip, IPv4 = 4 bytes, IPv6 = 16
|
||||
func (v IPVersion) Size() int {
|
||||
switch v {
|
||||
case IPv4:
|
||||
return 4
|
||||
case IPv6:
|
||||
return 16
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p IPProtocol) String() string {
|
||||
switch p {
|
||||
case RAW:
|
||||
return "RAW"
|
||||
case TCP:
|
||||
return "TCP"
|
||||
case UDP:
|
||||
return "UDP"
|
||||
case ICMP:
|
||||
return "ICMP"
|
||||
case ICMPv6:
|
||||
return "ICMPv6"
|
||||
case IGMP:
|
||||
return "IGMP"
|
||||
}
|
||||
return fmt.Sprintf("<unknown protocol, %d>", uint8(p))
|
||||
}
|
||||
|
||||
func (v Verdict) String() string {
|
||||
switch v {
|
||||
case DROP:
|
||||
return "DROP"
|
||||
case ACCEPT:
|
||||
return "ACCEPT"
|
||||
}
|
||||
return fmt.Sprintf("<unsupported verdict, %d>", uint8(v))
|
||||
}
|
||||
|
||||
type IPHeader struct {
|
||||
Version IPVersion
|
||||
|
||||
Tos, TTL uint8
|
||||
Protocol IPProtocol
|
||||
Src, Dst net.IP
|
||||
}
|
||||
|
||||
type TCPUDPHeader struct {
|
||||
SrcPort, DstPort uint16
|
||||
Checksum uint16 //not implemented
|
||||
}
|
||||
|
||||
// TODO handle other protocols
|
||||
|
||||
type Packet struct {
|
||||
QueueId uint16
|
||||
Id uint32
|
||||
HWProtocol uint16
|
||||
Hook uint8
|
||||
Mark uint32
|
||||
Payload []byte
|
||||
*IPHeader
|
||||
*TCPUDPHeader
|
||||
|
||||
verdict chan uint32
|
||||
}
|
||||
|
||||
func (pkt *Packet) String() string {
|
||||
return fmt.Sprintf("<Packet QId: %d, Id: %d, Type: %s, Src: %s:%d, Dst: %s:%d, Mark: 0x%X, Checksum: 0x%X, TOS: 0x%X, TTL: %d>",
|
||||
pkt.QueueId, pkt.Id, pkt.Protocol, pkt.Src, pkt.SrcPort, pkt.Dst, pkt.DstPort, pkt.Mark, pkt.Checksum, pkt.Tos, pkt.TTL)
|
||||
}
|
||||
|
||||
func (pkt *Packet) setVerdict(v Verdict) (err error) {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
err = ErrVerdictSentOrTimedOut
|
||||
}
|
||||
}()
|
||||
pkt.verdict <- uint32(v)
|
||||
close(pkt.verdict)
|
||||
return err
|
||||
}
|
||||
|
||||
func (pkt *Packet) Accept() error {
|
||||
return pkt.setVerdict(ACCEPT)
|
||||
}
|
||||
|
||||
func (pkt *Packet) Drop() error {
|
||||
return pkt.setVerdict(DROP)
|
||||
}
|
||||
|
||||
//HUGE warning, if the iptables rules aren't set correctly this can cause some problems.
|
||||
// func (pkt *Packet) Repeat() error {
|
||||
// return this.SetVerdict(REPEAT)
|
||||
// }
|
@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/subgraph/fw-daemon/nfqueue"
|
||||
)
|
||||
|
||||
type pendingPkt struct {
|
||||
policy *Policy
|
||||
hostname string
|
||||
pkt *nfqueue.Packet
|
||||
proc *ProcInfo
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
fw *Firewall
|
||||
path string
|
||||
application string
|
||||
icon string
|
||||
rules RuleList
|
||||
pendingQueue []*pendingPkt
|
||||
promptInProgress bool
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (fw *Firewall) policyForPath(path string) *Policy {
|
||||
fw.lock.Lock()
|
||||
defer fw.lock.Unlock()
|
||||
if _, ok := fw.policyMap[path]; !ok {
|
||||
p := new(Policy)
|
||||
p.fw = fw
|
||||
p.path = path
|
||||
p.application = path
|
||||
entry := entryForPath(path)
|
||||
if entry != nil {
|
||||
p.application = entry.name
|
||||
p.icon = entry.icon
|
||||
}
|
||||
fw.policyMap[path] = p
|
||||
fw.policies = append(fw.policies, p)
|
||||
}
|
||||
return fw.policyMap[path]
|
||||
}
|
||||
|
||||
func (p *Policy) processPacket(pkt *nfqueue.Packet, proc *ProcInfo) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
name := p.fw.dns.Lookup(pkt.Dst)
|
||||
log.Info("Lookup(%s): %s", pkt.Dst.String(), name)
|
||||
result := p.rules.filter(pkt, proc, name)
|
||||
switch result {
|
||||
case FILTER_DENY:
|
||||
pkt.Drop()
|
||||
case FILTER_ALLOW:
|
||||
pkt.Accept()
|
||||
case FILTER_PROMPT:
|
||||
p.processPromptResult(&pendingPkt{policy: p, hostname: name, pkt: pkt, proc: proc})
|
||||
default:
|
||||
log.Warning("Unexpected filter result: %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Policy) processPromptResult(pp *pendingPkt) {
|
||||
p.pendingQueue = append(p.pendingQueue, pp)
|
||||
if !p.promptInProgress {
|
||||
p.promptInProgress = true
|
||||
go p.fw.dbus.prompt(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Policy) nextPending() *pendingPkt {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
if len(p.pendingQueue) == 0 {
|
||||
return nil
|
||||
}
|
||||
return p.pendingQueue[0]
|
||||
}
|
||||
|
||||
func (p *Policy) removePending(pp *pendingPkt) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
remaining := []*pendingPkt{}
|
||||
for _, pkt := range p.pendingQueue {
|
||||
if pkt != pp {
|
||||
remaining = append(remaining, pkt)
|
||||
}
|
||||
}
|
||||
if len(remaining) != len(p.pendingQueue) {
|
||||
p.pendingQueue = remaining
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Policy) processNewRule(r *Rule, scope int32) bool {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if scope != APPLY_ONCE {
|
||||
p.rules = append(p.rules, r)
|
||||
}
|
||||
|
||||
p.filterPending(r)
|
||||
if len(p.pendingQueue) == 0 {
|
||||
p.promptInProgress = false
|
||||
}
|
||||
|
||||
return p.promptInProgress
|
||||
}
|
||||
|
||||
func (p *Policy) filterPending(rule *Rule) {
|
||||
remaining := []*pendingPkt{}
|
||||
for _, pp := range p.pendingQueue {
|
||||
if rule.match(pp.pkt, pp.hostname) {
|
||||
log.Info("Also applying %s to %s", rule, printPacket(pp.pkt, pp.hostname))
|
||||
if rule.rtype == RULE_ALLOW {
|
||||
pp.pkt.Accept()
|
||||
} else {
|
||||
pp.pkt.Drop()
|
||||
}
|
||||
} else {
|
||||
remaining = append(remaining, pp)
|
||||
}
|
||||
}
|
||||
if len(remaining) != len(p.pendingQueue) {
|
||||
p.pendingQueue = remaining
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Policy) hasPersistentRules() bool {
|
||||
for _, r := range p.rules {
|
||||
if !r.sessionOnly {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func printPacket(pkt *nfqueue.Packet, hostname string) string {
|
||||
proto := func() string {
|
||||
switch pkt.Protocol {
|
||||
case nfqueue.TCP:
|
||||
return "TCP"
|
||||
case nfqueue.UDP:
|
||||
return "UDP"
|
||||
default:
|
||||
return "???"
|
||||
}
|
||||
}()
|
||||
name := hostname
|
||||
if name == "" {
|
||||
name = pkt.Dst.String()
|
||||
}
|
||||
return fmt.Sprintf("(%s %s:%d --> %s:%d)", proto, pkt.Src, pkt.SrcPort, name, pkt.DstPort)
|
||||
}
|
||||
|
||||
func (fw *Firewall) filterPacket(pkt *nfqueue.Packet) {
|
||||
if pkt.Protocol == nfqueue.UDP && pkt.SrcPort == 53 {
|
||||
pkt.Accept()
|
||||
fw.dns.processDNS(pkt)
|
||||
return
|
||||
}
|
||||
log.Debug("filterPacket %s", printPacket(pkt, fw.dns.Lookup(pkt.Dst)))
|
||||
if basicAllowPacket(pkt) {
|
||||
pkt.Accept()
|
||||
return
|
||||
}
|
||||
|
||||
proc := findProcessForPacket(pkt)
|
||||
|
||||
if proc == nil {
|
||||
log.Warning("No process for: %v", pkt)
|
||||
pkt.Accept()
|
||||
return
|
||||
}
|
||||
policy := fw.policyForPath(proc.exePath)
|
||||
policy.processPacket(pkt, proc)
|
||||
}
|
||||
|
||||
func basicAllowPacket(pkt *nfqueue.Packet) bool {
|
||||
return pkt.Dst.IsLoopback() ||
|
||||
pkt.Dst.IsLinkLocalMulticast() ||
|
||||
pkt.Protocol != nfqueue.TCP
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/subgraph/fw-daemon/nfqueue"
|
||||
)
|
||||
|
||||
type ProcInfo struct {
|
||||
pid int
|
||||
uid int
|
||||
exePath string
|
||||
cmdLine string
|
||||
}
|
||||
|
||||
type socketAddr struct {
|
||||
ip net.IP
|
||||
port uint16
|
||||
}
|
||||
|
||||
type socketStatus struct {
|
||||
local socketAddr
|
||||
remote socketAddr
|
||||
uid int
|
||||
inode uint64
|
||||
pid int
|
||||
}
|
||||
|
||||
func findProcessForPacket(pkt *nfqueue.Packet) *ProcInfo {
|
||||
ss := getSocketForPacket(pkt)
|
||||
if ss == nil {
|
||||
return nil
|
||||
}
|
||||
exePath, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", ss.pid))
|
||||
if err != nil {
|
||||
log.Warning("Error reading exe link for pid %d: %v", ss.pid, err)
|
||||
return nil
|
||||
}
|
||||
bs, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", ss.pid))
|
||||
if err != nil {
|
||||
log.Warning("Error reading cmdline for pid %d: %v", ss.pid, err)
|
||||
return nil
|
||||
}
|
||||
for i, b := range bs {
|
||||
if b == 0 {
|
||||
bs[i] = byte(' ')
|
||||
}
|
||||
}
|
||||
|
||||
finfo, err := os.Stat(fmt.Sprintf("/proc/%d", ss.pid))
|
||||
if err != nil {
|
||||
log.Warning("Could not stat /proc/%d: %v", ss.pid, err)
|
||||
return nil
|
||||
}
|
||||
finfo.Sys()
|
||||
return &ProcInfo{
|
||||
pid: ss.pid,
|
||||
uid: ss.uid,
|
||||
exePath: exePath,
|
||||
cmdLine: string(bs),
|
||||
}
|
||||
}
|
||||
|
||||
func getSocketLinesForPacket(pkt *nfqueue.Packet) []string {
|
||||
if pkt.Protocol == nfqueue.TCP {
|
||||
return getSocketLines("tcp")
|
||||
} else if pkt.Protocol == nfqueue.UDP {
|
||||
return getSocketLines("udp")
|
||||
} else {
|
||||
log.Warning("Cannot lookup socket for protocol %s", pkt.Protocol)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getSocketLines(proto string) []string {
|
||||
path := fmt.Sprintf("/proc/net/%s", proto)
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Warning("Error reading %s: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
if len(lines) > 0 {
|
||||
lines = lines[1:]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (sa *socketAddr) parse(s string) error {
|
||||
ipPort := strings.Split(s, ":")
|
||||
if len(ipPort) != 2 {
|
||||
return fmt.Errorf("badly formatted socket address field: %s", s)
|
||||
}
|
||||
ip, err := ParseIp(ipPort[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing ip field [%s]: %v", ipPort[0], err)
|
||||
}
|
||||
port, err := ParsePort(ipPort[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing port field [%s]: %v", ipPort[1], err)
|
||||
}
|
||||
sa.ip = ip
|
||||
sa.port = port
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *socketStatus) parseLine(line string) error {
|
||||
fs := strings.Fields(line)
|
||||
if len(fs) < 10 {
|
||||
return errors.New("insufficient fields")
|
||||
}
|
||||
if err := ss.local.parse(fs[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ss.remote.parse(fs[2]); err != nil {
|
||||
return err
|
||||
}
|
||||
uid, err := strconv.ParseUint(fs[7], 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ss.uid = int(uid)
|
||||
inode, err := strconv.ParseUint(fs[9], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ss.inode = inode
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSocketForPacket(pkt *nfqueue.Packet) *socketStatus {
|
||||
ss := findSocket(pkt)
|
||||
if ss == nil {
|
||||
return nil
|
||||
}
|
||||
pid := findPidForInode(ss.inode)
|
||||
if pid == -1 {
|
||||
return nil
|
||||
}
|
||||
ss.pid = pid
|
||||
return ss
|
||||
}
|
||||
|
||||
func findSocket(pkt *nfqueue.Packet) *socketStatus {
|
||||
var status socketStatus
|
||||
for _, line := range getSocketLinesForPacket(pkt) {
|
||||
if err := status.parseLine(line); err != nil {
|
||||
log.Warning("Unable to parse line [%s]: %v", line, err)
|
||||
} else {
|
||||
if status.remote.ip.Equal(pkt.Dst) && status.remote.port == pkt.DstPort {
|
||||
return &status
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseIp(ip string) (net.IP, error) {
|
||||
var result net.IP
|
||||
dst, err := hex.DecodeString(ip)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("Error parsing IP: %s", err)
|
||||
}
|
||||
// Reverse byte order -- /proc/net/tcp etc. is little-endian
|
||||
// TODO: Does this vary by architecture?
|
||||
for i, j := 0, len(dst)-1; i < j; i, j = i+1, j-1 {
|
||||
dst[i], dst[j] = dst[j], dst[i]
|
||||
}
|
||||
result = net.IP(dst)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ParsePort(port string) (uint16, error) {
|
||||
p64, err := strconv.ParseInt(port, 16, 32)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Error parsing port: %s", err)
|
||||
}
|
||||
return uint16(p64), nil
|
||||
}
|
||||
|
||||
func findPidForInode(inode uint64) int {
|
||||
search := fmt.Sprintf("socket:[%d]", inode)
|
||||
for _, pid := range getAllPids() {
|
||||
if matchesSocketLink(pid, search) {
|
||||
return pid
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func matchesSocketLink(pid int, search string) bool {
|
||||
paths, _ := filepath.Glob(fmt.Sprintf("/proc/%d/fd/*", pid))
|
||||
for _, p := range paths {
|
||||
link, err := os.Readlink(p)
|
||||
if err == nil && link == search {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getAllPids() []int {
|
||||
var pids []int
|
||||
d, err := os.Open("/proc")
|
||||
if err != nil {
|
||||
log.Warning("Error opening /proc: %v", err)
|
||||
return nil
|
||||
}
|
||||
names, err := d.Readdirnames(0)
|
||||
if err != nil {
|
||||
log.Warning("Error reading directory names from /proc: %v", err)
|
||||
return nil
|
||||
}
|
||||
for _, n := range names {
|
||||
if pid, err := strconv.ParseUint(n, 10, 32); err == nil {
|
||||
pids = append(pids, int(pid))
|
||||
}
|
||||
}
|
||||
return pids
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/godbus/dbus"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
APPLY_ONCE = iota
|
||||
APPLY_SESSION
|
||||
APPLY_FOREVER
|
||||
)
|
||||
|
||||
func newPrompter(conn *dbus.Conn) *prompter {
|
||||
p := new(prompter)
|
||||
p.cond = sync.NewCond(&p.lock)
|
||||
p.dbusObj = conn.Object("com.subgraph.FirewallPrompt", "/com/subgraph/FirewallPrompt")
|
||||
p.policyMap = make(map[string]*Policy)
|
||||
go p.promptLoop()
|
||||
return p
|
||||
}
|
||||
|
||||
type prompter struct {
|
||||
dbusObj dbus.BusObject
|
||||
lock sync.Mutex
|
||||
cond *sync.Cond
|
||||
policyMap map[string]*Policy
|
||||
policyQueue []*Policy
|
||||
}
|
||||
|
||||
func (p *prompter) prompt(policy *Policy) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
_, ok := p.policyMap[policy.path]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
p.policyMap[policy.path] = policy
|
||||
p.policyQueue = append(p.policyQueue, policy)
|
||||
p.cond.Signal()
|
||||
}
|
||||
|
||||
func (p *prompter) promptLoop() {
|
||||
p.lock.Lock()
|
||||
for {
|
||||
for p.processNextPacket() {
|
||||
}
|
||||
p.cond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *prompter) processNextPacket() bool {
|
||||
pp := p.nextPacket()
|
||||
if pp == nil {
|
||||
return false
|
||||
}
|
||||
p.lock.Unlock()
|
||||
defer p.lock.Lock()
|
||||
p.processPacket(pp)
|
||||
return true
|
||||
}
|
||||
|
||||
func printScope(scope int32) string {
|
||||
switch scope {
|
||||
case APPLY_FOREVER:
|
||||
return "APPLY_FOREVER"
|
||||
case APPLY_SESSION:
|
||||
return "APPLY_SESSION"
|
||||
case APPLY_ONCE:
|
||||
return "APPLY_ONCE"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown (%d)", scope)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *prompter) processPacket(pp *pendingPkt) {
|
||||
var scope int32
|
||||
var rule string
|
||||
|
||||
addr := pp.hostname
|
||||
if addr == "" {
|
||||
addr = pp.pkt.Dst.String()
|
||||
}
|
||||
|
||||
call := p.dbusObj.Call("com.subgraph.FirewallPrompt.RequestPrompt", 0,
|
||||
pp.policy.application,
|
||||
pp.policy.icon,
|
||||
pp.policy.path,
|
||||
addr,
|
||||
int32(pp.pkt.DstPort),
|
||||
pp.pkt.Dst.String(),
|
||||
uidToUser(pp.proc.uid),
|
||||
int32(pp.proc.pid))
|
||||
err := call.Store(&scope, &rule)
|
||||
if err != nil {
|
||||
log.Warning("Error sending dbus RequestPrompt message: %v", err)
|
||||
pp.policy.removePending(pp)
|
||||
pp.pkt.Drop()
|
||||
return
|
||||
}
|
||||
log.Debug("Received prompt response: %s [%s]", printScope(scope), rule)
|
||||
|
||||
r, err := parseRule(rule)
|
||||
if err != nil {
|
||||
log.Warning("Error parsing rule string returned from dbus RequestPrompt: %v", err)
|
||||
pp.policy.removePending(pp)
|
||||
pp.pkt.Drop()
|
||||
return
|
||||
}
|
||||
if scope == APPLY_SESSION {
|
||||
r.sessionOnly = true
|
||||
}
|
||||
if !pp.policy.processNewRule(r, scope) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
p.removePolicy(pp.policy)
|
||||
}
|
||||
if scope == APPLY_FOREVER {
|
||||
pp.policy.fw.saveRules()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *prompter) nextPacket() *pendingPkt {
|
||||
for {
|
||||
if len(p.policyQueue) == 0 {
|
||||
return nil
|
||||
}
|
||||
policy := p.policyQueue[0]
|
||||
pp := policy.nextPending()
|
||||
if pp == nil {
|
||||
p.removePolicy(policy)
|
||||
} else {
|
||||
return pp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *prompter) removePolicy(policy *Policy) {
|
||||
newQueue := make([]*Policy, 0, len(p.policyQueue)-1)
|
||||
for _, pol := range p.policyQueue {
|
||||
if pol != policy {
|
||||
newQueue = append(newQueue, pol)
|
||||
}
|
||||
}
|
||||
p.policyQueue = newQueue
|
||||
delete(p.policyMap, policy.path)
|
||||
}
|
||||
|
||||
var userMap = make(map[int]string)
|
||||
|
||||
func lookupUser(uid int) string {
|
||||
u, err := user.LookupId(strconv.Itoa(uid))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%d", uid)
|
||||
}
|
||||
return u.Name
|
||||
}
|
||||
|
||||
func uidToUser(uid int) string {
|
||||
uname, ok := userMap[uid]
|
||||
if ok {
|
||||
return uname
|
||||
}
|
||||
uname = lookupUser(uid)
|
||||
userMap[uid] = uname
|
||||
return uname
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/subgraph/fw-daemon/nfqueue"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
RULE_DENY = iota
|
||||
RULE_ALLOW
|
||||
)
|
||||
|
||||
const matchAny = 0
|
||||
const noAddress = uint32(0xffffffff)
|
||||
|
||||
type Rule struct {
|
||||
sessionOnly bool
|
||||
rtype int
|
||||
hostname string
|
||||
addr uint32
|
||||
port uint16
|
||||
}
|
||||
|
||||
func (r *Rule) String() string {
|
||||
addr := "*"
|
||||
port := "*"
|
||||
rtype := "DENY"
|
||||
|
||||
if r.hostname != "" {
|
||||
addr = r.hostname
|
||||
} else if r.addr != matchAny && r.addr != noAddress {
|
||||
bs := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(bs, r.addr)
|
||||
addr = fmt.Sprintf("%d.%d.%d.%d", bs[0], bs[1], bs[2], bs[3])
|
||||
}
|
||||
|
||||
if r.port != matchAny {
|
||||
port = fmt.Sprintf("%d", r.port)
|
||||
}
|
||||
|
||||
if r.rtype == RULE_ALLOW {
|
||||
rtype = "ALLOW"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s:%s", rtype, addr, port)
|
||||
}
|
||||
|
||||
type RuleList []*Rule
|
||||
|
||||
func (r *Rule) match(pkt *nfqueue.Packet, name string) bool {
|
||||
if r.port != matchAny && r.port != pkt.DstPort {
|
||||
return false
|
||||
}
|
||||
if r.addr == matchAny {
|
||||
return true
|
||||
}
|
||||
if r.hostname != "" {
|
||||
return r.hostname == name
|
||||
}
|
||||
return r.addr == binary.BigEndian.Uint32(pkt.Dst)
|
||||
}
|
||||
|
||||
type FilterResult int
|
||||
|
||||
const (
|
||||
FILTER_DENY FilterResult = iota
|
||||
FILTER_ALLOW
|
||||
FILTER_PROMPT
|
||||
)
|
||||
|
||||
func (rl *RuleList) filter(p *nfqueue.Packet, proc *ProcInfo, hostname string) FilterResult {
|
||||
if rl == nil {
|
||||
return FILTER_PROMPT
|
||||
}
|
||||
result := FILTER_PROMPT
|
||||
for _, r := range *rl {
|
||||
if r.match(p, hostname) {
|
||||
log.Info("%s (%s -> %s:%d)", r, proc.exePath, p.Dst.String(), p.DstPort)
|
||||
if r.rtype == RULE_DENY {
|
||||
return FILTER_DENY
|
||||
} else if r.rtype == RULE_ALLOW {
|
||||
result = FILTER_ALLOW
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseError(s string) error {
|
||||
return fmt.Errorf("unable to parse rule string: %s", s)
|
||||
}
|
||||
|
||||
func (r *Rule) parse(s string) bool {
|
||||
r.addr = noAddress
|
||||
parts := strings.Split(s, "|")
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
return r.parseVerb(parts[0]) && r.parseTarget(parts[1])
|
||||
}
|
||||
|
||||
func (r *Rule) parseVerb(v string) bool {
|
||||
switch v {
|
||||
case "ALLOW":
|
||||
r.rtype = RULE_ALLOW
|
||||
return true
|
||||
case "DENY":
|
||||
r.rtype = RULE_DENY
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Rule) parseTarget(t string) bool {
|
||||
addrPort := strings.Split(t, ":")
|
||||
if len(addrPort) != 2 {
|
||||
return false
|
||||
}
|
||||
return r.parseAddr(addrPort[0]) && r.parsePort(addrPort[1])
|
||||
}
|
||||
|
||||
func (r *Rule) parseAddr(a string) bool {
|
||||
if a == "*" {
|
||||
r.hostname = ""
|
||||
r.addr = matchAny
|
||||
return true
|
||||
}
|
||||
if strings.IndexFunc(a, unicode.IsLetter) != -1 {
|
||||
r.hostname = a
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(a)
|
||||
if ip == nil || len(ip) != 4 {
|
||||
return false
|
||||
}
|
||||
r.addr = binary.BigEndian.Uint32(ip)
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Rule) parsePort(p string) bool {
|
||||
if p == "*" {
|
||||
r.port = matchAny
|
||||
return true
|
||||
}
|
||||
var err error
|
||||
port, err := strconv.ParseUint(p, 10, 16)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
r.port = uint16(port)
|
||||
return true
|
||||
}
|
||||
|
||||
func parseRule(s string) (*Rule, error) {
|
||||
r := new(Rule)
|
||||
if !r.parse(s) {
|
||||
return nil, parseError(s)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
const ruleFile = ".sgfw_rules"
|
||||
|
||||
func rulesPath() string {
|
||||
home := os.Getenv("HOME")
|
||||
if home != "" {
|
||||
return filepath.Join(home, ruleFile)
|
||||
}
|
||||
// XXX try something else?
|
||||
return ""
|
||||
}
|
||||
|
||||
func (fw *Firewall) saveRules() {
|
||||
fw.lock.Lock()
|
||||
defer fw.lock.Unlock()
|
||||
|
||||
f, err := os.Create(rulesPath())
|
||||
if err != nil {
|
||||
log.Warning("Failed to open %s for writing: %v", rulesPath(), err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
for _, p := range fw.policies {
|
||||
savePolicy(f, p)
|
||||
}
|
||||
}
|
||||
|
||||
func savePolicy(f *os.File, p *Policy) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
if !p.hasPersistentRules() {
|
||||
return
|
||||
}
|
||||
|
||||
if !writeLine(f, "["+p.path+"]") {
|
||||
return
|
||||
}
|
||||
for _, r := range p.rules {
|
||||
if !r.sessionOnly {
|
||||
if !writeLine(f, r.String()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeLine(f *os.File, line string) bool {
|
||||
_, err := f.WriteString(line + "\n")
|
||||
if err != nil {
|
||||
log.Warning("Error writing to rule file: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (fw *Firewall) loadRules() {
|
||||
fw.lock.Lock()
|
||||
defer fw.lock.Unlock()
|
||||
|
||||
bs, err := ioutil.ReadFile(rulesPath())
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Warning("Failed to open %s for reading: %v", rulesPath(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
var policy *Policy
|
||||
for _, line := range strings.Split(string(bs), "\n") {
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
policy = fw.processPathLine(line)
|
||||
} else {
|
||||
processRuleLine(policy, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fw *Firewall) processPathLine(line string) *Policy {
|
||||
path := line[1 : len(line)-1]
|
||||
policy := fw.policyForPath(path)
|
||||
policy.lock.Lock()
|
||||
defer policy.lock.Unlock()
|
||||
policy.rules = nil
|
||||
return policy
|
||||
}
|
||||
|
||||
func processRuleLine(policy *Policy, line string) {
|
||||
if policy == nil {
|
||||
log.Warning("Cannot process rule line without first seeing path line: %s", line)
|
||||
return
|
||||
}
|
||||
rule, err := parseRule(line)
|
||||
if err != nil {
|
||||
log.Warning("Error parsing rule (%s): %v", line, err)
|
||||
return
|
||||
}
|
||||
policy.lock.Lock()
|
||||
defer policy.lock.Unlock()
|
||||
policy.rules = append(policy.rules, rule)
|
||||
}
|
Loading…
Reference in new issue