misc: small fix or general refactoring i did not bother commenting

This commit is contained in:
evilsocket 2018-04-02 05:25:32 +02:00
parent 2a08686497
commit 534ec8cd73
Failed to generate hash of commit
21 changed files with 1195 additions and 7 deletions

9
.gitignore vendored
View file

@ -1,7 +1,2 @@
*.pyc
build
dist
*.egg-info
.idea
*.swp
*.sw*
osgui
osd

7
Makefile Normal file
View file

@ -0,0 +1,7 @@
all: osd
osd:
go build -o osd github.com/evilsocket/opensnitch/daemon
clean:
rm -rf osd ui

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# OpenSnitch
OpenSnitch is a GNU/Linux port of the Little Snitch application firewall.
## Work in Progress
This branch is the reimplementation in Go of the project which is still work in progress, for the previous Python version, checkout the [python-poc](https://github.com/evilsocket/opensnitch/tree/python-poc) branch.

1
daemon/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
daemon

138
daemon/conman/connection.go Normal file
View file

@ -0,0 +1,138 @@
package conman
import (
"fmt"
"net"
"github.com/evilsocket/opensnitch/daemon/dns"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/netstat"
"github.com/evilsocket/opensnitch/daemon/procmon"
"github.com/evilsocket/go-netfilter-queue"
"github.com/google/gopacket/layers"
)
type Connection struct {
Protocol string
SrcIP net.IP
SrcPort int
DstIP net.IP
DstPort int
DstHost string
Entry *netstat.Entry
Process *procmon.Process
pkt *netfilter.NFPacket
}
func Parse(nfp netfilter.NFPacket) *Connection {
ipLayer := nfp.Packet.Layer(layers.LayerTypeIPv4)
if ipLayer == nil {
return nil
}
ip, ok := ipLayer.(*layers.IPv4)
if ok == false || ip == nil {
return nil
}
// we're not interested in connections
// from/to the localhost interface
if ip.SrcIP.IsLoopback() {
return nil
}
// skip multicast stuff
if ip.SrcIP.IsMulticast() || ip.DstIP.IsMulticast() {
return nil
}
// skip broadcasted stuff
// FIXME: this is ugly
if ip.DstIP[3] == 0xff {
return nil
}
con, err := NewConnection(&nfp, ip)
if err != nil {
log.Warning("%s", err)
return nil
} else if con == nil {
return nil
}
return con
}
func (c *Connection) checkLayers() bool {
for _, layer := range c.pkt.Packet.Layers() {
if layer.LayerType() == layers.LayerTypeTCP {
if tcp, ok := layer.(*layers.TCP); ok == true && tcp != nil {
c.Protocol = "tcp"
c.DstPort = int(tcp.DstPort)
c.SrcPort = int(tcp.SrcPort)
return true
}
} else if layer.LayerType() == layers.LayerTypeUDP {
if udp, ok := layer.(*layers.UDP); ok == true && udp != nil {
c.Protocol = "udp"
c.DstPort = int(udp.DstPort)
c.SrcPort = int(udp.SrcPort)
return true
}
}
}
return false
}
func NewConnection(nfp *netfilter.NFPacket, ip *layers.IPv4) (c *Connection, err error) {
c = &Connection{
SrcIP: ip.SrcIP,
DstIP: ip.DstIP,
DstHost: dns.HostOr(ip.DstIP, ""),
pkt: nfp,
}
// no errors but not enough info neither
if c.checkLayers() == false {
return nil, nil
}
// Lookup uid and inode using /proc/net/(udp|tcp)
if c.Entry = netstat.FindEntry(c.Protocol, c.SrcIP, c.SrcPort, c.DstIP, c.DstPort); c.Entry == nil {
return nil, fmt.Errorf("Could not find netstat entry for: %s", c)
}
// snapshot a map of: inode -> pid
sockets := procmon.GetOpenSockets()
// lookup pid by inode and process by pid
if pid, found := sockets[c.Entry.INode]; found == false {
return nil, fmt.Errorf("Could not find process id for: %s", c)
} else if c.Process = procmon.FindProcess(pid); c.Process == nil {
return nil, fmt.Errorf("Could not find process by its pid %d for: %s", pid, c)
}
return c, nil
}
func (c *Connection) To() string {
if c.DstHost == "" {
return c.DstIP.String()
}
return c.DstHost
}
func (c *Connection) String() string {
if c.Entry == nil {
return fmt.Sprintf("%s ->(%s)-> %s:%d", c.SrcIP, c.Protocol, c.To(), c.DstPort)
}
if c.Process == nil {
return fmt.Sprintf("%s (uid:%d) ->(%s)-> %s:%d", c.SrcIP, c.Entry.UserId, c.Protocol, c.To(), c.DstPort)
}
return fmt.Sprintf("%s (%d) -> %s:%d (proto:%s uid:%d)", c.Process.Path, c.Process.ID, c.To(), c.DstPort, c.Protocol, c.Entry.UserId)
}

56
daemon/core/core.go Normal file
View file

@ -0,0 +1,56 @@
package core
import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
)
const (
defaultTrimSet = "\r\n\t "
)
func Trim(s string) string {
return strings.Trim(s, defaultTrimSet)
}
func Exec(executable string, args []string) (string, error) {
path, err := exec.LookPath(executable)
if err != nil {
return "", err
}
raw, err := exec.Command(path, args...).CombinedOutput()
if err != nil {
fmt.Printf("ERROR: path=%s args=%s err=%s out='%s'\n", path, args, err, raw)
return "", err
} else {
return Trim(string(raw)), nil
}
}
func Exists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
func ExpandPath(path string) (string, error) {
// Check if path is empty
if path != "" {
if strings.HasPrefix(path, "~") {
usr, err := user.Current()
if err != nil {
return "", err
}
// Replace only the first occurrence of ~
path = strings.Replace(path, "~", usr.HomeDir, 1)
}
return filepath.Abs(path)
}
return "", nil
}

78
daemon/dns/track.go Normal file
View file

@ -0,0 +1,78 @@
package dns
import (
"net"
"sync"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
var (
responses = make(map[string]string, 0)
lock = sync.Mutex{}
)
func TrackAnswers(packet gopacket.Packet) bool {
udpLayer := packet.Layer(layers.LayerTypeUDP)
if udpLayer == nil {
return false
}
udp, ok := udpLayer.(*layers.UDP)
if ok == false || udp == nil {
return false
}
if udp.SrcPort != 53 {
return false
}
dnsLayer := packet.Layer(layers.LayerTypeDNS)
if dnsLayer == nil {
return false
}
dnsAns, ok := dnsLayer.(*layers.DNS)
if ok == false || dnsAns == nil {
return false
}
for _, ans := range dnsAns.Answers {
if ans.Name != nil && ans.IP != nil {
Track(ans.IP, string(ans.Name))
}
}
return true
}
func Track(ip net.IP, hostname string) {
address := ip.String()
lock.Lock()
defer lock.Unlock()
responses[address] = hostname
log.Debug("New DNS record: %s -> %s", address, hostname)
}
func Host(ip net.IP) (host string, found bool) {
address := ip.String()
lock.Lock()
defer lock.Unlock()
host, found = responses[address]
return
}
func HostOr(ip net.IP, or string) string {
if host, found := Host(ip); found == true {
return host
}
return or
}

67
daemon/firewall/rules.go Normal file
View file

@ -0,0 +1,67 @@
package firewall
import (
"fmt"
"sync"
"github.com/evilsocket/opensnitch/daemon/core"
)
const DropMark = 0x18BA5
// make sure we don't mess with multiple rules
// at the same time
var lock = sync.Mutex{}
func RunRule(enable bool, rule []string) (err error) {
action := "-A"
if enable == false {
action = "-D"
}
rule = append([]string{action}, rule...)
lock.Lock()
defer lock.Unlock()
// fmt.Printf("iptables %s\n", rule)
_, err = core.Exec("iptables", rule)
return
}
// INPUT --protocol udp --sport 53 -j NFQUEUE --queue-num 0 --queue-bypass
func QueueDNSResponses(enable bool, queueNum int) (err error) {
return RunRule(enable, []string{
"INPUT",
"--protocol", "udp",
"--sport", "53",
"-j", "NFQUEUE",
"--queue-num", fmt.Sprintf("%d", queueNum),
"--queue-bypass",
})
}
// OUTPUT -t mangle -m conntrack --ctstate NEW -j NFQUEUE --queue-num 0 --queue-bypass
func QueueConnections(enable bool, queueNum int) (err error) {
return RunRule(enable, []string{
"OUTPUT",
"-t", "mangle",
"-m", "conntrack",
"--ctstate", "NEW",
"-j", "NFQUEUE",
"--queue-num", fmt.Sprintf("%d", queueNum),
"--queue-bypass",
})
}
// Reject packets marked by OpenSnitch
// OUTPUT -m mark --mark 101285 -j REJECT
func RejectMarked(enable bool) (err error) {
return RunRule(enable, []string{
"OUTPUT",
"-m", "mark",
"--mark", fmt.Sprintf("%d", DropMark),
"-j", "REJECT",
})
}

150
daemon/log/log.go Normal file
View file

@ -0,0 +1,150 @@
package log
import (
"fmt"
"os"
"strings"
"sync"
"time"
)
type Handler func(format string, args ...interface{})
// https://misc.flogisoft.com/bash/tip_colors_and_formatting
const (
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
BLUE = "\033[34m"
YELLOW = "\033[33m"
FG_BLACK = "\033[30m"
FG_WHITE = "\033[97m"
BG_DGRAY = "\033[100m"
BG_RED = "\033[41m"
BG_GREEN = "\033[42m"
BG_YELLOW = "\033[43m"
BG_LBLUE = "\033[104m"
RESET = "\033[0m"
)
const (
DEBUG = iota
INFO
IMPORTANT
WARNING
ERROR
FATAL
)
var (
WithColors = true
Output = os.Stderr
DateFormat = "2006-01-02 15:04:05"
MinLevel = INFO
mutex = &sync.Mutex{}
labels = map[int]string{
DEBUG: "DBG",
INFO: "INF",
IMPORTANT: "IMP",
WARNING: "WAR",
ERROR: "ERR",
FATAL: "!!!",
}
colors = map[int]string{
DEBUG: DIM + FG_BLACK + BG_DGRAY,
INFO: FG_WHITE + BG_GREEN,
IMPORTANT: FG_WHITE + BG_LBLUE,
WARNING: FG_WHITE + BG_YELLOW,
ERROR: FG_WHITE + BG_RED,
FATAL: FG_WHITE + BG_RED + BOLD,
}
)
func Wrap(s, effect string) string {
if WithColors == true {
s = effect + s + RESET
}
return s
}
func Dim(s string) string {
return Wrap(s, DIM)
}
func Bold(s string) string {
return Wrap(s, BOLD)
}
func Red(s string) string {
return Wrap(s, RED)
}
func Green(s string) string {
return Wrap(s, GREEN)
}
func Blue(s string) string {
return Wrap(s, BLUE)
}
func Yellow(s string) string {
return Wrap(s, YELLOW)
}
func Raw(s string) {
mutex.Lock()
defer mutex.Unlock()
fmt.Fprintf(Output, "%s", s)
}
func Log(level int, format string, args ...interface{}) {
if level >= MinLevel {
mutex.Lock()
defer mutex.Unlock()
label := labels[level]
color := colors[level]
when := time.Now().UTC().Format(DateFormat)
what := fmt.Sprintf(format, args...)
if strings.HasSuffix(what, "\n") == false {
what += "\n"
}
l := Dim("[%s]")
r := Wrap(" %s ", color) + " %s"
fmt.Printf(l+" "+r, when, label, what)
}
}
func Debug(format string, args ...interface{}) {
Log(DEBUG, format, args...)
}
func Info(format string, args ...interface{}) {
Log(INFO, format, args...)
}
func Important(format string, args ...interface{}) {
Log(IMPORTANT, format, args...)
}
func Warning(format string, args ...interface{}) {
Log(WARNING, format, args...)
}
func Error(format string, args ...interface{}) {
Log(ERROR, format, args...)
}
func Fatal(format string, args ...interface{}) {
Log(FATAL, format, args...)
os.Exit(1)
}

160
daemon/main.go Normal file
View file

@ -0,0 +1,160 @@
package main
import (
"flag"
"io/ioutil"
golog "log"
"os"
"os/signal"
"syscall"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/dns"
"github.com/evilsocket/opensnitch/daemon/firewall"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/rule"
"github.com/evilsocket/opensnitch/daemon/ui"
"github.com/evilsocket/go-netfilter-queue"
)
var (
rulesPath = "rules"
queueNum = 0
workers = 16
uiSocketPath = "osui.sock"
uiClient = (*ui.Client)(nil)
err = (error)(nil)
rules = rule.NewLoader()
queue = (*netfilter.NFQueue)(nil)
pktChan = (<-chan netfilter.NFPacket)(nil)
wrkChan = (chan netfilter.NFPacket)(nil)
sigChan = (chan os.Signal)(nil)
)
func init() {
flag.StringVar(&uiSocketPath, "ui-socket-path", uiSocketPath, "UNIX socket of the UI gRPC service.")
flag.StringVar(&rulesPath, "rules-path", rulesPath, "Path to load JSON rules from.")
flag.IntVar(&queueNum, "queue-num", queueNum, "Netfilter queue number.")
flag.IntVar(&workers, "workers", workers, "Number of concurrent workers.")
}
func setupSignals() {
sigChan = make(chan os.Signal, 1)
signal.Notify(sigChan,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)
go func() {
sig := <-sigChan
log.Raw("\n")
log.Important("Got signal: %v", sig)
doCleanup()
os.Exit(0)
}()
}
func worker(id int) {
log.Debug("Worker #%d started.", id)
for true {
select {
case pkt := <-wrkChan:
onPacket(pkt)
}
}
}
func setupWorkers() {
log.Info("Starting %d workers ...", workers)
// setup the workers
wrkChan = make(chan netfilter.NFPacket)
for i := 0; i < workers; i++ {
go worker(i)
}
}
func doCleanup() {
log.Info("Cleaning up ...")
firewall.QueueDNSResponses(false, queueNum)
firewall.QueueConnections(false, queueNum)
firewall.RejectMarked(false)
}
func onPacket(packet netfilter.NFPacket) {
// DNS response, just parse, track and accept.
if dns.TrackAnswers(packet.Packet) == true {
packet.SetVerdict(netfilter.NF_ACCEPT)
return
}
// Parse the connection state
con := conman.Parse(packet)
if con == nil {
packet.SetVerdict(netfilter.NF_ACCEPT)
return
}
r := rules.FindFirstMatch(con)
// no rule matched, prompt the user
if r == nil {
r = uiClient.Ask(con)
}
if r.Action == rule.Allow {
packet.SetVerdict(netfilter.NF_ACCEPT)
ruleName := log.Green(r.Name)
if r.Rule.What == rule.OpTrue {
ruleName = log.Dim(r.Name)
}
log.Info("%s %s -> %s:%d (%s)", log.Bold(log.Green("✔")), log.Bold(con.Process.Path), log.Bold(con.To()), con.DstPort, ruleName)
return
}
packet.SetVerdict(netfilter.NF_DROP)
log.Warning("%s %s -> %s:%d (%s)", log.Bold(log.Red("✘")), log.Bold(con.Process.Path), log.Bold(con.To()), con.DstPort, log.Red(r.Name))
}
func main() {
golog.SetOutput(ioutil.Discard)
flag.Parse()
setupSignals()
setupWorkers()
log.Info("Loading rules from %s ...", rulesPath)
if err := rules.Load(rulesPath); err != nil {
log.Fatal("%s", err)
}
log.Info("Loaded %d rules.", rules.NumRules())
// prepare the queue
queue, err := netfilter.NewNFQueue(uint16(queueNum), 4096, netfilter.NF_DEFAULT_PACKET_SIZE)
if err != nil {
log.Fatal("Error while creating queue #%d: %s", queueNum, err)
}
pktChan = queue.GetPackets()
// queue is ready, run firewall rules
if err = firewall.QueueDNSResponses(true, queueNum); err != nil {
log.Fatal("Error while running DNS firewall rule: %s", err)
} else if err = firewall.QueueConnections(true, queueNum); err != nil {
log.Fatal("Error while running conntrack firewall rule: %s", err)
} else if err = firewall.RejectMarked(true); err != nil {
log.Fatal("Error while running reject firewall rule: %s", err)
}
uiClient = ui.NewClient(uiSocketPath)
log.Info("Running on netfilter queue #%d ...", queueNum)
for true {
select {
case pkt := <-pktChan:
wrkChan <- pkt
}
}
}

27
daemon/netstat/entry.go Normal file
View file

@ -0,0 +1,27 @@
package netstat
import (
"net"
)
type Entry struct {
Proto string
SrcIP net.IP
SrcPort int
DstIP net.IP
DstPort int
UserId int
INode int
}
func NewEntry(proto string, srcIP net.IP, srcPort int, dstIP net.IP, dstPort int, userId int, iNode int) Entry {
return Entry{
Proto: proto,
SrcIP: srcIP,
SrcPort: srcPort,
DstIP: dstIP,
DstPort: dstPort,
UserId: userId,
INode: iNode,
}
}

23
daemon/netstat/find.go Normal file
View file

@ -0,0 +1,23 @@
package netstat
import (
"net"
"github.com/evilsocket/opensnitch/daemon/log"
)
func FindEntry(proto string, srcIP net.IP, srcPort int, dstIP net.IP, dstPort int) *Entry {
entries, err := Parse(proto)
if err != nil {
log.Warning("Error while searching for %s netstat entry: %s", proto, err)
return nil
}
for _, entry := range entries {
if srcIP.Equal(entry.SrcIP) && srcPort == entry.SrcPort && dstIP.Equal(entry.DstIP) && dstPort == entry.DstPort {
return &entry
}
}
return nil
}

89
daemon/netstat/parse.go Normal file
View file

@ -0,0 +1,89 @@
package netstat
import (
"bufio"
"encoding/binary"
"fmt"
"net"
"os"
"regexp"
"strconv"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
)
var (
parser = regexp.MustCompile(`(?i)` +
`\d+:\s+` + // sl
`([a-f0-9]{8}):([a-f0-9]{4})\s+` + // local_address
`([a-f0-9]{8}):([a-f0-9]{4})\s+` + // rem_address
`[a-f0-9]{2}\s+` + // st
`[a-f0-9]{8}:[a-f0-9]{8}\s+` + // tx_queue rx_queue
`[a-f0-9]{2}:[a-f0-9]{8}\s+` + // tr tm->when
`[a-f0-9]{8}\s+` + // retrnsmt
`(\d+)\s+` + // uid
`\d+\s+` + // timeout
`(\d+)\s+` + // inode
`.+`) // stuff we don't care about
)
func decToInt(n string) int {
d, err := strconv.ParseInt(n, 10, 64)
if err != nil {
log.Fatal("Error while parsing %s to int: %s", n, err)
}
return int(d)
}
func hexToInt(h string) int {
d, err := strconv.ParseInt(h, 16, 64)
if err != nil {
log.Fatal("Error while parsing %s to int: %s", h, err)
}
return int(d)
}
func hexToIP(h string) net.IP {
n := hexToInt(h)
ip := make(net.IP, 4)
binary.LittleEndian.PutUint32(ip, uint32(n))
return ip
}
func Parse(proto string) ([]Entry, error) {
filename := fmt.Sprintf("/proc/net/%s", proto)
fd, err := os.Open(filename)
if err != nil {
return nil, err
}
defer fd.Close()
entries := make([]Entry, 0)
scanner := bufio.NewScanner(fd)
for lineno := 0; scanner.Scan(); lineno++ {
// skip column names
if lineno == 0 {
continue
}
line := core.Trim(scanner.Text())
m := parser.FindStringSubmatch(line)
if m == nil {
log.Warning("Could not parse netstat line from %s: %s", filename, line)
continue
}
entries = append(entries, NewEntry(
proto,
hexToIP(m[1]),
hexToInt(m[2]),
hexToIP(m[3]),
hexToInt(m[4]),
decToInt(m[5]),
decToInt(m[6]),
))
}
return entries, nil
}

78
daemon/procmon/parse.go Normal file
View file

@ -0,0 +1,78 @@
package procmon
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/evilsocket/opensnitch/daemon/core"
)
// [inode] -> pid
func GetOpenSockets() map[int]int {
m := make(map[int]int)
ls, err := ioutil.ReadDir("/proc/")
if err == nil {
for _, f := range ls {
// check if it's a folder to skip atoi if not needed
if f.IsDir() == false {
continue
} else if pid, err := strconv.Atoi(f.Name()); err == nil {
// loop process descriptors
path := fmt.Sprintf("/proc/%s/fd/", f.Name())
descriptors, err := ioutil.ReadDir(path)
if err == nil {
for _, desc := range descriptors {
descLink := fmt.Sprintf("%s%s", path, desc.Name())
// resolve the symlink and compare to what we expect
if link, err := os.Readlink(descLink); err == nil {
// only consider sockets
if strings.HasPrefix(link, "socket:[") == true {
socket := link[8 : len(link)-1]
inode, err := strconv.Atoi(socket)
if err == nil {
m[inode] = pid
}
}
}
}
}
}
}
}
return m
}
func FindProcess(pid int) *Process {
linkName := fmt.Sprintf("/proc/%d/exe", pid)
if core.Exists(linkName) == false {
return nil
}
if link, err := os.Readlink(linkName); err == nil && core.Exists(link) == true {
proc := NewProcess(pid, link)
if data, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)); err == nil {
for i, b := range data {
if b == 0x00 {
data[i] = byte(' ')
}
}
args := strings.Split(string(data), " ")
for _, arg := range args {
arg = core.Trim(arg)
if arg != "" {
proc.Args = append(proc.Args, arg)
}
}
}
return proc
}
return nil
}

15
daemon/procmon/process.go Normal file
View file

@ -0,0 +1,15 @@
package procmon
type Process struct {
ID int
Path string
Args []string
}
func NewProcess(pid int, path string) *Process {
return &Process{
ID: pid,
Path: path,
Args: make([]string, 0),
}
}

101
daemon/rule/loader.go Normal file
View file

@ -0,0 +1,101 @@
package rule
import (
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"sync"
"time"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
)
type Loader struct {
sync.RWMutex
path string
rules []*Rule
}
func NewLoader() *Loader {
return &Loader{
path: "",
rules: make([]*Rule, 0),
}
}
func (l *Loader) NumRules() int {
l.RLock()
defer l.RUnlock()
return len(l.rules)
}
func (l *Loader) Load(path string) error {
if core.Exists(path) == false {
return fmt.Errorf("Path '%s' does not exist.", path)
}
expr := filepath.Join(path, "*.json")
matches, err := filepath.Glob(expr)
if err != nil {
return fmt.Errorf("Error globbing '%s': %s", expr, err)
}
l.Lock()
defer l.Unlock()
l.path = path
l.rules = make([]*Rule, 0)
for _, fileName := range matches {
raw, err := ioutil.ReadFile(fileName)
if err != nil {
return fmt.Errorf("Error while reading %s: %s", fileName, err)
}
var r Rule
err = json.Unmarshal(raw, &r)
if err != nil {
return fmt.Errorf("Error while parsing rule from %s: %s", fileName, err)
}
log.Debug("Loaded rule from %s: %s", fileName, r.String())
l.rules = append(l.rules, &r)
}
return nil
}
func (l *Loader) Reload() error {
return l.Load(l.path)
}
func (l *Loader) Save(rule *Rule, path string) error {
rule.Updated = time.Now()
raw, err := json.Marshal(rule)
if err != nil {
return fmt.Errorf("Error while saving rule %s to %s: %s", rule, err)
}
if err = ioutil.WriteFile(path, raw, 0644); err != nil {
return fmt.Errorf("Error while saving rule %s to %s: %s", rule, err)
}
return nil
}
func (l *Loader) FindFirstMatch(con *conman.Connection) *Rule {
l.RLock()
defer l.RUnlock()
for _, rule := range l.rules {
if rule.Match(con) == true {
return rule
}
}
return nil
}

86
daemon/rule/rule.go Normal file
View file

@ -0,0 +1,86 @@
package rule
import (
"fmt"
"time"
"github.com/evilsocket/opensnitch/daemon/conman"
)
type OperandType string
const (
OpTrue = OperandType("true")
OpProcessPath = OperandType("process.path")
OpDstIP = OperandType("dest.ip")
OpDstHost = OperandType("dest.host")
)
type Cmp struct {
What OperandType
With string
}
type Action string
const (
Allow = Action("allow")
Deny = Action("deny")
)
type Duration string
const (
Once = Duration("once")
Restart = Duration("until restart")
Always = Duration("always")
)
type Type string
const (
Simple = Type("simple")
Complex = Type("complex") // for future use
)
type Rule struct {
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Action Action `json:"action"`
Duration Duration `json:"duration"`
Type Type `json:"type"`
Rule Cmp `json:"rule"`
}
func Create(name string, action Action, duration Duration, rule Cmp) *Rule {
return &Rule{
Created: time.Now(),
Enabled: true,
Name: name,
Action: action,
Duration: duration,
Type: Simple,
Rule: rule,
}
}
func (r *Rule) String() string {
return fmt.Sprintf("%s: if(%s == '%s'){ %s %s }", r.Name, r.Rule.What, r.Rule.With, r.Action, r.Duration)
}
func (r *Rule) Match(con *conman.Connection) bool {
if r.Enabled == false {
return false
} else if r.Rule.What == OpTrue {
return true
} else if r.Rule.What == OpProcessPath {
return con.Process.Path == r.Rule.With
} else if r.Rule.What == OpDstIP {
return con.DstIP.String() == r.Rule.With
} else if r.Rule.What == OpDstHost {
return con.DstHost == r.Rule.With
}
return false
}

71
daemon/ui/client.go Normal file
View file

@ -0,0 +1,71 @@
package ui
import (
"net"
"sync"
"time"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/rule"
"google.golang.org/grpc"
)
var clientDisconnectedRule = rule.Create("ui.client.disconnected", rule.Allow, rule.Once, rule.Cmp{
What: rule.OpTrue,
})
type Client struct {
sync.Mutex
socketPath string
con *grpc.ClientConn
}
func NewClient(path string) *Client {
c := &Client{
socketPath: path,
}
go c.poller()
return c
}
func (c *Client) poller() {
log.Debug("UI service poller started for socket %s", c.socketPath)
t := time.NewTicker(time.Second * 1)
for _ = range t.C {
err := c.connect()
if err != nil {
log.Warning("Error while connecting to UI service: %s", err)
}
}
}
func (c *Client) connect() (err error) {
c.Lock()
defer c.Unlock()
if c.con != nil {
return
}
c.con, err = grpc.Dial(c.socketPath, grpc.WithInsecure(),
grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
return net.DialTimeout("unix", addr, timeout)
}))
if err != nil {
c.con = nil
}
return err
}
func (c *Client) Ask(con *conman.Connection) *rule.Rule {
c.Lock()
defer c.Unlock()
// TODO: if connected, send request
return clientDisconnectedRule
}

13
rules/block_chrome.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "Block Chrome",
"enabled": false,
"created": "2018-04-02T03:27:08.137712802+02:00",
"updated": "2018-04-02T03:27:08.137713274+02:00",
"action": "block",
"duration": "forever",
"type": "simple",
"rule": {
"what": "process.path",
"with": "/opt/google/chrome/chrome"
}
}

13
rules/block_firefox.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "Block Firefox",
"enabled": true,
"created": "2018-04-02T03:27:08.137712802+02:00",
"updated": "2018-04-02T03:27:08.137713274+02:00",
"action": "block",
"duration": "forever",
"type": "simple",
"rule": {
"what": "process.path",
"with": "/usr/lib/firefox/firefox"
}
}

13
rules/systemd.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "whitelist systemd-resolved",
"enabled": true,
"created": "2018-04-02T03:27:08.137712802+02:00",
"updated": "2018-04-02T03:27:08.137713274+02:00",
"action": "allow",
"duration": "forever",
"type": "simple",
"rule": {
"what": "process.path",
"with": "/lib/systemd/systemd-resolved"
}
}