#define PY_SSIZE_T_CLEAN #include #include "pyproxy/settings.cpp" #include "pyproxy/pyproxy.cpp" #include "classes/netfilter.cpp" #include #include #include #include #include "utils.cpp" using namespace std; using namespace Firegex::PyProxy; using Firegex::NfQueue::MultiThreadQueue; /* How python code is handles: User code example: ```python from firegex.pyfilters import DROP, ACCEPT, pyfilter @pyfilter def invalid_curl_agent(http): if "curl" in http.headers.get("User-Agent", ""): return DROP return ACCEPT ``` The code is now edited adding an intestation and a end statement: ```python __firegex_pyfilter_enabled = ["invalid_curl_agent", "func3"] # This list is dynamically generated by firegex backend import firegex.pyfilters.internals firegex.pyfilters.internals.compile(globals(), locals()) # This function can save other global variables, to use by the packet handler and is used generally to check and optimize the code ```` (First lines are the same to keep line of code consistent on exceptions messages) This code will be executed only once, and is needed to build the global and local context to use The globals and locals generated here are copied for each connection, and are used to handle the packets Using C API will be injected in global context the following informations: __firegex_packet_info = { "data" = b"the application payload", "is_input" = True, # If the packet is incoming from a client "is_ipv6" = False, # If the packet is ipv6 "is_tcp" = True, # If the packet is tcp "src_ip" = "10.0.0.7", "dst_ip" = "10.0.0.1", "src_port" = 51234, "dst_port" = 80, } Nothing below the application layer crosses this boundary as bytes: the addresses and ports are metadata a filter reads, and nothing comes back but a verdict. The proxy datapath terminates the connection and writes its own headers, so a filter that could rewrite one here and not there would mean two different things depending on which network layer it happened to be attached to. The protocol is not passed in either: the library reads it off the filters' parameter annotations, so a file that asks for an HttpRequest is an HTTP filter and one that asks only for a RawPacket runs on anything. As result the packet handler is responsible to return a dictionary in the global context with the following dictionary: __firegex_pyfilter_result = { "action": REJECT, # One of PyFilterResponse "matched_by": "invalid_curl_agent", # The function that matched the packet (used if action = DROP or REJECT) } PyFilterResponse { ACCEPT = 0, DROP = 1, REJECT = 2, // 3 was MANGLE, a payload rewrite; retired, and refused as INVALID EXCEPTION = 4, INVALID = 5 }; Every time a packet is received, the packet handler will execute the following code: ```python firegex.pyfilters.internals.handle_packet(globals()) ```` The TCP stream is sorted by libtins using c++ code, but the c++ code is not responsabile di buffer the stream, but only to sort those So firegex handle_packet has to implement a way to limit memory usage, this dipends on what methods you choose to use to filter packets firegex lib will give you all the needed possibilities to do this is many ways Final note: is not raccomanded to use variables that starts with __firegex_ in your code, because they may break the pyfilter */ /* Upper bound for a filter code blob. Without it a desynchronised or corrupt length prefix makes us try to allocate an arbitrary amount of memory. */ const uint32_t MAX_PYFILTER_CODE_SIZE = 64 * 1024 * 1024; void config_updater (){ while (true){ PyThreadState* state = PyEval_SaveThread(); // Release GIL while doing IO operation uint32_t code_size; string code; try{ string raw_code_size = control_socket.recv(4); memcpy(&code_size, raw_code_size.c_str(), 4); code_size = be32toh(code_size); if (code_size > MAX_PYFILTER_CODE_SIZE){ throw runtime_error("Announced filter code size is out of range"); } code = control_socket.recv(code_size); }catch(const std::exception& e){ // The control socket is the only link with the backend: if it is // gone there is nothing left to filter for, so exit cleanly instead // of letting the exception terminate the process. PyEval_AcquireThread(state); cerr << "[fatal] [updater] Control socket error: " << e.what() << endl; exit(EXIT_FAILURE); } cerr << "[info] [updater] Updating configuration" << endl; PyEval_AcquireThread(state); //Restore GIL before executing python code try{ config.store(make_shared(code)); cerr << "[info] [updater] Config update done" << endl; control_socket << "ACK OK" << endl; }catch(const std::exception& e){ cerr << "[error] [updater] Failed to build new configuration!" << endl; control_socket << "ACK FAIL " << e.what() << endl; }catch(...){ cerr << "[error] [updater] Failed to build new configuration!" << endl; control_socket << "ACK FAIL unknown error" << endl; } } } int main(int argc, char *argv[]) { // Connect to the python backend using the unix socket init_control_socket(); // Initialize the python interpreter Py_Initialize(); atexit(Py_Finalize); init_handle_packet_code(); //Compile the static code used to handle packets int n_of_threads = 1; char * n_threads_str = getenv("NTHREADS"); if (n_threads_str != nullptr) n_of_threads = ::atoi(n_threads_str); if(n_of_threads <= 0) n_of_threads = 1; config.store(make_shared()); MultiThreadQueue queue(n_of_threads); control_socket << "QUEUE " << queue.queue_num() << endl; cerr << "[info] [main] Queue: " << queue.queue_num() << " threads assigned: " << n_of_threads << endl; thread qthr([&](){ /* An exception escaping a std::thread is std::terminate(): exit explicitly instead, so the backend sees a failed process it can react to rather than an abort. */ try{ queue.start(); }catch(const std::exception& e){ cerr << "[fatal] [main] Queue loop stopped: " << e.what() << endl; exit(EXIT_FAILURE); }catch(...){ cerr << "[fatal] [main] Queue loop stopped with an unknown error" << endl; exit(EXIT_FAILURE); } }); config_updater(); qthr.join(); }