mirror of
https://github.com/opnsense/plugins.git
synced 2026-04-21 06:06:59 -04:00
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:
parent
3a35e2a259
commit
86ccd7bb46
8 changed files with 3 additions and 1579 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
# LightScope for OPNsense
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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")
|
||||
Loading…
Reference in a new issue