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()