├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── daemon ├── .gitignore ├── Gopkg.toml ├── Makefile ├── conman │ └── connection.go ├── core │ ├── core.go │ ├── system.go │ └── version.go ├── default-config.json ├── dns │ ├── parse.go │ └── track.go ├── firewall │ ├── config.go │ └── rules.go ├── go.mod ├── log │ └── log.go ├── main.go ├── netfilter │ ├── packet.go │ ├── queue.c │ ├── queue.go │ └── queue.h ├── netlink │ ├── socket.go │ └── socket_linux.go ├── netstat │ ├── entry.go │ ├── find.go │ └── parse.go ├── opensnitch.spec ├── opensnitchd.service ├── procmon │ ├── audit │ │ ├── client.go │ │ └── parse.go │ ├── cache.go │ ├── details.go │ ├── find.go │ ├── find_test.go │ ├── parse.go │ ├── process.go │ ├── process_test.go │ └── watcher.go ├── rule │ ├── loader.go │ ├── loader_test.go │ ├── operator.go │ ├── operator_test.go │ ├── rule.go │ ├── rule_test.go │ └── testdata │ │ ├── 000-allow-chrome.json │ │ ├── 001-deny-chrome.json │ │ └── live_reload │ │ ├── test-live-reload-delete.json │ │ └── test-live-reload-remove.json ├── statistics │ ├── event.go │ └── stats.go ├── system-fw.json └── ui │ ├── client.go │ ├── config.go │ ├── notifications.go │ └── protocol │ ├── .gitkeep │ └── ui.pb.go ├── debian ├── changelog ├── control ├── copyright ├── gbp.conf ├── gitlab-ci.yml ├── opensnitch.init ├── opensnitch.install ├── opensnitch.logrotate ├── opensnitch.service ├── postinst ├── prerm ├── rules ├── source │ └── format └── watch ├── make_ads_rules.py ├── proto ├── .gitignore ├── Makefile └── ui.proto ├── release.sh ├── screenshots ├── opensnitch-ui-general-tab-deny.png ├── opensnitch-ui-proc-details.png └── screenshot.png └── ui ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── bin └── opensnitch-ui ├── debian ├── changelog ├── compat ├── config ├── control ├── copyright ├── postinst ├── postrm ├── prerm ├── rules ├── source │ ├── format │ └── options └── templates ├── i18n ├── Makefile ├── README.md ├── generate_i18n.sh ├── locales │ ├── es_ES │ │ └── opensnitch-es_ES.ts │ └── eu_ES │ │ └── opensnitch-eu_ES.ts └── opensnitch_i18n.pro ├── opensnitch-ui.spec ├── opensnitch ├── __init__.py ├── config.py ├── customwidgets.py ├── database.py ├── desktop_parser.py ├── dialogs │ ├── __init__.py │ ├── preferences.py │ ├── processdetails.py │ ├── prompt.py │ ├── ruleseditor.py │ └── stats.py ├── nodes.py ├── res │ ├── __init__.py │ ├── icon-alert.png │ ├── icon-off.png │ ├── icon-red.png │ ├── icon-white.png │ ├── icon-white.svg │ ├── icon.png │ ├── preferences.ui │ ├── process_details.ui │ ├── prompt.ui │ ├── resources.qrc │ ├── ruleseditor.ui │ └── stats.ui ├── resources_rc.py ├── service.py ├── ui_pb2.py ├── ui_pb2_grpc.py └── version.py ├── requirements.txt ├── resources ├── kcm_opensnitch.desktop ├── opensnitch-ui.png ├── opensnitch-ui.svg └── opensnitch_ui.desktop └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: evilsocket 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Present yourself (or at least say "Hello" or "Hi") and be kind && respectful. 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Describe in detail as much as you can what happened. 17 | 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Post error logs:** 25 | If it's a crash of the GUI: 26 | - Launch it from a terminal and reproduce the issue. 27 | - Post the errors logged to the terminal. 28 | 29 | If the daemon doesn't start: 30 | - Post last 15 lines of the log file `/var/log/opensnitchd.log` 31 | - Or launch it from a terminal (`/usr/bin/opensnitchd -rules-path /etc/opensnitchd/rules`) and post the errors logged to the terminal. 32 | 33 | If the deb or rpm packages fail to install: 34 | - Install them from a terminal (`dpkg -i opensnitch*` / `yum install opensnitch*`), and post the errors logged to stdout. 35 | 36 | **Expected behavior (optional)** 37 | A clear and concise description of what you expected to happen. 38 | 39 | **Screenshots** 40 | If applicable, add screenshots to help explain your problem. 41 | 42 | **OS (please complete the following information):** 43 | - OS: [e.g. Debian GNU/Linux, ArchLinux, Slackware, ...] 44 | - Window Manager: [e.g. GNOME shell, KDE, enlightenment, ...] 45 | - Kernel version: echo $(uname -a) 46 | - Version [e.g. Buster, 10.3, 20.04] 47 | 48 | **Additional context** 49 | Add any other context about the problem here. 50 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Build status 2 | on: [push, pull_request] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v2 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | if [ -f Gopkg.toml ]; then 23 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 24 | dep ensure 25 | fi 26 | sudo apt-get install git libnetfilter-queue-dev libmnl-dev libpcap-dev protobuf-compiler 27 | 28 | - name: Build 29 | run: | 30 | cd daemon 31 | go build -v . 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sock 2 | *.pyc 3 | *.profile 4 | rules 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: protocol daemon/opensnitchd ui/resources_rc.py 2 | 3 | install: 4 | @cd daemon && make install 5 | @cd ui && make install 6 | 7 | protocol: 8 | @cd proto && make 9 | 10 | daemon/opensnitchd: 11 | @cd daemon && make 12 | 13 | ui/resources_rc.py: 14 | @cd ui && make 15 | 16 | deps: 17 | @cd daemon && make deps 18 | @cd ui && make deps 19 | 20 | clean: 21 | @cd daemon && make clean 22 | @cd proto && make clean 23 | 24 | run: 25 | cd ui && pip3 install --upgrade . && cd .. 26 | opensnitch-ui --socket unix:///tmp/osui.sock & 27 | ./daemon/opensnitchd -rules-path /etc/opensnitchd/rules -ui-socket unix:///tmp/osui.sock -cpu-profile cpu.profile -mem-profile mem.profile 28 | 29 | test: 30 | clear 31 | make clean 32 | clear 33 | mkdir -p rules 34 | make 35 | clear 36 | make run 37 | 38 | adblocker: 39 | clear 40 | make clean 41 | clear 42 | make 43 | clear 44 | python make_ads_rules.py 45 | clear 46 | cd ui && pip3 install --upgrade . && cd .. 47 | opensnitch-ui --socket unix:///tmp/osui.sock & 48 | ./daemon/opensnitchd -rules-path /etc/opensnitchd/rules -ui-socket unix:///tmp/osui.sock 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | UPDATE: development of Opensnitch has moved to a new place https://github.com/evilsocket/opensnitch 2 | 3 | The code in this repository is outdated. 4 | 5 | Please start any discussions / open issues in the new repository. 6 | 7 | 8 |

9 | opensnitch 10 |

11 | 12 | Release 13 | Software License 14 | Go Report Card 15 |

16 |

17 | 18 | **OpenSnitch** is a GNU/Linux application firewall. 19 | 20 |

21 | OpenSnitch 22 |

23 | 24 | ### Installation and configuration 25 | 26 | Please, refer to [the documentation](https://github.com/gustavo-iniguez-goya/opensnitch/wiki) for detailed information. 27 | 28 | ### Contributors 29 | 30 | [See the list](https://github.com/gustavo-iniguez-goya/opensnitch/graphs/contributors) 31 | 32 | ### Disclaimer 33 | 34 | THIS SOFTWARE IS A WORK IN PROGRESS, DO NOT EXPECT IT TO BE BUG FREE AND DO NOT RELY ON IT FOR ANY TYPE OF SECURITY. 35 | -------------------------------------------------------------------------------- /daemon/.gitignore: -------------------------------------------------------------------------------- 1 | opensnitchd 2 | vendor 3 | -------------------------------------------------------------------------------- /daemon/Gopkg.toml: -------------------------------------------------------------------------------- 1 | [[constraint]] 2 | name = "github.com/fsnotify/fsnotify" 3 | version = "1.4.7" 4 | 5 | [[constraint]] 6 | name = "github.com/google/gopacket" 7 | version = "~1.1.14" 8 | 9 | [[constraint]] 10 | name = "google.golang.org/grpc" 11 | version = "~1.11.2" 12 | 13 | [[constraint]] 14 | name = "github.com/evilsocket/ftrace" 15 | version = "~1.2.0" 16 | 17 | [prune] 18 | go-tests = true 19 | unused-packages = true 20 | -------------------------------------------------------------------------------- /daemon/Makefile: -------------------------------------------------------------------------------- 1 | all: opensnitchd 2 | 3 | install: 4 | @mkdir -p /etc/opensnitchd/rules 5 | @cp opensnitchd /usr/local/bin/ 6 | @cp opensnitchd.service /etc/systemd/system/ 7 | @cp default-config.json /etc/opensnitchd/ 8 | @cp system-fw.json /etc/opensnitchd/ 9 | @systemctl daemon-reload 10 | 11 | opensnitchd: 12 | @go build -o opensnitchd . 13 | 14 | clean: 15 | @rm -rf opensnitchd 16 | 17 | 18 | -------------------------------------------------------------------------------- /daemon/conman/connection.go: -------------------------------------------------------------------------------- 1 | package conman 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | 9 | "github.com/evilsocket/opensnitch/daemon/core" 10 | "github.com/evilsocket/opensnitch/daemon/dns" 11 | "github.com/evilsocket/opensnitch/daemon/log" 12 | "github.com/evilsocket/opensnitch/daemon/netfilter" 13 | "github.com/evilsocket/opensnitch/daemon/netlink" 14 | "github.com/evilsocket/opensnitch/daemon/netstat" 15 | "github.com/evilsocket/opensnitch/daemon/procmon" 16 | "github.com/evilsocket/opensnitch/daemon/ui/protocol" 17 | 18 | "github.com/google/gopacket/layers" 19 | ) 20 | 21 | // Connection represents an outgoing connection. 22 | type Connection struct { 23 | Protocol string 24 | SrcIP net.IP 25 | SrcPort uint 26 | DstIP net.IP 27 | DstPort uint 28 | DstHost string 29 | Entry *netstat.Entry 30 | Process *procmon.Process 31 | 32 | pkt *netfilter.Packet 33 | } 34 | 35 | var showUnknownCons = false 36 | 37 | // Parse extracts the IP layers from a network packet to determine what 38 | // process generated a connection. 39 | func Parse(nfp netfilter.Packet, interceptUnknown bool) *Connection { 40 | showUnknownCons = interceptUnknown 41 | 42 | if nfp.IsIPv4() { 43 | con, err := NewConnection(&nfp) 44 | if err != nil { 45 | log.Debug("%s", err) 46 | return nil 47 | } else if con == nil { 48 | return nil 49 | } 50 | return con 51 | } 52 | 53 | if core.IPv6Enabled == false { 54 | return nil 55 | } 56 | con, err := NewConnection6(&nfp) 57 | if err != nil { 58 | log.Debug("%s", err) 59 | return nil 60 | } else if con == nil { 61 | return nil 62 | } 63 | return con 64 | 65 | } 66 | 67 | func newConnectionImpl(nfp *netfilter.Packet, c *Connection, protoType string) (cr *Connection, err error) { 68 | // no errors but not enough info neither 69 | if c.parseDirection(protoType) == false { 70 | return nil, nil 71 | } 72 | log.Debug("new connection %s => %d:%v -> %v:%d uid: ", c.Protocol, c.SrcPort, c.SrcIP, c.DstIP, c.DstPort, nfp.UID) 73 | 74 | c.Entry = &netstat.Entry{ 75 | Proto: c.Protocol, 76 | SrcIP: c.SrcIP, 77 | SrcPort: c.SrcPort, 78 | DstIP: c.DstIP, 79 | DstPort: c.DstPort, 80 | UserId: -1, 81 | INode: -1, 82 | } 83 | 84 | // 0. lookup uid and inode via netlink. Can return several inodes. 85 | // 1. lookup uid and inode using /proc/net/(udp|tcp|udplite) 86 | // 2. lookup pid by inode 87 | // 3. if this is coming from us, just accept 88 | // 4. lookup process info by pid 89 | uid, inodeList := netlink.GetSocketInfo(c.Protocol, c.SrcIP, c.SrcPort, c.DstIP, c.DstPort) 90 | if len(inodeList) == 0 { 91 | if c.Entry = netstat.FindEntry(c.Protocol, c.SrcIP, c.SrcPort, c.DstIP, c.DstPort); c.Entry == nil { 92 | return nil, fmt.Errorf("Could not find netstat entry for: %s", c) 93 | } 94 | if c.Entry.INode != -1 { 95 | inodeList = append([]int{c.Entry.INode}, inodeList...) 96 | } 97 | } 98 | if len(inodeList) == 0 { 99 | log.Debug("<== no inodes found, applying default action.") 100 | return nil, nil 101 | } 102 | 103 | if uid != -1 { 104 | c.Entry.UserId = uid 105 | } else if c.Entry.UserId == -1 && nfp.UID != 0xffffffff { 106 | c.Entry.UserId = int(nfp.UID) 107 | } 108 | 109 | pid := -1 110 | for n, inode := range inodeList { 111 | if pid = procmon.GetPIDFromINode(inode, fmt.Sprint(inode, c.SrcIP, c.SrcPort, c.DstIP, c.DstPort)); pid == os.Getpid() { 112 | // return a Process object with our PID, to be able to exclude our own connections 113 | // (to the UI on a local socket for example) 114 | c.Process = procmon.NewProcess(pid, "") 115 | return c, nil 116 | } 117 | if pid != -1 { 118 | log.Debug("[%d] PID found %d", n, pid) 119 | c.Entry.INode = inode 120 | break 121 | } 122 | } 123 | if c.Process = procmon.FindProcess(pid, showUnknownCons); c.Process == nil { 124 | return nil, fmt.Errorf("Could not find process by its pid %d for: %s", pid, c) 125 | } 126 | 127 | return c, nil 128 | 129 | } 130 | 131 | // NewConnection creates a new Connection object, and returns the details of it. 132 | func NewConnection(nfp *netfilter.Packet) (c *Connection, err error) { 133 | ipv4 := nfp.Packet.Layer(layers.LayerTypeIPv4) 134 | if ipv4 == nil { 135 | return nil, errors.New("Error getting IPv4 layer") 136 | } 137 | ip, ok := ipv4.(*layers.IPv4) 138 | if !ok { 139 | return nil, errors.New("Error getting IPv4 layer data") 140 | } 141 | c = &Connection{ 142 | SrcIP: ip.SrcIP, 143 | DstIP: ip.DstIP, 144 | DstHost: dns.HostOr(ip.DstIP, ""), 145 | pkt: nfp, 146 | } 147 | return newConnectionImpl(nfp, c, "") 148 | } 149 | 150 | // NewConnection6 creates a IPv6 new Connection object, and returns the details of it. 151 | func NewConnection6(nfp *netfilter.Packet) (c *Connection, err error) { 152 | ipv6 := nfp.Packet.Layer(layers.LayerTypeIPv6) 153 | if ipv6 == nil { 154 | return nil, errors.New("Error getting IPv6 layer") 155 | } 156 | ip, ok := ipv6.(*layers.IPv6) 157 | if !ok { 158 | return nil, errors.New("Error getting IPv6 layer data") 159 | } 160 | c = &Connection{ 161 | SrcIP: ip.SrcIP, 162 | DstIP: ip.DstIP, 163 | DstHost: dns.HostOr(ip.DstIP, ""), 164 | pkt: nfp, 165 | } 166 | return newConnectionImpl(nfp, c, "6") 167 | } 168 | 169 | func (c *Connection) parseDirection(protoType string) bool { 170 | ret := false 171 | if tcpLayer := c.pkt.Packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { 172 | if tcp, ok := tcpLayer.(*layers.TCP); ok == true && tcp != nil { 173 | c.Protocol = "tcp" + protoType 174 | c.DstPort = uint(tcp.DstPort) 175 | c.SrcPort = uint(tcp.SrcPort) 176 | ret = true 177 | 178 | if tcp.DstPort == 53 { 179 | c.getDomains(c.pkt, c) 180 | } 181 | } 182 | } else if udpLayer := c.pkt.Packet.Layer(layers.LayerTypeUDP); udpLayer != nil { 183 | if udp, ok := udpLayer.(*layers.UDP); ok == true && udp != nil { 184 | c.Protocol = "udp" + protoType 185 | c.DstPort = uint(udp.DstPort) 186 | c.SrcPort = uint(udp.SrcPort) 187 | ret = true 188 | 189 | if udp.DstPort == 53 { 190 | c.getDomains(c.pkt, c) 191 | } 192 | } 193 | } else if udpliteLayer := c.pkt.Packet.Layer(layers.LayerTypeUDPLite); udpliteLayer != nil { 194 | if udplite, ok := udpliteLayer.(*layers.UDPLite); ok == true && udplite != nil { 195 | c.Protocol = "udplite" + protoType 196 | c.DstPort = uint(udplite.DstPort) 197 | c.SrcPort = uint(udplite.SrcPort) 198 | ret = true 199 | } 200 | } 201 | 202 | return ret 203 | } 204 | 205 | func (c *Connection) getDomains(nfp *netfilter.Packet, con *Connection) { 206 | domains := dns.GetQuestions(nfp) 207 | if len(domains) > 0 { 208 | for _, dns := range domains { 209 | con.DstHost = dns 210 | } 211 | } 212 | } 213 | 214 | // To returns the destination host of a connection. 215 | func (c *Connection) To() string { 216 | if c.DstHost == "" { 217 | return c.DstIP.String() 218 | } 219 | return c.DstHost 220 | } 221 | 222 | func (c *Connection) String() string { 223 | if c.Entry == nil { 224 | return fmt.Sprintf("%s ->(%s)-> %s:%d", c.SrcIP, c.Protocol, c.To(), c.DstPort) 225 | } 226 | 227 | if c.Process == nil { 228 | return fmt.Sprintf("%s (uid:%d) ->(%s)-> %s:%d", c.SrcIP, c.Entry.UserId, c.Protocol, c.To(), c.DstPort) 229 | } 230 | 231 | 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) 232 | } 233 | 234 | // Serialize returns a connection serialized. 235 | func (c *Connection) Serialize() *protocol.Connection { 236 | return &protocol.Connection{ 237 | Protocol: c.Protocol, 238 | SrcIp: c.SrcIP.String(), 239 | SrcPort: uint32(c.SrcPort), 240 | DstIp: c.DstIP.String(), 241 | DstHost: c.DstHost, 242 | DstPort: uint32(c.DstPort), 243 | UserId: uint32(c.Entry.UserId), 244 | ProcessId: uint32(c.Process.ID), 245 | ProcessPath: c.Process.Path, 246 | ProcessArgs: c.Process.Args, 247 | ProcessEnv: c.Process.Env, 248 | ProcessCwd: c.Process.CWD, 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /daemon/core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "os/user" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | defaultTrimSet = "\r\n\t " 13 | ) 14 | 15 | // Trim remove trailing spaces from a string. 16 | func Trim(s string) string { 17 | return strings.Trim(s, defaultTrimSet) 18 | } 19 | 20 | // Exec spawns a new process and reurns the output. 21 | func Exec(executable string, args []string) (string, error) { 22 | path, err := exec.LookPath(executable) 23 | if err != nil { 24 | return "", err 25 | } 26 | 27 | raw, err := exec.Command(path, args...).CombinedOutput() 28 | if err != nil { 29 | return "", err 30 | } 31 | return Trim(string(raw)), nil 32 | } 33 | 34 | // Exists checks if a path exists. 35 | func Exists(path string) bool { 36 | if _, err := os.Stat(path); os.IsNotExist(err) { 37 | return false 38 | } 39 | return true 40 | } 41 | 42 | // ExpandPath replaces '~' shorthand with the user's home directory. 43 | func ExpandPath(path string) (string, error) { 44 | // Check if path is empty 45 | if path != "" { 46 | if strings.HasPrefix(path, "~") { 47 | usr, err := user.Current() 48 | if err != nil { 49 | return "", err 50 | } 51 | // Replace only the first occurrence of ~ 52 | path = strings.Replace(path, "~", usr.HomeDir, 1) 53 | } 54 | return filepath.Abs(path) 55 | } 56 | return "", nil 57 | } 58 | -------------------------------------------------------------------------------- /daemon/core/system.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | // IPv6Enabled indicates if IPv6 protocol is enabled in the system 10 | IPv6Enabled = Exists("/proc/sys/net/ipv6") 11 | ) 12 | 13 | // GetHostname returns the name of the host where the daemon is running. 14 | func GetHostname() string { 15 | hostname, _ := ioutil.ReadFile("/proc/sys/kernel/hostname") 16 | return strings.Replace(string(hostname), "\n", "", -1) 17 | } 18 | 19 | // GetKernelVersion returns the name of the host where the daemon is running. 20 | func GetKernelVersion() string { 21 | version, _ := ioutil.ReadFile("/proc/sys/kernel/version") 22 | return strings.Replace(string(version), "\n", "", -1) 23 | } 24 | -------------------------------------------------------------------------------- /daemon/core/version.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | // version related consts 4 | const ( 5 | Name = "opensnitch-daemon" 6 | Version = "1.3.0" 7 | Author = "Simone 'evilsocket' Margaritelli" 8 | Website = "https://github.com/evilsocket/opensnitch" 9 | ) 10 | -------------------------------------------------------------------------------- /daemon/default-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Server": 3 | { 4 | "Address":"unix:///tmp/osui.sock", 5 | "LogFile":"/var/log/opensnitchd.log" 6 | }, 7 | "DefaultAction": "allow", 8 | "DefaultDuration": "once", 9 | "InterceptUnknown": false, 10 | "ProcMonitorMethod": "proc", 11 | "LogLevel": 2 12 | } 13 | -------------------------------------------------------------------------------- /daemon/dns/parse.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "github.com/evilsocket/opensnitch/daemon/netfilter" 5 | "github.com/google/gopacket/layers" 6 | ) 7 | 8 | // GetQuestions retrieves the domain names a process is trying to resolve. 9 | func GetQuestions(nfp *netfilter.Packet) (questions []string) { 10 | dnsLayer := nfp.Packet.Layer(layers.LayerTypeDNS) 11 | if dnsLayer == nil { 12 | return questions 13 | } 14 | 15 | dns, _ := dnsLayer.(*layers.DNS) 16 | for _, dnsQuestion := range dns.Questions { 17 | questions = append(questions, string(dnsQuestion.Name)) 18 | } 19 | 20 | return questions 21 | } 22 | -------------------------------------------------------------------------------- /daemon/dns/track.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | "github.com/evilsocket/opensnitch/daemon/log" 8 | 9 | "github.com/google/gopacket" 10 | "github.com/google/gopacket/layers" 11 | ) 12 | 13 | var ( 14 | responses = make(map[string]string, 0) 15 | lock = sync.RWMutex{} 16 | ) 17 | 18 | // TrackAnswers obtains the resolved domains of a DNS query. 19 | // If the packet is UDP DNS, the domain names are added to the list of resolved domains. 20 | func TrackAnswers(packet gopacket.Packet) bool { 21 | udpLayer := packet.Layer(layers.LayerTypeUDP) 22 | if udpLayer == nil { 23 | return false 24 | } 25 | 26 | udp, ok := udpLayer.(*layers.UDP) 27 | if ok == false || udp == nil { 28 | return false 29 | } 30 | if udp.SrcPort != 53 { 31 | return false 32 | } 33 | 34 | dnsLayer := packet.Layer(layers.LayerTypeDNS) 35 | if dnsLayer == nil { 36 | return false 37 | } 38 | 39 | dnsAns, ok := dnsLayer.(*layers.DNS) 40 | if ok == false || dnsAns == nil { 41 | return false 42 | } 43 | 44 | for _, ans := range dnsAns.Answers { 45 | if ans.Name != nil { 46 | if ans.IP != nil { 47 | Track(ans.IP.String(), string(ans.Name)) 48 | } else if ans.CNAME != nil { 49 | Track(string(ans.CNAME), string(ans.Name)) 50 | } 51 | } 52 | } 53 | 54 | return true 55 | } 56 | 57 | // Track adds a resolved domain to the list. 58 | func Track(resolved string, hostname string) { 59 | lock.Lock() 60 | defer lock.Unlock() 61 | 62 | if resolved == "127.0.0.1" { 63 | return 64 | } 65 | responses[resolved] = hostname 66 | 67 | log.Debug("New DNS record: %s -> %s", resolved, hostname) 68 | } 69 | 70 | // Host returns if a resolved domain is in the list. 71 | func Host(resolved string) (host string, found bool) { 72 | lock.RLock() 73 | defer lock.RUnlock() 74 | 75 | host, found = responses[resolved] 76 | return 77 | } 78 | 79 | // HostOr checks if an IP has a domain name already resolved. 80 | // If the domain is in the list it's returned, otherwise the IP will be returned. 81 | func HostOr(ip net.IP, or string) string { 82 | if host, found := Host(ip.String()); found == true { 83 | // host might have been CNAME; go back until we reach the "root" 84 | seen := make(map[string]bool) // prevent possibility of loops 85 | for { 86 | orig, had := Host(host) 87 | if seen[orig] { 88 | break 89 | } 90 | if !had { 91 | break 92 | } 93 | seen[orig] = true 94 | host = orig 95 | } 96 | return host 97 | } 98 | return or 99 | } 100 | -------------------------------------------------------------------------------- /daemon/firewall/config.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "sync" 8 | 9 | "github.com/evilsocket/opensnitch/daemon/log" 10 | "github.com/fsnotify/fsnotify" 11 | ) 12 | 13 | var ( 14 | configFile = "/etc/opensnitchd/system-fw.json" 15 | configWatcher *fsnotify.Watcher 16 | fwConfig config 17 | ) 18 | 19 | type fwRule struct { 20 | Description string 21 | Table string 22 | Chain string 23 | Parameters string 24 | Target string 25 | TargetParameters string 26 | } 27 | 28 | type rulesList struct { 29 | Rule *fwRule 30 | } 31 | 32 | type config struct { 33 | sync.RWMutex 34 | SystemRules []*rulesList 35 | } 36 | 37 | func loadDiskConfiguration(reload bool) { 38 | raw, err := ioutil.ReadFile(configFile) 39 | if err != nil { 40 | fmt.Errorf("Error loading disk firewall configuration %s: %s", configFile, err) 41 | } 42 | 43 | if ok := loadConfiguration(raw); ok { 44 | configWatcher.Remove(configFile) 45 | if err := configWatcher.Add(configFile); err != nil { 46 | log.Error("Could not watch firewall configuration: %s", err) 47 | return 48 | } 49 | } 50 | 51 | if reload { 52 | return 53 | } 54 | 55 | go monitorConfigWorker() 56 | } 57 | 58 | // loadConfigutation reads the system firewall rules from disk. 59 | // Then the rules are added based on the configuration defined. 60 | func loadConfiguration(rawConfig []byte) bool { 61 | fwConfig.Lock() 62 | defer fwConfig.Unlock() 63 | 64 | // delete old system rules, that may be different from the new ones 65 | DeleteSystemRules(false, log.GetLogLevel() == log.DEBUG) 66 | if err := json.Unmarshal(rawConfig, &fwConfig); err != nil { 67 | log.Error("Error parsing firewall configuration %s: %s", configFile, err) 68 | return false 69 | } 70 | 71 | DeleteSystemRules(true, log.GetLogLevel() == log.DEBUG) 72 | for _, r := range fwConfig.SystemRules { 73 | if r.Rule.Chain == "" { 74 | continue 75 | } 76 | CreateSystemRule(r.Rule, true) 77 | AddSystemRule(ADD, r.Rule, true) 78 | } 79 | 80 | return true 81 | } 82 | 83 | func saveConfiguration(rawConfig string) error { 84 | conf, err := json.Marshal([]byte(rawConfig)) 85 | if err != nil { 86 | log.Error("saving json firewall configuration: ", err, conf) 87 | return err 88 | } 89 | 90 | if loadConfiguration([]byte(rawConfig)) != true { 91 | return fmt.Errorf("Error parsing firewall configuration %s: %s", rawConfig, err) 92 | } 93 | 94 | if err = ioutil.WriteFile(configFile, []byte(rawConfig), 0644); err != nil { 95 | log.Error("writing firewall configuration to disk: ", err) 96 | return err 97 | } 98 | return nil 99 | } 100 | 101 | func monitorConfigWorker() { 102 | for { 103 | select { 104 | case <-rulesCheckerChan: 105 | return 106 | case event := <-configWatcher.Events: 107 | if (event.Op&fsnotify.Write == fsnotify.Write) || (event.Op&fsnotify.Remove == fsnotify.Remove) { 108 | loadDiskConfiguration(true) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /daemon/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/evilsocket/opensnitch/daemon 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/evilsocket/ftrace v1.2.0 7 | github.com/fsnotify/fsnotify v1.4.7 8 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 9 | github.com/golang/protobuf v1.0.0 10 | github.com/google/gopacket v1.1.14 11 | github.com/vishvananda/netlink v1.1.0 12 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect 13 | golang.org/x/net v0.0.0-20180417003750-8d16fa6dc9a8 14 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect 15 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444 // indirect 16 | golang.org/x/text v0.3.0 // indirect 17 | google.golang.org/genproto v0.0.0-20180413175816-7fd901a49ba6 // indirect 18 | google.golang.org/grpc v1.11.3 19 | ) 20 | -------------------------------------------------------------------------------- /daemon/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type Handler func(format string, args ...interface{}) 12 | 13 | // https://misc.flogisoft.com/bash/tip_colors_and_formatting 14 | const ( 15 | BOLD = "\033[1m" 16 | DIM = "\033[2m" 17 | 18 | RED = "\033[31m" 19 | GREEN = "\033[32m" 20 | BLUE = "\033[34m" 21 | YELLOW = "\033[33m" 22 | 23 | FG_BLACK = "\033[30m" 24 | FG_WHITE = "\033[97m" 25 | 26 | BG_DGRAY = "\033[100m" 27 | BG_RED = "\033[41m" 28 | BG_GREEN = "\033[42m" 29 | BG_YELLOW = "\033[43m" 30 | BG_LBLUE = "\033[104m" 31 | 32 | RESET = "\033[0m" 33 | ) 34 | 35 | // log level constants 36 | const ( 37 | DEBUG = iota 38 | INFO 39 | IMPORTANT 40 | WARNING 41 | ERROR 42 | FATAL 43 | ) 44 | 45 | // 46 | var ( 47 | WithColors = true 48 | Output = os.Stdout 49 | StdoutFile = "/dev/stdout" 50 | DateFormat = "2006-01-02 15:04:05" 51 | MinLevel = INFO 52 | 53 | mutex = &sync.RWMutex{} 54 | labels = map[int]string{ 55 | DEBUG: "DBG", 56 | INFO: "INF", 57 | IMPORTANT: "IMP", 58 | WARNING: "WAR", 59 | ERROR: "ERR", 60 | FATAL: "!!!", 61 | } 62 | colors = map[int]string{ 63 | DEBUG: DIM + FG_BLACK + BG_DGRAY, 64 | INFO: FG_WHITE + BG_GREEN, 65 | IMPORTANT: FG_WHITE + BG_LBLUE, 66 | WARNING: FG_WHITE + BG_YELLOW, 67 | ERROR: FG_WHITE + BG_RED, 68 | FATAL: FG_WHITE + BG_RED + BOLD, 69 | } 70 | ) 71 | 72 | // Wrap wraps a text with effects 73 | func Wrap(s, effect string) string { 74 | if WithColors == true { 75 | s = effect + s + RESET 76 | } 77 | return s 78 | } 79 | 80 | // Dim dims a text 81 | func Dim(s string) string { 82 | return Wrap(s, DIM) 83 | } 84 | 85 | // Bold bolds a text 86 | func Bold(s string) string { 87 | return Wrap(s, BOLD) 88 | } 89 | 90 | // Red reds the text 91 | func Red(s string) string { 92 | return Wrap(s, RED) 93 | } 94 | 95 | // Green greens the text 96 | func Green(s string) string { 97 | return Wrap(s, GREEN) 98 | } 99 | 100 | // Blue blues the text 101 | func Blue(s string) string { 102 | return Wrap(s, BLUE) 103 | } 104 | 105 | // Yellow yellows the text 106 | func Yellow(s string) string { 107 | return Wrap(s, YELLOW) 108 | } 109 | 110 | // Raw prints out a text without colors 111 | func Raw(format string, args ...interface{}) { 112 | mutex.Lock() 113 | defer mutex.Unlock() 114 | fmt.Fprintf(Output, format, args...) 115 | } 116 | 117 | // SetLogLevel sets the log level 118 | func SetLogLevel(newLevel int) { 119 | mutex.Lock() 120 | defer mutex.Unlock() 121 | MinLevel = newLevel 122 | } 123 | 124 | // GetLogLevel returns the current log level configured. 125 | func GetLogLevel() int { 126 | mutex.Lock() 127 | defer mutex.Unlock() 128 | 129 | return MinLevel 130 | } 131 | 132 | // Log prints out a text with the given color and format 133 | func Log(level int, format string, args ...interface{}) { 134 | mutex.Lock() 135 | defer mutex.Unlock() 136 | if level >= MinLevel { 137 | label := labels[level] 138 | color := colors[level] 139 | when := time.Now().UTC().Format(DateFormat) 140 | 141 | what := fmt.Sprintf(format, args...) 142 | if strings.HasSuffix(what, "\n") == false { 143 | what += "\n" 144 | } 145 | 146 | l := Dim("[%s]") 147 | r := Wrap(" %s ", color) + " %s" 148 | 149 | fmt.Fprintf(Output, l+" "+r, when, label, what) 150 | } 151 | } 152 | 153 | func setDefaultLogOutput() { 154 | mutex.Lock() 155 | Output = os.Stdout 156 | mutex.Unlock() 157 | } 158 | 159 | // OpenFile opens a file to print out the logs 160 | func OpenFile(logFile string) (err error) { 161 | if logFile == StdoutFile { 162 | setDefaultLogOutput() 163 | return 164 | } 165 | 166 | if Output, err = os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil { 167 | Error("Error opening log: ", logFile, err) 168 | //fallback to stdout 169 | setDefaultLogOutput() 170 | } 171 | Important("Start writing logs to ", logFile) 172 | 173 | return err 174 | } 175 | 176 | // Close closes the current output file descriptor 177 | func Close() { 178 | if Output != os.Stdout { 179 | Output.Close() 180 | } 181 | } 182 | 183 | // Debug is the log level for debugging purposes 184 | func Debug(format string, args ...interface{}) { 185 | Log(DEBUG, format, args...) 186 | } 187 | 188 | // Info is the log level for informative messages 189 | func Info(format string, args ...interface{}) { 190 | Log(INFO, format, args...) 191 | } 192 | 193 | // Important is the log level for things that must pay attention 194 | func Important(format string, args ...interface{}) { 195 | Log(IMPORTANT, format, args...) 196 | } 197 | 198 | // Warning is the log level for non-critical errors 199 | func Warning(format string, args ...interface{}) { 200 | Log(WARNING, format, args...) 201 | } 202 | 203 | // Error is the log level for errors that should be corrected 204 | func Error(format string, args ...interface{}) { 205 | Log(ERROR, format, args...) 206 | } 207 | 208 | // Fatal is the log level for errors that must be corrected before continue 209 | func Fatal(format string, args ...interface{}) { 210 | Log(FATAL, format, args...) 211 | os.Exit(1) 212 | } 213 | -------------------------------------------------------------------------------- /daemon/netfilter/packet.go: -------------------------------------------------------------------------------- 1 | package netfilter 2 | 3 | import "C" 4 | 5 | import ( 6 | "github.com/google/gopacket" 7 | ) 8 | 9 | // packet consts 10 | const ( 11 | IPv4 = 4 12 | ) 13 | 14 | // Verdict holds the action to perform on a packet (NF_DROP, NF_ACCEPT, etc) 15 | type Verdict C.uint 16 | 17 | type VerdictContainer struct { 18 | Verdict Verdict 19 | Mark uint32 20 | Packet []byte 21 | } 22 | 23 | // Packet holds the data of a network packet 24 | type Packet struct { 25 | Packet gopacket.Packet 26 | Mark uint32 27 | verdictChannel chan VerdictContainer 28 | UID uint32 29 | NetworkProtocol uint8 30 | } 31 | 32 | // SetVerdict emits a veredict on a packet 33 | func (p *Packet) SetVerdict(v Verdict) { 34 | p.verdictChannel <- VerdictContainer{Verdict: v, Packet: nil, Mark: 0} 35 | } 36 | 37 | // SetVerdictAndMark emits a veredict on a packet and marks it in order to not 38 | // analyze it again. 39 | func (p *Packet) SetVerdictAndMark(v Verdict, mark uint32) { 40 | p.verdictChannel <- VerdictContainer{Verdict: v, Packet: nil, Mark: mark} 41 | } 42 | 43 | func (p *Packet) SetRequeueVerdict(newQueueId uint16) { 44 | v := uint(NF_QUEUE) 45 | q := (uint(newQueueId) << 16) 46 | v = v | q 47 | p.verdictChannel <- VerdictContainer{Verdict: Verdict(v), Packet: nil, Mark: 0} 48 | } 49 | 50 | func (p *Packet) SetVerdictWithPacket(v Verdict, packet []byte) { 51 | p.verdictChannel <- VerdictContainer{Verdict: v, Packet: packet, Mark: 0} 52 | } 53 | 54 | // IsIPv4 returns if the packet is IPv4 55 | func (p *Packet) IsIPv4() bool { 56 | return p.NetworkProtocol == IPv4 57 | } 58 | -------------------------------------------------------------------------------- /daemon/netfilter/queue.c: -------------------------------------------------------------------------------- 1 | #include "queue.h" 2 | 3 | -------------------------------------------------------------------------------- /daemon/netfilter/queue.go: -------------------------------------------------------------------------------- 1 | package netfilter 2 | 3 | /* 4 | #cgo pkg-config: libnetfilter_queue 5 | #cgo CFLAGS: -Wall -I/usr/include 6 | #cgo LDFLAGS: -L/usr/lib64/ -ldl 7 | 8 | #include "queue.h" 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "fmt" 14 | "os" 15 | "sync" 16 | "time" 17 | "unsafe" 18 | 19 | "github.com/google/gopacket" 20 | "github.com/google/gopacket/layers" 21 | "github.com/evilsocket/opensnitch/daemon/log" 22 | ) 23 | 24 | const ( 25 | AF_INET = 2 26 | AF_INET6 = 10 27 | 28 | NF_DROP Verdict = 0 29 | NF_ACCEPT Verdict = 1 30 | NF_STOLEN Verdict = 2 31 | NF_QUEUE Verdict = 3 32 | NF_REPEAT Verdict = 4 33 | NF_STOP Verdict = 5 34 | 35 | NF_DEFAULT_QUEUE_SIZE uint32 = 4096 36 | NF_DEFAULT_PACKET_SIZE uint32 = 4096 37 | ) 38 | 39 | var ( 40 | queueIndex = make(map[uint32]*chan Packet, 0) 41 | queueIndexLock = sync.RWMutex{} 42 | exitChan = make(chan bool, 1) 43 | 44 | gopacketDecodeOptions = gopacket.DecodeOptions{Lazy: true, NoCopy: true} 45 | ) 46 | 47 | // VerdictContainerC is the struct that contains the mark, action, length and 48 | // payload of a packet. 49 | // It's defined in queue.h, and filled on go_callback() 50 | type VerdictContainerC C.verdictContainer 51 | 52 | // Queue holds the information of a netfilter queue. 53 | // The handles of the connection to the kernel and the created queue. 54 | // A channel where the intercepted packets will be received. 55 | // The ID of the queue. 56 | type Queue struct { 57 | h *C.struct_nfq_handle 58 | qh *C.struct_nfq_q_handle 59 | fd C.int 60 | packets chan Packet 61 | idx uint32 62 | } 63 | 64 | // NewQueue opens a new netfilter queue to receive packets marked with a mark. 65 | func NewQueue(queueID uint16) (q *Queue, err error) { 66 | q = &Queue{ 67 | idx: uint32(time.Now().UnixNano()), 68 | packets: make(chan Packet), 69 | } 70 | 71 | if err = q.create(queueID); err != nil { 72 | return nil, err 73 | } else if err = q.setup(); err != nil { 74 | return nil, err 75 | } 76 | 77 | go q.run(exitChan) 78 | 79 | return q, nil 80 | } 81 | 82 | func (q *Queue) create(queueID uint16) (err error) { 83 | var ret C.int 84 | 85 | if q.h, err = C.nfq_open(); err != nil { 86 | return fmt.Errorf("Error opening Queue handle: %v", err) 87 | } else if ret, err = C.nfq_unbind_pf(q.h, AF_INET); err != nil || ret < 0 { 88 | return fmt.Errorf("Error unbinding existing q handler from AF_INET protocol family: %v", err) 89 | } else if ret, err = C.nfq_unbind_pf(q.h, AF_INET6); err != nil || ret < 0 { 90 | return fmt.Errorf("Error unbinding existing q handler from AF_INET6 protocol family: %v", err) 91 | } else if ret, err := C.nfq_bind_pf(q.h, AF_INET); err != nil || ret < 0 { 92 | return fmt.Errorf("Error binding to AF_INET protocol family: %v", err) 93 | } else if ret, err := C.nfq_bind_pf(q.h, AF_INET6); err != nil || ret < 0 { 94 | return fmt.Errorf("Error binding to AF_INET6 protocol family: %v", err) 95 | } else if q.qh, err = C.CreateQueue(q.h, C.u_int16_t(queueID), C.u_int32_t(q.idx)); err != nil || q.qh == nil { 96 | q.destroy() 97 | return fmt.Errorf("Error binding to queue: %v", err) 98 | } 99 | 100 | queueIndexLock.Lock() 101 | queueIndex[q.idx] = &q.packets 102 | queueIndexLock.Unlock() 103 | 104 | return nil 105 | } 106 | 107 | func (q *Queue) setup() (err error) { 108 | var ret C.int 109 | 110 | queueSize := C.u_int32_t(NF_DEFAULT_QUEUE_SIZE) 111 | bufferSize := C.uint(NF_DEFAULT_PACKET_SIZE) 112 | totSize := C.uint(NF_DEFAULT_QUEUE_SIZE * NF_DEFAULT_PACKET_SIZE) 113 | 114 | if ret, err = C.nfq_set_queue_maxlen(q.qh, queueSize); err != nil || ret < 0 { 115 | q.destroy() 116 | return fmt.Errorf("Unable to set max packets in queue: %v", err) 117 | } else if C.nfq_set_mode(q.qh, C.u_int8_t(2), bufferSize) < 0 { 118 | q.destroy() 119 | return fmt.Errorf("Unable to set packets copy mode: %v", err) 120 | } else if q.fd, err = C.nfq_fd(q.h); err != nil { 121 | q.destroy() 122 | return fmt.Errorf("Unable to get queue file-descriptor. %v", err) 123 | } else if C.nfnl_rcvbufsiz(C.nfq_nfnlh(q.h), totSize) < 0 { 124 | q.destroy() 125 | return fmt.Errorf("Unable to increase netfilter buffer space size") 126 | } 127 | 128 | return nil 129 | } 130 | 131 | func (q *Queue) run(exitCh chan<- bool) { 132 | if errno := C.Run(q.h, q.fd); errno != 0 { 133 | fmt.Fprintf(os.Stderr, "Terminating, unable to receive packet due to errno=%d", errno) 134 | } 135 | exitChan <- true 136 | } 137 | 138 | // Close ensures that nfqueue resources are freed and closed. 139 | // C.stop_reading_packets() stops the reading packets loop, which causes 140 | // go-subroutine run() to exit. 141 | // After exit, listening queue is destroyed and closed. 142 | // If for some reason any of the steps stucks while closing it, we'll exit by timeout. 143 | func (q *Queue) Close() { 144 | close(q.packets) 145 | C.stop_reading_packets() 146 | q.destroy() 147 | queueIndexLock.Lock() 148 | delete(queueIndex, q.idx) 149 | queueIndexLock.Unlock() 150 | } 151 | 152 | func (q *Queue) destroy() { 153 | // we'll try to exit cleanly, but sometimes nfqueue gets stuck 154 | time.AfterFunc(5*time.Second, func() { 155 | log.Warning("queue stuck, closing by timeout") 156 | if q != nil { 157 | C.close(q.fd) 158 | q.closeNfq() 159 | } 160 | os.Exit(0) 161 | }) 162 | C.nfq_unbind_pf(q.h, AF_INET) 163 | C.nfq_unbind_pf(q.h, AF_INET6) 164 | if q.qh != nil { 165 | if ret := C.nfq_destroy_queue(q.qh); ret != 0 { 166 | log.Warning("Queue.destroy(), nfq_destroy_queue() not closed: %d", ret) 167 | } 168 | } 169 | 170 | q.closeNfq() 171 | } 172 | 173 | func (q *Queue) closeNfq() { 174 | if q.h != nil { 175 | if ret := C.nfq_close(q.h); ret != 0 { 176 | log.Warning("Queue.destroy(), nfq_close() not closed: %d", ret) 177 | } 178 | } 179 | } 180 | 181 | // Packets return the list of enqueued packets. 182 | func (q *Queue) Packets() <-chan Packet { 183 | return q.packets 184 | } 185 | 186 | // FYI: the export keyword is mandatory to specify that go_callback is defined elsewhere 187 | 188 | //export go_callback 189 | func go_callback(queueID C.int, data *C.uchar, length C.int, mark C.uint, idx uint32, vc *VerdictContainerC, uid uint32) { 190 | (*vc).verdict = C.uint(NF_ACCEPT) 191 | (*vc).data = nil 192 | (*vc).mark_set = 0 193 | (*vc).length = 0 194 | 195 | queueIndexLock.RLock() 196 | queueChannel, found := queueIndex[idx] 197 | queueIndexLock.RUnlock() 198 | if !found { 199 | fmt.Fprintf(os.Stderr, "Unexpected queue idx %d\n", idx) 200 | return 201 | } 202 | 203 | xdata := C.GoBytes(unsafe.Pointer(data), length) 204 | 205 | p := Packet{ 206 | verdictChannel: make(chan VerdictContainer), 207 | Mark: uint32(mark), 208 | UID: uid, 209 | NetworkProtocol: xdata[0] >> 4, // first 4 bits is the version 210 | } 211 | 212 | var packet gopacket.Packet 213 | if p.IsIPv4() { 214 | packet = gopacket.NewPacket(xdata, layers.LayerTypeIPv4, gopacketDecodeOptions) 215 | } else { 216 | packet = gopacket.NewPacket(xdata, layers.LayerTypeIPv6, gopacketDecodeOptions) 217 | } 218 | 219 | p.Packet = packet 220 | 221 | select { 222 | case *queueChannel <- p: 223 | select { 224 | case v := <-p.verdictChannel: 225 | if v.Packet == nil { 226 | (*vc).verdict = C.uint(v.Verdict) 227 | } else { 228 | (*vc).verdict = C.uint(v.Verdict) 229 | (*vc).data = (*C.uchar)(unsafe.Pointer(&v.Packet[0])) 230 | (*vc).length = C.uint(len(v.Packet)) 231 | } 232 | 233 | if v.Mark != 0 { 234 | (*vc).mark_set = C.uint(1) 235 | (*vc).mark = C.uint(v.Mark) 236 | } 237 | } 238 | 239 | default: 240 | fmt.Fprintf(os.Stderr, "Error sending packet to queue channel %d\n", idx) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /daemon/netfilter/queue.h: -------------------------------------------------------------------------------- 1 | #ifndef _NETFILTER_QUEUE_H 2 | #define _NETFILTER_QUEUE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | typedef struct { 18 | uint verdict; 19 | uint mark; 20 | uint mark_set; 21 | uint length; 22 | unsigned char *data; 23 | } verdictContainer; 24 | 25 | static void *get_uid = NULL; 26 | 27 | extern void go_callback(int id, unsigned char* data, int len, uint mark, u_int32_t idx, verdictContainer *vc, uint32_t uid); 28 | 29 | static uint8_t stop = 0; 30 | 31 | static inline void configure_uid_if_available(struct nfq_q_handle *qh){ 32 | void *hndl = dlopen("libnetfilter_queue.so.1", RTLD_LAZY); 33 | if (!hndl) { 34 | hndl = dlopen("libnetfilter_queue.so", RTLD_LAZY); 35 | if (!hndl){ 36 | printf("WARNING: libnetfilter_queue not available\n"); 37 | return; 38 | } 39 | } 40 | if ((get_uid = dlsym(hndl, "nfq_get_uid")) == NULL){ 41 | printf("WARNING: nfq_get_uid not available\n"); 42 | return; 43 | } 44 | printf("OK: libnetfiler_queue supports nfq_get_uid\n"); 45 | #ifdef NFQA_CFG_F_UID_GID 46 | if (qh != NULL && nfq_set_queue_flags(qh, NFQA_CFG_F_UID_GID, NFQA_CFG_F_UID_GID)){ 47 | printf("WARNING: UID not available on this kernel/libnetfilter_queue\n"); 48 | } 49 | #endif 50 | } 51 | 52 | static int nf_callback(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfa, void *arg){ 53 | if (stop) { 54 | return -1; 55 | } 56 | 57 | uint32_t id = -1, idx = 0, mark = 0; 58 | struct nfqnl_msg_packet_hdr *ph = NULL; 59 | unsigned char *buffer = NULL; 60 | int size = 0; 61 | verdictContainer vc = {0}; 62 | uint32_t uid = 0xffffffff; 63 | 64 | mark = nfq_get_nfmark(nfa); 65 | ph = nfq_get_msg_packet_hdr(nfa); 66 | id = ntohl(ph->packet_id); 67 | size = nfq_get_payload(nfa, &buffer); 68 | idx = (uint32_t)((uintptr_t)arg); 69 | 70 | #ifdef NFQA_CFG_F_UID_GID 71 | if (get_uid) 72 | nfq_get_uid(nfa, &uid); 73 | #endif 74 | 75 | go_callback(id, buffer, size, mark, idx, &vc, uid); 76 | 77 | if( vc.mark_set == 1 ) { 78 | return nfq_set_verdict2(qh, id, vc.verdict, vc.mark, vc.length, vc.data); 79 | } 80 | return nfq_set_verdict2(qh, id, vc.verdict, vc.mark, vc.length, vc.data); 81 | } 82 | 83 | static inline struct nfq_q_handle* CreateQueue(struct nfq_handle *h, u_int16_t queue, u_int32_t idx) { 84 | struct nfq_q_handle* qh = nfq_create_queue(h, queue, &nf_callback, (void*)((uintptr_t)idx)); 85 | if (qh == NULL){ 86 | printf("ERROR: nfq_create_queue() queue not created\n"); 87 | } else { 88 | configure_uid_if_available(qh); 89 | } 90 | return qh; 91 | } 92 | 93 | static inline void stop_reading_packets() { 94 | stop = 1; 95 | } 96 | 97 | static inline int Run(struct nfq_handle *h, int fd) { 98 | char buf[4096] __attribute__ ((aligned)); 99 | int rcvd, opt = 1; 100 | 101 | setsockopt(fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &opt, sizeof(int)); 102 | 103 | while ((rcvd = recv(fd, buf, sizeof(buf), 0)) >= 0) { 104 | if (stop == 1) { 105 | return errno; 106 | } 107 | nfq_handle_packet(h, buf, rcvd); 108 | } 109 | 110 | return errno; 111 | } 112 | 113 | #endif 114 | -------------------------------------------------------------------------------- /daemon/netlink/socket.go: -------------------------------------------------------------------------------- 1 | package netlink 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "syscall" 8 | 9 | "github.com/evilsocket/opensnitch/daemon/log" 10 | ) 11 | 12 | // GetSocketInfo asks the kernel via netlink for a given connection. 13 | // If the connection is found, we return the uid and the possible 14 | // associated inodes. 15 | // If the outgoing connection is not found but there're entries with the source 16 | // port and same protocol, add all the inodes to the list. 17 | // 18 | // Some examples: 19 | // outgoing connection as seen by netfilter || connection details dumped from kernel 20 | // 21 | // 47344:192.168.1.106 -> 151.101.65.140:443 || in kernel: 47344:192.168.1.106 -> 151.101.65.140:443 22 | // 8612:192.168.1.5 -> 192.168.1.255:8612 || in kernel: 8612:192.168.1.105 -> 0.0.0.0:0 23 | // 123:192.168.1.5 -> 217.144.138.234:123 || in kernel: 123:0.0.0.0 -> 0.0.0.0:0 24 | // 45015:127.0.0.1 -> 239.255.255.250:1900 || in kernel: 45015:127.0.0.1 -> 0.0.0.0:0 25 | // 50416:fe80::9fc2:ddcf:df22:aa50 -> fe80::1:53 || in kernel: 50416:254.128.0.0 -> 254.128.0.0:53 26 | // 51413:192.168.1.106 -> 103.224.182.250:1337 || in kernel: 51413:0.0.0.0 -> 0.0.0.0:0 27 | func GetSocketInfo(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint) (uid int, inodes []int) { 28 | uid = -1 29 | family := uint8(syscall.AF_INET) 30 | ipproto := uint8(syscall.IPPROTO_TCP) 31 | protoLen := len(proto) 32 | if proto[protoLen-1:protoLen] == "6" { 33 | family = syscall.AF_INET6 34 | } 35 | 36 | if proto[:3] == "udp" { 37 | ipproto = syscall.IPPROTO_UDP 38 | if protoLen >= 7 && proto[:7] == "udplite" { 39 | ipproto = syscall.IPPROTO_UDPLITE 40 | } 41 | } 42 | if sockList, err := SocketGet(family, ipproto, uint16(srcPort), uint16(dstPort), srcIP, dstIP); err == nil { 43 | for n, sock := range sockList { 44 | if sock.UID != 0xffffffff { 45 | uid = int(sock.UID) 46 | } 47 | log.Debug("[%d/%d] outgoing connection: %d:%v -> %v:%d || netlink response: %d:%v -> %v:%d inode: %d - loopback: %v multicast: %v unspecified: %v linklocalunicast: %v ifaceLocalMulticast: %v GlobalUni: %v ", 48 | n, len(sockList), 49 | srcPort, srcIP, dstIP, dstPort, 50 | sock.ID.SourcePort, sock.ID.Source, 51 | sock.ID.Destination, sock.ID.DestinationPort, sock.INode, 52 | sock.ID.Destination.IsLoopback(), 53 | sock.ID.Destination.IsMulticast(), 54 | sock.ID.Destination.IsUnspecified(), 55 | sock.ID.Destination.IsLinkLocalUnicast(), 56 | sock.ID.Destination.IsLinkLocalMulticast(), 57 | sock.ID.Destination.IsGlobalUnicast(), 58 | ) 59 | 60 | if sock.ID.SourcePort == uint16(srcPort) && sock.ID.Source.Equal(srcIP) && 61 | (sock.ID.DestinationPort == uint16(dstPort)) && 62 | ((sock.ID.Destination.IsGlobalUnicast() || sock.ID.Destination.IsLoopback()) && sock.ID.Destination.Equal(dstIP)) { 63 | inodes = append([]int{int(sock.INode)}, inodes...) 64 | continue 65 | } else if sock.ID.SourcePort == uint16(srcPort) && sock.ID.Source.Equal(srcIP) && 66 | (sock.ID.DestinationPort == uint16(dstPort)) { 67 | inodes = append([]int{int(sock.INode)}, inodes...) 68 | continue 69 | } 70 | log.Debug("GetSocketInfo() invalid: %d:%v -> %v:%d", sock.ID.SourcePort, sock.ID.Source, sock.ID.Destination, sock.ID.DestinationPort) 71 | } 72 | 73 | if len(inodes) == 0 && len(sockList) > 0 { 74 | for n, sock := range sockList { 75 | inodes = append([]int{int(sock.INode)}, inodes...) 76 | log.Debug("netlink socket not found, adding entry: %d:%v -> %v:%d || %d:%v -> %v:%d inode: %d state: %s", 77 | srcPort, srcIP, dstIP, dstPort, 78 | sockList[n].ID.SourcePort, sockList[n].ID.Source, 79 | sockList[n].ID.Destination, sockList[n].ID.DestinationPort, 80 | sockList[n].INode, TCPStatesMap[sock.State]) 81 | } 82 | } 83 | } else { 84 | log.Debug("netlink socket error: %v - %d:%v -> %v:%d", err, srcPort, srcIP, dstIP, dstPort) 85 | } 86 | 87 | return uid, inodes 88 | } 89 | 90 | // GetSocketInfoByInode dumps the kernel sockets table and searches the given 91 | // inode on it. 92 | func GetSocketInfoByInode(inodeStr string) (*Socket, error) { 93 | inode, err := strconv.ParseUint(inodeStr, 10, 32) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | type inetStruct struct{ family, proto uint8 } 99 | socketTypes := []inetStruct{ 100 | {syscall.AF_INET, syscall.IPPROTO_TCP}, 101 | {syscall.AF_INET, syscall.IPPROTO_UDP}, 102 | {syscall.AF_INET6, syscall.IPPROTO_TCP}, 103 | {syscall.AF_INET6, syscall.IPPROTO_UDP}, 104 | } 105 | 106 | for _, socket := range socketTypes { 107 | socketList, err := SocketsDump(socket.family, socket.proto) 108 | if err != nil { 109 | return nil, err 110 | } 111 | for idx := range socketList { 112 | if uint32(inode) == socketList[idx].INode { 113 | return socketList[idx], nil 114 | } 115 | } 116 | } 117 | return nil, fmt.Errorf("Inode not found") 118 | } 119 | -------------------------------------------------------------------------------- /daemon/netlink/socket_linux.go: -------------------------------------------------------------------------------- 1 | package netlink 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "syscall" 9 | 10 | "github.com/evilsocket/opensnitch/daemon/log" 11 | "github.com/vishvananda/netlink/nl" 12 | ) 13 | 14 | // This is a modification of https://github.com/vishvananda/netlink socket_linux.go - Apache2.0 license 15 | // which adds support for query UDP, UDPLITE and IPv6 sockets to SocketGet() 16 | 17 | const ( 18 | sizeofSocketID = 0x30 19 | sizeofSocketRequest = sizeofSocketID + 0x8 20 | sizeofSocket = sizeofSocketID + 0x18 21 | ) 22 | 23 | var ( 24 | native = nl.NativeEndian() 25 | networkOrder = binary.BigEndian 26 | TCP_ALL = uint32(0xfff) 27 | ) 28 | 29 | // https://elixir.bootlin.com/linux/latest/source/include/net/tcp_states.h 30 | const ( 31 | TCP_INVALID = iota 32 | TCP_ESTABLISHED 33 | TCP_SYN_SENT 34 | TCP_SYN_RECV 35 | TCP_FIN_WAIT1 36 | TCP_FIN_WAIT2 37 | TCP_TIME_WAIT 38 | TCP_CLOSE 39 | TCP_CLOSE_WAIT 40 | TCP_LAST_ACK 41 | TCP_LISTEN 42 | TCP_CLOSING 43 | TCP_NEW_SYN_REC 44 | TCP_MAX_STATES 45 | ) 46 | 47 | // TCPStatesMap holds the list of TCP states 48 | var TCPStatesMap = map[uint8]string{ 49 | TCP_INVALID: "invalid", 50 | TCP_ESTABLISHED: "established", 51 | TCP_SYN_SENT: "syn_sent", 52 | TCP_SYN_RECV: "syn_recv", 53 | TCP_FIN_WAIT1: "fin_wait1", 54 | TCP_FIN_WAIT2: "fin_wait2", 55 | TCP_TIME_WAIT: "time_wait", 56 | TCP_CLOSE: "close", 57 | TCP_CLOSE_WAIT: "close_wait", 58 | TCP_LAST_ACK: "last_ack", 59 | TCP_LISTEN: "listen", 60 | TCP_CLOSING: "closing", 61 | } 62 | 63 | // SocketID holds the socket information of a request/response to the kernel 64 | type SocketID struct { 65 | SourcePort uint16 66 | DestinationPort uint16 67 | Source net.IP 68 | Destination net.IP 69 | Interface uint32 70 | Cookie [2]uint32 71 | } 72 | 73 | // Socket represents a netlink socket. 74 | type Socket struct { 75 | Family uint8 76 | State uint8 77 | Timer uint8 78 | Retrans uint8 79 | ID SocketID 80 | Expires uint32 81 | RQueue uint32 82 | WQueue uint32 83 | UID uint32 84 | INode uint32 85 | } 86 | 87 | // SocketRequest holds the request/response of a connection to the kernel 88 | type SocketRequest struct { 89 | Family uint8 90 | Protocol uint8 91 | Ext uint8 92 | pad uint8 93 | States uint32 94 | ID SocketID 95 | } 96 | 97 | type writeBuffer struct { 98 | Bytes []byte 99 | pos int 100 | } 101 | 102 | func (b *writeBuffer) Write(c byte) { 103 | b.Bytes[b.pos] = c 104 | b.pos++ 105 | } 106 | 107 | func (b *writeBuffer) Next(n int) []byte { 108 | s := b.Bytes[b.pos : b.pos+n] 109 | b.pos += n 110 | return s 111 | } 112 | 113 | // Serialize convert SocketRequest struct to bytes. 114 | func (r *SocketRequest) Serialize() []byte { 115 | b := writeBuffer{Bytes: make([]byte, sizeofSocketRequest)} 116 | b.Write(r.Family) 117 | b.Write(r.Protocol) 118 | b.Write(r.Ext) 119 | b.Write(r.pad) 120 | native.PutUint32(b.Next(4), r.States) 121 | networkOrder.PutUint16(b.Next(2), r.ID.SourcePort) 122 | networkOrder.PutUint16(b.Next(2), r.ID.DestinationPort) 123 | if r.Family == syscall.AF_INET6 { 124 | copy(b.Next(16), r.ID.Source) 125 | copy(b.Next(16), r.ID.Destination) 126 | } else { 127 | copy(b.Next(4), r.ID.Source.To4()) 128 | b.Next(12) 129 | copy(b.Next(4), r.ID.Destination.To4()) 130 | b.Next(12) 131 | } 132 | native.PutUint32(b.Next(4), r.ID.Interface) 133 | native.PutUint32(b.Next(4), r.ID.Cookie[0]) 134 | native.PutUint32(b.Next(4), r.ID.Cookie[1]) 135 | return b.Bytes 136 | } 137 | 138 | // Len returns the size of a socket request 139 | func (r *SocketRequest) Len() int { return sizeofSocketRequest } 140 | 141 | type readBuffer struct { 142 | Bytes []byte 143 | pos int 144 | } 145 | 146 | func (b *readBuffer) Read() byte { 147 | c := b.Bytes[b.pos] 148 | b.pos++ 149 | return c 150 | } 151 | 152 | func (b *readBuffer) Next(n int) []byte { 153 | s := b.Bytes[b.pos : b.pos+n] 154 | b.pos += n 155 | return s 156 | } 157 | 158 | func (s *Socket) deserialize(b []byte) error { 159 | if len(b) < sizeofSocket { 160 | return fmt.Errorf("socket data short read (%d); want %d", len(b), sizeofSocket) 161 | } 162 | rb := readBuffer{Bytes: b} 163 | s.Family = rb.Read() 164 | s.State = rb.Read() 165 | s.Timer = rb.Read() 166 | s.Retrans = rb.Read() 167 | s.ID.SourcePort = networkOrder.Uint16(rb.Next(2)) 168 | s.ID.DestinationPort = networkOrder.Uint16(rb.Next(2)) 169 | if s.Family == syscall.AF_INET6 { 170 | s.ID.Source = net.IP(rb.Next(16)) 171 | s.ID.Destination = net.IP(rb.Next(16)) 172 | } else { 173 | s.ID.Source = net.IPv4(rb.Read(), rb.Read(), rb.Read(), rb.Read()) 174 | rb.Next(12) 175 | s.ID.Destination = net.IPv4(rb.Read(), rb.Read(), rb.Read(), rb.Read()) 176 | rb.Next(12) 177 | } 178 | s.ID.Interface = native.Uint32(rb.Next(4)) 179 | s.ID.Cookie[0] = native.Uint32(rb.Next(4)) 180 | s.ID.Cookie[1] = native.Uint32(rb.Next(4)) 181 | s.Expires = native.Uint32(rb.Next(4)) 182 | s.RQueue = native.Uint32(rb.Next(4)) 183 | s.WQueue = native.Uint32(rb.Next(4)) 184 | s.UID = native.Uint32(rb.Next(4)) 185 | s.INode = native.Uint32(rb.Next(4)) 186 | return nil 187 | } 188 | 189 | // SocketGet returns the list of active connections in the kernel 190 | // filtered by several fields. Currently it returns connections 191 | // filtered by source port and protocol. 192 | func SocketGet(family uint8, proto uint8, srcPort, dstPort uint16, local, remote net.IP) ([]*Socket, error) { 193 | _Id := SocketID{ 194 | SourcePort: srcPort, 195 | Cookie: [2]uint32{nl.TCPDIAG_NOCOOKIE, nl.TCPDIAG_NOCOOKIE}, 196 | } 197 | 198 | sockReq := &SocketRequest{ 199 | Family: family, 200 | Protocol: proto, 201 | States: TCP_ALL, 202 | ID: _Id, 203 | } 204 | 205 | return netlinkRequest(sockReq, family, proto, srcPort, dstPort, local, remote) 206 | } 207 | 208 | // SocketsDump returns the list of all connections from the kernel 209 | func SocketsDump(family uint8, proto uint8) ([]*Socket, error) { 210 | sockReq := &SocketRequest{ 211 | Family: family, 212 | Protocol: proto, 213 | States: TCP_ALL, 214 | } 215 | return netlinkRequest(sockReq, 0, 0, 0, 0, nil, nil) 216 | } 217 | 218 | func netlinkRequest(sockReq *SocketRequest, family uint8, proto uint8, srcPort, dstPort uint16, local, remote net.IP) ([]*Socket, error) { 219 | req := nl.NewNetlinkRequest(nl.SOCK_DIAG_BY_FAMILY, syscall.NLM_F_DUMP) 220 | req.AddData(sockReq) 221 | msgs, err := req.Execute(syscall.NETLINK_INET_DIAG, 0) 222 | if err != nil { 223 | return nil, err 224 | } 225 | if len(msgs) == 0 { 226 | return nil, errors.New("Warning, no message nor error from netlink") 227 | } 228 | var sock []*Socket 229 | for n, m := range msgs { 230 | s := &Socket{} 231 | if err = s.deserialize(m); err != nil { 232 | log.Error("[%d] netlink socket error: %s, %d:%v -> %v:%d - %d:%v -> %v:%d", 233 | n, TCPStatesMap[s.State], 234 | srcPort, local, remote, dstPort, 235 | s.ID.SourcePort, s.ID.Source, s.ID.Destination, s.ID.DestinationPort) 236 | continue 237 | } 238 | if s.INode == 0 { 239 | continue 240 | } 241 | 242 | sock = append([]*Socket{s}, sock...) 243 | } 244 | return sock, err 245 | } 246 | -------------------------------------------------------------------------------- /daemon/netstat/entry.go: -------------------------------------------------------------------------------- 1 | package netstat 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // Entry holds the information of a /proc/net/* entry. 8 | // For example, /proc/net/tcp: 9 | // sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 10 | // 0: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 18083222 11 | type Entry struct { 12 | Proto string 13 | SrcIP net.IP 14 | SrcPort uint 15 | DstIP net.IP 16 | DstPort uint 17 | UserId int 18 | INode int 19 | } 20 | 21 | // NewEntry creates a new entry with values from /proc/net/ 22 | func NewEntry(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint, userId int, iNode int) Entry { 23 | return Entry{ 24 | Proto: proto, 25 | SrcIP: srcIP, 26 | SrcPort: srcPort, 27 | DstIP: dstIP, 28 | DstPort: dstPort, 29 | UserId: userId, 30 | INode: iNode, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /daemon/netstat/find.go: -------------------------------------------------------------------------------- 1 | package netstat 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/evilsocket/opensnitch/daemon/core" 8 | "github.com/evilsocket/opensnitch/daemon/log" 9 | ) 10 | 11 | // FindEntry looks for the connection in the list of known connections in ProcFS. 12 | func FindEntry(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint) *Entry { 13 | if entry := findEntryForProtocol(proto, srcIP, srcPort, dstIP, dstPort); entry != nil { 14 | return entry 15 | } 16 | 17 | ipv6Suffix := "6" 18 | if core.IPv6Enabled && strings.HasSuffix(proto, ipv6Suffix) == false { 19 | otherProto := proto + ipv6Suffix 20 | log.Debug("Searching for %s netstat entry instead of %s", otherProto, proto) 21 | if entry := findEntryForProtocol(otherProto, srcIP, srcPort, dstIP, dstPort); entry != nil { 22 | return entry 23 | } 24 | } 25 | 26 | return &Entry{ 27 | Proto: proto, 28 | SrcIP: srcIP, 29 | SrcPort: srcPort, 30 | DstIP: dstIP, 31 | DstPort: dstPort, 32 | UserId: -1, 33 | INode: -1, 34 | } 35 | } 36 | 37 | func findEntryForProtocol(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint) *Entry { 38 | entries, err := Parse(proto) 39 | if err != nil { 40 | log.Warning("Error while searching for %s netstat entry: %s", proto, err) 41 | return nil 42 | } 43 | 44 | for _, entry := range entries { 45 | if srcIP.Equal(entry.SrcIP) && srcPort == entry.SrcPort && dstIP.Equal(entry.DstIP) && dstPort == entry.DstPort { 46 | return &entry 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /daemon/netstat/parse.go: -------------------------------------------------------------------------------- 1 | package netstat 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "net" 8 | "os" 9 | "regexp" 10 | "strconv" 11 | 12 | "github.com/evilsocket/opensnitch/daemon/core" 13 | "github.com/evilsocket/opensnitch/daemon/log" 14 | ) 15 | 16 | var ( 17 | parser = regexp.MustCompile(`(?i)` + 18 | `\d+:\s+` + // sl 19 | `([a-f0-9]{8,32}):([a-f0-9]{4})\s+` + // local_address 20 | `([a-f0-9]{8,32}):([a-f0-9]{4})\s+` + // rem_address 21 | `[a-f0-9]{2}\s+` + // st 22 | `[a-f0-9]{8}:[a-f0-9]{8}\s+` + // tx_queue rx_queue 23 | `[a-f0-9]{2}:[a-f0-9]{8}\s+` + // tr tm->when 24 | `[a-f0-9]{8}\s+` + // retrnsmt 25 | `(\d+)\s+` + // uid 26 | `\d+\s+` + // timeout 27 | `(\d+)\s+` + // inode 28 | `.+`) // stuff we don't care about 29 | ) 30 | 31 | func decToInt(n string) int { 32 | d, err := strconv.ParseInt(n, 10, 64) 33 | if err != nil { 34 | log.Fatal("Error while parsing %s to int: %s", n, err) 35 | } 36 | return int(d) 37 | } 38 | 39 | func hexToInt(h string) uint { 40 | d, err := strconv.ParseUint(h, 16, 64) 41 | if err != nil { 42 | log.Fatal("Error while parsing %s to int: %s", h, err) 43 | } 44 | return uint(d) 45 | } 46 | 47 | func hexToInt2(h string) (uint, uint) { 48 | if len(h) > 16 { 49 | d, err := strconv.ParseUint(h[:16], 16, 64) 50 | if err != nil { 51 | log.Fatal("Error while parsing %s to int: %s", h[16:], err) 52 | } 53 | d2, err := strconv.ParseUint(h[16:], 16, 64) 54 | if err != nil { 55 | log.Fatal("Error while parsing %s to int: %s", h[16:], err) 56 | } 57 | return uint(d), uint(d2) 58 | } 59 | 60 | d, err := strconv.ParseUint(h, 16, 64) 61 | if err != nil { 62 | log.Fatal("Error while parsing %s to int: %s", h[16:], err) 63 | } 64 | return uint(d), 0 65 | } 66 | 67 | func hexToIP(h string) net.IP { 68 | n, m := hexToInt2(h) 69 | var ip net.IP 70 | if m != 0 { 71 | ip = make(net.IP, 16) 72 | // TODO: Check if this depends on machine endianness? 73 | binary.LittleEndian.PutUint32(ip, uint32(n>>32)) 74 | binary.LittleEndian.PutUint32(ip[4:], uint32(n)) 75 | binary.LittleEndian.PutUint32(ip[8:], uint32(m>>32)) 76 | binary.LittleEndian.PutUint32(ip[12:], uint32(m)) 77 | } else { 78 | ip = make(net.IP, 4) 79 | binary.LittleEndian.PutUint32(ip, uint32(n)) 80 | } 81 | return ip 82 | } 83 | 84 | // Parse scans and retrieves the opened connections, from /proc/net/ files 85 | func Parse(proto string) ([]Entry, error) { 86 | filename := fmt.Sprintf("/proc/net/%s", proto) 87 | fd, err := os.Open(filename) 88 | if err != nil { 89 | return nil, err 90 | } 91 | defer fd.Close() 92 | 93 | entries := make([]Entry, 0) 94 | scanner := bufio.NewScanner(fd) 95 | for lineno := 0; scanner.Scan(); lineno++ { 96 | // skip column names 97 | if lineno == 0 { 98 | continue 99 | } 100 | 101 | line := core.Trim(scanner.Text()) 102 | m := parser.FindStringSubmatch(line) 103 | if m == nil { 104 | log.Warning("Could not parse netstat line from %s: %s", filename, line) 105 | continue 106 | } 107 | 108 | entries = append(entries, NewEntry( 109 | proto, 110 | hexToIP(m[1]), 111 | hexToInt(m[2]), 112 | hexToIP(m[3]), 113 | hexToInt(m[4]), 114 | decToInt(m[5]), 115 | decToInt(m[6]), 116 | )) 117 | } 118 | 119 | return entries, nil 120 | } 121 | -------------------------------------------------------------------------------- /daemon/opensnitch.spec: -------------------------------------------------------------------------------- 1 | Name: opensnitch 2 | Version: 1.3.0 3 | Release: 1%{?dist} 4 | Summary: OpenSnitch is a GNU/Linux application firewall 5 | 6 | License: GPLv3+ 7 | URL: https://github.com/evilsocket/%{name} 8 | Source0: https://github.com/evilsocket/%{name}/releases/download/v%{version}/%{name}_%{version}.orig.tar.gz 9 | #BuildArch: x86_64 10 | 11 | #BuildRequires: godep 12 | Requires(post): info 13 | Requires(preun): info 14 | 15 | %description 16 | Whenever a program makes a connection, it'll prompt the user to allow or deny 17 | it. 18 | 19 | The user can decide if block the outgoing connection based on properties of 20 | the connection: by port, by uid, by dst ip, by program or a combination 21 | of them. 22 | 23 | These rules can last forever, until the app restart or just one time. 24 | 25 | The GUI allows the user to view live outgoing connections, as well as search 26 | by process, user, host or port. 27 | 28 | %prep 29 | rm -rf %{buildroot} 30 | 31 | %setup 32 | 33 | %build 34 | mkdir -p go/src/github.com/evilsocket 35 | ln -s $(pwd) go/src/github.com/evilsocket/opensnitch 36 | export GOPATH=$(pwd)/go 37 | cd go/src/github.com/evilsocket/opensnitch/daemon/ 38 | go build -o opensnitchd . 39 | 40 | %install 41 | mkdir -p %{buildroot}/usr/bin/ %{buildroot}/usr/lib/systemd/system/ %{buildroot}/etc/opensnitchd/rules %{buildroot}/etc/logrotate.d 42 | sed -i 's/\/usr\/local/\/usr/' daemon/opensnitchd.service 43 | install -m 755 daemon/opensnitchd %{buildroot}/usr/bin/opensnitchd 44 | install -m 644 daemon/opensnitchd.service %{buildroot}/usr/lib/systemd/system/opensnitch.service 45 | install -m 644 debian/opensnitch.logrotate %{buildroot}/etc/logrotate.d/opensnitch 46 | 47 | B="" 48 | if [ -f /etc/opensnitchd/default-config.json ]; then 49 | B="-b" 50 | fi 51 | install -m 644 -b $B daemon/default-config.json %{buildroot}/etc/opensnitchd/default-config.json 52 | 53 | B="" 54 | if [ -f /etc/opensnitchd/system-fw.json ]; then 55 | B="-b" 56 | fi 57 | install -m 644 -b $B daemon/system-fw.json %{buildroot}/etc/opensnitchd/system-fw.json 58 | 59 | # upgrade, uninstall 60 | %preun 61 | systemctl stop opensnitch.service || true 62 | 63 | %post 64 | if [ $1 -eq 1 ]; then 65 | systemctl enable opensnitch.service 66 | fi 67 | systemctl start opensnitch.service 68 | 69 | # uninstall,upgrade 70 | %postun 71 | if [ $1 -eq 0 ]; then 72 | systemctl disable opensnitch.service 73 | fi 74 | if [ $1 -eq 0 -a -f /etc/logrotate.d/opensnitch ]; then 75 | rm /etc/logrotate.d/opensnitch 76 | fi 77 | 78 | # postun is the last step after reinstalling 79 | if [ $1 -eq 1 ]; then 80 | systemctl start opensnitch.service 81 | fi 82 | 83 | %clean 84 | rm -rf %{buildroot} 85 | 86 | %files 87 | %{_bindir}/opensnitchd 88 | /usr/lib/systemd/system/opensnitch.service 89 | %{_sysconfdir}/opensnitchd/default-config.json 90 | %{_sysconfdir}/opensnitchd/system-fw.json 91 | %{_sysconfdir}/logrotate.d/opensnitch 92 | -------------------------------------------------------------------------------- /daemon/opensnitchd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenSnitch is a GNU/Linux port of the Little Snitch application firewall. 3 | Documentation=https://github.com/gustavo-iniguez-goya/opensnitch/wiki 4 | Wants=network.target 5 | After=network.target 6 | 7 | [Service] 8 | Type=simple 9 | PermissionsStartOnly=true 10 | ExecStartPre=/bin/mkdir -p /etc/opensnitchd/rules 11 | ExecStart=/usr/local/bin/opensnitchd -rules-path /etc/opensnitchd/rules 12 | Restart=always 13 | RestartSec=30 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /daemon/procmon/cache.go: -------------------------------------------------------------------------------- 1 | package procmon 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "time" 8 | ) 9 | 10 | // Inode represents an item of the InodesCache. 11 | // the key is formed as follow: 12 | // inode+srcip+srcport+dstip+dstport 13 | type Inode struct { 14 | Pid int 15 | FdPath string 16 | } 17 | 18 | // ProcEntry represents an item of the pidsCache 19 | type ProcEntry struct { 20 | Pid int 21 | FdPath string 22 | Descriptors []string 23 | Time time.Time 24 | } 25 | 26 | var ( 27 | // cache of inodes, which help to not iterate over all the pidsCache and 28 | // descriptors of /proc//fd/ 29 | // 20-50us vs 50-80ms 30 | inodesCache = make(map[string]*Inode) 31 | maxCachedInodes = 128 32 | // 2nd cache of already known running pids, which also saves time by 33 | // iterating only over a few pids' descriptors, (30us-2ms vs. 50-80ms) 34 | // since it's more likely that most of the connections will be made by the 35 | // same (running) processes. 36 | // The cache is ordered by time, placing in the first places those PIDs with 37 | // active connections. 38 | pidsCache []*ProcEntry 39 | pidsDescriptorsCache = make(map[int][]string) 40 | maxCachedPids = 24 41 | ) 42 | 43 | func addProcEntry(fdPath string, fdList []string, pid int) { 44 | for n := range pidsCache { 45 | if pidsCache[n].Pid == pid { 46 | pidsCache[n].Time = time.Now() 47 | return 48 | } 49 | } 50 | procEntry := &ProcEntry{ 51 | Pid: pid, 52 | FdPath: fdPath, 53 | Descriptors: fdList, 54 | Time: time.Now(), 55 | } 56 | pidsCache = append([]*ProcEntry{procEntry}, pidsCache...) 57 | } 58 | 59 | func sortProcEntries() { 60 | sort.Slice(pidsCache, func(i, j int) bool { 61 | t := pidsCache[i].Time.UnixNano() 62 | u := pidsCache[j].Time.UnixNano() 63 | return t > u || t == u 64 | }) 65 | } 66 | 67 | func deleteProcEntry(pid int) { 68 | for n, procEntry := range pidsCache { 69 | if procEntry.Pid == pid { 70 | pidsCache = append(pidsCache[:n], pidsCache[n+1:]...) 71 | deleteInodeEntry(pid) 72 | break 73 | } 74 | } 75 | } 76 | 77 | func deleteInodeEntry(pid int) { 78 | for k, inodeEntry := range inodesCache { 79 | if inodeEntry.Pid == pid { 80 | delete(inodesCache, k) 81 | } 82 | } 83 | } 84 | 85 | func cleanUpCaches() { 86 | if len(inodesCache) > maxCachedInodes { 87 | for k := range inodesCache { 88 | delete(inodesCache, k) 89 | } 90 | } 91 | if len(pidsCache) > maxCachedPids { 92 | pidsCache = nil 93 | } 94 | } 95 | 96 | func getPidByInodeFromCache(inodeKey string) int { 97 | if _, found := inodesCache[inodeKey]; found == true { 98 | // sometimes the process may have disappeared at this point 99 | if _, err := os.Lstat(fmt.Sprint("/proc/", inodesCache[inodeKey].Pid, "/exe")); err == nil { 100 | return inodesCache[inodeKey].Pid 101 | } 102 | deleteProcEntry(inodesCache[inodeKey].Pid) 103 | } 104 | 105 | return -1 106 | } 107 | 108 | func getPidDescriptorsFromCache(pid int, fdPath string, expect string, descriptors []string) int { 109 | for fdIdx := 0; fdIdx < len(descriptors); fdIdx++ { 110 | descLink := fmt.Sprint(fdPath, descriptors[fdIdx]) 111 | if link, err := os.Readlink(descLink); err == nil && link == expect { 112 | return fdIdx 113 | } 114 | } 115 | 116 | return -1 117 | } 118 | 119 | func getPidFromCache(inode int, inodeKey string, expect string) (int, int) { 120 | // loop over the processes that have generated connections 121 | for n := 0; n < len(pidsCache); n++ { 122 | procEntry := pidsCache[n] 123 | 124 | if idxDesc := getPidDescriptorsFromCache(procEntry.Pid, procEntry.FdPath, expect, procEntry.Descriptors); idxDesc != -1 { 125 | pidsCache[n].Time = time.Now() 126 | return procEntry.Pid, n 127 | } 128 | 129 | descriptors := lookupPidDescriptors(procEntry.FdPath) 130 | if descriptors == nil { 131 | deleteProcEntry(procEntry.Pid) 132 | continue 133 | } 134 | 135 | pidsCache[n].Descriptors = descriptors 136 | if idxDesc := getPidDescriptorsFromCache(procEntry.Pid, procEntry.FdPath, expect, descriptors); idxDesc != -1 { 137 | pidsCache[n].Time = time.Now() 138 | return procEntry.Pid, n 139 | } 140 | } 141 | 142 | return -1, -1 143 | } 144 | -------------------------------------------------------------------------------- /daemon/procmon/details.go: -------------------------------------------------------------------------------- 1 | package procmon 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/evilsocket/opensnitch/daemon/core" 13 | "github.com/evilsocket/opensnitch/daemon/dns" 14 | "github.com/evilsocket/opensnitch/daemon/netlink" 15 | ) 16 | 17 | var socketsRegex, _ = regexp.Compile(`socket:\[([0-9]+)\]`) 18 | 19 | // GetInfo collects information of a process. 20 | func (p *Process) GetInfo() error { 21 | if err := p.readPath(); err != nil { 22 | return err 23 | } 24 | p.readCwd() 25 | p.readCmdline() 26 | p.readEnv() 27 | p.readDescriptors() 28 | p.readIOStats() 29 | p.readStatus() 30 | p.cleanPath() 31 | 32 | return nil 33 | } 34 | 35 | func (p *Process) setCwd(cwd string) { 36 | p.CWD = cwd 37 | } 38 | 39 | func (p *Process) readCwd() error { 40 | link, err := os.Readlink(fmt.Sprintf("/proc/%d/cwd", p.ID)) 41 | if err != nil { 42 | return err 43 | } 44 | p.CWD = link 45 | return nil 46 | } 47 | 48 | // read and parse environment variables of a process. 49 | func (p *Process) readEnv() { 50 | if data, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/environ", p.ID)); err == nil { 51 | for _, s := range strings.Split(string(data), "\x00") { 52 | parts := strings.SplitN(core.Trim(s), "=", 2) 53 | if parts != nil && len(parts) == 2 { 54 | key := core.Trim(parts[0]) 55 | val := core.Trim(parts[1]) 56 | p.Env[key] = val 57 | } 58 | } 59 | } 60 | } 61 | 62 | func (p *Process) readPath() error { 63 | linkName := fmt.Sprint("/proc/", p.ID, "/exe") 64 | if _, err := os.Lstat(linkName); err != nil { 65 | return err 66 | } 67 | 68 | if link, err := os.Readlink(linkName); err == nil { 69 | p.Path = link 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func (p *Process) readCmdline() { 76 | if data, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", p.ID)); err == nil { 77 | for i, b := range data { 78 | if b == 0x00 { 79 | data[i] = byte(' ') 80 | } 81 | } 82 | 83 | p.Args = make([]string, 0) 84 | 85 | args := strings.Split(string(data), " ") 86 | for _, arg := range args { 87 | arg = core.Trim(arg) 88 | if arg != "" { 89 | p.Args = append(p.Args, arg) 90 | } 91 | } 92 | } 93 | } 94 | 95 | func (p *Process) readDescriptors() { 96 | f, err := os.Open(fmt.Sprint("/proc/", p.ID, "/fd/")) 97 | if err != nil { 98 | return 99 | } 100 | fDesc, err := f.Readdir(-1) 101 | f.Close() 102 | p.Descriptors = nil 103 | 104 | for _, fd := range fDesc { 105 | tempFd := &procDescriptors{ 106 | Name: fd.Name(), 107 | } 108 | if link, err := os.Readlink(fmt.Sprint("/proc/", p.ID, "/fd/", fd.Name())); err == nil { 109 | tempFd.SymLink = link 110 | socket := socketsRegex.FindStringSubmatch(link) 111 | if len(socket) > 0 { 112 | socketInfo, err := netlink.GetSocketInfoByInode(socket[1]) 113 | if err == nil { 114 | tempFd.SymLink = fmt.Sprintf("socket:[%s] - %d:%s -> %s:%d, state: %s", fd.Name(), 115 | socketInfo.ID.SourcePort, 116 | socketInfo.ID.Source.String(), 117 | dns.HostOr(socketInfo.ID.Destination, socketInfo.ID.Destination.String()), 118 | socketInfo.ID.DestinationPort, 119 | netlink.TCPStatesMap[socketInfo.State]) 120 | } 121 | } 122 | 123 | if linkInfo, err := os.Lstat(link); err == nil { 124 | tempFd.Size = linkInfo.Size() 125 | tempFd.ModTime = linkInfo.ModTime() 126 | } 127 | } 128 | p.Descriptors = append(p.Descriptors, tempFd) 129 | } 130 | } 131 | 132 | func (p *Process) readIOStats() { 133 | f, err := os.Open(fmt.Sprint("/proc/", p.ID, "/io")) 134 | if err != nil { 135 | return 136 | } 137 | defer f.Close() 138 | 139 | p.IOStats = &procIOstats{} 140 | 141 | scanner := bufio.NewScanner(f) 142 | for scanner.Scan() { 143 | s := strings.Split(scanner.Text(), " ") 144 | switch s[0] { 145 | case "rchar:": 146 | p.IOStats.RChar, _ = strconv.ParseInt(s[1], 10, 64) 147 | case "wchar:": 148 | p.IOStats.WChar, _ = strconv.ParseInt(s[1], 10, 64) 149 | case "syscr:": 150 | p.IOStats.SyscallRead, _ = strconv.ParseInt(s[1], 10, 64) 151 | case "syscw:": 152 | p.IOStats.SyscallWrite, _ = strconv.ParseInt(s[1], 10, 64) 153 | case "read_bytes:": 154 | p.IOStats.ReadBytes, _ = strconv.ParseInt(s[1], 10, 64) 155 | case "write_bytes:": 156 | p.IOStats.WriteBytes, _ = strconv.ParseInt(s[1], 10, 64) 157 | } 158 | } 159 | } 160 | 161 | func (p *Process) readStatus() { 162 | if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/status")); err == nil { 163 | p.Status = string(data) 164 | } 165 | if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/stat")); err == nil { 166 | p.Stat = string(data) 167 | } 168 | if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/stack")); err == nil { 169 | p.Stack = string(data) 170 | } 171 | if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/maps")); err == nil { 172 | p.Maps = string(data) 173 | } 174 | if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/statm")); err == nil { 175 | p.Statm = &procStatm{} 176 | fmt.Sscanf(string(data), "%d %d %d %d %d %d %d", &p.Statm.Size, &p.Statm.Resident, &p.Statm.Shared, &p.Statm.Text, &p.Statm.Lib, &p.Statm.Data, &p.Statm.Dt) 177 | } 178 | } 179 | 180 | func (p *Process) cleanPath() { 181 | pathLen := len(p.Path) 182 | if pathLen >= 10 && p.Path[pathLen-10:] == " (deleted)" { 183 | p.Path = p.Path[:len(p.Path)-10] 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /daemon/procmon/find.go: -------------------------------------------------------------------------------- 1 | package procmon 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strconv" 8 | ) 9 | 10 | func sortPidsByTime(fdList []os.FileInfo) []os.FileInfo { 11 | sort.Slice(fdList, func(i, j int) bool { 12 | t := fdList[i].ModTime().UnixNano() 13 | u := fdList[j].ModTime().UnixNano() 14 | return t > u 15 | }) 16 | return fdList 17 | } 18 | 19 | // inodeFound searches for the given inode in /proc//fd/ or 20 | // /proc//task//fd/ and gets the symbolink link it points to, 21 | // in order to compare it against the given inode. 22 | // 23 | // If the inode is found, the cache is updated ans sorted. 24 | func inodeFound(pidsPath, expect, inodeKey string, inode, pid int) bool { 25 | fdPath := fmt.Sprint(pidsPath, pid, "/fd/") 26 | fdList := lookupPidDescriptors(fdPath) 27 | if fdList == nil { 28 | return false 29 | } 30 | 31 | for idx := 0; idx < len(fdList); idx++ { 32 | descLink := fmt.Sprint(fdPath, fdList[idx]) 33 | if link, err := os.Readlink(descLink); err == nil && link == expect { 34 | inodesCache[inodeKey] = &Inode{FdPath: descLink, Pid: pid} 35 | addProcEntry(fdPath, fdList, pid) 36 | return true 37 | } 38 | } 39 | 40 | return false 41 | } 42 | 43 | // lookupPidInProc searches for an inode in /proc. 44 | // First it gets the running PIDs and obtains the opened sockets. 45 | // TODO: If the inode is not found, search again in the task/threads 46 | // of every PID (costly). 47 | func lookupPidInProc(pidsPath, expect, inodeKey string, inode int) int { 48 | pidList := getProcPids(pidsPath) 49 | for _, pid := range pidList { 50 | if inodeFound(pidsPath, expect, inodeKey, inode, pid) { 51 | return pid 52 | } 53 | } 54 | return -1 55 | } 56 | 57 | // lookupPidDescriptors returns the list of descriptors inside 58 | // /proc//fd/ 59 | // TODO: search in /proc//task//fd/ . 60 | func lookupPidDescriptors(fdPath string) []string { 61 | f, err := os.Open(fdPath) 62 | if err != nil { 63 | return nil 64 | } 65 | fdList, err := f.Readdir(-1) 66 | f.Close() 67 | if err != nil { 68 | return nil 69 | } 70 | fdList = sortPidsByTime(fdList) 71 | 72 | s := make([]string, len(fdList)) 73 | for n, f := range fdList { 74 | s[n] = f.Name() 75 | } 76 | 77 | return s 78 | } 79 | 80 | // getProcPids returns the list of running PIDs, /proc or /proc//task/ . 81 | func getProcPids(pidsPath string) (pidList []int) { 82 | f, err := os.Open(pidsPath) 83 | if err != nil { 84 | return pidList 85 | } 86 | ls, err := f.Readdir(-1) 87 | f.Close() 88 | if err != nil { 89 | return pidList 90 | } 91 | ls = sortPidsByTime(ls) 92 | 93 | for _, f := range ls { 94 | if f.IsDir() == false { 95 | continue 96 | } 97 | if pid, err := strconv.Atoi(f.Name()); err == nil { 98 | pidList = append(pidList, []int{pid}...) 99 | } 100 | } 101 | 102 | return pidList 103 | } 104 | -------------------------------------------------------------------------------- /daemon/procmon/find_test.go: -------------------------------------------------------------------------------- 1 | package procmon 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGetProcPids(t *testing.T) { 9 | pids := getProcPids("/proc") 10 | 11 | if len(pids) == 0 { 12 | t.Error("getProcPids() should not be 0", pids) 13 | } 14 | } 15 | 16 | func TestLookupPidDescriptors(t *testing.T) { 17 | pidsFd := lookupPidDescriptors(fmt.Sprint("/proc/", myPid, "/fd/")) 18 | 19 | if len(pidsFd) == 0 { 20 | t.Error("getProcPids() should not be 0", pidsFd) 21 | } 22 | } 23 | 24 | func TestLookupPidInProc(t *testing.T) { 25 | pidsFd := lookupPidDescriptors(fmt.Sprint("/proc/", myPid, "/fd/")) 26 | 27 | if len(pidsFd) == 0 { 28 | t.Error("lookupPidInProc() pids length should not be 0", pidsFd) 29 | } 30 | 31 | // we expect that the inode 1 points to /dev/null 32 | expect := "/dev/null" 33 | foundPid := lookupPidInProc("/proc/", expect, "", 1) 34 | if foundPid != myPid { 35 | t.Error("lookupPidInProc() found PID (x) should be (y)", foundPid, myPid) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /daemon/procmon/parse.go: -------------------------------------------------------------------------------- 1 | package procmon 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/evilsocket/opensnitch/daemon/log" 9 | "github.com/evilsocket/opensnitch/daemon/procmon/audit" 10 | ) 11 | 12 | func getPIDFromAuditEvents(inode int, inodeKey string, expect string) (int, int) { 13 | audit.Lock.RLock() 14 | defer audit.Lock.RUnlock() 15 | 16 | auditEvents := audit.GetEvents() 17 | for n := 0; n < len(auditEvents); n++ { 18 | pid := auditEvents[n].Pid 19 | if inodeFound("/proc/", expect, inodeKey, inode, pid) { 20 | return pid, n 21 | } 22 | } 23 | for n := 0; n < len(auditEvents); n++ { 24 | ppid := auditEvents[n].PPid 25 | if inodeFound("/proc/", expect, inodeKey, inode, ppid) { 26 | return ppid, n 27 | } 28 | } 29 | return -1, -1 30 | } 31 | 32 | // GetPIDFromINode tries to get the PID from a socket inode following these steps: 33 | // 1. Get the PID from the cache of Inodes. 34 | // 2. Get the PID from the cache of PIDs. 35 | // 3. Look for the PID using one of these methods: 36 | // - ftrace: listening processes execs/exits from /sys/kernel/debug/tracing/ 37 | // - audit: listening for socket creation from auditd. 38 | // - proc: search /proc 39 | // 40 | // If the PID is not found by one of the 2 first methods, it'll try it using /proc. 41 | func GetPIDFromINode(inode int, inodeKey string) int { 42 | found := -1 43 | if inode <= 0 { 44 | return found 45 | } 46 | start := time.Now() 47 | cleanUpCaches() 48 | 49 | expect := fmt.Sprintf("socket:[%d]", inode) 50 | if cachedPidInode := getPidByInodeFromCache(inodeKey); cachedPidInode != -1 { 51 | log.Debug("Inode found in cache: %v %v %v %v", time.Since(start), inodesCache[inodeKey], inode, inodeKey) 52 | return cachedPidInode 53 | } 54 | 55 | cachedPid, pos := getPidFromCache(inode, inodeKey, expect) 56 | if cachedPid != -1 { 57 | log.Debug("Socket found in known pids %v, pid: %d, inode: %d, pos: %d, pids in cache: %d", time.Since(start), cachedPid, inode, pos, len(pidsCache)) 58 | sortProcEntries() 59 | return cachedPid 60 | } 61 | 62 | if methodIsAudit() { 63 | if aPid, pos := getPIDFromAuditEvents(inode, inodeKey, expect); aPid != -1 { 64 | log.Debug("PID found via audit events: %v, position: %d", time.Since(start), pos) 65 | return aPid 66 | } 67 | } else if methodIsFtrace() && IsWatcherAvailable() { 68 | forEachProcess(func(pid int, path string, args []string) bool { 69 | if inodeFound("/proc/", expect, inodeKey, inode, pid) { 70 | found = pid 71 | return true 72 | } 73 | // keep looping 74 | return false 75 | }) 76 | } 77 | if found == -1 || methodIsProc() { 78 | found = lookupPidInProc("/proc/", expect, inodeKey, inode) 79 | } 80 | log.Debug("new pid lookup took (%d): %v", found, time.Since(start)) 81 | 82 | return found 83 | } 84 | 85 | // FindProcess checks if a process exists given a PID. 86 | // If it exists in /proc, a new Process{} object is returned with the details 87 | // to identify a process (cmdline, name, environment variables, etc). 88 | func FindProcess(pid int, interceptUnknown bool) *Process { 89 | if interceptUnknown && pid < 0 { 90 | return NewProcess(0, "") 91 | } 92 | if methodIsAudit() { 93 | if aevent := audit.GetEventByPid(pid); aevent != nil { 94 | audit.Lock.RLock() 95 | proc := NewProcess(pid, aevent.ProcPath) 96 | proc.readCmdline() 97 | proc.setCwd(aevent.ProcDir) 98 | audit.Lock.RUnlock() 99 | // if the proc dir contains non alhpa-numeric chars the field is empty 100 | if proc.CWD == "" { 101 | proc.readCwd() 102 | } 103 | proc.readEnv() 104 | proc.cleanPath() 105 | 106 | return proc 107 | } 108 | } 109 | 110 | linkName := fmt.Sprint("/proc/", pid, "/exe") 111 | if _, err := os.Lstat(linkName); err != nil { 112 | return nil 113 | } 114 | 115 | if link, err := os.Readlink(linkName); err == nil { 116 | proc := NewProcess(pid, link) 117 | 118 | proc.readCmdline() 119 | proc.readCwd() 120 | proc.readEnv() 121 | proc.cleanPath() 122 | 123 | return proc 124 | } 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /daemon/procmon/process.go: -------------------------------------------------------------------------------- 1 | package procmon 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/evilsocket/opensnitch/daemon/log" 7 | "github.com/evilsocket/opensnitch/daemon/procmon/audit" 8 | ) 9 | 10 | // man 5 proc; man procfs 11 | type procIOstats struct { 12 | RChar int64 13 | WChar int64 14 | SyscallRead int64 15 | SyscallWrite int64 16 | ReadBytes int64 17 | WriteBytes int64 18 | } 19 | 20 | type procDescriptors struct { 21 | Name string 22 | SymLink string 23 | Size int64 24 | ModTime time.Time 25 | } 26 | 27 | type procStatm struct { 28 | Size int64 29 | Resident int64 30 | Shared int64 31 | Text int64 32 | Lib int64 33 | Data int64 // data + stack 34 | Dt int 35 | } 36 | 37 | // Process holds the details of a process. 38 | type Process struct { 39 | ID int 40 | Path string 41 | Args []string 42 | Env map[string]string 43 | CWD string 44 | Descriptors []*procDescriptors 45 | IOStats *procIOstats 46 | Status string 47 | Stat string 48 | Statm *procStatm 49 | Stack string 50 | Maps string 51 | } 52 | 53 | // NewProcess returns a new Process structure. 54 | func NewProcess(pid int, path string) *Process { 55 | return &Process{ 56 | ID: pid, 57 | Path: path, 58 | Args: make([]string, 0), 59 | Env: make(map[string]string), 60 | } 61 | } 62 | 63 | // SetMonitorMethod configures a new method for parsing connections. 64 | func SetMonitorMethod(newMonitorMethod string) { 65 | lock.Lock() 66 | defer lock.Unlock() 67 | 68 | monitorMethod = newMonitorMethod 69 | } 70 | 71 | func methodIsFtrace() bool { 72 | lock.RLock() 73 | defer lock.RUnlock() 74 | 75 | return monitorMethod == MethodFtrace 76 | } 77 | 78 | func methodIsAudit() bool { 79 | lock.RLock() 80 | defer lock.RUnlock() 81 | 82 | return monitorMethod == MethodAudit 83 | } 84 | 85 | func methodIsProc() bool { 86 | lock.RLock() 87 | defer lock.RUnlock() 88 | 89 | return monitorMethod == MethodProc 90 | } 91 | 92 | // End stops the way of parsing new connections. 93 | func End() { 94 | if methodIsAudit() { 95 | audit.Stop() 96 | } else if methodIsFtrace() { 97 | go func() { 98 | if err := Stop(); err != nil { 99 | log.Warning("procmon.End() stop ftrace error: %v", err) 100 | } 101 | }() 102 | } 103 | } 104 | 105 | // Init starts parsing connections using the method specified. 106 | func Init() { 107 | if methodIsFtrace() { 108 | err := Start() 109 | if err == nil { 110 | log.Info("Process monitor method ftrace") 111 | return 112 | } 113 | log.Warning("error starting ftrace monitor method: %v", err) 114 | 115 | } else if methodIsAudit() { 116 | auditConn, err := audit.Start() 117 | if err == nil { 118 | log.Info("Process monitor method audit") 119 | go audit.Reader(auditConn, (chan<- audit.Event)(audit.EventChan)) 120 | return 121 | } 122 | log.Warning("error starting audit monitor method: %v", err) 123 | } 124 | 125 | // if any of the above methods have failed, fallback to proc 126 | log.Info("Process monitor method /proc") 127 | SetMonitorMethod(MethodProc) 128 | } 129 | -------------------------------------------------------------------------------- /daemon/procmon/process_test.go: -------------------------------------------------------------------------------- 1 | package procmon 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | myPid = os.Getpid() 10 | proc = NewProcess(myPid, "/fake/path") 11 | ) 12 | 13 | func TestNewProcess(t *testing.T) { 14 | if proc.ID != myPid { 15 | t.Error("NewProcess PID not equal to ", myPid) 16 | } 17 | if proc.Path != "/fake/path" { 18 | t.Error("NewProcess path not equal to /fake/path") 19 | } 20 | } 21 | 22 | func TestProcPath(t *testing.T) { 23 | if err := proc.readPath(); err != nil { 24 | t.Error("Proc path error:", err) 25 | } 26 | if proc.Path == "/fake/path" { 27 | t.Error("Proc path equal to /fake/path, should be different:", proc.Path) 28 | } 29 | } 30 | 31 | func TestProcCwd(t *testing.T) { 32 | err := proc.readCwd() 33 | 34 | if proc.CWD == "" { 35 | t.Error("Proc readCwd() not read:", err) 36 | } 37 | 38 | proc.setCwd("/home") 39 | if proc.CWD != "/home" { 40 | t.Error("Proc setCwd() should be /home:", proc.CWD) 41 | } 42 | } 43 | 44 | func TestProcCmdline(t *testing.T) { 45 | proc.readCmdline() 46 | 47 | if len(proc.Args) == 0 { 48 | t.Error("Proc Args should not be empty:", proc.Args) 49 | } 50 | } 51 | 52 | func TestProcDescriptors(t *testing.T) { 53 | proc.readDescriptors() 54 | 55 | if len(proc.Descriptors) == 0 { 56 | t.Error("Proc Descriptors should not be empty:", proc.Descriptors) 57 | } 58 | } 59 | 60 | func TestProcEnv(t *testing.T) { 61 | proc.readEnv() 62 | 63 | if len(proc.Env) == 0 { 64 | t.Error("Proc Env should not be empty:", proc.Env) 65 | } 66 | } 67 | 68 | func TestProcIOStats(t *testing.T) { 69 | proc.readIOStats() 70 | 71 | if proc.IOStats.RChar == 0 { 72 | t.Error("Proc.IOStats.RChar should not be 0:", proc.IOStats) 73 | } 74 | if proc.IOStats.WChar == 0 { 75 | t.Error("Proc.IOStats.WChar should not be 0:", proc.IOStats) 76 | } 77 | if proc.IOStats.SyscallRead == 0 { 78 | t.Error("Proc.IOStats.SyscallRead should not be 0:", proc.IOStats) 79 | } 80 | if proc.IOStats.SyscallWrite == 0 { 81 | t.Error("Proc.IOStats.SyscallWrite should not be 0:", proc.IOStats) 82 | } 83 | /*if proc.IOStats.ReadBytes == 0 { 84 | t.Error("Proc.IOStats.ReadBytes should not be 0:", proc.IOStats) 85 | } 86 | if proc.IOStats.WriteBytes == 0 { 87 | t.Error("Proc.IOStats.WriteBytes should not be 0:", proc.IOStats) 88 | }*/ 89 | } 90 | 91 | func TestProcStatus(t *testing.T) { 92 | proc.readStatus() 93 | 94 | if proc.Status == "" { 95 | t.Error("Proc Status should not be empty:", proc) 96 | } 97 | if proc.Stat == "" { 98 | t.Error("Proc Stat should not be empty:", proc) 99 | } 100 | /*if proc.Stack == "" { 101 | t.Error("Proc Stack should not be empty:", proc) 102 | }*/ 103 | if proc.Maps == "" { 104 | t.Error("Proc Maps should not be empty:", proc) 105 | } 106 | if proc.Statm.Size == 0 { 107 | t.Error("Proc Statm Size should not be 0:", proc.Statm) 108 | } 109 | if proc.Statm.Resident == 0 { 110 | t.Error("Proc Statm Resident should not be 0:", proc.Statm) 111 | } 112 | if proc.Statm.Shared == 0 { 113 | t.Error("Proc Statm Shared should not be 0:", proc.Statm) 114 | } 115 | if proc.Statm.Text == 0 { 116 | t.Error("Proc Statm Text should not be 0:", proc.Statm) 117 | } 118 | if proc.Statm.Lib != 0 { 119 | t.Error("Proc Statm Lib should not be 0:", proc.Statm) 120 | } 121 | if proc.Statm.Data == 0 { 122 | t.Error("Proc Statm Data should not be 0:", proc.Statm) 123 | } 124 | if proc.Statm.Dt != 0 { 125 | t.Error("Proc Statm Dt should not be 0:", proc.Statm) 126 | } 127 | } 128 | 129 | func TestProcCleanPath(t *testing.T) { 130 | proc.Path = "/fake/path/binary (deleted)" 131 | proc.cleanPath() 132 | if proc.Path != "/fake/path/binary" { 133 | t.Error("Proc cleanPath() not cleaned:", proc.Path) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /daemon/procmon/watcher.go: -------------------------------------------------------------------------------- 1 | package procmon 2 | 3 | import ( 4 | "io/ioutil" 5 | "strconv" 6 | "sync" 7 | 8 | "github.com/evilsocket/ftrace" 9 | "github.com/evilsocket/opensnitch/daemon/log" 10 | ) 11 | 12 | // monitor method supported types 13 | const ( 14 | MethodFtrace = "ftrace" 15 | MethodProc = "proc" 16 | MethodAudit = "audit" 17 | ) 18 | 19 | const ( 20 | probeName = "opensnitch_exec_probe" 21 | syscallName = "do_execve" 22 | ) 23 | 24 | type procData struct { 25 | path string 26 | args []string 27 | } 28 | 29 | var ( 30 | subEvents = []string{ 31 | "sched/sched_process_fork", 32 | "sched/sched_process_exec", 33 | "sched/sched_process_exit", 34 | } 35 | 36 | watcher = ftrace.NewProbe(probeName, syscallName, subEvents) 37 | isAvailable = false 38 | monitorMethod = MethodProc 39 | 40 | index = make(map[int]*procData) 41 | lock = sync.RWMutex{} 42 | ) 43 | 44 | func forEachProcess(cb func(pid int, path string, args []string) bool) { 45 | lock.RLock() 46 | defer lock.RUnlock() 47 | 48 | for pid, data := range index { 49 | if cb(pid, data.path, data.args) == true { 50 | break 51 | } 52 | } 53 | } 54 | 55 | func trackProcess(pid int) { 56 | lock.Lock() 57 | defer lock.Unlock() 58 | if _, found := index[pid]; found == false { 59 | index[pid] = &procData{} 60 | } 61 | } 62 | 63 | func trackProcessArgs(e ftrace.Event) { 64 | lock.Lock() 65 | defer lock.Unlock() 66 | 67 | if d, found := index[e.PID]; found == false { 68 | index[e.PID] = &procData{ 69 | args: e.Argv(), 70 | path: "", 71 | } 72 | } else { 73 | d.args = e.Argv() 74 | } 75 | } 76 | 77 | func trackProcessPath(e ftrace.Event) { 78 | lock.Lock() 79 | defer lock.Unlock() 80 | if d, found := index[e.PID]; found == false { 81 | index[e.PID] = &procData{ 82 | path: e.Args["filename"], 83 | } 84 | } else { 85 | d.path = e.Args["filename"] 86 | } 87 | } 88 | 89 | func trackProcessExit(e ftrace.Event) { 90 | lock.Lock() 91 | defer lock.Unlock() 92 | delete(index, e.PID) 93 | } 94 | 95 | func eventConsumer() { 96 | for event := range watcher.Events() { 97 | if event.IsSyscall == true { 98 | trackProcessArgs(event) 99 | } else if _, ok := event.Args["filename"]; ok && event.Name == "sched_process_exec" { 100 | trackProcessPath(event) 101 | } else if event.Name == "sched_process_exit" { 102 | trackProcessExit(event) 103 | } 104 | } 105 | } 106 | 107 | // Start enables the ftrace monitor method. 108 | // This method configures a kprobe to intercept execve() syscalls. 109 | // The kernel must have configured and enabled debugfs. 110 | func Start() (err error) { 111 | // start from a clean state 112 | if err := watcher.Reset(); err != nil && watcher.Enabled() { 113 | log.Warning("ftrace.Reset() error: %v", err) 114 | } 115 | 116 | if err = watcher.Enable(); err == nil { 117 | isAvailable = true 118 | 119 | go eventConsumer() 120 | // track running processes 121 | if ls, err := ioutil.ReadDir("/proc/"); err == nil { 122 | for _, f := range ls { 123 | if pid, err := strconv.Atoi(f.Name()); err == nil && f.IsDir() { 124 | trackProcess(pid) 125 | } 126 | } 127 | } 128 | } else { 129 | isAvailable = false 130 | } 131 | return 132 | } 133 | 134 | // Stop disables ftrace monitor method, removing configured kprobe. 135 | func Stop() error { 136 | isAvailable = false 137 | return watcher.Disable() 138 | } 139 | 140 | // IsWatcherAvailable checks if ftrace (debugfs) is 141 | func IsWatcherAvailable() bool { 142 | return isAvailable 143 | } 144 | -------------------------------------------------------------------------------- /daemon/rule/loader.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "sort" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/evilsocket/opensnitch/daemon/conman" 16 | "github.com/evilsocket/opensnitch/daemon/core" 17 | "github.com/evilsocket/opensnitch/daemon/log" 18 | 19 | "github.com/fsnotify/fsnotify" 20 | ) 21 | 22 | // Loader is the object that holds the rules loaded from disk, as well as the 23 | // rules watcher. 24 | type Loader struct { 25 | sync.RWMutex 26 | path string 27 | rules map[string]*Rule 28 | rulesKeys []string 29 | watcher *fsnotify.Watcher 30 | liveReload bool 31 | liveReloadRunning bool 32 | } 33 | 34 | // NewLoader loads rules from disk, and watches for changes made to the rules files 35 | // on disk. 36 | func NewLoader(liveReload bool) (*Loader, error) { 37 | watcher, err := fsnotify.NewWatcher() 38 | if err != nil { 39 | return nil, err 40 | } 41 | return &Loader{ 42 | path: "", 43 | rules: make(map[string]*Rule), 44 | liveReload: liveReload, 45 | watcher: watcher, 46 | liveReloadRunning: false, 47 | }, nil 48 | } 49 | 50 | // NumRules returns he number of loaded rules. 51 | func (l *Loader) NumRules() int { 52 | l.RLock() 53 | defer l.RUnlock() 54 | return len(l.rules) 55 | } 56 | 57 | // Load loads rules files from disk. 58 | func (l *Loader) Load(path string) error { 59 | if core.Exists(path) == false { 60 | return fmt.Errorf("Path '%s' does not exist", path) 61 | } 62 | 63 | expr := filepath.Join(path, "*.json") 64 | matches, err := filepath.Glob(expr) 65 | if err != nil { 66 | return fmt.Errorf("Error globbing '%s': %s", expr, err) 67 | } 68 | 69 | l.Lock() 70 | defer l.Unlock() 71 | 72 | l.path = path 73 | if len(l.rules) == 0 { 74 | l.rules = make(map[string]*Rule) 75 | } 76 | diskRules := make(map[string]string) 77 | 78 | for _, fileName := range matches { 79 | log.Debug("Reading rule from %s", fileName) 80 | raw, err := ioutil.ReadFile(fileName) 81 | if err != nil { 82 | return fmt.Errorf("Error while reading %s: %s", fileName, err) 83 | } 84 | 85 | var r Rule 86 | 87 | err = json.Unmarshal(raw, &r) 88 | if err != nil { 89 | log.Error("Error parsing rule from %s: %s", fileName, err) 90 | continue 91 | } 92 | 93 | r.Operator.Compile() 94 | diskRules[r.Name] = r.Name 95 | 96 | log.Debug("Loaded rule from %s: %s", fileName, r.String()) 97 | l.rules[r.Name] = &r 98 | } 99 | for ruleName, inMemoryRule := range l.rules { 100 | if _, ok := diskRules[ruleName]; ok == false { 101 | if inMemoryRule.Duration == Always { 102 | log.Debug("Rule deleted from disk, updating rules list: %s", ruleName) 103 | delete(l.rules, ruleName) 104 | } 105 | } 106 | } 107 | 108 | l.sortRules() 109 | 110 | if l.liveReload && l.liveReloadRunning == false { 111 | go l.liveReloadWorker() 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (l *Loader) liveReloadWorker() { 118 | l.liveReloadRunning = true 119 | 120 | log.Debug("Rules watcher started on path %s ...", l.path) 121 | if err := l.watcher.Add(l.path); err != nil { 122 | log.Error("Could not watch path: %s", err) 123 | l.liveReloadRunning = false 124 | return 125 | } 126 | 127 | for { 128 | select { 129 | case event := <-l.watcher.Events: 130 | // a new rule json file has been created or updated 131 | if (event.Op&fsnotify.Write == fsnotify.Write) || (event.Op&fsnotify.Remove == fsnotify.Remove) { 132 | if strings.HasSuffix(event.Name, ".json") { 133 | log.Important("Ruleset changed due to %s, reloading ...", path.Base(event.Name)) 134 | if err := l.Reload(); err != nil { 135 | log.Error("%s", err) 136 | } 137 | } 138 | } 139 | case err := <-l.watcher.Errors: 140 | log.Error("File system watcher error: %s", err) 141 | } 142 | } 143 | } 144 | 145 | // Reload reloads the rules from disk. 146 | func (l *Loader) Reload() error { 147 | return l.Load(l.path) 148 | } 149 | 150 | // GetAll returns the loaded rules. 151 | func (l *Loader) GetAll() map[string]*Rule { 152 | l.RLock() 153 | defer l.RUnlock() 154 | return l.rules 155 | } 156 | 157 | func (l *Loader) isUniqueName(name string) bool { 158 | _, found := l.rules[name] 159 | return !found 160 | } 161 | 162 | func (l *Loader) setUniqueName(rule *Rule) { 163 | l.Lock() 164 | defer l.Unlock() 165 | 166 | idx := 1 167 | base := rule.Name 168 | for l.isUniqueName(rule.Name) == false { 169 | idx++ 170 | rule.Name = fmt.Sprintf("%s-%d", base, idx) 171 | } 172 | } 173 | 174 | func (l *Loader) sortRules() { 175 | l.rulesKeys = make([]string, 0, len(l.rules)) 176 | for k := range l.rules { 177 | l.rulesKeys = append(l.rulesKeys, k) 178 | } 179 | sort.Strings(l.rulesKeys) 180 | } 181 | 182 | func (l *Loader) addUserRule(rule *Rule) { 183 | if rule.Duration == Once { 184 | return 185 | } 186 | 187 | l.setUniqueName(rule) 188 | l.replaceUserRule(rule) 189 | } 190 | 191 | func (l *Loader) replaceUserRule(rule *Rule) { 192 | l.Lock() 193 | if rule.Operator.Type == List { 194 | if err := json.Unmarshal([]byte(rule.Operator.Data), &rule.Operator.List); err != nil { 195 | log.Error("Error loading rule of type list: %s", err) 196 | } 197 | } 198 | l.rules[rule.Name] = rule 199 | l.sortRules() 200 | l.Unlock() 201 | 202 | if rule.Duration == Restart || rule.Duration == Always { 203 | return 204 | } 205 | 206 | tTime, err := time.ParseDuration(string(rule.Duration)) 207 | if err != nil { 208 | return 209 | } 210 | 211 | time.AfterFunc(tTime, func() { 212 | l.Lock() 213 | delete(l.rules, rule.Name) 214 | l.sortRules() 215 | l.Unlock() 216 | }) 217 | } 218 | 219 | // Add adds a rule to the list of rules, and optionally saves it to disk. 220 | func (l *Loader) Add(rule *Rule, saveToDisk bool) error { 221 | l.addUserRule(rule) 222 | if saveToDisk { 223 | fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name)) 224 | return l.Save(rule, fileName) 225 | } 226 | return nil 227 | } 228 | 229 | // Replace adds a rule to the list of rules, and optionally saves it to disk. 230 | func (l *Loader) Replace(rule *Rule, saveToDisk bool) error { 231 | l.replaceUserRule(rule) 232 | if saveToDisk { 233 | l.Lock() 234 | defer l.Unlock() 235 | 236 | fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name)) 237 | return l.Save(rule, fileName) 238 | } 239 | return nil 240 | } 241 | 242 | // Save a rule to disk. 243 | func (l *Loader) Save(rule *Rule, path string) error { 244 | rule.Updated = time.Now() 245 | raw, err := json.MarshalIndent(rule, "", " ") 246 | if err != nil { 247 | return fmt.Errorf("Error while saving rule %s to %s: %s", rule, path, err) 248 | } 249 | 250 | if err = ioutil.WriteFile(path, raw, 0644); err != nil { 251 | return fmt.Errorf("Error while saving rule %s to %s: %s", rule, path, err) 252 | } 253 | 254 | return nil 255 | } 256 | 257 | // Delete deletes a rule from the list. 258 | // If the duration is Always (i.e: saved on disk), it'll attempt to delete 259 | // it from disk. 260 | func (l *Loader) Delete(ruleName string) error { 261 | l.Lock() 262 | defer l.Unlock() 263 | 264 | rule := l.rules[ruleName] 265 | if rule == nil { 266 | return nil 267 | } 268 | 269 | delete(l.rules, ruleName) 270 | l.sortRules() 271 | 272 | if rule.Duration != Always { 273 | return nil 274 | } 275 | 276 | log.Info("Delete() rule: %s", rule) 277 | path := fmt.Sprint(l.path, "/", ruleName, ".json") 278 | return os.Remove(path) 279 | } 280 | 281 | // FindFirstMatch will try match the connection against the existing rule set. 282 | func (l *Loader) FindFirstMatch(con *conman.Connection) (match *Rule) { 283 | l.RLock() 284 | defer l.RUnlock() 285 | 286 | for _, idx := range l.rulesKeys { 287 | rule, _ := l.rules[idx] 288 | if rule.Enabled == false { 289 | continue 290 | } 291 | if rule.Match(con) { 292 | // We have a match. 293 | // Save the rule in order to don't ask the user to take action, 294 | // and keep iterating until a Deny or a Priority rule appears. 295 | match = rule 296 | if rule.Action == Deny || rule.Precedence == true { 297 | return rule 298 | } 299 | } 300 | } 301 | 302 | return match 303 | } 304 | -------------------------------------------------------------------------------- /daemon/rule/loader_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "io" 5 | "math/rand" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | var tmpDir string 12 | 13 | func TestMain(m *testing.M) { 14 | tmpDir = "/tmp/ostest_" + randString() 15 | os.Mkdir(tmpDir, 0777) 16 | defer os.RemoveAll(tmpDir) 17 | os.Exit(m.Run()) 18 | } 19 | func TestRuleLoader(t *testing.T) { 20 | t.Parallel() 21 | t.Log("Test rules loader") 22 | 23 | var list []Operator 24 | dur1s := Duration("1s") 25 | dummyOper, _ := NewOperator(Simple, false, OpTrue, "", list) 26 | inMem1sRule := Create("000-xxx-name", true, false, Allow, dur1s, dummyOper) 27 | inMemUntilRestartRule := Create("000-aaa-name", true, false, Allow, Restart, dummyOper) 28 | 29 | l, err := NewLoader(false) 30 | if err != nil { 31 | t.Fail() 32 | } 33 | if err = l.Load("/non/existent/path/"); err == nil { 34 | t.Error("non existent path test: err should not be nil") 35 | } 36 | 37 | if err = l.Load("testdata/"); err != nil { 38 | t.Error("Error loading test rules: ", err) 39 | } 40 | 41 | testNumRules(t, l, 2) 42 | 43 | if err = l.Add(inMem1sRule, false); err != nil { 44 | t.Error("Error adding temporary rule") 45 | } 46 | testNumRules(t, l, 3) 47 | 48 | // test auto deletion of temporary rule 49 | time.Sleep(time.Second * 2) 50 | testNumRules(t, l, 2) 51 | 52 | if err = l.Add(inMemUntilRestartRule, false); err != nil { 53 | t.Error("Error adding temporary rule (2)") 54 | } 55 | testNumRules(t, l, 3) 56 | testRulesOrder(t, l) 57 | testSortRules(t, l) 58 | testFindMatch(t, l) 59 | testFindEnabled(t, l) 60 | } 61 | 62 | func TestLiveReload(t *testing.T) { 63 | t.Parallel() 64 | t.Log("Test rules loader with live reload") 65 | l, err := NewLoader(true) 66 | if err != nil { 67 | t.Fail() 68 | } 69 | if err = Copy("testdata/000-allow-chrome.json", tmpDir+"/000-allow-chrome.json"); err != nil { 70 | t.Error("Error copying rule into a temp dir") 71 | } 72 | if err = Copy("testdata/001-deny-chrome.json", tmpDir+"/001-deny-chrome.json"); err != nil { 73 | t.Error("Error copying rule into a temp dir") 74 | } 75 | if err = l.Load(tmpDir); err != nil { 76 | t.Error("Error loading test rules: ", err) 77 | } 78 | //wait for watcher to activate 79 | time.Sleep(time.Second) 80 | if err = Copy("testdata/live_reload/test-live-reload-remove.json", tmpDir+"/test-live-reload-remove.json"); err != nil { 81 | t.Error("Error copying rules into temp dir") 82 | } 83 | if err = Copy("testdata/live_reload/test-live-reload-delete.json", tmpDir+"/test-live-reload-delete.json"); err != nil { 84 | t.Error("Error copying rules into temp dir") 85 | } 86 | //wait for watcher to pick up the changes 87 | time.Sleep(time.Second) 88 | testNumRules(t, l, 4) 89 | if err = os.Remove(tmpDir + "/test-live-reload-remove.json"); err != nil { 90 | t.Error("Error Remove()ing file from temp dir") 91 | } 92 | if err = l.Delete("test-live-reload-delete"); err != nil { 93 | t.Error("Error Delete()ing file from temp dir") 94 | } 95 | //wait for watcher to pick up the changes 96 | time.Sleep(time.Second) 97 | testNumRules(t, l, 2) 98 | } 99 | 100 | func randString() string { 101 | rand.Seed(time.Now().UnixNano()) 102 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 103 | b := make([]rune, 10) 104 | for i := range b { 105 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 106 | } 107 | return string(b) 108 | } 109 | 110 | func Copy(src, dst string) error { 111 | in, err := os.Open(src) 112 | if err != nil { 113 | return err 114 | } 115 | defer in.Close() 116 | 117 | out, err := os.Create(dst) 118 | if err != nil { 119 | return err 120 | } 121 | defer out.Close() 122 | 123 | _, err = io.Copy(out, in) 124 | if err != nil { 125 | return err 126 | } 127 | return out.Close() 128 | } 129 | 130 | func testNumRules(t *testing.T, l *Loader, num int) { 131 | if l.NumRules() != num { 132 | t.Error("rules number should be (2): ", num) 133 | } 134 | } 135 | 136 | func testRulesOrder(t *testing.T, l *Loader) { 137 | if l.rulesKeys[0] != "000-aaa-name" { 138 | t.Error("Rules not in order (0): ", l.rulesKeys) 139 | } 140 | if l.rulesKeys[1] != "000-allow-chrome" { 141 | t.Error("Rules not in order (1): ", l.rulesKeys) 142 | } 143 | if l.rulesKeys[2] != "001-deny-chrome" { 144 | t.Error("Rules not in order (2): ", l.rulesKeys) 145 | } 146 | } 147 | 148 | func testSortRules(t *testing.T, l *Loader) { 149 | l.rulesKeys[1] = "001-deny-chrome" 150 | l.rulesKeys[2] = "000-allow-chrome" 151 | l.sortRules() 152 | if l.rulesKeys[1] != "000-allow-chrome" { 153 | t.Error("Rules not in order (1): ", l.rulesKeys) 154 | } 155 | if l.rulesKeys[2] != "001-deny-chrome" { 156 | t.Error("Rules not in order (2): ", l.rulesKeys) 157 | } 158 | } 159 | 160 | func testFindMatch(t *testing.T, l *Loader) { 161 | conn.Process.Path = "/opt/google/chrome/chrome" 162 | 163 | testFindPriorityMatch(t, l) 164 | testFindDenyMatch(t, l) 165 | testFindAllowMatch(t, l) 166 | 167 | restoreConnection() 168 | } 169 | 170 | func testFindPriorityMatch(t *testing.T, l *Loader) { 171 | match := l.FindFirstMatch(conn) 172 | if match == nil { 173 | t.Error("FindPriorityMatch didn't match") 174 | } 175 | // test 000-allow-chrome, priority == true 176 | if match.Name != "000-allow-chrome" { 177 | t.Error("findPriorityMatch: priority rule failed: ", match) 178 | } 179 | 180 | } 181 | 182 | func testFindDenyMatch(t *testing.T, l *Loader) { 183 | l.rules["000-allow-chrome"].Precedence = false 184 | // test 000-allow-chrome, priority == false 185 | // 001-deny-chrome must match 186 | match := l.FindFirstMatch(conn) 187 | if match == nil { 188 | t.Error("FindDenyMatch deny didn't match") 189 | } 190 | if match.Name != "001-deny-chrome" { 191 | t.Error("findDenyMatch: deny rule failed: ", match) 192 | } 193 | } 194 | 195 | func testFindAllowMatch(t *testing.T, l *Loader) { 196 | l.rules["000-allow-chrome"].Precedence = false 197 | l.rules["001-deny-chrome"].Action = Allow 198 | // test 000-allow-chrome, priority == false 199 | // 001-deny-chrome must match 200 | match := l.FindFirstMatch(conn) 201 | if match == nil { 202 | t.Error("FindAllowMatch allow didn't match") 203 | } 204 | if match.Name != "001-deny-chrome" { 205 | t.Error("findAllowMatch: allow rule failed: ", match) 206 | } 207 | } 208 | 209 | func testFindEnabled(t *testing.T, l *Loader) { 210 | l.rules["000-allow-chrome"].Precedence = false 211 | l.rules["001-deny-chrome"].Action = Allow 212 | l.rules["001-deny-chrome"].Enabled = false 213 | // test 000-allow-chrome, priority == false 214 | // 001-deny-chrome must match 215 | match := l.FindFirstMatch(conn) 216 | if match == nil { 217 | t.Error("FindEnabledMatch, match nil") 218 | } 219 | if match.Name == "001-deny-chrome" { 220 | t.Error("findEnabkedMatch: deny rule shouldn't have matched: ", match) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /daemon/rule/operator.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/evilsocket/opensnitch/daemon/conman" 10 | "github.com/evilsocket/opensnitch/daemon/core" 11 | "github.com/evilsocket/opensnitch/daemon/log" 12 | ) 13 | 14 | // Type is the type of rule. 15 | // Every type has its own way of checking the user data against connections. 16 | type Type string 17 | 18 | // Sensitive defines if a rule is case-sensitive or not. By default no. 19 | type Sensitive bool 20 | 21 | // Operand is what we check on a connection. 22 | type Operand string 23 | 24 | // Available types 25 | const ( 26 | Simple = Type("simple") 27 | Regexp = Type("regexp") 28 | Complex = Type("complex") // for future use 29 | List = Type("list") 30 | Network = Type("network") 31 | ) 32 | 33 | // Available operands 34 | const ( 35 | OpTrue = Operand("true") 36 | OpProcessID = Operand("process.id") 37 | OpProcessPath = Operand("process.path") 38 | OpProcessCmd = Operand("process.command") 39 | OpProcessEnvPrefix = Operand("process.env.") 40 | OpProcessEnvPrefixLen = 12 41 | OpUserID = Operand("user.id") 42 | OpDstIP = Operand("dest.ip") 43 | OpDstHost = Operand("dest.host") 44 | OpDstPort = Operand("dest.port") 45 | OpDstNetwork = Operand("dest.network") 46 | OpProto = Operand("protocol") 47 | OpList = Operand("list") 48 | ) 49 | 50 | type opCallback func(value interface{}) bool 51 | 52 | // Operator represents what we want to filter of a connection, and how. 53 | type Operator struct { 54 | Type Type `json:"type"` 55 | Operand Operand `json:"operand"` 56 | Sensitive Sensitive `json:"sensitive"` 57 | Data string `json:"data"` 58 | List []Operator `json:"list"` 59 | 60 | cb opCallback 61 | re *regexp.Regexp 62 | netMask *net.IPNet 63 | } 64 | 65 | // NewOperator returns a new operator object 66 | func NewOperator(t Type, s Sensitive, o Operand, data string, list []Operator) (*Operator, error) { 67 | op := Operator{ 68 | Type: t, 69 | Sensitive: s, 70 | Operand: o, 71 | Data: data, 72 | List: list, 73 | } 74 | if err := op.Compile(); err != nil { 75 | log.Error("NewOperator() failed to compile: %s", err) 76 | return nil, err 77 | } 78 | return &op, nil 79 | } 80 | 81 | // Compile translates the operator type field to its callback counterpart 82 | func (o *Operator) Compile() error { 83 | if o.Type == Simple { 84 | o.cb = o.simpleCmp 85 | } else if o.Type == Regexp { 86 | o.cb = o.reCmp 87 | if o.Sensitive == false { 88 | o.Data = strings.ToLower(o.Data) 89 | } 90 | re, err := regexp.Compile(o.Data) 91 | if err != nil { 92 | return err 93 | } 94 | o.re = re 95 | } else if o.Type == List { 96 | o.Operand = OpList 97 | } else if o.Type == Network { 98 | var err error 99 | _, o.netMask, err = net.ParseCIDR(o.Data) 100 | if err != nil { 101 | return err 102 | } 103 | o.cb = o.cmpNetwork 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (o *Operator) String() string { 110 | how := "is" 111 | if o.Type == Regexp { 112 | how = "matches" 113 | } 114 | return fmt.Sprintf("%s %s '%s'", log.Bold(string(o.Operand)), how, log.Yellow(string(o.Data))) 115 | } 116 | 117 | func (o *Operator) simpleCmp(v interface{}) bool { 118 | if o.Sensitive == false { 119 | return strings.EqualFold(v.(string), o.Data) 120 | } 121 | return v == o.Data 122 | } 123 | 124 | func (o *Operator) reCmp(v interface{}) bool { 125 | if o.Sensitive == false { 126 | v = strings.ToLower(v.(string)) 127 | } 128 | return o.re.MatchString(v.(string)) 129 | } 130 | 131 | func (o *Operator) cmpNetwork(destIP interface{}) bool { 132 | // 192.0.2.1/24, 2001:db8:a0b:12f0::1/32 133 | if o.netMask == nil { 134 | log.Warning("cmpNetwork() NULL: %s", destIP) 135 | return false 136 | } 137 | return o.netMask.Contains(destIP.(net.IP)) 138 | } 139 | 140 | func (o *Operator) listMatch(con interface{}) bool { 141 | res := true 142 | for i := 0; i < len(o.List); i += 1 { 143 | o := o.List[i] 144 | if err := o.Compile(); err != nil { 145 | return false 146 | } 147 | res = res && o.Match(con.(*conman.Connection)) 148 | } 149 | return res 150 | } 151 | 152 | // Match tries to match parts of a connection with the given operator. 153 | func (o *Operator) Match(con *conman.Connection) bool { 154 | 155 | if o.Operand == OpTrue { 156 | return true 157 | } else if o.Operand == OpUserID { 158 | return o.cb(fmt.Sprintf("%d", con.Entry.UserId)) 159 | } else if o.Operand == OpProcessID { 160 | return o.cb(fmt.Sprint(con.Process.ID)) 161 | } else if o.Operand == OpProcessPath { 162 | return o.cb(con.Process.Path) 163 | } else if o.Operand == OpProcessCmd { 164 | return o.cb(strings.Join(con.Process.Args, " ")) 165 | } else if strings.HasPrefix(string(o.Operand), string(OpProcessEnvPrefix)) { 166 | envVarName := core.Trim(string(o.Operand[OpProcessEnvPrefixLen:])) 167 | envVarValue, _ := con.Process.Env[envVarName] 168 | return o.cb(envVarValue) 169 | } else if o.Operand == OpDstIP { 170 | return o.cb(con.DstIP.String()) 171 | } else if o.Operand == OpDstHost && con.DstHost != "" { 172 | return o.cb(con.DstHost) 173 | } else if o.Operand == OpProto { 174 | return o.cb(con.Protocol) 175 | } else if o.Operand == OpDstPort { 176 | return o.cb(fmt.Sprintf("%d", con.DstPort)) 177 | } else if o.Operand == OpDstNetwork { 178 | return o.cb(con.DstIP) 179 | } else if o.Operand == OpList { 180 | return o.listMatch(con) 181 | } 182 | 183 | return false 184 | } 185 | -------------------------------------------------------------------------------- /daemon/rule/rule.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/evilsocket/opensnitch/daemon/conman" 8 | "github.com/evilsocket/opensnitch/daemon/log" 9 | "github.com/evilsocket/opensnitch/daemon/ui/protocol" 10 | ) 11 | 12 | // Action of a rule 13 | type Action string 14 | 15 | // Actions of rules 16 | const ( 17 | Allow = Action("allow") 18 | Deny = Action("deny") 19 | ) 20 | 21 | // Duration of a rule 22 | type Duration string 23 | 24 | // daemon possible durations 25 | const ( 26 | Once = Duration("once") 27 | Restart = Duration("until restart") 28 | Always = Duration("always") 29 | ) 30 | 31 | // Rule represents an action on a connection. 32 | // The fields match the ones saved as json to disk. 33 | // If a .json rule file is modified on disk, it's reloaded automatically. 34 | type Rule struct { 35 | Created time.Time `json:"created"` 36 | Updated time.Time `json:"updated"` 37 | Name string `json:"name"` 38 | Enabled bool `json:"enabled"` 39 | Precedence bool `json:"precedence"` 40 | Action Action `json:"action"` 41 | Duration Duration `json:"duration"` 42 | Operator Operator `json:"operator"` 43 | } 44 | 45 | // Create creates a new rule object with the specified parameters. 46 | func Create(name string, enabled bool, precedence bool, action Action, duration Duration, op *Operator) *Rule { 47 | return &Rule{ 48 | Created: time.Now(), 49 | Enabled: enabled, 50 | Precedence: precedence, 51 | Name: name, 52 | Action: action, 53 | Duration: duration, 54 | Operator: *op, 55 | } 56 | } 57 | 58 | func (r *Rule) String() string { 59 | return fmt.Sprintf("%s: if(%s){ %s %s }", r.Name, r.Operator.String(), r.Action, r.Duration) 60 | } 61 | 62 | // Match performs on a connection the checks a Rule has, to determine if it 63 | // must be allowed or denied. 64 | func (r *Rule) Match(con *conman.Connection) bool { 65 | return r.Operator.Match(con) 66 | } 67 | 68 | // Deserialize translates back the rule received to a Rule object 69 | func Deserialize(reply *protocol.Rule) (*Rule, error) { 70 | if reply.Operator == nil { 71 | log.Warning("Deserialize rule, Operator nil") 72 | return nil, fmt.Errorf("invalid operator") 73 | } 74 | operator, err := NewOperator( 75 | Type(reply.Operator.Type), 76 | Sensitive(reply.Operator.Sensitive), 77 | Operand(reply.Operator.Operand), 78 | reply.Operator.Data, 79 | make([]Operator, 0), 80 | ) 81 | if err != nil { 82 | log.Warning("Deserialize rule, NewOperator() error: %s", err) 83 | return nil, err 84 | } 85 | 86 | return Create( 87 | reply.Name, 88 | reply.Enabled, 89 | reply.Precedence, 90 | Action(reply.Action), 91 | Duration(reply.Duration), 92 | operator, 93 | ), nil 94 | } 95 | 96 | // Serialize translates a Rule to the protocol object 97 | func (r *Rule) Serialize() *protocol.Rule { 98 | if r == nil { 99 | return nil 100 | } 101 | return &protocol.Rule{ 102 | Name: string(r.Name), 103 | Enabled: bool(r.Enabled), 104 | Precedence: bool(r.Precedence), 105 | Action: string(r.Action), 106 | Duration: string(r.Duration), 107 | Operator: &protocol.Operator{ 108 | Type: string(r.Operator.Type), 109 | Sensitive: bool(r.Operator.Sensitive), 110 | Operand: string(r.Operator.Operand), 111 | Data: string(r.Operator.Data), 112 | }, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /daemon/rule/rule_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import "testing" 4 | 5 | func TestCreate(t *testing.T) { 6 | t.Log("Test: Create rule") 7 | 8 | var list []Operator 9 | oper, _ := NewOperator(Simple, false, OpTrue, "", list) 10 | r := Create("000-test-name", true, false, Allow, Once, oper) 11 | t.Run("New rule must not be nil", func(t *testing.T) { 12 | if r == nil { 13 | t.Error("Create() returned nil") 14 | t.Fail() 15 | } 16 | }) 17 | t.Run("Rule name must be 000-test-name", func(t *testing.T) { 18 | if r.Name != "000-test-name" { 19 | t.Error("Rule name error:", r.Name) 20 | t.Fail() 21 | } 22 | }) 23 | t.Run("Rule must be enabled", func(t *testing.T) { 24 | if r.Enabled == false { 25 | t.Error("Rule Enabled is false:", r) 26 | t.Fail() 27 | } 28 | }) 29 | t.Run("Rule Precedence must be false", func(t *testing.T) { 30 | if r.Precedence == true { 31 | t.Error("Rule Precedence is true:", r) 32 | t.Fail() 33 | } 34 | }) 35 | t.Run("Rule Action must be Allow", func(t *testing.T) { 36 | if r.Action != Allow { 37 | t.Error("Rule Action is not Allow:", r.Action) 38 | t.Fail() 39 | } 40 | }) 41 | t.Run("Rule Duration should be Once", func(t *testing.T) { 42 | if r.Duration != Once { 43 | t.Error("Rule Duration is not Once:", r.Duration) 44 | t.Fail() 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /daemon/rule/testdata/000-allow-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2020-12-13T18:06:52.209804547+01:00", 3 | "updated": "2020-12-13T18:06:52.209857713+01:00", 4 | "name": "000-allow-chrome", 5 | "enabled": true, 6 | "precedence": true, 7 | "action": "allow", 8 | "duration": "always", 9 | "operator": { 10 | "type": "simple", 11 | "operand": "process.path", 12 | "sensitive": false, 13 | "data": "/opt/google/chrome/chrome", 14 | "list": [] 15 | } 16 | } -------------------------------------------------------------------------------- /daemon/rule/testdata/001-deny-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2020-12-13T17:54:49.067148304+01:00", 3 | "updated": "2020-12-13T17:54:49.067213602+01:00", 4 | "name": "001-deny-chrome", 5 | "enabled": true, 6 | "precedence": false, 7 | "action": "deny", 8 | "duration": "always", 9 | "operator": { 10 | "type": "simple", 11 | "operand": "process.path", 12 | "sensitive": false, 13 | "data": "/opt/google/chrome/chrome", 14 | "list": [] 15 | } 16 | } -------------------------------------------------------------------------------- /daemon/rule/testdata/live_reload/test-live-reload-delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2020-12-13T18:06:52.209804547+01:00", 3 | "updated": "2020-12-13T18:06:52.209857713+01:00", 4 | "name": "test-live-reload-delete", 5 | "enabled": true, 6 | "precedence": true, 7 | "action": "deny", 8 | "duration": "always", 9 | "operator": { 10 | "type": "simple", 11 | "operand": "process.path", 12 | "sensitive": false, 13 | "data": "/usr/bin/curl", 14 | "list": [] 15 | } 16 | } -------------------------------------------------------------------------------- /daemon/rule/testdata/live_reload/test-live-reload-remove.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2020-12-13T18:06:52.209804547+01:00", 3 | "updated": "2020-12-13T18:06:52.209857713+01:00", 4 | "name": "test-live-reload-remove", 5 | "enabled": true, 6 | "precedence": true, 7 | "action": "deny", 8 | "duration": "always", 9 | "operator": { 10 | "type": "simple", 11 | "operand": "process.path", 12 | "sensitive": false, 13 | "data": "/usr/bin/curl", 14 | "list": [] 15 | } 16 | } -------------------------------------------------------------------------------- /daemon/statistics/event.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/evilsocket/opensnitch/daemon/conman" 7 | "github.com/evilsocket/opensnitch/daemon/rule" 8 | "github.com/evilsocket/opensnitch/daemon/ui/protocol" 9 | ) 10 | 11 | type Event struct { 12 | Time time.Time 13 | Connection *conman.Connection 14 | Rule *rule.Rule 15 | } 16 | 17 | func NewEvent(con *conman.Connection, match *rule.Rule) *Event { 18 | return &Event{ 19 | Time: time.Now(), 20 | Connection: con, 21 | Rule: match, 22 | } 23 | } 24 | 25 | func (e *Event) Serialize() *protocol.Event { 26 | return &protocol.Event{ 27 | Time: e.Time.Format("2006-01-02 15:04:05"), 28 | Connection: e.Connection.Serialize(), 29 | Rule: e.Rule.Serialize(), 30 | Unixnano: e.Time.UnixNano(), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /daemon/statistics/stats.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/evilsocket/opensnitch/daemon/conman" 9 | "github.com/evilsocket/opensnitch/daemon/core" 10 | "github.com/evilsocket/opensnitch/daemon/log" 11 | "github.com/evilsocket/opensnitch/daemon/rule" 12 | "github.com/evilsocket/opensnitch/daemon/ui/protocol" 13 | ) 14 | 15 | const ( 16 | // max number of events to keep in the buffer 17 | maxEvents = 100 18 | // max number of entries for each By* map 19 | maxStats = 25 20 | ) 21 | 22 | type conEvent struct { 23 | con *conman.Connection 24 | match *rule.Rule 25 | wasMissed bool 26 | } 27 | 28 | type Statistics struct { 29 | sync.RWMutex 30 | 31 | Started time.Time 32 | DNSResponses int 33 | Connections int 34 | Ignored int 35 | Accepted int 36 | Dropped int 37 | RuleHits int 38 | RuleMisses int 39 | Events []*Event 40 | ByProto map[string]uint64 41 | ByAddress map[string]uint64 42 | ByHost map[string]uint64 43 | ByPort map[string]uint64 44 | ByUID map[string]uint64 45 | ByExecutable map[string]uint64 46 | 47 | rules *rule.Loader 48 | jobs chan conEvent 49 | } 50 | 51 | func New(rules *rule.Loader) (stats *Statistics) { 52 | stats = &Statistics{ 53 | Started: time.Now(), 54 | Events: make([]*Event, 0), 55 | ByProto: make(map[string]uint64), 56 | ByAddress: make(map[string]uint64), 57 | ByHost: make(map[string]uint64), 58 | ByPort: make(map[string]uint64), 59 | ByUID: make(map[string]uint64), 60 | ByExecutable: make(map[string]uint64), 61 | 62 | rules: rules, 63 | jobs: make(chan conEvent), 64 | } 65 | 66 | go stats.eventWorker(0) 67 | go stats.eventWorker(1) 68 | go stats.eventWorker(2) 69 | go stats.eventWorker(3) 70 | 71 | return stats 72 | } 73 | 74 | func (s *Statistics) OnDNSResponse() { 75 | s.Lock() 76 | defer s.Unlock() 77 | s.DNSResponses++ 78 | s.Accepted++ 79 | } 80 | 81 | func (s *Statistics) OnIgnored() { 82 | s.Lock() 83 | defer s.Unlock() 84 | s.Ignored++ 85 | s.Accepted++ 86 | } 87 | 88 | func (s *Statistics) incMap(m *map[string]uint64, key string) { 89 | if val, found := (*m)[key]; found == false { 90 | // do we have enough space left? 91 | nElems := len(*m) 92 | if nElems >= maxStats { 93 | // find the element with less hits 94 | nMin := uint64(9999999999) 95 | minKey := "" 96 | for k, v := range *m { 97 | if v < nMin { 98 | minKey = k 99 | nMin = v 100 | } 101 | } 102 | // remove it 103 | if minKey != "" { 104 | delete(*m, minKey) 105 | } 106 | } 107 | 108 | (*m)[key] = 1 109 | } else { 110 | (*m)[key] = val + 1 111 | } 112 | } 113 | 114 | func (s *Statistics) eventWorker(id int) { 115 | log.Debug("Stats worker #%d started.", id) 116 | 117 | for true { 118 | select { 119 | case job := <-s.jobs: 120 | s.onConnection(job.con, job.match, job.wasMissed) 121 | } 122 | } 123 | } 124 | 125 | func (s *Statistics) onConnection(con *conman.Connection, match *rule.Rule, wasMissed bool) { 126 | s.Lock() 127 | defer s.Unlock() 128 | 129 | s.Connections++ 130 | 131 | if wasMissed { 132 | s.RuleMisses++ 133 | } else { 134 | s.RuleHits++ 135 | } 136 | 137 | if wasMissed == false && match.Action == rule.Allow { 138 | s.Accepted++ 139 | } else { 140 | s.Dropped++ 141 | } 142 | 143 | s.incMap(&s.ByProto, con.Protocol) 144 | s.incMap(&s.ByAddress, con.DstIP.String()) 145 | if con.DstHost != "" { 146 | s.incMap(&s.ByHost, con.DstHost) 147 | } 148 | s.incMap(&s.ByPort, fmt.Sprintf("%d", con.DstPort)) 149 | s.incMap(&s.ByUID, fmt.Sprintf("%d", con.Entry.UserId)) 150 | s.incMap(&s.ByExecutable, con.Process.Path) 151 | 152 | // if we reached the limit, shift everything back 153 | // by one position 154 | nEvents := len(s.Events) 155 | if nEvents == maxEvents { 156 | s.Events = s.Events[1:] 157 | } 158 | if wasMissed { 159 | return 160 | } 161 | s.Events = append(s.Events, NewEvent(con, match)) 162 | } 163 | 164 | func (s *Statistics) OnConnectionEvent(con *conman.Connection, match *rule.Rule, wasMissed bool) { 165 | s.jobs <- conEvent{ 166 | con: con, 167 | match: match, 168 | wasMissed: wasMissed, 169 | } 170 | } 171 | 172 | func (s *Statistics) serializeEvents() []*protocol.Event { 173 | nEvents := len(s.Events) 174 | serialized := make([]*protocol.Event, nEvents) 175 | 176 | for i, e := range s.Events { 177 | serialized[i] = e.Serialize() 178 | } 179 | 180 | return serialized 181 | } 182 | 183 | func (s *Statistics) Serialize() *protocol.Statistics { 184 | s.Lock() 185 | defer s.Unlock() 186 | 187 | return &protocol.Statistics{ 188 | DaemonVersion: core.Version, 189 | Rules: uint64(s.rules.NumRules()), 190 | Uptime: uint64(time.Since(s.Started).Seconds()), 191 | DnsResponses: uint64(s.DNSResponses), 192 | Connections: uint64(s.Connections), 193 | Ignored: uint64(s.Ignored), 194 | Accepted: uint64(s.Accepted), 195 | Dropped: uint64(s.Dropped), 196 | RuleHits: uint64(s.RuleHits), 197 | RuleMisses: uint64(s.RuleMisses), 198 | Events: s.serializeEvents(), 199 | ByProto: s.ByProto, 200 | ByAddress: s.ByAddress, 201 | ByHost: s.ByHost, 202 | ByPort: s.ByPort, 203 | ByUid: s.ByUID, 204 | ByExecutable: s.ByExecutable, 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /daemon/system-fw.json: -------------------------------------------------------------------------------- 1 | { 2 | "SystemRules": [ 3 | { 4 | "Rule": { 5 | "Description": "Allow icmp", 6 | "Table": "mangle", 7 | "Chain": "OUTPUT", 8 | "Parameters": "-p icmp", 9 | "Target": "ACCEPT", 10 | "TargetParameters": "" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /daemon/ui/client.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/evilsocket/opensnitch/daemon/conman" 10 | "github.com/evilsocket/opensnitch/daemon/log" 11 | "github.com/evilsocket/opensnitch/daemon/rule" 12 | "github.com/evilsocket/opensnitch/daemon/statistics" 13 | "github.com/evilsocket/opensnitch/daemon/ui/protocol" 14 | 15 | "github.com/fsnotify/fsnotify" 16 | "golang.org/x/net/context" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/connectivity" 19 | ) 20 | 21 | var ( 22 | configFile = "/etc/opensnitchd/default-config.json" 23 | dummyOperator, _ = rule.NewOperator(rule.Simple, false, rule.OpTrue, "", make([]rule.Operator, 0)) 24 | clientDisconnectedRule = rule.Create("ui.client.disconnected", true, false, rule.Allow, rule.Once, dummyOperator) 25 | clientErrorRule = rule.Create("ui.client.error", true, false, rule.Allow, rule.Once, dummyOperator) 26 | config Config 27 | ) 28 | 29 | type serverConfig struct { 30 | Address string `json:"Address"` 31 | LogFile string `json:"LogFile"` 32 | } 33 | 34 | // Config holds the values loaded from configFile 35 | type Config struct { 36 | sync.RWMutex 37 | Server serverConfig `json:"Server"` 38 | DefaultAction string `json:"DefaultAction"` 39 | DefaultDuration string `json:"DefaultDuration"` 40 | InterceptUnknown bool `json:"InterceptUnknown"` 41 | ProcMonitorMethod string `json:"ProcMonitorMethod"` 42 | LogLevel *uint32 `json:"LogLevel"` 43 | } 44 | 45 | // Client holds the connection information of a client. 46 | type Client struct { 47 | sync.RWMutex 48 | clientCtx context.Context 49 | clientCancel context.CancelFunc 50 | 51 | stats *statistics.Statistics 52 | rules *rule.Loader 53 | socketPath string 54 | isUnixSocket bool 55 | con *grpc.ClientConn 56 | client protocol.UIClient 57 | configWatcher *fsnotify.Watcher 58 | streamNotifications protocol.UI_NotificationsClient 59 | } 60 | 61 | // NewClient creates and configures a new client. 62 | func NewClient(socketPath string, stats *statistics.Statistics, rules *rule.Loader) *Client { 63 | c := &Client{ 64 | stats: stats, 65 | rules: rules, 66 | isUnixSocket: false, 67 | } 68 | c.clientCtx, c.clientCancel = context.WithCancel(context.Background()) 69 | 70 | if watcher, err := fsnotify.NewWatcher(); err == nil { 71 | c.configWatcher = watcher 72 | } 73 | c.loadDiskConfiguration(false) 74 | if socketPath != "" { 75 | c.setSocketPath(c.getSocketPath(socketPath)) 76 | } 77 | 78 | go c.poller() 79 | return c 80 | } 81 | 82 | // Close cancels the running tasks: pinging the server and (re)connection poller. 83 | func (c *Client) Close() { 84 | c.clientCancel() 85 | } 86 | 87 | // ProcMonitorMethod returns the monitor method configured. 88 | // If it's not present in the config file, it'll return an empty string. 89 | func (c *Client) ProcMonitorMethod() string { 90 | config.RLock() 91 | defer config.RUnlock() 92 | return config.ProcMonitorMethod 93 | } 94 | 95 | // InterceptUnknown returns 96 | func (c *Client) InterceptUnknown() bool { 97 | config.RLock() 98 | defer config.RUnlock() 99 | return config.InterceptUnknown 100 | } 101 | 102 | // DefaultAction returns the default configured action for 103 | func (c *Client) DefaultAction() rule.Action { 104 | c.RLock() 105 | defer c.RUnlock() 106 | return clientDisconnectedRule.Action 107 | } 108 | 109 | // DefaultDuration returns the default duration configured for a rule. 110 | // For example it can be: once, always, "until restart". 111 | func (c *Client) DefaultDuration() rule.Duration { 112 | c.RLock() 113 | defer c.RUnlock() 114 | return clientDisconnectedRule.Duration 115 | } 116 | 117 | // Connected checks if the client has established a connection with the server. 118 | func (c *Client) Connected() bool { 119 | c.Lock() 120 | defer c.Unlock() 121 | if c.con == nil || c.con.GetState() != connectivity.Ready { 122 | return false 123 | } 124 | return true 125 | } 126 | 127 | func (c *Client) poller() { 128 | log.Debug("UI service poller started for socket %s", c.socketPath) 129 | wasConnected := false 130 | for { 131 | select { 132 | case <-c.clientCtx.Done(): 133 | log.Info("Client.poller() exit, Done()") 134 | goto Exit 135 | default: 136 | isConnected := c.Connected() 137 | if wasConnected != isConnected { 138 | c.onStatusChange(isConnected) 139 | wasConnected = isConnected 140 | } 141 | 142 | if c.Connected() == false { 143 | // connect and create the client if needed 144 | if err := c.connect(); err != nil { 145 | log.Warning("Error while connecting to UI service: %s", err) 146 | } 147 | } 148 | if c.Connected() == true { 149 | // if the client is connected and ready, send a ping 150 | if err := c.ping(time.Now()); err != nil { 151 | log.Warning("Error while pinging UI service: %s", err) 152 | } 153 | } 154 | 155 | time.Sleep(1 * time.Second) 156 | } 157 | } 158 | Exit: 159 | log.Info("uiClient exit") 160 | } 161 | 162 | func (c *Client) onStatusChange(connected bool) { 163 | if connected { 164 | log.Info("Connected to the UI service on %s", c.socketPath) 165 | go c.Subscribe() 166 | } else { 167 | log.Error("Connection to the UI service lost.") 168 | c.disconnect() 169 | } 170 | } 171 | 172 | func (c *Client) connect() (err error) { 173 | if c.Connected() { 174 | return 175 | } 176 | 177 | if c.con != nil { 178 | if c.con.GetState() == connectivity.TransientFailure || c.con.GetState() == connectivity.Shutdown { 179 | c.disconnect() 180 | } else { 181 | return 182 | } 183 | } 184 | 185 | if err := c.openSocket(); err != nil { 186 | c.disconnect() 187 | return err 188 | } 189 | 190 | if c.client == nil { 191 | c.client = protocol.NewUIClient(c.con) 192 | } 193 | return nil 194 | } 195 | 196 | func (c *Client) openSocket() (err error) { 197 | c.Lock() 198 | defer c.Unlock() 199 | 200 | if c.isUnixSocket { 201 | c.con, err = grpc.Dial(c.socketPath, grpc.WithInsecure(), 202 | grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { 203 | return net.DialTimeout("unix", addr, timeout) 204 | })) 205 | } else { 206 | c.con, err = grpc.Dial(c.socketPath, grpc.WithInsecure()) 207 | } 208 | 209 | return err 210 | } 211 | 212 | func (c *Client) disconnect() { 213 | c.Lock() 214 | defer c.Unlock() 215 | 216 | c.client = nil 217 | if c.con != nil { 218 | c.con.Close() 219 | c.con = nil 220 | log.Debug("client.disconnect()") 221 | } 222 | } 223 | 224 | func (c *Client) ping(ts time.Time) (err error) { 225 | if c.Connected() == false { 226 | return fmt.Errorf("service is not connected") 227 | } 228 | 229 | c.Lock() 230 | defer c.Unlock() 231 | 232 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 233 | defer cancel() 234 | reqID := uint64(ts.UnixNano()) 235 | 236 | pReq := &protocol.PingRequest{ 237 | Id: reqID, 238 | Stats: c.stats.Serialize(), 239 | } 240 | c.stats.RLock() 241 | pong, err := c.client.Ping(ctx, pReq) 242 | c.stats.RUnlock() 243 | if err != nil { 244 | return err 245 | } 246 | 247 | if pong.Id != reqID { 248 | return fmt.Errorf("Expected pong with id 0x%x, got 0x%x", reqID, pong.Id) 249 | } 250 | 251 | return nil 252 | } 253 | 254 | // Ask sends a request to the server, with the values of a connection to be 255 | // allowed or denied. 256 | func (c *Client) Ask(con *conman.Connection) (*rule.Rule, bool) { 257 | if c.Connected() == false { 258 | return clientDisconnectedRule, false 259 | } 260 | 261 | c.Lock() 262 | defer c.Unlock() 263 | 264 | // FIXME: if timeout is fired, the rule is not added to the list in the GUI 265 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*120) 266 | defer cancel() 267 | reply, err := c.client.AskRule(ctx, con.Serialize()) 268 | if err != nil { 269 | log.Warning("Error while asking for rule: %s - %v", err, con) 270 | return nil, false 271 | } 272 | 273 | r, err := rule.Deserialize(reply) 274 | if err != nil { 275 | return nil, false 276 | } 277 | return r, true 278 | } 279 | 280 | func (c *Client) monitorConfigWorker() { 281 | for { 282 | select { 283 | case event := <-c.configWatcher.Events: 284 | if (event.Op&fsnotify.Write == fsnotify.Write) || (event.Op&fsnotify.Remove == fsnotify.Remove) { 285 | c.loadDiskConfiguration(true) 286 | } 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /daemon/ui/config.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "strings" 8 | 9 | "github.com/evilsocket/opensnitch/daemon/log" 10 | "github.com/evilsocket/opensnitch/daemon/procmon" 11 | "github.com/evilsocket/opensnitch/daemon/rule" 12 | ) 13 | 14 | func (c *Client) getSocketPath(socketPath string) string { 15 | c.Lock() 16 | defer c.Unlock() 17 | 18 | if strings.HasPrefix(socketPath, "unix://") == true { 19 | c.isUnixSocket = true 20 | return socketPath[7:] 21 | } 22 | 23 | c.isUnixSocket = false 24 | return socketPath 25 | } 26 | 27 | func (c *Client) setSocketPath(socketPath string) { 28 | c.Lock() 29 | defer c.Unlock() 30 | 31 | c.socketPath = socketPath 32 | } 33 | 34 | func (c *Client) isProcMonitorEqual(newMonitorMethod string) bool { 35 | config.RLock() 36 | defer config.RUnlock() 37 | 38 | return newMonitorMethod == config.ProcMonitorMethod 39 | } 40 | 41 | func (c *Client) parseConf(rawConfig string) (conf Config, err error) { 42 | err = json.Unmarshal([]byte(rawConfig), &conf) 43 | return conf, err 44 | } 45 | 46 | func (c *Client) loadDiskConfiguration(reload bool) { 47 | raw, err := ioutil.ReadFile(configFile) 48 | if err != nil { 49 | fmt.Errorf("Error loading disk configuration %s: %s", configFile, err) 50 | } 51 | 52 | if ok := c.loadConfiguration(raw); ok { 53 | if err := c.configWatcher.Add(configFile); err != nil { 54 | log.Error("Could not watch path: %s", err) 55 | return 56 | } 57 | } 58 | 59 | if reload { 60 | return 61 | } 62 | 63 | go c.monitorConfigWorker() 64 | } 65 | 66 | func (c *Client) loadConfiguration(rawConfig []byte) bool { 67 | config.Lock() 68 | defer config.Unlock() 69 | 70 | if err := json.Unmarshal(rawConfig, &config); err != nil { 71 | log.Error("Error parsing configuration %s: %s", configFile, err) 72 | return false 73 | } 74 | // firstly load config level, to detect further errors if any 75 | if config.LogLevel != nil { 76 | log.SetLogLevel(int(*config.LogLevel)) 77 | } 78 | if config.Server.LogFile != "" { 79 | log.Close() 80 | log.OpenFile(config.Server.LogFile) 81 | } 82 | 83 | if config.Server.Address != "" { 84 | tempSocketPath := c.getSocketPath(config.Server.Address) 85 | if tempSocketPath != c.socketPath { 86 | // disconnect, and let the connection poller reconnect to the new address 87 | c.disconnect() 88 | } 89 | c.setSocketPath(tempSocketPath) 90 | } 91 | if config.DefaultAction != "" { 92 | clientDisconnectedRule.Action = rule.Action(config.DefaultAction) 93 | clientErrorRule.Action = rule.Action(config.DefaultAction) 94 | } 95 | if config.DefaultDuration != "" { 96 | clientDisconnectedRule.Duration = rule.Duration(config.DefaultDuration) 97 | clientErrorRule.Duration = rule.Duration(config.DefaultDuration) 98 | } 99 | if config.ProcMonitorMethod != "" { 100 | procmon.SetMonitorMethod(config.ProcMonitorMethod) 101 | } 102 | 103 | return true 104 | } 105 | 106 | func (c *Client) saveConfiguration(rawConfig string) (err error) { 107 | if c.loadConfiguration([]byte(rawConfig)) != true { 108 | return fmt.Errorf("Error parsing configuration %s: %s", rawConfig, err) 109 | } 110 | 111 | if err = ioutil.WriteFile(configFile, []byte(rawConfig), 0644); err != nil { 112 | log.Error("writing configuration to disk: ", err) 113 | return err 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /daemon/ui/protocol/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/daemon/ui/protocol/.gitkeep -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | opensnitch (1.3.0-1) unstable; urgency=medium 2 | 3 | * Fixed how we check rules 4 | * Fixed cpu spike after disable interception. 5 | * Fixed cleaning up fw rules on exit. 6 | * make regexp rules case-insensitive by default 7 | * allow to filter by dst network. 8 | 9 | -- gustavo-iniguez-goya Wed, 16 Dec 2020 01:15:03 +0100 10 | 11 | opensnitch (1.3.0~rc-1) unstable; urgency=medium 12 | 13 | * Non-maintainer upload. 14 | 15 | -- gustavo-iniguez-goya Fri, 13 Nov 2020 00:51:34 +0100 16 | 17 | opensnitch (1.2.0-1) unstable; urgency=medium 18 | 19 | * Fixed memleaks. 20 | * Sort rules by name 21 | * Added priority field to rules. 22 | * Other fixes 23 | 24 | -- gustavo-iniguez-goya Mon, 09 Nov 2020 22:55:13 +0100 25 | 26 | opensnitch (1.0.1-1) unstable; urgency=medium 27 | 28 | * Fixed app exit when IPv6 is not supported. 29 | * Other fixes. 30 | 31 | -- gustavo-iniguez-goya Thu, 30 Jul 2020 21:56:20 +0200 32 | 33 | opensnitch (1.0.0-1) unstable; urgency=medium 34 | 35 | * v1.0.0 released. 36 | 37 | -- gustavo-iniguez-goya Thu, 16 Jul 2020 00:19:26 +0200 38 | 39 | opensnitch (1.0.0rc11-1) unstable; urgency=medium 40 | 41 | * Fixed multiple race conditions. 42 | * Fixed CWD parsing when using audit proc monitor method. 43 | 44 | -- gustavo-iniguez-goya Wed, 24 Jun 2020 00:10:38 +0200 45 | 46 | opensnitch (1.0.0rc10-1) unstable; urgency=medium 47 | 48 | * Fixed checking UID functions availability. 49 | * Improved process path parsing. 50 | * Fixed applying config from the UI. 51 | * Fixed default log level. 52 | * Gather CWD and process environment vars. 53 | * Increase default timeout when asking for a rule. 54 | 55 | -- gustavo-iniguez-goya Sat, 13 Jun 2020 18:45:02 +0200 56 | 57 | opensnitch (1.0.0rc9-1) unstable; urgency=medium 58 | 59 | * Ignore malformed rules from loading. 60 | * Allow to modify and add rules from the UI. 61 | 62 | -- gustavo-iniguez-goya Sun, 17 May 2020 18:18:24 +0200 63 | 64 | opensnitch (1.0.0rc8) unstable; urgency=medium 65 | 66 | * Allow to change settings from the UI. 67 | * Improved connection handling with the UI. 68 | 69 | -- gustavo-iniguez-goya Wed, 29 Apr 2020 21:52:27 +0200 70 | 71 | opensnitch (1.0.0rc7-1) unstable; urgency=medium 72 | 73 | * Stability, performance and realiability improvements. 74 | 75 | -- gustavo-iniguez-goya Sun, 12 Apr 2020 23:25:41 +0200 76 | 77 | opensnitch (1.0.0rc6-1) unstable; urgency=medium 78 | 79 | * Fixed iptables rules deletion. 80 | * Improved PIDs cache. 81 | * Added audit process monitoring method. 82 | * Added logrotate file. 83 | * Added default configuration file. 84 | 85 | -- gustavo-iniguez-goya Sun, 08 Mar 2020 20:47:58 +0100 86 | 87 | opensnitch (1.0.0rc-5) unstable; urgency=medium 88 | 89 | * Fixed netlink socket querying. 90 | * Added check to reload firewall rules if missing. 91 | 92 | -- gustavo-iniguez-goya Mon, 24 Feb 2020 19:55:06 +0100 93 | 94 | opensnitch (1.0.0rc-3) unstable; urgency=medium 95 | 96 | * @see: https://github.com/gustavo-iniguez-goya/opensnitch/releases 97 | 98 | -- gustavo-iniguez-goya Tue, 18 Feb 2020 10:09:45 +0100 99 | 100 | opensnitch (1.0.0rc-2) unstable; urgency=medium 101 | 102 | * UI minor changes 103 | * Expand deb package compatibility. 104 | 105 | -- gustavo-iniguez-goya Wed, 05 Feb 2020 21:50:20 +0100 106 | 107 | opensnitch (1.0.0rc-1) unstable; urgency=medium 108 | 109 | * Initial release 110 | 111 | -- gustavo-iniguez-goya Fri, 22 Nov 2019 01:14:08 +0100 112 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: opensnitch 2 | Maintainer: Debian Go Packaging Team 3 | Uploaders: 4 | Gustavo Iniguez Goya , 5 | Section: devel 6 | Testsuite: autopkgtest-pkg-go 7 | Priority: optional 8 | Build-Depends: 9 | debhelper-compat (= 12), 10 | debhelper (>= 9), 11 | dh-systemd (>= 1.5), 12 | dh-golang, 13 | golang-any, 14 | golang-github-vishvananda-netlink-dev, 15 | golang-github-evilsocket-ftrace-dev, 16 | golang-github-google-gopacket-dev, 17 | golang-github-fsnotify-fsnotify-dev, 18 | golang-golang-x-net-dev, 19 | golang-google-grpc-dev, 20 | golang-goprotobuf-dev, 21 | pkg-config, 22 | libnetfilter-queue-dev, 23 | libmnl-dev 24 | Standards-Version: 4.4.0 25 | Vcs-Browser: https://salsa.debian.org/go-team/packages/opensnitch 26 | Vcs-Git: https://salsa.debian.org/go-team/packages/opensnitch.git 27 | Homepage: https://github.com/evilsocket/opensnitch 28 | Rules-Requires-Root: no 29 | XS-Go-Import-Path: github.com/evilsocket/opensnitch 30 | 31 | Package: opensnitch 32 | Section: net 33 | Architecture: any 34 | Depends: 35 | ${misc:Depends}, ${shlibs:Depends}, 36 | Built-Using: ${misc:Built-Using} 37 | Description: GNU/Linux firewall application 38 | OpenSnitch is a GNU/Linux firewall application. 39 | Whenever a program makes a connection, it'll prompt the user to allow or deny 40 | it. 41 | . 42 | The user can decide if block the outgoing connection based on properties of 43 | the connection: by port, by uid, by dst ip, by program or a combination 44 | of them. 45 | . 46 | These rules can last forever, until the app restart or just one time. 47 | . 48 | The GUI allows the user to view live outgoing connections, as well as search 49 | by process, user, host or port. 50 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://github.com/evilsocket/opensnitch 3 | Upstream-Name: opensnitch 4 | Files-Excluded: 5 | Godeps/_workspace 6 | 7 | Files: * 8 | Copyright: 9 | 2017-2018 evilsocket 10 | 2019-2020 Gustavo Iñiguez Goia 11 | Comment: Debian packaging is licensed under the same terms as upstream 12 | License: GPL-3.0 13 | This program is free software; you can redistribute it 14 | and/or modify it under the terms of the GNU General Public 15 | License as published by the Free Software Foundation; either 16 | version 3 of the License, or (at your option) any later 17 | version. 18 | . 19 | This program is distributed in the hope that it will be 20 | useful, but WITHOUT ANY WARRANTY; without even the implied 21 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 22 | PURPOSE. See the GNU General Public License for more 23 | details. 24 | . 25 | You should have received a copy of the GNU General Public 26 | License along with this program. If not, If not, see 27 | http://www.gnu.org/licenses/. 28 | . 29 | On Debian systems, the full text of the GNU General Public 30 | License version 3 can be found in the file 31 | '/usr/share/common-licenses/GPL-3'. 32 | -------------------------------------------------------------------------------- /debian/gbp.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | pristine-tar = True 3 | -------------------------------------------------------------------------------- /debian/gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # auto-generated, DO NOT MODIFY. 2 | # The authoritative copy of this file lives at: 3 | # https://salsa.debian.org/go-team/ci/blob/master/config/gitlabciyml.go 4 | 5 | # TODO: publish under debian-go-team/ci 6 | image: stapelberg/ci2 7 | 8 | test_the_archive: 9 | artifacts: 10 | paths: 11 | - before-applying-commit.json 12 | - after-applying-commit.json 13 | script: 14 | # Create an overlay to discard writes to /srv/gopath/src after the build: 15 | - "rm -rf /cache/overlay/{upper,work}" 16 | - "mkdir -p /cache/overlay/{upper,work}" 17 | - "mount -t overlay overlay -o lowerdir=/srv/gopath/src,upperdir=/cache/overlay/upper,workdir=/cache/overlay/work /srv/gopath/src" 18 | - "export GOPATH=/srv/gopath" 19 | - "export GOCACHE=/cache/go" 20 | # Build the world as-is: 21 | - "ci-build -exemptions=/var/lib/ci-build/exemptions.json > before-applying-commit.json" 22 | # Copy this package into the overlay: 23 | - "GBP_CONF_FILES=:debian/gbp.conf gbp buildpackage --git-no-pristine-tar --git-ignore-branch --git-ignore-new --git-export-dir=/tmp/export --git-no-overlay --git-tarball-dir=/nonexistant --git-cleaner=/bin/true --git-builder='dpkg-buildpackage -S -d --no-sign'" 24 | - "pgt-gopath -dsc /tmp/export/*.dsc" 25 | # Rebuild the world: 26 | - "ci-build -exemptions=/var/lib/ci-build/exemptions.json > after-applying-commit.json" 27 | - "ci-diff before-applying-commit.json after-applying-commit.json" 28 | -------------------------------------------------------------------------------- /debian/opensnitch.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: opensnitchd 5 | # Required-Start: $network $local_fs 6 | # Required-Stop: $network $local_fs 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: opensnitchd daemon 10 | # Description: opensnitch application firewall 11 | ### END INIT INFO 12 | 13 | NAME=opensnitchd 14 | PIDDIR=/var/run/$NAME 15 | OPENSNITCHDPID=$PIDDIR/$NAME.pid 16 | 17 | # clear conflicting settings from the environment 18 | unset TMPDIR 19 | 20 | test -x /usr/bin/$NAME || exit 0 21 | 22 | . /lib/lsb/init-functions 23 | 24 | case $1 in 25 | start) 26 | log_daemon_msg "Starting opensnitch daemon" $NAME 27 | if [ ! -d /etc/$NAME/rules ]; then 28 | mkdir -p /etc/$NAME/rules &>/dev/null 29 | fi 30 | 31 | # Make sure we have our PIDDIR, even if it's on a tmpfs 32 | install -o root -g root -m 755 -d $PIDDIR 33 | 34 | if ! start-stop-daemon --start --quiet --oknodo --pidfile $OPENSNITCHDPID --background --exec /usr/bin/$NAME -- -rules-path /etc/$NAME/rules; then 35 | log_end_msg 1 36 | exit 1 37 | fi 38 | 39 | log_end_msg 0 40 | ;; 41 | stop) 42 | 43 | log_daemon_msg "Stopping $NAME daemon" $NAME 44 | 45 | start-stop-daemon --stop --quiet --signal QUIT --name $NAME 46 | # Wait a little and remove stale PID file 47 | sleep 1 48 | if [ -f $OPENSNITCHDPID ] && ! ps h `cat $OPENSNITCHDPID` > /dev/null 49 | then 50 | rm -f $OPENSNITCHDPID 51 | fi 52 | 53 | log_end_msg 0 54 | 55 | ;; 56 | reload) 57 | log_daemon_msg "Reloading $NAME" $NAME 58 | 59 | start-stop-daemon --stop --quiet --signal HUP --pidfile $OPENSNITCHDPID 60 | 61 | log_end_msg 0 62 | ;; 63 | restart|force-reload) 64 | $0 stop 65 | sleep 1 66 | $0 start 67 | ;; 68 | status) 69 | status_of_proc /usr/bin/$NAME $NAME 70 | exit $? 71 | ;; 72 | *) 73 | echo "Usage: /etc/init.d/opensnitchd {start|stop|reload|restart|force-reload|status}" 74 | exit 1 75 | ;; 76 | esac 77 | 78 | exit 0 79 | -------------------------------------------------------------------------------- /debian/opensnitch.install: -------------------------------------------------------------------------------- 1 | daemon/default-config.json etc/opensnitchd/ 2 | daemon/system-fw.json etc/opensnitchd/ 3 | -------------------------------------------------------------------------------- /debian/opensnitch.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/opensnitchd.log { 2 | rotate 7 3 | # order of the fields is important 4 | maxsize 50M 5 | # we need this option in order to keep logging 6 | copytruncate 7 | missingok 8 | notifempty 9 | delaycompress 10 | compress 11 | create 640 root root 12 | weekly 13 | } 14 | -------------------------------------------------------------------------------- /debian/opensnitch.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenSnitch is a GNU/Linux application firewall. 3 | Documentation=https://github.com/gustavo-iniguez-goya/opensnitch/wiki 4 | Wants=network.target 5 | After=network.target 6 | 7 | [Service] 8 | Type=simple 9 | PermissionsStartOnly=true 10 | ExecStartPre=/bin/mkdir -p /etc/opensnitchd/rules 11 | ExecStart=/usr/bin/opensnitchd -rules-path /etc/opensnitchd/rules 12 | Restart=always 13 | RestartSec=30 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # FIXME: remove in favor of dh_installsystemd 6 | systemctl unmask opensnitch.service 7 | systemctl enable opensnitch.service 8 | service opensnitch restart 9 | -------------------------------------------------------------------------------- /debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | service opensnitch stop || true 6 | systemctl disable opensnitch.service || true 7 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | export DH_VERBOSE = 1 3 | export DESTDIR = "debian/opensnitch" 4 | 5 | override_dh_dwz: 6 | dwz -- $(DESTDIR)/usr/bin/daemon || true 7 | mv $(DESTDIR)/usr/bin/daemon $(DESTDIR)/usr/bin/opensnitchd 8 | 9 | override_dh_installsystemd: 10 | dh_installsystemd --restart-after-upgrade 11 | 12 | %: 13 | dh $@ --builddirectory=_build --buildsystem=golang --with=golang 14 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | version=4 2 | opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/opensnitch-\$1\.tar\.gz/,\ 3 | uversionmangle=s/(\d)[_\.\-\+]?(RC|rc|pre|dev|beta|alpha)[.]?(\d*)$/\$1~\$2\$3/ \ 4 | https://github.com/evilsocket/opensnitch/tags .*/v?(\d\S*)\.tar\.gz 5 | -------------------------------------------------------------------------------- /make_ads_rules.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | import ipaddress 4 | import datetime 5 | import os 6 | 7 | lists = ( \ 8 | "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", 9 | "https://mirror1.malwaredomains.com/files/justdomains", 10 | "http://sysctl.org/cameleon/hosts", 11 | "https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist", 12 | "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt", 13 | "https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt", 14 | "https://hosts-file.net/ad_servers.txt" ) 15 | 16 | domains = {} 17 | 18 | for url in lists: 19 | print "Downloading %s ..." % url 20 | r = requests.get(url) 21 | if r.status_code != 200: 22 | print "Error, status code %d" % r.status_code 23 | continue 24 | 25 | for line in r.text.split("\n"): 26 | line = line.strip() 27 | if line == "": 28 | continue 29 | 30 | elif line[0] == "#": 31 | continue 32 | 33 | for part in re.split(r'\s+', line): 34 | part = part.strip() 35 | if part == "": 36 | continue 37 | 38 | try: 39 | duh = ipaddress.ip_address(part) 40 | except ValueError: 41 | if part != "localhost": 42 | domains[part] = 1 43 | 44 | print "Got %d unique domains, saving as rules to ./rules/ ..." % len(domains) 45 | 46 | os.system("mkdir -p rules") 47 | 48 | idx = 0 49 | for domain, _ in domains.iteritems(): 50 | with open("rules/adv-%d.json" % idx, "wt") as fp: 51 | tpl = """ 52 | { 53 | "created": "%s", 54 | "updated": "%s", 55 | "name": "deny-adv-%d", 56 | "enabled": true, 57 | "action": "deny", 58 | "duration": "always", 59 | "operator": { 60 | "type": "simple", 61 | "operand": "dest.host", 62 | "data": "%s" 63 | } 64 | }""" 65 | now = datetime.datetime.utcnow().isoformat("T") + "Z" 66 | data = tpl % ( now, now, idx, domain ) 67 | fp.write(data) 68 | 69 | idx = idx + 1 70 | -------------------------------------------------------------------------------- /proto/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /proto/Makefile: -------------------------------------------------------------------------------- 1 | all: ../daemon/ui/protocol/ui.pb.go ../ui/opensnitch/ui_pb2.py 2 | 3 | ../daemon/ui/protocol/ui.pb.go: ui.proto 4 | protoc -I. ui.proto --go_out=plugins=grpc:../daemon/ui/protocol/ 5 | 6 | ../ui/opensnitch/ui_pb2.py: ui.proto 7 | python3 -m grpc_tools.protoc -I. --python_out=../ui/opensnitch/ --grpc_python_out=../ui/opensnitch/ ui.proto 8 | 9 | clean: 10 | @rm -rf ../daemon/ui/protocol/ui.pb.go 11 | @rm -rf ../ui/opensnitch/ui_pb2.py 12 | @rm -rf ../ui/opensnitch/ui_pb2_grpc.py 13 | -------------------------------------------------------------------------------- /proto/ui.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | 5 | service UI { 6 | rpc Ping(PingRequest) returns (PingReply) {} 7 | rpc AskRule (Connection) returns (Rule) {} 8 | rpc Subscribe (ClientConfig) returns (ClientConfig) {} 9 | rpc Notifications (stream NotificationReply) returns (stream Notification) {} 10 | } 11 | 12 | message Event { 13 | string time = 1; 14 | Connection connection = 2; 15 | Rule rule = 3; 16 | int64 unixnano = 4; 17 | } 18 | 19 | message Statistics { 20 | string daemon_version = 1; 21 | uint64 rules = 2; 22 | uint64 uptime = 3; 23 | uint64 dns_responses = 4; 24 | uint64 connections = 5; 25 | uint64 ignored = 6; 26 | uint64 accepted = 7; 27 | uint64 dropped = 8; 28 | uint64 rule_hits = 9; 29 | uint64 rule_misses = 10; 30 | map by_proto = 11; 31 | map by_address = 12; 32 | map by_host = 13; 33 | map by_port = 14; 34 | map by_uid = 15; 35 | map by_executable = 16; 36 | repeated Event events = 17; 37 | } 38 | 39 | message PingRequest { 40 | uint64 id = 1; 41 | Statistics stats = 2; 42 | } 43 | 44 | message PingReply { 45 | uint64 id = 1; 46 | } 47 | 48 | message Connection { 49 | string protocol = 1; 50 | string src_ip = 2; 51 | uint32 src_port = 3; 52 | string dst_ip = 4; 53 | string dst_host = 5; 54 | uint32 dst_port = 6; 55 | uint32 user_id = 7; 56 | uint32 process_id = 8; 57 | string process_path = 9; 58 | string process_cwd = 10; 59 | repeated string process_args = 11; 60 | map process_env = 12; 61 | } 62 | 63 | message Operator { 64 | string type = 1; 65 | string operand = 2; 66 | string data = 3; 67 | bool sensitive = 4; 68 | } 69 | 70 | message Rule { 71 | string name = 1; 72 | bool enabled = 2; 73 | bool precedence = 3; 74 | string action = 4; 75 | string duration = 5; 76 | Operator operator = 6; 77 | } 78 | 79 | enum Action { 80 | NONE = 0; 81 | LOAD_FIREWALL = 1; 82 | UNLOAD_FIREWALL = 2; 83 | CHANGE_CONFIG = 3; 84 | ENABLE_RULE = 4; 85 | DISABLE_RULE = 5; 86 | DELETE_RULE = 6; 87 | CHANGE_RULE = 7; 88 | LOG_LEVEL = 8; 89 | STOP = 9; 90 | MONITOR_PROCESS = 10; 91 | STOP_MONITOR_PROCESS = 11; 92 | } 93 | 94 | // client configuration sent on Subscribe() 95 | message ClientConfig { 96 | uint64 id = 1; 97 | string name = 2; 98 | string version = 3; 99 | bool isFirewallRunning = 4; 100 | // daemon configuration as json string 101 | string config = 5; 102 | uint32 logLevel = 6; 103 | repeated Rule rules = 7; 104 | } 105 | 106 | // notification sent to the clients (daemons) 107 | message Notification { 108 | uint64 id = 1; 109 | string clientName = 2; 110 | string serverName = 3; 111 | // CHANGE_CONFIG: 2, data: {"default_timeout": 1, ...} 112 | Action type = 4; 113 | string data = 5; 114 | repeated Rule rules = 6; 115 | } 116 | 117 | // notification reply sent to the server (GUI) 118 | message NotificationReply { 119 | uint64 id = 1; 120 | NotificationReplyCode code = 2; 121 | string data = 3; 122 | } 123 | 124 | enum NotificationReplyCode { 125 | OK = 0; 126 | ERROR = 1; 127 | } 128 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # nothing to see here, just a utility i use to create new releases ^_^ 3 | 4 | CURRENT_VERSION=$(cat daemon/core/version.go | grep Version | cut -d '"' -f 2) 5 | TO_UPDATE=( 6 | daemon/core/version.go 7 | ui/version.py 8 | ) 9 | 10 | echo -n "Current version is $CURRENT_VERSION, select new version: " 11 | read NEW_VERSION 12 | echo "Creating version $NEW_VERSION ...\n" 13 | 14 | for file in "${TO_UPDATE[@]}" 15 | do 16 | echo "Patching $file ..." 17 | sed -i "s/$CURRENT_VERSION/$NEW_VERSION/g" $file 18 | git add $file 19 | done 20 | 21 | git commit -m "Releasing v$NEW_VERSION" 22 | git push 23 | 24 | git tag -a v$NEW_VERSION -m "Release v$NEW_VERSION" 25 | git push origin v$NEW_VERSION 26 | 27 | echo 28 | echo "All done, v$NEW_VERSION released ^_^" 29 | -------------------------------------------------------------------------------- /screenshots/opensnitch-ui-general-tab-deny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/screenshots/opensnitch-ui-general-tab-deny.png -------------------------------------------------------------------------------- /screenshots/opensnitch-ui-proc-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/screenshots/opensnitch-ui-proc-details.png -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/screenshots/screenshot.png -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | dist 4 | *.egg-info 5 | __pycache__ 6 | -------------------------------------------------------------------------------- /ui/LICENSE: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://github.com/gustavo-iniguez-goya/opensnitch 3 | Upstream-Name: python3-opensnitch-ui 4 | Files: * 5 | Copyright: 6 | 2017-2018 evilsocket 7 | 2019-2020 Gustavo Iñiguez Goia 8 | Comment: Debian packaging is licensed under the same terms as upstream 9 | License: GPL-3.0 10 | This program is free software; you can redistribute it 11 | and/or modify it under the terms of the GNU General Public 12 | License as published by the Free Software Foundation; either 13 | version 3 of the License, or (at your option) any later 14 | version. 15 | . 16 | This program is distributed in the hope that it will be 17 | useful, but WITHOUT ANY WARRANTY; without even the implied 18 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 19 | PURPOSE. See the GNU General Public License for more 20 | details. 21 | . 22 | You should have received a copy of the GNU General Public 23 | License along with this program. If not, If not, see 24 | http://www.gnu.org/licenses/. 25 | . 26 | On Debian systems, the full text of the GNU General Public 27 | License version 3 can be found in the file 28 | '/usr/share/common-licenses/GPL-3'. 29 | -------------------------------------------------------------------------------- /ui/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include opensnitch/res * 2 | recursive-include opensnitch/i18n * 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /ui/Makefile: -------------------------------------------------------------------------------- 1 | all: opensnitch/resources_rc.py 2 | 3 | install: 4 | @pip3 install --upgrade . 5 | 6 | opensnitch/resources_rc.py: translations deps 7 | @pyrcc5 -o opensnitch/resources_rc.py opensnitch/res/resources.qrc 8 | 9 | translations: 10 | @cd i18n ; make 11 | for lang in $$(ls i18n/locales/); do \ 12 | if [ ! -d opensnitch/i18n/$$lang ]; then mkdir -p opensnitch/i18n/$$lang ; fi ; \ 13 | cp i18n/locales/$$lang/opensnitch-$$lang.qm opensnitch/i18n/$$lang/ ; \ 14 | done 15 | 16 | deps: 17 | @pip3 install -r requirements.txt 18 | 19 | clean: 20 | @rm -rf *.pyc 21 | @rm -rf opensnitch/resources_rc.py 22 | -------------------------------------------------------------------------------- /ui/bin/opensnitch-ui: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from PyQt5 import QtWidgets, QtGui, QtCore 4 | 5 | import sys 6 | import os 7 | import time 8 | import signal 9 | import argparse 10 | import logging 11 | 12 | logging.getLogger().disabled = True 13 | 14 | from concurrent import futures 15 | 16 | import grpc 17 | 18 | dist_path = '/usr/lib/python3/dist-packages/' 19 | if dist_path not in sys.path: 20 | sys.path.append(dist_path) 21 | 22 | from opensnitch.service import UIService 23 | from opensnitch.config import Config 24 | import opensnitch.version 25 | import opensnitch.ui_pb2 26 | from opensnitch.ui_pb2_grpc import add_UIServicer_to_server 27 | 28 | def on_exit(): 29 | app.quit() 30 | server.stop(0) 31 | sys.exit(0) 32 | 33 | def supported_qt_version(major, medium, minor): 34 | q = QtCore.QT_VERSION_STR.split(".") 35 | return int(q[0]) >= major and int(q[1]) >= medium and int(q[2]) >= minor 36 | 37 | if __name__ == '__main__': 38 | parser = argparse.ArgumentParser(description='OpenSnitch UI service.') 39 | parser.add_argument("--socket", dest="socket", default="unix:///tmp/osui.sock", help="Path of the unix socket for the gRPC service (https://github.com/grpc/grpc/blob/master/doc/naming.md).", metavar="FILE") 40 | parser.add_argument("--max-clients", dest="serverWorkers", default=10, help="Max number of allowed clients (incoming connections).") 41 | 42 | args = parser.parse_args() 43 | 44 | os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" 45 | if supported_qt_version(5,6,0): 46 | try: 47 | # NOTE: maybe we also need Qt::AA_UseHighDpiPixmaps 48 | QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) 49 | except Exception: 50 | pass 51 | 52 | locale = QtCore.QLocale.system() 53 | i18n_path = os.path.dirname(os.path.realpath(opensnitch.__file__)) + "/i18n" 54 | print("Loading translations:", i18n_path, "locale:", locale.name()) 55 | translator = QtCore.QTranslator() 56 | translator.load(i18n_path + "/" + locale.name() + "/opensnitch-" + locale.name() + ".qm") 57 | app = QtWidgets.QApplication(sys.argv) 58 | app.installTranslator(translator) 59 | 60 | service = UIService(app, on_exit) 61 | # @doc: https://grpc.github.io/grpc/python/grpc.html#server-object 62 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=int(args.serverWorkers))) 63 | 64 | add_UIServicer_to_server(service, server) 65 | 66 | if args.socket.startswith("unix://"): 67 | socket = args.socket[7:] 68 | socket = os.path.abspath(socket) 69 | server.add_insecure_port("unix:%s" % socket) 70 | else: 71 | server.add_insecure_port(args.socket) 72 | 73 | # https://stackoverflow.com/questions/5160577/ctrl-c-doesnt-work-with-pyqt 74 | signal.signal(signal.SIGINT, signal.SIG_DFL) 75 | 76 | try: 77 | # print "OpenSnitch UI service running on %s ..." % socket 78 | server.start() 79 | app.exec_() 80 | except KeyboardInterrupt: 81 | on_exit() 82 | 83 | -------------------------------------------------------------------------------- /ui/debian/changelog: -------------------------------------------------------------------------------- 1 | opensnitch-ui (1.3.0-1) unstable; urgency=medium 2 | 3 | * Allow to filter by dst networks. 4 | * Added check for configure showing pop-ups. 5 | 6 | -- Gustavo Iñiguez Goia Wed, 16 Dec 2020 01:18:31 +0100 7 | 8 | opensnitch-ui (1.3.0~rc-1) unstable; urgency=medium 9 | 10 | * Non-maintainer upload. 11 | 12 | -- Gustavo Iñiguez Goia Fri, 20 Nov 2020 13:32:07 +0100 13 | 14 | opensnitch-ui (1.2.0-1) unstable; urgency=medium 15 | 16 | * Sort rules by name. 17 | * Allow to set priority on rules. 18 | * Rules are case-insensitive by default. 19 | * Other fixes. 20 | 21 | -- Gustavo Iñiguez Goia Mon, 09 Nov 2020 23:00:38 +0100 22 | 23 | opensnitch-ui (1.0.1-1) unstable; urgency=medium 24 | 25 | * Fixed crash when clicking on General tab columns. 26 | * Added literal DstHost to the pop-up combo box. 27 | * Shorten autogenerated rules names. 28 | 29 | -- Gustavo Iñiguez Goia Tue, 28 Jul 2020 23:43:15 +0200 30 | 31 | opensnitch-ui (1.0.0-1) unstable; urgency=medium 32 | 33 | * v1.0.0 released. 34 | 35 | -- Gustavo Iñiguez Goia Thu, 16 Jul 2020 00:20:19 +0200 36 | 37 | opensnitch-ui (1.0.0rc11-1) unstable; urgency=medium 38 | 39 | * Added CWD field. 40 | * Fixed columns resizing/restoring. 41 | * Fixed General tab fields filtering. 42 | * Pop-up window: display process path if it's hidden. 43 | * Display better regexp errors on the rules editor. 44 | 45 | -- Gustavo Iñiguez Goia Wed, 24 Jun 2020 00:20:57 +0200 46 | 47 | opensnitch-ui (1.0.0rc10-2) unstable; urgency=medium 48 | 49 | * Fixed crash when selecting a user (closes #38). 50 | 51 | -- Gustavo Iñiguez Goia Wed, 17 Jun 2020 20:50:54 +0200 52 | 53 | opensnitch-ui (1.0.0rc10-1) unstable; urgency=medium 54 | 55 | * Allow to filter data in all tabs. 56 | * Refresh rules list after deleting a rule. 57 | * Fixed high CPU usage while showing a notification. 58 | * Fixed columns sort order. 59 | * Allow to delete rules in batch. 60 | * Remember the columns size. 61 | 62 | -- Gustavo Iñiguez Goia Sat, 13 Jun 2020 18:49:11 +0200 63 | 64 | opensnitch-ui (1.0.0rc9-1) unstable; urgency=medium 65 | 66 | * Added rules editor dialog. 67 | * Restart UI upon starting a new X session. 68 | * Allow to configure max clients from the cli. 69 | 70 | -- Gustavo Iñiguez Goia Sun, 17 May 2020 18:19:38 +0200 71 | 72 | opensnitch-ui (1.0.0rc8) unstable; urgency=medium 73 | 74 | * Allow to change settings (daemon && UI) from the UI. 75 | * Added Nodes view. 76 | * Improved UI performance, specially when remote nodes connected. 77 | * Fixed race condition when adding stats of remote nodes. 78 | 79 | -- Gustavo Iñiguez Goia Wed, 29 Apr 2020 21:56:54 +0200 80 | 81 | opensnitch-ui (1.0.0rc7-1) unstable; urgency=medium 82 | 83 | * Added help menu. 84 | * Added option to filter by command line. 85 | * Fixed UI icons. 86 | 87 | -- Gustavo Iñiguez Goia Sun, 12 Apr 2020 23:49:13 +0200 88 | 89 | opensnitch-ui (1.0.0rc6-1) unstable; urgency=medium 90 | 91 | * Fixed showing systray icon in Cinnamon. 92 | 93 | -- Gustavo Iñiguez Goia Sun, 08 Mar 2020 20:50:52 +0100 94 | 95 | opensnitch-ui (1.0.0rc5-1) unstable; urgency=medium 96 | 97 | * Workaround for crash parsing non-utf8 desktop files. 98 | * Fixed crash loading sqlite driver. 99 | * Fixed HighDpi scaling. 100 | * Fixed prompt layout. 101 | 102 | -- Gustavo Iñiguez Goia Mon, 24 Feb 2020 19:56:01 +0100 103 | 104 | opensnitch-ui (1.0.0rc3-1) unstable; urgency=medium 105 | 106 | * Fixed regex patterns. 107 | * Display alerts for not answered questions. 108 | * Added option to allow/deny second level domains. 109 | 110 | -- Gustavo Iñiguez Goia Tue, 18 Feb 2020 10:14:59 +0100 111 | 112 | opensnitch-ui (1.0.0rc2-1) unstable; urgency=low 113 | 114 | * initial release 115 | 116 | -- Gustavo Iñiguez Goia Thu, 06 Feb 2020 00:20:02 +0100 117 | -------------------------------------------------------------------------------- /ui/debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /ui/debian/config: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | . /usr/share/debconf/confmodule 4 | 5 | # set default value, otherwise the question is not shown on first install 6 | db_fset python3-opensnitch-ui/question1 seen false 7 | 8 | db_input high python3-opensnitch-ui/question1 || true 9 | db_go 10 | -------------------------------------------------------------------------------- /ui/debian/control: -------------------------------------------------------------------------------- 1 | Source: opensnitch-ui 2 | Maintainer: Gustavo Iñiguez Goia 3 | Uploaders: 4 | Gustavo Iniguez Goya , 5 | Priority: optional 6 | Homepage: https://github.com/evilsocket/opensnitch 7 | Build-Depends: python3-setuptools, python3-all, debhelper (>= 7.4.3), dh-python 8 | Standards-Version: 3.9.1 9 | 10 | 11 | Package: python3-opensnitch-ui 12 | Architecture: all 13 | Section: net 14 | Depends: 15 | debconf, libqt5sql5-sqlite, python3:any, python3-setuptools, python3-six, python3-pyqt5, 16 | python3-pyqt5.qtsql, python3-pyinotify, python3-pip, whiptail | dialog 17 | Description: opensnitch application firewall GUI 18 | opensnitch-ui is a GUI for opensnitch written in Python. 19 | It allows the user to view live outgoing connections, as well as search 20 | for details of the intercepted connections. 21 | . 22 | The user can decide if block outgoing connections based on properties of 23 | the connection: by port, by uid, by dst ip, by program or a combination 24 | of them. 25 | . 26 | These rules can last forever, until restart the daemon or just one time. 27 | -------------------------------------------------------------------------------- /ui/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://github.com/evilsocket/opensnitch 3 | Upstream-Name: opensnitch-ui 4 | Files: * 5 | Copyright: 6 | 2017-2018 evilsocket 7 | 2019-2020 Gustavo Iñiguez Goia 8 | Comment: Debian packaging is licensed under the same terms as upstream 9 | License: GPL-3.0 10 | This program is free software; you can redistribute it 11 | and/or modify it under the terms of the GNU General Public 12 | License as published by the Free Software Foundation; either 13 | version 3 of the License, or (at your option) any later 14 | version. 15 | . 16 | This program is distributed in the hope that it will be 17 | useful, but WITHOUT ANY WARRANTY; without even the implied 18 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 19 | PURPOSE. See the GNU General Public License for more 20 | details. 21 | . 22 | You should have received a copy of the GNU General Public 23 | License along with this program. If not, If not, see 24 | http://www.gnu.org/licenses/. 25 | . 26 | On Debian systems, the full text of the GNU General Public 27 | License version 3 can be found in the file 28 | '/usr/share/common-licenses/GPL-3'. 29 | -------------------------------------------------------------------------------- /ui/debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | . /usr/share/debconf/confmodule 5 | 6 | install_pip_pkgs() 7 | { 8 | db_get python3-opensnitch-ui/question1 9 | if [ -z "$RET" -o "$RET" = "true" -o "$RET" = "yes" ]; then 10 | echo "Installing grpcio-tools..." 11 | pip3 -q install grpcio-tools || echo "Unable to install grpcio, try it manually." 12 | echo 13 | echo "Installing unicode_slugify..." 14 | pip3 -q install unicode_slugify || echo "Unable to install unicode_slugify, try it manually." 15 | echo "Done." 16 | else 17 | echo "Not installing extra packages by user choice (debconf)" 18 | fi 19 | exit 0 20 | } 21 | 22 | for i in $(ls /home) 23 | do 24 | if grep -q /home/$i /etc/passwd ; then 25 | path=/home/$i/.config/autostart/ 26 | if [ ! -d $path ]; then 27 | mkdir -p $path 28 | fi 29 | if [ -f /usr/share/applications/opensnitch_ui.desktop ];then 30 | ln -s /usr/share/applications/opensnitch_ui.desktop $path 2>/dev/null || true 31 | fi 32 | fi 33 | done 34 | 35 | gtk-update-icon-cache /usr/share/icons/hicolor/ || true 36 | 37 | set +e 38 | 39 | case "$1" in 40 | configure) 41 | install_pip_pkgs 42 | ;; 43 | esac 44 | 45 | -------------------------------------------------------------------------------- /ui/debian/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | . /usr/share/debconf/confmodule 5 | 6 | purge_files() 7 | { 8 | if [ -e /usr/share/debconf/confmodule ]; then 9 | . /usr/share/debconf/confmodule 10 | fi 11 | 12 | for i in $(ls /home) 13 | do 14 | path=/home/$i/.config/ 15 | if [ -h $path/autostart/opensnitch_ui.desktop -o -f $path/autostart/opensnitch_ui.desktop ];then 16 | rm -f $path/autostart/opensnitch_ui.desktop 17 | fi 18 | if [ -d $path/opensnitch/ ]; then 19 | rm -rf $path/opensnitch/ 20 | fi 21 | done 22 | } 23 | 24 | pkill -15 opensnitch-ui || true 25 | db_purge 26 | 27 | case "$1" in 28 | purge) 29 | purge_files 30 | ;; 31 | remove) 32 | db_purge 33 | ;; 34 | esac 35 | -------------------------------------------------------------------------------- /ui/debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | . /usr/share/debconf/confmodule 5 | 6 | db_purge 7 | 8 | case "$1" in 9 | remove) 10 | echo 11 | echo " If you don't need them anymore, remember to uninstall unicode_slugify, grcpio-tools and protobuf:" 12 | echo 13 | echo " pip3 uninstall unicode_slugify" 14 | echo " pip3 uninstall grcpio-tools" 15 | echo " pip3 uninstall protobuf" 16 | echo 17 | 18 | ;; 19 | esac 20 | -------------------------------------------------------------------------------- /ui/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # This file was automatically generated by stdeb 0.9.0 at 4 | # Thu, 06 Feb 2020 00:20:02 +0100 5 | 6 | %: 7 | dh $@ --with python3 --buildsystem=python_distutils 8 | 9 | 10 | override_dh_auto_clean: 11 | python3 setup.py clean -a 12 | find . -name \*.pyc -exec rm {} \; 13 | 14 | 15 | 16 | override_dh_auto_build: 17 | python3 setup.py build --force 18 | 19 | 20 | 21 | override_dh_auto_install: 22 | python3 setup.py install --force --root=debian/python3-opensnitch-ui --no-compile -O0 --install-layout=deb 23 | 24 | 25 | 26 | override_dh_python2: 27 | dh_python2 --no-guessing-versions 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ui/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /ui/debian/source/options: -------------------------------------------------------------------------------- 1 | extend-diff-ignore="\.egg-info$" -------------------------------------------------------------------------------- /ui/debian/templates: -------------------------------------------------------------------------------- 1 | Template: python3-opensnitch-ui/question1 2 | Type: boolean 3 | Description: Do you want to install them now? 4 | OpenSnitch GUI needs to install system-wide packages, using python3-pip: 5 | . 6 | unicode_slugify, grpcio-tools and their dependencies (protobuf). 7 | . 8 | -------------------------------------------------------------------------------- /ui/i18n/Makefile: -------------------------------------------------------------------------------- 1 | all: update_langs gen_qm 2 | 3 | update_langs: 4 | @pylupdate5 opensnitch_i18n.pro 5 | 6 | gen_qm: 7 | @./generate_i18n.sh 8 | -------------------------------------------------------------------------------- /ui/i18n/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Adding a new translation: 3 | 4 | 1. mkdir `locales//` 5 | (echo $LANG) 6 | 2. add the path to opensnitch_i18n.pro: 7 | ``` 8 | TRANSLATIONS += locales/es_ES/opensnitch-es_ES.ts \ 9 | locales//opensnitch-.ts 10 | ``` 11 | 3. make 12 | 13 | ### Updating translations: 14 | 15 | 1. update translations definitions: 16 | - pylupdate5 opensnitch_i18n.pro 17 | 18 | 2. translate a language: 19 | - linguist locales/es_ES/opensnitch-es_ES.ts 20 | 21 | 3. create .qm file: 22 | - lrelease locales/es_ES/opensnitch-es_ES.ts -qm locales/es_ES/opensnitch-es_ES.qm 23 | 24 | or: 25 | 26 | 1. make 27 | 2. linguist locales/es_ES/opensnitch-es_ES.ts 28 | 3. make 29 | 30 | ### Installing translations (manually) 31 | 32 | In order to test a new translation: 33 | 34 | `mkdir -p /usr/lib/python3/dist-packages/opensnitch/i18n//` 35 | `cp locales//opensnitch-.qm /usr/lib/python3/dist-packages/opensnitch/i18n//` 36 | 37 | Note: the destination path may vary depending on your system. 38 | -------------------------------------------------------------------------------- /ui/i18n/generate_i18n.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | app_name="opensnitch" 4 | langs_dir="./locales" 5 | 6 | #pylupdate5 opensnitch_i18n.pro 7 | 8 | for lang in $(ls $langs_dir) 9 | do 10 | lang_path="$langs_dir/$lang/$app_name-$lang" 11 | lrelease $lang_path.ts -qm $lang_path.qm 12 | done 13 | -------------------------------------------------------------------------------- /ui/i18n/opensnitch_i18n.pro: -------------------------------------------------------------------------------- 1 | #TEMPLATE = app 2 | #TARGET = ts 3 | #INCLUDEPATH += opensnitch 4 | 5 | 6 | # Input 7 | SOURCES += ../opensnitch/service.py \ 8 | ../opensnitch/dialogs/prompt.py \ 9 | ../opensnitch/dialogs/preferences.py \ 10 | ../opensnitch/dialogs/ruleseditor.py \ 11 | ../opensnitch/dialogs/processdetails.py \ 12 | ../opensnitch/dialogs/stats.py 13 | 14 | FORMS += ../opensnitch/res/prompt.ui \ 15 | ../opensnitch/res/ruleseditor.ui \ 16 | ../opensnitch/res/preferences.ui \ 17 | ../opensnitch/res/process_details.ui \ 18 | ../opensnitch/res/stats.ui 19 | TRANSLATIONS += locales/es_ES/opensnitch-es_ES.ts \ 20 | locales/eu_ES/opensnitch-eu_ES.ts 21 | -------------------------------------------------------------------------------- /ui/opensnitch-ui.spec: -------------------------------------------------------------------------------- 1 | %define name opensnitch-ui 2 | %define version 1.3.0 3 | %define unmangled_version 1.3.0 4 | %define release 1 5 | %define __python python3 6 | %define desktop_file opensnitch_ui.desktop 7 | 8 | Summary: Prompt service and UI for the opensnitch application firewall. 9 | Name: %{name} 10 | Version: %{version} 11 | Release: %{release} 12 | Source0: %{name}-%{unmangled_version}.tar.gz 13 | License: GPL-3.0 14 | Group: Development/Libraries 15 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot 16 | Prefix: %{_prefix} 17 | BuildArch: noarch 18 | Vendor: Simone "evilsocket" Margaritelli 19 | Url: https://github.com/evilsocket/opensnitch 20 | Requires: python3, python3-pip, (python3-pyinotify or python3-inotify), python3-qt5 21 | Recommends: (python3-slugify or python3-python-slugify), python3-protobuf >= 3.0 22 | 23 | # avoid to depend on a particular python version 24 | %global __requires_exclude ^python\\(abi\\) = 3\\..$ 25 | 26 | %description 27 | GUI for the opensnitch application firewall 28 | opensnitch-ui is a GUI for opensnitch written in Python. 29 | It allows the user to view live outgoing connections, as well as search 30 | to make connections. 31 | . 32 | The user can decide if block the outgoing connection based on properties of 33 | the connection: by port, by uid, by dst ip, by program or a combination 34 | of them. 35 | . 36 | These rules can last forever, until the app restart or just one time. 37 | 38 | %prep 39 | %setup -n %{name}-%{unmangled_version} -n %{name}-%{unmangled_version} 40 | 41 | %post 42 | 43 | if [ $1 -ge 1 ]; then 44 | for i in $(ls /home) 45 | do 46 | if grep /home/$i /etc/passwd &>/dev/null; then 47 | path=/home/$i/.config/autostart/ 48 | if [ ! -d $path ]; then 49 | mkdir -p $path 50 | fi 51 | if [ -f /usr/share/applications/%{desktop_file} ];then 52 | ln -s /usr/share/applications/%{desktop_file} $path 2>/dev/null || true 53 | else 54 | echo "No desktop file: %{desktop_file}" 55 | fi 56 | fi 57 | done 58 | 59 | gtk-update-icon-cache /usr/share/icons/hicolor/ || true 60 | fi 61 | 62 | if [ $1 -eq 1 ]; then 63 | echo -e "\n You need to install 2 more packages: 64 | unicode_slugify and grpcio-tools. 65 | 66 | pip3 install grpcio-tools 67 | pip3 install unicode_slugify 68 | " 69 | fi 70 | 71 | %postun 72 | if [ $1 -eq 0 ]; then 73 | for i in $(ls /home) 74 | do 75 | if grep /home/$i /etc/passwd &>/dev/null; then 76 | path=/home/$i/.config/autostart/%{desktop_file} 77 | if [ -h $path -o -f $path ]; then 78 | rm -f $path 79 | else 80 | echo "No desktop file for this user: $path" 81 | fi 82 | fi 83 | done 84 | 85 | pkill -15 opensnitch-ui 2>/dev/null || true 86 | 87 | echo "" 88 | echo " Remember to uninstall grpcio-tools and unicode_slugify if you don't" 89 | echo " need them anymore:" 90 | echo " pip3 uninstall unicode_slugify" 91 | echo " pip3 uninstall grpcio-tools" 92 | echo "" 93 | fi 94 | 95 | 96 | %build 97 | python3 setup.py build 98 | 99 | %install 100 | python3 setup.py install --install-lib=/usr/lib/python3/dist-packages/ --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --prefix=/usr --record=INSTALLED_FILES 101 | 102 | %clean 103 | rm -rf $RPM_BUILD_ROOT 104 | 105 | %files -f INSTALLED_FILES 106 | %defattr(-,root,root) 107 | -------------------------------------------------------------------------------- /ui/opensnitch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/opensnitch/__init__.py -------------------------------------------------------------------------------- /ui/opensnitch/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PyQt5 import QtCore 3 | 4 | class Config: 5 | __instance = None 6 | 7 | HELP_URL = "https://github.com/gustavo-iniguez-goya/opensnitch/wiki/Configurations" 8 | 9 | # don't translate 10 | ACTION_ALLOW = "allow" 11 | ACTION_DENY = "deny" 12 | DURATION_UNTIL_RESTART = "until restart" 13 | DURATION_ALWAYS = "always" 14 | DURATION_ONCE = "once" 15 | # don't translate 16 | 17 | @staticmethod 18 | def init(): 19 | Config.__instance = Config() 20 | return Config.__instance 21 | 22 | @staticmethod 23 | def get(): 24 | if Config.__instance == None: 25 | Config._instance = Config() 26 | return Config.__instance 27 | 28 | def __init__(self): 29 | self.settings = QtCore.QSettings("opensnitch", "settings") 30 | 31 | if self.settings.value("global/default_timeout") == None: 32 | self.setSettings("global/default_timeout", 15) 33 | if self.settings.value("global/default_action") == None: 34 | self.setSettings("global/default_action", "allow") 35 | if self.settings.value("global/default_duration") == None: 36 | self.setSettings("global/default_duration", "until restart") 37 | if self.settings.value("global/default_target") == None: 38 | self.setSettings("global/default_target", 0) 39 | 40 | def reload(self): 41 | self.settings = QtCore.QSettings("opensnitch", "settings") 42 | 43 | def hasKey(self, key): 44 | return self.settings.contains(key) 45 | 46 | def setSettings(self, path, value): 47 | self.settings.setValue(path, value) 48 | self.settings.sync() 49 | 50 | def getSettings(self, path): 51 | return self.settings.value(path) 52 | 53 | def getBool(self, path): 54 | return self.settings.value(path, False, type=bool) 55 | 56 | def getInt(self, path): 57 | try: 58 | return self.settings.value(path, False, type=int) 59 | except Exception: 60 | return 0 61 | -------------------------------------------------------------------------------- /ui/opensnitch/desktop_parser.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | import configparser 3 | import pyinotify 4 | import threading 5 | import glob 6 | import os 7 | import re 8 | import shutil 9 | 10 | DESKTOP_PATHS = tuple([ 11 | os.path.join(d, 'applications') 12 | for d in os.getenv('XDG_DATA_DIRS', '/usr/share/').split(':') 13 | ]) 14 | 15 | class LinuxDesktopParser(threading.Thread): 16 | def __init__(self): 17 | threading.Thread.__init__(self) 18 | self.lock = Lock() 19 | self.daemon = True 20 | self.running = False 21 | self.apps = {} 22 | self.apps_by_name = {} 23 | # some things are just weird 24 | # (not really, i don't want to keep track of parent pids 25 | # just because of icons though, this hack is way easier) 26 | self.fixes = { 27 | '/opt/google/chrome/chrome': '/opt/google/chrome/google-chrome', 28 | '/usr/lib/firefox/firefox': '/usr/lib/firefox/firefox.sh', 29 | '/usr/bin/pidgin.orig': '/usr/bin/pidgin' 30 | } 31 | 32 | for desktop_path in DESKTOP_PATHS: 33 | if not os.path.exists(desktop_path): 34 | continue 35 | for desktop_file in glob.glob(os.path.join(desktop_path, '*.desktop')): 36 | self._parse_desktop_file(desktop_file) 37 | 38 | self.start() 39 | 40 | def _parse_exec(self, cmd): 41 | # remove stuff like %U 42 | cmd = re.sub( r'%[a-zA-Z]+', '', cmd) 43 | # remove 'env .... command' 44 | cmd = re.sub( r'^env\s+[^\s]+\s', '', cmd) 45 | # split && trim 46 | cmd = cmd.split(' ')[0].strip() 47 | # remove quotes 48 | cmd = re.sub( r'["\']+', '', cmd) 49 | # check if we need to resolve the path 50 | if len(cmd) > 0 and cmd[0] != '/': 51 | for path in os.environ["PATH"].split(os.pathsep): 52 | filename = os.path.join(path, cmd) 53 | if os.path.exists(filename): 54 | cmd = filename 55 | break 56 | 57 | return cmd 58 | 59 | def _discover_app_icon(self, app_name): 60 | # more hacks 61 | # normally qt will find icons if the system if configured properly. 62 | # if it's not, qt won't be able to find the icon by using QIcon().fromTheme(""), 63 | # so we fallback to try to determine if the icon exist in some well known system paths. 64 | icon_dirs = ("/usr/share/icons/gnome/48x48/apps/", "/usr/share/pixmaps/", "/usr/share/icons/hicolor/48x48/apps/") 65 | icon_exts = (".png", ".xpm", ".svg") 66 | 67 | for idir in icon_dirs: 68 | for iext in icon_exts: 69 | iconPath = idir + app_name + iext 70 | if os.path.exists(iconPath): 71 | print("found on last chance: ", iconPath) 72 | return iconPath 73 | 74 | def _parse_desktop_file(self, desktop_path): 75 | parser = configparser.ConfigParser(strict=False) # Allow duplicate config entries 76 | try: 77 | basename = os.path.basename(desktop_path)[:-8] 78 | parser.read(desktop_path, 'utf8') 79 | 80 | cmd = parser.get('Desktop Entry', 'exec', raw=True, fallback=None) 81 | if cmd == None: 82 | cmd = parser.get('Desktop Entry', 'Exec', raw=True, fallback=None) 83 | if cmd is not None: 84 | cmd = self._parse_exec(cmd) 85 | icon = parser.get('Desktop Entry', 'Icon', raw=True, fallback=None) 86 | name = parser.get('Desktop Entry', 'Name', raw=True, fallback=None) 87 | if icon == None: 88 | # Some .desktop files doesn't have the Icon entry 89 | # FIXME: even if we return an icon, if the DE is not properly configured, 90 | # it won't be loaded/displayed. 91 | icon = self._discover_app_icon(basename) 92 | 93 | with self.lock: 94 | # The Exec entry may have an absolute path to a binary or just the binary with parameters. 95 | # /path/binary or binary, so save both 96 | self.apps[cmd] = (name, icon, desktop_path) 97 | self.apps[basename] = (name, icon, desktop_path) 98 | # if the command is a symlink, add the real binary too 99 | if os.path.islink(cmd): 100 | link_to = os.path.realpath(cmd) 101 | self.apps[link_to] = (name, icon, desktop_path) 102 | except: 103 | print("Exception parsing .desktop file ", desktop_path) 104 | 105 | def get_info_by_path(self, path, default_icon): 106 | def_name = os.path.basename(path) 107 | # apply fixes 108 | for orig, to in self.fixes.items(): 109 | if path == orig: 110 | path = to 111 | break 112 | 113 | app_name = self.apps.get(path) 114 | if app_name == None: 115 | return self.apps.get(def_name, (def_name, default_icon, None)) 116 | 117 | return self.apps.get(path, (def_name, default_icon, None)) 118 | 119 | def get_info_by_binname(self, name, default_icon): 120 | def_name = os.path.basename(name) 121 | return self.apps.get(def_name, (def_name, default_icon, None)) 122 | 123 | def run(self): 124 | self.running = True 125 | wm = pyinotify.WatchManager() 126 | notifier = pyinotify.Notifier(wm) 127 | 128 | def inotify_callback(event): 129 | if event.mask == pyinotify.IN_CLOSE_WRITE: 130 | self._parse_desktop_file(event.pathname) 131 | 132 | elif event.mask == pyinotify.IN_DELETE: 133 | with self.lock: 134 | for cmd, data in self.apps.items(): 135 | if data[2] == event.pathname: 136 | del self.apps[cmd] 137 | break 138 | 139 | for p in DESKTOP_PATHS: 140 | if os.path.exists(p): 141 | wm.add_watch(p, 142 | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_DELETE, 143 | inotify_callback) 144 | notifier.loop() 145 | -------------------------------------------------------------------------------- /ui/opensnitch/dialogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/opensnitch/dialogs/__init__.py -------------------------------------------------------------------------------- /ui/opensnitch/res/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/opensnitch/res/__init__.py -------------------------------------------------------------------------------- /ui/opensnitch/res/icon-alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/opensnitch/res/icon-alert.png -------------------------------------------------------------------------------- /ui/opensnitch/res/icon-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/opensnitch/res/icon-off.png -------------------------------------------------------------------------------- /ui/opensnitch/res/icon-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/opensnitch/res/icon-red.png -------------------------------------------------------------------------------- /ui/opensnitch/res/icon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/opensnitch/res/icon-white.png -------------------------------------------------------------------------------- /ui/opensnitch/res/icon-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /ui/opensnitch/res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/opensnitch/res/icon.png -------------------------------------------------------------------------------- /ui/opensnitch/res/process_details.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProcessDetailsDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 731 10 | 478 11 | 12 | 13 | 14 | Process details 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 48 26 | 48 27 | 28 | 29 | 30 | 31 | 64 32 | 64 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | QFrame::NoFrame 46 | 47 | 48 | loading... 49 | 50 | 51 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 52 | 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | loading... 62 | 63 | 64 | Qt::PlainText 65 | 66 | 67 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 68 | 69 | 70 | true 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | CWD: loading... 82 | 83 | 84 | true 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | mem stats: loading... 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Qt::Horizontal 105 | 106 | 107 | 108 | 109 | 110 | 111 | QTabWidget::South 112 | 113 | 114 | 0 115 | 116 | 117 | true 118 | 119 | 120 | 121 | Status 122 | 123 | 124 | 125 | 126 | 127 | false 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Open files 136 | 137 | 138 | 139 | 140 | 141 | false 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | I/O Statistics 150 | 151 | 152 | 153 | 154 | 155 | false 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | Memory mapped files 164 | 165 | 166 | 167 | 168 | 169 | false 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | Stack 178 | 179 | 180 | 181 | 182 | 183 | false 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | Environment variables 192 | 193 | 194 | 195 | 196 | 197 | false 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | Application pids 211 | 212 | 213 | 214 | 215 | 216 | 217 | 100 218 | 219 | 220 | QComboBox::AdjustToContents 221 | 222 | 223 | 224 | 225 | 226 | 227 | Qt::Horizontal 228 | 229 | 230 | 231 | 40 232 | 20 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | Start or stop monitoring this process 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | true 250 | 251 | 252 | 253 | 254 | 255 | 256 | Close 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /ui/opensnitch/res/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon-white.svg 4 | icon-white.png 5 | icon-red.png 6 | icon.png 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui/opensnitch/ui_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | import grpc 3 | 4 | import ui_pb2 as ui__pb2 5 | 6 | 7 | class UIStub(object): 8 | # missing associated documentation comment in .proto file 9 | pass 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.Ping = channel.unary_unary( 18 | '/protocol.UI/Ping', 19 | request_serializer=ui__pb2.PingRequest.SerializeToString, 20 | response_deserializer=ui__pb2.PingReply.FromString, 21 | ) 22 | self.AskRule = channel.unary_unary( 23 | '/protocol.UI/AskRule', 24 | request_serializer=ui__pb2.Connection.SerializeToString, 25 | response_deserializer=ui__pb2.Rule.FromString, 26 | ) 27 | self.Subscribe = channel.unary_unary( 28 | '/protocol.UI/Subscribe', 29 | request_serializer=ui__pb2.ClientConfig.SerializeToString, 30 | response_deserializer=ui__pb2.ClientConfig.FromString, 31 | ) 32 | self.Notifications = channel.stream_stream( 33 | '/protocol.UI/Notifications', 34 | request_serializer=ui__pb2.NotificationReply.SerializeToString, 35 | response_deserializer=ui__pb2.Notification.FromString, 36 | ) 37 | 38 | 39 | class UIServicer(object): 40 | # missing associated documentation comment in .proto file 41 | pass 42 | 43 | def Ping(self, request, context): 44 | # missing associated documentation comment in .proto file 45 | pass 46 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 47 | context.set_details('Method not implemented!') 48 | raise NotImplementedError('Method not implemented!') 49 | 50 | def AskRule(self, request, context): 51 | # missing associated documentation comment in .proto file 52 | pass 53 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 54 | context.set_details('Method not implemented!') 55 | raise NotImplementedError('Method not implemented!') 56 | 57 | def Subscribe(self, request, context): 58 | # missing associated documentation comment in .proto file 59 | pass 60 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 61 | context.set_details('Method not implemented!') 62 | raise NotImplementedError('Method not implemented!') 63 | 64 | def Notifications(self, request_iterator, context): 65 | # missing associated documentation comment in .proto file 66 | pass 67 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 68 | context.set_details('Method not implemented!') 69 | raise NotImplementedError('Method not implemented!') 70 | 71 | 72 | def add_UIServicer_to_server(servicer, server): 73 | rpc_method_handlers = { 74 | 'Ping': grpc.unary_unary_rpc_method_handler( 75 | servicer.Ping, 76 | request_deserializer=ui__pb2.PingRequest.FromString, 77 | response_serializer=ui__pb2.PingReply.SerializeToString, 78 | ), 79 | 'AskRule': grpc.unary_unary_rpc_method_handler( 80 | servicer.AskRule, 81 | request_deserializer=ui__pb2.Connection.FromString, 82 | response_serializer=ui__pb2.Rule.SerializeToString, 83 | ), 84 | 'Subscribe': grpc.unary_unary_rpc_method_handler( 85 | servicer.Subscribe, 86 | request_deserializer=ui__pb2.ClientConfig.FromString, 87 | response_serializer=ui__pb2.ClientConfig.SerializeToString, 88 | ), 89 | 'Notifications': grpc.stream_stream_rpc_method_handler( 90 | servicer.Notifications, 91 | request_deserializer=ui__pb2.NotificationReply.FromString, 92 | response_serializer=ui__pb2.Notification.SerializeToString, 93 | ), 94 | } 95 | generic_handler = grpc.method_handlers_generic_handler( 96 | 'protocol.UI', rpc_method_handlers) 97 | server.add_generic_rpc_handlers((generic_handler,)) 98 | -------------------------------------------------------------------------------- /ui/opensnitch/version.py: -------------------------------------------------------------------------------- 1 | version = '1.3.0' 2 | -------------------------------------------------------------------------------- /ui/requirements.txt: -------------------------------------------------------------------------------- 1 | grpcio-tools==1.10.1 2 | pyinotify==0.9.6 3 | unicode_slugify==0.1.3 4 | pyqt5>=5.6 5 | protobuf 6 | -------------------------------------------------------------------------------- /ui/resources/kcm_opensnitch.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Exec=opensnitch-ui 3 | Icon=opensnitch-ui 4 | Type=Service 5 | X-KDE-ServiceTypes=SystemSettingsExternalApp 6 | X-KDE-System-Settings-Parent-Category=system-administration 7 | 8 | Name=OpenSnitch Firewall 9 | 10 | Comment=OpenSnitch Firewall Graphical Interface 11 | 12 | X-KDE-Keywords=system,firewall,policies,security,polkit,policykit,douane 13 | -------------------------------------------------------------------------------- /ui/resources/opensnitch-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/50b07f66b638a0f27a72a15f3367fd1d6148f389/ui/resources/opensnitch-ui.png -------------------------------------------------------------------------------- /ui/resources/opensnitch-ui.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /ui/resources/opensnitch_ui.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=OpenSnitch 4 | Exec=/bin/sh -c 'pkill -15 opensnitch-ui; opensnitch-ui' 5 | Icon=opensnitch-ui 6 | GenericName=OpenSnitch Firewall 7 | Terminal=false 8 | NoDisplay=false 9 | Categories=System;Filesystem;Network; 10 | Keywords=system;firewall;policies;security;polkit;policykit;douane; 11 | X-GNOME-Autostart-Delay=3 12 | X-GNOME-Autostart-enabled=true 13 | -------------------------------------------------------------------------------- /ui/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | import os 4 | import sys 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.append(path) 8 | 9 | from opensnitch.version import version 10 | 11 | setup(name='opensnitch-ui', 12 | version=version, 13 | description='Prompt service and UI for the opensnitch application firewall.', 14 | long_description='GUI for the opensnitch application firewall\n\ 15 | opensnitch-ui is a GUI for opensnitch written in Python.\n\ 16 | It allows the user to view live outgoing connections, as well as search\n\ 17 | to make connections.\n\ 18 | .\n\ 19 | The user can decide if block the outgoing connection based on properties of\n\ 20 | the connection: by port, by uid, by dst ip, by program or a combination\n\ 21 | of them.\n\ 22 | .\n\ 23 | These rules can last forever, until the app restart or just one time.', 24 | url='https://github.com/evilsocket/opensnitch', 25 | author='Simone "evilsocket" Margaritelli', 26 | author_email='evilsocket@protonmail.com', 27 | license='GPL-3.0', 28 | packages=find_packages(), 29 | include_package_data = True, 30 | package_data={'': ['*.*']}, 31 | data_files=[('/usr/share/applications', ['resources/opensnitch_ui.desktop']), 32 | ('/usr/share/kservices5', ['resources/kcm_opensnitch.desktop']), 33 | ('/usr/share/icons/hicolor/scalable/apps', ['resources/opensnitch-ui.svg']), 34 | ('/usr/share/icons/hicolor/48x48/apps', ['resources/opensnitch-ui.png'])], 35 | scripts = [ 'bin/opensnitch-ui' ], 36 | zip_safe=False) 37 | --------------------------------------------------------------------------------