schema: introduce port-forward and traffic-allow firewall settings

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
Jo-Philipp Wich
2022-04-06 23:40:56 +02:00
committed by John Crispin
parent 084b847b8a
commit 2bf1645d3a
11 changed files with 777 additions and 0 deletions

View File

@@ -65,6 +65,18 @@
return;
}
// Port forwardings are only supported on downstream interfaces
if ((interface.ipv4?.port_forward || interface.ipv6?.port_forward) && interface.role != 'downstream') {
warn("Port forwardings are only supported on downstream interfaces.");
return;
}
// Traffic accept rules are only supported on downstream interfaces
if (interface.ipv6?.traffic_allow && interface.role != 'downstream') {
warn("Traffic accept rules are only supported on downstream interfaces.");
return;
}
// Gather related BSS modes and ethernet ports.
let bss_modes = map(interface.ssids, ssid => ssid.bss_mode);
let eth_ports = ethernet.lookup_by_interface_vlan(interface);

View File

@@ -140,3 +140,32 @@ set firewall.@rule[-1].family='ipv6'
set firewall.@rule[-1].proto='udp'
set firewall.@rule[-1].target='ACCEPT'
{% endif %}
{%
for (let forward in interface.ipv4?.port_forward)
include('firewall/forward.uc', {
forward,
family: 'ipv4',
source_zone: ethernet.find_interface('upstream', interface.vlan?.id),
destination_zone: name,
destination_subnet: interface.ipv4.subnet
});
for (let forward in interface.ipv6?.port_forward)
include('firewall/forward.uc', {
forward,
family: 'ipv6',
source_zone: ethernet.find_interface('upstream', interface.vlan?.id),
destination_zone: name,
destination_subnet: interface.ipv6.subnet
});
for (let allow in interface.ipv6?.traffic_allow)
include('firewall/allow.uc', {
allow,
family: 'ipv6',
source_zone: ethernet.find_interface('upstream', interface.vlan?.id),
destination_zone: name,
destination_subnet: interface.ipv6.subnet
});
%}

View File

@@ -0,0 +1,20 @@
add firewall rule
set firewall.@rule[-1].name='Allow traffic to {{ allow.destination_address }}'
set firewall.@rule[-1].family={{ s(family) }}
set firewall.@rule[-1].src={{ s(source_zone || '*') }}
set firewall.@rule[-1].dest={{ s(destination_zone) }}
{% for (let proto in ((allow.protocol in ['any', 'all', '*'] && (allow.source_ports || allow.destination_ports)) ? ['tcp', 'udp'] : [ allow.protocol ])): %}
add_list firewall.@rule[-1].proto={{ s(proto) }}
{% endfor %}
{% if (allow.source_address): %}
set firewall.@rule[-1].src_ip={{ s(allow.source_address) }}
{% endif %}
{% for (let sport in allow.source_ports): %}
add_list firewall.@rule[-1].src_port={{ s(sport) }}
{% endfor %}
set firewall.@rule[-1].dest_ip={{ ipcalc.expand_wildcard_address(allow.destination_address, destination_subnet) }}
{% for (let dport in allow.destination_ports): %}
add_list firewall.@rule[-1].dest_port={{ s(dport) }}
{% endfor %}
set firewall.@rule[-1].target=ACCEPT

View File

@@ -0,0 +1,15 @@
{% if (true || source_zone): %}
add firewall redirect
set firewall.@redirect[-1].name='Forward port {{ forward.external_port }} to {{ forward.internal_address }}'
set firewall.@redirect[-1].family={{ s(family) }}
set firewall.@redirect[-1].src={{ s(source_zone || '*') }}
set firewall.@redirect[-1].dest={{ s(destination_zone) }}
{% for (let proto in ((forward.protocol in ['any', 'all', '*']) ? ['tcp', 'udp'] : [ forward.protocol ])): %}
add_list firewall.@redirect[-1].proto={{ s(proto) }}
{% endfor %}
set firewall.@redirect[-1].src_dport={{ s(forward.external_port) }}
set firewall.@redirect[-1].dest_ip={{ ipcalc.expand_wildcard_address(forward.internal_address, destination_subnet) }}
set firewall.@redirect[-1].dest_port={{ s(forward.internal_port) }}
set firewall.@redirect[-1].target=DNAT
{% endif %}

View File

@@ -0,0 +1,41 @@
description:
This section describes an IPv4 port forwarding.
type: object
properties:
protocol:
description:
The layer 3 protocol to match.
type: string
enum:
- tcp
- udp
- any
default: any
external-port:
description:
The external port(s) to forward.
type:
- integer
- string
minimum: 0
maximum: 65535
format: uc-portrange
internal-address:
description:
The internal IP to forward to. The address will be masked and concatenated
with the effective interface subnet.
type: string
format: ipv4
example: '0.0.0.120'
internal-port:
description:
The internal port to forward to. Defaults to the external port if omitted.
type:
- integer
- string
minimum: 0
maximum: 65535
format: uc-portrange
required:
- external-port
- internal-address

View File

@@ -52,3 +52,7 @@ properties:
type: array
items:
$ref: "https://ucentral.io/schema/v1/interface/ipv4/dhcp-lease/"
port-forward:
type: array
items:
$ref: "https://ucentral.io/schema/v1/interface/ipv4/port-forward/"

View File

@@ -0,0 +1,41 @@
description:
This section describes an IPv6 port forwarding.
type: object
properties:
protocol:
description:
The layer 3 protocol to match.
type: string
enum:
- tcp
- udp
- any
default: any
external-port:
description:
The external port(s) to forward.
type:
- integer
- string
minimum: 0
maximum: 65535
format: uc-portrange
internal-address:
description:
The internal IP to forward to. The address will be masked and concatenated
with the effective interface subnet.
type: string
format: ipv6
example: '::1234:abcd'
internal-port:
description:
The internal port to forward to. Defaults to the external port if omitted.
type:
- integer
- string
minimum: 0
maximum: 65535
format: uc-portrange
required:
- external-port
- internal-address

View File

@@ -0,0 +1,49 @@
description:
This section describes an IPv6 traffic accept rule.
type: object
properties:
protocol:
description:
The layer 3 protocol to match.
type: string
default: any
source-address:
description:
The source IP to allow traffic from.
type: string
format: uc-cidr6
example: 2001:db8:1234:abcd::/64
default: ::/0
source-ports:
description:
The source port(s) to accept.
type: array
minItems: 1
items:
type:
- integer
- string
minimum: 0
maximum: 65535
format: uc-portrange
destination-address:
description:
The destination IP to allow traffic to. The address will be masked and
concatenated with the effective interface subnet.
type: string
format: ipv6
example: ::1000
destination-ports:
description:
The destination ports to accept.
type: array
minItems: 1
items:
type:
- integer
- string
minimum: 0
maximum: 65535
format: uc-portrange
required:
- destination-address

View File

@@ -50,3 +50,11 @@ properties:
minimum: 0
dhcpv6:
$ref: "https://ucentral.io/schema/v1/interface/ipv6/dhcpv6/"
port-forward:
type: array
items:
$ref: "https://ucentral.io/schema/v1/interface/ipv6/port-forward/"
traffic-allow:
type: array
items:
$ref: "https://ucentral.io/schema/v1/interface/ipv6/traffic-allow/"

View File

@@ -37,6 +37,13 @@ function matchUcBase64(value) {
return b64dec(value) != null;
}
function matchUcPortrange(value) {
let ports = match(value, /^([0-9]|[1-9][0-9]*)(-([0-9]|[1-9][0-9]*))?$/);
if (!ports) return false;
let min = +ports[1], max = ports[2] ? +ports[3] : min;
return (min <= 65535 && max <= 65535 && max >= min);
}
function matchHostname(value) {
if (length(value) > 255) return false;
let labels = split(value, ".");
@@ -1457,6 +1464,111 @@ function instantiateInterfaceIpv4DhcpLease(location, value, errors) {
return value;
}
function instantiateInterfaceIpv4PortForward(location, value, errors) {
if (type(value) == "object") {
let obj = {};
function parseProtocol(location, value, errors) {
if (type(value) != "string")
push(errors, [ location, "must be of type string" ]);
if (!(value in [ "tcp", "udp", "any" ]))
push(errors, [ location, "must be one of \"tcp\", \"udp\" or \"any\"" ]);
return value;
}
if (exists(value, "protocol")) {
obj.protocol = parseProtocol(location + "/protocol", value["protocol"], errors);
}
else {
obj.protocol = "any";
}
function parseExternalPort(location, value, errors) {
if (type(value) in [ "int", "double" ]) {
if (value > 65535)
push(errors, [ location, "must be lower than or equal to 65535" ]);
if (value < 0)
push(errors, [ location, "must be bigger than or equal to 0" ]);
}
if (type(value) == "string") {
if (!matchUcPortrange(value))
push(errors, [ location, "must be a valid network port range" ]);
}
if (type(value) != "int" && type(value) != "string")
push(errors, [ location, "must be of type integer or string" ]);
return value;
}
if (exists(value, "external-port")) {
obj.external_port = parseExternalPort(location + "/external-port", value["external-port"], errors);
}
else {
push(errors, [ location, "is required" ]);
}
function parseInternalAddress(location, value, errors) {
if (type(value) == "string") {
if (!matchIpv4(value))
push(errors, [ location, "must be a valid IPv4 address" ]);
}
if (type(value) != "string")
push(errors, [ location, "must be of type string" ]);
return value;
}
if (exists(value, "internal-address")) {
obj.internal_address = parseInternalAddress(location + "/internal-address", value["internal-address"], errors);
}
else {
push(errors, [ location, "is required" ]);
}
function parseInternalPort(location, value, errors) {
if (type(value) in [ "int", "double" ]) {
if (value > 65535)
push(errors, [ location, "must be lower than or equal to 65535" ]);
if (value < 0)
push(errors, [ location, "must be bigger than or equal to 0" ]);
}
if (type(value) == "string") {
if (!matchUcPortrange(value))
push(errors, [ location, "must be a valid network port range" ]);
}
if (type(value) != "int" && type(value) != "string")
push(errors, [ location, "must be of type integer or string" ]);
return value;
}
if (exists(value, "internal-port")) {
obj.internal_port = parseInternalPort(location + "/internal-port", value["internal-port"], errors);
}
return obj;
}
if (type(value) != "object")
push(errors, [ location, "must be of type object" ]);
return value;
}
function instantiateInterfaceIpv4(location, value, errors) {
if (type(value) == "object") {
let obj = {};
@@ -1570,6 +1682,21 @@ function instantiateInterfaceIpv4(location, value, errors) {
obj.dhcp_leases = parseDhcpLeases(location + "/dhcp-leases", value["dhcp-leases"], errors);
}
function parsePortForward(location, value, errors) {
if (type(value) == "array") {
return map(value, (item, i) => instantiateInterfaceIpv4PortForward(location + "/" + i, item, errors));
}
if (type(value) != "array")
push(errors, [ location, "must be of type array" ]);
return value;
}
if (exists(value, "port-forward")) {
obj.port_forward = parsePortForward(location + "/port-forward", value["port-forward"], errors);
}
return obj;
}
@@ -1654,6 +1781,258 @@ function instantiateInterfaceIpv6Dhcpv6(location, value, errors) {
return value;
}
function instantiateInterfaceIpv6PortForward(location, value, errors) {
if (type(value) == "object") {
let obj = {};
function parseProtocol(location, value, errors) {
if (type(value) != "string")
push(errors, [ location, "must be of type string" ]);
if (!(value in [ "tcp", "udp", "any" ]))
push(errors, [ location, "must be one of \"tcp\", \"udp\" or \"any\"" ]);
return value;
}
if (exists(value, "protocol")) {
obj.protocol = parseProtocol(location + "/protocol", value["protocol"], errors);
}
else {
obj.protocol = "any";
}
function parseExternalPort(location, value, errors) {
if (type(value) in [ "int", "double" ]) {
if (value > 65535)
push(errors, [ location, "must be lower than or equal to 65535" ]);
if (value < 0)
push(errors, [ location, "must be bigger than or equal to 0" ]);
}
if (type(value) == "string") {
if (!matchUcPortrange(value))
push(errors, [ location, "must be a valid network port range" ]);
}
if (type(value) != "int" && type(value) != "string")
push(errors, [ location, "must be of type integer or string" ]);
return value;
}
if (exists(value, "external-port")) {
obj.external_port = parseExternalPort(location + "/external-port", value["external-port"], errors);
}
else {
push(errors, [ location, "is required" ]);
}
function parseInternalAddress(location, value, errors) {
if (type(value) == "string") {
if (!matchIpv6(value))
push(errors, [ location, "must be a valid IPv6 address" ]);
}
if (type(value) != "string")
push(errors, [ location, "must be of type string" ]);
return value;
}
if (exists(value, "internal-address")) {
obj.internal_address = parseInternalAddress(location + "/internal-address", value["internal-address"], errors);
}
else {
push(errors, [ location, "is required" ]);
}
function parseInternalPort(location, value, errors) {
if (type(value) in [ "int", "double" ]) {
if (value > 65535)
push(errors, [ location, "must be lower than or equal to 65535" ]);
if (value < 0)
push(errors, [ location, "must be bigger than or equal to 0" ]);
}
if (type(value) == "string") {
if (!matchUcPortrange(value))
push(errors, [ location, "must be a valid network port range" ]);
}
if (type(value) != "int" && type(value) != "string")
push(errors, [ location, "must be of type integer or string" ]);
return value;
}
if (exists(value, "internal-port")) {
obj.internal_port = parseInternalPort(location + "/internal-port", value["internal-port"], errors);
}
return obj;
}
if (type(value) != "object")
push(errors, [ location, "must be of type object" ]);
return value;
}
function instantiateInterfaceIpv6TrafficAllow(location, value, errors) {
if (type(value) == "object") {
let obj = {};
function parseProtocol(location, value, errors) {
if (type(value) != "string")
push(errors, [ location, "must be of type string" ]);
return value;
}
if (exists(value, "protocol")) {
obj.protocol = parseProtocol(location + "/protocol", value["protocol"], errors);
}
else {
obj.protocol = "any";
}
function parseSourceAddress(location, value, errors) {
if (type(value) == "string") {
if (!matchUcCidr6(value))
push(errors, [ location, "must be a valid IPv6 CIDR" ]);
}
if (type(value) != "string")
push(errors, [ location, "must be of type string" ]);
return value;
}
if (exists(value, "source-address")) {
obj.source_address = parseSourceAddress(location + "/source-address", value["source-address"], errors);
}
else {
obj.source_address = "::/0";
}
function parseSourcePorts(location, value, errors) {
if (type(value) == "array") {
if (length(value) < 1)
push(errors, [ location, "must have at least 1 items" ]);
function parseItem(location, value, errors) {
if (type(value) in [ "int", "double" ]) {
if (value > 65535)
push(errors, [ location, "must be lower than or equal to 65535" ]);
if (value < 0)
push(errors, [ location, "must be bigger than or equal to 0" ]);
}
if (type(value) == "string") {
if (!matchUcPortrange(value))
push(errors, [ location, "must be a valid network port range" ]);
}
if (type(value) != "int" && type(value) != "string")
push(errors, [ location, "must be of type integer or string" ]);
return value;
}
return map(value, (item, i) => parseItem(location + "/" + i, item, errors));
}
if (type(value) != "array")
push(errors, [ location, "must be of type array" ]);
return value;
}
if (exists(value, "source-ports")) {
obj.source_ports = parseSourcePorts(location + "/source-ports", value["source-ports"], errors);
}
function parseDestinationAddress(location, value, errors) {
if (type(value) == "string") {
if (!matchIpv6(value))
push(errors, [ location, "must be a valid IPv6 address" ]);
}
if (type(value) != "string")
push(errors, [ location, "must be of type string" ]);
return value;
}
if (exists(value, "destination-address")) {
obj.destination_address = parseDestinationAddress(location + "/destination-address", value["destination-address"], errors);
}
else {
push(errors, [ location, "is required" ]);
}
function parseDestinationPorts(location, value, errors) {
if (type(value) == "array") {
if (length(value) < 1)
push(errors, [ location, "must have at least 1 items" ]);
function parseItem(location, value, errors) {
if (type(value) in [ "int", "double" ]) {
if (value > 65535)
push(errors, [ location, "must be lower than or equal to 65535" ]);
if (value < 0)
push(errors, [ location, "must be bigger than or equal to 0" ]);
}
if (type(value) == "string") {
if (!matchUcPortrange(value))
push(errors, [ location, "must be a valid network port range" ]);
}
if (type(value) != "int" && type(value) != "string")
push(errors, [ location, "must be of type integer or string" ]);
return value;
}
return map(value, (item, i) => parseItem(location + "/" + i, item, errors));
}
if (type(value) != "array")
push(errors, [ location, "must be of type array" ]);
return value;
}
if (exists(value, "destination-ports")) {
obj.destination_ports = parseDestinationPorts(location + "/destination-ports", value["destination-ports"], errors);
}
return obj;
}
if (type(value) != "object")
push(errors, [ location, "must be of type object" ]);
return value;
}
function instantiateInterfaceIpv6(location, value, errors) {
if (type(value) == "object") {
let obj = {};
@@ -1730,6 +2109,36 @@ function instantiateInterfaceIpv6(location, value, errors) {
obj.dhcpv6 = instantiateInterfaceIpv6Dhcpv6(location + "/dhcpv6", value["dhcpv6"], errors);
}
function parsePortForward(location, value, errors) {
if (type(value) == "array") {
return map(value, (item, i) => instantiateInterfaceIpv6PortForward(location + "/" + i, item, errors));
}
if (type(value) != "array")
push(errors, [ location, "must be of type array" ]);
return value;
}
if (exists(value, "port-forward")) {
obj.port_forward = parsePortForward(location + "/port-forward", value["port-forward"], errors);
}
function parseTrafficAllow(location, value, errors) {
if (type(value) == "array") {
return map(value, (item, i) => instantiateInterfaceIpv6TrafficAllow(location + "/" + i, item, errors));
}
if (type(value) != "array")
push(errors, [ location, "must be of type array" ]);
return value;
}
if (exists(value, "traffic-allow")) {
obj.traffic_allow = parseTrafficAllow(location + "/traffic-allow", value["traffic-allow"], errors);
}
return obj;
}

View File

@@ -652,6 +652,47 @@
}
}
},
"interface.ipv4.port-forward": {
"type": "object",
"properties": {
"protocol": {
"type": "string",
"enum": [
"tcp",
"udp",
"any"
],
"default": "any"
},
"external-port": {
"type": [
"integer",
"string"
],
"minimum": 0,
"maximum": 65535,
"format": "uc-portrange"
},
"internal-address": {
"type": "string",
"format": "ipv4",
"example": "0.0.0.120"
},
"internal-port": {
"type": [
"integer",
"string"
],
"minimum": 0,
"maximum": 65535,
"format": "uc-portrange"
}
},
"required": [
"external-port",
"internal-address"
]
},
"interface.ipv4": {
"type": "object",
"properties": {
@@ -705,6 +746,12 @@
"items": {
"$ref": "#/$defs/interface.ipv4.dhcp-lease"
}
},
"port-forward": {
"type": "array",
"items": {
"$ref": "#/$defs/interface.ipv4.port-forward"
}
}
}
},
@@ -734,6 +781,96 @@
}
}
},
"interface.ipv6.port-forward": {
"type": "object",
"properties": {
"protocol": {
"type": "string",
"enum": [
"tcp",
"udp",
"any"
],
"default": "any"
},
"external-port": {
"type": [
"integer",
"string"
],
"minimum": 0,
"maximum": 65535,
"format": "uc-portrange"
},
"internal-address": {
"type": "string",
"format": "ipv6",
"example": "::1234:abcd"
},
"internal-port": {
"type": [
"integer",
"string"
],
"minimum": 0,
"maximum": 65535,
"format": "uc-portrange"
}
},
"required": [
"external-port",
"internal-address"
]
},
"interface.ipv6.traffic-allow": {
"type": "object",
"properties": {
"protocol": {
"type": "string",
"default": "any"
},
"source-address": {
"type": "string",
"format": "uc-cidr6",
"example": "2001:db8:1234:abcd::/64",
"default": "::/0"
},
"source-ports": {
"type": "array",
"minItems": 1,
"items": {
"type": [
"integer",
"string"
],
"minimum": 0,
"maximum": 65535,
"format": "uc-portrange"
}
},
"destination-address": {
"type": "string",
"format": "ipv6",
"example": "::1000"
},
"destination-ports": {
"type": "array",
"minItems": 1,
"items": {
"type": [
"integer",
"string"
],
"minimum": 0,
"maximum": 65535,
"format": "uc-portrange"
}
}
},
"required": [
"destination-address"
]
},
"interface.ipv6": {
"type": "object",
"properties": {
@@ -765,6 +902,18 @@
},
"dhcpv6": {
"$ref": "#/$defs/interface.ipv6.dhcpv6"
},
"port-forward": {
"type": "array",
"items": {
"$ref": "#/$defs/interface.ipv6.port-forward"
}
},
"traffic-allow": {
"type": "array",
"items": {
"$ref": "#/$defs/interface.ipv6.traffic-allow"
}
}
}
},