security/lightscope: Refactor to use separate port package

- Remove core Python files (now in security/lightscope port)
  - Change PLUGIN_DEPENDS from python311 to lightscope
  - Update rc.d script to call /usr/local/bin/lightscope
This commit is contained in:
Eric Kapitanski 2025-12-31 16:38:46 -08:00
parent 3a35e2a259
commit 86ccd7bb46
8 changed files with 3 additions and 1579 deletions

View file

@ -3,6 +3,6 @@ PLUGIN_VERSION= 1.0
PLUGIN_COMMENT= LightScope Cybersecurity Research Honeypot and Telescope
PLUGIN_MAINTAINER= e@alumni.usc.edu
PLUGIN_WWW= https://thelightscope.com
PLUGIN_DEPENDS= python311
PLUGIN_DEPENDS= lightscope
.include "../../Mk/plugins.mk"

View file

@ -1,27 +0,0 @@
[Settings]
# LightScope Configuration for OPNsense
#
# Database identifier - auto-generated if empty
# This is used to identify your installation on thelightscope.com
database =
# IP randomization key - auto-generated if empty
# Used to anonymize destination IPs in uploaded data
randomization_key =
# Comma-separated honeypot ports
# These ports will be opened and forwarded to the remote honeypot server
# Leave empty to disable honeypot functionality
# Default: 8080,2323,8443,3389,5900
honeypot_ports = 8080,2323,8443,3389,5900
# Remote honeypot server
# The server that receives forwarded honeypot connections
honeypot_server = 128.9.28.79
# Remote honeypot ports
# SSH connections (even local ports) forward to this port
honeypot_ssh_port = 12345
# TELNET connections (odd local ports) forward to this port
honeypot_telnet_port = 12346

View file

@ -28,7 +28,7 @@ fi
: ${lightscope_config:="/usr/local/etc/lightscope.conf"}
pidfile="/var/run/${name}.pid"
procname="/usr/local/bin/python3"
procname="/usr/local/bin/lightscope"
start_cmd="${name}_start"
stop_cmd="${name}_stop"
@ -39,7 +39,7 @@ start_precmd="${name}_prestart"
lightscope_start()
{
echo "Starting ${name}."
/usr/sbin/daemon -c -f -p ${pidfile} -o /var/log/lightscope.log /usr/local/bin/python3 -u /usr/local/opnsense/scripts/lightscope/lightscope_daemon.py
/usr/sbin/daemon -c -f -p ${pidfile} -o /var/log/lightscope.log /usr/local/bin/lightscope
}
lightscope_stop()

View file

@ -1 +0,0 @@
# LightScope for OPNsense

View file

@ -1,504 +0,0 @@
#!/usr/local/bin/python3
"""
honeypot.py - Honeypot listener with PROXY protocol forwarding
This module opens sockets on configured ports and forwards connections
to a remote honeypot server using the PROXY protocol to preserve
the original attacker IP address.
"""
import socket
import select
import threading
import time
import sys
import subprocess
import re
class HoneypotListener:
"""
Manages honeypot listener sockets and forwards connections
to remote honeypot server using PROXY protocol.
"""
def __init__(self, config, upload_pipe, database):
"""
Initialize honeypot listener.
Args:
config: Dict with honeypot configuration
upload_pipe: Pipe for sending honeypot connection data
database: Database identifier for PROXY header
"""
self.config = config
self.upload_pipe = upload_pipe
self.database = database
self.honeypot_server = config.get('honeypot_server', '128.9.28.79')
self.ssh_port = int(config.get('honeypot_ssh_port', 12345))
self.telnet_port = int(config.get('honeypot_telnet_port', 12346))
self.sockets = {} # socket -> port mapping
self.sockets_lock = threading.Lock()
self.running = False
# Connection limiting
self.MAX_CONCURRENT_CONNECTIONS = 50
self.connection_semaphore = threading.Semaphore(self.MAX_CONCURRENT_CONNECTIONS)
self.active_connections = 0
self.connection_lock = threading.Lock()
# Firewall monitoring
self.FIREWALL_CHECK_INTERVAL = 10 # seconds
def parse_ports(self, ports_string):
"""Parse comma-separated port string into list of integers."""
if not ports_string:
return []
ports = []
for p in ports_string.split(','):
p = p.strip()
if p.isdigit():
port = int(p)
if 1 <= port <= 65535:
ports.append(port)
return ports
def get_firewall_allowed_ports(self):
"""
Get list of ports that have PASS/ALLOW rules in the firewall.
These are ports where legitimate services may be running.
Returns a tuple of (set of ports, list of (start, end) range tuples).
"""
allowed_ports = set()
allowed_ranges = []
try:
result = subprocess.run(
["pfctl", "-sr"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
for line in result.stdout.split('\n'):
# Look for pass rules with port specifications
if line.startswith('pass') and 'proto tcp' in line:
# Match port ranges like "port 1:4343" or "port 80:443"
range_match = re.search(r'port\s*[=]?\s*(\d+):(\d+)', line)
if range_match:
start_port = int(range_match.group(1))
end_port = int(range_match.group(2))
allowed_ranges.append((start_port, end_port))
continue # Don't also match as single port
# Match single port patterns like "port = 22" or "port 22"
port_match = re.search(r'port\s*[=]?\s*(\d+)(?![\d:])', line)
if port_match:
allowed_ports.add(int(port_match.group(1)))
# Match port lists in braces
brace_match = re.search(r'port\s*[=]?\s*\{([^}]+)\}', line)
if brace_match:
ports_str = brace_match.group(1)
for p in re.findall(r'\d+', ports_str):
allowed_ports.add(int(p))
except Exception as e:
print(f"honeypot: Error checking firewall rules: {e}", flush=True)
return allowed_ports, allowed_ranges
def is_port_allowed_by_firewall(self, port, allowed_ports, allowed_ranges):
"""Check if a port is allowed by firewall rules (including ranges)."""
if port in allowed_ports:
return True
for start, end in allowed_ranges:
if start <= port <= end:
return True
return False
def is_port_in_use(self, port):
"""
Check if a port is already in use by another service.
Returns True if port is in use, False if available.
"""
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
test_sock.bind(("", port))
test_sock.close()
return False # Port is available
except OSError:
return True # Port is in use
def _firewall_monitor_thread(self):
"""
Monitor firewall rules and port usage every FIREWALL_CHECK_INTERVAL seconds.
If a new ALLOW rule is detected or port becomes in use, close that honeypot port.
"""
print("honeypot: Started firewall/port monitor (checking every 10s)", flush=True)
while self.running:
time.sleep(self.FIREWALL_CHECK_INTERVAL)
if not self.running:
break
try:
allowed_ports, allowed_ranges = self.get_firewall_allowed_ports()
with self.sockets_lock:
sockets_to_close = []
for sock, port in list(self.sockets.items()):
if self.is_port_allowed_by_firewall(port, allowed_ports, allowed_ranges):
print(f"honeypot: WARNING - Firewall ALLOW rule detected for port {port}, closing honeypot on this port", flush=True)
sockets_to_close.append((sock, port, "firewall conflict"))
for sock, port, reason in sockets_to_close:
try:
sock.close()
except:
pass
del self.sockets[sock]
print(f"honeypot: Closed listener on port {port} due to {reason}", flush=True)
except Exception as e:
print(f"honeypot: Error in firewall monitor: {e}", flush=True)
def open_port(self, port):
"""
Try to bind and listen on a port.
Returns:
(socket, port) on success, (None, None) on failure
"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("", port))
s.listen(10)
s.setblocking(False)
return s, port
except OSError as e:
if e.errno == 48: # EADDRINUSE on FreeBSD
print(f"honeypot: Port {port} already in use", flush=True)
elif e.errno == 13: # EACCES
print(f"honeypot: Permission denied for port {port}", flush=True)
else:
print(f"honeypot: Error binding port {port}: {e}", flush=True)
return None, None
except Exception as e:
print(f"honeypot: Unexpected error binding port {port}: {e}", flush=True)
return None, None
def start(self, ports_string):
"""
Start honeypot listeners on configured ports.
Args:
ports_string: Comma-separated list of ports
"""
ports = self.parse_ports(ports_string)
if not ports:
print("honeypot: No valid ports configured", flush=True)
return
# Check for firewall conflicts and ports already in use
allowed_ports, allowed_ranges = self.get_firewall_allowed_ports()
safe_ports = []
for port in ports:
if self.is_port_allowed_by_firewall(port, allowed_ports, allowed_ranges):
print(f"honeypot: WARNING - Port {port} has a firewall ALLOW rule, skipping (may conflict with legitimate service)", flush=True)
elif self.is_port_in_use(port):
print(f"honeypot: WARNING - Port {port} is already in use by another service, skipping", flush=True)
else:
safe_ports.append(port)
if not safe_ports:
print("honeypot: No ports available (all have conflicts or are in use)", flush=True)
return
print(f"honeypot: Opening ports: {safe_ports}", flush=True)
for port in safe_ports:
s, actual_port = self.open_port(port)
if s and actual_port:
with self.sockets_lock:
self.sockets[s] = actual_port
print(f"honeypot: Listening on port {actual_port}", flush=True)
if not self.sockets:
print("honeypot: Failed to open any ports", flush=True)
return
self.running = True
# Start firewall monitor thread
monitor_thread = threading.Thread(target=self._firewall_monitor_thread, daemon=True)
monitor_thread.start()
self._run_accept_loop()
def stop(self):
"""Stop all honeypot listeners."""
self.running = False
with self.sockets_lock:
for s in list(self.sockets.keys()):
try:
s.close()
except:
pass
self.sockets.clear()
print("honeypot: Stopped all listeners", flush=True)
def get_open_ports(self):
"""Return list of currently open honeypot ports."""
with self.sockets_lock:
return list(self.sockets.values())
def _run_accept_loop(self):
"""Main accept loop for honeypot connections."""
print("honeypot: Starting accept loop", flush=True)
while self.running:
with self.sockets_lock:
sock_list = list(self.sockets.keys())
if not sock_list:
time.sleep(1.0)
continue
try:
# Wait for incoming connections
readable, _, _ = select.select(sock_list, [], [], 1.0)
for sock in readable:
try:
self._handle_connection(sock)
except Exception as e:
print(f"honeypot: Error handling connection: {e}", flush=True)
except Exception as e:
if self.running:
print(f"honeypot: Error in accept loop: {e}", flush=True)
time.sleep(1.0)
def _handle_connection(self, listen_sock):
"""Handle an incoming connection on a honeypot port."""
try:
local_conn, addr = listen_sock.accept()
with self.sockets_lock:
port = self.sockets.get(listen_sock)
if port is None:
local_conn.close()
return
attacker_ip = addr[0]
attacker_port = addr[1]
print(f"honeypot: Connection from {attacker_ip}:{attacker_port} to port {port}", flush=True)
# Determine service type: even ports = SSH, odd ports = TELNET
if port % 2 == 0:
service = 'SSH'
remote_port = self.ssh_port
else:
service = 'TELNET'
remote_port = self.telnet_port
# Check connection limit
if not self.connection_semaphore.acquire(blocking=False):
print(f"honeypot: Connection limit reached, rejecting {attacker_ip}", flush=True)
local_conn.close()
return
with self.connection_lock:
self.active_connections += 1
# Start forwarding in a new thread
threading.Thread(
target=self._forward_connection,
args=(local_conn, attacker_ip, attacker_port, port, service, remote_port),
daemon=True
).start()
except Exception as e:
print(f"honeypot: Error accepting connection: {e}", flush=True)
def _forward_connection(self, local_conn, attacker_ip, attacker_port, honeypot_port, service, remote_port):
"""
Forward a connection to the remote honeypot server using PROXY protocol.
"""
remote_conn = None
connection_released = False
try:
# Connect to remote honeypot
remote_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote_conn.settimeout(30.0)
try:
remote_conn.connect((self.honeypot_server, remote_port))
except Exception as e:
print(f"honeypot: Failed to connect to remote server: {e}", flush=True)
return
# Send PROXY protocol header
# Format: PROXY TCP4 src_ip dest_ip src_port dest_port\r\n
proxy_header = f"PROXY TCP4 {attacker_ip} {self.database} {attacker_port} {honeypot_port}\r\n"
print(f"honeypot: Sending PROXY header: {proxy_header.strip()}", flush=True)
remote_conn.sendall(proxy_header.encode())
# Set timeouts for idle connections (3 minutes)
CONNECTION_TIMEOUT = 180
local_conn.settimeout(CONNECTION_TIMEOUT)
remote_conn.settimeout(CONNECTION_TIMEOUT)
# Send honeypot connection data for logging
self._send_connection_data(attacker_ip, attacker_port, honeypot_port, service)
# Bidirectional forwarding
def forward(src, dst, direction):
try:
while True:
data = src.recv(4096)
if not data:
break
dst.sendall(data)
except Exception:
pass
finally:
try:
src.close()
except:
pass
try:
dst.close()
except:
pass
# Start forwarding threads
t1 = threading.Thread(target=forward, args=(local_conn, remote_conn, "client->server"))
t2 = threading.Thread(target=forward, args=(remote_conn, local_conn, "server->client"))
t1.daemon = True
t2.daemon = True
t1.start()
t2.start()
# Wait for both threads to complete
t1.join()
t2.join()
print(f"honeypot: Connection from {attacker_ip} closed", flush=True)
except Exception as e:
print(f"honeypot: Forwarding error: {e}", flush=True)
finally:
# Clean up
for conn in (local_conn, remote_conn):
if conn:
try:
conn.close()
except:
pass
# Release connection slot
if not connection_released:
self.connection_semaphore.release()
with self.connection_lock:
self.active_connections -= 1
def _send_connection_data(self, attacker_ip, attacker_port, honeypot_port, service):
"""Send honeypot connection data to upload pipe."""
try:
data = {
"db_name": self.database,
"system_time": str(time.time()),
"ip_version": "HP",
"ip_ihl": "HP",
"ip_tos": "HP",
"ip_len": "HP",
"ip_id": "HP",
"ip_flags": "HP",
"ip_frag": "HP",
"ip_ttl": "HP",
"ip_proto": "HP",
"ip_chksum": "HP",
"ip_src": attacker_ip,
"ip_dst_randomized": "HP",
"ip_options": "HP",
"tcp_sport": str(attacker_port),
"tcp_dport": str(honeypot_port),
"tcp_seq": 0,
"tcp_ack": "HP",
"tcp_dataofs": "HP",
"tcp_reserved": "HP",
"tcp_flags": "HP",
"tcp_window": "HP",
"tcp_chksum": "HP",
"tcp_urgptr": "HP",
"tcp_options": "HP",
"ext_dst_ip_country": "HP",
"type": "HP",
"ASN": "HP",
"domain": "HP",
"city": "HP",
"as_type": "HP",
"ip_dst_is_private": "HP",
"external_is_private": "HP",
"open_ports": "",
"previously_open_ports": "",
"interface": "honeypot",
"internal_ip_randomized": "HP",
"external_ip_randomized": "HP",
"System_info": "OPNsense",
"Release_info": "HP",
"Version_info": "HP",
"Machine_info": "HP",
"Total_Memory": "HP",
"processor": "HP",
"architecture": "HP",
"honeypot_status": "True",
"payload": service,
"ls_version": "opnsense-1.0"
}
if self.upload_pipe:
self.upload_pipe.send(data)
except Exception as e:
print(f"honeypot: Error sending connection data: {e}", flush=True)
def run_honeypot(config, upload_pipe, database):
"""
Main entry point for honeypot process.
Args:
config: Configuration dict
upload_pipe: Pipe for sending connection data
database: Database identifier
"""
listener = HoneypotListener(config, upload_pipe, database)
ports = config.get('honeypot_ports', '')
listener.start(ports)
if __name__ == "__main__":
# Test mode
print("honeypot: Running in test mode")
test_config = {
'honeypot_ports': '8080,2323',
'honeypot_server': '128.9.28.79',
'honeypot_ssh_port': 12345,
'honeypot_telnet_port': 12346
}
listener = HoneypotListener(test_config, None, "test_database")
try:
listener.start(test_config['honeypot_ports'])
except KeyboardInterrupt:
listener.stop()

View file

@ -1,490 +0,0 @@
#!/usr/local/bin/python3
"""
lightscope_daemon.py - Main LightScope daemon for OPNsense
This is the main entry point that:
1. Reads configuration from OPNsense model or config file
2. Spawns the pflog reader process
3. Spawns the honeypot listener process
4. Spawns the uploader process
5. Processes captured packets and sends to uploader
"""
import os
import sys
import signal
import time
import hashlib
import ipaddress
import random
import string
import configparser
import multiprocessing
import threading
from collections import deque
# Add script directory to path for imports
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)
from pflog_reader import read_pflog
from honeypot import run_honeypot
from uploader import send_data, send_honeypot_data
try:
import requests
except ImportError:
print("Error: requests not found", file=sys.stderr)
sys.exit(1)
try:
import psutil
except ImportError:
psutil = None
# Version
LS_VERSION = "opnsense-1.0"
# Configuration paths
CONFIG_FILE = "/usr/local/etc/lightscope.conf"
CONFIG_FILE_SAMPLE = "/usr/local/etc/lightscope.conf.sample"
class LightScopeConfig:
"""Configuration manager for LightScope."""
def __init__(self, config_file=CONFIG_FILE):
self.config_file = config_file
self.database = ""
self.randomization_key = ""
self.honeypot_ports = "8080,2323,8443,3389,5900"
self.honeypot_server = "128.9.28.79"
self.honeypot_ssh_port = 12345
self.honeypot_telnet_port = 12346
self._load_or_create_config()
def _generate_database_name(self):
"""Generate a unique database name with OPNsense prefix."""
import datetime
today = datetime.date.today().strftime("%Y%m%d")
rand_part = ''.join(random.choices(string.ascii_lowercase, k=44))
return f"opn{today}_{rand_part}"
def _generate_randomization_key(self):
"""Generate a randomization key."""
rand_part = ''.join(random.choices(string.ascii_lowercase, k=46))
return f"randomization_key_{rand_part}"
def _load_or_create_config(self):
"""Load config from file or create with defaults."""
config = configparser.ConfigParser()
# Try to load existing config
if os.path.exists(self.config_file):
config.read(self.config_file)
else:
# Copy from sample if available
if os.path.exists(CONFIG_FILE_SAMPLE):
import shutil
shutil.copy(CONFIG_FILE_SAMPLE, self.config_file)
config.read(self.config_file)
# Ensure Settings section exists
if 'Settings' not in config:
config.add_section('Settings')
# Load or generate database name
self.database = config.get('Settings', 'database', fallback='').strip()
if not self.database:
self.database = self._generate_database_name()
config.set('Settings', 'database', self.database)
print(f"Generated new database name: {self.database}")
# Load or generate randomization key
self.randomization_key = config.get('Settings', 'randomization_key', fallback='').strip()
if not self.randomization_key:
self.randomization_key = self._generate_randomization_key()
config.set('Settings', 'randomization_key', self.randomization_key)
# Load honeypot settings
self.honeypot_ports = config.get('Settings', 'honeypot_ports', fallback=self.honeypot_ports)
self.honeypot_server = config.get('Settings', 'honeypot_server', fallback=self.honeypot_server)
self.honeypot_ssh_port = config.getint('Settings', 'honeypot_ssh_port', fallback=self.honeypot_ssh_port)
self.honeypot_telnet_port = config.getint('Settings', 'honeypot_telnet_port', fallback=self.honeypot_telnet_port)
print(f"Loaded honeypot_ports from config: '{self.honeypot_ports}'")
# Save config
try:
with open(self.config_file, 'w') as f:
config.write(f)
except Exception as e:
print(f"Warning: Could not save config: {e}")
print(f"Database: {self.database}")
print(f"View reports at: https://thelightscope.com/light_table/{self.database}")
def get_dict(self):
"""Return config as dictionary."""
return {
'database': self.database,
'randomization_key': self.randomization_key,
'honeypot_ports': self.honeypot_ports,
'honeypot_server': self.honeypot_server,
'honeypot_ssh_port': self.honeypot_ssh_port,
'honeypot_telnet_port': self.honeypot_telnet_port
}
def fetch_external_info():
"""Fetch external network information from thelightscope.com."""
try:
resp = requests.get("https://thelightscope.com/ipinfo", timeout=10)
resp.raise_for_status()
data = resp.json()
queried_ip = data.get("queried_ip")
asn_rec = data["results"].get("asn", {}).get("record", {})
loc_rec = data["results"].get("location", {}).get("record", {})
company_rec = data["results"].get("company", {}).get("record", {})
return {
"queried_ip": queried_ip,
"ASN": asn_rec.get("asn"),
"domain": asn_rec.get("domain"),
"city": loc_rec.get("city"),
"country": loc_rec.get("country"),
"as_type": company_rec.get("as_type"),
"type": asn_rec.get("type")
}
except Exception as e:
print(f"Warning: Could not fetch external info: {e}")
return {
"queried_ip": "0.0.0.0",
"ASN": "unknown",
"domain": "unknown",
"city": "unknown",
"country": "unknown",
"as_type": "unknown",
"type": "unknown"
}
def get_system_info():
"""Get system information."""
import platform
info = {
"System": platform.system(),
"Release": platform.release(),
"Version": platform.version(),
"Machine": platform.machine(),
"processor": platform.processor(),
"architecture": platform.architecture()[0]
}
if psutil:
info["Total_Memory"] = f"{psutil.virtual_memory().total / (1024 ** 3):.2f} GB"
else:
info["Total_Memory"] = "unknown"
return info
def randomize_ip(ip_str, key):
"""Randomize an IP address for privacy."""
try:
ip = ipaddress.ip_address(ip_str)
except ValueError:
return "unknown"
def hash_segment(segment, k):
combined = f"{segment}-{k}"
h = hashlib.sha256(combined.encode()).hexdigest()
return int(h[:2], 16) % 256
orig_bytes = ip.packed
new_bytes = bytes(hash_segment(b, key) for b in orig_bytes)
try:
rand_ip = ipaddress.IPv4Address(new_bytes) if ip.version == 4 else ipaddress.IPv6Address(new_bytes)
return str(rand_ip)
except:
return "unknown"
def check_ip_is_private(ip_str):
"""Check if IP is private."""
try:
ip_obj = ipaddress.ip_address(ip_str)
return "True" if ip_obj.is_private else "False"
except ValueError:
return "unknown"
class PacketProcessor:
"""Processes packets from pflog and prepares them for upload."""
def __init__(self, config, external_info, system_info, upload_pipe):
self.config = config
self.external_info = external_info
self.system_info = system_info
self.upload_pipe = upload_pipe
self.packet_count = 0
self.HEARTBEAT_INTERVAL = 15 * 60 # 15 minutes
def process_batch(self, batch):
"""Process a batch of packets from pflog."""
for pkt in batch:
self.packet_count += 1
self._prepare_and_send(pkt)
def _prepare_and_send(self, pkt):
"""Prepare packet data and send to uploader."""
payload = {
"db_name": self.config['database'],
"system_time": str(pkt.packet_time),
"ip_version": pkt.ip_version,
"ip_ihl": pkt.ip_ihl,
"ip_tos": pkt.ip_tos,
"ip_len": pkt.ip_len,
"ip_id": pkt.ip_id,
"ip_flags": pkt.ip_flags if isinstance(pkt.ip_flags, str) else ",".join(str(v) for v in pkt.ip_flags),
"ip_frag": pkt.ip_frag,
"ip_ttl": pkt.ip_ttl,
"ip_proto": pkt.ip_proto,
"ip_chksum": pkt.ip_chksum,
"ip_src": pkt.ip_src,
"ip_dst_randomized": randomize_ip(pkt.ip_dst, self.config['randomization_key']),
"ip_options": pkt.ip_options if isinstance(pkt.ip_options, str) else ",".join(str(v) for v in pkt.ip_options),
"tcp_sport": pkt.tcp_sport,
"tcp_dport": pkt.tcp_dport,
"tcp_seq": pkt.tcp_seq,
"tcp_ack": pkt.tcp_ack,
"tcp_dataofs": pkt.tcp_dataofs,
"tcp_reserved": pkt.tcp_reserved,
"tcp_flags": pkt.tcp_flags,
"tcp_window": pkt.tcp_window,
"tcp_chksum": pkt.tcp_chksum,
"tcp_urgptr": pkt.tcp_urgptr,
"tcp_options": "",
"ext_dst_ip_country": self.external_info.get('country', 'unknown'),
"type": self.external_info.get('type', 'unknown'),
"ASN": self.external_info.get('ASN', 'unknown'),
"domain": self.external_info.get('domain', 'unknown'),
"city": self.external_info.get('city', 'unknown'),
"as_type": self.external_info.get('as_type', 'unknown'),
"ip_dst_is_private": check_ip_is_private(pkt.ip_dst),
"external_is_private": check_ip_is_private(self.external_info.get('queried_ip', '')),
"open_ports": "",
"previously_open_ports": "",
"interface": "pflog0",
"internal_ip_randomized": randomize_ip(pkt.ip_dst, self.config['randomization_key']),
"external_ip_randomized": randomize_ip(self.external_info.get('queried_ip', ''), self.config['randomization_key']),
"System_info": self.system_info.get('System', 'OPNsense'),
"Release_info": self.system_info.get('Release', 'unknown'),
"Version_info": self.system_info.get('Version', 'unknown'),
"Machine_info": self.system_info.get('Machine', 'unknown'),
"Total_Memory": self.system_info.get('Total_Memory', 'unknown'),
"processor": self.system_info.get('processor', 'unknown'),
"architecture": self.system_info.get('architecture', 'unknown'),
"honeypot_status": "False",
"payload": "N/A",
"ls_version": LS_VERSION
}
try:
self.upload_pipe.send(payload)
except Exception as e:
print(f"Error sending packet data: {e}", flush=True)
def _send_heartbeat(self):
"""Send heartbeat message."""
heartbeat = {
"db_name": "heartbeats",
"unwanted_db": self.config['database'],
"pkts_last_hb": self.packet_count,
"ext_dst_ip_country": self.external_info.get('country', 'unknown'),
"type": self.external_info.get('type', 'unknown'),
"ASN": self.external_info.get('ASN', 'unknown'),
"domain": self.external_info.get('domain', 'unknown'),
"city": self.external_info.get('city', 'unknown'),
"as_type": self.external_info.get('as_type', 'unknown'),
"external_is_private": check_ip_is_private(self.external_info.get('queried_ip', '')),
"open_ports": self.config['honeypot_ports'],
"previously_open_ports": "",
"interface": "pflog0",
"internal_ip_randomized": "",
"external_ip_randomized": randomize_ip(self.external_info.get('queried_ip', ''), self.config['randomization_key']),
"System_info": self.system_info.get('System', 'OPNsense'),
"Release_info": self.system_info.get('Release', 'unknown'),
"Version_info": self.system_info.get('Version', 'unknown'),
"Machine_info": self.system_info.get('Machine', 'unknown'),
"Total_Memory": self.system_info.get('Total_Memory', 'unknown'),
"processor": self.system_info.get('processor', 'unknown'),
"architecture": self.system_info.get('architecture', 'unknown'),
"open_honeypot_ports": self.config['honeypot_ports'],
"ls_version": LS_VERSION
}
try:
self.upload_pipe.send(heartbeat)
self.packet_count = 0
print("Sent heartbeat", flush=True)
except Exception as e:
print(f"Error sending heartbeat: {e}", flush=True)
def packet_handler(pflog_pipe, upload_pipe, config, external_info, system_info):
"""
Process packets from pflog reader and send to uploader.
This runs in its own process.
"""
processor = PacketProcessor(config, external_info, system_info, upload_pipe)
print("packet_handler: Started", flush=True)
# Send initial heartbeat at startup
processor._send_heartbeat()
# Start a background thread for periodic heartbeats
def heartbeat_loop():
while True:
time.sleep(processor.HEARTBEAT_INTERVAL)
processor._send_heartbeat()
hb_thread = threading.Thread(target=heartbeat_loop, daemon=True)
hb_thread.start()
while True:
try:
batch = pflog_pipe.recv()
processor.process_batch(batch)
except (EOFError, OSError):
print("packet_handler: Pipe closed, exiting", flush=True)
break
except Exception as e:
print(f"packet_handler: Error: {e}", flush=True)
time.sleep(1)
def main():
"""Main entry point for LightScope daemon."""
print(f"LightScope for OPNsense v{LS_VERSION}", flush=True)
print("Starting...", flush=True)
# Write PID file
pid = os.getpid()
try:
with open("/var/run/lightscope.pid", "w") as f:
f.write(str(pid))
except Exception as e:
print(f"Warning: Could not write PID file: {e}")
# Load configuration
config = LightScopeConfig()
config_dict = config.get_dict()
# Fetch external network info
print("Fetching external network information...", flush=True)
external_info = fetch_external_info()
print(f"External IP: {external_info.get('queried_ip', 'unknown')}", flush=True)
# Get system info
system_info = get_system_info()
# Create pipes
pflog_consumer, pflog_producer = multiprocessing.Pipe(duplex=False)
upload_consumer, upload_producer = multiprocessing.Pipe(duplex=False)
hp_upload_consumer, hp_upload_producer = multiprocessing.Pipe(duplex=False)
# Start processes
processes = []
# 1. pflog reader
p_pflog = multiprocessing.Process(
target=read_pflog,
args=(pflog_producer,),
name="pflog_reader"
)
p_pflog.start()
processes.append(p_pflog)
print("Started pflog reader", flush=True)
# 2. Packet handler
p_handler = multiprocessing.Process(
target=packet_handler,
args=(pflog_consumer, upload_producer, config_dict, external_info, system_info),
name="packet_handler"
)
p_handler.start()
processes.append(p_handler)
print("Started packet handler", flush=True)
# 3. Data uploader
p_uploader = multiprocessing.Process(
target=send_data,
args=(upload_consumer,),
name="uploader"
)
p_uploader.start()
processes.append(p_uploader)
print("Started uploader", flush=True)
# 4. Honeypot uploader
p_hp_uploader = multiprocessing.Process(
target=send_honeypot_data,
args=(hp_upload_consumer,),
name="honeypot_uploader"
)
p_hp_uploader.start()
processes.append(p_hp_uploader)
print("Started honeypot uploader", flush=True)
# 5. Honeypot listener
if config_dict.get('honeypot_ports'):
p_honeypot = multiprocessing.Process(
target=run_honeypot,
args=(config_dict, hp_upload_producer, config_dict['database']),
name="honeypot"
)
p_honeypot.start()
processes.append(p_honeypot)
print("Started honeypot listener", flush=True)
else:
print("Honeypot disabled (no ports configured)", flush=True)
# Signal handler for clean shutdown
def shutdown(signum, frame):
print("\nShutting down...", flush=True)
for p in processes:
if p.is_alive():
p.terminate()
for p in processes:
p.join(timeout=5)
try:
os.remove("/var/run/lightscope.pid")
except:
pass
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
print("LightScope is running. Press Ctrl+C to stop.", flush=True)
# Monitor processes
while True:
time.sleep(60)
for p in processes:
if not p.is_alive():
print(f"Process {p.name} died, restarting...", flush=True)
# For now, just exit and let the service manager restart us
shutdown(None, None)
if __name__ == "__main__":
multiprocessing.freeze_support()
main()

View file

@ -1,320 +0,0 @@
#!/usr/local/bin/python3
"""
pflog_reader.py - Captures blocked TCP SYN packets from pflog0 interface
This module reads packets from the pflog0 interface (pf firewall log)
and extracts TCP SYN packets that were blocked by the firewall.
"""
import struct
import socket
import datetime
import time
import sys
from collections import namedtuple, deque
import threading
# Try to import pcap library (python-libpcap on FreeBSD)
try:
import pcap
except ImportError:
print("Error: pypcap not found. Install with: pkg install py311-pypcap", file=sys.stderr)
sys.exit(1)
try:
import dpkt
except ImportError:
print("Error: dpkt not found. Install with: pkg install py311-dpkt", file=sys.stderr)
sys.exit(1)
# pflog header structure (FreeBSD)
# See /usr/include/net/if_pflog.h
# Structure size: 1+1+1+1+16+16+4+4+4+4+4+4+1+3+4+1+3 = 72 bytes
PFLOG_HDRLEN = 72 # FreeBSD pflog header length
# pflog action values (from /usr/include/netpfil/pf/pf.h)
PF_PASS = 0
PF_DROP = 1
# PacketInfo namedtuple - compatible with lightscope_core.py
PacketInfo = namedtuple("PacketInfo", [
"packet_num", "proto", "packet_time",
"ip_version", "ip_ihl", "ip_tos", "ip_len", "ip_id", "ip_flags", "ip_frag",
"ip_ttl", "ip_proto", "ip_chksum", "ip_src", "ip_dst", "ip_options",
"tcp_sport", "tcp_dport", "tcp_seq", "tcp_ack", "tcp_dataofs",
"tcp_reserved", "tcp_flags", "tcp_window", "tcp_chksum", "tcp_urgptr", "tcp_options"
])
def tcp_flags_to_str(flags_value):
"""Convert dpkt TCP flags value to a comma-separated string."""
flag_names = []
if flags_value & dpkt.tcp.TH_FIN:
flag_names.append("FIN")
if flags_value & dpkt.tcp.TH_SYN:
flag_names.append("SYN")
if flags_value & dpkt.tcp.TH_RST:
flag_names.append("RST")
if flags_value & dpkt.tcp.TH_PUSH:
flag_names.append("PSH")
if flags_value & dpkt.tcp.TH_ACK:
flag_names.append("ACK")
if flags_value & dpkt.tcp.TH_URG:
flag_names.append("URG")
return ",".join(flag_names) if flag_names else ""
def parse_pflog_packet(buf, packet_num):
"""
Parse a pflog packet and extract TCP SYN information.
pflog packets have a pflog header followed by the IP packet.
We only care about blocked TCP SYN packets.
Returns PacketInfo namedtuple or None if not a blocked TCP SYN.
"""
if len(buf) < PFLOG_HDRLEN:
return None
# Extract action from pflog header (byte offset 2)
# Only process blocked packets (PF_DROP)
action = buf[2]
if action != PF_DROP:
return None
# Skip pflog header to get to IP packet
ip_data = buf[PFLOG_HDRLEN:]
if len(ip_data) < 20: # Minimum IP header length
return None
# Check IP version
ip_version = (ip_data[0] >> 4) & 0xF
if ip_version == 4:
try:
ip = dpkt.ip.IP(ip_data)
except Exception:
return None
# Only process TCP
if ip.p != dpkt.ip.IP_PROTO_TCP:
return None
try:
tcp = ip.data
if not isinstance(tcp, dpkt.tcp.TCP):
tcp = dpkt.tcp.TCP(ip.data)
except Exception:
return None
# Only capture SYN packets (SYN flag set, ACK flag not set)
if not (tcp.flags & dpkt.tcp.TH_SYN) or (tcp.flags & dpkt.tcp.TH_ACK):
return None
# Extract IP flags
flags = []
if ip.df:
flags.append("DF")
if ip.mf:
flags.append("MF")
ip_flags_str = ",".join(flags) if flags else ""
ip_opts = ip.opts.hex() if ip.opts else ""
return PacketInfo(
packet_num=packet_num,
proto="TCP",
packet_time=datetime.datetime.now().timestamp(),
ip_version=ip.v,
ip_ihl=ip.hl,
ip_tos=ip.tos,
ip_len=ip.len,
ip_id=ip.id,
ip_flags=ip_flags_str,
ip_frag=ip.offset >> 3,
ip_ttl=ip.ttl,
ip_proto=ip.p,
ip_chksum=ip.sum,
ip_src=socket.inet_ntoa(ip.src),
ip_dst=socket.inet_ntoa(ip.dst),
ip_options=ip_opts,
tcp_sport=tcp.sport,
tcp_dport=tcp.dport,
tcp_seq=tcp.seq,
tcp_ack=tcp.ack,
tcp_dataofs=tcp.off * 4,
tcp_reserved=0,
tcp_flags=tcp_flags_to_str(tcp.flags),
tcp_window=tcp.win,
tcp_chksum=tcp.sum,
tcp_urgptr=tcp.urp,
tcp_options=tcp.opts
)
elif ip_version == 6:
try:
ip6 = dpkt.ip6.IP6(ip_data)
except Exception:
return None
# Only process TCP
if ip6.nxt != dpkt.ip.IP_PROTO_TCP:
return None
try:
tcp = ip6.data
if not isinstance(tcp, dpkt.tcp.TCP):
tcp = dpkt.tcp.TCP(ip6.data)
except Exception:
return None
# Only capture SYN packets
if not (tcp.flags & dpkt.tcp.TH_SYN) or (tcp.flags & dpkt.tcp.TH_ACK):
return None
return PacketInfo(
packet_num=packet_num,
proto="TCP",
packet_time=datetime.datetime.now().timestamp(),
ip_version=6,
ip_ihl=None,
ip_tos=None,
ip_len=ip6.plen,
ip_id=None,
ip_flags="",
ip_frag=0,
ip_ttl=ip6.hlim,
ip_proto=ip6.nxt,
ip_chksum=None,
ip_src=socket.inet_ntop(socket.AF_INET6, ip6.src),
ip_dst=socket.inet_ntop(socket.AF_INET6, ip6.dst),
ip_options="",
tcp_sport=tcp.sport,
tcp_dport=tcp.dport,
tcp_seq=tcp.seq,
tcp_ack=tcp.ack,
tcp_dataofs=tcp.off * 4,
tcp_reserved=0,
tcp_flags=tcp_flags_to_str(tcp.flags),
tcp_window=tcp.win,
tcp_chksum=tcp.sum,
tcp_urgptr=tcp.urp,
tcp_options=tcp.opts
)
return None
def read_pflog(output_pipe, interface="pflog0"):
"""
Main pflog reader function.
Captures packets from pflog0 interface, parses TCP SYNs,
and sends them to output_pipe in batches.
Args:
output_pipe: multiprocessing.Pipe connection for sending packet batches
interface: Network interface to capture from (default: pflog0)
"""
BATCH_SIZE = 100
IDLE_FLUSH_SECS = 1.0
MAX_QUEUE_SIZE = 10000
send_deque = deque(maxlen=MAX_QUEUE_SIZE)
last_activity = time.monotonic()
def sender_thread():
"""Thread to batch and send packets."""
nonlocal last_activity
prior_time = time.monotonic()
packets_sent = 0
while True:
now = time.monotonic()
to_send = 0
# Log stats every second
if (now - prior_time) >= 1.0:
if packets_sent > 0:
print(f"pflog_reader: sent {packets_sent} packets, queue={len(send_deque)}", flush=True)
packets_sent = 0
prior_time = now
# Check if we should send a batch
if len(send_deque) >= BATCH_SIZE:
to_send = BATCH_SIZE
elif send_deque and (now - last_activity) >= IDLE_FLUSH_SECS:
to_send = len(send_deque)
else:
time.sleep(0.01)
continue
# Build and send batch
batch = [send_deque.popleft() for _ in range(to_send)]
try:
output_pipe.send(batch)
packets_sent += len(batch)
except Exception as e:
print(f"pflog_reader: pipe send error: {e}", file=sys.stderr)
return
# Start sender thread
threading.Thread(target=sender_thread, daemon=True).start()
# Open pflog0 for capture
try:
# DLT_PFLOG = 117 on FreeBSD
sniffer = pcap.pcap(
name=interface,
snaplen=65535,
promisc=False,
immediate=True,
timeout_ms=100
)
# Filter for TCP only
sniffer.setfilter("tcp")
except Exception as e:
print(f"pflog_reader: Failed to open {interface}: {e}", file=sys.stderr)
sys.exit(1)
print(f"pflog_reader: Capturing on {interface}", flush=True)
packet_num = 0
for ts, buf in sniffer:
packet_num += 1
try:
pkt_info = parse_pflog_packet(buf, packet_num)
if pkt_info:
send_deque.append(pkt_info)
last_activity = time.monotonic()
except Exception as e:
# Skip malformed packets
continue
if __name__ == "__main__":
# Test mode - print captured packets
print("pflog_reader: Running in test mode (printing to stdout)")
try:
sniffer = pcap.pcap(
name="pflog0",
snaplen=65535,
promisc=False,
immediate=True,
timeout_ms=1000
)
sniffer.setfilter("tcp")
except Exception as e:
print(f"Failed to open pflog0: {e}")
sys.exit(1)
packet_num = 0
for ts, buf in sniffer:
packet_num += 1
pkt_info = parse_pflog_packet(buf, packet_num)
if pkt_info:
print(f"SYN: {pkt_info.ip_src}:{pkt_info.tcp_sport} -> {pkt_info.ip_dst}:{pkt_info.tcp_dport}")

View file

@ -1,234 +0,0 @@
#!/usr/local/bin/python3
"""
uploader.py - Data upload module for LightScope
Handles batching and uploading packet data and heartbeats to thelightscope.com
"""
import time
import threading
import sys
from collections import deque
try:
import requests
from requests.adapters import HTTPAdapter
except ImportError:
print("Error: requests not found. Install with: pkg install py311-requests", file=sys.stderr)
sys.exit(1)
# API Configuration
DATA_URL = "https://thelightscope.com/log_mysql_data"
HEARTBEAT_URL = "https://thelightscope.com/heartbeat"
HEADERS = {
"Content-Type": "application/json",
"X-API-Key": "lightscopeAPIkey2025_please_dont_distribute_me_but_im_write_only_anyways"
}
# Upload settings
BATCH_SIZE = 600
IDLE_FLUSH_SEC = 5.0
RETRY_BACKOFF = 5
MAX_QUEUE_SIZE = 100000
def send_data(consumer_pipe):
"""
Main data upload function.
Receives data from consumer_pipe, batches it, and uploads to thelightscope.com.
Handles heartbeat messages separately.
Args:
consumer_pipe: multiprocessing.Pipe connection to receive data from
"""
session = requests.Session()
adapter = HTTPAdapter(pool_connections=4, pool_maxsize=4)
session.mount("https://", adapter)
session.mount("http://", adapter)
queue = deque(maxlen=MAX_QUEUE_SIZE)
last_activity = time.monotonic()
stop_event = threading.Event()
def reader():
"""Thread to read from pipe and add to queue."""
nonlocal last_activity
while not stop_event.is_set():
try:
item = consumer_pipe.recv()
queue.append(item)
last_activity = time.monotonic()
except (EOFError, OSError):
stop_event.set()
break
except Exception as e:
print(f"uploader: Error receiving data: {e}", flush=True)
# Drain any remaining data
while consumer_pipe.poll(0):
try:
queue.append(consumer_pipe.recv())
except:
break
# Start reader thread
reader_thread = threading.Thread(target=reader, daemon=True)
reader_thread.start()
print("uploader: Started data upload service", flush=True)
try:
while not stop_event.is_set() or queue:
# 1) Process heartbeats first
hb_count = 0
n = len(queue)
for _ in range(n):
try:
item = queue.popleft()
except IndexError:
break
if item.get("db_name") == "heartbeats":
hb_count += 1
try:
resp = session.post(
HEARTBEAT_URL,
json=item,
headers=HEADERS,
timeout=10
)
if resp.status_code != 200:
print(f"uploader: Heartbeat rejected ({resp.status_code})", flush=True)
except requests.RequestException as e:
print(f"uploader: Heartbeat error: {e}", flush=True)
else:
# Put non-heartbeat items back
queue.append(item)
if hb_count:
print(f"uploader: Sent {hb_count} heartbeat(s)", flush=True)
# 2) Batch and send regular data
now = time.monotonic()
elapsed = now - last_activity
if queue and (len(queue) >= BATCH_SIZE or elapsed >= IDLE_FLUSH_SEC):
to_send = min(len(queue), BATCH_SIZE)
batch = [queue.popleft() for _ in range(to_send)]
try:
resp = session.post(
DATA_URL,
json={"batch": batch},
headers=HEADERS,
timeout=10
)
if resp.status_code == 200:
print(f"uploader: Sent {to_send} items", flush=True)
else:
print(f"uploader: Upload rejected ({resp.status_code})", flush=True)
last_activity = time.monotonic()
except requests.RequestException as e:
print(f"uploader: Upload error, will retry: {e}", flush=True)
# Put items back at front of queue
for item in reversed(batch):
queue.appendleft(item)
time.sleep(RETRY_BACKOFF)
else:
time.sleep(0.1)
except KeyboardInterrupt:
print("uploader: Shutting down...", flush=True)
finally:
stop_event.set()
reader_thread.join(timeout=2)
print("uploader: Stopped", flush=True)
def send_honeypot_data(consumer_pipe):
"""
Honeypot-specific data upload function.
Similar to send_data but with smaller batch sizes for honeypot data.
Args:
consumer_pipe: multiprocessing.Pipe connection to receive honeypot data from
"""
session = requests.Session()
adapter = HTTPAdapter(pool_connections=2, pool_maxsize=2)
session.mount("https://", adapter)
session.mount("http://", adapter)
queue = deque(maxlen=MAX_QUEUE_SIZE)
last_activity = time.monotonic()
stop_event = threading.Event()
HONEYPOT_BATCH_SIZE = 100
def reader():
nonlocal last_activity
while not stop_event.is_set():
try:
item = consumer_pipe.recv()
queue.append(item)
last_activity = time.monotonic()
except (EOFError, OSError):
stop_event.set()
break
except Exception as e:
print(f"honeypot_uploader: Error receiving data: {e}", flush=True)
while consumer_pipe.poll(0):
try:
queue.append(consumer_pipe.recv())
except:
break
reader_thread = threading.Thread(target=reader, daemon=True)
reader_thread.start()
print("honeypot_uploader: Started", flush=True)
try:
while not stop_event.is_set() or queue:
now = time.monotonic()
elapsed = now - last_activity
if queue and (len(queue) >= HONEYPOT_BATCH_SIZE or elapsed >= IDLE_FLUSH_SEC):
to_send = min(len(queue), HONEYPOT_BATCH_SIZE)
batch = [queue.popleft() for _ in range(to_send)]
try:
resp = session.post(
DATA_URL,
json={"batch": batch},
headers=HEADERS,
timeout=10
)
if resp.status_code == 200:
print(f"honeypot_uploader: Sent {to_send} items", flush=True)
else:
print(f"honeypot_uploader: Upload rejected ({resp.status_code})", flush=True)
last_activity = time.monotonic()
except requests.RequestException as e:
print(f"honeypot_uploader: Error, will retry: {e}", flush=True)
for item in reversed(batch):
queue.appendleft(item)
time.sleep(RETRY_BACKOFF)
else:
time.sleep(0.1)
except KeyboardInterrupt:
pass
finally:
stop_event.set()
reader_thread.join(timeout=2)
print("honeypot_uploader: Stopped", flush=True)
if __name__ == "__main__":
print("uploader: This module should be run as part of lightscope_daemon.py")