From 2bf1645d3a64e067b652c6d824b2b317dc86c15b Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Wed, 6 Apr 2022 23:40:56 +0200 Subject: [PATCH] schema: introduce `port-forward` and `traffic-allow` firewall settings Signed-off-by: Jo-Philipp Wich --- renderer/templates/interface.uc | 12 + renderer/templates/interface/firewall.uc | 29 ++ .../templates/interface/firewall/allow.uc | 20 + .../templates/interface/firewall/forward.uc | 15 + schema/interface.ipv4.port-forward.yml | 41 ++ schema/interface.ipv4.yml | 4 + schema/interface.ipv6.port-forward.yml | 41 ++ schema/interface.ipv6.traffic-allow.yml | 49 +++ schema/interface.ipv6.yml | 8 + schemareader.uc | 409 ++++++++++++++++++ ucentral.schema.json | 149 +++++++ 11 files changed, 777 insertions(+) create mode 100644 renderer/templates/interface/firewall/allow.uc create mode 100644 renderer/templates/interface/firewall/forward.uc create mode 100644 schema/interface.ipv4.port-forward.yml create mode 100644 schema/interface.ipv6.port-forward.yml create mode 100644 schema/interface.ipv6.traffic-allow.yml diff --git a/renderer/templates/interface.uc b/renderer/templates/interface.uc index fe5ed32..24c215e 100644 --- a/renderer/templates/interface.uc +++ b/renderer/templates/interface.uc @@ -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); diff --git a/renderer/templates/interface/firewall.uc b/renderer/templates/interface/firewall.uc index b215762..8ec0c5f 100644 --- a/renderer/templates/interface/firewall.uc +++ b/renderer/templates/interface/firewall.uc @@ -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 + }); +%} diff --git a/renderer/templates/interface/firewall/allow.uc b/renderer/templates/interface/firewall/allow.uc new file mode 100644 index 0000000..44ec208 --- /dev/null +++ b/renderer/templates/interface/firewall/allow.uc @@ -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 + diff --git a/renderer/templates/interface/firewall/forward.uc b/renderer/templates/interface/firewall/forward.uc new file mode 100644 index 0000000..d9f8f80 --- /dev/null +++ b/renderer/templates/interface/firewall/forward.uc @@ -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 %} diff --git a/schema/interface.ipv4.port-forward.yml b/schema/interface.ipv4.port-forward.yml new file mode 100644 index 0000000..3761d22 --- /dev/null +++ b/schema/interface.ipv4.port-forward.yml @@ -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 diff --git a/schema/interface.ipv4.yml b/schema/interface.ipv4.yml index 882828e..c705c6c 100644 --- a/schema/interface.ipv4.yml +++ b/schema/interface.ipv4.yml @@ -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/" diff --git a/schema/interface.ipv6.port-forward.yml b/schema/interface.ipv6.port-forward.yml new file mode 100644 index 0000000..fe1615a --- /dev/null +++ b/schema/interface.ipv6.port-forward.yml @@ -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 diff --git a/schema/interface.ipv6.traffic-allow.yml b/schema/interface.ipv6.traffic-allow.yml new file mode 100644 index 0000000..e2b3e02 --- /dev/null +++ b/schema/interface.ipv6.traffic-allow.yml @@ -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 diff --git a/schema/interface.ipv6.yml b/schema/interface.ipv6.yml index 1d0a0ba..320bcf2 100644 --- a/schema/interface.ipv6.yml +++ b/schema/interface.ipv6.yml @@ -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/" diff --git a/schemareader.uc b/schemareader.uc index 1bdf0f0..52e1431 100644 --- a/schemareader.uc +++ b/schemareader.uc @@ -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; } diff --git a/ucentral.schema.json b/ucentral.schema.json index 5b3634b..8af9dd8 100644 --- a/ucentral.schema.json +++ b/ucentral.schema.json @@ -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" + } } } },