Merge opensnitch 1.3.0-rc2

This commit is contained in:
Gustavo Iñiguez Goia 2020-12-09 15:41:18 +01:00
commit 3a3d3d8f42
113 changed files with 14163 additions and 2470 deletions

49
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,49 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
Present yourself (or at least say "Hello" or "Hi") and be kind && respectful.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Describe in detail as much as you can what happened.
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Post error logs:**
If it's a crash of the GUI:
- Launch it from a terminal and reproduce the issue.
- Post the errors logged to the terminal.
If the daemon doesn't start:
- Post last 15 lines of the log file `/var/log/opensnitchd.log`
- Or launch it from a terminal (`/usr/bin/opensnitchd -rules-path /etc/opensnitchd/rules`) and post the errors logged to the terminal.
If the deb or rpm packages fail to install:
- Install them from a terminal (`dpkg -i opensnitch*` / `yum install opensnitch*`), and post the errors logged to stdout.
**Expected behavior (optional)**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**OS (please complete the following information):**
- OS: [e.g. Debian GNU/Linux, ArchLinux, Slackware, ...]
- Window Manager: [e.g. GNOME shell, KDE, enlightenment, ...]
- Kernel version: echo $(uname -a)
- Version [e.g. Buster, 10.3, 20.04]
**Additional context**
Add any other context about the problem here.

31
.github/workflows/go.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: Build status
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
sudo apt-get install git libnetfilter-queue-dev libmnl-dev libpcap-dev protobuf-compiler
- name: Build
run: |
cd daemon
go build -v .

847
LICENSE
View file

@ -1,197 +1,635 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and other kinds of works.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
?This License? refers to version 3 of the GNU General Public License.
?Copyright? also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
?The Program? refers to any copyrightable work licensed under this License. Each licensee is addressed as ?you?. ?Licensees? and ?recipients? may be individuals or organizations.
To ?modify? a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a ?modified version? of the earlier work or a work ?based on? the earlier work.
A ?covered work? means either the unmodified Program or a work based on the Program.
To ?propagate? a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To ?convey? a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays ?Appropriate Legal Notices? to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The ?source code? for a work means the preferred form of the work for making modifications to it. ?Object code? means any non-source form of a work.
A ?Standard Interface? means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The ?System Libraries? of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A ?Major Component?, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The ?Corresponding Source? for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
* a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
* b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to ?keep intact all notices?.
* c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
* d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an ?aggregate? if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
* a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
* b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
* c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
* d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
* e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A ?User Product? is either (1) a ?consumer product?, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, ?normally used? refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
?Installation Information? for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
?Additional permissions? are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
* a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
* b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
* c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
* d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
* e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
* f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered ?further restrictions? within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An ?entity transaction? is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A ?contributor? is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's ?contributor version?.
A contributor's ?essential patent claims? are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, ?control? includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a ?patent license? is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To ?grant? such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. ?Knowingly relying? means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is ?discriminatory? if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License ?or any later version? applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM ?AS IS? WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the ?copyright? line and a pointer to where the full notice is found.
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
@ -207,19 +645,30 @@ To do so, attach the following notices to the program. It is safest to attach th
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an ?about box?.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school, if any, to sign a ?copyright disclaimer? for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

185
README.md
View file

@ -1,186 +1,29 @@
<p align="center">
<img alt="opensnitch" src="https://raw.githubusercontent.com/evilsocket/opensnitch/master/ui/opensnitch/res/icon.png" height="160" />
<img alt="opensnitch" src="https://raw.githubusercontent.com/gustavo-iniguez-goya/opensnitch/master/ui/opensnitch/res/icon.png" height="160" />
<p align="center">
<a href="https://github.com/evilsocket/opensnitch/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/evilsocket/opensnitch.svg?style=flat-square"></a>
<a href="https://github.com/evilsocket/opensnitch/blob/master/LICENSE.md"><img alt="Software License" src="https://img.shields.io/badge/license-GPL3-brightgreen.svg?style=flat-square"></a>
<a href="https://goreportcard.com/report/github.com/evilsocket/opensnitch/daemon"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/evilsocket/opensnitch/daemon?style=flat-square"></a>
<img src="https://github.com/gustavo-iniguez-goya/opensnitch/workflows/Build%20status/badge.svg" />
<a href="https://github.com/gustavo-iniguez-goya/opensnitch/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/gustavo-iniguez-goya/opensnitch.svg?style=flat-square"></a>
<a href="https://github.com/gustavo-iniguez-goya/opensnitch/blob/master/LICENSE.md"><img alt="Software License" src="https://img.shields.io/badge/license-GPL3-brightgreen.svg?style=flat-square"></a>
<a href="https://goreportcard.com/report/github.com/gustavo-iniguez-goya/opensnitch/daemon"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/gustavo-iniguez-goya/opensnitch/daemon?style=flat-square"></a>
</p>
</p>
**OpenSnitch** is a GNU/Linux port of the Little Snitch application firewall.
**OpenSnitch** is a GNU/Linux application firewall.
<p align="center">
<img src="https://raw.githubusercontent.com/evilsocket/opensnitch/master/screenshot.png" alt="OpenSnitch"/>
<img src="https://user-images.githubusercontent.com/2742953/85205382-6ba9cb00-b31b-11ea-8e9a-bd4b8b05a236.png" alt="OpenSnitch"/>
</p>
**THIS SOFTWARE IS WORK IN PROGRESS, DO NOT EXPECT IT TO BE BUG FREE AND DO NOT RELY ON IT FOR ANY TYPE OF SECURITY.**
### Installation and configuration
### TL;DR
Please, refer to [the documentation](https://github.com/gustavo-iniguez-goya/opensnitch/wiki) for detailed information.
Make sure you have a correctly configured **Go >= 1.8** environment, that the `$GOPATH` environment variable is defined and then:
### Credits
```bash
# install dependencies
sudo apt-get install git libnetfilter-queue-dev libpcap-dev protobuf-compiler python3-pip
go get github.com/golang/protobuf/protoc-gen-go
go get -u github.com/golang/dep/cmd/dep
cd $GOPATH/src/github.com/golang/dep
./install.sh
export PATH=$PATH:$GOPATH/bin
python3 -m pip install --user grpcio-tools
# clone the repository (ignore the message about no Go files being found)
go get github.com/evilsocket/opensnitch
cd $GOPATH/src/github.com/evilsocket/opensnitch
# compile && install
make
sudo make install
# enable opensnitchd as a systemd service and start the UI
sudo systemctl enable opensnitchd
sudo service opensnitchd start
opensnitch-ui
```
OpenSnitch was originally created by **Simone Margaritelli** ([evilsocket](https://github.com/evilsocket)), 2017-2019.
### Daemon
Many others have also contributed over time, [see the list](https://github.com/gustavo-iniguez-goya/opensnitch/graphs/contributors)
The `daemon` is implemented in Go and needs to run as root in order to interact with the Netfilter packet queue, edit
iptables rules and so on, in order to compile it you will need to install the `protobuf-compiler`, `libpcap-dev` and `libnetfilter-queue-dev`
packages on your system, then just:
### Disclaimer
cd daemon
make
You can then install it as a systemd service by doing:
sudo make install
The new `opensnitchd` service will log to `/var/log/opensnitchd.log`, save the rules inside `/etc/opensnitchd/rules` and connect to the default UI service socket `unix:///tmp/osui.sock`.
### UI
The user interface is a Python 3 software running as a `gRPC` server on a unix socket, to order to install its dependencies:
cd ui
sudo pip3 install -r requirements.txt
You will also need to install the package `python-pyqt5` for your system (if anyone finds a way to make this work from
the `requirements.txt` file feel free to send a PR).
The UI is pip installable itself:
sudo pip3 install .
This will install the `opensnitch-ui` command on your system (you can auto startup it by `cp opensnitch_ui.desktop ~/.config/autostart/`).
#### UI Configuration
By default the UI will load its configuration from `~/.opensnitch/ui-config.json` (customizable with the `--config` argument), the
default contents of this file are:
```json
{
"default_timeout": 15,
"default_action": "allow",
"default_duration": "until restart"
}
```
The `default_timeout` is the number of seconds after which the UI will take its default action, the `default_action` can be `allow` or `deny`
and the `default_duration`, which indicates for how long the default action should be taken, can be `once`, `until restart` or `always` to
persist the action as a new rule on disk.
### Running
Once you installed both the daemon and the UI, you can enable the `opensnitchd` service to run at boot time:
sudo systemctl enable opensnitchd
And run it with:
sudo service opensnitchd start
While the UI can be started just by executing the `opensnitch-ui` command.
#### Single UI with many computers
You can also use `--socket "[::]:50051"` to have the UI use TCP instead of a unix socket and run the daemon on another
computer with `-ui-socket "x.x.x.x:50051"` (where `x.x.x.x` is the IP of the computer running the UI service).
### Rules
Rules are stored as JSON files inside the `-rule-path` folder, in the simplest cast a rule looks like this:
```json
{
"created": "2018-04-07T14:13:27.903996051+02:00",
"updated": "2018-04-07T14:13:27.904060088+02:00",
"name": "deny-simple-www-google-analytics-l-google-com",
"enabled": true,
"action": "deny",
"duration": "always",
"operator": {
"type": "simple",
"operand": "dest.host",
"data": "www-google-analytics.l.google.com"
}
}
```
| Field | Description |
| -----------------|---------------|
| created | UTC date and time of creation. |
| update | UTC date and time of the last update. |
| name | The name of the rule. |
| enabled | Use to temporarily disable and enable rules without moving their files. |
| action | Can be `deny` or `allow`. |
| duration | For rules persisting on disk, this value is default to `always`. |
| operator.type | Can be `simple`, in which case a simple `==` comparison will be performed, or `regexp` if the `data` field is a regular expression to match. |
| operator.operand | What element of the connection to compare, can be one of: `true` (will always match), `process.path` (the path of the executable), `process.command` (full command line, including path and arguments), `provess.env.ENV_VAR_NAME` (use the value of an environment variable of the process given its name), `user.id`, `dest.ip`, `dest.host` or `dest.port`. |
| operator.data | The data to compare the `operand` to, can be a regular expression if `type` is `regexp`. |
An example with a regular expression:
```json
{
"created": "2018-04-07T14:13:27.903996051+02:00",
"updated": "2018-04-07T14:13:27.904060088+02:00",
"name": "deny-any-google-analytics",
"enabled": true,
"action": "deny",
"duration": "always",
"operator": {
"type": "regexp",
"operand": "dest.host",
"data": "(?i).*analytics.*\\.google\\.com"
}
}
```
An example whitelisting a whole process:
```json
{
"created": "2018-04-07T15:00:48.156737519+02:00",
"updated": "2018-04-07T15:00:48.156772601+02:00",
"name": "allow-simple-opt-google-chrome-chrome",
"enabled": true,
"action": "allow",
"duration": "always",
"operator": {
"type": "simple",
"operand": "process.path",
"data": "/opt/google/chrome/chrome"
}
}
```
### FAQ
##### Why Qt and not GTK?
I tried, but for very fast updates it failed bad on my configuration (failed bad = SIGSEGV), moreover I find Qt5 layout system superior and easier to use.
##### Why gRPC and not DBUS?
The UI service is able to use a TCP listener instead of a UNIX socket, that means the UI service itself can be executed on any
operating system, while receiving messages from a single local daemon instance or multiple instances from remote computers in the network,
therefore DBUS would have made the protocol and logic uselessly GNU/Linux specific.
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.

120
daemon/Gopkg.lock generated
View file

@ -1,120 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/evilsocket/ftrace"
packages = ["."]
revision = "06529699d3b47fd1adae671b6851dd6f7539c841"
version = "v1.2.0"
[[projects]]
name = "github.com/fsnotify/fsnotify"
packages = ["."]
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
name = "github.com/golang/protobuf"
packages = [
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp"
]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
name = "github.com/google/gopacket"
packages = [
".",
"layers"
]
revision = "11c65f1ca9081dfea43b4f9643f5c155583b73ba"
version = "v1.1.14"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/timeseries",
"lex/httplex",
"trace"
]
revision = "8d16fa6dc9a85c1cd3ed24ad08ff21cf94f10888"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "b126b21c05a91c856b027c16779c12e3bf236954"
[[projects]]
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
revision = "7fd901a49ba6a7f87732eb344f6e3c5b19d1b200"
[[projects]]
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"codes",
"connectivity",
"credentials",
"encoding",
"encoding/proto",
"grpclb/grpc_lb_v1/messages",
"grpclog",
"internal",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"resolver/dns",
"resolver/passthrough",
"stats",
"status",
"tap",
"transport"
]
revision = "d11072e7ca9811b1100b80ca0269ac831f06d024"
version = "v1.11.3"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "61c68a1d6debe86e99277786bae9dde7665632da5d854cc5e40f9d349aacd99e"
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -4,12 +4,10 @@ install:
@mkdir -p /etc/opensnitchd/rules
@cp opensnitchd /usr/local/bin/
@cp opensnitchd.service /etc/systemd/system/
@cp default-config.json /etc/opensnitchd/
@systemctl daemon-reload
deps:
@dep ensure
opensnitchd: deps
opensnitchd:
@go build -o opensnitchd .
clean:

View file

@ -1,20 +1,24 @@
package conman
import (
"errors"
"fmt"
"net"
"os"
"github.com/evilsocket/opensnitch/daemon/dns"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/netfilter"
"github.com/evilsocket/opensnitch/daemon/netstat"
"github.com/evilsocket/opensnitch/daemon/procmon"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/dns"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/netfilter"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/netlink"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/netstat"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/procmon"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
"github.com/google/gopacket/layers"
)
// Connection represents an outgoing connecion.
type Connection struct {
Protocol string
SrcIP net.IP
@ -28,139 +32,186 @@ type Connection struct {
pkt *netfilter.Packet
}
func Parse(nfp netfilter.Packet) *Connection {
ipLayer := nfp.Packet.Layer(layers.LayerTypeIPv4)
ipLayer6 := nfp.Packet.Layer(layers.LayerTypeIPv6)
if ipLayer == nil && ipLayer6 == nil {
var showUnknownCons = false
// Parse extracts the IP layers from a network packet to determine what
// process generated a connection.
func Parse(nfp netfilter.Packet, interceptUnknown bool) *Connection {
showUnknownCons = interceptUnknown
if nfp.IsIPv4() {
con, err := NewConnection(&nfp)
if err != nil {
log.Debug("%s", err)
return nil
} else if con == nil {
return nil
}
return con
}
if core.IPv6Enabled == false {
return nil
}
if (ipLayer == nil) {
ip, ok := ipLayer6.(*layers.IPv6)
if ok == false || ip == nil {
return nil
}
// we're not interested in connections
// from/to the localhost interface
if ip.SrcIP.IsLoopback() {
return nil
}
// skip multicast stuff
if ip.SrcIP.IsMulticast() || ip.DstIP.IsMulticast() {
return nil
}
con, err := NewConnection6(&nfp, ip)
if err != nil {
log.Debug("%s", err)
return nil
} else if con == nil {
return nil
}
return con
} else {
ip, ok := ipLayer.(*layers.IPv4)
if ok == false || ip == nil {
return nil
}
// we're not interested in connections
// from/to the localhost interface
if ip.SrcIP.IsLoopback() {
return nil
}
// skip multicast stuff
if ip.SrcIP.IsMulticast() || ip.DstIP.IsMulticast() {
return nil
}
con, err := NewConnection(&nfp, ip)
if err != nil {
log.Debug("%s", err)
return nil
} else if con == nil {
return nil
}
return con
con, err := NewConnection6(&nfp)
if err != nil {
log.Debug("%s", err)
return nil
} else if con == nil {
return nil
}
return con
}
func newConnectionImpl(nfp *netfilter.Packet, c *Connection) (cr *Connection, err error) {
func newConnectionImpl(nfp *netfilter.Packet, c *Connection, protoType string) (cr *Connection, err error) {
// no errors but not enough info neither
if c.parseDirection() == false {
if c.parseDirection(protoType) == false {
return nil, nil
}
log.Debug("new connection %s => %d:%v -> %v:%d uid: ", c.Protocol, c.SrcPort, c.SrcIP, c.DstIP, c.DstPort, nfp.UID)
// 1. lookup uid and inode using /proc/net/(udp|tcp)
c.Entry = &netstat.Entry{
Proto: c.Protocol,
SrcIP: c.SrcIP,
SrcPort: c.SrcPort,
DstIP: c.DstIP,
DstPort: c.DstPort,
UserId: -1,
INode: -1,
}
// 0. lookup uid and inode via netlink. Can return several inodes.
// 1. lookup uid and inode using /proc/net/(udp|tcp|udplite)
// 2. lookup pid by inode
// 3. if this is coming from us, just accept
// 4. lookup process info by pid
if c.Entry = netstat.FindEntry(c.Protocol, c.SrcIP, c.SrcPort, c.DstIP, c.DstPort); c.Entry == nil {
return nil, fmt.Errorf("Could not find netstat entry for: %s", c)
} else if pid := procmon.GetPIDFromINode(c.Entry.INode); pid == -1 {
return nil, fmt.Errorf("Could not find process id for: %s", c)
} else if pid == os.Getpid() {
uid, inodeList := netlink.GetSocketInfo(c.Protocol, c.SrcIP, c.SrcPort, c.DstIP, c.DstPort)
if len(inodeList) == 0 {
if c.Entry = netstat.FindEntry(c.Protocol, c.SrcIP, c.SrcPort, c.DstIP, c.DstPort); c.Entry == nil {
return nil, fmt.Errorf("Could not find netstat entry for: %s", c)
}
if c.Entry.INode != -1 {
inodeList = append([]int{c.Entry.INode}, inodeList...)
}
}
if len(inodeList) == 0 {
log.Debug("<== no inodes found, applying default action.")
return nil, nil
} else if c.Process = procmon.FindProcess(pid); c.Process == nil {
}
if uid != -1 {
c.Entry.UserId = uid
} else if c.Entry.UserId == -1 && nfp.UID != 0xffffffff {
c.Entry.UserId = int(nfp.UID)
}
pid := -1
for n, inode := range inodeList {
if pid = procmon.GetPIDFromINode(inode, fmt.Sprint(inode, c.SrcIP, c.SrcPort, c.DstIP, c.DstPort)); pid == os.Getpid() {
// return a Process object with our PID, to be able to exclude our own connections
// (to the UI on a local socket for example)
c.Process = procmon.NewProcess(pid, "")
return c, nil
}
if pid != -1 {
log.Debug("[%d] PID found %d", n, pid)
c.Entry.INode = inode
break
}
}
if c.Process = procmon.FindProcess(pid, showUnknownCons); c.Process == nil {
return nil, fmt.Errorf("Could not find process by its pid %d for: %s", pid, c)
}
return c, nil
}
func NewConnection(nfp *netfilter.Packet, ip *layers.IPv4) (c *Connection, err error) {
// NewConnection creates a new Connection object, and returns the details of it.
func NewConnection(nfp *netfilter.Packet) (c *Connection, err error) {
ipv4 := nfp.Packet.Layer(layers.LayerTypeIPv4)
if ipv4 == nil {
return nil, errors.New("Error getting IPv4 layer")
}
ip, ok := ipv4.(*layers.IPv4)
if !ok {
return nil, errors.New("Error getting IPv4 layer data")
}
c = &Connection{
SrcIP: ip.SrcIP,
DstIP: ip.DstIP,
DstHost: dns.HostOr(ip.DstIP, ip.DstIP.String()),
DstHost: dns.HostOr(ip.DstIP, ""),
pkt: nfp,
}
return newConnectionImpl(nfp, c)
return newConnectionImpl(nfp, c, "")
}
func NewConnection6(nfp *netfilter.Packet, ip *layers.IPv6) (c *Connection, err error) {
// NewConnection6 creates a IPv6 new Connection object, and returns the details of it.
func NewConnection6(nfp *netfilter.Packet) (c *Connection, err error) {
ipv6 := nfp.Packet.Layer(layers.LayerTypeIPv6)
if ipv6 == nil {
return nil, errors.New("Error getting IPv6 layer")
}
ip, ok := ipv6.(*layers.IPv6)
if !ok {
return nil, errors.New("Error getting IPv6 layer data")
}
c = &Connection{
SrcIP: ip.SrcIP,
DstIP: ip.DstIP,
DstHost: dns.HostOr(ip.DstIP, ip.DstIP.String()),
DstHost: dns.HostOr(ip.DstIP, ""),
pkt: nfp,
}
return newConnectionImpl(nfp, c)
return newConnectionImpl(nfp, c, "6")
}
func (c *Connection) parseDirection() bool {
func (c *Connection) parseDirection(protoType string) bool {
ret := false
for _, layer := range c.pkt.Packet.Layers() {
if layer.LayerType() == layers.LayerTypeTCP {
if tcp, ok := layer.(*layers.TCP); ok == true && tcp != nil {
c.Protocol = "tcp"
c.DstPort = uint(tcp.DstPort)
c.SrcPort = uint(tcp.SrcPort)
ret = true
if tcpLayer := c.pkt.Packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
if tcp, ok := tcpLayer.(*layers.TCP); ok == true && tcp != nil {
c.Protocol = "tcp" + protoType
c.DstPort = uint(tcp.DstPort)
c.SrcPort = uint(tcp.SrcPort)
ret = true
if tcp.DstPort == 53 {
c.getDomains(c.pkt, c)
}
} else if layer.LayerType() == layers.LayerTypeUDP {
if udp, ok := layer.(*layers.UDP); ok == true && udp != nil {
c.Protocol = "udp"
c.DstPort = uint(udp.DstPort)
c.SrcPort = uint(udp.SrcPort)
ret = true
}
} else if udpLayer := c.pkt.Packet.Layer(layers.LayerTypeUDP); udpLayer != nil {
if udp, ok := udpLayer.(*layers.UDP); ok == true && udp != nil {
c.Protocol = "udp" + protoType
c.DstPort = uint(udp.DstPort)
c.SrcPort = uint(udp.SrcPort)
ret = true
if udp.DstPort == 53 {
c.getDomains(c.pkt, c)
}
}
} else if udpliteLayer := c.pkt.Packet.Layer(layers.LayerTypeUDPLite); udpliteLayer != nil {
if udplite, ok := udpliteLayer.(*layers.UDPLite); ok == true && udplite != nil {
c.Protocol = "udplite" + protoType
c.DstPort = uint(udplite.DstPort)
c.SrcPort = uint(udplite.SrcPort)
ret = true
}
}
for _, layer := range c.pkt.Packet.Layers() {
if layer.LayerType() == layers.LayerTypeIPv6 {
if tcp, ok := layer.(*layers.IPv6); ok == true && tcp != nil {
c.Protocol += "6"
}
}
}
return ret
}
func (c *Connection) getDomains(nfp *netfilter.Packet, con *Connection) {
domains := dns.GetQuestions(nfp)
if len(domains) > 0 {
for _, dns := range domains {
con.DstHost = dns
}
}
}
// To returns the destination host of a connection.
func (c *Connection) To() string {
if c.DstHost == "" {
return c.DstIP.String()
@ -180,6 +231,7 @@ func (c *Connection) String() string {
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)
}
// Serialize returns a connection serialized.
func (c *Connection) Serialize() *protocol.Connection {
return &protocol.Connection{
Protocol: c.Protocol,
@ -192,5 +244,7 @@ func (c *Connection) Serialize() *protocol.Connection {
ProcessId: uint32(c.Process.ID),
ProcessPath: c.Process.Path,
ProcessArgs: c.Process.Args,
ProcessEnv: c.Process.Env,
ProcessCwd: c.Process.CWD,
}
}

View file

@ -1,7 +1,6 @@
package core
import (
"fmt"
"os"
"os/exec"
"os/user"
@ -13,10 +12,12 @@ const (
defaultTrimSet = "\r\n\t "
)
// Trim remove trailing spaces from a string.
func Trim(s string) string {
return strings.Trim(s, defaultTrimSet)
}
// Exec spawns a new process and reurns the output.
func Exec(executable string, args []string) (string, error) {
path, err := exec.LookPath(executable)
if err != nil {
@ -25,13 +26,12 @@ func Exec(executable string, args []string) (string, error) {
raw, err := exec.Command(path, args...).CombinedOutput()
if err != nil {
fmt.Printf("ERROR: path=%s args=%s err=%s out='%s'\n", path, args, err, raw)
return "", err
} else {
return Trim(string(raw)), nil
}
return Trim(string(raw)), nil
}
// Exists checks if a path exists.
func Exists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
@ -39,6 +39,7 @@ func Exists(path string) bool {
return true
}
// ExpandPath replaces '~' shorthand with the user's home directory.
func ExpandPath(path string) (string, error) {
// Check if path is empty
if path != "" {

23
daemon/core/system.go Normal file
View file

@ -0,0 +1,23 @@
package core
import (
"io/ioutil"
"strings"
)
var (
// IPv6Enabled indicates if IPv6 protocol is enabled in the system
IPv6Enabled = Exists("/proc/sys/net/ipv6")
)
// GetHostname returns the name of the host where the damon is running.
func GetHostname() string {
hostname, _ := ioutil.ReadFile("/proc/sys/kernel/hostname")
return strings.Replace(string(hostname), "\n", "", -1)
}
// GetKernelVersion returns the name of the host where the damon is running.
func GetKernelVersion() string {
version, _ := ioutil.ReadFile("/proc/sys/kernel/version")
return strings.Replace(string(version), "\n", "", -1)
}

View file

@ -1,8 +1,9 @@
package core
// version related consts
const (
Name = "opensnitch-daemon"
Version = "1.0.0b"
Version = "1.3.0rc2"
Author = "Simone 'evilsocket' Margaritelli"
Website = "https://github.com/evilsocket/opensnitch"
)

View file

@ -0,0 +1,12 @@
{
"Server":
{
"Address":"unix:///tmp/osui.sock",
"LogFile":"/var/log/opensnitchd.log"
},
"DefaultAction": "allow",
"DefaultDuration": "once",
"InterceptUnknown": false,
"ProcMonitorMethod": "proc",
"LogLevel": 2
}

21
daemon/dns/parse.go Normal file
View file

@ -0,0 +1,21 @@
package dns
import (
"github.com/google/gopacket/layers"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/netfilter"
)
// GetQuestions retrieves the domain names a process is trying to resolve.
func GetQuestions(nfp *netfilter.Packet) (questions []string) {
dnsLayer := nfp.Packet.Layer(layers.LayerTypeDNS)
if dnsLayer == nil {
return questions
}
dns, _ := dnsLayer.(*layers.DNS)
for _, dnsQuestion := range dns.Questions {
questions = append(questions, string(dnsQuestion.Name))
}
return questions
}

View file

@ -4,7 +4,7 @@ import (
"net"
"sync"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
@ -15,6 +15,8 @@ var (
lock = sync.RWMutex{}
)
// TrackAnswers obtains the resolved domains of a DNS query.
// If the packet is UDP DNS, the domain names are added to the list of resolved domains.
func TrackAnswers(packet gopacket.Packet) bool {
udpLayer := packet.Layer(layers.LayerTypeUDP)
if udpLayer == nil {
@ -25,7 +27,6 @@ func TrackAnswers(packet gopacket.Packet) bool {
if ok == false || udp == nil {
return false
}
if udp.SrcPort != 53 {
return false
}
@ -53,15 +54,20 @@ func TrackAnswers(packet gopacket.Packet) bool {
return true
}
// Track adds a resolved domain to the list.
func Track(resolved string, hostname string) {
lock.Lock()
defer lock.Unlock()
if resolved == "127.0.0.1" {
return
}
responses[resolved] = hostname
log.Debug("New DNS record: %s -> %s", resolved, hostname)
}
// Host returns if a resolved domain is in the list.
func Host(resolved string) (host string, found bool) {
lock.RLock()
defer lock.RUnlock()
@ -70,6 +76,8 @@ func Host(resolved string) (host string, found bool) {
return
}
// HostOr checks if an IP has a domain name already resolved.
// If the domain is in the list it's returned, otherwise the IP will be returned.
func HostOr(ip net.IP, or string) string {
if host, found := Host(ip.String()); found == true {
// host might have been CNAME; go back until we reach the "root"

110
daemon/firewall/config.go Normal file
View file

@ -0,0 +1,110 @@
package firewall
import (
"encoding/json"
"fmt"
"io/ioutil"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
var (
configFile = "/etc/opensnitchd/system-fw.json"
configWatcher *fsnotify.Watcher
fwConfig config
)
type fwRule struct {
Description string
Table string
Chain string
Parameters string
Target string
TargetParameters string
}
type rulesList struct {
Rule *fwRule
}
type config struct {
sync.RWMutex
SystemRules []*rulesList
}
func loadDiskConfiguration(reload bool) {
raw, err := ioutil.ReadFile(configFile)
if err != nil {
fmt.Errorf("Error loading disk firewall configuration %s: %s", configFile, err)
}
if ok := loadConfiguration(raw); ok {
configWatcher.Remove(configFile)
if err := configWatcher.Add(configFile); err != nil {
log.Error("Could not watch firewall configuration: %s", err)
return
}
}
if reload {
return
}
go monitorConfigWorker()
}
// loadConfigutation reads the system firewall rules from disk.
// Then the rules are added based on the configuration defined.
func loadConfiguration(rawConfig []byte) bool {
fwConfig.Lock()
defer fwConfig.Unlock()
DeleteSystemRules(log.GetLogLevel() == log.DEBUG)
if err := json.Unmarshal(rawConfig, &fwConfig); err != nil {
log.Error("Error parsing firewall configuration %s: %s", configFile, err)
return false
}
for _, r := range fwConfig.SystemRules {
if r.Rule.Chain == "" {
continue
}
CreateSystemRule(r.Rule, true)
AddSystemRule(ADD, r.Rule, true)
}
return true
}
func saveConfiguration(rawConfig string) error {
conf, err := json.Marshal([]byte(rawConfig))
if err != nil {
log.Error("saving json firewall configuration: ", err, conf)
return err
}
if loadConfiguration([]byte(rawConfig)) != true {
return fmt.Errorf("Error parsing firewall configuration %s: %s", rawConfig, err)
}
if err = ioutil.WriteFile(configFile, []byte(rawConfig), 0644); err != nil {
log.Error("writing firewall configuration to disk: ", err)
return err
}
return nil
}
func monitorConfigWorker() {
for {
select {
case <-rulesCheckerChan:
return
case event := <-configWatcher.Events:
if (event.Op&fsnotify.Write == fsnotify.Write) || (event.Op&fsnotify.Remove == fsnotify.Remove) {
loadDiskConfiguration(true)
}
}
}
}

View file

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

19
daemon/go.mod Normal file
View file

@ -0,0 +1,19 @@
module github.com/gustavo-iniguez-goya/opensnitch/daemon
go 1.14
require (
github.com/evilsocket/ftrace v1.2.0
github.com/fsnotify/fsnotify v1.4.7
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/protobuf v1.0.0
github.com/google/gopacket v1.1.14
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
github.com/vishvananda/netlink v1.1.0
golang.org/x/net v0.0.0-20180417003750-8d16fa6dc9a8
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444 // indirect
golang.org/x/text v0.3.0 // indirect
google.golang.org/genproto v0.0.0-20180413175816-7fd901a49ba6 // indirect
google.golang.org/grpc v1.11.3
)

View file

@ -32,6 +32,7 @@ const (
RESET = "\033[0m"
)
// log level constants
const (
DEBUG = iota
INFO
@ -41,13 +42,15 @@ const (
FATAL
)
//
var (
WithColors = true
Output = os.Stdout
StdoutFile = "/dev/stdout"
DateFormat = "2006-01-02 15:04:05"
MinLevel = INFO
mutex = &sync.Mutex{}
mutex = &sync.RWMutex{}
labels = map[int]string{
DEBUG: "DBG",
INFO: "INF",
@ -66,6 +69,7 @@ var (
}
)
// Wrap wraps a text with effects
func Wrap(s, effect string) string {
if WithColors == true {
s = effect + s + RESET
@ -73,41 +77,63 @@ func Wrap(s, effect string) string {
return s
}
// Dim dims a text
func Dim(s string) string {
return Wrap(s, DIM)
}
// Bold bolds a text
func Bold(s string) string {
return Wrap(s, BOLD)
}
// Red reds the text
func Red(s string) string {
return Wrap(s, RED)
}
// Green greens the text
func Green(s string) string {
return Wrap(s, GREEN)
}
// Blue blues the text
func Blue(s string) string {
return Wrap(s, BLUE)
}
// Yellow yellows the text
func Yellow(s string) string {
return Wrap(s, YELLOW)
}
// Raw prints out a text without colors
func Raw(format string, args ...interface{}) {
mutex.Lock()
defer mutex.Unlock()
fmt.Fprintf(Output, format, args...)
}
func Log(level int, format string, args ...interface{}) {
if level >= MinLevel {
mutex.Lock()
defer mutex.Unlock()
// SetLogLevel sets the log level
func SetLogLevel(newLevel int) {
mutex.Lock()
defer mutex.Unlock()
MinLevel = newLevel
}
// GetLogLevel returns the current log level configured.
func GetLogLevel() int {
mutex.Lock()
defer mutex.Unlock()
return MinLevel
}
// Log prints out a text with the given color and format
func Log(level int, format string, args ...interface{}) {
mutex.Lock()
defer mutex.Unlock()
if level >= MinLevel {
label := labels[level]
color := colors[level]
when := time.Now().UTC().Format(DateFormat)
@ -124,26 +150,62 @@ func Log(level int, format string, args ...interface{}) {
}
}
func setDefaultLogOutput() {
mutex.Lock()
Output = os.Stdout
mutex.Unlock()
}
// OpenFile opens a file to print out the logs
func OpenFile(logFile string) (err error) {
if logFile == StdoutFile {
setDefaultLogOutput()
return
}
if Output, err = os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
Error("Error opening log: ", logFile, err)
//fallback to stdout
setDefaultLogOutput()
}
Important("Start writing logs to ", logFile)
return err
}
// Close closes the current output file descriptor
func Close() {
if Output != os.Stdout {
Output.Close()
}
}
// Debug is the log level for debugging purposes
func Debug(format string, args ...interface{}) {
Log(DEBUG, format, args...)
}
// Info is the log level for informative messages
func Info(format string, args ...interface{}) {
Log(INFO, format, args...)
}
// Important is the log level for things that must pay attention
func Important(format string, args ...interface{}) {
Log(IMPORTANT, format, args...)
}
// Warning is the log level for non-critical errors
func Warning(format string, args ...interface{}) {
Log(WARNING, format, args...)
}
// Error is the log level for errors that should be corrected
func Error(format string, args ...interface{}) {
Log(ERROR, format, args...)
}
// Fatal is the log level for errors that must be corrected before continue
func Fatal(format string, args ...interface{}) {
Log(FATAL, format, args...)
os.Exit(1)

View file

@ -1,6 +1,7 @@
package main
import (
"context"
"flag"
"fmt"
"io/ioutil"
@ -9,44 +10,54 @@ import (
"os/signal"
"runtime"
"runtime/pprof"
"sync"
"syscall"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/dns"
"github.com/evilsocket/opensnitch/daemon/firewall"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/netfilter"
"github.com/evilsocket/opensnitch/daemon/procmon"
"github.com/evilsocket/opensnitch/daemon/rule"
"github.com/evilsocket/opensnitch/daemon/statistics"
"github.com/evilsocket/opensnitch/daemon/ui"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/conman"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/dns"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/firewall"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/netfilter"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/procmon"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/rule"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/statistics"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui"
)
var (
logFile = ""
rulesPath = "rules"
noLiveReload = false
queueNum = 0
workers = 16
debug = false
lock sync.RWMutex
procmonMethod = ""
logFile = ""
rulesPath = "rules"
noLiveReload = false
queueNum = 0
workers = 16
debug = false
warning = false
important = false
errorlog = false
uiSocket = "unix:///tmp/osui.sock"
uiSocket = ""
uiClient = (*ui.Client)(nil)
cpuProfile = ""
memProfile = ""
err = (error)(nil)
rules = (*rule.Loader)(nil)
stats = (*statistics.Statistics)(nil)
queue = (*netfilter.Queue)(nil)
pktChan = (<-chan netfilter.Packet)(nil)
wrkChan = (chan netfilter.Packet)(nil)
sigChan = (chan os.Signal)(nil)
ctx = (context.Context)(nil)
cancel = (context.CancelFunc)(nil)
err = (error)(nil)
rules = (*rule.Loader)(nil)
stats = (*statistics.Statistics)(nil)
queue = (*netfilter.Queue)(nil)
pktChan = (<-chan netfilter.Packet)(nil)
wrkChan = (chan netfilter.Packet)(nil)
sigChan = (chan os.Signal)(nil)
exitChan = (chan bool)(nil)
)
func init() {
flag.StringVar(&procmonMethod, "process-monitor-method", procmonMethod, "How to search for processes path. Options: ftrace, audit (experimental), proc (default)")
flag.StringVar(&uiSocket, "ui-socket", uiSocket, "Path the UI gRPC service listener (https://github.com/grpc/grpc/blob/master/doc/naming.md).")
flag.StringVar(&rulesPath, "rules-path", rulesPath, "Path to load JSON rules from.")
flag.IntVar(&queueNum, "queue-num", queueNum, "Netfilter queue number.")
@ -54,29 +65,44 @@ func init() {
flag.BoolVar(&noLiveReload, "no-live-reload", debug, "Disable rules live reloading.")
flag.StringVar(&logFile, "log-file", logFile, "Write logs to this file instead of the standard output.")
flag.BoolVar(&debug, "debug", debug, "Enable debug logs.")
flag.BoolVar(&debug, "debug", debug, "Enable debug level logs.")
flag.BoolVar(&warning, "warning", warning, "Enable warning level logs.")
flag.BoolVar(&important, "important", important, "Enable important level logs.")
flag.BoolVar(&errorlog, "error", errorlog, "Enable error level logs.")
flag.StringVar(&cpuProfile, "cpu-profile", cpuProfile, "Write CPU profile to this file.")
flag.StringVar(&memProfile, "mem-profile", memProfile, "Write memory profile to this file.")
}
func overwriteLogging() bool {
return debug || warning || important || errorlog || logFile != ""
}
func setupLogging() {
golog.SetOutput(ioutil.Discard)
if debug {
log.MinLevel = log.DEBUG
log.SetLogLevel(log.DEBUG)
} else if warning {
log.SetLogLevel(log.WARNING)
} else if important {
log.SetLogLevel(log.IMPORTANT)
} else if errorlog {
log.SetLogLevel(log.ERROR)
} else {
log.MinLevel = log.INFO
log.SetLogLevel(log.INFO)
}
if logFile != "" {
if log.Output, err = os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
panic(err)
log.Close()
if err := log.OpenFile(logFile); err != nil {
log.Error("Error opening user defined log: ", logFile, err)
}
}
}
func setupSignals() {
sigChan = make(chan os.Signal, 1)
exitChan = make(chan bool, workers+1)
signal.Notify(sigChan,
syscall.SIGHUP,
syscall.SIGINT,
@ -86,8 +112,7 @@ func setupSignals() {
sig := <-sigChan
log.Raw("\n")
log.Important("Got signal: %v", sig)
doCleanup()
os.Exit(0)
cancel()
}()
}
@ -95,10 +120,19 @@ func worker(id int) {
log.Debug("Worker #%d started.", id)
for true {
select {
case pkt := <-wrkChan:
case <-ctx.Done():
goto Exit
default:
pkt, ok := <-wrkChan
if !ok {
log.Debug("worker channel closed", id)
goto Exit
}
onPacket(pkt)
}
}
Exit:
log.Debug("worker #%d exit", id)
}
func setupWorkers() {
@ -110,13 +144,12 @@ func setupWorkers() {
}
}
func doCleanup() {
func doCleanup(queue *netfilter.Queue) {
log.Info("Cleaning up ...")
firewall.QueueDNSResponses(false, queueNum)
firewall.QueueConnections(false, queueNum)
firewall.DropMarked(false)
go procmon.Stop()
firewall.Stop(&queueNum)
procmon.End()
uiClient.Close()
queue.Close()
if cpuProfile != "" {
pprof.StopCPUProfile()
@ -145,22 +178,50 @@ func onPacket(packet netfilter.Packet) {
}
// Parse the connection state
con := conman.Parse(packet)
con := conman.Parse(packet, uiClient.InterceptUnknown())
if con == nil {
applyDefaultAction(&packet)
return
}
// accept our own connections
if con.Process.ID == os.Getpid() {
packet.SetVerdict(netfilter.NF_ACCEPT)
stats.OnIgnored()
return
}
// search a match in preloaded rules
r := acceptOrDeny(&packet, con)
stats.OnConnectionEvent(con, r, r == nil)
}
func applyDefaultAction(packet *netfilter.Packet) {
if uiClient.DefaultAction() == rule.Allow {
packet.SetVerdict(netfilter.NF_ACCEPT)
} else {
if uiClient.DefaultDuration() == rule.Always {
packet.SetVerdictAndMark(netfilter.NF_DROP, firewall.DropMark)
} else {
packet.SetVerdict(netfilter.NF_DROP)
}
}
}
func acceptOrDeny(packet *netfilter.Packet, con *conman.Connection) *rule.Rule {
lock.Lock()
defer lock.Unlock()
connected := false
missed := false
r := rules.FindFirstMatch(con)
if r == nil {
missed = true
// no rule matched, send a request to the
// UI client if connected and running
r, connected = uiClient.Ask(con)
if r == nil {
log.Error("Invalid rule received, applying default action")
applyDefaultAction(packet)
return nil
}
if connected {
ok := false
pers := ""
@ -172,15 +233,7 @@ func onPacket(packet netfilter.Packet) {
}
// check if and how the rule needs to be saved
if r.Duration == rule.Restart {
pers = "Added"
// add to the rules but do not save to disk
if err := rules.Add(r, false); err != nil {
log.Error("Error while adding rule: %s", err)
} else {
ok = true
}
} else if r.Duration == rule.Always {
if r.Duration == rule.Always {
pers = "Saved"
// add to the loaded rules and persist on disk
if err := rules.Add(r, true); err != nil {
@ -188,6 +241,14 @@ func onPacket(packet netfilter.Packet) {
} else {
ok = true
}
} else {
pers = "Added"
// add to the rules but do not save to disk
if err := rules.Add(r, false); err != nil {
log.Error("Error while adding rule: %s", err)
} else {
ok = true
}
}
if ok {
@ -196,10 +257,15 @@ func onPacket(packet netfilter.Packet) {
}
}
stats.OnConnectionEvent(con, r, missed)
if r.Enabled == false {
applyDefaultAction(packet)
ruleName := log.Green(r.Name)
log.Info("DISABLED (%s) %s %s -> %s:%d (%s)", uiClient.DefaultAction(), log.Bold(log.Green("✔")), log.Bold(con.Process.Path), log.Bold(con.To()), con.DstPort, ruleName)
if r.Action == rule.Allow {
packet.SetVerdict(netfilter.NF_ACCEPT)
} else if r.Action == rule.Allow {
if packet != nil {
packet.SetVerdict(netfilter.NF_ACCEPT)
}
ruleName := log.Green(r.Name)
if r.Operator.Operand == rule.OpTrue {
@ -207,15 +273,24 @@ func onPacket(packet netfilter.Packet) {
}
log.Debug("%s %s -> %s:%d (%s)", log.Bold(log.Green("✔")), log.Bold(con.Process.Path), log.Bold(con.To()), con.DstPort, ruleName)
} else {
packet.SetVerdictAndMark(netfilter.NF_DROP, firewall.DropMark)
if packet != nil {
packet.SetVerdictAndMark(netfilter.NF_DROP, firewall.DropMark)
}
log.Warning("%s %s -> %s:%d (%s)", log.Bold(log.Red("✘")), log.Bold(con.Process.Path), log.Bold(con.To()), con.DstPort, log.Red(r.Name))
log.Debug("%s %s -> %s:%d (%s)", log.Bold(log.Red("✘")), log.Bold(con.Process.Path), log.Bold(con.To()), con.DstPort, log.Red(r.Name))
}
return r
}
func main() {
ctx, cancel = context.WithCancel(context.Background())
defer cancel()
flag.Parse()
// clean any possible residual firewall rule
firewall.CleanRules(false)
setupLogging()
if cpuProfile != "" {
@ -228,10 +303,6 @@ func main() {
log.Important("Starting %s v%s", core.Name, core.Version)
if err := procmon.Start(); err != nil {
log.Fatal("%s", err)
}
rulesPath, err := core.ExpandPath(rulesPath)
if err != nil {
log.Fatal("%s", err)
@ -251,26 +322,39 @@ func main() {
setupWorkers()
queue, err := netfilter.NewQueue(uint16(queueNum))
if err != nil {
log.Warning("Is opnensitchd already running?")
log.Fatal("Error while creating queue #%d: %s", queueNum, err)
}
pktChan = queue.Packets()
// queue is ready, run firewall rules
if err = firewall.QueueDNSResponses(true, queueNum); err != nil {
log.Fatal("Error while running DNS firewall rule: %s", err)
} else if err = firewall.QueueConnections(true, queueNum); err != nil {
log.Fatal("Error while running conntrack firewall rule: %s", err)
} else if err = firewall.DropMarked(true); err != nil {
log.Fatal("Error while running drop firewall rule: %s", err)
uiClient = ui.NewClient(uiSocket, stats, rules)
if overwriteLogging() {
setupLogging()
}
// overwrite monitor method from configuration if the user has passed
// the option via command line.
if procmonMethod != "" {
procmon.SetMonitorMethod(procmonMethod)
}
procmon.Init()
uiClient = ui.NewClient(uiSocket, stats)
// queue is ready, run firewall rules
firewall.Init(&queueNum)
log.Info("Running on netfilter queue #%d ...", queueNum)
for true {
for {
select {
case pkt := <-pktChan:
case <-ctx.Done():
goto Exit
case pkt, ok := <-pktChan:
if !ok {
goto Exit
}
wrkChan <- pkt
}
}
Exit:
close(wrkChan)
doCleanup(queue)
os.Exit(0)
}

View file

@ -6,6 +6,12 @@ import (
"github.com/google/gopacket"
)
// packet consts
const (
IPv4 = 4
)
// Verdict holds the action to perform on a packet (NF_DROP, NF_ACCEPT, etc)
type Verdict C.uint
type VerdictContainer struct {
@ -14,16 +20,22 @@ type VerdictContainer struct {
Packet []byte
}
// Packet holds the data of a network packet
type Packet struct {
Packet gopacket.Packet
Mark uint32
verdictChannel chan VerdictContainer
Packet gopacket.Packet
Mark uint32
verdictChannel chan VerdictContainer
UID uint32
NetworkProtocol uint8
}
// SetVerdict emits a veredict on a packet
func (p *Packet) SetVerdict(v Verdict) {
p.verdictChannel <- VerdictContainer{Verdict: v, Packet: nil, Mark: 0}
}
// SetVerdictAndMark emits a veredict on a packet and marks it in order to not
// analyze it again.
func (p *Packet) SetVerdictAndMark(v Verdict, mark uint32) {
p.verdictChannel <- VerdictContainer{Verdict: v, Packet: nil, Mark: mark}
}
@ -38,3 +50,8 @@ func (p *Packet) SetRequeueVerdict(newQueueId uint16) {
func (p *Packet) SetVerdictWithPacket(v Verdict, packet []byte) {
p.verdictChannel <- VerdictContainer{Verdict: v, Packet: packet, Mark: 0}
}
// IsIPv4 returns if the packet is IPv4
func (p *Packet) IsIPv4() bool {
return p.NetworkProtocol == IPv4
}

View file

@ -3,7 +3,7 @@ package netfilter
/*
#cgo pkg-config: libnetfilter_queue
#cgo CFLAGS: -Wall -I/usr/include
#cgo LDFLAGS: -L/usr/lib64/
#cgo LDFLAGS: -L/usr/lib64/ -ldl
#include "queue.h"
*/
@ -18,6 +18,7 @@ import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
const (
@ -38,12 +39,20 @@ const (
var (
queueIndex = make(map[uint32]*chan Packet, 0)
queueIndexLock = sync.RWMutex{}
exitChan = make(chan bool, 1)
gopacketDecodeOptions = gopacket.DecodeOptions{Lazy: true, NoCopy: true}
)
// VerdictContainerC is the struct that contains the mark, action, length and
// payload of a packet.
// It's defined in queue.h, and filled on go_callback()
type VerdictContainerC C.verdictContainer
// Queue holds the information of a netfilter queue.
// The handles of the connection to the kernel and the created queue.
// A channel where the intercepted packets will be received.
// The ID of the queue.
type Queue struct {
h *C.struct_nfq_handle
qh *C.struct_nfq_q_handle
@ -52,36 +61,25 @@ type Queue struct {
idx uint32
}
func NewQueue(queueId uint16) (q *Queue, err error) {
// NewQueue opens a new netfilter queue to receive packets marked with a mark.
func NewQueue(queueID uint16) (q *Queue, err error) {
q = &Queue{
idx: uint32(time.Now().UnixNano()),
packets: make(chan Packet),
}
if err = q.create(queueId); err != nil {
if err = q.create(queueID); err != nil {
return nil, err
} else if err = q.setup(); err != nil {
return nil, err
}
go q.run()
go q.run(exitChan)
return
return q, nil
}
func (q *Queue) destroy() {
if q.qh != nil {
C.nfq_destroy_queue(q.qh)
q.qh = nil
}
if q.h != nil {
C.nfq_close(q.h)
q.h = nil
}
}
func (q *Queue) create(queueId uint16) (err error) {
func (q *Queue) create(queueID uint16) (err error) {
var ret C.int
if q.h, err = C.nfq_open(); err != nil {
@ -94,7 +92,7 @@ func (q *Queue) create(queueId uint16) (err error) {
return fmt.Errorf("Error binding to AF_INET protocol family: %v", err)
} else if ret, err := C.nfq_bind_pf(q.h, AF_INET6); err != nil || ret < 0 {
return fmt.Errorf("Error binding to AF_INET6 protocol family: %v", err)
} 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 {
} 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 {
q.destroy()
return fmt.Errorf("Error binding to queue: %v", err)
}
@ -124,31 +122,71 @@ func (q *Queue) setup() (err error) {
return fmt.Errorf("Unable to get queue file-descriptor. %v", err)
} else if C.nfnl_rcvbufsiz(C.nfq_nfnlh(q.h), totSize) < 0 {
q.destroy()
return fmt.Errorf("Unable to increase netfilter buffer space size.")
return fmt.Errorf("Unable to increase netfilter buffer space size")
}
return nil
}
func (q *Queue) run(exitCh chan<- bool) {
if errno := C.Run(q.h, q.fd); errno != 0 {
fmt.Fprintf(os.Stderr, "Terminating, unable to receive packet due to errno=%d", errno)
}
exitChan <- true
}
// Close ensures that nfqueue resources are freed and closed.
// C.stop_reading_packets() stops the reading packets loop, which causes
// go-subroutine run() to exit.
// After exit, listening queue is destroyed and closed.
// If for some reason any of the steps stucks while closing it, we'll exit by timeout.
func (q *Queue) Close() {
close(q.packets)
C.stop_reading_packets()
q.destroy()
queueIndexLock.Lock()
delete(queueIndex, q.idx)
queueIndexLock.Unlock()
}
func (q *Queue) destroy() {
// we'll try to exit cleanly, but sometimes nfqueue gets stucked
time.AfterFunc(5*time.Second, func() {
log.Warning("queue stucked, closing by timeout")
if q != nil {
C.close(q.fd)
q.closeNfq()
}
os.Exit(0)
})
C.nfq_unbind_pf(q.h, AF_INET)
C.nfq_unbind_pf(q.h, AF_INET6)
if q.qh != nil {
if ret := C.nfq_destroy_queue(q.qh); ret != 0 {
log.Warning("Queue.destroy(), nfq_destroy_queue() not closed: %d", ret)
}
}
q.closeNfq()
}
func (q *Queue) closeNfq() {
if q.h != nil {
if ret := C.nfq_close(q.h); ret != 0 {
log.Warning("Queue.destroy(), nfq_close() not closed: %d", ret)
}
}
}
// Packets return the list of enqueued packets.
func (q *Queue) Packets() <-chan Packet {
return q.packets
}
func (q *Queue) run() {
if errno := C.Run(q.h, q.fd); errno != 0 {
fmt.Fprintf(os.Stderr, "Terminating, unable to receive packet due to errno=%d", errno)
}
}
// FYI: the export keyword is mandatory to specify that go_callback is defined elsewhere
//export go_callback
func go_callback(queueId C.int, data *C.uchar, length C.int, mark C.uint, idx uint32, vc *VerdictContainerC) {
func go_callback(queueID C.int, data *C.uchar, length C.int, mark C.uint, idx uint32, vc *VerdictContainerC, uid uint32) {
(*vc).verdict = C.uint(NF_ACCEPT)
(*vc).data = nil
(*vc).mark_set = 0
@ -164,18 +202,21 @@ func go_callback(queueId C.int, data *C.uchar, length C.int, mark C.uint, idx ui
xdata := C.GoBytes(unsafe.Pointer(data), length)
p := Packet{
verdictChannel: make(chan VerdictContainer),
Mark: uint32(mark),
UID: uid,
NetworkProtocol: xdata[0] >> 4, // first 4 bits is the version
}
var packet gopacket.Packet
if (xdata[0] >> 4) == 4 { // first 4 bits is the version
if p.IsIPv4() {
packet = gopacket.NewPacket(xdata, layers.LayerTypeIPv4, gopacketDecodeOptions)
} else {
packet = gopacket.NewPacket(xdata, layers.LayerTypeIPv6, gopacketDecodeOptions)
}
p := Packet{
verdictChannel: make(chan VerdictContainer),
Mark: uint32(mark),
Packet: packet,
}
p.Packet = packet
select {
case *queueChannel <- p:

View file

@ -7,6 +7,7 @@
#include <errno.h>
#include <math.h>
#include <unistd.h>
#include <dlfcn.h>
#include <netinet/in.h>
#include <linux/types.h>
#include <linux/socket.h>
@ -21,14 +22,44 @@ typedef struct {
unsigned char *data;
} verdictContainer;
extern void go_callback(int id, unsigned char* data, int len, uint mark, u_int32_t idx, verdictContainer *vc);
static void *get_uid = NULL;
extern void go_callback(int id, unsigned char* data, int len, uint mark, u_int32_t idx, verdictContainer *vc, uint32_t uid);
static uint8_t stop = 0;
static inline void configure_uid_if_available(struct nfq_q_handle *qh){
void *hndl = dlopen("libnetfilter_queue.so.1", RTLD_LAZY);
if (!hndl) {
hndl = dlopen("libnetfilter_queue.so", RTLD_LAZY);
if (!hndl){
printf("WARNING: libnetfilter_queue not available\n");
return;
}
}
if ((get_uid = dlsym(hndl, "nfq_get_uid")) == NULL){
printf("WARNING: nfq_get_uid not available\n");
return;
}
printf("OK: libnetfiler_queue supports nfq_get_uid\n");
#ifdef NFQA_CFG_F_UID_GID
if (qh != NULL && nfq_set_queue_flags(qh, NFQA_CFG_F_UID_GID, NFQA_CFG_F_UID_GID)){
printf("WARNING: UID not available on this kernel/libnetfilter_queue\n");
}
#endif
}
static int nf_callback(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfa, void *arg){
if (stop) {
return -1;
}
uint32_t id = -1, idx = 0, mark = 0;
struct nfqnl_msg_packet_hdr *ph = NULL;
unsigned char *buffer = NULL;
int size = 0;
verdictContainer vc = {0};
uint32_t uid = 0xffffffff;
mark = nfq_get_nfmark(nfa);
ph = nfq_get_msg_packet_hdr(nfa);
@ -36,17 +67,31 @@ static int nf_callback(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct n
size = nfq_get_payload(nfa, &buffer);
idx = (uint32_t)((uintptr_t)arg);
go_callback(id, buffer, size, mark, idx, &vc);
#ifdef NFQA_CFG_F_UID_GID
if (get_uid)
nfq_get_uid(nfa, &uid);
#endif
go_callback(id, buffer, size, mark, idx, &vc, uid);
if( vc.mark_set == 1 ) {
return nfq_set_verdict2(qh, id, vc.verdict, vc.mark, vc.length, vc.data);
} else {
return nfq_set_verdict(qh, id, vc.verdict, vc.length, vc.data);
}
return nfq_set_verdict2(qh, id, vc.verdict, vc.mark, vc.length, vc.data);
}
static inline struct nfq_q_handle* CreateQueue(struct nfq_handle *h, u_int16_t queue, u_int32_t idx) {
return nfq_create_queue(h, queue, &nf_callback, (void*)((uintptr_t)idx));
struct nfq_q_handle* qh = nfq_create_queue(h, queue, &nf_callback, (void*)((uintptr_t)idx));
if (qh == NULL){
printf("ERROR: nfq_create_queue() queue not created\n");
} else {
configure_uid_if_available(qh);
}
return qh;
}
static inline void stop_reading_packets() {
stop = 1;
}
static inline int Run(struct nfq_handle *h, int fd) {
@ -55,7 +100,10 @@ static inline int Run(struct nfq_handle *h, int fd) {
setsockopt(fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &opt, sizeof(int));
while ((rcvd = recv(fd, buf, sizeof(buf), 0)) && rcvd >= 0) {
while ((rcvd = recv(fd, buf, sizeof(buf), 0)) >= 0) {
if (stop == 1) {
return errno;
}
nfq_handle_packet(h, buf, rcvd);
}

118
daemon/netlink/socket.go Normal file
View file

@ -0,0 +1,118 @@
package netlink
import (
"fmt"
"net"
"strconv"
"syscall"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
// GetSocketInfo asks the kernel via netlink for a given connection.
// If the connection is found, we return the uid and the possible
// associated inodes.
// If the outgoing connection is not found but there're entries with the source
// port and same protocol, add all the inodes to the list.
//
// Some examples:
// outgoing connection as seen by netfilter || connection details dumped from kernel
//
// 47344:192.168.1.106 -> 151.101.65.140:443 || in kernel: 47344:192.168.1.106 -> 151.101.65.140:443
// 8612:192.168.1.5 -> 192.168.1.255:8612 || in kernel: 8612:192.168.1.105 -> 0.0.0.0:0
// 123:192.168.1.5 -> 217.144.138.234:123 || in kernel: 123:0.0.0.0 -> 0.0.0.0:0
// 45015:127.0.0.1 -> 239.255.255.250:1900 || in kernel: 45015:127.0.0.1 -> 0.0.0.0:0
// 50416:fe80::9fc2:ddcf:df22:aa50 -> fe80::1:53 || in kernel: 50416:254.128.0.0 -> 254.128.0.0:53
// 51413:192.168.1.106 -> 103.224.182.250:1337 || in kernel: 51413:0.0.0.0 -> 0.0.0.0:0
func GetSocketInfo(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint) (uid int, inodes []int) {
uid = -1
family := uint8(syscall.AF_INET)
ipproto := uint8(syscall.IPPROTO_TCP)
protoLen := len(proto)
if proto[protoLen-1:protoLen] == "6" {
family = syscall.AF_INET6
}
if proto[:3] == "udp" {
ipproto = syscall.IPPROTO_UDP
if protoLen >= 7 && proto[:7] == "udplite" {
ipproto = syscall.IPPROTO_UDPLITE
}
}
if sockList, err := SocketGet(family, ipproto, uint16(srcPort), uint16(dstPort), srcIP, dstIP); err == nil {
for n, sock := range sockList {
if sock.UID != 0xffffffff {
uid = int(sock.UID)
}
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 ",
n, len(sockList),
srcPort, srcIP, dstIP, dstPort,
sock.ID.SourcePort, sock.ID.Source,
sock.ID.Destination, sock.ID.DestinationPort, sock.INode,
sock.ID.Destination.IsLoopback(),
sock.ID.Destination.IsMulticast(),
sock.ID.Destination.IsUnspecified(),
sock.ID.Destination.IsLinkLocalUnicast(),
sock.ID.Destination.IsLinkLocalMulticast(),
sock.ID.Destination.IsGlobalUnicast(),
)
if sock.ID.SourcePort == uint16(srcPort) && sock.ID.Source.Equal(srcIP) &&
(sock.ID.DestinationPort == uint16(dstPort)) &&
((sock.ID.Destination.IsGlobalUnicast() || sock.ID.Destination.IsLoopback()) && sock.ID.Destination.Equal(dstIP)) {
inodes = append([]int{int(sock.INode)}, inodes...)
continue
} else if sock.ID.SourcePort == uint16(srcPort) && sock.ID.Source.Equal(srcIP) &&
(sock.ID.DestinationPort == uint16(dstPort)) {
inodes = append([]int{int(sock.INode)}, inodes...)
continue
}
log.Debug("GetSocketInfo() invalid: %d:%v -> %v:%d", sock.ID.SourcePort, sock.ID.Source, sock.ID.Destination, sock.ID.DestinationPort)
}
if len(inodes) == 0 && len(sockList) > 0 {
for n, sock := range sockList {
inodes = append([]int{int(sock.INode)}, inodes...)
log.Debug("netlink socket not found, adding entry: %d:%v -> %v:%d || %d:%v -> %v:%d inode: %d state: %s",
srcPort, srcIP, dstIP, dstPort,
sockList[n].ID.SourcePort, sockList[n].ID.Source,
sockList[n].ID.Destination, sockList[n].ID.DestinationPort,
sockList[n].INode, TCPStatesMap[sock.State])
}
}
} else {
log.Debug("netlink socket error: %v - %d:%v -> %v:%d", err, srcPort, srcIP, dstIP, dstPort)
}
return uid, inodes
}
// GetSocketInfoByInode dumps the kernel sockets table and searchs the given
// inode on it.
func GetSocketInfoByInode(inodeStr string) (*Socket, error) {
inode, err := strconv.ParseUint(inodeStr, 10, 32)
if err != nil {
return nil, err
}
type inetStruct struct{ family, proto uint8 }
socketTypes := []inetStruct{
{syscall.AF_INET, syscall.IPPROTO_TCP},
{syscall.AF_INET, syscall.IPPROTO_UDP},
{syscall.AF_INET6, syscall.IPPROTO_TCP},
{syscall.AF_INET6, syscall.IPPROTO_UDP},
}
for _, socket := range socketTypes {
socketList, err := SocketsDump(socket.family, socket.proto)
if err != nil {
return nil, err
}
for idx := range socketList {
if uint32(inode) == socketList[idx].INode {
return socketList[idx], nil
}
}
}
return nil, fmt.Errorf("Inode not found")
}

View file

@ -0,0 +1,245 @@
package netlink
import (
"encoding/binary"
"errors"
"fmt"
"net"
"syscall"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/vishvananda/netlink/nl"
)
// This is a modification of https://github.com/vishvananda/netlink socket_linux.go - Apache2.0 license
// which adds support for query UDP, UDPLITE and IPv6 sockets to SocketGet()
const (
sizeofSocketID = 0x30
sizeofSocketRequest = sizeofSocketID + 0x8
sizeofSocket = sizeofSocketID + 0x18
)
var (
native = nl.NativeEndian()
networkOrder = binary.BigEndian
TCP_ALL = uint32(0xfff)
)
// https://elixir.bootlin.com/linux/latest/source/include/net/tcp_states.h
const (
TCP_INVALID = iota
TCP_ESTABLISHED
TCP_SYN_SENT
TCP_SYN_RECV
TCP_FIN_WAIT1
TCP_FIN_WAIT2
TCP_TIME_WAIT
TCP_CLOSE
TCP_CLOSE_WAIT
TCP_LAST_ACK
TCP_LISTEN
TCP_CLOSING
TCP_NEW_SYN_REC
TCP_MAX_STATES
)
// TCPStatesMap holds the list of TCP states
var TCPStatesMap = map[uint8]string{
TCP_INVALID: "invalid",
TCP_ESTABLISHED: "established",
TCP_SYN_SENT: "syn_sent",
TCP_SYN_RECV: "syn_recv",
TCP_FIN_WAIT1: "fin_wait1",
TCP_FIN_WAIT2: "fin_wait2",
TCP_TIME_WAIT: "time_wait",
TCP_CLOSE: "close",
TCP_CLOSE_WAIT: "close_wait",
TCP_LAST_ACK: "last_ack",
TCP_LISTEN: "listen",
TCP_CLOSING: "closing",
}
// SocketID holds the socket information of a request/response to the kernel
type SocketID struct {
SourcePort uint16
DestinationPort uint16
Source net.IP
Destination net.IP
Interface uint32
Cookie [2]uint32
}
// Socket represents a netlink socket.
type Socket struct {
Family uint8
State uint8
Timer uint8
Retrans uint8
ID SocketID
Expires uint32
RQueue uint32
WQueue uint32
UID uint32
INode uint32
}
// SocketRequest holds the request/response of a connection to the kernel
type SocketRequest struct {
Family uint8
Protocol uint8
Ext uint8
pad uint8
States uint32
ID SocketID
}
type writeBuffer struct {
Bytes []byte
pos int
}
func (b *writeBuffer) Write(c byte) {
b.Bytes[b.pos] = c
b.pos++
}
func (b *writeBuffer) Next(n int) []byte {
s := b.Bytes[b.pos : b.pos+n]
b.pos += n
return s
}
// Serialize convert SocketRequest struct to bytes.
func (r *SocketRequest) Serialize() []byte {
b := writeBuffer{Bytes: make([]byte, sizeofSocketRequest)}
b.Write(r.Family)
b.Write(r.Protocol)
b.Write(r.Ext)
b.Write(r.pad)
native.PutUint32(b.Next(4), r.States)
networkOrder.PutUint16(b.Next(2), r.ID.SourcePort)
networkOrder.PutUint16(b.Next(2), r.ID.DestinationPort)
if r.Family == syscall.AF_INET6 {
copy(b.Next(16), r.ID.Source)
copy(b.Next(16), r.ID.Destination)
} else {
copy(b.Next(4), r.ID.Source.To4())
b.Next(12)
copy(b.Next(4), r.ID.Destination.To4())
b.Next(12)
}
native.PutUint32(b.Next(4), r.ID.Interface)
native.PutUint32(b.Next(4), r.ID.Cookie[0])
native.PutUint32(b.Next(4), r.ID.Cookie[1])
return b.Bytes
}
// Len returns the size of a socket request
func (r *SocketRequest) Len() int { return sizeofSocketRequest }
type readBuffer struct {
Bytes []byte
pos int
}
func (b *readBuffer) Read() byte {
c := b.Bytes[b.pos]
b.pos++
return c
}
func (b *readBuffer) Next(n int) []byte {
s := b.Bytes[b.pos : b.pos+n]
b.pos += n
return s
}
func (s *Socket) deserialize(b []byte) error {
if len(b) < sizeofSocket {
return fmt.Errorf("socket data short read (%d); want %d", len(b), sizeofSocket)
}
rb := readBuffer{Bytes: b}
s.Family = rb.Read()
s.State = rb.Read()
s.Timer = rb.Read()
s.Retrans = rb.Read()
s.ID.SourcePort = networkOrder.Uint16(rb.Next(2))
s.ID.DestinationPort = networkOrder.Uint16(rb.Next(2))
if s.Family == syscall.AF_INET6 {
s.ID.Source = net.IP(rb.Next(16))
s.ID.Destination = net.IP(rb.Next(16))
} else {
s.ID.Source = net.IPv4(rb.Read(), rb.Read(), rb.Read(), rb.Read())
rb.Next(12)
s.ID.Destination = net.IPv4(rb.Read(), rb.Read(), rb.Read(), rb.Read())
rb.Next(12)
}
s.ID.Interface = native.Uint32(rb.Next(4))
s.ID.Cookie[0] = native.Uint32(rb.Next(4))
s.ID.Cookie[1] = native.Uint32(rb.Next(4))
s.Expires = native.Uint32(rb.Next(4))
s.RQueue = native.Uint32(rb.Next(4))
s.WQueue = native.Uint32(rb.Next(4))
s.UID = native.Uint32(rb.Next(4))
s.INode = native.Uint32(rb.Next(4))
return nil
}
// SocketGet returns the list of active connections in the kernel
// filtered by several fields. Currently it returns connections
// filtered by source port and protocol.
func SocketGet(family uint8, proto uint8, srcPort, dstPort uint16, local, remote net.IP) ([]*Socket, error) {
_Id := SocketID{
SourcePort: srcPort,
Cookie: [2]uint32{nl.TCPDIAG_NOCOOKIE, nl.TCPDIAG_NOCOOKIE},
}
sockReq := &SocketRequest{
Family: family,
Protocol: proto,
States: TCP_ALL,
ID: _Id,
}
return netlinkRequest(sockReq, family, proto, srcPort, dstPort, local, remote)
}
// SocketsDump returns the list of all connections from the kernel
func SocketsDump(family uint8, proto uint8) ([]*Socket, error) {
sockReq := &SocketRequest{
Family: family,
Protocol: proto,
States: TCP_ALL,
}
return netlinkRequest(sockReq, 0, 0, 0, 0, nil, nil)
}
func netlinkRequest(sockReq *SocketRequest, family uint8, proto uint8, srcPort, dstPort uint16, local, remote net.IP) ([]*Socket, error) {
req := nl.NewNetlinkRequest(nl.SOCK_DIAG_BY_FAMILY, syscall.NLM_F_DUMP)
req.AddData(sockReq)
msgs, err := req.Execute(syscall.NETLINK_INET_DIAG, 0)
if err != nil {
return nil, err
}
if len(msgs) == 0 {
return nil, errors.New("Warning, no message nor error from netlink")
}
var sock []*Socket
for n, m := range msgs {
s := &Socket{}
if err = s.deserialize(m); err != nil {
log.Error("[%d] netlink socket error: %s, %d:%v -> %v:%d - %d:%v -> %v:%d",
n, TCPStatesMap[s.State],
srcPort, local, remote, dstPort,
s.ID.SourcePort, s.ID.Source, s.ID.Destination, s.ID.DestinationPort)
continue
}
if s.INode == 0 {
continue
}
sock = append([]*Socket{s}, sock...)
}
return sock, err
}

View file

@ -4,6 +4,10 @@ import (
"net"
)
// Entry holds the information of a /proc/net/* entry.
// For example, /proc/net/tcp:
// sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
// 0: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 18083222
type Entry struct {
Proto string
SrcIP net.IP
@ -14,6 +18,7 @@ type Entry struct {
INode int
}
// NewEntry creates a new entry with values from /proc/net/
func NewEntry(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint, userId int, iNode int) Entry {
return Entry{
Proto: proto,

View file

@ -3,23 +3,35 @@ package netstat
import (
"net"
"strings"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
// FindEntry looks for the connection in the list of known connections in ProcFS.
func FindEntry(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint) *Entry {
if entry := findEntryForProtocol(proto, srcIP, srcPort, dstIP, dstPort); entry != nil {
return entry
}
ipv6Suffix := "6"
if strings.HasSuffix(proto, ipv6Suffix) == false {
if core.IPv6Enabled && strings.HasSuffix(proto, ipv6Suffix) == false {
otherProto := proto + ipv6Suffix
log.Debug("Searching for %s netstat entry instead of %s", otherProto, proto)
return findEntryForProtocol(otherProto, srcIP, srcPort, dstIP, dstPort)
if entry := findEntryForProtocol(otherProto, srcIP, srcPort, dstIP, dstPort); entry != nil {
return entry
}
}
return &Entry{
Proto: proto,
SrcIP: srcIP,
SrcPort: srcPort,
DstIP: dstIP,
DstPort: dstPort,
UserId: -1,
INode: -1,
}
return nil;
}
func findEntryForProtocol(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint) *Entry {

View file

@ -9,8 +9,8 @@ import (
"regexp"
"strconv"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
var (
@ -44,7 +44,6 @@ func hexToInt(h string) uint {
return uint(d)
}
func hexToInt2(h string) (uint, uint) {
if len(h) > 16 {
d, err := strconv.ParseUint(h[:16], 16, 64)
@ -56,13 +55,13 @@ func hexToInt2(h string) (uint, uint) {
log.Fatal("Error while parsing %s to int: %s", h[16:], err)
}
return uint(d), uint(d2)
} else {
d, err := strconv.ParseUint(h, 16, 64)
if err != nil {
log.Fatal("Error while parsing %s to int: %s", h[16:], err)
}
return uint(d), 0
}
d, err := strconv.ParseUint(h, 16, 64)
if err != nil {
log.Fatal("Error while parsing %s to int: %s", h[16:], err)
}
return uint(d), 0
}
func hexToIP(h string) net.IP {
@ -71,9 +70,9 @@ func hexToIP(h string) net.IP {
if m != 0 {
ip = make(net.IP, 16)
// TODO: Check if this depends on machine endianness?
binary.LittleEndian.PutUint32(ip, uint32(n >> 32))
binary.LittleEndian.PutUint32(ip, uint32(n>>32))
binary.LittleEndian.PutUint32(ip[4:], uint32(n))
binary.LittleEndian.PutUint32(ip[8:], uint32(m >> 32))
binary.LittleEndian.PutUint32(ip[8:], uint32(m>>32))
binary.LittleEndian.PutUint32(ip[12:], uint32(m))
} else {
ip = make(net.IP, 4)
@ -82,6 +81,7 @@ func hexToIP(h string) net.IP {
return ip
}
// Parse scans and retrieves the opened connections, from /proc/net/ files
func Parse(proto string) ([]Entry, error) {
filename := fmt.Sprintf("/proc/net/%s", proto)
fd, err := os.Open(filename)

91
daemon/opensnitch.spec Normal file
View file

@ -0,0 +1,91 @@
Name: opensnitch
Version: 1.3.0rc2
Release: 1%{?dist}
Summary: OpenSnitch is a GNU/Linux application firewall
License: GPLv3+
URL: https://github.com/gustavo-iniguez-goya/%{name}
Source0: https://github.com/gustavo-iniguez-goya/%{name}/releases/download/v%{version}/%{name}_%{version}.orig.tar.gz
#BuildArch: x86_64
#BuildRequires: godep
Requires(post): info
Requires(preun): info
%description
Whenever a program makes a connection, it'll prompt the user to allow or deny
it.
The user can decide if block the outgoing connection based on properties of
the connection: by port, by uid, by dst ip, by program or a combination
of them.
These rules can last forever, until the app restart or just one time.
The GUI allows the user to view live outgoing connections, as well as search
by process, user, host or port.
%prep
rm -rf %{buildroot}
%setup
%build
mkdir -p go/src/github.com/gustavo-iniguez-goya
ln -s $(pwd) go/src/github.com/gustavo-iniguez-goya/opensnitch
export GOPATH=$(pwd)/go
cd go/src/github.com/gustavo-iniguez-goya/opensnitch/daemon/
go build -o opensnitchd .
%install
mkdir -p %{buildroot}/usr/bin/ %{buildroot}/usr/lib/systemd/system/ %{buildroot}/etc/opensnitchd/rules %{buildroot}/etc/logrotate.d
sed -i 's/\/usr\/local/\/usr/' daemon/opensnitchd.service
install -m 755 daemon/opensnitchd %{buildroot}/usr/bin/opensnitchd
install -m 644 daemon/opensnitchd.service %{buildroot}/usr/lib/systemd/system/opensnitch.service
install -m 644 debian/opensnitch.logrotate %{buildroot}/etc/logrotate.d/opensnitch
B=""
if [ -f /etc/opensnitchd/default-config.json ]; then
B="-b"
fi
install -m 644 -b $B daemon/default-config.json %{buildroot}/etc/opensnitchd/default-config.json
B=""
if [ -f /etc/opensnitchd/system-fw.json ]; then
B="-b"
fi
install -m 644 -b $B daemon/system-fw.json %{buildroot}/etc/opensnitchd/system-fw.json
# upgrade, uninstall
%preun
systemctl stop opensnitch.service || true
%post
if [ $1 -eq 1 ]; then
systemctl enable opensnitch.service
fi
systemctl start opensnitch.service
# uninstall,upgrade
%postun
if [ $1 -eq 0 ]; then
systemctl disable opensnitch.service
fi
if [ $1 -eq 0 -a -f /etc/logrotate.d/opensnitch ]; then
rm /etc/logrotate.d/opensnitch
fi
# postun is the last step after reinstalling
if [ $1 -eq 1 ]; then
systemctl start opensnitch.service
fi
%clean
rm -rf %{buildroot}
%files
%{_bindir}/opensnitchd
/usr/lib/systemd/system/opensnitch.service
%{_sysconfdir}/opensnitchd/default-config.json
%{_sysconfdir}/opensnitchd/system-fw.json
%{_sysconfdir}/logrotate.d/opensnitch

View file

@ -1,6 +1,6 @@
[Unit]
Description=OpenSnitch is a GNU/Linux port of the Little Snitch application firewall.
Documentation=https://github.com/evilsocket/opensnitch
Documentation=https://github.com/gustavo-iniguez-goya/opensnitch/wiki
Wants=network.target
After=network.target
@ -8,7 +8,7 @@ After=network.target
Type=simple
PermissionsStartOnly=true
ExecStartPre=/bin/mkdir -p /etc/opensnitchd/rules
ExecStart=/usr/local/bin/opensnitchd -log-file /var/log/opensnitchd.log -rules-path /etc/opensnitchd/rules -ui-socket unix:///tmp/osui.sock
ExecStart=/usr/local/bin/opensnitchd -rules-path /etc/opensnitchd/rules
Restart=always
RestartSec=30

View file

@ -0,0 +1,345 @@
// Package audit reads auditd events from the builtin af_unix plugin, and parses
// the messages in order to proactively monitor pids which make connections.
// Once a connection is made and redirected to us via NFQUEUE, we
// lookup the connection inode in /proc, and add the corresponding PID with all
// the information of the process to a list of known PIDs.
//
// TODO: Prompt the user to allow/deny a connection/program as soon as it's
// started.
//
// Requisities:
// - install auditd and audispd-plugins
// - enable af_unix plugin /etc/audisp/plugins.d/af_unix.conf (active = yes)
// - auditctl -a always,exit -F arch=b64 -S socket,connect,execve -k opensnitchd
// - increase /etc/audisp/audispd.conf q_depth if there're dropped events
// - set write_logs to no if you don't need/want audit logs to be stored in the disk.
//
// read messages from the pipe to verify that it's working:
// socat unix-connect:/var/run/audispd_events stdio
//
// Audit event fields:
// https://github.com/linux-audit/audit-documentation/blob/master/specs/fields/field-dictionary.csv
// Record types:
// https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Security_Guide/sec-Audit_Record_Types.html
//
// Documentation:
// https://github.com/linux-audit/audit-documentation
package audit
import (
"bufio"
"fmt"
"io"
"net"
"os"
"runtime"
"sort"
"sync"
"time"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
// Event represents an audit event, which in our case can be an event of type
// socket, execve, socketpair or connect.
type Event struct {
Timestamp string // audit(xxxxxxx:nnnn)
Serial string
ProcName string // comm
ProcPath string // exe
ProcCmdLine string // proctitle
ProcDir string // cwd
ProcMode string // mode
TTY string
Pid int
UID int
Gid int
PPid int
EUid int
EGid int
OUid int
OGid int
UserName string // auid
DstHost net.IP
DstPort int
NetFamily string // inet, inet6, local
Success string
INode int
Dev string
Syscall int
Exit int
EventType string
RawEvent string
LastSeen time.Time
}
// MaxEventAge is the maximum minutes an audit process can live without network activity.
const (
MaxEventAge = int(10)
)
var (
// Lock holds a mutex
Lock sync.RWMutex
ourPid = os.Getpid()
// cache of events
events []*Event
eventsCleaner *time.Ticker
eventsCleanerChan = make(chan bool)
// TODO: EventChan is an output channel where incoming auditd events will be written.
// If a client opens it.
EventChan = (chan Event)(nil)
eventsExitChan = make(chan bool)
auditConn net.Conn
// TODO: we may need arm arch
rule64 = []string{"exit,always", "-F", "arch=b64", "-F", fmt.Sprint("ppid!=", ourPid), "-F", fmt.Sprint("pid!=", ourPid), "-S", "socket,connect", "-k", "opensnitch"}
rule32 = []string{"exit,always", "-F", "arch=b32", "-F", fmt.Sprint("ppid!=", ourPid), "-F", fmt.Sprint("pid!=", ourPid), "-S", "socketcall", "-F", "a0=1", "-k", "opensnitch"}
audispdPath = "/var/run/audispd_events"
)
// OPENSNITCH_RULES_KEY is the mark we place on every event we are interested in.
const (
OpensnitchRulesKey = "key=\"opensnitch\""
)
// GetEvents returns the list of processes which have opened a connection.
func GetEvents() []*Event {
return events
}
// GetEventByPid returns an event given a pid.
func GetEventByPid(pid int) *Event {
Lock.RLock()
defer Lock.RUnlock()
for _, event := range events {
if pid == event.Pid {
return event
}
}
return nil
}
// sortEvents sorts received events by time and elapsed time since latest network activity.
// newest PIDs will be placed on top of the list.
func sortEvents() {
sort.Slice(events, func(i, j int) bool {
now := time.Now()
elapsedTimeT := now.Sub(events[i].LastSeen)
elapsedTimeU := now.Sub(events[j].LastSeen)
t := events[i].LastSeen.UnixNano()
u := events[j].LastSeen.UnixNano()
return t > u && elapsedTimeT < elapsedTimeU
})
}
// cleanOldEvents deletes the PIDs which do not exist or that are too old to
// live.
// We start searching from the oldest to the newest.
// If the last network activity of a PID has been greater than MaxEventAge,
// then it'll be deleted.
func cleanOldEvents() {
Lock.Lock()
defer Lock.Unlock()
for n := len(events) - 1; n >= 0; n-- {
now := time.Now()
elapsedTime := now.Sub(events[n].LastSeen)
if int(elapsedTime.Minutes()) >= MaxEventAge {
events = append(events[:n], events[n+1:]...)
continue
}
if core.Exists(fmt.Sprint("/proc/", events[n].Pid)) == false {
events = append(events[:n], events[n+1:]...)
}
}
}
func deleteEvent(pid int) {
for n := range events {
if events[n].Pid == pid || events[n].PPid == pid {
deleteEventByIndex(n)
break
}
}
}
func deleteEventByIndex(index int) {
Lock.Lock()
events = append(events[:index], events[index+1:]...)
Lock.Unlock()
}
// AddEvent adds new event to the list of PIDs which have generate network
// activity.
// If the PID is already in the list, the LastSeen field is updated, to keep
// it alive.
func AddEvent(aevent *Event) {
if aevent == nil {
return
}
Lock.Lock()
defer Lock.Unlock()
for n := 0; n < len(events); n++ {
if events[n].Pid == aevent.Pid && events[n].Syscall == aevent.Syscall {
if aevent.ProcCmdLine != "" || (aevent.ProcCmdLine == events[n].ProcCmdLine) {
events[n] = aevent
}
events[n].LastSeen = time.Now()
sortEvents()
return
}
}
aevent.LastSeen = time.Now()
events = append([]*Event{aevent}, events...)
}
// startEventsCleaner will review if the events in the cache need to be cleaned
// every 5 minutes.
func startEventsCleaner() {
for {
select {
case <-eventsCleanerChan:
goto Exit
case <-eventsCleaner.C:
cleanOldEvents()
}
}
Exit:
log.Debug("audit: cleanerRoutine stopped")
}
func addRules() bool {
r64 := append([]string{"-A"}, rule64...)
r32 := append([]string{"-A"}, rule32...)
_, err64 := core.Exec("auditctl", r64)
_, err32 := core.Exec("auditctl", r32)
if err64 == nil && err32 == nil {
return true
}
log.Error("Error adding audit rule, err32=%v, err=%v", err32, err64)
return false
}
func configureSyscalls() {
// XXX: what about a i386 process running on a x86_64 system?
if runtime.GOARCH == "386" {
syscallSOCKET = "1"
syscallCONNECT = "3"
syscallSOCKETPAIR = "8"
}
}
func deleteRules() bool {
r64 := []string{"-D", "-k", "opensnitch"}
r32 := []string{"-D", "-k", "opensnitch"}
_, err64 := core.Exec("auditctl", r64)
_, err32 := core.Exec("auditctl", r32)
if err64 == nil && err32 == nil {
return true
}
log.Error("Error deleting audit rules, err32=%v, err64=%v", err32, err64)
return false
}
func checkRules() bool {
// TODO
return true
}
func checkStatus() bool {
// TODO
return true
}
// Reader reads events from audisd af_unix pipe plugin.
// If the auditd daemon is stopped or restarted, the reader handle
// is closed, so we need to restablished the connection.
func Reader(r io.Reader, eventChan chan<- Event) {
if r == nil {
log.Error("Error reading auditd events. Is auditd running? is af_unix plugin enabled?")
return
}
reader := bufio.NewReader(r)
go startEventsCleaner()
for {
select {
case <-eventsExitChan:
goto Exit
default:
buf, _, err := reader.ReadLine()
if err != nil {
if err == io.EOF {
log.Error("AuditReader: auditd stopped, reconnecting in 30s", err)
if newReader, err := reconnect(); err == nil {
reader = bufio.NewReader(newReader)
log.Important("Auditd reconnected, continue reading")
}
continue
}
log.Warning("AuditReader: auditd error", err)
break
}
parseEvent(string(buf[0:len(buf)]), eventChan)
}
}
Exit:
log.Debug("audit.Reader() closed")
}
// StartChannel creates a channel to receive events from Audit.
// Launch audit.Reader() in a goroutine:
// go audit.Reader(c, (chan<- audit.Event)(audit.EventChan))
func StartChannel() {
EventChan = make(chan Event, 0)
}
func reconnect() (net.Conn, error) {
deleteRules()
time.Sleep(30 * time.Second)
return connect()
}
func connect() (net.Conn, error) {
addRules()
// TODO: make the unix socket path configurable
return net.Dial("unix", audispdPath)
}
// Stop stops listening for events from auditd and delete the auditd rules.
func Stop() {
eventsExitChan <- true
eventsCleanerChan <- true
eventsCleaner.Stop()
if auditConn != nil {
if err := auditConn.Close(); err != nil {
log.Warning("audit.Stop() error closing socket: %v", err)
}
}
deleteRules()
if EventChan != nil {
close(EventChan)
}
}
// Start makes a new connection to the audisp af_unix socket.
func Start() (net.Conn, error) {
auditConn, err := connect()
if err != nil {
log.Error("auditd Start() connection error %v", err)
deleteRules()
return nil, err
}
configureSyscalls()
eventsCleaner = time.NewTicker(time.Minute * 5)
return auditConn, err
}

View file

@ -0,0 +1,298 @@
package audit
import (
"encoding/hex"
"fmt"
"net"
"regexp"
"strconv"
"strings"
)
var (
newEvent = false
netEvent = &Event{}
// RegExp for parse audit messages
// https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/security_guide/sec-understanding_audit_log_files
auditRE, _ = regexp.Compile(`([a-zA-Z0-9\-_]+)=([a-zA-Z0-9:'\-\/\"\.\,_\(\)]+)`)
rawEvent = make(map[string]string)
)
// amd64 syscalls definition
// if the platform is not amd64, it's redefined on Start()
var (
syscallSOCKET = "41"
syscallCONNECT = "42"
syscallSOCKETPAIR = "53"
syscallEXECVE = "59"
syscallSOCKETCALL = "102"
)
// /usr/include/x86_64-linux-gnu/bits/socket_type.h
const (
sockSTREAM = "1"
sockDGRAM = "2"
sockRAW = "3"
sockSEQPACKET = "5"
sockPACKET = "10"
// /usr/include/x86_64-linux-gnu/bits/socket.h
pfUNSPEC = "0"
pfLOCAL = "1" // PF_UNIX
pfINET = "2"
pfINET6 = "10"
// /etc/protocols
protoIP = "0"
protoTCP = "6"
protoUDP = "17"
)
// https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Security_Guide/sec-Audit_Record_Types.html
const (
AuditTypePROCTITLE = "type=PROCTITLE"
AuditTypeCWD = "type=CWD"
AuditTypePATH = "type=PATH"
AuditTypeEXECVE = "type=EXECVE"
AuditTypeSOCKADDR = "type=SOCKADDR"
AuditTypeSOCKETCALL = "type=SOCKETCALL"
AuditTypeEOE = "type=EOE"
)
var (
syscallSOCKETstr = fmt.Sprint("syscall=", syscallSOCKET)
syscallCONNECTstr = fmt.Sprint("syscall=", syscallCONNECT)
syscallSOCKETPAIRstr = fmt.Sprint("syscall=", syscallSOCKETPAIR)
syscallEXECVEstr = fmt.Sprint("syscall=", syscallEXECVE)
syscallSOCKETCALLstr = fmt.Sprint("syscall=", syscallSOCKETCALL)
)
// parseNetLine parses a SOCKADDR message type of the form:
// saddr string: inet6 host:2001:4860:4860::8888 serv:53
func parseNetLine(line string, decode bool) (family string, dstHost net.IP, dstPort int) {
// 0:4 - type
// 4:8 - port
// 8:16 - ip
switch family := line[0:4]; family {
// local
// case "0100":
// ipv4
case "0200":
octet2 := decodeString(line[4:8])
octet := decodeString(line[8:16])
host := fmt.Sprint(octet[0], ".", octet[1], ".", octet[2], ".", octet[3])
fmt.Printf("dest ip: %s -- %s:%s\n", line[4:8], octet2, host)
// ipv6
//case "0A00":
}
if decode == true {
line = decodeString(line)
}
pieces := strings.Split(line, " ")
family = pieces[0]
if family[:4] != "inet" {
return family, dstHost, 0
}
if len(pieces) > 1 && pieces[1][:5] == "host:" {
dstHost = net.ParseIP(strings.Split(pieces[1], "host:")[1])
}
if len(pieces) > 2 && pieces[2][:5] == "serv:" {
_dstPort, err := strconv.Atoi(strings.Split(line, "serv:")[1])
if err != nil {
dstPort = -1
} else {
dstPort = _dstPort
}
}
return family, dstHost, dstPort
}
// decodeString will try to decode a string encoded in hexadecimal.
// If the string can not be decoded, the original string will be returned.
// In that case, usually it means that it's a non-encoded string.
func decodeString(s string) string {
decoded, err := hex.DecodeString(s)
if err != nil {
return s
}
return fmt.Sprintf("%s", decoded)
}
// extractFields parsed an audit raw message, and extracts all the fields.
func extractFields(rawMessage string, newEvent *map[string]string) {
Lock.Lock()
defer Lock.Unlock()
if auditRE == nil {
newEvent = nil
return
}
fieldList := auditRE.FindAllStringSubmatch(rawMessage, -1)
if fieldList == nil {
newEvent = nil
return
}
for _, field := range fieldList {
(*newEvent)[field[1]] = field[2]
}
}
// populateEvent populates our Event from a raw parsed message.
func populateEvent(aevent *Event, eventFields *map[string]string) *Event {
if aevent == nil {
return nil
}
Lock.Lock()
defer Lock.Unlock()
for k, v := range *eventFields {
switch k {
//case "a0":
//case "a1":
//case "a2":
case "fam":
if v == "local" {
return nil
}
aevent.NetFamily = v
case "lport":
aevent.DstPort, _ = strconv.Atoi(v)
// TODO
/*case "addr":
fmt.Println("addr: ", v)
case "daddr":
fmt.Println("daddr: ", v)
case "laddr":
aevent.DstHost = net.ParseIP(v)
case "saddr":
parseNetLine(v, true)
fmt.Println("saddr:", v)
*/
case "exe":
aevent.ProcPath = strings.Trim(decodeString(v), "\"")
case "comm":
aevent.ProcName = strings.Trim(decodeString(v), "\"")
// proctitle may be truncated to 128 characters, so don't rely on it, parse /proc/<pid>/instead
//case "proctitle":
// aevent.ProcCmdLine = strings.Trim(decodeString(v), "\"")
case "tty":
aevent.TTY = v
case "pid":
aevent.Pid, _ = strconv.Atoi(v)
case "ppid":
aevent.PPid, _ = strconv.Atoi(v)
case "uid":
aevent.UID, _ = strconv.Atoi(v)
case "gid":
aevent.Gid, _ = strconv.Atoi(v)
case "success":
aevent.Success = v
case "cwd":
aevent.ProcDir = strings.Trim(decodeString(v), "\"")
case "inode":
aevent.INode, _ = strconv.Atoi(v)
case "dev":
aevent.Dev = v
case "mode":
aevent.ProcMode = v
case "ouid":
aevent.OUid, _ = strconv.Atoi(v)
case "ogid":
aevent.OGid, _ = strconv.Atoi(v)
case "syscall":
aevent.Syscall, _ = strconv.Atoi(v)
case "exit":
aevent.Exit, _ = strconv.Atoi(v)
case "type":
aevent.EventType = v
case "msg":
parts := strings.Split(v[6:], ":")
aevent.Timestamp = parts[0]
aevent.Serial = parts[1][:len(parts[1])-1]
}
}
return aevent
}
// parseEvent parses an auditd event, discards the unwanted ones, and adds
// the ones we're interested in to an array.
// We're only interested in the socket,socketpair,connect and execve syscalls.
// Events from us are excluded.
//
// When we received an event, we parse and add it to the list as soon as we can.
// If the next messages of the set have additional information, we update the
// event.
func parseEvent(rawMessage string, eventChan chan<- Event) {
if newEvent == false && strings.Index(rawMessage, OpensnitchRulesKey) == -1 {
return
}
aEvent := make(map[string]string)
if strings.Index(rawMessage, syscallSOCKETstr) != -1 ||
strings.Index(rawMessage, syscallCONNECTstr) != -1 ||
strings.Index(rawMessage, syscallSOCKETPAIRstr) != -1 ||
strings.Index(rawMessage, syscallEXECVEstr) != -1 ||
strings.Index(rawMessage, syscallSOCKETCALLstr) != -1 {
extractFields(rawMessage, &aEvent)
if aEvent == nil {
return
}
newEvent = true
netEvent = &Event{}
netEvent = populateEvent(netEvent, &aEvent)
AddEvent(netEvent)
} else if newEvent == true && strings.Index(rawMessage, AuditTypePROCTITLE) != -1 {
extractFields(rawMessage, &aEvent)
if aEvent == nil {
return
}
netEvent = populateEvent(netEvent, &aEvent)
AddEvent(netEvent)
} else if newEvent == true && strings.Index(rawMessage, AuditTypeCWD) != -1 {
extractFields(rawMessage, &aEvent)
if aEvent == nil {
return
}
netEvent = populateEvent(netEvent, &aEvent)
AddEvent(netEvent)
} else if newEvent == true && strings.Index(rawMessage, AuditTypeEXECVE) != -1 {
extractFields(rawMessage, &aEvent)
if aEvent == nil {
return
}
netEvent = populateEvent(netEvent, &aEvent)
AddEvent(netEvent)
} else if newEvent == true && strings.Index(rawMessage, AuditTypePATH) != -1 {
extractFields(rawMessage, &aEvent)
if aEvent == nil {
return
}
netEvent = populateEvent(netEvent, &aEvent)
AddEvent(netEvent)
} else if newEvent == true && strings.Index(rawMessage, AuditTypeSOCKADDR) != -1 {
extractFields(rawMessage, &aEvent)
if aEvent == nil {
return
}
netEvent = populateEvent(netEvent, &aEvent)
AddEvent(netEvent)
if EventChan != nil {
eventChan <- *netEvent
}
} else if newEvent == true && strings.Index(rawMessage, AuditTypeEOE) != -1 {
newEvent = false
AddEvent(netEvent)
if EventChan != nil {
eventChan <- *netEvent
}
}
}

143
daemon/procmon/cache.go Normal file
View file

@ -0,0 +1,143 @@
package procmon
import (
"fmt"
"os"
"sort"
"time"
)
// Inode represents an item of the InodesCache.
// the key is formed as follow:
// inode+srcip+srcport+dstip+dstport
type Inode struct {
Pid int
FdPath string
}
// ProcEntry represents an item of the pidsCache
type ProcEntry struct {
Pid int
FdPath string
Descriptors []string
Time time.Time
}
var (
// cache of inodes, which help to not iterate over all the pidsCache and
// descriptors of /proc/<pid>/fd/
// 20-50us vs 50-80ms
inodesCache = make(map[string]*Inode)
maxCachedInodes = 128
// 2nd cache of already known running pids, which also saves time by
// iterating only over a few pids' descriptors, (30us-2ms vs. 50-80ms)
// since it's more likely that most of the connections will be made by the
// same (running) processes.
// The cache is ordered by time, placing in the first places those PIDs with
// active connections.
pidsCache []*ProcEntry
pidsDescriptorsCache = make(map[int][]string)
maxCachedPids = 24
)
func addProcEntry(fdPath string, fdList []string, pid int) {
for n := range pidsCache {
if pidsCache[n].Pid == pid {
pidsCache[n].Time = time.Now()
return
}
}
procEntry := &ProcEntry{
Pid: pid,
FdPath: fdPath,
Descriptors: fdList,
Time: time.Now(),
}
pidsCache = append([]*ProcEntry{procEntry}, pidsCache...)
}
func sortProcEntries() {
sort.Slice(pidsCache, func(i, j int) bool {
t := pidsCache[i].Time.UnixNano()
u := pidsCache[j].Time.UnixNano()
return t > u || t == u
})
}
func deleteProcEntry(pid int) {
for n, procEntry := range pidsCache {
if procEntry.Pid == pid {
pidsCache = append(pidsCache[:n], pidsCache[n+1:]...)
deleteInodeEntry(pid)
break
}
}
}
func deleteInodeEntry(pid int) {
for k, inodeEntry := range inodesCache {
if inodeEntry.Pid == pid {
delete(inodesCache, k)
}
}
}
func cleanUpCaches() {
if len(inodesCache) > maxCachedInodes {
for k := range inodesCache {
delete(inodesCache, k)
}
}
if len(pidsCache) > maxCachedPids {
pidsCache = nil
}
}
func getPidByInodeFromCache(inodeKey string) int {
if _, found := inodesCache[inodeKey]; found == true {
// sometimes the process may have dissapeared at this point
if _, err := os.Lstat(fmt.Sprint("/proc/", inodesCache[inodeKey].Pid, "/exe")); err == nil {
return inodesCache[inodeKey].Pid
}
deleteProcEntry(inodesCache[inodeKey].Pid)
}
return -1
}
func getPidDescriptorsFromCache(pid int, fdPath string, expect string, descriptors []string) int {
for fdIdx := 0; fdIdx < len(descriptors); fdIdx++ {
descLink := fmt.Sprint(fdPath, descriptors[fdIdx])
if link, err := os.Readlink(descLink); err == nil && link == expect {
return fdIdx
}
}
return -1
}
func getPidFromCache(inode int, inodeKey string, expect string) (int, int) {
// loop over the processes that have generated connections
for n := 0; n < len(pidsCache); n++ {
procEntry := pidsCache[n]
if idxDesc := getPidDescriptorsFromCache(procEntry.Pid, procEntry.FdPath, expect, procEntry.Descriptors); idxDesc != -1 {
pidsCache[n].Time = time.Now()
return procEntry.Pid, n
}
descriptors := lookupPidDescriptors(procEntry.FdPath)
if descriptors == nil {
deleteProcEntry(procEntry.Pid)
continue
}
pidsCache[n].Descriptors = descriptors
if idxDesc := getPidDescriptorsFromCache(procEntry.Pid, procEntry.FdPath, expect, descriptors); idxDesc != -1 {
pidsCache[n].Time = time.Now()
return procEntry.Pid, n
}
}
return -1, -1
}

182
daemon/procmon/details.go Normal file
View file

@ -0,0 +1,182 @@
package procmon
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"regexp"
"strconv"
"strings"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/dns"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/netlink"
)
var socketsRegex, _ = regexp.Compile(`socket:\[([0-9]+)\]`)
// GetInfo collects information of a process.
func (p *Process) GetInfo() error {
if err := p.readPath(); err != nil {
return err
}
p.readCwd()
p.readCmdline()
p.readEnv()
p.readDescriptors()
p.readIOStats()
p.readStatus()
p.cleanPath()
return nil
}
func (p *Process) setCwd(cwd string) {
p.CWD = cwd
}
func (p *Process) readCwd() {
if link, err := os.Readlink(fmt.Sprintf("/proc/%d/cwd", p.ID)); err == nil {
p.CWD = link
}
}
// read and parse environment variables of a process.
func (p *Process) readEnv() {
if data, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/environ", p.ID)); err == nil {
for _, s := range strings.Split(string(data), "\x00") {
parts := strings.SplitN(core.Trim(s), "=", 2)
if parts != nil && len(parts) == 2 {
key := core.Trim(parts[0])
val := core.Trim(parts[1])
p.Env[key] = val
}
}
}
}
func (p *Process) readPath() error {
linkName := fmt.Sprint("/proc/", p.ID, "/exe")
if _, err := os.Lstat(linkName); err != nil {
return err
}
if link, err := os.Readlink(linkName); err == nil {
p.Path = link
}
return nil
}
func (p *Process) readCmdline() {
if data, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", p.ID)); err == nil {
for i, b := range data {
if b == 0x00 {
data[i] = byte(' ')
}
}
p.Args = make([]string, 0)
args := strings.Split(string(data), " ")
for _, arg := range args {
arg = core.Trim(arg)
if arg != "" {
p.Args = append(p.Args, arg)
}
}
}
}
func (p *Process) readDescriptors() {
f, err := os.Open(fmt.Sprint("/proc/", p.ID, "/fd/"))
if err != nil {
return
}
fDesc, err := f.Readdir(-1)
f.Close()
p.Descriptors = nil
for _, fd := range fDesc {
tempFd := &procDescriptors{
Name: fd.Name(),
}
if link, err := os.Readlink(fmt.Sprint("/proc/", p.ID, "/fd/", fd.Name())); err == nil {
tempFd.SymLink = link
socket := socketsRegex.FindStringSubmatch(link)
if len(socket) > 0 {
socketInfo, err := netlink.GetSocketInfoByInode(socket[1])
if err == nil {
tempFd.SymLink = fmt.Sprintf("socket:[%s] - %d:%s -> %s:%d, state: %s", fd.Name(),
socketInfo.ID.SourcePort,
socketInfo.ID.Source.String(),
dns.HostOr(socketInfo.ID.Destination, socketInfo.ID.Destination.String()),
socketInfo.ID.DestinationPort,
netlink.TCPStatesMap[socketInfo.State])
}
}
if linkInfo, err := os.Lstat(link); err == nil {
tempFd.Size = linkInfo.Size()
tempFd.ModTime = linkInfo.ModTime()
}
}
p.Descriptors = append(p.Descriptors, tempFd)
}
}
func (p *Process) readIOStats() {
f, err := os.Open(fmt.Sprint("/proc/", p.ID, "/io"))
if err != nil {
return
}
defer f.Close()
p.IOStats = &procIOstats{}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
s := strings.Split(scanner.Text(), " ")
switch s[0] {
case "rchar:":
p.IOStats.RChar, _ = strconv.ParseInt(s[1], 10, 64)
case "wchar:":
p.IOStats.WChar, _ = strconv.ParseInt(s[1], 10, 64)
case "syscr:":
p.IOStats.SyscallRead, _ = strconv.ParseInt(s[1], 10, 64)
case "syscw:":
p.IOStats.SyscallWrite, _ = strconv.ParseInt(s[1], 10, 64)
case "read_bytes:":
p.IOStats.ReadBytes, _ = strconv.ParseInt(s[1], 10, 64)
case "write_bytes:":
p.IOStats.WriteBytes, _ = strconv.ParseInt(s[1], 10, 64)
}
}
}
func (p *Process) readStatus() {
if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/status")); err == nil {
p.Status = string(data)
}
if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/stat")); err == nil {
p.Stat = string(data)
}
if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/stack")); err == nil {
p.Stack = string(data)
}
if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/maps")); err == nil {
p.Maps = string(data)
}
if data, err := ioutil.ReadFile(fmt.Sprint("/proc/", p.ID, "/statm")); err == nil {
p.Statm = &procStatm{}
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)
}
}
func (p *Process) cleanPath() {
pathLen := len(p.Path)
if pathLen >= 10 && p.Path[pathLen-10:] == " (deleted)" {
p.Path = p.Path[:len(p.Path)-10]
}
}

103
daemon/procmon/find.go Normal file
View file

@ -0,0 +1,103 @@
package procmon
import (
"fmt"
"os"
"sort"
"strconv"
)
func sortPidsByTime(fdList []os.FileInfo) []os.FileInfo {
sort.Slice(fdList, func(i, j int) bool {
t := fdList[i].ModTime().UnixNano()
u := fdList[j].ModTime().UnixNano()
return t > u
})
return fdList
}
// inodeFound searches for the given inode in /proc/<pid>/fd/ or
// /proc/<pid>/task/<tid>/fd/ and gets the symbolink link it points to,
// in order to compare it against the given inode.
//
// If the inode is found, the cache is updated ans sorted.
func inodeFound(pidsPath, expect, inodeKey string, inode, pid int) bool {
fdPath := fmt.Sprint(pidsPath, pid, "/fd/")
fdList := lookupPidDescriptors(fdPath)
if fdList == nil {
return false
}
for idx := 0; idx < len(fdList); idx++ {
descLink := fmt.Sprint(fdPath, fdList[idx])
if link, err := os.Readlink(descLink); err == nil && link == expect {
inodesCache[inodeKey] = &Inode{FdPath: descLink, Pid: pid}
addProcEntry(fdPath, fdList, pid)
return true
}
}
return false
}
// lookupPidInProc searches for an inode in /proc.
// First it gets the running PIDs and obtains the opened sockets.
// TODO: If the inode is not found, search again in the task/threads
// of every PID (costly).
func lookupPidInProc(pidsPath, expect, inodeKey string, inode int) int {
pidList := getProcPids(pidsPath)
for _, pid := range pidList {
if inodeFound(pidsPath, expect, inodeKey, inode, pid) {
return pid
}
}
return -1
}
// lookupPidDescriptors returns the list of descriptors inside
// /proc/<pid>/fd/
// TODO: search in /proc/<pid>/task/<tid>/fd/ .
func lookupPidDescriptors(fdPath string) []string {
f, err := os.Open(fdPath)
if err != nil {
return nil
}
fdList, err := f.Readdir(-1)
f.Close()
if err != nil {
return nil
}
fdList = sortPidsByTime(fdList)
s := make([]string, len(fdList))
for n, f := range fdList {
s[n] = f.Name()
}
return s
}
// getProcPids returns the list of running PIDs, /proc or /proc/<pid>/task/ .
func getProcPids(pidsPath string) (pidList []int) {
f, err := os.Open(pidsPath)
if err != nil {
return pidList
}
ls, err := f.Readdir(-1)
f.Close()
if err != nil {
return pidList
}
ls = sortPidsByTime(ls)
for _, f := range ls {
if f.IsDir() == false {
continue
}
if pid, err := strconv.Atoi(f.Name()); err == nil {
pidList = append(pidList, []int{pid}...)
}
}
return pidList
}

View file

@ -2,79 +2,123 @@ package procmon
import (
"fmt"
"io/ioutil"
"os"
"strings"
"time"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/procmon/audit"
)
func GetPIDFromINode(inode int) int {
expect := fmt.Sprintf("socket:[%d]", inode)
found := -1
func getPIDFromAuditEvents(inode int, inodeKey string, expect string) (int, int) {
audit.Lock.RLock()
defer audit.Lock.RUnlock()
forEachProcess(func(pid int, path string, args []string) bool {
// for every descriptor
fdPath := fmt.Sprintf("/proc/%d/fd/", pid)
if descriptors, err := ioutil.ReadDir(fdPath); err == nil {
for _, desc := range descriptors {
descLink := fmt.Sprintf("%s%s", fdPath, desc.Name())
// resolve the symlink and compare to what we expect
if link, err := os.Readlink(descLink); err == nil && link == expect {
found = pid
return true
}
}
auditEvents := audit.GetEvents()
for n := 0; n < len(auditEvents); n++ {
pid := auditEvents[n].Pid
if inodeFound("/proc/", expect, inodeKey, inode, pid) {
return pid, n
}
// keep looping
return false
})
}
for n := 0; n < len(auditEvents); n++ {
ppid := auditEvents[n].PPid
if inodeFound("/proc/", expect, inodeKey, inode, ppid) {
return ppid, n
}
}
return -1, -1
}
// GetPIDFromINode tries to get the PID from a socket inode follwing these steps:
// 1. Get the PID from the cache of Inodes.
// 2. Get the PID from the cache of PIDs.
// 3. Look for the PID using one of these methods:
// - ftrace: listening processes execs/exits from /sys/kernel/debug/tracing/
// - audit: listening for socket creation from auditd.
// - proc: search /proc
//
// If the PID is not found by one of the 2 first methods, it'll try it using /proc.
func GetPIDFromINode(inode int, inodeKey string) int {
found := -1
if inode <= 0 {
return found
}
start := time.Now()
cleanUpCaches()
expect := fmt.Sprintf("socket:[%d]", inode)
if cachedPidInode := getPidByInodeFromCache(inodeKey); cachedPidInode != -1 {
log.Debug("Inode found in cache", time.Since(start), inodesCache[inodeKey], inode, inodeKey)
return cachedPidInode
}
cachedPid, pos := getPidFromCache(inode, inodeKey, expect)
if cachedPid != -1 {
log.Debug("Socket found in known pids %v, pid: %d, inode: %d, pids in cache: %d", time.Since(start), cachedPid, inode, "pos", pos, len(pidsCache))
sortProcEntries()
return cachedPid
}
if methodIsAudit() {
if aPid, pos := getPIDFromAuditEvents(inode, inodeKey, expect); aPid != -1 {
log.Debug("PID found via audit events", time.Since(start), "position", pos)
return aPid
}
} else if methodIsFtrace() && IsWatcherAvailable() {
forEachProcess(func(pid int, path string, args []string) bool {
if inodeFound("/proc/", expect, inodeKey, inode, pid) {
found = pid
return true
}
// keep looping
return false
})
}
if found == -1 || methodIsProc() {
found = lookupPidInProc("/proc/", expect, inodeKey, inode)
}
log.Debug("new pid lookup took", found, time.Since(start))
return found
}
func parseCmdLine(proc *Process) {
if data, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", proc.ID)); err == nil {
for i, b := range data {
if b == 0x00 {
data[i] = byte(' ')
// FindProcess checks if a process exists given a PID.
// If it exists in /proc, a new Process{} object is returned with the details
// to identify a process (cmdline, name, environment variables, etc).
func FindProcess(pid int, interceptUnknown bool) *Process {
if interceptUnknown && pid < 0 {
return NewProcess(0, "")
}
if methodIsAudit() {
if aevent := audit.GetEventByPid(pid); aevent != nil {
audit.Lock.RLock()
proc := NewProcess(pid, aevent.ProcPath)
proc.readCmdline()
proc.setCwd(aevent.ProcDir)
audit.Lock.RUnlock()
// if the proc dir contains non alhpa-numeric chars the field is empty
if proc.CWD == "" {
proc.readCwd()
}
}
proc.readEnv()
proc.cleanPath()
args := strings.Split(string(data), " ")
for _, arg := range args {
arg = core.Trim(arg)
if arg != "" {
proc.Args = append(proc.Args, arg)
}
return proc
}
}
}
func parseEnv(proc *Process) {
if data, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/environ", proc.ID)); err == nil {
for _, s := range strings.Split(string(data), "\x00") {
parts := strings.SplitN(core.Trim(s), "=", 2)
if parts != nil && len(parts) == 2 {
key := core.Trim(parts[0])
val := core.Trim(parts[1])
proc.Env[key] = val
}
}
}
}
func FindProcess(pid int) *Process {
linkName := fmt.Sprintf("/proc/%d/exe", pid)
if core.Exists(linkName) == false {
linkName := fmt.Sprint("/proc/", pid, "/exe")
if _, err := os.Lstat(linkName); err != nil {
return nil
}
if link, err := os.Readlink(linkName); err == nil && core.Exists(link) == true {
if link, err := os.Readlink(linkName); err == nil {
proc := NewProcess(pid, link)
parseCmdLine(proc)
parseEnv(proc)
proc.readCmdline()
proc.readCwd()
proc.readEnv()
proc.cleanPath()
return proc
}

View file

@ -1,12 +1,56 @@
package procmon
type Process struct {
ID int
Path string
Args []string
Env map[string]string
import (
"time"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/procmon/audit"
)
// man 5 proc; man procfs
type procIOstats struct {
RChar int64
WChar int64
SyscallRead int64
SyscallWrite int64
ReadBytes int64
WriteBytes int64
}
type procDescriptors struct {
Name string
SymLink string
Size int64
ModTime time.Time
}
type procStatm struct {
Size int64
Resident int64
Shared int64
Text int64
Lib int64
Data int64 // data + stack
Dt int
}
// Process holds the details of a process.
type Process struct {
ID int
Path string
Args []string
Env map[string]string
CWD string
Descriptors []*procDescriptors
IOStats *procIOstats
Status string
Stat string
Statm *procStatm
Stack string
Maps string
}
// NewProcess returns a new Process structure.
func NewProcess(pid int, path string) *Process {
return &Process{
ID: pid,
@ -15,3 +59,70 @@ func NewProcess(pid int, path string) *Process {
Env: make(map[string]string),
}
}
// SetMonitorMethod configures a new method for parsing connections.
func SetMonitorMethod(newMonitorMethod string) {
lock.Lock()
defer lock.Unlock()
monitorMethod = newMonitorMethod
}
func methodIsFtrace() bool {
lock.RLock()
defer lock.RUnlock()
return monitorMethod == MethodFtrace
}
func methodIsAudit() bool {
lock.RLock()
defer lock.RUnlock()
return monitorMethod == MethodAudit
}
func methodIsProc() bool {
lock.RLock()
defer lock.RUnlock()
return monitorMethod == MethodProc
}
// End stops the way of parsing new connections.
func End() {
if methodIsAudit() {
audit.Stop()
} else if methodIsFtrace() {
go func() {
if err := Stop(); err != nil {
log.Warning("procmon.End() stop ftrace error: %v", err)
}
}()
}
}
// Init starts parsing connections using the method specified.
func Init() {
if methodIsFtrace() {
err := Start()
if err == nil {
log.Info("Process monitor method ftrace")
return
}
log.Warning("error starting ftrace monitor method: %v", err)
} else if methodIsAudit() {
auditConn, err := audit.Start()
if err == nil {
log.Info("Process monitor method audit")
go audit.Reader(auditConn, (chan<- audit.Event)(audit.EventChan))
return
}
log.Warning("error starting audit monitor method: %v", err)
}
// if any of the above methods have failed, fallback to proc
log.Info("Process monitor method /proc")
SetMonitorMethod(MethodProc)
}

View file

@ -6,6 +6,14 @@ import (
"sync"
"github.com/evilsocket/ftrace"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
// monitor method supported types
const (
MethodFtrace = "ftrace"
MethodProc = "proc"
MethodAudit = "audit"
)
const (
@ -25,7 +33,9 @@ var (
"sched/sched_process_exit",
}
watcher = ftrace.NewProbe(probeName, syscallName, subEvents)
watcher = ftrace.NewProbe(probeName, syscallName, subEvents)
isAvailable = false
monitorMethod = MethodProc
index = make(map[int]*procData)
lock = sync.RWMutex{}
@ -94,11 +104,18 @@ func eventConsumer() {
}
}
// Start enables the ftrace monitor method.
// This method configures a kprobe to intercept execve() syscalls.
// The kernel must have configured and enabled debugfs.
func Start() (err error) {
// start from a clean state
watcher.Reset()
if err := watcher.Reset(); err != nil && watcher.Enabled() {
log.Warning("ftrace.Reset() error: %v", err)
}
if err = watcher.Enable(); err == nil {
isAvailable = true
go eventConsumer()
// track running processes
if ls, err := ioutil.ReadDir("/proc/"); err == nil {
@ -108,10 +125,19 @@ func Start() (err error) {
}
}
}
} else {
isAvailable = false
}
return
}
// Stop disables ftrace monitor method, removing configured kprobe.
func Stop() error {
isAvailable = false
return watcher.Disable()
}
// IsWatcherAvailable checks if ftrace (debugfs) is
func IsWatcherAvailable() bool {
return isAvailable
}

View file

@ -4,28 +4,35 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/conman"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/fsnotify/fsnotify"
)
// Loader is the object that holds the rules loaded from disk, as well as the
// rules watcher.
type Loader struct {
sync.RWMutex
path string
rules map[string]*Rule
rulesKeys []string
watcher *fsnotify.Watcher
liveReload bool
liveReloadRunning bool
}
// NewLoader loads rules from disk, and watches for changes made to the rules files
// on disk.
func NewLoader(liveReload bool) (*Loader, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
@ -40,15 +47,17 @@ func NewLoader(liveReload bool) (*Loader, error) {
}, nil
}
// NumRules returns he number of loaded rules.
func (l *Loader) NumRules() int {
l.RLock()
defer l.RUnlock()
return len(l.rules)
}
// Load loads rules files from disk.
func (l *Loader) Load(path string) error {
if core.Exists(path) == false {
return fmt.Errorf("Path '%s' does not exist.", path)
return fmt.Errorf("Path '%s' does not exist", path)
}
expr := filepath.Join(path, "*.json")
@ -61,7 +70,10 @@ func (l *Loader) Load(path string) error {
defer l.Unlock()
l.path = path
l.rules = make(map[string]*Rule)
if len(l.rules) == 0 {
l.rules = make(map[string]*Rule)
}
diskRules := make(map[string]string)
for _, fileName := range matches {
log.Debug("Reading rule from %s", fileName)
@ -74,14 +86,26 @@ func (l *Loader) Load(path string) error {
err = json.Unmarshal(raw, &r)
if err != nil {
return fmt.Errorf("Error while parsing rule from %s: %s", fileName, err)
log.Error("Error parsing rule from %s: %s", fileName, err)
continue
}
r.Operator.Compile()
diskRules[r.Name] = r.Name
log.Debug("Loaded rule from %s: %s", fileName, r.String())
l.rules[r.Name] = &r
}
for ruleName, inMemoryRule := range l.rules {
if _, ok := diskRules[ruleName]; ok == false {
if inMemoryRule.Duration == Always {
log.Debug("Rule deleted from disk, updating rules list: ", ruleName)
delete(l.rules, ruleName)
}
}
}
l.sortRules()
if l.liveReload && l.liveReloadRunning == false {
go l.liveReloadWorker()
@ -118,16 +142,27 @@ func (l *Loader) liveReloadWorker() {
}
}
// Reload reloads the rules from disk.
func (l *Loader) Reload() error {
return l.Load(l.path)
}
// GetAll returns the loaded rules.
func (l *Loader) GetAll() map[string]*Rule {
l.RLock()
defer l.RUnlock()
return l.rules
}
func (l *Loader) isUniqueName(name string) bool {
_, found := l.rules[name]
return !found
}
func (l *Loader) setUniqueName(rule *Rule) {
l.Lock()
defer l.Unlock()
idx := 1
base := rule.Name
for l.isUniqueName(rule.Name) == false {
@ -136,13 +171,52 @@ func (l *Loader) setUniqueName(rule *Rule) {
}
}
func (l *Loader) addUserRule(rule *Rule) {
l.Lock()
l.setUniqueName(rule)
l.rules[rule.Name] = rule
l.Unlock()
func (l *Loader) sortRules() {
l.rulesKeys = make([]string, 0, len(l.rules))
for k := range l.rules {
l.rulesKeys = append(l.rulesKeys, k)
}
sort.Strings(l.rulesKeys)
}
func (l *Loader) addUserRule(rule *Rule) {
if rule.Duration == Once {
return
}
l.setUniqueName(rule)
l.replaceUserRule(rule)
}
func (l *Loader) replaceUserRule(rule *Rule) {
l.Lock()
if rule.Operator.Type == List {
if err := json.Unmarshal([]byte(rule.Operator.Data), &rule.Operator.List); err != nil {
log.Error("Error loading rule of type list", err)
}
}
l.rules[rule.Name] = rule
l.sortRules()
l.Unlock()
if rule.Duration == Restart || rule.Duration == Always {
return
}
tTime, err := time.ParseDuration(string(rule.Duration))
if err != nil {
return
}
time.AfterFunc(tTime, func() {
l.Lock()
delete(l.rules, rule.Name)
l.sortRules()
l.Unlock()
})
}
// Add adds a rule to the list of rules, and optionally saves it to disk.
func (l *Loader) Add(rule *Rule, saveToDisk bool) error {
l.addUserRule(rule)
if saveToDisk {
@ -152,6 +226,20 @@ func (l *Loader) Add(rule *Rule, saveToDisk bool) error {
return nil
}
// Replace adds a rule to the list of rules, and optionally saves it to disk.
func (l *Loader) Replace(rule *Rule, saveToDisk bool) error {
l.replaceUserRule(rule)
if saveToDisk {
l.Lock()
defer l.Unlock()
fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name))
return l.Save(rule, fileName)
}
return nil
}
// Save a rule to disk.
func (l *Loader) Save(rule *Rule, path string) error {
rule.Updated = time.Now()
raw, err := json.MarshalIndent(rule, "", " ")
@ -166,16 +254,48 @@ func (l *Loader) Save(rule *Rule, path string) error {
return nil
}
// Delete deletes a rule from the list.
// If the duration is Always (i.e: saved on disk), it'll attempt to delete
// it from disk.
func (l *Loader) Delete(ruleName string) error {
l.Lock()
defer l.Unlock()
rule := l.rules[ruleName]
if rule == nil {
return nil
}
delete(l.rules, ruleName)
l.sortRules()
if rule.Duration != Always {
return nil
}
log.Info("Delete() rule: ", rule)
path := fmt.Sprint(l.path, "/", ruleName, ".json")
return os.Remove(path)
}
// FindFirstMatch will try match the connection against the existing rule set.
func (l *Loader) FindFirstMatch(con *conman.Connection) (match *Rule) {
l.RLock()
defer l.RUnlock()
for _, rule := range l.rules {
for _, ruleIdx := range l.rulesKeys {
rule, valid := l.rules[ruleIdx]
if !valid {
continue
}
// if we already have a match, we don't need
// to evaluate 'allow' rules anymore, we only
// need to make sure there's no 'deny' rule
// matching this specific connection
if match != nil && rule.Action == Allow {
if rule.Precedence {
break
}
continue
} else if rule.Match(con) == true {
// only return if we found a deny

View file

@ -2,70 +2,103 @@ package rule
import (
"fmt"
"net"
"regexp"
"strings"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/conman"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
)
// Type is the type of rule.
// Every type has its own way of checking the user data against connections.
type Type string
// Sensitive defines if a rule is case-sensitive or not. By default no.
type Sensitive bool
// Operand is what we check on a connection.
type Operand string
// Available types
const (
Simple = Type("simple")
Regexp = Type("regexp")
Complex = Type("complex") // for future use
List = Type("list")
Network = Type("network")
)
type Operand string
// Available operands
const (
OpTrue = Operand("true")
OpProcessID = Operand("process.id")
OpProcessPath = Operand("process.path")
OpProcessCmd = Operand("process.command")
OpProcessEnvPrefix = Operand("process.env.")
OpProcessEnvPrefixLen = 12
OpUserId = Operand("user.id")
OpUserID = Operand("user.id")
OpDstIP = Operand("dest.ip")
OpDstHost = Operand("dest.host")
OpDstPort = Operand("dest.port")
OpDstNetwork = Operand("dest.network")
OpProto = Operand("protocol")
OpList = Operand("list")
)
type opCallback func(value string) bool
type opCallback func(value interface{}) bool
// Operator represents what we want to filter of a connection, and how.
type Operator struct {
Type Type `json:"type"`
Operand Operand `json:"operand"`
Data string `json:"data"`
List []Operator `json:"list"`
Type Type `json:"type"`
Operand Operand `json:"operand"`
Sensitive Sensitive `json:"sensitive"`
Data string `json:"data"`
List []Operator `json:"list"`
cb opCallback
re *regexp.Regexp
cb opCallback
re *regexp.Regexp
netMask *net.IPNet
}
func NewOperator(t Type, o Operand, data string, list []Operator) Operator {
// NewOperator returns a new operator object
func NewOperator(t Type, s Sensitive, o Operand, data string, list []Operator) (*Operator, error) {
op := Operator{
Type: t,
Operand: o,
Data: data,
List: list,
Type: t,
Sensitive: s,
Operand: o,
Data: data,
List: list,
}
op.Compile()
return op
if err := op.Compile(); err != nil {
log.Error("NewOperator() failed to compile:", err)
return nil, err
}
return &op, nil
}
func (o *Operator) Compile() {
// Compile translates the operator type field to its callback counterpart
func (o *Operator) Compile() error {
if o.Type == Simple {
o.cb = o.simpleCmp
} else if o.Type == Regexp {
o.cb = o.reCmp
o.re = regexp.MustCompile(o.Data)
re, err := regexp.Compile(o.Data)
if err != nil {
return err
}
o.re = re
} else if o.Type == List {
o.Operand = OpList
o.cb = o.listMatch
} else if o.Type == Network {
o.cb = o.cmpNetwork
// compile data once, so we don't have to do it on every rule check.
_, o.netMask, _ = net.ParseCIDR(o.Data)
}
return nil
}
func (o *Operator) String() string {
@ -76,29 +109,49 @@ func (o *Operator) String() string {
return fmt.Sprintf("%s %s '%s'", log.Bold(string(o.Operand)), how, log.Yellow(string(o.Data)))
}
func (o *Operator) simpleCmp(v string) bool {
func (o *Operator) simpleCmp(v interface{}) bool {
if o.Sensitive == false {
return strings.EqualFold(v.(string), o.Data)
}
return v == o.Data
}
func (o *Operator) reCmp(v string) bool {
return o.re.MatchString(v)
func (o *Operator) reCmp(v interface{}) bool {
if o.Sensitive == false {
v = strings.ToLower(v.(string))
}
return o.re.MatchString(v.(string))
}
func (o *Operator) listMatch(con *conman.Connection) bool {
func (o *Operator) cmpNetwork(destIP interface{}) bool {
// 192.0.2.1/24, 2001:db8:a0b:12f0::1/32
if o.netMask == nil {
return false
}
return o.netMask.Contains(destIP.(net.IP))
}
func (o *Operator) listMatch(con interface{}) bool {
res := true
for i := 0; i < len(o.List); i += 1 {
o := o.List[i]
o.Compile()
res = res && o.Match(con)
if err := o.Compile(); err != nil {
return false
}
res = res && o.Match(con.(*conman.Connection))
}
return res
}
// Match tries to match parts of a connection with the given operator.
func (o *Operator) Match(con *conman.Connection) bool {
if o.Operand == OpTrue {
return true
} else if o.Operand == OpUserId {
} else if o.Operand == OpUserID {
return o.cb(fmt.Sprintf("%d", con.Entry.UserId))
} else if o.Operand == OpProcessID {
return o.cb(fmt.Sprint(con.Process.ID))
} else if o.Operand == OpProcessPath {
return o.cb(con.Process.Path)
} else if o.Operand == OpProcessCmd {
@ -109,12 +162,16 @@ func (o *Operator) Match(con *conman.Connection) bool {
return o.cb(envVarValue)
} else if o.Operand == OpDstIP {
return o.cb(con.DstIP.String())
} else if o.Operand == OpDstHost {
} else if o.Operand == OpDstHost && con.DstHost != "" {
return o.cb(con.DstHost)
} else if o.Operand == OpProto {
return o.cb(con.Protocol)
} else if o.Operand == OpDstPort {
return o.cb(fmt.Sprintf("%d", con.DstPort))
} else if o.Operand == OpDstNetwork {
return o.cmpNetwork(con.DstIP)
} else if o.Operand == OpList {
return o.listMatch(con)
return o.cb(con)
}
return false

View file

@ -4,43 +4,54 @@ import (
"fmt"
"time"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/conman"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
)
// Action of a rule
type Action string
// Actions of rules
const (
Allow = Action("allow")
Deny = Action("deny")
)
// Duration of a rule
type Duration string
// daemon possible durations
const (
Once = Duration("once")
Restart = Duration("until restart")
Always = Duration("always")
)
// Rule represents an action on a connection.
// The fields match the ones saved as json to disk.
// If a .json rule file is modified on disk, it's reloaded automatically.
type Rule struct {
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Action Action `json:"action"`
Duration Duration `json:"duration"`
Operator Operator `json:"operator"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Precedence bool `json:"precedence"`
Action Action `json:"action"`
Duration Duration `json:"duration"`
Operator Operator `json:"operator"`
}
func Create(name string, action Action, duration Duration, op Operator) *Rule {
// Create creates a new rule object with the specified parameters.
func Create(name string, enabled bool, precedence bool, action Action, duration Duration, op *Operator) *Rule {
return &Rule{
Created: time.Now(),
Enabled: true,
Name: name,
Action: action,
Duration: duration,
Operator: op,
Created: time.Now(),
Enabled: enabled,
Precedence: precedence,
Name: name,
Action: action,
Duration: duration,
Operator: *op,
}
}
@ -48,6 +59,8 @@ func (r *Rule) String() string {
return fmt.Sprintf("%s: if(%s){ %s %s }", r.Name, r.Operator.String(), r.Action, r.Duration)
}
// Match performs on a connection the checks a Rule has, to determine if it
// must be allowed or denied.
func (r *Rule) Match(con *conman.Connection) bool {
if r.Enabled == false {
return false
@ -55,31 +68,50 @@ func (r *Rule) Match(con *conman.Connection) bool {
return r.Operator.Match(con)
}
func Deserialize(reply *protocol.Rule) *Rule {
operator := NewOperator(
// Deserialize translates back the rule received to a Rule object
func Deserialize(reply *protocol.Rule) (*Rule, error) {
if reply.Operator == nil {
log.Warning("Deserialize rule, Operator nil")
return nil, fmt.Errorf("invalid operator")
}
operator, err := NewOperator(
Type(reply.Operator.Type),
Sensitive(reply.Operator.Sensitive),
Operand(reply.Operator.Operand),
reply.Operator.Data,
make([]Operator, 0),
)
if err != nil {
log.Warning("Deserialize rule, NewOperator() error:", err)
return nil, err
}
return Create(
reply.Name,
reply.Enabled,
reply.Precedence,
Action(reply.Action),
Duration(reply.Duration),
operator,
)
), nil
}
// Serialize translates a Rule to the protocol object
func (r *Rule) Serialize() *protocol.Rule {
if r == nil {
return nil
}
return &protocol.Rule{
Name: string(r.Name),
Action: string(r.Action),
Duration: string(r.Duration),
Name: string(r.Name),
Enabled: bool(r.Enabled),
Precedence: bool(r.Precedence),
Action: string(r.Action),
Duration: string(r.Duration),
Operator: &protocol.Operator{
Type: string(r.Operator.Type),
Operand: string(r.Operator.Operand),
Data: string(r.Operator.Data),
Type: string(r.Operator.Type),
Sensitive: bool(r.Operator.Sensitive),
Operand: string(r.Operator.Operand),
Data: string(r.Operator.Data),
},
}
}

View file

@ -3,9 +3,9 @@ package statistics
import (
"time"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/rule"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/conman"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/rule"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
)
type Event struct {

View file

@ -5,11 +5,11 @@ import (
"sync"
"time"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/rule"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/conman"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/rule"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
)
const (
@ -26,7 +26,7 @@ type conEvent struct {
}
type Statistics struct {
sync.Mutex
sync.RWMutex
Started time.Time
DNSResponses int
@ -134,7 +134,7 @@ func (s *Statistics) onConnection(con *conman.Connection, match *rule.Rule, wasM
s.RuleHits++
}
if match.Action == rule.Allow {
if wasMissed == false && match.Action == rule.Allow {
s.Accepted++
} else {
s.Dropped++
@ -155,6 +155,9 @@ func (s *Statistics) onConnection(con *conman.Connection, match *rule.Rule, wasM
if nEvents == maxEvents {
s.Events = s.Events[1:]
}
if wasMissed {
return
}
s.Events = append(s.Events, NewEvent(con, match))
}

14
daemon/system-fw.json Normal file
View file

@ -0,0 +1,14 @@
{
"SystemRules": [
{
"Rule": {
"Description": "Allow icmp",
"Table": "mangle",
"Chain": "OUTPUT",
"Parameters": "-p icmp",
"Target": "ACCEPT",
"TargetParameters": ""
}
}
]
}

View file

@ -3,52 +3,118 @@ package ui
import (
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/evilsocket/opensnitch/daemon/conman"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/rule"
"github.com/evilsocket/opensnitch/daemon/statistics"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/conman"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/rule"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/statistics"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
"github.com/fsnotify/fsnotify"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
var (
clientDisconnectedRule = rule.Create("ui.client.disconnected", rule.Allow, rule.Once, rule.NewOperator(rule.Simple, rule.OpTrue, "", make([]rule.Operator, 0)))
clientErrorRule = rule.Create("ui.client.error", rule.Allow, rule.Once, rule.NewOperator(rule.Simple, rule.OpTrue, "", make([]rule.Operator, 0)))
configFile = "/etc/opensnitchd/default-config.json"
dummyOperator, _ = rule.NewOperator(rule.Simple, false, rule.OpTrue, "", make([]rule.Operator, 0))
clientDisconnectedRule = rule.Create("ui.client.disconnected", true, false, rule.Allow, rule.Once, dummyOperator)
clientErrorRule = rule.Create("ui.client.error", true, false, rule.Allow, rule.Once, dummyOperator)
config Config
)
type Client struct {
sync.Mutex
stats *statistics.Statistics
socketPath string
isUnixSocket bool
con *grpc.ClientConn
client protocol.UIClient
type serverConfig struct {
Address string `json:"Address"`
LogFile string `json:"LogFile"`
}
func NewClient(path string, stats *statistics.Statistics) *Client {
// Config holds the values loaded from configFile
type Config struct {
sync.RWMutex
Server serverConfig `json:"Server"`
DefaultAction string `json:"DefaultAction"`
DefaultDuration string `json:"DefaultDuration"`
InterceptUnknown bool `json:"InterceptUnknown"`
ProcMonitorMethod string `json:"ProcMonitorMethod"`
LogLevel *uint32 `json:"LogLevel"`
}
// Client holds the connection information of a client.
type Client struct {
sync.RWMutex
clientCtx context.Context
clientCancel context.CancelFunc
stats *statistics.Statistics
rules *rule.Loader
socketPath string
isUnixSocket bool
con *grpc.ClientConn
client protocol.UIClient
configWatcher *fsnotify.Watcher
streamNotifications protocol.UI_NotificationsClient
}
// NewClient creates and configures a new client.
func NewClient(socketPath string, stats *statistics.Statistics, rules *rule.Loader) *Client {
c := &Client{
socketPath: path,
stats: stats,
rules: rules,
isUnixSocket: false,
}
if strings.HasPrefix(c.socketPath, "unix://") == true {
c.isUnixSocket = true
c.socketPath = c.socketPath[7:]
c.clientCtx, c.clientCancel = context.WithCancel(context.Background())
if watcher, err := fsnotify.NewWatcher(); err == nil {
c.configWatcher = watcher
}
c.loadDiskConfiguration(false)
if socketPath != "" {
c.setSocketPath(c.getSocketPath(socketPath))
}
go c.poller()
return c
}
// Close cancels the running tasks: pinging the server and (re)connection poller.
func (c *Client) Close() {
c.clientCancel()
}
// ProcMonitorMethod returns the monitor method configured.
// If it's not present in the config file, it'll return an emptry string.
func (c *Client) ProcMonitorMethod() string {
config.RLock()
defer config.RUnlock()
return config.ProcMonitorMethod
}
// InterceptUnknown returns
func (c *Client) InterceptUnknown() bool {
config.RLock()
defer config.RUnlock()
return config.InterceptUnknown
}
// DefaultAction returns the default configured action for
func (c *Client) DefaultAction() rule.Action {
c.RLock()
defer c.RUnlock()
return clientDisconnectedRule.Action
}
// DefaultDuration returns the default duration configured for a rule.
// For example it can be: once, always, "until restart".
func (c *Client) DefaultDuration() rule.Duration {
c.RLock()
defer c.RUnlock()
return clientDisconnectedRule.Duration
}
// Connected checks if the client has established a connection with the server.
func (c *Client) Connected() bool {
c.Lock()
defer c.Unlock()
@ -61,32 +127,45 @@ func (c *Client) Connected() bool {
func (c *Client) poller() {
log.Debug("UI service poller started for socket %s", c.socketPath)
wasConnected := false
for true {
isConnected := c.Connected()
if wasConnected != isConnected {
c.onStatusChange(isConnected)
wasConnected = isConnected
}
// connect and create the client if needed
if err := c.connect(); err != nil {
log.Warning("Error while connecting to UI service: %s", err)
} else if c.Connected() == true {
// if the client is connected and ready, send a ping
if err := c.ping(time.Now()); err != nil {
log.Warning("Error while pinging UI service: %s", err)
for {
select {
case <-c.clientCtx.Done():
log.Info("Client.poller() exit, Done()")
goto Exit
default:
isConnected := c.Connected()
if wasConnected != isConnected {
c.onStatusChange(isConnected)
wasConnected = isConnected
}
}
time.Sleep(1 * time.Second)
if c.Connected() == false {
// connect and create the client if needed
if err := c.connect(); err != nil {
log.Warning("Error while connecting to UI service: %s", err)
}
}
if c.Connected() == true {
// if the client is connected and ready, send a ping
if err := c.ping(time.Now()); err != nil {
log.Warning("Error while pinging UI service: %s", err)
}
}
time.Sleep(1 * time.Second)
}
}
Exit:
log.Info("uiClient exit")
}
func (c *Client) onStatusChange(connected bool) {
if connected {
log.Info("Connected to the UI service on %s", c.socketPath)
go c.Subscribe()
} else {
log.Error("Connection to the UI service lost.")
c.disconnect()
}
}
@ -95,6 +174,29 @@ func (c *Client) connect() (err error) {
return
}
if c.con != nil {
if c.con.GetState() == connectivity.TransientFailure || c.con.GetState() == connectivity.Shutdown {
c.disconnect()
} else {
return
}
}
if err := c.openSocket(); err != nil {
c.disconnect()
return err
}
if c.client == nil {
c.client = protocol.NewUIClient(c.con)
}
return nil
}
func (c *Client) openSocket() (err error) {
c.Lock()
defer c.Unlock()
if c.isUnixSocket {
c.con, err = grpc.Dial(c.socketPath, grpc.WithInsecure(),
grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
@ -104,18 +206,24 @@ func (c *Client) connect() (err error) {
c.con, err = grpc.Dial(c.socketPath, grpc.WithInsecure())
}
if err != nil {
c.con = nil
return err
}
return err
}
c.client = protocol.NewUIClient(c.con)
return nil
func (c *Client) disconnect() {
c.Lock()
defer c.Unlock()
c.client = nil
if c.con != nil {
c.con.Close()
c.con = nil
log.Debug("client.disconnect()")
}
}
func (c *Client) ping(ts time.Time) (err error) {
if c.Connected() == false {
return fmt.Errorf("service is not connected.")
return fmt.Errorf("service is not connected")
}
c.Lock()
@ -123,23 +231,28 @@ func (c *Client) ping(ts time.Time) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
reqId := uint64(ts.UnixNano())
reqID := uint64(ts.UnixNano())
pong, err := c.client.Ping(ctx, &protocol.PingRequest{
Id: reqId,
pReq := &protocol.PingRequest{
Id: reqID,
Stats: c.stats.Serialize(),
})
}
c.stats.RLock()
pong, err := c.client.Ping(ctx, pReq)
c.stats.RUnlock()
if err != nil {
return err
}
if pong.Id != reqId {
return fmt.Errorf("Expected pong with id 0x%x, got 0x%x", reqId, pong.Id)
if pong.Id != reqID {
return fmt.Errorf("Expected pong with id 0x%x, got 0x%x", reqID, pong.Id)
}
return nil
}
// Ask sends a request to the server, with the values of a connection to be
// allowed or denied.
func (c *Client) Ask(con *conman.Connection) (*rule.Rule, bool) {
if c.Connected() == false {
return clientDisconnectedRule, false
@ -148,13 +261,29 @@ func (c *Client) Ask(con *conman.Connection) (*rule.Rule, bool) {
c.Lock()
defer c.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
// FIXME: if timeout is fired, the rule is not added to the list in the GUI
ctx, cancel := context.WithTimeout(context.Background(), time.Second*120)
defer cancel()
reply, err := c.client.AskRule(ctx, con.Serialize())
if err != nil {
log.Warning("Error while asking for rule: %s", err)
return clientErrorRule, false
log.Warning("Error while asking for rule: %s - %v", err, con)
return nil, false
}
return rule.Deserialize(reply), true
r, err := rule.Deserialize(reply)
if err != nil {
return nil, false
}
return r, true
}
func (c *Client) monitorConfigWorker() {
for {
select {
case event := <-c.configWatcher.Events:
if (event.Op&fsnotify.Write == fsnotify.Write) || (event.Op&fsnotify.Remove == fsnotify.Remove) {
c.loadDiskConfiguration(true)
}
}
}
}

116
daemon/ui/config.go Normal file
View file

@ -0,0 +1,116 @@
package ui
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/procmon"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/rule"
)
func (c *Client) getSocketPath(socketPath string) string {
c.Lock()
defer c.Unlock()
if strings.HasPrefix(socketPath, "unix://") == true {
c.isUnixSocket = true
return socketPath[7:]
}
c.isUnixSocket = false
return socketPath
}
func (c *Client) setSocketPath(socketPath string) {
c.Lock()
defer c.Unlock()
c.socketPath = socketPath
}
func (c *Client) isProcMonitorEqual(newMonitorMethod string) bool {
config.RLock()
defer config.RUnlock()
return newMonitorMethod == config.ProcMonitorMethod
}
func (c *Client) parseConf(rawConfig string) (conf Config, err error) {
err = json.Unmarshal([]byte(rawConfig), &conf)
return conf, err
}
func (c *Client) loadDiskConfiguration(reload bool) {
raw, err := ioutil.ReadFile(configFile)
if err != nil {
fmt.Errorf("Error loading disk configuration %s: %s", configFile, err)
}
if ok := c.loadConfiguration(raw); ok {
if err := c.configWatcher.Add(configFile); err != nil {
log.Error("Could not watch path: %s", err)
return
}
}
if reload {
return
}
go c.monitorConfigWorker()
}
func (c *Client) loadConfiguration(rawConfig []byte) bool {
config.Lock()
defer config.Unlock()
if err := json.Unmarshal(rawConfig, &config); err != nil {
log.Error("Error parsing configuration %s: %s", configFile, err)
return false
}
// firstly load config level, to detect further errors if any
if config.LogLevel != nil {
log.SetLogLevel(int(*config.LogLevel))
}
if config.Server.LogFile != "" {
log.Close()
log.OpenFile(config.Server.LogFile)
}
if config.Server.Address != "" {
tempSocketPath := c.getSocketPath(config.Server.Address)
if tempSocketPath != c.socketPath {
// disconnect, and let the connection poller reconnect to the new address
c.disconnect()
}
c.setSocketPath(tempSocketPath)
}
if config.DefaultAction != "" {
clientDisconnectedRule.Action = rule.Action(config.DefaultAction)
clientErrorRule.Action = rule.Action(config.DefaultAction)
}
if config.DefaultDuration != "" {
clientDisconnectedRule.Duration = rule.Duration(config.DefaultDuration)
clientErrorRule.Duration = rule.Duration(config.DefaultDuration)
}
if config.ProcMonitorMethod != "" {
procmon.SetMonitorMethod(config.ProcMonitorMethod)
}
return true
}
func (c *Client) saveConfiguration(rawConfig string) (err error) {
if c.loadConfiguration([]byte(rawConfig)) != true {
return fmt.Errorf("Error parsing configuration %s: %s", rawConfig, err)
}
if err = ioutil.WriteFile(configFile, []byte(rawConfig), 0644); err != nil {
log.Error("writing configuration to disk: ", err)
return err
}
return nil
}

295
daemon/ui/notifications.go Normal file
View file

@ -0,0 +1,295 @@
package ui
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
"time"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/core"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/firewall"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/procmon"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/rule"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
"golang.org/x/net/context"
)
var stopMonitoringProcess = make(chan int)
// NewReply constructs a new protocol notification reply
func NewReply(rID uint64, replyCode protocol.NotificationReplyCode, data string) *protocol.NotificationReply {
return &protocol.NotificationReply{
Id: rID,
Code: replyCode,
Data: data,
}
}
func (c *Client) getClientConfig() *protocol.ClientConfig {
raw, _ := ioutil.ReadFile(configFile)
nodeName := core.GetHostname()
nodeVersion := core.GetKernelVersion()
var ts time.Time
rulesTotal := len(c.rules.GetAll())
ruleList := make([]*protocol.Rule, rulesTotal)
idx := 0
for _, r := range c.rules.GetAll() {
ruleList[idx] = r.Serialize()
idx++
}
return &protocol.ClientConfig{
Id: uint64(ts.UnixNano()),
Name: nodeName,
Version: nodeVersion,
IsFirewallRunning: firewall.IsRunning(),
Config: strings.Replace(string(raw), "\n", "", -1),
LogLevel: uint32(log.MinLevel),
Rules: ruleList,
}
}
func (c *Client) monitorProcessDetails(pid int, stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
p := procmon.NewProcess(pid, "")
ticker := time.NewTicker(2 * time.Second)
for {
select {
case _pid := <-stopMonitoringProcess:
if _pid != pid {
continue
}
goto Exit
case <-ticker.C:
if err := p.GetInfo(); err != nil {
c.sendNotificationReply(stream, notification.Id, notification.Data, err)
goto Exit
}
pJSON, err := json.Marshal(p)
notification.Data = string(pJSON)
if errs := c.sendNotificationReply(stream, notification.Id, notification.Data, err); errs != nil {
goto Exit
}
}
}
Exit:
ticker.Stop()
}
func (c *Client) handleActionChangeConfig(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
log.Info("[notification] Reloading configuration")
// Parse received configuration first, to get the new proc monitor method.
newConf, err := c.parseConf(notification.Data)
if err != nil {
log.Warning("[notification] error parsing received config: %v", notification.Data)
c.sendNotificationReply(stream, notification.Id, "", err)
return
}
// check if the current monitor method is different from the one received.
// in such case close the current method, and start the new one.
procMonitorEqual := c.isProcMonitorEqual(newConf.ProcMonitorMethod)
if procMonitorEqual == false {
procmon.End()
}
// this save operation triggers a re-loadConfiguration()
err = c.saveConfiguration(notification.Data)
if err != nil {
log.Warning("[notification] CHANGE_CONFIG not applied", err)
} else if err == nil && procMonitorEqual == false {
procmon.Init()
}
c.sendNotificationReply(stream, notification.Id, "", err)
}
func (c *Client) handleActionEnableRule(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
var err error
for _, rul := range notification.Rules {
log.Info("[notification] enable rule: ", rul.Name)
// protocol.Rule(protobuf) != rule.Rule(json)
r, _ := rule.Deserialize(rul)
r.Enabled = true
// save to disk only if the duration is rule.Always
err = c.rules.Replace(r, r.Duration == rule.Always)
}
c.sendNotificationReply(stream, notification.Id, "", err)
}
func (c *Client) handleActionDisableRule(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
var err error
for _, rul := range notification.Rules {
log.Info("[notification] disable rule: ", rul)
r, _ := rule.Deserialize(rul)
r.Enabled = false
err = c.rules.Replace(r, r.Duration == rule.Always)
}
c.sendNotificationReply(stream, notification.Id, "", err)
}
func (c *Client) handleActionChangeRule(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
var rErr error
for _, rul := range notification.Rules {
r, err := rule.Deserialize(rul)
if r == nil {
rErr = fmt.Errorf("Invalid rule, %s", err)
continue
}
log.Info("[notification] change rule: ", r, notification.Id)
if err := c.rules.Replace(r, r.Duration == rule.Always); err != nil {
log.Warning("[notification] Error changing rule: ", err, r)
rErr = err
}
}
c.sendNotificationReply(stream, notification.Id, "", rErr)
}
func (c *Client) handleActionDeleteRule(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
var err error
for _, rul := range notification.Rules {
log.Info("[notification] delete rule: ", rul.Name, notification.Id)
err = c.rules.Delete(rul.Name)
if err != nil {
log.Error("[notification] Error deleting rule: ", err, rul)
}
}
c.sendNotificationReply(stream, notification.Id, "", err)
}
func (c *Client) handleActionMonitorProcess(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
pid, err := strconv.Atoi(notification.Data)
if err != nil {
log.Error("parsing PID to monitor")
return
}
if !core.Exists(fmt.Sprint("/proc/", pid)) {
c.sendNotificationReply(stream, notification.Id, "", fmt.Errorf("The process is no longer running"))
return
}
go c.monitorProcessDetails(pid, stream, notification)
}
func (c *Client) handleActionStopMonitorProcess(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
pid, err := strconv.Atoi(notification.Data)
if err != nil {
log.Error("parsing PID to stop monitor")
c.sendNotificationReply(stream, notification.Id, "", fmt.Errorf("Error stopping monitor: ", notification.Data))
return
}
stopMonitoringProcess <- pid
c.sendNotificationReply(stream, notification.Id, "", nil)
}
func (c *Client) handleNotification(stream protocol.UI_NotificationsClient, notification *protocol.Notification) {
switch {
case notification.Type == protocol.Action_MONITOR_PROCESS:
c.handleActionMonitorProcess(stream, notification)
case notification.Type == protocol.Action_STOP_MONITOR_PROCESS:
c.handleActionStopMonitorProcess(stream, notification)
case notification.Type == protocol.Action_CHANGE_CONFIG:
c.handleActionChangeConfig(stream, notification)
case notification.Type == protocol.Action_LOAD_FIREWALL:
log.Info("[notification] starting firewall")
firewall.Init(nil)
c.sendNotificationReply(stream, notification.Id, "", nil)
case notification.Type == protocol.Action_UNLOAD_FIREWALL:
log.Info("[notification] stopping firewall")
firewall.Stop(nil)
c.sendNotificationReply(stream, notification.Id, "", nil)
// ENABLE_RULE just replaces the rule on disk
case notification.Type == protocol.Action_ENABLE_RULE:
c.handleActionEnableRule(stream, notification)
case notification.Type == protocol.Action_DISABLE_RULE:
c.handleActionDisableRule(stream, notification)
case notification.Type == protocol.Action_DELETE_RULE:
c.handleActionDeleteRule(stream, notification)
// CHANGE_RULE can add() or replace) an existing rule.
case notification.Type == protocol.Action_CHANGE_RULE:
c.handleActionChangeRule(stream, notification)
}
}
func (c *Client) sendNotificationReply(stream protocol.UI_NotificationsClient, nID uint64, data string, err error) error {
reply := NewReply(nID, protocol.NotificationReplyCode_OK, data)
if err != nil {
reply.Code = protocol.NotificationReplyCode_ERROR
reply.Data = fmt.Sprint(err)
}
if err := stream.Send(reply); err != nil {
log.Error("Error replying to notification:", err, reply.Id)
return err
}
return nil
}
// Subscribe opens a connection with the server (UI), to start
// receiving notifications.
// It firstly sends the daemon status and configuration.
func (c *Client) Subscribe() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if _, err := c.client.Subscribe(ctx, c.getClientConfig()); err != nil {
log.Error("Subscribing to GUI", err)
return
}
c.listenForNotifications()
}
// Notifications is the channel where the daemon receives messages from the server.
// It consists of 2 grpc streams (send/receive) that are never closed,
// this way we can share messages in realtime.
// If the GUI is closed, we'll receive an error reading from the channel.
func (c *Client) listenForNotifications() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// open the stream channel
streamReply := &protocol.NotificationReply{Id: 0, Code: protocol.NotificationReplyCode_OK}
notisStream, err := c.client.Notifications(ctx)
if err != nil {
log.Error("establishing notifications channel", err)
return
}
// send the first notification
if err := notisStream.Send(streamReply); err != nil {
log.Error("sending notfication HELLO", err)
return
}
log.Info("Start receiving notifications")
for {
select {
case <-c.clientCtx.Done():
goto Exit
default:
noti, err := notisStream.Recv()
if err == io.EOF {
log.Warning("notification channel closed by the server")
goto Exit
}
if err != nil {
log.Error("getting notifications: ", err, noti)
goto Exit
}
c.handleNotification(notisStream, noti)
}
}
Exit:
notisStream.CloseSend()
log.Info("Stop receiving notifications")
}

View file

@ -1,30 +1,16 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: ui.proto
/*
Package protocol is a generated protocol buffer package.
It is generated from these files:
ui.proto
It has these top-level messages:
Event
Statistics
PingRequest
PingReply
Connection
Operator
Rule
*/
package protocol
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
@ -38,16 +24,98 @@ var _ = math.Inf
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Event struct {
Time string `protobuf:"bytes,1,opt,name=time" json:"time,omitempty"`
Connection *Connection `protobuf:"bytes,2,opt,name=connection" json:"connection,omitempty"`
Rule *Rule `protobuf:"bytes,3,opt,name=rule" json:"rule,omitempty"`
type Action int32
const (
Action_NONE Action = 0
Action_LOAD_FIREWALL Action = 1
Action_UNLOAD_FIREWALL Action = 2
Action_CHANGE_CONFIG Action = 3
Action_ENABLE_RULE Action = 4
Action_DISABLE_RULE Action = 5
Action_DELETE_RULE Action = 6
Action_CHANGE_RULE Action = 7
Action_LOG_LEVEL Action = 8
Action_STOP Action = 9
Action_MONITOR_PROCESS Action = 10
Action_STOP_MONITOR_PROCESS Action = 11
)
var Action_name = map[int32]string{
0: "NONE",
1: "LOAD_FIREWALL",
2: "UNLOAD_FIREWALL",
3: "CHANGE_CONFIG",
4: "ENABLE_RULE",
5: "DISABLE_RULE",
6: "DELETE_RULE",
7: "CHANGE_RULE",
8: "LOG_LEVEL",
9: "STOP",
10: "MONITOR_PROCESS",
11: "STOP_MONITOR_PROCESS",
}
func (m *Event) Reset() { *m = Event{} }
func (m *Event) String() string { return proto.CompactTextString(m) }
func (*Event) ProtoMessage() {}
func (*Event) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
var Action_value = map[string]int32{
"NONE": 0,
"LOAD_FIREWALL": 1,
"UNLOAD_FIREWALL": 2,
"CHANGE_CONFIG": 3,
"ENABLE_RULE": 4,
"DISABLE_RULE": 5,
"DELETE_RULE": 6,
"CHANGE_RULE": 7,
"LOG_LEVEL": 8,
"STOP": 9,
"MONITOR_PROCESS": 10,
"STOP_MONITOR_PROCESS": 11,
}
func (x Action) String() string {
return proto.EnumName(Action_name, int32(x))
}
func (Action) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{0}
}
type NotificationReplyCode int32
const (
NotificationReplyCode_OK NotificationReplyCode = 0
NotificationReplyCode_ERROR NotificationReplyCode = 1
)
var NotificationReplyCode_name = map[int32]string{
0: "OK",
1: "ERROR",
}
var NotificationReplyCode_value = map[string]int32{
"OK": 0,
"ERROR": 1,
}
func (x NotificationReplyCode) String() string {
return proto.EnumName(NotificationReplyCode_name, int32(x))
}
func (NotificationReplyCode) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{1}
}
type Event struct {
Time string `protobuf:"bytes,1,opt,name=time,proto3" json:"time,omitempty"`
Connection *Connection `protobuf:"bytes,2,opt,name=connection,proto3" json:"connection,omitempty"`
Rule *Rule `protobuf:"bytes,3,opt,name=rule,proto3" json:"rule,omitempty"`
}
func (m *Event) Reset() { *m = Event{} }
func (m *Event) String() string { return proto.CompactTextString(m) }
func (*Event) ProtoMessage() {}
func (*Event) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{0}
}
func (m *Event) GetTime() string {
if m != nil {
@ -71,29 +139,31 @@ func (m *Event) GetRule() *Rule {
}
type Statistics struct {
DaemonVersion string `protobuf:"bytes,1,opt,name=daemon_version,json=daemonVersion" json:"daemon_version,omitempty"`
Rules uint64 `protobuf:"varint,2,opt,name=rules" json:"rules,omitempty"`
Uptime uint64 `protobuf:"varint,3,opt,name=uptime" json:"uptime,omitempty"`
DnsResponses uint64 `protobuf:"varint,4,opt,name=dns_responses,json=dnsResponses" json:"dns_responses,omitempty"`
Connections uint64 `protobuf:"varint,5,opt,name=connections" json:"connections,omitempty"`
Ignored uint64 `protobuf:"varint,6,opt,name=ignored" json:"ignored,omitempty"`
Accepted uint64 `protobuf:"varint,7,opt,name=accepted" json:"accepted,omitempty"`
Dropped uint64 `protobuf:"varint,8,opt,name=dropped" json:"dropped,omitempty"`
RuleHits uint64 `protobuf:"varint,9,opt,name=rule_hits,json=ruleHits" json:"rule_hits,omitempty"`
RuleMisses uint64 `protobuf:"varint,10,opt,name=rule_misses,json=ruleMisses" json:"rule_misses,omitempty"`
ByProto map[string]uint64 `protobuf:"bytes,11,rep,name=by_proto,json=byProto" json:"by_proto,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
ByAddress map[string]uint64 `protobuf:"bytes,12,rep,name=by_address,json=byAddress" json:"by_address,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
ByHost map[string]uint64 `protobuf:"bytes,13,rep,name=by_host,json=byHost" json:"by_host,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
ByPort map[string]uint64 `protobuf:"bytes,14,rep,name=by_port,json=byPort" json:"by_port,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
ByUid map[string]uint64 `protobuf:"bytes,15,rep,name=by_uid,json=byUid" json:"by_uid,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
ByExecutable map[string]uint64 `protobuf:"bytes,16,rep,name=by_executable,json=byExecutable" json:"by_executable,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
Events []*Event `protobuf:"bytes,17,rep,name=events" json:"events,omitempty"`
DaemonVersion string `protobuf:"bytes,1,opt,name=daemon_version,json=daemonVersion,proto3" json:"daemon_version,omitempty"`
Rules uint64 `protobuf:"varint,2,opt,name=rules,proto3" json:"rules,omitempty"`
Uptime uint64 `protobuf:"varint,3,opt,name=uptime,proto3" json:"uptime,omitempty"`
DnsResponses uint64 `protobuf:"varint,4,opt,name=dns_responses,json=dnsResponses,proto3" json:"dns_responses,omitempty"`
Connections uint64 `protobuf:"varint,5,opt,name=connections,proto3" json:"connections,omitempty"`
Ignored uint64 `protobuf:"varint,6,opt,name=ignored,proto3" json:"ignored,omitempty"`
Accepted uint64 `protobuf:"varint,7,opt,name=accepted,proto3" json:"accepted,omitempty"`
Dropped uint64 `protobuf:"varint,8,opt,name=dropped,proto3" json:"dropped,omitempty"`
RuleHits uint64 `protobuf:"varint,9,opt,name=rule_hits,json=ruleHits,proto3" json:"rule_hits,omitempty"`
RuleMisses uint64 `protobuf:"varint,10,opt,name=rule_misses,json=ruleMisses,proto3" json:"rule_misses,omitempty"`
ByProto map[string]uint64 `protobuf:"bytes,11,rep,name=by_proto,json=byProto,proto3" json:"by_proto,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
ByAddress map[string]uint64 `protobuf:"bytes,12,rep,name=by_address,json=byAddress,proto3" json:"by_address,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
ByHost map[string]uint64 `protobuf:"bytes,13,rep,name=by_host,json=byHost,proto3" json:"by_host,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
ByPort map[string]uint64 `protobuf:"bytes,14,rep,name=by_port,json=byPort,proto3" json:"by_port,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
ByUid map[string]uint64 `protobuf:"bytes,15,rep,name=by_uid,json=byUid,proto3" json:"by_uid,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
ByExecutable map[string]uint64 `protobuf:"bytes,16,rep,name=by_executable,json=byExecutable,proto3" json:"by_executable,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
Events []*Event `protobuf:"bytes,17,rep,name=events,proto3" json:"events,omitempty"`
}
func (m *Statistics) Reset() { *m = Statistics{} }
func (m *Statistics) String() string { return proto.CompactTextString(m) }
func (*Statistics) ProtoMessage() {}
func (*Statistics) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *Statistics) Reset() { *m = Statistics{} }
func (m *Statistics) String() string { return proto.CompactTextString(m) }
func (*Statistics) ProtoMessage() {}
func (*Statistics) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{1}
}
func (m *Statistics) GetDaemonVersion() string {
if m != nil {
@ -215,14 +285,16 @@ func (m *Statistics) GetEvents() []*Event {
}
type PingRequest struct {
Id uint64 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
Stats *Statistics `protobuf:"bytes,2,opt,name=stats" json:"stats,omitempty"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Stats *Statistics `protobuf:"bytes,2,opt,name=stats,proto3" json:"stats,omitempty"`
}
func (m *PingRequest) Reset() { *m = PingRequest{} }
func (m *PingRequest) String() string { return proto.CompactTextString(m) }
func (*PingRequest) ProtoMessage() {}
func (*PingRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *PingRequest) Reset() { *m = PingRequest{} }
func (m *PingRequest) String() string { return proto.CompactTextString(m) }
func (*PingRequest) ProtoMessage() {}
func (*PingRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{2}
}
func (m *PingRequest) GetId() uint64 {
if m != nil {
@ -239,13 +311,15 @@ func (m *PingRequest) GetStats() *Statistics {
}
type PingReply struct {
Id uint64 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
}
func (m *PingReply) Reset() { *m = PingReply{} }
func (m *PingReply) String() string { return proto.CompactTextString(m) }
func (*PingReply) ProtoMessage() {}
func (*PingReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
func (m *PingReply) Reset() { *m = PingReply{} }
func (m *PingReply) String() string { return proto.CompactTextString(m) }
func (*PingReply) ProtoMessage() {}
func (*PingReply) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{3}
}
func (m *PingReply) GetId() uint64 {
if m != nil {
@ -255,22 +329,26 @@ func (m *PingReply) GetId() uint64 {
}
type Connection struct {
Protocol string `protobuf:"bytes,1,opt,name=protocol" json:"protocol,omitempty"`
SrcIp string `protobuf:"bytes,2,opt,name=src_ip,json=srcIp" json:"src_ip,omitempty"`
SrcPort uint32 `protobuf:"varint,3,opt,name=src_port,json=srcPort" json:"src_port,omitempty"`
DstIp string `protobuf:"bytes,4,opt,name=dst_ip,json=dstIp" json:"dst_ip,omitempty"`
DstHost string `protobuf:"bytes,5,opt,name=dst_host,json=dstHost" json:"dst_host,omitempty"`
DstPort uint32 `protobuf:"varint,6,opt,name=dst_port,json=dstPort" json:"dst_port,omitempty"`
UserId uint32 `protobuf:"varint,7,opt,name=user_id,json=userId" json:"user_id,omitempty"`
ProcessId uint32 `protobuf:"varint,8,opt,name=process_id,json=processId" json:"process_id,omitempty"`
ProcessPath string `protobuf:"bytes,9,opt,name=process_path,json=processPath" json:"process_path,omitempty"`
ProcessArgs []string `protobuf:"bytes,10,rep,name=process_args,json=processArgs" json:"process_args,omitempty"`
Protocol string `protobuf:"bytes,1,opt,name=protocol,proto3" json:"protocol,omitempty"`
SrcIp string `protobuf:"bytes,2,opt,name=src_ip,json=srcIp,proto3" json:"src_ip,omitempty"`
SrcPort uint32 `protobuf:"varint,3,opt,name=src_port,json=srcPort,proto3" json:"src_port,omitempty"`
DstIp string `protobuf:"bytes,4,opt,name=dst_ip,json=dstIp,proto3" json:"dst_ip,omitempty"`
DstHost string `protobuf:"bytes,5,opt,name=dst_host,json=dstHost,proto3" json:"dst_host,omitempty"`
DstPort uint32 `protobuf:"varint,6,opt,name=dst_port,json=dstPort,proto3" json:"dst_port,omitempty"`
UserId uint32 `protobuf:"varint,7,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
ProcessId uint32 `protobuf:"varint,8,opt,name=process_id,json=processId,proto3" json:"process_id,omitempty"`
ProcessPath string `protobuf:"bytes,9,opt,name=process_path,json=processPath,proto3" json:"process_path,omitempty"`
ProcessCwd string `protobuf:"bytes,10,opt,name=process_cwd,json=processCwd,proto3" json:"process_cwd,omitempty"`
ProcessArgs []string `protobuf:"bytes,11,rep,name=process_args,json=processArgs,proto3" json:"process_args,omitempty"`
ProcessEnv map[string]string `protobuf:"bytes,12,rep,name=process_env,json=processEnv,proto3" json:"process_env,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (m *Connection) Reset() { *m = Connection{} }
func (m *Connection) String() string { return proto.CompactTextString(m) }
func (*Connection) ProtoMessage() {}
func (*Connection) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
func (m *Connection) Reset() { *m = Connection{} }
func (m *Connection) String() string { return proto.CompactTextString(m) }
func (*Connection) ProtoMessage() {}
func (*Connection) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{4}
}
func (m *Connection) GetProtocol() string {
if m != nil {
@ -335,6 +413,13 @@ func (m *Connection) GetProcessPath() string {
return ""
}
func (m *Connection) GetProcessCwd() string {
if m != nil {
return m.ProcessCwd
}
return ""
}
func (m *Connection) GetProcessArgs() []string {
if m != nil {
return m.ProcessArgs
@ -342,16 +427,26 @@ func (m *Connection) GetProcessArgs() []string {
return nil
}
type Operator struct {
Type string `protobuf:"bytes,1,opt,name=type" json:"type,omitempty"`
Operand string `protobuf:"bytes,2,opt,name=operand" json:"operand,omitempty"`
Data string `protobuf:"bytes,3,opt,name=data" json:"data,omitempty"`
func (m *Connection) GetProcessEnv() map[string]string {
if m != nil {
return m.ProcessEnv
}
return nil
}
func (m *Operator) Reset() { *m = Operator{} }
func (m *Operator) String() string { return proto.CompactTextString(m) }
func (*Operator) ProtoMessage() {}
func (*Operator) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} }
type Operator struct {
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Operand string `protobuf:"bytes,2,opt,name=operand,proto3" json:"operand,omitempty"`
Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
Sensitive bool `protobuf:"varint,4,opt,name=sensitive,proto3" json:"sensitive,omitempty"`
}
func (m *Operator) Reset() { *m = Operator{} }
func (m *Operator) String() string { return proto.CompactTextString(m) }
func (*Operator) ProtoMessage() {}
func (*Operator) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{5}
}
func (m *Operator) GetType() string {
if m != nil {
@ -374,17 +469,28 @@ func (m *Operator) GetData() string {
return ""
}
type Rule struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Action string `protobuf:"bytes,2,opt,name=action" json:"action,omitempty"`
Duration string `protobuf:"bytes,3,opt,name=duration" json:"duration,omitempty"`
Operator *Operator `protobuf:"bytes,4,opt,name=operator" json:"operator,omitempty"`
func (m *Operator) GetSensitive() bool {
if m != nil {
return m.Sensitive
}
return false
}
func (m *Rule) Reset() { *m = Rule{} }
func (m *Rule) String() string { return proto.CompactTextString(m) }
func (*Rule) ProtoMessage() {}
func (*Rule) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
type Rule struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"`
Precedence bool `protobuf:"varint,3,opt,name=precedence,proto3" json:"precedence,omitempty"`
Action string `protobuf:"bytes,4,opt,name=action,proto3" json:"action,omitempty"`
Duration string `protobuf:"bytes,5,opt,name=duration,proto3" json:"duration,omitempty"`
Operator *Operator `protobuf:"bytes,6,opt,name=operator,proto3" json:"operator,omitempty"`
}
func (m *Rule) Reset() { *m = Rule{} }
func (m *Rule) String() string { return proto.CompactTextString(m) }
func (*Rule) ProtoMessage() {}
func (*Rule) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{6}
}
func (m *Rule) GetName() string {
if m != nil {
@ -393,6 +499,20 @@ func (m *Rule) GetName() string {
return ""
}
func (m *Rule) GetEnabled() bool {
if m != nil {
return m.Enabled
}
return false
}
func (m *Rule) GetPrecedence() bool {
if m != nil {
return m.Precedence
}
return false
}
func (m *Rule) GetAction() string {
if m != nil {
return m.Action
@ -414,7 +534,172 @@ func (m *Rule) GetOperator() *Operator {
return nil
}
// client configuration sent on Subscribe()
type ClientConfig struct {
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"`
IsFirewallRunning bool `protobuf:"varint,4,opt,name=isFirewallRunning,proto3" json:"isFirewallRunning,omitempty"`
// daemon configuration as json string
Config string `protobuf:"bytes,5,opt,name=config,proto3" json:"config,omitempty"`
LogLevel uint32 `protobuf:"varint,6,opt,name=logLevel,proto3" json:"logLevel,omitempty"`
Rules []*Rule `protobuf:"bytes,7,rep,name=rules,proto3" json:"rules,omitempty"`
}
func (m *ClientConfig) Reset() { *m = ClientConfig{} }
func (m *ClientConfig) String() string { return proto.CompactTextString(m) }
func (*ClientConfig) ProtoMessage() {}
func (*ClientConfig) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{7}
}
func (m *ClientConfig) GetId() uint64 {
if m != nil {
return m.Id
}
return 0
}
func (m *ClientConfig) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *ClientConfig) GetVersion() string {
if m != nil {
return m.Version
}
return ""
}
func (m *ClientConfig) GetIsFirewallRunning() bool {
if m != nil {
return m.IsFirewallRunning
}
return false
}
func (m *ClientConfig) GetConfig() string {
if m != nil {
return m.Config
}
return ""
}
func (m *ClientConfig) GetLogLevel() uint32 {
if m != nil {
return m.LogLevel
}
return 0
}
func (m *ClientConfig) GetRules() []*Rule {
if m != nil {
return m.Rules
}
return nil
}
// notification sent to the clients (daemons)
type Notification struct {
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
ClientName string `protobuf:"bytes,2,opt,name=clientName,proto3" json:"clientName,omitempty"`
ServerName string `protobuf:"bytes,3,opt,name=serverName,proto3" json:"serverName,omitempty"`
// CHANGE_CONFIG: 2, data: {"default_timeout": 1, ...}
Type Action `protobuf:"varint,4,opt,name=type,proto3,enum=protocol.Action" json:"type,omitempty"`
Data string `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"`
Rules []*Rule `protobuf:"bytes,6,rep,name=rules,proto3" json:"rules,omitempty"`
}
func (m *Notification) Reset() { *m = Notification{} }
func (m *Notification) String() string { return proto.CompactTextString(m) }
func (*Notification) ProtoMessage() {}
func (*Notification) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{8}
}
func (m *Notification) GetId() uint64 {
if m != nil {
return m.Id
}
return 0
}
func (m *Notification) GetClientName() string {
if m != nil {
return m.ClientName
}
return ""
}
func (m *Notification) GetServerName() string {
if m != nil {
return m.ServerName
}
return ""
}
func (m *Notification) GetType() Action {
if m != nil {
return m.Type
}
return Action_NONE
}
func (m *Notification) GetData() string {
if m != nil {
return m.Data
}
return ""
}
func (m *Notification) GetRules() []*Rule {
if m != nil {
return m.Rules
}
return nil
}
// notification reply sent to the server (GUI)
type NotificationReply struct {
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Code NotificationReplyCode `protobuf:"varint,2,opt,name=code,proto3,enum=protocol.NotificationReplyCode" json:"code,omitempty"`
Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
}
func (m *NotificationReply) Reset() { *m = NotificationReply{} }
func (m *NotificationReply) String() string { return proto.CompactTextString(m) }
func (*NotificationReply) ProtoMessage() {}
func (*NotificationReply) Descriptor() ([]byte, []int) {
return fileDescriptor_63867a62624c1283, []int{9}
}
func (m *NotificationReply) GetId() uint64 {
if m != nil {
return m.Id
}
return 0
}
func (m *NotificationReply) GetCode() NotificationReplyCode {
if m != nil {
return m.Code
}
return NotificationReplyCode_OK
}
func (m *NotificationReply) GetData() string {
if m != nil {
return m.Data
}
return ""
}
func init() {
proto.RegisterEnum("protocol.Action", Action_name, Action_value)
proto.RegisterEnum("protocol.NotificationReplyCode", NotificationReplyCode_name, NotificationReplyCode_value)
proto.RegisterType((*Event)(nil), "protocol.Event")
proto.RegisterType((*Statistics)(nil), "protocol.Statistics")
proto.RegisterType((*PingRequest)(nil), "protocol.PingRequest")
@ -422,6 +707,100 @@ func init() {
proto.RegisterType((*Connection)(nil), "protocol.Connection")
proto.RegisterType((*Operator)(nil), "protocol.Operator")
proto.RegisterType((*Rule)(nil), "protocol.Rule")
proto.RegisterType((*ClientConfig)(nil), "protocol.ClientConfig")
proto.RegisterType((*Notification)(nil), "protocol.Notification")
proto.RegisterType((*NotificationReply)(nil), "protocol.NotificationReply")
}
func init() {
proto.RegisterFile("ui.proto", fileDescriptor_63867a62624c1283)
}
var fileDescriptor_63867a62624c1283 = []byte{
// 1315 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xdd, 0x73, 0xd3, 0x46,
0x10, 0x8f, 0x1d, 0x5b, 0x96, 0xd6, 0x1f, 0x71, 0x16, 0x42, 0x55, 0xd3, 0x42, 0x30, 0xb4, 0xcd,
0x64, 0x3a, 0x99, 0x36, 0x30, 0x1d, 0x60, 0x60, 0x3a, 0xc6, 0x08, 0x70, 0x31, 0xb6, 0xe7, 0x42,
0xe8, 0xa3, 0x46, 0x1f, 0x87, 0xa3, 0x62, 0x24, 0x55, 0x77, 0x36, 0xf5, 0x7f, 0xd6, 0xa7, 0xbe,
0xf6, 0xa5, 0x8f, 0x7d, 0xea, 0x5f, 0xd2, 0xc7, 0xce, 0xdd, 0x49, 0x96, 0xe2, 0x7c, 0x74, 0xf2,
0x64, 0xed, 0xef, 0xb7, 0xbf, 0xd5, 0xee, 0x6a, 0xef, 0xd6, 0xa0, 0xcf, 0x83, 0x83, 0x38, 0x89,
0x78, 0x84, 0xba, 0xfc, 0xf1, 0xa2, 0x59, 0x77, 0x0e, 0x55, 0x6b, 0x41, 0x43, 0x8e, 0x08, 0x15,
0x1e, 0x7c, 0xa4, 0x66, 0x69, 0xb7, 0xb4, 0x67, 0x10, 0xf9, 0x8c, 0x0f, 0x00, 0xbc, 0x28, 0x0c,
0xa9, 0xc7, 0x83, 0x28, 0x34, 0xcb, 0xbb, 0xa5, 0xbd, 0xfa, 0xe1, 0xf5, 0x83, 0x4c, 0x7b, 0xd0,
0x5f, 0x71, 0xa4, 0xe0, 0x87, 0x5d, 0xa8, 0x24, 0xf3, 0x19, 0x35, 0x37, 0xa5, 0x7f, 0x2b, 0xf7,
0x27, 0xf3, 0x19, 0x25, 0x92, 0xeb, 0xfe, 0xa9, 0x03, 0x1c, 0x71, 0x87, 0x07, 0x8c, 0x07, 0x1e,
0xc3, 0xaf, 0xa0, 0xe5, 0x3b, 0xf4, 0x63, 0x14, 0xda, 0x0b, 0x9a, 0x30, 0xf1, 0x32, 0x95, 0x46,
0x53, 0xa1, 0xef, 0x14, 0x88, 0xd7, 0xa1, 0x2a, 0xd4, 0x4c, 0xa6, 0x52, 0x21, 0xca, 0xc0, 0x1b,
0xa0, 0xcd, 0x63, 0x99, 0xfb, 0xa6, 0x84, 0x53, 0x0b, 0xef, 0x42, 0xd3, 0x0f, 0x99, 0x9d, 0x50,
0x16, 0x47, 0x21, 0xa3, 0xcc, 0xac, 0x48, 0xba, 0xe1, 0x87, 0x8c, 0x64, 0x18, 0xee, 0x42, 0x3d,
0x4f, 0x9d, 0x99, 0x55, 0xe9, 0x52, 0x84, 0xd0, 0x84, 0x5a, 0x30, 0x0d, 0xa3, 0x84, 0xfa, 0xa6,
0x26, 0xd9, 0xcc, 0xc4, 0x0e, 0xe8, 0x8e, 0xe7, 0xd1, 0x98, 0x53, 0xdf, 0xac, 0x49, 0x6a, 0x65,
0x0b, 0x95, 0x9f, 0x44, 0x71, 0x4c, 0x7d, 0x53, 0x57, 0xaa, 0xd4, 0xc4, 0x9b, 0x60, 0x88, 0xbc,
0xed, 0x93, 0x80, 0x33, 0xd3, 0x50, 0x32, 0x01, 0xbc, 0x0a, 0x38, 0xc3, 0xdb, 0x50, 0x97, 0xe4,
0xc7, 0x80, 0x89, 0x8c, 0x41, 0xd2, 0x20, 0xa0, 0x37, 0x12, 0xc1, 0x27, 0xa0, 0xbb, 0x4b, 0x5b,
0xb6, 0xd4, 0xac, 0xef, 0x6e, 0xee, 0xd5, 0x0f, 0xef, 0xe4, 0x0d, 0xce, 0x3b, 0x7a, 0xf0, 0x6c,
0x39, 0x11, 0xa8, 0x15, 0xf2, 0x64, 0x49, 0x6a, 0xae, 0xb2, 0xf0, 0x19, 0x80, 0xbb, 0xb4, 0x1d,
0xdf, 0x4f, 0x28, 0x63, 0x66, 0x43, 0xea, 0xef, 0x5e, 0xa0, 0xef, 0x29, 0x2f, 0x15, 0xc1, 0x70,
0x33, 0x1b, 0x1f, 0x41, 0xcd, 0x5d, 0xda, 0x27, 0x11, 0xe3, 0x66, 0x53, 0x06, 0xd8, 0xbd, 0x20,
0xc0, 0xab, 0x88, 0x71, 0xa5, 0xd6, 0x5c, 0x69, 0xa4, 0xd2, 0x38, 0x4a, 0xb8, 0xd9, 0xba, 0x54,
0x3a, 0x89, 0x92, 0x5c, 0x2a, 0x0c, 0xfc, 0x01, 0x34, 0x77, 0x69, 0xcf, 0x03, 0xdf, 0xdc, 0x92,
0xca, 0xdb, 0x17, 0x28, 0x8f, 0x03, 0x5f, 0x09, 0xab, 0xae, 0x78, 0xc6, 0xd7, 0xd0, 0x74, 0x97,
0x36, 0xfd, 0x8d, 0x7a, 0x73, 0xee, 0xb8, 0x33, 0x6a, 0xb6, 0xa5, 0xfc, 0xeb, 0x0b, 0xe4, 0xd6,
0xca, 0x51, 0x45, 0x69, 0xb8, 0x05, 0x08, 0xbf, 0x01, 0x8d, 0x8a, 0xc3, 0xc2, 0xcc, 0x6d, 0x19,
0x65, 0x2b, 0x8f, 0x22, 0x0f, 0x11, 0x49, 0xe9, 0xce, 0x63, 0x68, 0x14, 0x3f, 0x00, 0xb6, 0x61,
0xf3, 0x03, 0x5d, 0xa6, 0x43, 0x2d, 0x1e, 0xc5, 0x28, 0x2f, 0x9c, 0xd9, 0x9c, 0x66, 0xa3, 0x2c,
0x8d, 0xc7, 0xe5, 0x87, 0xa5, 0xce, 0x13, 0x68, 0x9d, 0x6e, 0xfe, 0x95, 0xd4, 0x8f, 0xa0, 0x5e,
0xe8, 0xfc, 0xd5, 0xa5, 0xab, 0xce, 0x5f, 0x49, 0xfa, 0x10, 0x20, 0x6f, 0xfd, 0x95, 0x94, 0x3f,
0xc2, 0xf6, 0x99, 0xae, 0x5f, 0x25, 0x40, 0x77, 0x00, 0xf5, 0x49, 0x10, 0x4e, 0x09, 0xfd, 0x75,
0x4e, 0x19, 0xc7, 0x16, 0x94, 0x03, 0x5f, 0x2a, 0x2b, 0xa4, 0x1c, 0xf8, 0xb8, 0x0f, 0x55, 0xc6,
0x1d, 0xce, 0xce, 0xde, 0x5e, 0xf9, 0x77, 0x27, 0xca, 0xa5, 0x7b, 0x13, 0x0c, 0x15, 0x2a, 0x9e,
0x2d, 0xd7, 0x03, 0x75, 0xff, 0xda, 0x04, 0xc8, 0x2f, 0x3c, 0x71, 0xf6, 0xb3, 0x48, 0x69, 0x9e,
0x2b, 0x1b, 0x77, 0x40, 0x63, 0x89, 0x67, 0x07, 0xb1, 0x7c, 0xa9, 0x41, 0xaa, 0x2c, 0xf1, 0x06,
0x31, 0x7e, 0x0e, 0xba, 0x80, 0xe5, 0xf8, 0x8b, 0x9b, 0xaa, 0x49, 0x6a, 0x2c, 0xf1, 0xe4, 0x74,
0xef, 0x80, 0xe6, 0x33, 0x2e, 0x14, 0x15, 0xa5, 0xf0, 0x19, 0x57, 0x0a, 0x01, 0xcb, 0xb3, 0x56,
0x95, 0x44, 0xcd, 0x67, 0x5c, 0x1e, 0xa5, 0x94, 0x92, 0xc1, 0x34, 0x15, 0xcc, 0x67, 0x5c, 0x06,
0xfb, 0x0c, 0x6a, 0x73, 0x46, 0x13, 0x3b, 0x50, 0xb7, 0x52, 0x93, 0x68, 0xc2, 0x1c, 0xf8, 0xf8,
0x25, 0x40, 0x9c, 0x44, 0x1e, 0x65, 0x4c, 0x70, 0xba, 0xe4, 0x8c, 0x14, 0x19, 0xf8, 0x78, 0x07,
0x1a, 0x19, 0x1d, 0x3b, 0xfc, 0x44, 0xde, 0x4d, 0x06, 0xa9, 0xa7, 0xd8, 0xc4, 0xe1, 0x27, 0xe2,
0x7a, 0xca, 0x5c, 0xbc, 0x4f, 0xbe, 0xbc, 0x9e, 0x0c, 0x92, 0x05, 0xed, 0x7f, 0x3a, 0x15, 0xc3,
0x49, 0xa6, 0x4c, 0x5e, 0x51, 0x79, 0x8c, 0x5e, 0x32, 0x65, 0x68, 0xe5, 0x31, 0x68, 0xb8, 0x48,
0x2f, 0xa1, 0x7b, 0xe7, 0x6d, 0x95, 0x83, 0x89, 0xf2, 0xb3, 0xc2, 0x85, 0x3a, 0x8d, 0xd9, 0x9b,
0xac, 0x70, 0xd1, 0x79, 0x0a, 0x5b, 0x6b, 0xf4, 0xff, 0x8d, 0x8d, 0x51, 0x1c, 0x9b, 0x5f, 0x40,
0x1f, 0xc7, 0x34, 0x71, 0x78, 0x94, 0xc8, 0xd5, 0xb7, 0x8c, 0xf3, 0xd5, 0xb7, 0x8c, 0xa9, 0xb8,
0xbf, 0x23, 0xc1, 0x87, 0x7e, 0xaa, 0xcd, 0x4c, 0xe1, 0xed, 0x3b, 0xdc, 0x91, 0x9f, 0xd0, 0x20,
0xf2, 0x19, 0xbf, 0x00, 0x83, 0xd1, 0x90, 0x05, 0x3c, 0x58, 0x50, 0xf9, 0x09, 0x75, 0x92, 0x03,
0xdd, 0xdf, 0x4b, 0x50, 0x11, 0xbb, 0x4f, 0x48, 0x43, 0x27, 0xdf, 0xb1, 0xe2, 0x59, 0xbc, 0x88,
0x86, 0x62, 0xf4, 0xd5, 0x8b, 0x74, 0x92, 0x99, 0x78, 0x4b, 0x7c, 0x2e, 0xea, 0x51, 0x9f, 0x86,
0x9e, 0xda, 0x6d, 0x3a, 0x29, 0x20, 0x62, 0xef, 0x39, 0x6a, 0x33, 0xab, 0xa1, 0x49, 0x2d, 0x31,
0x9a, 0xfe, 0x3c, 0x71, 0x24, 0xa3, 0xa6, 0x66, 0x65, 0xe3, 0x01, 0xe8, 0x51, 0x5a, 0xb6, 0x1c,
0x9b, 0xfa, 0x21, 0xe6, 0x9d, 0xcf, 0x1a, 0x42, 0x56, 0x3e, 0xdd, 0xbf, 0x4b, 0xd0, 0xe8, 0xcf,
0x02, 0x1a, 0xf2, 0x7e, 0x14, 0xbe, 0x0f, 0xa6, 0x67, 0xce, 0x57, 0x56, 0x52, 0xf9, 0x74, 0x49,
0xd9, 0x1a, 0x57, 0x4d, 0xca, 0x4c, 0xfc, 0x16, 0xb6, 0x03, 0xf6, 0x22, 0x48, 0xe8, 0x27, 0x67,
0x36, 0x23, 0xf3, 0x30, 0x0c, 0xc2, 0x69, 0xda, 0xaf, 0xb3, 0x84, 0x28, 0xd0, 0x93, 0x6f, 0x4d,
0xcb, 0x48, 0x2d, 0x51, 0xe0, 0x2c, 0x9a, 0x0e, 0xe9, 0x82, 0xce, 0xd2, 0xd9, 0x5f, 0xd9, 0x78,
0x2f, 0xfb, 0x8b, 0x50, 0x93, 0x73, 0xb5, 0xfe, 0xef, 0x43, 0x91, 0xdd, 0x3f, 0x4a, 0xd0, 0x18,
0x45, 0x3c, 0x78, 0x1f, 0x78, 0xaa, 0x2f, 0xeb, 0x65, 0xdd, 0x02, 0xf0, 0x64, 0xd9, 0xa3, 0xbc,
0xb8, 0x02, 0x22, 0x78, 0x46, 0x93, 0x05, 0x4d, 0x24, 0xaf, 0xaa, 0x2c, 0x20, 0x78, 0x2f, 0x1d,
0x29, 0x51, 0x5b, 0xeb, 0xb0, 0x9d, 0x67, 0xd1, 0x53, 0xff, 0x97, 0xd4, 0x90, 0x65, 0xa3, 0x54,
0x2d, 0x8c, 0xd2, 0xaa, 0x00, 0xed, 0xb2, 0x02, 0x66, 0xb0, 0x5d, 0xcc, 0xff, 0xdc, 0x2b, 0x0b,
0xef, 0x43, 0xc5, 0x8b, 0x7c, 0x95, 0x7e, 0xab, 0xb8, 0x31, 0xcf, 0x48, 0xfb, 0x91, 0x4f, 0x89,
0x74, 0x3e, 0x6f, 0xbc, 0xf7, 0xff, 0x29, 0x81, 0xa6, 0x12, 0x47, 0x1d, 0x2a, 0xa3, 0xf1, 0xc8,
0x6a, 0x6f, 0xe0, 0x36, 0x34, 0x87, 0xe3, 0xde, 0x73, 0xfb, 0xc5, 0x80, 0x58, 0x3f, 0xf7, 0x86,
0xc3, 0x76, 0x09, 0xaf, 0xc1, 0xd6, 0xf1, 0xe8, 0x34, 0x58, 0x16, 0x7e, 0xfd, 0x57, 0xbd, 0xd1,
0x4b, 0xcb, 0xee, 0x8f, 0x47, 0x2f, 0x06, 0x2f, 0xdb, 0x9b, 0xb8, 0x05, 0x75, 0x6b, 0xd4, 0x7b,
0x36, 0xb4, 0x6c, 0x72, 0x3c, 0xb4, 0xda, 0x15, 0x6c, 0x43, 0xe3, 0xf9, 0xe0, 0x28, 0x47, 0xaa,
0xc2, 0xe5, 0xb9, 0x35, 0xb4, 0xde, 0xa6, 0x80, 0x26, 0x80, 0x34, 0x8c, 0x04, 0x6a, 0xd8, 0x04,
0x63, 0x38, 0x7e, 0x69, 0x0f, 0xad, 0x77, 0xd6, 0xb0, 0xad, 0x8b, 0xc4, 0x8e, 0xde, 0x8e, 0x27,
0x6d, 0x43, 0x64, 0xf1, 0x66, 0x3c, 0x1a, 0xbc, 0x1d, 0x13, 0x7b, 0x42, 0xc6, 0x7d, 0xeb, 0xe8,
0xa8, 0x0d, 0x68, 0xc2, 0x75, 0x41, 0xdb, 0xeb, 0x4c, 0x7d, 0x7f, 0x1f, 0x76, 0xce, 0xed, 0x07,
0x6a, 0x50, 0x1e, 0xbf, 0x6e, 0x6f, 0xa0, 0x01, 0x55, 0x8b, 0x90, 0x31, 0x69, 0x97, 0x0e, 0xff,
0x2d, 0x41, 0xf9, 0x78, 0x80, 0x0f, 0xa0, 0x22, 0x16, 0x05, 0xee, 0xe4, 0x2d, 0x2d, 0xec, 0xa0,
0xce, 0xb5, 0x75, 0x38, 0x9e, 0x2d, 0xbb, 0x1b, 0xf8, 0x3d, 0xd4, 0x7a, 0xec, 0x83, 0xbc, 0x08,
0xce, 0xfd, 0x13, 0xdd, 0x59, 0xfb, 0xd6, 0xdd, 0x0d, 0x7c, 0x0a, 0xc6, 0xd1, 0xdc, 0x65, 0x5e,
0x12, 0xb8, 0x14, 0x6f, 0x14, 0x44, 0x85, 0x23, 0xd9, 0xb9, 0x00, 0xef, 0x6e, 0xe0, 0x4f, 0xd0,
0x2c, 0x96, 0xc6, 0xf0, 0xe6, 0x25, 0x33, 0x50, 0x8c, 0x53, 0x24, 0xbb, 0x1b, 0x7b, 0xa5, 0xef,
0x4a, 0xae, 0x26, 0xc9, 0xfb, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0xcb, 0xf1, 0x95, 0x55, 0x45,
0x0c, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -432,11 +811,14 @@ var _ grpc.ClientConn
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for UI service
// UIClient is the client API for UI service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type UIClient interface {
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingReply, error)
AskRule(ctx context.Context, in *Connection, opts ...grpc.CallOption) (*Rule, error)
Subscribe(ctx context.Context, in *ClientConfig, opts ...grpc.CallOption) (*ClientConfig, error)
Notifications(ctx context.Context, opts ...grpc.CallOption) (UI_NotificationsClient, error)
}
type uIClient struct {
@ -449,7 +831,7 @@ func NewUIClient(cc *grpc.ClientConn) UIClient {
func (c *uIClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingReply, error) {
out := new(PingReply)
err := grpc.Invoke(ctx, "/protocol.UI/Ping", in, out, c.cc, opts...)
err := c.cc.Invoke(ctx, "/protocol.UI/Ping", in, out, opts...)
if err != nil {
return nil, err
}
@ -458,18 +840,76 @@ func (c *uIClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallO
func (c *uIClient) AskRule(ctx context.Context, in *Connection, opts ...grpc.CallOption) (*Rule, error) {
out := new(Rule)
err := grpc.Invoke(ctx, "/protocol.UI/AskRule", in, out, c.cc, opts...)
err := c.cc.Invoke(ctx, "/protocol.UI/AskRule", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for UI service
func (c *uIClient) Subscribe(ctx context.Context, in *ClientConfig, opts ...grpc.CallOption) (*ClientConfig, error) {
out := new(ClientConfig)
err := c.cc.Invoke(ctx, "/protocol.UI/Subscribe", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *uIClient) Notifications(ctx context.Context, opts ...grpc.CallOption) (UI_NotificationsClient, error) {
stream, err := c.cc.NewStream(ctx, &_UI_serviceDesc.Streams[0], "/protocol.UI/Notifications", opts...)
if err != nil {
return nil, err
}
x := &uINotificationsClient{stream}
return x, nil
}
type UI_NotificationsClient interface {
Send(*NotificationReply) error
Recv() (*Notification, error)
grpc.ClientStream
}
type uINotificationsClient struct {
grpc.ClientStream
}
func (x *uINotificationsClient) Send(m *NotificationReply) error {
return x.ClientStream.SendMsg(m)
}
func (x *uINotificationsClient) Recv() (*Notification, error) {
m := new(Notification)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// UIServer is the server API for UI service.
type UIServer interface {
Ping(context.Context, *PingRequest) (*PingReply, error)
AskRule(context.Context, *Connection) (*Rule, error)
Subscribe(context.Context, *ClientConfig) (*ClientConfig, error)
Notifications(UI_NotificationsServer) error
}
// UnimplementedUIServer can be embedded to have forward compatible implementations.
type UnimplementedUIServer struct {
}
func (*UnimplementedUIServer) Ping(ctx context.Context, req *PingRequest) (*PingReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
}
func (*UnimplementedUIServer) AskRule(ctx context.Context, req *Connection) (*Rule, error) {
return nil, status.Errorf(codes.Unimplemented, "method AskRule not implemented")
}
func (*UnimplementedUIServer) Subscribe(ctx context.Context, req *ClientConfig) (*ClientConfig, error) {
return nil, status.Errorf(codes.Unimplemented, "method Subscribe not implemented")
}
func (*UnimplementedUIServer) Notifications(srv UI_NotificationsServer) error {
return status.Errorf(codes.Unimplemented, "method Notifications not implemented")
}
func RegisterUIServer(s *grpc.Server, srv UIServer) {
@ -512,6 +952,50 @@ func _UI_AskRule_Handler(srv interface{}, ctx context.Context, dec func(interfac
return interceptor(ctx, in, info, handler)
}
func _UI_Subscribe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClientConfig)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UIServer).Subscribe(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/protocol.UI/Subscribe",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UIServer).Subscribe(ctx, req.(*ClientConfig))
}
return interceptor(ctx, in, info, handler)
}
func _UI_Notifications_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(UIServer).Notifications(&uINotificationsServer{stream})
}
type UI_NotificationsServer interface {
Send(*Notification) error
Recv() (*NotificationReply, error)
grpc.ServerStream
}
type uINotificationsServer struct {
grpc.ServerStream
}
func (x *uINotificationsServer) Send(m *Notification) error {
return x.ServerStream.SendMsg(m)
}
func (x *uINotificationsServer) Recv() (*NotificationReply, error) {
m := new(NotificationReply)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
var _UI_serviceDesc = grpc.ServiceDesc{
ServiceName: "protocol.UI",
HandlerType: (*UIServer)(nil),
@ -524,66 +1008,18 @@ var _UI_serviceDesc = grpc.ServiceDesc{
MethodName: "AskRule",
Handler: _UI_AskRule_Handler,
},
{
MethodName: "Subscribe",
Handler: _UI_Subscribe_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "Notifications",
Handler: _UI_Notifications_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "ui.proto",
}
func init() { proto.RegisterFile("ui.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 838 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0x6d, 0x6f, 0xdc, 0x44,
0x10, 0x6e, 0xee, 0xc5, 0x67, 0xcf, 0xbd, 0xb4, 0x5d, 0x1a, 0x58, 0xae, 0x42, 0xbd, 0xba, 0x02,
0x22, 0x3e, 0x9c, 0x44, 0xa8, 0x50, 0x5b, 0x55, 0x42, 0x29, 0x8a, 0xd4, 0x13, 0x20, 0xa2, 0x45,
0xe5, 0xab, 0x65, 0x7b, 0x57, 0x89, 0xd5, 0x8b, 0xd7, 0xec, 0xac, 0x23, 0xfc, 0x85, 0x7f, 0xc3,
0x6f, 0xe1, 0x6f, 0xa1, 0x9d, 0xb5, 0xcf, 0x47, 0x4a, 0x2a, 0xdd, 0xa7, 0xdb, 0xe7, 0x79, 0xe6,
0x99, 0x1b, 0x8d, 0x67, 0x67, 0x21, 0xac, 0x8b, 0x75, 0x65, 0xb4, 0xd5, 0x2c, 0xa4, 0x9f, 0x5c,
0x6f, 0xe3, 0x1a, 0xc6, 0xe7, 0x37, 0xaa, 0xb4, 0x8c, 0xc1, 0xc8, 0x16, 0xd7, 0x8a, 0x1f, 0xad,
0x8e, 0x4e, 0x22, 0x41, 0x67, 0xf6, 0x1c, 0x20, 0xd7, 0x65, 0xa9, 0x72, 0x5b, 0xe8, 0x92, 0x0f,
0x56, 0x47, 0x27, 0xd3, 0xd3, 0x47, 0xeb, 0xce, 0xbb, 0xfe, 0x71, 0xa7, 0x89, 0xbd, 0x38, 0x16,
0xc3, 0xc8, 0xd4, 0x5b, 0xc5, 0x87, 0x14, 0xbf, 0xe8, 0xe3, 0x45, 0xbd, 0x55, 0x82, 0xb4, 0xf8,
0x9f, 0x10, 0xe0, 0x37, 0x9b, 0xda, 0x02, 0x6d, 0x91, 0x23, 0xfb, 0x12, 0x16, 0x32, 0x55, 0xd7,
0xba, 0x4c, 0x6e, 0x94, 0x41, 0xf7, 0x67, 0xbe, 0x8c, 0xb9, 0x67, 0x7f, 0xf7, 0x24, 0x7b, 0x04,
0x63, 0xe7, 0x46, 0x2a, 0x65, 0x24, 0x3c, 0x60, 0x9f, 0x42, 0x50, 0x57, 0x54, 0xfb, 0x90, 0xe8,
0x16, 0xb1, 0x67, 0x30, 0x97, 0x25, 0x26, 0x46, 0x61, 0xa5, 0x4b, 0x54, 0xc8, 0x47, 0x24, 0xcf,
0x64, 0x89, 0xa2, 0xe3, 0xd8, 0x0a, 0xa6, 0x7d, 0xe9, 0xc8, 0xc7, 0x14, 0xb2, 0x4f, 0x31, 0x0e,
0x93, 0xe2, 0xb2, 0xd4, 0x46, 0x49, 0x1e, 0x90, 0xda, 0x41, 0xb6, 0x84, 0x30, 0xcd, 0x73, 0x55,
0x59, 0x25, 0xf9, 0x84, 0xa4, 0x1d, 0x76, 0x2e, 0x69, 0x74, 0x55, 0x29, 0xc9, 0x43, 0xef, 0x6a,
0x21, 0x7b, 0x0c, 0x91, 0xab, 0x3b, 0xb9, 0x2a, 0x2c, 0xf2, 0xc8, 0xdb, 0x1c, 0xf1, 0xb6, 0xb0,
0xc8, 0x9e, 0xc0, 0x94, 0xc4, 0xeb, 0x02, 0x5d, 0xc5, 0x40, 0x32, 0x38, 0xea, 0x17, 0x62, 0xd8,
0x6b, 0x08, 0xb3, 0x26, 0xa1, 0x96, 0xf2, 0xe9, 0x6a, 0x78, 0x32, 0x3d, 0x7d, 0xda, 0x37, 0xb8,
0xef, 0xe8, 0xfa, 0x4d, 0x73, 0xe1, 0xd8, 0xf3, 0xd2, 0x9a, 0x46, 0x4c, 0x32, 0x8f, 0xd8, 0x1b,
0x80, 0xac, 0x49, 0x52, 0x29, 0x8d, 0x42, 0xe4, 0x33, 0xf2, 0x3f, 0xbb, 0xc3, 0x7f, 0xe6, 0xa3,
0x7c, 0x86, 0x28, 0xeb, 0x30, 0x7b, 0x09, 0x93, 0xac, 0x49, 0xae, 0x34, 0x5a, 0x3e, 0xa7, 0x04,
0xab, 0x3b, 0x12, 0xbc, 0xd5, 0x68, 0xbd, 0x3b, 0xc8, 0x08, 0xb4, 0xd6, 0x4a, 0x1b, 0xcb, 0x17,
0x1f, 0xb5, 0x5e, 0x68, 0xd3, 0x5b, 0x1d, 0x60, 0xdf, 0x43, 0x90, 0x35, 0x49, 0x5d, 0x48, 0x7e,
0x9f, 0x9c, 0x4f, 0xee, 0x70, 0xbe, 0x2b, 0xa4, 0x37, 0x8e, 0x33, 0x77, 0x66, 0x3f, 0xc1, 0x3c,
0x6b, 0x12, 0xf5, 0xa7, 0xca, 0x6b, 0x9b, 0x66, 0x5b, 0xc5, 0x1f, 0x90, 0xfd, 0xab, 0x3b, 0xec,
0xe7, 0xbb, 0x40, 0x9f, 0x65, 0x96, 0xed, 0x51, 0xec, 0x6b, 0x08, 0x94, 0xbb, 0x2c, 0xc8, 0x1f,
0x52, 0x96, 0xfb, 0x7d, 0x16, 0xba, 0x44, 0xa2, 0x95, 0x97, 0xaf, 0x60, 0xb6, 0xff, 0x01, 0xd8,
0x03, 0x18, 0xbe, 0x57, 0x4d, 0x3b, 0xd4, 0xee, 0xe8, 0x46, 0xf9, 0x26, 0xdd, 0xd6, 0xaa, 0x1b,
0x65, 0x02, 0xaf, 0x06, 0x2f, 0x8e, 0x96, 0xaf, 0x61, 0xf1, 0xdf, 0xe6, 0x1f, 0xe4, 0x7e, 0x09,
0xd3, 0xbd, 0xce, 0x1f, 0x6e, 0xdd, 0x75, 0xfe, 0x20, 0xeb, 0x0b, 0x80, 0xbe, 0xf5, 0x07, 0x39,
0x7f, 0x80, 0x87, 0x1f, 0x74, 0xfd, 0x90, 0x04, 0xf1, 0x06, 0xa6, 0x17, 0x45, 0x79, 0x29, 0xd4,
0x1f, 0xb5, 0x42, 0xcb, 0x16, 0x30, 0x28, 0x24, 0x39, 0x47, 0x62, 0x50, 0x48, 0xf6, 0x0d, 0x8c,
0xd1, 0xa6, 0x16, 0x3f, 0xdc, 0x5e, 0xfd, 0x77, 0x17, 0x3e, 0x24, 0x7e, 0x0c, 0x91, 0x4f, 0x55,
0x6d, 0x9b, 0xdb, 0x89, 0xe2, 0xbf, 0x07, 0x00, 0xfd, 0xc2, 0x73, 0x77, 0xbf, 0xcb, 0xd4, 0xd6,
0xb9, 0xc3, 0xec, 0x18, 0x02, 0x34, 0x79, 0x52, 0x54, 0xf4, 0xa7, 0x91, 0x18, 0xa3, 0xc9, 0x37,
0x15, 0xfb, 0x1c, 0x42, 0x47, 0xd3, 0xf8, 0xbb, 0x4d, 0x35, 0x17, 0x13, 0x34, 0x39, 0x4d, 0xf7,
0x31, 0x04, 0x12, 0xad, 0x73, 0x8c, 0xbc, 0x43, 0xa2, 0xf5, 0x0e, 0x47, 0xd3, 0x5d, 0x1b, 0x93,
0x30, 0x91, 0x68, 0xe9, 0x2a, 0xb5, 0x12, 0x25, 0x0b, 0x7c, 0x32, 0x89, 0x96, 0x92, 0x7d, 0x06,
0x93, 0x1a, 0x95, 0x49, 0x0a, 0xbf, 0x95, 0xe6, 0x22, 0x70, 0x70, 0x23, 0xd9, 0x17, 0x00, 0x95,
0xd1, 0xb9, 0x42, 0x74, 0x5a, 0x48, 0x5a, 0xd4, 0x32, 0x1b, 0xc9, 0x9e, 0xc2, 0xac, 0x93, 0xab,
0xd4, 0x5e, 0xd1, 0x6e, 0x8a, 0xc4, 0xb4, 0xe5, 0x2e, 0x52, 0x7b, 0xb5, 0x1f, 0x92, 0x9a, 0x4b,
0xb7, 0x9f, 0x86, 0x7b, 0x21, 0x67, 0xe6, 0x12, 0xe3, 0x9f, 0x21, 0xfc, 0xb5, 0x52, 0x26, 0xb5,
0xda, 0xd0, 0x9b, 0xd2, 0x54, 0xfd, 0x9b, 0xd2, 0x54, 0xca, 0x2d, 0x46, 0xed, 0xf4, 0x52, 0xb6,
0xdd, 0xe9, 0xa0, 0x8b, 0x96, 0xa9, 0x4d, 0xa9, 0x37, 0x91, 0xa0, 0x73, 0xfc, 0x17, 0x8c, 0xdc,
0xab, 0xe1, 0xb4, 0x32, 0xed, 0x5f, 0x27, 0x77, 0x76, 0x7b, 0x3f, 0xed, 0x5f, 0xa6, 0x48, 0xb4,
0xc8, 0x7d, 0x1a, 0x59, 0x9b, 0x94, 0x14, 0x9f, 0x6b, 0x87, 0xd9, 0x1a, 0x42, 0xdd, 0x56, 0x47,
0xad, 0x9e, 0x9e, 0xb2, 0x7e, 0x22, 0xba, 0xba, 0xc5, 0x2e, 0xe6, 0xf4, 0x1a, 0x06, 0xef, 0x36,
0xec, 0x39, 0x8c, 0xdc, 0x60, 0xb0, 0xe3, 0x3e, 0x76, 0x6f, 0xe6, 0x96, 0x9f, 0xdc, 0xa6, 0xab,
0x6d, 0x13, 0xdf, 0x63, 0xdf, 0xc2, 0xe4, 0x0c, 0xdf, 0x53, 0xf9, 0xff, 0xfb, 0x68, 0x2e, 0x6f,
0x3d, 0x8d, 0xf1, 0xbd, 0x2c, 0x20, 0xe2, 0xbb, 0x7f, 0x03, 0x00, 0x00, 0xff, 0xff, 0x81, 0x94,
0x16, 0x93, 0xaa, 0x07, 0x00, 0x00,
}

108
debian/changelog vendored Normal file
View file

@ -0,0 +1,108 @@
opensnitch (1.3.0~rc-2) UNRELEASED; urgency=medium
* Non-maintainer upload.
*
-- gustavo-iniguez-goya <gooffy1@gmail.com> Fri, 27 Nov 2020 23:21:31 +0100
opensnitch (1.3.0~rc-1) unstable; urgency=medium
* Non-maintainer upload.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Fri, 13 Nov 2020 00:51:34 +0100
opensnitch (1.2.0-1) unstable; urgency=medium
* Fixed memleaks.
* Sort rules by name
* Added priority field to rules.
* Other fixes
-- gustavo-iniguez-goya <gooffy1@gmail.com> Mon, 09 Nov 2020 22:55:13 +0100
opensnitch (1.0.1-1) unstable; urgency=medium
* Fixed app exit when IPv6 is not supported.
* Other fixes.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Thu, 30 Jul 2020 21:56:20 +0200
opensnitch (1.0.0-1) unstable; urgency=medium
* v1.0.0 released.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Thu, 16 Jul 2020 00:19:26 +0200
opensnitch (1.0.0rc11-1) unstable; urgency=medium
* Fixed multiple race conditions.
* Fixed CWD parsing when using audit proc monitor method.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Wed, 24 Jun 2020 00:10:38 +0200
opensnitch (1.0.0rc10-1) unstable; urgency=medium
* Fixed checking UID functions availability.
* Improved process path parsing.
* Fixed applying config from the UI.
* Fixed default log level.
* Gather CWD and process environment vars.
* Increase default timeout when asking for a rule.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Sat, 13 Jun 2020 18:45:02 +0200
opensnitch (1.0.0rc9-1) unstable; urgency=medium
* Ignore malformed rules from loading.
* Allow to modify and add rules from the UI.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Sun, 17 May 2020 18:18:24 +0200
opensnitch (1.0.0rc8) unstable; urgency=medium
* Allow to change settings from the UI.
* Improved connection handling with the UI.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Wed, 29 Apr 2020 21:52:27 +0200
opensnitch (1.0.0rc7-1) unstable; urgency=medium
* Stability, performance and realiability improvements.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Sun, 12 Apr 2020 23:25:41 +0200
opensnitch (1.0.0rc6-1) unstable; urgency=medium
* Fixed iptables rules deletion.
* Improved PIDs cache.
* Added audit process monitoring method.
* Added logrotate file.
* Added default configuration file.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Sun, 08 Mar 2020 20:47:58 +0100
opensnitch (1.0.0rc-5) unstable; urgency=medium
* Fixed netlink socket querying.
* Added check to reload firewall rules if missing.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Mon, 24 Feb 2020 19:55:06 +0100
opensnitch (1.0.0rc-3) unstable; urgency=medium
* @see: https://github.com/gustavo-iniguez-goya/opensnitch/releases
-- gustavo-iniguez-goya <gooffy1@gmail.com> Tue, 18 Feb 2020 10:09:45 +0100
opensnitch (1.0.0rc-2) unstable; urgency=medium
* UI minor changes
* Expand deb package compatibility.
-- gustavo-iniguez-goya <gooffy1@gmail.com> Wed, 05 Feb 2020 21:50:20 +0100
opensnitch (1.0.0rc-1) unstable; urgency=medium
* Initial release
-- gustavo-iniguez-goya <gooffy1@gmail.com> Fri, 22 Nov 2019 01:14:08 +0100

49
debian/control vendored Normal file
View file

@ -0,0 +1,49 @@
Source: opensnitch
Maintainer: Debian Go Packaging Team <team+pkg-go@tracker.debian.org>
Uploaders:
Gustavo Iniguez Goya <gooffy@gmail.com>,
Section: devel
Testsuite: autopkgtest-pkg-go
Priority: optional
Build-Depends:
debhelper-compat (= 12),
debhelper (>= 9),
dh-systemd (>= 1.5),
dh-golang,
golang-any,
golang-github-vishvananda-netlink-dev,
golang-github-evilsocket-ftrace-dev,
golang-github-google-gopacket-dev,
golang-github-fsnotify-fsnotify-dev,
golang-golang-x-net-dev,
golang-google-grpc-dev,
golang-goprotobuf-dev,
pkg-config,
libnetfilter-queue-dev,
libmnl-dev
Standards-Version: 4.4.0
Vcs-Browser: https://salsa.debian.org/go-team/packages/opensnitch
Vcs-Git: https://salsa.debian.org/go-team/packages/opensnitch.git
Homepage: https://github.com/gustavo-iniguez-goya/opensnitch
Rules-Requires-Root: no
XS-Go-Import-Path: github.com/gustavo-iniguez-goya/opensnitch
Package: opensnitch
Section: net
Architecture: any
Depends:
${misc:Depends}, ${shlibs:Depends},
Built-Using: ${misc:Built-Using}
Description: GNU/Linux firewall application
OpenSnitch is a GNU/Linux firewall application.
Whenever a program makes a connection, it'll prompt the user to allow or deny
it.
.
The user can decide if block the outgoing connection based on properties of
the connection: by port, by uid, by dst ip, by program or a combination
of them.
.
These rules can last forever, until the app restart or just one time.
.
The GUI allows the user to view live outgoing connections, as well as search
by process, user, host or port.

31
debian/copyright vendored Normal file
View file

@ -0,0 +1,31 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Source: https://github.com/gustavo-iniguez-goya/opensnitch
Upstream-Name: opensnitch
Files-Excluded:
Godeps/_workspace
Files: *
Copyright:
2017-2018 evilsocket
2019-2020 Gustavo Iñiguez Goia
Comment: Debian packaging is licensed under the same terms as upstream
License: GPL-3.0
This program is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later
version.
.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more
details.
.
You should have received a copy of the GNU General Public
License along with this program. If not, If not, see
http://www.gnu.org/licenses/.
.
On Debian systems, the full text of the GNU General Public
License version 3 can be found in the file
'/usr/share/common-licenses/GPL-3'.

2
debian/gbp.conf vendored Normal file
View file

@ -0,0 +1,2 @@
[DEFAULT]
pristine-tar = True

27
debian/gitlab-ci.yml vendored Normal file
View file

@ -0,0 +1,27 @@
# auto-generated, DO NOT MODIFY.
# The authoritative copy of this file lives at:
# https://salsa.debian.org/go-team/ci/blob/master/config/gitlabciyml.go
# TODO: publish under debian-go-team/ci
image: stapelberg/ci2
test_the_archive:
artifacts:
paths:
- before-applying-commit.json
- after-applying-commit.json
script:
# Create an overlay to discard writes to /srv/gopath/src after the build:
- "rm -rf /cache/overlay/{upper,work}"
- "mkdir -p /cache/overlay/{upper,work}"
- "mount -t overlay overlay -o lowerdir=/srv/gopath/src,upperdir=/cache/overlay/upper,workdir=/cache/overlay/work /srv/gopath/src"
- "export GOPATH=/srv/gopath"
- "export GOCACHE=/cache/go"
# Build the world as-is:
- "ci-build -exemptions=/var/lib/ci-build/exemptions.json > before-applying-commit.json"
# Copy this package into the overlay:
- "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'"
- "pgt-gopath -dsc /tmp/export/*.dsc"
# Rebuild the world:
- "ci-build -exemptions=/var/lib/ci-build/exemptions.json > after-applying-commit.json"
- "ci-diff before-applying-commit.json after-applying-commit.json"

78
debian/opensnitch.init vendored Normal file
View file

@ -0,0 +1,78 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: opensnitchd
# Required-Start: $network $local_fs
# Required-Stop: $network $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: opensnitchd daemon
# Description: opensnitch application firewall
### END INIT INFO
NAME=opensnitchd
PIDDIR=/var/run/$NAME
OPENSNITCHDPID=$PIDDIR/$NAME.pid
# clear conflicting settings from the environment
unset TMPDIR
test -x /usr/bin/$NAME || exit 0
. /lib/lsb/init-functions
case $1 in
start)
log_daemon_msg "Starting opensnitch daemon" $NAME
if [ ! -d /etc/$NAME/rules ]; then
mkdir -p /etc/$NAME/rules &>/dev/null
fi
# Make sure we have our PIDDIR, even if it's on a tmpfs
install -o root -g root -m 755 -d $PIDDIR
if ! start-stop-daemon --start --quiet --oknodo --pidfile $OPENSNITCHDPID --background --exec /usr/bin/$NAME -- -rules-path /etc/$NAME/rules; then
log_end_msg 1
exit 1
fi
log_end_msg 0
;;
stop)
log_daemon_msg "Stopping $NAME daemon" $NAME
start-stop-daemon --stop --quiet --signal QUIT --name $NAME
# Wait a little and remove stale PID file
sleep 1
if [ -f $OPENSNITCHDPID ] && ! ps h `cat $OPENSNITCHDPID` > /dev/null
then
rm -f $OPENSNITCHDPID
fi
log_end_msg 0
;;
reload)
log_daemon_msg "Reloading $NAME" $NAME
start-stop-daemon --stop --quiet --signal HUP --pidfile $OPENSNITCHDPID
log_end_msg 0
;;
restart|force-reload)
$0 stop
sleep 1
$0 start
;;
status)
status_of_proc /usr/bin/$NAME $NAME
exit $?
;;
*)
echo "Usage: /etc/init.d/opensnitchd {start|stop|reload|restart|force-reload|status}"
exit 1
;;
esac
exit 0

2
debian/opensnitch.install vendored Normal file
View file

@ -0,0 +1,2 @@
daemon/default-config.json etc/opensnitchd/
daemon/system-fw.json etc/opensnitchd/

13
debian/opensnitch.logrotate vendored Normal file
View file

@ -0,0 +1,13 @@
/var/log/opensnitchd.log {
rotate 7
# order of the fields is important
maxsize 50M
# we need this option in order to keep logging
copytruncate
missingok
notifempty
delaycompress
compress
create 640 root root
weekly
}

16
debian/opensnitch.service vendored Normal file
View file

@ -0,0 +1,16 @@
[Unit]
Description=OpenSnitch is a GNU/Linux application firewall.
Documentation=https://github.com/gustavo-iniguez-goya/opensnitch/wiki
Wants=network.target
After=network.target
[Service]
Type=simple
PermissionsStartOnly=true
ExecStartPre=/bin/mkdir -p /etc/opensnitchd/rules
ExecStart=/usr/bin/opensnitchd -rules-path /etc/opensnitchd/rules
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

8
debian/postinst vendored Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
# FIXME: remove in favor of dh_installsystemd
systemctl unmask opensnitch.service
systemctl enable opensnitch.service
service opensnitch restart

6
debian/prerm vendored Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
set -e
service opensnitch stop || true
systemctl disable opensnitch.service || true

13
debian/rules vendored Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
export DESTDIR = "debian/opensnitch"
override_dh_dwz:
dwz -- $(DESTDIR)/usr/bin/daemon || true
mv $(DESTDIR)/usr/bin/daemon $(DESTDIR)/usr/bin/opensnitchd
override_dh_installsystemd:
dh_installsystemd --restart-after-upgrade
%:
dh $@ --builddirectory=_build --buildsystem=golang --with=golang

1
debian/source/format vendored Normal file
View file

@ -0,0 +1 @@
3.0 (quilt)

4
debian/watch vendored Normal file
View file

@ -0,0 +1,4 @@
version=4
opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/opensnitch-\$1\.tar\.gz/,\
uversionmangle=s/(\d)[_\.\-\+]?(RC|rc|pre|dev|beta|alpha)[.]?(\d*)$/\$1~\$2\$3/ \
https://github.com/gustavo-iniguez-goya/opensnitch/tags .*/v?(\d\S*)\.tar\.gz

View file

@ -5,6 +5,8 @@ package protocol;
service UI {
rpc Ping(PingRequest) returns (PingReply) {}
rpc AskRule (Connection) returns (Rule) {}
rpc Subscribe (ClientConfig) returns (ClientConfig) {}
rpc Notifications (stream NotificationReply) returns (stream Notification) {}
}
message Event {
@ -52,18 +54,73 @@ message Connection {
uint32 user_id = 7;
uint32 process_id = 8;
string process_path = 9;
repeated string process_args = 10;
string process_cwd = 10;
repeated string process_args = 11;
map<string, string> process_env = 12;
}
message Operator {
string type = 1;
string operand = 2;
string data = 3;
bool sensitive = 4;
}
message Rule {
string name = 1;
string action = 2;
string duration = 3;
Operator operator = 4;
bool enabled = 2;
bool precedence = 3;
string action = 4;
string duration = 5;
Operator operator = 6;
}
enum Action {
NONE = 0;
LOAD_FIREWALL = 1;
UNLOAD_FIREWALL = 2;
CHANGE_CONFIG = 3;
ENABLE_RULE = 4;
DISABLE_RULE = 5;
DELETE_RULE = 6;
CHANGE_RULE = 7;
LOG_LEVEL = 8;
STOP = 9;
MONITOR_PROCESS = 10;
STOP_MONITOR_PROCESS = 11;
}
// client configuration sent on Subscribe()
message ClientConfig {
uint64 id = 1;
string name = 2;
string version = 3;
bool isFirewallRunning = 4;
// daemon configuration as json string
string config = 5;
uint32 logLevel = 6;
repeated Rule rules = 7;
}
// notification sent to the clients (daemons)
message Notification {
uint64 id = 1;
string clientName = 2;
string serverName = 3;
// CHANGE_CONFIG: 2, data: {"default_timeout": 1, ...}
Action type = 4;
string data = 5;
repeated Rule rules = 6;
}
// notification reply sent to the server (GUI)
message NotificationReply {
uint64 id = 1;
NotificationReplyCode code = 2;
string data = 3;
}
enum NotificationReplyCode {
OK = 0;
ERROR = 1;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

Before

Width:  |  Height:  |  Size: 443 KiB

After

Width:  |  Height:  |  Size: 443 KiB

82
server/api/api.go Normal file
View file

@ -0,0 +1,82 @@
package api
/*
Package api holds the functionality to interact with opensnitch
nodes/clients.
If a new client wants to connect to the server (UI, proxy2db, ...),
it must follow these steps:
1. Subscribe() - tell the server who we are.
2. Notifications() - open and keep opened a communication channel
3. Ping() - ping the server every n seconds, and send the statistics.
4. AskRule() - called when a new outgoing connection is about to be established.
*/
import (
"net"
"time"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
type server struct {
srv *protocol.UIServer
apiClient *Client
}
// Ping receives every second the stats of a node.
func (s *server) Ping(ctx context.Context, ping *protocol.PingRequest) (*protocol.PingReply, error) {
s.apiClient.UpdateStats(ctx, ping.Stats)
return &protocol.PingReply{Id: ping.Id}, nil
}
// AskRule waits for action on a new outgoing connection.
// If it not answered, after n seconds it'll apply the default action
func (s *server) AskRule(ctx context.Context, con *protocol.Connection) (*protocol.Rule, error) {
resultChan := s.apiClient.AskRule(con)
select {
case rule := <-resultChan:
return rule, nil
// XXX: the daemon as of v1.0.1 has this timeout hardcoded
case <-time.After(120 * time.Second):
// TODO: apply default action
return nil, nil
}
}
// Subscribe receives connections from new nodes with their configuration.
// The nodes are saved to keep a list of connected nodes.
func (s *server) Subscribe(ctx context.Context, clientConf *protocol.ClientConfig) (*protocol.ClientConfig, error) {
s.apiClient.AddNewNode(ctx, clientConf)
return &protocol.ClientConfig{}, nil
}
// Notifications opens a permanent channel to send commands back to the nodes.
// This function can't return until the connection with the node is closed,
// in order to maintain the communication channel opened.
func (s *server) Notifications(streamChannel protocol.UI_NotificationsServer) error {
s.apiClient.OpenChannelWithNode(streamChannel)
return nil
}
// StartServer start listening for incoming nodes/clients.
func startServer(client *Client, proto, port string) {
sockFd, err := net.Listen(proto, port)
if err != nil {
log.Error("failed to listen on %s: %v", port, err)
}
// create a server instance
s := server{}
s.apiClient = client
grpcServer := grpc.NewServer()
protocol.RegisterUIServer(grpcServer, &s)
// start the server
if err := grpcServer.Serve(sockFd); err != nil {
log.Error("failed to listen for new nodes: %s", err)
}
}

132
server/api/client.go Normal file
View file

@ -0,0 +1,132 @@
package api
import (
"sync"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
"github.com/gustavo-iniguez-goya/opensnitch/server/api/nodes"
"golang.org/x/net/context"
)
// Client struct groups the API functionality to communicate with the nodes
type Client struct {
Lock sync.RWMutex
lastStats *protocol.Statistics
nodesChan chan bool
rulesInChan chan *protocol.Connection
rulesOutChan chan *protocol.Rule
}
// rules related constants
const (
ActionAllow = "allow"
ActionDeny = "deny"
RuleSimple = "simple"
RuleList = "list"
RuleRegexp = "regexp"
RuleOnce = "once"
Rule15s = "15s"
Rule30s = "30s"
Rule5m = "5m"
Rule1h = "1h"
RuleRestart = "until restart"
RuleAlways = "always"
FilterByPath = "process.path"
FilterByCommand = "process.command"
FilterByUserID = "user.id"
FilterByDstIP = "dest.ip"
FilterByDstPort = "dest.port"
FilterByDstHost = "dest.host"
)
// NewClient setups a new client and starts the server to listen for new nodes.
func NewClient(serverProto, serverPort string) *Client {
c := &Client{
nodesChan: make(chan bool),
rulesInChan: make(chan *protocol.Connection, 1),
rulesOutChan: make(chan *protocol.Rule, 1),
}
go startServer(c, serverProto, serverPort)
return c
}
// UpdateStats save latest stats received from a node.
func (c *Client) UpdateStats(ctx context.Context, stats *protocol.Statistics) {
if stats == nil {
return
}
c.Lock.Lock()
defer c.Lock.Unlock()
c.lastStats = stats
nodes.UpdateStats(ctx, stats)
}
// GetLastStats returns latest stasts from a node.
func (c *Client) GetLastStats() *protocol.Statistics {
c.Lock.RLock()
defer c.Lock.RUnlock()
// TODO: return last stats for a given node
return c.lastStats
}
// AskRule sends the connection details through a channel.
// A client must consume data on that channel, and send the response via the
// rulesOutChan channel.
func (c *Client) AskRule(con *protocol.Connection) chan *protocol.Rule {
c.rulesInChan <- con
return c.rulesOutChan
}
// AddNewNode adds a new node to the list of connected nodes.
func (c *Client) AddNewNode(ctx context.Context, nodeConf *protocol.ClientConfig) {
log.Info("AddNewNode: %s - %s, %v", nodeConf.Name, nodeConf.Version)
nodes.Add(ctx, nodeConf)
c.nodesChan <- true
}
// OpenChannelWithNode updates the node with the streaming channel.
// This channel is used to send notifications to the nodes (change debug level,
// stop/start interception, etc).
func (c *Client) OpenChannelWithNode(notificationsStream protocol.UI_NotificationsServer) {
log.Info("opening communication channel with new node...", notificationsStream)
node := nodes.SetNotificationsChannel(notificationsStream)
if node == nil {
log.Warning("node not found, channel comms not opened")
return
}
// XXX: go nodes.Channel(node) ?
for {
select {
case <-node.NotificationsStream.Context().Done():
log.Important("client.ChannelWithNode() Node exited: ", node.Addr())
goto Exit
case notif := <-node.GetNotifications():
log.Important("client.ChannelWithNode() sending notification:", notif)
node.NotificationsStream.Send(notif)
}
}
Exit:
node.Close()
return
}
// FIXME: remove when nodes implementation is done
func (c *Client) WaitForNodes() {
<-c.nodesChan
}
// WaitForRules returns the channel where we listen for new outgoing connections.
func (c *Client) WaitForRules() chan *protocol.Connection {
return c.rulesInChan
}
// AddNewRule sends a new rule to the node.
func (c *Client) AddNewRule(rule *protocol.Rule) {
c.rulesOutChan <- rule
}

3
server/api/go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/gustavo-iniguez-goya/opensnitch/server/api
go 1.14

87
server/api/nodes/node.go Normal file
View file

@ -0,0 +1,87 @@
package nodes
import (
"fmt"
"net"
"time"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
"golang.org/x/net/context"
"google.golang.org/grpc/peer"
)
// Status represents the current connectivity status of a node.
type Status string
// Statuses of a node.
var (
Online = Status(log.Bold(log.Green("online")))
Offline = Status(log.Bold(log.Red("offline")))
)
type node struct {
addr net.Addr
ctx context.Context
lastSeen time.Time
status Status
NotificationsStream protocol.UI_NotificationsServer
notificationsChannel chan *protocol.Notification
config *protocol.ClientConfig
stats *protocol.Statistics
}
// NewNode instanstiates a new node.
func NewNode(ctx context.Context, nodeConf *protocol.ClientConfig) *node {
p, _ := peer.FromContext(ctx)
log.Info("NewNode: %s - %s, %v", nodeConf.Name, nodeConf.Version, p.Addr)
return &node{
addr: p.Addr,
ctx: ctx,
lastSeen: time.Now(),
status: Online,
config: nodeConf,
notificationsChannel: make(chan *protocol.Notification, 1),
}
}
func (n *node) String() string {
return fmt.Sprintf("[%v] [%10s] %s - %s", n.lastSeen, n.addr, n.config.Name, n.config.Version)
}
// Addr returns the address of the node.
func (n *node) Addr() string {
return n.addr.String()
}
func (n *node) Close() {
n.status = Offline
}
func (n *node) Status() Status {
return n.status
}
// LastSeen returns the last time the node was seen by the server.
func (n *node) LastSeen() time.Time {
return n.lastSeen
}
// SendNotification to the node via the channel and grpc.ServerStream channel.
func (n *node) SendNotification(notif *protocol.Notification) {
n.notificationsChannel <- notif
}
func (n *node) UpdateStats(stats *protocol.Statistics) {
n.stats = stats
n.lastSeen = time.Now()
}
func (n *node) GetConfig() *protocol.ClientConfig {
return n.config
}
// GetNotifications returns the notifications channel.
func (n *node) GetNotifications() chan *protocol.Notification {
return n.notificationsChannel
}

98
server/api/nodes/nodes.go Normal file
View file

@ -0,0 +1,98 @@
package nodes
import (
"github.com/gustavo-iniguez-goya/opensnitch/daemon/log"
"github.com/gustavo-iniguez-goya/opensnitch/daemon/ui/protocol"
"golang.org/x/net/context"
"google.golang.org/grpc/peer"
)
type nodeStats struct {
events []*protocol.Event
n *node
}
var (
nodeList = make(map[string]*node)
statsList = make(map[string]*nodeStats)
)
// Add a new node the list of nodes.
func Add(ctx context.Context, nodeConf *protocol.ClientConfig) {
p := GetPeer(ctx)
addr := p.Addr.String()
nodeList[addr] = NewNode(ctx, nodeConf)
}
// SetNotificationsChannel sets the communication channel for a given node.
// https://github.com/grpc/grpc-go/blob/master/stream.go
func SetNotificationsChannel(notificationsStream protocol.UI_NotificationsServer) *node {
ctx := notificationsStream.Context()
addr := GetAddr(ctx)
// ctx.AddCallback() ?
if !isConnected(addr) {
log.Warning("nodes.SetNotificationsChannel() not found: %s", addr)
return nil
}
nodeList[addr].NotificationsStream = notificationsStream
return nodeList[addr]
}
// UpdateStats of a node.
func UpdateStats(ctx context.Context, stats *protocol.Statistics) {
addr := GetAddr(ctx)
if !isConnected(addr) {
log.Warning("nodes.UpdateStats() not found: %s", addr)
return
}
nodeList[addr].UpdateStats(stats)
}
// Delete a node from the list of nodes.
func Delete(n *node) bool {
n.Close()
delete(nodeList, n.Addr())
return true
}
// Get a node from the list of nodes.
func Get(addr string) *node {
return nodeList[addr]
}
// GetPeer gets the address:port of a node.
func GetPeer(ctx context.Context) *peer.Peer {
p, _ := peer.FromContext(ctx)
return p
}
// GetAddr of a node from the context
func GetAddr(ctx context.Context) string {
p := GetPeer(ctx)
return p.Addr.String()
}
// GetAll nodes.
func GetAll() *map[string]*node {
return &nodeList
}
// GetStats returns the stats of all nodes combined.
func GetStats() (stats []*protocol.Statistics) {
for addr, node := range *GetAll() {
println(addr, node)
}
return stats
}
// Total returns the number of saved nodes.
func Total() int {
return len(nodeList)
}
func isConnected(addr string) bool {
_, found := nodeList[addr]
return found
}

28
ui/LICENSE Normal file
View file

@ -0,0 +1,28 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Source: https://github.com/gustavo-iniguez-goya/opensnitch
Upstream-Name: python3-opensnitch-ui
Files: *
Copyright:
2017-2018 evilsocket
2019-2020 Gustavo Iñiguez Goia
Comment: Debian packaging is licensed under the same terms as upstream
License: GPL-3.0
This program is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later
version.
.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more
details.
.
You should have received a copy of the GNU General Public
License along with this program. If not, If not, see
http://www.gnu.org/licenses/.
.
On Debian systems, the full text of the GNU General Public
License version 3 can be found in the file
'/usr/share/common-licenses/GPL-3'.

2
ui/MANIFEST.in Normal file
View file

@ -0,0 +1,2 @@
recursive-include opensnitch/res *
include LICENSE

View file

@ -1,7 +1,7 @@
all: opensnitch/resources_rc.py
install:
@pip3 install .
@pip3 install --upgrade .
opensnitch/resources_rc.py: deps
@pyrcc5 -o opensnitch/resources_rc.py opensnitch/res/resources.qrc

View file

@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python3
from PyQt5 import QtWidgets, QtGui, QtCore
@ -15,6 +15,10 @@ from concurrent import futures
import grpc
dist_path = '/usr/lib/python3/dist-packages/'
if dist_path not in sys.path:
sys.path.append(dist_path)
from opensnitch.service import UIService
from opensnitch.config import Config
import opensnitch.version
@ -26,22 +30,33 @@ def on_exit():
server.stop(0)
sys.exit(0)
def supported_qt_version(major, medium, minor):
q = QtCore.QT_VERSION_STR.split(".")
return int(q[0]) >= major and int(q[1]) >= medium and int(q[2]) >= minor
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OpenSnitch UI service.')
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")
parser.add_argument("--config", dest="config", default="~/.opensnitch/ui-config.json", help="Path of the UI json configuration file.", metavar="FILE")
parser.add_argument("--max-clients", dest="serverWorkers", default=10, help="Max number of allowed clients (incoming connections).")
args = parser.parse_args()
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
app = QtWidgets.QApplication(sys.argv)
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
service = UIService(app, on_exit, args.config)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
if supported_qt_version(5,6,0):
try:
# NOTE: maybe we also need Qt::AA_UseHighDpiPixmaps
QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
except Exception:
pass
app = QtWidgets.QApplication(sys.argv)
service = UIService(app, on_exit)
# @doc: https://grpc.github.io/grpc/python/grpc.html#server-object
server = grpc.server(futures.ThreadPoolExecutor(max_workers=int(args.serverWorkers)))
add_UIServicer_to_server(service, server)
if args.socket.startswith("unix://"):
socket = args.socket[7:]
socket = os.path.abspath(socket)

116
ui/debian/changelog Normal file
View file

@ -0,0 +1,116 @@
opensnitch-ui (1.3.0~rc-2) UNRELEASED; urgency=medium
* Non-maintainer upload.
*
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Fri, 27 Nov 2020 23:22:08 +0100
opensnitch-ui (1.3.0~rc-1) unstable; urgency=medium
* Non-maintainer upload.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Fri, 20 Nov 2020 13:32:07 +0100
opensnitch-ui (1.2.0-1) unstable; urgency=medium
* Sort rules by name.
* Allow to set priority on rules.
* Rules are case-insensitive by default.
* Other fixes.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Mon, 09 Nov 2020 23:00:38 +0100
opensnitch-ui (1.0.1-1) unstable; urgency=medium
* Fixed crash when clicking on General tab columns.
* Added literal DstHost to the pop-up combo box.
* Shorten autogenerated rules names.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Tue, 28 Jul 2020 23:43:15 +0200
opensnitch-ui (1.0.0-1) unstable; urgency=medium
* v1.0.0 released.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Thu, 16 Jul 2020 00:20:19 +0200
opensnitch-ui (1.0.0rc11-1) unstable; urgency=medium
* Added CWD field.
* Fixed columns resizing/restoring.
* Fixed General tab fields filtering.
* Pop-up window: display process path if it's hidden.
* Display better regexp errors on the rules editor.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Wed, 24 Jun 2020 00:20:57 +0200
opensnitch-ui (1.0.0rc10-2) unstable; urgency=medium
* Fixed crash when selecting a user (closes #38).
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Wed, 17 Jun 2020 20:50:54 +0200
opensnitch-ui (1.0.0rc10-1) unstable; urgency=medium
* Allow to filter data in all tabs.
* Refresh rules list after deleting a rule.
* Fixed high CPU usage while showing a notification.
* Fixed columns sort order.
* Allow to delete rules in batch.
* Remember the columns size.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Sat, 13 Jun 2020 18:49:11 +0200
opensnitch-ui (1.0.0rc9-1) unstable; urgency=medium
* Added rules editor dialog.
* Restart UI upon starting a new X session.
* Allow to configure max clients from the cli.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Sun, 17 May 2020 18:19:38 +0200
opensnitch-ui (1.0.0rc8) unstable; urgency=medium
* Allow to change settings (daemon && UI) from the UI.
* Added Nodes view.
* Improved UI performance, specially when remote nodes connected.
* Fixed race condition when adding stats of remote nodes.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Wed, 29 Apr 2020 21:56:54 +0200
opensnitch-ui (1.0.0rc7-1) unstable; urgency=medium
* Added help menu.
* Added option to filter by command line.
* Fixed UI icons.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Sun, 12 Apr 2020 23:49:13 +0200
opensnitch-ui (1.0.0rc6-1) unstable; urgency=medium
* Fixed showing systray icon in Cinnamon.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Sun, 08 Mar 2020 20:50:52 +0100
opensnitch-ui (1.0.0rc5-1) unstable; urgency=medium
* Workaround for crash parsing non-utf8 desktop files.
* Fixed crash loading sqlite driver.
* Fixed HighDpi scaling.
* Fixed prompt layout.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Mon, 24 Feb 2020 19:56:01 +0100
opensnitch-ui (1.0.0rc3-1) unstable; urgency=medium
* Fixed regex patterns.
* Display alerts for not answered questions.
* Added option to allow/deny second level domains.
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Tue, 18 Feb 2020 10:14:59 +0100
opensnitch-ui (1.0.0rc2-1) unstable; urgency=low
* initial release
-- Gustavo Iñiguez Goia <gooffy1@gmail.com> Thu, 06 Feb 2020 00:20:02 +0100

1
ui/debian/compat Normal file
View file

@ -0,0 +1 @@
9

9
ui/debian/config Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh -e
. /usr/share/debconf/confmodule
# set default value, otherwise the question is not shown on first install
db_fset python3-opensnitch-ui/question1 seen false
db_input high python3-opensnitch-ui/question1 || true
db_go

26
ui/debian/control Normal file
View file

@ -0,0 +1,26 @@
Source: opensnitch-ui
Maintainer: Gustavo Iñiguez Goia <gooffy1@gmail.com>
Uploaders:
Gustavo Iniguez Goya <gooffy@gmail.com>,
Priority: optional
Homepage: https://github.com/gustavo-iniguez-goya/opensnitch
Build-Depends: python3-setuptools, python3-all, debhelper (>= 7.4.3), dh-python
Standards-Version: 3.9.1
Package: python3-opensnitch-ui
Architecture: all
Section: net
Depends:
debconf, libqt5sql5-sqlite, python3:any, python3-setuptools, python3-six, python3-pyqt5,
python3-pyqt5.qtsql, python3-pyinotify, python3-pip, whiptail | dialog
Description: opensnitch application firewall GUI
opensnitch-ui is a GUI for opensnitch written in Python.
It allows the user to view live outgoing connections, as well as search
for details of the intercepted connections.
.
The user can decide if block outgoing connections based on properties of
the connection: by port, by uid, by dst ip, by program or a combination
of them.
.
These rules can last forever, until restart the daemon or just one time.

28
ui/debian/copyright Normal file
View file

@ -0,0 +1,28 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Source: https://github.com/gustavo-iniguez-goya/opensnitch
Upstream-Name: opensnitch-ui
Files: *
Copyright:
2017-2018 evilsocket
2019-2020 Gustavo Iñiguez Goia
Comment: Debian packaging is licensed under the same terms as upstream
License: GPL-3.0
This program is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later
version.
.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more
details.
.
You should have received a copy of the GNU General Public
License along with this program. If not, If not, see
http://www.gnu.org/licenses/.
.
On Debian systems, the full text of the GNU General Public
License version 3 can be found in the file
'/usr/share/common-licenses/GPL-3'.

44
ui/debian/postinst Executable file
View file

@ -0,0 +1,44 @@
#!/bin/sh
set -e
. /usr/share/debconf/confmodule
install_pip_pkgs()
{
db_get python3-opensnitch-ui/question1
if [ -z "$RET" -o "$RET" = "true" -o "$RET" = "yes" ]; then
echo "Installing grpcio-tools..."
pip3 -q install grpcio-tools || echo "Unable to install grpcio, try it manually."
echo
echo "Installing slugify..."
pip3 -q install unicode_slugify || echo "Unable to install unicode_slugify, try it manually."
echo "Done."
else
echo "Not installing extra packages by user choice (debconf)"
fi
exit 0
}
for i in $(ls /home)
do
if grep -q /home/$i /etc/passwd ; then
path=/home/$i/.config/autostart/
if [ ! -d $path ]; then
mkdir -p $path
fi
if [ -f /usr/share/applications/opensnitch_ui.desktop ];then
ln -s /usr/share/applications/opensnitch_ui.desktop $path 2>/dev/null || true
fi
fi
done
gtk-update-icon-cache /usr/share/icons/hicolor/ || true
set +e
case "$1" in
configure)
install_pip_pkgs
;;
esac

34
ui/debian/postrm Executable file
View file

@ -0,0 +1,34 @@
#!/bin/sh
set -e
. /usr/share/debconf/confmodule
purge_files()
{
if [ -e /usr/share/debconf/confmodule ]; then
. /usr/share/debconf/confmodule
fi
for i in $(ls /home)
do
path=/home/$i/.config/
if [ -h $path/autostart/opensnitch_ui.desktop -o -f $path/autostart/opensnitch_ui.desktop ];then
rm -f $path/autostart/opensnitch_ui.desktop
fi
if [ -d $path/opensnitch/ ]; then
rm -rf $path/opensnitch/
fi
done
}
pkill -15 opensnitch-ui || true
db_purge
case "$1" in
purge)
purge_files
;;
remove)
db_purge
;;
esac

19
ui/debian/prerm Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
set -e
. /usr/share/debconf/confmodule
db_purge
case "$1" in
remove)
echo
echo " If you don't need them anymore, remember to uninstall unicode_slugify, grcpio-tools and protobuf:"
echo
echo " pip3 uninstall unicode_slugify"
echo " pip3 uninstall grcpio-tools"
echo " pip3 uninstall protobuf"
echo
;;
esac

31
ui/debian/rules Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/make -f
# This file was automatically generated by stdeb 0.9.0 at
# Thu, 06 Feb 2020 00:20:02 +0100
%:
dh $@ --with python3 --buildsystem=python_distutils
override_dh_auto_clean:
python3 setup.py clean -a
find . -name \*.pyc -exec rm {} \;
override_dh_auto_build:
python3 setup.py build --force
override_dh_auto_install:
python3 setup.py install --force --root=debian/python3-opensnitch-ui --no-compile -O0 --install-layout=deb
override_dh_python2:
dh_python2 --no-guessing-versions

1
ui/debian/source/format Normal file
View file

@ -0,0 +1 @@
3.0 (quilt)

1
ui/debian/source/options Normal file
View file

@ -0,0 +1 @@
extend-diff-ignore="\.egg-info$"

7
ui/debian/templates Normal file
View file

@ -0,0 +1,7 @@
Template: python3-opensnitch-ui/question1
Type: boolean
Description: Do you want to install them now?
OpenSnitch GUI needs to install manually 3 more packages using python3-pip:
.
unicode_slugify, grpcio-tools and protobuf.
.

106
ui/opensnitch-ui.spec Normal file
View file

@ -0,0 +1,106 @@
%define name opensnitch-ui
%define version 1.3.0rc2
%define unmangled_version 1.3.0rc2
%define release 1
%define __python python3
%define desktop_file opensnitch_ui.desktop
Summary: Prompt service and UI for the opensnitch application firewall.
Name: %{name}
Version: %{version}
Release: %{release}
Source0: %{name}-%{unmangled_version}.tar.gz
License: GPL-3.0
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Prefix: %{_prefix}
BuildArch: noarch
Vendor: Simone "evilsocket" Margaritelli <evilsocket@protonmail.com>
Url: https://github.com/evilsocket/opensnitch
Requires: python3, python3-pip, (python3-pyinotify or python3-inotify), python3-qt5
Recommends: (python3-slugify or python3-python-slugify), python3-protobuf >= 3.0
# avoid to depend on a particular python version
%global __requires_exclude ^python\\(abi\\) = 3\\..$
%description
GUI for the opensnitch application firewall
opensnitch-ui is a GUI for opensnitch written in Python.
It allows the user to view live outgoing connections, as well as search
to make connections.
.
The user can decide if block the outgoing connection based on properties of
the connection: by port, by uid, by dst ip, by program or a combination
of them.
.
These rules can last forever, until the app restart or just one time.
%prep
%setup -n %{name}-%{unmangled_version} -n %{name}-%{unmangled_version}
%post
if [ $1 -ge 1 ]; then
for i in $(ls /home)
do
if grep /home/$i /etc/passwd &>/dev/null; then
path=/home/$i/.config/autostart/
if [ ! -d $path ]; then
mkdir -p $path
fi
if [ -f /usr/share/applications/%{desktop_file} ];then
ln -s /usr/share/applications/%{desktop_file} $path 2>/dev/null || true
else
echo "No desktop file: %{desktop_file}"
fi
fi
done
gtk-update-icon-cache /usr/share/icons/hicolor/ || true
fi
if [ $1 -eq 1 ]; then
echo -e "\n You need to install 2 more packages:
unicode_slugify and grpcio-tools.
pip3 install grpcio-tools
pip3 install unicode_slugify
"
fi
%postun
if [ $1 -eq 0 ]; then
for i in $(ls /home)
do
if grep /home/$i /etc/passwd &>/dev/null; then
path=/home/$i/.config/autostart/%{desktop_file}
if [ -h $path -o -f $path ]; then
rm -f $path
else
echo "No desktop file for this user: $path"
fi
fi
done
pkill -15 opensnitch-ui 2>/dev/null || true
echo ""
echo " Remember to uninstall grpcio-tools and unicode_slugify if you don't"
echo " need them anymore:"
echo " pip3 uninstall unicode_slugify"
echo " pip3 uninstall grpcio-tools"
echo ""
fi
%build
python3 setup.py build
%install
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
%clean
rm -rf $RPM_BUILD_ROOT
%files -f INSTALLED_FILES
%defattr(-,root,root)

View file

@ -1,44 +1,43 @@
import os
import json
from PyQt5 import QtCore
class Config:
__instance = None
HELP_URL = "https://github.com/gustavo-iniguez-goya/opensnitch/wiki/Configurations"
@staticmethod
def init(filename):
Config.__instance = Config(filename)
def init():
Config.__instance = Config()
return Config.__instance
@staticmethod
def get():
if Config.__instance == None:
Config._instance = Config()
return Config.__instance
def __init__(self, filename):
self.filename = os.path.abspath( os.path.expanduser(filename) )
self.exists = os.path.isfile(self.filename)
def __init__(self):
self.settings = QtCore.QSettings("opensnitch", "settings")
self.default_timeout = 15
self.default_action = "allow"
self.default_duration = "until restart"
if self.settings.value("global/default_timeout") == None:
self.setSettings("global/default_timeout", 15)
if self.settings.value("global/default_action") == None:
self.setSettings("global/default_action", "allow")
if self.settings.value("global/default_duration") == None:
self.setSettings("global/default_duration", "until restart")
if self.settings.value("global/default_target") == None:
self.setSettings("global/default_target", 0)
if self.exists:
# print( "Loading configuration from %s ..." % self.filename )
data = json.load(open(self.filename))
def reload(self):
self.settings = QtCore.QSettings("opensnitch", "settings")
self.default_timeout = data["default_timeout"]
self.default_action = data["default_action"]
self.default_duration = data["default_duration"]
def hasKey(self, key):
return self.settings.contains(key)
def save(self):
dirname = os.path.dirname(self.filename)
if os.path.isdir(dirname) == False:
os.makedirs(dirname, exist_ok=True)
def setSettings(self, path, value):
self.settings.setValue(path, value)
self.settings.sync()
with open(self.filename, 'w') as fp:
data = {
'default_timeout': self.default_timeout,
'default_action': self.default_action,
'default_duration': self.default_duration
}
json.dump(data, fp)
self.exists = True
def getSettings(self, path):
return self.settings.value(path)

View file

@ -0,0 +1,68 @@
from PyQt5 import Qt, QtCore
from PyQt5.QtGui import QColor, QPen, QBrush
from PyQt5.QtSql import QSqlDatabase, QSqlQueryModel
class ColorizedDelegate(Qt.QItemDelegate):
def __init__(self, parent=None, *args, config=None):
Qt.QItemDelegate.__init__(self, parent, *args)
self._config = config
self._alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignHCenter
def paint(self, painter, option, index):
if not index.isValid():
return super().paint(painter, option, index)
nocolor=True
value = index.data(QtCore.Qt.DisplayRole)
for _, what in enumerate(self._config):
if what == value:
nocolor=False
painter.save()
painter.setPen(self._config[what])
if 'alignment' in self._config:
self._alignment = self._config['alignment']
if option.state & Qt.QStyle.State_Selected:
painter.setBrush(painter.brush())
painter.setPen(painter.pen())
painter.drawText(option.rect, self._alignment, value)
painter.restore()
if nocolor == True:
super().paint(painter, option, index)
class ColorizedQSqlQueryModel(QSqlQueryModel):
"""
model=CustomQSqlQueryModel(
modelData=
{
'colorize':
{'offline': (QColor(QtCore.Qt.red), 2)},
'alignment': { Qt.AlignLeft, 2 }
}
)
"""
RED = QColor(QtCore.Qt.red)
GREEN = QColor(QtCore.Qt.green)
def __init__(self, modelData={}):
QSqlQueryModel.__init__(self)
self._model_data = modelData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return QSqlQueryModel.data(self, index, role)
column = index.column()
row = index.row()
if role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignCenter
if role == QtCore.Qt.TextColorRole:
for _, what in enumerate(self._model_data):
d = QSqlQueryModel.data(self, self.index(row, self._model_data[what][1]), QtCore.Qt.DisplayRole)
if column == self._model_data[what][1] and what in d:
return self._model_data[what][0]
return QSqlQueryModel.data(self, index, role)

260
ui/opensnitch/database.py Normal file
View file

@ -0,0 +1,260 @@
from PyQt5.QtSql import QSqlDatabase, QSqlQueryModel, QSqlQuery
import threading
import sys
class Database:
__instance = None
DB_IN_MEMORY = ":memory:"
DB_TYPE_MEMORY = 0
DB_TYPE_FILE = 1
@staticmethod
def instance():
if Database.__instance == None:
Database.__instance = Database()
return Database.__instance
def __init__(self, dbname="db"):
self._lock = threading.RLock()
self.db = None
self.db_type = Database.DB_IN_MEMORY
self.db_name = dbname
self.initialize()
def initialize(self):
self.db = QSqlDatabase.addDatabase("QSQLITE", self.db_name)
self.db.setDatabaseName(self.db_type)
if not self.db.open():
print("\n ** Error opening DB: SQLite driver not loaded. DB name: %s\n" % self.db_type)
print("\n Available drivers: ", QSqlDatabase.drivers())
sys.exit(-1)
self._create_tables()
def close(self):
self.db.close()
def get_db(self):
return self.db
def get_new_qsql_model(self):
return QSqlQueryModel()
def get_db_name(self):
return self.db_name
def _create_tables(self):
# https://www.sqlite.org/wal.html
q = QSqlQuery("PRAGMA journal_mode = OFF", self.db)
q.exec_()
q = QSqlQuery("PRAGMA synchronous = OFF", self.db)
q.exec_()
q = QSqlQuery("PRAGMA cache_size=10000", self.db)
q.exec_()
q = QSqlQuery("create table if not exists connections (" \
"time text, " \
"node text, " \
"action text, " \
"protocol text, " \
"src_ip text, " \
"src_port text, " \
"dst_ip text, " \
"dst_host text, " \
"dst_port text, " \
"uid text, " \
"pid text, " \
"process text, " \
"process_args text, " \
"process_cwd text, " \
"rule text, " \
"UNIQUE(node, action, protocol, src_ip, src_port, dst_ip, dst_port, uid, pid, process, process_args))",
self.db)
q.exec_()
q = QSqlQuery("create table if not exists rules (" \
"time text, " \
"node text, " \
"name text, " \
"enabled text, " \
"precedence text, " \
"action text, " \
"duration text, " \
"operator_type text, " \
"operator_sensitive text, " \
"operator_operand text, " \
"operator_data text, " \
"UNIQUE(node, name)"
")", self.db)
q.exec_()
q = QSqlQuery("create table if not exists hosts (what text primary key, hits integer)", self.db)
q.exec_()
q = QSqlQuery("create table if not exists procs (what text primary key, hits integer)", self.db)
q.exec_()
q = QSqlQuery("create table if not exists addrs (what text primary key, hits integer)", self.db)
q.exec_()
q = QSqlQuery("create table if not exists ports (what text primary key, hits integer)", self.db)
q.exec_()
q = QSqlQuery("create table if not exists users (what text primary key, hits integer)", self.db)
q.exec_()
q = QSqlQuery("create table if not exists nodes (" \
"addr text primary key," \
"hostname text," \
"daemon_version text," \
"daemon_uptime text," \
"daemon_rules text," \
"cons text," \
"cons_dropped text," \
"version text," \
"status text, " \
"last_connection text)"
, self.db)
q.exec_()
def clean(self, table):
with self._lock:
q = QSqlQuery("delete from " + table, self.db)
q.exec_()
def clone_db(self, name):
return QSqlDatabase.cloneDatabase(self.db, name)
def clone(self):
q = QSqlQuery(".dump", self.db)
q.exec_()
def transaction(self):
self.db.transaction()
def commit(self):
self.db.commit()
def rollback(self):
self.db.rollback()
def select(self, qstr):
try:
return QSqlQuery(qstr, self.db)
except Exception as e:
print("db, select() exception: ", e)
return None
def remove(self, qstr):
try:
q = QSqlQuery(qstr, self.db)
if q.exec_():
return True
else:
print("db, remove() ERROR: ", qstr)
print(q.lastError().driverText())
except Exception as e:
print("db, remove exception: ", e)
return False
def _insert(self, query_str, columns):
with self._lock:
try:
q = QSqlQuery(self.db)
q.prepare(query_str)
for idx, v in enumerate(columns):
q.bindValue(idx, v)
if q.exec_():
return True
else:
print("_insert() ERROR", query_str)
print(q.lastError().driverText())
except Exception as e:
print("_insert exception", e)
finally:
q.finish()
return False
def insert(self, table, fields, columns, update_field=None, update_value=None, action_on_conflict="REPLACE"):
if update_field != None:
action_on_conflict = ""
qstr = "INSERT OR " + action_on_conflict + " INTO " + table + " " + fields + " VALUES("
update_fields=""
for col in columns:
qstr += "?,"
qstr = qstr[0:len(qstr)-1] + ")"
if update_field != None:
for field in fields:
update_fields += str(field) + "=excluded." + str(field)
qstr += " ON CONFLICT(" + update_field + ") DO UPDATE SET " + \
update_fields + \
" WHERE " + update_field + "=excluded." + update_field
return self._insert(qstr, columns)
def update(self, table, fields, values, action_on_conflict="OR IGNORE"):
qstr = "UPDATE " + action_on_conflict + " " + table + " SET " + fields
try:
with self._lock:
q = QSqlQuery(qstr, self.db)
q.prepare(qstr)
for idx, v in enumerate(values):
q.bindValue(idx, v)
if not q.exec_():
print("update ERROR", qstr)
print(q.lastError().driverText())
except Exception as e:
print("update() exception:", e)
finally:
q.finish()
def _insert_batch(self, query_str, fields, values):
result=True
with self._lock:
try:
q = QSqlQuery(self.db)
q.prepare(query_str)
q.addBindValue(fields)
q.addBindValue(values)
if not q.execBatch():
print("_insert_batch() error", query_str)
print(q.lastError().driverText())
result=False
except Exception as e:
print("_insert_batch() exception:", e)
finally:
q.finish()
return result
def insert_batch(self, table, db_fields, db_columns, fields, values, update_field=None, update_value=None, action_on_conflict="REPLACE"):
action = "OR " + action_on_conflict
if update_field != None:
action = ""
qstr = "INSERT " + action + " INTO " + table + " (" + db_fields[0] + "," + db_fields[1] + ") VALUES("
for idx in db_columns:
qstr += "?,"
qstr = qstr[0:len(qstr)-1] + ")"
if self._insert_batch(qstr, fields, values) == False:
self.update_batch(table, db_fields, db_columns, fields, values, update_field, update_value, action_on_conflict)
def update_batch(self, table, db_fields, db_columns, fields, values, update_field=None, update_value=None, action_on_conflict="REPLACE"):
for idx, i in enumerate(values):
s = "UPDATE " + table + " SET " + "%s=(select hits from %s)+%s" % (db_fields[1], table, values[idx])
s += " WHERE %s=\"%s\"," % (db_fields[0], fields[idx])
s = s[0:len(s)-1]
with self._lock:
q = QSqlQuery(s, self.db)
if not q.exec_():
print("update batch ERROR", s)
print(q.lastError().driverText())
def dump(self):
q = QSqlQuery(".dump", db=self.db)
q.exec_()
def get_query(self, table, fields):
return "SELECT " + fields + " FROM " + table

View file

@ -19,6 +19,7 @@ class LinuxDesktopParser(threading.Thread):
self.daemon = True
self.running = False
self.apps = {}
self.apps_by_name = {}
# some things are just weird
# (not really, i don't want to keep track of parent pids
# just because of icons tho, this hack is way easier)
@ -55,21 +56,51 @@ class LinuxDesktopParser(threading.Thread):
return cmd
def _discover_app_icon(self, app_name):
# more hacks
# normally qt will find icons if the system if configured properly.
# if it's not, qt won't be able to find the icon by using QIcon().fromTheme(""),
# so we fallback to try to determine if the icon exist in some well known system paths.
icon_dirs = ("/usr/share/icons/gnome/48x48/apps/", "/usr/share/pixmaps/", "/usr/share/icons/hicolor/48x48/apps/")
icon_exts = (".png", ".xpm", ".svg")
for idir in icon_dirs:
for iext in icon_exts:
iconPath = idir + app_name + iext
if os.path.exists(iconPath):
print("found on last chance: ", iconPath)
return iconPath
def _parse_desktop_file(self, desktop_path):
parser = configparser.ConfigParser(strict=False) # Allow duplicate config entries
parser.read(desktop_path, 'utf8')
try:
basename = os.path.basename(desktop_path)[:-8]
parser.read(desktop_path, 'utf8')
cmd = parser.get('Desktop Entry', 'exec', raw=True, fallback=None)
if cmd is not None:
cmd = self._parse_exec(cmd)
icon = parser.get('Desktop Entry', 'Icon', raw=True, fallback=None)
name = parser.get('Desktop Entry', 'Name', raw=True, fallback=None)
with self.lock:
self.apps[cmd] = (name, icon, desktop_path)
# if the command is a symlink, add the real binary too
if os.path.islink(cmd):
link_to = os.path.realpath(cmd)
self.apps[link_to] = (name, icon, desktop_path)
cmd = parser.get('Desktop Entry', 'exec', raw=True, fallback=None)
if cmd == None:
cmd = parser.get('Desktop Entry', 'Exec', raw=True, fallback=None)
if cmd is not None:
cmd = self._parse_exec(cmd)
icon = parser.get('Desktop Entry', 'Icon', raw=True, fallback=None)
name = parser.get('Desktop Entry', 'Name', raw=True, fallback=None)
if icon == None:
# Some .desktop files doesn't have the Icon entry
# FIXME: even if we return an icon, if the DE is not properly configured,
# it won't be loaded/displayed.
icon = self._discover_app_icon(basename)
with self.lock:
# The Exec entry may have an absolute path to a binary or just the binary with parameters.
# /path/binary or binary, so save both
self.apps[cmd] = (name, icon, desktop_path)
self.apps[basename] = (name, icon, desktop_path)
# if the command is a symlink, add the real binary too
if os.path.islink(cmd):
link_to = os.path.realpath(cmd)
self.apps[link_to] = (name, icon, desktop_path)
except:
print("Exception parsing .desktop file ", desktop_path)
def get_info_by_path(self, path, default_icon):
def_name = os.path.basename(path)
@ -79,8 +110,16 @@ class LinuxDesktopParser(threading.Thread):
path = to
break
app_name = self.apps.get(path)
if app_name == None:
return self.apps.get(def_name, (def_name, default_icon, None))
return self.apps.get(path, (def_name, default_icon, None))
def get_info_by_binname(self, name, default_icon):
def_name = os.path.basename(name)
return self.apps.get(def_name, (def_name, default_icon, None))
def run(self):
self.running = True
wm = pyinotify.WatchManager()

View file

@ -0,0 +1,268 @@
import sys
import time
import os
import json
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from config import Config
from nodes import Nodes
import ui_pb2
DIALOG_UI_PATH = "%s/../res/preferences.ui" % os.path.dirname(sys.modules[__name__].__file__)
class PreferencesDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
CFG_DEFAULT_ACTION = "global/default_action"
CFG_DEFAULT_DURATION = "global/default_duration"
CFG_DEFAULT_TARGET = "global/default_target"
CFG_DEFAULT_TIMEOUT = "global/default_timeout"
CFG_SHOW_POPUPS = "global/show_popups"
LOG_TAG = "[Preferences] "
_notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
def __init__(self, parent=None):
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)
self._cfg = Config.get()
self._nodes = Nodes.instance()
self._notification_callback.connect(self._cb_notification_callback)
self._notifications_sent = {}
self.setupUi(self)
self.acceptButton.clicked.connect(self._cb_accept_button_clicked)
self.applyButton.clicked.connect(self._cb_apply_button_clicked)
self.cancelButton.clicked.connect(self._cb_cancel_button_clicked)
self.popupsCheck.clicked.connect(self._cb_popups_check_toggled)
if QtGui.QIcon.hasThemeIcon("emblem-default") == False:
self.applyButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogApplyButton")))
self.cancelButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCloseButton")))
self.acceptButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogSaveButton")))
def showEvent(self, event):
super(PreferencesDialog, self).showEvent(event)
try:
self._reset_status_message()
self._hide_status_label()
self.comboNodes.clear()
self._node_list = self._nodes.get()
for addr in self._node_list:
self.comboNodes.addItem(addr)
if len(self._node_list) == 0:
self._reset_node_settings()
except Exception as e:
print(self.LOG_TAG + "exception loading nodes", e)
self._load_settings()
# connect the signals after loading settings, to avoid firing
# the signals
self.comboNodes.currentIndexChanged.connect(self._cb_node_combo_changed)
self.comboNodeAction.currentIndexChanged.connect(self._cb_node_needs_update)
self.comboNodeDuration.currentIndexChanged.connect(self._cb_node_needs_update)
self.comboNodeMonitorMethod.currentIndexChanged.connect(self._cb_node_needs_update)
self.comboNodeLogLevel.currentIndexChanged.connect(self._cb_node_needs_update)
self.comboNodeLogFile.currentIndexChanged.connect(self._cb_node_needs_update)
self.comboNodeAddress.currentIndexChanged.connect(self._cb_node_needs_update)
self.checkInterceptUnknown.clicked.connect(self._cb_node_needs_update)
self.checkApplyToNodes.clicked.connect(self._cb_node_needs_update)
# True when any node option changes
self._node_needs_update = False
def _load_settings(self):
self._default_action = self._cfg.getSettings(self.CFG_DEFAULT_ACTION)
self._default_duration = self._cfg.getSettings(self.CFG_DEFAULT_DURATION)
self._default_target = self._cfg.getSettings(self.CFG_DEFAULT_TARGET)
self._default_timeout = self._cfg.getSettings(self.CFG_DEFAULT_TIMEOUT)
self._show_popups = self._cfg.getSettings(self.CFG_SHOW_POPUPS)
self.comboUIDuration.setCurrentText(self._default_duration)
self.comboUIAction.setCurrentText(self._default_action)
self.comboUITarget.setCurrentIndex(int(self._default_target))
self.spinUITimeout.setValue(int(self._default_timeout))
self.popupsCheck.setChecked(bool(self._show_popups))
if self._show_popups != None:
self.spinUITimeout.setEnabled(not self.popupsCheck.isChecked())
self._load_node_settings()
def _load_node_settings(self):
addr = self.comboNodes.currentText()
if addr != "":
try:
node_data = self._node_list[addr]['data']
self.labelNodeVersion.setText(node_data.version)
self.labelNodeName.setText(node_data.name)
self.comboNodeLogLevel.setCurrentIndex(node_data.logLevel)
node_config = json.loads(node_data.config)
self.comboNodeAction.setCurrentText(node_config['DefaultAction'])
self.comboNodeDuration.setCurrentText(node_config['DefaultDuration'])
self.comboNodeMonitorMethod.setCurrentText(node_config['ProcMonitorMethod'])
self.checkInterceptUnknown.setChecked(node_config['InterceptUnknown'])
self.comboNodeLogLevel.setCurrentIndex(int(node_config['LogLevel']))
if node_config.get('Server') != None:
self.comboNodeAddress.setEnabled(True)
self.comboNodeLogFile.setEnabled(True)
self.comboNodeAddress.setCurrentText(node_config['Server']['Address'])
self.comboNodeLogFile.setCurrentText(node_config['Server']['LogFile'])
else:
self.comboNodeAddress.setEnabled(False)
self.comboNodeLogFile.setEnabled(False)
except Exception as e:
print(self.LOG_TAG + "exception loading config: ", e)
def _reset_node_settings(self):
self.comboNodeAction.setCurrentIndex(0)
self.comboNodeDuration.setCurrentIndex(0)
self.comboNodeMonitorMethod.setCurrentIndex(0)
self.checkInterceptUnknown.setChecked(False)
self.comboNodeLogLevel.setCurrentIndex(0)
self.labelNodeName.setText("")
self.labelNodeVersion.setText("")
def _save_settings(self):
if self.tabWidget.currentIndex() == 0:
self._cfg.setSettings(self.CFG_DEFAULT_ACTION, self.comboUIAction.currentText())
self._cfg.setSettings(self.CFG_DEFAULT_DURATION, self.comboUIDuration.currentText())
self._cfg.setSettings(self.CFG_DEFAULT_TARGET, self.comboUITarget.currentIndex())
self._cfg.setSettings(self.CFG_DEFAULT_TIMEOUT, self.spinUITimeout.value())
self._cfg.setSettings(self.CFG_SHOW_POPUPS, self.popupsCheck.isChecked())
# this is a workaround for not display pop-ups.
# see #79 for more information.
if self.popupsCheck.isChecked():
self._cfg.setSettings(self.CFG_DEFAULT_TIMEOUT, 0)
elif self.tabWidget.currentIndex() == 1:
self._show_status_label()
addr = self.comboNodes.currentText()
if (self._node_needs_update or self.checkApplyToNodes.isChecked()) and addr != "":
try:
notif = ui_pb2.Notification(
id=int(str(time.time()).replace(".", "")),
type=ui_pb2.CHANGE_CONFIG,
data="",
rules=[])
if self.checkApplyToNodes.isChecked():
for addr in self._nodes.get_nodes():
error = self._save_node_config(notif, addr)
if error != None:
self._set_status_error(error)
return
else:
error = self._save_node_config(notif, addr)
if error != None:
self._set_status_error(error)
return
except Exception as e:
print(self.LOG_TAG + "exception saving config: ", e)
self._set_status_error("Exception saving config: %s" % str(e))
self._node_needs_update = False
def _save_node_config(self, notifObject, addr):
try:
self._set_status_message("Applying configuration on %s ..." % addr)
notifObject.data, error = self._load_node_config(addr)
if error != None:
return error
self._nodes.save_node_config(addr, notifObject.data)
nid = self._nodes.send_notification(addr, notifObject, self._notification_callback)
self._notifications_sent[nid] = notifObject
except Exception as e:
print(self.LOG_TAG + "exception saving node config on %s: " % addr, e)
self._set_status_error("Exception saving node config %s: %s" % (addr, str(e)))
return addr + ": " + str(e)
return None
def _load_node_config(self, addr):
try:
if self.comboNodeAddress.currentText() == "":
return None, "Server address can not be empty"
node_config = json.loads(self._nodes.get_node_config(addr))
node_config['DefaultAction'] = self.comboNodeAction.currentText()
node_config['DefaultDuration'] = self.comboNodeDuration.currentText()
node_config['ProcMonitorMethod'] = self.comboNodeMonitorMethod.currentText()
node_config['LogLevel'] = self.comboNodeLogLevel.currentIndex()
node_config['InterceptUnknown'] = self.checkInterceptUnknown.isChecked()
if node_config.get('Server') != None:
# skip setting Server Address if we're applying the config to all nodes
if self.checkApplyToNodes.isChecked():
print("skipping server address")
node_config['Server']['Address'] = self.comboNodeAddress.currentText()
node_config['Server']['LogFile'] = self.comboNodeLogFile.currentText()
#else:
# print(addr, " doesn't have Server item")
return json.dumps(node_config), None
except Exception as e:
print(self.LOG_TAG + "exception loading node config on %s: " % addr, e)
return None, "Error loading %s configuration" % addr
def _hide_status_label(self):
self.statusLabel.hide()
def _show_status_label(self):
self.statusLabel.show()
def _set_status_error(self, msg):
self.statusLabel.setStyleSheet('color: red')
self.statusLabel.setText(msg)
def _set_status_successful(self, msg):
self.statusLabel.setStyleSheet('color: green')
self.statusLabel.setText(msg)
def _set_status_message(self, msg):
self.statusLabel.setStyleSheet('color: darkorange')
self.statusLabel.setText(msg)
def _reset_status_message(self):
self.statusLabel.setText("")
@QtCore.pyqtSlot(ui_pb2.NotificationReply)
def _cb_notification_callback(self, reply):
#print(self.LOG_TAG, "Config notification received: ", reply.id, reply.code)
if reply.id in self._notifications_sent:
if reply.code == ui_pb2.OK:
self._set_status_successful("Configuration applied.")
else:
self._set_status_error("Error applying configuration: %s" % reply.data)
del self._notifications_sent[reply.id]
def _cb_accept_button_clicked(self):
self._save_settings()
self.accept()
def _cb_apply_button_clicked(self):
self._save_settings()
def _cb_cancel_button_clicked(self):
self.reject()
def _cb_popups_check_toggled(self, checked):
self.spinUITimeout.setEnabled(not checked)
if not checked:
self.spinUITimeout.setValue(15)
def _cb_node_combo_changed(self, index):
self._load_node_settings()
def _cb_node_needs_update(self):
self._node_needs_update = True

View file

@ -0,0 +1,321 @@
import os
import sys
import json
from PyQt5 import QtCore, QtGui, uic, QtWidgets
import ui_pb2
from nodes import Nodes
from desktop_parser import LinuxDesktopParser
DIALOG_UI_PATH = "%s/../res/process_details.ui" % os.path.dirname(sys.modules[__name__].__file__)
class ProcessDetailsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
LOG_TAG = "[ProcessDetails]: "
_notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
TAB_STATUS = 0
TAB_DESCRIPTORS = 1
TAB_IOSTATS = 2
TAB_MAPS = 3
TAB_STACK = 4
TAB_ENVS = 5
TABS = {
TAB_STATUS: {
"text": None,
"scrollPos": 0
},
TAB_DESCRIPTORS: {
"text": None,
"scrollPos": 0
},
TAB_IOSTATS: {
"text": None,
"scrollPos": 0
},
TAB_MAPS: {
"text": None,
"scrollPos": 0
},
TAB_STACK: {
"text": None,
"scrollPos": 0
},
TAB_ENVS: {
"text": None,
"scrollPos": 0
}
}
def __init__(self, parent=None):
super(ProcessDetailsDialog, self).__init__(parent)
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)
self.setWindowFlags(QtCore.Qt.Window)
self.setupUi(self)
self._app_name = None
self._app_icon = None
self._apps_parser = LinuxDesktopParser()
self._nodes = Nodes.instance()
self._notification_callback.connect(self._cb_notification_callback)
self._nid = None
self._pid = ""
self._notifications_sent = {}
self.cmdClose.clicked.connect(self._cb_close_clicked)
self.cmdAction.clicked.connect(self._cb_action_clicked)
self.comboPids.currentIndexChanged.connect(self._cb_combo_pids_changed)
self.TABS[self.TAB_STATUS]['text'] = self.textStatus
self.TABS[self.TAB_DESCRIPTORS]['text'] = self.textOpenedFiles
self.TABS[self.TAB_IOSTATS]['text'] = self.textIOStats
self.TABS[self.TAB_MAPS]['text'] = self.textMappedFiles
self.TABS[self.TAB_STACK]['text'] = self.textStack
self.TABS[self.TAB_ENVS]['text'] = self.textEnv
self.TABS[self.TAB_DESCRIPTORS]['text'].setFont(QtGui.QFont("monospace"))
self.iconStart = QtGui.QIcon.fromTheme("media-playback-start")
self.iconPause = QtGui.QIcon.fromTheme("media-playback-pause")
if QtGui.QIcon.hasThemeIcon("window-close") == False:
self.cmdClose.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCloseButton")))
self.iconStart = self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_MediaPlay"))
self.iconPause = self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_MediaPause"))
@QtCore.pyqtSlot(ui_pb2.NotificationReply)
def _cb_notification_callback(self, reply):
if reply.id in self._notifications_sent:
noti = self._notifications_sent[reply.id]
if reply.code == ui_pb2.ERROR:
self._show_message("<b>Error loading process information:</b> <br><br>\n\n" + reply.data)
self.cmdAction.setChecked(False)
self._pid = ""
# if we haven't loaded any data yet, just close the window
if self._data_loaded == False:
# but if there're more than 1 pid keep the window open.
# we may have one pid already closed and one alive.
if self.comboPids.count() <= 1:
self._close()
self._delete_notification(reply.id)
return
if noti.type == ui_pb2.MONITOR_PROCESS and reply.data != "":
self._load_data(reply.data)
elif noti.type == ui_pb2.STOP_MONITOR_PROCESS:
if reply.data != "":
self.show_message("<b>Error stopping monitoring process:</b><br><br>" + reply.data)
self._delete_notification(reply.id)
else:
print("[stats] unknown notification received: ", reply.id)
def closeEvent(self, e):
self._close()
def _cb_close_clicked(self):
self._close()
def _cb_combo_pids_changed(self, idx):
if idx == -1:
return
# TODO: this event causes to send to 2 Start notifications
#if self._pid != "" and self._pid != self.comboPids.currentText():
# self._stop_monitoring()
# self._pid = self.comboPids.currentText()
# self._start_monitoring()
def _cb_action_clicked(self):
if not self.cmdAction.isChecked():
self._stop_monitoring()
self.cmdAction.setIcon(self.iconStart)
else:
self.cmdAction.setIcon(self.iconPause)
self._start_monitoring()
def _show_message(self, message):
msgBox = QtWidgets.QMessageBox()
msgBox.setText(message)
msgBox.setIcon(QtWidgets.QMessageBox.Warning)
msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok)
msgBox.exec_()
def _delete_notification(self, nid):
if nid in self._notifications_sent:
del self._notifications_sent[nid]
def _reset(self):
self._app_name = None
self._app_icon = None
self.comboPids.clear()
self.labelProcName.setText("loading...")
self.labelProcArgs.setText("loading...")
self.labelProcIcon.clear()
self.labelStatm.setText("")
self.labelCwd.setText("")
for tidx in range(0, len(self.TABS)):
self.TABS[tidx]['text'].setPlainText("")
def _close(self):
self._stop_monitoring()
self.comboPids.clear()
self._pid = ""
self.hide()
def monitor(self, pids):
if self._pid != "":
self._stop_monitoring()
self._data_loaded = False
self._pids = pids
self._reset()
for pid in pids:
self.comboPids.addItem(pid)
self.show()
self._start_monitoring()
def _set_tab_text(self, tab_idx, text):
self.TABS[tab_idx]['scrollPos'] = self.TABS[tab_idx]['text'].verticalScrollBar().value()
self.TABS[tab_idx]['text'].setPlainText(text)
self.TABS[tab_idx]['text'].verticalScrollBar().setValue(self.TABS[tab_idx]['scrollPos'])
def _start_monitoring(self):
try:
# avoid to send notifications without a pid
if self._pid != "":
return
self._pid = self.comboPids.currentText()
if self._pid == "":
return
self.cmdAction.setIcon(self.iconPause)
self.cmdAction.setChecked(True)
noti = ui_pb2.Notification(clientName="", serverName="", type=ui_pb2.MONITOR_PROCESS, data=self._pid, rules=[])
self._nid = self._nodes.send_notification(self._pids[self._pid], noti, self._notification_callback)
self._notifications_sent[self._nid] = noti
except Exception as e:
print(self.LOG_TAG + "exception starting monitoring: ", e)
def _stop_monitoring(self):
if self._pid == "":
return
self.cmdAction.setIcon(self.iconStart)
self.cmdAction.setChecked(False)
noti = ui_pb2.Notification(clientName="", serverName="", type=ui_pb2.STOP_MONITOR_PROCESS, data=str(self._pid), rules=[])
self._nid = self._nodes.send_notification(self._pids[self._pid], noti, self._notification_callback)
self._notifications_sent[self._nid] = noti
self._pid = ""
self._app_icon = None
def _load_data(self, data):
tab_idx = self.tabWidget.currentIndex()
try:
proc = json.loads(data)
self._load_app_icon(proc['Path'])
if self._app_name != None:
self.labelProcName.setText("<b>" + self._app_name + "</b>")
#if proc['Path'] not in proc['Args']:
# proc['Args'].insert(0, proc['Path'])
self.labelProcArgs.setText(" ".join(proc['Args']))
self.labelCwd.setText("<b>CWD: </b>" + proc['CWD'])
self._load_mem_data(proc['Statm'])
if tab_idx == self.TAB_STATUS:
self._set_tab_text(tab_idx, proc['Status'])
elif tab_idx == self.TAB_DESCRIPTORS:
self._load_descriptors(proc['Descriptors'])
elif tab_idx == self.TAB_IOSTATS:
self._load_iostats(proc['IOStats'])
elif tab_idx == self.TAB_MAPS:
self._set_tab_text(tab_idx, proc['Maps'])
elif tab_idx == self.TAB_STACK:
self._set_tab_text(tab_idx, proc['Stack'])
elif tab_idx == self.TAB_ENVS:
self._load_env_vars(proc['Env'])
self._data_loaded = True
except Exception as e:
print(self.LOG_TAG + "exception loading data: ", e)
def _load_app_icon(self, proc_path):
if self._app_icon != None:
return
self._app_name, self._app_icon, _ = self._apps_parser.get_info_by_path(proc_path, "terminal")
icon = QtGui.QIcon().fromTheme(self._app_icon)
pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48)))
self.labelProcIcon.setPixmap(pixmap)
if self._app_name == None:
self._app_name = proc_path
def _load_iostats(self, iostats):
ioText = "%-16s %dMB<br>%-16s %dMB<br>%-16s %d<br>%-16s %d<br>%-16s %dMB<br>%-16s %dMB<br>" % (
"<b>Chars read:</b>",
((iostats['RChar'] / 1024) / 1024),
"<b>Chars written:</b>",
((iostats['WChar'] / 1024) / 1024),
"<b>Syscalls read:</b>",
(iostats['SyscallRead']),
"<b>Syscalls write:</b>",
(iostats['SyscallWrite']),
"<b>KB read:</b>",
((iostats['ReadBytes'] / 1024) / 1024),
"<b>KB written: </b>",
((iostats['WriteBytes'] / 1024) / 1024)
)
self.textIOStats.setPlainText("")
self.textIOStats.appendHtml(ioText)
def _load_mem_data(self, mem):
# assuming page size == 4096
pagesize = 4096
memText = "<b>VIRT:</b> %dMB, <b>RSS:</b> %dMB, <b>Libs:</b> %dMB, <b>Data:</b> %dMB, <b>Text:</b> %dMB" % (
((mem['Size'] * pagesize) / 1024) / 1024,
((mem['Resident'] * pagesize) / 1024) / 1024,
((mem['Lib'] * pagesize) / 1024) / 1024,
((mem['Data'] * pagesize) / 1024) / 1024,
((mem['Text'] * pagesize) / 1024) / 1024
)
self.labelStatm.setText(memText)
def _load_descriptors(self, descriptors):
text = "%-12s%-40s%-8s -> %s\n\n" % ("Size", "Time", "Name", "Symlink")
for d in descriptors:
text += "{:<12}{:<40}{:<8} -> {}\n".format(str(d['Size']), d['ModTime'], d['Name'], d['SymLink'])
self._set_tab_text(self.TAB_DESCRIPTORS, text)
def _load_env_vars(self, envs):
if envs == {}:
self._set_tab_text(self.TAB_ENVS, "<no environment variables>")
return
text = "%-15s\t%s\n\n" % ("Name", "Value")
for env_name in envs:
text += "%-15s:\t%s\n" % (env_name, envs[env_name])
self._set_tab_text(self.TAB_ENVS, text)

View file

@ -1,9 +1,10 @@
import threading
import logging
import sys
import time
import os
import pwd
import json
import ipaddress
from PyQt5 import QtCore, QtGui, uic, QtWidgets
@ -21,14 +22,51 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
_tick_trigger = QtCore.pyqtSignal()
_timeout_trigger = QtCore.pyqtSignal()
DEFAULT_TIMEOUT = 15
ACTION_ALLOW = "allow"
ACTION_DENY = "deny"
FIELD_REGEX_HOST = "regex_host"
FIELD_REGEX_IP = "regex_ip"
FIELD_PROC_PATH = "process_path"
FIELD_PROC_ARGS = "process_args"
FIELD_USER_ID = "user_id"
FIELD_DST_IP = "dst_ip"
FIELD_DST_PORT = "dst_port"
FIELD_DST_NETWORK = "dst_network"
FIELD_DST_HOST = "simple_host"
DURATION_once = "once"
DURATION_30s = "30s"
DURATION_5m = "5m"
DURATION_15m = "15m"
DURATION_30m = "30m"
DURATION_1h = "1h"
# label displayed in the pop-up combo
DURATION_session = "for this session"
# field of a rule
DURATION_restart = "until restart"
# label displayed in the pop-up combo
DURATION_forever = "forever"
# field of a rule
DURATION_always = "always"
CFG_DEFAULT_TIMEOUT = "global/default_timeout"
CFG_DEFAULT_ACTION = "global/default_action"
def __init__(self, parent=None):
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)
# Other interesting flags: QtCore.Qt.Tool | QtCore.Qt.BypassWindowManagerHint
self._cfg = Config.get()
self.setupUi(self)
dialog_geometry = self._cfg.getSettings("promptDialog/geometry")
if dialog_geometry == QtCore.QByteArray:
self.restoreGeometry(dialog_geometry)
self.setWindowTitle("OpenSnitch v%s" % version)
self._cfg = Config.get()
self._lock = threading.Lock()
self._con = None
self._rule = None
@ -37,35 +75,67 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._prompt_trigger.connect(self.on_connection_prompt_triggered)
self._timeout_trigger.connect(self.on_timeout_triggered)
self._tick_trigger.connect(self.on_tick_triggered)
self._tick = self._cfg.default_timeout
self._tick = int(self._cfg.getSettings(self.CFG_DEFAULT_TIMEOUT)) if self._cfg.hasKey(self.CFG_DEFAULT_TIMEOUT) else self.DEFAULT_TIMEOUT
self._tick_thread = None
self._done = threading.Event()
self._timeout_text = ""
self._timeout_triggered = False
self._apps_parser = LinuxDesktopParser()
self._app_name_label = self.findChild(QtWidgets.QLabel, "appNameLabel")
self._app_icon_label = self.findChild(QtWidgets.QLabel, "iconLabel")
self._message_label = self.findChild(QtWidgets.QLabel, "messageLabel")
self.denyButton.clicked.connect(self._on_deny_clicked)
# also accept button
self.applyButton.clicked.connect(self._on_apply_clicked)
self._apply_text = "Allow"
self._deny_text = "Deny"
self._default_action = self._cfg.getSettings(self.CFG_DEFAULT_ACTION)
self._src_ip_label = self.findChild(QtWidgets.QLabel, "sourceIPLabel")
self._dst_ip_label = self.findChild(QtWidgets.QLabel, "destIPLabel")
self._uid_label = self.findChild(QtWidgets.QLabel, "uidLabel")
self._pid_label = self.findChild(QtWidgets.QLabel, "pidLabel")
self._args_label = self.findChild(QtWidgets.QLabel, "argsLabel")
self.whatIPCombo.setVisible(False)
self.checkDstIP.setVisible(False)
self.checkDstPort.setVisible(False)
self.checkUserID.setVisible(False)
self._apply_button = self.findChild(QtWidgets.QPushButton, "applyButton")
self._apply_button.clicked.connect(self._on_apply_clicked)
self._ischeckAdvanceded = False
self.checkAdvanced.toggled.connect(self._checkbox_toggled)
self._action_combo = self.findChild(QtWidgets.QComboBox, "actionCombo")
self._what_combo = self.findChild(QtWidgets.QComboBox, "whatCombo")
self._duration_combo = self.findChild(QtWidgets.QComboBox, "durationCombo")
if QtGui.QIcon.hasThemeIcon("emblem-default") == False:
self.applyButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogApplyButton")))
self.denyButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCancelButton")))
def showEvent(self, event):
super(PromptDialog, self).showEvent(event)
self.resize(540, 300)
self.activateWindow()
def _checkbox_toggled(self, state):
self.applyButton.setText("%s" % self._apply_text)
self.denyButton.setText("%s" % self._deny_text)
self._tick_thread.stop = state
self.checkDstIP.setVisible(state)
self.whatIPCombo.setVisible(state)
self.destIPLabel.setVisible(not state)
self.checkDstPort.setVisible(state)
self.checkUserID.setVisible(state)
self._ischeckAdvanceded = state
def _set_elide_text(self, widget, text, max_size=128):
if len(text) > max_size:
text = text[:max_size] + "..."
widget.setText(text)
def promptUser(self, connection, is_local, peer):
# one at a time
with self._lock:
# reset state
self._tick = self._cfg.default_timeout
if self._tick_thread != None and self._tick_thread.is_alive():
self._tick_thread.join()
self._cfg.reload()
self._tick = int(self._cfg.getSettings(self.CFG_DEFAULT_TIMEOUT)) if self._cfg.hasKey(self.CFG_DEFAULT_TIMEOUT) else self.DEFAULT_TIMEOUT
self._tick_thread = threading.Thread(target=self._timeout_worker)
self._tick_thread.stop = self._ischeckAdvanceded
self._timeout_triggered = False
self._rule = None
self._local = is_local
self._peer = peer
@ -77,45 +147,101 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self._tick_thread.start()
# wait for user choice or timeout
self._done.wait()
return self._rule
return self._rule, self._timeout_triggered
def _timeout_worker(self):
if self._tick == 0:
self._timeout_trigger.emit()
return
while self._tick > 0 and self._done.is_set() is False:
t = threading.currentThread()
# stop only stops the coundtdown, not the thread itself.
if getattr(t, "stop", True):
self._tick = int(self._cfg.getSettings(self.CFG_DEFAULT_TIMEOUT))
time.sleep(1)
continue
self._tick -= 1
self._tick_trigger.emit()
time.sleep(1)
if not self._done.is_set():
self._timeout_trigger.emit()
@QtCore.pyqtSlot()
def on_connection_prompt_triggered(self):
self._render_connection(self._con)
self.show()
if self._tick > 0:
self.show()
@QtCore.pyqtSlot()
def on_tick_triggered(self):
self._apply_button.setText("Apply (%d)" % self._tick)
if self._cfg.getSettings(self.CFG_DEFAULT_ACTION) == self.ACTION_ALLOW:
self._timeout_text = "%s (%d)" % (self._apply_text, self._tick)
self.applyButton.setText(self._timeout_text)
else:
self._timeout_text = "%s (%d)" % (self._deny_text, self._tick)
self.denyButton.setText(self._timeout_text)
@QtCore.pyqtSlot()
def on_timeout_triggered(self):
self._on_apply_clicked()
self._timeout_triggered = True
self._send_rule()
def _configure_default_duration(self):
if self._cfg.getSettings("global/default_duration") == self.DURATION_once:
self.durationCombo.setCurrentIndex(0)
elif self._cfg.getSettings("global/default_duration") == self.DURATION_30s:
self.durationCombo.setCurrentIndex(1)
elif self._cfg.getSettings("global/default_duration") == self.DURATION_5m:
self.durationCombo.setCurrentIndex(2)
elif self._cfg.getSettings("global/default_duration") == self.DURATION_15m:
self.durationCombo.setCurrentIndex(3)
elif self._cfg.getSettings("global/default_duration") == self.DURATION_30m:
self.durationCombo.setCurrentIndex(4)
elif self._cfg.getSettings("global/default_duration") == self.DURATION_1h:
self.durationCombo.setCurrentIndex(5)
elif self._cfg.getSettings("global/default_duration") == self.DURATION_session:
self.durationCombo.setCurrentIndex(6)
elif self._cfg.getSettings("global/default_duration") == self.DURATION_forever:
self.durationCombo.setCurrentIndex(7)
else:
# default to "for this session"
self.durationCombo.setCurrentIndex(6)
def _set_cmd_action_text(self):
if self._cfg.getSettings(self.CFG_DEFAULT_ACTION) == self.ACTION_ALLOW:
self.applyButton.setText("%s (%d)" % (self._apply_text, self._tick))
self.denyButton.setText(self._deny_text)
else:
self.denyButton.setText("%s (%d)" % (self._deny_text, self._tick))
self.applyButton.setText(self._apply_text)
self.checkAdvanced.setFocus()
def _render_connection(self, con):
if self._local:
app_name, app_icon, _ = self._apps_parser.get_info_by_path(con.process_path, "terminal")
app_name, app_icon, _ = self._apps_parser.get_info_by_path(con.process_path, "terminal")
if app_name != con.process_path and len(con.process_args) > 1 and con.process_path not in con.process_args:
self.appPathLabel.setToolTip("Process path: %s" % con.process_path)
self._set_elide_text(self.appPathLabel, "(%s)" % con.process_path)
else:
app_name, app_icon = "", "terminal"
self.appPathLabel.setFixedHeight(1)
self.appPathLabel.setText("")
if app_name == "":
self._app_name_label.setText(con.process_path)
app_name = "Unknown process"
self.appNameLabel.setText("Outgoing connection")
else:
self._app_name_label.setText(app_name)
self.appNameLabel.setText(app_name)
self.appNameLabel.setToolTip(app_name)
self.cwdLabel.setToolTip("Process launched from: %s" % con.process_cwd)
self._set_elide_text(self.cwdLabel, con.process_cwd, max_size=32)
icon = QtGui.QIcon().fromTheme(app_icon)
pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48)))
self._app_icon_label.setPixmap(pixmap)
self.iconLabel.setPixmap(pixmap)
if self._local:
message = "<b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \
@ -124,54 +250,74 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
con.protocol,
con.dst_port )
else:
message = "The process <b>%s</b> running on the computer <b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \
message = "<b>Remote</b> process <b>%s</b> running on <b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \
app_name,
self._peer.split(':')[1],
con.dst_host or con.dst_ip,
con.protocol,
con.dst_port )
self._message_label.setText(message)
self.messageLabel.setText(message)
self.messageLabel.setToolTip(message)
self._src_ip_label.setText(con.src_ip)
self._dst_ip_label.setText(con.dst_ip)
self.sourceIPLabel.setText(con.src_ip)
self.destIPLabel.setText(con.dst_ip)
self.destPortLabel.setText(str(con.dst_port))
if self._local:
uid = "%d (%s)" % (con.user_id, pwd.getpwuid(con.user_id).pw_name)
try:
uid = "%d (%s)" % (con.user_id, pwd.getpwuid(con.user_id).pw_name)
except:
uid = ""
else:
uid = "%d" % con.user_id
self._uid_label.setText(uid)
self._pid_label.setText("%s" % con.process_id)
self._args_label.setText(' '.join(con.process_args))
self.uidLabel.setText(uid)
self.pidLabel.setText("%s" % con.process_id)
self._set_elide_text(self.argsLabel, ' '.join(con.process_args))
self.argsLabel.setToolTip(' '.join(con.process_args))
self._what_combo.clear()
self._what_combo.addItem("from this process")
self._what_combo.addItem("from user %d" % con.user_id)
self._what_combo.addItem("to port %d" % con.dst_port)
self._what_combo.addItem("to %s" % con.dst_ip)
if con.dst_host != "":
self._what_combo.addItem("to %s" % con.dst_host)
parts = con.dst_host.split('.')[1:]
nparts = len(parts)
for i in range(0, nparts - 1):
self._what_combo.addItem("to *.%s" % '.'.join(parts[i:]))
self.whatCombo.clear()
self.whatIPCombo.clear()
if int(con.process_id) > 0:
self.whatCombo.addItem("from this executable", self.FIELD_PROC_PATH)
if self._cfg.default_action == "allow":
self._action_combo.setCurrentIndex(0)
self.whatCombo.addItem("from this command line", self.FIELD_PROC_ARGS)
if self.argsLabel.text() == "":
self._set_elide_text(self.argsLabel, con.process_path)
# the order of the entries must match those in the preferences dialog
# prefs -> UI -> Default target
self.whatCombo.addItem("to port %d" % con.dst_port, self.FIELD_DST_PORT)
self.whatCombo.addItem("to %s" % con.dst_ip, self.FIELD_DST_IP)
if int(con.user_id) >= 0:
self.whatCombo.addItem("from user %s" % uid, self.FIELD_USER_ID)
self._add_dst_networks_to_combo(self.whatCombo, con.dst_ip)
if con.dst_host != "" and con.dst_host != con.dst_ip:
self._add_dsthost_to_combo(con.dst_host)
self.whatIPCombo.addItem("to %s" % con.dst_ip, self.FIELD_DST_IP)
parts = con.dst_ip.split('.')
nparts = len(parts)
for i in range(1, nparts):
self.whatCombo.addItem("to %s.*" % '.'.join(parts[:i]), self.FIELD_REGEX_IP)
self.whatIPCombo.addItem("to %s.*" % '.'.join(parts[:i]), self.FIELD_REGEX_IP)
self._add_dst_networks_to_combo(self.whatIPCombo, con.dst_ip)
self._default_action = self._cfg.getSettings(self.CFG_DEFAULT_ACTION)
self._configure_default_duration()
if int(con.process_id) > 0:
self.whatCombo.setCurrentIndex(int(self._cfg.getSettings("global/default_target")))
else:
self._action_combo.setCurrentIndex(1)
self.whatCombo.setCurrentIndex(2)
if self._cfg.default_duration == "once":
self._duration_combo.setCurrentIndex(0)
elif self._cfg.default_duration == "until restart":
self._duration_combo.setCurrentIndex(1)
else:
self._duration_combo.setCurrentIndex(2)
self._what_combo.setCurrentIndex(0)
self._apply_button.setText("Apply (%d)" % self._tick)
self._set_cmd_action_text()
self.setFixedSize(self.size())
@ -180,64 +326,146 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
if not event.key() == QtCore.Qt.Key_Escape:
super(PromptDialog, self).keyPressEvent(event)
# prevent a click on the window's x
# prevent a click on the window's x
# from quitting the whole application
def closeEvent(self, e):
self._on_apply_clicked()
self._send_rule()
e.ignore()
def _on_apply_clicked(self):
self._rule = ui_pb2.Rule(name="user.choice")
action_idx = self._action_combo.currentIndex()
if action_idx == 0:
self._rule.action = "allow"
def _add_dst_networks_to_combo(self, combo, dst_ip):
if type(ipaddress.ip_address(dst_ip)) == ipaddress.IPv4Address:
combo.addItem("to %s" % ipaddress.ip_network(dst_ip + "/24", strict=False), self.FIELD_DST_NETWORK)
combo.addItem("to %s" % ipaddress.ip_network(dst_ip + "/16", strict=False), self.FIELD_DST_NETWORK)
combo.addItem("to %s" % ipaddress.ip_network(dst_ip + "/8", strict=False), self.FIELD_DST_NETWORK)
else:
self._rule.action = "deny"
combo.addItem("to %s" % ipaddress.ip_network(dst_ip + "/64", strict=False), self.FIELD_DST_NETWORK)
combo.addItem("to %s" % ipaddress.ip_network(dst_ip + "/128", strict=False), self.FIELD_DST_NETWORK)
duration_idx = self._duration_combo.currentIndex()
def _add_dsthost_to_combo(self, dst_host):
self.whatCombo.addItem("%s" % dst_host, self.FIELD_DST_HOST)
self.whatIPCombo.addItem("%s" % dst_host, self.FIELD_DST_HOST)
parts = dst_host.split('.')[1:]
nparts = len(parts)
for i in range(0, nparts - 1):
self.whatCombo.addItem("to *.%s" % '.'.join(parts[i:]), self.FIELD_REGEX_HOST)
self.whatIPCombo.addItem("to *.%s" % '.'.join(parts[i:]), self.FIELD_REGEX_HOST)
if nparts == 1:
self.whatCombo.addItem("to *%s" % dst_host, self.FIELD_REGEX_HOST)
self.whatIPCombo.addItem("to *%s" % dst_host, self.FIELD_REGEX_HOST)
def _get_duration(self, duration_idx):
if duration_idx == 0:
self._rule.duration = "once"
return self.DURATION_once
elif duration_idx == 1:
self._rule.duration = "until restart"
return self.DURATION_30s
elif duration_idx == 2:
return self.DURATION_5m
elif duration_idx == 3:
return self.DURATION_15m
elif duration_idx == 4:
return self.DURATION_30m
elif duration_idx == 5:
return self.DURATION_1h
elif duration_idx == 6:
return self.DURATION_restart
else:
self._rule.duration = "always"
return self.DURATION_always
what_idx = self._what_combo.currentIndex()
if what_idx == 0:
self._rule.operator.type = "simple"
self._rule.operator.operand = "process.path"
self._rule.operator.data = self._con.process_path
def _get_combo_operator(self, combo, what_idx):
if combo.itemData(what_idx) == self.FIELD_PROC_PATH:
return "simple", "process.path", self._con.process_path
elif what_idx == 1:
self._rule.operator.type = "simple"
self._rule.operator.operand = "user.id"
self._rule.operator.data = "%s" % self._con.user_id
elif what_idx == 2:
self._rule.operator.type = "simple"
self._rule.operator.operand = "dest.port"
self._rule.operator.data = "%s" % self._con.dst_port
elif combo.itemData(what_idx) == self.FIELD_PROC_ARGS:
return "simple", "process.command", ' '.join(self._con.process_args)
elif what_idx == 3:
self._rule.operator.type = "simple"
self._rule.operator.operand = "dest.ip"
self._rule.operator.data = self._con.dst_ip
elif what_idx == 4:
self._rule.operator.type = "simple"
self._rule.operator.operand = "dest.host"
self._rule.operator.data = self._con.dst_host
elif combo.itemData(what_idx) == self.FIELD_USER_ID:
return "simple", "user.id", "%s" % self._con.user_id
elif combo.itemData(what_idx) == self.FIELD_DST_PORT:
return "simple", "dest.port", "%s" % self._con.dst_port
elif combo.itemData(what_idx) == self.FIELD_DST_IP:
return "simple", "dest.ip", self._con.dst_ip
elif combo.itemData(what_idx) == self.FIELD_DST_HOST:
return "simple", "dest.host", combo.currentText()
elif combo.itemData(what_idx) == self.FIELD_DST_NETWORK:
# strip "to ": "to x.x.x/20" -> "x.x.x/20"
return "simple", "dest.network", combo.currentText()[3:]
elif combo.itemData(what_idx) == self.FIELD_REGEX_HOST:
return "regexp", "dest.host", "%s" % '\.'.join(combo.currentText().split('.')).replace("*", ".*")[3:]
elif combo.itemData(what_idx) == self.FIELD_REGEX_IP:
return "regexp", "dest.ip", "%s" % '\.'.join(combo.currentText().split('.')).replace("*", ".*")[3:]
def _on_deny_clicked(self):
self._default_action = self.ACTION_DENY
self._send_rule()
def _on_apply_clicked(self):
self._default_action = self.ACTION_ALLOW
self._send_rule()
def _get_rule_name(self, rule):
rule_temp_name = slugify("%s %s" % (rule.action, rule.duration))
if self._ischeckAdvanceded:
rule_temp_name = "%s-list" % rule_temp_name
else:
self._rule.operator.type = "regexp"
self._rule.operator.operand = "dest.host"
self._rule.operator.data = ".*\.%s" % '\.'.join(self._con.dst_host.split('.')[what_idx - 4:])
rule_temp_name = "%s-simple" % rule_temp_name
rule_temp_name = slugify("%s %s" % (rule_temp_name, rule.operator.data))
return rule_temp_name
def _send_rule(self):
self._cfg.setSettings("promptDialog/geometry", self.saveGeometry())
self._rule = ui_pb2.Rule(name="user.choice")
self._rule.enabled = True
self._rule.action = self._default_action
self._rule.duration = self._get_duration(self.durationCombo.currentIndex())
what_idx = self.whatCombo.currentIndex()
self._rule.operator.type, self._rule.operator.operand, self._rule.operator.data = self._get_combo_operator(self.whatCombo, what_idx)
if self._rule.operator.data == "":
self._rule = None
self._done.set()
print("Invalid rule, discarding")
return
rule_temp_name = self._get_rule_name(self._rule)
self._rule.name = rule_temp_name
# TODO: move to a method
data=[]
if self._ischeckAdvanceded and self.checkDstIP.isChecked() and self.whatCombo.itemData(what_idx) != self.FIELD_DST_IP:
_type, _operand, _data = self._get_combo_operator(self.whatIPCombo, self.whatIPCombo.currentIndex())
data.append({"type": _type, "operand": _operand, "data": _data})
rule_temp_name = slugify("%s %s" % (rule_temp_name, _data))
if self._ischeckAdvanceded and self.checkDstPort.isChecked() and self.whatCombo.itemData(what_idx) != self.FIELD_DST_PORT:
data.append({"type": "simple", "operand": "dest.port", "data": str(self._con.dst_port)})
rule_temp_name = slugify("%s %s" % (rule_temp_name, str(self._con.dst_port)))
if self._ischeckAdvanceded and self.checkUserID.isChecked() and self.whatCombo.itemData(what_idx) != self.FIELD_USER_ID:
data.append({"type": "simple", "operand": "user.id", "data": str(self._con.user_id)})
rule_temp_name = slugify("%s %s" % (rule_temp_name, str(self._con.user_id)))
if self._ischeckAdvanceded:
data.append({"type": self._rule.operator.type, "operand": self._rule.operator.operand, "data": self._rule.operator.data})
self._rule.operator.data = json.dumps(data)
self._rule.operator.type = "list"
self._rule.operator.operand = ""
self._rule.name = rule_temp_name
self._rule.name = slugify("%s %s %s" % (self._rule.action, self._rule.operator.type, self._rule.operator.data))
self.hide()
# signal that the user took a decision and
if self._ischeckAdvanceded:
self.checkAdvanced.toggle()
self._idcheckAdvanceded = False
# signal that the user took a decision and
# a new rule is available
self._done.set()

View file

@ -0,0 +1,504 @@
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from slugify import slugify
from datetime import datetime
import re
import json
import sys
import os
import ui_pb2
import time
import ipaddress
from config import Config
from nodes import Nodes
from database import Database
DIALOG_UI_PATH = "%s/../res/ruleseditor.ui" % os.path.dirname(sys.modules[__name__].__file__)
class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
LOG_TAG = "[rules editor]"
classA_net = "10\.\d{1,3}\.\d{1,3}\.\d{1,3}"
classB_net = "172\.1[6-9]\.\d+\.\d+|172\.2[0-9]\.\d+\.\d+|172\.3[0-1]+\.\d{1,3}\.\d{1,3}"
classC_net = "192\.168\.\d{1,3}\.\d{1,3}"
others_net = "127\.\d{1,3}\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3}"
LAN_RANGES = "^(" + others_net + "|" + classC_net + "|" + classB_net + "|" + classA_net + "|::1|f[cde].*::.*)$"
LAN_LABEL = "LAN"
_notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
def __init__(self, parent=None, _rule=None):
super(RulesEditorDialog, self).__init__(parent)
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)
self._notifications_sent = {}
self._nodes = Nodes.instance()
self._db = Database.instance()
self._notification_callback.connect(self._cb_notification_callback)
self._old_rule_name = None
self.setupUi(self)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Reset).clicked.connect(self._cb_reset_clicked)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).clicked.connect(self._cb_close_clicked)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._cb_apply_clicked)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self._cb_help_clicked)
self.protoCheck.toggled.connect(self._cb_proto_check_toggled)
self.procCheck.toggled.connect(self._cb_proc_check_toggled)
self.cmdlineCheck.toggled.connect(self._cb_cmdline_check_toggled)
self.dstPortCheck.toggled.connect(self._cb_dstport_check_toggled)
self.uidCheck.toggled.connect(self._cb_uid_check_toggled)
self.dstIPCheck.toggled.connect(self._cb_dstip_check_toggled)
self.dstHostCheck.toggled.connect(self._cb_dsthost_check_toggled)
if QtGui.QIcon.hasThemeIcon("emblem-default") == False:
self.actionAllowRadio.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogApplyButton")))
self.actionDenyRadio.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCancelButton")))
if _rule != None:
self._load_rule(rule=_rule)
def _bool(self, s):
return s == 'True'
def _cb_accept_clicked(self):
pass
def _cb_close_clicked(self):
self.hide()
def _cb_reset_clicked(self):
self._reset_state()
def _cb_help_clicked(self):
QtGui.QDesktopServices.openUrl(QtCore.QUrl(Config.HELP_URL))
def _cb_proto_check_toggled(self, state):
self.protoCombo.setEnabled(state)
def _cb_proc_check_toggled(self, state):
self.procLine.setEnabled(state)
def _cb_cmdline_check_toggled(self, state):
self.cmdlineLine.setEnabled(state)
def _cb_dstport_check_toggled(self, state):
self.dstPortLine.setEnabled(state)
def _cb_uid_check_toggled(self, state):
self.uidLine.setEnabled(state)
def _cb_dstip_check_toggled(self, state):
self.dstIPCombo.setEnabled(state)
def _cb_dsthost_check_toggled(self, state):
self.dstHostLine.setEnabled(state)
def _set_status_error(self, msg):
self.statusLabel.setStyleSheet('color: red')
self.statusLabel.setText(msg)
def _set_status_message(self, msg):
self.statusLabel.setStyleSheet('color: green')
self.statusLabel.setText(msg)
def _cb_apply_clicked(self):
result, error = self._save_rule()
if result == False:
self._set_status_error(error)
return
if self.nodesCombo.count() == 0:
self._set_status_error("There're no nodes connected.")
return
self._add_rule()
self._delete_rule()
@QtCore.pyqtSlot(ui_pb2.NotificationReply)
def _cb_notification_callback(self, reply):
#print(self.LOG_TAG, "Rule notification received: ", reply.id, reply.code)
if reply.id in self._notifications_sent:
if reply.code == ui_pb2.OK:
self._set_status_message("Rule applied.")
else:
self._set_status_error("Error applying rule: %s" % reply.data)
del self._notifications_sent[reply.id]
def _is_regex(self, text):
charset="\\*{[|^?$"
for c in charset:
if c in text:
return True
return False
def _is_valid_regex(self, regex):
try:
re.compile(regex)
return True
except re.error as e:
self.statusLabel.setText(str(e))
return False
def _reset_state(self):
self.ruleNameEdit.setText("")
self.statusLabel.setText("")
self.actionDenyRadio.setChecked(True)
self.durationCombo.setCurrentIndex(0)
self.protoCheck.setChecked(False)
self.protoCombo.setCurrentText("")
self.procCheck.setChecked(False)
self.procLine.setText("")
self.cmdlineCheck.setChecked(False)
self.cmdlineLine.setText("")
self.uidCheck.setChecked(False)
self.uidLine.setText("")
self.dstPortCheck.setChecked(False)
self.dstPortLine.setText("")
self.dstIPCheck.setChecked(False)
self.dstIPCombo.setCurrentText("")
self.dstHostCheck.setChecked(False)
self.dstHostLine.setText("")
def _load_rule(self, addr=None, rule=None):
self._load_nodes(addr)
self.ruleNameEdit.setText(rule.name)
self.enableCheck.setChecked(rule.enabled)
self.precedenceCheck.setChecked(rule.precedence)
if rule.action == "deny":
self.actionDenyRadio.setChecked(True)
if rule.action == "allow":
self.actionAllowRadio.setChecked(True)
self.durationCombo.setCurrentText(self.rule.duration)
if self.rule.operator.type != "list":
self._load_rule_operator(self.rule.operator)
else:
rule_options = json.loads(self.rule.operator.data)
for r in rule_options:
_sensitive = False
if 'sensitive' in r:
_sensitive = r['sensitive']
op = ui_pb2.Operator(type=r['type'], operand=r['operand'], data=r['data'], sensitive=_sensitive)
self._load_rule_operator(op)
def _load_rule_operator(self, operator):
self.sensitiveCheck.setChecked(operator.sensitive)
if operator.operand == "protocol":
self.protoCheck.setChecked(True)
self.protoCombo.setEnabled(True)
self.protoCombo.setCurrentText(operator.data.upper())
if operator.operand == "process.path":
self.procCheck.setChecked(True)
self.procLine.setEnabled(True)
self.procLine.setText(operator.data)
if operator.operand == "process.command":
self.cmdlineCheck.setChecked(True)
self.cmdlineLine.setEnabled(True)
self.cmdlineLine.setText(operator.data)
if operator.operand == "user.id":
self.uidCheck.setChecked(True)
self.uidLine.setEnabled(True)
self.uidLine.setText(operator.data)
if operator.operand == "dest.port":
self.dstPortCheck.setChecked(True)
self.dstPortLine.setEnabled(True)
self.dstPortLine.setText(operator.data)
if operator.operand == "dest.ip" or operator.operand == "dest.network":
self.dstIPCheck.setChecked(True)
self.dstIPCombo.setEnabled(True)
if operator.data == self.LAN_RANGES:
self.dstIPCombo.setCurrentText(self.LAN_LABEL)
else:
self.dstIPCombo.setCurrentText(operator.data)
if operator.operand == "dest.host":
self.dstHostCheck.setChecked(True)
self.dstHostLine.setEnabled(True)
self.dstHostLine.setText(operator.data)
def _load_nodes(self, addr=None):
try:
self.nodesCombo.clear()
self._node_list = self._nodes.get()
if len(self._node_list) <= 1:
self.nodeApplyAllCheck.setVisible(False)
for node in self._node_list:
self.nodesCombo.addItem(node)
if addr != None:
self.nodesCombo.setCurrentText(addr)
except Exception as e:
print(self.LOG_TAG, "exception loading nodes: ", e, addr)
def _insert_rule_to_db(self, node_addr):
self._db.insert("rules",
"(time, node, name, enabled, precedence, action, duration, operator_type, operator_sensitive, operator_operand, operator_data)",
(datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
node_addr, self.rule.name,
str(self.rule.enabled), str(self.rule.precedence),
self.rule.action, self.rule.duration, self.rule.operator.type,
str(self.rule.operator.sensitive), self.rule.operator.operand, self.rule.operator.data),
action_on_conflict="REPLACE")
def _add_rule(self):
try:
if self.nodeApplyAllCheck.isChecked():
for pos in range(self.nodesCombo.count()):
self._insert_rule_to_db(self.nodesCombo.itemText(pos))
else:
self._insert_rule_to_db(self.nodesCombo.currentText())
notif = ui_pb2.Notification(
id=int(str(time.time()).replace(".", "")),
type=ui_pb2.CHANGE_RULE,
data="",
rules=[self.rule])
if self.nodeApplyAllCheck.isChecked():
nid = self._nodes.send_notifications(notif, self._notification_callback)
else:
nid = self._nodes.send_notification(self.nodesCombo.currentText(), notif, self._notification_callback)
self._notifications_sent[nid] = notif
except Exception as e:
print(self.LOG_TAG, "add_rule() exception: ", e)
def _delete_rule(self):
try:
if self._old_rule_name != None:
# if the rule name has changed, we need to remove the old one
if self._old_rule_name != self.rule.name:
self._db.remove("DELETE FROM rules WHERE name='%s'" % self._old_rule_name)
old_rule = self.rule
old_rule.name = self._old_rule_name
notif_delete = ui_pb2.Notification(type=ui_pb2.DELETE_RULE, rules=[old_rule])
if self.nodeApplyAllCheck.isChecked():
nid = self._nodes.send_notifications(notif_delete, self._notification_callback)
else:
nid = self._nodes.send_notification(self.nodesCombo.currentText(), notif_delete, self._notification_callback)
self._old_rule_name = None
except Exception as e:
print(self.LOG_TAG, "delete_rule() exception: ", e)
def _save_rule(self):
"""
Create a new rule based on the fields selected.
Ensure that some constraints are met:
- Determine if a field can be a regexp.
- Validate regexp.
- Fields cam not be empty.
- If the user has not provided a rule name, auto assign one.
"""
self.rule = ui_pb2.Rule()
self.rule.name = self.ruleNameEdit.text()
self.rule.enabled = self.enableCheck.isChecked()
self.rule.precedence = self.precedenceCheck.isChecked()
self.rule.action = "deny" if self.actionDenyRadio.isChecked() else "allow"
self.rule.duration = self.durationCombo.currentText()
self.rule.operator.type = "simple"
# FIXME: there should be a sensitive checkbox per operand
self.rule.operator.sensitive = self.sensitiveCheck.isChecked()
rule_data = []
if self.protoCheck.isChecked():
if self.protoCombo.currentText() == "":
return False, "Protocol can not be empty"
self.rule.operator.operand = "protocol"
self.rule.operator.data = self.protoCombo.currentText()
rule_data.append(
{
"type": "simple",
"operand": "protocol",
"data": self.protoCombo.currentText().lower(),
"sensitive": self.sensitiveCheck.isChecked()
})
if self._is_regex(self.protoCombo.currentText()):
rule_data[len(rule_data)-1]['type'] = "regexp"
if self._is_valid_regex(self.protoCombo.currentText()) == False:
return False, "Protocol error: invalid regular expression"
if self.procCheck.isChecked():
if self.procLine.text() == "":
return False, "Process path can not be empty"
self.rule.operator.operand = "process.path"
self.rule.operator.data = self.procLine.text()
rule_data.append(
{
"type": "simple",
"operand": "process.path",
"data": self.procLine.text(),
"sensitive": self.sensitiveCheck.isChecked()
})
if self._is_regex(self.procLine.text()):
rule_data[len(rule_data)-1]['type'] = "regexp"
if self._is_valid_regex(self.procLine.text()) == False:
return False, "Process path regexp error"
if self.cmdlineCheck.isChecked():
if self.cmdlineLine.text() == "":
return False, "Command line can not be empty"
self.rule.operator.operand = "process.command"
self.rule.operator.data = self.cmdlineLine.text()
rule_data.append(
{
'type': 'simple',
'operand': 'process.command',
'data': self.cmdlineLine.text(),
"sensitive": self.sensitiveCheck.isChecked()
})
if self._is_regex(self.cmdlineLine.text()):
rule_data[len(rule_data)-1]['type'] = "regexp"
if self._is_valid_regex(self.cmdlineLine.text()) == False:
return False, "Command line regexp error"
if self.dstPortCheck.isChecked():
if self.dstPortLine.text() == "":
return False, "Destination port can not be empty"
self.rule.operator.operand = "dest.port"
self.rule.operator.data = self.dstPortLine.text()
rule_data.append(
{
'type': 'simple',
'operand': 'dest.port',
'data': self.dstPortLine.text(),
"sensitive": self.sensitiveCheck.isChecked()
})
if self._is_regex(self.dstPortLine.text()):
rule_data[len(rule_data)-1]['type'] = "regexp"
if self._is_valid_regex(self.dstPortLine.text()) == False:
return False, "Destination port error: regular expression not valid"
if self.dstHostCheck.isChecked():
if self.dstHostLine.text() == "":
return False, "Destination host can not be empty"
self.rule.operator.operand = "dest.host"
self.rule.operator.data = self.dstHostLine.text()
rule_data.append(
{
'type': 'simple',
'operand': 'dest.host',
'data': self.dstHostLine.text(),
"sensitive": self.sensitiveCheck.isChecked()
})
if self._is_regex(self.dstHostLine.text()):
rule_data[len(rule_data)-1]['type'] = "regexp"
if self._is_valid_regex(self.dstHostLine.text()) == False:
return False, "Destination host error: regular expression not valid"
if self.dstIPCheck.isChecked():
if self.dstIPCombo.currentText() == "":
return False, "Destination IP/Network can not be empty"
dstIPtext = self.dstIPCombo.currentText()
if dstIPtext == self.LAN_LABEL:
self.rule.operator.operand = "dest.ip"
self.rule.operator.type = "regexp"
dstIPtext = self.LAN_RANGES
else:
try:
if type(ipaddress.ip_address(self.dstIPCombo.currentText())) == ipaddress.IPv4Address \
or type(ipaddress.ip_address(self.dstIPCombo.currentText())) == ipaddress.IPv6Address:
self.rule.operator.operand = "dest.ip"
self.rule.operator.type = "simple"
except Exception:
self.rule.operator.operand = "dest.network"
self.rule.operator.type = "network"
if self._is_regex(dstIPtext):
self.rule.operator.type = "regexp"
if self._is_valid_regex(self.dstIPCombo.currentText()) == False:
return False, "Destination IP error: regular expression not valid"
rule_data.append(
{
'type': self.rule.operator.type,
'operand': self.rule.operator.operand,
'data': dstIPtext,
"sensitive": self.sensitiveCheck.isChecked()
})
if self.uidCheck.isChecked():
if self.uidLine.text() == "":
return False, "User ID can not be empty"
self.rule.operator.operand = "user.id"
self.rule.operator.data = self.uidLine.text()
rule_data.append(
{
'type': 'simple',
'operand': 'user.id',
'data': self.uidLine.text(),
"sensitive": self.sensitiveCheck.isChecked()
})
if self._is_regex(self.uidLine.text()):
rule_data[len(rule_data)-1]['type'] = "regexp"
if self._is_valid_regex(self.uidLine.text()) == False:
return False, "User ID error: invalid regular expression"
if len(rule_data) > 1:
self.rule.operator.type = "list"
self.rule.operator.operand = ""
self.rule.operator.data = json.dumps(rule_data)
elif len(rule_data) == 1:
self.rule.operator.operand = rule_data[0]['operand']
self.rule.operator.data = rule_data[0]['data']
if self._is_regex(self.rule.operator.data):
self.rule.operator.type = "regexp"
if self.ruleNameEdit.text() == "":
self.rule.name = slugify("%s %s %s" % (self.rule.action, self.rule.operator.type, self.rule.operator.data))
return True, ""
def edit_rule(self, records, _addr=None):
self._reset_state()
self.rule = ui_pb2.Rule(name=records.value(2))
self.rule.enabled = self._bool(records.value(3))
self.rule.precedence = self._bool(records.value(4))
self.rule.action = records.value(5)
self.rule.duration = records.value(6)
self.rule.operator.type = records.value(7)
self.rule.operator.sensitive = self._bool(records.value(8))
self.rule.operator.operand = records.value(9)
self.rule.operator.data = "" if records.value(10) == None else str(records.value(10))
self._old_rule_name = records.value(2)
self._load_rule(addr=_addr, rule=self.rule)
self.show()
def new_rule(self):
self._reset_state()
self._load_nodes()
self.show()

File diff suppressed because it is too large Load diff

212
ui/opensnitch/nodes.py Normal file
View file

@ -0,0 +1,212 @@
from queue import Queue
from datetime import datetime
import time
import json
import ui_pb2
from database import Database
class Nodes():
__instance = None
LOG_TAG = "[Nodes]: "
ONLINE = "\u2713 online"
OFFLINE = "\u2613 offline"
WARNING = "\u26a0"
@staticmethod
def instance():
if Nodes.__instance == None:
Nodes.__instance = Nodes()
return Nodes.__instance
def __init__(self):
self._db = Database.instance()
self._nodes = {}
self._notifications_sent = {}
def count(self):
return len(self._nodes)
def add(self, context, client_config=None):
try:
proto, _addr = self.get_addr(context.peer())
addr = "%s:%s" % (proto, _addr)
if addr not in self._nodes:
self._nodes[addr] = {
'notifications': Queue(),
'online': True,
'last_seen': datetime.now()
}
self.add_data(addr, client_config)
return self._nodes[addr]
self._nodes[addr]['last_seen'] = datetime.now()
self.add_data(addr, client_config)
self._nodes.update(proto, addr)
return self._nodes[addr]
except Exception as e:
print(self.LOG_TAG + " exception adding/updating node: ", e, addr, client_config)
return None
def add_data(self, addr, client_config):
if client_config != None:
self._nodes[addr]['data'] = self.get_client_config(client_config)
self.add_rules(addr, client_config.rules)
def add_rules(self, addr, rules):
try:
for _,r in enumerate(rules):
self._db.insert("rules",
"(time, node, name, enabled, precedence, action, duration, operator_type, operator_sensitive, operator_operand, operator_data)",
(datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
addr,
r.name, str(r.enabled), str(r.precedence), r.action, r.duration,
r.operator.type,
str(r.operator.sensitive),
r.operator.operand,
r.operator.data),
action_on_conflict="IGNORE")
except Exception as e:
print(self.LOG_TAG + " exception adding node to db: ", e)
def delete_all(self):
self.send_notifications(None)
self._nodes = {}
def delete(self, peer):
proto, addr = self.get_addr(peer)
addr = "%s:%s" % (proto, addr)
# Force the node to get one new item from queue,
# in order to loop and exit.
self._nodes[addr]['notifications'].put(None)
if addr in self._nodes:
del self._nodes[addr]
def get(self):
return self._nodes
def get_node(self, addr):
try:
return self._nodes[addr]
except Exception as e:
return None
def get_nodes(self):
return self._nodes
def get_node_config(self, addr):
try:
return self._nodes[addr]['data'].config
except Exception as e:
print(self.LOG_TAG + " exception get_node_config(): ", e)
return None
def get_client_config(self, client_config):
try:
node_config = json.loads(client_config.config)
if 'LogLevel' not in node_config:
node_config['LogLevel'] = 1
client_config.config = json.dumps(node_config)
except Exception as e:
print(self.LOG_TAG, "exception parsing client config", e)
return client_config
def get_addr(self, peer):
peer = peer.split(":")
# WA for backward compatibility
if peer[0] == "unix" and peer[1] == "":
peer[1] = "local"
return peer[0], peer[1]
def get_notifications(self):
notlist = []
try:
for c in self._nodes:
if self._nodes[c]['online'] == False:
continue
if self._nodes[c]['notifications'].empty():
continue
notif = self._nodes[c]['notifications'].get(False)
if notif != None:
self._nodes[c]['notifications'].task_done()
notlist.append(notif)
except Exception as e:
print(self.LOG_TAG + " exception get_notifications(): ", e)
return notlist
def save_node_config(self, addr, config):
try:
self._nodes[addr]['data'].config = config
except Exception as e:
print(self.LOG_TAG + " exception saving node config: ", e, addr, config)
def save_nodes_config(self, config):
try:
for c in self._nodes:
self._nodes[c]['data'].config = config
except Exception as e:
print(self.LOG_TAG + " exception saving nodes config: ", e, config)
def send_notification(self, addr, notification, callback_signal=None):
try:
notification.id = int(str(time.time()).replace(".", ""))
self._nodes[addr]['notifications'].put(notification)
self._notifications_sent[notification.id] = {
'callback': callback_signal,
'type': notification.type
}
except Exception as e:
print(self.LOG_TAG + " exception sending notification: ", e, addr, notification)
return notification.id
def send_notifications(self, notification, callback_signal=None):
"""
Enqueues a notification to the clients queue.
It'll be retrieved and delivered by get_notifications
"""
try:
notification.id = int(str(time.time()).replace(".", ""))
for c in self._nodes:
self._nodes[c]['notifications'].put(notification)
self._notifications_sent[notification.id] = {
'callback': callback_signal,
'type': notification.type
}
except Exception as e:
print(self.LOG_TAG + " exception sending notifications: ", e, notification)
return notification.id
def reply_notification(self, addr, reply):
if reply == None:
print(self.LOG_TAG, " reply notification None")
return
if reply.id in self._notifications_sent:
if self._notifications_sent[reply.id] != None:
self._notifications_sent[reply.id]['callback'].emit(reply)
# delete only one-time notifications
# we need the ID of streaming notifications from the server
# (monitor_process for example) to keep track of the data sent to us.
if self._notifications_sent[reply.id]['type'] != ui_pb2.MONITOR_PROCESS:
del self._notifications_sent[reply.id]
def update(self, proto, addr, status=ONLINE):
try:
self._db.update("nodes",
"hostname=?,version=?,last_connection=?,status=? WHERE addr=?",
(
self._nodes[proto+":"+addr]['data'].name,
self._nodes[proto+":"+addr]['data'].version,
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
status,
addr)
)
except Exception as e:
print(self.LOG_TAG + " exception updating DB: ", e, addr)

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="opensnitch.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="19.303571"
inkscape:cy="20.32272"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1600"
inkscape:window-height="843"
inkscape:window-x="0"
inkscape:window-y="22"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-284.30001)">
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.0245797;stroke-opacity:1"
d="m 2.4110472,295.34968 c -1.0890422,-0.15882 -1.97641286,-0.9635 -2.26943585,-2.05789 -0.0569801,-0.21285 -0.06364,-0.28265 -0.06364,-0.66784 0,-0.38518 0.00663,-0.45498 0.06364,-0.6678 0.13469986,-0.50309 0.37403805,-0.92281 0.72817063,-1.27693 0.29692112,-0.29693 0.71853312,-0.56133 1.05654452,-0.66262 l 0.094435,-0.0281 0.014905,-0.2737 c 0.06535,-1.20079 0.8896319,-2.19921 2.0774265,-2.51629 0.2102802,-0.0562 0.2858346,-0.0637 0.6555276,-0.0645 0.2358646,-5.9e-4 0.4658404,0.0114 0.5280299,0.027 0.1101628,0.0281 0.1101856,0.0281 0.181866,-0.0629 0.142668,-0.18079 0.4658296,-0.4742 0.6804028,-0.6177 0.5045072,-0.33746 1.0168304,-0.5065 1.6234736,-0.53562 0.5389081,-0.0256 1.0259376,0.0746 1.5098826,0.31109 0.6657931,0.32548 1.1376885,0.79739 1.4631855,1.46319 0.234048,0.47874 0.335764,0.96701 0.312284,1.49906 l -0.01219,0.27678 0.19938,0.13128 c 0.109657,0.0722 0.317056,0.24806 0.460886,0.39074 0.81399,0.8075 1.115088,1.93261 0.817664,3.05541 -0.121324,0.45797 -0.457927,1.0356 -0.806849,1.3845 -0.333376,0.33338 -0.915819,0.67934 -1.341539,0.79684 -0.4507555,0.12445 -0.3946655,0.12289 -4.2399943,0.11981 -1.9737469,-0.002 -3.6540593,-0.0125 -3.7340265,-0.0242 z m 7.6534148,-0.82834 c 0.546395,-0.1461 1.032515,-0.47864 1.360332,-0.93055 0.135216,-0.18643 0.303054,-0.55751 0.365694,-0.80853 0.07729,-0.30967 0.07745,-0.79285 3.73e-4,-1.10158 -0.18597,-0.74499 -0.76082,-1.39061 -1.461267,-1.64119 -0.140048,-0.0501 -0.213134,-0.0884 -0.205663,-0.1079 0.231186,-0.60245 0.205751,-1.2656 -0.07094,-1.84969 -0.2423058,-0.51155 -0.6123029,-0.88174 -1.1213949,-1.12201 -0.3650686,-0.1723 -0.5821245,-0.2198 -1.0044907,-0.2198 -0.2921095,0 -0.3940948,0.0101 -0.565332,0.0557 -0.6762958,0.17987 -1.2431499,0.63564 -1.5377998,1.23642 l -0.06841,0.13949 -0.1124917,-0.0569 c -0.2092944,-0.10576 -0.5176658,-0.18751 -0.7638191,-0.20252 -0.85275,-0.052 -1.6823045,0.51274 -1.9549381,1.33076 -0.1543876,0.46323 -0.1388056,0.90554 0.049015,1.39126 0.012596,0.0324 -0.0081,0.0365 -0.1433824,0.0278 -0.190616,-0.0122 -0.5005549,0.0466 -0.7439852,0.14113 -0.50046,0.19422 -0.9543977,0.67125 -1.12429124,1.18155 -0.15751069,0.47307 -0.14731701,0.90357 0.032379,1.36764 0.19430044,0.50172 0.67077894,0.95419 1.18542864,1.12568 0.1036062,0.0344 0.2473974,0.0725 0.3195359,0.0848 0.07423,0.0126 1.6994369,0.0205 3.7443725,0.0181 l 3.6132124,-0.004 z m -5.2712705,-1.2975 c -0.527233,-0.31641 -0.9586066,-0.58634 -0.9586066,-0.59989 0,-0.0137 0.4313736,-0.28347 0.9586066,-0.59989 l 0.9586077,-0.5752 0.00675,0.39089 0.00675,0.39094 h 1.5724847 1.5724903 v 0.39326 0.39327 H 7.3377568 5.7652653 l -0.00675,0.39094 -0.00675,0.39093 z m 2.1507227,-2.17014 v -0.39612 H 5.3708161 3.7977151 v -0.39326 -0.39328 h 1.5724905 1.5724916 l 0.00675,-0.39093 0.00675,-0.39093 0.9585929,0.57524 c 0.5272291,0.3164 0.961696,0.58453 0.9654831,0.59591 0.00375,0.0115 -0.4054584,0.26764 -0.9094363,0.56956 -0.5039759,0.30187 -0.9412097,0.56487 -0.9716265,0.58441 l -0.055308,0.0354 z"
id="path826"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -0,0 +1,737 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PreferencesDialog</class>
<widget class="QDialog" name="PreferencesDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>662</width>
<height>415</height>
</rect>
</property>
<property name="windowTitle">
<string>Preferences</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="tabPosition">
<enum>QTabWidget::North</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>false</bool>
</property>
<widget class="QWidget" name="tab">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<attribute name="title">
<string>UI</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This timeout is the countdown you see when a pop-up dialog is shown.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Default timeout</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="toolTip">
<string>Pop-up default duration</string>
</property>
<property name="text">
<string>Default duration</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>Pop-up default action</string>
</property>
<property name="text">
<string>Default action</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Default target</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QSpinBox" name="spinUITimeout">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="value">
<number>15</number>
</property>
</widget>
</item>
<item row="5" column="3">
<widget class="QComboBox" name="comboUIDialogPos">
<property name="enabled">
<bool>false</bool>
</property>
<item>
<property name="text">
<string>center</string>
</property>
</item>
<item>
<property name="text">
<string>top right</string>
</property>
</item>
<item>
<property name="text">
<string>bottom right</string>
</property>
</item>
<item>
<property name="text">
<string>top left</string>
</property>
</item>
<item>
<property name="text">
<string>bottom left</string>
</property>
</item>
</widget>
</item>
<item row="6" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Prompt dialog default position on screen</string>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QComboBox" name="comboUITarget">
<item>
<property name="text">
<string>by executable</string>
</property>
</item>
<item>
<property name="text">
<string>by command line</string>
</property>
</item>
<item>
<property name="text">
<string>by destination port</string>
</property>
</item>
<item>
<property name="text">
<string>by destination ip</string>
</property>
</item>
<item>
<property name="text">
<string>by user id</string>
</property>
</item>
</widget>
</item>
<item row="3" column="3">
<widget class="QComboBox" name="comboUIDuration">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>once</string>
</property>
</item>
<item>
<property name="text">
<string>30s</string>
</property>
</item>
<item>
<property name="text">
<string>5m</string>
</property>
</item>
<item>
<property name="text">
<string>15m</string>
</property>
</item>
<item>
<property name="text">
<string>30m</string>
</property>
</item>
<item>
<property name="text">
<string>1h</string>
</property>
</item>
<item>
<property name="text">
<string>for this session</string>
</property>
</item>
<item>
<property name="text">
<string>forever</string>
</property>
</item>
</widget>
</item>
<item row="2" column="3">
<widget class="QComboBox" name="comboUIAction">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>deny</string>
</property>
<property name="icon">
<iconset theme="emblem-important">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>allow</string>
</property>
<property name="icon">
<iconset theme="emblem-default">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Disable pop-ups, only display an alert</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="popupsCheck">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Nodes</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="9" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Process monitor method</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_11">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The default duration will take place when there's no UI connected.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Default duration</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_15">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Address of the node.&lt;/p&gt;&lt;p&gt;Default: unix:///tmp/osui.sock (unix:// is mandatory if it's a Unix socket)&lt;/p&gt;&lt;p&gt;It can also be an IP address with the port: 127.0.0.1:50051&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Address</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Default log level</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="comboNodes">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_9">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_10">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The default action will take place when there's no UI connected.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Default action</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="labelNodeVersion">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="9" column="2">
<widget class="QComboBox" name="comboNodeMonitorMethod">
<item>
<property name="text">
<string>proc</string>
</property>
</item>
<item>
<property name="text">
<string>audit</string>
</property>
</item>
<item>
<property name="text">
<string>ftrace</string>
</property>
</item>
</widget>
</item>
<item row="11" column="0">
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>60</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="2">
<widget class="QComboBox" name="comboNodeAction">
<property name="editable">
<bool>false</bool>
</property>
<item>
<property name="text">
<string>deny</string>
</property>
<property name="icon">
<iconset theme="emblem-important">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
<item>
<property name="text">
<string>allow</string>
</property>
<property name="icon">
<iconset theme="emblem-default">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</item>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_7">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Log file to write logs.&lt;br/&gt;&lt;/p&gt;&lt;p&gt;/dev/stdout will print logs to the standard output.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Log file</string>
</property>
</widget>
</item>
<item row="10" column="2">
<widget class="QComboBox" name="comboNodeLogLevel">
<item>
<property name="text">
<string>DEBUG</string>
</property>
</item>
<item>
<property name="text">
<string>INFO</string>
</property>
</item>
<item>
<property name="text">
<string>IMPORTANT</string>
</property>
</item>
<item>
<property name="text">
<string>WARNING</string>
</property>
</item>
<item>
<property name="text">
<string>ERROR</string>
</property>
</item>
<item>
<property name="text">
<string>FATAL</string>
</property>
</item>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_12">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, opensnitch will prompt you to allow or deny connections that don't have an asocciated PID, due to several reasons.&lt;/p&gt;&lt;p&gt;The pop-up dialog will only contain information about the network connection.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Intercept Unknown Connections</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="labelNodeName">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>HostName</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QComboBox" name="comboNodeAddress">
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>unix:///tmp/osui.sock</string>
</property>
</item>
</widget>
</item>
<item row="7" column="2">
<widget class="QComboBox" name="comboNodeDuration">
<item>
<property name="text">
<string>once</string>
</property>
</item>
<item>
<property name="text">
<string>until restart</string>
</property>
</item>
<item>
<property name="text">
<string>always</string>
</property>
</item>
</widget>
</item>
<item row="5" column="2">
<widget class="QComboBox" name="comboNodeLogFile">
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>/var/log/opensnitchd.log</string>
</property>
</item>
<item>
<property name="text">
<string>/dev/stdout</string>
</property>
</item>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="checkApplyToNodes">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Apply configuration to all nodes</string>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QCheckBox" name="checkInterceptUnknown">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0" colspan="3">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Database</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3" rowstretch="0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
<number>9</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="0" column="2">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Database name</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QComboBox" name="comboDBType">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>In memory</string>
</property>
</item>
<item>
<property name="text">
<string>File</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0" colspan="4">
<widget class="QLineEdit" name="lineFileDB">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="placeholderText">
<string>/path/to/the/file.db</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Close</string>
</property>
<property name="icon">
<iconset theme="window-close">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="applyButton">
<property name="text">
<string>Apply</string>
</property>
<property name="icon">
<iconset theme="document-save"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="acceptButton">
<property name="text">
<string>Save</string>
</property>
<property name="icon">
<iconset theme="emblem-default">
<normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="statusLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,269 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ProcessDetailsDialog</class>
<widget class="QDialog" name="ProcessDetailsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>731</width>
<height>478</height>
</rect>
</property>
<property name="windowTitle">
<string>Process details</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="labelProcIcon">
<property name="minimumSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="labelProcName">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string>loading...</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelProcArgs">
<property name="text">
<string>loading...</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="labelCwd">
<property name="text">
<string>CWD: loading...</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="labelStatm">
<property name="text">
<string>mem stats: loading...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="tabPosition">
<enum>QTabWidget::South</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Status</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="1">
<widget class="QPlainTextEdit" name="textStatus">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Open files</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="0">
<widget class="QPlainTextEdit" name="textOpenedFiles">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabWidgetPage1">
<attribute name="title">
<string>I/O Statistics</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPlainTextEdit" name="textIOStats">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabWidgetPage2">
<attribute name="title">
<string>Memory mapped files</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QPlainTextEdit" name="textMappedFiles">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Stack</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QPlainTextEdit" name="textStack">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_4">
<attribute name="title">
<string>Environment variables</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QPlainTextEdit" name="textEnv">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Application pids</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboPids">
<property name="maxVisibleItems">
<number>100</number>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cmdAction">
<property name="toolTip">
<string>Start or stop monitoring this process</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="media-playback-start"/>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cmdClose">
<property name="text">
<string>Close</string>
</property>
<property name="icon">
<iconset theme="window-close"/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

Some files were not shown because too many files have changed in this diff Show more