Skip to content

Commit 4b928f1

Browse files
committed
File locking bug fix and performance improvement. New TTL > NOW handling (cache responses) for missed initial queries
1 parent 59f477c commit 4b928f1

File tree

2 files changed

+67
-37
lines changed

2 files changed

+67
-37
lines changed

pfui_firewall.py

+65-36
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,26 @@
2424

2525
import ast
2626
import sys
27+
import errno
2728
import socket
2829
import logging
2930
import subprocess
3031

31-
import ipaddress
32+
from os import rename
3233

3334
from ctypes import *
34-
from fcntl import ioctl, flock, LOCK_EX, LOCK_UN
35+
from fcntl import ioctl, flock, LOCK_EX, LOCK_NB, LOCK_UN
3536

3637
from time import sleep, time
3738
from json import loads
3839
from yaml import safe_load
3940
from redis import StrictRedis
41+
4042
from threading import Thread, Event
4143
from service import find_syslog, Service
4244
from logging.handlers import SysLogHandler
4345

46+
4447
CONFIG_LOCATION = "/etc/pfui_firewall.yml"
4548

4649
# Constants
@@ -220,9 +223,11 @@ def db_push(logger, log: bool, db, table: str, data: list):
220223
for ip, ttl in data:
221224
key = "{}{}{}".format(table, "^", ip)
222225
if ttl < now: # Real TTL
226+
if ttl < 3600: # Always allow for min 1 hour
227+
ttl = 3600
223228
pipe.hmset(key, {'epoch': now, 'ttl': ttl})
224229
else: # Cached entry TTL = Future Expiry Epoch
225-
pipe.hmset(key, {'epoch': now}) # TODO: Risk of records with no ttl..
230+
pipe.hmset(key, {'epoch': now, 'expires': ttl})
226231
pipe.execute()
227232
return True
228233
except Exception as e:
@@ -244,42 +249,55 @@ def db_pop(logger, log: bool, db, table: str, ip: str):
244249
def file_push(logger, log: bool, file: str, ip_list: list):
245250
if log:
246251
logger.info("PFUIFW: Installing {} into file {}".format(ip_list, file))
247-
inserts = []
252+
unique = []
248253
try:
249-
with open(file, "r+") as f:
250-
for ip in ip_list:
251-
found = next((l for l in f if "{}\n".format(ip) == l), False)
252-
if ip and not found:
253-
inserts.append(ip)
254-
inserts.append("\n")
254+
with open(file, "r+") as f: # Open for reading and writing with pointer at beginning
255+
while True:
256+
try:
257+
flock(f, LOCK_EX | LOCK_NB)
258+
break
259+
except IOError as e:
260+
if e.errno != errno.EAGAIN:
261+
raise # raise other file access issues
262+
else:
263+
if log:
264+
logger.info("PFUIFW: File {} Locked.".format(file))
265+
sleep(0.001) # 1ms
266+
lines = f.readlines()
267+
unique = ["{}\n".format(ip) for ip in ip_list if "{}\n".format(ip) not in lines] # Check not exists
255268
try:
256-
flock(f, LOCK_EX)
257-
f.write("".join(inserts)) # append missing
269+
f.write("".join(unique)) # append new
258270
except Exception as e:
259271
logger.error("PFUIFW: f.write error {}".format(e))
260-
finally:
261-
flock(f, LOCK_UN)
272+
flock(f, LOCK_UN)
262273
return True
263274
except Exception as e:
264-
logger.error("PFUIFW: Failed to install {} to file {}. {}".format(inserts, file, e))
275+
logger.error("PFUIFW: Failed to install {} to file {}. {}".format(unique, file, e))
265276
return False
266277

267278

268279
def file_pop(logger, log: bool, file: str, ip: str):
280+
# TODO: Implement multiple parallel deletes to reduce disk IO (requires rework of Scanner)
269281
if log:
270282
logger.info("PFUIFW: Clearing {} from file {}".format(ip, file))
271283
try:
272-
with open(file, "r") as f:
273-
lines = f.readlines()
274-
nlines = [l for l in lines if l not in [ip, "\n", ""]]
275-
with open(file, "w") as f:
276-
try:
277-
flock(f, LOCK_EX)
278-
f.writelines(nlines)
279-
except Exception as e:
280-
logger.error("PFUIFW: f.writelines error {}".format(e))
281-
finally:
282-
flock(f, LOCK_UN)
284+
with open(file, "r") as f, open(file + "~", "w") as tmp:
285+
lines = [l for l in f if l not in ["{}\n".format(ip), ""]]
286+
lines = list(dict.fromkeys(lines)) # Strip dups
287+
tmp.writelines(lines)
288+
while True: # Set lock - blocking
289+
try:
290+
flock(f, LOCK_EX | LOCK_NB)
291+
break
292+
except IOError as e:
293+
if e.errno != errno.EAGAIN:
294+
raise # raise other file access issues
295+
else:
296+
if log:
297+
logger.info("PFUIFW: File {} Locked.".format(file))
298+
sleep(0.001) # 1ms
299+
rename(file + "~", file)
300+
flock(f, LOCK_UN)
283301
except Exception as e:
284302
logger.error("PFUIFW: Failed to delete IP {} from {}. {}".format(ip, file, e))
285303
return False
@@ -349,19 +367,31 @@ def scan_redis_db(self):
349367
""" Expire IPs with last update epoch/timestamp older than (TTL * TTL_MULTIPLIER). """
350368
if self.cfg['LOGGING']:
351369
self.logger.info("PFUIFW: Scan DB({}) for expiring {} IPs.".format(self.cfg['REDIS_DB'], self.table))
370+
now = int(time())
352371
try:
353372
keys = self.db.keys("{}*".format(self.table))
354373
except Exception as e:
355-
self.logger.error("PFUIFW: Failed to read keys from Redis. {}".format(e))
356-
now = int(time())
374+
self.logger.error("PFUIFW: Failed to get keys from Redis. {}".format(e))
375+
return
357376
for k in keys:
377+
db_last, db_ttl, db_expires = 0, 0, 0
358378
try:
359379
v = self.db.hgetall(k)
360-
ttl = int(v[b'ttl'].decode('utf-8'))
361-
db_epoch = int(v[b'epoch'].decode('utf-8'))
362-
except:
363-
db_epoch, ttl = now, 0
364-
if db_epoch <= now - (ttl * self.cfg['TTL_MULTIPLIER']):
380+
db_last = int(v[b'epoch'].decode('utf-8'))
381+
db_ttl = int(v[b'ttl'].decode('utf-8'))
382+
except KeyError as e:
383+
self.logger.error("PFUIFW: Metadata not found! Trying 'expires' timestamp. {}".format(e))
384+
try:
385+
db_expires = int(v[b'expires'].decode('utf-8'))
386+
except KeyError as e:
387+
self.logger.error("PFUIFW: No 'expires' found either! {}".format(e))
388+
continue
389+
if db_expires is None or db_expires <= now:
390+
db_last, db_ttl = now, 0
391+
except Exception as e:
392+
self.logger.error("PFUIFW: Exception getting key '{}' values. {}".format(k, e))
393+
continue
394+
if db_last <= now - (db_ttl * self.cfg['TTL_MULTIPLIER']):
365395
ip = k.decode('utf-8').split("^")[1]
366396
if self.cfg['LOGGING']:
367397
self.logger.info("PFUIFW: TTL Expired for IP {}".format(ip))
@@ -509,7 +539,7 @@ def run(self):
509539
if self.cfg['SOCKET_PROTO'] == "UDP":
510540
self.soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP Datagram Socket
511541
self.soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
512-
self.soc.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 36)
542+
self.soc.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 36) # 'ACK' = 36bytes
513543
self.soc.settimeout(self.cfg['SOCKET_TIMEOUT']) # accept() & recv() blocking timeouts
514544
self.soc.bind((self.cfg['SOCKET_LISTEN'], self.cfg['SOCKET_PORT']))
515545
while not self.got_sigterm(): # Watch Socket until Signal
@@ -615,8 +645,7 @@ def disconnect(proto, soc, conn):
615645
ntime = time()
616646
self.logger.info("PFUIFW: Received {} from {}:{} ({})".format(data, ip, port, proto))
617647

618-
# Get Valid Data
619-
af4_data, af6_data = [], []
648+
# Guard Statements
620649
if isinstance(data, dict):
621650
try:
622651
af4_data = [(rr['ip'], rr['ttl']) for rr in data['AF4'] if is_ipv4(rr['ip']) and rr['ttl']]

tests/test_unbound_faults.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
{'ip': '192.0.2', 'ttl': 100},
2222
{'ip': '192.0.', 'ttl': 100}],
2323
'AF6': [{'ip': '2001::1:1::1', 'ttl': 3600},
24-
{'ip': '2001:DB8:1:2::1', 'ttl': 100}]}
24+
{'ip': '2001:DB8:1:2::1', 'ttl': 100},
25+
{'ip': '1:DB8:1:3::53', 'ttl': 100}]}
2526

2627

2728
def log_info(message):

0 commit comments

Comments
 (0)