feat(mitmproxy): Add transparent mode, filtering addon, and whitelist
- Add nftables transparent mode support with automatic REDIRECT rules - Create SecuBox Python filter addon for CDN/Media/Ad tracking - Add whitelist/bypass configuration for IPs and domains - Expand UCI config with transparent, whitelist, filtering sections - Update RPCD backend with new config methods and firewall control - Update LuCI settings view with all new configuration options - Add new API methods: firewall_setup, firewall_clear, list management Features: - Transparent proxy with nftables integration - CDN tracking (Cloudflare, Akamai, Fastly, etc.) - Media streaming tracking (YouTube, Netflix, Spotify) - Ad/tracker blocking - IP and domain whitelist bypass Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4e5d5275f9
commit
fe222d542c
@ -12,6 +12,26 @@ var callGetConfig = rpc.declare({
|
||||
method: 'get_config'
|
||||
});
|
||||
|
||||
var callGetTransparentConfig = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'get_transparent_config'
|
||||
});
|
||||
|
||||
var callGetWhitelistConfig = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'get_whitelist_config'
|
||||
});
|
||||
|
||||
var callGetFilteringConfig = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'get_filtering_config'
|
||||
});
|
||||
|
||||
var callGetAllConfig = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'get_all_config'
|
||||
});
|
||||
|
||||
var callGetStats = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'get_stats'
|
||||
@ -20,7 +40,7 @@ var callGetStats = rpc.declare({
|
||||
var callGetRequests = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'get_requests',
|
||||
params: ['limit']
|
||||
params: ['limit', 'category']
|
||||
});
|
||||
|
||||
var callGetTopHosts = rpc.declare({
|
||||
@ -49,12 +69,34 @@ var callServiceRestart = rpc.declare({
|
||||
method: 'service_restart'
|
||||
});
|
||||
|
||||
var callFirewallSetup = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'firewall_setup'
|
||||
});
|
||||
|
||||
var callFirewallClear = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'firewall_clear'
|
||||
});
|
||||
|
||||
var callSetConfig = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'set_config',
|
||||
params: ['key', 'value']
|
||||
});
|
||||
|
||||
var callAddToList = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'add_to_list',
|
||||
params: ['key', 'value']
|
||||
});
|
||||
|
||||
var callRemoveFromList = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'remove_from_list',
|
||||
params: ['key', 'value']
|
||||
});
|
||||
|
||||
var callClearData = rpc.declare({
|
||||
object: 'luci.mitmproxy',
|
||||
method: 'clear_data'
|
||||
@ -73,14 +115,45 @@ return baseclass.extend({
|
||||
});
|
||||
},
|
||||
|
||||
getStats: function() {
|
||||
return callGetStats().catch(function() {
|
||||
return { total_requests: 0, unique_hosts: 0, flow_file_size: 0 };
|
||||
getTransparentConfig: function() {
|
||||
return callGetTransparentConfig().catch(function() {
|
||||
return { enabled: false };
|
||||
});
|
||||
},
|
||||
|
||||
getRequests: function(limit) {
|
||||
return callGetRequests(limit || 50).catch(function() {
|
||||
getWhitelistConfig: function() {
|
||||
return callGetWhitelistConfig().catch(function() {
|
||||
return { enabled: true, bypass_ip: [], bypass_domain: [] };
|
||||
});
|
||||
},
|
||||
|
||||
getFilteringConfig: function() {
|
||||
return callGetFilteringConfig().catch(function() {
|
||||
return { enabled: false };
|
||||
});
|
||||
},
|
||||
|
||||
getAllConfig: function() {
|
||||
return callGetAllConfig().catch(function() {
|
||||
return { main: {}, transparent: {}, whitelist: {}, filtering: {} };
|
||||
});
|
||||
},
|
||||
|
||||
getStats: function() {
|
||||
return callGetStats().catch(function() {
|
||||
return {
|
||||
total_requests: 0,
|
||||
unique_hosts: 0,
|
||||
flow_file_size: 0,
|
||||
cdn_requests: 0,
|
||||
media_requests: 0,
|
||||
blocked_ads: 0
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getRequests: function(limit, category) {
|
||||
return callGetRequests(limit || 50, category || 'all').catch(function() {
|
||||
return { requests: [] };
|
||||
});
|
||||
},
|
||||
@ -109,10 +182,26 @@ return baseclass.extend({
|
||||
return callServiceRestart();
|
||||
},
|
||||
|
||||
firewallSetup: function() {
|
||||
return callFirewallSetup();
|
||||
},
|
||||
|
||||
firewallClear: function() {
|
||||
return callFirewallClear();
|
||||
},
|
||||
|
||||
setConfig: function(key, value) {
|
||||
return callSetConfig(key, value);
|
||||
},
|
||||
|
||||
addToList: function(key, value) {
|
||||
return callAddToList(key, value);
|
||||
},
|
||||
|
||||
removeFromList: function(key, value) {
|
||||
return callRemoveFromList(key, value);
|
||||
},
|
||||
|
||||
clearData: function() {
|
||||
return callClearData();
|
||||
},
|
||||
@ -121,14 +210,15 @@ return baseclass.extend({
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
self.getStatus(),
|
||||
self.getConfig(),
|
||||
self.getAllConfig(),
|
||||
self.getStats(),
|
||||
self.getTopHosts(10),
|
||||
self.getCaInfo()
|
||||
]).then(function(results) {
|
||||
return {
|
||||
status: results[0],
|
||||
config: results[1],
|
||||
config: results[1].main || results[1],
|
||||
allConfig: results[1],
|
||||
stats: results[2],
|
||||
topHosts: results[3],
|
||||
caInfo: results[4]
|
||||
|
||||
@ -45,9 +45,11 @@ return view.extend({
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('mitmproxy', _('mitmproxy Settings'),
|
||||
_('Configure the mitmproxy HTTPS interception proxy.'));
|
||||
_('Configure the mitmproxy HTTPS interception proxy with transparent mode and filtering options.'));
|
||||
|
||||
// Main settings
|
||||
// =====================================================================
|
||||
// Main Proxy Configuration
|
||||
// =====================================================================
|
||||
s = m.section(form.TypedSection, 'mitmproxy', _('Proxy Configuration'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
@ -59,18 +61,13 @@ return view.extend({
|
||||
|
||||
o = s.option(form.ListValue, 'mode', _('Proxy Mode'),
|
||||
_('How clients connect to the proxy'));
|
||||
o.value('transparent', _('Transparent - Intercept traffic automatically'));
|
||||
o.value('regular', _('Regular - Clients must configure proxy settings'));
|
||||
o.value('transparent', _('Transparent - Intercept traffic automatically via nftables'));
|
||||
o.value('upstream', _('Upstream - Forward to another proxy'));
|
||||
o.default = 'transparent';
|
||||
o.value('reverse', _('Reverse - Reverse proxy mode'));
|
||||
o.default = 'regular';
|
||||
|
||||
o = s.option(form.Value, 'listen_host', _('Listen Address'),
|
||||
_('IP address to bind the proxy to'));
|
||||
o.default = '0.0.0.0';
|
||||
o.placeholder = '0.0.0.0';
|
||||
o.datatype = 'ipaddr';
|
||||
|
||||
o = s.option(form.Value, 'listen_port', _('Proxy Port'),
|
||||
o = s.option(form.Value, 'proxy_port', _('Proxy Port'),
|
||||
_('Port for HTTP/HTTPS interception'));
|
||||
o.default = '8080';
|
||||
o.placeholder = '8080';
|
||||
@ -88,10 +85,27 @@ return view.extend({
|
||||
o.placeholder = '8081';
|
||||
o.datatype = 'port';
|
||||
|
||||
o = s.option(form.Value, 'data_path', _('Data Path'),
|
||||
_('Directory for storing certificates and data'));
|
||||
o.default = '/srv/mitmproxy';
|
||||
|
||||
o = s.option(form.Value, 'memory_limit', _('Memory Limit'),
|
||||
_('Maximum memory for the LXC container'));
|
||||
o.default = '256M';
|
||||
o.placeholder = '256M';
|
||||
|
||||
o = s.option(form.Flag, 'ssl_insecure', _('Allow Insecure SSL'),
|
||||
_('Accept invalid/self-signed SSL certificates from upstream servers'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Flag, 'anticache', _('Anti-Cache'),
|
||||
_('Strip cache headers to force fresh responses'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Flag, 'anticomp', _('Anti-Compression'),
|
||||
_('Disable compression to allow content inspection'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.ListValue, 'flow_detail', _('Log Detail Level'),
|
||||
_('Amount of detail in flow logs'));
|
||||
o.value('0', _('Minimal'));
|
||||
@ -99,87 +113,141 @@ return view.extend({
|
||||
o.value('2', _('Full headers'));
|
||||
o.value('3', _('Full headers + body preview'));
|
||||
o.value('4', _('Full headers + full body'));
|
||||
o.default = '2';
|
||||
o.default = '1';
|
||||
|
||||
// Capture settings
|
||||
o = s.option(form.Value, 'upstream_proxy', _('Upstream Proxy'),
|
||||
_('Forward traffic to this upstream proxy (e.g., http://proxy:8080)'));
|
||||
o.depends('mode', 'upstream');
|
||||
o.placeholder = 'http://proxy:8080';
|
||||
|
||||
o = s.option(form.Value, 'reverse_target', _('Reverse Target'),
|
||||
_('Target server for reverse proxy mode (e.g., http://localhost:80)'));
|
||||
o.depends('mode', 'reverse');
|
||||
o.placeholder = 'http://localhost:80';
|
||||
|
||||
// =====================================================================
|
||||
// Transparent Mode Settings
|
||||
// =====================================================================
|
||||
s = m.section(form.TypedSection, 'transparent', _('Transparent Mode'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
s.tab('transparent', _('Firewall Settings'));
|
||||
|
||||
o = s.taboption('transparent', form.Flag, 'enabled', _('Enable Transparent Firewall'),
|
||||
_('Automatically setup nftables rules to redirect traffic'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.taboption('transparent', form.Value, 'interface', _('Intercept Interface'),
|
||||
_('Network interface to intercept traffic from'));
|
||||
o.default = 'br-lan';
|
||||
o.placeholder = 'br-lan';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.taboption('transparent', form.Flag, 'redirect_http', _('Redirect HTTP'),
|
||||
_('Intercept plain HTTP traffic'));
|
||||
o.default = '1';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.taboption('transparent', form.Flag, 'redirect_https', _('Redirect HTTPS'),
|
||||
_('Intercept HTTPS traffic (requires CA certificate on clients)'));
|
||||
o.default = '1';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.taboption('transparent', form.Value, 'http_port', _('HTTP Port'),
|
||||
_('Source port to intercept for HTTP'));
|
||||
o.default = '80';
|
||||
o.datatype = 'port';
|
||||
o.depends('redirect_http', '1');
|
||||
|
||||
o = s.taboption('transparent', form.Value, 'https_port', _('HTTPS Port'),
|
||||
_('Source port to intercept for HTTPS'));
|
||||
o.default = '443';
|
||||
o.datatype = 'port';
|
||||
o.depends('redirect_https', '1');
|
||||
|
||||
// =====================================================================
|
||||
// Whitelist/Bypass Settings
|
||||
// =====================================================================
|
||||
s = m.section(form.TypedSection, 'whitelist', _('Whitelist / Bypass'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Whitelist'),
|
||||
_('Skip interception for whitelisted IPs and domains'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.DynamicList, 'bypass_ip', _('Bypass IP Addresses'),
|
||||
_('IP addresses or CIDR ranges that bypass the proxy'));
|
||||
o.placeholder = '192.168.1.0/24';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.DynamicList, 'bypass_domain', _('Bypass Domains'),
|
||||
_('Domain patterns that bypass the proxy (for domain-based bypass, requires additional configuration)'));
|
||||
o.placeholder = 'banking.com';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
// =====================================================================
|
||||
// Filtering / CDN Tracking
|
||||
// =====================================================================
|
||||
s = m.section(form.TypedSection, 'filtering', _('Filtering & Analytics'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Filtering Addon'),
|
||||
_('Load the SecuBox filtering addon for CDN/Media tracking and ad blocking'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Flag, 'log_requests', _('Log All Requests'),
|
||||
_('Log request details to JSON file for analysis'));
|
||||
o.default = '1';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Flag, 'filter_cdn', _('Track CDN Traffic'),
|
||||
_('Log and categorize CDN requests (Cloudflare, Akamai, Fastly, etc.)'));
|
||||
o.default = '0';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Flag, 'filter_media', _('Track Media Streaming'),
|
||||
_('Log and categorize streaming media requests (YouTube, Netflix, Spotify, etc.)'));
|
||||
o.default = '0';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Flag, 'block_ads', _('Block Ads & Trackers'),
|
||||
_('Block known advertising and tracking domains'));
|
||||
o.default = '0';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'addon_script', _('Addon Script Path'),
|
||||
_('Path to the Python filtering addon'));
|
||||
o.default = '/etc/mitmproxy/addons/secubox_filter.py';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
// =====================================================================
|
||||
// Capture Settings
|
||||
// =====================================================================
|
||||
s = m.section(form.TypedSection, 'capture', _('Capture Settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'save_flows', _('Save Flows'),
|
||||
_('Save captured flows to disk for later replay'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Value, 'flow_file', _('Flow File'),
|
||||
_('Path to save captured flows'));
|
||||
o.default = '/tmp/mitmproxy/flows.bin';
|
||||
o.depends('save_flows', '1');
|
||||
|
||||
o = s.option(form.Flag, 'capture_urls', _('Capture URLs'),
|
||||
_('Log full URLs of requests'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'capture_cookies', _('Capture Cookies'),
|
||||
_('Log cookie headers'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'capture_headers', _('Capture Headers'),
|
||||
_('Log all HTTP headers'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'capture_body', _('Capture Body'),
|
||||
_('Log request/response bodies (increases storage usage)'));
|
||||
o.default = '0';
|
||||
|
||||
// Logging settings
|
||||
s = m.section(form.TypedSection, 'logging', _('Logging'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Request Logging'),
|
||||
_('Log requests to file'));
|
||||
o = s.option(form.Flag, 'capture_request_headers', _('Capture Request Headers'),
|
||||
_('Include request headers in logs'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Value, 'log_file', _('Log File'),
|
||||
_('Path to request log file'));
|
||||
o.default = '/tmp/mitmproxy/requests.log';
|
||||
o.depends('enabled', '1');
|
||||
o = s.option(form.Flag, 'capture_response_headers', _('Capture Response Headers'),
|
||||
_('Include response headers in logs'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.ListValue, 'log_format', _('Log Format'),
|
||||
_('Format of log entries'));
|
||||
o.value('json', _('JSON'));
|
||||
o.value('text', _('Plain text'));
|
||||
o.default = 'json';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'max_size', _('Max Log Size (MB)'),
|
||||
_('Rotate log when it reaches this size'));
|
||||
o.default = '10';
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
// Filter settings
|
||||
s = m.section(form.TypedSection, 'filter', _('Filtering'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Filtering'),
|
||||
_('Enable content filtering'));
|
||||
o = s.option(form.Flag, 'capture_request_body', _('Capture Request Body'),
|
||||
_('Include request body in logs (increases storage usage)'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Flag, 'block_ads', _('Block Ads'),
|
||||
_('Block known advertising domains'));
|
||||
o = s.option(form.Flag, 'capture_response_body', _('Capture Response Body'),
|
||||
_('Include response body in logs (increases storage usage)'));
|
||||
o.default = '0';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Flag, 'block_trackers', _('Block Trackers'),
|
||||
_('Block known tracking domains'));
|
||||
o.default = '0';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.DynamicList, 'ignore_host', _('Ignore Hosts'),
|
||||
_('Hosts to pass through without interception'));
|
||||
o.placeholder = '*.example.com';
|
||||
|
||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||
wrapper.appendChild(SbHeader.render());
|
||||
|
||||
@ -12,13 +12,6 @@ CONF_DIR="$DATA_DIR"
|
||||
LOG_FILE="$DATA_DIR/requests.log"
|
||||
FLOW_FILE="$DATA_DIR/flows.bin"
|
||||
|
||||
# JSON helpers
|
||||
json_init() { echo "{"; }
|
||||
json_close() { echo "}"; }
|
||||
json_add_string() { printf '"%s":"%s"' "$1" "$2"; }
|
||||
json_add_int() { printf '"%s":%d' "$1" "${2:-0}"; }
|
||||
json_add_bool() { [ "$2" = "1" ] && printf '"%s":true' "$1" || printf '"%s":false' "$1"; }
|
||||
|
||||
# Get service status
|
||||
get_status() {
|
||||
local running=0
|
||||
@ -26,6 +19,7 @@ get_status() {
|
||||
local mode="unknown"
|
||||
local web_url=""
|
||||
local lxc_state=""
|
||||
local nft_active="false"
|
||||
|
||||
# Check LXC container status
|
||||
if command -v lxc-info >/dev/null 2>&1; then
|
||||
@ -50,10 +44,16 @@ get_status() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check nftables rules
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
nft list table inet mitmproxy >/dev/null 2>&1 && nft_active="true"
|
||||
fi
|
||||
|
||||
local enabled=$(uci -q get mitmproxy.main.enabled || echo "0")
|
||||
local proxy_port=$(uci -q get mitmproxy.main.proxy_port || echo "8080")
|
||||
local web_port=$(uci -q get mitmproxy.main.web_port || echo "8081")
|
||||
local proxy_mode=$(uci -q get mitmproxy.main.mode || echo "regular")
|
||||
local filtering_enabled=$(uci -q get mitmproxy.filtering.enabled || echo "0")
|
||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
|
||||
[ "$running" = "1" ] && [ "$mode" = "mitmweb" ] && web_url="http://${router_ip}:${web_port}"
|
||||
@ -69,12 +69,14 @@ get_status() {
|
||||
"proxy_port": $proxy_port,
|
||||
"web_port": $web_port,
|
||||
"web_url": "$web_url",
|
||||
"ca_installed": $([ -f "$CONF_DIR/mitmproxy-ca-cert.pem" ] && echo "true" || echo "false")
|
||||
"ca_installed": $([ -f "$CONF_DIR/mitmproxy-ca-cert.pem" ] && echo "true" || echo "false"),
|
||||
"nft_active": $nft_active,
|
||||
"filtering_enabled": $([ "$filtering_enabled" = "1" ] && echo "true" || echo "false")
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get configuration
|
||||
# Get main configuration
|
||||
get_config() {
|
||||
local enabled=$(uci -q get mitmproxy.main.enabled || echo "0")
|
||||
local mode=$(uci -q get mitmproxy.main.mode || echo "regular")
|
||||
@ -105,16 +107,102 @@ get_config() {
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get transparent mode configuration
|
||||
get_transparent_config() {
|
||||
local enabled=$(uci -q get mitmproxy.transparent.enabled || echo "0")
|
||||
local interface=$(uci -q get mitmproxy.transparent.interface || echo "br-lan")
|
||||
local redirect_http=$(uci -q get mitmproxy.transparent.redirect_http || echo "1")
|
||||
local redirect_https=$(uci -q get mitmproxy.transparent.redirect_https || echo "1")
|
||||
local http_port=$(uci -q get mitmproxy.transparent.http_port || echo "80")
|
||||
local https_port=$(uci -q get mitmproxy.transparent.https_port || echo "443")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"interface": "$interface",
|
||||
"redirect_http": $([ "$redirect_http" = "1" ] && echo "true" || echo "false"),
|
||||
"redirect_https": $([ "$redirect_https" = "1" ] && echo "true" || echo "false"),
|
||||
"http_port": $http_port,
|
||||
"https_port": $https_port
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get whitelist configuration
|
||||
get_whitelist_config() {
|
||||
local enabled=$(uci -q get mitmproxy.whitelist.enabled || echo "1")
|
||||
|
||||
# Get bypass_ip list
|
||||
local bypass_ips=$(uci -q get mitmproxy.whitelist.bypass_ip 2>/dev/null | tr ' ' '\n' | while read ip; do
|
||||
[ -n "$ip" ] && printf '"%s",' "$ip"
|
||||
done | sed 's/,$//')
|
||||
|
||||
# Get bypass_domain list
|
||||
local bypass_domains=$(uci -q get mitmproxy.whitelist.bypass_domain 2>/dev/null | tr ' ' '\n' | while read domain; do
|
||||
[ -n "$domain" ] && printf '"%s",' "$domain"
|
||||
done | sed 's/,$//')
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"bypass_ip": [${bypass_ips}],
|
||||
"bypass_domain": [${bypass_domains}]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get filtering configuration
|
||||
get_filtering_config() {
|
||||
local enabled=$(uci -q get mitmproxy.filtering.enabled || echo "0")
|
||||
local log_requests=$(uci -q get mitmproxy.filtering.log_requests || echo "1")
|
||||
local filter_cdn=$(uci -q get mitmproxy.filtering.filter_cdn || echo "0")
|
||||
local filter_media=$(uci -q get mitmproxy.filtering.filter_media || echo "0")
|
||||
local block_ads=$(uci -q get mitmproxy.filtering.block_ads || echo "0")
|
||||
local addon_script=$(uci -q get mitmproxy.filtering.addon_script || echo "/etc/mitmproxy/addons/secubox_filter.py")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"log_requests": $([ "$log_requests" = "1" ] && echo "true" || echo "false"),
|
||||
"filter_cdn": $([ "$filter_cdn" = "1" ] && echo "true" || echo "false"),
|
||||
"filter_media": $([ "$filter_media" = "1" ] && echo "true" || echo "false"),
|
||||
"block_ads": $([ "$block_ads" = "1" ] && echo "true" || echo "false"),
|
||||
"addon_script": "$addon_script"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get all configuration in one call
|
||||
get_all_config() {
|
||||
cat <<EOF
|
||||
{
|
||||
"main": $(get_config),
|
||||
"transparent": $(get_transparent_config),
|
||||
"whitelist": $(get_whitelist_config),
|
||||
"filtering": $(get_filtering_config)
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get statistics
|
||||
get_stats() {
|
||||
local total_requests=0
|
||||
local unique_hosts=0
|
||||
local flow_size="0"
|
||||
local cdn_requests=0
|
||||
local media_requests=0
|
||||
local blocked_ads=0
|
||||
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
total_requests=$(wc -l < "$LOG_FILE" 2>/dev/null || echo "0")
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
unique_hosts=$(jq -r '.request.host // .host // empty' "$LOG_FILE" 2>/dev/null | sort -u | wc -l)
|
||||
# Use jsonfilter for parsing (OpenWrt native)
|
||||
if command -v jsonfilter >/dev/null 2>&1; then
|
||||
unique_hosts=$(cat "$LOG_FILE" 2>/dev/null | while read line; do
|
||||
echo "$line" | jsonfilter -e '@.request.host' 2>/dev/null
|
||||
done | sort -u | wc -l)
|
||||
cdn_requests=$(grep -c '"category":"cdn"' "$LOG_FILE" 2>/dev/null || echo "0")
|
||||
media_requests=$(grep -c '"category":"media"' "$LOG_FILE" 2>/dev/null || echo "0")
|
||||
blocked_ads=$(grep -c '"category":"blocked_ad"' "$LOG_FILE" 2>/dev/null || echo "0")
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -126,7 +214,10 @@ get_stats() {
|
||||
{
|
||||
"total_requests": $total_requests,
|
||||
"unique_hosts": $unique_hosts,
|
||||
"flow_file_size": $flow_size
|
||||
"flow_file_size": $flow_size,
|
||||
"cdn_requests": $cdn_requests,
|
||||
"media_requests": $media_requests,
|
||||
"blocked_ads": $blocked_ads
|
||||
}
|
||||
EOF
|
||||
}
|
||||
@ -134,18 +225,24 @@ EOF
|
||||
# Get recent requests
|
||||
get_requests() {
|
||||
local limit="${1:-50}"
|
||||
local category="${2:-}"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo '{"requests":[]}'
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
echo '{"requests":'
|
||||
tail -"$limit" "$LOG_FILE" 2>/dev/null | jq -s '.' 2>/dev/null || echo '[]'
|
||||
echo '}'
|
||||
# Filter by category if specified
|
||||
if [ -n "$category" ] && [ "$category" != "all" ]; then
|
||||
echo '{"requests":['
|
||||
grep "\"category\":\"$category\"" "$LOG_FILE" 2>/dev/null | tail -"$limit" | \
|
||||
awk 'BEGIN{first=1}{if(!first)printf ",";first=0;print}' 2>/dev/null || echo ""
|
||||
echo ']}'
|
||||
else
|
||||
echo '{"requests":[]}'
|
||||
echo '{"requests":['
|
||||
tail -"$limit" "$LOG_FILE" 2>/dev/null | \
|
||||
awk 'BEGIN{first=1}{if(!first)printf ",";first=0;print}' 2>/dev/null || echo ""
|
||||
echo ']}'
|
||||
fi
|
||||
}
|
||||
|
||||
@ -153,13 +250,15 @@ get_requests() {
|
||||
get_top_hosts() {
|
||||
local limit="${1:-20}"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ] || ! command -v jq >/dev/null 2>&1; then
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo '{"hosts":[]}'
|
||||
return
|
||||
fi
|
||||
|
||||
echo '{"hosts":['
|
||||
jq -r '.request.host // .host // "unknown"' "$LOG_FILE" 2>/dev/null | \
|
||||
# Parse JSON using grep/sed for compatibility
|
||||
grep -o '"host":"[^"]*"' "$LOG_FILE" 2>/dev/null | \
|
||||
sed 's/"host":"//;s/"$//' | \
|
||||
sort | uniq -c | sort -rn | head -"$limit" | \
|
||||
awk 'BEGIN{first=1} {
|
||||
if(!first) printf ",";
|
||||
@ -189,6 +288,28 @@ service_restart() {
|
||||
get_status
|
||||
}
|
||||
|
||||
# Setup firewall rules
|
||||
firewall_setup() {
|
||||
/usr/sbin/mitmproxyctl firewall-setup 2>&1
|
||||
local result=$?
|
||||
if [ $result -eq 0 ]; then
|
||||
echo '{"success":true,"message":"Firewall rules applied"}'
|
||||
else
|
||||
echo '{"success":false,"message":"Failed to apply firewall rules"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Clear firewall rules
|
||||
firewall_clear() {
|
||||
/usr/sbin/mitmproxyctl firewall-clear 2>&1
|
||||
local result=$?
|
||||
if [ $result -eq 0 ]; then
|
||||
echo '{"success":true,"message":"Firewall rules cleared"}'
|
||||
else
|
||||
echo '{"success":false,"message":"Failed to clear firewall rules"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Set configuration
|
||||
set_config() {
|
||||
local key="$1"
|
||||
@ -199,6 +320,21 @@ set_config() {
|
||||
save_flows|capture_*)
|
||||
section="capture"
|
||||
;;
|
||||
redirect_*|interface|http_port|https_port)
|
||||
section="transparent"
|
||||
;;
|
||||
bypass_ip|bypass_domain)
|
||||
section="whitelist"
|
||||
;;
|
||||
filter_*|log_requests|block_ads|addon_script)
|
||||
section="filtering"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle boolean conversion
|
||||
case "$value" in
|
||||
true) value="1" ;;
|
||||
false) value="0" ;;
|
||||
esac
|
||||
|
||||
uci set "mitmproxy.$section.$key=$value"
|
||||
@ -206,6 +342,28 @@ set_config() {
|
||||
echo '{"success":true}'
|
||||
}
|
||||
|
||||
# Add to list (for bypass_ip, bypass_domain)
|
||||
add_to_list() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local section="whitelist"
|
||||
|
||||
uci add_list "mitmproxy.$section.$key=$value"
|
||||
uci commit mitmproxy
|
||||
echo '{"success":true}'
|
||||
}
|
||||
|
||||
# Remove from list
|
||||
remove_from_list() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local section="whitelist"
|
||||
|
||||
uci del_list "mitmproxy.$section.$key=$value"
|
||||
uci commit mitmproxy
|
||||
echo '{"success":true}'
|
||||
}
|
||||
|
||||
# Clear captured data
|
||||
clear_data() {
|
||||
rm -f "$DATA_DIR"/*.log "$DATA_DIR"/*.bin 2>/dev/null
|
||||
@ -249,14 +407,22 @@ case "$1" in
|
||||
{
|
||||
"get_status": {},
|
||||
"get_config": {},
|
||||
"get_transparent_config": {},
|
||||
"get_whitelist_config": {},
|
||||
"get_filtering_config": {},
|
||||
"get_all_config": {},
|
||||
"get_stats": {},
|
||||
"get_requests": {"limit": 50},
|
||||
"get_requests": {"limit": 50, "category": "all"},
|
||||
"get_top_hosts": {"limit": 20},
|
||||
"get_ca_info": {},
|
||||
"service_start": {},
|
||||
"service_stop": {},
|
||||
"service_restart": {},
|
||||
"firewall_setup": {},
|
||||
"firewall_clear": {},
|
||||
"set_config": {"key": "string", "value": "string"},
|
||||
"add_to_list": {"key": "string", "value": "string"},
|
||||
"remove_from_list": {"key": "string", "value": "string"},
|
||||
"clear_data": {}
|
||||
}
|
||||
EOF
|
||||
@ -269,13 +435,26 @@ EOF
|
||||
get_config)
|
||||
get_config
|
||||
;;
|
||||
get_transparent_config)
|
||||
get_transparent_config
|
||||
;;
|
||||
get_whitelist_config)
|
||||
get_whitelist_config
|
||||
;;
|
||||
get_filtering_config)
|
||||
get_filtering_config
|
||||
;;
|
||||
get_all_config)
|
||||
get_all_config
|
||||
;;
|
||||
get_stats)
|
||||
get_stats
|
||||
;;
|
||||
get_requests)
|
||||
read -r input
|
||||
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null || echo "50")
|
||||
get_requests "$limit"
|
||||
category=$(echo "$input" | jsonfilter -e '@.category' 2>/dev/null || echo "all")
|
||||
get_requests "$limit" "$category"
|
||||
;;
|
||||
get_top_hosts)
|
||||
read -r input
|
||||
@ -294,12 +473,30 @@ EOF
|
||||
service_restart)
|
||||
service_restart
|
||||
;;
|
||||
firewall_setup)
|
||||
firewall_setup
|
||||
;;
|
||||
firewall_clear)
|
||||
firewall_clear
|
||||
;;
|
||||
set_config)
|
||||
read -r input
|
||||
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
set_config "$key" "$value"
|
||||
;;
|
||||
add_to_list)
|
||||
read -r input
|
||||
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
add_to_list "$key" "$value"
|
||||
;;
|
||||
remove_from_list)
|
||||
read -r input
|
||||
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
remove_from_list "$key" "$value"
|
||||
;;
|
||||
clear_data)
|
||||
clear_data
|
||||
;;
|
||||
|
||||
@ -14,3 +14,55 @@ config mitmproxy 'main'
|
||||
option anticache '0'
|
||||
option anticomp '0'
|
||||
option flow_detail '1'
|
||||
|
||||
# Transparent mode settings
|
||||
config transparent 'transparent'
|
||||
option enabled '0'
|
||||
# Interface to intercept traffic from (e.g., br-lan)
|
||||
option interface 'br-lan'
|
||||
# Redirect HTTP traffic (port 80)
|
||||
option redirect_http '1'
|
||||
# Redirect HTTPS traffic (port 443)
|
||||
option redirect_https '1'
|
||||
# Custom HTTP port (default 80)
|
||||
option http_port '80'
|
||||
# Custom HTTPS port (default 443)
|
||||
option https_port '443'
|
||||
|
||||
# Whitelist/bypass - IPs and domains that bypass the proxy
|
||||
config whitelist 'whitelist'
|
||||
option enabled '1'
|
||||
# Bypass local networks by default
|
||||
list bypass_ip '10.0.0.0/8'
|
||||
list bypass_ip '172.16.0.0/12'
|
||||
list bypass_ip '192.168.0.0/16'
|
||||
list bypass_ip '127.0.0.0/8'
|
||||
# Bypass sensitive domains (banking, medical, etc.)
|
||||
list bypass_domain 'banking'
|
||||
list bypass_domain 'paypal.com'
|
||||
list bypass_domain 'stripe.com'
|
||||
# Add custom bypasses here
|
||||
# list bypass_ip 'x.x.x.x'
|
||||
# list bypass_domain 'example.com'
|
||||
|
||||
# CDN/MediaFlow filtering addon
|
||||
config filtering 'filtering'
|
||||
option enabled '0'
|
||||
# Log all requests to JSON file
|
||||
option log_requests '1'
|
||||
# Filter CDN traffic (e.g., cloudflare, akamai, fastly)
|
||||
option filter_cdn '0'
|
||||
# Filter streaming media
|
||||
option filter_media '0'
|
||||
# Block ads and trackers
|
||||
option block_ads '0'
|
||||
# Custom filter script path
|
||||
option addon_script '/etc/mitmproxy/addons/secubox_filter.py'
|
||||
|
||||
# Capture settings
|
||||
config capture 'capture'
|
||||
option save_flows '0'
|
||||
option capture_request_headers '1'
|
||||
option capture_response_headers '1'
|
||||
option capture_request_body '0'
|
||||
option capture_response_body '0'
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
#!/bin/sh
|
||||
# SecuBox mitmproxy manager - LXC container support
|
||||
# Copyright (C) 2024 CyberMind.fr
|
||||
# SecuBox mitmproxy manager - LXC container support with transparent mode
|
||||
# Copyright (C) 2024-2025 CyberMind.fr
|
||||
|
||||
CONFIG="mitmproxy"
|
||||
LXC_NAME="mitmproxy"
|
||||
OPKG_UPDATED=0
|
||||
NFT_TABLE="mitmproxy"
|
||||
|
||||
# Paths
|
||||
LXC_PATH="/srv/lxc"
|
||||
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
|
||||
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
|
||||
ADDON_PATH="/etc/mitmproxy/addons"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@ -23,12 +25,14 @@ Commands:
|
||||
logs Show mitmproxy logs (use -f to follow)
|
||||
shell Open shell in container
|
||||
cert Show CA certificate info / export path
|
||||
firewall-setup Setup nftables rules for transparent mode
|
||||
firewall-clear Remove nftables transparent mode rules
|
||||
service-run Internal: run container under procd
|
||||
service-stop Stop container
|
||||
|
||||
Modes (configure in /etc/config/mitmproxy):
|
||||
regular - Standard HTTP/HTTPS proxy (default)
|
||||
transparent - Transparent proxy (requires iptables redirect)
|
||||
transparent - Transparent proxy (auto-configures nftables)
|
||||
upstream - Forward to upstream proxy
|
||||
reverse - Reverse proxy mode
|
||||
|
||||
@ -43,23 +47,44 @@ log_info() { echo "[INFO] $*"; }
|
||||
log_warn() { echo "[WARN] $*" >&2; }
|
||||
log_error() { echo "[ERROR] $*" >&2; }
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.main.$1; }
|
||||
uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; }
|
||||
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||
uci_get_list() { uci -q get ${CONFIG}.$1 2>/dev/null; }
|
||||
|
||||
# Load configuration with defaults
|
||||
load_config() {
|
||||
proxy_port="$(uci_get proxy_port || echo 8080)"
|
||||
web_port="$(uci_get web_port || echo 8081)"
|
||||
web_host="$(uci_get web_host || echo 0.0.0.0)"
|
||||
data_path="$(uci_get data_path || echo /srv/mitmproxy)"
|
||||
memory_limit="$(uci_get memory_limit || echo 256M)"
|
||||
mode="$(uci_get mode || echo regular)"
|
||||
upstream_proxy="$(uci_get upstream_proxy || echo '')"
|
||||
reverse_target="$(uci_get reverse_target || echo '')"
|
||||
ssl_insecure="$(uci_get ssl_insecure || echo 0)"
|
||||
anticache="$(uci_get anticache || echo 0)"
|
||||
anticomp="$(uci_get anticomp || echo 0)"
|
||||
flow_detail="$(uci_get flow_detail || echo 1)"
|
||||
# Main settings
|
||||
proxy_port="$(uci_get main.proxy_port || echo 8080)"
|
||||
web_port="$(uci_get main.web_port || echo 8081)"
|
||||
web_host="$(uci_get main.web_host || echo 0.0.0.0)"
|
||||
data_path="$(uci_get main.data_path || echo /srv/mitmproxy)"
|
||||
memory_limit="$(uci_get main.memory_limit || echo 256M)"
|
||||
mode="$(uci_get main.mode || echo regular)"
|
||||
upstream_proxy="$(uci_get main.upstream_proxy || echo '')"
|
||||
reverse_target="$(uci_get main.reverse_target || echo '')"
|
||||
ssl_insecure="$(uci_get main.ssl_insecure || echo 0)"
|
||||
anticache="$(uci_get main.anticache || echo 0)"
|
||||
anticomp="$(uci_get main.anticomp || echo 0)"
|
||||
flow_detail="$(uci_get main.flow_detail || echo 1)"
|
||||
|
||||
# Transparent mode settings
|
||||
transparent_enabled="$(uci_get transparent.enabled || echo 0)"
|
||||
transparent_iface="$(uci_get transparent.interface || echo br-lan)"
|
||||
redirect_http="$(uci_get transparent.redirect_http || echo 1)"
|
||||
redirect_https="$(uci_get transparent.redirect_https || echo 1)"
|
||||
http_port="$(uci_get transparent.http_port || echo 80)"
|
||||
https_port="$(uci_get transparent.https_port || echo 443)"
|
||||
|
||||
# Whitelist settings
|
||||
whitelist_enabled="$(uci_get whitelist.enabled || echo 1)"
|
||||
|
||||
# Filtering settings
|
||||
filtering_enabled="$(uci_get filtering.enabled || echo 0)"
|
||||
log_requests="$(uci_get filtering.log_requests || echo 1)"
|
||||
filter_cdn="$(uci_get filtering.filter_cdn || echo 0)"
|
||||
filter_media="$(uci_get filtering.filter_media || echo 0)"
|
||||
block_ads="$(uci_get filtering.block_ads || echo 0)"
|
||||
addon_script="$(uci_get filtering.addon_script || echo /etc/mitmproxy/addons/secubox_filter.py)"
|
||||
}
|
||||
|
||||
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
|
||||
@ -69,6 +94,10 @@ has_lxc() {
|
||||
command -v lxc-stop >/dev/null 2>&1
|
||||
}
|
||||
|
||||
has_nft() {
|
||||
command -v nft >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Ensure required packages are installed
|
||||
ensure_packages() {
|
||||
require_root
|
||||
@ -83,6 +112,128 @@ ensure_packages() {
|
||||
done
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# NFTABLES TRANSPARENT MODE FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
nft_setup() {
|
||||
load_config
|
||||
require_root
|
||||
|
||||
if ! has_nft; then
|
||||
log_error "nftables not available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$mode" != "transparent" ]; then
|
||||
log_warn "Proxy mode is '$mode', not 'transparent'. Firewall rules not needed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Setting up nftables for transparent proxy..."
|
||||
|
||||
# Create mitmproxy table
|
||||
nft add table inet $NFT_TABLE 2>/dev/null || true
|
||||
|
||||
# Create chains
|
||||
nft add chain inet $NFT_TABLE prerouting { type nat hook prerouting priority -100 \; } 2>/dev/null || true
|
||||
nft add chain inet $NFT_TABLE output { type nat hook output priority -100 \; } 2>/dev/null || true
|
||||
|
||||
# Create bypass set for whitelisted IPs
|
||||
nft add set inet $NFT_TABLE bypass_ipv4 { type ipv4_addr \; flags interval \; } 2>/dev/null || true
|
||||
nft add set inet $NFT_TABLE bypass_ipv6 { type ipv6_addr \; flags interval \; } 2>/dev/null || true
|
||||
|
||||
# Load whitelist IPs into bypass set
|
||||
if [ "$whitelist_enabled" = "1" ]; then
|
||||
local bypass_ips=$(uci_get_list whitelist.bypass_ip 2>/dev/null)
|
||||
for ip in $bypass_ips; do
|
||||
case "$ip" in
|
||||
*:*) nft add element inet $NFT_TABLE bypass_ipv6 { $ip } 2>/dev/null || true ;;
|
||||
*) nft add element inet $NFT_TABLE bypass_ipv4 { $ip } 2>/dev/null || true ;;
|
||||
esac
|
||||
done
|
||||
log_info "Loaded whitelist bypass IPs"
|
||||
fi
|
||||
|
||||
# Get interface index if specified
|
||||
local iif_match=""
|
||||
if [ -n "$transparent_iface" ]; then
|
||||
iif_match="iifname \"$transparent_iface\""
|
||||
fi
|
||||
|
||||
# Flush existing rules in our chains
|
||||
nft flush chain inet $NFT_TABLE prerouting 2>/dev/null || true
|
||||
nft flush chain inet $NFT_TABLE output 2>/dev/null || true
|
||||
|
||||
# Add bypass rules first (before redirect)
|
||||
nft add rule inet $NFT_TABLE prerouting ip daddr @bypass_ipv4 return 2>/dev/null || true
|
||||
nft add rule inet $NFT_TABLE prerouting ip6 daddr @bypass_ipv6 return 2>/dev/null || true
|
||||
|
||||
# Don't intercept traffic from the proxy itself
|
||||
nft add rule inet $NFT_TABLE prerouting meta skuid mitmproxy return 2>/dev/null || true
|
||||
|
||||
# Redirect HTTP traffic
|
||||
if [ "$redirect_http" = "1" ]; then
|
||||
if [ -n "$iif_match" ]; then
|
||||
nft add rule inet $NFT_TABLE prerouting $iif_match tcp dport $http_port redirect to :$proxy_port
|
||||
else
|
||||
nft add rule inet $NFT_TABLE prerouting tcp dport $http_port redirect to :$proxy_port
|
||||
fi
|
||||
log_info "HTTP redirect: port $http_port -> $proxy_port"
|
||||
fi
|
||||
|
||||
# Redirect HTTPS traffic
|
||||
if [ "$redirect_https" = "1" ]; then
|
||||
if [ -n "$iif_match" ]; then
|
||||
nft add rule inet $NFT_TABLE prerouting $iif_match tcp dport $https_port redirect to :$proxy_port
|
||||
else
|
||||
nft add rule inet $NFT_TABLE prerouting tcp dport $https_port redirect to :$proxy_port
|
||||
fi
|
||||
log_info "HTTPS redirect: port $https_port -> $proxy_port"
|
||||
fi
|
||||
|
||||
log_info "nftables transparent mode rules applied"
|
||||
log_info "Table: inet $NFT_TABLE"
|
||||
}
|
||||
|
||||
nft_teardown() {
|
||||
require_root
|
||||
|
||||
if ! has_nft; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Removing nftables transparent mode rules..."
|
||||
|
||||
# Delete the entire table (removes all chains and rules)
|
||||
nft delete table inet $NFT_TABLE 2>/dev/null || true
|
||||
|
||||
log_info "nftables rules removed"
|
||||
}
|
||||
|
||||
nft_status() {
|
||||
if ! has_nft; then
|
||||
echo "nftables not available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "=== mitmproxy nftables rules ==="
|
||||
if nft list table inet $NFT_TABLE 2>/dev/null; then
|
||||
echo ""
|
||||
echo "Bypass IPv4 set:"
|
||||
nft list set inet $NFT_TABLE bypass_ipv4 2>/dev/null || echo " (empty or not created)"
|
||||
echo ""
|
||||
echo "Bypass IPv6 set:"
|
||||
nft list set inet $NFT_TABLE bypass_ipv6 2>/dev/null || echo " (empty or not created)"
|
||||
else
|
||||
echo "No mitmproxy rules configured"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# LXC CONTAINER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
lxc_check_prereqs() {
|
||||
log_info "Checking LXC prerequisites..."
|
||||
ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1
|
||||
@ -171,7 +322,7 @@ apk add --no-cache \
|
||||
pip3 install --break-system-packages mitmproxy
|
||||
|
||||
# Create directories
|
||||
mkdir -p /data /var/log/mitmproxy
|
||||
mkdir -p /data /var/log/mitmproxy /etc/mitmproxy/addons
|
||||
|
||||
# Create startup script
|
||||
cat > /opt/start-mitmproxy.sh << 'START'
|
||||
@ -183,6 +334,8 @@ MODE="${MITMPROXY_MODE:-regular}"
|
||||
PROXY_PORT="${MITMPROXY_PROXY_PORT:-8080}"
|
||||
WEB_PORT="${MITMPROXY_WEB_PORT:-8081}"
|
||||
WEB_HOST="${MITMPROXY_WEB_HOST:-0.0.0.0}"
|
||||
ADDON_SCRIPT="${MITMPROXY_ADDON_SCRIPT:-}"
|
||||
FILTERING_ENABLED="${MITMPROXY_FILTERING_ENABLED:-0}"
|
||||
|
||||
# Build command arguments
|
||||
ARGS="--listen-host 0.0.0.0 --listen-port $PROXY_PORT"
|
||||
@ -207,6 +360,12 @@ esac
|
||||
[ "$ANTICOMP" = "1" ] && ARGS="$ARGS --anticomp"
|
||||
[ -n "$FLOW_DETAIL" ] && ARGS="$ARGS --flow-detail $FLOW_DETAIL"
|
||||
|
||||
# Load addon script if filtering is enabled
|
||||
if [ "$FILTERING_ENABLED" = "1" ] && [ -n "$ADDON_SCRIPT" ] && [ -f "$ADDON_SCRIPT" ]; then
|
||||
ARGS="$ARGS -s $ADDON_SCRIPT"
|
||||
echo "Loading addon: $ADDON_SCRIPT"
|
||||
fi
|
||||
|
||||
# Run mitmweb (web interface + proxy)
|
||||
exec mitmweb $ARGS --web-host "$WEB_HOST" --web-port "$WEB_PORT" --no-web-open-browser
|
||||
START
|
||||
@ -225,11 +384,174 @@ SETUP
|
||||
}
|
||||
|
||||
rm -f "$rootfs/tmp/setup-mitmproxy.sh"
|
||||
|
||||
# Install the SecuBox filter addon
|
||||
install_addon_script
|
||||
}
|
||||
|
||||
install_addon_script() {
|
||||
load_config
|
||||
ensure_dir "$ADDON_PATH"
|
||||
ensure_dir "$LXC_ROOTFS/etc/mitmproxy/addons"
|
||||
|
||||
# Create the SecuBox filter addon
|
||||
cat > "$ADDON_PATH/secubox_filter.py" << 'ADDON'
|
||||
"""
|
||||
SecuBox mitmproxy Filter Addon
|
||||
CDN/MediaFlow filtering and request logging
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from mitmproxy import http, ctx
|
||||
|
||||
# CDN domains to track
|
||||
CDN_DOMAINS = [
|
||||
r'\.cloudflare\.com$',
|
||||
r'\.cloudflareinsights\.com$',
|
||||
r'\.akamai\.net$',
|
||||
r'\.akamaized\.net$',
|
||||
r'\.fastly\.net$',
|
||||
r'\.cloudfront\.net$',
|
||||
r'\.azureedge\.net$',
|
||||
r'\.jsdelivr\.net$',
|
||||
r'\.unpkg\.com$',
|
||||
r'\.cdnjs\.cloudflare\.com$',
|
||||
]
|
||||
|
||||
# Media streaming domains
|
||||
MEDIA_DOMAINS = [
|
||||
r'\.googlevideo\.com$',
|
||||
r'\.youtube\.com$',
|
||||
r'\.ytimg\.com$',
|
||||
r'\.netflix\.com$',
|
||||
r'\.nflxvideo\.net$',
|
||||
r'\.spotify\.com$',
|
||||
r'\.scdn\.co$',
|
||||
r'\.twitch\.tv$',
|
||||
r'\.ttvnw\.net$',
|
||||
]
|
||||
|
||||
# Ad/Tracker domains to block
|
||||
AD_DOMAINS = [
|
||||
r'\.doubleclick\.net$',
|
||||
r'\.googlesyndication\.com$',
|
||||
r'\.googleadservices\.com$',
|
||||
r'\.facebook\.net$',
|
||||
r'\.analytics\.google\.com$',
|
||||
r'\.google-analytics\.com$',
|
||||
r'\.hotjar\.com$',
|
||||
r'\.segment\.io$',
|
||||
r'\.mixpanel\.com$',
|
||||
r'\.amplitude\.com$',
|
||||
]
|
||||
|
||||
class SecuBoxFilter:
|
||||
def __init__(self):
|
||||
self.log_file = os.environ.get('MITMPROXY_LOG_FILE', '/data/requests.log')
|
||||
self.filter_cdn = os.environ.get('MITMPROXY_FILTER_CDN', '0') == '1'
|
||||
self.filter_media = os.environ.get('MITMPROXY_FILTER_MEDIA', '0') == '1'
|
||||
self.block_ads = os.environ.get('MITMPROXY_BLOCK_ADS', '0') == '1'
|
||||
self.log_requests = os.environ.get('MITMPROXY_LOG_REQUESTS', '1') == '1'
|
||||
|
||||
ctx.log.info(f"SecuBox Filter initialized")
|
||||
ctx.log.info(f" Log requests: {self.log_requests}")
|
||||
ctx.log.info(f" Filter CDN: {self.filter_cdn}")
|
||||
ctx.log.info(f" Filter Media: {self.filter_media}")
|
||||
ctx.log.info(f" Block Ads: {self.block_ads}")
|
||||
|
||||
def _match_domain(self, host, patterns):
|
||||
"""Check if host matches any pattern"""
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, host, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _log_request(self, flow: http.HTTPFlow, category: str = "normal"):
|
||||
"""Log request to JSON file"""
|
||||
if not self.log_requests:
|
||||
return
|
||||
|
||||
try:
|
||||
entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"category": category,
|
||||
"request": {
|
||||
"method": flow.request.method,
|
||||
"host": flow.request.host,
|
||||
"port": flow.request.port,
|
||||
"path": flow.request.path,
|
||||
"scheme": flow.request.scheme,
|
||||
},
|
||||
}
|
||||
|
||||
if flow.response:
|
||||
entry["response"] = {
|
||||
"status_code": flow.response.status_code,
|
||||
"content_type": flow.response.headers.get("content-type", ""),
|
||||
"content_length": len(flow.response.content) if flow.response.content else 0,
|
||||
}
|
||||
|
||||
with open(self.log_file, 'a') as f:
|
||||
f.write(json.dumps(entry) + '\n')
|
||||
except Exception as e:
|
||||
ctx.log.error(f"Failed to log request: {e}")
|
||||
|
||||
def request(self, flow: http.HTTPFlow):
|
||||
"""Process incoming request"""
|
||||
host = flow.request.host
|
||||
|
||||
# Check for ad/tracker domains
|
||||
if self.block_ads and self._match_domain(host, AD_DOMAINS):
|
||||
ctx.log.info(f"Blocked ad/tracker: {host}")
|
||||
flow.response = http.Response.make(
|
||||
403,
|
||||
b"Blocked by SecuBox",
|
||||
{"Content-Type": "text/plain"}
|
||||
)
|
||||
self._log_request(flow, "blocked_ad")
|
||||
return
|
||||
|
||||
# Track CDN requests
|
||||
if self._match_domain(host, CDN_DOMAINS):
|
||||
self._log_request(flow, "cdn")
|
||||
if self.filter_cdn:
|
||||
ctx.log.info(f"CDN request: {host}{flow.request.path[:50]}")
|
||||
return
|
||||
|
||||
# Track media requests
|
||||
if self._match_domain(host, MEDIA_DOMAINS):
|
||||
self._log_request(flow, "media")
|
||||
if self.filter_media:
|
||||
ctx.log.info(f"Media request: {host}{flow.request.path[:50]}")
|
||||
return
|
||||
|
||||
# Log normal request
|
||||
self._log_request(flow, "normal")
|
||||
|
||||
def response(self, flow: http.HTTPFlow):
|
||||
"""Process response - update log entry if needed"""
|
||||
pass
|
||||
|
||||
addons = [SecuBoxFilter()]
|
||||
ADDON
|
||||
|
||||
# Copy to container rootfs
|
||||
cp "$ADDON_PATH/secubox_filter.py" "$LXC_ROOTFS/etc/mitmproxy/addons/" 2>/dev/null || true
|
||||
|
||||
log_info "Addon script installed: $ADDON_PATH/secubox_filter.py"
|
||||
}
|
||||
|
||||
lxc_create_config() {
|
||||
load_config
|
||||
|
||||
# Build addon path for container
|
||||
local container_addon=""
|
||||
if [ "$filtering_enabled" = "1" ] && [ -f "$LXC_ROOTFS$addon_script" ]; then
|
||||
container_addon="$addon_script"
|
||||
fi
|
||||
|
||||
cat > "$LXC_CONFIG" << EOF
|
||||
# mitmproxy LXC Configuration
|
||||
lxc.uts.name = $LXC_NAME
|
||||
@ -243,6 +565,7 @@ lxc.net.0.type = none
|
||||
# Mounts
|
||||
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||
lxc.mount.entry = $data_path data none bind,create=dir 0 0
|
||||
lxc.mount.entry = $ADDON_PATH etc/mitmproxy/addons none bind,create=dir 0 0
|
||||
|
||||
# Environment variables for configuration
|
||||
lxc.environment = MITMPROXY_MODE=$mode
|
||||
@ -255,6 +578,13 @@ lxc.environment = SSL_INSECURE=$ssl_insecure
|
||||
lxc.environment = ANTICACHE=$anticache
|
||||
lxc.environment = ANTICOMP=$anticomp
|
||||
lxc.environment = FLOW_DETAIL=$flow_detail
|
||||
lxc.environment = MITMPROXY_FILTERING_ENABLED=$filtering_enabled
|
||||
lxc.environment = MITMPROXY_ADDON_SCRIPT=$addon_script
|
||||
lxc.environment = MITMPROXY_LOG_REQUESTS=$log_requests
|
||||
lxc.environment = MITMPROXY_FILTER_CDN=$filter_cdn
|
||||
lxc.environment = MITMPROXY_FILTER_MEDIA=$filter_media
|
||||
lxc.environment = MITMPROXY_BLOCK_ADS=$block_ads
|
||||
lxc.environment = MITMPROXY_LOG_FILE=/data/requests.log
|
||||
|
||||
# Capabilities
|
||||
lxc.cap.drop = sys_admin sys_module mac_admin mac_override
|
||||
@ -293,19 +623,44 @@ lxc_run() {
|
||||
|
||||
# Ensure mount points exist
|
||||
ensure_dir "$data_path"
|
||||
ensure_dir "$ADDON_PATH"
|
||||
|
||||
# Setup firewall rules if in transparent mode
|
||||
if [ "$mode" = "transparent" ]; then
|
||||
nft_setup
|
||||
fi
|
||||
|
||||
log_info "Starting mitmproxy LXC container..."
|
||||
log_info "Mode: $mode"
|
||||
log_info "Web interface: http://0.0.0.0:$web_port"
|
||||
log_info "Proxy port: $proxy_port"
|
||||
[ "$filtering_enabled" = "1" ] && log_info "Filtering: enabled"
|
||||
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
|
||||
}
|
||||
|
||||
lxc_status() {
|
||||
load_config
|
||||
echo "=== mitmproxy Status ==="
|
||||
echo ""
|
||||
|
||||
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
|
||||
lxc-info -n "$LXC_NAME"
|
||||
else
|
||||
echo "LXC container '$LXC_NAME' not found or not configured"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Configuration ==="
|
||||
echo "Mode: $mode"
|
||||
echo "Proxy port: $proxy_port"
|
||||
echo "Web port: $web_port"
|
||||
echo "Data path: $data_path"
|
||||
echo "Filtering: $([ "$filtering_enabled" = "1" ] && echo "enabled" || echo "disabled")"
|
||||
|
||||
if [ "$mode" = "transparent" ]; then
|
||||
echo ""
|
||||
nft_status
|
||||
fi
|
||||
}
|
||||
|
||||
lxc_logs() {
|
||||
@ -342,6 +697,10 @@ lxc_destroy() {
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# COMMANDS
|
||||
# =============================================================================
|
||||
|
||||
cmd_install() {
|
||||
require_root
|
||||
load_config
|
||||
@ -355,11 +714,12 @@ cmd_install() {
|
||||
|
||||
# Create directories
|
||||
ensure_dir "$data_path"
|
||||
ensure_dir "$ADDON_PATH"
|
||||
|
||||
lxc_check_prereqs || exit 1
|
||||
lxc_create_rootfs || exit 1
|
||||
|
||||
uci_set enabled '1'
|
||||
uci_set main.enabled '1'
|
||||
/etc/init.d/mitmproxy enable
|
||||
|
||||
log_info "mitmproxy installed."
|
||||
@ -378,6 +738,12 @@ cmd_check() {
|
||||
else
|
||||
log_warn "LXC: not available"
|
||||
fi
|
||||
|
||||
if has_nft; then
|
||||
log_info "nftables: available"
|
||||
else
|
||||
log_warn "nftables: not available (needed for transparent mode)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
@ -429,6 +795,14 @@ cmd_cert() {
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_firewall_setup() {
|
||||
nft_setup
|
||||
}
|
||||
|
||||
cmd_firewall_clear() {
|
||||
nft_teardown
|
||||
}
|
||||
|
||||
cmd_service_run() {
|
||||
require_root
|
||||
load_config
|
||||
@ -444,6 +818,13 @@ cmd_service_run() {
|
||||
|
||||
cmd_service_stop() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
# Remove firewall rules
|
||||
if [ "$mode" = "transparent" ]; then
|
||||
nft_teardown
|
||||
fi
|
||||
|
||||
lxc_stop
|
||||
}
|
||||
|
||||
@ -456,6 +837,8 @@ case "${1:-}" in
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
shell) shift; cmd_shell "$@" ;;
|
||||
cert) shift; cmd_cert "$@" ;;
|
||||
firewall-setup) shift; cmd_firewall_setup "$@" ;;
|
||||
firewall-clear) shift; cmd_firewall_clear "$@" ;;
|
||||
service-run) shift; cmd_service_run "$@" ;;
|
||||
service-stop) shift; cmd_service_stop "$@" ;;
|
||||
help|--help|-h|'') usage ;;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user