From fdbcaf7cfbf41dbb4b7c9eaadd940ca75503c318 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 6 Nov 2025 14:48:54 +0100 Subject: [PATCH 1/5] Expose `ipcalc.libsonnet` as a component library --- component/espejote-templates/ipcalc.libsonnet | 61 +---------------- lib/cilium-ipcalc.libsonnet | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 60 deletions(-) mode change 100644 => 120000 component/espejote-templates/ipcalc.libsonnet create mode 100644 lib/cilium-ipcalc.libsonnet diff --git a/component/espejote-templates/ipcalc.libsonnet b/component/espejote-templates/ipcalc.libsonnet deleted file mode 100644 index d80ae763f..000000000 --- a/component/espejote-templates/ipcalc.libsonnet +++ /dev/null @@ -1,60 +0,0 @@ -// Convert an IPv4 address in A.B.C.D format that's already been split into an -// array to decimal format according to the formula `A*256^3 + B*256^2 + C*256 -// + D`. The decimal format allows us to make range comparisons and compute -// offsets into a range. -// Parameter ip can either be the IP as a string, or already split into an -// array holding each dotted part. -local ipval(ip) = - local iparr = - if std.type(ip) == 'array' then - ip - else - std.split(ip, '.'); - std.foldl( - function(v, p) v * 256 + p, - std.map(std.parseInt, iparr), - 0 - ); - -// Extract start and end from the provided range, stripping any -// whitespace. `prefix` is only used for the error message. -local parse_ip_range(prefix, rangespec) = - local range_parts = std.map( - function(s) std.stripChars(s, ' '), - std.split(rangespec, '-') - ); - if std.length(range_parts) != 2 then - error 'Expected IP range for "%s" in format "192.0.2.32-192.0.2.63", got %s' % [ - prefix, - rangespec, - ] - else - { - start: range_parts[0], - end: range_parts[1], - }; - -local format_ipval(val) = - assert - val >= 0 && val < ipval('255.255.255.255') - : '%s not an IPv4 address in decimal' % val; - - local iparr = std.reverse(std.foldl( - function(st, i) - local arr = st.arr; - local rem = st.rem; - { - arr: arr + [ rem % 256 ], - rem: rem / 256, - }, - [ 0, 0, 0, 0 ], - { arr: [], rem: val } - ).arr); - - std.join('.', std.map(function(v) '%d' % v, iparr)); - -{ - ipval: ipval, - parse_ip_range: parse_ip_range, - format_ipval: format_ipval, -} diff --git a/component/espejote-templates/ipcalc.libsonnet b/component/espejote-templates/ipcalc.libsonnet new file mode 120000 index 000000000..4a4a4c459 --- /dev/null +++ b/component/espejote-templates/ipcalc.libsonnet @@ -0,0 +1 @@ +../../lib/cilium-ipcalc.libsonnet \ No newline at end of file diff --git a/lib/cilium-ipcalc.libsonnet b/lib/cilium-ipcalc.libsonnet new file mode 100644 index 000000000..c08e0833b --- /dev/null +++ b/lib/cilium-ipcalc.libsonnet @@ -0,0 +1,67 @@ +// NOTE(sg): This file is symlinked to `component/espejote-templates` in +// component-cilium to allow the `espejote-templates/egress-gateway.libsonnet` +// library to work regardless of whether it's used by Espejote or the +// component. We export this as a component library since it might be useful +// for other components on Cilium-enabled clusters. + +// Convert an IPv4 address in A.B.C.D format that's already been split into an +// array to decimal format according to the formula `A*256^3 + B*256^2 + C*256 +// + D`. The decimal format allows us to make range comparisons and compute +// offsets into a range. +// Parameter ip can either be the IP as a string, or already split into an +// array holding each dotted part. +local ipval(ip) = + local iparr = + if std.type(ip) == 'array' then + ip + else + std.split(ip, '.'); + std.foldl( + function(v, p) v * 256 + p, + std.map(std.parseInt, iparr), + 0 + ); + +// Extract start and end from the provided range, stripping any +// whitespace. `prefix` is only used for the error message. +local parse_ip_range(prefix, rangespec) = + local range_parts = std.map( + function(s) std.stripChars(s, ' '), + std.split(rangespec, '-') + ); + if std.length(range_parts) != 2 then + error 'Expected IP range for "%s" in format "192.0.2.32-192.0.2.63", got %s' % [ + prefix, + rangespec, + ] + else + { + start: range_parts[0], + end: range_parts[1], + }; + +local format_ipval(val) = + assert + val >= 0 && val < ipval('255.255.255.255') + : '%s not an IPv4 address in decimal' % val; + + local iparr = std.reverse(std.foldl( + function(st, i) + local arr = st.arr; + local rem = st.rem; + { + arr: arr + [ rem % 256 ], + rem: rem / 256, + }, + [ 0, 0, 0, 0 ], + { arr: [], rem: val } + ).arr); + + std.join('.', std.map(function(v) '%d' % v, iparr)); + +{ + ipval: ipval, + parse_ip_range: parse_ip_range, + parse_cidr: parse_cidr, + format_ipval: format_ipval, +} From d76c155b2a63f7351dde213436e18b440ddd3075 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 6 Nov 2025 17:01:07 +0100 Subject: [PATCH 2/5] ipcalc.libsonnet: Support formatting `255.255.255.255` --- lib/cilium-ipcalc.libsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cilium-ipcalc.libsonnet b/lib/cilium-ipcalc.libsonnet index c08e0833b..d6cc98e46 100644 --- a/lib/cilium-ipcalc.libsonnet +++ b/lib/cilium-ipcalc.libsonnet @@ -42,7 +42,7 @@ local parse_ip_range(prefix, rangespec) = local format_ipval(val) = assert - val >= 0 && val < ipval('255.255.255.255') + val >= 0 && val <= ipval('255.255.255.255') : '%s not an IPv4 address in decimal' % val; local iparr = std.reverse(std.foldl( From 84c4a7a096bd0dc5d0d7470289a99bf630599f52 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Fri, 7 Nov 2025 08:57:56 +0100 Subject: [PATCH 3/5] ipcalc.libsonnet: Raise error in `ipval()` when passed an invalid IPv4 address --- lib/cilium-ipcalc.libsonnet | 17 ++++++++++++----- .../cilium/40_egress_ip_managed_resource.yaml | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/cilium-ipcalc.libsonnet b/lib/cilium-ipcalc.libsonnet index d6cc98e46..d43bba2f8 100644 --- a/lib/cilium-ipcalc.libsonnet +++ b/lib/cilium-ipcalc.libsonnet @@ -16,11 +16,18 @@ local ipval(ip) = ip else std.split(ip, '.'); - std.foldl( - function(v, p) v * 256 + p, - std.map(std.parseInt, iparr), - 0 - ); + local iparr_int = std.map(std.parseInt, iparr); + + if std.any(std.map(function(v) v > 255, iparr_int)) then + error 'Error parsing IPv4 address: %s is not a valid address' % [ + ip, + ] + else + std.foldl( + function(v, p) v * 256 + p, + iparr_int, + 0 + ); // Extract start and end from the provided range, stripping any // whitespace. `prefix` is only used for the error message. diff --git a/tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml b/tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml index 8719b5aec..84e7615aa 100644 --- a/tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml +++ b/tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml @@ -355,11 +355,18 @@ spec: ip else std.split(ip, '.'); - std.foldl( - function(v, p) v * 256 + p, - std.map(std.parseInt, iparr), - 0 - ); + local iparr_int = std.map(std.parseInt, iparr); + + if std.any(std.map(function(v) v > 255, iparr_int)) then + error 'Error parsing IPv4 address: %s is not a valid address' % [ + ip, + ] + else + std.foldl( + function(v, p) v * 256 + p, + iparr_int, + 0 + ); // Extract start and end from the provided range, stripping any // whitespace. `prefix` is only used for the error message. From dfc6cea6710abd4b1aee7191b3316c643c5e5398 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 6 Nov 2025 14:49:17 +0100 Subject: [PATCH 4/5] Add `parse_cidr()` method in `ipcalc.libsonnet` --- lib/cilium-ipcalc.libsonnet | 64 ++++++++++++++++ .../cilium/40_egress_ip_managed_resource.yaml | 73 ++++++++++++++++++- 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/lib/cilium-ipcalc.libsonnet b/lib/cilium-ipcalc.libsonnet index d43bba2f8..97ded7580 100644 --- a/lib/cilium-ipcalc.libsonnet +++ b/lib/cilium-ipcalc.libsonnet @@ -66,6 +66,70 @@ local format_ipval(val) = std.join('.', std.map(function(v) '%d' % v, iparr)); +// Parse network in CIDR notation. Leading and trailing whitespace is +// stripped. `prefix` is only used for the error message. +// +// This function correctly parses the full network info from arbitrary IPs in +// CIDR notation. We return an object that's inspired by the output of the +// Linux utility `ipcalc`. +// +// The return value contains the network address, broadcast address, count of +// IPs in the CIDR and prefix length. For prefix lengths of less than 32, the +// return value additionally contains the first and last host (in `min_host` +// and `max_host`) and netmask. +local parse_cidr(prefix, cidr) = + local parts = std.split(std.stripChars(cidr, ' '), '/'); + if std.length(parts) != 2 then + error 'Expected value for "%s" to be in CIDR notation, got "%s"' % [ + prefix, + cidr, + ] + else + local prefix_length = std.parseInt(parts[1]); + if prefix_length < 0 || prefix_length > 32 then + error 'Invalid CIDR %s: prefix must be between 0 and 32' % cidr + else + // We compute count, netmask and network address using bitwise operations. + // Jsonnet uses 64 bit integers for bitwise ops, so we don't have to worry + // about overflowing when working with 32 bit values (IPv4 addresses). + // + // IPv4 CIDR notation works as follows: / defines a network + // where the first bits of the IP are the "network" and the last + // 32- bits are (mostly) freely selectable for addresses within + // that network. + // + // Bitwise glossary: + // - (1 << n) == 2**n + // - `&` is bitwise and (setting all bits that are set in either operand) + // - `~` is bitwise not (flipping all bits of the operand) + // Jsonnet operator precedence: binary +- bind higher than shifts + + // count is the number of available addresses (including the network and + // broadcast address in the network). It's a value which has the + // 32- low bits set to 1 and all other bits set to 0. + local count = (1 << 32 - prefix_length) - 1; + // Netmask has the high bits set to one and the 32- low + // bits set to 0. We can use `~count` as the mask to set the low + // 32- bits to 0, since count has only these bits set to 1 and + // bitwise not flips all bits. + local netmask = ((1 << 32) - 1) & ~count; + // The network address is the first address in the network. By converting + // the specified to an integer and using the netmask to set the low + // 32- bits to 0 we reliably get the network address regardless of + // which IP in the network that the user specified for a given prefix. + local net_addr = ipval(parts[0]) & netmask; + + { + network_address: format_ipval(net_addr), + broadcast_address: format_ipval(net_addr + count), + prefix_length: prefix_length, + count: count, + } + if prefix_length < 32 then { + host_min: format_ipval(net_addr + 1), + host_max: format_ipval(net_addr + count - 1), + netmask: format_ipval(netmask), + } else {}; + { ipval: ipval, parse_ip_range: parse_ip_range, diff --git a/tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml b/tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml index 84e7615aa..13f9f84a5 100644 --- a/tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml +++ b/tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml @@ -343,6 +343,12 @@ spec: find_egress_range: find_egress_range, } ipcalc.libsonnet: | + // NOTE(sg): This file is symlinked to `component/espejote-templates` in + // component-cilium to allow the `espejote-templates/egress-gateway.libsonnet` + // library to work regardless of whether it's used by Espejote or the + // component. We export this as a component library since it might be useful + // for other components on Cilium-enabled clusters. + // Convert an IPv4 address in A.B.C.D format that's already been split into an // array to decimal format according to the formula `A*256^3 + B*256^2 + C*256 // + D`. The decimal format allows us to make range comparisons and compute @@ -388,7 +394,7 @@ spec: local format_ipval(val) = assert - val >= 0 && val < ipval('255.255.255.255') + val >= 0 && val <= ipval('255.255.255.255') : '%s not an IPv4 address in decimal' % val; local iparr = std.reverse(std.foldl( @@ -405,9 +411,74 @@ spec: std.join('.', std.map(function(v) '%d' % v, iparr)); + // Parse network in CIDR notation. Leading and trailing whitespace is + // stripped. `prefix` is only used for the error message. + // + // This function correctly parses the full network info from arbitrary IPs in + // CIDR notation. We return an object that's inspired by the output of the + // Linux utility `ipcalc`. + // + // The return value contains the network address, broadcast address, count of + // IPs in the CIDR and prefix length. For prefix lengths of less than 32, the + // return value additionally contains the first and last host (in `min_host` + // and `max_host`) and netmask. + local parse_cidr(prefix, cidr) = + local parts = std.split(std.stripChars(cidr, ' '), '/'); + if std.length(parts) != 2 then + error 'Expected value for "%s" to be in CIDR notation, got "%s"' % [ + prefix, + cidr, + ] + else + local prefix_length = std.parseInt(parts[1]); + if prefix_length < 0 || prefix_length > 32 then + error 'Invalid CIDR %s: prefix must be between 0 and 32' % cidr + else + // We compute count, netmask and network address using bitwise operations. + // Jsonnet uses 64 bit integers for bitwise ops, so we don't have to worry + // about overflowing when working with 32 bit values (IPv4 addresses). + // + // IPv4 CIDR notation works as follows: / defines a network + // where the first bits of the IP are the "network" and the last + // 32- bits are (mostly) freely selectable for addresses within + // that network. + // + // Bitwise glossary: + // - (1 << n) == 2**n + // - `&` is bitwise and (setting all bits that are set in either operand) + // - `~` is bitwise not (flipping all bits of the operand) + // Jsonnet operator precedence: binary +- bind higher than shifts + + // count is the number of available addresses (including the network and + // broadcast address in the network). It's a value which has the + // 32- low bits set to 1 and all other bits set to 0. + local count = (1 << 32 - prefix_length) - 1; + // Netmask has the high bits set to one and the 32- low + // bits set to 0. We can use `~count` as the mask to set the low + // 32- bits to 0, since count has only these bits set to 1 and + // bitwise not flips all bits. + local netmask = ((1 << 32) - 1) & ~count; + // The network address is the first address in the network. By converting + // the specified to an integer and using the netmask to set the low + // 32- bits to 0 we reliably get the network address regardless of + // which IP in the network that the user specified for a given prefix. + local net_addr = ipval(parts[0]) & netmask; + + { + network_address: format_ipval(net_addr), + broadcast_address: format_ipval(net_addr + count), + prefix_length: prefix_length, + count: count, + } + if prefix_length < 32 then { + host_min: format_ipval(net_addr + 1), + host_max: format_ipval(net_addr + count - 1), + netmask: format_ipval(netmask), + } else {}; + { ipval: ipval, parse_ip_range: parse_ip_range, + parse_cidr: parse_cidr, format_ipval: format_ipval, } --- From cbc69c795d793ab793ef8ef27978fcba8b7dc797 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Thu, 6 Nov 2025 16:26:59 +0100 Subject: [PATCH 5/5] Add test case for `cilium-ipcalc.libsonnet` Currently, we only have test coverage for `parse_cidr()`. Rendered from template version: main (84a7c63) --- .cruft.json | 2 +- .github/workflows/test.yaml | 2 + Makefile.vars.mk | 2 +- .../lib-ipcalc/cilium/cilium/parse_cidr.yaml | 68 ++++++++++++++++ tests/ipcalc-tests.jsonnet | 21 +++++ tests/lib-ipcalc.yml | 81 +++++++++++++++++++ 6 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 tests/golden/lib-ipcalc/cilium/cilium/parse_cidr.yaml create mode 100644 tests/ipcalc-tests.jsonnet create mode 100644 tests/lib-ipcalc.yml diff --git a/.cruft.json b/.cruft.json index b425c785b..9d23312a3 100644 --- a/.cruft.json +++ b/.cruft.json @@ -7,7 +7,7 @@ "name": "Cilium", "slug": "cilium", "parameter_key": "cilium", - "test_cases": "defaults helm-opensource olm-opensource egress-gateway bgp-control-plane kubeproxyreplacement-strict l2-announcement clustermesh enterprise-bgp hubble-access", + "test_cases": "defaults helm-opensource olm-opensource egress-gateway bgp-control-plane kubeproxyreplacement-strict l2-announcement clustermesh enterprise-bgp hubble-access lib-ipcalc", "add_lib": "n", "add_pp": "n", "add_golden": "y", diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7ebde49a2..02f53b402 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -42,6 +42,7 @@ jobs: - clustermesh - enterprise-bgp - hubble-access + - lib-ipcalc defaults: run: working-directory: ${{ env.COMPONENT_NAME }} @@ -66,6 +67,7 @@ jobs: - clustermesh - enterprise-bgp - hubble-access + - lib-ipcalc defaults: run: working-directory: ${{ env.COMPONENT_NAME }} diff --git a/Makefile.vars.mk b/Makefile.vars.mk index 6711394c9..a24245ed2 100644 --- a/Makefile.vars.mk +++ b/Makefile.vars.mk @@ -57,4 +57,4 @@ KUBENT_IMAGE ?= ghcr.io/doitintl/kube-no-trouble:latest KUBENT_DOCKER ?= $(DOCKER_CMD) $(DOCKER_ARGS) $(root_volume) --entrypoint=/app/kubent $(KUBENT_IMAGE) instance ?= defaults -test_instances = tests/defaults.yml tests/helm-opensource.yml tests/olm-opensource.yml tests/egress-gateway.yml tests/bgp-control-plane.yml tests/kubeproxyreplacement-strict.yml tests/l2-announcement.yml tests/clustermesh.yml tests/enterprise-bgp.yml tests/hubble-access.yml +test_instances = tests/defaults.yml tests/helm-opensource.yml tests/olm-opensource.yml tests/egress-gateway.yml tests/bgp-control-plane.yml tests/kubeproxyreplacement-strict.yml tests/l2-announcement.yml tests/clustermesh.yml tests/enterprise-bgp.yml tests/hubble-access.yml tests/lib-ipcalc.yml diff --git a/tests/golden/lib-ipcalc/cilium/cilium/parse_cidr.yaml b/tests/golden/lib-ipcalc/cilium/cilium/parse_cidr.yaml new file mode 100644 index 000000000..aeb1c14b2 --- /dev/null +++ b/tests/golden/lib-ipcalc/cilium/cilium/parse_cidr.yaml @@ -0,0 +1,68 @@ +broadcast_address: 255.255.255.255 +count: 4294967295 +host_max: 255.255.255.254 +host_min: 0.0.0.1 +netmask: 0.0.0.0 +network_address: 0.0.0.0 +prefix_length: 0 +--- +broadcast_address: 10.255.255.255 +count: 16777215 +host_max: 10.255.255.254 +host_min: 10.0.0.1 +netmask: 255.0.0.0 +network_address: 10.0.0.0 +prefix_length: 8 +--- +broadcast_address: 10.127.255.255 +count: 8388607 +host_max: 10.127.255.254 +host_min: 10.0.0.1 +netmask: 255.128.0.0 +network_address: 10.0.0.0 +prefix_length: 9 +--- +broadcast_address: 10.100.127.255 +count: 32767 +host_max: 10.100.127.254 +host_min: 10.100.0.1 +netmask: 255.255.128.0 +network_address: 10.100.0.0 +prefix_length: 17 +--- +broadcast_address: 192.0.2.255 +count: 255 +host_max: 192.0.2.254 +host_min: 192.0.2.1 +netmask: 255.255.255.0 +network_address: 192.0.2.0 +prefix_length: 24 +--- +broadcast_address: 192.0.2.63 +count: 31 +host_max: 192.0.2.62 +host_min: 192.0.2.33 +netmask: 255.255.255.224 +network_address: 192.0.2.32 +prefix_length: 27 +--- +broadcast_address: 192.0.2.7 +count: 7 +host_max: 192.0.2.6 +host_min: 192.0.2.1 +netmask: 255.255.255.248 +network_address: 192.0.2.0 +prefix_length: 29 +--- +broadcast_address: 192.0.2.40 +count: 0 +network_address: 192.0.2.40 +prefix_length: 32 +--- +broadcast_address: 239.255.255.255 +count: 268435455 +host_max: 239.255.255.254 +host_min: 224.0.0.1 +netmask: 240.0.0.0 +network_address: 224.0.0.0 +prefix_length: 4 diff --git a/tests/ipcalc-tests.jsonnet b/tests/ipcalc-tests.jsonnet new file mode 100644 index 000000000..996116df6 --- /dev/null +++ b/tests/ipcalc-tests.jsonnet @@ -0,0 +1,21 @@ +local kap = import 'lib/kapitan.libjsonnet'; + +local ipcalc = import 'lib/cilium-ipcalc.libsonnet'; + +local inv = kap.inventory(); + +local test_cases = inv.parameters.test_cases; + +local test_parse_cidr(cidrspec) = + local cidr = ipcalc.parse_cidr('test', cidrspec); + assert + std.trace('%s' % [ cidr ], cidr) == test_cases.parse_cidr[cidrspec] : + 'parsing returned unexpected data for %s: %s' % [ cidrspec, cidr ]; + cidr; + +{ + parse_cidr: [ + test_parse_cidr(cidr) + for cidr in std.objectFields(test_cases.parse_cidr) + ], +} diff --git a/tests/lib-ipcalc.yml b/tests/lib-ipcalc.yml new file mode 100644 index 000000000..3b9ada19d --- /dev/null +++ b/tests/lib-ipcalc.yml @@ -0,0 +1,81 @@ +# Overwrite parameters here + +parameters: + kapitan: + ~compile: + - input_paths: + - ${_base_directory}/tests/ipcalc-tests.jsonnet + input_type: jsonnet + output_path: ${_instance}/ + + test_cases: + parse_cidr: + '192.0.2.0/24': + network_address: 192.0.2.0 + broadcast_address: 192.0.2.255 + host_min: 192.0.2.1 + host_max: 192.0.2.254 + count: 255 + prefix_length: 24 + netmask: 255.255.255.0 + '192.0.2.32/27': + network_address: 192.0.2.32 + broadcast_address: 192.0.2.63 + host_min: 192.0.2.33 + host_max: 192.0.2.62 + count: 31 + prefix_length: 27 + netmask: 255.255.255.224 + '192.0.2.40/32': + network_address: 192.0.2.40 + broadcast_address: 192.0.2.40 + count: 0 + prefix_length: 32 + '10.100.0.0/17': + network_address: 10.100.0.0 + broadcast_address: 10.100.127.255 + host_min: 10.100.0.1 + host_max: 10.100.127.254 + count: 32767 + prefix_length: 17 + netmask: 255.255.128.0 + '0.0.0.0/0': + network_address: 0.0.0.0 + broadcast_address: 255.255.255.255 + host_min: 0.0.0.1 + host_max: 255.255.255.254 + count: 4294967295 + prefix_length: 0 + netmask: 0.0.0.0 + '10.0.0.0/8': + network_address: 10.0.0.0 + broadcast_address: 10.255.255.255 + host_min: 10.0.0.1 + host_max: 10.255.255.254 + count: 16777215 + prefix_length: 8 + netmask: 255.0.0.0 + '10.0.0.0/9': + network_address: 10.0.0.0 + broadcast_address: 10.127.255.255 + host_min: 10.0.0.1 + host_max: 10.127.255.254 + count: 8388607 + prefix_length: 9 + netmask: 255.128.0.0 + '224.0.0.0/4': + network_address: 224.0.0.0 + broadcast_address: 239.255.255.255 + host_min: 224.0.0.1 + host_max: 239.255.255.254 + count: 268435455 + prefix_length: 4 + netmask: 240.0.0.0 + '192.0.2.4/29': + network_address: 192.0.2.0 + broadcast_address: 192.0.2.7 + host_min: 192.0.2.1 + host_max: 192.0.2.6 + count: 7 + prefix_length: 29 + netmask: 255.255.255.248