├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── CMakeLists.txt ├── Dockerfile-ubuntu-2010 ├── LICENSE ├── PKGBUILD ├── README.md ├── bpf_wrapper.cpp ├── bpf_wrapper.hpp ├── connection_manager.cpp ├── connection_manager.hpp ├── control_api.cpp ├── control_api.hpp ├── dns_cache.cpp ├── dns_cache.hpp ├── dns_parser.cpp ├── dns_parser.hpp ├── ebpf_event.hpp ├── ebpfsnitch_daemon.cpp ├── ebpfsnitch_daemon.hpp ├── ebpfsnitchd.service ├── lru_map.hpp ├── main.cpp ├── misc.cpp ├── misc.hpp ├── nfq_event.h ├── nfq_wrapper.cpp ├── nfq_wrapper.hpp ├── probes.c ├── process_manager.cpp ├── process_manager.hpp ├── rule_engine.cpp ├── rule_engine.hpp ├── screenshot.png ├── stopper.cpp ├── stopper.hpp ├── tests ├── lru_map_test.cpp └── stopper_test.cpp └── ui ├── MANIFEST.in ├── bin └── ebpfsnitch ├── ebpfsnitch ├── __init__.py ├── ebpfsnitch.png └── entry.py └── setup.py /.dockerignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | build -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [0.3.0] - 2021-04-11 4 | - Limit DNS caching with LRU 5 | - Refactor for fasting shutdown 6 | - Better exception handling 7 | - Reap dead connections 8 | - Enable and fix extra warnings 9 | - Do not display container in UI if not a container 10 | - Do not display domain in UI if there is no known domain 11 | 12 | ## [0.2.0] - 2021-04-04 13 | - Added IPv6 support 14 | - Refactor DNS caching 15 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0.2) 2 | 3 | # GCC bug 4 | set(CMAKE_C_COMPILER "clang") 5 | set(CMAKE_CXX_COMPILER "clang++") 6 | 7 | project(ebpfsnitch) 8 | 9 | set(CMAKE_CXX_STANDARD 20) 10 | 11 | # eBPF target ------------------------------------------------------------------ 12 | 13 | add_custom_command( 14 | OUTPUT 15 | vmlinux.h 16 | COMMAND 17 | bpftool btf dump file /sys/kernel/btf/vmlinux format c > 18 | ${CMAKE_CURRENT_BINARY_DIR}/vmlinux.h 19 | ) 20 | 21 | add_library( 22 | probes 23 | OBJECT 24 | probes.c 25 | ${CMAKE_CURRENT_BINARY_DIR}/vmlinux.h 26 | ) 27 | 28 | target_include_directories( 29 | probes 30 | PRIVATE 31 | ${CMAKE_CURRENT_BINARY_DIR} 32 | ) 33 | 34 | target_compile_options( 35 | probes 36 | PRIVATE 37 | -O2 -target bpf 38 | ) 39 | 40 | add_custom_command( 41 | OUTPUT 42 | probes_compiled.h 43 | DEPENDS 44 | probes 45 | COMMAND 46 | cd ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/probes.dir && 47 | xxd -i probes.c.o > ${CMAKE_CURRENT_BINARY_DIR}/probes_compiled.h 48 | ) 49 | 50 | # daemon target ---------------------------------------------------------------- 51 | 52 | find_package( Boost REQUIRED COMPONENTS container system program_options ) 53 | 54 | add_library( 55 | libebpfsnitchd 56 | STATIC 57 | ebpfsnitch_daemon.cpp 58 | rule_engine.cpp 59 | misc.cpp 60 | bpf_wrapper.cpp 61 | nfq_wrapper.cpp 62 | dns_parser.cpp 63 | process_manager.cpp 64 | dns_cache.cpp 65 | stopper.cpp 66 | connection_manager.cpp 67 | control_api.cpp 68 | ${CMAKE_CURRENT_BINARY_DIR}/probes_compiled.h 69 | ) 70 | 71 | target_compile_options( 72 | libebpfsnitchd 73 | PUBLIC 74 | -Wall -g3 75 | ) 76 | 77 | target_compile_definitions(libebpfsnitchd PUBLIC SPDLOG_FMT_EXTERNAL) 78 | 79 | target_include_directories( 80 | libebpfsnitchd 81 | PUBLIC 82 | ${Boost_INCLUDE_DIRS} 83 | ${CMAKE_CURRENT_BINARY_DIR} 84 | ) 85 | 86 | target_link_libraries( 87 | libebpfsnitchd 88 | PUBLIC 89 | bpf 90 | netfilter_queue 91 | pthread 92 | spdlog 93 | fmt 94 | nfnetlink 95 | mnl 96 | ${Boost_LIBRARIES} 97 | ) 98 | 99 | add_executable( 100 | ebpfsnitchd 101 | main.cpp 102 | ) 103 | 104 | target_link_libraries( 105 | ebpfsnitchd 106 | libebpfsnitchd 107 | ) 108 | 109 | install( 110 | TARGETS 111 | ebpfsnitchd 112 | DESTINATION 113 | ${CMAKE_INSTALL_PREFIX} 114 | ) 115 | 116 | # test targets ----------------------------------------------------------------- 117 | 118 | enable_testing() 119 | 120 | add_executable(lru_map_test tests/lru_map_test.cpp) 121 | target_link_libraries(lru_map_test PRIVATE libebpfsnitchd) 122 | target_include_directories(lru_map_test PRIVATE ${CMAKE_SOURCE_DIR}) 123 | add_test(lru_map_test lru_map_test) 124 | 125 | add_executable(stopper_test tests/stopper_test.cpp) 126 | target_link_libraries(stopper_test PRIVATE libebpfsnitchd) 127 | target_include_directories(stopper_test PRIVATE ${CMAKE_SOURCE_DIR}) 128 | add_test(stopper_test stopper_test) -------------------------------------------------------------------------------- /Dockerfile-ubuntu-2010: -------------------------------------------------------------------------------- 1 | # Used to ensure the build works in a clean Ubuntu 20.10 environment 2 | # This Dockerfile will fail if not run on a Linux 5.8 kernel 3 | 4 | FROM ubuntu:20.10 5 | 6 | ENV DEBIAN_FRONTEND noninteractive 7 | 8 | RUN apt-get update -y && apt-get install -y \ 9 | cmake \ 10 | clang \ 11 | libboost-all-dev \ 12 | libspdlog-dev \ 13 | libmnl-dev \ 14 | linux-tools-common \ 15 | nlohmann-json3-dev \ 16 | libbpf-dev \ 17 | linux-tools-generic \ 18 | conntrack \ 19 | python3 \ 20 | python3-pyqt5 \ 21 | libnfnetlink-dev \ 22 | xxd \ 23 | linux-tools-5.8.0-44-generic 24 | 25 | ADD http://mirrors.kernel.org/ubuntu/pool/universe/libn/libnetfilter-queue/libnetfilter-queue-dev_1.0.5-2_amd64.deb /tmp/ 26 | ADD http://mirrors.kernel.org/ubuntu/pool/universe/libn/libnetfilter-queue/libnetfilter-queue1_1.0.5-2_amd64.deb /tmp/ 27 | 28 | RUN dpkg --install /tmp/libnetfilter-queue1_1.0.5-2_amd64.deb && \ 29 | dpkg --install /tmp/libnetfilter-queue-dev_1.0.5-2_amd64.deb 30 | 31 | WORKDIR /app 32 | 33 | COPY . . 34 | 35 | RUN mkdir build && cd build && cmake .. && make 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Harpo Roeder 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Harpo Roeder 2 | 3 | pkgname='ebpfsnitch' 4 | pkgver=0.3.0 5 | pkgrel=3 6 | pkgdesc='eBPF based Application Firewall' 7 | arch=('x86_64') 8 | license=('BSD3') 9 | 10 | provides=('ebpfsnitch' 'ebpfsnitchd') 11 | 12 | depends=( 13 | 'cmake' 14 | 'clang' 15 | 'bpf' 16 | 'libbpf' 17 | 'libnetfilter_queue' 18 | 'spdlog' 19 | 'boost' 20 | 'libmnl' 21 | 'nlohmann-json' 22 | 'python3' 23 | 'python-pyqt5' 24 | 'conntrack-tools' 25 | 'vim' 26 | ) 27 | 28 | source=("https://github.com/harporoeder/ebpfsnitch/archive/refs/tags/$pkgver.tar.gz") 29 | sha256sums=('92d0c1da308ca0f5590f5a8c13dd025687f1279592f2ba27995731d33400c936') 30 | 31 | build() { 32 | cd "$srcdir/ebpfsnitch-$pkgver" 33 | mkdir build && cd build 34 | cmake -D CMAKE_INSTALL_PREFIX="/usr/bin" .. 35 | make 36 | } 37 | 38 | package() { 39 | cd "$srcdir/ebpfsnitch-$pkgver/build" 40 | make DESTDIR="$pkgdir/" install 41 | cd "$srcdir/ebpfsnitch-$pkgver/ui" 42 | python setup.py install --root="$pkgdir/" 43 | cd "$srcdir/ebpfsnitch-$pkgver" 44 | install -Dm644 ebpfsnitchd.service -t "$pkgdir/usr/lib/systemd/system" 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eBPFSnitch 2 | 3 | eBPFSnitch is a Linux Application Level Firewall based on eBPF and NFQUEUE. 4 | It is inspired by [OpenSnitch](https://github.com/evilsocket/opensnitch) and 5 | [Douane](https://douaneapp.com/) but utilizing modern kernel abstractions - 6 | without a kernel module. 7 | 8 | The eBPFSnitch daemon is implemented in C++ 20. The control interface 9 | is implemented in Python 3 utilizing Qt5. 10 | 11 | ![screenshot](screenshot.png) 12 | 13 | ## Disclaimer 14 | 15 | This is an experimental project. The security of this application has 16 | not been audited by a 3rd party, or even myself. There 17 | are likely mechanisms by which it could be bypassed. Currently the daemon 18 | control socket is unauthenticated and an attacker could impersonate the 19 | user interface to self authorize. 20 | 21 | ## Features 22 | 23 | eBPFSnitch supports filtering all outgoing IPv4 / IPv6 based protocols 24 | (TCP / UDP / ICMP / etc). Filtering incoming connections should 25 | be supported in the near future. 26 | 27 | A core goal of this project is to integrate well with containerized 28 | applications. If an application is running in a container that container 29 | can be controlled independently of the base system or other containers. 30 | 31 | Additionally targeting can occur against specific system users. Blanket 32 | permissions for every instance of Firefox for every user are not required. 33 | 34 | ## Daemon Configuration 35 | 36 | eBPFSnitch is configured via command line arguments. The available arguments 37 | can be listed with `--help`: 38 | 39 | ```bash 40 | eBPFSnitch Allowed options: 41 | -h [ --help ] produce help message 42 | -v [ --version ] print version 43 | --remove-rules remove iptables rules 44 | --group arg group name for control socket 45 | --rules-path arg file to load / store firewall rules 46 | ``` 47 | 48 | ### Control socket authorization 49 | 50 | The control interface and daemon communicate utilizing a Unix socket. By default 51 | the socket can be accessed by any system user. It is recommended to associate 52 | a specific group with the socket to limit access. For example `--group='wheel'`. 53 | 54 | ### Firewall rule persistence 55 | 56 | Firewall rules that are marked as persistent are stored on the filesystem in a 57 | JSON encoding. By default, the current working directory is used to store the 58 | file `rules.json`. To specify a custom path use the `--rules-path` option. 59 | 60 | ## System requirements 61 | 62 | eBPFSnitch currently requires a recent kernel. The minimum supported version 63 | is Linux 5.8. This required version may be lowered in the future. 64 | 65 | ## How firewall rules operate 66 | 67 | Each rule is comprised of a set of clauses and a verdict. Each clause matches 68 | a property of a packet to value. If every clause in a rule matches, then the 69 | packet matches the rule and the verdict for that rule is used (allow / deny). 70 | 71 | Rules are sorted by a configured priority. Each rule is tried until a match is 72 | found and a verdict can be determined. If no rule matches a packet, the daemon 73 | will send a query to the interface which then displays a dialog asking to create 74 | a new rule to match that packet. 75 | 76 | By default rules are not persisted to disk. When the daemon restarts rules 77 | will be lost. If through the dialog you check the `persistent` box, the new rule 78 | will be saved to disk and be active when the daemon is restarted. 79 | 80 | ## Installation with a package manager 81 | 82 | eBPFSnitch is currently only available on the Arch user repository. Other 83 | distributions will require building from source manually. 84 | 85 | ```bash 86 | # installation using the yay aur helper 87 | yay -S ebpfsnitch 88 | # start daemon 89 | sudo systemctl start ebpfsnitchd 90 | # start the ui 91 | ebpfsnitch 92 | ``` 93 | 94 | ## Compilation instructions 95 | 96 | If a package is not available for your distribution you can build eBPFSnitch 97 | from scratch as follows: 98 | 99 | ### Dependencies 100 | 101 | C++: 102 | [pthread](https://man7.org/linux/man-pages/man7/pthreads.7.html), 103 | [libbpf](https://github.com/libbpf/libbpf), 104 | [netfilter_queue](http://www.netfilter.org/projects/libnetfilter_queue/), 105 | [spdlog](https://github.com/gabime/spdlog), 106 | [fmt](https://github.com/fmtlib/fmt), 107 | [nfnetlink](https://www.netfilter.org/projects/libnfnetlink/index.html), 108 | [boost](https://www.boost.org/), 109 | [libmnl](https://www.netfilter.org/projects/libmnl/index.html) 110 | 111 | Python: [PyQT5](https://pypi.org/project/PyQt5/) 112 | 113 | ### Installing dependencies on Arch 114 | 115 | ```bash 116 | sudo pacman -S clang cmake bpf libbpf libnetfilter_queue spdlog boost libmnl \ 117 | nlohmann-json conntrack-tools python3 python-pyqt5 vim 118 | ``` 119 | 120 | ### Installing dependencies on Ubuntu 21.04 (minimum version) 121 | 122 | ```bash 123 | sudo apt-get install cmake clang libboost-all-dev libspdlog-dev \ 124 | libnfnetlink-dev libmnl-dev linux-tools-common nlohmann-json3-dev \ 125 | libbpf-dev linux-tools-generic conntrack python3 python3-pyqt5 \ 126 | xxd libnetfilter-queue-dev 127 | ``` 128 | 129 | ### Installing dependencies on OpenSuse TumbleWeed 130 | 131 | The program can be compiled on OpenSuse TumbleWeed, it took me an hour of fiddling. 132 | I'm not sure about the exact packages, but this works: 133 | 134 | ```bash 135 | sudo zypper install make gcc *boost* cmake clang bpftool *bpf* nlohmann* spd* *netfilter* 136 | ``` 137 | 138 | Some packages are not in the default repo but required 139 | [libmnl](https://software.opensuse.org/download/package?package=libmnl&project=openSUSE%3AFactory) 140 | [libnfnetlink](https://software.opensuse.org//download.html?project=openSUSE%3AFactory&package=libnfnetlink) 141 | [libnetfilter_queue](https://software.opensuse.org/download.html?project=security%3Anetfilter&package=libnetfilter_queue) 142 | 143 | ### Setting up the daemon 144 | 145 | From the eBPFSnitch repository directory: 146 | 147 | ```bash 148 | mkdir build 149 | cd build 150 | cmake .. 151 | make 152 | sudo ./ebpfsnitchd 153 | ``` 154 | 155 | ### Starting the GUI 156 | 157 | From the eBPFSnitch repository directory: 158 | 159 | ```bash 160 | python3 ui/ebpfsnitch/entry.py 161 | ``` 162 | -------------------------------------------------------------------------------- /bpf_wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include "bpf_wrapper.hpp" 7 | 8 | class bpf_wrapper_ring::impl { 9 | public: 10 | impl( 11 | const int p_fd, 12 | const std::function p_cb 13 | ); 14 | 15 | ~impl(); 16 | 17 | void poll(const int p_timeout_ms); 18 | 19 | void consume(); 20 | 21 | int get_fd(); 22 | 23 | private: 24 | struct ring_buffer *m_ring; 25 | 26 | static int 27 | cb_proxy( 28 | void *const p_cb_cookie, 29 | void *const p_data, 30 | const size_t p_data_size 31 | ); 32 | 33 | const std::function m_cb; 34 | }; 35 | 36 | bpf_wrapper_ring::impl::impl( 37 | const int p_fd, 38 | const std::function p_cb 39 | ): 40 | m_cb(p_cb) 41 | { 42 | m_ring = ring_buffer__new( 43 | p_fd, 44 | &bpf_wrapper_ring::impl::cb_proxy, 45 | (void *)this, 46 | NULL 47 | ); 48 | 49 | if (m_ring == NULL) { 50 | throw std::runtime_error("ring_buffer__new() failed"); 51 | } 52 | } 53 | 54 | bpf_wrapper_ring::impl::~impl() 55 | { 56 | ring_buffer__free(m_ring); 57 | } 58 | 59 | void 60 | bpf_wrapper_ring::impl::poll(const int p_timeout_ms) 61 | { 62 | if (ring_buffer__poll(m_ring, p_timeout_ms) < 0) { 63 | throw std::runtime_error("ring_buffer__poll() failed"); 64 | } 65 | } 66 | 67 | void 68 | bpf_wrapper_ring::impl::consume() 69 | { 70 | if (ring_buffer__consume(m_ring) < 0) { 71 | throw std::runtime_error("ring_buffer__consume() failed"); 72 | } 73 | } 74 | 75 | int 76 | bpf_wrapper_ring::impl::get_fd() 77 | { 78 | return ring_buffer__epoll_fd(m_ring); 79 | } 80 | 81 | int 82 | bpf_wrapper_ring::impl::cb_proxy( 83 | void *const p_cb_cookie, 84 | void *const p_data, 85 | const size_t p_data_size 86 | ){ 87 | impl *const l_self = static_cast(p_cb_cookie); 88 | 89 | assert(l_self != NULL); 90 | 91 | l_self->m_cb(p_data, p_data_size); 92 | 93 | return 0; 94 | } 95 | 96 | bpf_wrapper_ring::bpf_wrapper_ring( 97 | const int p_fd, 98 | const std::function p_cb 99 | ): 100 | m_impl(std::make_unique(p_fd, p_cb)) 101 | {} 102 | 103 | bpf_wrapper_ring::~bpf_wrapper_ring(){} 104 | 105 | void 106 | bpf_wrapper_ring::poll(const int p_timeout_ms) 107 | { 108 | m_impl->poll(p_timeout_ms); 109 | } 110 | 111 | void 112 | bpf_wrapper_ring::consume() 113 | { 114 | m_impl->consume(); 115 | } 116 | 117 | int 118 | bpf_wrapper_ring::get_fd() 119 | { 120 | return m_impl->get_fd(); 121 | } 122 | 123 | class bpf_wrapper_object::impl { 124 | public: 125 | impl( 126 | std::shared_ptr p_log, 127 | const std::string &p_object_path 128 | ); 129 | 130 | ~impl(); 131 | 132 | void 133 | attach_kprobe( 134 | const std::string &p_in_bfp_name, 135 | const std::string &p_in_kernel_name, 136 | const bool p_is_ret_probe 137 | ); 138 | 139 | int 140 | lookup_map_fd_by_name(const std::string &p_name); 141 | 142 | private: 143 | std::shared_ptr m_log; 144 | 145 | const std::unique_ptr 146 | m_object; 147 | 148 | std::vector m_links; 149 | }; 150 | 151 | bpf_wrapper_object::impl::impl ( 152 | std::shared_ptr p_log, 153 | const std::string &p_object 154 | ): 155 | m_log(p_log), 156 | m_object( 157 | bpf_object__open_mem(p_object.c_str(), p_object.size(), NULL), 158 | bpf_object__close 159 | ) 160 | { 161 | if (m_object == NULL) { 162 | throw std::runtime_error("bpf_object__open_mem failed " + p_object); 163 | } 164 | 165 | if (bpf_object__load(m_object.get()) != 0) { 166 | throw std::runtime_error("m_object__load() failed"); 167 | } 168 | } 169 | 170 | bpf_wrapper_object::impl::~impl() 171 | { 172 | for (const auto &l_link : m_links) { 173 | bpf_link__disconnect(l_link); 174 | 175 | if (bpf_link__destroy(l_link) != 0) { 176 | m_log->error("bpf_link__destroy() failed"); 177 | } 178 | } 179 | 180 | if (bpf_object__unload(m_object.get()) != 0) { 181 | m_log->error("bpf_object__unload() failed"); 182 | } 183 | } 184 | 185 | void 186 | bpf_wrapper_object::impl::attach_kprobe( 187 | const std::string &p_in_bfp_name, 188 | const std::string &p_in_kernel_name, 189 | const bool p_is_ret_probe 190 | ){ 191 | struct bpf_program *const l_hook = bpf_object__find_program_by_name( 192 | m_object.get(), 193 | p_in_bfp_name.c_str() 194 | ); 195 | 196 | if (l_hook == NULL) { 197 | throw std::runtime_error("bpf_object__find_program_by_name() failed"); 198 | } 199 | 200 | struct bpf_link *const l_link = bpf_program__attach_kprobe( 201 | l_hook, 202 | p_is_ret_probe, 203 | p_in_kernel_name.c_str() 204 | ); 205 | 206 | if (l_link == NULL) { 207 | throw std::runtime_error("bpf_program__attach_kprobe"); 208 | } 209 | 210 | m_links.push_back(l_link); 211 | } 212 | 213 | int 214 | bpf_wrapper_object::impl::lookup_map_fd_by_name(const std::string &p_name) 215 | { 216 | const int l_fd = bpf_object__find_map_fd_by_name( 217 | m_object.get(), 218 | p_name.c_str() 219 | ); 220 | 221 | if (l_fd < 0) { 222 | throw std::runtime_error("bpf_object__find_map_fd_by_name() failed"); 223 | } 224 | 225 | return l_fd; 226 | } 227 | 228 | bpf_wrapper_object::bpf_wrapper_object( 229 | std::shared_ptr p_log, 230 | const std::string &p_object 231 | ): 232 | m_impl(std::make_unique(p_log, p_object)) 233 | {} 234 | 235 | bpf_wrapper_object::~bpf_wrapper_object(){} 236 | 237 | void 238 | bpf_wrapper_object::attach_kprobe( 239 | const std::string &p_in_bfp_name, 240 | const std::string &p_in_kernel_name, 241 | const bool p_is_ret_probe 242 | ){ 243 | m_impl->attach_kprobe(p_in_bfp_name, p_in_kernel_name, p_is_ret_probe); 244 | } 245 | 246 | int 247 | bpf_wrapper_object::lookup_map_fd_by_name(const std::string &p_name) 248 | { 249 | return m_impl->lookup_map_fd_by_name(p_name); 250 | } -------------------------------------------------------------------------------- /bpf_wrapper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | class bpf_wrapper_ring { 9 | public: 10 | bpf_wrapper_ring( 11 | const int p_fd, 12 | const std::function p_cb 13 | ); 14 | 15 | ~bpf_wrapper_ring(); 16 | 17 | void poll(const int p_timeout_ms); 18 | 19 | void consume(); 20 | 21 | int get_fd(); 22 | 23 | private: 24 | class impl; 25 | 26 | const std::unique_ptr m_impl; 27 | }; 28 | 29 | class bpf_wrapper_object { 30 | public: 31 | bpf_wrapper_object( 32 | std::shared_ptr p_log, 33 | const std::string &p_object 34 | ); 35 | 36 | ~bpf_wrapper_object(); 37 | 38 | void 39 | attach_kprobe( 40 | const std::string &p_in_bfp_name, 41 | const std::string &p_in_kernel_name, 42 | const bool p_is_ret_probe 43 | ); 44 | 45 | int 46 | lookup_map_fd_by_name(const std::string &p_name); 47 | 48 | private: 49 | class impl; 50 | 51 | const std::unique_ptr m_impl; 52 | }; 53 | -------------------------------------------------------------------------------- /connection_manager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "connection_manager.hpp" 4 | 5 | connection_manager::connection_manager(): 6 | m_thread(&connection_manager::reaper_thread, this) 7 | {} 8 | 9 | connection_manager::~connection_manager() 10 | { 11 | m_stopper.stop(); 12 | 13 | m_thread.join(); 14 | } 15 | 16 | std::shared_ptr 17 | connection_manager::lookup_connection_info(const nfq_event_t &p_event) 18 | { 19 | const __uint128_t l_source_address = p_event.m_v6 ? 20 | p_event.m_source_address_v6 : p_event.m_source_address; 21 | 22 | const __uint128_t l_destination_address = p_event.m_v6 ? 23 | p_event.m_destination_address_v6 : p_event.m_destination_address; 24 | 25 | connection_tuple_t l_key = { 26 | .m_protocol = static_cast(p_event.m_protocol), 27 | .m_v6 = p_event.m_v6, 28 | .m_source_address = l_source_address, 29 | .m_destination_address = l_destination_address, 30 | .m_source_port = p_event.m_source_port, 31 | .m_destination_port = p_event.m_destination_port 32 | }; 33 | 34 | std::lock_guard l_guard(m_lock); 35 | 36 | const auto l_iter = m_mapping.find(l_key); 37 | 38 | if (l_iter != m_mapping.end()) { 39 | l_iter->second.m_last_active = std::chrono::steady_clock::now(); 40 | 41 | return l_iter->second.m_process; 42 | } else { 43 | l_key.m_source_address = 0; 44 | 45 | const auto l_iter2 = m_mapping.find(l_key); 46 | 47 | if (l_iter2 != m_mapping.end()) { 48 | l_iter2->second.m_last_active = std::chrono::steady_clock::now(); 49 | 50 | return l_iter2->second.m_process; 51 | } else { 52 | return nullptr; 53 | } 54 | } 55 | } 56 | 57 | void 58 | connection_manager::add_connection_info( 59 | const ebpf_event_t & p_event, 60 | std::shared_ptr p_process 61 | ) { 62 | const __uint128_t l_source_address = p_event.m_v6 ? 63 | p_event.m_source_address_v6 : p_event.m_source_address; 64 | 65 | const __uint128_t l_destination_address = p_event.m_v6 ? 66 | p_event.m_destination_address_v6 : p_event.m_destination_address; 67 | 68 | const connection_tuple_t l_key = { 69 | .m_protocol = static_cast(p_event.m_protocol), 70 | .m_v6 = p_event.m_v6, 71 | .m_source_address = l_source_address, 72 | .m_destination_address = l_destination_address, 73 | .m_source_port = p_event.m_source_port, 74 | .m_destination_port = p_event.m_destination_port 75 | }; 76 | 77 | const item_t l_item = { 78 | .m_last_active = std::chrono::steady_clock::now(), 79 | .m_process = p_process 80 | }; 81 | 82 | std::lock_guard l_guard(m_lock); 83 | 84 | m_mapping[l_key] = l_item; 85 | } 86 | 87 | void 88 | connection_manager::reap() 89 | { 90 | const auto l_now = std::chrono::steady_clock::now(); 91 | 92 | std::lock_guard l_guard(m_lock); 93 | 94 | std::erase_if(m_mapping, [&](const auto &l_iter) { 95 | return (l_now - l_iter.second.m_last_active) > 96 | std::chrono::seconds{60 * 5}; 97 | }); 98 | } 99 | 100 | void 101 | connection_manager::reaper_thread() 102 | { 103 | while (!m_stopper.await_stop_for_milliseconds(1000)) { 104 | reap(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /connection_manager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include "process_manager.hpp" 14 | #include "nfq_event.h" 15 | #include "ebpf_event.hpp" 16 | #include "stopper.hpp" 17 | 18 | struct connection_tuple_t { 19 | ip_protocol_t m_protocol; 20 | bool m_v6; 21 | __uint128_t m_source_address; 22 | __uint128_t m_destination_address; 23 | uint16_t m_source_port; 24 | uint16_t m_destination_port; 25 | 26 | bool 27 | operator == (const connection_tuple_t &p_other) const { 28 | return ( m_protocol == p_other.m_protocol ) 29 | && ( m_v6 == p_other.m_v6 ) 30 | && ( m_source_address == p_other.m_source_address ) 31 | && ( m_destination_address == p_other.m_destination_address ) 32 | && ( m_source_port == p_other.m_source_port ) 33 | && ( m_destination_port == p_other.m_destination_port ); 34 | }; 35 | 36 | struct hasher { 37 | std::size_t 38 | operator() (const connection_tuple_t &p_tuple) const 39 | { 40 | std::size_t l_seed = 0; 41 | 42 | boost::hash_combine( 43 | l_seed, 44 | boost::hash_value(p_tuple.m_protocol) 45 | ); 46 | 47 | boost::hash_combine( 48 | l_seed, 49 | boost::hash_value(p_tuple.m_v6) 50 | ); 51 | 52 | boost::hash_combine( 53 | l_seed, 54 | boost::hash_value(p_tuple.m_source_address) 55 | ); 56 | 57 | boost::hash_combine( 58 | l_seed, 59 | boost::hash_value(p_tuple.m_destination_address) 60 | ); 61 | 62 | boost::hash_combine( 63 | l_seed, 64 | boost::hash_value(p_tuple.m_source_port) 65 | ); 66 | 67 | boost::hash_combine( 68 | l_seed, 69 | boost::hash_value(p_tuple.m_destination_port) 70 | ); 71 | 72 | return l_seed; 73 | } 74 | }; 75 | }; 76 | 77 | class connection_manager { 78 | public: 79 | connection_manager(); 80 | 81 | ~connection_manager(); 82 | 83 | std::shared_ptr 84 | lookup_connection_info(const nfq_event_t &p_event); 85 | 86 | void 87 | add_connection_info( 88 | const ebpf_event_t & p_event, 89 | std::shared_ptr p_process 90 | ); 91 | 92 | private: 93 | void reap(); 94 | void reaper_thread(); 95 | 96 | struct item_t { 97 | std::chrono::time_point m_last_active; 98 | std::shared_ptr m_process; 99 | }; 100 | 101 | std::thread m_thread; 102 | stopper m_stopper; 103 | std::mutex m_lock; 104 | 105 | std::unordered_map< 106 | connection_tuple_t, 107 | item_t, 108 | connection_tuple_t::hasher 109 | > m_mapping; 110 | }; 111 | -------------------------------------------------------------------------------- /control_api.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "control_api.hpp" 5 | 6 | control_api::control_api( 7 | std::shared_ptr p_log, 8 | std::optional p_group, 9 | on_connect_fn_t p_on_connect 10 | ): 11 | m_log(p_log), 12 | m_on_connect(p_on_connect) 13 | { 14 | const char *const l_path = "/tmp/ebpfsnitch.sock"; 15 | 16 | unlink(l_path); 17 | 18 | m_acceptor = 19 | std::make_unique( 20 | m_service, 21 | boost::asio::local::stream_protocol::endpoint(l_path) 22 | ); 23 | 24 | if (p_group) { 25 | m_log->info("setting socket group {}", p_group.value()); 26 | 27 | const struct group *const l_group = getgrnam( 28 | p_group.value().c_str() 29 | ); 30 | 31 | if (l_group == NULL) { 32 | throw std::runtime_error("getgrnam()"); 33 | } 34 | 35 | if (chown("/tmp/ebpfsnitch.sock", 0, l_group->gr_gid) == -1) { 36 | throw std::runtime_error("chown()"); 37 | } 38 | 39 | if (chmod("/tmp/ebpfsnitch.sock", 660) != 0){ 40 | throw std::runtime_error("chmod()"); 41 | } 42 | } else { 43 | m_log->info("setting control socket world writable"); 44 | 45 | if (chmod("/tmp/ebpfsnitch.sock", 666) != 0){ 46 | throw std::runtime_error("chmod()"); 47 | } 48 | } 49 | 50 | m_thread = std::thread(&control_api::thread, this); 51 | } 52 | 53 | control_api::~control_api() 54 | { 55 | m_service.stop(); 56 | 57 | m_thread.join(); 58 | 59 | m_log->info("control_api destructed"); 60 | } 61 | 62 | control_api::session::session( 63 | key p_key, 64 | boost::asio::io_service &p_service, 65 | std::shared_ptr p_log 66 | ): 67 | m_socket(p_service), 68 | m_service(p_service), 69 | m_log(p_log) 70 | {} 71 | 72 | control_api::session::~session() 73 | { 74 | m_log->info("session destructed"); 75 | } 76 | 77 | void 78 | control_api::session::start(key p_key) 79 | { 80 | handle_reads(); 81 | handle_writes(); 82 | } 83 | 84 | void 85 | control_api::session::handle_reads() 86 | { 87 | std::weak_ptr l_self_weak(shared_from_this()); 88 | 89 | boost::asio::async_read_until( 90 | m_socket, 91 | m_buffer, 92 | "\n", 93 | [this, l_self_weak]( 94 | const boost::system::error_code p_error, 95 | const std::size_t p_length 96 | ) { 97 | std::shared_ptr l_self = l_self_weak.lock(); 98 | 99 | if (!l_self) { 100 | return; 101 | } 102 | 103 | if (p_error) { 104 | m_log->info("async_read() error {}", p_error.message()); 105 | 106 | handle_disconnect();; 107 | 108 | return; 109 | } 110 | 111 | try { 112 | std::istream l_istream(&m_buffer); 113 | std::string l_line; 114 | std::getline(l_istream, l_line); 115 | 116 | m_log->trace("got command {}", l_line); 117 | 118 | if (m_on_message.has_value()) { 119 | m_on_message.value()(nlohmann::json::parse(l_line)); 120 | } 121 | } catch (const std::exception &p_error) { 122 | m_log->warn("connection error {}", p_error.what()); 123 | 124 | handle_disconnect(); 125 | 126 | return; 127 | } 128 | 129 | handle_reads(); 130 | } 131 | ); 132 | } 133 | 134 | void 135 | control_api::session::handle_writes() 136 | { 137 | std::shared_ptr l_self(shared_from_this()); 138 | 139 | if (m_outgoing.size() == 0) { 140 | return; 141 | } 142 | 143 | std::shared_ptr l_message = std::make_shared( 144 | m_outgoing.front().dump() + "\n" 145 | ); 146 | 147 | m_outgoing.pop_front(); 148 | 149 | boost::asio::async_write( 150 | m_socket, 151 | boost::asio::buffer(l_message->c_str(), l_message->size()), 152 | [this, l_self, l_message]( 153 | const boost::system::error_code p_error, 154 | const std::size_t p_length 155 | ) { 156 | if (p_error) { 157 | m_log->info("async_write() error {}", p_error.message()); 158 | 159 | handle_disconnect(); 160 | 161 | return; 162 | } 163 | 164 | handle_writes(); 165 | } 166 | ); 167 | } 168 | 169 | void 170 | control_api::session::handle_disconnect() 171 | { 172 | if (m_on_disconnect.has_value()) { 173 | m_on_disconnect.value()(); 174 | } 175 | } 176 | 177 | boost::asio::local::stream_protocol::socket & 178 | control_api::session::get_socket(key p_key) 179 | { 180 | return m_socket; 181 | } 182 | 183 | void 184 | control_api::session::queue_outgoing_json(const nlohmann::json p_message) 185 | { 186 | std::shared_ptr l_self(shared_from_this()); 187 | 188 | m_service.post([this, l_self, p_message]() { 189 | m_outgoing.push_back(p_message); 190 | 191 | if (m_outgoing.size() == 1) { 192 | handle_writes(); 193 | } 194 | }); 195 | } 196 | 197 | void 198 | control_api::session::set_on_message_cb(on_message_fn_t p_cb) 199 | { 200 | m_on_message = std::optional(p_cb); 201 | } 202 | 203 | void 204 | control_api::session::set_on_disconnect_cb(on_disconnect_fn_t p_cb) 205 | { 206 | m_on_disconnect = std::optional(p_cb); 207 | } 208 | 209 | void 210 | control_api::accept() 211 | { 212 | std::shared_ptr l_session = std::make_shared( 213 | session::key(), 214 | m_service, 215 | m_log 216 | ); 217 | 218 | m_acceptor->async_accept( 219 | l_session->get_socket(session::key()), 220 | [this, l_session] ( 221 | const boost::system::error_code p_error 222 | ) mutable { 223 | if (p_error) { 224 | this->m_log->info("async_accept() error {}", p_error.message()); 225 | 226 | return; 227 | } 228 | 229 | m_on_connect(l_session); 230 | 231 | l_session->start(session::key()); 232 | 233 | accept(); 234 | } 235 | ); 236 | } 237 | 238 | void 239 | control_api::thread() 240 | { 241 | try { 242 | accept(); 243 | 244 | m_service.run(); 245 | } catch (const std::exception &p_err) { 246 | m_log->error("control_api::thread() {}", p_err.what()); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /control_api.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | class control_api : private boost::noncopyable { 18 | public: 19 | class session; 20 | 21 | typedef std::function)> on_connect_fn_t; 22 | 23 | class session : 24 | public std::enable_shared_from_this, 25 | private boost::noncopyable 26 | { 27 | public: 28 | typedef std::function on_message_fn_t; 29 | typedef std::function on_disconnect_fn_t; 30 | 31 | // limited friend access 32 | class key { 33 | private: 34 | friend class control_api; 35 | 36 | key() = default; 37 | }; 38 | 39 | session( 40 | key p_key, 41 | boost::asio::io_service &p_service, 42 | std::shared_ptr p_log 43 | ); 44 | 45 | ~session(); 46 | 47 | void start(key p_key); 48 | 49 | boost::asio::local::stream_protocol::socket &get_socket(key p_key); 50 | 51 | void queue_outgoing_json(const nlohmann::json p_message); 52 | 53 | void set_on_message_cb(on_message_fn_t p_cb); 54 | 55 | void set_on_disconnect_cb(on_disconnect_fn_t p_cb); 56 | 57 | private: 58 | boost::asio::local::stream_protocol::socket m_socket; 59 | boost::asio::io_service &m_service; 60 | const std::shared_ptr m_log; 61 | boost::asio::streambuf m_buffer; 62 | std::deque m_outgoing; 63 | 64 | std::optional m_on_message; 65 | std::optional m_on_disconnect; 66 | 67 | void handle_reads(); 68 | void handle_writes(); 69 | void handle_disconnect(); 70 | }; 71 | 72 | control_api( 73 | std::shared_ptr p_log, 74 | std::optional p_group, 75 | on_connect_fn_t p_on_connect 76 | ); 77 | 78 | ~control_api(); 79 | 80 | private: 81 | const std::shared_ptr m_log; 82 | const on_connect_fn_t m_on_connect; 83 | 84 | boost::asio::io_service m_service; 85 | std::unique_ptr m_acceptor; 86 | std::thread m_thread; 87 | 88 | void accept(); 89 | void thread(); 90 | }; -------------------------------------------------------------------------------- /dns_cache.cpp: -------------------------------------------------------------------------------- 1 | #include "dns_cache.hpp" 2 | 3 | dns_cache::dns_cache(): 4 | m_ipv4_to_domain(1000), 5 | m_ipv6_to_domain(1000) 6 | {}; 7 | 8 | dns_cache::~dns_cache(){}; 9 | 10 | std::optional 11 | dns_cache::lookup_domain_v4(const uint32_t p_address) 12 | { 13 | std::lock_guard l_guard(m_lock); 14 | 15 | return m_ipv4_to_domain.lookup(p_address); 16 | } 17 | 18 | std::optional 19 | dns_cache::lookup_domain_v6(const __uint128_t p_address) 20 | { 21 | std::lock_guard l_guard(m_lock); 22 | 23 | return m_ipv6_to_domain.lookup(p_address); 24 | } 25 | 26 | void 27 | dns_cache::add_ipv4_mapping(uint32_t p_address, std::string p_domain) 28 | { 29 | std::lock_guard l_guard(m_lock); 30 | 31 | m_ipv4_to_domain.insert(p_address, p_domain); 32 | } 33 | 34 | void 35 | dns_cache::add_ipv6_mapping(__uint128_t p_address, std::string p_domain) 36 | { 37 | std::lock_guard l_guard(m_lock); 38 | 39 | m_ipv6_to_domain.insert(p_address, p_domain); 40 | } 41 | -------------------------------------------------------------------------------- /dns_cache.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "lru_map.hpp" 8 | 9 | class dns_cache { 10 | public: 11 | dns_cache(); 12 | 13 | ~dns_cache(); 14 | 15 | std::optional 16 | lookup_domain_v4(const uint32_t p_address); 17 | 18 | std::optional 19 | lookup_domain_v6(const __uint128_t p_address); 20 | 21 | void add_ipv4_mapping(uint32_t p_address, std::string p_domain); 22 | 23 | void add_ipv6_mapping(__uint128_t p_address, std::string p_domain); 24 | 25 | private: 26 | std::mutex m_lock; 27 | 28 | lru_map m_ipv4_to_domain; 29 | lru_map<__uint128_t, std::string> m_ipv6_to_domain; 30 | }; 31 | -------------------------------------------------------------------------------- /dns_parser.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "dns_parser.hpp" 8 | 9 | bool 10 | dns_parse_header( 11 | const char *const p_packet, 12 | const size_t p_packet_size, 13 | struct dns_header_t *const p_result 14 | ) { 15 | assert(p_packet); 16 | assert(p_result); 17 | 18 | if (p_packet_size < 12) { 19 | return false; 20 | } 21 | 22 | p_result->m_question_count = read_network_u16(p_packet + 4); 23 | p_result->m_answer_count = read_network_u16(p_packet + 6); 24 | p_result->m_authority_count = read_network_u16(p_packet + 8); 25 | p_result->m_additional_count = read_network_u16(p_packet + 10); 26 | 27 | return true; 28 | } 29 | 30 | const char * 31 | dns_get_body(const char *const l_buffer) 32 | { 33 | assert(l_buffer); 34 | 35 | return l_buffer + 12; 36 | } 37 | 38 | static const char * 39 | dns_skip_qname( 40 | const char *const p_packet, 41 | const size_t p_packet_size, 42 | const char * p_iter 43 | ){ 44 | assert(p_packet); 45 | assert(p_iter); 46 | 47 | while (p_iter < (p_packet + p_packet_size)) { 48 | const uint8_t l_byte = *p_iter; 49 | 50 | if (l_byte == 0) { 51 | return p_iter + 1; 52 | } else if (l_byte > 63) { 53 | if ((p_iter + 2) <= (p_packet + p_packet_size)) { 54 | return p_iter + 2; 55 | } else { 56 | return NULL; 57 | } 58 | } 59 | 60 | p_iter += l_byte + 1; 61 | } 62 | 63 | return NULL; 64 | } 65 | 66 | const char * 67 | dns_parse_question( 68 | const char *const p_packet, 69 | const size_t p_packet_size, 70 | const char * p_iter, 71 | struct dns_question_t *const p_result 72 | ){ 73 | assert(p_packet); 74 | assert(p_iter); 75 | assert(p_result); 76 | 77 | p_result->m_name = p_iter; 78 | 79 | p_iter = dns_skip_qname(p_packet, p_packet_size, p_iter); 80 | 81 | if ((p_iter == NULL) || ((p_iter + 4) > (p_packet + p_packet_size))) { 82 | return NULL; 83 | } 84 | 85 | p_result->m_type = 86 | static_cast(read_network_u16(p_iter)); 87 | 88 | p_result->m_class = 89 | static_cast(read_network_u16(p_iter + 2)); 90 | 91 | return p_iter + 4; 92 | } 93 | 94 | const char * 95 | dns_parse_record( 96 | const char *const p_packet, 97 | const size_t p_packet_size, 98 | const char * p_iter, 99 | struct dns_resource_record_t *const p_result 100 | ){ 101 | assert(p_packet); 102 | assert(p_iter); 103 | assert(p_result); 104 | 105 | p_result->m_name = p_iter; 106 | 107 | p_iter = dns_skip_qname(p_packet, p_packet_size, p_iter); 108 | 109 | if ((p_iter == NULL) || ((p_iter + 10) > (p_packet + p_packet_size))) { 110 | return NULL; 111 | } 112 | 113 | p_result->m_type = 114 | static_cast(read_network_u16(p_iter)); 115 | 116 | p_result->m_class = 117 | static_cast(read_network_u16(p_iter + 2)); 118 | 119 | p_result->m_data_length = read_network_u16(p_iter + 8); 120 | 121 | p_result->m_data = p_iter + 10; 122 | 123 | p_iter += 10 + p_result->m_data_length; 124 | 125 | if (p_iter > (p_packet + p_packet_size)) { 126 | return NULL; 127 | } 128 | 129 | return p_iter; 130 | } 131 | 132 | std::optional 133 | dns_decode_qname( 134 | const char *const p_packet, 135 | const size_t p_packet_size, 136 | const char * p_iter, 137 | const bool p_recurse 138 | ){ 139 | assert(p_packet); 140 | assert(p_iter); 141 | 142 | std::string l_name; 143 | 144 | while (p_iter < (p_packet + p_packet_size)) { 145 | const uint8_t l_count = *p_iter; 146 | 147 | if (l_count == 0) { 148 | return std::optional(l_name); 149 | } else if (l_count > 63) { 150 | if ((p_iter + 2) >= (p_packet + p_packet_size)) { 151 | return std::nullopt; 152 | } else { 153 | if (p_recurse) { 154 | const uint16_t l_offset = read_network_u16(p_iter) 155 | & 0b00111111; 156 | 157 | return dns_decode_qname( 158 | p_packet, 159 | p_packet_size, 160 | p_packet + l_offset, 161 | false 162 | ); 163 | } else { 164 | return std::nullopt; 165 | } 166 | } 167 | } 168 | 169 | p_iter++; 170 | 171 | if ((p_iter + l_count) > (p_packet + p_packet_size)) { 172 | return std::nullopt; 173 | } 174 | 175 | l_name += std::string(p_iter, l_count) + "."; 176 | 177 | p_iter += l_count; 178 | } 179 | 180 | return std::nullopt; 181 | } 182 | 183 | uint16_t 184 | read_network_u16(const char *const l_buffer) 185 | { 186 | assert(l_buffer); 187 | 188 | return ntohs(*((uint16_t *)l_buffer)); 189 | } 190 | -------------------------------------------------------------------------------- /dns_parser.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | enum class dns_class : uint16_t { 7 | INTERNET = 1, 8 | CSNET = 2, 9 | CHAOS = 3, 10 | HESOID = 4 11 | }; 12 | 13 | // https://en.wikipedia.org/wiki/List_of_DNS_record_types 14 | enum class dns_resource_record_type : uint16_t { 15 | A = 1, 16 | NS = 2, 17 | CNAME = 5, 18 | SOA = 6, 19 | PTR = 12, 20 | HINFO = 13, 21 | MX = 15, 22 | TXT = 16, 23 | RP = 17, 24 | AFSDB = 18, 25 | SIG = 24, 26 | KEY = 25, 27 | AAAA = 28, 28 | LOC = 29, 29 | SRV = 33 30 | }; 31 | 32 | struct dns_header_t { 33 | uint16_t m_question_count; 34 | uint16_t m_answer_count; 35 | uint16_t m_authority_count; 36 | uint16_t m_additional_count; 37 | }; 38 | 39 | struct dns_question_t { 40 | const char * m_name; 41 | dns_resource_record_type m_type; 42 | dns_class m_class; 43 | }; 44 | 45 | struct dns_resource_record_t { 46 | const char * m_name; 47 | dns_resource_record_type m_type; 48 | dns_class m_class; 49 | uint32_t m_ttl; 50 | uint16_t m_data_length; 51 | const char * m_data; 52 | }; 53 | 54 | bool 55 | dns_parse_header( 56 | const char *const p_packet, 57 | const size_t p_packet_size, 58 | struct dns_header_t *const p_result 59 | ) __attribute__((nonnull (1, 3))); 60 | 61 | const char * 62 | dns_parse_question( 63 | const char *const p_packet, 64 | const size_t p_packet_size, 65 | const char * p_iter, 66 | struct dns_question_t *const p_result 67 | ) __attribute__((nonnull (1, 3, 4))); 68 | 69 | const char * 70 | dns_parse_record( 71 | const char *const p_packet, 72 | const size_t p_packet_size, 73 | const char * p_iter, 74 | struct dns_resource_record_t *const p_result 75 | ) __attribute__((nonnull (1, 3, 4))); 76 | 77 | std::optional 78 | dns_decode_qname( 79 | const char *const p_packet, 80 | const size_t p_packet_size, 81 | const char * p_iter, 82 | const bool p_recurse=true 83 | ) __attribute__((nonnull (1, 3))); 84 | 85 | const char * 86 | dns_get_body(const char *const p_buffer __attribute__((nonnull))); 87 | 88 | uint16_t 89 | read_network_u16(const char *const p_buffer __attribute__((nonnull))); 90 | -------------------------------------------------------------------------------- /ebpf_event.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct ebpf_event_t { 4 | bool m_v6; 5 | void * m_handle; 6 | bool m_remove; 7 | uint32_t m_user_id; 8 | uint32_t m_process_id; 9 | uint32_t m_source_address; 10 | __uint128_t m_source_address_v6; 11 | uint16_t m_source_port; 12 | uint32_t m_destination_address; 13 | __uint128_t m_destination_address_v6; 14 | uint16_t m_destination_port; 15 | uint64_t m_timestamp; 16 | uint8_t m_protocol; 17 | } __attribute__((packed)); 18 | -------------------------------------------------------------------------------- /ebpfsnitch_daemon.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | #include "dns_parser.hpp" 21 | #include "ebpfsnitch_daemon.hpp" 22 | #include "probes_compiled.h" 23 | 24 | iptables_raii::iptables_raii(std::shared_ptr p_log): 25 | m_log(p_log) 26 | { 27 | m_log->trace("adding iptables rules"); 28 | 29 | ::std::system( 30 | "iptables --append OUTPUT --table mangle --match conntrack " 31 | "--ctstate NEW,RELATED " 32 | "--jump NFQUEUE --queue-num 0" 33 | ); 34 | 35 | ::std::system( 36 | "ip6tables --append OUTPUT --table mangle --match conntrack " 37 | "--ctstate NEW,RELATED " 38 | "--jump NFQUEUE --queue-num 2" 39 | ); 40 | 41 | ::std::system("iptables --append INPUT --jump NFQUEUE --queue-num 1"); 42 | 43 | ::std::system("ip6tables --append INPUT --jump NFQUEUE --queue-num 3"); 44 | 45 | ::std::system( 46 | "iptables --insert DOCKER-USER " 47 | "--match conntrack --ctstate NEW,RELATED " 48 | "--jump NFQUEUE --queue-num 0" 49 | ); 50 | 51 | ::std::system("conntrack --flush"); 52 | } 53 | 54 | iptables_raii::~iptables_raii() 55 | { 56 | m_log->trace("removing iptables rules"); 57 | 58 | remove_rules(); 59 | } 60 | 61 | void 62 | iptables_raii::remove_rules() 63 | { 64 | ::std::system( 65 | "iptables --delete OUTPUT --table mangle --match conntrack " 66 | "--ctstate NEW,RELATED " 67 | "--jump NFQUEUE --queue-num 0" 68 | ); 69 | 70 | ::std::system( 71 | "ip6tables --delete OUTPUT --table mangle --match conntrack " 72 | "--ctstate NEW,RELATED " 73 | "--jump NFQUEUE --queue-num 2" 74 | ); 75 | 76 | ::std::system("iptables --delete INPUT --jump NFQUEUE --queue-num 1"); 77 | 78 | ::std::system("ip6tables --delete INPUT --jump NFQUEUE --queue-num 3"); 79 | 80 | ::std::system( 81 | "iptables --delete DOCKER-USER " 82 | "--match conntrack --ctstate NEW,RELATED " 83 | "--jump NFQUEUE --queue-num 0" 84 | ); 85 | } 86 | 87 | ebpfsnitch_daemon::ebpfsnitch_daemon( 88 | std::shared_ptr p_log, 89 | std::optional p_group, 90 | std::optional p_rules_path 91 | ): 92 | m_rule_engine(p_rules_path.value_or("rules.json")), 93 | m_log(p_log), 94 | m_group(p_group), 95 | m_process_manager(p_log), 96 | m_bpf_wrapper( 97 | p_log, 98 | std::string( 99 | reinterpret_cast(probes_c_o), sizeof(probes_c_o) 100 | ) 101 | ) 102 | { 103 | m_log->trace("ebpfsnitch_daemon constructor"); 104 | 105 | m_log->trace("setting up ebpf"); 106 | 107 | m_bpf_wrapper.attach_kprobe( 108 | "kprobe_security_socket_send_msg", 109 | "security_socket_sendmsg", 110 | false 111 | ); 112 | 113 | m_bpf_wrapper.attach_kprobe( 114 | "kretprobe_security_socket_send_msg", 115 | "security_socket_sendmsg", 116 | true 117 | ); 118 | 119 | m_bpf_wrapper.attach_kprobe( 120 | "kprobe_tcp_v4_connect", 121 | "tcp_v4_connect", 122 | false 123 | ); 124 | 125 | m_bpf_wrapper.attach_kprobe( 126 | "kretprobe_tcp_v4_connect", 127 | "tcp_v4_connect", 128 | true 129 | ); 130 | 131 | m_bpf_wrapper.attach_kprobe( 132 | "kprobe_tcp_v6_connect", 133 | "tcp_v6_connect", 134 | false 135 | ); 136 | 137 | m_bpf_wrapper.attach_kprobe( 138 | "kretprobe_tcp_v6_connect", 139 | "tcp_v6_connect", 140 | true 141 | ); 142 | 143 | m_ring_buffer = std::make_shared( 144 | m_bpf_wrapper.lookup_map_fd_by_name("g_probe_ipv4_events"), 145 | ::std::bind( 146 | &ebpfsnitch_daemon::bpf_reader, 147 | this, 148 | std::placeholders::_1, 149 | std::placeholders::_2 150 | ) 151 | ); 152 | 153 | m_nfq = std::make_shared( 154 | 0, 155 | ::std::bind( 156 | &ebpfsnitch_daemon::nfq_handler, 157 | this, 158 | std::placeholders::_1, 159 | std::placeholders::_2, 160 | std::placeholders::_3 161 | ), 162 | address_family_t::INET 163 | ); 164 | 165 | m_nfqv6 = std::make_shared( 166 | 2, 167 | ::std::bind( 168 | &ebpfsnitch_daemon::nfq_handler, 169 | this, 170 | std::placeholders::_1, 171 | std::placeholders::_2, 172 | std::placeholders::_3 173 | ), 174 | address_family_t::INET6 175 | ); 176 | 177 | m_nfq_incoming = std::make_shared( 178 | 1, 179 | ::std::bind( 180 | &ebpfsnitch_daemon::nfq_handler_incoming, 181 | this, 182 | std::placeholders::_1, 183 | std::placeholders::_2, 184 | std::placeholders::_3 185 | ), 186 | address_family_t::INET 187 | ); 188 | 189 | m_nfq_incomingv6 = std::make_shared( 190 | 3, 191 | ::std::bind( 192 | &ebpfsnitch_daemon::nfq_handler_incoming, 193 | this, 194 | std::placeholders::_1, 195 | std::placeholders::_2, 196 | std::placeholders::_3 197 | ), 198 | address_family_t::INET6 199 | ); 200 | 201 | m_iptables_raii = std::make_unique(p_log); 202 | 203 | m_control_api = std::make_shared( 204 | p_log, 205 | p_group, 206 | [this](std::shared_ptr p_session) { 207 | m_log->info("on_connect_cb"); 208 | 209 | p_session->queue_outgoing_json({ 210 | { "kind", "setRules" }, 211 | { "rules", m_rule_engine.rules_to_json(false) } 212 | }); 213 | 214 | p_session->queue_outgoing_json({ 215 | { "kind", "setProcesses" }, 216 | { "processes", m_process_manager.processes_to_json() } 217 | }); 218 | 219 | std::shared_ptr l_context = 220 | std::make_shared(); 221 | 222 | l_context->m_pending_verdict = false; 223 | l_context->m_session = p_session; 224 | 225 | std::weak_ptr l_context_weak = l_context; 226 | 227 | p_session->set_on_message_cb( 228 | std::bind( 229 | &ebpfsnitch_daemon::handle_control_message, 230 | this, 231 | l_context_weak, 232 | std::placeholders::_1 233 | ) 234 | ); 235 | 236 | p_session->set_on_disconnect_cb( 237 | std::bind( 238 | &ebpfsnitch_daemon::handle_disconnect, 239 | this, 240 | l_context_weak 241 | ) 242 | ); 243 | 244 | { 245 | std::lock_guard l_guard(m_control_connections_lock); 246 | m_control_connections.insert(l_context); 247 | } 248 | 249 | process_unhandled(); 250 | } 251 | ); 252 | 253 | m_process_manager.set_load_process_cb( 254 | [&] (const process_info_t &p_process) { 255 | nlohmann::json l_json = { 256 | { "kind", "addProcess" }, 257 | { "processId" , p_process.m_process_id }, 258 | { "executable", p_process.m_executable }, 259 | { "userId", p_process.m_user_id }, 260 | { "groupId", p_process.m_group_id } 261 | }; 262 | 263 | send_to_all_control_connections(l_json); 264 | } 265 | ); 266 | 267 | m_process_manager.set_remove_process_cb( 268 | [&] (const uint32_t p_process_id) { 269 | nlohmann::json l_json = { 270 | { "kind", "removeProcess" }, 271 | { "processId" , p_process_id } 272 | }; 273 | 274 | send_to_all_control_connections(l_json); 275 | } 276 | ); 277 | 278 | m_thread_group.push_back( 279 | ::std::thread(&ebpfsnitch_daemon::filter_thread, this, m_nfq) 280 | ); 281 | 282 | m_thread_group.push_back( 283 | ::std::thread(&ebpfsnitch_daemon::filter_thread, this, m_nfqv6) 284 | ); 285 | 286 | m_thread_group.push_back( 287 | ::std::thread(&ebpfsnitch_daemon::filter_thread, this, m_nfq_incoming) 288 | ); 289 | 290 | m_thread_group.push_back( 291 | ::std::thread(&ebpfsnitch_daemon::filter_thread, this, m_nfq_incomingv6) 292 | ); 293 | 294 | m_thread_group.push_back( 295 | ::std::thread(&ebpfsnitch_daemon::probe_thread, this) 296 | ); 297 | } 298 | 299 | ebpfsnitch_daemon::~ebpfsnitch_daemon() 300 | { 301 | m_log->trace("ebpfsnitch_daemon destructor"); 302 | 303 | shutdown(); 304 | 305 | for (auto &l_thread : m_thread_group) { 306 | l_thread.join(); 307 | } 308 | } 309 | 310 | void 311 | ebpfsnitch_daemon::filter_thread(std::shared_ptr p_nfq) 312 | { 313 | m_log->trace("ebpfsnitch_daemon::filter_thread() entry"); 314 | 315 | try { 316 | fd_set l_fd_set; 317 | 318 | const int l_stop_fd = m_stopper.get_stop_fd(); 319 | const int l_nfq_fd = p_nfq->get_fd(); 320 | const int l_max_fd = std::max(l_stop_fd, l_nfq_fd); 321 | 322 | while (true) { 323 | FD_ZERO(&l_fd_set); 324 | FD_SET(l_stop_fd, &l_fd_set); 325 | FD_SET(l_nfq_fd, &l_fd_set); 326 | 327 | const int l_count = select( 328 | l_max_fd + 1, 329 | &l_fd_set, 330 | NULL, 331 | NULL, 332 | NULL 333 | ); 334 | 335 | if (l_count == -1) { 336 | m_log->error("probe_thread() select() error"); 337 | 338 | break; 339 | } else if (FD_ISSET(l_stop_fd, &l_fd_set)) { 340 | break; 341 | } else if (FD_ISSET(l_nfq_fd, &l_fd_set)) { 342 | p_nfq->step(); 343 | } else { 344 | m_log->error("filter_thread() select() unknown fd"); 345 | 346 | break; 347 | } 348 | } 349 | } catch (const std::exception &p_err) { 350 | m_log->error("filter_thread() exception {}", p_err.what()); 351 | } 352 | 353 | m_stopper.stop(); 354 | 355 | m_log->trace("ebpfsnitch_daemon::filter_thread() exit"); 356 | } 357 | 358 | void 359 | ebpfsnitch_daemon::probe_thread() 360 | { 361 | m_log->trace("ebpfsnitch_daemon::probe_thread() entry"); 362 | 363 | try { 364 | fd_set l_fd_set; 365 | 366 | const int l_stop_fd = m_stopper.get_stop_fd(); 367 | const int l_ring_fd = m_ring_buffer->get_fd(); 368 | const int l_max_fd = std::max(l_stop_fd, l_ring_fd); 369 | 370 | while (true) { 371 | FD_ZERO(&l_fd_set); 372 | FD_SET(l_stop_fd, &l_fd_set); 373 | FD_SET(l_ring_fd, &l_fd_set); 374 | 375 | const int l_count = select( 376 | l_max_fd + 1, 377 | &l_fd_set, 378 | NULL, 379 | NULL, 380 | NULL 381 | ); 382 | 383 | if (l_count == -1) { 384 | m_log->error("probe_thread() select() error"); 385 | 386 | break; 387 | } else if (FD_ISSET(l_stop_fd, &l_fd_set)) { 388 | break; 389 | } else if (FD_ISSET(l_ring_fd, &l_fd_set)) { 390 | m_ring_buffer->consume(); 391 | } else { 392 | m_log->error("probe_thread() select() unknown fd"); 393 | 394 | break; 395 | } 396 | } 397 | } catch (const std::exception &p_err) { 398 | m_log->error("probe_thread() exception {}", p_err.what()); 399 | } 400 | 401 | m_stopper.stop(); 402 | 403 | m_log->trace("ebpfsnitch_daemon::probe_thread() exit"); 404 | } 405 | 406 | void 407 | ebpfsnitch_daemon::bpf_reader( 408 | void *const p_data, 409 | const int p_data_size 410 | ){ 411 | assert(p_data); 412 | assert(p_data_size == sizeof(ebpf_event_t)); 413 | 414 | const struct ebpf_event_t *const l_info = 415 | static_cast(p_data); 416 | 417 | const std::shared_ptr l_process_info = 418 | m_process_manager.lookup_process_info(l_info->m_process_id); 419 | 420 | if (l_process_info == nullptr) { 421 | m_log->error("process does not exist {}", l_info->m_process_id); 422 | 423 | return; 424 | } 425 | 426 | // sanity check compare expected properties 427 | if (l_info->m_user_id != l_process_info->m_user_id) { 428 | m_log->error("ebpf and proc mismatch userid"); 429 | 430 | return; 431 | } 432 | 433 | struct ebpf_event_t l_info2; 434 | memcpy(&l_info2, l_info, sizeof(ebpf_event_t)); 435 | l_info2.m_destination_port = ntohs(l_info->m_destination_port); 436 | 437 | /* 438 | m_log->trace( 439 | "got bpf event {} src {}:{} dst {}:{}", 440 | ip_protocol_to_string(static_cast(l_info2.m_protocol)), 441 | ipv4_to_string(l_info2.m_source_address), 442 | std::to_string(l_info2.m_source_port), 443 | ipv4_to_string(l_info2.m_destination_address), 444 | std::to_string(l_info2.m_destination_port) 445 | ); 446 | */ 447 | 448 | m_connection_manager.add_connection_info(l_info2, l_process_info); 449 | 450 | process_unassociated(); 451 | } 452 | 453 | bool 454 | ebpfsnitch_daemon::process_associated_event( 455 | const struct nfq_event_t &l_nfq_event, 456 | const struct process_info_t &l_info 457 | ) { 458 | const std::optional l_verdict = m_rule_engine.get_verdict( 459 | l_nfq_event, 460 | l_info 461 | ); 462 | 463 | if (l_verdict) { 464 | if (l_verdict.value()) { 465 | l_nfq_event.m_queue->send_verdict( 466 | l_nfq_event.m_nfq_id, 467 | nfq_verdict_t::ACCEPT 468 | ); 469 | 470 | return true; 471 | } else { 472 | l_nfq_event.m_queue->send_verdict( 473 | l_nfq_event.m_nfq_id, 474 | nfq_verdict_t::DROP 475 | ); 476 | 477 | return true; 478 | } 479 | } 480 | 481 | return false; 482 | } 483 | 484 | void 485 | ebpfsnitch_daemon::ask_verdict( 486 | const std::shared_ptr l_info, 487 | const struct nfq_event_t &l_nfq_event 488 | ) { 489 | const std::optional l_domain = l_nfq_event.m_v6 490 | ? m_dns_cache.lookup_domain_v6( 491 | l_nfq_event.m_destination_address_v6 492 | ) 493 | : m_dns_cache.lookup_domain_v4( 494 | l_nfq_event.m_destination_address 495 | ); 496 | 497 | const std::string l_destination_address = [&]() { 498 | if (l_nfq_event.m_v6) { 499 | return ipv6_to_string(l_nfq_event.m_destination_address_v6); 500 | } else { 501 | return ipv4_to_string(l_nfq_event.m_destination_address); 502 | } 503 | }(); 504 | 505 | const std::string l_source_address = [&]() { 506 | if (l_nfq_event.m_v6) { 507 | return ipv6_to_string(l_nfq_event.m_source_address_v6); 508 | } else { 509 | return ipv4_to_string(l_nfq_event.m_source_address); 510 | } 511 | }(); 512 | 513 | nlohmann::json l_json = { 514 | { "kind", "query" }, 515 | { "executable", l_info->m_executable }, 516 | { "userId", l_info->m_user_id }, 517 | { "processId", l_info->m_process_id }, 518 | { "sourceAddress", l_source_address }, 519 | { "sourcePort", l_nfq_event.m_source_port }, 520 | { "destinationPort", l_nfq_event.m_destination_port }, 521 | { "destinationAddress", l_destination_address }, 522 | { "protocol", 523 | ip_protocol_to_string(l_nfq_event.m_protocol) } 524 | }; 525 | 526 | if (l_domain.has_value()) { 527 | l_json["domain"] = l_domain.value(); 528 | } 529 | 530 | if (l_info->m_container_id.has_value()) { 531 | l_json["container"] = l_info->m_container_id.value(); 532 | } 533 | 534 | std::lock_guard l_guard(m_control_connections_lock); 535 | 536 | for (const auto &l_context: m_control_connections) { 537 | if (l_context->m_pending_verdict == false) { 538 | l_context->m_session->queue_outgoing_json(l_json); 539 | l_context->m_pending_verdict = true; 540 | } 541 | } 542 | } 543 | 544 | bool 545 | ebpfsnitch_daemon::process_nfq_event( 546 | const struct nfq_event_t &l_nfq_event, 547 | const bool p_queue_unassociated 548 | ) { 549 | const std::shared_ptr l_info = 550 | m_connection_manager.lookup_connection_info(l_nfq_event); 551 | 552 | if (l_info) { 553 | if (process_associated_event(l_nfq_event, *l_info)) { 554 | return true; 555 | } 556 | } 557 | 558 | if (p_queue_unassociated) { 559 | if (l_info) { 560 | { 561 | std::lock_guard l_guard(m_undecided_packets_lock); 562 | 563 | m_undecided_packets.push(l_nfq_event); 564 | } 565 | 566 | process_unhandled(); 567 | } else { 568 | std::lock_guard l_guard( 569 | m_unassociated_packets_lock 570 | ); 571 | 572 | m_unassociated_packets.push(l_nfq_event); 573 | } 574 | } 575 | 576 | return false; 577 | } 578 | 579 | nfq_cb_result_t 580 | ebpfsnitch_daemon::nfq_handler( 581 | nfq_wrapper *const p_queue, 582 | const uint32_t p_packet_id, 583 | const std::span &p_packet 584 | ) { 585 | assert(p_queue); 586 | 587 | const uint16_t l_payload_length = p_packet.size(); 588 | const char *const l_data = (char *)p_packet.data(); 589 | 590 | if (l_payload_length < 24) { 591 | m_log->error("unknown dropping malformed"); 592 | 593 | p_queue->send_verdict(p_packet_id, nfq_verdict_t::DROP); 594 | 595 | return nfq_cb_result_t::OK; 596 | } 597 | 598 | const uint8_t l_ip_version = (*l_data & 0b11110000) >> 4; 599 | 600 | if (l_ip_version != 4 && l_ip_version != 6) { 601 | m_log->warn("got unknown ip protocol version {}", l_ip_version); 602 | 603 | p_queue->send_verdict(p_packet_id, nfq_verdict_t::DROP); 604 | 605 | return nfq_cb_result_t::OK; 606 | } 607 | 608 | struct nfq_event_t l_nfq_event = { 609 | .m_v6 = l_ip_version == 6, 610 | .m_user_id = 0, 611 | .m_group_id = 0, 612 | .m_nfq_id = p_packet_id, 613 | .m_timestamp = nanoseconds(), 614 | .m_queue = p_queue 615 | }; 616 | 617 | if (l_ip_version == 4) { 618 | l_nfq_event.m_source_address = *((uint32_t*) (l_data + 12)); 619 | l_nfq_event.m_destination_address = *((uint32_t*) (l_data + 16)); 620 | l_nfq_event.m_protocol = 621 | static_cast(*((uint8_t*) (l_data + 9))); 622 | } else { 623 | l_nfq_event.m_source_address_v6 = *((__uint128_t*) (l_data + 8)); 624 | l_nfq_event.m_destination_address_v6 = *((__uint128_t*) (l_data + 24)); 625 | l_nfq_event.m_protocol = 626 | static_cast(*((uint8_t*) (l_data + 6))); 627 | } 628 | 629 | const char *const l_ip_body = 630 | (l_ip_version == 6) ? (l_data + 40) : (l_data + 20); 631 | 632 | if ( 633 | l_nfq_event.m_protocol == ip_protocol_t::TCP || 634 | l_nfq_event.m_protocol == ip_protocol_t::UDP 635 | ) { 636 | l_nfq_event.m_source_port = ntohs(*((uint16_t*) l_ip_body)); 637 | l_nfq_event.m_destination_port = ntohs(*((uint16_t*) (l_ip_body + 2))); 638 | } else { 639 | l_nfq_event.m_source_port = 0; 640 | l_nfq_event.m_destination_port = 0; 641 | } 642 | 643 | process_nfq_event(l_nfq_event, true); 644 | 645 | return nfq_cb_result_t::OK; 646 | } 647 | 648 | nfq_cb_result_t 649 | ebpfsnitch_daemon::nfq_handler_incoming( 650 | nfq_wrapper *const p_queue, 651 | const uint32_t p_packet_id, 652 | const std::span &p_packet 653 | ) { 654 | assert(p_queue); 655 | 656 | const uint16_t l_payload_length = p_packet.size(); 657 | const char *const l_data = (char *)p_packet.data(); 658 | 659 | if (l_payload_length < 24) { 660 | m_log->error("unknown dropping malformed"); 661 | 662 | p_queue->send_verdict(p_packet_id, nfq_verdict_t::DROP); 663 | 664 | return nfq_cb_result_t::OK; 665 | } 666 | 667 | const uint8_t l_ip_version = (*l_data & 0b11110000) >> 4; 668 | 669 | if (l_ip_version != 4 && l_ip_version != 6) { 670 | m_log->warn("got unknown ip protocol version {}", l_ip_version); 671 | 672 | p_queue->send_verdict(p_packet_id, nfq_verdict_t::DROP); 673 | 674 | return nfq_cb_result_t::OK; 675 | } 676 | 677 | const ip_protocol_t l_proto = l_ip_version == 6 678 | ? static_cast(*((uint8_t*) (l_data + 6))) 679 | : static_cast(*((uint8_t*) (l_data + 9))); 680 | 681 | if (l_proto == ip_protocol_t::UDP) { 682 | const char *const l_ip_body = 683 | (l_ip_version == 6) ? (l_data + 40) : (l_data + 20); 684 | 685 | const char *const l_udp_body = l_ip_body + 8; 686 | 687 | if (ntohs(*((uint16_t*) l_ip_body)) == 53) { 688 | process_dns(l_udp_body, l_data + l_payload_length); 689 | } 690 | } 691 | 692 | p_queue->send_verdict(p_packet_id, nfq_verdict_t::ACCEPT); 693 | 694 | return nfq_cb_result_t::OK; 695 | } 696 | 697 | void 698 | ebpfsnitch_daemon::process_dns( 699 | const char *const p_packet, 700 | const char *const l_dns_end 701 | ){ 702 | const size_t l_packet_size = l_dns_end - p_packet; 703 | 704 | struct dns_header_t l_header; 705 | 706 | if (!dns_parse_header(p_packet, l_packet_size, &l_header)) { 707 | m_log->warn("dns_parse_header() failed"); 708 | 709 | return; 710 | } 711 | 712 | if (l_header.m_question_count != 1) { 713 | m_log->warn( 714 | "dns got {} questions, ignoring", 715 | l_header.m_question_count 716 | ); 717 | 718 | return; 719 | } 720 | 721 | if (l_header.m_answer_count == 0) { 722 | m_log->warn("dns got {} answers, ignoring", l_header.m_answer_count); 723 | 724 | return; 725 | } 726 | 727 | const char *l_iter = dns_get_body(p_packet); 728 | 729 | struct dns_question_t l_question; 730 | 731 | l_iter = dns_parse_question(p_packet, l_packet_size, l_iter, &l_question); 732 | 733 | if (l_iter == NULL) { 734 | m_log->warn("dns_parse_question() failed"); 735 | 736 | return; 737 | } 738 | 739 | const std::optional l_question_name = dns_decode_qname( 740 | p_packet, l_packet_size, l_question.m_name, true 741 | ); 742 | 743 | if (!l_question_name) { 744 | m_log->warn("dns_decode_qname() for question failed"); 745 | 746 | return; 747 | } 748 | 749 | for (uint l_i = 0; l_i < l_header.m_answer_count; l_i++) { 750 | struct dns_resource_record_t l_resource; 751 | 752 | l_iter = dns_parse_record(p_packet, l_packet_size, l_iter, &l_resource); 753 | 754 | if (l_iter == NULL) { 755 | m_log->warn("dns_parse_record() failed"); 756 | 757 | return; 758 | } 759 | 760 | if (l_resource.m_type == dns_resource_record_type::A) { 761 | if (l_resource.m_data_length != 4) { 762 | m_log->warn("record length A expected 4 bytes"); 763 | 764 | return; 765 | } 766 | 767 | const uint32_t l_address = *((uint32_t *)l_resource.m_data); 768 | 769 | const std::optional l_record_name = dns_decode_qname( 770 | p_packet, l_packet_size, l_resource.m_name, true 771 | ); 772 | 773 | if (!l_record_name) { 774 | m_log->warn("dns_decode_qname() for record failed"); 775 | 776 | return; 777 | } 778 | 779 | m_log->info( 780 | "Got A record for {} {} {}", 781 | l_question_name.value(), 782 | l_record_name.value(), 783 | ipv4_to_string(l_address) 784 | ); 785 | 786 | m_dns_cache.add_ipv4_mapping(l_address, l_question_name.value()); 787 | } else if (l_resource.m_type == dns_resource_record_type::AAAA) { 788 | if (l_resource.m_data_length != 16) { 789 | m_log->warn("record length AAAA expected 16 bytes"); 790 | 791 | return; 792 | } 793 | 794 | const __uint128_t l_address = *((__uint128_t *)l_resource.m_data); 795 | 796 | const std::optional l_record_name = dns_decode_qname( 797 | p_packet, l_packet_size, l_resource.m_name, true 798 | ); 799 | 800 | if (!l_record_name) { 801 | m_log->warn("dns_decode_qname() for record failed"); 802 | 803 | return; 804 | } 805 | 806 | m_log->info( 807 | "Got AAAA record for {} {} {}", 808 | l_question_name.value(), 809 | l_record_name.value(), 810 | ipv6_to_string(l_address) 811 | ); 812 | 813 | m_dns_cache.add_ipv6_mapping(l_address, l_question_name.value()); 814 | } else { 815 | return; 816 | } 817 | } 818 | } 819 | 820 | void 821 | ebpfsnitch_daemon::process_unassociated() 822 | { 823 | std::queue l_remaining; 824 | 825 | // m_log->info("process unassociated"); 826 | 827 | std::lock_guard l_guard(m_unassociated_packets_lock); 828 | 829 | while (m_unassociated_packets.size()) { 830 | struct nfq_event_t l_nfq_event = m_unassociated_packets.front(); 831 | 832 | std::shared_ptr l_info = 833 | m_connection_manager.lookup_connection_info(l_nfq_event); 834 | 835 | if (l_info) { 836 | if (!process_associated_event(l_nfq_event, *l_info)) { 837 | { 838 | std::lock_guard l_guard( 839 | m_undecided_packets_lock 840 | ); 841 | 842 | m_undecided_packets.push(l_nfq_event); 843 | } 844 | 845 | process_unhandled(); 846 | } 847 | } else { 848 | // two seconds 849 | if (nanoseconds() > (l_nfq_event.m_timestamp + 2000000000 )) { 850 | /* 851 | m_log->error( 852 | "dropping still unassociated {}", 853 | nfq_event_to_string(l_nfq_event) 854 | ); 855 | */ 856 | 857 | l_nfq_event.m_queue->send_verdict( 858 | l_nfq_event.m_nfq_id, 859 | nfq_verdict_t::DROP 860 | ); 861 | } else { 862 | l_remaining.push(l_nfq_event); 863 | } 864 | } 865 | 866 | m_unassociated_packets.pop(); 867 | } 868 | 869 | m_unassociated_packets = l_remaining; 870 | } 871 | 872 | void 873 | ebpfsnitch_daemon::process_unhandled() 874 | { 875 | std::queue l_remaining; 876 | 877 | // m_log->info("process unhandled"); 878 | 879 | std::unique_lock l_guard(m_undecided_packets_lock); 880 | 881 | while (m_undecided_packets.size()) { 882 | struct nfq_event_t l_event = m_undecided_packets.front(); 883 | 884 | const std::shared_ptr l_info = 885 | m_connection_manager.lookup_connection_info(l_event); 886 | 887 | if (l_info) { 888 | if (!process_associated_event(l_event, *l_info)) { 889 | // m_log->info("still undecided"); 890 | 891 | l_remaining.push(l_event); 892 | 893 | ask_verdict(l_info, l_event); 894 | } 895 | } else { 896 | m_log->error("event unassociated when it should be, dropping"); 897 | } 898 | 899 | m_undecided_packets.pop(); 900 | } 901 | 902 | m_undecided_packets = l_remaining; 903 | } 904 | 905 | void 906 | ebpfsnitch_daemon::handle_control_message( 907 | std::weak_ptr p_context, 908 | nlohmann::json p_message 909 | ) { 910 | std::shared_ptr l_context = p_context.lock(); 911 | 912 | if (!l_context) { 913 | m_log->error("handle_control_message weak_ptr expired"); 914 | 915 | return; 916 | } 917 | 918 | if (p_message["kind"] == "addRule") { 919 | m_log->info("adding rule"); 920 | 921 | const std::string l_rule_id = m_rule_engine.add_rule(p_message); 922 | 923 | p_message["ruleId"] = l_rule_id; 924 | 925 | const nlohmann::json l_json = { 926 | { "kind", "addRule" }, 927 | { "body", p_message } 928 | }; 929 | 930 | send_to_all_control_connections(l_json); 931 | 932 | l_context->m_pending_verdict = false; 933 | 934 | process_unhandled(); 935 | } else if (p_message["kind"] == "removeRule") { 936 | m_log->info("removing rule"); 937 | 938 | m_rule_engine.delete_rule(p_message["ruleId"]); 939 | } 940 | } 941 | 942 | void 943 | ebpfsnitch_daemon::handle_disconnect( 944 | std::weak_ptr p_context 945 | ) { 946 | m_log->info("handle_disconnect"); 947 | 948 | std::shared_ptr l_context = p_context.lock(); 949 | 950 | if (!l_context) { 951 | m_log->error("handle_disconnect weak_ptr expired"); 952 | 953 | return; 954 | } 955 | 956 | std::lock_guard l_guard(m_control_connections_lock); 957 | 958 | m_control_connections.erase(l_context); 959 | } 960 | 961 | void 962 | ebpfsnitch_daemon::send_to_all_control_connections( 963 | const nlohmann::json p_message 964 | ) { 965 | std::lock_guard l_guard(m_control_connections_lock); 966 | 967 | for (const auto &l_context: m_control_connections) { 968 | l_context->m_session->queue_outgoing_json(p_message); 969 | } 970 | } 971 | 972 | std::string 973 | nfq_event_to_string(const nfq_event_t &p_event) 974 | { 975 | return 976 | " proto " + ip_protocol_to_string(p_event.m_protocol) + 977 | " sourceAddress " + ipv4_to_string(p_event.m_source_address) + 978 | " sourcePort " + std::to_string(p_event.m_source_port) + 979 | " destinationAddress " + ipv4_to_string(p_event.m_destination_address) + 980 | " destinationPort " + std::to_string(p_event.m_destination_port) + 981 | " timestamp " + std::to_string(p_event.m_timestamp); 982 | } 983 | 984 | void 985 | ebpfsnitch_daemon::await_shutdown() 986 | { 987 | m_stopper.await_stop_block(); 988 | } 989 | 990 | void 991 | ebpfsnitch_daemon::shutdown() 992 | { 993 | m_log->trace("ebpfsnitch_daemon::shutdown");; 994 | 995 | m_stopper.stop(); 996 | } 997 | -------------------------------------------------------------------------------- /ebpfsnitch_daemon.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "control_api.hpp" 13 | #include "misc.hpp" 14 | #include "rule_engine.hpp" 15 | #include "bpf_wrapper.hpp" 16 | #include "nfq_wrapper.hpp" 17 | #include "process_manager.hpp" 18 | #include "nfq_event.h" 19 | #include "dns_cache.hpp" 20 | #include "stopper.hpp" 21 | #include "ebpf_event.hpp" 22 | #include "connection_manager.hpp" 23 | 24 | std::string nfq_event_to_string(const nfq_event_t &p_event); 25 | 26 | class iptables_raii { 27 | public: 28 | iptables_raii(std::shared_ptr p_log); 29 | 30 | ~iptables_raii(); 31 | 32 | static void remove_rules(); 33 | 34 | private: 35 | std::shared_ptr m_log; 36 | }; 37 | 38 | class ebpfsnitch_daemon { 39 | public: 40 | ebpfsnitch_daemon( 41 | std::shared_ptr p_log, 42 | std::optional p_group, 43 | std::optional p_rules_path 44 | ); 45 | 46 | ~ebpfsnitch_daemon(); 47 | 48 | void await_shutdown(); 49 | 50 | void shutdown(); 51 | 52 | private: 53 | rule_engine_t m_rule_engine; 54 | 55 | void filter_thread(std::shared_ptr p_nfq); 56 | void probe_thread(); 57 | 58 | std::mutex m_response_lock; 59 | void process_unhandled(); 60 | 61 | void 62 | bpf_reader( 63 | void *const p_data, 64 | const int p_data_size 65 | ); 66 | 67 | nfq_cb_result_t 68 | nfq_handler( 69 | nfq_wrapper *const p_queue, 70 | const uint32_t p_packet_id, 71 | const std::span &p_packet 72 | ); 73 | 74 | nfq_cb_result_t 75 | nfq_handler_incoming( 76 | nfq_wrapper *const p_queue, 77 | const uint32_t p_packet_id, 78 | const std::span &p_packet 79 | ); 80 | 81 | bool 82 | process_nfq_event( 83 | const struct nfq_event_t &l_nfq_event, 84 | const bool p_queue_unassociated 85 | ); 86 | // packets with an application without a user verdict 87 | std::queue m_undecided_packets; 88 | std::mutex m_undecided_packets_lock; 89 | 90 | // packets not yet associated with an application 91 | std::queue m_unassociated_packets; 92 | std::mutex m_unassociated_packets_lock; 93 | void process_unassociated(); 94 | 95 | void 96 | ask_verdict( 97 | const std::shared_ptr l_info, 98 | const struct nfq_event_t &l_nfq_event 99 | ); 100 | 101 | std::shared_ptr m_ring_buffer; 102 | std::shared_ptr m_log; 103 | const std::optional m_group; 104 | std::shared_ptr m_nfq; 105 | std::shared_ptr m_nfqv6; 106 | std::shared_ptr m_nfq_incoming; 107 | std::shared_ptr m_nfq_incomingv6; 108 | process_manager m_process_manager; 109 | dns_cache m_dns_cache; 110 | connection_manager m_connection_manager; 111 | 112 | bool 113 | process_associated_event( 114 | const struct nfq_event_t &l_nfq_event, 115 | const struct process_info_t &l_info 116 | ); 117 | 118 | stopper m_stopper; 119 | bpf_wrapper_object m_bpf_wrapper; 120 | std::shared_ptr m_control_api; 121 | std::unique_ptr m_iptables_raii; 122 | 123 | std::vector m_thread_group; 124 | 125 | void 126 | process_dns( 127 | const char *const p_start, 128 | const char *const p_end 129 | ); 130 | 131 | struct connection_context { 132 | bool m_pending_verdict; 133 | std::shared_ptr m_session; 134 | }; 135 | 136 | std::mutex m_control_connections_lock; 137 | std::unordered_set> 138 | m_control_connections; 139 | 140 | void 141 | handle_control_message( 142 | std::weak_ptr p_context, 143 | nlohmann::json p_message 144 | ); 145 | 146 | void handle_disconnect(std::weak_ptr p_context); 147 | 148 | void send_to_all_control_connections(const nlohmann::json p_message); 149 | }; 150 | -------------------------------------------------------------------------------- /ebpfsnitchd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=eBPFSnitch Firewall Daemon 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | RestartSec=10 9 | ExecStart=/usr/bin/ebpfsnitchd --rules-path='/etc/ebpfsnitchd.json' 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /lru_map.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | template 8 | class lru_map { 9 | public: 10 | lru_map(const size_t p_max_size): m_max_size(p_max_size) {}; 11 | 12 | void 13 | insert(const key_t &p_key, const value_t &p_value) 14 | { 15 | const auto l_iterator = m_map.find(p_key); 16 | 17 | if (l_iterator != m_map.end()) { 18 | l_iterator->second->second = p_value; 19 | 20 | m_list.splice(m_list.begin(), m_list, l_iterator->second); 21 | } else { 22 | m_list.push_front(std::pair{p_key, p_value}); 23 | m_map[p_key] = m_list.begin(); 24 | } 25 | 26 | if (m_map.size() > m_max_size) { 27 | m_map.erase(m_list.back().first); 28 | m_list.pop_back(); 29 | } 30 | } 31 | 32 | std::optional 33 | lookup(const key_t &p_key) const 34 | { 35 | const auto l_iterator = m_map.find(p_key); 36 | 37 | if (l_iterator != m_map.end()) { 38 | return std::optional(l_iterator->second->second); 39 | } else { 40 | return std::nullopt; 41 | } 42 | } 43 | 44 | private: 45 | const size_t m_max_size; 46 | 47 | std::list> m_list; 48 | 49 | std::unordered_map< 50 | key_t, 51 | typename std::list>::iterator 52 | > m_map; 53 | }; 54 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "ebpfsnitch_daemon.hpp" 12 | 13 | std::shared_ptr g_log; 14 | std::unique_ptr g_daemon; 15 | 16 | /* 17 | static void 18 | trace_ebpf() 19 | { 20 | std::ifstream l_pipe("/sys/kernel/debug/tracing/trace_pipe"); 21 | std::string l_line; 22 | 23 | while (true) { 24 | if (std::getline(l_pipe, l_line)) { 25 | g_log->trace("eBPF log: {}", l_line); 26 | } else { 27 | sleep(1); 28 | } 29 | } 30 | } 31 | */ 32 | 33 | static void 34 | signal_stop(const int p_sig) 35 | { 36 | g_log->info("signal_stop {}", p_sig); 37 | 38 | g_daemon->shutdown(); 39 | } 40 | 41 | static void 42 | signal_pipe(const int p_sig) 43 | { 44 | (void)p_sig; 45 | 46 | g_log->error("SIGPIPE"); 47 | } 48 | 49 | static void 50 | set_limits() 51 | { 52 | struct rlimit l_limit = { 53 | .rlim_cur = RLIM_INFINITY, 54 | .rlim_max = RLIM_INFINITY, 55 | }; 56 | 57 | if (setrlimit(RLIMIT_MEMLOCK, &l_limit)) { 58 | throw std::runtime_error("failed to set limits"); 59 | } 60 | } 61 | 62 | int 63 | main(const int p_argc, const char** p_argv) 64 | { 65 | g_log = spdlog::stdout_color_mt("console"); 66 | g_log->set_level(spdlog::level::trace); 67 | 68 | try { 69 | boost::program_options::options_description 70 | l_description("eBPFSnitch Allowed options"); 71 | 72 | l_description.add_options() 73 | ( "help,h", "produce help message" ) 74 | ( "version,v", "print version" ) 75 | ( "remove-rules", "remove iptables rules" ) 76 | ( 77 | "group", 78 | boost::program_options::value(), 79 | "group name for control socket" 80 | ) 81 | ( 82 | "rules-path", 83 | boost::program_options::value(), 84 | "file to load / store firewall rules" 85 | ); 86 | 87 | boost::program_options::variables_map l_map; 88 | 89 | boost::program_options::store( 90 | boost::program_options::parse_command_line( 91 | p_argc, 92 | p_argv, 93 | l_description 94 | ), 95 | l_map 96 | ); 97 | 98 | boost::program_options::notify(l_map); 99 | 100 | if (l_map.count("help")) { 101 | std::cout << l_description; 102 | 103 | return 0; 104 | } 105 | 106 | if (l_map.count("version")) { 107 | std::cout << "0.3.0" << std::endl; 108 | 109 | return 0; 110 | } 111 | 112 | if (l_map.count("remove-rules")) { 113 | iptables_raii::remove_rules(); 114 | 115 | return 0; 116 | } 117 | 118 | const std::optional l_group = [&](){ 119 | if (l_map.count("group")) { 120 | return std::optional(l_map["group"].as()); 121 | } else { 122 | return std::optional(); 123 | } 124 | }(); 125 | 126 | const std::optional l_rules_path = [&](){ 127 | if (l_map.count("rules-path")) { 128 | return std::optional(l_map["rules-path"].as()); 129 | } else { 130 | return std::optional(); 131 | } 132 | }(); 133 | 134 | signal(SIGPIPE, signal_pipe); 135 | 136 | set_limits(); 137 | 138 | g_daemon = std::make_unique( 139 | g_log, 140 | l_group, 141 | l_rules_path 142 | ); 143 | 144 | signal(SIGINT, signal_stop); 145 | signal(SIGTERM, signal_stop); 146 | 147 | g_daemon->await_shutdown(); 148 | } catch (const std::exception &p_error) { 149 | g_log->error("main() exception: {}", p_error.what()); 150 | 151 | return 1; 152 | } 153 | 154 | return 0; 155 | } 156 | -------------------------------------------------------------------------------- /misc.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "misc.hpp" 14 | 15 | uint64_t 16 | nanoseconds() 17 | { 18 | struct timespec l_timespec; 19 | 20 | clock_gettime(CLOCK_MONOTONIC, &l_timespec); 21 | 22 | return l_timespec.tv_nsec + (l_timespec.tv_sec * 1000000000); 23 | } 24 | 25 | std::string 26 | nf_hook_to_string(const nf_hook_t p_hook) 27 | { 28 | switch (p_hook) { 29 | case nf_hook_t::IP_PRE_ROUTING: 30 | return std::string("IP_PRE_ROUTING"); break; 31 | case nf_hook_t::IP_LOCAL_IN: 32 | return std::string("IP_LOCAL_IN"); break; 33 | case nf_hook_t::IP_FORWARD: 34 | return std::string("IP_FORWARD"); break; 35 | case nf_hook_t::IP_LOCAL_OUT: 36 | return std::string("IP_LOCAL_OUT"); break; 37 | case nf_hook_t::IP_POST_ROUTING: 38 | return std::string("IP_POST_ROUTING"); break; 39 | } 40 | 41 | return std::string("unknown"); 42 | } 43 | 44 | typedef boost::bimaps::bimap< 45 | ip_protocol_t, 46 | std::string, 47 | boost::container::allocator 48 | > g_protocol_map_type; 49 | 50 | const g_protocol_map_type g_protocol_map = 51 | boost::assign::list_of 52 | ( ip_protocol_t::HOPOPT, "HOPOPT" ) 53 | ( ip_protocol_t::ICMP, "ICMP" ) 54 | ( ip_protocol_t::IGMP, "IGMP" ) 55 | ( ip_protocol_t::GGP, "GGP" ) 56 | ( ip_protocol_t::IPV4, "IPV4" ) 57 | ( ip_protocol_t::ST, "ST" ) 58 | ( ip_protocol_t::TCP, "TCP" ) 59 | ( ip_protocol_t::CBT, "CBT" ) 60 | ( ip_protocol_t::EGP, "EGP" ) 61 | ( ip_protocol_t::IGP, "IGP" ) 62 | ( ip_protocol_t::BBNRCCMON, "BBNRCCMON" ) 63 | ( ip_protocol_t::NVPII, "NVPII" ) 64 | ( ip_protocol_t::PUP, "PUP" ) 65 | ( ip_protocol_t::ARGUS, "ARGUS" ) 66 | ( ip_protocol_t::EMCON, "EMCON" ) 67 | ( ip_protocol_t::XNET, "XNET" ) 68 | ( ip_protocol_t::CHAOS, "CHAOS" ) 69 | ( ip_protocol_t::UDP, "UDP" ); 70 | 71 | ip_protocol_t 72 | ip_protocol_from_string(const std::string &p_protocol) 73 | { 74 | return g_protocol_map.right.find(p_protocol)->second; 75 | } 76 | 77 | std::string 78 | ip_protocol_to_string(const ip_protocol_t p_protocol) 79 | { 80 | return g_protocol_map.left.find(p_protocol)->second; 81 | } 82 | 83 | std::string 84 | file_to_string(const std::string &p_path) { 85 | std::ifstream l_stream(p_path); 86 | 87 | if (l_stream.is_open() == false) { 88 | throw std::runtime_error("std::ifstream() failed"); 89 | } 90 | 91 | return std::string( 92 | (std::istreambuf_iterator(l_stream)), 93 | std::istreambuf_iterator() 94 | ); 95 | } 96 | 97 | void 98 | atomically_write_file(const std::string &p_path, const std::string &p_data) 99 | { 100 | const std::string l_temp_path = p_path + ".tmp"; 101 | 102 | std::ofstream l_stream(l_temp_path); 103 | 104 | if (!l_stream) { 105 | throw std::runtime_error("std::of_stream() failed"); 106 | } 107 | 108 | l_stream << p_data; 109 | 110 | l_stream.close(); 111 | 112 | if (std::rename(l_temp_path.c_str(), p_path.c_str())) { 113 | throw std::runtime_error("std::rename failed"); 114 | } 115 | } 116 | 117 | std::string 118 | ipv4_to_string(const uint32_t p_address) 119 | { 120 | char l_buffer[INET_ADDRSTRLEN]; 121 | 122 | const char *const l_status = inet_ntop( 123 | AF_INET, 124 | &p_address, 125 | l_buffer, 126 | INET_ADDRSTRLEN 127 | ); 128 | 129 | if (l_status == NULL) { 130 | throw std::runtime_error("inet_ntop() failed"); 131 | } 132 | 133 | return std::string(l_buffer); 134 | } 135 | 136 | std::string 137 | ipv6_to_string(const __uint128_t p_address) 138 | { 139 | char l_buffer[INET6_ADDRSTRLEN]; 140 | 141 | const char *const l_status = inet_ntop( 142 | AF_INET6, 143 | &p_address, 144 | l_buffer, 145 | INET6_ADDRSTRLEN 146 | ); 147 | 148 | if (l_status == NULL) { 149 | throw std::runtime_error("inet_ntop() failed"); 150 | } 151 | 152 | return std::string(l_buffer); 153 | } 154 | -------------------------------------------------------------------------------- /misc.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/netfilter_ipv4.h#L16 7 | enum class nf_hook_t : uint8_t { 8 | // After promisc drops, checksum checks. 9 | IP_PRE_ROUTING = 0, 10 | // If the packet is destined for this box. 11 | IP_LOCAL_IN = 1, 12 | // If the packet is destined for another interface. 13 | IP_FORWARD = 2, 14 | // Packets coming from a local process 15 | IP_LOCAL_OUT = 3, 16 | // Packets about to hit the wire. 17 | IP_POST_ROUTING = 4 18 | }; 19 | 20 | std::string 21 | nf_hook_to_string(const nf_hook_t p_hook); 22 | 23 | // https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 24 | enum class ip_protocol_t : uint8_t { 25 | HOPOPT = 0, 26 | ICMP = 1, 27 | IGMP = 2, 28 | GGP = 3, 29 | IPV4 = 4, 30 | ST = 5, 31 | TCP = 6, 32 | CBT = 7, 33 | EGP = 8, 34 | IGP = 9, 35 | BBNRCCMON = 10, 36 | NVPII = 11, 37 | PUP = 12, 38 | ARGUS = 13, 39 | EMCON = 14, 40 | XNET = 15, 41 | CHAOS = 16, 42 | UDP = 17 43 | }; 44 | 45 | std::string 46 | ip_protocol_to_string(const ip_protocol_t p_protocol); 47 | 48 | ip_protocol_t 49 | ip_protocol_from_string(const std::string &p_protocol); 50 | 51 | // https://github.com/torvalds/linux/blob/master/include/linux/socket.h#L175 52 | enum class address_family_t : uint16_t { 53 | INET = 2, 54 | INET6 = 10 55 | }; 56 | 57 | std::string 58 | ipv4_to_string(const uint32_t p_address); 59 | 60 | std::string 61 | ipv6_to_string(const __uint128_t p_address); 62 | 63 | std::string 64 | file_to_string(const std::string &p_path); 65 | 66 | void 67 | atomically_write_file(const std::string &p_path, const std::string &p_data); 68 | 69 | uint64_t 70 | nanoseconds(); 71 | -------------------------------------------------------------------------------- /nfq_event.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "nfq_wrapper.hpp" 4 | 5 | struct nfq_event_t { 6 | bool m_v6; 7 | uint32_t m_user_id; 8 | uint32_t m_group_id; 9 | uint32_t m_source_address; 10 | __uint128_t m_source_address_v6; 11 | uint16_t m_source_port; 12 | uint32_t m_destination_address; 13 | __uint128_t m_destination_address_v6; 14 | uint16_t m_destination_port; 15 | uint32_t m_nfq_id; 16 | uint64_t m_timestamp; 17 | ip_protocol_t m_protocol; 18 | nfq_wrapper * m_queue; 19 | }; 20 | -------------------------------------------------------------------------------- /nfq_wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "nfq_wrapper.hpp" 9 | 10 | nfq_wrapper::nfq_wrapper( 11 | const unsigned int p_queue_index, 12 | cb_t p_cb, 13 | const address_family_t p_family 14 | ): 15 | m_buffer(0xffff + (MNL_SOCKET_BUFFER_SIZE/2)), 16 | m_socket(mnl_socket_open(NETLINK_NETFILTER), &mnl_socket_close), 17 | m_queue_index(p_queue_index), 18 | m_cb(p_cb) 19 | { 20 | if (m_socket == NULL) { 21 | throw std::runtime_error("mnl_socket_open() failed"); 22 | } 23 | 24 | if (mnl_socket_bind(m_socket.get(), 0, MNL_SOCKET_AUTOPID) < 0) { 25 | throw std::runtime_error("mnl_socket_bind() failed"); 26 | } 27 | 28 | struct nlmsghdr *l_header = nfq_nlmsg_put( 29 | m_buffer.data(), 30 | NFQNL_MSG_CONFIG, 31 | p_queue_index 32 | ); 33 | 34 | if (l_header == NULL) { 35 | throw std::runtime_error("nfq_nlmsg_put() failed"); 36 | } 37 | 38 | nfq_nlmsg_cfg_put_cmd( 39 | l_header, 40 | static_cast(p_family), 41 | NFQNL_CFG_CMD_BIND 42 | ); 43 | 44 | if (mnl_socket_sendto(m_socket.get(), l_header, l_header->nlmsg_len) < 0) { 45 | throw std::runtime_error("mnl_socket_sendto() failed"); 46 | } 47 | 48 | l_header = nfq_nlmsg_put(m_buffer.data(), NFQNL_MSG_CONFIG, p_queue_index); 49 | 50 | if (l_header == NULL) { 51 | throw std::runtime_error("nfq_nlmsg_put() failed"); 52 | } 53 | 54 | nfq_nlmsg_cfg_put_params(l_header, NFQNL_COPY_PACKET, 0xffff); 55 | 56 | mnl_attr_put_u32(l_header, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO)); 57 | mnl_attr_put_u32(l_header, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO)); 58 | 59 | if (mnl_socket_sendto(m_socket.get(), l_header, l_header->nlmsg_len) < 0) { 60 | throw std::runtime_error("mnl_socket_sendto() failed"); 61 | } 62 | 63 | m_port_id = mnl_socket_get_portid(m_socket.get()); 64 | } 65 | 66 | nfq_wrapper::~nfq_wrapper() {} 67 | 68 | int 69 | nfq_wrapper::get_fd() 70 | { 71 | return mnl_socket_get_fd(m_socket.get()); 72 | } 73 | 74 | int 75 | nfq_wrapper::queue_cb_proxy( 76 | const struct nlmsghdr *const p_header, 77 | void *const p_context 78 | ) { 79 | nfq_wrapper *const l_self = static_cast(p_context); 80 | 81 | assert(l_self != NULL); 82 | 83 | struct nlattr *l_attributes[NFQA_MAX + 1] = {}; 84 | 85 | if (nfq_nlmsg_parse(p_header, l_attributes) < 0) { 86 | return MNL_CB_ERROR; 87 | } 88 | 89 | struct nfqnl_msg_packet_hdr *const l_packet_header = 90 | static_cast( 91 | mnl_attr_get_payload(l_attributes[NFQA_PACKET_HDR]) 92 | ); 93 | 94 | const std::span l_payload( 95 | static_cast( 96 | mnl_attr_get_payload(l_attributes[NFQA_PAYLOAD]) 97 | ), 98 | mnl_attr_get_payload_len( 99 | l_attributes[NFQA_PAYLOAD] 100 | ) 101 | ); 102 | 103 | l_self->m_cb(l_self, ntohl(l_packet_header->packet_id), l_payload); 104 | 105 | return 0; 106 | } 107 | 108 | void 109 | nfq_wrapper::step() 110 | { 111 | const int l_status = mnl_socket_recvfrom( 112 | m_socket.get(), 113 | m_buffer.data(), 114 | m_buffer.size() 115 | ); 116 | 117 | if (l_status < 0) { 118 | if (errno == ENOBUFS) { 119 | return; 120 | } else { 121 | throw std::runtime_error( 122 | "mnl_socket_recvfrom() " + std::string(strerror(errno)) 123 | ); 124 | } 125 | } 126 | 127 | const int l_status2 = mnl_cb_run( 128 | m_buffer.data(), 129 | l_status, 130 | 0, 131 | m_port_id, 132 | &nfq_wrapper::queue_cb_proxy, 133 | this 134 | ); 135 | 136 | if (l_status2 < 0) { 137 | throw std::runtime_error( 138 | "mnl_cb_run() " + std::string(strerror(errno)) 139 | ); 140 | } 141 | } 142 | 143 | void 144 | nfq_wrapper::send_verdict(const uint32_t p_id, const nfq_verdict_t p_verdict) 145 | { 146 | char l_buffer[MNL_SOCKET_BUFFER_SIZE]; 147 | 148 | std::lock_guard l_guard(m_send_lock); 149 | 150 | struct nlmsghdr *const l_header = nfq_nlmsg_put( 151 | l_buffer, 152 | NFQNL_MSG_VERDICT, 153 | m_queue_index 154 | ); 155 | 156 | if (l_header == NULL) { 157 | throw std::runtime_error("nfq_nlmsg_put()"); 158 | } 159 | 160 | nfq_nlmsg_verdict_put(l_header, p_id, static_cast(p_verdict)); 161 | 162 | if (mnl_socket_sendto(m_socket.get(), l_header, l_header->nlmsg_len) < 0) { 163 | throw std::runtime_error("mnl_socket_sendto() failed"); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /nfq_wrapper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "misc.hpp" 16 | 17 | // https://elixir.bootlin.com/linux/v4.4/source/include/uapi/linux/netfilter.h 18 | enum class nfq_verdict_t : int { 19 | DROP = 0, 20 | ACCEPT = 1, 21 | STOLEN = 2, 22 | QUEUE = 3, 23 | REPEAT = 4, 24 | STOP = 5 25 | }; 26 | 27 | // https://www.nftables.org/projects/libmnl/doxygen/html/libmnl_8h_source.html 28 | enum class nfq_cb_result_t : int { 29 | ERROR = -1, 30 | STOP = 0, 31 | OK = 1 32 | }; 33 | 34 | class nfq_wrapper { 35 | public: 36 | typedef std::function & 40 | )> cb_t; 41 | 42 | nfq_wrapper( 43 | const unsigned int p_queue_index, 44 | cb_t p_cb, 45 | const address_family_t p_family 46 | ); 47 | 48 | ~nfq_wrapper(); 49 | 50 | int get_fd(); 51 | 52 | void step(); 53 | 54 | void send_verdict(const uint32_t p_id, const nfq_verdict_t p_verdict); 55 | 56 | private: 57 | std::vector m_buffer; 58 | 59 | const std::unique_ptr 60 | m_socket; 61 | 62 | const unsigned int m_queue_index; 63 | 64 | unsigned int m_port_id; 65 | 66 | static int queue_cb_proxy( 67 | const struct nlmsghdr *const p_header, 68 | void *const p_context 69 | ); 70 | 71 | const cb_t m_cb; 72 | 73 | std::mutex m_send_lock; 74 | }; 75 | -------------------------------------------------------------------------------- /probes.c: -------------------------------------------------------------------------------- 1 | #define bpf_target_x86 2 | 3 | #include "vmlinux.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | char LICENSE[] SEC("license") = "Dual BSD/GPL"; 10 | 11 | struct ebpf_event_t { 12 | bool m_v6; 13 | void * m_handle; 14 | bool m_remove; 15 | uint32_t m_user_id; 16 | uint32_t m_process_id; 17 | uint32_t m_source_address; 18 | __uint128_t m_source_address_v6; 19 | uint16_t m_source_port; 20 | uint32_t m_destination_address; 21 | __uint128_t m_destination_address_v6; 22 | uint16_t m_destination_port; 23 | uint64_t m_timestamp; 24 | uint8_t m_protocol; 25 | } __attribute__((packed)); 26 | 27 | struct bpf_map_def SEC("maps") g_probe_ipv4_events = { 28 | .type = BPF_MAP_TYPE_RINGBUF, 29 | .max_entries = 4096 * 64 30 | }; 31 | 32 | struct bpf_map_def SEC("maps") g_ipv4_tcp_connect_map = { 33 | .type = BPF_MAP_TYPE_HASH, 34 | .key_size = sizeof(uint64_t), 35 | .value_size = sizeof(struct sock *), 36 | .max_entries = 1000 37 | }; 38 | 39 | struct bpf_map_def SEC("maps") g_ipv6_tcp_connect_map = { 40 | .type = BPF_MAP_TYPE_HASH, 41 | .key_size = sizeof(uint64_t), 42 | .value_size = sizeof(struct sock *), 43 | .max_entries = 1000 44 | }; 45 | 46 | struct bpf_map_def SEC("maps") g_send_map1 = { 47 | .type = BPF_MAP_TYPE_HASH, 48 | .key_size = sizeof(uint64_t), 49 | .value_size = sizeof(struct socket *), 50 | .max_entries = 1000 51 | }; 52 | 53 | struct bpf_map_def SEC("maps") g_send_map2 = { 54 | .type = BPF_MAP_TYPE_HASH, 55 | .key_size = sizeof(uint64_t), 56 | .value_size = sizeof(struct msghdr *), 57 | .max_entries = 1000 58 | }; 59 | 60 | #define AF_INET 2 61 | 62 | SEC("kprobe/tcp_v4_connect") int 63 | kprobe_tcp_v4_connect(const struct pt_regs *const p_context) 64 | { 65 | struct sock *const l_sock = (void *)PT_REGS_PARM1(p_context); 66 | 67 | const uint64_t l_id = bpf_get_current_pid_tgid(); 68 | 69 | bpf_map_update_elem(&g_ipv4_tcp_connect_map, &l_id, &l_sock, 0); 70 | 71 | return 0; 72 | } 73 | 74 | SEC("kretprobe/tcp_v4_connect") int 75 | kretprobe_tcp_v4_connect(const struct pt_regs *const p_context) 76 | { 77 | const uint64_t l_id = bpf_get_current_pid_tgid(); 78 | const uint32_t l_pid = l_id >> 32; 79 | 80 | struct sock **const l_sock_ref = bpf_map_lookup_elem( 81 | &g_ipv4_tcp_connect_map, 82 | &l_id 83 | ); 84 | 85 | if (!l_sock_ref) { 86 | return 0; 87 | } 88 | 89 | struct sock *const l_sock = *l_sock_ref; 90 | 91 | if (bpf_map_delete_elem(&g_ipv4_tcp_connect_map, &l_id) != 0) { 92 | bpf_printk("bpf_map_delete_elem failed"); 93 | 94 | return 0; 95 | } 96 | 97 | uint16_t l_source_port; 98 | uint16_t l_destination_port; 99 | uint32_t l_source_address; 100 | uint32_t l_destination_address; 101 | 102 | bpf_probe_read( 103 | &l_source_port, 104 | sizeof(l_source_port), 105 | &l_sock->__sk_common.skc_num 106 | ); 107 | 108 | bpf_probe_read( 109 | &l_destination_port, 110 | sizeof(l_destination_port), 111 | &l_sock->__sk_common.skc_dport 112 | ); 113 | 114 | bpf_probe_read( 115 | &l_source_address, 116 | sizeof(l_source_address), 117 | &l_sock->__sk_common.skc_rcv_saddr 118 | ); 119 | 120 | bpf_probe_read( 121 | &l_destination_address, 122 | sizeof(l_destination_address), 123 | &l_sock->__sk_common.skc_daddr 124 | ); 125 | 126 | struct ebpf_event_t *const l_event = bpf_ringbuf_reserve( 127 | &g_probe_ipv4_events, 128 | sizeof(struct ebpf_event_t), 129 | 0 130 | ); 131 | 132 | if (!l_event) { 133 | return 0; 134 | } 135 | 136 | l_event->m_v6 = false; 137 | l_event->m_timestamp = bpf_ktime_get_ns(); 138 | l_event->m_user_id = bpf_get_current_uid_gid(); 139 | l_event->m_process_id = l_pid; 140 | l_event->m_handle = l_sock; 141 | l_event->m_remove = false; 142 | l_event->m_protocol = 6; 143 | l_event->m_source_address = l_source_address; 144 | l_event->m_source_port = l_source_port; 145 | l_event->m_destination_port = l_destination_port; 146 | l_event->m_destination_address = l_destination_address; 147 | 148 | bpf_ringbuf_submit(l_event, BPF_RB_FORCE_WAKEUP); 149 | 150 | return 0; 151 | } 152 | 153 | SEC("kprobe/security_socket_sendmsg") int 154 | kprobe_security_socket_send_msg(const struct pt_regs *const p_context) 155 | { 156 | struct socket *const l_socket = (void *)PT_REGS_PARM1(p_context); 157 | struct msghdr *const l_msg = (void *)PT_REGS_PARM2(p_context); 158 | 159 | const uint64_t l_id = bpf_get_current_pid_tgid(); 160 | 161 | bpf_map_update_elem(&g_send_map1, &l_id, &l_socket, 0); 162 | bpf_map_update_elem(&g_send_map2, &l_id, &l_msg, 0); 163 | 164 | return 0; 165 | } 166 | 167 | SEC("kprobe/tcp_v6_connect") int 168 | kprobe_tcp_v6_connect(const struct pt_regs *const p_context) 169 | { 170 | struct sock *const l_sock = (void *)PT_REGS_PARM1(p_context); 171 | 172 | const uint64_t l_id = bpf_get_current_pid_tgid(); 173 | 174 | bpf_map_update_elem(&g_ipv6_tcp_connect_map, &l_id, &l_sock, 0); 175 | 176 | return 0; 177 | } 178 | 179 | SEC("kretprobe/tcp_v6_connect") int 180 | kretprobe_tcp_v6_connect(const struct pt_regs *const p_context) 181 | { 182 | const uint64_t l_id = bpf_get_current_pid_tgid(); 183 | const uint32_t l_pid = l_id >> 32; 184 | 185 | struct sock **l_sock_ref = bpf_map_lookup_elem( 186 | &g_ipv6_tcp_connect_map, 187 | &l_id 188 | ); 189 | 190 | if (!l_sock_ref) { 191 | bpf_printk("tcp_v6_connect_return no entry"); 192 | 193 | return 0; 194 | } 195 | 196 | if (bpf_map_delete_elem(&g_ipv6_tcp_connect_map, &l_id) != 0) { 197 | bpf_printk("bpf_map_delete_elem failed"); 198 | 199 | return 0; 200 | } 201 | 202 | struct sock *const l_sock = *l_sock_ref; 203 | struct inet_sock *const l_inet = (struct inet_sock *)l_sock; 204 | 205 | uint16_t l_source_port; 206 | uint16_t l_destination_port; 207 | __uint128_t l_source_address; 208 | __uint128_t l_destination_address; 209 | 210 | bpf_probe_read( 211 | &l_source_port, 212 | sizeof(l_source_port), 213 | &l_sock->__sk_common.skc_num 214 | ); 215 | 216 | bpf_probe_read( 217 | &l_destination_port, 218 | sizeof(l_destination_port), 219 | &l_sock->__sk_common.skc_dport 220 | ); 221 | 222 | bpf_probe_read( 223 | &l_source_address, 224 | sizeof(l_source_address), 225 | &l_sock->__sk_common.skc_v6_rcv_saddr 226 | ); 227 | 228 | bpf_probe_read( 229 | &l_destination_address, 230 | sizeof(l_destination_address), 231 | &l_sock->__sk_common.skc_v6_daddr 232 | ); 233 | 234 | struct ebpf_event_t *const l_event = bpf_ringbuf_reserve( 235 | &g_probe_ipv4_events, 236 | sizeof(struct ebpf_event_t), 237 | 0 238 | ); 239 | 240 | if (!l_event) { 241 | return 0; 242 | } 243 | 244 | l_event->m_v6 = true; 245 | l_event->m_timestamp = bpf_ktime_get_ns(); 246 | l_event->m_user_id = bpf_get_current_uid_gid(); 247 | l_event->m_process_id = l_pid; 248 | l_event->m_handle = l_sock; 249 | l_event->m_remove = false; 250 | l_event->m_protocol = 6; 251 | l_event->m_source_address_v6 = l_source_address; 252 | l_event->m_source_port = l_source_port; 253 | l_event->m_destination_port = l_destination_port; 254 | l_event->m_destination_address_v6 = l_destination_address; 255 | 256 | bpf_ringbuf_submit(l_event, BPF_RB_FORCE_WAKEUP); 257 | 258 | return 0; 259 | } 260 | 261 | SEC("kretprobe/security_socket_sendmsg") int 262 | kretprobe_security_socket_send_msg(const struct pt_regs *const p_context_ignore) 263 | { 264 | const uint64_t l_id = bpf_get_current_pid_tgid(); 265 | 266 | struct socket **const l_sock_ref = bpf_map_lookup_elem( 267 | &g_send_map1, 268 | &l_id 269 | ); 270 | 271 | struct msghdr **const l_msg_ref = bpf_map_lookup_elem( 272 | &g_send_map2, 273 | &l_id 274 | ); 275 | 276 | if (!l_sock_ref) { 277 | bpf_printk("bpf_map_lookup_elem l_sock_ref failed"); 278 | 279 | return 0; 280 | } 281 | 282 | if (!l_msg_ref) { 283 | bpf_printk("bpf_map_lookup_elem l_msg_ref failed"); 284 | 285 | return 0; 286 | } 287 | 288 | struct socket *const l_socket = *l_sock_ref; 289 | struct msghdr *const l_msg = *l_msg_ref; 290 | 291 | if (bpf_map_delete_elem(&g_send_map1, &l_id) != 0) { 292 | bpf_printk("bpf_map_delete_elem failed"); 293 | 294 | return 0; 295 | } 296 | 297 | if (bpf_map_delete_elem(&g_send_map2, &l_id) != 0) { 298 | bpf_printk("bpf_map_delete_elem failed"); 299 | 300 | return 0; 301 | } 302 | 303 | const struct sock * l_sock = 0; 304 | const struct sockaddr_in *l_usin = 0; 305 | short int l_type = 0; 306 | sa_family_t l_family = 0; 307 | uint16_t l_source_port = 0; 308 | uint16_t l_destination_port = 0; 309 | uint32_t l_source_address = 0; 310 | uint32_t l_destination_address = 0; 311 | uint8_t l_protocol = 0; 312 | 313 | if ( 314 | bpf_probe_read( 315 | &l_sock, 316 | sizeof(l_sock), 317 | &l_socket->sk 318 | ) != 0 319 | ) { 320 | bpf_printk("bpf_probe_read failed line %d", __LINE__); 321 | 322 | return 0; 323 | } 324 | 325 | if ( 326 | bpf_probe_read( 327 | &l_family, 328 | sizeof(l_family), 329 | &l_sock->__sk_common.skc_family 330 | ) != 0 331 | ) { 332 | bpf_printk("bpf_probe_read failed line %d", __LINE__); 333 | 334 | return 0; 335 | } 336 | 337 | if (l_family != AF_INET) { 338 | return 0; 339 | } 340 | 341 | if ( 342 | bpf_probe_read( 343 | &l_type, 344 | sizeof(l_type), 345 | &l_socket->type 346 | ) != 0 347 | ) { 348 | bpf_printk("bpf_probe_read failed line %d", __LINE__); 349 | 350 | return 0; 351 | } 352 | 353 | if ( 354 | bpf_probe_read( 355 | &l_type, 356 | sizeof(l_type), 357 | &l_socket->type 358 | ) != 0 359 | ) { 360 | bpf_printk("bpf_probe_read failed line %d", __LINE__); 361 | 362 | return 0; 363 | } 364 | 365 | if ( 366 | bpf_probe_read( 367 | &l_protocol, 368 | sizeof(l_protocol), 369 | &l_sock->sk_protocol 370 | ) != 0 371 | ) { 372 | bpf_printk("bpf_probe_read failed line %d", __LINE__); 373 | 374 | return 0; 375 | } 376 | 377 | if ( 378 | bpf_probe_read( 379 | &l_source_port, 380 | sizeof(l_source_port), 381 | &l_sock->__sk_common.skc_num 382 | ) != 0 383 | ) { 384 | bpf_printk("bpf_probe_read failed line %d", __LINE__); 385 | 386 | return 0; 387 | } 388 | 389 | if ( 390 | bpf_probe_read( 391 | &l_source_address, 392 | sizeof(l_source_address), 393 | &l_sock->__sk_common.skc_rcv_saddr 394 | ) != 0 395 | ) { 396 | bpf_printk("bpf_probe_read failed line %d", __LINE__); 397 | 398 | return 0; 399 | } 400 | 401 | if ( 402 | bpf_probe_read( 403 | &l_usin, 404 | sizeof(l_usin), 405 | &l_msg->msg_name 406 | ) != 0 407 | ) { 408 | bpf_printk("bpf_probe_read failed line %d", __LINE__); 409 | 410 | return 0; 411 | } 412 | 413 | if ( 414 | bpf_probe_read( 415 | &l_destination_port, 416 | sizeof(l_destination_port), 417 | &l_sock->__sk_common.skc_dport 418 | ) != 0 || l_destination_port == 0 419 | ) { 420 | if ( 421 | bpf_probe_read( 422 | &l_destination_port, 423 | sizeof(l_destination_port), 424 | &l_usin->sin_port 425 | ) != 0 426 | ) { 427 | bpf_printk("bpf_probe_read port failed %d", __LINE__); 428 | 429 | return 0; 430 | } 431 | } 432 | 433 | if ( 434 | bpf_probe_read( 435 | &l_destination_address, 436 | sizeof(l_destination_address), 437 | &l_sock->__sk_common.skc_daddr 438 | ) != 0 || l_destination_address == 0 439 | ) { 440 | if ( 441 | bpf_probe_read( 442 | &l_destination_address, 443 | sizeof(l_destination_address), 444 | &l_usin->sin_addr 445 | ) != 0 446 | ) { 447 | bpf_printk("bpf_probe_read address failed %d", __LINE__); 448 | 449 | return 0; 450 | } 451 | } 452 | 453 | struct ebpf_event_t *const l_event = bpf_ringbuf_reserve( 454 | &g_probe_ipv4_events, 455 | sizeof(struct ebpf_event_t), 456 | 0 457 | ); 458 | 459 | if (!l_event) { 460 | bpf_printk("bpf_ringbuf_reserve failed %d", __LINE__); 461 | 462 | return 0; 463 | } 464 | 465 | l_event->m_v6 = false; 466 | l_event->m_timestamp = bpf_ktime_get_ns(); 467 | l_event->m_user_id = bpf_get_current_uid_gid(); 468 | l_event->m_process_id = bpf_get_current_pid_tgid() >> 32; 469 | l_event->m_handle = l_socket; 470 | l_event->m_remove = false; 471 | l_event->m_source_address = l_source_address; 472 | l_event->m_source_port = l_source_port; 473 | l_event->m_destination_port = l_destination_port; 474 | l_event->m_destination_address = l_destination_address; 475 | l_event->m_protocol = l_protocol; 476 | 477 | bpf_ringbuf_submit(l_event, BPF_RB_FORCE_WAKEUP); 478 | 479 | return 0; 480 | } 481 | -------------------------------------------------------------------------------- /process_manager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "misc.hpp" 11 | #include "process_manager.hpp" 12 | 13 | process_manager::process_manager(std::shared_ptr p_log): 14 | m_docker_regex(".*/docker/(\\w+)\n"), 15 | m_log(p_log), 16 | m_thread(&process_manager::reaper_thread, this) 17 | {} 18 | 19 | process_manager::~process_manager() 20 | { 21 | m_stopper.stop(); 22 | 23 | m_thread.join(); 24 | } 25 | 26 | std::shared_ptr 27 | process_manager::load_process_info(const uint32_t p_process_id) 28 | { 29 | struct stat l_stat; 30 | const std::string l_stat_path = "/proc/" + std::to_string(p_process_id); 31 | 32 | if (stat(l_stat_path.c_str(), &l_stat) == -1) { 33 | m_log->error("stat() {}", strerror(errno)); 34 | 35 | return nullptr; 36 | } 37 | 38 | const std::string l_path = 39 | "/proc/" + 40 | std::to_string(p_process_id) + 41 | "/exe"; 42 | 43 | char l_readlink_buffer[1024 * 32]; 44 | 45 | const ssize_t l_readlink_status = readlink( 46 | l_path.c_str(), 47 | l_readlink_buffer, 48 | sizeof(l_readlink_buffer) - 1 49 | ); 50 | 51 | if (l_readlink_status == -1) { 52 | return nullptr; 53 | } 54 | 55 | l_readlink_buffer[l_readlink_status] = '\0'; 56 | 57 | const std::string l_path_cgroup = 58 | "/proc/" + 59 | std::to_string(p_process_id) + 60 | "/cgroup"; 61 | 62 | process_info_t l_process_info; 63 | 64 | l_process_info.m_process_id = p_process_id; 65 | l_process_info.m_executable = std::string(l_readlink_buffer); 66 | l_process_info.m_container_id = std::nullopt; 67 | l_process_info.m_user_id = l_stat.st_uid; 68 | l_process_info.m_group_id = l_stat.st_gid; 69 | 70 | try { 71 | const std::string l_cgroup = file_to_string(l_path_cgroup); 72 | 73 | l_process_info.m_start_time = load_process_start_time(p_process_id); 74 | 75 | std::smatch l_match; 76 | 77 | if (std::regex_search( 78 | l_cgroup.begin(), 79 | l_cgroup.end(), 80 | l_match, 81 | m_docker_regex) 82 | ){ 83 | l_process_info.m_container_id = 84 | std::optional(l_match[1]); 85 | } 86 | } catch (const std::exception &err) { 87 | return nullptr; 88 | } 89 | 90 | return std::make_shared(l_process_info); 91 | } 92 | 93 | uint64_t 94 | process_manager::load_process_start_time(const uint32_t p_process_id) 95 | { 96 | const std::string l_path = 97 | "/proc/" + 98 | std::to_string(p_process_id) + 99 | "/stat"; 100 | 101 | const std::string l_stat = file_to_string(l_path); 102 | 103 | std::vector l_segments; 104 | 105 | boost::split( 106 | l_segments, 107 | l_stat, 108 | boost::is_any_of(" "), 109 | boost::token_compress_on 110 | ); 111 | 112 | if (l_segments.size() < 22) { 113 | throw std::runtime_error("parse /proc failed"); 114 | } 115 | 116 | return boost::lexical_cast(l_segments[21]); 117 | } 118 | 119 | std::shared_ptr 120 | process_manager::lookup_process_info(const uint32_t p_process_id) 121 | { 122 | std::lock_guard l_guard(m_lock); 123 | 124 | const auto l_iter = m_process_cache.find(p_process_id); 125 | 126 | if (l_iter != m_process_cache.end()) { 127 | return l_iter->second; 128 | } 129 | 130 | const std::shared_ptr l_process = 131 | load_process_info(p_process_id); 132 | 133 | if (l_process == nullptr) { 134 | return nullptr; 135 | } 136 | 137 | m_process_cache[p_process_id] = l_process; 138 | 139 | if (m_add_process_cb.has_value()) { 140 | m_add_process_cb.value()(*l_process); 141 | } 142 | 143 | return l_process; 144 | } 145 | 146 | void 147 | process_manager::set_load_process_cb(add_process_cb_t p_cb) 148 | { 149 | m_add_process_cb = std::optional(p_cb); 150 | } 151 | 152 | void 153 | process_manager::set_remove_process_cb(remove_process_cb_t p_cb) 154 | { 155 | m_remove_process_cb = std::optional(p_cb); 156 | } 157 | 158 | void 159 | process_manager::reap_dead() 160 | { 161 | std::lock_guard l_guard(m_lock); 162 | 163 | std::erase_if(m_process_cache, 164 | [&](const auto &l_process) { 165 | try { 166 | if ( 167 | l_process.second->m_start_time == 168 | load_process_start_time(l_process.first) 169 | ) { 170 | return false; 171 | } 172 | } catch (...) {} 173 | 174 | m_log->info("filtering process {}", l_process.first); 175 | 176 | if (m_remove_process_cb.has_value()) { 177 | m_remove_process_cb.value()(l_process.first); 178 | } 179 | 180 | return true; 181 | } 182 | ); 183 | } 184 | 185 | void 186 | process_manager::reaper_thread() 187 | { 188 | while (!m_stopper.await_stop_for_milliseconds(1000)) { 189 | reap_dead(); 190 | } 191 | } 192 | 193 | nlohmann::json 194 | process_manager::processes_to_json() 195 | { 196 | nlohmann::json l_result = nlohmann::json::array(); 197 | 198 | std::lock_guard l_guard(m_lock); 199 | 200 | for (const auto &l_iter : m_process_cache) { 201 | l_result.push_back(l_iter.second->to_json()); 202 | } 203 | 204 | return l_result; 205 | } 206 | 207 | nlohmann::json 208 | process_info_t::to_json() const 209 | { 210 | nlohmann::json l_result = { 211 | { "processId" , m_process_id }, 212 | { "executable", m_executable }, 213 | { "userId", m_user_id }, 214 | { "groupId", m_group_id } 215 | }; 216 | 217 | if (m_container_id.has_value()) { 218 | l_result["containerId"] = m_container_id.value(); 219 | } 220 | 221 | return l_result; 222 | } 223 | -------------------------------------------------------------------------------- /process_manager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include "stopper.hpp" 16 | 17 | struct process_info_t { 18 | uint32_t m_process_id; 19 | std::string m_executable; 20 | std::optional m_container_id; 21 | uint64_t m_start_time; 22 | uint32_t m_user_id; 23 | uint32_t m_group_id; 24 | 25 | nlohmann::json to_json() const; 26 | }; 27 | 28 | class process_manager { 29 | public: 30 | typedef std::function add_process_cb_t; 31 | typedef std::function remove_process_cb_t; 32 | 33 | process_manager(std::shared_ptr p_log); 34 | 35 | ~process_manager(); 36 | 37 | std::shared_ptr 38 | lookup_process_info(const uint32_t p_process_id); 39 | 40 | void set_load_process_cb(add_process_cb_t p_cb); 41 | 42 | void set_remove_process_cb(remove_process_cb_t p_cb); 43 | 44 | nlohmann::json processes_to_json(); 45 | 46 | private: 47 | const std::regex m_docker_regex; 48 | 49 | std::unordered_map> 50 | m_process_cache; 51 | 52 | std::mutex m_lock; 53 | 54 | std::optional m_add_process_cb; 55 | std::optional m_remove_process_cb; 56 | 57 | std::shared_ptr 58 | load_process_info(const uint32_t p_process_id); 59 | 60 | uint64_t 61 | load_process_start_time(const uint32_t p_process_id); 62 | 63 | void 64 | reap_dead(); 65 | 66 | void 67 | reaper_thread(); 68 | 69 | std::shared_ptr m_log; 70 | stopper m_stopper; 71 | std::thread m_thread; 72 | }; 73 | -------------------------------------------------------------------------------- /rule_engine.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "rule_engine.hpp" 12 | 13 | typedef boost::bimaps::bimap< 14 | field_t, 15 | std::string, 16 | boost::container::allocator 17 | > g_field_map_type; 18 | 19 | const g_field_map_type g_field_map = 20 | boost::assign::list_of 21 | ( field_t::executable, "executable" ) 22 | ( field_t::destination_address, "destinationAddress" ) 23 | ( field_t::destination_port, "destinationPort" ) 24 | ( field_t::source_address, "sourceAddress" ) 25 | ( field_t::source_port, "sourcePort" ) 26 | ( field_t::container_id, "containerId" ) 27 | ( field_t::protocol, "protocol" ) 28 | ( field_t::user_id, "userId" ); 29 | 30 | field_t 31 | field_from_string(const std::string &p_field) 32 | { 33 | return g_field_map.right.find(p_field)->second; 34 | } 35 | 36 | std::string 37 | field_to_string(const field_t p_field) 38 | { 39 | return g_field_map.left.find(p_field)->second; 40 | } 41 | 42 | rule_engine_t::clause_t::clause_t(const nlohmann::json &p_json) 43 | { 44 | m_field = field_from_string(p_json["field"]); 45 | m_value = p_json["value"]; 46 | } 47 | 48 | rule_engine_t::rule_t::rule_t( 49 | const nlohmann::json &p_json, 50 | const std::string &p_rule_id 51 | ){ 52 | m_allow = p_json["allow"]; 53 | m_priority = p_json["priority"]; 54 | m_persistent = p_json["persistent"]; 55 | m_rule_id = p_rule_id; 56 | 57 | for (const auto &p_it : p_json["clauses"]) { 58 | m_clauses.push_back(clause_t(p_it)); 59 | } 60 | } 61 | 62 | rule_engine_t::rule_engine_t(const std::string &p_path): 63 | m_path(p_path) 64 | { 65 | try_load_rules(); 66 | }; 67 | 68 | rule_engine_t::~rule_engine_t(){}; 69 | 70 | std::string 71 | rule_engine_t::add_rule(const nlohmann::json &p_json) 72 | { 73 | const std::string l_uuid = boost::uuids::to_string( 74 | boost::uuids::random_generator()() 75 | ); 76 | 77 | std::unique_lock l_guard(m_lock); 78 | 79 | m_rules.push_back(rule_t(p_json, l_uuid)); 80 | 81 | std::sort(m_rules.begin(), m_rules.end(), 82 | [](const auto &p_left, const auto &p_right) { 83 | return p_left.m_priority < p_right.m_priority; 84 | } 85 | ); 86 | 87 | save_rules(); 88 | 89 | return l_uuid; 90 | } 91 | 92 | void 93 | rule_engine_t::delete_rule(const std::string &p_rule_id) noexcept 94 | { 95 | std::unique_lock l_guard(m_lock); 96 | 97 | m_rules.erase( 98 | std::remove_if(m_rules.begin(), m_rules.end(), [&](const auto &l_rule) { 99 | return l_rule.m_rule_id == p_rule_id; 100 | }), 101 | m_rules.end() 102 | ); 103 | 104 | save_rules(); 105 | } 106 | 107 | const std::optional 108 | rule_engine_t::get_verdict( 109 | const struct nfq_event_t &p_nfq_event, 110 | const struct process_info_t &p_info 111 | ) noexcept { 112 | std::shared_lock l_guard(m_lock); 113 | 114 | for (const auto &l_rule : m_rules) { 115 | bool l_match = true; 116 | 117 | for (const auto &l_clause : l_rule.m_clauses) { 118 | switch (l_clause.m_field) { 119 | case field_t::executable: { 120 | if (l_clause.m_value != p_info.m_executable) { 121 | l_match = false; 122 | } 123 | 124 | break; 125 | } 126 | case field_t::destination_address: { 127 | const std::string l_addr = p_nfq_event.m_v6 128 | ? ipv6_to_string(p_nfq_event.m_destination_address_v6) 129 | : ipv4_to_string(p_nfq_event.m_destination_address); 130 | 131 | if (l_clause.m_value != l_addr) { 132 | l_match = false; 133 | } 134 | 135 | break; 136 | } 137 | case field_t::destination_port: { 138 | if (l_clause.m_value != 139 | std::to_string(p_nfq_event.m_destination_port)) 140 | { 141 | l_match = false; 142 | } 143 | 144 | break; 145 | } 146 | case field_t::source_address: { 147 | const std::string l_addr = p_nfq_event.m_v6 148 | ? ipv6_to_string(p_nfq_event.m_source_address_v6) 149 | : ipv4_to_string(p_nfq_event.m_source_address); 150 | 151 | if (l_clause.m_value != l_addr) { 152 | l_match = false; 153 | } 154 | 155 | break; 156 | } 157 | case field_t::source_port: { 158 | if (l_clause.m_value != 159 | std::to_string(p_nfq_event.m_source_port)) 160 | { 161 | l_match = false; 162 | } 163 | 164 | break; 165 | } 166 | case field_t::container_id: { 167 | if (l_clause.m_value != p_info.m_container_id) { 168 | l_match = false; 169 | } 170 | 171 | break; 172 | } 173 | case field_t::protocol: { 174 | const std::string l_protocol = 175 | ip_protocol_to_string(p_nfq_event.m_protocol); 176 | 177 | if (l_clause.m_value != l_protocol) { 178 | l_match = false; 179 | } 180 | 181 | break; 182 | } 183 | case field_t::user_id: { 184 | if (l_clause.m_value != 185 | std::to_string(p_info.m_user_id)) 186 | { 187 | l_match = false; 188 | } 189 | 190 | break; 191 | } 192 | } 193 | 194 | if (l_match == false) { 195 | break; 196 | } 197 | } 198 | 199 | if (l_match) { 200 | return std::optional(l_rule.m_allow); 201 | } 202 | } 203 | 204 | return std::nullopt; 205 | } 206 | 207 | nlohmann::json 208 | rule_engine_t::clause_to_json(const clause_t &p_clause) 209 | { 210 | return { 211 | { "field", field_to_string(p_clause.m_field) }, 212 | { "value", p_clause.m_value } 213 | }; 214 | } 215 | 216 | nlohmann::json 217 | rule_engine_t::rule_to_json(const rule_t &p_rule) 218 | { 219 | std::vector l_clauses; 220 | 221 | for (const auto &l_clause : p_rule.m_clauses) { 222 | l_clauses.push_back(clause_to_json(l_clause)); 223 | } 224 | 225 | return { 226 | { "ruleId", p_rule.m_rule_id }, 227 | { "allow", p_rule.m_allow }, 228 | { "clauses", l_clauses }, 229 | { "priority", p_rule.m_priority }, 230 | { "persistent", p_rule.m_persistent } 231 | }; 232 | } 233 | 234 | const nlohmann::json 235 | rule_engine_t::rules_to_json(const bool p_filter_temporary) 236 | { 237 | nlohmann::json l_result = nlohmann::json::array(); 238 | 239 | for (const auto &l_rule : m_rules) { 240 | if (p_filter_temporary && !l_rule.m_persistent) { 241 | continue; 242 | } 243 | 244 | l_result.push_back(rule_to_json(l_rule)); 245 | } 246 | 247 | return l_result; 248 | } 249 | 250 | void 251 | rule_engine_t::save_rules() 252 | { 253 | atomically_write_file( 254 | m_path, 255 | rules_to_json(true).dump(4) 256 | ); 257 | } 258 | 259 | void 260 | rule_engine_t::try_load_rules() 261 | { 262 | std::ifstream l_stream(m_path); 263 | 264 | if (l_stream.is_open() == false) { 265 | return; 266 | } 267 | 268 | const nlohmann::json l_rules_json = nlohmann::json::parse( 269 | std::string( 270 | (std::istreambuf_iterator(l_stream)), 271 | std::istreambuf_iterator() 272 | ) 273 | ); 274 | 275 | for (const auto &p_it : l_rules_json) { 276 | add_rule(p_it); 277 | } 278 | } -------------------------------------------------------------------------------- /rule_engine.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "misc.hpp" 9 | #include "process_manager.hpp" 10 | #include "nfq_event.h" 11 | 12 | enum class field_t { 13 | executable, 14 | destination_address, 15 | destination_port, 16 | source_address, 17 | source_port, 18 | container_id, 19 | protocol, 20 | user_id 21 | }; 22 | 23 | field_t field_from_string(const std::string &p_field); 24 | 25 | std::string field_to_string(const field_t p_field); 26 | 27 | class rule_engine_t { 28 | public: 29 | rule_engine_t(const std::string &p_path); 30 | 31 | ~rule_engine_t(); 32 | 33 | std::string add_rule(const nlohmann::json &p_json); 34 | 35 | const std::optional 36 | get_verdict( 37 | const struct nfq_event_t &p_nfq_event, 38 | const struct process_info_t &p_info 39 | ) noexcept; 40 | 41 | void delete_rule(const std::string &p_rule_id) noexcept; 42 | 43 | const nlohmann::json rules_to_json(const bool p_filter_temporary=false); 44 | 45 | private: 46 | struct clause_t { 47 | clause_t(const nlohmann::json &p_json); 48 | 49 | field_t m_field; 50 | std::string m_value; 51 | }; 52 | 53 | struct rule_t { 54 | rule_t(const nlohmann::json &p_json, const std::string &p_rule_id); 55 | 56 | std::vector m_clauses; 57 | std::string m_rule_id; 58 | bool m_allow; 59 | uint32_t m_priority; 60 | bool m_persistent; 61 | }; 62 | 63 | static nlohmann::json clause_to_json(const clause_t &p_clause); 64 | static nlohmann::json rule_to_json(const rule_t &p_rule); 65 | 66 | const std::string m_path; 67 | 68 | std::shared_mutex m_lock; 69 | 70 | std::vector m_rules; 71 | 72 | void save_rules(); 73 | void try_load_rules(); 74 | }; -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harporoeder/ebpfsnitch/1220a38af76864b2ea50afea501f4da14debf212/screenshot.png -------------------------------------------------------------------------------- /stopper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "stopper.hpp" 4 | 5 | stopper::stopper(): m_stop_state(false) 6 | { 7 | if (pipe(m_pipe_fd) == -1) { 8 | throw std::runtime_error("pipe() failed"); 9 | } 10 | } 11 | 12 | stopper::~stopper() 13 | { 14 | stop(); 15 | 16 | close(m_pipe_fd[0]); 17 | } 18 | 19 | void 20 | stopper::stop() 21 | { 22 | { 23 | std::unique_lock l_guard(m_lock); 24 | 25 | if (m_stop_state == false) { 26 | m_stop_state = true; 27 | 28 | close(m_pipe_fd[1]); 29 | } 30 | } 31 | 32 | m_condition.notify_all(); 33 | } 34 | 35 | void 36 | stopper::await_stop_block() 37 | { 38 | std::unique_lock l_guard(m_lock); 39 | 40 | if (m_stop_state == true) { 41 | return; 42 | } 43 | 44 | m_condition.wait(l_guard); 45 | } 46 | 47 | bool 48 | stopper::should_stop() 49 | { 50 | std::lock_guard l_guard(m_lock); 51 | 52 | return m_stop_state; 53 | } 54 | 55 | bool 56 | stopper::await_stop_for_milliseconds(const unsigned int m_timeout) 57 | { 58 | std::unique_lock l_guard(m_lock); 59 | 60 | if (m_stop_state == true) { 61 | return true; 62 | } 63 | 64 | m_condition.wait_for(l_guard, std::chrono::milliseconds(m_timeout)); 65 | 66 | return m_stop_state; 67 | } 68 | 69 | int 70 | stopper::get_stop_fd() 71 | { 72 | return m_pipe_fd[0]; 73 | } -------------------------------------------------------------------------------- /stopper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class stopper { 7 | public: 8 | stopper(); 9 | 10 | ~stopper(); 11 | 12 | void stop(); 13 | 14 | void await_stop_block(); 15 | 16 | bool should_stop(); 17 | 18 | bool await_stop_for_milliseconds(const unsigned int m_timeout); 19 | 20 | int get_stop_fd(); 21 | 22 | private: 23 | std::mutex m_lock; 24 | std::condition_variable m_condition; 25 | bool m_stop_state; 26 | int m_pipe_fd[2]; 27 | }; 28 | -------------------------------------------------------------------------------- /tests/lru_map_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "lru_map.hpp" 4 | 5 | static void 6 | test_lookup_does_not_exist() 7 | { 8 | const lru_map map(1); 9 | 10 | assert(map.lookup(52).has_value() == false); 11 | } 12 | 13 | static void 14 | test_insert_and_get() 15 | { 16 | lru_map map(1); 17 | 18 | map.insert(5, 10); 19 | 20 | assert(map.lookup(5) == std::optional(10)); 21 | } 22 | 23 | static void 24 | test_eviction() 25 | { 26 | lru_map map(2); 27 | 28 | map.insert(1, 2); 29 | map.insert(3, 4); 30 | map.insert(5, 6); 31 | 32 | assert(map.lookup(1).has_value() == false); 33 | assert(map.lookup(3) == std::optional(4)); 34 | assert(map.lookup(5) == std::optional(6)); 35 | } 36 | 37 | static void 38 | test_insert_updates_existing() 39 | { 40 | lru_map map(2); 41 | 42 | map.insert(1, 2); 43 | map.insert(3, 4); 44 | map.insert(1, 5); 45 | map.insert(6, 7); 46 | 47 | assert(map.lookup(3).has_value() == false); 48 | assert(map.lookup(1) == std::optional(5)); 49 | assert(map.lookup(6) == std::optional(7)); 50 | } 51 | 52 | int 53 | main() 54 | { 55 | test_lookup_does_not_exist(); 56 | test_insert_and_get(); 57 | test_eviction(); 58 | test_insert_updates_existing(); 59 | 60 | return 0; 61 | } 62 | -------------------------------------------------------------------------------- /tests/stopper_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "stopper.hpp" 12 | 13 | static void 14 | test_stopper_await_stop_block() 15 | { 16 | stopper l_stopper; 17 | 18 | std::thread l_thread([&](){ 19 | // technically a race but hard to work around 20 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 21 | 22 | l_stopper.stop(); 23 | }); 24 | 25 | l_stopper.await_stop_block(); 26 | 27 | l_thread.join(); 28 | } 29 | 30 | static void 31 | test_stopper_stop_fd() 32 | { 33 | stopper l_stopper; 34 | 35 | std::thread l_thread([&](){ 36 | // technically a race but hard to work around 37 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 38 | 39 | l_stopper.stop(); 40 | }); 41 | 42 | fd_set l_fd_set; 43 | 44 | FD_ZERO(&l_fd_set); 45 | 46 | FD_SET(l_stopper.get_stop_fd(), &l_fd_set); 47 | assert(select(l_stopper.get_stop_fd() + 1, &l_fd_set, NULL, &l_fd_set, NULL) == 1); 48 | 49 | l_thread.join(); 50 | } 51 | 52 | int 53 | main() 54 | { 55 | test_stopper_await_stop_block(); 56 | test_stopper_stop_fd(); 57 | 58 | return 0; 59 | } 60 | -------------------------------------------------------------------------------- /ui/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include ebpfsnitch/ebpfsnitch.png -------------------------------------------------------------------------------- /ui/bin/ebpfsnitch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from ebpfsnitch import entry -------------------------------------------------------------------------------- /ui/ebpfsnitch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harporoeder/ebpfsnitch/1220a38af76864b2ea50afea501f4da14debf212/ui/ebpfsnitch/__init__.py -------------------------------------------------------------------------------- /ui/ebpfsnitch/ebpfsnitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harporoeder/ebpfsnitch/1220a38af76864b2ea50afea501f4da14debf212/ui/ebpfsnitch/ebpfsnitch.png -------------------------------------------------------------------------------- /ui/ebpfsnitch/entry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import sys 5 | import select 6 | import threading 7 | import json 8 | import queue 9 | import time 10 | import os 11 | 12 | from PyQt5 import QtCore 13 | from PyQt5.QtGui import * 14 | from PyQt5.QtWidgets import * 15 | from PyQt5.QtCore import Qt 16 | 17 | class PromptDialog(QDialog): 18 | def __init__(self, question, parent=None): 19 | super().__init__(parent=parent) 20 | 21 | self.setWindowTitle("eBPFSnitch Dialog") 22 | 23 | allowButton = QPushButton("Allow") 24 | denyButton = QPushButton("Deny") 25 | 26 | self.forAllDestinationAddresses = QCheckBox("All Destination Addresses") 27 | self.forAllDestinationPorts = QCheckBox("All Destination Ports") 28 | self.forAllSourceAddresses = QCheckBox("All Source Addresses") 29 | self.forAllSourcePorts = QCheckBox("All Source Ports") 30 | self.forAllProtocols = QCheckBox("All Protocols") 31 | self.forAllUIDs = QCheckBox("All UIDs") 32 | self.persistent = QCheckBox("Persistent") 33 | self.priority = QSpinBox() 34 | 35 | self.forAllSourcePorts.setChecked(True) 36 | 37 | self.priority.setRange(0, 2147483647) 38 | self.priority.setValue(50) 39 | self.priority.setSingleStep(1) 40 | priorityLayout = QHBoxLayout() 41 | priorityLayout.addWidget(QLabel("Priority:")) 42 | priorityLayout.addWidget(self.priority) 43 | 44 | allowButton.clicked.connect(self.accept) 45 | denyButton.clicked.connect(self.reject) 46 | allowButton.setAutoDefault(False) 47 | denyButton.setAutoDefault(False) 48 | buttonLayout = QHBoxLayout() 49 | buttonLayout.addWidget(allowButton) 50 | buttonLayout.addWidget(denyButton) 51 | 52 | source = question["sourceAddress"] + ":" + str(question["sourcePort"]) 53 | 54 | destination = \ 55 | question["destinationAddress"] + ":" + \ 56 | str(question["destinationPort"]) 57 | 58 | if "domain" in question: 59 | destination += " (" + question["domain"] + ")" 60 | 61 | self.layout = QVBoxLayout() 62 | self.layout.addWidget(QLabel("Application: " + question["executable"])) 63 | self.layout.addWidget(QLabel("Protocol: " + str(question["protocol"]))) 64 | self.layout.addWidget(QLabel("Source: " + source)) 65 | self.layout.addWidget(QLabel("Destination: " + destination)) 66 | 67 | if "container" in question: 68 | self.layout.addWidget(QLabel("Container: " + str(question["container"]))) 69 | 70 | self.layout.addWidget(QLabel("UID: " + str(question["userId"]))) 71 | self.layout.addWidget(self.forAllDestinationAddresses) 72 | self.layout.addWidget(self.forAllDestinationPorts) 73 | self.layout.addWidget(self.forAllSourceAddresses) 74 | self.layout.addWidget(self.forAllSourcePorts) 75 | self.layout.addWidget(self.forAllProtocols) 76 | self.layout.addWidget(self.forAllUIDs) 77 | self.layout.addWidget(self.persistent) 78 | self.layout.addLayout(priorityLayout) 79 | self.layout.addLayout(buttonLayout) 80 | self.setLayout(self.layout) 81 | 82 | class MainWindow(QMainWindow): 83 | _clear_state_trigger = QtCore.pyqtSignal() 84 | _show_state_trigger = QtCore.pyqtSignal() 85 | _new_event_trigger = QtCore.pyqtSignal() 86 | 87 | def __init__(self): 88 | super().__init__() 89 | 90 | self.setWindowTitle("eBPFSnitch") 91 | self.resize(920, 600) 92 | 93 | rulesScroll = self.__make_scroll() 94 | self._rules = rulesScroll.widget().layout() 95 | 96 | processesScroll = self.__make_scroll() 97 | self._processes = processesScroll.widget().layout() 98 | 99 | body_widget = QTableWidget() 100 | body_widget.setEditTriggers(QTableWidget.NoEditTriggers) 101 | body_widget.setSelectionMode(QAbstractItemView.NoSelection) 102 | body_widget.setColumnCount(4) 103 | body_widget.setRowCount(0) 104 | body_widget.resizeRowsToContents() 105 | body_widget.verticalScrollBar().setDisabled(True); 106 | body_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 107 | body_widget.setHorizontalHeaderLabels(["Executable", "PID", "UID", "GID"]) 108 | body_widget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 109 | processesScroll.widget().layout().addWidget(body_widget) 110 | self._processes = body_widget 111 | 112 | self.tabs = QTabWidget() 113 | self.tabs.addTab(rulesScroll, "Firewall Rules") 114 | self.tabs.addTab(processesScroll, "Processes") 115 | 116 | disconnectedLabel = QLabel("Attempting to connect to daemon") 117 | disconnectedLabel.setAlignment(Qt.AlignCenter) 118 | 119 | self.stack = QStackedWidget(self) 120 | self.stack.addWidget(disconnectedLabel) 121 | self.stack.addWidget(self.tabs) 122 | 123 | self.setCentralWidget(self.stack) 124 | 125 | self._done = threading.Event() 126 | self._allow = False 127 | 128 | self._clear_state_trigger.connect(self.on_clear_state_trigger) 129 | self._show_state_trigger.connect(self.on_show_state_trigger) 130 | self._new_event_trigger.connect(self.on_new_event_trigger) 131 | 132 | def __make_scroll(self): 133 | scroll = QScrollArea(self) 134 | scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 135 | scroll.setWidgetResizable(True) 136 | inner = QFrame(scroll) 137 | layout = QVBoxLayout(scroll) 138 | layout.setAlignment(Qt.AlignTop) 139 | inner.setLayout(layout) 140 | scroll.setWidget(inner) 141 | return scroll 142 | 143 | def set_daemon_client(self, client): 144 | self._client = client 145 | 146 | def on_delete_rule_trigger(self, ruleId, widget): 147 | print("clicked rule delete: " + ruleId); 148 | 149 | command = { 150 | "kind": "removeRule", 151 | "ruleId": ruleId 152 | } 153 | 154 | self._client.send_dict(command) 155 | 156 | widget.deleteLater() 157 | 158 | def __add_process(self, event): 159 | self._processes.insertRow(self._processes.rowCount()) 160 | self._processes.setItem(self._processes.rowCount() - 1, 0, QTableWidgetItem(event["executable"])) 161 | self._processes.setItem(self._processes.rowCount() - 1, 1, QTableWidgetItem(str(event["processId"]))) 162 | self._processes.setItem(self._processes.rowCount() - 1, 2, QTableWidgetItem(str(event["userId"]))) 163 | self._processes.setItem(self._processes.rowCount() - 1, 3, QTableWidgetItem(str(event["groupId"]))) 164 | 165 | def __add_rule(self, event): 166 | ruleId = event["ruleId"] 167 | delete_button = QPushButton("Remove Rule") 168 | 169 | header = QHBoxLayout() 170 | header.addWidget(QLabel("Rule UUID: " + event["ruleId"])) 171 | header.addWidget(QLabel("Allow: " + str(event["allow"]))) 172 | header.addWidget(QLabel("Persistent: " + str(event["persistent"]))) 173 | header.addWidget(QLabel("Priority: " + str(event["priority"]))) 174 | header.addWidget(delete_button) 175 | header_widget = QWidget() 176 | header_widget.setLayout(header) 177 | 178 | body_widget = QTableWidget() 179 | body_widget.setEditTriggers(QTableWidget.NoEditTriggers) 180 | body_widget.setSelectionMode(QAbstractItemView.NoSelection) 181 | body_widget.setColumnCount(2) 182 | body_widget.setRowCount(0) 183 | body_widget.resizeRowsToContents() 184 | body_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 185 | body_widget.verticalScrollBar().setDisabled(True); 186 | body_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 187 | body_widget.setHorizontalHeaderLabels(["Selector", "Matches"]) 188 | body_widget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) 189 | 190 | for clause in event["clauses"]: 191 | body_widget.insertRow(body_widget.rowCount()) 192 | body_widget.setItem(body_widget.rowCount() - 1, 0, QTableWidgetItem(clause["field"])) 193 | body_widget.setItem(body_widget.rowCount() - 1, 1, QTableWidgetItem(clause["value"])) 194 | 195 | body_widget.setMaximumHeight(body_widget.rowHeight(0) * (body_widget.rowCount()) + body_widget.horizontalHeader().height()) 196 | 197 | container = QVBoxLayout() 198 | container.setAlignment(Qt.AlignTop) 199 | container.addWidget(header_widget) 200 | container.addWidget(body_widget) 201 | 202 | item = QWidget() 203 | item.setLayout(container) 204 | item.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 205 | 206 | delete_button.clicked.connect(lambda: self.on_delete_rule_trigger(ruleId, item)) 207 | 208 | self._rules.addWidget(item) 209 | 210 | @QtCore.pyqtSlot() 211 | def on_new_event_trigger(self): 212 | event = self._new_event 213 | self._event_result = None 214 | 215 | if event["kind"] == "addProcess": 216 | self.__add_process(event) 217 | 218 | elif event["kind"] == "removeProcess": 219 | process = str(event["processId"]) 220 | for row in range(self._processes.rowCount()): 221 | if self._processes.item(row, 1).text() == process: 222 | self._processes.removeRow(row) 223 | break 224 | 225 | elif event["kind"] == "setProcesses": 226 | for process in event["processes"]: 227 | self.__add_process(process) 228 | 229 | elif event["kind"] == "addRule": 230 | self.__add_rule(event["body"]) 231 | 232 | elif event["kind"] == "setRules": 233 | for rule in event["rules"]: 234 | self.__add_rule(rule) 235 | self.on_show_state_trigger() 236 | 237 | elif event["kind"] == "query": 238 | parsed = event 239 | 240 | dlg = PromptDialog(event) 241 | 242 | command = { 243 | "kind": "addRule", 244 | "allow": bool(dlg.exec_()), 245 | "priority": dlg.priority.value(), 246 | "persistent": dlg.persistent.isChecked(), 247 | "clauses": [ 248 | { 249 | "field": "executable", 250 | "value": event["executable"] 251 | } 252 | ] 253 | } 254 | 255 | if dlg.forAllDestinationAddresses.isChecked() == False: 256 | command["clauses"].append( 257 | { 258 | "field": "destinationAddress", 259 | "value": parsed["destinationAddress"] 260 | } 261 | ) 262 | 263 | if dlg.forAllDestinationPorts.isChecked() == False: 264 | command["clauses"].append( 265 | { 266 | "field": "destinationPort", 267 | "value": str(parsed["destinationPort"]) 268 | } 269 | ) 270 | 271 | if dlg.forAllSourceAddresses.isChecked() == False: 272 | command["clauses"].append( 273 | { 274 | "field": "sourceAddress", 275 | "value": parsed["sourceAddress"] 276 | } 277 | ) 278 | 279 | if dlg.forAllSourcePorts.isChecked() == False: 280 | command["clauses"].append( 281 | { 282 | "field": "sourcePort", 283 | "value": str(parsed["sourcePort"]) 284 | } 285 | ) 286 | 287 | if dlg.forAllProtocols.isChecked() == False: 288 | command["clauses"].append( 289 | { 290 | "field": "protocol", 291 | "value": parsed["protocol"] 292 | } 293 | ) 294 | 295 | if dlg.forAllUIDs.isChecked() == False: 296 | command["clauses"].append( 297 | { 298 | "field": "userId", 299 | "value": str(parsed["userId"]) 300 | } 301 | ) 302 | 303 | self._event_result = command 304 | 305 | self._done.set() 306 | 307 | @QtCore.pyqtSlot() 308 | def on_clear_state_trigger(self): 309 | self.stack.setCurrentIndex(0) 310 | 311 | for i in reversed(range(self._rules.count())): 312 | self._rules.itemAt(i).widget().deleteLater() 313 | 314 | self._processes.setRowCount(0) 315 | 316 | self._done.set() 317 | 318 | @QtCore.pyqtSlot() 319 | def on_show_state_trigger(self): 320 | self.stack.setCurrentIndex(1) 321 | 322 | def handle_show_state(self): 323 | self._show_state_trigger.emit() 324 | 325 | def handle_clear_state(self): 326 | self._done.clear() 327 | self._clear_state_trigger.emit() 328 | self._done.wait() 329 | 330 | def handle_event(self, process): 331 | self._done.clear() 332 | self._new_event = process 333 | self._new_event_trigger.emit() 334 | self._done.wait() 335 | return self._event_result 336 | 337 | class DaemonClient: 338 | def __init__(self, address, window): 339 | self._address = address 340 | self._stopper = threading.Event() 341 | self._outbox = queue.Queue() 342 | self._window = window 343 | self._thread = threading.Thread(target=self.__run_supervisor) 344 | 345 | def start(self): 346 | self._thread.start() 347 | 348 | def __run_supervisor(self): 349 | while self._stopper.is_set() == False: 350 | try: 351 | self.__run() 352 | except Exception as err: 353 | print(repr(err)) 354 | self._window.handle_clear_state() 355 | self._stopper.wait(1) 356 | 357 | def __run(self): 358 | self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 359 | self.sock.connect(self._address) 360 | 361 | self.read_buffer = "" 362 | 363 | while self._stopper.is_set() == False: 364 | read_ready, _, _ = select.select([self.sock], [], [], 0.1) 365 | 366 | self.__handle_write() 367 | 368 | if read_ready: 369 | self.__handle_read() 370 | 371 | def __handle_read(self): 372 | msg = self.sock.recv(1024) 373 | 374 | if not msg: 375 | self.sock.close() 376 | raise 377 | 378 | self.read_buffer += msg.decode("utf-8") 379 | 380 | while True: 381 | lineEnd = self.read_buffer.find("\n") 382 | if lineEnd == -1: 383 | break 384 | line = self.read_buffer[:lineEnd] 385 | self.read_buffer = self.read_buffer[lineEnd+1:] 386 | self.__handle_line(line) 387 | 388 | def __handle_write(self): 389 | while self._outbox.qsize() > 0: 390 | item = self._outbox.get() 391 | self.sock.sendall(item) 392 | self._outbox.task_done() 393 | 394 | def __handle_line(self, line): 395 | parsed = json.loads(line) 396 | command = self._window.handle_event(parsed) 397 | if command != None: 398 | self.send_dict(command) 399 | 400 | def stop(self): 401 | self._stopper.set() 402 | self._thread.join() 403 | 404 | def send_dict(self, message): 405 | self._outbox.put(str.encode(json.dumps(message) + "\n")) 406 | 407 | def main(): 408 | app = QApplication(sys.argv) 409 | app.setQuitOnLastWindowClosed(False) 410 | 411 | window = MainWindow() 412 | window.show() 413 | 414 | icon = QIcon(os.path.dirname(os.path.abspath(__file__)) + "/ebpfsnitch.png") 415 | tray = QSystemTrayIcon() 416 | tray.setIcon(icon) 417 | tray.setVisible(True) 418 | 419 | menu = QMenu() 420 | showMenuAction = QAction("show") 421 | showMenuAction.triggered.connect(window.show) 422 | menu.addAction(showMenuAction) 423 | 424 | hideMenuAction = QAction("hide") 425 | hideMenuAction.triggered.connect(window.hide) 426 | menu.addAction(hideMenuAction) 427 | 428 | quitMenuAction = QAction("Quit") 429 | quitMenuAction.triggered.connect(app.quit) 430 | menu.addAction(quitMenuAction) 431 | 432 | tray.setContextMenu(menu) 433 | 434 | daemonClient = DaemonClient("/tmp/ebpfsnitch.sock", window) 435 | window.set_daemon_client(daemonClient) 436 | 437 | daemonClient.start() 438 | app.exec_() 439 | daemonClient.stop() 440 | 441 | main() -------------------------------------------------------------------------------- /ui/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='ebpfsnitch', 5 | version='0.2.0', 6 | description='UI for eBPFSnitch', 7 | author='Harpo Roeder', 8 | author_email='roederharpo@protonmail.ch', 9 | packages=find_packages(), 10 | include_package_data=True, 11 | install_requires=['PyQt5'], 12 | scripts=['bin/ebpfsnitch'] 13 | ) 14 | --------------------------------------------------------------------------------