samldb: ensure subnets have proper net ranges
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>
Wed, 23 Sep 2015 03:10:56 +0000 (15:10 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Thu, 24 Dec 2015 03:09:29 +0000 (04:09 +0100)
A subnet name needs to be a valid CIDR address range -- that's the
ones that look like 10.9.8.0/22, where the number after the /
determines how many bits are in the address suffix. It can be IPv4 or
IPv6. There are a few odd constraints (see MS-ADTS v20150630
6.1.1.2.2.2.1 "Subnet Object") -- for example, with IPv4, the implied
bit mask can't equal the address. That is, you can't have a subnet
named "255.255.255.0/24" in a Windows subnet. This rule does not apply
to IPv6.

Windows and Samba both make some ensure that subnets have a unique
valid name, though unfortunately Windows 2008R2 is rather slack when
it comes to IPv6. We follow Windows 2012R2, which roughly follows
RFC5952 -- with one caveat: Windows will allow an address like
"::ffff:0:1:2", which translates to the IPv4 address "0.1.0.2" using
the SIIT translation scheme, and which inet_ntop() would render as
"::ffff:0:0.1.0.2". In the Samba implementation we use an inet_pton()/
inet_ntop() round-trip to establish canonicality, so these addresses
fail. Windows wisely does not allow the SIIT style addresses (the
acronym is widely agreed to be off-by-one in the second letter), and
it will regard "::ffff:0:1:2" as simply "::ffff:0:1:2" and allow it.
We would like to do that too.

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/tests/samba_tool/sites.py
source4/dsdb/samdb/ldb_modules/samldb.c
source4/dsdb/tests/python/sites.py

index 81cc66d73b080428d6fa5740e87f6f8dc5167174..ee281094d2ac9674e0997be985f911b909b95ac5 100644 (file)
@@ -110,3 +110,27 @@ class SitesSubnetCmdTestCase(BaseSitesCmdTestCase):
             self.assertIsNotNone(ret)
             self.assertEqual(len(ret), 1)
             self.samdb.delete(dnsubnet, ["tree_delete:0"])
+
+    def test_site_subnet_create_should_fail(self):
+        cidrs = (("10.9.8.0/33", self.sitename),    # mask too big
+                 ("50.60.0.0/8", self.sitename2),   # insufficient zeros
+                 ("50.261.0.0/16", self.sitename2), # bad octet
+                 ("7.0.0.0.0/0", self.sitename),    # insufficient zeros
+                 ("aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119",
+                  self.sitename),                   # insufficient zeros
+             )
+
+        for cidr, sitename in cidrs:
+            result, out, err = self.runsubcmd("sites", "subnet", "create",
+                                              cidr, sitename,
+                                              "-H", self.dburl,
+                                              self.creds_string)
+            self.assertCmdFail(result)
+
+            ret = self.samdb.search(base=self.config_dn,
+                                    scope=ldb.SCOPE_SUBTREE,
+                                    expression=('(&(objectclass=subnet)(cn=%s))'
+                                                % cidr))
+
+            self.assertIsNotNone(ret)
+            self.assertEqual(len(ret), 0)
index 153c85ea571a824fa6cd3f83704424c7aec53595..91e9625bf40231de72615f52ee4116763992ad59 100644 (file)
@@ -42,6 +42,7 @@
 #include "ldb_wrap.h"
 #include "param/param.h"
 #include "libds/common/flag_mapping.h"
+#include "system/network.h"
 
 struct samldb_ctx;
 enum samldb_add_type {
@@ -2644,6 +2645,253 @@ static int samldb_fsmo_role_owner_check(struct samldb_ctx *ac)
        return LDB_SUCCESS;
 }
 
+/*
+ * Return zero if the number of zero bits in the address (looking from low to
+ * high) is equal to or greater than the length minus the mask. Otherwise it
+ * returns -1.
+ */
+static int check_cidr_zero_bits(uint8_t *address, unsigned int len,
+                               unsigned int mask)
+{
+       /* <address> is an integer in big-endian form, <len> bits long. All
+          bits between <mask> and <len> must be zero. */
+       int i;
+       unsigned int byte_len;
+       unsigned int byte_mask;
+       unsigned int bit_mask;
+       if (len == 32) {
+               DBG_INFO("Looking at address %02x%02x%02x%02x, mask %u\n",
+                        address[0], address[1], address[2], address[3],
+                         mask);
+       } else if (len == 128){
+               DBG_INFO("Looking at address "
+                        "%02x%02x-%02x%02x-%02x%02x-%02x%02x-"
+                        "%02x%02x-%02x%02x-%02x%02x-%02x%02x, mask %u\n",
+                        address[0], address[1], address[2], address[3],
+                        address[4], address[5], address[6], address[7],
+                        address[8], address[9], address[10], address[11],
+                        address[12], address[13], address[14], address[15],
+                        mask);
+       }
+
+       if (mask > len){
+               DBG_INFO("mask %u is too big (> %u)\n", mask, len);
+               return -1;
+       }
+       if (mask == len){
+               /* single address subnet.
+                * In IPv4 all 255s is invalid by the bitmask != address rule
+                * in MS-ADTS. IPv6 does not suffer.
+                */
+               if (len == 32){
+                       if (address[0] == 255 &&
+                           address[1] == 255 &&
+                           address[2] == 255 &&
+                           address[3] == 255){
+                               return -1;
+                       }
+               }
+               return 0;
+       }
+
+       byte_len = len / 8;
+       byte_mask = mask / 8;
+
+       for (i = byte_len - 1; i > byte_mask; i--){
+               DBG_DEBUG("checking byte %d %02x\n", i, address[i]);
+               if (address[i] != 0){
+                       return -1;
+               }
+       }
+       bit_mask = (1 << (8 - (mask & 7))) - 1;
+       DBG_DEBUG("checking bitmask %02x & %02x overlap %02x\n", bit_mask, address[byte_mask],
+                 bit_mask & address[byte_mask]);
+       if (address[byte_mask] & bit_mask){
+               return -1;
+       }
+
+       /* According to MS-ADTS, the mask can't exactly equal the bitmask for
+        * IPv4 (but this is fine for v6). That is 255.255.80.0/17 is bad,
+        * because the bitmask implied by "/17" is 255.255.80.0.
+        *
+        * The bit_mask used in the previous check is the complement of what
+        * we want here.
+        */
+       if (len == 32 && address[byte_mask] == (uint8_t)~bit_mask){
+               bool ok = false;
+               for (i = 0; i < byte_mask; i++){
+                       if (address[i] != 255){
+                               ok = true;
+                               break;
+                       }
+               }
+               if (ok == false){
+                       return -1;
+               }
+       }
+       return 0;
+}
+
+
+
+static int check_address_roundtrip(const char *address, int family,
+                                  const uint8_t *address_bytes,
+                                  char *buffer, int buffer_len)
+{
+       /*
+        * Check that the address is in the canonical RFC5952 format for IPv6,
+        * and lacks extra leading zeros for each dotted decimal for IPv4.
+        * Handily this is what inet_ntop() gives you.
+        */
+       const char *address_redux = inet_ntop(family, address_bytes,
+                                             buffer, buffer_len);
+       if (address_redux == NULL){
+               DBG_INFO("Address round trip %s failed unexpectedly"
+                        " with errno %d\n", address, errno);
+               return -1;
+       }
+       if (strcasecmp(address, address_redux) != 0){
+               DBG_INFO("Address %s round trips to %s; fail!\n",
+                        address, address_redux);
+               return -1;
+       }
+       return 0;
+}
+
+
+
+/*
+ * MS-ADTS v20150630 6.1.1.2.2.2.1 Subnet Object, refers to RFC1166 and
+ * RFC2373. It specifies something seemingly indistinguishable from an RFC4632
+ * CIDR address range without saying so explicitly. Here we follow the CIDR
+ * spec.
+ *
+ * Return 0 on success, -1 on error.
+ */
+static int verify_cidr(const char *cidr)
+{
+       char *address = NULL, *slash = NULL, *endptr = NULL;
+       bool has_colon, has_dot;
+       int res, ret;
+       unsigned long mask;
+       uint8_t *address_bytes = NULL;
+       char *address_redux = NULL;
+       unsigned int address_len;
+       TALLOC_CTX *frame = NULL;
+
+       DBG_DEBUG("CIDR is %s\n", cidr);
+       frame = talloc_stackframe();
+       address = talloc_strdup(frame, cidr);
+       if (address == NULL){
+               goto error;
+       }
+
+       /* there must be a '/' */
+       slash = strchr(address, '/');
+       if (slash == NULL){
+               goto error;
+       }
+       /* terminate the address for strchr, inet_pton */
+       *slash = '\0';
+
+       mask = strtoul(slash + 1, &endptr, 10);
+       if (mask == 0){
+               DBG_INFO("Windows does not like the zero mask, "
+                        "so nor do we: %s\n", cidr);
+               goto error;
+       }
+
+       if (*endptr != '\0' || endptr == slash + 1){
+               DBG_INFO("CIDR mask is not a proper integer: %s\n", cidr);
+               goto error;
+       }
+
+       address_bytes = talloc_size(frame, sizeof(struct in6_addr));
+       if (address_bytes == NULL){
+               goto error;
+       }
+
+       address_redux = talloc_size(frame, INET6_ADDRSTRLEN);
+       if (address_redux == NULL){
+               goto error;
+       }
+
+       DBG_INFO("found address %s, mask %lu\n", address, mask);
+       has_colon = (strchr(address, ':') == NULL) ? false : true;
+       has_dot = (strchr(address, '.') == NULL) ? false : true;
+       if (has_dot && has_colon){
+               /* This seems to be an IPv4 address embedded in IPv6, which is
+                  icky. We don't support it. */
+               DBG_INFO("Refusing to consider cidr '%s' with dots and colons\n",
+                         cidr);
+               goto error;
+       } else if (has_colon){  /* looks like IPv6 */
+               res = inet_pton(AF_INET6, address, address_bytes);
+               if (res != 1) {
+                       DBG_INFO("Address in %s fails to parse as IPv6\n", cidr);
+                       goto error;
+               }
+               address_len = 128;
+               if (check_address_roundtrip(address, AF_INET6, address_bytes,
+                                           address_redux, INET6_ADDRSTRLEN)){
+                       goto error;
+               }
+       } else if (has_dot) {
+               /* looks like IPv4 */
+               if (strcmp(address, "0.0.0.0") == 0){
+                       DBG_INFO("Windows does not like the zero IPv4 address, "
+                                "so nor do we.\n");
+                       goto error;
+               }
+               res = inet_pton(AF_INET, address, address_bytes);
+               if (res != 1) {
+                       DBG_INFO("Address in %s fails to parse as IPv4\n", cidr);
+                       goto error;
+               }
+               address_len = 32;
+
+               if (check_address_roundtrip(address, AF_INET, address_bytes,
+                                           address_redux, INET_ADDRSTRLEN)){
+                       goto error;
+               }
+       } else {
+               /* This doesn't look like an IP address at all. */
+               goto error;
+       }
+
+       ret = check_cidr_zero_bits(address_bytes, address_len, mask);
+       talloc_free(frame);
+       return ret;
+  error:
+       talloc_free(frame);
+       return -1;
+}
+
+
+static int samldb_verify_subnet(struct samldb_ctx *ac)
+{
+       struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
+       const char *cidr = NULL;
+       const struct ldb_val *rdn_value = NULL;
+
+       rdn_value = ldb_dn_get_rdn_val(ac->msg->dn);
+       cidr = ldb_dn_escape_value(ac, *rdn_value);
+       DBG_INFO("looking at cidr '%s'\n", cidr);
+       if (cidr == NULL) {
+               ldb_set_errstring(ldb,
+                                 "samldb: adding an empty subnet cidr seems wrong");
+               return LDB_ERR_UNWILLING_TO_PERFORM;
+       }
+
+       if (verify_cidr(cidr)){
+               ldb_set_errstring(ldb,
+                                 "samldb: subnet value is invalid");
+               return LDB_ERR_INVALID_DN_SYNTAX;
+       }
+
+       return LDB_SUCCESS;
+}
+
 
 /* add */
 static int samldb_add(struct ldb_module *module, struct ldb_request *req)
@@ -2752,6 +3000,17 @@ static int samldb_add(struct ldb_module *module, struct ldb_request *req)
                return samldb_fill_object(ac);
        }
 
+       if (samdb_find_attribute(ldb, ac->msg,
+                                "objectclass", "subnet") != NULL) {
+               ret = samldb_verify_subnet(ac);
+               if (ret != LDB_SUCCESS) {
+                       talloc_free(ac);
+                       return ret;
+               }
+               /* We are just checking the value is valid, and there are no
+                  values to fill in. */
+       }
+
        talloc_free(ac);
 
        /* nothing matched, go on */
@@ -3099,6 +3358,15 @@ static int check_rename_constraints(struct ldb_message *msg,
                return LDB_ERR_UNWILLING_TO_PERFORM;
        }
 
+       /* subnet objects */
+       if (samdb_find_attribute(ldb, msg, "objectclass", "subnet") != NULL) {
+               ret = samldb_verify_subnet(ac);
+               if (ret != LDB_SUCCESS) {
+                       talloc_free(ac);
+                       return ret;
+               }
+       }
+
        /* systemFlags */
 
        dn1 = ldb_dn_get_parent(ac, olddn);
index 6242a9dbda0518a3c57809608a3a85126f718429..1714ff814c7deaad33681579bc1b58dde2f8b551 100755 (executable)
@@ -183,5 +183,293 @@ class SimpleSubnetTests(SitesBaseTests):
         self.assertRaises(subnets.SubnetNotFound,
                           subnets.delete_subnet, self.ldb, basedn, cidr)
 
+    def test_create_bad_ranges(self):
+        """These CIDR ranges all have something wrong with them, and they
+        should all fail."""
+        basedn = self.ldb.get_config_basedn()
+
+        cidrs = [
+            # IPv4
+            # insufficient zeros
+            "10.11.12.0/14",
+            "110.0.0.0/6",
+            "1.0.0.0/0",
+            "10.11.13.1/24",
+            "1.2.3.4/29",
+            "10.11.12.0/21",
+            # out of range mask
+            "110.0.0.0/33",
+            "110.0.0.0/-1",
+            "4.0.0.0/111",
+            # out of range address
+            "310.0.0.0/24",
+            "10.0.0.256/32",
+            "1.1.-20.0/24",
+            # badly formed
+            "1.0.0.0/1e",
+            "1.0.0.0/24.0",
+            "1.0.0.0/1/1",
+            "1.0.0.0",
+            "1.c.0.0/24",
+            "1.2.0.0.0/27",
+            "1.23.0/24",
+            "1.23.0.-7/24",
+            "1.-23.0.7/24",
+            "1.23.-0.7/24",
+            "1.23.0.0/0x10",
+            # IPv6 insufficient zeros -- this could be a subtle one
+            # due to the vagaries of endianness in the 16 bit groups.
+            "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119",
+            "aaaa:bbbb::/31",
+            "a:b::/31",
+            "c000::/1",
+            "a::b00/119",
+            "1::1/127",
+            "1::2/126",
+            "1::100/119",
+            "1::8000/112",
+            # out of range mask
+            "a:b::/130",
+            "a:b::/-1",
+            "::/129",
+            # An IPv4 address can't be exactly the bitmask (MS ADTS)
+            "128.0.0.0/1",
+            "192.0.0.0/2",
+            "255.192.0.0/10",
+            "255.255.255.0/24",
+            "255.255.255.255/32",
+            "0.0.0.0/0",
+            # The address can't have leading zeros (not RFC 4632, but MS ADTS)
+            "00.1.2.0/24",
+            "003.1.2.0/24",
+            "022.1.0.0/16",
+            "00000000000000000000000003.1.2.0/24",
+            "09876::abfc/126",
+            "0aaaa:bbbb::/32",
+            "009876::abfc/126",
+            "000a:bbbb::/32",
+
+            # How about extraneous zeros later on
+            "3.01.2.0/24",
+            "3.1.2.00/24",
+            "22.001.0.0/16",
+            "3.01.02.0/24",
+            "100a:0bbb:0023::/48",
+            "100a::0023/128",
+
+            # Windows doesn't like the zero IPv4 address
+            "0.0.0.0/8",
+            # or the zero mask on IPv6
+            "::/0",
+
+            # various violations of RFC5952
+            "0:0:0:0:0:0:0:0/8",
+            "0::0/0",
+            "::0:0/48",
+            "::0:4/128",
+            "0::/8",
+            "0::4f/128",
+            "0::42:0:0:0:0/64",
+            "4f::0/48",
+
+            # badly formed -- mostly the wrong arrangement of colons
+            "a::b::0/120",
+            "a::abcdf:0/120",
+            "a::g:0/120",
+            "::0::3/48",
+            "2001:3::110::3/118",
+            "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1111:0000/128",
+            "a:::5:0/120",
+
+            # non-canonical representations (vs RFC 5952)
+            # "2001:0:c633:63::1:0/120"  is correct
+            "2001:0:c633:63:0:0:1:0/120",
+            "2001::c633:63:0:0:1:0/120",
+            "2001:0:c633:63:0:0:1::/120",
+
+            # "10:0:0:42::/64" is correct
+            "10::42:0:0:0:0/64",
+            "10:0:0:42:0:0:0:0/64",
+
+            # "1::4:5:0:0:8/127" is correct
+            "1:0:0:4:5:0:0:8/127",
+            "1:0:0:4:5::8/127",
+
+            # "2001:db8:0:1:1:1:1:1/128" is correct
+            "2001:db8::1:1:1:1:1/128",
+
+            # IP4 embedded - rejected
+            "a::10.0.0.0/120",
+            "a::10.9.8.7/128",
+            "::ffff:0:0/96", #this one fails on WIN2012r2
+            # completely wrong
+            None,
+            "bob",
+            3.1415,
+            False,
+            "10.11.16.0/24\x00hidden bytes past a zero",
+            self,
+        ]
+
+        failures = []
+        for cidr in cidrs:
+            try:
+                subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+            except subnets.SubnetInvalid:
+                print >> sys.stderr, "%s fails properly" % (cidr,)
+                continue
+
+            # we are here because it succeeded when it shouldn't have.
+            print >> sys.stderr, "CIDR %s fails to fail" % (cidr,)
+            failures.append(cidr)
+            subnets.delete_subnet(self.ldb, basedn, cidr)
+
+        if failures:
+            print "These bad subnet names were accepted:"
+            for cidr in failures:
+                print "    %s" % cidr
+            self.fail()
+
+    def test_create_good_ranges(self):
+        """All of these CIDRs are good, and the subnet creation should
+        succeed."""
+        basedn = self.ldb.get_config_basedn()
+
+        cidrs = [
+            # IPv4
+            "10.11.12.0/24",
+            "10.11.12.0/23",
+            "10.11.12.0/25",
+            "110.0.0.0/7",
+            "1.0.0.0/32",
+            "10.11.13.0/32",
+            "10.11.13.1/32",
+            "99.0.97.0/24",
+            "1.2.3.4/30",
+            "10.11.12.0/22",
+            "0.12.13.0/24",
+            # IPv6
+            "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/120",
+            "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11f0/124",
+            "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11fc/126",
+            # don't forget upper case
+            "FFFF:FFFF:FFFF:FFFF:ABCD:EfFF:FFFF:FFeF/128",
+            "9876::ab00/120",
+            "9876::abf0/124",
+            "9876::abfc/126",
+            "aaaa:bbbb::/32",
+            "aaaa:bbba::/31",
+            "aaaa:ba00::/23",
+            "aaaa:bb00::/24",
+            "aaaa:bb00::/77",
+            "::/48",
+            "a:b::/32",
+            "c000::/2",
+            "a::b00/120",
+            "1::2/127",
+            # this pattern of address suffix == mask is forbidden with
+            # IPv4 but OK for IPv6.
+            "8000::/1",
+            "c000::/2",
+            "ffff:ffff:ffc0::/42",
+            "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF/128",
+            # leading zeros are forbidden, but implicit IPv6 zeros
+            # (via "::") are OK.
+            "::1000/116",
+            "::8000/113",
+            # taken to the logical conclusion, "::/0" should be OK, but no.
+            "::/48",
+
+            # Try some reserved ranges, which it might be reasonable
+            # to exclude, but which are not excluded in practice.
+            "129.0.0.0/16",
+            "129.255.0.0/16",
+            "100.64.0.0/10",
+            "127.0.0.0/8",
+            "127.0.0.0/24",
+            "169.254.0.0/16",
+            "169.254.1.0/24",
+            "192.0.0.0/24",
+            "192.0.2.0/24",
+            "198.18.0.0/15",
+            "198.51.100.0/24",
+            "203.0.113.0/24",
+            "224.0.0.0/4",
+            "130.129.0.0/16",
+            "130.255.0.0/16",
+            "192.12.0.0/24",
+            "223.255.255.0/24",
+            "240.255.255.0/24",
+            "224.0.0.0/8",
+            "::/96",
+            "100::/64",
+            "2001:10::/28",
+            "fec0::/10",
+            "ff00::/8",
+            "::1/128",
+            "2001:db8::/32",
+            "2001:10::/28",
+            "2002::/24",
+            "2002:a00::/24",
+            "2002:7f00::/24",
+            "2002:a9fe::/32",
+            "2002:ac10::/28",
+            "2002:c000::/40",
+            "2002:c000:200::/40",
+            "2002:c0a8::/32",
+            "2002:c612::/31",
+            "2002:c633:6400::/40",
+            "2002:cb00:7100::/40",
+            "2002:e000::/20",
+            "2002:f000::/20",
+            "2002:ffff:ffff::/48",
+            "2001::/40",
+            "2001:0:a00::/40",
+            "2001:0:7f00::/40",
+            "2001:0:a9fe::/48",
+            "2001:0:ac10::/44",
+            "2001:0:c000::/56",
+            "2001:0:c000:200::/56",
+            "2001:0:c0a8::/48",
+            "2001:0:c612::/47",
+            "2001:0:c633:6400::/56",
+            "2001:0:cb00:7100::/56",
+            "2001:0:e000::/36",
+            "2001:0:f000::/36",
+            "2001:0:ffff:ffff::/64",
+
+            # non-RFC-5952 versions of these are tested in create_bad_ranges
+            "2001:0:c633:63::1:0/120",
+            "10:0:0:42::/64",
+            "1::4:5:0:0:8/127",
+            "2001:db8:0:1:1:1:1:1/128",
+        ]
+        failures = []
+
+        for cidr in cidrs:
+            try:
+                subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+            except subnets.SubnetInvalid, e:
+                print e
+                failures.append(cidr)
+                continue
+
+            ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+                                  expression=('(&(objectclass=subnet)(cn=%s))' %
+                                              cidr))
+
+            if len(ret) != 1:
+                print "%s was not created" % cidr
+                failures.append(cidr)
+                continue
+            subnets.delete_subnet(self.ldb, basedn, cidr)
+
+        if failures:
+            print "These good subnet names were not accepted:"
+            for cidr in failures:
+                print "    %s" % cidr
+            self.fail()
+
+
 
 TestProgram(module=__name__, opts=subunitopts)