How to filter out ip4 and ip6 subnets

I have a case where I get a list of ip subnets. Some are ip4 subnets some are ip6. Anyone know a way in terraform to format/filter the addresses into two separate lists? Ansible has ip4() and ip6() functions. I see no equivalent in terraform. Anyone have a trick for doing this?

Hi @mjdouble,

If by “IP subnets” you mean CIDR prefixes, like 192.168.0.0/16 or the IPv6 equivalent, there isn’t a direct way to do that with Terraform but I can think of one strategy to get the result you want by using some Terraform features a little outside of their intended use-cases.

The cidrnetmask function is defined to work only with IPv4 addresses, because the “netmask” notation is not defined for IPv6 (which requires CIDR notation).

The can function is a special function that returns true if its given expression can be evaluated without error, or false if it produces an error.

By combining those two together, we can potentially define “IPv4 address” as “any address that cidrnetmask will accept”, and conversely “IPv6 address” as “any address that cidrnetmask rejects”. For example:

locals {
  all_subnets  = ["10.0.0.0/8", "2001:db8::/48"]
  ipv4_subnets = [
    for cidr in local.all_subnets : cidr
    if can(cidrnetmask(cidr))
  ]
  ipv6_subnets = [
    for cidr in local.all_subnets : cidr
    if !can(cidrnetmask(cidr))
  ]
}

One caveat with this approximation is that any string that is neither a valid IPv4 nor an IPv6 CIDR prefix would be classified as an IPv6 prefix, because cidrnetmask would also fail in that case.

@apparentlymart I don’t think the documentation is correct. At least not for Terraform v0.14.5. When I run cidrnetmask on ipv6 address it is returning an appropriate Ipv6 subnet mask

cidrnetmask("2a0c:93c0:8022:300::/64")
> ffff:ffff:ffff:ffff::

Oh, interesting! That isn’t intended to work, so it seems like there was a regression in the error handling at some point.

In that case, I suppose it’ll be necessary to resort to some more direct testing to recognize the differences between these two formats, such as testing whether the string contains at least one colon to recognize it as IPv6. Not the most ideal solution by any means, but should work as long as you can rely on your inputs always being valid as either IPv4 or IPv6 ranges.

Coming in late, but compact([for x in [list]: can(regex("::", x)) ? "" : x]) will filter out IPV6 addresses.

Example:

data "github_ip_ranges" "whitelist" {}

locals {
 ipv4_subnets = compact([for x in data.github_ip_ranges.whitelist.actions : can(regex("::", x)) ? "" : x])
}

This should be even shorter and does not rely on compact:

locals {
  # Some ip ranges
  ip_ranges = []

  ipv4_subnets = [for cidr in local.ip_ranges : cidr if ! can(regex("::", cidr)) ]
  ipv6_subnets = [for cidr in local.ip_ranges : cidr if can(regex("::", cidr)) ]
}

I love this can(regex()) idiom, but would suggest matching a single colon instead of a double colon – just in case you encounter an IPv6 address like 2001:db8:123:45:6:789:abc:de which does not contain any zero-valued hextets.

(Granted, that’s unlikely to happen for subnets per se, but if you’re separating ipv4 from ipv6 cidrs for a security group rule, there could be /128s in there)