opensnitch/daemon/firewall/rules.go
2020-11-15 00:53:13 +01:00

296 lines
7.9 KiB
Go

package firewall
import (
"fmt"
"regexp"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
// DropMark is the mark we place on a connection when we deny it.
// The connection is dropped later on OUTPUT chain.
const DropMark = 0x18BA5
// Action is the modifier we apply to a rule.
type Action string
// Actions we apply to the firewall.
const (
ADD = Action("-A")
INSERT = Action("-I")
DELETE = Action("-D")
FLUSH = Action("-F")
NEWCHAIN = Action("-N")
DELCHAIN = Action("-X")
systemRulePrefix = "opensnitch-filter"
)
// make sure we don't mess with multiple rules
// at the same time
var (
lock = sync.Mutex{}
queueNum = 0
running = false
// check that rules are loaded every 30s
rulesChecker = time.NewTicker(time.Second * 30)
rulesCheckerChan = make(chan bool)
regexRulesQuery, _ = regexp.Compile(`NFQUEUE.*ctstate NEW,RELATED.*NFQUEUE num.*bypass`)
regexDropQuery, _ = regexp.Compile(`DROP.*mark match 0x18ba5`)
regexSystemRulesQuery, _ = regexp.Compile(systemRulePrefix + ".*")
systemChains = make(map[string]*fwRule)
)
// RunRule inserts or deletes a firewall rule.
func RunRule(action Action, enable bool, logError bool, rule []string) error {
if enable == false {
action = "-D"
}
rule = append([]string{string(action)}, rule...)
lock.Lock()
defer lock.Unlock()
_, err4 := core.Exec("iptables", rule)
_, err6 := core.Exec("ip6tables", rule)
if err4 != nil && err6 != nil {
if logError {
log.Error("Error while running firewall rule, ipv4 err: %s, ipv6 err: %s", err4, err6)
log.Error("rule: %s", rule)
}
return nil
} else if err4 != nil {
return err4
} else if err6 != nil {
return err6
}
return nil
}
// QueueDNSResponses redirects DNS responses to us, in order to keep a cache
// of resolved domains.
// INPUT --protocol udp --sport 53 -j NFQUEUE --queue-num 0 --queue-bypass
func QueueDNSResponses(enable bool, logError bool, qNum int) (err error) {
return RunRule(INSERT, enable, logError, []string{
"INPUT",
"--protocol", "udp",
"--sport", "53",
"-j", "NFQUEUE",
"--queue-num", fmt.Sprintf("%d", qNum),
"--queue-bypass",
})
}
// QueueConnections inserts the firewall rule which redirects connections to us.
// They 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 QueueConnections(enable bool, logError bool, qNum int) (err error) {
return RunRule(INSERT, enable, logError, []string{
"OUTPUT",
"-t", "mangle",
"-m", "conntrack",
"--ctstate", "NEW,RELATED",
"-j", "NFQUEUE",
"--queue-num", fmt.Sprintf("%d", qNum),
"--queue-bypass",
})
}
// DropMarked rejects packets marked by OpenSnitch.
// OUTPUT -m mark --mark 101285 -j DROP
func DropMarked(enable bool, logError bool) (err error) {
return RunRule(ADD, enable, logError, []string{
"OUTPUT",
"-m", "mark",
"--mark", fmt.Sprintf("%d", DropMark),
"-j", "DROP",
})
}
// CreateSystemRule create the custom firewall chains and adds them to system.
func CreateSystemRule(rule *fwRule, logErrors bool) {
chainName := systemRulePrefix + "-" + rule.Chain
if _, ok := systemChains[rule.Table+"-"+chainName]; ok {
return
}
RunRule(NEWCHAIN, true, logErrors, []string{chainName, "-t", rule.Table})
// Insert the rule at the top of the chain
if err := RunRule(INSERT, true, logErrors, []string{rule.Chain, "-t", rule.Table, "-j", chainName}); err == nil {
systemChains[rule.Table+"-"+chainName] = rule
}
}
// DeleteSystemRules deletes the system rules
func DeleteSystemRules(logErrors bool) {
for _, r := range fwConfig.SystemRules {
chain := systemRulePrefix + "-" + r.Rule.Chain
if _, ok := systemChains[r.Rule.Table+"-"+chain]; !ok {
continue
}
RunRule(FLUSH, true, logErrors, []string{chain, "-t", r.Rule.Table})
RunRule(DELETE, false, logErrors, []string{r.Rule.Chain, "-t", r.Rule.Table, "-j", chain})
RunRule(DELCHAIN, true, logErrors, []string{chain, "-t", r.Rule.Table})
delete(systemChains, r.Rule.Table+"-"+chain)
}
}
// AddSystemRule inserts a new rule.
func AddSystemRule(action Action, rule *fwRule, enable bool) (err error) {
chain := systemRulePrefix + "-" + rule.Chain
if rule.Table == "" {
rule.Table = "filter"
}
r := []string{chain, "-t", rule.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 RunRule(action, enable, true, r)
}
// AreRulesLoaded checks if the firewall rules are loaded.
func AreRulesLoaded() bool {
lock.Lock()
defer lock.Unlock()
outDrop, err := core.Exec("iptables", []string{"-n", "-L", "OUTPUT"})
if err != nil {
return false
}
outDrop6, err := core.Exec("ip6tables", []string{"-n", "-L", "OUTPUT"})
if err != nil {
return false
}
outMangle, err := core.Exec("iptables", []string{"-n", "-L", "OUTPUT", "-t", "mangle"})
if err != nil {
return false
}
outMangle6, err := core.Exec("ip6tables", []string{"-n", "-L", "OUTPUT", "-t", "mangle"})
if err != nil {
return false
}
systemRulesLoaded := true
if len(systemChains) > 0 {
for _, rule := range systemChains {
if chainOut4, err4 := core.Exec("iptables", []string{"-n", "-L", rule.Chain, "-t", rule.Table}); err4 == nil {
if regexSystemRulesQuery.FindString(chainOut4) == "" {
systemRulesLoaded = false
break
}
}
if chainOut6, err6 := core.Exec("ip6tables", []string{"-n", "-L", rule.Chain, "-t", rule.Table}); err6 == nil {
if regexSystemRulesQuery.FindString(chainOut6) == "" {
systemRulesLoaded = false
break
}
}
}
}
return regexRulesQuery.FindString(outMangle) != "" &&
regexRulesQuery.FindString(outMangle6) != "" &&
regexDropQuery.FindString(outDrop) != "" &&
regexDropQuery.FindString(outDrop6) != "" &&
systemRulesLoaded
}
// StartCheckingRules checks periodically if the rules are loaded.
// If they're not, we insert them again.
func StartCheckingRules() {
for {
select {
case <-rulesCheckerChan:
goto Exit
case <-rulesChecker.C:
if rules := AreRulesLoaded(); rules == false {
log.Important("firewall rules changed, reloading")
CleanRules(log.GetLogLevel() == log.DEBUG)
insertRules()
loadDiskConfiguration(true)
}
}
}
Exit:
log.Info("exit checking fw rules")
}
// StopCheckingRules stops checking if the firewall rules are loaded.
func StopCheckingRules() {
rulesChecker.Stop()
rulesCheckerChan <- true
}
// IsRunning returns if the firewall rules are loaded or not.
func IsRunning() bool {
return running
}
// CleanRules deletes the rules we added.
func CleanRules(logErrors bool) {
QueueDNSResponses(false, logErrors, queueNum)
QueueConnections(false, logErrors, queueNum)
DropMarked(false, logErrors)
DeleteSystemRules(logErrors)
}
func insertRules() {
if err := QueueDNSResponses(true, true, queueNum); err != nil {
log.Error("Error while running DNS firewall rule: %s", err)
} else if err = QueueConnections(true, true, queueNum); err != nil {
log.Fatal("Error while running conntrack firewall rule: %s", err)
} else if err = DropMarked(true, true); err != nil {
log.Fatal("Error while running drop firewall rule: %s", err)
}
}
// Stop deletes the firewall rules, allowing network traffic.
func Stop(qNum *int) {
if running == false {
return
}
if qNum != nil {
queueNum = *qNum
}
configWatcher.Close()
StopCheckingRules()
CleanRules(log.GetLogLevel() == log.DEBUG)
running = false
}
// Init inserts the firewall rules.
func Init(qNum *int) {
if running {
return
}
if qNum != nil {
queueNum = *qNum
}
insertRules()
if watcher, err := fsnotify.NewWatcher(); err == nil {
configWatcher = watcher
}
loadDiskConfiguration(false)
go StartCheckingRules()
running = true
}