mirror of
https://github.com/evilsocket/opensnitch.git
synced 2025-03-04 00:24:40 +01:00
Merge opensnitch 1.3.0-rc2
This commit is contained in:
commit
3a3d3d8f42
113 changed files with 14163 additions and 2470 deletions
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
31
.github/workflows/go.yml
vendored
Normal 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
847
LICENSE
|
@ -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
185
README.md
|
@ -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
120
daemon/Gopkg.lock
generated
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
23
daemon/core/system.go
Normal 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)
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
|
12
daemon/default-config.json
Normal file
12
daemon/default-config.json
Normal 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
21
daemon/dns/parse.go
Normal 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
|
||||
}
|
|
@ -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
110
daemon/firewall/config.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
19
daemon/go.mod
Normal 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
|
||||
)
|
|
@ -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)
|
||||
|
|
224
daemon/main.go
224
daemon/main.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
118
daemon/netlink/socket.go
Normal 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")
|
||||
}
|
245
daemon/netlink/socket_linux.go
Normal file
245
daemon/netlink/socket_linux.go
Normal 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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
91
daemon/opensnitch.spec
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
345
daemon/procmon/audit/client.go
Normal file
345
daemon/procmon/audit/client.go
Normal 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
|
||||
}
|
298
daemon/procmon/audit/parse.go
Normal file
298
daemon/procmon/audit/parse.go
Normal 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
143
daemon/procmon/cache.go
Normal 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
182
daemon/procmon/details.go
Normal 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
103
daemon/procmon/find.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
14
daemon/system-fw.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"SystemRules": [
|
||||
{
|
||||
"Rule": {
|
||||
"Description": "Allow icmp",
|
||||
"Table": "mangle",
|
||||
"Chain": "OUTPUT",
|
||||
"Parameters": "-p icmp",
|
||||
"Target": "ACCEPT",
|
||||
"TargetParameters": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
116
daemon/ui/config.go
Normal 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
295
daemon/ui/notifications.go
Normal 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")
|
||||
}
|
|
@ -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
108
debian/changelog
vendored
Normal 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
49
debian/control
vendored
Normal 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
31
debian/copyright
vendored
Normal 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
2
debian/gbp.conf
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[DEFAULT]
|
||||
pristine-tar = True
|
27
debian/gitlab-ci.yml
vendored
Normal file
27
debian/gitlab-ci.yml
vendored
Normal 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
78
debian/opensnitch.init
vendored
Normal 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
2
debian/opensnitch.install
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
daemon/default-config.json etc/opensnitchd/
|
||||
daemon/system-fw.json etc/opensnitchd/
|
13
debian/opensnitch.logrotate
vendored
Normal file
13
debian/opensnitch.logrotate
vendored
Normal 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
16
debian/opensnitch.service
vendored
Normal 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
8
debian/postinst
vendored
Executable 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
6
debian/prerm
vendored
Executable file
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
service opensnitch stop || true
|
||||
systemctl disable opensnitch.service || true
|
13
debian/rules
vendored
Executable file
13
debian/rules
vendored
Executable 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
1
debian/source/format
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
4
debian/watch
vendored
Normal file
4
debian/watch
vendored
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
BIN
screenshots/opensnitch-ui-general-tab-deny.png
Normal file
BIN
screenshots/opensnitch-ui-general-tab-deny.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
BIN
screenshots/opensnitch-ui-proc-details.png
Normal file
BIN
screenshots/opensnitch-ui-proc-details.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 443 KiB |
82
server/api/api.go
Normal file
82
server/api/api.go
Normal 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
132
server/api/client.go
Normal 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
3
server/api/go.mod
Normal 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
87
server/api/nodes/node.go
Normal 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
98
server/api/nodes/nodes.go
Normal 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
28
ui/LICENSE
Normal 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
2
ui/MANIFEST.in
Normal file
|
@ -0,0 +1,2 @@
|
|||
recursive-include opensnitch/res *
|
||||
include LICENSE
|
|
@ -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
|
||||
|
|
|
@ -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
116
ui/debian/changelog
Normal 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
1
ui/debian/compat
Normal file
|
@ -0,0 +1 @@
|
|||
9
|
9
ui/debian/config
Executable file
9
ui/debian/config
Executable 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
26
ui/debian/control
Normal 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
28
ui/debian/copyright
Normal 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
44
ui/debian/postinst
Executable 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
34
ui/debian/postrm
Executable 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
19
ui/debian/prerm
Executable 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
31
ui/debian/rules
Executable 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
1
ui/debian/source/format
Normal file
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
1
ui/debian/source/options
Normal file
1
ui/debian/source/options
Normal file
|
@ -0,0 +1 @@
|
|||
extend-diff-ignore="\.egg-info$"
|
7
ui/debian/templates
Normal file
7
ui/debian/templates
Normal 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
106
ui/opensnitch-ui.spec
Normal 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)
|
|
@ -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)
|
||||
|
|
68
ui/opensnitch/customwidgets.py
Normal file
68
ui/opensnitch/customwidgets.py
Normal 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
260
ui/opensnitch/database.py
Normal 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
|
|
@ -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()
|
||||
|
|
268
ui/opensnitch/dialogs/preferences.py
Normal file
268
ui/opensnitch/dialogs/preferences.py
Normal 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
|
321
ui/opensnitch/dialogs/processdetails.py
Normal file
321
ui/opensnitch/dialogs/processdetails.py
Normal 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)
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
504
ui/opensnitch/dialogs/ruleseditor.py
Normal file
504
ui/opensnitch/dialogs/ruleseditor.py
Normal 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
212
ui/opensnitch/nodes.py
Normal 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)
|
BIN
ui/opensnitch/res/icon-alert.png
Normal file
BIN
ui/opensnitch/res/icon-alert.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
63
ui/opensnitch/res/icon-white.svg
Normal file
63
ui/opensnitch/res/icon-white.svg
Normal 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 |
737
ui/opensnitch/res/preferences.ui
Normal file
737
ui/opensnitch/res/preferences.ui
Normal 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><html><head/><body><p>This timeout is the countdown you see when a pop-up dialog is shown.</p></body></html></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><html><head/><body><p>The default duration will take place when there's no UI connected.</p></body></html></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><html><head/><body><p>Address of the node.</p><p>Default: unix:///tmp/osui.sock (unix:// is mandatory if it's a Unix socket)</p><p>It can also be an IP address with the port: 127.0.0.1:50051</p></body></html></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><html><head/><body><p>The default action will take place when there's no UI connected.</p></body></html></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><html><head/><body><p>Log file to write logs.<br/></p><p>/dev/stdout will print logs to the standard output.</p></body></html></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><html><head/><body><p>If checked, opensnitch will prompt you to allow or deny connections that don't have an asocciated PID, due to several reasons.</p><p>The pop-up dialog will only contain information about the network connection.</p></body></html></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>
|
269
ui/opensnitch/res/process_details.ui
Normal file
269
ui/opensnitch/res/process_details.ui
Normal 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
Loading…
Add table
Reference in a new issue