Skip to content

Commit

Permalink
added support for list of domains
Browse files Browse the repository at this point in the history
Initial support to filter connections using lists of domains.

The lists must be in hosts format:
- 0.0.0.0 www.domain.com
- 127.0.0.1 www.domain.com

From the rules editor, create a new rule, and select
[x] To this lists of domains

Select a directory with files in hosts format, select [x] Priority rule,
select [x] Deny and click on Apply.

An example of a list in hosts format:
https://www.github.developerdan.com/hosts/lists/ads-and-tracking-extended.txt

Note: you can also add a list of domains to allow, not only domains to
block.

TODOs:
- support for URLs besides directories (local lists).
- support for scheduled updates of the above URLs.

related #298
  • Loading branch information
gustavo-iniguez-goya committed Feb 25, 2021
1 parent fab5d97 commit 26671de
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 179 deletions.
16 changes: 14 additions & 2 deletions daemon/rule/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ func (l *Loader) Load(path string) error {
r.Operator.Compile()
if r.Operator.Type == List {
for i := 0; i < len(r.Operator.List); i++ {
r.Operator.List[i].Compile()
if err := r.Operator.List[i].Compile(); err != nil {
log.Warning("Operator.Compile() error: %s: ", err)
}
}
}
diskRules[r.Name] = r.Name
Expand Down Expand Up @@ -211,14 +213,24 @@ func (l *Loader) replaceUserRule(rule *Rule) (err error) {
l.rules[rule.Name] = rule
l.sortRules()
l.Unlock()

rule.Operator.isCompiled = false
if err := rule.Operator.Compile(); err != nil {
log.Warning("Operator.Compile() error: %s: ", err, rule.Operator.Data)
}

if rule.Operator.Type == List {
// TODO: use List protobuf object instead of un/marshalling to/from json
if err = json.Unmarshal([]byte(rule.Operator.Data), &rule.Operator.List); err != nil {
return fmt.Errorf("Error loading rule of type list: %s", err)
}

// force re-Compile() changed rule
for i := 0; i < len(rule.Operator.List); i++ {
rule.Operator.List[i].isCompiled = false
rule.Operator.List[i].Compile()
if err := rule.Operator.Compile(); err != nil {
log.Warning("Operator.Compile() error: %s: ", err)
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions daemon/rule/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
Complex = Type("complex") // for future use
List = Type("list")
Network = Type("network")
Lists = Type("lists")
)

// Available operands
Expand All @@ -46,6 +47,7 @@ const (
OpDstNetwork = Operand("dest.network")
OpProto = Operand("protocol")
OpList = Operand("list")
OpDomainsLists = Operand("lists.domains")
)

type opCallback func(value interface{}) bool
Expand All @@ -62,6 +64,7 @@ type Operator struct {
re *regexp.Regexp
netMask *net.IPNet
isCompiled bool
lists map[string]string
}

// NewOperator returns a new operator object
Expand Down Expand Up @@ -97,6 +100,14 @@ func (o *Operator) Compile() error {
return err
}
o.re = re
} else if o.Type == Lists && o.Operand == OpDomainsLists {
if o.Data == "" {
return fmt.Errorf("Operand lists is empty, nothing to load: %s", o)
}
if err := o.loadLists(); err != nil {
return err
}
o.cb = o.domainsListCmp
} else if o.Type == List {
o.Operand = OpList
} else if o.Type == Network {
Expand Down Expand Up @@ -148,6 +159,18 @@ func (o *Operator) cmpNetwork(destIP interface{}) bool {
return o.netMask.Contains(destIP.(net.IP))
}

func (o *Operator) domainsListCmp(v interface{}) bool {
dstHost := v.(string)
if dstHost == "" {
return false
}
if _, found := o.lists[dstHost]; found {
log.Debug("%s: %s, %s", log.Red("domain list match"), dstHost, o.lists[dstHost])
return true
}
return false
}

func (o *Operator) listMatch(con interface{}) bool {
res := true
for i := 0; i < len(o.List); i++ {
Expand Down Expand Up @@ -188,6 +211,8 @@ func (o *Operator) Match(con *conman.Connection) bool {
return o.cb(con.DstIP)
} else if o.Operand == OpList {
return o.listMatch(con)
} else if o.Operand == OpDomainsLists {
return o.cb(con.DstHost)
}

return false
Expand Down
2 changes: 1 addition & 1 deletion ui/opensnitch/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Config:

HELP_URL = "https://github.com/gustavo-iniguez-goya/opensnitch/wiki/Configurations"

RulesTypes = ("list", "simple", "regexp", "network")
RulesTypes = ("list", "lists", "simple", "regexp", "network")

DEFAULT_DURATION_IDX = 6 # until restart
DEFAULT_TARGET_PROCESS = 0
Expand Down
45 changes: 42 additions & 3 deletions ui/opensnitch/dialogs/ruleseditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from nodes import Nodes
from database import Database
from version import version
from utils import Message
from utils import Message, FileDialog

DIALOG_UI_PATH = "%s/../res/ruleseditor.ui" % os.path.dirname(sys.modules[__name__].__file__)
class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
Expand Down Expand Up @@ -46,13 +46,15 @@ def __init__(self, parent=None, _rule=None):
self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).clicked.connect(self._cb_close_clicked)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._cb_apply_clicked)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._cb_help_clicked)
self.selectListButton.clicked.connect(self._cb_select_list_button_clicked)
self.protoCheck.toggled.connect(self._cb_proto_check_toggled)
self.procCheck.toggled.connect(self._cb_proc_check_toggled)
self.cmdlineCheck.toggled.connect(self._cb_cmdline_check_toggled)
self.dstPortCheck.toggled.connect(self._cb_dstport_check_toggled)
self.uidCheck.toggled.connect(self._cb_uid_check_toggled)
self.dstIPCheck.toggled.connect(self._cb_dstip_check_toggled)
self.dstHostCheck.toggled.connect(self._cb_dsthost_check_toggled)
self.dstListsCheck.toggled.connect(self._cb_dstlists_check_toggled)

if QtGui.QIcon.hasThemeIcon("emblem-default") == False:
self.actionAllowRadio.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogApplyButton")))
Expand All @@ -76,6 +78,11 @@ def _cb_reset_clicked(self):
def _cb_help_clicked(self):
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Config.HELP_URL))

def _cb_select_list_button_clicked(self):
dirName = FileDialog.select_dir(self, self.dstListsLine.text())
if dirName != None and dirName != "":
self.dstListsLine.setText(dirName)

def _cb_proto_check_toggled(self, state):
self.protoCombo.setEnabled(state)

Expand All @@ -97,6 +104,10 @@ def _cb_dstip_check_toggled(self, state):
def _cb_dsthost_check_toggled(self, state):
self.dstHostLine.setEnabled(state)

def _cb_dstlists_check_toggled(self, state):
self.dstListsLine.setEnabled(state)
self.selectListButton.setEnabled(state)

def _set_status_error(self, msg):
self.statusLabel.setStyleSheet('color: red')
self.statusLabel.setText(msg)
Expand Down Expand Up @@ -184,6 +195,10 @@ def _reset_state(self):
self.dstHostCheck.setChecked(False)
self.dstHostLine.setText("")

self.selectListButton.setEnabled(False)
self.dstListsCheck.setChecked(False)
self.dstListsLine.setText("")

def _load_rule(self, addr=None, rule=None):
self._load_nodes(addr)

Expand Down Expand Up @@ -255,6 +270,12 @@ def _load_rule_operator(self, operator):
self.dstHostLine.setEnabled(True)
self.dstHostLine.setText(operator.data)

if operator.operand == "lists.domains":
self.dstListsCheck.setChecked(True)
self.dstListsCheck.setEnabled(True)
self.dstListsLine.setText(operator.data)
self.selectListButton.setEnabled(True)

def _load_nodes(self, addr=None):
try:
self.nodesCombo.clear()
Expand Down Expand Up @@ -332,7 +353,7 @@ def _save_rule(self):
Ensure that some constraints are met:
- Determine if a field can be a regexp.
- Validate regexp.
- Fields cam not be empty.
- Fields cannot be empty.
- If the user has not provided a rule name, auto assign one.
"""
self.rule = ui_pb2.Rule()
Expand Down Expand Up @@ -497,11 +518,29 @@ def _save_rule(self):
if self._is_valid_regex(self.uidLine.text()) == False:
return False, QC.translate("rules", "User ID regexp error")

if self.dstListsCheck.isChecked():
if self.dstListsLine.text() == "":
return False, QC.translate("rules", "Lists field cannot be empty")
if os.path.isdir(self.dstListsLine.text()) == False:
return False, QC.translate("rules", "Lists field must be a directory")

self.rule.operator.type = "lists"
self.rule.operator.operand = "lists.domains"
rule_data.append(
{
'type': 'lists',
'operand': 'lists.domains',
'data': self.dstListsLine.text(),
'sensitive': self.sensitiveCheck.isChecked()
})
self.rule.operator.data = json.dumps(rule_data)


if len(rule_data) > 1:
self.rule.operator.type = "list"
self.rule.operator.operand = ""
self.rule.operator.data = json.dumps(rule_data)
elif len(rule_data) == 1:
else:
self.rule.operator.operand = rule_data[0]['operand']
self.rule.operator.data = rule_data[0]['data']
if self._is_regex(self.rule.operator.data):
Expand Down

0 comments on commit 26671de

Please sign in to comment.