├── .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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | **OpenSnitch** is a GNU/Linux application firewall.
19 |
20 |
21 |
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 |
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 |
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 |
--------------------------------------------------------------------------------