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:
CyberMind-FR 2026-01-17 06:55:45 +01:00
parent 4e5d5275f9
commit fe222d542c
5 changed files with 915 additions and 125 deletions

View File

@ -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]

View File

@ -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());

View File

@ -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
;;

View File

@ -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'

View File

@ -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 ;;