From 6da2df4ba0d2c9f71c38c1fc83a2ef7339c4d6af Mon Sep 17 00:00:00 2001 From: Wen Liang Date: Thu, 17 Mar 2022 17:14:00 -0400 Subject: [PATCH] Add support for routing rules In order to enable the user using the policy routing (e.g. source routing) or instructing the system which table to look up to determine the correct route, add support for routing rules. Signed-off-by: Wen Liang --- README.md | 51 ++++ library/network_connections.py | 70 +++++ .../network_lsr/argument_validator.py | 222 ++++++++++++++ tests/ensure_provider_tests.py | 1 + tests/playbooks/tests_routing_rules.yml | 223 ++++++++++++++ tests/tests_routing_rules_nm.yml | 20 ++ tests/unit/test_network_connections.py | 282 ++++++++++++++++++ 7 files changed, 869 insertions(+) create mode 100644 tests/playbooks/tests_routing_rules.yml create mode 100644 tests/tests_routing_rules_nm.yml diff --git a/README.md b/README.md index ddfcd22..db6f727 100644 --- a/README.md +++ b/README.md @@ -501,6 +501,57 @@ The IP configuration supports the following options: Note that Classless inter-domain routing (CIDR) notation or network mask notation are not supported yet. +- `routing_rule` + + The policy routing rules can be specified via a list of rules given in the + `routing_rule` option, which allow routing the packets on other packet fields + except for destination address. The default value is a an empty list. Each rule is + a dictionary with the following entries: + - `priority` - + The priority of the rule. A valid priority ranges from 0 to 4294967295. Higher + number means lower priority. + - `action` - + The action of the rule. The possible values are `to-table` (default), + `blackhole`, `prohibit`, `unreachable`. + - `dport`- + The range of the destination port (e.g. `1000 - 2000`). A valid dport value for + both start and end ranges from 0 to 65534. And the start cannot be greater than + the end. + - `family` - + The IP family of the rule. The possible values are `ipv4` and `ipv6`. + - `from` - + The source address of the packet to match (e.g. `192.168.100.58/24`). + - `fwmark` - + The fwmark value of the packet to match. + - `fwmask` - + The fwmask value of the packet to match. + - `iif` - + Select the incoming interface name to match. + - `invert` - + Invert the selected match of the rule. The possible values are boolean values + `True` and `False` (default). If the value is `True`, this is equivalent to match + any packet that not satisfying selected match of the rule. + - `ipproto` - + Select the IP protocol value to match, the valid value ranges from 1 to 255. + - `oif` - + Select the outgoing interface name to match. + - `sport` - + The range of the source port (e.g. `1000 - 2000`). A valid sport value for both + start and end ranges from 0 to 65534. And the start cannot be greater than the + end. + - `suppress_prefixlength` - + Reject routing decisions that have a prefix length of the specified or less. + - `table` - + The route table to look up for the `to-table` action. + - `to` - + The destination address of the packet to match (e.g. `192.168.100.58/24`). + - `tos` - + Select the tos value to match. + - `uid` - + The range of the uid to match (e.g. `1000 - 2000`). A valid uid value for both + start and end ranges from 0 to 4294967295. And the start cannot be greater than + the end. + - `route_append_only` The `route_append_only` option allows only to add new routes to the diff --git a/library/network_connections.py b/library/network_connections.py index 5e51acf..cef8e57 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -1148,6 +1148,76 @@ class NMUtil: s_ip4.add_route(rr) else: s_ip6.add_route(rr) + for routing_rule in ip["routing_rule"]: + nm_routing_rule = NM.IPRoutingRule.new(routing_rule["family"]) + NM.IPRoutingRule.set_priority(nm_routing_rule, routing_rule["priority"]) + + # check the link below for the enum value of supported action + # https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/fib_rules.h?id=f443e374ae131c168a065ea1748feac6b2e76613#n88 + action_ids = { + "to-table": 1, + "blackhole": 6, + "prohibit": 7, + "unreachable": 8, + } + NM.IPRoutingRule.set_action( + nm_routing_rule, action_ids[routing_rule["action"]] + ) + if routing_rule["dport"]: + NM.IPRoutingRule.set_destination_port( + nm_routing_rule, + routing_rule["dport"][0], + routing_rule["dport"][1], + ) + if routing_rule["from"]: + NM.IPRoutingRule.set_from( + nm_routing_rule, + routing_rule["from"]["address"], + routing_rule["from"]["prefix"], + ) + if routing_rule["fwmark"]: + NM.IPRoutingRule.set_fwmark( + nm_routing_rule, routing_rule["fwmark"], routing_rule["fwmask"] + ) + if routing_rule["iif"]: + NM.IPRoutingRule.set_iifname(nm_routing_rule, routing_rule["iif"]) + NM.IPRoutingRule.set_invert(nm_routing_rule, routing_rule["invert"]) + if routing_rule["ipproto"]: + NM.IPRoutingRule.set_ipproto( + nm_routing_rule, routing_rule["ipproto"] + ) + if routing_rule["oif"]: + NM.IPRoutingRule.set_oifname(nm_routing_rule, routing_rule["oif"]) + if routing_rule["sport"]: + NM.IPRoutingRule.set_source_port( + nm_routing_rule, + routing_rule["sport"][0], + routing_rule["sport"][1], + ) + if routing_rule["suppress_prefixlength"] is not None: + NM.IPRoutingRule.set_suppress_prefixlength( + nm_routing_rule, routing_rule["suppress_prefixlength"] + ) + if routing_rule["table"]: + NM.IPRoutingRule.set_table(nm_routing_rule, routing_rule["table"]) + if routing_rule["to"]: + NM.IPRoutingRule.set_to( + nm_routing_rule, + routing_rule["to"]["address"], + routing_rule["to"]["prefix"], + ) + if routing_rule["tos"]: + NM.IPRoutingRule.set_tos(nm_routing_rule, routing_rule["tos"]) + if routing_rule["uid"]: + NM.IPRoutingRule.set_uid_range( + nm_routing_rule, + routing_rule["uid"][0], + routing_rule["uid"][1], + ) + if routing_rule["family"] == socket.AF_INET: + s_ip4.add_routing_rule(nm_routing_rule) + else: + s_ip6.add_routing_rule(nm_routing_rule) if connection["ieee802_1x"]: s_8021x = self.connection_ensure_setting(con, NM.Setting8021x) diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index 46c9158..8157902 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -353,6 +353,67 @@ class ArgValidatorNum(ArgValidator): return v +class ArgValidatorRange(ArgValidator): + def __init__( # pylint: disable=too-many-arguments + self, + name, + required=False, + val_min=None, + val_max=None, + default_value=None, + ): + ArgValidator.__init__(self, name, required, default_value) + self.val_min = val_min + self.val_max = val_max + + def _validate_impl(self, value, name): + range = None + if isinstance(value, Util.STRING_TYPE): + match_group = re.match(r"^ *([0-9]+) *- *([0-9]+) *$", value) + if match_group: + try: + range = (int(match_group.group(1)), int(match_group.group(2))) + except Exception: + pass + else: + try: + range = (int(value), int(value)) + except Exception: + pass + elif isinstance(value, bool): + # bool can (probably) be converted to integer type, + # but here we don't want to accept a boolean value. + pass + elif isinstance(value, int): + range = (value, value) + + if range is None: + raise ValidationError(name, "the range value {0} is invalid".format(value)) + if range[0] > range[1]: + raise ValidationError( + name, + "the range start cannot be greater than range end", + ) + if self.val_min is not None: + if range[0] < self.val_min: + raise ValidationError( + name, + "lower range value is {0} but cannot be less than {1}".format( + range[0], self.val_min + ), + ) + if self.val_max is not None: + if range[1] > self.val_max: + raise ValidationError( + name, + "upper range value is {0} but cannot be greater than {1}".format( + range[1], self.val_max + ), + ) + + return range + + class ArgValidatorBool(ArgValidator): def __init__(self, name, required=False, default_value=False): ArgValidator.__init__(self, name, required, default_value) @@ -633,6 +694,133 @@ class ArgValidatorIPRoute(ArgValidatorDict): return result +class ArgValidatorIPRoutingRule(ArgValidatorDict): + def __init__(self, name, required=False): + ArgValidatorDict.__init__( + self, + name, + required, + nested=[ + ArgValidatorNum( + "priority", + default_value=None, + required=True, + val_min=0, + val_max=0xFFFFFFFF, + ), + ArgValidatorStr( + "action", + default_value="to-table", + enum_values=["to-table", "blackhole", "prohibit", "unreachable"], + ), + ArgValidatorRange("dport", val_min=1, val_max=65534), + ArgValidatorStr( + "family", + default_value=None, + enum_values=["ipv4", "ipv6"], + ), + ArgValidatorIPAddr("from"), + ArgValidatorNum( + "fwmark", default_value=None, val_min=1, val_max=0xFFFFFFFF + ), + ArgValidatorNum( + "fwmask", default_value=None, val_min=1, val_max=0xFFFFFFFF + ), + ArgValidatorStr("iif", default_value=None), + ArgValidatorBool("invert", default_value=False), + ArgValidatorNum("ipproto", default_value=None, val_min=1, val_max=255), + ArgValidatorStr("oif", default_value=None), + ArgValidatorRange("sport", val_min=1, val_max=65534), + ArgValidatorNum("suppress_prefixlength", default_value=None, val_min=0), + ArgValidatorRouteTable("table"), + ArgValidatorIPAddr("to"), + ArgValidatorNum("tos", default_value=None, val_min=1, val_max=255), + ArgValidatorRange("uid", val_min=0, val_max=0xFFFFFFFF), + ], + default_value=None, + ) + + def _validate_post(self, value, name, result): + family = None + if result["family"]: + family = Util.addr_family_norm(result["family"]) + elif result["from"]: + family = result["from"]["family"] + elif result["to"]: + family = result["to"]["family"] + if not family: + raise ValidationError(name, "specify the address family 'family'") + + if result["from"]: + if result["from"]["family"] != family: + raise ValidationError(name, "invalid address family in 'from'") + + if result["to"]: + if result["to"]["family"] != family: + raise ValidationError(name, "invalid address family in 'to'") + + result["family"] = family + if result["action"] == "to-table": + if result["table"] is None: + raise ValidationError( + name, + "missing 'table' for the routing rule", + ) + + if result["from"] is not None: + if result["from"]["prefix"] == 0: + raise ValidationError( + name, + "the prefix length for 'from' cannot be zero", + ) + + if result["to"] is not None: + if result["to"]["prefix"] == 0: + raise ValidationError( + name, + "the prefix length for 'to' cannot be zero", + ) + + if (result["fwmask"] is None) != (result["fwmark"] is None): + raise ValidationError( + name, + "'fwmask' and 'fwmark' must be set together", + ) + + if result["iif"] is not None: + if not Util.ifname_valid(result["iif"]): + raise ValidationError( + name, + "the incoming interface '{0}' specified in the routing rule is " + "invalid interface_name".format(result["iif"]), + ) + + if result["oif"] is not None: + if not Util.ifname_valid(result["oif"]): + raise ValidationError( + name, + "the outgoing interface '{0}' specified in the routing rule is " + "invalid interface_name".format(result["oif"]), + ) + + if result["suppress_prefixlength"] is not None: + if not Util.addr_family_valid_prefix( + result["family"], result["suppress_prefixlength"] + ): + raise ValidationError( + name, + "The specified 'suppress_prefixlength' cannot be greater than " + "{0}".format(Util.addr_family_prefix_length(result["family"])), + ) + + if result["action"] != "to-table": + raise ValidationError( + name, + "'suppress_prefixlength' is only allowed with the to-table action", + ) + return result + + class ArgValidator_DictIP(ArgValidatorDict): REGEX_DNS_OPTIONS = [ r"^attempts:([1-9]\d*|0)$", @@ -699,6 +887,11 @@ class ArgValidator_DictIP(ArgValidatorDict): ), default_value=list, ), + ArgValidatorList( + "routing_rule", + nested=ArgValidatorIPRoutingRule("routing_rule[?]"), + default_value=list, + ), ], default_value=lambda: { "dhcp4": True, @@ -712,6 +905,7 @@ class ArgValidator_DictIP(ArgValidatorDict): "address": [], "auto_gateway": None, "route": [], + "routing_rule": [], "route_append_only": False, "rule_append_only": False, "dns": [], @@ -2311,6 +2505,34 @@ class ArgValidator_ListConnections(ArgValidatorList): "the bond option peer_notif_delay is not supported in " "NetworkManger until NM 1.30", ) + + if connection["ip"]["routing_rule"]: + if mode == self.VALIDATE_ONE_MODE_INITSCRIPTS: + raise ValidationError.from_connection( + idx, + "ip.routing_rule is not supported by initscripts", + ) + for routing_rule in connection["ip"]["routing_rule"]: + if routing_rule["suppress_prefixlength"] is not None: + if not hasattr( + Util.NM(), "NM_IP_ROUTING_RULE_ATTR_SUPPRESS_PREFIXLENGTH" + ): + raise ValidationError.from_connection( + idx, + "the routing rule selector 'suppress_prefixlength' is not " + "supported in NetworkManger until NM 1.20", + ) + for routing_rule in connection["ip"]["routing_rule"]: + if routing_rule["uid"] is not None: + if not hasattr( + Util.NM(), "NM_IP_ROUTING_RULE_ATTR_UID_RANGE_START" + ): + raise ValidationError.from_connection( + idx, + "the routing rule selector 'uid' is not supported in " + "NetworkManger until NM 1.34", + ) + self.validate_route_tables(connection, idx) diff --git a/tests/ensure_provider_tests.py b/tests/ensure_provider_tests.py index 4d5dff6..38adae2 100755 --- a/tests/ensure_provider_tests.py +++ b/tests/ensure_provider_tests.py @@ -84,6 +84,7 @@ ibution_major_version | int < 9", }, "playbooks/tests_reapply.yml": {}, "playbooks/tests_route_table.yml": {}, + "playbooks/tests_routing_rules.yml": {}, # team interface is not supported on Fedora "playbooks/tests_team.yml": { EXTRA_RUN_CONDITION: "ansible_distribution != 'Fedora'", diff --git a/tests/playbooks/tests_routing_rules.yml b/tests/playbooks/tests_routing_rules.yml new file mode 100644 index 0000000..a659a6a --- /dev/null +++ b/tests/playbooks/tests_routing_rules.yml @@ -0,0 +1,223 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + +- name: Test configuring ethernet devices + hosts: all + vars: + type: veth + interface: ethtest0 + + tasks: + - name: "set type={{ type }} and interface={{ interface }}" + set_fact: + type: "{{ type }}" + interface: "{{ interface }}" + - include_tasks: tasks/show_interfaces.yml + - include_tasks: tasks/manage_test_interface.yml + vars: + state: present + - include_tasks: tasks/assert_device_present.yml + + - name: Configure connection profile and specify the numeric table in + static routes + import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: "{{ interface }}" + interface_name: "{{ interface }}" + state: up + type: ethernet + autoconnect: yes + ip: + dhcp4: no + address: + - 198.51.100.3/26 + - 2001:db8::2/32 + route: + - network: 198.51.100.64 + prefix: 26 + gateway: 198.51.100.6 + metric: 4 + table: 30200 + - network: 198.51.100.128 + prefix: 26 + gateway: 198.51.100.1 + metric: 2 + table: 30400 + - network: 2001:db8::4 + prefix: 32 + gateway: 2001:db8::1 + metric: 2 + table: 30600 + routing_rule: + - priority: 30200 + from: 198.51.100.58/26 + table: 30200 + - priority: 30201 + family: ipv4 + fwmark: 1 + fwmask: 1 + table: 30200 + - priority: 30202 + family: ipv4 + ipproto: 6 + table: 30200 + - priority: 30203 + family: ipv4 + sport: 128 - 256 + table: 30200 + - priority: 30204 + family: ipv4 + tos: 8 + table: 30200 + - priority: 30400 + to: 198.51.100.128/26 + table: 30400 + - priority: 30401 + family: ipv4 + iif: iiftest + table: 30400 + - priority: 30402 + family: ipv4 + oif: oiftest + table: 30400 + - priority: 30600 + to: 2001:db8::4/32 + table: 30600 + - priority: 30601 + family: ipv6 + dport: 128 - 256 + invert: True + table: 30600 + # the routing rule selector sport and ipproto are not supported by iproute + # since v4.17.0, and the iproute installed in CentOS-7 and RHEL-7 is + # v4.11.0 + - name: Get the routing rule for looking up the table 30200 + command: ip rule list table 30200 + register: route_rule_table_30200 + ignore_errors: yes + changed_when: false + when: ansible_distribution_major_version != "7" + + - name: Get the routing rule for looking up the table 30400 + command: ip rule list table 30400 + register: route_rule_table_30400 + ignore_errors: yes + changed_when: false + when: ansible_distribution_major_version != "7" + + - name: Get the routing rule for looking up the table 30600 + command: ip -6 rule list table 30600 + register: route_rule_table_30600 + ignore_errors: yes + changed_when: false + when: ansible_distribution_major_version != "7" + + - name: Get the IPv4 routing rule for the connection "{{ interface }}" + command: nmcli -f ipv4.routing-rules c show "{{ interface }}" + register: connection_route_rule + ignore_errors: yes + changed_when: false + + - name: Get the IPv6 routing rule for the connection "{{ interface }}" + command: nmcli -f ipv6.routing-rules c show "{{ interface }}" + register: connection_route_rule6 + ignore_errors: yes + changed_when: false + + - name: Assert that the routing rule with table lookup 30200 matches the + specified rule + assert: + that: + - route_rule_table_30200.stdout is search("30200:(\s+)from + 198.51.100.58/26 lookup 30200") + - route_rule_table_30200.stdout is search("30201:(\s+)from all fwmark + 0x1/0x1 lookup 30200") + - route_rule_table_30200.stdout is search("30202:(\s+)from all + ipproto tcp lookup 30200") + - route_rule_table_30200.stdout is search("30203:(\s+)from all sport + 128-256 lookup 30200") + - route_rule_table_30200.stdout is search("30204:(\s+)from all tos + (0x08|throughput) lookup 30200") + msg: "the routing rule with table lookup 30200 does not match the + specified rule" + when: ansible_distribution_major_version != "7" + + - name: Assert that the routing rule with table lookup 30400 matches the + specified rule + assert: + that: + - route_rule_table_30400.stdout is search("30400:(\s+)from all to + 198.51.100.128/26 lookup 30400") + - route_rule_table_30400.stdout is search("30401:(\s+)from all iif + iiftest \[detached\] lookup 30400") + - route_rule_table_30400.stdout is search("30402:(\s+)from all oif + oiftest \[detached\] lookup 30400") + msg: "the routing rule with table lookup 30400 does not match the + specified rule" + when: ansible_distribution_major_version != "7" + + - name: Assert that the routing rule with table lookup 30600 matches the + specified rule + assert: + that: + - route_rule_table_30600.stdout is search("30600:(\s+)from all to + 2001:db8::4/32 lookup 30600") + - route_rule_table_30600.stdout is search("30601:(\s+)not from all + dport 128-256 lookup 30600") + msg: "the routing rule with table lookup 30600 does not match the + specified rule" + when: ansible_distribution_major_version != "7" + + - name: Assert that the IPv4 routing rule in the connection + "{{ interface }}" matches the specified rule + assert: + that: + - connection_route_rule.stdout is search("priority 30200 from + 198.51.100.58/26 table 30200") + - connection_route_rule.stdout is search("priority 30201 from + 0.0.0.0/0 fwmark 0x1/0x1 table 30200") + - connection_route_rule.stdout is search("priority 30202 from + 0.0.0.0/0 ipproto 6 table 30200") + - connection_route_rule.stdout is search("priority 30203 from + 0.0.0.0/0 sport 128-256 table 30200") + - connection_route_rule.stdout is search("priority 30204 from + 0.0.0.0/0 tos 0x08 table 30200") + - connection_route_rule.stdout is search("priority 30400 to + 198.51.100.128/26 table 30400") + - connection_route_rule.stdout is search("priority 30401 from + 0.0.0.0/0 iif iiftest table 30400") + - connection_route_rule.stdout is search("priority 30402 from + 0.0.0.0/0 oif oiftest table 30400") + msg: "the IPv4 routing rule in the connection '{{ interface }}' does + not match the specified rule" + + - name: Assert that the IPv6 routing rule in the connection + "{{ interface }}" matches the specified rule + assert: + that: + - connection_route_rule6.stdout is search("priority 30600 to + 2001:db8::4/32 table 30600") + - connection_route_rule6.stdout is search("priority 30601 not from + ::/0 dport 128-256 table 30600") or + connection_route_rule6.stdout is search("not priority 30601 from + ::/0 dport 128-256 table 30600") + msg: "the IPv6 routing rule in the connection '{{ interface }}' does + not match the specified rule" +- import_playbook: down_profile+delete_interface.yml + vars: + profile: "{{ interface }}" +# FIXME: assert profile/device down +- import_playbook: remove_profile.yml + vars: + profile: "{{ interface }}" +- name: Assert device and profile are absent + hosts: all + tasks: + - include_tasks: tasks/assert_profile_absent.yml + vars: + profile: "{{ interface }}" + - include_tasks: tasks/assert_device_absent.yml +... diff --git a/tests/tests_routing_rules_nm.yml b/tests/tests_routing_rules_nm.yml new file mode 100644 index 0000000..1bf7553 --- /dev/null +++ b/tests/tests_routing_rules_nm.yml @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: BSD-3-Clause +# This file was generated by ensure_provider_tests.py +--- +# set network provider and gather facts +- hosts: all + name: Run playbook 'playbooks/tests_routing_rules.yml' with nm as provider + tasks: + - include_tasks: tasks/el_repo_setup.yml + - name: Set network provider to 'nm' + set_fact: + network_provider: nm + tags: + - always + + +# The test requires or should run with NetworkManager, therefore it cannot run +# on RHEL/CentOS 6 +- import_playbook: playbooks/tests_routing_rules.yml + when: + - ansible_distribution_major_version != '6' diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index 24c6b4c..bd253a4 100644 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -194,6 +194,7 @@ class TestValidator(Python26CompatTestCase): "rule_append_only": False, "route": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, "dns": [], "dns_options": [], @@ -409,6 +410,45 @@ class TestValidator(Python26CompatTestCase): self.assertValidationError(v, False) self.assertValidationError(v, True) + def test_validate_range(self): + + v = network_lsr.argument_validator.ArgValidatorRange( + "range", val_min=0, val_max=65534 + ) + self.assertEqual((1, 1), v.validate(1)) + self.assertEqual((10, 1000), v.validate("10-1000")) + self.assertEqual((256, 256), v.validate("256")) + self.assertRaisesRegex( + ValidationError, + "the range value True is invalid", + v.validate, + True, + ) + self.assertRaisesRegex( + ValidationError, + "the range value 2.5 is invalid", + v.validate, + 2.5, + ) + self.assertRaisesRegex( + ValidationError, + "the range start cannot be greater than range end", + v.validate, + "2000-1000", + ) + self.assertRaisesRegex( + ValidationError, + "upper range value is 65535 but cannot be greater than 65534", + v.validate, + "1-65535", + ) + self.assertRaisesRegex( + ValidationError, + "lower range value is -1 but cannot be less than 0", + v.validate, + -1, + ) + def test_validate_bool(self): v = network_lsr.argument_validator.ArgValidatorBool("state") @@ -543,6 +583,7 @@ class TestValidator(Python26CompatTestCase): "rule_append_only": False, "route": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, "dns": [], "dns_options": [], @@ -602,6 +643,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -652,6 +694,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -747,6 +790,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "rule_append_only": False, "route": [], + "routing_rule": [], }, "mac": "52:54:00:44:9f:ba", "controller": None, @@ -813,6 +857,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "rule_append_only": False, "route": [], + "routing_rule": [], }, "mac": None, "match": {}, @@ -887,6 +932,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -980,6 +1026,7 @@ class TestValidator(Python26CompatTestCase): "route": [], "route_metric6": None, "route_metric4": None, + "routing_rule": [], "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, @@ -1048,6 +1095,7 @@ class TestValidator(Python26CompatTestCase): "table": None, } ], + "routing_rule": [], }, "mac": None, "controller": None, @@ -1124,6 +1172,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "rule_append_only": False, "route": [], + "routing_rule": [], }, "mac": None, "match": {}, @@ -1200,6 +1249,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "rule_append_only": False, "route": [], + "routing_rule": [], }, "mac": None, "match": {}, @@ -1299,6 +1349,7 @@ class TestValidator(Python26CompatTestCase): "route": [], "route_metric6": None, "route_metric4": None, + "routing_rule": [], "dns_options": [], "dns_search": [], "dhcp4_send_hostname": None, @@ -1368,6 +1419,7 @@ class TestValidator(Python26CompatTestCase): "table": None, } ], + "routing_rule": [], }, "mac": None, "controller": None, @@ -1444,6 +1496,7 @@ class TestValidator(Python26CompatTestCase): "route": [], "route_metric6": None, "route_metric4": None, + "routing_rule": [], "dns_options": [], "ipv6_disabled": False, "dns_search": [], @@ -1507,6 +1560,7 @@ class TestValidator(Python26CompatTestCase): "table": None, } ], + "routing_rule": [], }, "mac": None, "macvlan": {"mode": "bridge", "promiscuous": True, "tap": False}, @@ -1564,6 +1618,7 @@ class TestValidator(Python26CompatTestCase): "table": None, } ], + "routing_rule": [], }, "mac": None, "macvlan": {"mode": "passthru", "promiscuous": False, "tap": True}, @@ -1654,6 +1709,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "route_metric4": None, "route_metric6": None, + "routing_rule": [], "rule_append_only": False, }, "mac": None, @@ -1696,6 +1752,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "route_metric4": None, "route_metric6": None, + "routing_rule": [], "rule_append_only": False, }, "mac": None, @@ -1790,6 +1847,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "rule_append_only": False, "route": [], + "routing_rule": [], }, "mac": None, "match": {}, @@ -1868,6 +1926,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "rule_append_only": False, "route": [], + "routing_rule": [], }, "mac": None, "match": {}, @@ -1918,6 +1977,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "rule_append_only": False, "route": [], + "routing_rule": [], "auto6": True, "ipv6_disabled": False, "dhcp4": True, @@ -1977,6 +2037,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -2054,6 +2115,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "route_metric4": None, "route_metric6": None, + "routing_rule": [], "rule_append_only": False, }, "mac": None, @@ -2096,6 +2158,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "route_metric4": None, "route_metric6": None, + "routing_rule": [], "rule_append_only": False, }, "mac": None, @@ -2154,6 +2217,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "route_metric4": None, "route_metric6": None, + "routing_rule": [], "rule_append_only": False, }, "mac": None, @@ -2229,6 +2293,7 @@ class TestValidator(Python26CompatTestCase): "route_append_only": False, "route_metric4": None, "route_metric6": None, + "routing_rule": [], "rule_append_only": False, }, "mac": "11:22:33:44:55:66:77:88:99:00:" @@ -2324,6 +2389,7 @@ class TestValidator(Python26CompatTestCase): "table": None, }, ], + "routing_rule": [], "dns": [], "dns_options": [], "dns_search": ["aa", "bb"], @@ -2431,6 +2497,7 @@ class TestValidator(Python26CompatTestCase): "table": None, }, ], + "routing_rule": [], "dns": [], "dns_options": [], "dns_search": ["aa", "bb"], @@ -2552,6 +2619,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -2631,6 +2699,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -2710,6 +2779,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -2787,6 +2857,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -2854,6 +2925,7 @@ class TestValidator(Python26CompatTestCase): "dns_options": [], "dns_search": [], "route_metric6": None, + "routing_rule": [], "dhcp4_send_hostname": None, }, "mac": None, @@ -4374,6 +4446,216 @@ class TestValidatorRouteTable(Python26CompatTestCase): ) +class TestValidatorRoutingRules(Python26CompatTestCase): + def setUp(self): + self.test_connections = [ + { + "name": "eth0", + "type": "ethernet", + "ip": { + "dhcp4": False, + "address": ["198.51.100.3/26"], + "route": [ + { + "network": "198.51.100.128", + "prefix": 26, + "gateway": "198.51.100.1", + "metric": 2, + "table": 30400, + }, + ], + "routing_rule": [ + { + "action": "to-table", + "priority": 256, + }, + ], + }, + } + ] + self.validator = network_lsr.argument_validator.ArgValidator_ListConnections() + + def test_routing_rule_missing_address_family(self): + """ + Test that the address family has to be specified if cannot be derived from src + or dst address + """ + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.test_connections[0]["ip"]["routing_rule"][0]["suppress_prefixlength"] = 32 + + self.assertRaisesRegex( + ValidationError, + "specify the address family 'family'", + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_validate_address_family(self): + """ + Test that the derived address family and the specified address family should be + consistent + """ + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.test_connections[0]["ip"]["routing_rule"][0]["family"] = "ipv6" + self.test_connections[0]["ip"]["routing_rule"][0]["from"] = "198.51.100.58/24" + self.assertRaisesRegex( + ValidationError, + "invalid address family in 'from'", + self.validator.validate, + self.test_connections, + ) + self.test_connections[0]["ip"]["routing_rule"][0]["from"] = "2001:db8::2/32" + self.test_connections[0]["ip"]["routing_rule"][0]["to"] = "198.51.100.60/24" + self.assertRaisesRegex( + ValidationError, + "invalid address family in 'to'", + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_missing_table(self): + """ + Test that table has to be defined when the action of the routing rule is + "to-table" + """ + self.test_connections[0]["ip"]["routing_rule"][0]["family"] = "ipv4" + self.assertRaisesRegex( + ValidationError, + "missing 'table' for the routing rule", + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_invalid_from_prefix_length(self): + """ + Test that the prefix length for from/src cannot be zero when from/src is + specified + """ + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.test_connections[0]["ip"]["routing_rule"][0]["from"] = "198.51.100.58/0" + self.assertRaisesRegex( + ValidationError, + "the prefix length for 'from' cannot be zero", + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_invalid_to_prefix_length(self): + """ + Test that the prefix length for to/dst cannot be zero when to/dst is specified + """ + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.test_connections[0]["ip"]["routing_rule"][0]["to"] = "198.51.100.58/0" + self.assertRaisesRegex( + ValidationError, + "the prefix length for 'to' cannot be zero", + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_validate_fwmark(self): + """ + Test that fwmark requires fwmask to be specified + """ + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.test_connections[0]["ip"]["routing_rule"][0]["family"] = "ipv4" + self.test_connections[0]["ip"]["routing_rule"][0]["fwmark"] = 1 + self.assertRaisesRegex( + ValidationError, + "'fwmask' and 'fwmark' must be set together", + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_validate_fwmask(self): + """ + Test that fwmask requires fwmark to be specified + """ + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.test_connections[0]["ip"]["routing_rule"][0]["family"] = "ipv4" + self.test_connections[0]["ip"]["routing_rule"][0]["fwmask"] = 1 + self.assertRaisesRegex( + ValidationError, + "'fwmask' and 'fwmark' must be set together", + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_invalid_incoming_interface_name(self): + """ + Test the invalid incoming interface name specified in the routing rule + """ + self.test_connections[0]["ip"]["routing_rule"][0]["iif"] = " test " + self.test_connections[0]["ip"]["routing_rule"][0]["family"] = "ipv4" + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.assertRaisesRegex( + ValidationError, + "the incoming interface '{0}' specified in the routing rule is invalid " + "interface_name".format( + self.test_connections[0]["ip"]["routing_rule"][0]["iif"] + ), + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_invalid_outgoing_interface_name(self): + """ + Test the invalid outgoing interface name specified in the routing rule + """ + self.test_connections[0]["ip"]["routing_rule"][0]["oif"] = " test " + self.test_connections[0]["ip"]["routing_rule"][0]["family"] = "ipv4" + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.assertRaisesRegex( + ValidationError, + "the outgoing interface '{0}' specified in the routing rule is invalid " + "interface_name".format( + self.test_connections[0]["ip"]["routing_rule"][0]["oif"] + ), + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_validate_uid(self): + """ + Test the invalid uid specified in the routing rule + """ + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + self.test_connections[0]["ip"]["routing_rule"][0]["uid"] = "2000 - 1000" + self.assertRaisesRegex( + ValidationError, + "the range start cannot be greater than range end", + self.validator.validate, + self.test_connections, + ) + + def test_routing_rule_validate_suppress_prefixlength(self): + """ + Test the invalid suppress_prefixlength setting + """ + self.test_connections[0]["ip"]["routing_rule"][0]["suppress_prefixlength"] = 40 + self.test_connections[0]["ip"]["routing_rule"][0]["family"] = "ipv4" + self.test_connections[0]["ip"]["routing_rule"][0]["table"] = 256 + suppress_prefixlength_val_max = Util.addr_family_prefix_length( + self.test_connections[0]["ip"]["routing_rule"][0]["family"] + ) + self.assertRaisesRegex( + ValidationError, + "The specified 'suppress_prefixlength' cannot be greater than {0}".format( + suppress_prefixlength_val_max + ), + self.validator.validate, + self.test_connections, + ) + self.test_connections[0]["ip"]["routing_rule"][0]["family"] = "ipv6" + self.test_connections[0]["ip"]["routing_rule"][0]["action"] = "blackhole" + self.assertRaisesRegex( + ValidationError, + "'suppress_prefixlength' is only allowed with the to-table action", + self.validator.validate, + self.test_connections, + ) + + class TestValidatorDictBond(Python26CompatTestCase): def setUp(self): self.validator = network_lsr.argument_validator.ArgValidator_ListConnections()