mirror of
https://github.com/evilsocket/opensnitch.git
synced 2025-03-04 08:34:40 +01:00
misc: small fix or general refactoring i did not bother commenting
This commit is contained in:
parent
2a08686497
commit
534ec8cd73
21 changed files with 1195 additions and 7 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,7 +1,2 @@
|
|||
*.pyc
|
||||
build
|
||||
dist
|
||||
*.egg-info
|
||||
.idea
|
||||
*.swp
|
||||
*.sw*
|
||||
osgui
|
||||
osd
|
||||
|
|
7
Makefile
Normal file
7
Makefile
Normal 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
7
README.md
Normal 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
1
daemon/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
daemon
|
138
daemon/conman/connection.go
Normal file
138
daemon/conman/connection.go
Normal 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
56
daemon/core/core.go
Normal 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
78
daemon/dns/track.go
Normal 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
67
daemon/firewall/rules.go
Normal 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
150
daemon/log/log.go
Normal 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
160
daemon/main.go
Normal 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
27
daemon/netstat/entry.go
Normal 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
23
daemon/netstat/find.go
Normal 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
89
daemon/netstat/parse.go
Normal 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
78
daemon/procmon/parse.go
Normal 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
15
daemon/procmon/process.go
Normal 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
101
daemon/rule/loader.go
Normal 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
86
daemon/rule/rule.go
Normal 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
71
daemon/ui/client.go
Normal 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
13
rules/block_chrome.json
Normal 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
13
rules/block_firefox.json
Normal 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
13
rules/systemd.json
Normal 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"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue