Allow to configure firewall rules from the GUI (#660)

* Allow to configure firewall rules from the GUI (WIP)

New features:
- Configure and list system firewall rules from the GUI (nftables).
- Configure chains' policies.
- Add simple rules to allow incoming ports.
- Add simple rules to exclude apps (ports) from being intercepted.

This feature is only available for nftables. iptables is still supported,
you can add rules to the configuration file and they'll be loaded, but
you can't configure them from the GUI.

More information: #592
This commit is contained in:
Gustavo Iñiguez Goia 2022-05-03 22:05:12 +02:00 committed by GitHub
parent 16c95d77fd
commit d9e0c59158
Failed to generate hash of commit
56 changed files with 6574 additions and 464 deletions

View file

@ -9,7 +9,7 @@
"InterceptUnknown": false,
"ProcMonitorMethod": "ebpf",
"LogLevel": 2,
"Firewall": "iptables",
"Firewall": "nftables",
"Stats": {
"MaxEvents": 150,
"MaxStats": 25

View file

@ -12,22 +12,24 @@ type (
callbackBool func() bool
stopChecker struct {
sync.RWMutex
ch chan bool
sync.RWMutex
}
// Common holds common fields and functionality of both firewalls,
// iptables and nftables.
Common struct {
sync.RWMutex
QueueNum uint16
Running bool
RulesChecker *time.Ticker
stopCheckerChan *stopChecker
QueueNum uint16
Running bool
Intercepting bool
FwEnabled bool
sync.RWMutex
}
)
func (s *stopChecker) exit() chan bool {
func (s *stopChecker) exit() <-chan bool {
s.RLock()
defer s.RUnlock()
return s.ch
@ -64,13 +66,35 @@ func (c *Common) IsRunning() bool {
return c != nil && c.Running
}
// NewRulesChecker starts monitoring firewall for configuration or rules changes.
// IsFirewallEnabled returns if the firewall is running or not.
func (c *Common) IsFirewallEnabled() bool {
c.RLock()
defer c.RUnlock()
return c != nil && c.FwEnabled
}
// IsIntercepting returns if the firewall is running or not.
func (c *Common) IsIntercepting() bool {
c.RLock()
defer c.RUnlock()
return c != nil && c.Intercepting
}
// NewRulesChecker starts monitoring interception rules.
// We expect to have 2 rules loaded: one to intercept DNS responses and another one
// to intercept network traffic.
func (c *Common) NewRulesChecker(areRulesLoaded callbackBool, reloadRules callback) {
c.Lock()
defer c.Unlock()
if c.stopCheckerChan != nil {
c.stopCheckerChan.stop()
c.stopCheckerChan = nil
}
c.stopCheckerChan = &stopChecker{ch: make(chan bool, 1)}
c.RulesChecker = time.NewTicker(time.Second * 30)
c.RulesChecker = time.NewTicker(time.Second * 15)
go c.startCheckingRules(areRulesLoaded, reloadRules)
}
@ -90,13 +114,22 @@ func (c *Common) startCheckingRules(areRulesLoaded callbackBool, reloadRules cal
}
Exit:
log.Info("exit checking iptables rules")
log.Info("exit checking firewall rules")
}
// StopCheckingRules stops checking if firewall rules are loaded.
func (c *Common) StopCheckingRules() {
c.RLock()
defer c.RUnlock()
if c.RulesChecker != nil {
c.RulesChecker.Stop()
}
c.stopCheckerChan.stop()
if c.stopCheckerChan != nil {
c.stopCheckerChan.stop()
}
}
func (c *Common) reloadCallback(callback func()) {
callback()
}

View file

@ -17,52 +17,120 @@ import (
"github.com/fsnotify/fsnotify"
)
type callback func()
// ExprValues holds the statements' options:
// "Name": "ct",
// "Values": [
// {
// "Key": "state",
// "Value": "established"
// },
// {
// "Key": "state",
// "Value": "related"
// }]
type ExprValues struct {
Key string
Value string
}
// ExprStatement holds the definition of matches to use against connections.
//{
// "Op": "!=",
// "Name": "tcp",
// "Values": [
// {
// "Key": "dport",
// "Value": "443"
// }
// ]
//}
type ExprStatement struct {
Op string // ==, !=, ... Only one per expression set.
Name string // tcp, udp, ct, daddr, log, ...
Values []*ExprValues // dport 8000
}
// Expressions holds the array of expressions that create the rules
type Expressions struct {
Statement *ExprStatement
}
// FwRule holds the fields of a rule
type FwRule struct {
sync.RWMutex
// we need to keep old fields in the struct. Otherwise when receiving a conf from the GUI, the legacy rules would be deleted.
Chain string // TODO: deprecated, remove
Table string // TODO: deprecated, remove
Parameters string // TODO: deprecated: remove
UUID string
Description string
Table string
Chain string
Parameters string
Expressions []*Expressions
Target string
TargetParameters string
Position uint64
Enabled bool
*sync.RWMutex
}
// FwChain holds the information that defines a firewall chain.
// It also contains the firewall table definition that it belongs to.
type FwChain struct {
// table fields
Table string
Family string
// chain fields
Name string
Description string
Priority string
Type string
Hook string
Policy string
Rules []*FwRule
}
// IsInvalid checks if the chain has been correctly configured.
func (fc *FwChain) IsInvalid() bool {
return fc.Name == "" || fc.Family == "" || fc.Table == ""
}
type rulesList struct {
sync.RWMutex
Rule *FwRule
}
type chainsList struct {
Chains []*FwChain
Rule *FwRule // TODO: deprecated, remove
}
// SystemConfig holds the list of rules to be added to the system
type SystemConfig struct {
sync.RWMutex
SystemRules []*rulesList
SystemRules []*chainsList
Version uint32
Enabled bool
}
// Config holds the functionality to re/load the firewall configuration from disk.
// This is the configuration to manage the system firewall (iptables, nftables).
type Config struct {
sync.Mutex
file string
watcher *fsnotify.Watcher
monitorExitChan chan bool
SysConfig SystemConfig
// subscribe to this channel to receive config reload events
ReloadConfChan chan bool
// preloadCallback is called before reloading the configuration,
// in order to delete old fw rules.
preloadCallback callback
preloadCallback func()
// reloadCallback is called after the configuration is written.
reloadCallback func()
// preload will be called after daemon startup, whilst reload when a modification is performed.
}
// NewSystemFwConfig initializes config fields
func (c *Config) NewSystemFwConfig(preLoadCb callback) (*Config, error) {
func (c *Config) NewSystemFwConfig(preLoadCb, reLoadCb func()) (*Config, error) {
var err error
watcher, err := fsnotify.NewWatcher()
if err != nil {
@ -76,8 +144,8 @@ func (c *Config) NewSystemFwConfig(preLoadCb callback) (*Config, error) {
c.file = "/etc/opensnitchd/system-fw.json"
c.monitorExitChan = make(chan bool, 1)
c.preloadCallback = preLoadCb
c.reloadCallback = reLoadCb
c.watcher = watcher
c.ReloadConfChan = make(chan bool, 1)
return c, nil
}
@ -102,7 +170,7 @@ func (c *Config) LoadDiskConfiguration(reload bool) {
}
if reload {
c.ReloadConfChan <- true
c.reloadCallback()
return
}
@ -125,8 +193,10 @@ func (c *Config) loadConfiguration(rawConfig []byte) {
log.Info("fw configuration loaded")
}
func (c *Config) saveConfiguration(rawConfig string) error {
conf, err := json.Marshal([]byte(rawConfig))
// SaveConfiguration saves configuration to disk.
// This event dispatches a reload of the configuration.
func (c *Config) SaveConfiguration(rawConfig string) error {
conf, err := json.MarshalIndent([]byte(rawConfig), " ", " ")
if err != nil {
log.Error("saving json firewall configuration: %s %s", err, conf)
return err
@ -150,10 +220,6 @@ func (c *Config) StopConfigWatcher() {
c.monitorExitChan <- true
close(c.monitorExitChan)
}
if c.ReloadConfChan != nil {
c.ReloadConfChan <- false // exit
close(c.ReloadConfChan)
}
if c.watcher != nil {
c.watcher.Remove(c.file)
@ -178,22 +244,3 @@ Exit:
c.monitorExitChan = nil
c.Unlock()
}
// MonitorSystemFw waits for configuration reloads.
func (c *Config) MonitorSystemFw(reloadCallback callback) {
for {
select {
case reload := <-c.ReloadConfChan:
if reload {
reloadCallback()
} else {
goto Exit
}
}
}
Exit:
log.Info("iptables, stop monitoring system fw rules")
c.Lock()
c.ReloadConfChan = nil
c.Unlock()
}

View file

@ -1,13 +1,18 @@
package iptables
import (
"bytes"
"encoding/json"
"os/exec"
"regexp"
"strings"
"sync"
"github.com/evilsocket/opensnitch/daemon/firewall/common"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"github.com/golang/protobuf/jsonpb"
)
// Action is the modifier we apply to a rule.
@ -28,17 +33,27 @@ const (
FLUSH = Action("-F")
NEWCHAIN = Action("-N")
DELCHAIN = Action("-X")
POLICY = Action("-P")
DROP = Action("DROP")
ACCEPT = Action("ACCEPT")
)
// SystemChains holds the fw rules defined by the user
// SystemRule blabla
type SystemRule struct {
Table string
Chain string
Rule *config.FwRule
}
// SystemChains keeps track of the fw rules that have been added to the system.
type SystemChains struct {
Rules map[string]*SystemRule
sync.RWMutex
Rules map[string]config.FwRule
}
// Iptables struct holds the fields of the iptables fw
type Iptables struct {
sync.Mutex
config.Config
common.Common
@ -49,6 +64,8 @@ type Iptables struct {
regexSystemRulesQuery *regexp.Regexp
chains SystemChains
sync.Mutex
}
// Fw initializes a new Iptables object
@ -65,7 +82,9 @@ func Fw() (*Iptables, error) {
bin6: "ip6tables",
regexRulesQuery: reRulesQuery,
regexSystemRulesQuery: reSystemRulesQuery,
chains: SystemChains{Rules: make(map[string]config.FwRule)},
chains: SystemChains{
Rules: make(map[string]*SystemRule),
},
}
return ipt, nil
}
@ -84,18 +103,15 @@ func (ipt *Iptables) Init(qNum *int) {
ipt.SetQueueNum(qNum)
// In order to clean up any existing firewall rule before start,
// we need to load the fw configuration first.
ipt.NewSystemFwConfig(ipt.preloadConfCallback)
go ipt.MonitorSystemFw(ipt.AddSystemRules)
// we need to load the fw configuration first to know what rules
// were configured.
ipt.NewSystemFwConfig(ipt.preloadConfCallback, ipt.reloadRulesCallback)
ipt.LoadDiskConfiguration(false)
// start from a clean state
ipt.CleanRules(false)
ipt.InsertRules()
ipt.AddSystemRules()
// start monitoring firewall rules to intercept network traffic
ipt.NewRulesChecker(ipt.AreRulesLoaded, ipt.reloadRulesCallback)
ipt.EnableInterception()
ipt.AddSystemRules(false)
ipt.Running = true
}
@ -113,6 +129,7 @@ func (ipt *Iptables) Stop() {
}
// IsAvailable checks if iptables is installed in the system.
// If it's not, we'll default to nftables.
func IsAvailable() error {
_, err := exec.Command("iptables", []string{"-V"}...).CombinedOutput()
if err != nil {
@ -121,18 +138,64 @@ func IsAvailable() error {
return nil
}
// InsertRules adds fw rules to intercept connections
func (ipt *Iptables) InsertRules() {
if err4, err6 := ipt.QueueDNSResponses(true, true); err4 != nil || err6 != nil {
log.Error("Error while running DNS firewall rule: %s %s", err4, err6)
} else if err4, err6 = ipt.QueueConnections(true, true); err4 != nil || err6 != nil {
// EnableInterception adds fw rules to intercept connections.
func (ipt *Iptables) EnableInterception() {
if err4, err6 := ipt.QueueConnections(true, true); err4 != nil || err6 != nil {
log.Fatal("Error while running conntrack firewall rule: %s %s", err4, err6)
} else if err4, err6 = ipt.QueueDNSResponses(true, true); err4 != nil || err6 != nil {
log.Error("Error while running DNS firewall rule: %s %s", err4, err6)
}
// start monitoring firewall rules to intercept network traffic
ipt.NewRulesChecker(ipt.AreRulesLoaded, ipt.reloadRulesCallback)
}
// DisableInterception removes firewall rules to intercept outbound connections.
func (ipt *Iptables) DisableInterception(logErrors bool) {
ipt.StopCheckingRules()
ipt.QueueDNSResponses(false, logErrors)
ipt.QueueConnections(false, logErrors)
}
// CleanRules deletes the rules we added.
func (ipt *Iptables) CleanRules(logErrors bool) {
ipt.QueueDNSResponses(false, logErrors)
ipt.QueueConnections(false, logErrors)
ipt.DisableInterception(logErrors)
ipt.DeleteSystemRules(true, logErrors)
}
// Serialize converts the configuration from json to protobuf
func (ipt *Iptables) Serialize() (*protocol.SysFirewall, error) {
sysfw := &protocol.SysFirewall{}
jun := jsonpb.Unmarshaler{
AllowUnknownFields: true,
}
rawConfig, err := json.Marshal(&ipt.SysConfig)
if err != nil {
log.Error("nfables.Serialize() struct to string error: %s", err)
return nil, err
}
// string to proto
if err := jun.Unmarshal(strings.NewReader(string(rawConfig)), sysfw); err != nil {
log.Error("nfables.Serialize() string to protobuf error: %s", err)
return nil, err
}
return sysfw, nil
}
// Deserialize converts a protocolbuffer structure to json.
func (ipt *Iptables) Deserialize(sysfw *protocol.SysFirewall) ([]byte, error) {
jun := jsonpb.Marshaler{
OrigName: true,
EmitDefaults: false,
Indent: " ",
}
var b bytes.Buffer
if err := jun.Marshal(&b, sysfw); err != nil {
log.Error("nfables.Deserialize() error 2: %s", err)
return nil, err
}
return b.Bytes(), nil
//return nil, fmt.Errorf("iptables.Deserialize() not implemented")
}

View file

@ -53,10 +53,16 @@ func (ipt *Iptables) AreRulesLoaded() bool {
return result
}
// reloadRulesCallback gets called when the interception rules are not present or after the configuration file changes.
func (ipt *Iptables) reloadRulesCallback() {
log.Important("firewall rules changed, reloading")
ipt.QueueDNSResponses(false, false)
ipt.QueueConnections(false, false)
ipt.InsertRules()
ipt.AddSystemRules()
ipt.CleanRules(false)
ipt.AddSystemRules(true)
ipt.EnableInterception()
}
// preloadConfCallback gets called before the fw configuration is reloaded
func (ipt *Iptables) preloadConfCallback() {
log.Info("iptables config changed, reloading")
ipt.DeleteSystemRules(true, log.GetLogLevel() == log.DEBUG)
}

View file

@ -54,10 +54,10 @@ func (ipt *Iptables) QueueDNSResponses(enable bool, logError bool) (err4, err6 e
}
// QueueConnections inserts the firewall rule which redirects connections to us.
// They are queued until the user denies/accept them, or reaches a timeout.
// Connections are queued until the user denies/accept them, or reaches a timeout.
// OUTPUT -t mangle -m conntrack --ctstate NEW,RELATED -j NFQUEUE --queue-num 0 --queue-bypass
func (ipt *Iptables) QueueConnections(enable bool, logError bool) (error, error) {
err4, err6 := ipt.RunRule(INSERT, enable, logError, []string{
err4, err6 := ipt.RunRule(ADD, enable, logError, []string{
"OUTPUT",
"-t", "mangle",
"-m", "conntrack",

View file

@ -4,26 +4,61 @@ import (
"strings"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/evilsocket/opensnitch/daemon/log"
)
// CreateSystemRule creates the custom firewall chains and adds them to the system.
func (ipt *Iptables) CreateSystemRule(rule *config.FwRule, logErrors bool) {
func (ipt *Iptables) CreateSystemRule(rule *config.FwRule, table, chain, hook string, logErrors bool) bool {
ipt.chains.Lock()
defer ipt.chains.Unlock()
if rule == nil {
return
return false
}
if table == "" {
table = "filter"
}
if hook == "" {
hook = rule.Chain
}
chainName := SystemRulePrefix + "-" + rule.Chain
if _, ok := ipt.chains.Rules[rule.Table+"-"+chainName]; ok {
return
chainName := SystemRulePrefix + "-" + hook
if _, ok := ipt.chains.Rules[table+"-"+chainName]; ok {
return false
}
ipt.RunRule(NEWCHAIN, true, logErrors, []string{chainName, "-t", rule.Table})
ipt.RunRule(NEWCHAIN, true, logErrors, []string{chainName, "-t", table})
// Insert the rule at the top of the chain
if err4, err6 := ipt.RunRule(INSERT, true, logErrors, []string{rule.Chain, "-t", rule.Table, "-j", chainName}); err4 == nil && err6 == nil {
ipt.chains.Rules[rule.Table+"-"+chainName] = *rule
if err4, err6 := ipt.RunRule(INSERT, true, logErrors, []string{hook, "-t", table, "-j", chainName}); err4 == nil && err6 == nil {
ipt.chains.Rules[table+"-"+chainName] = &SystemRule{
Table: table,
Chain: chain,
Rule: rule,
}
}
return true
}
// AddSystemRules creates the system firewall from configuration.
func (ipt *Iptables) AddSystemRules(reload bool) {
// Version 0 has no Enabled field, so it'd be always false
if ipt.SysConfig.Enabled == false && ipt.SysConfig.Version > 0 {
return
}
for _, cfg := range ipt.SysConfig.SystemRules {
if cfg.Rule != nil {
ipt.CreateSystemRule(cfg.Rule, cfg.Rule.Table, cfg.Rule.Chain, cfg.Rule.Chain, true)
ipt.AddSystemRule(ADD, cfg.Rule, cfg.Rule.Table, cfg.Rule.Chain, true)
continue
}
if cfg.Chains != nil {
for _, chn := range cfg.Chains {
if chn.Hook != "" && chn.Type != "" {
ipt.ConfigureChainPolicy(chn.Type, chn.Hook, chn.Policy, true)
}
}
}
}
}
@ -34,34 +69,68 @@ func (ipt *Iptables) DeleteSystemRules(force, logErrors bool) {
ipt.chains.Lock()
defer ipt.chains.Unlock()
for _, r := range ipt.SysConfig.SystemRules {
if r.Rule == nil {
for _, fwCfg := range ipt.SysConfig.SystemRules {
if fwCfg.Rule == nil {
continue
}
chain := SystemRulePrefix + "-" + r.Rule.Chain
if _, ok := ipt.chains.Rules[r.Rule.Table+"-"+chain]; !ok && !force {
chain := SystemRulePrefix + "-" + fwCfg.Rule.Chain
if _, ok := ipt.chains.Rules[fwCfg.Rule.Table+"-"+chain]; !ok && !force {
continue
}
ipt.RunRule(FLUSH, true, false, []string{chain, "-t", r.Rule.Table})
ipt.RunRule(DELETE, false, logErrors, []string{r.Rule.Chain, "-t", r.Rule.Table, "-j", chain})
ipt.RunRule(DELCHAIN, true, false, []string{chain, "-t", r.Rule.Table})
delete(ipt.chains.Rules, r.Rule.Table+"-"+chain)
ipt.RunRule(FLUSH, true, false, []string{chain, "-t", fwCfg.Rule.Table})
ipt.RunRule(DELETE, false, logErrors, []string{fwCfg.Rule.Chain, "-t", fwCfg.Rule.Table, "-j", chain})
ipt.RunRule(DELCHAIN, true, false, []string{chain, "-t", fwCfg.Rule.Table})
delete(ipt.chains.Rules, fwCfg.Rule.Table+"-"+chain)
for _, chn := range fwCfg.Chains {
if chn.Table == "" {
chn.Table = "filter"
}
chain := SystemRulePrefix + "-" + chn.Hook
if _, ok := ipt.chains.Rules[chn.Type+"-"+chain]; !ok && !force {
continue
}
ipt.RunRule(FLUSH, true, logErrors, []string{chain, "-t", chn.Type})
ipt.RunRule(DELETE, false, logErrors, []string{chn.Hook, "-t", chn.Type, "-j", chain})
ipt.RunRule(DELCHAIN, true, logErrors, []string{chain, "-t", chn.Type})
delete(ipt.chains.Rules, chn.Type+"-"+chain)
}
}
}
// DeleteSystemRule deletes a new rule.
func (ipt *Iptables) DeleteSystemRule(action Action, rule *config.FwRule, table, chain string, enable bool) (err4, err6 error) {
chainName := SystemRulePrefix + "-" + chain
if table == "" {
table = "filter"
}
r := []string{chainName, "-t", table}
if rule.Parameters != "" {
r = append(r, strings.Split(rule.Parameters, " ")...)
}
r = append(r, []string{"-j", rule.Target}...)
if rule.TargetParameters != "" {
r = append(r, strings.Split(rule.TargetParameters, " ")...)
}
return ipt.RunRule(action, enable, true, r)
}
// AddSystemRule inserts a new rule.
func (ipt *Iptables) AddSystemRule(rule *config.FwRule, enable bool) (err4, err6 error) {
func (ipt *Iptables) AddSystemRule(action Action, rule *config.FwRule, table, chain string, enable bool) (err4, err6 error) {
if rule == nil {
return nil, nil
}
rule.RLock()
defer rule.RUnlock()
ipt.RLock()
defer ipt.RUnlock()
chain := SystemRulePrefix + "-" + rule.Chain
if rule.Table == "" {
rule.Table = "filter"
chainName := SystemRulePrefix + "-" + chain
if table == "" {
table = "filter"
}
r := []string{chain, "-t", rule.Table}
r := []string{chainName, "-t", table}
if rule.Parameters != "" {
r = append(r, strings.Split(rule.Parameters, " ")...)
}
@ -73,17 +142,13 @@ func (ipt *Iptables) AddSystemRule(rule *config.FwRule, enable bool) (err4, err6
return ipt.RunRule(ADD, enable, true, r)
}
// AddSystemRules creates the system firewall from configuration.
func (ipt *Iptables) AddSystemRules() {
ipt.DeleteSystemRules(true, false)
for _, r := range ipt.SysConfig.SystemRules {
ipt.CreateSystemRule(r.Rule, true)
ipt.AddSystemRule(r.Rule, true)
}
}
// preloadConfCallback gets called before the fw configuration is reloaded
func (ipt *Iptables) preloadConfCallback() {
ipt.DeleteSystemRules(true, log.GetLogLevel() == log.DEBUG)
// ConfigureChainPolicy configures chains policy.
func (ipt *Iptables) ConfigureChainPolicy(table, hook, policy string, logError bool) {
// TODO: list all policies before modify them, and restore the original state on exit.
// still, if we exit abruptly, we might left the system badly configured.
ipt.RunRule(POLICY, true, logError, []string{
hook,
strings.ToUpper(policy),
"-t", table,
})
}

View file

@ -0,0 +1,122 @@
package nftables
import (
"fmt"
"strings"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/google/nftables"
)
// AddChain adds a new chain to nftables.
// https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook
func (n *Nft) AddChain(name, table, family string, priority nftables.ChainPriority, ctype nftables.ChainType, hook nftables.ChainHook, policy nftables.ChainPolicy) *nftables.Chain {
if family == "" {
family = exprs.NFT_FAMILY_INET
}
tbl := getTable(table, family)
if tbl == nil {
log.Error("%s addChain, Error getting table: %s, %s", logTag, table, family)
return nil
}
// nft list chains
chain := n.conn.AddChain(&nftables.Chain{
Name: strings.ToLower(name),
Table: tbl,
Type: ctype,
Hooknum: hook,
Priority: priority,
Policy: &policy,
})
if chain == nil {
return nil
}
key := getChainKey(name, tbl)
sysChains[key] = chain
return chain
}
// getChainKey returns the identifier that will be used to link chains and rules.
// When adding a new chain the key is stored, then later when adding a rule we get
// the chain that the rule belongs to by this key.
func getChainKey(name string, table *nftables.Table) string {
if table == nil {
return ""
}
return fmt.Sprintf("%s-%s-%d", name, table.Name, table.Family)
}
// get an existing chain
func getChain(name string, table *nftables.Table) *nftables.Chain {
key := getChainKey(name, table)
return sysChains[key]
}
// regular chains are user-defined chains, to better organize fw rules.
// https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Adding_regular_chains
func (n *Nft) addRegularChain(name, table, family string) error {
tbl := getTable(table, family)
if tbl == nil {
return fmt.Errorf("%s addRegularChain, Error getting table: %s, %s", logTag, table, family)
}
chain := n.conn.AddChain(&nftables.Chain{
Name: name,
Table: tbl,
})
if chain == nil {
return fmt.Errorf("%s error adding regular chain: %s", logTag, name)
}
key := getChainKey(name, tbl)
sysChains[key] = chain
return nil
}
func (n *Nft) addInterceptionChains() error {
var filterPolicy nftables.ChainPolicy
var manglePolicy nftables.ChainPolicy
filterPolicy = nftables.ChainPolicyAccept
manglePolicy = nftables.ChainPolicyAccept
tbl := getTable(exprs.NFT_CHAIN_FILTER, exprs.NFT_FAMILY_INET)
if tbl != nil {
key := getChainKey(exprs.NFT_HOOK_INPUT, tbl)
if key != "" && sysChains[key] != nil {
filterPolicy = *sysChains[key].Policy
}
}
tbl = getTable(exprs.NFT_CHAIN_MANGLE, exprs.NFT_FAMILY_INET)
if tbl != nil {
key := getChainKey(exprs.NFT_HOOK_OUTPUT, tbl)
if key != "" && sysChains[key] != nil {
manglePolicy = *sysChains[key].Policy
}
}
// nft list tables
n.AddChain(exprs.NFT_HOOK_INPUT, exprs.NFT_CHAIN_FILTER, exprs.NFT_FAMILY_INET,
nftables.ChainPriorityFilter, nftables.ChainTypeFilter, nftables.ChainHookInput, filterPolicy)
n.AddChain(exprs.NFT_HOOK_OUTPUT, exprs.NFT_CHAIN_MANGLE, exprs.NFT_FAMILY_INET,
nftables.ChainPriorityMangle, nftables.ChainTypeRoute, nftables.ChainHookOutput, manglePolicy)
// apply changes
if !n.Commit() {
return fmt.Errorf("Error adding interception chains")
}
return nil
}
func (n *Nft) delChain(chain *nftables.Chain) error {
n.conn.DelChain(chain)
delete(sysChains, getChainKey(chain.Name, chain.Table))
if !n.Commit() {
return fmt.Errorf("delChain, error deleting %s", chain.Name)
}
return nil
}

View file

@ -0,0 +1,15 @@
package exprs
import (
"github.com/google/nftables/expr"
)
// NewExprCounter returns a counter for packets or bytes.
func NewExprCounter(counterName string) *[]expr.Any {
return &[]expr.Any{
&expr.Objref{
Type: 1,
Name: counterName,
},
}
}

View file

@ -0,0 +1,85 @@
package exprs
import (
"fmt"
"strconv"
"strings"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
)
// Example https://github.com/google/nftables/blob/master/nftables_test.go#L1234
// https://wiki.nftables.org/wiki-nftables/index.php/Setting_packet_metainformation
// NewExprCtMark returns a new ct expression.
// # set
// # nft --debug netlink add rule filter output mark set 1
// ip filter output
// [ immediate reg 1 0x00000001 ]
// [ meta set mark with reg 1 ]
//
// match mark:
// nft --debug netlink add rule mangle prerouting ct mark 123
// [ ct load mark => reg 1 ]
// [ cmp eq reg 1 0x0000007b ]
func NewExprCtMark(setMark bool, value string) (*[]expr.Any, error) {
mark, err := strconv.Atoi(value)
if err != nil {
return nil, fmt.Errorf("Invalid conntrack mark: %s (%s)", err, value)
}
exprCtMark := []expr.Any{}
exprCtMark = append(exprCtMark, []expr.Any{
&expr.Immediate{
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(uint32(mark)),
},
&expr.Ct{
Key: expr.CtKeyMARK,
Register: 1,
SourceRegister: setMark,
},
}...)
if setMark == false {
exprCtMark = append(exprCtMark, []expr.Any{
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(uint32(mark))},
}...)
}
return &exprCtMark, nil
}
// NewExprCtState returns a new ct expression.
func NewExprCtState(ctFlags []*config.ExprValues) (*[]expr.Any, error) {
mask := uint32(0)
for _, flag := range ctFlags {
switch strings.ToLower(flag.Value) {
case CT_STATE_NEW:
mask |= expr.CtStateBitNEW
case CT_STATE_ESTABLISHED:
mask |= expr.CtStateBitESTABLISHED
case CT_STATE_RELATED:
mask |= expr.CtStateBitRELATED
case CT_STATE_INVALID:
mask |= expr.CtStateBitINVALID
default:
return nil, fmt.Errorf("Invalid conntrack flag: %s", flag)
}
}
return &[]expr.Any{
&expr.Ct{
Register: 1, SourceRegister: false, Key: expr.CtKeySTATE,
},
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Mask: binaryutil.NativeEndian.PutUint32(mask),
Xor: binaryutil.NativeEndian.PutUint32(0),
},
}, nil
}

View file

@ -0,0 +1,173 @@
package exprs
// keywords used in the configuration to define rules.
const (
// https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook
NFT_CHAIN_MANGLE = "mangle"
NFT_CHAIN_FILTER = "filter"
NFT_CHAIN_RAW = "raw"
NFT_CHAIN_SECURITY = "security"
NFT_CHAIN_NATDEST = "natdest"
NFT_CHAIN_NATSOURCE = "natsource"
NFT_CHAIN_CONNTRACK = "conntrack"
NFT_CHAIN_SELINUX = "selinux"
NFT_HOOK_INPUT = "input"
NFT_HOOK_OUTPUT = "output"
NFT_HOOK_PREROUTING = "prerouting"
NFT_HOOK_POSTROUTING = "postrouting"
NFT_HOOK_INGRESS = "ingress"
NFT_HOOK_EGRESS = "egress"
NFT_HOOK_FORWARD = "forward"
NFT_TABLE_INET = "inet"
NFT_TABLE_NAT = "nat"
// TODO
NFT_TABLE_ARP = "arp"
NFT_TABLE_BRIDGE = "bridge"
NFT_TABLE_NETDEV = "netdev"
NFT_FAMILY_IP = "ip"
NFT_FAMILY_IP6 = "ip6"
NFT_FAMILY_INET = "inet"
NFT_FAMILY_BRIDGE = "bridge"
NFT_FAMILY_ARP = "arp"
NFT_FAMILY_NETDEV = "netdev"
VERDICT_ACCEPT = "accept"
VERDICT_DROP = "drop"
VERDICT_REJECT = "reject"
VERDICT_RETURN = "return"
VERDICT_QUEUE = "queue"
VERDICT_JUMP = "jump"
// TODO
VERDICT_GOTO = "goto"
VERDICT_STOP = "stop"
VERDICT_STOLEN = "stolen"
VERDICT_CONTINUE = "continue"
VERDICT_MASQUERADE = "masquerade"
VERDICT_DNAT = "dnat"
VERDICT_SNAT = "snat"
VERDICT_REDIRECT = "redirect"
VERDICT_TPROXY = "tproxy"
NFT_PARM_TO = "to"
NFT_QUEUE_NUM = "num"
NFT_QUEUE_BY_PASS = "queue-bypass"
NFT_MASQ_RANDOM = "random"
NFT_MASQ_FULLY_RANDOM = "fully-random"
NFT_MASQ_PERSISTENT = "persistent"
NFT_PROTOCOL = "protocol"
NFT_SPORT = "sport"
NFT_DPORT = "dport"
NFT_SADDR = "saddr"
NFT_DADDR = "daddr"
NFT_ICMP_CODE = "code"
NFT_ICMP_TYPE = "type"
NFT_IIFNAME = "iifname"
NFT_OIFNAME = "oifname"
NFT_LOG = "log"
NFT_LOG_PREFIX = "prefix"
// TODO
NFT_LOG_LEVEL = "level"
NFT_LOG_FLAGS = "flags"
NFT_CT = "ct"
NFT_CT_STATE = "state"
NFT_CT_SET_MARK = "set"
NFT_CT_MARK = "mark"
CT_STATE_NEW = "new"
CT_STATE_ESTABLISHED = "established"
CT_STATE_RELATED = "related"
CT_STATE_INVALID = "invalid"
NFT_NOTRACK = "notrack"
NFT_QUOTA = "quota"
NFT_QUOTA_UNTIL = "until"
NFT_QUOTA_OVER = "over"
NFT_QUOTA_USED = "used"
NFT_QUOTA_UNIT_BYTES = "bytes"
NFT_QUOTA_UNIT_KB = "kbytes"
NFT_QUOTA_UNIT_MB = "mbytes"
NFT_QUOTA_UNIT_GB = "gbytes"
NFT_COUNTER = "counter"
NFT_COUNTER_NAME = "name"
NFT_COUNTER_PACKETS = "packets"
NFT_COUNTER_BYTES = "bytes"
NFT_LIMIT = "limit"
NFT_LIMIT_OVER = "over"
NFT_LIMIT_BURST = "burst"
NFT_LIMIT_UNITS_RATE = "rate-units"
NFT_LIMIT_UNITS_TIME = "time-units"
NFT_LIMIT_UNITS = "units"
NFT_LIMIT_UNIT_SECOND = "second"
NFT_LIMIT_UNIT_MINUTE = "minute"
NFT_LIMIT_UNIT_HOUR = "hour"
NFT_LIMIT_UNIT_DAY = "day"
NFT_LIMIT_UNIT_KBYTES = "kbytes"
NFT_LIMIT_UNIT_MBYTES = "mbytes"
NFT_META = "meta"
NFT_META_MARK = "mark"
NFT_META_SET_MARK = "set"
NFT_META_PRIORITY = "priority"
NFT_META_NFTRACE = "nftrace"
NFT_META_SET = "set"
NFT_PROTO_UDP = "udp"
NFT_PROTO_UDPLITE = "udplite"
NFT_PROTO_TCP = "tcp"
NFT_PROTO_SCTP = "sctp"
NFT_PROTO_DCCP = "dccp"
NFT_PROTO_ICMP = "icmp"
NFT_PROTO_ICMPX = "icmpx"
NFT_PROTO_ICMPv6 = "icmpv6"
NFT_PROTO_AH = "ah"
NFT_PROTO_ETHERNET = "ethernet"
NFT_PROTO_GRE = "gre"
NFT_PROTO_IP = "ip"
NFT_PROTO_IPIP = "ipip"
NFT_PROTO_L2TP = "l2tp"
NFT_PROTO_COMP = "comp"
NFT_PROTO_IGMP = "igmp"
NFT_PROTO_ESP = "esp"
NFT_PROTO_RAW = "raw"
NFT_PROTO_ENCAP = "encap"
ICMP_NO_ROUTE = "no-route"
ICMP_PROT_UNREACHABLE = "prot-unreachable"
ICMP_PORT_UNREACHABLE = "port-unreachable"
ICMP_NET_UNREACHABLE = "net-unreachable"
ICMP_ADDR_UNREACHABLE = "addr-unreachable"
ICMP_HOST_UNREACHABLE = "host-unreachable"
ICMP_NET_PROHIBITED = "net-prohibited"
ICMP_HOST_PROHIBITED = "host-prohibited"
ICMP_ADMIN_PROHIBITED = "admin-prohibited"
ICMP_REJECT_ROUTE = "reject-route"
ICMP_REJECT_POLICY_FAIL = "policy-fail"
ICMP_ECHO_REPLY = "echo-reply"
ICMP_ECHO_REQUEST = "echo-request"
ICMP_SOURCE_QUENCH = "source-quench"
ICMP_DEST_UNREACHABLE = "destination-unreachable"
ICMP_REDIRECT = "redirect"
ICMP_TIME_EXCEEDED = "time-exceeded"
ICMP_INFO_REQUEST = "info-request"
ICMP_INFO_REPLY = "info-reply"
ICMP_PARAMETER_PROBLEM = "parameter-problem"
ICMP_TIMESTAMP_REQUEST = "timestamp-request"
ICMP_TIMESTAMP_REPLY = "timestamp-reply"
ICMP_ROUTER_ADVERTISEMENT = "router-advertisement"
ICMP_ROUTER_SOLICITATION = "router-solicitation"
ICMP_ADDRESS_MASK_REQUEST = "address-mask-request"
ICMP_ADDRESS_MASK_REPLY = "address-mask-reply"
)

View file

@ -0,0 +1,26 @@
package exprs
import "github.com/google/nftables/expr"
// NewExprIface returns a new network interface expression
func NewExprIface(iface string, isOut bool, cmpOp expr.CmpOp) *[]expr.Any {
keyDev := expr.MetaKeyIIFNAME
if isOut {
keyDev = expr.MetaKeyOIFNAME
}
return &[]expr.Any{
&expr.Meta{Key: keyDev, Register: 1},
&expr.Cmp{
Op: cmpOp,
Register: 1,
Data: ifname(iface),
},
}
}
// https://github.com/google/nftables/blob/master/nftables_test.go#L81
func ifname(n string) []byte {
b := make([]byte, 16)
copy(b, []byte(n+"\x00"))
return b
}

View file

@ -0,0 +1,173 @@
package exprs
import (
"fmt"
"net"
"strings"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/google/nftables/expr"
"golang.org/x/sys/unix"
)
// NewExprIP returns a new IP expression.
// You can use multiple statements to specify daddr + saddr, or combine them
// in a single statement expression:
// Example 1 (filtering by source and dest address):
// "Name": "ip",
// "Values": [ {"Key": "saddr": "Value": "1.2.3.4"},{"Key": "daddr": "Value": "1.2.3.5"} ]
// Example 2 (filtering by multiple dest addrs IPs):
// "Name": "ip",
// "Values": [
// {"Key": "daddr": "Value": "1.2.3.4"},
// {"Key": "daddr": "Value": "1.2.3.5"}
// ]
// Example 3 (filtering by network range):
// "Name": "ip",
// "Values": [
// {"Key": "daddr": "Value": "1.2.3.4-1.2.9.254"}
// ]
// TODO (filter by multiple dest addrs separated by commas):
// "Values": [
// {"Key": "daddr": "Value": "1.2.3.4,1.2.9.254"}
// ]
func NewExprIP(ipOptions []*config.ExprValues, cmpOp expr.CmpOp) (*[]expr.Any, error) {
var exprIP []expr.Any
for _, ipOpt := range ipOptions {
// TODO: ipv6
switch ipOpt.Key {
case NFT_SADDR, NFT_DADDR:
payload := getExprIPPayload(ipOpt.Key)
exprIP = append(exprIP, payload)
exprIPtemp, err := getExprIP(ipOpt.Value, cmpOp)
if err != nil {
return nil, err
}
exprIP = append(exprIP, *exprIPtemp...)
case NFT_PROTOCOL:
payload := getExprIPPayload(ipOpt.Key)
exprIP = append(exprIP, payload)
protoCode, err := getProtocolCode(ipOpt.Value)
if err != nil {
return nil, err
}
exprIP = append(exprIP, []expr.Any{
&expr.Cmp{
Op: cmpOp,
Register: 1,
Data: []byte{protoCode},
},
}...)
}
}
return &exprIP, nil
}
func getExprIPPayload(what string) *expr.Payload {
switch what {
case NFT_PROTOCOL:
return &expr.Payload{
DestRegister: 1,
Offset: 9, // daddr
Base: expr.PayloadBaseNetworkHeader,
Len: 1, // 16 ipv6
}
case NFT_DADDR:
// NOTE 1: if "what" is daddr and SourceRegister is part of the Payload{} expression,
// the rule is not added.
return &expr.Payload{
DestRegister: 1,
Offset: 16, // daddr
Base: expr.PayloadBaseNetworkHeader,
Len: 4, // 16 ipv6
}
default:
return &expr.Payload{
SourceRegister: 1,
DestRegister: 1,
Offset: 12, // saddr
Base: expr.PayloadBaseNetworkHeader,
Len: 4, // 16 ipv6
}
}
}
// Supported IP types: a.b.c.d, a.b.c.d-w.x.y.z
// TODO: support IPs separated by commas: a.b.c.d, e.f.g.h,...
func getExprIP(value string, cmpOp expr.CmpOp) (*[]expr.Any, error) {
if strings.Index(value, "-") != -1 {
ips := strings.Split(value, "-")
ipSrc := net.ParseIP(ips[0])
ipDst := net.ParseIP(ips[1])
if ipSrc == nil || ipDst == nil {
return nil, fmt.Errorf("Invalid IPs range: %v", ips)
}
return &[]expr.Any{
&expr.Range{
Op: cmpOp,
Register: 1,
FromData: ipSrc.To4(),
ToData: ipDst.To4(),
},
}, nil
}
ip := net.ParseIP(value)
if ip == nil {
return nil, fmt.Errorf("Invalid IP: %s", value)
}
return &[]expr.Any{
&expr.Cmp{
Op: cmpOp,
Register: 1,
Data: ip.To4(),
},
}, nil
}
func getProtocolCode(value string) (byte, error) {
switch value {
case NFT_PROTO_TCP:
return unix.IPPROTO_TCP, nil
case NFT_PROTO_UDP:
return unix.IPPROTO_UDP, nil
case NFT_PROTO_UDPLITE:
return unix.IPPROTO_UDPLITE, nil
case NFT_PROTO_SCTP:
return unix.IPPROTO_SCTP, nil
case NFT_PROTO_DCCP:
return unix.IPPROTO_DCCP, nil
case NFT_PROTO_ICMP:
return unix.IPPROTO_ICMP, nil
case NFT_PROTO_ICMPv6:
return unix.IPPROTO_ICMPV6, nil
case NFT_PROTO_AH:
return unix.IPPROTO_AH, nil
case NFT_PROTO_ETHERNET:
return unix.IPPROTO_ETHERNET, nil
case NFT_PROTO_GRE:
return unix.IPPROTO_GRE, nil
case NFT_PROTO_IP:
return unix.IPPROTO_IP, nil
case NFT_PROTO_IPIP:
return unix.IPPROTO_IPIP, nil
case NFT_PROTO_L2TP:
return unix.IPPROTO_L2TP, nil
case NFT_PROTO_COMP:
return unix.IPPROTO_COMP, nil
case NFT_PROTO_IGMP:
return unix.IPPROTO_IGMP, nil
case NFT_PROTO_ESP:
return unix.IPPROTO_ESP, nil
case NFT_PROTO_RAW:
return unix.IPPROTO_RAW, nil
case NFT_PROTO_ENCAP:
return unix.IPPROTO_ENCAP, nil
}
return 0, fmt.Errorf("Invalid protocol code, or not supported yet: %s", value)
}

View file

@ -0,0 +1,83 @@
package exprs
import (
"fmt"
"strconv"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/google/nftables/expr"
)
// NewExprLimit returns a new limit expression.
// limit rate [over] 1/second
// to express bytes units, we use: 10-mbytes instead of nft's 10 mbytes
func NewExprLimit(statement *config.ExprStatement) (*[]expr.Any, error) {
var err error
exprLimit := &expr.Limit{
Type: expr.LimitTypePkts,
Over: false,
Unit: expr.LimitTimeSecond,
}
for _, values := range statement.Values {
switch values.Key {
case NFT_LIMIT_OVER:
exprLimit.Over = true
case NFT_LIMIT_UNITS:
exprLimit.Rate, err = strconv.ParseUint(values.Value, 10, 64)
if err != nil {
return nil, fmt.Errorf("Invalid limit rate: %s", values.Value)
}
case NFT_LIMIT_BURST:
limitBurst := 0
limitBurst, err = strconv.Atoi(values.Value)
if err != nil || limitBurst == 0 {
return nil, fmt.Errorf("Invalid burst limit: %s, err: %s", values.Value, err)
}
exprLimit.Burst = uint32(limitBurst)
case NFT_LIMIT_UNITS_RATE:
// units rate must be placed AFTER the rate
exprLimit.Type, exprLimit.Rate = getLimitRate(values.Value, exprLimit.Rate)
case NFT_LIMIT_UNITS_TIME:
exprLimit.Unit = getLimitUnits(values.Value)
}
}
return &[]expr.Any{exprLimit}, nil
}
func getLimitUnits(units string) (limitUnits expr.LimitTime) {
switch units {
case NFT_LIMIT_UNIT_MINUTE:
limitUnits = expr.LimitTimeMinute
case NFT_LIMIT_UNIT_HOUR:
limitUnits = expr.LimitTimeHour
case NFT_LIMIT_UNIT_DAY:
limitUnits = expr.LimitTimeDay
default:
limitUnits = expr.LimitTimeSecond
}
return limitUnits
}
func getLimitRate(units string, rate uint64) (limitType expr.LimitType, limitRate uint64) {
switch units {
case NFT_LIMIT_UNIT_KBYTES:
limitRate = rate * 1024
limitType = expr.LimitTypePktBytes
case NFT_LIMIT_UNIT_MBYTES:
limitRate = (rate * 1024) * 1024
limitType = expr.LimitTypePktBytes
default:
limitType = expr.LimitTypePkts
limitRate, _ = strconv.ParseUint(units, 10, 64)
}
return
}

View file

@ -0,0 +1,29 @@
package exprs
import (
"github.com/google/nftables/expr"
"golang.org/x/sys/unix"
)
// NewExprLog returns a new log expression.
func NewExprLog(what, options string) *[]expr.Any {
exprLog := []expr.Any{}
options += " "
switch what {
case NFT_LOG_PREFIX:
exprLog = append(exprLog, []expr.Any{
&expr.Log{
Key: 1 << unix.NFTA_LOG_PREFIX,
Data: []byte(options),
},
}...)
// TODO
//case exprs.NFT_LOG_LEVEL:
//case exprs.NFT_LOG_FLAGS:
default:
return nil
}
return &exprLog
}

View file

@ -0,0 +1,97 @@
package exprs
import (
"fmt"
"strconv"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
)
// NewExprMeta creates a new meta selector to match or set packet metainformation.
// https://wiki.nftables.org/wiki-nftables/index.php/Matching_packet_metainformation
func NewExprMeta(values []*config.ExprValues) (*[]expr.Any, error) {
setValue := false
metaExpr := []expr.Any{}
for _, meta := range values {
switch meta.Key {
case NFT_META_SET_MARK:
setValue = true
continue
case NFT_META_MARK:
mark, err := getMetaValue(meta.Value)
if err != nil {
return nil, err
}
if setValue {
metaExpr = append(metaExpr, []expr.Any{
&expr.Immediate{
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(uint32(mark)),
}}...)
}
metaExpr = append(metaExpr, []expr.Any{
&expr.Meta{Key: expr.MetaKeyMARK, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(uint32(mark)),
}}...)
return &metaExpr, nil
case NFT_META_PRIORITY:
mark, err := getMetaValue(meta.Value)
if err != nil {
return nil, err
}
if setValue {
metaExpr = append(metaExpr, []expr.Any{
&expr.Immediate{
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(uint32(mark)),
}}...)
}
return &[]expr.Any{
&expr.Meta{Key: expr.MetaKeyPRIORITY, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(uint32(mark)),
},
}, nil
case NFT_META_NFTRACE:
mark, err := getMetaValue(meta.Value)
if err != nil {
return nil, err
}
if mark != 0 && mark != 1 {
return nil, fmt.Errorf("%s Invalid nftrace value: %d. Only 1 or 0 allowed", "nftables", mark)
}
// TODO: not working yet
return &[]expr.Any{
&expr.Meta{Key: expr.MetaKeyNFTRACE, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(uint32(mark)),
},
}, nil
default:
// not supported yet
}
}
return nil, fmt.Errorf("%s meta keyword not supported yet, open a new issue on github", "nftables")
}
func getMetaValue(value string) (int, error) {
metaVal, err := strconv.Atoi(value)
if err != nil {
return 0, err
}
return metaVal, nil
}

View file

@ -0,0 +1,140 @@
package exprs
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
"golang.org/x/sys/unix"
)
// NewExprNATFlags returns the nat flags configured.
// common to masquerade, snat and dnat
func NewExprNATFlags(parms string) (random, fullrandom, persistent bool) {
masqParms := strings.Split(parms, ",")
for _, mParm := range masqParms {
switch mParm {
case NFT_MASQ_RANDOM:
random = true
case NFT_MASQ_FULLY_RANDOM:
fullrandom = true
case NFT_MASQ_PERSISTENT:
persistent = true
}
}
return
}
// NewExprNAT parses the redirection of redirect, snat, dnat, tproxy and masquerade verdict:
// to x.y.z.a:abcd
// If only the IP is specified (to 1.2.3.4), only NAT.RegAddrMin must be present (regAddr == true)
// If only the port is specified (to :1234), only NAT.RegPortMin must be present (regPort == true)
// If both addr and port are specified (to 1.2.3.4:1234), NAT.RegPortMin and NAT.RegAddrMin must be present.
func NewExprNAT(parms, verdict string) (bool, bool, *[]expr.Any, error) {
regAddr := false
regProto := false
exprNAT := []expr.Any{}
NATParms := strings.Split(parms, " ")
idx := 0
// exclude first parameter if it's "to"
if NATParms[idx] == NFT_PARM_TO {
idx++
}
dParms := strings.Split(NATParms[idx], ":")
// masquerade doesn't allow "to IP"
if dParms[0] != "" && verdict != VERDICT_MASQUERADE {
dIP := dParms[0]
destIP := net.ParseIP(dIP)
if destIP == nil {
return regAddr, regProto, &exprNAT, fmt.Errorf("Invalid IP: %s", dIP)
}
exprNAT = append(exprNAT, []expr.Any{
&expr.Immediate{
Register: 1,
Data: destIP.To4(),
}}...)
regAddr = true
}
if len(dParms) == 2 {
dPort := dParms[1]
// TODO: support ranges. 9000-9100
destPort, err := strconv.Atoi(dPort)
if err != nil {
return regAddr, regProto, &exprNAT, fmt.Errorf("Invalid Port: %s", dPort)
}
reg := uint32(2)
if verdict == VERDICT_TPROXY || verdict == VERDICT_MASQUERADE || verdict == VERDICT_REDIRECT {
reg = 1
}
exprNAT = append(exprNAT, []expr.Any{
&expr.Immediate{
Register: reg,
Data: binaryutil.BigEndian.PutUint16(uint16(destPort)),
}}...)
regProto = true
}
return regAddr, regProto, &exprNAT, nil
}
// NewExprMasquerade returns a new masquerade expression.
func NewExprMasquerade(toPorts, random, fullRandom, persistent bool) *[]expr.Any {
exprMasq := &expr.Masq{
ToPorts: toPorts,
Random: random,
FullyRandom: fullRandom,
Persistent: persistent,
}
if toPorts {
exprMasq.RegProtoMin = 1
}
return &[]expr.Any{
exprMasq,
}
}
// NewExprRedirect returns a new redirect expression.
func NewExprRedirect() *[]expr.Any {
return &[]expr.Any{
// Redirect is a special case of DNAT where the destination is the current machine
&expr.Redir{
RegisterProtoMin: 1,
},
}
}
// NewExprSNAT returns a new snat expression.
func NewExprSNAT() *expr.NAT {
return &expr.NAT{
Type: expr.NATTypeSourceNAT,
Family: unix.NFPROTO_IPV4,
}
}
// NewExprDNAT returns a new dnat expression.
func NewExprDNAT() *expr.NAT {
return &expr.NAT{
Type: expr.NATTypeDestNAT,
Family: unix.NFPROTO_IPV4,
}
}
// NewExprTproxy returns a new tproxy expression.
// XXX: is "to x.x.x.x:1234" supported by google/nftables lib? or only "to :1234"?
// it creates an erronous rule.
func NewExprTproxy() *[]expr.Any {
return &[]expr.Any{
&expr.TProxy{
Family: byte(nftables.TableFamilyIPv4),
TableFamily: byte(nftables.TableFamilyIPv4),
RegPort: 1,
}}
}

View file

@ -0,0 +1,9 @@
package exprs
import "github.com/google/nftables/expr"
func NewNoTrack() *[]expr.Any {
return &[]expr.Any{
&expr.Notrack{},
}
}

View file

@ -0,0 +1,33 @@
package exprs
import (
"github.com/google/nftables/expr"
)
// NewOperator translates a string comparator operator to nftables operator
func NewOperator(operator string) expr.CmpOp {
switch operator {
case "!=":
return expr.CmpOpNeq
case ">":
return expr.CmpOpGt
case ">=":
return expr.CmpOpGte
case "<":
return expr.CmpOpLt
case "<=":
return expr.CmpOpLte
}
return expr.CmpOpEq
}
// NewExprOperator returns a new comparator operator
func NewExprOperator(op expr.CmpOp) *[]expr.Any {
return &[]expr.Any{
&expr.Cmp{
Register: 1,
Op: op,
},
}
}

View file

@ -0,0 +1,91 @@
package exprs
import (
"fmt"
"strconv"
"strings"
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
)
// NewExprPort returns a new port expression with the given matching operator.
func NewExprPort(port string, op *expr.CmpOp) *[]expr.Any {
eport, _ := strconv.Atoi(port)
return &[]expr.Any{
&expr.Cmp{
Register: 1,
Op: *op,
Data: binaryutil.BigEndian.PutUint16(uint16(eport))},
}
}
// NewExprPortRange returns a new port range expression.
func NewExprPortRange(sport string) *[]expr.Any {
ports := strings.Split(sport, "-")
iport, _ := strconv.Atoi(ports[0])
eport, _ := strconv.Atoi(ports[1])
return &[]expr.Any{
&expr.Cmp{
Register: 1,
Op: expr.CmpOpGte,
Data: binaryutil.BigEndian.PutUint16(uint16(iport))},
&expr.Cmp{
Register: 1,
Op: expr.CmpOpLte,
Data: binaryutil.BigEndian.PutUint16(uint16(eport))},
}
}
// NewExprPortSet returns a new set of ports.
func NewExprPortSet(portv string) *[]nftables.SetElement {
setElements := []nftables.SetElement{}
ports := strings.Split(portv, ",")
for _, portv := range ports {
portExpr := exprPortSubSet(portv)
if portExpr != nil {
setElements = append(setElements, *portExpr...)
}
}
return &setElements
}
func exprPortSubSet(portv string) *[]nftables.SetElement {
port, err := strconv.Atoi(portv)
if err != nil {
return nil
}
return &[]nftables.SetElement{
{Key: binaryutil.BigEndian.PutUint16(uint16(port))},
}
}
// NewExprPortDirection returns a new expression to match connections based on
// the direction of the connection (source, dest)
func NewExprPortDirection(direction string) (*expr.Payload, error) {
switch direction {
case NFT_DPORT:
return &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
}, nil
case NFT_SPORT:
return &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 0,
Len: 2,
}, nil
default:
return nil, fmt.Errorf("Not valid protocol direction: %s", direction)
}
}

View file

@ -0,0 +1,78 @@
package exprs
import (
"fmt"
"strings"
"github.com/google/nftables/expr"
"golang.org/x/sys/unix"
)
// NewExprProtocol creates a new expression to filter connections by protocol
func NewExprProtocol(proto string) (*[]expr.Any, error) {
switch strings.ToLower(proto) {
case NFT_PROTO_UDP:
return &[]expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_UDP},
},
}, nil
case NFT_PROTO_TCP:
return &[]expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_TCP},
},
}, nil
case NFT_PROTO_UDPLITE:
return &[]expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_UDPLITE},
},
}, nil
case NFT_PROTO_SCTP:
return &[]expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_SCTP},
},
}, nil
case NFT_PROTO_DCCP:
return &[]expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_DCCP},
},
}, nil
case NFT_PROTO_ICMP:
return &[]expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_ICMP},
},
}, nil
default:
return nil, fmt.Errorf("Not valid protocol rule, invalid or not supported protocol: %s", proto)
}
}

View file

@ -0,0 +1,64 @@
package exprs
import (
"fmt"
"strconv"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/google/nftables/expr"
)
// NewQuota returns a new quota expression.
// TODO: named quotas
func NewQuota(opts []*config.ExprValues) (*[]expr.Any, error) {
over := false
bytes := int64(0)
used := int64(0)
for _, opt := range opts {
switch opt.Key {
case NFT_QUOTA_OVER:
over = true
case NFT_QUOTA_UNIT_BYTES:
b, err := strconv.ParseInt(opt.Value, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid quota bytes: %s", opt.Value)
}
bytes = b
case NFT_QUOTA_USED:
// TODO: support for other size units
b, err := strconv.ParseInt(opt.Value, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid quota initial consumed bytes: %s", opt.Value)
}
used = b
case NFT_QUOTA_UNIT_KB:
b, err := strconv.ParseInt(opt.Value, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid quota bytes: %s", opt.Value)
}
bytes = b * 1024
case NFT_QUOTA_UNIT_MB:
b, err := strconv.ParseInt(opt.Value, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid quota bytes: %s", opt.Value)
}
bytes = (b * 1024) * 1024
case NFT_QUOTA_UNIT_GB:
b, err := strconv.ParseInt(opt.Value, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid quota bytes: %s", opt.Value)
}
bytes = ((b * 1024) * 1024) * 1024
}
}
if bytes == 0 {
return nil, fmt.Errorf("quota bytes cannot be 0")
}
return &[]expr.Any{
&expr.Quota{
Bytes: uint64(bytes),
Consumed: uint64(used),
Over: over,
},
}, nil
}

View file

@ -0,0 +1,96 @@
package exprs
import (
"github.com/google/gopacket/layers"
"golang.org/x/sys/unix"
)
func getICMPRejectCode(reason string) uint8 {
switch reason {
case ICMP_HOST_UNREACHABLE, ICMP_ADDR_UNREACHABLE:
return layers.ICMPv4CodeHost
case ICMP_PROT_UNREACHABLE:
return layers.ICMPv4CodeProtocol
case ICMP_PORT_UNREACHABLE:
return layers.ICMPv4CodePort
case ICMP_ADMIN_PROHIBITED:
return layers.ICMPv4CodeCommAdminProhibited
case ICMP_HOST_PROHIBITED:
return layers.ICMPv4CodeHostAdminProhibited
case ICMP_NET_PROHIBITED:
return layers.ICMPv4CodeNetAdminProhibited
}
return layers.ICMPv4CodeNet
}
func getICMPxRejectCode(reason string) uint8 {
// https://github.com/torvalds/linux/blob/master/net/netfilter/nft_reject.c#L96
// https://github.com/google/gopacket/blob/3aa782ce48d4a525acaebab344cedabfb561f870/layers/icmp4.go#L37
switch reason {
case ICMP_HOST_UNREACHABLE, ICMP_NET_UNREACHABLE:
return unix.NFT_REJECT_ICMP_UNREACH // results in -> net-unreachable???
case ICMP_PROT_UNREACHABLE:
return unix.NFT_REJECT_ICMPX_HOST_UNREACH // results in -> prot-unreachable???
case ICMP_PORT_UNREACHABLE:
return unix.NFT_REJECT_ICMPX_PORT_UNREACH // results in -> host-unreachable???
case ICMP_NO_ROUTE:
return unix.NFT_REJECT_ICMPX_NO_ROUTE // results in -> net-unreachable
}
return unix.NFT_REJECT_ICMP_UNREACH // results in -> net-unreachable???
}
// GetICMPType returns an ICMP type code
func GetICMPType(icmpType string) uint8 {
switch icmpType {
case ICMP_ECHO_REPLY:
return layers.ICMPv4TypeEchoReply
case ICMP_ECHO_REQUEST:
return layers.ICMPv4TypeEchoRequest
case ICMP_SOURCE_QUENCH:
return layers.ICMPv4TypeSourceQuench
case ICMP_DEST_UNREACHABLE:
return layers.ICMPv4TypeDestinationUnreachable
case ICMP_ROUTER_ADVERTISEMENT:
return layers.ICMPv4TypeRouterAdvertisement
case ICMP_ROUTER_SOLICITATION:
return layers.ICMPv4TypeRouterSolicitation
case ICMP_REDIRECT:
return layers.ICMPv4TypeRedirect
case ICMP_TIME_EXCEEDED:
return layers.ICMPv4TypeTimeExceeded
case ICMP_INFO_REQUEST:
return layers.ICMPv4TypeInfoRequest
case ICMP_INFO_REPLY:
return layers.ICMPv4TypeInfoReply
case ICMP_PARAMETER_PROBLEM:
return layers.ICMPv4TypeParameterProblem
case ICMP_TIMESTAMP_REQUEST:
return layers.ICMPv4TypeTimestampRequest
case ICMP_TIMESTAMP_REPLY:
return layers.ICMPv4TypeTimestampReply
case ICMP_ADDRESS_MASK_REQUEST:
return layers.ICMPv4TypeAddressMaskRequest
case ICMP_ADDRESS_MASK_REPLY:
return layers.ICMPv4TypeAddressMaskReply
}
return 0
}
func getICMPv6RejectCode(reason string) uint8 {
switch reason {
case ICMP_HOST_UNREACHABLE, ICMP_NET_UNREACHABLE, ICMP_NO_ROUTE:
return layers.ICMPv6CodeNoRouteToDst
case ICMP_ADDR_UNREACHABLE:
return layers.ICMPv6CodeAddressUnreachable
case ICMP_PORT_UNREACHABLE:
return layers.ICMPv6CodePortUnreachable
case ICMP_REJECT_POLICY_FAIL:
return layers.ICMPv6CodeSrcAddressFailedPolicy
case ICMP_REJECT_ROUTE:
return layers.ICMPv6CodeRejectRouteToDst
}
return layers.ICMPv6CodeNoRouteToDst
}

View file

@ -0,0 +1,176 @@
package exprs
import (
"strconv"
"strings"
"github.com/google/nftables/expr"
"golang.org/x/sys/unix"
)
// NewExprVerdict constructs a new verdict to apply on connections.
func NewExprVerdict(verdict, parms string) *[]expr.Any {
switch strings.ToLower(verdict) {
case VERDICT_ACCEPT:
return NewExprAccept()
case VERDICT_DROP:
return &[]expr.Any{&expr.Verdict{
Kind: expr.VerdictDrop,
}}
// FIXME: this verdict is not added to nftables
case VERDICT_STOP:
return &[]expr.Any{&expr.Verdict{
Kind: expr.VerdictStop,
}}
case VERDICT_REJECT:
reject := NewExprReject(parms)
return &[]expr.Any{reject}
case VERDICT_RETURN:
return &[]expr.Any{&expr.Verdict{
Kind: expr.VerdictReturn,
}}
case VERDICT_JUMP:
return &[]expr.Any{
&expr.Verdict{
Kind: expr.VerdictKind(unix.NFT_JUMP),
Chain: parms,
},
}
case VERDICT_QUEUE:
queueNum := 0
p := strings.Split(parms, " ")
if len(p) > 0 {
if p[0] == NFT_QUEUE_NUM {
queueNum, _ = strconv.Atoi(p[len(p)-1])
}
}
return &[]expr.Any{
&expr.Queue{
Num: uint16(queueNum),
// TODO: allow to configure this flag
Flag: expr.QueueFlagBypass,
}}
case VERDICT_SNAT:
snat := NewExprSNAT()
snat.Random, snat.FullyRandom, snat.Persistent = NewExprNATFlags(parms)
snatExpr := &[]expr.Any{snat}
if regAddr, regProto, natParms, err := NewExprNAT(parms, VERDICT_SNAT); err == nil {
if regAddr {
snat.RegAddrMin = 1
}
if regProto {
snat.RegProtoMin = 2
}
*snatExpr = append(*natParms, *snatExpr...)
}
return snatExpr
case VERDICT_DNAT:
dnat := NewExprDNAT()
dnat.Random, dnat.FullyRandom, dnat.Persistent = NewExprNATFlags(parms)
dnatExpr := &[]expr.Any{dnat}
if regAddr, regProto, natParms, err := NewExprNAT(parms, VERDICT_DNAT); err == nil {
if regAddr {
dnat.RegAddrMin = 1
}
if regProto {
dnat.RegProtoMin = 2
}
*dnatExpr = append(*natParms, *dnatExpr...)
}
return dnatExpr
case VERDICT_MASQUERADE:
m := &expr.Masq{}
m.Random, m.FullyRandom, m.Persistent = NewExprNATFlags(parms)
masqExpr := &[]expr.Any{m}
if parms == "" {
return masqExpr
}
// if any of the flag is set to true, toPorts must be false
toPorts := !(m.Random == true || m.FullyRandom == true || m.Persistent == true)
masqExpr = NewExprMasquerade(toPorts, m.Random, m.FullyRandom, m.Persistent)
if _, _, natParms, err := NewExprNAT(parms, VERDICT_MASQUERADE); err == nil {
*masqExpr = append(*natParms, *masqExpr...)
}
return masqExpr
case VERDICT_REDIRECT:
if _, _, rewriteParms, err := NewExprNAT(parms, VERDICT_REDIRECT); err == nil {
redirExpr := NewExprRedirect()
*redirExpr = append(*rewriteParms, *redirExpr...)
return redirExpr
}
case VERDICT_TPROXY:
if _, _, rewriteParms, err := NewExprNAT(parms, VERDICT_TPROXY); err == nil {
tproxyExpr := &[]expr.Any{}
*tproxyExpr = append(*tproxyExpr, *rewriteParms...)
tVerdict := NewExprTproxy()
*tproxyExpr = append(*tproxyExpr, *tVerdict...)
*tproxyExpr = append(*tproxyExpr, *NewExprAccept()...)
return tproxyExpr
}
}
// target can be empty, "ct set mark" or "log" for example
return &[]expr.Any{}
}
// NewExprAccept creates the accept verdict.
func NewExprAccept() *[]expr.Any {
return &[]expr.Any{&expr.Verdict{
Kind: expr.VerdictAccept,
}}
}
// NewExprReject creates new Reject expression
// icmpx, to reject the IPv4 and IPv6 traffic, icmp for ipv4, icmpv6 for ...
// Ex.: "Target": "reject", "TargetParameters": "with tcp reset"
// https://wiki.nftables.org/wiki-nftables/index.php/Rejecting_traffic
func NewExprReject(parms string) *expr.Reject {
reject := &expr.Reject{}
reject.Code = unix.NFT_REJECT_ICMP_UNREACH
reject.Type = unix.NFT_REJECT_ICMP_UNREACH
parmList := strings.Split(parms, " ")
length := len(parmList)
if length <= 1 {
return reject
}
what := parmList[1]
how := parmList[length-1]
switch what {
case NFT_PROTO_TCP:
reject.Type = unix.NFT_REJECT_TCP_RST
reject.Code = unix.NFT_REJECT_TCP_RST
case NFT_PROTO_ICMP:
reject.Type = unix.NFT_REJECT_ICMP_UNREACH
reject.Code = getICMPRejectCode(how)
return reject
case NFT_PROTO_ICMPX:
// icmp and icmpv6
reject.Type = unix.NFT_REJECT_ICMPX_UNREACH
reject.Code = getICMPxRejectCode(how)
return reject
case NFT_PROTO_ICMPv6:
reject.Type = 1
reject.Code = getICMPv6RejectCode(how)
default:
}
return reject
}

View file

@ -1,6 +1,9 @@
package nftables
import (
"time"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
)
@ -10,36 +13,30 @@ func (n *Nft) AreRulesLoaded() bool {
defer n.Unlock()
nRules := 0
for _, table := range n.mangleTables {
rules, err := n.conn.GetRule(table, n.outputChains[table])
if err != nil {
log.Error("nftables mangle rules error: %s, %s", table.Name, n.outputChains[table].Name)
return false
}
for _, r := range rules {
if string(r.UserData) == fwKey {
nRules++
}
}
}
if nRules != 2 {
log.Warning("nftables mangle rules not loaded: %d", nRules)
chains, err := n.conn.ListChains()
if err != nil {
log.Warning("[nftables] error listing nftables chains: %s", err)
return false
}
nRules = 0
for _, table := range n.filterTables {
rules, err := n.conn.GetRule(table, n.inputChains[table])
for _, c := range chains {
rules, err := n.conn.GetRule(c.Table, c)
if err != nil {
log.Error("nftables filter rules error: %s, %s", table.Name, n.inputChains[table].Name)
return false
log.Warning("[nftables] Error listing rules: %s", err)
continue
}
for _, r := range rules {
if string(r.UserData) == fwKey {
for rdx, r := range rules {
if string(r.UserData) == interceptionRuleKey {
nRules++
if c.Table.Name == exprs.NFT_CHAIN_MANGLE && rdx+1 != len(rules) {
log.Warning("nfables queue rule is not the latest of the list, reloading")
return false
}
}
}
}
// we expect to have exactly 2 rules (queue and dns). If there're less or more, then we
// need to reload them.
if nRules != 2 {
log.Warning("nfables filter rules not loaded: %d", nRules)
return false
@ -48,8 +45,23 @@ func (n *Nft) AreRulesLoaded() bool {
return true
}
// reloadConfCallback gets called after the configuration changes.
func (n *Nft) reloadConfCallback() {
log.Important("reloadConfCallback changed, reloading")
n.DeleteSystemRules(false, log.GetLogLevel() == log.DEBUG)
n.AddSystemRules(true)
}
// reloadRulesCallback gets called when the interception rules are not present.
func (n *Nft) reloadRulesCallback() {
log.Important("nftables firewall rules changed, reloading")
n.AddSystemRules()
n.InsertRules()
n.DisableInterception(true)
time.Sleep(time.Millisecond * 500)
n.EnableInterception()
}
// preloadConfCallback gets called before the fw configuration is loaded
func (n *Nft) preloadConfCallback() {
log.Info("nftables config changed, reloading")
n.DeleteSystemRules(false, log.GetLogLevel() == log.DEBUG)
}

View file

@ -1,45 +1,41 @@
package nftables
import (
"bytes"
"encoding/json"
"strings"
"sync"
"github.com/evilsocket/opensnitch/daemon/firewall/common"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/evilsocket/opensnitch/daemon/firewall/iptables"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"github.com/golang/protobuf/jsonpb"
"github.com/google/nftables"
)
const (
// Name is the name that identifies this firewall
Name = "nftables"
// Action is the modifier we apply to a rule.
type Action string
mangleTableName = "mangle"
filterTableName = "filter"
// The following chains will be under our own mangle or filter tables.
// There shouldn't be other chains with the same name here.
outputChain = "output"
inputChain = "input"
// key assigned to every fw rule we add, in order to get rules by this key.
fwKey = "opensnitch-key"
// Actions we apply to the firewall.
const (
fwKey = "opensnitch-key"
interceptionRuleKey = fwKey + "-interception"
systemRuleKey = fwKey + "-system"
Name = "nftables"
)
var (
filterTable = &nftables.Table{
Family: nftables.TableFamilyIPv4,
Name: filterTableName,
}
filterTable6 = &nftables.Table{
Family: nftables.TableFamilyIPv6,
Name: filterTableName,
Family: nftables.TableFamilyINet,
Name: exprs.NFT_CHAIN_FILTER,
}
mangleTable = &nftables.Table{
Family: nftables.TableFamilyIPv4,
Name: mangleTableName,
}
mangleTable6 = &nftables.Table{
Family: nftables.TableFamilyIPv6,
Name: mangleTableName,
Family: nftables.TableFamilyINet,
Name: exprs.NFT_CHAIN_FILTER,
}
)
@ -49,13 +45,7 @@ type Nft struct {
config.Config
common.Common
conn *nftables.Conn
mangleTables []*nftables.Table
filterTables []*nftables.Table
outputChains map[*nftables.Table]*nftables.Chain
inputChains map[*nftables.Table]*nftables.Chain
conn *nftables.Conn
chains iptables.SystemChains
}
@ -67,9 +57,9 @@ func NewNft() *nftables.Conn {
// Fw initializes a new nftables object
func Fw() (*Nft, error) {
n := &Nft{
outputChains: make(map[*nftables.Table]*nftables.Chain),
inputChains: make(map[*nftables.Table]*nftables.Chain),
chains: iptables.SystemChains{Rules: make(map[string]config.FwRule)},
chains: iptables.SystemChains{
Rules: make(map[string]*iptables.SystemRule),
},
}
return n, nil
}
@ -85,22 +75,22 @@ func (n *Nft) Init(qNum *int) {
if n.IsRunning() {
return
}
initMapsStore()
n.SetQueueNum(qNum)
n.conn = NewNft()
// In order to clean up any existing firewall rule before start,
// we need to load the fw configuration first.
n.NewSystemFwConfig(n.preloadConfCallback)
go n.MonitorSystemFw(n.AddSystemRules)
// we need to load the fw configuration first to know what rules
// were configured.
n.NewSystemFwConfig(n.preloadConfCallback, n.reloadConfCallback)
n.LoadDiskConfiguration(false)
// start from a clean state
n.CleanRules(false)
n.AddSystemRules()
n.InsertRules()
// start monitoring firewall rules to intercept network traffic.
n.NewRulesChecker(n.AreRulesLoaded, n.reloadRulesCallback)
// The daemon may have exited unexpectedly, leaving residual fw rules, so we
// need to clean them up to avoid duplicated rules.
n.delInterceptionRules()
n.AddSystemRules(false)
n.EnableInterception()
n.Running = true
}
@ -117,25 +107,86 @@ func (n *Nft) Stop() {
n.Running = false
}
// InsertRules adds fw rules to intercept connections
func (n *Nft) InsertRules() {
n.delInterceptionRules()
n.addGlobalTables()
n.addGlobalChains()
// EnableInterception adds firewall rules to intercept connections
func (n *Nft) EnableInterception() {
if err := n.addInterceptionTables(); err != nil {
log.Error("Error while adding interception tables: %s", err)
return
}
if err := n.addInterceptionChains(); err != nil {
log.Error("Error while adding interception chains: %s", err)
return
}
if err, _ := n.QueueDNSResponses(true, true); err != nil {
log.Error("Error while Running DNS nftables rule: %s", err)
} else if err, _ = n.QueueConnections(true, true); err != nil {
log.Fatal("Error while Running conntrack nftables rule: %s", err)
log.Error("Error while running DNS nftables rule: %s", err)
}
if err, _ := n.QueueConnections(true, true); err != nil {
log.Error("Error while running conntrack nftables rule: %s", err)
}
// start monitoring firewall rules to intercept network traffic.
n.NewRulesChecker(n.AreRulesLoaded, n.reloadRulesCallback)
}
// DisableInterception removes firewall rules to intercept outbound connections.
func (n *Nft) DisableInterception(logErrors bool) {
n.delInterceptionRules()
n.StopCheckingRules()
}
// CleanRules deletes the rules we added.
func (n *Nft) CleanRules(logErrors bool) {
n.delInterceptionRules()
err := n.conn.Flush()
if err != nil && logErrors {
log.Error("Error cleaning nftables tables: %s", err)
}
n.DisableInterception(logErrors)
n.DeleteSystemRules(true, logErrors)
}
// Commit applies the queued changes, creating new objects (tables, chains, etc).
// You add rules, chains or tables, and after calling to Flush() they're added to the system.
// NOTE: it's very important not to call Flush() without queued tasks.
func (n *Nft) Commit() bool {
if err := n.conn.Flush(); err != nil {
log.Warning("%s error applying changes: %s", logTag, err)
return false
}
return true
}
// Serialize converts the configuration from json to protobuf
func (n *Nft) Serialize() (*protocol.SysFirewall, error) {
sysfw := &protocol.SysFirewall{}
jun := jsonpb.Unmarshaler{
AllowUnknownFields: true,
}
rawConfig, err := json.Marshal(&n.SysConfig)
if err != nil {
log.Error("nftables.Serialize() struct to string error: %s", err)
return nil, err
}
// string to proto
if err := jun.Unmarshal(strings.NewReader(string(rawConfig)), sysfw); err != nil {
log.Error("nftables.Serialize() string to protobuf error: %s", err)
return nil, err
}
return sysfw, nil
}
// Deserialize converts a protocolbuffer structure to byte array.
func (n *Nft) Deserialize(sysfw *protocol.SysFirewall) ([]byte, error) {
jun := jsonpb.Marshaler{
OrigName: true,
EmitDefaults: true,
Indent: " ",
}
// NOTE: '<' and '>' characters are encoded to unicode (\u003c).
// This has no effect on adding rules to nftables.
// Users can still write "<" if they want to, rules are added ok.
var b bytes.Buffer
if err := jun.Marshal(&b, sysfw); err != nil {
log.Error("nfables.Deserialize() error 2: %s", err)
return nil, err
}
return b.Bytes(), nil
}

View file

@ -0,0 +1,161 @@
package nftables
import (
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/google/nftables"
"github.com/google/nftables/expr"
)
// nftables rules are composed of expressions, for example:
// tcp dport 443 ip daddr 192.168.1.1
// \-----------/ \------------------/
// with these format:
// keyword1<SPACE>keyword2<SPACE>value...
//
// here we parse the expression, and based on keyword1, we build the rule with the given options.
//
// If the rule has multiple values (tcp dport 80,443,8080), no spaces are allowed,
// and the separator is a ",", instead of the format { 80, 443, 8080 }
//
// In order to debug invalid expressions, or how to build new ones, use the following command:
// # nft --debug netlink add rule filter output mark set 1
// ip filter output
// [ immediate reg 1 0x00000001 ]
// [ meta set mark with reg 1 ]
//
// Debugging added rules:
// nft --debug netlink list ruleset
//
// https://wiki.archlinux.org/title/Nftables#Expressions
// https://wiki.nftables.org/wiki-nftables/index.php/Building_rules_through_expressions
func (n *Nft) parseExpression(table, chain, family string, expression *config.Expressions) *[]expr.Any {
var exprList []expr.Any
cmpOp := exprs.NewOperator(expression.Statement.Op)
switch expression.Statement.Name {
case exprs.NFT_CT:
exprCt := n.buildConntrackRule(expression.Statement.Values)
if exprCt == nil {
log.Warning("%s Ct statement error", logTag)
return nil
}
exprList = append(exprList, *exprCt...)
case exprs.NFT_META:
metaExpr, err := exprs.NewExprMeta(expression.Statement.Values)
if err != nil {
log.Warning("%s meta statement error: %s", logTag, err)
return nil
}
return metaExpr
// TODO: support iif, oif
case exprs.NFT_IIFNAME, exprs.NFT_OIFNAME:
isOut := expression.Statement.Name == exprs.NFT_OIFNAME
iface := expression.Statement.Values[0].Key
if iface == "" {
log.Warning("%s network interface statement error: %s", logTag, expression.Statement.Name)
return nil
}
exprList = append(exprList, *exprs.NewExprIface(iface, isOut, cmpOp)...)
case exprs.NFT_FAMILY_IP, exprs.NFT_FAMILY_IP6:
exprIP, err := exprs.NewExprIP(expression.Statement.Values, cmpOp)
if err != nil {
log.Warning("%s addr statement error: %s", logTag, err)
return nil
}
exprList = append(exprList, *exprIP...)
case exprs.NFT_PROTO_ICMP, exprs.NFT_PROTO_ICMPv6:
exprICMP := n.buildICMPRule(table, family, expression.Statement.Values)
if exprICMP == nil {
log.Warning("%s icmp statement error", logTag)
return nil
}
exprList = append(exprList, *exprICMP...)
case exprs.NFT_LOG:
defaultLog := "opensnitch"
if len(expression.Statement.Values) > 0 {
defaultLog = expression.Statement.Values[0].Value
}
exprLog := exprs.NewExprLog(exprs.NFT_LOG_PREFIX, defaultLog)
if exprLog == nil {
log.Warning("%s log statement error", logTag)
return nil
}
exprList = append(exprList, *exprLog...)
case exprs.NFT_LIMIT:
exprLimit, err := exprs.NewExprLimit(expression.Statement)
if err != nil {
log.Warning("%s %s", logTag, err)
return nil
}
exprList = append(exprList, *exprLimit...)
case exprs.NFT_PROTO_UDP, exprs.NFT_PROTO_TCP, exprs.NFT_PROTO_UDPLITE, exprs.NFT_PROTO_SCTP, exprs.NFT_PROTO_DCCP:
exprProto, err := exprs.NewExprProtocol(expression.Statement.Name)
if err != nil {
log.Warning("%s proto statement error: %s", logTag, err)
return nil
}
exprList = append(exprList, *exprProto...)
for _, exprValue := range expression.Statement.Values {
switch exprValue.Key {
case exprs.NFT_DPORT, exprs.NFT_SPORT:
exprPDir, err := exprs.NewExprPortDirection(exprValue.Key)
if err != nil {
log.Warning("%s ports statement error: %s", logTag, err)
return nil
}
exprList = append(exprList, []expr.Any{exprPDir}...)
exprList = append(exprList, *n.buildProtocolRule(table, family, exprValue.Value, &cmpOp)...)
}
}
case exprs.NFT_QUOTA:
exprQuota, err := exprs.NewQuota(expression.Statement.Values)
if err != nil {
log.Warning("%s quota statement error: %s", logTag, err)
return nil
}
exprList = append(exprList, *exprQuota...)
case exprs.NFT_NOTRACK:
exprList = append(exprList, *exprs.NewNoTrack()...)
case exprs.NFT_COUNTER:
defaultCounterName := "opensnitch"
counterObj := &nftables.CounterObj{
Table: &nftables.Table{Name: table, Family: nftables.TableFamilyIPv4},
Name: defaultCounterName,
Bytes: 0,
Packets: 0,
}
for _, counterOption := range expression.Statement.Values {
switch counterOption.Key {
case exprs.NFT_COUNTER_NAME:
defaultCounterName = counterOption.Value
counterObj.Name = defaultCounterName
case exprs.NFT_COUNTER_BYTES:
// TODO: allow to set initial bytes/packets?
counterObj.Bytes = 1
case exprs.NFT_COUNTER_PACKETS:
counterObj.Packets = 1
}
}
n.conn.AddObj(counterObj)
exprList = append(exprList, *exprs.NewExprCounter(defaultCounterName)...)
}
return &exprList
}

View file

@ -0,0 +1,157 @@
package nftables
import (
"strings"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/google/nftables"
"github.com/google/nftables/expr"
)
// rules examples: https://github.com/google/nftables/blob/master/nftables_test.go
func (n *Nft) buildICMPRule(table, family string, icmpOptions []*config.ExprValues) *[]expr.Any {
tbl := getTable(table, family)
if tbl == nil {
return nil
}
offset := uint32(0)
setType := nftables.TypeICMPType
exprICMP, _ := exprs.NewExprProtocol(exprs.NFT_PROTO_ICMP)
ICMPrule := []expr.Any{}
ICMPrule = append(ICMPrule, *exprICMP...)
ICMPtemp := []expr.Any{}
setElements := []nftables.SetElement{}
for _, icmp := range icmpOptions {
switch icmp.Key {
case exprs.NFT_ICMP_TYPE:
exprCmp := &expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{exprs.GetICMPType(icmp.Value)},
}
ICMPtemp = append(ICMPtemp, []expr.Any{exprCmp}...)
// fill setElements. If there're more than 1 icmp type we'll use it later
setElements = append(setElements,
[]nftables.SetElement{
{
Key: []byte{exprs.GetICMPType(icmp.Value)},
},
}...)
case exprs.NFT_ICMP_CODE:
// TODO
offset = 1
}
}
ICMPrule = append(ICMPrule, []expr.Any{
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: offset, // 0 type, 1 code
Len: 1,
},
}...)
if len(setElements) == 1 {
ICMPrule = append(ICMPrule, ICMPtemp...)
} else {
set := &nftables.Set{
Anonymous: true,
Constant: true,
Table: tbl,
KeyType: setType,
}
if err := n.conn.AddSet(set, setElements); err != nil {
log.Warning("%s AddSet() error: %s", logTag, err)
return nil
}
sysSets = append(sysSets, []*nftables.Set{set}...)
ICMPrule = append(ICMPrule, []expr.Any{
&expr.Lookup{
SourceRegister: 1,
SetName: set.Name,
SetID: set.ID,
}}...)
}
return &ICMPrule
}
func (n *Nft) buildConntrackRule(ctOptions []*config.ExprValues) *[]expr.Any {
exprList := []expr.Any{}
setMark := false
for _, ctOption := range ctOptions {
switch ctOption.Key {
// we expect to have multiple "state" keys:
// { "state": "established", "state": "related" }
case exprs.NFT_CT_STATE:
ctExprState, err := exprs.NewExprCtState(ctOptions)
if err != nil {
log.Warning("%s ct set state error: %s", logTag, err)
return nil
}
exprList = append(exprList, *ctExprState...)
exprList = append(exprList,
&expr.Cmp{Op: expr.CmpOpNeq, Register: 1, Data: []byte{0, 0, 0, 0}},
)
// we only need to iterate once here
goto Exit
case exprs.NFT_CT_SET_MARK:
setMark = true
case exprs.NFT_CT_MARK:
ctExprMark, err := exprs.NewExprCtMark(setMark, ctOption.Value)
if err != nil {
log.Warning("%s ct mark error: %s", logTag, err)
return nil
}
exprList = append(exprList, *ctExprMark...)
goto Exit
default:
log.Warning("%s invalid conntrack option: %s", logTag, ctOption)
return nil
}
}
Exit:
return &exprList
}
func (n *Nft) buildProtocolRule(table, family, ports string, cmpOp *expr.CmpOp) *[]expr.Any {
tbl := getTable(table, family)
if tbl == nil {
return nil
}
exprList := []expr.Any{}
if strings.Index(ports, ",") != -1 {
set := &nftables.Set{
Anonymous: true,
Constant: true,
Table: tbl,
KeyType: nftables.TypeInetService,
}
setElements := exprs.NewExprPortSet(ports)
if err := n.conn.AddSet(set, *setElements); err != nil {
log.Warning("%s AddSet() error: %s", logTag, err)
}
exprList = append(exprList, &expr.Lookup{
SourceRegister: 1,
SetName: set.Name,
SetID: set.ID,
})
sysSets = append(sysSets, []*nftables.Set{set}...)
} else if strings.Index(ports, "-") != -1 {
exprList = append(exprList, *exprs.NewExprPortRange(ports)...)
} else {
exprList = append(exprList, *exprs.NewExprPort(ports, cmpOp)...)
}
return &exprList
}

View file

@ -1,6 +1,9 @@
package nftables
import (
"fmt"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
@ -9,65 +12,32 @@ import (
"golang.org/x/sys/unix"
)
func (n *Nft) addGlobalTables() error {
filter := n.conn.AddTable(filterTable)
filter6 := n.conn.AddTable(filterTable6)
mangle := n.conn.AddTable(mangleTable)
mangle6 := n.conn.AddTable(mangleTable6)
n.mangleTables = []*nftables.Table{mangle, mangle6}
n.filterTables = []*nftables.Table{filter, filter6}
// apply changes
if err := n.conn.Flush(); err != nil {
return err
}
return nil
}
// TODO: add more parameters, make it more generic
func (n *Nft) addChain(name string, table *nftables.Table, prio nftables.ChainPriority, ctype nftables.ChainType, hook nftables.ChainHook) *nftables.Chain {
// nft list chains
return n.conn.AddChain(&nftables.Chain{
Name: name,
Table: table,
Type: ctype,
Hooknum: hook,
Priority: prio,
//Policy: nftables.ChainPolicyDrop
})
}
func (n *Nft) addGlobalChains() error {
// nft list tables
for _, table := range n.mangleTables {
n.outputChains[table] = n.addChain(outputChain, table, nftables.ChainPriorityMangle, nftables.ChainTypeRoute, nftables.ChainHookOutput)
}
for _, table := range n.filterTables {
n.inputChains[table] = n.addChain(inputChain, table, nftables.ChainPriorityFilter, nftables.ChainTypeFilter, nftables.ChainHookInput)
}
// apply changes
if err := n.conn.Flush(); err != nil {
log.Warning("Error adding nftables mangle tables: %v", err)
}
return nil
}
// QueueDNSResponses redirects DNS responses to us, in order to keep a cache
// of resolved domains.
// This rule must be added in top of the system rules, otherwise it may get bypassed.
// nft insert rule ip filter input udp sport 53 queue num 0 bypass
func (n *Nft) QueueDNSResponses(enable bool, logError bool) (error, error) {
if n.conn == nil {
return nil, nil
}
for _, table := range n.filterTables {
families := []string{exprs.NFT_FAMILY_INET}
for _, fam := range families {
table := getTable(exprs.NFT_CHAIN_FILTER, fam)
chain := getChain(exprs.NFT_HOOK_INPUT, table)
if table == nil {
log.Error("QueueDNSResponses() Error getting table: %s-filter", fam)
continue
}
if chain == nil {
log.Error("QueueDNSResponses() Error getting chain: %s-%d", table.Name, table.Family)
continue
}
// nft list ruleset -a
n.conn.InsertRule(&nftables.Rule{
Position: 0,
Table: table,
Chain: n.inputChains[table],
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
@ -92,24 +62,62 @@ func (n *Nft) QueueDNSResponses(enable bool, logError bool) (error, error) {
},
},
// rule key, to allow get it later by key
UserData: []byte(fwKey),
UserData: []byte(interceptionRuleKey),
})
}
// apply changes
if err := n.conn.Flush(); err != nil {
return err, nil
if !n.Commit() {
return fmt.Errorf("Error adding DNS interception rules"), nil
}
return nil, nil
}
// QueueConnections inserts the firewall rule which redirects connections to us.
// They are queued until the user denies/accept them, or reaches a timeout.
// Connections are queued until the user denies/accept them, or reaches a timeout.
// This rule must be added at the end of all the other rules, that way we can add
// rules above this one to exclude a service/app from being intercepted.
// nft insert rule ip mangle OUTPUT ct state new queue num 0 bypass
func (n *Nft) QueueConnections(enable bool, logError bool) (error, error) {
if n.conn == nil {
return nil, nil
return nil, fmt.Errorf("nftables QueueConnections: netlink connection not active")
}
table := getTable(exprs.NFT_CHAIN_MANGLE, exprs.NFT_FAMILY_INET)
if table == nil {
return nil, fmt.Errorf("QueueConnections() Error getting table mangle-inet")
}
chain := getChain(exprs.NFT_HOOK_OUTPUT, table)
if chain == nil {
return nil, fmt.Errorf("QueueConnections() Error getting outputChain: output-%s", table.Name)
}
n.conn.AddRule(&nftables.Rule{
Position: 0,
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Ct{Register: 1, SourceRegister: false, Key: expr.CtKeySTATE},
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitNEW | expr.CtStateBitRELATED),
Xor: binaryutil.NativeEndian.PutUint32(0),
},
&expr.Cmp{Op: expr.CmpOpNeq, Register: 1, Data: []byte{0, 0, 0, 0}},
&expr.Queue{
Num: n.QueueNum,
Flag: expr.QueueFlagBypass,
},
},
// rule key, to allow get it later by key
UserData: []byte(interceptionRuleKey),
})
// apply changes
if !n.Commit() {
return fmt.Errorf("Error adding interception rule "), nil
}
if enable {
// flush conntrack as soon as netfilter rule is set. This ensures that already-established
// connections will go to netfilter queue.
@ -118,84 +126,99 @@ func (n *Nft) QueueConnections(enable bool, logError bool) (error, error) {
}
}
for _, table := range n.mangleTables {
n.conn.InsertRule(&nftables.Rule{
Position: 0,
Table: table,
Chain: n.outputChains[table],
Exprs: []expr.Any{
&expr.Ct{Register: 1, SourceRegister: false, Key: expr.CtKeySTATE},
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitNEW | expr.CtStateBitRELATED),
Xor: binaryutil.NativeEndian.PutUint32(0),
},
&expr.Cmp{Op: expr.CmpOpNeq, Register: 1, Data: []byte{0, 0, 0, 0}},
&expr.Queue{
Num: n.QueueNum,
Flag: expr.QueueFlagBypass,
},
},
// rule key, to allow get it later by key
UserData: []byte(fwKey),
})
}
// apply changes
if err := n.conn.Flush(); err != nil {
return err, nil
}
return nil, nil
}
func (n *Nft) delInterceptionRules() {
n.delRulesByKey(fwKey)
func (n *Nft) insertRule(chain, table, family string, position uint64, exprs *[]expr.Any) error {
tbl := getTable(table, family)
if tbl == nil {
return fmt.Errorf("%s addRule, Error getting table: %s, %s", logTag, table, family)
}
chainKey := getChainKey(chain, tbl)
chn := sysChains[chainKey]
rule := &nftables.Rule{
Position: position,
Table: tbl,
Chain: chn,
Exprs: *exprs,
UserData: []byte(systemRuleKey),
}
n.conn.InsertRule(rule)
if !n.Commit() {
return fmt.Errorf("%s Error adding rule", logTag)
}
return nil
}
func (n *Nft) delRulesByKey(key string) {
func (n *Nft) addRule(chain, table, family string, position uint64, exprs *[]expr.Any) error {
tbl := getTable(table, family)
if tbl == nil {
return fmt.Errorf("%s addRule, Error getting table: %s, %s", logTag, table, family)
}
chainKey := getChainKey(chain, tbl)
chn := sysChains[chainKey]
rule := &nftables.Rule{
Position: position,
Table: tbl,
Chain: chn,
Exprs: *exprs,
UserData: []byte(systemRuleKey),
}
n.conn.AddRule(rule)
if !n.Commit() {
return fmt.Errorf("%s Error adding rule", logTag)
}
return nil
}
func (n *Nft) delRulesByKey(key string) error {
chains, err := n.conn.ListChains()
if err != nil {
log.Warning("nftables, error listing chains: %s", err)
return
return fmt.Errorf("error listing nftables chains (%s): %s", key, err)
}
commit := false
for _, c := range chains {
rules, err := n.conn.GetRule(c.Table, c)
if err != nil {
log.Warning("nftables, error listing rules (%s): %s", c.Table.Name, err)
log.Warning("Error listing rules (%s): %s", key, err)
continue
}
commit = false
delRules := 0
for _, r := range rules {
if string(r.UserData) != key {
continue
}
// just passing the rule object doesn't work.
// just passing the r object doesn't work.
if err := n.conn.DelRule(&nftables.Rule{
Table: c.Table,
Chain: c,
Handle: r.Handle,
}); err != nil {
log.Warning("nftables, error adding rule to be deleted (%s/%s): %s", c.Table.Name, c.Name, err)
log.Warning("[nftables] error deleting rule (%s): %s", key, err)
continue
}
commit = true
delRules++
}
if commit {
if err := n.conn.Flush(); err != nil {
log.Warning("nftables, error deleting interception rules (%s/%s): %s", c.Table.Name, c.Name, err)
if delRules > 0 {
if !n.Commit() {
log.Warning("%s error deleting rules: %s", logTag, err)
}
}
if rules, err := n.conn.GetRule(c.Table, c); err == nil {
if commit && len(rules) == 0 {
n.conn.DelChain(c)
n.conn.Flush()
if len(rules) == 0 || len(rules) == delRules {
if _, ok := sysChains[getChainKey(c.Name, c.Table)]; ok {
n.delChain(c)
}
}
}
return
return nil
}
func (n *Nft) delInterceptionRules() {
n.delRulesByKey(interceptionRuleKey)
}

View file

@ -1,40 +1,130 @@
package nftables
import (
"strings"
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"github.com/evilsocket/opensnitch/daemon/firewall/iptables"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/google/nftables"
"github.com/google/nftables/expr"
"github.com/google/uuid"
)
var (
logTag = "nftables:"
sysTables map[string]*nftables.Table
sysChains map[string]*nftables.Chain
sysSets []*nftables.Set
)
func initMapsStore() {
sysTables = make(map[string]*nftables.Table)
sysChains = make(map[string]*nftables.Chain)
}
// CreateSystemRule create the custom firewall chains and adds them to system.
// nft insert rule ip opensnitch-filter opensnitch-input udp dport 1153
func (n *Nft) CreateSystemRule(rule *config.FwRule, logErrors bool) {
// TODO
func (n *Nft) CreateSystemRule(chain *config.FwChain, logErrors bool) bool {
if chain.IsInvalid() {
log.Warning("%s CreateSystemRule(), Chain's field Name and Family cannot be empty", logTag)
return false
}
tableName := chain.Table
n.AddTable(chain.Table, chain.Family)
// regular chains doesn't have a hook, nor a type
if chain.Hook == "" && chain.Type == "" {
n.addRegularChain(chain.Name, tableName, chain.Family)
return n.Commit()
}
chainPolicy := nftables.ChainPolicyAccept
if iptables.Action(strings.ToLower(chain.Policy)) == exprs.VERDICT_DROP {
chainPolicy = nftables.ChainPolicyDrop
}
chainHook := getHook(chain.Hook)
chainPrio, chainType := getChainPriority(chain.Family, chain.Type, chain.Hook)
if chainPrio == nil {
log.Warning("%s Invalid system firewall combination: %s, %s", logTag, chain.Type, chain.Hook)
return false
}
if ret := n.AddChain(chain.Name, chain.Table, chain.Family, *chainPrio,
chainType, *chainHook, chainPolicy); ret == nil {
log.Warning("%s error adding chain: %s, table: %s", logTag, chain.Name, chain.Table)
return false
}
return n.Commit()
}
// AddSystemRules creates the system firewall from configuration.
func (n *Nft) AddSystemRules(reload bool) {
n.SysConfig.RLock()
defer n.SysConfig.RUnlock()
if n.SysConfig.Enabled == false {
log.Important("[nftables] AddSystemRules() fw disabled")
return
}
for _, fwCfg := range n.SysConfig.SystemRules {
for _, chain := range fwCfg.Chains {
if !n.CreateSystemRule(chain, true) {
log.Info("createSystem failed: %s %s", chain.Name, chain.Table)
continue
}
for i := len(chain.Rules) - 1; i >= 0; i-- {
if chain.Rules[i].UUID == "" {
uuid := uuid.New()
chain.Rules[i].UUID = uuid.String()
}
if chain.Rules[i].Enabled {
n.AddSystemRule(chain.Rules[i], chain)
}
}
}
}
}
// DeleteSystemRules deletes the system rules.
// If force is false and the rule has not been previously added,
// it won't try to delete the rules. Otherwise it'll try to delete them.
// it won't try to delete the tables and chains. Otherwise it'll try to delete them.
func (n *Nft) DeleteSystemRules(force, logErrors bool) {
// TODO
}
n.Lock()
defer n.Unlock()
// AddSystemRule inserts a new rule.
func (n *Nft) AddSystemRule(rule *config.FwRule, enable bool) (error, error) {
// TODO
return nil, nil
}
if err := n.delRulesByKey(systemRuleKey); err != nil {
log.Warning("error deleting interception rules: %s", err)
}
// AddSystemRules creates the system firewall from configuration
func (n *Nft) AddSystemRules() {
n.DeleteSystemRules(true, false)
for _, r := range n.SysConfig.SystemRules {
n.CreateSystemRule(r.Rule, true)
n.AddSystemRule(r.Rule, true)
if force {
n.delSystemTables()
}
}
// preloadConfCallback gets called before the fw configuration is reloaded
func (n *Nft) preloadConfCallback() {
n.DeleteSystemRules(true, log.GetLogLevel() == log.DEBUG)
// AddSystemRule inserts a new rule.
func (n *Nft) AddSystemRule(rule *config.FwRule, chain *config.FwChain) (err4, err6 error) {
n.Lock()
defer n.Unlock()
exprList := []expr.Any{}
for _, expression := range rule.Expressions {
if exprsOfRule := n.parseExpression(chain.Table, chain.Name, chain.Family, expression); exprsOfRule != nil {
exprList = append(exprList, *exprsOfRule...)
}
}
if len(exprList) > 0 {
exprVerdict := exprs.NewExprVerdict(rule.Target, rule.TargetParameters)
exprList = append(exprList, *exprVerdict...)
if err := n.insertRule(chain.Name, chain.Table, chain.Family, rule.Position, &exprList); err != nil {
log.Warning("error adding rule: %v", rule)
}
}
return nil, nil
}

View file

@ -0,0 +1,86 @@
package nftables
import (
"fmt"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/google/nftables"
)
// AddTable adds a new table to nftables.
func (n *Nft) AddTable(name, family string) *nftables.Table {
famCode := getFamilyCode(family)
tbl := &nftables.Table{
Family: famCode,
Name: name,
}
n.conn.AddTable(tbl)
if !n.Commit() {
log.Error("%s error adding system firewall table: %s, family: %s (%d)", logTag, name, family, famCode)
return nil
}
key := getTableKey(name, family)
sysTables[key] = tbl
return tbl
}
func getTable(name, family string) *nftables.Table {
return sysTables[getTableKey(name, family)]
}
func getTableKey(name string, family interface{}) string {
return fmt.Sprint(name, "-", family)
}
func (n *Nft) addInterceptionTables() error {
n.AddTable(exprs.NFT_CHAIN_MANGLE, exprs.NFT_FAMILY_INET)
n.AddTable(exprs.NFT_CHAIN_FILTER, exprs.NFT_FAMILY_INET)
return nil
}
// Contrary to iptables, in nftables there're no predefined rules.
// Convention is though to use the iptables names by default.
// We need at least: mangle and filter tables, inet family (IPv4 and IPv6).
func (n *Nft) addSystemTables() {
n.AddTable(exprs.NFT_CHAIN_MANGLE, exprs.NFT_FAMILY_INET)
n.AddTable(exprs.NFT_CHAIN_FILTER, exprs.NFT_FAMILY_INET)
}
// return the number of rules that we didn't add.
func (n *Nft) nonSystemRules(tbl *nftables.Table) int {
chains, err := n.conn.ListChains()
if err != nil {
return -1
}
t := 0
for _, c := range chains {
if tbl.Name != c.Table.Name && tbl.Family != c.Table.Family {
continue
}
rules, err := n.conn.GetRule(c.Table, c)
if err != nil {
return -1
}
t += len(rules)
}
return t
}
// FIXME: if the user configured chains policies to drop and disables the firewall,
// the policy is not restored.
func (n *Nft) delSystemTables() {
for k, tbl := range sysTables {
if n.nonSystemRules(tbl) != 0 {
continue
}
n.conn.DelTable(tbl)
if !n.Commit() {
log.Warning("error deleting system tables")
continue
}
delete(sysTables, k)
}
}

View file

@ -0,0 +1,173 @@
package nftables
import (
"strings"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables/exprs"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/google/nftables"
)
func getFamilyCode(family string) nftables.TableFamily {
famCode := nftables.TableFamilyINet
switch family {
// [filter]: prerouting forward input output postrouting
// [nat]: prerouting, input output postrouting
// [route]: output
case exprs.NFT_FAMILY_IP6:
famCode = nftables.TableFamilyIPv6
case exprs.NFT_FAMILY_IP:
famCode = nftables.TableFamilyIPv4
case exprs.NFT_FAMILY_BRIDGE:
// [filter]: prerouting forward input output postrouting
famCode = nftables.TableFamilyBridge
case exprs.NFT_FAMILY_ARP:
// [filter]: input output
famCode = nftables.TableFamilyARP
case exprs.NFT_FAMILY_NETDEV:
// [filter]: egress, ingress
famCode = nftables.TableFamilyNetdev
}
return famCode
}
func getHook(chain string) *nftables.ChainHook {
hook := nftables.ChainHookOutput
// https://github.com/google/nftables/blob/master/chain.go#L33
switch strings.ToLower(chain) {
case exprs.NFT_HOOK_INPUT:
hook = nftables.ChainHookInput
case exprs.NFT_HOOK_PREROUTING:
hook = nftables.ChainHookPrerouting
case exprs.NFT_HOOK_POSTROUTING:
hook = nftables.ChainHookPostrouting
case exprs.NFT_HOOK_FORWARD:
hook = nftables.ChainHookForward
case exprs.NFT_HOOK_INGRESS:
hook = nftables.ChainHookIngress
}
return &hook
}
// getChainPriority gets the corresponding priority for the given chain, based
// on the following configuration matrix:
// https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook
// https://github.com/google/nftables/blob/master/chain.go#L48
// man nft (table 6.)
func getChainPriority(family, cType, hook string) (*nftables.ChainPriority, nftables.ChainType) {
// types: route, nat, filter
chainType := nftables.ChainTypeFilter
// priorities: raw, conntrack, mangle, natdest, filter, security
chainPrio := nftables.ChainPriorityFilter
family = strings.ToLower(family)
cType = strings.ToLower(cType)
hook = strings.ToLower(hook)
// constraints
// https://www.netfilter.org/projects/nftables/manpage.html#lbAQ
if (cType == exprs.NFT_CHAIN_NATDEST || cType == exprs.NFT_CHAIN_NATSOURCE) && hook == exprs.NFT_HOOK_FORWARD {
log.Warning("[nftables] invalid nat combination of tables and hooks. chain: %s, hook: %s", cType, hook)
return nil, chainType
}
if family == exprs.NFT_FAMILY_NETDEV && (cType != exprs.NFT_CHAIN_FILTER || (hook != exprs.NFT_HOOK_EGRESS || hook != exprs.NFT_HOOK_INGRESS)) {
log.Warning("[nftables] invalid netdev combination of tables and hooks. chain: %s, hook: %s", cType, hook)
return nil, chainType
}
if family == exprs.NFT_FAMILY_ARP && (cType != exprs.NFT_CHAIN_FILTER || (hook != exprs.NFT_HOOK_OUTPUT || hook != exprs.NFT_HOOK_INPUT)) {
log.Warning("[nftables] invalid arp combination of tables and hooks. chain: %s, hook: %s", cType, hook)
return nil, chainType
}
if family == exprs.NFT_FAMILY_BRIDGE && (cType != exprs.NFT_CHAIN_FILTER || (hook == exprs.NFT_HOOK_EGRESS || hook == exprs.NFT_HOOK_INGRESS)) {
log.Warning("[nftables] invalid bridge combination of tables and hooks. chain: %s, hook: %s", cType, hook)
return nil, chainType
}
// Standard priority names, family and hook compatibility matrix
// https://www.netfilter.org/projects/nftables/manpage.html#lbAQ
switch cType {
case exprs.NFT_CHAIN_FILTER:
if family == exprs.NFT_FAMILY_BRIDGE {
// bridge all filter -200 NF_BR_PRI_FILTER_BRIDGED
chainPrio = nftables.ChainPriorityConntrack
switch hook {
case exprs.NFT_HOOK_PREROUTING: // -300
chainPrio = nftables.ChainPriorityRaw
case exprs.NFT_HOOK_OUTPUT: // -100
chainPrio = nftables.ChainPriorityNATSource
case exprs.NFT_HOOK_POSTROUTING: // 300
chainPrio = nftables.ChainPriorityConntrackHelper
}
}
case exprs.NFT_CHAIN_MANGLE:
// hooks: all
// XXX: check hook input?
chainPrio = nftables.ChainPriorityMangle
// https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_types
// (...) equivalent semantics to the mangle table but only for the output hook (for other hooks use type filter instead).
// Despite of what is said on the wiki, mangle chains must be of filter type,
// otherwise on some kernels (4.19.x) table MANGLE hook OUTPUT chain is not created
chainType = nftables.ChainTypeFilter
case exprs.NFT_CHAIN_RAW:
// hook: all
chainPrio = nftables.ChainPriorityRaw
case exprs.NFT_CHAIN_CONNTRACK:
chainPrio, chainType = getConntrackPriority(hook)
case exprs.NFT_CHAIN_NATDEST:
// hook: prerouting
chainPrio = nftables.ChainPriorityNATDest
switch hook {
case exprs.NFT_HOOK_OUTPUT:
chainPrio = nftables.ChainPriorityNATSource
}
chainType = nftables.ChainTypeNAT
case exprs.NFT_CHAIN_NATSOURCE:
// hook: postrouting
chainPrio = nftables.ChainPriorityNATSource
chainType = nftables.ChainTypeNAT
case exprs.NFT_CHAIN_SECURITY:
// hook: all
chainPrio = nftables.ChainPrioritySecurity
case exprs.NFT_CHAIN_SELINUX:
// hook: all
if hook != exprs.NFT_HOOK_POSTROUTING {
chainPrio = nftables.ChainPrioritySELinuxLast
} else {
chainPrio = nftables.ChainPrioritySELinuxFirst
}
}
return &chainPrio, chainType
}
// https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook
func getConntrackPriority(hook string) (nftables.ChainPriority, nftables.ChainType) {
chainType := nftables.ChainTypeFilter
chainPrio := nftables.ChainPriorityConntrack
switch hook {
case exprs.NFT_HOOK_PREROUTING:
chainPrio = nftables.ChainPriorityConntrack
// ChainTypeNAT not allowed here
case exprs.NFT_HOOK_OUTPUT:
chainPrio = nftables.ChainPriorityNATSource // 100 - ChainPriorityConntrack
case exprs.NFT_HOOK_POSTROUTING:
chainPrio = nftables.ChainPriorityConntrackHelper
chainType = nftables.ChainTypeNAT
case exprs.NFT_HOOK_INPUT:
// can also be hook == NFT_HOOK_POSTROUTING
chainPrio = nftables.ChainPriorityConntrackConfirm
}
return chainPrio, chainType
}

View file

@ -1,10 +1,12 @@
package firewall
import (
"github.com/evilsocket/opensnitch/daemon/firewall/config"
"fmt"
"github.com/evilsocket/opensnitch/daemon/firewall/iptables"
"github.com/evilsocket/opensnitch/daemon/firewall/nftables"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
)
// Firewall is the interface that all firewalls (iptables, nftables) must implement.
@ -15,18 +17,62 @@ type Firewall interface {
IsRunning() bool
SetQueueNum(num *int)
InsertRules()
SaveConfiguration(rawConfig string) error
EnableInterception()
DisableInterception(bool)
QueueDNSResponses(bool, bool) (error, error)
QueueConnections(bool, bool) (error, error)
CleanRules(bool)
AddSystemRules()
AddSystemRules(bool)
DeleteSystemRules(bool, bool)
AddSystemRule(*config.FwRule, bool) (error, error)
CreateSystemRule(*config.FwRule, bool)
Serialize() (*protocol.SysFirewall, error)
Deserialize(sysfw *protocol.SysFirewall) ([]byte, error)
}
var fw Firewall
var (
fw Firewall
queueNum = 0
)
// Init initializes the firewall and loads firewall rules.
// We'll try to use the firewall configured in the configuration (iptables/nftables).
// If iptables is not installed, we can add nftables rules directly to the kernel,
// without relying on any binaries.
func Init(fwType string, qNum *int) {
var err error
if fwType == iptables.Name {
fw, err = iptables.Fw()
if err != nil {
log.Warning("iptables not available: %s", err)
}
}
if fwType == nftables.Name || err != nil {
fw, err = nftables.Fw()
if err != nil {
log.Warning("nftables not available: %s", err)
}
}
if err != nil {
log.Error("firewall error: %s, not iptables nor nftables are available or are usable. Please, report it on github.", err)
return
}
if fw == nil {
log.Error("firewall not initialized.")
return
}
fw.Stop()
fw.Init(qNum)
queueNum = *qNum
log.Info("Using %s firewall", fw.Name())
}
// IsRunning returns if the firewall is running or not.
func IsRunning() bool {
@ -41,6 +87,36 @@ func CleanRules(logErrors bool) {
fw.CleanRules(logErrors)
}
// Reload deletes existing firewall rules and readds them.
func Reload() {
fw.Stop()
fw.Init(&queueNum)
}
// ReloadSystemRules deletes existing rules, and add them again
func ReloadSystemRules() {
fw.DeleteSystemRules(false, true)
fw.AddSystemRules(true)
}
// EnableInterception removes the rules to intercept outbound connections.
func EnableInterception() error {
if fw == nil {
return fmt.Errorf("firewall not initialized when trying to enable interception, report please")
}
fw.EnableInterception()
return nil
}
// DisableInterception removes the rules to intercept outbound connections.
func DisableInterception() error {
if fw == nil {
return fmt.Errorf("firewall not initialized when trying to disable interception, report please")
}
fw.DisableInterception(true)
return nil
}
// Stop deletes the firewall rules, allowing network traffic.
func Stop() {
if fw == nil {
@ -49,37 +125,17 @@ func Stop() {
fw.Stop()
}
// Init initializes the firewall and loads firewall rules.
func Init(fwType string, qNum *int) {
var err error
if fwType == iptables.Name {
fw, err = iptables.Fw()
if err != nil {
log.Warning("iptables not available: %s", err)
}
}
// if iptables is not installed, we can add nftables rules directly to the kernel,
// without relying on any binaries.
if fwType == nftables.Name || err != nil {
fw, err = nftables.Fw()
if err != nil {
log.Warning("nftables not available: %s", err)
}
}
if err != nil {
log.Warning("firewall error: %s, not iptables nor nftables are available or are usable. Please, report it on github.", err)
return
}
if fw == nil {
log.Error("firewall not initialized.")
return
}
fw.Stop()
fw.Init(qNum)
log.Info("Using %s firewall", fw.Name())
// SaveConfiguration saves configuration string to disk
func SaveConfiguration(rawConfig []byte) error {
return fw.SaveConfiguration(string(rawConfig))
}
// Serialize transforms firewall json configuration to protobuf
func Serialize() (*protocol.SysFirewall, error) {
return fw.Serialize()
}
// Deserialize transforms firewall json configuration to protobuf
func Deserialize(sysfw *protocol.SysFirewall) ([]byte, error) {
return fw.Deserialize(sysfw)
}

View file

@ -5,15 +5,14 @@ go 1.14
require (
github.com/evilsocket/ftrace v1.2.0
github.com/fsnotify/fsnotify v1.4.7
github.com/golang/protobuf v1.5.0
github.com/google/gopacket v1.1.14
github.com/google/nftables v0.0.0-20220210072902-edf9fe8cd04f
github.com/google/nftables v0.0.0-20220329160011-5a9391c12fe3
github.com/google/uuid v1.3.0
github.com/iovisor/gobpf v0.2.0
github.com/vishvananda/netlink v0.0.0-20210811191823-e1a867c6b452
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1
golang.org/x/text v0.3.0 // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
google.golang.org/grpc v1.32.0
google.golang.org/protobuf v1.26.0 // indirect
)

View file

@ -1,14 +1,238 @@
{
"SystemRules": [
"Enabled": true,
"Version": 1,
"SystemRules": [
{
"Rule": {
"Table": "mangle",
"Chain": "OUTPUT",
"Enabled": false,
"Position": "0",
"Description": "Allow icmp",
"Parameters": "-p icmp",
"Expressions": [],
"Target": "ACCEPT",
"TargetParameters": ""
},
"Chains": []
},
{
"Chains": [
{
"Rule": {
"Description": "Allow icmp",
"Table": "mangle",
"Chain": "OUTPUT",
"Parameters": "-p icmp",
"Target": "ACCEPT",
"TargetParameters": ""
"Name": "forward",
"Table": "filter",
"Family": "inet",
"Priority": "",
"Type": "filter",
"Hook": "forward",
"Policy": "accept",
"Rules": []
},
{
"Name": "output",
"Table": "filter",
"Family": "inet",
"Priority": "",
"Type": "filter",
"Hook": "output",
"Policy": "accept",
"Rules": []
},
{
"Name": "input",
"Table": "filter",
"Family": "inet",
"Priority": "",
"Type": "filter",
"Hook": "input",
"Policy": "accept",
"Rules": [
{
"Enabled": false,
"Position": "0",
"Description": "Allow SSH server connections when input policy is DROP",
"Parameters": "",
"Expressions": [
{
"Statement": {
"Op": "",
"Name": "tcp",
"Values": [
{
"Key": "dport",
"Value": "22"
}
]
}
}
],
"Target": "accept",
"TargetParameters": ""
}
]
},
{
"Name": "filter-prerouting",
"Table": "nat",
"Family": "inet",
"Priority": "",
"Type": "filter",
"Hook": "prerouting",
"Policy": "accept",
"Rules": []
},
{
"Name": "prerouting",
"Table": "mangle",
"Family": "inet",
"Priority": "",
"Type": "mangle",
"Hook": "prerouting",
"Policy": "accept",
"Rules": []
},
{
"Name": "postrouting",
"Table": "mangle",
"Family": "inet",
"Priority": "",
"Type": "mangle",
"Hook": "postrouting",
"Policy": "accept",
"Rules": []
},
{
"Name": "prerouting",
"Table": "nat",
"Family": "inet",
"Priority": "",
"Type": "natdest",
"Hook": "prerouting",
"Policy": "accept",
"Rules": []
},
{
"Name": "postrouting",
"Table": "nat",
"Family": "inet",
"Priority": "",
"Type": "natsource",
"Hook": "postrouting",
"Policy": "accept",
"Rules": []
},
{
"Name": "input",
"Table": "nat",
"Family": "inet",
"Priority": "",
"Type": "natsource",
"Hook": "input",
"Policy": "accept",
"Rules": []
},
{
"Name": "output",
"Table": "nat",
"Family": "inet",
"Priority": "",
"Type": "natdest",
"Hook": "output",
"Policy": "accept",
"Rules": []
},
{
"Name": "output",
"Table": "mangle",
"Family": "inet",
"Priority": "",
"Type": "mangle",
"Hook": "output",
"Policy": "accept",
"Rules": [
{
"Enabled": true,
"Position": "0",
"Description": "Allow ICMP",
"Expressions": [
{
"Statement": {
"Op": "",
"Name": "icmp",
"Values": [
{
"Key": "type",
"Value": "echo-request"
},
{
"Key": "type",
"Value": "echo-reply"
}
]
}
}
],
"Target": "accept",
"TargetParameters": ""
},
{
"Enabled": false,
"Position": "0",
"Description": "Exclude WireGuard VPN from being intercepted",
"Parameters": "",
"Expressions": [
{
"Statement": {
"Op": "",
"Name": "tcp",
"Values": [
{
"Key": "dport",
"Value": "51820"
}
]
}
}
],
"Target": "accept",
"TargetParameters": ""
}
]
},
{
"Name": "forward",
"Table": "mangle",
"Family": "inet",
"Priority": "",
"Type": "mangle",
"Hook": "forward",
"Policy": "accept",
"Rules": [
{
"UUID": "7d7394e1-100d-4b87-a90a-cd68c46edb0b",
"Enabled": false,
"Position": "0",
"Description": "Intercept forwarded connections (docker, etc)",
"Expressions": [
{
"Statement": {
"Op": "",
"Name": "ct",
"Values": [
{
"Key": "state",
"Value": "new"
}
]
}
}
],
"Target": "queue",
"TargetParameters": "num 0"
}
]
}
]
]
}
]
}

View file

@ -42,6 +42,10 @@ func (c *Client) getClientConfig() *protocol.ClientConfig {
ruleList[idx] = r.Serialize()
idx++
}
sysfw, err := firewall.Serialize()
if err != nil {
log.Warning("firewall.Serialize() error: %s", err)
}
return &protocol.ClientConfig{
Id: uint64(ts.UnixNano()),
Name: nodeName,
@ -50,6 +54,7 @@ func (c *Client) getClientConfig() *protocol.ClientConfig {
Config: strings.Replace(string(raw), "\n", "", -1),
LogLevel: uint32(log.MinLevel),
Rules: ruleList,
SystemFirewall: sysfw,
}
}
@ -194,14 +199,37 @@ func (c *Client) handleNotification(stream protocol.UI_NotificationsClient, noti
case notification.Type == protocol.Action_CHANGE_CONFIG:
c.handleActionChangeConfig(stream, notification)
case notification.Type == protocol.Action_LOAD_FIREWALL:
log.Info("[notification] starting firewall")
firewall.Init(c.GetFirewallType(), nil)
case notification.Type == protocol.Action_ENABLE_INTERCEPTION:
log.Info("[notification] starting interception")
if err := firewall.EnableInterception(); err != nil {
log.Warning("firewall.EnableInterception() error: %s", err)
c.sendNotificationReply(stream, notification.Id, "", err)
return
}
c.sendNotificationReply(stream, notification.Id, "", nil)
case notification.Type == protocol.Action_UNLOAD_FIREWALL:
log.Info("[notification] stopping firewall")
firewall.Stop()
case notification.Type == protocol.Action_DISABLE_INTERCEPTION:
log.Info("[notification] stopping interception")
if err := firewall.DisableInterception(); err != nil {
log.Warning("firewall.DisableInterception() error: %s", err)
c.sendNotificationReply(stream, notification.Id, "", err)
return
}
c.sendNotificationReply(stream, notification.Id, "", nil)
case notification.Type == protocol.Action_RELOAD_FW_RULES:
log.Info("[notification] reloading firewall")
sysfw, err := firewall.Deserialize(notification.SysFirewall)
if err != nil {
log.Warning("firewall.Deserialize() error: %s", err)
c.sendNotificationReply(stream, notification.Id, "", fmt.Errorf("Error reloading firewall, invalid rules"))
return
}
if err := firewall.SaveConfiguration(sysfw); err != nil {
c.sendNotificationReply(stream, notification.Id, "", fmt.Errorf("Error saving system firewall rules: %s", err))
return
}
c.sendNotificationReply(stream, notification.Id, "", nil)
// ENABLE_RULE just replaces the rule on disk

View file

@ -80,17 +80,73 @@ message Rule {
enum Action {
NONE = 0;
LOAD_FIREWALL = 1;
UNLOAD_FIREWALL = 2;
CHANGE_CONFIG = 3;
ENABLE_RULE = 4;
DISABLE_RULE = 5;
DELETE_RULE = 6;
CHANGE_RULE = 7;
LOG_LEVEL = 8;
STOP = 9;
MONITOR_PROCESS = 10;
STOP_MONITOR_PROCESS = 11;
ENABLE_INTERCEPTION = 1;
DISABLE_INTERCEPTION = 2;
ENABLE_FIREWALL = 3;
DISABLE_FIREWALL = 4;
RELOAD_FW_RULES = 5;
CHANGE_CONFIG = 6;
ENABLE_RULE = 7;
DISABLE_RULE = 8;
DELETE_RULE = 9;
CHANGE_RULE = 10;
LOG_LEVEL = 11;
STOP = 12;
MONITOR_PROCESS = 13;
STOP_MONITOR_PROCESS = 14;
}
message StatementValues {
string Key = 1;
string Value = 2;
}
message Statement {
string Op = 1;
string Name = 2;
repeated StatementValues Values = 3;
}
message Expressions {
Statement Statement = 1;
}
message FwRule {
// DEPRECATED: for backward compatibility with iptables
string Table = 1;
string Chain = 2;
string UUID = 3;
bool Enabled = 4;
uint64 Position = 5;
string Description = 6;
string Parameters = 7;
repeated Expressions Expressions = 8;
string Target = 9;
string TargetParameters = 10;
}
message FwChain {
string Name = 1;
string Table = 2;
string Family = 3;
string Priority = 4;
string Type = 5;
string Hook = 6;
string Policy = 7;
repeated FwRule Rules = 8;
}
message FwChains {
// DEPRECATED: backward compatibility with iptables
FwRule Rule = 1;
repeated FwChain Chains = 2;
}
message SysFirewall {
bool Enabled = 1;
uint32 Version = 2;
repeated FwChains SystemRules = 3;
}
// client configuration sent on Subscribe()
@ -103,6 +159,7 @@ message ClientConfig {
string config = 5;
uint32 logLevel = 6;
repeated Rule rules = 7;
SysFirewall systemFirewall = 8;
}
// notification sent to the clients (daemons)
@ -114,6 +171,7 @@ message Notification {
Action type = 4;
string data = 5;
repeated Rule rules = 6;
SysFirewall sysFirewall = 7;
}
// notification reply sent to the server (GUI)

View file

@ -6,6 +6,7 @@ class Config:
HELP_URL = "https://github.com/evilsocket/opensnitch/wiki/"
HELP_RULES_URL = "https://github.com/evilsocket/opensnitch/wiki/Rules"
HELP_SYS_RULES_URL = "https://github.com/evilsocket/opensnitch/wiki/System-rules"
HELP_CONFIG_URL = "https://github.com/evilsocket/opensnitch/wiki/Configurations"
RULE_TYPE_LIST = "list"
@ -26,6 +27,19 @@ class Config:
ACTION_ALLOW = "allow"
ACTION_DENY = "deny"
ACTION_REJECT = "reject"
ACTION_ACCEPT = "accept"
ACTION_DROP = "drop"
ACTION_JUMP = "jump"
ACTION_REDIRECT = "redirect"
ACTION_RETURN = "return"
ACTION_TPROXY = "tproxy"
ACTION_SNAT = "snat"
ACTION_DNAT = "dnat"
ACTION_MASQUERADE = "masquerade"
ACTION_QUEUE = "queue"
ACTION_LOG = "log"
ACTION_STOP = "stop"
DURATION_UNTIL_RESTART = "until restart"
DURATION_ALWAYS = "always"
DURATION_ONCE = "once"
@ -77,6 +91,7 @@ class Config:
STATS_GENERAL_FILTER_TEXT = "statsDialog/"
STATS_GENERAL_FILTER_ACTION = "statsDialog/"
STATS_RULES_COL_STATE = "statsDialog/rules_columns_state"
STATS_FW_COL_STATE = "statsDialog/firewall_columns_state"
STATS_RULES_TREE_EXPANDED_0 = "statsDialog/rules_tree_0_expanded"
STATS_RULES_TREE_EXPANDED_1 = "statsDialog/rules_tree_1_expanded"
STATS_RULES_SPLITTER_POS = "statsDialog/rules_splitter_pos"

View file

@ -0,0 +1,296 @@
from PyQt5 import Qt, QtCore
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtSql import QSqlQuery, QSqlError
from PyQt5.QtWidgets import QTableView, QAbstractSlider, QItemDelegate, QAbstractItemView, QPushButton, QWidget, QVBoxLayout
from PyQt5.QtCore import QItemSelectionModel, pyqtSignal, pyqtSlot, QEvent
from PyQt5.QtCore import QCoreApplication as QC
import math
from opensnitch.nodes import Nodes
from opensnitch.firewall import Firewall
from opensnitch.customwidgets.updownbtndelegate import UpDownButtonDelegate
class FirewallTableModel(QStandardItemModel):
rowCountChanged = pyqtSignal()
columnCountChanged = pyqtSignal(int)
rowsUpdated = pyqtSignal(int, tuple)
rowsReordered = pyqtSignal(int, str, str, int, int) # filter, addr, key, old_pos, new_pos
tableName = ""
# total row count which must de displayed in the view
totalRowCount = 0
# last column count to compare against with
lastColumnCount = 0
FILTER_ALL = 0
FILTER_BY_NODE = 1
FILTER_BY_TABLE = 2
FILTER_BY_CHAIN = 3
FILTER_BY_QUERY = 4
activeFilter = FILTER_ALL
UP_BTN = -1
DOWN_BTN = 1
COL_BTNS = 0
COL_UUID = 1
COL_ADDR = 2
COL_CHAIN_NAME = 3
COL_CHAIN_TABLE = 4
COL_CHAIN_FAMILY = 5
COL_CHAIN_HOOK = 6
headersAll = [
"", # buttons
"", # uuid
QC.translate("firewall", "Node", ""),
QC.translate("firewall", "Name", ""),
QC.translate("firewall", "Table", ""),
QC.translate("firewall", "Family", ""),
QC.translate("firewall", "Hook", ""),
QC.translate("firewall", "Enabled", ""),
QC.translate("firewall", "Description", ""),
QC.translate("firewall", "Parameters", ""),
QC.translate("firewall", "Action", ""),
]
items = []
lastRules = []
position = 0
def __init__(self, tableName):
self.tableName = tableName
self._nodes = Nodes.instance()
self._fw = Firewall.instance()
self.lastColumnCount = len(self.headersAll)
self.lastQueryArgs = ()
QStandardItemModel.__init__(self, 0, self.lastColumnCount)
self.setHorizontalHeaderLabels(self.headersAll)
def filterByNode(self, addr):
self.activeFilter = self.FILTER_BY_NODE
self.fillRows(0, True, addr)
def filterAll(self):
self.activeFilter = self.FILTER_ALL
self.fillRows(0, True)
def filterByTable(self, addr, name, family):
self.activeFilter = self.FILTER_BY_TABLE
self.fillRows(0, True, addr, name, family)
def filterByChain(self, addr, table, family, chain, hook):
self.activeFilter = self.FILTER_BY_CHAIN
self.fillRows(0, True, addr, table, family, chain, hook)
def filterByQuery(self, query):
self.activeFilter = self.FILTER_BY_QUERY
self.fillRows(0, True, query)
def reorderRows(self, action, row):
if (row.row()+action == self.rowCount() and action == self.DOWN_BTN) or \
(row.row() == 0 and action == self.UP_BTN):
return
# XXX: better use moveRow()?
newRow = []
# save the row we're about to overwrite
for c in range(self.columnCount()):
item = self.index(row.row()+action, c)
itemText = item.data()
newRow.append(itemText)
# overwrite next item with current data
for c in range(self.columnCount()):
curItem = self.index(row.row(), c).data()
nextIdx = self.index(row.row()+action, c)
self.setData(nextIdx, curItem, QtCore.Qt.DisplayRole)
# restore row with the overwritten data
for i, nr in enumerate(newRow):
idx = self.index(row.row(), i)
self.setData(idx, nr, QtCore.Qt.DisplayRole)
self.rowsReordered.emit(
self.activeFilter,
self.index(row.row()+action, self.COL_ADDR).data(), # address
self.index(row.row()+action, self.COL_UUID).data(), # key
row.row(),
row.row()+action)
def refresh(self, force=False):
self.fillRows(0, force, *self.lastQueryArgs)
#Some QSqlQueryModel methods must be mimiced so that this class can serve as a drop-in replacement
#mimic QSqlQueryModel.query()
def query(self):
return self
#mimic QSqlQueryModel.query().lastError()
def lastError(self):
return QSqlError()
#mimic QSqlQueryModel.clear()
def clear(self):
self.items = []
self.removeColumns(0, self.lastColumnCount)
self.setColumnCount(0)
self.setRowCount(0)
# set columns based on query's fields
def setModelColumns(self, headers):
count = len(headers)
self.clear()
self.setHorizontalHeaderLabels(headers)
self.lastColumnCount = count
self.setColumnCount(self.lastColumnCount)
self.columnCountChanged.emit(count)
def query(self):
return QSqlQuery()
def setQuery(self, q, db, args=None):
self.refresh()
def nextRecord(self, offset):
self.position += 1
def prevRecord(self, offset):
self.position -= 1
def fillRows(self, upperBound, force, *data):
if self.activeFilter == self.FILTER_BY_NODE and len(data) == 0:
return
cols = []
rules = []
#don't trigger setItem's signals for each cell, instead emit dataChanged for all cells
self.blockSignals(True)
# mandatory for rows refreshing
self.layoutAboutToBeChanged.emit()
if self.activeFilter == self.FILTER_BY_NODE:
rules = self._fw.get_node_rules(data[0])
self.setModelColumns(self.headersAll)
elif self.activeFilter == self.FILTER_BY_TABLE:
rules = self._fw.filter_by_table(data[0], data[1], data[2])
self.setModelColumns(self.headersAll)
elif self.activeFilter == self.FILTER_BY_CHAIN:
rules = self._fw.filter_by_chain(data[0], data[1], data[2], data[3], data[4])
self.setModelColumns(self.headersAll)
elif self.activeFilter == self.FILTER_BY_QUERY:
rules = self._fw.filter_rules(data[0])
self.setModelColumns(self.headersAll)
else:
self.setModelColumns(self.headersAll)
rules = self._fw.get_rules()
self.addRows(rules)
self.blockSignals(False)
if self.lastRules != rules or force == True:
self.layoutChanged.emit()
self.totalRowCount = len(rules)
self.setRowCount(self.totalRowCount)
self.rowsUpdated.emit(self.activeFilter, data)
self.dataChanged.emit(self.createIndex(0,0), self.createIndex(self.rowCount(), self.columnCount()))
self.lastRules = rules
self.lastQueryArgs = data
del cols
del rules
def addRows(self, rules):
self.items = []
for rows in rules:
cols = []
cols.append(QStandardItem("")) # buttons column
for cl in rows:
item = QStandardItem(cl)
item.setData(cl, QtCore.Qt.UserRole+1)
cols.append(item)
self.appendRow(cols)
class FirewallTableView(QTableView):
# how many rows can potentially be displayed in viewport
# the actual number of rows currently displayed may be less than this
maxRowsInViewport = 0
rowsReordered = pyqtSignal(str) # addr
def __init__(self, parent):
QTableView.__init__(self, parent)
self._fw = Firewall.instance()
self._fw.rules.rulesUpdated.connect(self._cb_fw_rules_updated)
self.verticalHeader().setVisible(True)
self.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignCenter)
self.horizontalHeader().setStretchLastSection(True)
# FIXME: if the firewall being used is iptables, hide the column to
# reorder rules, it's not supported.
updownBtn = UpDownButtonDelegate(self)
self.setItemDelegateForColumn(0, updownBtn)
updownBtn.clicked.connect(self._cb_fw_rule_position_changed)
def _cb_fw_rules_updated(self):
self.model().refresh(True)
def _cb_column_count_changed(self, num):
for i in range(num):
self.resizeColumnToContents(i)
def _cb_fw_rule_position_changed(self, action, row):
self.model().reorderRows(action, row)
def _cb_rows_reordered(self, view, node_addr, uuid, old_pos, new_pos):
if self._fw.swap_rules(view, node_addr, uuid, old_pos, new_pos):
self.rowsReordered.emit(node_addr)
#@QtCore.pyqtSlot(int, tuple)
def _cb_rows_updated(self, view, data):
for c in range(self.model().rowCount()):
self.setColumnHidden(c, False)
#self.horizontalHeader().setSectionResizeMode(
# c, QHeaderView.ResizeToContents
#)
self.setColumnHidden(FirewallTableModel.COL_BTNS, True)
self.setColumnHidden(FirewallTableModel.COL_UUID, True)
if view >= self.model().FILTER_BY_NODE:
# hide address column
self.setColumnHidden(FirewallTableModel.COL_ADDR, True)
if view >= self.model().FILTER_BY_TABLE:
self.setColumnHidden(FirewallTableModel.COL_CHAIN_TABLE, True)
self.setColumnHidden(FirewallTableModel.COL_CHAIN_FAMILY, True)
if view >= self.model().FILTER_BY_CHAIN:
# hide chain's name, family and hook
self.setColumnHidden(FirewallTableModel.COL_CHAIN_NAME, True)
self.setColumnHidden(FirewallTableModel.COL_CHAIN_HOOK, True)
self.setColumnHidden(FirewallTableModel.COL_BTNS, False)
def filterAll(self):
self.model().filterAll()
def filterByNode(self, addr):
self.model().filterByNode(addr)
def filterByTable(self, addr, name, family):
self.model().filterByTable(addr, name, family)
def filterByChain(self, addr, table, family, chain, hook):
self.model().filterByChain(addr, table, family, chain, hook)
def filterByQuery(self, query):
self.model().filterByQuery(query)
def refresh(self):
self.model().refresh(True)
def setModel(self, model):
super().setModel(model)
self.horizontalHeader().sortIndicatorChanged.disconnect()
self.setSortingEnabled(True)
self.model().columnCountChanged.connect(self._cb_column_count_changed)
model.rowsUpdated.connect(self._cb_rows_updated)
model.rowsReordered.connect(self._cb_rows_reordered)

View file

@ -0,0 +1,53 @@
from PyQt5 import Qt, QtCore
from PyQt5.QtGui import QRegion
from PyQt5.QtWidgets import QItemDelegate, QAbstractItemView, QPushButton, QWidget, QVBoxLayout, QSizePolicy
from PyQt5.QtCore import pyqtSignal
class UpDownButtonDelegate(QItemDelegate):
clicked = pyqtSignal(int, QtCore.QModelIndex)
UP=-1
DOWN=1
def paint(self, painter, option, index):
if (
isinstance(self.parent(), QAbstractItemView)
and self.parent().model() is index.model()
):
self.parent().openPersistentEditor(index)
def createEditor(self, parent, option, index):
w = QWidget(parent)
w.setContentsMargins(0, 0, 0, 0)
w.setAutoFillBackground(True)
layout = QVBoxLayout(w)
layout.setContentsMargins(0, 0, 0, 0)
btnUp = QPushButton(parent)
btnUp.setText("")
btnUp.setFlat(True)
btnUp.clicked.connect(lambda: self._cb_button_clicked(self.UP, index))
btnDown = QPushButton(parent)
btnDown.setText("")
btnDown.setFlat(True)
btnDown.clicked.connect(lambda: self._cb_button_clicked(self.DOWN, index))
layout.addWidget(btnUp)
layout.addWidget(btnDown)
return w
def _cb_button_clicked(self, action, idx):
self.clicked.emit(action, idx)
def updateEditorGeometry(self, editor, option, index):
rect = QtCore.QRect(option.rect)
minWidth = editor.minimumSizeHint().width()
if rect.width() < minWidth:
rect.setWidth(minWidth)
editor.setGeometry(rect)
# create a new mask based on the option rectangle, then apply it
mask = QRegion(0, 0, option.rect.width(), option.rect.height())
editor.setProperty('offMask', mask)
editor.setMask(mask)

View file

@ -0,0 +1,266 @@
import sys
import time
import os
import os.path
import json
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from PyQt5.QtCore import QCoreApplication as QC
from opensnitch.config import Config
from opensnitch.nodes import Nodes
from opensnitch.dialogs.firewall_rule import FwRuleDialog
from opensnitch import ui_pb2
import opensnitch.firewall as Fw
import opensnitch.firewall.profiles as FwProfiles
DIALOG_UI_PATH = "%s/../res/firewall.ui" % os.path.dirname(sys.modules[__name__].__file__)
class FirewallDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
LOG_TAG = "[fw dialog]"
COMBO_IN = 0
COMBO_OUT = 1
_notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
def __init__(self, parent=None, appicon=None, node=None):
QtWidgets.QDialog.__init__(self, parent)
self.setupUi(self)
self.setWindowIcon(appicon)
self.appicon = appicon
# TODO: profiles are ready to be used. They need to be tested, and
# create some default profiles (home, office, public, ...)
self.comboProfile.setVisible(False)
self.lblProfile.setVisible(False)
self.secHighIcon = QtGui.QIcon.fromTheme("security-high")
self.secMediumIcon = QtGui.QIcon.fromTheme("security-medium")
self.secLowIcon = QtGui.QIcon.fromTheme("security-low")
self.lblStatusIcon.setPixmap( self.secHighIcon.pixmap(96,96) );
self._fwrule_dialog = FwRuleDialog(appicon=self.appicon)
self._cfg = Config.get()
self._fw = Fw.Firewall.instance()
self._nodes = Nodes.instance()
self._fw_profiles = {}
self._notification_callback.connect(self._cb_notification_callback)
self._notifications_sent = {}
self._nodes.nodesUpdated.connect(self._cb_nodes_updated)
self.cmdNewRule.clicked.connect(self._cb_new_rule_clicked)
self.cmdExcludeService.clicked.connect(self._cb_exclude_service_clicked)
self.comboInput.currentIndexChanged.connect(lambda: self._cb_combo_policy_changed(self.COMBO_IN))
self.comboOutput.currentIndexChanged.connect(lambda: self._cb_combo_policy_changed(self.COMBO_OUT))
self.comboProfile.currentIndexChanged.connect(self._cb_combo_profile_changed)
self.sliderFwEnable.valueChanged.connect(self._cb_enable_fw_changed)
self.cmdClose.clicked.connect(self._cb_close_clicked)
@QtCore.pyqtSlot(ui_pb2.NotificationReply)
def _cb_notification_callback(self, reply):
if reply.id in self._notifications_sent:
if reply.code == ui_pb2.OK:
rep = self._notifications_sent[reply.id]
self._set_status_successful(QC.translate("firewall", "Configuration applied."))
else:
self._set_status_error(QC.translate("firewall", "Error: {0}").format(reply.data))
del self._notifications_sent[reply.id]
else:
print(self.LOG_TAG, "unknown notification:", reply)
@QtCore.pyqtSlot(int)
def _cb_nodes_updated(self, total):
self._check_fw_status()
def _cb_combo_profile_changed(self, idx):
combo_profile = self._fw_profiles[idx]
json_profile = json.dumps(list(combo_profile.values())[0]['Profile'])
for addr in self._nodes.get():
fwcfg = self._nodes.get_node(addr)['firewall']
ok, err = self._fw.apply_profile(addr, json_profile)
if ok:
self.send_notification(addr, fwcfg)
else:
self._set_status_error(QC.translate("firewall", "error adding profile extra rules:", err))
def _cb_combo_policy_changed(self, combo):
wantedProfile = FwProfiles.ProfileAcceptInput.value
if combo == self.COMBO_OUT:
wantedProfile = FwProfiles.ProfileAcceptOutput.value
if self.comboOutput.currentIndex() == 1:
wantedProfile = FwProfiles.ProfileDropOutput.value
else:
if self.comboInput.currentIndex() == 1:
wantedProfile = FwProfiles.ProfileDropInput.value
for addr in self._nodes.get():
fwcfg = self._nodes.get_node(addr)['firewall']
json_profile = json.dumps(wantedProfile)
ok, err = self._fw.apply_profile(addr, json_profile)
if ok:
self.send_notification(addr, fwcfg)
else:
self._set_status_error(QC.translate("firewall", "Policy not applied: {0}".format(err)))
def _cb_new_rule_clicked(self):
self.new_rule()
def _cb_exclude_service_clicked(self):
self.exclude_service()
def _cb_enable_fw_changed(self, enable):
if enable:
self._set_status_message(QC.translate("firewall", "Enabling firewall..."))
else:
self._set_status_message(QC.translate("firewall", "Disabling firewall..."))
for addr in self._nodes.get():
fwcfg = self._nodes.get_node(addr)['firewall']
fwcfg.Enabled = True if enable else False
self.send_notification(addr, fwcfg)
self.lblStatusIcon.setEnabled(enable)
self.policiesBox.setEnabled(enable)
time.sleep(1)
def _cb_close_clicked(self):
self._close()
def _load_nodes(self):
self._nodes = self._nodes.get()
def _close(self):
self.hide()
def showEvent(self, event):
super(FirewallDialog, self).showEvent(event)
self._reset_fields()
self._check_fw_status()
self._fw_profiles = FwProfiles.Profiles.load_predefined_profiles()
self.comboProfile.blockSignals(True)
for pr in self._fw_profiles:
self.comboProfile.addItem([pr[k] for k in pr][0]['Name'])
self.comboProfile.blockSignals(False)
def send_notification(self, node_addr, fw_config):
self._set_status_message(QC.translate("firewall", "Applying changes..."))
nid, notif = self._nodes.reload_fw(node_addr, fw_config, self._notification_callback)
self._notifications_sent[nid] = {'addr': node_addr, 'notif': notif}
def _check_fw_status(self):
self.lblFwStatus.setText("")
self.sliderFwEnable.blockSignals(True)
self.comboInput.blockSignals(True)
self.comboOutput.blockSignals(True)
self.comboProfile.blockSignals(True)
if self._nodes.count() == 0:
self._disable_widgets()
return
# TODO: handle nodes' firewall properly
enableFw = False
for addr in self._nodes.get():
self._fwConfig = self._nodes.get_node(addr)['firewall']
enableFw |= self._fwConfig.Enabled
n = self._nodes.get_node(addr)
j = json.loads(n['data'].config)
if j['Firewall'] == "iptables":
self._disable_widgets()
self.lblFwStatus.setText(
QC.translate("firewall",
"OpenSnitch is using 'iptables' as firewall, but it's not configurable from the GUI.\n"
"Set 'Firewall' option to 'nftables' in /etc/opensnitchd/default-config.json \n"
"if you want to configure firewall rules from the GUI."
))
return
if n['data'].systemFirewall.Version == 0:
self._disable_widgets()
self.lblFwStatus.setText(
QC.translate("firewall", "<html>The firewall configuration is outdated,\n"
"you need to update it to the new format: <a href=\""+ Config.HELP_SYS_RULES_URL + "\">learn more</a>"
"</html>"
))
return
# XXX: Here we loop twice over the chains. We could have 1 loop.
pol_in = self._fw.chains.get_policy(addr, Fw.Hooks.INPUT.value)
pol_out = self._fw.chains.get_policy(addr, Fw.Hooks.OUTPUT.value)
if pol_in != None:
self.comboInput.setCurrentIndex(
Fw.Policy.values().index(pol_in)
)
else:
self._set_status_error(QC.translate("firewall", "Error getting INPUT chain policy"))
self._disable_widgets()
if pol_out != None:
self.comboOutput.setCurrentIndex(
Fw.Policy.values().index(pol_out)
)
else:
self._set_status_error(QC.translate("firewall", "Error getting OUTPUT chain policy"))
self._disable_widgets()
# some nodes may have the firewall disabled whilst other enabled
#if not enableFw:
# self.lblFwStatus(QC.translate("firewall", "Some nodes have the firewall disabled"))
self._disable_widgets(False)
self.lblStatusIcon.setEnabled(enableFw)
self.sliderFwEnable.setValue(enableFw)
self.sliderFwEnable.blockSignals(False)
self.comboInput.blockSignals(False)
self.comboOutput.blockSignals(False)
self.comboProfile.blockSignals(False)
def load_rule(self, addr, uuid):
self._fwrule_dialog.load(addr, uuid)
def new_rule(self):
self._fwrule_dialog.new()
def exclude_service(self):
self._fwrule_dialog.exclude_service()
def _set_status_error(self, msg):
self.statusLabel.show()
self.statusLabel.setStyleSheet('color: red')
self.statusLabel.setText(msg)
def _set_status_successful(self, msg):
self.statusLabel.show()
self.statusLabel.setStyleSheet('color: green')
self.statusLabel.setText(msg)
def _set_status_message(self, msg):
self.statusLabel.show()
self.statusLabel.setStyleSheet('color: darkorange')
self.statusLabel.setText(msg)
def _reset_status_message(self):
self.statusLabel.setText("")
self.statusLabel.hide()
def _reset_fields(self):
self._reset_status_message()
def _disable_widgets(self, disable=True):
self.sliderFwEnable.setEnabled(not disable)
self.comboInput.setEnabled(not disable)
self.comboOutput.setEnabled(not disable)
self.cmdNewRule.setEnabled(not disable)
self.cmdExcludeService.setEnabled(not disable)

View file

@ -0,0 +1,401 @@
import sys
import os
import os.path
from PyQt5 import QtCore, uic, QtWidgets
from PyQt5.QtCore import QCoreApplication as QC
from opensnitch.nodes import Nodes
from opensnitch.utils import NetworkServices, QuickHelp
from opensnitch import ui_pb2
import opensnitch.firewall as Fw
from opensnitch.firewall.utils import Utils as FwUtils
DIALOG_UI_PATH = "%s/../res/firewall_rule.ui" % os.path.dirname(sys.modules[__name__].__file__)
class FwRuleDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
LOG_TAG = "[fw rule dialog]"
ACTION_IDX_DENY = 0
ACTION_IDX_ALLOW = 1
IN = 0
OUT = 1
OP_NEW = 0
OP_SAVE = 1
OP_DELETE = 2
FORM_TYPE_SIMPLE = 0
FORM_TYPE_EXCLUDE_SERVICE = 1
FORM_TYPE = FORM_TYPE_SIMPLE
_notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
def __init__(self, parent=None, appicon=None):
QtWidgets.QDialog.__init__(self, parent)
self.setupUi(self)
self.setWindowIcon(appicon)
# Other interesting flags: QtCore.Qt.Tool | QtCore.Qt.BypassWindowManagerHint
self._fw = Fw.Firewall.instance()
self._nodes = Nodes.instance()
self._notification_callback.connect(self._cb_notification_callback)
self._notifications_sent = {}
self.uuid = ""
self.simple_port_idx = None
self._nodes.nodesUpdated.connect(self._cb_nodes_updated)
self.cmdClose.clicked.connect(self._cb_close_clicked)
self.cmdAdd.clicked.connect(self._cb_add_clicked)
self.cmdSave.clicked.connect(self._cb_save_clicked)
self.cmdDelete.clicked.connect(self._cb_delete_clicked)
self.helpButton.clicked.connect(self._cb_help_button_clicked)
self.net_srv = NetworkServices()
self.comboPorts.addItems(self.net_srv.to_array())
self.comboPorts.currentIndexChanged.connect(self._cb_combo_ports_index_changed)
def showEvent(self, event):
super(FwRuleDialog, self).showEvent(event)
self._reset_fields()
if FwUtils.isProtobufSupported() == False:
self._disable_controls()
self._disable_buttons()
self._set_status_error(
QC.translate(
"firewall",
"Your protobuf version is incompatible, you need to install protobuf 3.8.0 or superior\n(pip3 install --ignore-installed protobuf==3.8.0"
)
)
return
self._load_nodes()
@QtCore.pyqtSlot(ui_pb2.NotificationReply)
def _cb_notification_callback(self, reply):
self._enable_buttons()
if reply.id in self._notifications_sent:
if reply.code == ui_pb2.OK:
rep = self._notifications_sent[reply.id]
if 'operation' in rep and rep['operation'] == self.OP_DELETE:
self.tabWidget.setDisabled(True)
self._set_status_successful(QC.translate("firewall", "Rule deleted"))
self._disable_controls()
return
self._set_status_successful(QC.translate("firewall", "Rule added"))
else:
self._set_status_error(QC.translate("firewall", "Error: {0}").format(reply.data))
del self._notifications_sent[reply.id]
@QtCore.pyqtSlot(int)
def _cb_nodes_updated(self, total):
self.tabWidget.setDisabled(True if total == 0 else False)
def closeEvent(self, e):
self._close()
def _cb_combo_ports_index_changed(self, idx):
self.simple_port_idx = idx
def _cb_help_button_clicked(self):
QuickHelp.show(
QC.translate("firewall",
"You can use ',' or '-' to specify multiple ports or a port range:<br>" \
"22 or 22,443 or 50000-60000"
)
)
def _cb_close_clicked(self):
self._close()
def _cb_delete_clicked(self):
node_addr, node, chain = self.form_to_protobuf()
if node_addr == None:
self._set_status_error(QC.translate("firewall", "Invalid rule, review parameters"))
return
self._set_status_message(QC.translate("firewall", "Deleting rule, wait"))
ok, fw_config = self._fw.delete_rule(node_addr, self.uuid)
if not ok:
self._set_status_error(QC.translate("firewall", "Error updating rule"))
return
self.send_notification(node_addr, node['firewall'], self.OP_DELETE)
def _cb_save_clicked(self):
node_addr, node, chain = self.form_to_protobuf()
if node_addr == None:
self._set_status_error(QC.translate("firewall", "Invalid rule, review parameters"))
return
self._set_status_message(QC.translate("firewall", "Adding rule, wait"))
ok, err = self._fw.update_rule(node_addr, self.uuid, chain)
if not ok:
self._set_status_error(QC.translate("firewall", "Error updating rule: {0}".format(err)))
return
self._enable_buttons(False)
self.send_notification(node_addr, node['firewall'], self.OP_SAVE)
def _cb_add_clicked(self):
node_addr, node, chain = self.form_to_protobuf()
if node_addr == None:
self._set_status_error(QC.translate("firewall", "Invalid rule, review parameters"))
return
ok, err = self._fw.insert_rule(node_addr, chain)
if not ok:
self._set_status_error(QC.translate("firewall", "Error adding rule: {0}".format(err)))
return
self._set_status_message(QC.translate("firewall", "Adding rule, wait"))
self._enable_buttons(False)
self.send_notification(node_addr, node['firewall'], self.OP_NEW)
def _close(self):
self.hide()
def _load_nodes(self):
self.comboNodes.clear()
self._node_list = self._nodes.get()
for addr in self._node_list:
self.comboNodes.addItem(addr)
if len(self._node_list) == 0:
self.tabWidget.setDisabled(True)
def load(self, addr, uuid):
self.show()
self.FORM_TYPE = self.FORM_TYPE_SIMPLE
self.setWindowTitle(QC.translate("firewall", "Firewall rule"))
self.cmdDelete.setVisible(True)
self.cmdSave.setVisible(True)
self.cmdAdd.setVisible(False)
self.checkEnable.setVisible(True)
self.checkEnable.setEnabled(True)
self.checkEnable.setChecked(True)
self.comboOperator.setVisible(True)
self.comboOperator.setCurrentIndex(0)
self.frameDirection.setVisible(True)
self.frameAction.setVisible(True)
self.cmdSave.setVisible(False)
self.cmdDelete.setVisible(False)
self.cmdAdd.setVisible(True)
self.cmdSave.setVisible(True)
self.cmdAdd.setVisible(False)
self.cmdDelete.setVisible(True)
self.show()
self.uuid = uuid
node, rule = self._fw.get_rule_by_uuid(uuid)
# TODO: implement complex rules
if rule == None or \
(rule.Hook.lower() != Fw.Hooks.INPUT.value and rule.Hook.lower() != Fw.Hooks.OUTPUT.value):
hook = "invalid" if rule == None else rule.Hook
self._set_status_error(QC.translate("firewall", "Rule type ({0}) not supported yet".format(hook)))
self._disable_controls()
return
if len(rule.Rules[0].Expressions) > 1:
self._set_status_error(QC.translate("firewall", "Complex rules types not supported yet"))
self._disable_controls()
return
self.checkEnable.setChecked(rule.Rules[0].Enabled)
self.lineDescription.setText(rule.Rules[0].Description)
# TODO: support complex expressions: tcp dport 22 ip daddr != 127.0.0.1
isNotSupported = True
for exp in rule.Rules[0].Expressions:
if Fw.Utils.isExprPort(exp.Statement.Name):
try:
self.comboPorts.setCurrentIndex(
self.net_srv.index_by_port(exp.Statement.Values[0].Value)
)
except:
self.comboPorts.setCurrentText(exp.Statement.Values[0].Value)
op = Fw.Operator.EQUAL.value if exp.Statement.Op == "" else exp.Statement.Op
self.comboOperator.setCurrentIndex(
Fw.Operator.values().index(op)
)
isNotSupported = False
break
if isNotSupported:
self._set_status_error(QC.translate("firewall", "Only port rules can be edited for now."))
self._disable_controls()
return
if rule.Hook.lower() == Fw.Hooks.INPUT.value:
self.comboDirection.setCurrentIndex(0)
else:
self.comboDirection.setCurrentIndex(1)
# TODO: changing the direction of an existed rule needs work, it causes
# some nasty effects. Disabled for now.
self.comboDirection.setEnabled(False)
self.comboVerdict.setCurrentIndex(
Fw.Verdicts.values().index(
rule.Rules[0].Target.lower()
)-1
)
def new(self):
self.show()
self.FORM_TYPE = self.FORM_TYPE_SIMPLE
self.setWindowTitle(QC.translate("firewall", "Firewall rule"))
self.cmdDelete.setVisible(False)
self.cmdSave.setVisible(False)
self.cmdAdd.setVisible(True)
self.checkEnable.setVisible(True)
self.checkEnable.setEnabled(True)
self.checkEnable.setChecked(True)
self.comboOperator.setVisible(True)
self.comboOperator.setCurrentIndex(0)
self.frameDirection.setVisible(True)
self.frameAction.setVisible(True)
self.cmdSave.setVisible(False)
self.cmdDelete.setVisible(False)
self.cmdAdd.setVisible(True)
def exclude_service(self):
self.show()
self.FORM_TYPE = self.FORM_TYPE_EXCLUDE_SERVICE
self.setWindowTitle(QC.translate("firewall", "Exclude service"))
self.cmdDelete.setVisible(False)
self.cmdSave.setVisible(False)
self.cmdAdd.setVisible(True)
self.checkEnable.setVisible(False)
self.checkEnable.setEnabled(True)
self.comboOperator.setVisible(False)
self.comboOperator.setCurrentIndex(0)
self.frameDirection.setVisible(False)
self.frameAction.setVisible(False)
self.checkEnable.setChecked(True)
def form_to_protobuf(self):
"""Transform form widgets to protouf struct
"""
chain = Fw.ChainFilter.input()
# output rules must be placed under mangle table and before
# interception rules. Otherwise we'd intercept them.
if self.comboDirection.currentIndex() == self.OUT or self.FORM_TYPE == self.FORM_TYPE_EXCLUDE_SERVICE:
chain = Fw.ChainMangle.output()
rule = Fw.Rules.new(
enabled=self.checkEnable.isChecked(),
_uuid=self.uuid,
description=self.lineDescription.text(),
target=Fw.Verdicts.values()[self.comboVerdict.currentIndex()+1] # index 0 is ""
)
if self.comboPorts.currentText() != "":
portValue = "0"
try:
if "," in self.comboPorts.currentText() or "-" in self.comboPorts.currentText():
raise ValueError("port entered is multiport or a port range")
if self.simple_port_idx == None:
raise ValueError("user didn't select a port from the list")
portValue = self.net_srv.port_by_index(
self.comboPorts.currentIndex()
)
except:
portValue = self.comboPorts.currentText()
if portValue == "" or portValue == "0":
return
# TODO: should we add a TCP/UDP port?
portValue = portValue.replace(" ", "")
exprs = Fw.Expr.new(
Fw.Operator.values()[self.comboOperator.currentIndex()],
Fw.Statements.TCP.value,
[(Fw.Statements.DPORT.value, portValue)]
)
rule.Expressions.append(exprs)
chain.Rules.append(rule)
node_addr = self.comboNodes.currentText()
node = self._nodes.get_node(node_addr)
return node_addr, node, chain
def send_notification(self, node_addr, fw_config, op):
nid, notif = self._nodes.reload_fw(node_addr, fw_config, self._notification_callback)
self._notifications_sent[nid] = {'addr': node_addr, 'operation': op, 'notif': notif}
def _set_status_error(self, msg):
self.statusLabel.show()
self.statusLabel.setStyleSheet('color: red')
self.statusLabel.setText(msg)
def _set_status_successful(self, msg):
self.statusLabel.show()
self.statusLabel.setStyleSheet('color: green')
self.statusLabel.setText(msg)
def _set_status_message(self, msg):
self.statusLabel.show()
self.statusLabel.setStyleSheet('color: darkorange')
self.statusLabel.setText(msg)
def _reset_status_message(self):
self.statusLabel.setText("")
self.statusLabel.hide()
def _reset_fields(self):
self.FORM_TYPE = self.FORM_TYPE_SIMPLE
self.setWindowTitle(QC.translate("firewall", "Firewall rule"))
self.cmdDelete.setVisible(False)
self.cmdSave.setVisible(False)
self.cmdAdd.setVisible(True)
self.checkEnable.setVisible(True)
self.checkEnable.setEnabled(True)
self.checkEnable.setChecked(True)
self.comboOperator.setVisible(True)
self.comboOperator.setCurrentIndex(0)
self.frameDirection.setVisible(True)
self.frameAction.setVisible(True)
self._reset_status_message()
self._enable_buttons()
self.tabWidget.setDisabled(False)
self.lineDescription.setText("")
self.comboPorts.setCurrentText("")
self.comboDirection.setCurrentIndex(0)
self.comboDirection.setEnabled(True)
self.comboVerdict.setCurrentIndex(0)
self.uuid = ""
def _enable_buttons(self, enable=True):
"""Disable add/save buttons until a response is received from the daemon.
"""
self.cmdSave.setEnabled(enable)
self.cmdAdd.setEnabled(enable)
self.cmdDelete.setEnabled(enable)
def _disable_buttons(self, disabled=True):
self.cmdSave.setDisabled(disabled)
self.cmdAdd.setDisabled(disabled)
self.cmdDelete.setDisabled(disabled)
def _disable_controls(self):
self._disable_buttons()
self.tabWidget.setDisabled(True)

View file

@ -12,10 +12,13 @@ from opensnitch import ui_pb2
from opensnitch.config import Config
from opensnitch.version import version
from opensnitch.nodes import Nodes
from opensnitch.firewall import Firewall
from opensnitch.dialogs.firewall import FirewallDialog
from opensnitch.dialogs.preferences import PreferencesDialog
from opensnitch.dialogs.ruleseditor import RulesEditorDialog
from opensnitch.dialogs.processdetails import ProcessDetailsDialog
from opensnitch.customwidgets.main import ColorizedDelegate, ConnectionsTableModel
from opensnitch.customwidgets.firewalltableview import FirewallTableModel
from opensnitch.customwidgets.generictableview import GenericTableModel
from opensnitch.customwidgets.addresstablemodel import AddressTableModel
from opensnitch.utils import Message, QuickHelp, AsnDB
@ -70,15 +73,19 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
TAB_ADDRS = 5
TAB_PORTS = 6
TAB_USERS = 7
TAB_FIREWALL = 8
# row of entries
# tree's top level items
RULES_TREE_APPS = 0
RULES_TREE_NODES = 1
RULES_TREE_FIREWALL = 2
RULES_TREE_PERMANENT = 0
RULES_TREE_TEMPORARY = 1
RULES_COMBO_PERMANENT = 1
RULES_COMBO_TEMPORARY = 2
RULES_COMBO_FW = 3
RULES_TYPE_PERMANENT = 0
RULES_TYPE_TEMPORARY = 1
@ -86,6 +93,10 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
FILTER_TREE_APPS = 0
FILTER_TREE_NODES = 3
FILTER_TREE_FW_NODE = 0
FILTER_TREE_FW_TABLE = 1
FILTER_TREE_FW_CHAIN = 2
# FIXME: don't translate, used only for default argument on _update_status_label
FIREWALL_DISABLED = "Disabled"
@ -99,7 +110,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
TAB_PROCS: False,
TAB_ADDRS: False,
TAB_PORTS: False,
TAB_USERS: False
TAB_USERS: False,
TAB_FIREWALL: False
}
# restore scrollbar position when going back from a detail view
LAST_SCROLL_VALUE = None
@ -194,6 +206,38 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"last_order_to": 0,
"rows_selected": False
},
TAB_FIREWALL: {
"name": "firewall",
"label": None,
"cmd": None,
"cmdCleanStats": None,
"view": None,
"filterLine": None,
"model": None,
"delegate": {
Config.ACTION_DENY: RED,
Config.ACTION_DROP: RED,
Config.ACTION_STOP: RED,
"DROP": RED,
"ACCEPT": GREEN,
Config.ACTION_REJECT: PURPLE,
Config.ACTION_RETURN: PURPLE,
Config.ACTION_ACCEPT: GREEN,
Config.ACTION_JUMP: GREEN,
Config.ACTION_MASQUERADE: GREEN,
Config.ACTION_SNAT: GREEN,
Config.ACTION_DNAT: GREEN,
Config.ACTION_TPROXY: GREEN,
"True": GREEN,
"False": RED,
'alignment': QtCore.Qt.AlignCenter | QtCore.Qt.AlignHCenter
},
"display_fields": "*",
"header_labels": [],
"last_order_by": "2",
"last_order_to": 0,
"rows_selected": False
},
TAB_HOSTS: {
"name": "hosts",
"label": None,
@ -319,6 +363,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._cfg = Config.get()
self._nodes = Nodes.instance()
self._fw = Firewall().instance()
self._fw.rules.rulesUpdated.connect(self._cb_fw_rules_updated)
# TODO: allow to display multiples dialogs
self._proc_details_dialog = ProcessDetailsDialog(appicon=appicon)
@ -326,6 +372,10 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.prevButton.setVisible(False)
self.nextButton.setVisible(False)
self.fwTable.setVisible(False)
self.rulesTable.setVisible(True)
self.daemon_connected = False
# skip table updates if a context menu is active
self._context_menu_active = False
@ -337,6 +387,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._stats = None
self._notifications_sent = {}
self._fw_dialog = FirewallDialog(appicon=appicon)
self._prefs_dialog = PreferencesDialog(appicon=appicon)
self._rules_dialog = RulesEditorDialog(appicon=appicon)
self._prefs_dialog.saved.connect(self._on_settings_saved)
@ -347,9 +398,13 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.nodeLabel.setStyleSheet('color: green;font-size:12pt; font-weight:600;')
self.rulesSplitter.setStretchFactor(0,0)
self.rulesSplitter.setStretchFactor(1,2)
self.rulesTreePanel.resizeColumnToContents(0)
self.rulesTreePanel.resizeColumnToContents(1)
self.rulesTreePanel.itemExpanded.connect(self._cb_rules_tree_item_expanded)
self.startButton.clicked.connect(self._cb_start_clicked)
self.prefsButton.clicked.connect(self._cb_prefs_clicked)
self.fwButton.clicked.connect(lambda: self._fw_dialog.show())
self.saveButton.clicked.connect(self._on_save_clicked)
self.comboAction.currentIndexChanged.connect(self._cb_combo_action_changed)
self.limitCombo.currentIndexChanged.connect(self._cb_limit_combo_changed)
@ -357,6 +412,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.delRuleButton.clicked.connect(self._cb_del_rule_clicked)
self.rulesSplitter.splitterMoved.connect(self._cb_rules_splitter_moved)
self.rulesTreePanel.itemClicked.connect(self._cb_rules_tree_item_clicked)
self.rulesTreePanel.itemDoubleClicked.connect(self._cb_rules_tree_item_double_clicked)
self.enableRuleCheck.clicked.connect(self._cb_enable_rule_toggled)
self.editRuleButton.clicked.connect(self._cb_edit_rule_clicked)
self.newRuleButton.clicked.connect(self._cb_new_rule_clicked)
@ -443,6 +499,13 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
delegate=self.TABLES[self.TAB_RULES]['delegate'],
order_by="2",
sort_direction=self.SORT_ORDER[0])
self.TABLES[self.TAB_FIREWALL]['view'] = self._setup_table(QtWidgets.QTableView,
self.fwTable, "firewall",
model=FirewallTableModel("firewall"),
verticalScrollBar=None,
delegate=self.TABLES[self.TAB_FIREWALL]['delegate'],
order_by="2",
sort_direction=self.SORT_ORDER[0])
self.TABLES[self.TAB_HOSTS]['view'] = self._setup_table(QtWidgets.QTableView,
self.hostsTable, "hosts",
model=GenericTableModel("hosts", self.TABLES[self.TAB_HOSTS]['header_labels']),
@ -525,18 +588,22 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.TABLES[self.TAB_RULES]['view'].setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.TABLES[self.TAB_RULES]['view'].customContextMenuRequested.connect(self._cb_table_context_menu)
for idx in range(1,8):
self.TABLES[idx]['cmd'].hide()
self.TABLES[idx]['cmd'].setVisible(False)
self.TABLES[idx]['cmd'].clicked.connect(lambda: self._cb_cmd_back_clicked(idx))
for idx in range(1,9):
if self.TABLES[idx]['cmd'] != None:
self.TABLES[idx]['cmd'].hide()
self.TABLES[idx]['cmd'].setVisible(False)
self.TABLES[idx]['cmd'].clicked.connect(lambda: self._cb_cmd_back_clicked(idx))
if self.TABLES[idx]['cmdCleanStats'] != None:
self.TABLES[idx]['cmdCleanStats'].clicked.connect(lambda: self._cb_clean_sql_clicked(idx))
self.TABLES[idx]['label'].setStyleSheet('color: blue; font-size:9pt; font-weight:600;')
self.TABLES[idx]['label'].setVisible(False)
if self.TABLES[idx]['label'] != None:
self.TABLES[idx]['label'].setStyleSheet('color: blue; font-size:9pt; font-weight:600;')
self.TABLES[idx]['label'].setVisible(False)
self.TABLES[idx]['view'].doubleClicked.connect(self._cb_table_double_clicked)
self.TABLES[idx]['view'].selectionModel().selectionChanged.connect(self._cb_table_selection_changed)
self.TABLES[idx]['view'].installEventFilter(self)
self.TABLES[self.TAB_FIREWALL]['view'].rowsReordered.connect(self._cb_fw_table_rows_reordered)
self._load_settings()
self._tables = ( \
@ -563,6 +630,17 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.iconStart = QtGui.QIcon().fromTheme("media-playback-start")
self.iconPause = QtGui.QIcon().fromTheme("media-playback-pause")
self.fwTreeEdit = QtWidgets.QPushButton()
self.fwTreeEdit.setIcon(QtGui.QIcon().fromTheme("preferences-desktop"))
self.fwTreeEdit.autoFillBackground = True
self.fwTreeEdit.setFlat(True)
self.fwTreeEdit.setSizePolicy(
QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
)
if QtGui.QIcon().hasThemeIcon("preferences-desktop") == False:
self.fwTreeEdit.setText("+")
self.fwTreeEdit.clicked.connect(self._cb_tree_edit_firewall_clicked)
if QtGui.QIcon.hasThemeIcon("document-new") == False:
self._configure_buttons_icons()
@ -586,6 +664,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.nodeLabel.setText(self._address)
self._load_settings()
self._add_rulesTree_nodes()
self._add_rulesTree_fw_chains()
self.setWindowTitle(window_title)
self._refresh_active_table()
@ -656,11 +735,12 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.comboRulesFilter.setVisible(rulesSizes[0] == 0)
else:
w = self.rulesSplitter.width()
self.rulesSplitter.setSizes([int(w/4), int(w/2)])
self.rulesSplitter.setSizes([int(w/3), int(w/2)])
self._restore_details_view_columns(self.eventsTable.horizontalHeader(), Config.STATS_GENERAL_COL_STATE)
self._restore_details_view_columns(self.nodesTable.horizontalHeader(), Config.STATS_NODES_COL_STATE)
self._restore_details_view_columns(self.rulesTable.horizontalHeader(), Config.STATS_RULES_COL_STATE)
self._restore_details_view_columns(self.fwTable.horizontalHeader(), Config.STATS_FW_COL_STATE)
rulesTreeNodes_expanded = self._cfg.getBool(Config.STATS_RULES_TREE_EXPANDED_1)
if rulesTreeNodes_expanded != None:
@ -686,6 +766,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._cfg.setSettings(Config.STATS_NODES_COL_STATE, nodesHeader.saveState())
rulesHeader = self.rulesTable.horizontalHeader()
self._cfg.setSettings(Config.STATS_RULES_COL_STATE, rulesHeader.saveState())
fwHeader = self.fwTable.horizontalHeader()
self._cfg.setSettings(Config.STATS_FW_COL_STATE, fwHeader.saveState())
rules_tree_apps = self._get_rulesTree_item(self.RULES_TREE_APPS)
if rules_tree_apps != None:
@ -702,6 +784,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
# https://stackoverflow.com/questions/40225270/copy-paste-multiple-items-from-qtableview-in-pyqt4
def _copy_selected_rows(self):
cur_idx = self.tabWidget.currentIndex()
if self.tabWidget.currentIndex() == self.TAB_RULES and self.fwTable.isVisible():
cur_idx = self.TAB_FIREWALL
selection = self.TABLES[cur_idx]['view'].selectedIndexes()
if selection:
rows = sorted(index.row() for index in selection)
@ -901,10 +985,22 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
if ret == QtWidgets.QMessageBox.Cancel:
return False
for idx in selection:
name = model.index(idx.row(), self.COL_R_NAME).data()
node = model.index(idx.row(), self.COL_R_NODE).data()
self._del_rule(name, node)
if self.tabWidget.currentIndex() == self.TAB_RULES and self.fwTable.isVisible():
do_refresh = False
for idx in selection:
uuid = model.index(idx.row(), 1).data()
node = model.index(idx.row(), 2).data()
ok, fw_config = self._fw.delete_rule(node, uuid)
do_refresh |= ok
if do_refresh:
nid, noti = self._nodes.reload_fw(node, fw_config, self._notification_callback)
self._notifications_sent[nid] = noti
else:
for idx in selection:
name = model.index(idx.row(), self.COL_R_NAME).data()
node = model.index(idx.row(), self.COL_R_NODE).data()
self._del_rule(name, node)
def _table_menu_edit(self, cur_idx, model, selection):
@ -920,6 +1016,15 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._rules_dialog.edit_rule(records, node)
break
def _cb_fw_rules_updated(self):
self._add_rulesTree_fw_chains()
@QtCore.pyqtSlot(str)
def _cb_fw_table_rows_reordered(self, node_addr):
node = self._nodes.get_node(node_addr)
nid, notif = self._nodes.reload_fw(node_addr, node['firewall'], self._notification_callback)
self._notifications_sent[nid] = {'addr': node_addr, 'notif': notif}
# ignore updates while the user is using the scrollbar.
def _cb_scrollbar_pressed(self):
self.scrollbar_active = True
@ -927,6 +1032,9 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
def _cb_scrollbar_released(self):
self.scrollbar_active = False
def _cb_tree_edit_firewall_clicked(self):
self._fw_dialog.show()
def _cb_proc_details_clicked(self):
table = self._tables[self.tabWidget.currentIndex()]
nrows = table.model().rowCount()
@ -1019,6 +1127,9 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
return
elif cur_idx == StatsDialog.TAB_NODES:
qstr = self._get_nodes_filter_query(model.query().lastQuery(), text)
elif cur_idx == StatsDialog.TAB_RULES and self.fwTable.isVisible():
self.TABLES[self.TAB_FIREWALL]['view'].filterByQuery(text)
return
elif self.IN_DETAIL_VIEW[cur_idx] == True:
qstr = self._get_indetail_filter_query(model.query().lastQuery(), text)
else:
@ -1161,6 +1272,14 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
cur_idx = self.tabWidget.currentIndex()
if self.IN_DETAIL_VIEW[cur_idx]:
return
if cur_idx == self.TAB_RULES and self.fwTable.isVisible():
uuid = row.model().index(row.row(), 1).data(QtCore.Qt.UserRole+1)
addr = row.model().index(row.row(), 2).data(QtCore.Qt.UserRole+1)
self._fw_dialog.load_rule(addr, uuid)
return
self.IN_DETAIL_VIEW[cur_idx] = True
self.LAST_SELECTED_ITEM = row.model().index(row.row(), self.COL_TIME).data()
self.LAST_SCROLL_VALUE = self.TABLES[cur_idx]['view'].vScrollBar.value()
@ -1235,19 +1354,60 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._set_rules_filter(self.RULES_TREE_APPS, self.RULES_TREE_PERMANENT)
elif idx == self.RULES_COMBO_TEMPORARY:
self._set_rules_filter(self.RULES_TREE_APPS, self.RULES_TREE_TEMPORARY)
elif idx == self.RULES_COMBO_FW:
self._set_rules_filter(-1, self.RULES_TREE_FIREWALL)
def _cb_rules_tree_item_expanded(self, item):
self.rulesTreePanel.resizeColumnToContents(0)
self.rulesTreePanel.resizeColumnToContents(1)
def _cb_rules_tree_item_double_clicked(self, item, col):
# TODO: open fw chain editor
pass
def _cb_rules_tree_item_clicked(self, item, col):
"""
Event fired when the user clicks on the left panel of the rules tab
"""
item_model = self.rulesTreePanel.indexFromItem(item, col)
item_row = item_model.row()
parent = item.parent()
parent_row = -1
if parent != None:
parent_model = self.rulesTreePanel.indexFromItem(parent, col)
parent_row = parent_model.row()
node_addr = ""
fw_table = ""
self._set_rules_filter(parent_row, item_model.row(), item.text(0))
# FIXME: find a clever way of handling these options
# top level items
if parent != None:
parent_model = self.rulesTreePanel.indexFromItem(parent, 0)
parent_row = parent_model.row()
node_addr = parent_model.data()
# 1st level items: nodes, rules types
if parent.parent() != None:
parent = parent.parent()
parent_model = self.rulesTreePanel.indexFromItem(parent, 0)
item_row = self.FILTER_TREE_FW_TABLE
parent_row = self.RULES_TREE_FIREWALL
fw_table = parent_model.data()
# 2nd level items: chains
if parent.parent() != None:
parent = parent.parent()
parent_model = self.rulesTreePanel.indexFromItem(parent.parent(), 0)
item_row = self.FILTER_TREE_FW_CHAIN
parent_row = self.RULES_TREE_FIREWALL
if node_addr == None:
return
showFwTable = (parent_row == self.RULES_TREE_FIREWALL or (parent_row == -1 and item_row == self.RULES_TREE_FIREWALL))
self.fwTable.setVisible(showFwTable)
self.rulesTable.setVisible(not showFwTable)
self.rulesScrollBar.setVisible(self.rulesTable.isVisible())
self._set_rules_filter(parent_row, item_row, item.text(0), node_addr, fw_table)
def _cb_rules_splitter_moved(self, pos, index):
self.comboRulesFilter.setVisible(pos == 0)
@ -1352,6 +1512,9 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.statusLabel.setStyleSheet('color: rgb(206, 92, 0); margin: 5px')
self.startButton.setIcon(self.iconStart)
self._add_rulesTree_nodes()
self._add_rulesTree_fw_chains()
def _get_rulesTree_item(self, index):
try:
return self.rulesTreePanel.topLevelItem(index)
@ -1365,6 +1528,63 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
for n in self._nodes.get_nodes():
nodesItem.addChild(QtWidgets.QTreeWidgetItem([n]))
def _add_rulesTree_fw_chains(self):
expanded = list()
selected = None
scrollValue = self.rulesTreePanel.verticalScrollBar().value()
fwItem = self.rulesTreePanel.topLevelItem(self.RULES_TREE_FIREWALL)
it = QtWidgets.QTreeWidgetItemIterator(fwItem)
while it.value():
x = it.value()
if x.isExpanded():
expanded.append(x)
if x.isSelected():
selected = x
it += 1
self.rulesTreePanel.setAnimated(False)
fwItem.takeChildren()
self.rulesTreePanel.setItemWidget(fwItem, 1, self.fwTreeEdit)
chains = self._fw.get_chains()
for addr in chains:
# add nodes
nodeRoot = QtWidgets.QTreeWidgetItem(["{0}".format(addr)])
fwItem.addChild(nodeRoot)
for nodeChains in chains[addr]:
# exclude legacy system rules
if len(nodeChains) == 0:
continue
for cc in nodeChains:
# add tables
tableName = "{0}-{1}".format(cc.Table, cc.Family)
nodeTable = QtWidgets.QTreeWidgetItem([tableName])
chainName = "{0}-{1}".format(cc.Name, cc.Hook)
nodeChain = QtWidgets.QTreeWidgetItem([chainName, cc.Policy])
items = self.rulesTreePanel.findItems(tableName, QtCore.Qt.MatchRecursive, 0)
if len(items) == 0:
# add table
nodeTable.addChild(nodeChain)
nodeRoot.addChild(nodeTable)
else:
# add chains
node = items[0]
node.addChild(nodeChain)
for item in expanded:
items = self.rulesTreePanel.findItems(item.text(0), QtCore.Qt.MatchRecursive)
for it in items:
it.setExpanded(True)
if selected != None and selected.text(0) == it.text(0):
it.setSelected(True)
self.rulesTreePanel.verticalScrollBar().setValue(scrollValue)
self.rulesTreePanel.setAnimated(True)
self.rulesTreePanel.resizeColumnToContents(0)
self.rulesTreePanel.resizeColumnToContents(1)
expanded = None
def _clear_rows_selection(self):
cur_idx = self.tabWidget.currentIndex()
self.TABLES[cur_idx]['view'].selectionModel().reset()
@ -1418,6 +1638,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.setQuery(model, lastQuery)
def _get_active_table(self):
if self.tabWidget.currentIndex() == self.TAB_RULES and self.fwTable.isVisible():
return self.TABLES[self.TAB_FIREWALL]['view']
return self.TABLES[self.tabWidget.currentIndex()]['view']
def _set_active_widgets(self, state, label_txt=""):
@ -1489,14 +1711,13 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._set_rules_filter(parent.row(), item_m.row(), item_m.data())
def _set_rules_tab_active(self, row, cur_idx, name_idx, node_idx):
data = row.data()
self._restore_rules_tab_widgets(False)
self.comboRulesFilter.setVisible(False)
r_name = row.model().index(row.row(), name_idx).data()
node = row.model().index(row.row(), node_idx).data()
self.nodeRuleLabel.setText(node)
self.tabWidget.setCurrentIndex(cur_idx)
return r_name, node
@ -1593,13 +1814,18 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
return qstr
def _set_rules_filter(self, parent_row=-1, item_row=0, what=""):
def _set_rules_filter(self, parent_row=-1, item_row=0, what="", what1="", what2=""):
section = self.FILTER_TREE_APPS
if parent_row == -1:
self.fwTable.setVisible(item_row == self.RULES_TREE_FIREWALL)
self.rulesTable.setVisible(item_row != self.RULES_TREE_FIREWALL)
if item_row == self.RULES_TREE_NODES:
section=self.FILTER_TREE_NODES
what=""
elif item_row == self.RULES_TREE_FIREWALL:
self.TABLES[self.TAB_FIREWALL]['view'].model().filterAll()
return
else:
section=self.FILTER_TREE_APPS
what=""
@ -1615,6 +1841,18 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
elif parent_row == self.RULES_TREE_NODES:
section=self.FILTER_TREE_NODES
elif parent_row == self.RULES_TREE_FIREWALL:
if item_row == self.FILTER_TREE_FW_NODE:
self.TABLES[self.TAB_FIREWALL]['view'].filterByNode(what)
elif item_row == self.FILTER_TREE_FW_TABLE:
parm = what.split("-")
self.TABLES[self.TAB_FIREWALL]['view'].filterByTable(what1, parm[0], parm[1])
elif item_row == self.FILTER_TREE_FW_CHAIN: # + table
parm = what.split("-")
tbl = what1.split("-")
self.TABLES[self.TAB_FIREWALL]['view'].filterByChain(what2, tbl[0], tbl[1], parm[0], parm[1])
return
if section == self.FILTER_TREE_APPS:
if what == self.RULES_TYPE_TEMPORARY:
what = "WHERE r.duration != '%s'" % Config.DURATION_ALWAYS
@ -1934,8 +2172,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
if verticalScrollBar != None:
tableWidget.setVerticalScrollBar(verticalScrollBar)
tableWidget.vScrollBar.sliderPressed.connect(self._cb_scrollbar_pressed)
tableWidget.vScrollBar.sliderReleased.connect(self._cb_scrollbar_released)
tableWidget.verticalScrollBar().sliderPressed.connect(self._cb_scrollbar_pressed)
tableWidget.verticalScrollBar().sliderReleased.connect(self._cb_scrollbar_released)
self.setQuery(model, "SELECT " + fields + " FROM " + table_name + group_by + " ORDER BY " + order_by + " " + sort_direction + limit)
tableWidget.setModel(model)

View file

@ -0,0 +1,180 @@
from PyQt5.QtCore import QObject, QCoreApplication as QC
from google.protobuf import json_format
from opensnitch import ui_pb2
from opensnitch.nodes import Nodes
from .enums import *
from .rules import *
from .chains import *
from .utils import Utils
from .exprs import *
from .profiles import *
class Firewall(QObject):
__instance = None
@staticmethod
def instance():
if Firewall.__instance == None:
Firewall.__instance = Firewall()
return Firewall.__instance
def __init__(self, parent=None):
QObject.__init__(self)
self._nodes = Nodes.instance()
self.rules = Rules(self._nodes)
self.chains = Chains(self._nodes)
def switch_rules(self, key, old_pos, new_pos):
pass
def add_rule(self, addr, rule):
return self.rules.add(addr, rule)
def insert_rule(self, addr, rule, position=0):
return self.rules.insert(addr, rule, position)
def update_rule(self, addr, uuid, rule):
return self.rules.update(addr, uuid, rule)
def delete_rule(self, addr, uuid):
return self.rules.delete(addr, uuid)
def get_rule_by_uuid(self, uuid):
if uuid == "":
return None, None
for addr in self._nodes.get_nodes():
node = self._nodes.get_node(addr)
if not 'fwrules' in node:
continue
r = node['fwrules'].get(uuid)
if r != None:
return addr, r
return None, None
def filter_rules(self, nail):
"""
"""
chains = []
for addr in self._nodes.get_nodes():
node = self._nodes.get_node(addr)
if not 'firewall' in node:
return chains
for n in node['firewall'].SystemRules:
for c in n.Chains:
for r in c.Rules:
if nail in c.Family or \
nail in c.Hook or \
nail in r.Description or \
nail in r.Target or \
nail in r.TargetParameters:
# TODO: filter expressions
#nail in r.Expressions:
chains.append(Rules.to_array(addr, c, r))
return chains
def apply_profile(self, node_addr, json_profile):
"""
Apply a profile to the firewall configuration.
Given a chain (table+family+type+hook), apply its policy, and any rules
defined.
"""
try:
holder = ui_pb2.FwChain()
profile = json_format.Parse(json_profile, holder)
fwcfg = self._nodes.get_node(node_addr)['firewall']
for sdx, n in enumerate(fwcfg.SystemRules):
for cdx, c in enumerate(n.Chains):
if c.Hook.lower() == profile.Hook and \
c.Type.lower() == profile.Type and \
c.Family.lower() == profile.Family and \
c.Table.lower() == profile.Table:
fwcfg.SystemRules[sdx].Chains[cdx].Policy = profile.Policy
for r in profile.Rules:
temp_c = ui_pb2.FwChain()
temp_c.CopyFrom(c)
del temp_c.Rules[:]
temp_c.Rules.extend([r])
if self.rules.is_duplicated(node_addr, temp_c):
continue
fwcfg.SystemRules[sdx].Chains[cdx].Rules.extend([r])
self.rules.rulesUpdated.emit()
return True, ""
except Exception as e:
print("firewall: error applying profile:", e)
return False, "{0}".format(e)
return False, QC.translate("firewall", "profile not applied")
def delete_profile(self, node_addr, json_profile):
try:
holder = ui_pb2.FwChain()
profile = json_format.Parse(json_profile, holder)
fwcfg = self._nodes.get_node(node_addr)['firewall']
for sdx, n in enumerate(fwcfg.SystemRules):
for cdx, c in enumerate(n.Chains):
if c.Hook.lower() == profile.Hook and \
c.Type.lower() == profile.Type and \
c.Family.lower() == profile.Family and \
c.Table.lower() == profile.Table:
if profile.Policy == ProfileDropInput.value:
profile.Policy = ProfileAcceptInput.value
for rdx, r in enumerate(c.Rules):
for pr in profile.Rules:
if r.UUID == pr.UUID:
print("delete_profile, rule:", r.UUID, r.Description)
del fwcfg.SystemRules[sdx].Chains[cdx].Rules[rdx]
except Exception as e:
print("firewall: error deleting profile:", e)
def swap_rules(self, view, addr, uuid, old_pos, new_pos):
return self.rules.swap(view, addr, uuid, old_pos, new_pos)
def filter_by_table(self, addr, table, family):
"""get rules by table"""
chains = []
node = self._nodes.get_node(addr)
if not 'firewall' in node:
return chains
for n in node['firewall'].SystemRules:
for c in n.Chains:
for r in c.Rules:
if c.Table == table and c.Family == family:
chains.append(Rules.to_array(addr, c, r))
return chains
def filter_by_chain(self, addr, table, family, chain, hook):
"""get rules by chain"""
chains = []
node = self._nodes.get_node(addr)
if not 'firewall' in node:
return chains
for n in node['firewall'].SystemRules:
for c in n.Chains:
for r in c.Rules:
if c.Table == table and c.Family == family and c.Name == chain and c.Hook == hook:
chains.append(Rules.to_array(addr, c, r))
return chains
def get_node_rules(self, addr):
return self.rules.get_by_node(addr)
def get_chains(self):
return self.chains.get()
def get_rules(self):
return self.rules.get()

View file

@ -0,0 +1,212 @@
from opensnitch import ui_pb2
from .enums import *
class Chains():
def __init__(self, nodes):
self._nodes = nodes
def get(self):
chains = {}
for node in self._nodes.get_nodes():
chains[node] = self.get_node_chains(node)
return chains
def get_node_chains(self, addr):
node = self._nodes.get_node(addr)
if node == None:
return rules
if not 'firewall' in node:
return rules
chains = []
for c in node['firewall'].SystemRules:
# Chains node does not exist on <= v1.5.x
try:
chains.append(c.Chains)
except Exception:
pass
return chains
def get_node_chains(self, addr):
node = self._nodes.get_node(addr)
if node == None:
return rules
if not 'firewall' in node:
return rules
chains = []
for c in node['firewall'].SystemRules:
# Chains node does not exist on <= v1.5.x
try:
chains.append(c.Chains)
except Exception:
pass
return chains
def get_policy(self, node_addr=None, hook=Hooks.INPUT.value, _type=ChainType.FILTER.value, family=Family.INET.value):
fwcfg = self._nodes.get_node(node_addr)['firewall']
for sdx, n in enumerate(fwcfg.SystemRules):
for cdx, c in enumerate(n.Chains):
if c.Hook.lower() == hook and c.Type.lower() == _type and c.Family.lower() == family:
return c.Policy
return None
def set_policy(self, node_addr, hook=Hooks.INPUT.value, _type=ChainType.FILTER.value, family=Family.INET.value, policy=Policy.DROP):
fwcfg = self._nodes.get_node(node_addr)['firewall']
for sdx, n in enumerate(fwcfg.SystemRules):
for cdx, c in enumerate(n.Chains):
# XXX: support only "inet" family (ipv4/ipv6)? or allow to
# specify ipv4 OR/AND ipv6? some systems have ipv6 disabled
if c.Hook.lower() == hook and c.Type.lower() == _type and c.Family.lower() == family:
fwcfg.SystemRules[sdx].Chains[cdx].Policy = policy
if wantedHook == Fw.Hooks.INPUT.value and wantedPolicy == Fw.Policy.DROP.value:
fwcfg.SystemRules[sdx].Chains[cdx].Rules.append(rule.Rules[0])
self._nodes.add_fw_config(node_addr, fwcfg)
return True
return False
@staticmethod
def new(
name="",
table=Table.FILTER.value,
family=Family.INET.value,
ctype="",
hook=Hooks.INPUT.value
):
chain = ui_pb2.FwChain()
chain.Name = name
chain.Table = table
chain.Family = family
chain.Type = ctype
chain.Hook = hook
return chain
# man nft
# Table 6. Standard priority names, family and hook compatibility matrix
# Name │ Value │ Families │ Hooks
# raw │ -300 │ ip, ip6, inet │ all
# mangle │ -150 │ ip, ip6, inet │ all
# dstnat │ -100 │ ip, ip6, inet │ prerouting
# filter │ 0 │ ip, ip6, inet, arp, netdev │ all
# security │ 50 │ ip, ip6, inet │ all
# srcnat │ 100 │ ip, ip6, inet │ postrouting
#
class ChainFilter(Chains):
"""
ChainFilter returns a new chain of type filter.
The name of the chain is the one listed with: nft list table inet filter.
It corresponds with the hook name, but can be a random name.
"""
@staticmethod
def input(family=Family.INET.value):
chain = ui_pb2.FwChain()
chain.Name = Hooks.INPUT.value
chain.Table = Table.FILTER.value
chain.Family = family
chain.Type = ChainType.FILTER.value
chain.Hook = Hooks.INPUT.value
return chain
@staticmethod
def output(family=Family.INET.value):
chain = ui_pb2.FwChain()
chain.Name = Hooks.OUTPUT.value
chain.Table = Table.FILTER.value
chain.Family = family
chain.Type = ChainType.FILTER.value
chain.Hook = Hooks.OUTPUT.value
return chain
@staticmethod
def forward(family=Family.INET.value):
chain = ui_pb2.FwChain()
chain.Name = Hooks.FORWARD.value
chain.Table = Table.FILTER.value
chain.Family = family
chain.Type = ChainType.FILTER.value
chain.Hook = Hooks.FORWARD.value
return chain
class ChainMangle(Chains):
"""
ChainMangle returns a new chain of type mangle.
The name of the chain is the one listed with: nft list table inet mangle.
It corresponds with the hook name, but can be a random name.
"""
@staticmethod
def output(family=Family.INET.value):
chain = ui_pb2.FwChain()
chain.Name = Hooks.OUTPUT.value
chain.Table = Table.MANGLE.value
chain.Family = family
chain.Type = ChainType.MANGLE.value
chain.Hook = Hooks.OUTPUT.value
return chain
@staticmethod
def input(family=Family.INET.value):
chain = ui_pb2.FwChain(family=Family.INET.value)
chain.Name = Hooks.INPUT.value
chain.Table = Table.MANGLE.value
chain.Family = family
chain.Type = ChainType.MANGLE.value
chain.Hook = Hooks.INPUT.value
return chain
@staticmethod
def forward(family=Family.INET.value):
chain = ui_pb2.FwChain()
chain.Name = Hooks.FORWARD.value
chain.Table = Table.MANGLE.value
chain.Family = family
chain.Type = ChainType.MANGLE.value
chain.Hook = Hooks.PREROUTING.value
return chain
@staticmethod
def prerouting(family=Family.INET.value):
chain = ui_pb2.FwChain()
chain.Name = Hooks.PREROUTING.value
chain.Table = Table.MANGLE.value
chain.Family = family
chain.Type = ChainType.MANGLE.value
chain.Hook = Hooks.PREROUTING.value
return chain
@staticmethod
def postrouting(family=Family.INET.value):
chain = ui_pb2.FwChain()
chain.Name = Hooks.POSTROUTING.value
chain.Table = Table.MANGLE.value
chain.Family = family
chain.Type = ChainType.MANGLE.value
chain.Hook = Hooks.POSTROUTING.value
return chain

View file

@ -0,0 +1,64 @@
from opensnitch.utils import Enums
from opensnitch.config import Config
class Verdicts(Enums):
EMPTY = ""
ACCEPT = Config.ACTION_ACCEPT
DROP = Config.ACTION_DROP
REJECT = Config.ACTION_REJECT
RETURN = Config.ACTION_RETURN
STOP = Config.ACTION_STOP
class Policy(Enums):
ACCEPT = "accept"
DROP = "drop"
class Table(Enums):
FILTER = "filter"
MANGLE = "mangle"
NAT = "nat"
class Hooks(Enums):
INPUT ="input"
OUTPUT ="output"
FORWARD = "forward"
PREROUTING = "prerouting"
POSTROUTING = "postrouting"
class Family(Enums):
INET = "inet"
IPv4 = "ip"
IPv6 = "ip6"
class ChainType(Enums):
FILTER = "filter"
MANGLE = "mangle"
ROUTE = "route"
SNAT = "snat"
DNAT = "dnat"
class Operator(Enums):
EQUAL = "=="
NOT_EQUAL = "!="
GT_THAN = ">="
GT = ">"
LT_THAN = "<="
LT = "<"
class Statements(Enums):
"""Enum of known (allowed) statements:
[tcp,udp,ip] ...
"""
TCP = "tcp"
UDP = "udp"
UDPLITE = "udplite"
SCTP = "sctp"
DCCP = "dccp"
ICMP = "icmp"
SPORT = "sport"
DPORT = "dport"
IP = "ip"
IIFNAME = "iifname"
OIFNAME = "oifname"

View file

@ -0,0 +1,41 @@
from opensnitch import ui_pb2
from .enums import *
class Expr():
"""
Expr returns a new nftables expression that defines a match or an action:
tcp dport 22, udp sport 53
log prefix "xxx"
Attributes:
op (string): operator (==, !=, ...).
what (string): name of the statement (tcp, udp, ip, ...)
value (tuple): array of values (dport -> 22, etc).
"""
@staticmethod
def new(op, what, values):
expr = ui_pb2.Expressions()
expr.Statement.Op = op
expr.Statement.Name = what
for val in values:
exprValues = ui_pb2.StatementValues()
exprValues.Key = val[0]
exprValues.Value = val[1]
expr.Statement.Values.append(exprValues)
return expr
class ExprCt(Enums):
STATE = "state"
NEW = "new"
ESTABLISHED = "established"
RELATED = "related"
class ExprIface(Enums):
IIFNAME = "iifname"
OIFNAME = "oifname"
class ExprLog(Enums):
LOG = "log"

View file

@ -0,0 +1,157 @@
import glob
import json
import os.path
class Profiles():
@staticmethod
def load_predefined_profiles():
profiles = glob.glob("/etc/opensnitchd/system-fw.d/profiles/*.profile")
p = []
for pr_path in profiles:
with open(pr_path) as f:
p.append({os.path.basename(pr_path): json.load(f)})
return p
class ProfileAcceptOutput():
value = {
"Name": "accept-mangle-output",
"Table": "mangle",
"Family": "inet",
"Priority": "",
"Type": "mangle",
"Hook": "output",
"Policy": "accept",
"Rules": [
]
}
class ProfileDropOutput():
value = {
"Name": "drop-mangle-output",
"Table": "mangle",
"Family": "inet",
"Priority": "",
"Type": "mangle",
"Hook": "output",
"Policy": "drop",
"Rules": [
]
}
class ProfileAcceptForward():
value = {
"Name": "accept-mangle-forward",
"Table": "mangle",
"Family": "inet",
"Priority": "",
"Type": "mangle",
"Hook": "forward",
"Policy": "accept",
"Rules": [
]
}
class ProfileDropForward():
value = {
"Name": "drop-mangle-forward",
"Table": "mangle",
"Family": "inet",
"Priority": "",
"Type": "mangle",
"Hook": "forward",
"Policy": "drop",
"Rules": [
]
}
class ProfileAcceptInput():
value = {
"Name": "accept-filter-input",
"Table": "filter",
"Family": "inet",
"Priority": "",
"Type": "filter",
"Hook": "input",
"Policy": "accept",
"Rules": [
]
}
class ProfileDropInput():
"""
Set input filter table policy to DROP and add the needed rules to allow
outbound connections.
"""
# TODO: delete dropInput profile's rules
value = {
"Name": "drop-filter-input",
"Table": "filter",
"Family": "inet",
"Priority": "",
"Type": "filter",
"Hook": "input",
"Policy": "drop",
"Rules": [
{
"Table": "",
"Chain": "",
"UUID": "profile-drop-inbound-2d7e6fe4-c21d-11ec-99a6-3c970e298b0c",
"Enabled": True,
"Position": "0",
"Description": "[profile-drop-inbound] allow localhost connections",
"Parameters": "",
"Expressions": [
{
"Statement": {
"Op": "",
"Name": "iifname",
"Values": [
{
"Key": "lo",
"Value": ""
}
]
}
}
],
"Target": "accept",
"TargetParameters": ""
},
{
"Enabled": True,
"Description": "[profile-drop-inbound] allow established,related connections",
"UUID": "profile-drop-inbound-e1fc1a1c-c21c-11ec-9a2a-3c970e298b0c",
"Expressions": [
{
"Statement": {
"Op": "",
"Name": "ct",
"Values": [
{
"Key": "state",
"Value": "related"
},
{
"Key": "state",
"Value": "established"
}
]
}
}
],
"Target": "accept",
"TargetParameters": ""
}
]
}

View file

@ -0,0 +1,275 @@
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtCore import QCoreApplication as QC
import uuid
from opensnitch import ui_pb2
class Rules(QObject):
rulesUpdated = pyqtSignal()
def __init__(self, nodes):
QObject.__init__(self)
self._nodes = nodes
self.rulesUpdated.connect(self._cb_rules_updated)
def _cb_rules_updated(self):
pass
def add(self, addr, rule):
"""Add a new rule to the corresponding table on the given node
"""
node = self._nodes.get_node(addr)
if not 'firewall' in node:
return False, QC.translate("firewall", "rule not found by its ID.")
if self.is_duplicated(addr, rule):
return False, QC.translate("firewall", "duplicated.")
for sdx, n in enumerate(node['firewall'].SystemRules):
for cdx, c in enumerate(n.Chains):
if c.Name == rule.Name and \
c.Hook == rule.Hook and \
c.Table == rule.Table and \
c.Family == rule.Family and \
c.Type == rule.Type:
node['firewall'].SystemRules[sdx].Chains[cdx].Rules.append(rule.Rules[0])
node['fwrules'][rule.Rules[0].UUID] = rule
self._nodes.add_fw_config(addr, node['firewall'])
self._nodes.add_fw_rules(addr, node['fwrules'])
self.rulesUpdated.emit()
return True
return False, QC.translate("firewall", "firewall table/chain not properly configured.")
def insert(self, addr, rule, position=0):
"""Insert a new rule to the corresponding table on the given node
"""
node = self._nodes.get_node(addr)
if not 'firewall' in node:
return False, QC.translate("firewall", "this node doesn't have a firewall configuration, review it.")
if self.is_duplicated(addr, rule):
return False, QC.translate("firewall", "duplicated")
for sdx, n in enumerate(node['firewall'].SystemRules):
for cdx, c in enumerate(n.Chains):
if c.Name == rule.Name and \
c.Hook == rule.Hook and \
c.Table == rule.Table and \
c.Family == rule.Family and \
c.Type == rule.Type:
node['firewall'].SystemRules[sdx].Chains[cdx].Rules.insert(int(position), rule.Rules[0])
node['fwrules'][rule.Rules[0].UUID] = rule
self._nodes.add_fw_config(addr, node['firewall'])
self._nodes.add_fw_rules(addr, node['fwrules'])
self.rulesUpdated.emit()
return True, ""
return False, QC.translate("firewall", "firewall table/chain not properly configured.")
def update(self, addr, uuid, rule):
node = self._nodes.get_node(addr)
if not 'firewall' in node:
return False, QC.translate("firewall", "this node doesn't have a firewall configuration, review it.")
for sdx, n in enumerate(node['firewall'].SystemRules):
for cdx, c in enumerate(n.Chains):
for rdx, r in enumerate(c.Rules):
if r.UUID == uuid:
c.Rules[rdx].CopyFrom(rule.Rules[0])
node['firewall'].SystemRules[sdx].Chains[cdx].Rules[rdx].CopyFrom(rule.Rules[0])
self._nodes.add_fw_config(addr, node['firewall'])
node['fwrules'][uuid] = rule
self._nodes.add_fw_rules(addr, node['fwrules'])
self.rulesUpdated.emit()
return True, ""
return False, QC.translate("firewall", "rule not found by its ID.")
def get(self):
rules = []
for node in self._nodes.get_nodes():
node_rules = self.get_by_node(node)
rules += node_rules
return rules
def delete(self, addr, uuid):
node = self._nodes.get_node(addr)
if not 'firewall' in node:
return False
for sdx, n in enumerate(node['firewall'].SystemRules):
for cdx, c in enumerate(n.Chains):
for idx, r in enumerate(c.Rules):
if r.UUID == uuid:
del node['firewall'].SystemRules[sdx].Chains[cdx].Rules[idx]
self._nodes.add_fw_config(addr, node['firewall'])
if uuid in node['fwrules']:
del node['fwrules'][uuid]
self._nodes.add_fw_rules(addr, node['fwrules'])
else:
# raise Error("rules doesn't have UUID field")
print("[firewall] delete() error:", uuid)
return False, None
self.rulesUpdated.emit()
return True, node['firewall']
return False, None
def get_by_node(self, addr):
rules = []
node = self._nodes.get_node(addr)
if node == None:
return rules
if not 'firewall' in node:
return rules
for u in node['firewall'].SystemRules:
for c in u.Chains:
for r in c.Rules:
rules.append(Rules.to_array(addr, c, r))
return rules
def swap(self, view, addr, uuid, old_pos, new_pos):
"""
swap changes the order of 2 rows.
The list of rules is ordered from top to bottom: 0,1,2,3...
so a click on the down button sums +1, a click on the up button rest -1
"""
node = self._nodes.get_node(addr)
if node == None:
return
if not 'firewall' in node:
return
for sdx, c in enumerate(node['firewall'].SystemRules):
for cdx, u in enumerate(c.Chains):
nrules = len(u.Rules)
for rdx, r in enumerate(u.Rules):
# is the last rule
if new_pos > nrules and new_pos < nrules:
break
if u.Rules[rdx].UUID == uuid:
old_rule = u.Rules[old_pos]
new_rule = ui_pb2.FwRule()
new_rule.CopyFrom(u.Rules[new_pos])
node['firewall'].SystemRules[sdx].Chains[cdx].Rules[new_pos].CopyFrom(old_rule)
node['firewall'].SystemRules[sdx].Chains[cdx].Rules[old_pos].CopyFrom(new_rule)
self._nodes.add_fw_config(addr, node['firewall'])
#self._nodes.add_fw_rules(addr, node['fwrules'])
self.rulesUpdated.emit()
return True
return False
def is_duplicated(self, addr, orig_rule):
# we need to duplicate the rule, otherwise we'd modify the UUID of the
# orig rule.
temp_c = ui_pb2.FwChain()
temp_c.CopyFrom(orig_rule)
# the UUID will be different, so zero it out.
temp_c.Rules[0].UUID = ""
node = self._nodes.get_node(addr)
if node == None:
return False
if not 'firewall' in node:
return False
for n in node['firewall'].SystemRules:
for c in n.Chains:
if c.Name == temp_c.Name and \
c.Hook == temp_c.Hook and \
c.Table == temp_c.Table and \
c.Family == temp_c.Family and \
c.Type == temp_c.Type:
for rdx, r in enumerate(c.Rules):
uuid = c.Rules[rdx].UUID
c.Rules[rdx].UUID = ""
is_equal = c.Rules[rdx].SerializeToString() == temp_c.Rules[0].SerializeToString()
c.Rules[rdx].UUID = uuid
if is_equal:
return True
return False
@staticmethod
def new(
enabled=True,
_uuid="",
description="",
expressions=None,
target="",
target_parms=""
):
rule = ui_pb2.FwRule()
if _uuid == "":
rule.UUID = str(uuid.uuid1())
else:
rule.UUID = _uuid
rule.Enabled = enabled
rule.Description = description
if expressions != None:
rule.Expressions.append(expressions)
rule.Target = target
rule.TargetParameters = target_parms
return rule
@staticmethod
def new_flat(c, r):
"""Create a new "flat" rule from a hierarchical one.
Transform from:
{
xx:
{
yy: {
to:
{xx:, yy}
"""
chain = ui_pb2.FwChain()
chain.CopyFrom(c)
del chain.Rules[:]
chain.Rules.extend([r])
return chain
@staticmethod
def to_dict(sysRules):
"""Transform json/protobuf struct to flat structure.
This is the default format used to find rules in the table view.
"""
rules={}
for s in sysRules:
for c in s.Chains:
if len(c.Rules) == 0:
continue
for r in c.Rules:
rules[r.UUID] = Rules.new_flat(c, r)
return rules
@staticmethod
def to_array(addr, chain, rule):
cols = []
cols.append(rule.UUID)
cols.append(addr)
cols.append(chain.Name)
cols.append(chain.Table)
cols.append(chain.Family)
cols.append(chain.Hook)
cols.append(str(rule.Enabled))
cols.append(rule.Description)
exprs = ""
for e in rule.Expressions:
exprs += "{0} {1} {2}".format(
e.Statement.Op,
e.Statement.Name,
"".join(["{0} {1} ".format(h.Key, h.Value) for h in e.Statement.Values ])
)
cols.append(exprs)
cols.append(rule.Target)
return cols

View file

@ -0,0 +1,24 @@
from google.protobuf import __version__ as protobuf_version
from .enums import *
class Utils():
@staticmethod
def isExprPort(value):
"""Return true if the value is valid for a port based rule:
nft add rule ... tcp dport 22 accept
"""
return value == Statements.TCP.value or \
value == Statements.UDP.value or \
value == Statements.UDPLITE.value or \
value == Statements.SCTP.value or \
value == Statements.DCCP.value
@staticmethod
def isProtobufSupported():
"""
The protobuf operations append() and insert() were introduced on 3.8.0 version.
"""
vparts = protobuf_version.split(".")
return int(vparts[0]) >= 3 and int(vparts[1]) >= 8

View file

@ -1,3 +1,4 @@
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
from queue import Queue
from datetime import datetime
import time
@ -7,8 +8,10 @@ from opensnitch import ui_pb2
from opensnitch.database import Database
from opensnitch.config import Config
class Nodes():
class Nodes(QObject):
__instance = None
nodesUpdated = pyqtSignal(int) # total
LOG_TAG = "[Nodes]: "
ONLINE = "\u2713 online"
OFFLINE = "\u2613 offline"
@ -21,6 +24,7 @@ class Nodes():
return Nodes.__instance
def __init__(self):
QObject.__init__(self)
self._db = Database.instance()
self._nodes = {}
self._notifications_sent = {}
@ -45,18 +49,27 @@ class Nodes():
self.add_data(addr, client_config)
self.update(proto, _addr)
return self._nodes[addr]
self.nodesUpdated.emit(self.count())
return self._nodes[addr], addr
except Exception as e:
print(self.LOG_TAG, "exception adding/updating node: ", e, "addr:", addr, "config:", client_config)
return None
return None, None
def add_data(self, addr, client_config):
if client_config != None:
self._nodes[addr]['data'] = self.get_client_config(client_config)
self.add_fw_config(addr, client_config.systemFirewall)
self.add_rules(addr, client_config.rules)
def add_fw_config(self, addr, fwconfig):
self._nodes[addr]['firewall'] = fwconfig
def add_fw_rules(self, addr, fwconfig):
self._nodes[addr]['fwrules'] = fwconfig
def add_rule(self, time, node, name, enabled, precedence, action, duration, op_type, op_sensitive, op_operand, op_data):
# don't add rule if the user has selected to exclude temporary
# rules
@ -92,6 +105,7 @@ class Nodes():
def delete_all(self):
self.send_notifications(None)
self._nodes = {}
self.nodesUpdated.emit(self.count())
def delete(self, peer):
proto, addr = self.get_addr(peer)
@ -101,6 +115,7 @@ class Nodes():
self._nodes[addr]['notifications'].put(None)
if addr in self._nodes:
del self._nodes[addr]
self.nodesUpdated.emit(self.count())
def get(self):
return self._nodes
@ -170,12 +185,12 @@ class Nodes():
print(self.LOG_TAG + " exception saving nodes config: ", e, config)
def start_interception(self, _addr=None, _callback=None):
return self.firewall(not_type=ui_pb2.LOAD_FIREWALL, addr=_addr, callback=_callback)
return self.firewall(not_type=ui_pb2.ENABLE_INTERCEPTION, addr=_addr, callback=_callback)
def stop_interception(self, _addr=None, _callback=None):
return self.firewall(not_type=ui_pb2.UNLOAD_FIREWALL, addr=_addr, callback=_callback)
return self.firewall(not_type=ui_pb2.DISABLE_INTERCEPTION, addr=_addr, callback=_callback)
def firewall(self, not_type=ui_pb2.LOAD_FIREWALL, addr=None, callback=None):
def firewall(self, not_type=ui_pb2.ENABLE_INTERCEPTION, addr=None, callback=None):
noti = ui_pb2.Notification(clientName="", serverName="", type=not_type, data="", rules=[])
if addr == None:
nid = self.send_notifications(noti, callback)
@ -236,20 +251,23 @@ class Nodes():
return notification.id
def reply_notification(self, addr, reply):
if reply == None:
print(self.LOG_TAG, " reply notification None")
return
try:
if reply == None:
print(self.LOG_TAG, " reply notification None")
return
if reply.id in self._notifications_sent:
if self._notifications_sent[reply.id] != None:
if self._notifications_sent[reply.id]['callback'] != None:
self._notifications_sent[reply.id]['callback'].emit(reply)
if reply.id in self._notifications_sent:
if self._notifications_sent[reply.id] != None:
if self._notifications_sent[reply.id]['callback'] != None:
self._notifications_sent[reply.id]['callback'].emit(reply)
# delete only one-time notifications
# we need the ID of streaming notifications from the server
# (monitor_process for example) to keep track of the data sent to us.
if self._notifications_sent[reply.id]['type'] != ui_pb2.MONITOR_PROCESS:
del self._notifications_sent[reply.id]
# delete only one-time notifications
# we need the ID of streaming notifications from the server
# (monitor_process for example) to keep track of the data sent to us.
if self._notifications_sent[reply.id]['type'] != ui_pb2.MONITOR_PROCESS:
del self._notifications_sent[reply.id]
except Exception as e:
print(self.LOG_TAG, "notification exception:", e)
def update(self, proto, addr, status=ONLINE):
try:
@ -300,3 +318,12 @@ class Nodes():
self._db.delete_rule(rule.name, addr)
return nid, noti
def reload_fw(self, addr, fw_config, callback):
notif = ui_pb2.Notification(
id=int(str(time.time()).replace(".", "")),
type=ui_pb2.RELOAD_FW_RULES,
sysFirewall=fw_config
)
nid = self.send_notification(addr, notif, callback)
return nid, notif

View file

@ -0,0 +1,470 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>440</width>
<height>463</height>
</rect>
</property>
<property name="windowTitle">
<string>Firewall</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="styleSheet">
<string notr="true">QTabBar {
alignment: center;
}</string>
</property>
<property name="tabPosition">
<enum>QTabWidget::South</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<widget class="QWidget" name="tabMain">
<attribute name="title">
<string/>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:14pt; font-weight:600;&quot;&gt;Firewall&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QSlider" name="sliderFwEnable">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>48</width>
<height>16777215</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QSlider::groove:horizontal {
background-color: transparent;
border: 0px solid #424242;
height: 20px;
border-radius: 4px;
}
QSlider::handle:horizontal {
background-color: rgb(154, 153, 150);
border: 1px solid rgb(119, 118, 123);
width: 20px;
height: 20px;
line-height: 20px; /* do not delete this */
/*margin-left: -5px;*/
margin-top: -2px;
margin-bottom: -2px;
border-radius: 5px;
}
QSlider::handle:horizontal:hover {
background-color: rgb(79, 91, 98);
border-radius: 5px;
}
QSlider::add-page:horizontal {
border-radius: 4px;
background: rgb(237, 51, 59);
border: 1px solid rgb(192, 28, 40);
}
QSlider::sub-page:horizontal {
border-radius: 4px;
background: rgb(139, 195, 74);
border: 1px solid rgb(67, 190, 24);
}</string>
</property>
<property name="maximum">
<number>1</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="lblStatusIcon">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>96</width>
<height>0</height>
</size>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="lblFwStatus">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(237, 51, 59);</string>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="policiesBox">
<property name="title">
<string/>
</property>
<property name="flat">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>15</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>15</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="2" column="1">
<widget class="QComboBox" name="comboOutput">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Allow</string>
</property>
<property name="icon">
<iconset theme="emblem-default">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>Deny</string>
</property>
<property name="icon">
<iconset theme="process-stop">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboInput">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Allow</string>
</property>
<property name="icon">
<iconset theme="emblem-default">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>Deny</string>
</property>
<property name="icon">
<iconset theme="process-stop">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Outbound</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="topMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="cmdNewRule">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>New rule</string>
</property>
<property name="icon">
<iconset theme="document-new">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cmdExcludeService">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Exclude a service from being intercepted</string>
</property>
<property name="text">
<string>Exclude service</string>
</property>
<property name="icon">
<iconset theme="go-jump">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0" colspan="2">
<widget class="Line" name="line_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Inbound</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="lblProfile">
<property name="text">
<string>Profile</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboProfile"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cmdClose">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Close</string>
</property>
<property name="icon">
<iconset theme="window-close">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>sliderFwEnable</tabstop>
<tabstop>comboInput</tabstop>
<tabstop>comboOutput</tabstop>
<tabstop>cmdNewRule</tabstop>
<tabstop>cmdExcludeService</tabstop>
<tabstop>cmdClose</tabstop>
<tabstop>tabWidget</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,418 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>424</width>
<height>462</height>
</rect>
</property>
<property name="windowTitle">
<string>Firewall rule</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup/icon-white.svg</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup/icon-white.svg</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="styleSheet">
<string notr="true">QTabBar {
alignment: center;
}</string>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="elideMode">
<enum>Qt::ElideRight</enum>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Simple</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Node</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboNodes"/>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="checkEnable">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Description</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineDescription">
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Port</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QComboBox" name="comboOperator">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentText">
<string notr="true">equal</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<item>
<property name="text">
<string>equal</string>
</property>
</item>
<item>
<property name="text">
<string>not equal</string>
</property>
</item>
<item>
<property name="text">
<string>greater or equal than</string>
</property>
</item>
<item>
<property name="text">
<string>greater than</string>
</property>
</item>
<item>
<property name="text">
<string>less or equal than</string>
</property>
</item>
<item>
<property name="text">
<string>less than</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboPorts">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="currentText">
<string notr="true"/>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLength</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="frameDirection">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="leftMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Direction</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboDirection">
<item>
<property name="text">
<string>IN</string>
</property>
<property name="icon">
<iconset theme="go-down">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>OUT</string>
</property>
<property name="icon">
<iconset theme="go-up">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frameAction">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Action</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboVerdict">
<item>
<property name="text">
<string>ACCEPT</string>
</property>
<property name="icon">
<iconset theme="object-select-symbolic">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>DROP</string>
</property>
<property name="icon">
<iconset theme="window-close">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>REJECT</string>
</property>
<property name="icon">
<iconset theme="edit-delete">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>RETURN</string>
</property>
<property name="icon">
<iconset theme="edit-undo">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Advanced</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>TODO</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="helpButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>You can use , or - to specify multiple ports or a port range: 22 or 22,443 or 10000-20000</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="help-browser"/>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cmdClose">
<property name="text">
<string>Close</string>
</property>
<property name="icon">
<iconset theme="window-close">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cmdDelete">
<property name="text">
<string>Delete</string>
</property>
<property name="icon">
<iconset theme="edit-delete"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cmdSave">
<property name="text">
<string>Save</string>
</property>
<property name="icon">
<iconset theme="document-save">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cmdAdd">
<property name="text">
<string>Add</string>
</property>
<property name="icon">
<iconset theme="list-add">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -10,7 +10,7 @@
<x>0</x>
<y>0</y>
<width>863</width>
<height>600</height>
<height>597</height>
</rect>
</property>
<property name="sizePolicy">
@ -108,7 +108,7 @@
</property>
<property name="icon">
<iconset theme="emblem-default">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
@ -117,7 +117,7 @@
</property>
<property name="icon">
<iconset theme="emblem-important">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
@ -126,7 +126,7 @@
</property>
<property name="icon">
<iconset theme="window-close">
<normaloff>.</normaloff>.</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
</widget>
@ -167,7 +167,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>.</normaloff>.</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -185,7 +185,7 @@
</property>
<property name="icon">
<iconset theme="go-next">
<normaloff>.</normaloff>.</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -194,6 +194,9 @@
<property name="currentIndex">
<number>0</number>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<item>
<property name="text">
<string>50</string>
@ -231,7 +234,7 @@
</property>
<property name="icon">
<iconset theme="edit-clear-all">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="flat">
<bool>true</bool>
@ -245,7 +248,7 @@
</property>
<property name="icon">
<iconset theme="help-browser">
<normaloff>.</normaloff>.</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -288,7 +291,7 @@
</property>
<property name="icon">
<iconset theme="document-save">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
@ -311,7 +314,7 @@
</property>
<property name="icon">
<iconset theme="preferences-system">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="flat">
<bool>true</bool>
@ -334,7 +337,20 @@
</property>
<property name="icon">
<iconset theme="document-new">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="fwButton">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="security-high"/>
</property>
<property name="flat">
<bool>true</bool>
@ -439,7 +455,7 @@
</property>
<property name="icon">
<iconset theme="media-playback-start">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="checkable">
<bool>true</bool>
@ -474,7 +490,7 @@
<widget class="QWidget" name="tab">
<attribute name="icon">
<iconset theme="view-sort-ascending">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</attribute>
<attribute name="title">
<string>Events</string>
@ -545,7 +561,7 @@
<widget class="QWidget" name="tab_8">
<attribute name="icon">
<iconset theme="network-workgroup">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</attribute>
<attribute name="title">
<string>Nodes</string>
@ -572,7 +588,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -641,7 +657,7 @@
<widget class="QWidget" name="tab_3">
<attribute name="icon">
<iconset theme="address-book-new">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</attribute>
<attribute name="title">
<string>Rules</string>
@ -665,7 +681,13 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<attribute name="headerVisible">
<property name="animated">
<bool>true</bool>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
<column>
@ -673,6 +695,11 @@
<string notr="true">1</string>
</property>
</column>
<column>
<property name="text">
<string>2</string>
</property>
</column>
<item>
<property name="text">
<string>Application rules</string>
@ -686,7 +713,7 @@
</property>
<property name="icon">
<iconset theme="system-run">
<normaloff>../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</normaloff>../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</iconset>
</property>
<item>
<property name="text">
@ -699,7 +726,7 @@
</property>
<property name="icon">
<iconset theme="security-medium">
<normaloff>../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</normaloff>../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</iconset>
</property>
</item>
<item>
@ -713,7 +740,7 @@
</property>
<property name="icon">
<iconset theme="edit-clear">
<normaloff>../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</normaloff>../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</iconset>
</property>
</item>
</item>
@ -729,8 +756,23 @@
</font>
</property>
<property name="icon">
<iconset theme="system">
<normaloff>../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</normaloff>../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</iconset>
<iconset theme="network-workgroup">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../gustavo-iniguez-goya/opensnitch/ui/opensnitch/res</iconset>
</property>
</item>
<item>
<property name="text">
<string>System rules</string>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="icon">
<iconset theme="security-high">
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
</widget>
@ -739,6 +781,40 @@
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="FirewallTableView" name="fwTable">
<property name="autoScroll">
<bool>false</bool>
</property>
<property name="editTriggers">
<set>QAbstractItemView::AnyKeyPressed|QAbstractItemView::EditKeyPressed</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderMinimumSectionSize">
<number>25</number>
</attribute>
<attribute name="verticalHeaderDefaultSectionSize">
<number>42</number>
</attribute>
</widget>
</item>
<item>
<widget class="GenericTableView" name="rulesTable">
<property name="selectionBehavior">
@ -772,7 +848,7 @@
</property>
<property name="icon">
<iconset theme="system-run">
<normaloff>.</normaloff>.</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
@ -781,7 +857,7 @@
</property>
<property name="icon">
<iconset theme="security-medium">
<normaloff>.</normaloff>.</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
@ -790,7 +866,15 @@
</property>
<property name="icon">
<iconset theme="edit-clear">
<normaloff>.</normaloff>.</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>System rules</string>
</property>
<property name="icon">
<iconset theme="security-high"/>
</property>
</item>
</widget>
@ -821,7 +905,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -842,7 +926,7 @@
</property>
<property name="icon">
<iconset theme="accessories-text-editor">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -856,7 +940,7 @@
</property>
<property name="icon">
<iconset theme="edit-delete">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -887,7 +971,7 @@
<widget class="QWidget" name="tab_4">
<attribute name="icon">
<iconset theme="computer">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</attribute>
<attribute name="title">
<string>Hosts</string>
@ -911,7 +995,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -974,7 +1058,7 @@
<widget class="QWidget" name="tab_7">
<attribute name="icon">
<iconset theme="system-run">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</attribute>
<attribute name="title">
<string>Applications</string>
@ -1001,7 +1085,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -1012,7 +1096,7 @@
</property>
<property name="icon">
<iconset theme="system-search">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -1081,7 +1165,7 @@
<widget class="QWidget" name="tab_2">
<attribute name="icon">
<iconset theme="emblem-web">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</attribute>
<attribute name="title">
<string>Addresses</string>
@ -1105,7 +1189,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -1168,7 +1252,7 @@
<widget class="QWidget" name="tab_5">
<attribute name="icon">
<iconset theme="network-wired">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</attribute>
<attribute name="title">
<string>Ports</string>
@ -1192,7 +1276,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -1252,7 +1336,7 @@
<widget class="QWidget" name="tab_6">
<attribute name="icon">
<iconset theme="system-users">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</attribute>
<attribute name="title">
<string>Users</string>
@ -1276,7 +1360,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -1628,6 +1712,11 @@
<extends>QTableView</extends>
<header>customwidgets.generictableview</header>
</customwidget>
<customwidget>
<class>FirewallTableView</class>
<extends>QTableView</extends>
<header>customwidgets.firewalltableview</header>
</customwidget>
</customwidgets>
<resources>
<include location="resources.qrc"/>

View file

@ -17,6 +17,7 @@ from opensnitch.dialogs.prompt import PromptDialog
from opensnitch.dialogs.stats import StatsDialog
from opensnitch.notifications import DesktopNotifications
from opensnitch.firewall import Rules as FwRules
from opensnitch.nodes import Nodes
from opensnitch.config import Config
from opensnitch.version import version
@ -530,8 +531,12 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
@QtCore.pyqtSlot(dict)
def _on_node_actions(self, kwargs):
if kwargs['action'] == self.NODE_ADD:
n = self._nodes.add(kwargs['peer'], kwargs['node_config'])
n, addr = self._nodes.add(kwargs['peer'], kwargs['node_config'])
if n != None:
self._nodes.add_fw_rules(
addr,
FwRules.to_dict(kwargs['node_config'].systemFirewall.SystemRules)
)
self._status_change_trigger.emit(True)
# if there're more than one node, we can't update the status
# based on the fw status, only if the daemon is running or not

View file

@ -10,6 +10,8 @@ import fcntl
import struct
import array
import os, sys, glob
import enum
import re
class AsnDB():
__instance = None
@ -289,3 +291,58 @@ class FileDialog():
fileName = QtWidgets.QFileDialog.getExistingDirectory(parent, "", current_dir, options)
return fileName
# https://stackoverflow.com/questions/29503339/how-to-get-all-values-from-python-enum-class
class Enums(enum.Enum):
@classmethod
def to_dict(cls):
return {e.name: e.value for e in cls}
@classmethod
def keys(cls):
return cls._member_names_
@classmethod
def values(cls):
return [v.value for v in cls]
class NetworkServices():
"""Get a list of known ports. /etc/services
"""
__instance = None
@staticmethod
def instance():
if NetworkServices.__instance == None:
NetworkServices.__instance = NetworkServices()
return Services.__instance
srv_array = []
ports_list = []
def __init__(self):
etcServices = open("/etc/services")
for line in etcServices:
if line[0] == "#":
continue
g = re.search("([a-zA-Z0-9\-]+)( |\t)+([0-9]+)\/([a-zA-Z0-9\-]+)(.*)\n", line)
if g:
self.srv_array.append("{0}/{1} {2}".format(
g.group(1),
g.group(3),
"" if len(g.groups())>3 and g.group(4) == "" else "({0})".format(g.group(4).replace("\t", ""))
)
)
self.ports_list.append(g.group(3))
# extra ports that don't exist in /etc/services
self.srv_array.append("wireguard/51820 WireGuard VPN")
self.ports_list.append("51820")
def to_array(self):
return self.srv_array
def port_by_index(self, idx):
return self.ports_list[idx]
def index_by_port(self, port):
return self.ports_list.index(str(port))