import { CodeHighlight } from "@mantine/code-highlight"; import { Container, Title, Text, List, Code, Space, Badge, Box, } from "@mantine/core"; import { CgEditBlackPoint } from "react-icons/cg"; import { EXAMPLE_PYFILTER } from "./utils"; const IMPORT_CODE_EXAMPLE = `from firegex.nfproxy import pyfilter, ACCEPT, REJECT`; const FOO_FILTER_CODE = `from firegex.nfproxy import pyfilter, ACCEPT, REJECT # This is NOT a filter def useless_function() -> int: print("This is a useless function") return 42 @pyfilter def none_filter(): # This is a filter that does nothing useless_function() return ACCEPT `; const TYPING_ARGS_EXAMPLE = `from firegex.nfproxy import pyfilter, ACCEPT, REJECT from firegex.nfproxy.models import HttpRequest @pyfilter def filter_with_args(http_request: HttpRequest) -> int: if http_request.body: if b"ILLEGAL" in http_request.body: return REJECT `; const IMPORT_FULL_ACTION_STREAM = `from firegex.nfproxy import FullStreamAction # Here the definition of FullStreamAction enum class FullStreamAction(Enum): """Action to be taken by the filter when the stream is full""" FLUSH = 0 ACCEPT = 1 REJECT = 2 DROP = 3 `; const ENUM_IMPORT_AND_DEFINITION = `from firegex.nfproxy import ExceptionAction # Here the definition of ExceptionAction enum class ExceptionAction(Enum): """Action to be taken by the filter when an exception occurs (used in some cases)""" ACCEPT = 0 # Accept the packet that caused the exception DROP = 1 # Drop the connection that caused the exception REJECT = 2 # Reject the connection that caused the exception NOACTION = 3 # Do nothing, the excpetion will be signaled and the stream will be accepted without calling anymore the pyfilters (for the current stream) `; export const HELP_NFPROXY_SIM = `➤ fgex nfproxy -h Usage: fgex nfproxy [OPTIONS] FILTER_FILE ADDRESS PORT Run an nfproxy simulation ╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ * filter_file TEXT The path to the filter file [default: None] [required] │ │ * address TEXT The address of the target to proxy [default: None] [required] │ │ * port INTEGER The port of the target to proxy [default: None] [required] │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ --proto [tcp|http] The protocol to proxy [default: tcp] │ │ --from-address TEXT The address of the local server [default: None] │ │ --from-port INTEGER The port of the local server [default: 7474] │ │ -6 Use IPv6 for the connection │ │ --help -h Show this message and exit. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯`; const HttpBadge = () => { return ( HTTP ); }; const TCPBadge = () => { return ( TCP ); }; export const NFProxyDocs = () => { return ( <> 🌐 Netfilter Proxy Documentation 📖 Overview Netfilter Proxy is a simulated proxy that leverages{" "} nfqueue {" "} to intercept network packets. It follows a similar workflow to NFRegex but introduces Python-based filtering capabilities, providing users with the flexibility to upload custom filters. ⚙️ How to use Netfilter Proxy To use Netfilter Proxy, simply create and upload a Python filter. The filter is passed to the C++ binary, which then processes packets using the provided logic. This allows you to tailor the filtering behavior to your needs. 💡 How to write pyfilters? First of all install the firegex lib and update it running{" "} pip install -U fgex. After that you can use{" "} firegex module. With this code we imported the pyfilter decorator and the ACCEPT and REJECT statements.
Let's create a first (useless) filter to see the syntax: You see that the filter must be decorated with the{" "} pyfilter decorator and must return a statement about how to manage that packet.
You can save every data about the current flow in the global variables, the code you write will be executed only once for flow. The globals variables are isolated between flows. For each packet the filter functions will be called with the required paramethers and using the same globals as before.
Saving data in globals of other modules is not recommended, because that memory is shared by the flows managed by the same thread and lead to unexpected behaviors.
Global variables that starts with '__firegex' are reserved for internal use, don't use them.
You can manage when the function is called and also getting some data specifying some paramethers, using type decorators. Default values of the paramethers will be ignored, also kvargs values will be ignored.
Functions with no type decorator are considered invalid pyfilters!
In this code we are filtering all the http requests that contains the word "ILLEGAL" in the body. All the other packets will be accepted (default behavior). The function will be called only if at least internally teh HTTP request header has been parsed, and also when the body will be parsed.
If we have multiple paramether, the function will be called only if with the packet arrived is possible to build all the paramethers.
🔧 How can I test the filter? You can test your filter by using fgex command installed by firegex lib: This will run a local proxy to a remote destination with the filter you specified.
This can be done by running for instance:{" "} fgex nfproxy test_http.py 127.0.0.1 8080 --proto http You don't need to restart the proxy every time you change the filter, the filter will be reloaded automatically.
📦 Packet Statements Here there are all the statments you can return from a filter: ACCEPT: The packet will be accepted and forwarded to the destination. (default if None is returned) REJECT: The connection will be closed and all the packets will be dropped. DROP: This packet and all the following will be dropped. (This not simulate a connection closure) UNSTABLE_MANGLE: The packet will be modified and forwarded. You can edit the packet only with RawPacket data handler. (This is an unstable statement, use it carefully) ⚙️ Data Structures Here there are all the data structure you can use for your filters: <CgEditBlackPoint style={{ marginBottom: -3 }} /> RawPacket This data is the raw packet processed by nfqueue. It contains: data: The raw packet data assembled by libtins (read only). is_input: It's true if the packet is incoming, false if it's outgoing. (read only) is_ipv6: It's true if the packet is IPv6, false if it's IPv4. (read only) is_tcp: It's true if the packet is TCP, false if it's UDP. (read only) l4_size: The size of l4 payload (read only) raw_packet_header_len: The size of the raw packet header (read only) raw_packet: The raw packet data with ip and TCP header. You can edit all the packet content and it will be modified if you send the UNSTABLE_MANGLE statement.{" "} Be careful, beacause the associated layer 4 data can be different from 'data' filed that instead arrives from libtins. When you edit this field, l4_size and l4_data will be updated automatically. l4_data: The l4 payload data, directly taken by the raw packet. You can edit all the packet content and it will be modified if you send the UNSTABLE_MANGLE statement.{" "} Be careful, beacause the associated layer 4 data can be different from 'data' filed that instead arrives from libtins. {" "} When you edit this field, l4_size and raw_packet will be updated automatically. <CgEditBlackPoint style={{ marginBottom: -3 }} />{" "} TCPInputStream (alias: TCPClientStream) This data is the TCP input stream: this handler is called only on is_input=True packets. The filters that handles this data will be called only in this case. data: The entire stream in input direction. (read only) total_stream_size: The size of the entire stream in input direction. (read only) is_ipv6: It's true if the stream is IPv6, false if it's IPv4. (read only) <CgEditBlackPoint style={{ marginBottom: -3 }} />{" "} TCPOutputStream (alias TCPServerStream) This data is the TCP output stream: this handler is called only on is_input=False packets. The filters that handles this data will be called only in this case. data: The entire stream in output direction. (read only) total_stream_size: The size of the entire stream in output direction. (read only) is_ipv6: It's true if the stream is IPv6, false if it's IPv4. (read only) <CgEditBlackPoint style={{ marginBottom: -3 }} />{" "} HttpRequest This data is the Http request processed by nfqueue. This handler can be called twice per request: once when the http headers are complete, and once when the body is complete. If the http data arrives in 1 single TCP packet, this handler will be called once url: The url of the request (read only) headers: The headers of the request (read only). The keys and values are exactly the same as the original request (case sensitive). (values can be list in case the same header field is repeated) get_header(key:str, default = None): A function that returns the value of a header: it matches the key without case sensitivity. If the header is not found, it returns the default value. (if the same header field is repeated, its value is concatenated with a comma, this function will never return a list) user_agent: The user agent of the request (read only) content_encoding: The content encoding of the request (read only) content_length: The content length of the request (read only) body: The body of the request (read only). It's None if the body has not arrived yet. body_decoded: By default the body will be decoded following the content encoding. gzip, br, deflate and zstd are supported. If the decoding fails and body is not None this paramether will be False. http_version: The http version of the request (read only) keep_alive: It's true if the connection was marked for keep alive, false if it's not. (read only) should_upgrade: It's true if the connection should be upgraded, false if it's not. (read only) upgrading_to_h2: It's true if the connection is upgrading to h2, false if it's not. (read only) ws_stream: It's a list of websockets.frames.Frame decoded (permessage-deflate is supported). (read only) [ docs ] upgrading_to_ws: It's true if the connection is upgrading to ws, false if it's not. (read only) method: The method of the request (read only) headers_complete: It's true if the headers are complete, false if they are not. (read only) message_complete: It's true if the message is complete, false if it's not. (read only) total_size: The size of the entire http request (read only) stream: It's the buffer that contains the stream of the websocket traffic in input. This is used only if should_upgrade is True. (read only) <CgEditBlackPoint style={{ marginBottom: -3 }} />{" "} HttpRequestHeader Same as HttpRequest, but this handler is called only when the headers are complete and body is not buffered. Body will always be None <CgEditBlackPoint style={{ marginBottom: -3 }} />{" "} HttpFullRequest Same as HttpRequest, but this handler is called only when the request data is complete <CgEditBlackPoint style={{ marginBottom: -3 }} />{" "} HttpResponse This data is the Http response processed by nfqueue. This handler can be called twice per response: once when the http headers are complete, and once when the body is complete. If the http data arrives in 1 single TCP packet, this handler will be called once url: The url of the response (read only) headers: The headers of the response (read only). The keys and values are exactly the same as the original response (case sensitive). (values can be list in case the same header field is repeated) get_header(key:str, default = None): A function that returns the value of a header: it matches the key without case sensitivity. If the header is not found, it returns the default value. (if the same header field is repeated, its value is concatenated with a comma, this function will never return a list) user_agent: The user agent of the response (read only) content_encoding: The content encoding of the response (read only) content_length: The content length of the response (read only) body: The body of the response (read only). It's None if the body has not arrived yet. body_decoded: By default the body will be decoded following the content encoding. gzip, br, deflate and zstd are supported. If the decoding fails and body is not None this paramether will be False. http_version: The http version of the response (read only) keep_alive: It's true if the connection was marked for keep alive, false if it's not. (read only) should_upgrade: It's true if the connection should be upgraded, false if it's not. (read only) upgrading_to_h2: It's true if the connection is upgrading to h2, false if it's not. (read only) ws_stream: It's a list of websockets.frames.Frame decoded (permessage-deflate is supported). (read only) [ docs ] upgrading_to_ws: It's true if the connection is upgrading to ws, false if it's not. (read only) status_code: The status code of the response (read only) (int) headers_complete: It's true if the headers are complete, false if they are not. (read only) message_complete: It's true if the message is complete, false if it's not. (read only) total_size: The size of the entire http response (read only) stream: It's the buffer that contains the stream of the websocket traffic in output. This is used only if should_upgrade is True. (read only) <CgEditBlackPoint style={{ marginBottom: -3 }} />{" "} HttpResponseHeader Same as HttpResponse, but this handler is called only when the headers are complete and body is not buffered. Body will always be None <CgEditBlackPoint style={{ marginBottom: -3 }} />{" "} HttpFullResponse Same as HttpResponse, but this handler is called only when the response data is complete ⚠️ Stream Limiter What happen if in a specific TCP stream you have a lot of data? The stream limiter will be activated and some action will be taken. You can configure the action performed by setting some option in the globals:
First import the FullStreamAction enum: Then you can set in the globals these options: FGEX_STREAM_MAX_SIZE: Sets the maximum size of the stream. If the stream exceeds this size, the FGEX_FULL_STREAM_ACTION will be performed. (this limit is applyed at the single stream related to the single data handler). For example if TCPInputStream has reached the limit but HttpResponse has not, the action will be performed only on the TCPInputStream. The default is 1MB. FGEX_FULL_STREAM_ACTION: Sets the action performed when the stream exceeds the FGEX_STREAM_MAX_SIZE. The default is FullStreamAction.FLUSH. Heres will be explained every type of action you can set: FLUSH: Flush the stream and continue to acquire new packets (default) DROP: Drop the next stream packets - like a DROP action by filter REJECT: Reject the stream and close the connection - like a REJECT action by filter ACCEPT: Stops to call pyfilters and accept the traffic
⚠️ Other Options Here's other enums that you could need to use: Then you can set in the globals these options: FGEX_INVALID_ENCODING_ACTION: Sets the action performed when the stream has an invalid encoding (due to a parser crash). The default is ExceptionAction.REJECT. 🚀 How It Works The proxy is built on a multi-threaded architecture and integrates Python for dynamic filtering: Packet Interception: The{" "} nfqueue {" "} kernel module intercepts network packets(a{" "} netfilter module) 🔍
The rules for attach the nfqueue on the network traffic is done by the nftables lib with json APIs by the python manager.
Packet Reading: A dedicated thread reads packets from{" "} nfqueue . 🧵 Multi-threaded Analysis: The C++ binary launches multiple threads, each starting its own Python interpreter. Thanks to Python 3.12’s support for{" "} a per-interpeter GIL , real multithreading is achieved. Traffic is distributed among threads based on IP addresses and port hashing, ensuring that packets belonging to the same flow are processed by the same thread. ⚡️ Python Filter Integration: Users can upload custom Python filters which are then executed by the interpreter, allowing for dynamic and flexible packet handling. 🐍 HTTP Parsing: A Python wrapper for llhttp {" "} (forked and adapted for working with multi-interpeters) is used to parse HTTP connections, making it easier to handle and analyze HTTP traffic. 📡
📚 Additional Resources Here's a pyfilter code commented example: ); };