released v1.3.5

This commit is contained in:
Pari Malam
2026-06-27 15:49:28 +08:00
parent ca6a805223
commit 476e72a778
2 changed files with 121 additions and 47 deletions
+12 -8
View File
@@ -27,7 +27,7 @@ KID_PATCH_OFFSET="${KID_PATCH_OFFSET:-0x15625cd}"
PORT="${1:-1337}" PORT="${1:-1337}"
VERBOSE="${2:-2}" VERBOSE="${2:-2}"
BIND="${BIND:-127.0.0.1}" BIND="${BIND:-0.0.0.0}"
GOMEMLIMIT="${GOMEMLIMIT:-2GiB}" GOMEMLIMIT="${GOMEMLIMIT:-2GiB}"
KEEP_FALSE=true KEEP_FALSE=true
@@ -194,20 +194,24 @@ if [ "$MONITOR" = "true" ]; then
--log "logs/audit.log" --log "logs/audit.log"
--alerts "logs/audit_alerts.log" --alerts "logs/audit_alerts.log"
) )
if [ "$HLS_PROXY" = "true" ]; then
MONITOR_CMD+=(--hls-target "127.0.0.1:$HLS_PROXY_PORT")
fi
if [ -n "$O11_PID" ] && kill -0 "$O11_PID" 2>/dev/null; then if [ -n "$O11_PID" ] && kill -0 "$O11_PID" 2>/dev/null; then
MONITOR_CMD+=(--pid "$O11_PID") MONITOR_CMD+=(--pid "$O11_PID")
fi fi
[ -n "${MONITOR_ARGS:-}" ] && MONITOR_CMD+=($MONITOR_ARGS) [ -n "${MONITOR_ARGS:-}" ] && MONITOR_CMD+=($MONITOR_ARGS)
echo
echo -e " ${GRN}\xE2\x96\xB6${RST} ${BLD}Security monitor${RST} on :${MONITOR_PORT}" echo -e " ${GRN}\xE2\x96\xB6${RST} ${BLD}Security monitor${RST} on :${MONITOR_PORT}"
echo -e " ${DIM} Web UI http://${BIND}:${MONITOR_PORT}${RST}" "${MONITOR_CMD[@]}" &
echo -e " ${DIM} Audit log logs/audit.log${RST}" _BG_PIDS="$_BG_PIDS $!"
echo ok "monitor" "PID $!"
echo -e " ${DIM}Press Ctrl+C to stop all services${RST}"
echo
exec "${MONITOR_CMD[@]}" echo
echo -e " ${DIM} Web UI http://${BIND}:${PORT}${RST}"
echo -e " ${DIM} HLS proxy http://${BIND}:${HLS_PROXY_PORT}/channel/{name}/master.m3u8${RST}"
echo -e " ${DIM} Monitor http://${BIND}:${MONITOR_PORT} (optional, with HTTP inspection)${RST}"
echo -e " ${DIM} Audit log logs/audit.log${RST}"
fi fi
echo echo
+98 -28
View File
@@ -7,6 +7,7 @@ import json
import socket import socket
import struct import struct
import signal import signal
import ipaddress
import argparse import argparse
import threading import threading
import subprocess import subprocess
@@ -27,6 +28,32 @@ EXFIL_THRESHOLD = 100 * 1024 * 1024
MAX_LOG_SIZE = 100 * 1024 * 1024 MAX_LOG_SIZE = 100 * 1024 * 1024
MAX_LOG_FILES = 5 MAX_LOG_FILES = 5
TRUSTED_NETWORKS = [
'127.0.0.0/8',
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'::1/128',
]
ATTACKER_IPS = set()
def parse_xforwarded_for(header_value):
if not header_value:
return None
ips = [ip.strip() for ip in header_value.split(',')]
return ips[0] if ips else None
def is_trusted_ip(ip_str):
try:
addr = ipaddress.ip_address(ip_str)
for net in TRUSTED_NETWORKS:
if addr in ipaddress.ip_network(net, strict=False):
return True
return False
except ValueError:
return True
ATTACK_SIGNATURES = { ATTACK_SIGNATURES = {
"command_injection": [ "command_injection": [
# Shell metacharacters # Shell metacharacters
@@ -319,8 +346,7 @@ class AttackDetector:
compiled.append((re.compile(p, re.IGNORECASE), category)) compiled.append((re.compile(p, re.IGNORECASE), category))
self.compiled[category] = compiled self.compiled[category] = compiled
def scan_string(self, text, source='http', context=''): def scan_string(self, text, source='http', context='', client_ip=None):
"""Scan a string for attack patterns. Returns list of matches."""
if not text: if not text:
return [] return []
text_str = text if isinstance(text, str) else text.decode('utf-8', errors='replace') text_str = text if isinstance(text, str) else text.decode('utf-8', errors='replace')
@@ -328,11 +354,18 @@ class AttackDetector:
for category, patterns in self.compiled.items(): for category, patterns in self.compiled.items():
if category in ('suspicious_file_access', 'suspicious_process'): if category in ('suspicious_file_access', 'suspicious_process'):
continue # these are handled separately continue
for regex, desc in patterns: for regex, desc in patterns:
for m in regex.finditer(text_str): for m in regex.finditer(text_str):
severity = self._severity(category) severity = self._severity(category)
details = f"{desc}: matched '{m.group()[:80]}' in {context}" details = f"{desc}: matched '{m.group()[:80]}' in {context}"
if client_ip:
details += f" [client: {client_ip}"
if not is_trusted_ip(client_ip):
details += " EXTERNAL"
details += "]"
if not is_trusted_ip(client_ip):
ATTACKER_IPS.add(client_ip)
self.logger.log(f"attack_{category}", severity, source, details, text_str[:500]) self.logger.log(f"attack_{category}", severity, source, details, text_str[:500])
matches.append((category, desc, m.group())) matches.append((category, desc, m.group()))
@@ -350,23 +383,20 @@ class AttackDetector:
} }
return severities.get(category, 'MEDIUM') return severities.get(category, 'MEDIUM')
def scan_url(self, url, source='http'): def scan_url(self, url, source='http', client_ip=None):
"""Scan a URL for attacks.""" return self.scan_string(url, source, f'URL: {url[:100]}', client_ip)
return self.scan_string(url, source, f'URL: {url[:100]}')
def scan_headers(self, headers, source='http'): def scan_headers(self, headers, source='http', client_ip=None):
"""Scan HTTP headers for attacks."""
for key, value in headers.items(): for key, value in headers.items():
self.scan_string(value, source, f'header {key}') self.scan_string(value, source, f'header {key}', client_ip)
def scan_body(self, body, source='http'): def scan_body(self, body, source='http', client_ip=None):
"""Scan HTTP body for attacks."""
if isinstance(body, bytes): if isinstance(body, bytes):
try: try:
body = body.decode('utf-8', errors='replace') body = body.decode('utf-8', errors='replace')
except: except:
return [] return []
return self.scan_string(body, source, 'request body') return self.scan_string(body, source, 'request body', client_ip)
class ProcessMonitor: class ProcessMonitor:
@@ -600,27 +630,42 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
logger = None logger = None
target_host = '127.0.0.1' target_host = '127.0.0.1'
target_port = 19999 target_port = 19999
hls_host = None
hls_port = None
def _route_target(self):
if self.hls_host and self.hls_port and self.path.startswith(('/channel/', '/m/', '/s/')):
return self.hls_host, self.hls_port
return self.target_host, self.target_port
def log_message(self, format, *args): def log_message(self, format, *args):
pass # suppress default logging pass
def _forward(self, method): def _forward(self, method):
"""Forward request to target and scan for attacks."""
content_length = int(self.headers.get('Content-Length', 0)) content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length) if content_length > 0 else b'' body = self.rfile.read(content_length) if content_length > 0 else b''
self.logger.log('http_request', 'INFO', 'http', real_client_ip = parse_xforwarded_for(self.headers.get('X-Forwarded-For'))
f'{method} {self.path} from {self.client_address[0]}') if not real_client_ip:
self.detector.scan_url(self.path, 'http') real_client_ip = self.client_address[0]
self.detector.scan_headers(dict(self.headers), 'http') is_external = not is_trusted_ip(real_client_ip)
if body:
self.detector.scan_body(body, 'http')
self.logger.log('http_request', 'INFO', 'http',
f'{method} {self.path} from {real_client_ip} (proxy-conn: {self.client_address[0]})')
if is_external:
self.logger.log('external_request', 'MEDIUM', 'http',
f'External request from {real_client_ip}: {method} {self.path}')
self.detector.scan_url(self.path, 'http', client_ip=real_client_ip)
self.detector.scan_headers(dict(self.headers), 'http', client_ip=real_client_ip)
if body:
self.detector.scan_body(body, 'http', client_ip=real_client_ip)
dst_host, dst_port = self._route_target()
try: try:
conn = http.client.HTTPConnection(self.target_host, self.target_port, timeout=30) conn = http.client.HTTPConnection(dst_host, dst_port, timeout=30)
headers = dict(self.headers) headers = dict(self.headers)
headers['Host'] = f'{self.target_host}:{self.target_port}' headers['Host'] = f'{dst_host}:{dst_port}'
conn.request(method, self.path, body=body, headers=headers) conn.request(method, self.path, body=body, headers=headers)
response = conn.getresponse() response = conn.getresponse()
resp_body = response.read() resp_body = response.read()
@@ -671,15 +716,21 @@ class ThreadedProxyServer(ThreadingMixIn, HTTPServer):
allow_reuse_address = True allow_reuse_address = True
def run_proxy(port, target_host, target_port, detector, logger): def run_proxy(port, target_host, target_port, detector, logger, hls_target=None):
"""Run the HTTP proxy server."""
ProxyRequestHandler.detector = detector ProxyRequestHandler.detector = detector
ProxyRequestHandler.logger = logger ProxyRequestHandler.logger = logger
ProxyRequestHandler.target_host = target_host ProxyRequestHandler.target_host = target_host
ProxyRequestHandler.target_port = target_port ProxyRequestHandler.target_port = target_port
if hls_target:
hls_host, _, hls_port_str = hls_target.partition(':')
ProxyRequestHandler.hls_host = hls_host
ProxyRequestHandler.hls_port = int(hls_port_str)
server = ThreadedProxyServer(('0.0.0.0', port), ProxyRequestHandler) server = ThreadedProxyServer(('0.0.0.0', port), ProxyRequestHandler)
routes = f'o11pro API -> {target_host}:{target_port}'
if hls_target:
routes += f', HLS -> {hls_target}'
logger.log('proxy_start', 'INFO', 'http', logger.log('proxy_start', 'INFO', 'http',
f'Proxy listening on 0.0.0.0:{port}, forwarding to {target_host}:{target_port}') f'Proxy listening on 0.0.0.0:{port}, {routes}')
server.serve_forever() server.serve_forever()
@@ -786,6 +837,7 @@ def find_o11_pid():
except (FileNotFoundError, ValueError): except (FileNotFoundError, ValueError):
pass pass
try:
for entry in os.listdir('/proc'): for entry in os.listdir('/proc'):
if not entry.isdigit(): if not entry.isdigit():
continue continue
@@ -797,6 +849,8 @@ def find_o11_pid():
return int(entry) return int(entry)
except (FileNotFoundError, PermissionError): except (FileNotFoundError, PermissionError):
pass pass
except FileNotFoundError:
pass
return None return None
@@ -812,8 +866,21 @@ def main():
parser.add_argument('--no-proc', action='store_true', help='Disable process monitoring') parser.add_argument('--no-proc', action='store_true', help='Disable process monitoring')
parser.add_argument('--no-files', action='store_true', help='Disable file watching') parser.add_argument('--no-files', action='store_true', help='Disable file watching')
parser.add_argument('--once', action='store_true', help='Run one scan and exit') parser.add_argument('--once', action='store_true', help='Run one scan and exit')
parser.add_argument('--trusted-networks', default='',
help='Comma-separated CIDR ranges of your own network (e.g. "10.0.0.0/8,192.168.0.0/16"). IPs outside these ranges are treated as external attackers.')
parser.add_argument('--hls-target', default='',
help='HLS proxy backend host:port (e.g. "127.0.0.1:1338"). Requests to /channel/, /m/, /s/ will be forwarded here.')
args = parser.parse_args() args = parser.parse_args()
if args.trusted_networks:
custom_nets = [n.strip() for n in args.trusted_networks.split(',') if n.strip()]
for net in custom_nets:
try:
ipaddress.ip_network(net, strict=False)
TRUSTED_NETWORKS.append(net)
except ValueError as e:
print(f"Invalid trusted network CIDR '{net}': {e}", file=sys.stderr)
logger = AuditLogger(args.log, args.alerts) logger = AuditLogger(args.log, args.alerts)
detector = AttackDetector(logger) detector = AttackDetector(logger)
@@ -825,12 +892,15 @@ def main():
if args.proxy_mode: if args.proxy_mode:
proxy_thread = threading.Thread( proxy_thread = threading.Thread(
target=run_proxy, target=run_proxy,
args=(args.proxy_port, '127.0.0.1', args.target_port, detector, logger), args=(args.proxy_port, '127.0.0.1', args.target_port, detector, logger,
args.hls_target or None),
daemon=True daemon=True
) )
proxy_thread.start() proxy_thread.start()
logger.log('startup', 'INFO', 'main', msg = f'Proxy mode active on :{args.proxy_port}, forwarding to :{args.target_port}'
f'Proxy mode active point your client to :{args.proxy_port} instead of :{args.target_port}') if args.hls_target:
msg += f' (HLS -> {args.hls_target})'
logger.log('startup', 'INFO', 'main', msg)
pid = args.pid or find_o11_pid() pid = args.pid or find_o11_pid()
if not pid: if not pid: