⛅ Sync 2026-10-11 13:16:00

This commit is contained in:
github-actions[bot] committed 2026-10-11 13:16:00 +08:00
1 parent 705dadd362
commit e6d1b4e2d7
105 files changed
+26511 -50

No files matched your search

@@ -0,0 +1,73 @@
#!/bin/sh
# Copyright (C) 2008-2026 The OpenWrt Project
# Copyright (C) 2026 Manfred Jaider <info@masmbit.com>
#
# This is free software, licensed under the Apache License, Version 2.0.
# See /LICENSE for more information.
#
# luci-app-openvpn-plus : Network-ready bootup event handler with race-condition lock
# /etc/hotplug.d/iface/35-luci-app-openvpn-plus-bootup
# Only trigger this script when an interface finishes initializing
if [ "$ACTION" != "ifup" ]; then
exit 0
fi
# STRIP LOOPBACK: Ignore the internal system loopback to prevent a premature trigger
if [ "$INTERFACE" = "loopback" ]; then
exit 0
fi
LOCK_FILE="/var/run/luci-app-openvpn-plus-bootup.lock"
# Mutex implementation preventing parallel execution threads from racing each other
if [ -f "$LOCK_FILE" ]; then
# Clean verification via signal 0: check if the holding PID is still alive
if kill -0 $(cat "$LOCK_FILE" 2>/dev/null) 2>/dev/null; then
exit 0
fi
fi
# Register active PID into the lock file to serialize the bootup checks
echo "$$" > "$LOCK_FILE"
# Set traps to delete the lock file reliably on unexpected script exits
trap "rm -f $LOCK_FILE" EXIT INT TERM
logger -t openvpn-luci "Bootup: Interface '${INTERFACE}' is up. Checking active OpenVPN profile running state in 10 seconds..."
# Give the system firewall and routing tables enough time to init
sleep 10
# Loop through all configured openvpn sections inside the UCI configuration
uci -q show openvpn | grep "=openvpn" | while read -r line; do
# Extract the pure instance identity string from the current UCI line
inst="${line##*.}"
inst="${inst%%=*}"
# Process the instance only if it is explicitly enabled by the user
if [ "$(uci -q get openvpn.${inst}.enabled)" = "1" ]; then
# Bulletproof process check utilizing the immune syslog tag pattern
if ! ps | grep -v "grep" | grep -q "openvpn(${inst})"; then
logger -t openvpn-luci "Bootup: Detected unstarted instance '${inst}'. Initializing process."
# Restart the service cleanly to clear out any dead zombie states or locked ports
/etc/init.d/openvpn stop "$inst" >/dev/null 2>&1
sleep 1 >/dev/null 2>&1
/etc/init.d/openvpn restart "$inst" >/dev/null 2>&1
sleep 1 >/dev/null 2>&1
/etc/init.d/openvpn reload "$inst" >/dev/null 2>&1
else
logger -t openvpn-luci "Bootup: Instance '${inst}' is already running safely."
fi
fi
done
# Delete the lock file manually after successful execution
rm -f "$LOCK_FILE"
# Unregister and reset all traps to system default values before exiting
trap - EXIT INT TERM
exit 0
@@ -0,0 +1,111 @@
#!/bin/sh
# Copyright (C) 2008-2026 The OpenWrt Project
# Copyright (C) 2026 Manfred Jaider <info@masmbit.com>
#
# This is free software, licensed under the Apache License, Version 2.0.
# See /LICENSE for more information.
#
# luci-app-openvpn-plus : OpenWrt hotplug lifecycle manager for ddns server and client common name
# /etc/hotplug.d/openvpn/35-luci-app-openvpn-plus-lifecycle
# Ensure we catch the active OpenVPN name context across all OpenWrt variants
ovpn_instance="${INSTANCE:-$NAME}"
# Fallback to $1 if $dev is empty depending on the environment context
ovpn_dev="${dev:-$1}"
if [ -z "$ovpn_instance" ]; then
exit 0
fi
CONF_FILE="/etc/openvpn/luci/${ovpn_instance}.conf"
PID_FILE="/var/run/openvpn-ddns-${ovpn_instance}.pid"
case "$ACTION" in
up)
inst_role=$(uci -q get openvpn."$ovpn_instance".role)
# Save the active interface name into the OpenWrt UCI configuration
if [ "$inst_role" = "server" ] || [ "$inst_role" = "client" ]; then
if [ -n "$ovpn_dev" ]; then
current_devname=$(uci -q get openvpn."$ovpn_instance".devname)
if [ "$current_devname" != "$ovpn_dev" ]; then
uci -q set openvpn."$ovpn_instance".devname="$ovpn_dev"
uci commit openvpn
logger -t openvpn-luci "Hotplug (${ovpn_instance}): set devname ${ovpn_dev}"
fi
fi
fi
# SERVER: DDNS SERVICE START
if [ "$inst_role" = "server" ]; then
# Stop any old DDNS service processes if they are still running
if start-stop-daemon -K -q -p "$PID_FILE" -t 2>/dev/null; then
logger -t openvpn-luci "Hotplug: Stopping active DDNS service process for ${ovpn_instance} before configuration reload."
start-stop-daemon -K -q -p "$PID_FILE" 2>/dev/null
fi
rm -f "$PID_FILE"
# Process the new configuration only if the file exists
if [ -f "$CONF_FILE" ]; then
domain=$(grep -E "^setenv[[:space:]]+DDNS[[:space:]]+" "$CONF_FILE" | sed -E 's/^setenv[[:space:]]+DDNS[[:space:]]+"(.*)"/\1/')
url=$(grep -E "^setenv[[:space:]]+DDNS_URL[[:space:]]+" "$CONF_FILE" | sed -E 's/^setenv[[:space:]]+DDNS_URL[[:space:]]+"(.*)"/\1/')
if [ -n "$domain" ] && [ -n "$url" ]; then
logger -t openvpn-luci "Hotplug: Starting DDNS background service for ${ovpn_instance} (Domain: ${domain})"
start-stop-daemon -S -b -m -p "$PID_FILE" -x /usr/libexec/luci-app-openvpn-plus -- ddnsservice "$domain" "$url" "$ovpn_instance"
fi
fi
# CLIENT: EXTRACT COMMON NAME
elif [ "$inst_role" = "client" ]; then
cn_update="0"
# Extract the last connection initiation line from logread for this specific instance
log_line=$(logread | grep "openvpn(${ovpn_instance})" | grep "Peer Connection Initiated" | tail -n 1)
if [ -n "$log_line" ]; then
# Parse the pure CN string from inside the brackets (Example: [OfficeServer] -> OfficeServer)
server_cn=$(echo "$log_line" | sed -n 's/.*: \[\([^]]*\)\].*Peer Connection Initiated.*/\1/p')
if [ -n "$server_cn" ]; then
# Write the resolved Common Name safely into the OpenWrt UCI configuration
uci -q set openvpn."$ovpn_instance".server_common_name="$server_cn"
uci commit openvpn
cn_update="1"
fi
fi
if [ "$cn_update" = "1" ]; then
logger -t openvpn-luci "Hotplug (${ovpn_instance}): Server Common Name '${server_cn}' resolved"
else
logger -t openvpn-luci "Hotplug (${ovpn_instance}): Server Common Name not found in Peer Connection log entry"
fi
fi
;;
down)
inst_role=$(uci -q get openvpn."$ovpn_instance".role)
# Clear the mapped network device because the tunnel is offline
if [ "$inst_role" = "server" ] || [ "$inst_role" = "client" ]; then
uci -q delete openvpn."$ovpn_instance".devname
uci commit openvpn
fi
# SERVER: DDNS SERVICE STOP
if [ "$inst_role" = "server" ]; then
# Clean application stop via standard system manager signals
if start-stop-daemon -K -q -p "$PID_FILE" -t 2>/dev/null; then
logger -t openvpn-luci "Hotplug: Stopping DDNS service for ${ovpn_instance} because tunnel is down."
start-stop-daemon -K -q -p "$PID_FILE" 2>/dev/null
fi
rm -f "$PID_FILE"
fi
;;
esac
exit 0
@@ -0,0 +1,37 @@
# Modern OpenVPN Client Configuration Instance
# --- Connection Mode ---
client
dev tun
proto udp
# --- Network & IP Management ---
remote 127.0.0.1 1194
float
# --- Tuning & Optimization ---
explicit-exit-notify 1
mssfix 1360
tun-mtu 1420
# --- Certificates & Keys ---
ca /etc/openvpn/keys/ca_default.crt
cert /etc/openvpn/keys/client_default.crt
key /etc/openvpn/keys/client_default.key
# --- Hardening & Security ---
tls-crypt-v2 /etc/openvpn/keys/client_default.tls2.key
tls-groups secp256r1
cipher AES-256-GCM
data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305
# --- Verification & Logging ---
status /tmp/run/openvpn.instance.status 5
status-version 1
setenv logfilter on
connect-retry 10 60
resolv-retry infinite
nobind
persist-tun
remote-cert-tls server
verb 3
+158
View File
@@ -0,0 +1,158 @@
#!/bin/sh
# Copyright (C) 2008-2026 The OpenWrt Project
# Copyright (C) 2026 Manfred Jaider <info@masmbit.com>
#
# This is free software, licensed under the Apache License, Version 2.0.
# See /LICENSE for more information.
#
# luci-app-openvpn : smart firewall rules
# USEFUL COMMANDS FOR FIREWALL MANAGEMENT & LIVE MONITORING
#
# * Core Firewall Controls
# - Restart entire firewall framework: /etc/init.d/firewall restart
# - Reload UCI configuration profiles: /etc/init.d/firewall reload
# - View permanent firewall settings: cat /etc/config/firewall
#
# * Monitoring Chains & Structure
# - View entire dynamic fw4 table: nft -a list table inet fw4
# - View main input chain with handles: nft -a list chain inet fw4 input
# - View main forward chain with handles: nft -a list chain inet fw4 forward
# - View your custom OpenVPN sub-chain: nft -a list chain inet fw4 ovpn_filter_instance0
# - Quick view of custom sub-chain: nft -a list chain inet fw4 ovpn_filter_instance0 | sed -E -n 's/.*(openvpn[^"]+).*counter (packets [0-9]+ bytes [0-9]+).*(# handle [0-9]+).*/\3 -> "\1" counter \2/p'
#
# * Live Security & Attacker Tracking (The most useful commands)
# - View active IPv4 blocked IPs & timers: nft list set inet fw4 ovpn_block_ipv4_instance0
# - Flush/Clear a specific ban list now: nft flush set inet fw4 ovpn_block_ipv4_instance0
fwPort=443
fwProto="tcp"
SUB_CHAIN="ovpn_filter_instance0"
# STEP 1: CLEAN UP OLD RULES AND SETS (Prevents duplicates and memory leaks)
# Find and delete the old jump rule handle inside the main input chain to unlock the sub-chain
oldJumpHandle=$(nft -a list chain inet fw4 input | grep "jump $SUB_CHAIN" | grep -oE 'handle [0-9]+' | tail -n 1 | awk '{print $2}')
if [ -n "$oldJumpHandle" ]; then
nft "delete rule inet fw4 input handle $oldJumpHandle"
fi
# Now the sub-chain is unlocked and can be safely deleted without resource busy errors
nft "flush chain inet fw4 $SUB_CHAIN" 2>/dev/null
nft "delete chain inet fw4 $SUB_CHAIN" 2>/dev/null
nft "flush set inet fw4 ovpn_block_ipv4_instance0" 2>/dev/null
nft "delete set inet fw4 ovpn_block_ipv4_instance0" 2>/dev/null
nft "flush set inet fw4 ovpn_block_ipv6_instance0" 2>/dev/null
nft "delete set inet fw4 ovpn_block_ipv6_instance0" 2>/dev/null
# STEP 2: BUILD FRESH NAMED SETS WITH AUTOMATIC TIMEOUT
nft "add set inet fw4 ovpn_block_ipv4_instance0 { type ipv4_addr ; flags timeout ; }"
nft "add set inet fw4 ovpn_block_ipv6_instance0 { type ipv6_addr ; flags timeout ; }"
# STEP 3: CREATE THE CLEAN SUB-CHAIN CONTAINER
nft "add chain inet fw4 $SUB_CHAIN"
# STEP 4: BUILD THE CASCADE RULES (based on protocol)
if [ "$fwProto" = "tcp" ]; then
# TCP-S1: Instantly rejects traffic from IPv4/IPv6 addresses listed in the block sets.
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 ip saddr @ovpn_block_ipv4_instance0 log prefix \"openvpn(instance0)[firewall]{ban}: \" counter reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 ip6 saddr @ovpn_block_ipv6_instance0 log prefix \"openvpn(instance0)[firewall]{ban}: \" counter reject with tcp reset"
# TCP-S2: Blocks fragmented TCP packets before they can cause buffer overflows.
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 ip frag-off & 0x3fff != 0 log prefix \"openvpn(instance0)[firewall]{fragment}: \" counter reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 exthdr frag exists log prefix \"openvpn(instance0)[firewall]{fragment}: \" counter reject with tcp reset"
# TCP-S3: Validates strict TCP headers and forces standard handshake behaviors.
# - TCP Timestamps is required (This forces the TCP header size to be exactly 32 bytes for every data packets)
# - SYN packets must have maxseg, window and sack-perm option.
# - Data packets are forbidden to have the maxseg option.
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp option timestamp missing log prefix \"openvpn(instance0)[firewall]{timestamp}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == syn tcp option maxseg missing log prefix \"openvpn(instance0)[firewall]{maxseg}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == 0 tcp option maxseg exists log prefix \"openvpn(instance0)[firewall]{maxseg}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == syn tcp option window missing log prefix \"openvpn(instance0)[firewall]{wscale}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == syn tcp option sack-perm missing log prefix \"openvpn(instance0)[firewall]{sack}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp option timestamp missing log prefix \"openvpn(instance0)[firewall]{timestamp}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == syn tcp option maxseg missing log prefix \"openvpn(instance0)[firewall]{maxseg}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == 0 tcp option maxseg exists log prefix \"openvpn(instance0)[firewall]{maxseg}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == syn tcp option window missing log prefix \"openvpn(instance0)[firewall]{wscale}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == syn tcp option sack-perm missing log prefix \"openvpn(instance0)[firewall]{sack}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
# TCP-S4: Catches smart bots that delay data transmission to bypass early scanners.
# * CRITICAL MINIMUM DATA SIZES (Worst-Case Real OpenVPN Client):
# - Smallest possible tls-crypt-v2 key payload from a real client: 264 Bytes.
# - IPv4: Handshake (Packets 1-3) + Key (Packet 4) = 488 Bytes minimum.
# If network reception is very bad up to Packet 10 (empty ACKs): 748 Bytes total min.
# - IPv6: Handshake (Packets 1-3) + Key (Packet 4) = 548 Bytes minimum.
# If network reception is very bad up to Packet 10 (empty ACKs): 928 Bytes total min.
# * SAFE LOCKOUT THRESHOLDS (How the firewall decides):
# - IPv4 limit is set to < 700 Bytes. Bots with no data are smaller and get killed.
# - IPv6 limit is set to < 900 Bytes. Bots with no data are smaller and get killed.
# - Real worst-case OpenVPN users always stay safely above this byte radar.
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 ct packets > 9 meta nfproto ipv4 ct bytes < 700 log prefix \"openvpn(instance0)[firewall]{stealth}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 ct packets > 9 meta nfproto ipv6 ct bytes < 900 log prefix \"openvpn(instance0)[firewall]{stealth}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
# TCP-S5: Blocks payload data hidden inside the very first SYN packet (80 bytes for IPv4 packets and max 100 bytes for IPv6)
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == syn tcp option maxseg exists meta length > 80 log prefix \"openvpn(instance0)[firewall]{fastopen}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == syn tcp option maxseg exists meta length > 100 log prefix \"openvpn(instance0)[firewall]{fastopen}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
# TCP-S6: Deep Packet Inspection for bad protocol lengths and fake small sizes.
# - Starts only when a data packet is bigger than 53 bytes (IPv4) or 73 bytes (IPv6).
# - Reads the 16-bit OpenVPN packet length field at the exact bit offset pointer 256.
# - Catches bad opcodes, web text (like GET/POST), and fake sizes (payload < 155, payload > 1499).
# - Real users with tls-crypt-v2 are safe because encrypted traffic never matches this bad matrix.
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == 0 meta length > 53 @th,256,16 < 0x009b log prefix \"openvpn(instance0)[firewall]{payload}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == 0 meta length > 53 @th,256,16 > 0x05db log prefix \"openvpn(instance0)[firewall]{payload}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == 0 meta length > 73 @th,256,16 < 0x009b log prefix \"openvpn(instance0)[firewall]{payload}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == 0 meta length > 73 @th,256,16 > 0x05db log prefix \"openvpn(instance0)[firewall]{payload}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset"
# TCP-S7: Packets that survived all previous checks are genuine.
nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort accept"
else
# UDP-S1: Instantly drops traffic from IPv4/IPv6 addresses listed in the block sets.
# (Bleibt bei ct state new, damit bestehender established Tunnel-Traffic niemals blockiert wird!)
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new ip saddr @ovpn_block_ipv4_instance0 log prefix \"openvpn(instance0)[firewall]{ban}: \" counter drop"
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new ip6 saddr @ovpn_block_ipv6_instance0 log prefix \"openvpn(instance0)[firewall]{ban}: \" counter drop"
# UDP-S2: Blocks fragmented UDP packets before they can cause buffer overflows.
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 ip frag-off & 0x3fff != 0 log prefix \"openvpn(instance0)[firewall]{fragment}: \" counter drop"
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 exthdr frag exists log prefix \"openvpn(instance0)[firewall]{fragment}: \" counter drop"
# UDP-S3: Smallest valid OpenVPN packet is 48 bytes. Anything smaller is dropped.
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new udp length < 48 log prefix \"openvpn(instance0)[firewall]{short}: \" counter drop"
# UDP-S4: Drops and bans new IPv4 bots that scan with a zero checksum to save time.
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 udp checksum 0 log prefix \"openvpn(instance0)[firewall]{chksum}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } drop"
# UDP-S5: Whitelist valid initial OpenVPN opcodes P_CONTROL_HARD_RESET_CLIENT_V1,2,3 (skipping the 64-bit UDP header)
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x08 udp length > 250 accept"
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x38 udp length > 250 accept"
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x50 udp length > 250 accept"
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x08 udp length > 250 accept"
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x38 udp length > 250 accept"
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x50 udp length > 250 accept"
# UDP-S6: Instantly ban and drop any other packet that tries to start a new connection
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 log prefix \"openvpn(instance0)[firewall]{opcode}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } drop"
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 log prefix \"openvpn(instance0)[firewall]{opcode}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } drop"
# UDP-S7: Packets that survived all previous checks are genuine.
nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort accept"
fi
# STEP 4: LINK THE SUB-CHAIN TO THE MAIN INPUT (single jump rule)
# Find the starting anchor in the main input chain (Loopback handle)
mainAnchorHandle=$(nft -a list chain inet fw4 input | grep -E 'iif ["'\'']?lo["'\'']? accept' | grep -oE 'handle [0-9]+' | tail -n 1 | awk '{print $2}')
if [ -z "$mainAnchorHandle" ]; then
mainAnchorHandle=35
fi
# Inject the dynamic jump trigger right below the loopback anchor rule
nft "add rule inet fw4 input handle $mainAnchorHandle $fwProto dport $fwPort jump $SUB_CHAIN"
@@ -0,0 +1,47 @@
# Modern OpenVPN Server Configuration Instance
# --- Connection Mode ---
dev tun
proto udp
# --- Network & IP Management ---
port 1194
setenv portextern 1194
server 10.8.0.0 255.255.255.0
server-ipv6 fd00:db8:0:1::/64
topology subnet
# --- Tuning & Optimization ---
tls-timeout 5
mssfix 1360
tun-mtu 1420
# Increased max-clients absorbs attacks and client roaming
max-clients 4096
# --- Certificates & Keys ---
ca /etc/openvpn/keys/ca_default.crt
cert /etc/openvpn/keys/server_default.crt
key /etc/openvpn/keys/server_default.key
dh /etc/openvpn/keys/dh_default.pem
# --- Hardening & Security ---
tls-crypt-v2 /etc/openvpn/keys/server_default.tls2.key
tls-groups secp256r1
cipher AES-256-GCM
data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305
# --- Smart Firewall ---
setenv ovpnfirewall on
# --- Client Push Options ---
push "dhcp-option DNS 9.9.9.9"
push "dhcp-option DNS 149.112.112.112"
push "dhcp-option DNS6 2620:fe::fe"
push "dhcp-option DNS6 2620:fe::9"
# --- Verification & Logging ---
status /tmp/run/openvpn.instance.status 5
status-version 1
setenv logfilter on
keepalive 10 60
verb 3
@@ -0,0 +1,70 @@
#!/bin/sh
# Copyright (C) 2008-2026 The OpenWrt Project
# Copyright (C) 2026 Manfred Jaider <info@masmbit.com>
#
# This is free software, licensed under the Apache License, Version 2.0.
# See /LICENSE for more information.
#
# luci-app-openvpn-plus : clean default sample profiles and prepare empty configuration
# /etc/uci-defaults/35_luci-app-openvpn-plus
# Path to the UCI configuration file
CONFIG_FILE="${IPKG_INSTROOT}/etc/config/openvpn"
if [ -f "$CONFIG_FILE" ]; then
# 1. Verify that all three standard firmware sample entries exist exactly as delivered
if grep -q "config openvpn custom_config" "$CONFIG_FILE" 2>/dev/null && \
grep -q "config openvpn sample_server" "$CONFIG_FILE" 2>/dev/null && \
grep -q "config openvpn sample_client" "$CONFIG_FILE" 2>/dev/null; then
# 2. Check specifically for the UCI section pattern created by LuCI
if ! grep -q "config openvpn 'instance" "$CONFIG_FILE" 2>/dev/null; then
logger -t luci-app-openvpn-plus "Unmodified factory samples detected. Backing up to openvpn-def."
# Backup the cluttered firmware default file safely
cp -f "$CONFIG_FILE" "${IPKG_INSTROOT}/etc/config/openvpn-def" 2>/dev/null
# Create a clean, empty base configuration for your LuCI app
echo "# Clean OpenVPN base configuration" > "$CONFIG_FILE"
CLEAN_OPENVPN_SERVICE=1
fi
else
# If any check fails, the user has modified the file. DO NOT TOUCH IT!
logger -t luci-app-openvpn-plus "Modified or custom OpenVPN configuration detected. Keeping user data safe."
fi
# 3. Check if we are running on a live runtime system (opkg install / apk add)
if [ -z "${IPKG_INSTROOT}" ]; then
logger -t luci-app-openvpn-plus "Package installed at runtime. Clearing all cache and reloading services."
# Restart rpcd in the background after 3 seconds! This gives LuCI enough time to finish the installation in the browser.
(
# Restart the openvpn service only if we cleaned the config above
if [ "$CLEAN_OPENVPN_SERVICE" = "1" ] && [ -x "/etc/init.d/openvpn" ]; then
/etc/init.d/openvpn stop >/dev/null 2>&1
sleep 1
/etc/init.d/openvpn restart >/dev/null 2>&1
sleep 1
/etc/init.d/openvpn reload >/dev/null 2>&1
sleep 1
else
sleep 3
fi
# Delete ALL temporary LuCI files.
rm -rf /tmp/luci-* >/dev/null 2>&1
# Hard reset rpcd to finds the VPN menu and new ACL files.
if [ -x "/etc/init.d/rpcd" ]; then
/etc/init.d/rpcd restart >/dev/null 2>&1
fi
) &
else
logger -t luci-app-openvpn-plus "Image building state detected. Skipping live runtime triggers."
fi
fi
exit 0
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,9 @@
{
"admin/vpn/openvpn": {
"title": "OpenVPN",
"action": { "type": "view", "path": "vpn/openvpn" },
"order": 10,
"depends": { "acl": [ "luci-app-openvpn-plus" ] }
}
}
@@ -0,0 +1,19 @@
{
"luci-app-openvpn-plus-status": {
"description": "Status access for OpenVPN LuCI app",
"read": {
"cgi-bin": [ "luci" ],
"uci": [ "openvpn" ],
"ubus": {
"system": [ "info" ],
"service": [ "list" ],
"network": [ "status" ]
},
"file": {
"/etc/openvpn/luci/*": [ "read" ],
"/proc/uptime": [ "read" ],
"/usr/libexec/luci-app-openvpn-plus": [ "exec" ]
}
}
}
}
@@ -0,0 +1,39 @@
{
"luci-app-openvpn-plus": {
"description": "Configuration access for OpenVPN LuCI app",
"read": {
"cgi-bin": [ "luci" ],
"uci": [ "openvpn" ],
"ubus": {
"system": [ "info" ],
"service": [ "list" ],
"network": [ "status" ]
},
"file": {
"/etc/openvpn/luci/*": [ "read" ],
"/etc/openvpn/keys/*": [ "read" ],
"/var/run/openvpn.keygen.lock": [ "read" ],
"/proc/uptime": [ "read" ],
"/tmp/openvpn.keygen.log": [ "read" ],
"/tmp/openvpn.keygen.ca.tmp": [ "read" ],
"/tmp/openvpn.keygen.server-key.tmp": [ "read" ],
"/tmp/openvpn.keygen.client-key.tmp": [ "read" ],
"/tmp/openvpn.keygen.server-cert.tmp": [ "read" ],
"/tmp/openvpn.keygen.client-cert.tmp": [ "read" ],
"/tmp/openvpn.keygen.dh.tmp": [ "read" ],
"/tmp/openvpn.keygen.server-tls2.tmp": [ "read" ],
"/tmp/openvpn.keygen.client-tls2.tmp": [ "read" ],
"/sbin/logread": [ "exec" ],
"/usr/libexec/luci-app-openvpn-plus": [ "exec" ]
}
},
"write": {
"uci": [ "openvpn" ],
"file": {
"/etc/openvpn/luci/*": [ "write" ],
"/etc/openvpn/keys/*": [ "write" ],
"/etc/config/openvpn": [ "write" ]
}
}
}
}
@@ -0,0 +1,189 @@
/*
* Copyright (C) 2008-2026 The OpenWrt Project
* Copyright (C) 2026 Manfred Jaider <info@masmbit.com>
*
* This is free software, licensed under the Apache License, Version 2.0.
* See /LICENSE for more information.
*
* luci-app-openvpn-plus : LuCI status overview page
* /www/luci-static/resources/view/status/include/35_openvpn.js
*/
/* global E */
'use strict';
const TXT = {
STATUS: {
openvpn: _('OpenVPN'),
active: _('Active'),
disabled: _('Disabled'),
pending: _('Pending...'),
error: _('Error')
}
};
const CFG = Object.freeze({
FILE: Object.freeze({
vpn_disabled_img: '/luci-static/resources/icons/tunnel_disabled.svg',
vpn_enabled_img: '/luci-static/resources/icons/tunnel.svg'
})
});
const OPENVPN = Object.freeze({
STATE: Object.freeze({
pending: 'pending',
active: 'active',
error: 'error'
})
});
/**
* Main data storage for the OpenVPN page
*/
const APP_DATA_TEMPLATE = Object.freeze({
statusClass: null,
wizardClass: null,
keygenClass: null,
uptime: 0,
serverTemplate: '',
clientTemplate: '',
logread: '',
keysReady: false,
ovpnUciSections: null,
instances: null
});
/**
* Main entry point where the page starts loading
*/
return L.Class.extend({
title: TXT.STATUS.openvpn,
// Create main data storage
APP_DATA: Object.assign({}, APP_DATA_TEMPLATE, {
ovpnUciSections: [],
instances: []
}),
// Shared HTML container nodes
statusTableContainer: null,
ifaceBoxContainer: null,
/**
* Load code and data before rendering the page elements
*/
load: function () {
const self = this;
// Load all required classes
return Promise.all([
L.require('view.vpn.openvpn-status'),
L.uci.load('openvpn').catch(function () { return null; })
]).then(async function (results) {
const openvpnStatus = results[0];
if (!openvpnStatus) return null;
await openvpnStatus.onLoad();
// Gets the openvon sections from LuCI
const ovpnSections = L.uci.sections('openvpn', 'openvpn') || [];
// Sorts the array alphabetically based on the internal '.name' (instance1, instance2, ...)
ovpnSections.sort(function (a, b) {
return a['.name'].localeCompare(b['.name'], undefined, { numeric: true, sensitivity: 'base' });
});
// If no instances exist, clear title and hide the card block completely
if (ovpnSections.length === 0) {
self.title = '';
return null;
}
self.title = TXT.STATUS.openvpn;
self.statusTableContainer = E('div', { 'id': 'openvpn_overview_table_wrapper', 'style': 'display: block;' }, [E('div', {})]);
self.ifaceBoxContainer = E('div', { 'style': 'display:block; margin: 10px 0 15px 0; text-align: left;' }, []);
// Use the safe outer context reference "self" to read the view data
const appData = self.APP_DATA;
appData.ovpnUciSections = ovpnSections;
// Callback function to refresh the status table
const refreshStatusBoxCallback = function (ovpnState, tooltip, appData) {
if (!self.ifaceBoxContainer) return
let labelText = TXT.STATUS.disabled
let headBg = 'background:var(--background-color-medium, #f4f4f5) !important;'
let textColor = 'var(--text-color-high, #333333)'
let imgIcon = CFG.FILE.vpn_disabled_img
if (ovpnState === OPENVPN.STATE.active) {
labelText = TXT.STATUS.active
headBg = 'background:var(--success-color-high, #00ac59) !important;'
textColor = 'var(--on-success-color, white)'
imgIcon = CFG.FILE.vpn_enabled_img
} else if (ovpnState === OPENVPN.STATE.pending) {
labelText = TXT.STATUS.pending
headBg = 'background:var(--warn-color-high, #efbd0b) !important;'
textColor = 'var(--on-warn-color, #000000)'
} else if (ovpnState === OPENVPN.STATE.error) {
labelText = TXT.STATUS.error
headBg = 'background:var(--error-color-high, #f62b12) !important;'
textColor = 'var(--on-error-color, white)'
}
const boxHeadNode = E('div', {
'class': 'ifacebox-head',
'style': 'padding:3px 8px; font-size:12px; font-weight:bold; text-shadow:none !important; text-align:center; white-space:nowrap; ' + headBg
}, [
E('strong', { 'style': 'color: ' + textColor + ' !important;' }, labelText)
]);
const badgeImgNode = E('img', { 'src': imgIcon, 'style': 'width:32px; height:36px; vertical-align:middle;' });
const tooltipContainer = E('span', { 'class': 'cbi-tooltip-container' }, [
badgeImgNode,
tooltip
]);
const boxBodyNode = E('div', {
'class': 'ifacebox-body',
'style': 'padding:6px; text-align:center; min-height:0; background:transparent !important;'
}, [
tooltipContainer
]);
const flatIfaceBox = E('div', {
'class': 'ifacebox',
'style': 'display:inline-block; margin:0; border-radius:4px; overflow:hidden;'
}, [boxHeadNode, boxBodyNode]);
if (self.ifaceBoxContainer.firstChild) {
self.ifaceBoxContainer.replaceChild(flatIfaceBox, self.ifaceBoxContainer.firstChild);
} else {
self.ifaceBoxContainer.appendChild(flatIfaceBox);
}
};
try {
// Use the clean await operator to trigger the live dashboard refresh loop flatly
await openvpnStatus.refreshLiveDashboard(appData, self.statusTableContainer, refreshStatusBoxCallback);
return openvpnStatus;
} catch (err) {
console.error('Error during initial dashboard telemetry launch:', err);
return openvpnStatus;
}
});
},
/**
* Render the page elements onto the interface
*/
render: function (openvpnStatus) {
if (!openvpnStatus || !this.statusTableContainer || !this.ifaceBoxContainer) {
return null;
}
return E('div', { 'class': 'cbi-section', 'style': 'margin:0; padding:0; width:100%;' }, [
this.ifaceBoxContainer,
this.statusTableContainer
]);
}
});
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff