diff --git a/docker/routlin-dash/app/pages/ddns/content.json b/docker/routlin-dash/app/pages/ddns/content.json
index 38ff3d1..2b229f3 100644
--- a/docker/routlin-dash/app/pages/ddns/content.json
+++ b/docker/routlin-dash/app/pages/ddns/content.json
@@ -261,7 +261,11 @@
"input_type": "number",
"layout": "inline",
"value": "%DDNS_GEN_LOG_MAX_KB%",
- "min": "64"
+ "min": "64",
+ "hint": "Log will automatically be cleared when it reaches this size."
+ },
+ {
+ "type": "hr"
},
{
"type": "field",
diff --git a/docker/routlin-dash/app/pages/intervlan/content.json b/docker/routlin-dash/app/pages/intervlan/content.json
index 526dd18..00df5ad 100644
--- a/docker/routlin-dash/app/pages/intervlan/content.json
+++ b/docker/routlin-dash/app/pages/intervlan/content.json
@@ -143,29 +143,35 @@
"type": "field_row",
"cols": 3,
"items": [
+ {
+ "type": "field_row",
+ "cols": 2,
+ "items": [
+ {
+ "type": "field",
+ "label": "Dest Port Range",
+ "name": "dst_port_min",
+ "input_type": "number",
+ "min": 1,
+ "max": 65535,
+ "hint": "This exception only applies to traffic over this port range and protocol."
+ },
+ {
+ "type": "field",
+ "label": "",
+ "name": "dst_port_max",
+ "input_type": "number",
+ "min": 1,
+ "max": 65535
+ }
+ ]
+ },
{
"type": "field",
"label": "Protocol",
"name": "protocol",
"input_type": "select",
"options": "%PROTOCOL_OPTIONS%"
- },
- {
- "type": "field",
- "label": "Port Min",
- "name": "dst_port_min",
- "input_type": "number",
- "min": 1,
- "max": 65535,
- "hint": "This exception only applies to traffic over this port range and protocol."
- },
- {
- "type": "field",
- "label": "Port Max",
- "name": "dst_port_max",
- "input_type": "number",
- "min": 1,
- "max": 65535
}
]
},
diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py
index d985282..110da10 100644
--- a/docker/routlin-dash/app/view_page.py
+++ b/docker/routlin-dash/app/view_page.py
@@ -760,7 +760,7 @@ def collect_tokens():
'
'
''
''
- ''
+ ''
''
''
f'{hist_rows}'
diff --git a/routlin/core.py b/routlin/core.py
index cd8544a..fa7528d 100644
--- a/routlin/core.py
+++ b/routlin/core.py
@@ -67,10 +67,9 @@ Validation:
must be valid. Protocol must be tcp, udp, or both.
Generates DNAT rules only; no forward chain rules needed
since redirect_to is always a local IP (INPUT handles it).
- inter_vlan_exceptions -- src_ip_or_subnet and dst_ip_or_subnet must be valid IPv4 addresses declared in the vlans array.
- inter_vlan_exceptions -- src_ip_or_subnet and dst_ip_or_subnet must be valid IPv4 addresses
- or networks. dst_port must be valid (1-65535). Protocol
- must be tcp, udp, or both.
+ inter_vlan_exceptions -- src_ip_or_subnet and dst_ip_or_subnet may be a single IPv4 address
+ or a CIDR network. dst_port_min/dst_port_max are optional (1-65535).
+ Protocol must be tcp, udp, or both.
Usage:
sudo python3 core.py --apply Apply config fast: restart running services only
@@ -1559,15 +1558,22 @@ def build_nft_config(data, dry_run=False):
line(" # -- Inter-VLAN exceptions ------------------------------------------")
line()
for r in all_except:
- src = r["src_ip_or_subnet"]
- dst = r.get("dst_ip_or_subnet") or r.get("dst_ip", "")
- port = r.get("dst_port")
+ src = r["src_ip_or_subnet"]
+ dst = r.get("dst_ip_or_subnet") or r.get("dst_ip", "")
+ min_p = r.get("dst_port_min") or r.get("dst_port")
+ max_p = r.get("dst_port_max")
+ if min_p and max_p and str(min_p) != str(max_p):
+ port_spec = f"{min_p}-{max_p}"
+ elif min_p:
+ port_spec = str(min_p)
+ else:
+ port_spec = None
for proto, _, suffix in expand_protocols(r):
line(f" # {r['description']}{suffix}")
- if port is not None:
- line(f" ip saddr {src} ip daddr {dst} {proto} dport {port} ct state new accept")
+ if port_spec is not None:
+ line(f" ip saddr {src} ip daddr {dst} {proto} dport {port_spec} ct state new accept")
else:
- line(f" ip saddr {src} ip daddr {dst} ct state new accept")
+ line(f" ip saddr {src} ip daddr {dst} {proto} ct state new accept")
line()
if all_fwd:
@@ -1731,10 +1737,17 @@ def apply_nftables(data, dry_run=False):
print()
print("Active inter-VLAN exceptions:")
for r in active_except:
- src = r["src_ip_or_subnet"]
- dst = r.get("dst_ip_or_subnet") or r.get("dst_ip", "")
- port = r.get("dst_port")
- dst_str = f"{dst}:{port}" if port is not None else dst
+ src = r["src_ip_or_subnet"]
+ dst = r.get("dst_ip_or_subnet") or r.get("dst_ip", "")
+ min_p = r.get("dst_port_min") or r.get("dst_port")
+ max_p = r.get("dst_port_max")
+ if min_p and max_p and str(min_p) != str(max_p):
+ port_str = f":{min_p}-{max_p}"
+ elif min_p:
+ port_str = f":{min_p}"
+ else:
+ port_str = ""
+ dst_str = f"{dst}{port_str}"
print(f" [{r['protocol'].upper():<4}] {src} -> {dst_str} ({r['description']})")
def show_rules():