Skip to content

Commit afd7953

Browse files
authored
Merge pull request #15 from Pwnzer0tt1/dev-nfproxy
1.5.1 Release
2 parents 79861d7 + 16f6b6a commit afd7953

19 files changed

+573
-18
lines changed

backend/binsrc/nfregex.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ void config_updater (){
5050
}
5151

5252
int main(int argc, char *argv[]){
53+
54+
char * test_regex = getenv("FIREGEX_TEST_REGEX");
55+
if (test_regex != nullptr){
56+
cerr << "[info] [main] Testing regex: " << test_regex << endl;
57+
try{
58+
RegexRules::compile_regex(test_regex);
59+
cerr << "[info] [main] Test passed" << endl;
60+
return 0;
61+
}catch(const std::exception& e){
62+
cerr << "[error] [updater] Test failed" << endl;
63+
cout << e.what() << flush;
64+
return 1;
65+
}
66+
}
67+
5368
int n_of_threads = 1;
5469
char * n_threads_str = getenv("NTHREADS");
5570
if (n_threads_str != nullptr) n_of_threads = ::atoi(n_threads_str);

backend/binsrc/regex/regex_rules.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ class RegexRules{
5959
public:
6060
regex_ruleset output_ruleset, input_ruleset;
6161

62+
static void compile_regex(char* regex){
63+
hs_database_t* db = nullptr;
64+
hs_compile_error_t *compile_err = nullptr;
65+
if (
66+
hs_compile(
67+
regex,
68+
HS_FLAG_SINGLEMATCH | HS_FLAG_ALLOWEMPTY,
69+
HS_MODE_BLOCK,
70+
nullptr, &db, &compile_err
71+
) != HS_SUCCESS
72+
) {
73+
string err = string(compile_err->message);
74+
hs_free_compile_error(compile_err);
75+
throw runtime_error(err);
76+
}else{
77+
hs_free_database(db);
78+
}
79+
80+
}
81+
6282
private:
6383
static inline u_int16_t glob_seq = 0;
6484
u_int16_t version;
@@ -77,6 +97,8 @@ class RegexRules{
7797
}
7898
}
7999

100+
101+
80102
void fill_ruleset(vector<pair<string, decoded_regex>> & decoded, regex_ruleset & ruleset){
81103
size_t n_of_regex = decoded.size();
82104
if (n_of_regex == 0){

backend/modules/nfregex/firegex.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from modules.nfregex.nftables import FiregexTables
22
from utils import run_func
33
from modules.nfregex.models import Service, Regex
4-
import re
54
import os
65
import asyncio
76
import traceback
@@ -10,6 +9,20 @@
109

1110
nft = FiregexTables()
1211

12+
async def test_regex_validity(regex: str) -> bool:
13+
proxy_binary_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),"../cppregex")
14+
process = await asyncio.create_subprocess_exec(
15+
proxy_binary_path,
16+
stdout=asyncio.subprocess.PIPE,
17+
stdin=asyncio.subprocess.DEVNULL,
18+
env={"FIREGEX_TEST_REGEX": regex},
19+
)
20+
await process.wait()
21+
if process.returncode != 0:
22+
message = (await process.stdout.read()).decode()
23+
return False, message
24+
return True, "ok"
25+
1326
class RegexFilter:
1427
def __init__(
1528
self, regex,
@@ -44,7 +57,6 @@ def compile(self):
4457
self.regex = self.regex.encode()
4558
if not isinstance(self.regex, bytes):
4659
raise Exception("Invalid Regex Paramether")
47-
re.compile(self.regex) # raise re.error if it's invalid!
4860
case_sensitive = "1" if self.is_case_sensitive else "0"
4961
if self.input_mode:
5062
yield case_sensitive + "C" + self.regex.hex()

backend/routers/nfregex.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from base64 import b64decode
2-
import re
32
import secrets
43
import sqlite3
54
from fastapi import APIRouter, Response, HTTPException
@@ -9,6 +8,7 @@
98
from utils.sqlite import SQLite
109
from utils import ip_parse, refactor_name, socketio_emit, PortType
1110
from utils.models import ResetRequest, StatusMessageModel
11+
from modules.nfregex.firegex import test_regex_validity
1212

1313
class ServiceModel(BaseModel):
1414
status: str
@@ -299,10 +299,9 @@ async def regex_disable(regex_id: int):
299299
@app.post('/regexes', response_model=StatusMessageModel)
300300
async def add_new_regex(form: RegexAddForm):
301301
"""Add a new regex"""
302-
try:
303-
re.compile(b64decode(form.regex))
304-
except Exception:
305-
raise HTTPException(status_code=400, detail="Invalid regex")
302+
regex_correct, message = await test_regex_validity(b64decode(form.regex))
303+
if not regex_correct:
304+
raise HTTPException(status_code=400, detail=f"Invalid regex: {message}")
306305
try:
307306
db.query("INSERT INTO regexes (service_id, regex, mode, is_case_sensitive, active ) VALUES (?, ?, ?, ?, ?);",
308307
form.service_id, form.regex, form.mode, form.is_case_sensitive, True if form.active is None else form.active )

start.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,11 @@ def gen_args(args_to_parse: list[str]|None = None):
100100
#Start Command
101101
parser_start = subcommands.add_parser('start', help='Start the firewall')
102102
parser_start.add_argument('--threads', "-t", type=int, required=False, help='Number of threads started for each service/utility', default=-1)
103-
parser_start.add_argument('--psw-no-interactive',type=str, required=False, help='Password for no-interactive mode', default=None)
104-
parser_start.add_argument('--startup-psw','-P', required=False, action="store_true", help='Insert password in the startup screen of firegex', default=False)
103+
parser_start.add_argument('--startup-psw','-P', required=False, help='Insert password in the startup screen of firegex', type=str, default=None)
104+
parser_start.add_argument('--psw-on-web', required=False, help='Setup firegex password on the web interface', action="store_true", default=False)
105105
parser_start.add_argument('--port', "-p", type=int, required=False, help='Port where open the web service of the firewall', default=4444)
106106
parser_start.add_argument('--logs', required=False, action="store_true", help='Show firegex logs', default=False)
107-
parser_start.add_argument('--version', '-v', required=False, type=str , help='Version of the firegex image to use', default="latest")
107+
parser_start.add_argument('--version', '-v', required=False, type=str , help='Version of the firegex image to use', default=None)
108108

109109
#Stop Command
110110
parser_stop = subcommands.add_parser('stop', help='Stop the firewall')
@@ -221,10 +221,10 @@ def write_compose(skip_password = True):
221221
}))
222222

223223
def get_password():
224-
if volume_exists() or args.startup_psw:
224+
if volume_exists() or args.psw_on_web:
225225
return None
226-
if args.psw_no_interactive:
227-
return args.psw_no_interactive
226+
if args.startup_psw:
227+
return args.startup_psw
228228
psw_set = None
229229
while True:
230230
while True:

tests/README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,37 @@ You will find a new benchmark.csv file containg the results.
7979

8080
The test was performed on:
8181
- Macbook Air M2 16GB RAM
82-
- On a VM powered by OrbStack with Ubuntu 24.04.1 LTS aarch64
83-
- 6.12.10-orbstack-00297-gf8f6e015b993
82+
- On a VM powered by OrbStack with Fedora Linux 41 (Container Image) aarch64
83+
- Linux 6.12.13-orbstack-00304-gede1cf3337c4
8484

85-
Command: `./benchmark.py -p testpassword -r 50 -d 1 -s 60`
85+
Command: `./benchmark.py -p testpassword -r 50 -d 1 -s 50`
8686

87-
### NOTE: 8 threads performance do not change due to the fact that the source and destination ip is always the same, so the packets are sent to the same thread by the kernel.
87+
NOTE: 8 threads performance before 2.5.0 do not change due to the fact that the source and destination ip is always the same, so the packets are sent to the same thread by the kernel.
8888
[https://netfilter.vger.kernel.narkive.com/sTP7613Y/meaning-of-nfqueue-s-queue-balance-option](https://netfilter.vger.kernel.narkive.com/sTP7613Y/meaning-of-nfqueue-s-queue-balance-option)
8989

9090
Internally the kernel hashes the source and dest ip and choose the target thread based on the hash. If the source and dest ip are the same, the hash will be the same and the packets will be sent to the same thread.
91+
This is a problem in a CTF, where we usually have a NAT to hide real IPs.
92+
93+
Firegex 2.5.0 changes the way the threads are assigned to the packets, this is done userland, so we can have a better distribution of the packets between the threads.
94+
95+
The charts are labeled as follows: `[version]-[n_thread]T` eg. `2.5.0-8T` means Firegex version 2.5.0 with 8 threads.
9196

9297
![Firegex Benchmark](results/Benchmark-chart.png)
98+
99+
100+
From the benchmark above we can't see the real advantage of multithreading in 2.5.1, we can better see the advantage of multithreading in the chart below where a fake load in filtering is done.
101+
102+
The load is simulated by this code:
103+
```cpp
104+
volatile int x = 0;
105+
for (int i=0; i<50000; i++){
106+
x+=1;
107+
}
108+
```
109+
110+
![Firegex Benchmark](results/Benchmark-chart-with-load.png)
111+
112+
In the chart above we can see that the 2.5.1 version with 8 threads has a better performance than the 2.5.1 version with 1 threads, and we can see it as much as the load increases.
113+
114+
This particular advantage will be more noticeable with nfproxy module that is not implemented yet.
115+

tests/benchmark.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,22 @@ def exit_test(code):
4545

4646
#Create new Service
4747

48+
srvs = firegex.nf_get_services()
49+
for ele in srvs:
50+
if ele['name'] == args.service_name:
51+
firegex.nf_delete_service(ele['service_id'])
52+
4853
service_id = firegex.nf_add_service(args.service_name, args.port, "tcp", "127.0.0.1/24")
4954
if service_id:
5055
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
5156
else:
5257
puts("Test Failed: Failed to create service ✗", color=colors.red)
5358
exit(1)
5459

60+
args.port = int(args.port)
61+
args.duration = int(args.duration)
62+
args.num_of_streams = int(args.num_of_streams)
63+
5564
#Start iperf3
5665
def startServer():
5766
server = iperf3.Server()
@@ -66,6 +75,8 @@ def getReading(port):
6675
client.duration = args.duration
6776
client.server_hostname = '127.0.0.1'
6877
client.port = port
78+
client.zerocopy = True
79+
client.verbose = False
6980
client.protocol = 'tcp'
7081
client.num_streams = args.num_of_streams
7182
return round(client.run().json['end']['sum_received']['bits_per_second']/8e+6 , 3)
@@ -74,6 +85,19 @@ def getReading(port):
7485
server.start()
7586
sleep(1)
7687

88+
custom_regex = [
89+
'(?:[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*|"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])'
90+
]
91+
92+
def gen_regex():
93+
"""
94+
if len(custom_regex) == 0:
95+
regex = secrets.token_hex(8)
96+
else:
97+
regex = custom_regex.pop()
98+
"""
99+
regex = secrets.token_hex(20)
100+
return base64.b64encode(bytes(regex.encode())).decode()
77101

78102
#Get baseline reading
79103
puts("Baseline without proxy: ", color=colors.blue, end='')
@@ -95,7 +119,7 @@ def getReading(port):
95119

96120
#Add all the regexs
97121
for i in range(1,args.num_of_regexes+1):
98-
regex = base64.b64encode(bytes(secrets.token_hex(16).encode())).decode()
122+
regex = gen_regex()
99123
if not firegex.nf_add_regex(service_id,regex,"B",active=True,is_case_sensitive=False):
100124
puts("Benchmark Failed: Couldn't add the regex ✗", color=colors.red)
101125
exit_test(1)

tests/results/2.3.3-1T.csv

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
0,4090.616
2+
1,2211.62
3+
2,1165.45
4+
3,849.39
5+
4,828.635
6+
5,741.537
7+
6,632.721
8+
7,624.772
9+
8,529.234
10+
9,469.688
11+
10,336.33
12+
11,427.783
13+
12,400.662
14+
13,335.086
15+
14,342.042
16+
15,307.283
17+
16,239.694
18+
17,295.163
19+
18,285.787
20+
19,254.402
21+
20,250.553
22+
21,227.146
23+
22,238.747
24+
23,234.718
25+
24,210.484
26+
25,210.697
27+
26,205.943
28+
27,202.568
29+
28,194.341
30+
29,189.916
31+
30,154.228
32+
31,168.922
33+
32,173.623
34+
33,125.431
35+
34,162.154
36+
35,149.865
37+
36,150.088
38+
37,146.085
39+
38,137.182
40+
39,138.686
41+
40,136.302
42+
41,132.707
43+
42,100.928
44+
43,126.414
45+
44,125.271
46+
45,117.839
47+
46,89.494
48+
47,116.939
49+
48,112.517
50+
49,111.369
51+
50,108.568

tests/results/2.3.3-8T.csv

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
0,3789.988
2+
1,2069.487
3+
2,1484.554
4+
3,956.972
5+
4,1052.873
6+
5,739.658
7+
6,534.722
8+
7,638.524
9+
8,573.833
10+
9,531.658
11+
10,476.167
12+
11,443.746
13+
12,406.027
14+
13,385.739
15+
14,341.563
16+
15,318.699
17+
16,303.722
18+
17,284.924
19+
18,284.336
20+
19,267.32
21+
20,202.74
22+
21,243.849
23+
22,226.082
24+
23,214.348
25+
24,216.8
26+
25,188.98
27+
26,158.68
28+
27,166.556
29+
28,148.287
30+
29,149.681
31+
30,177.043
32+
31,175.321
33+
32,165.312
34+
33,166.943
35+
34,159.026
36+
35,156.759
37+
36,150.216
38+
37,144.932
39+
38,146.088
40+
39,135.897
41+
40,136.99
42+
41,128.557
43+
42,100.307
44+
43,103.249
45+
44,123.49
46+
45,120.39
47+
46,118.055
48+
47,115.0
49+
48,112.593
50+
49,109.55
51+
50,109.512

0 commit comments

Comments
 (0)