From aa3b56e289fba7425e649a608c333622ffd9c367 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 and defaults.fullcone6 are switches for IPv4 and IPv6
respectively, most IPv6 traffic do NOT need this FullCone NAT functionality.

Renew: ZiMing Mo <msylgj@immortalwrt.org>
---
 root/etc/config/firewall                      |  2 ++
 root/usr/share/firewall4/templates/ruleset.uc | 16 ++++++++++++++--
 .../firewall4/templates/zone-fullcone.uc      |  4 ++++
 root/usr/share/ucode/fw4.uc                   | 69 ++++++++++++++++++-
 4 files changed, 89 insertions(+), 4 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,10 @@ config defaults
 	option forward		REJECT
 # Uncomment this line to disable ipv6 rules
 #	option disable_ipv6	1
+	option flow_offloading	1
+	option flow_offloading_hw	1
+	option fullcone		1
+	option fullcone6	0
 
 config zone
 	option name		lan
--- a/root/usr/share/firewall4/templates/ruleset.uc
+++ b/root/usr/share/firewall4/templates/ruleset.uc
@@ -320,6 +320,12 @@ table inet fw4 {
 {%   for (let redirect in fw4.redirects(`dstnat_${zone.name}`)): %}
 		{%+ include("redirect.uc", { fw4, redirect }) %}
 {%   endfor %}
+{%   if (zone.masq && fw4.default_option("fullcone")): %}
+		{%+ include("zone-fullcone.uc", { fw4, zone, family: 4, direction: "dstnat" }) %}
+{%   endif %}
+{%   if (zone.masq6 && fw4.default_option("fullcone6")): %}
+		{%+ include("zone-fullcone.uc", { fw4, zone, family: 6, direction: "dstnat" }) %}
+{%   endif %}
 {%   fw4.includes('chain-append', `dstnat_${zone.name}`) %}
 	}
 
@@ -330,20 +336,26 @@ table inet fw4 {
 {%   for (let redirect in fw4.redirects(`srcnat_${zone.name}`)): %}
 		{%+ include("redirect.uc", { fw4, redirect }) %}
 {%   endfor %}
-{%   if (zone.masq): %}
+{%   if (zone.masq && !fw4.default_option("fullcone")): %}
 {%    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 && !fw4.default_option("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.masq && fw4.default_option("fullcone")): %}
+		{%+ include("zone-fullcone.uc", { fw4, zone, family: 4, direction: "srcnat" }) %}
+{%   endif %}
+{%   if (zone.masq6 && fw4.default_option("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");
@@ -490,6 +492,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() {
@@ -840,6 +861,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);
 	},
@@ -1021,6 +1054,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",
@@ -1460,6 +1508,7 @@ return {
 			"dnat",
 			"snat",
 			"masquerade",
+			"fullcone",
 			"accept",
 			"reject",
 			"drop"
@@ -1927,6 +1976,8 @@ return {
 		}
 
 		let defs = this.parse_options(data, {
+			fullcone: [ "bool", "0" ],
+			fullcone6: [ "bool", "0" ],
 			input: [ "policy", "drop" ],
 			output: [ "policy", "drop" ],
 			forward: [ "policy", "drop" ],
@@ -1961,6 +2012,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;
 	},
 
@@ -2186,10 +2242,23 @@ return {
 		zone.related_subnets = related_subnets;
 		zone.related_physdevs = related_physdevs;
 
-		if (zone.masq || zone.masq6)
+		if (zone.masq) {
 			zone.dflags.snat = true;
+			if (this.state.defaults.fullcone) {
+				zone.dflags.dnat = true;
+				this.myinfo_section(data, "IPv4 fullcone enabled for zone '" + zone.name + "'");
+			}
+		}
+
+		if (zone.masq6) {
+			zone.dflags.snat = true;
+			if (this.state.defaults.fullcone6) {
+				zone.dflags.dnat = true;
+				this.myinfo_section(data, "IPv6 fullcone enabled for zone '" + zone.name + "'");
+			}
+		}
 
-		if ((zone.auto_helper && !(zone.masq || zone.masq6)) || length(zone.helper)) {
+		if ((zone.auto_helper && !(zone.masq || zone.masq6 || this.state.defaults.fullcone || this.state.defaults.fullcone6)) || length(zone.helper)) {
 			zone.dflags.helper = true;
 
 			for (let helper in (length(zone.helper) ? zone.helper : this.state.helpers)) {
