From d4081c498ddca184578903fe5199d390bbc0707b Mon Sep 17 00:00:00 2001
From: Syrone Wong <wong.syrone@gmail.com>
Date: Sat, 9 Apr 2022 13:24:19 +0800
Subject: [PATCH] firewall4: add fullcone support

fullcone is drop-in replacement of masq for non-udp traffic

add runtime fullcone rule check, disable it globally if fullcone expr is
invalid

defaults.fullcone is the global switch, while zone.fullcone4 and
zone.fullcone6 are switches for IPv4 and IPv6 respectively, most
IPv6 traffic do NOT need this FullCone NAT functionality.
---
 root/etc/config/firewall                      |  3 +
 root/usr/share/firewall4/templates/ruleset.uc | 16 +++-
 .../firewall4/templates/zone-fullcone.uc      |  4 +
 root/usr/share/ucode/fw4.uc                   | 76 ++++++++++++++++++-
 4 files changed, 96 insertions(+), 3 deletions(-)
 create mode 100644 root/usr/share/firewall4/templates/zone-fullcone.uc

--- a/root/etc/config/firewall
+++ b/root/etc/config/firewall
@@ -5,6 +5,7 @@ config defaults
 	option forward		REJECT
 # Uncomment this line to disable ipv6 rules
 #	option disable_ipv6	1
+	option fullcone '1'
 
 config zone
 	option name		lan
@@ -20,6 +21,8 @@ config zone
 	option input		REJECT
 	option output		ACCEPT
 	option forward		DROP
+	option fullcone4 '1'
+	option fullcone6 '1'
 	option masq		1
 	option mtu_fix		1
 
--- a/root/usr/share/firewall4/templates/ruleset.uc
+++ b/root/usr/share/firewall4/templates/ruleset.uc
@@ -323,6 +323,12 @@ table inet fw4 {
 {%   for (let redirect in fw4.redirects(`dstnat_${zone.name}`)): %}
 		{%+ include("redirect.uc", { fw4, zone, redirect }) %}
 {%   endfor %}
+{%   if (zone.fullcone4): %}
+		{%+ include("zone-fullcone.uc", { fw4, zone, family: 4, direction: "dstnat" }) %}
+{%   endif %}
+{%   if (zone.fullcone6): %}
+		{%+ include("zone-fullcone.uc", { fw4, zone, family: 6, direction: "dstnat" }) %}
+{%   endif %}
 {%   fw4.includes('chain-append', `dstnat_${zone.name}`) %}
 	}
 
@@ -333,20 +339,26 @@ table inet fw4 {
 {%   for (let redirect in fw4.redirects(`srcnat_${zone.name}`)): %}
 		{%+ include("redirect.uc", { fw4, zone, redirect }) %}
 {%   endfor %}
-{%   if (zone.masq): %}
+{%   if (zone.masq && !zone.fullcone4): %}
 {%    for (let saddrs in zone.masq4_src_subnets): %}
 {%     for (let daddrs in zone.masq4_dest_subnets): %}
 		{%+ include("zone-masq.uc", { fw4, zone, family: 4, saddrs, daddrs }) %}
 {%     endfor %}
 {%    endfor %}
 {%   endif %}
-{%   if (zone.masq6): %}
+{%   if (zone.masq6 && !zone.fullcone6): %}
 {%    for (let saddrs in zone.masq6_src_subnets): %}
 {%     for (let daddrs in zone.masq6_dest_subnets): %}
 		{%+ include("zone-masq.uc", { fw4, zone, family: 6, saddrs, daddrs }) %}
 {%     endfor %}
 {%    endfor %}
 {%   endif %}
+{%   if (zone.fullcone4): %}
+		{%+ include("zone-fullcone.uc", { fw4, zone, family: 4, direction: "srcnat" }) %}
+{%   endif %}
+{%   if (zone.fullcone6): %}
+		{%+ include("zone-fullcone.uc", { fw4, zone, family: 6, direction: "srcnat" }) %}
+{%   endif %}
 {%   fw4.includes('chain-append', `srcnat_${zone.name}`) %}
 	}
 
--- /dev/null
+++ b/root/usr/share/firewall4/templates/zone-fullcone.uc
@@ -0,0 +1,4 @@
+{# /usr/share/firewall4/templates/zone-fullcone.uc #}
+		meta nfproto {{ fw4.nfproto(family) }} fullcone comment "!fw4: Handle {{
+		zone.name
+}} {{ fw4.nfproto(family, true) }} fullcone NAT {{ direction }} traffic"
--- a/root/usr/share/ucode/fw4.uc
+++ b/root/usr/share/ucode/fw4.uc
@@ -1,3 +1,5 @@
+// /usr/share/ucode/fw4.uc
+
 const fs = require("fs");
 const uci = require("uci");
 const ubus = require("ubus");
@@ -489,6 +491,25 @@ function nft_try_hw_offload(devices) {
 	return (rc == 0);
 }
 
+function nft_try_fullcone() {
+	let nft_test =
+		'add table inet fw4-fullcone-test; ' +
+		'add chain inet fw4-fullcone-test dstnat { ' +
+			'type nat hook prerouting priority -100; policy accept; ' +
+			'fullcone; ' +
+		'}; ' +
+		'add chain inet fw4-fullcone-test srcnat { ' +
+			'type nat hook postrouting priority -100; policy accept; ' +
+			'fullcone; ' +
+		'}; ';
+	let cmd = sprintf("/usr/sbin/nft -c '%s' 2>/dev/null", replace(nft_test, "'", "'\\''"));
+	let ok = system(cmd) == 0;
+	if (!ok) {
+		warn("nft_try_fullcone: cmd "+ cmd + "\n");
+	}
+	return ok;
+}
+
 
 return {
 	read_kernel_version: function() {
@@ -855,6 +876,18 @@ return {
 			warn(`[!] ${msg}\n`);
 	},
 
+	myinfo: function(fmt, ...args) {
+		if (getenv("QUIET"))
+			return;
+
+		let msg = sprintf(fmt, ...args);
+
+		if (getenv("TTY"))
+			warn(`\033[32m${msg}\033[m\n`);
+		else
+			warn(`[I] ${msg}\n`);
+	},
+
 	get: function(sid, opt) {
 		return this.cursor.get("firewall", sid, opt);
 	},
@@ -1036,6 +1069,21 @@ return {
 		}
 	},
 
+	myinfo_section: function(s, msg) {
+		if (s[".name"]) {
+			if (s.name)
+				this.myinfo("Section %s (%s) %s", this.section_id(s[".name"]), s.name, msg);
+			else
+				this.myinfo("Section %s %s", this.section_id(s[".name"]), msg);
+		}
+		else {
+			if (s.name)
+				this.myinfo("ubus %s (%s) %s", s.type || "rule", s.name, msg);
+			else
+				this.myinfo("ubus %s %s", s.type || "rule", msg);
+		}
+	},
+
 	parse_policy: function(val) {
 		return this.parse_enum(val, [
 			"accept",
@@ -1475,6 +1523,7 @@ return {
 			"dnat",
 			"snat",
 			"masquerade",
+			"fullcone",
 			"accept",
 			"reject",
 			"drop"
@@ -1946,6 +1995,7 @@ return {
 		}
 
 		let defs = this.parse_options(data, {
+			fullcone: [ "bool", "0" ],
 			input: [ "policy", "drop" ],
 			output: [ "policy", "drop" ],
 			forward: [ "policy", "drop" ],
@@ -1980,6 +2030,11 @@ return {
 
 		delete defs.syn_flood;
 
+		if (!nft_try_fullcone()) {
+			delete defs.fullcone;
+			warn("nft_try_fullcone failed, disable fullcone globally\n");
+		}
+
 		this.state.defaults = defs;
 	},
 
@@ -2004,6 +2059,8 @@ return {
 			masq_dest: [ "network", null, PARSE_LIST ],
 
 			masq6: [ "bool" ],
+			fullcone4: [ "bool", "0" ],
+			fullcone6: [ "bool", "0" ],
 
 			extra: [ "string", null, UNSUPPORTED ],
 			extra_src: [ "string", null, UNSUPPORTED ],
@@ -2036,6 +2093,18 @@ return {
 			}
 		}
 
+		if (this.state.defaults && !this.state.defaults.fullcone) {
+			this.warn_section(data, "fullcone in defaults not enabled, ignore zone fullcone settings");
+			zone.fullcone4 = false;
+			zone.fullcone6 = false;
+		}
+		if (zone.fullcone4) {
+			this.myinfo_section(data, "IPv4 fullcone enabled for zone '" + zone.name + "'");
+		}
+		if (zone.fullcone6) {
+			this.myinfo_section(data, "IPv6 fullcone enabled for zone '" + zone.name + "'");
+		}
+
 		if (zone.mtu_fix && this.kernel < 0x040a0000) {
 			this.warn_section(data, "option 'mtu_fix' requires kernel 4.10 or later");
 			return;
@@ -2205,10 +2274,15 @@ return {
 		zone.related_subnets = related_subnets;
 		zone.related_physdevs = related_physdevs;
 
+		if (zone.fullcone4 || zone.fullcone6) {
+			zone.dflags.snat = true;
+			zone.dflags.dnat = true;
+		}
+
 		if (zone.masq || zone.masq6)
 			zone.dflags.snat = true;
 
-		if ((zone.auto_helper && !(zone.masq || zone.masq6)) || length(zone.helper)) {
+		if ((zone.auto_helper && !(zone.masq || zone.masq6 || zone.fullcone4 || zone.fullcone6)) || length(zone.helper)) {
 			zone.dflags.helper = true;
 
 			for (let helper in (length(zone.helper) ? zone.helper : this.state.helpers)) {
