hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io/
Other
41.67k stars 9.41k forks source link

Functions to help with creation of reverse dns zones #9404

Open evansj opened 7 years ago

evansj commented 7 years ago

I would like a new function to return the reverse DNS name for a CIDR if a CIDR is passed as the first argument:

"${reversedns("10.1.0.0/16")}" => "1.10.in-addr.arpa"

Also, the same function could return the reverse FQDN if an IP is passed as the first argument:

"${reversedns("10.1.2.3")}" => "3.2.1.10.in-addr.arpa"

and finally it could also return just the name part to be added to an existing reverse zone if two arguments are passed in, the CIDR and the IP:

"${reversedns("10.1.0.0/16", "10.1.2.3")}" => "3.2"

Alternatively the functionality could be spread over more than one function. It should probably support IPv6 as well.

Terraform Version

Terraform v0.7.5

discordianfish commented 7 years ago

Just came across this requirement too. The best I came up with, based on https://liviu.io/2016/terraform-interpolation-dns-ptr-records/ (assume to be in a count: X resource):

  name   = "${format("%s.%s.%s.%s.in-addr.arpa",
      element(split(".", element(
        digitalocean_droplet.satellite.*.ipv4_address, count.index
      )), 3),
      element(split(".", element(
        digitalocean_droplet.satellite.*.ipv4_address, count.index
      )), 2),
      element(split(".", element(
        digitalocean_droplet.satellite.*.ipv4_address, count.index
      )), 1),
      element(split(".", element(
        digitalocean_droplet.satellite.*.ipv4_address, count.index
      )), 0)
    )
  }"

Any ideas to make this nicer? Unfortunately there seems to be no 'reverse' operator to reverse an array. Otherwise something like this would work:

name  = "${format("%s.in-addr.arpa", join(".", reverse(split(".", element(
  digitalocean_droplet.satellite.*.ipv4_address, count.index
)))))}"
evansj commented 7 years ago

@discordianfish I'm using very similar ugly code in my application! In my case I already have a var.internal_reverse_domain containing something like 123.10.in-addr.arpa so I use:

resource "aws_route53_record" "internal-reverse" {
  count   = "${var.count}"
  zone_id = "${var.internal_reverse_zone_id}"
  name    = "${element(split(".", element(aws_instance.node.*.private_ip, count.index)), 3)}.${element(split(".", element(aws_instance.node.*.private_ip, count.index)), 2)}.${var.internal_reverse_domain}"
  type    = "PTR"
  ttl     = "300"
  records = ["${element(aws_route53_record.internal.*.fqdn, count.index)}"]
}

Like you I'm also dealing with multiple nodes and creating an aws_route53_record resource for each of them.

apparentlymart commented 7 years ago

Hi @evansj!

This seems like a fine idea to me. We should probably also think about how (and whether) we would support RFC2317 for delegating CIDR blocks that don't fall on octet boundaries... I think that would require a couple different functions, to compute a name for the NS record and then a list of information to populate the necessary CNAME records.

The latter might be hard for us to do right now due to Terraform's limited support for wrangling lists, but maybe as a middle-ground we could require round-octet CIDR blocks to start but get a sense of what the more-general solution would look like to make sure the middle-ground is compatible with it so we can expand it later.

discordianfish commented 7 years ago

fwiw, I figured out later digitlaocean doesn't support PTR records, the general point still stands :)

Gary-Armstrong commented 6 years ago

I wandered into this today as I am trying to set up reverse lookup in AWS Route53. Going to see about using the above format-element-split soup to create the zone as well as the PTR records.

Gary-Armstrong commented 6 years ago

Defined the reverse zone:

resource "aws_route53_zone" "reverse-private" {
  vpc_id  = "${data.terraform_remote_state.remotestate.vpc-id}"
  comment = "Reverse Private DNS Zone"

  name = "${format("%s.%s.%s.in-addr.arpa.",
      element( split(".", data.terraform_remote_state.remotestate.sn_cidr) ,2),
      element( split(".", data.terraform_remote_state.remotestate.sn_cidr) ,1),
      element( split(".", data.terraform_remote_state.remotestate.sn_cidr) ,0),
    )
  }"
}

Made some records:

resource "aws_route53_record" "reverse-private" {
  count   = "${var.instance-count}"
  zone_id = "${var.rev-zone-id}"

  name = "${format(
    "%s.%s.%s.%s.in-addr.arpa.",
      element( split(".", element(aws_instance.instance.*.private_ip, count.index)) ,3),
      element( split(".", element(aws_instance.instance.*.private_ip, count.index)) ,2),
      element( split(".", element(aws_instance.instance.*.private_ip, count.index)) ,1),
      element( split(".", element(aws_instance.instance.*.private_ip, count.index)) ,0),
    )
  }"

  type    = "PTR"
  ttl     = "600"
  records = ["${var.servername}-${format("%03d", count.index)}-${var.region}"]
}
apparentlymart commented 6 years ago

Over in #16942. @naftulikay made a similar request for IPv6 reverse DNS entries. I'd like to capture those requirements here so that we might try to come up with a solution that works for both, or at least two solutions that are consistent as possible if a single solution is not possible.

The requirements for IPv6 are, per @naftulikay's request:


The difficulty here is that I need to be able to expand a CIDR block like 3731:54:65fe:2::a7/54 into a series of nibbles:

 3731005465fe000200000000000000a7

Then to reverse this series and interpose periods between each nibble:

7.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.0.e.f.5.6.4.5.0.0.1.3.7.3

Then finally to append .ip6.arpa at the end.


This issue was also covering the idea of producing the zone name(s) for a particular CIDR block address, as well as the whole record name for a particular IP address.

The approaches for both share some characteristics that make me hopeful that we could define a single function or pair of functions that would work for both IPv4 and IPv6 addresses, continuing the precedent set for the cidr... family of functions:

For whole addresses, the methodology is to split the address at some boundary, reverse it, and then combine it together into a period-separated DNS heirarchy with a fixed suffix:

A more complex part of the problem is dealing with the reverse DNS delegation zones themselves. Since addresses are not always assigned on the boundaries we use to split addresses, there can be potentially multiple zones associated with a given CIDR block delegation, and then within that block each given address belongs to just one of those zones. For example:

The above principle works similarly for IPv6 ranges, but with the per-nibble granularity instead of per-octet.

So far, I've counted possibly three distinct functions here:

Unfortunately that formulation is not convenient since in many of the hosted DNS services supported by Terraform it's necessary to provide some sort of vendor-specific "zone id" rather than directly the target DNS zone as returned by rdnszone. To model that reasonably with Terraform, I expect you'd want to use the pattern of creating a CNAME for each delegated zone onto a single zone as described in RFC 2317:

16-12.172.in-addr.arpa. IN NS <nameserver-for-aggregate-zone>
16.172.in-addr.arpa. IN CNAME 16-12.172.in-addr.arpa.
17.172.in-addr.arpa. IN CNAME 16-12.172.in-addr.arpa.
; ...
31.172.in-addr.arpa. IN CNAME 16-12.172.in-addr.arpa.

...and then you'd just place all of the individual address records (as returned by rdnshost) into the 16-12.172.in-addr.arpa. zone.

This exception makes things a little trickier, since there are now two cases to deal with:

With some careful design of our functions, we can make both cases expressible with a single Terraform configuration, like this:

# DESIGN SKETCH: not yet implemented and may change during implementation

locals {
  cidr_block = "172.16.0.0/12"
  rdns_delegation_zones = "${rdnszones(local.cidr_block)}"
}

# In practice, this resource would probably be in a separate Terraform config
# that manages the network and its addresses.
resource "aws_route53_zone" "rfc2317" {
  name = "${rdnscidrzone(local.cidr_block)}"
}

# In practice, this resource would probably be in a separate Terraform config
# that manages the network and its addresses.
resource "aws_route53_record" "delegation" {
  count   = "${length(local.rdns_delegation_zones)}"
  zone_id = "${aws_route53_zone.rdns.id}" # (not illustrated here)
  name    = "${local.rdns_delegation_zones[count.index]}"
  type    = "CNAME"
  records = ["${aws_route53_zone.rfc2317.name}"]
}

resource "aws_route53_record" "a" {
  zone_id = "${aws_route53_zone.example_dot_com.id}" # (not illustrated here)
  name    = "private-ec2-instance.example.com" # (EC2 instance not shown here)
  type    = "A"
  records = ["${aws_instance.example.private_ip}"]
}

resource "aws_route53_record" "rdns" {
  zone_id = "${aws_route53_zone.rfc2317.id}"
  name    = "${rdnshost(aws_instance.example.private_ip)}" # (EC2 instance not shown here)
  type    = "PTR"
  records = ["${aws_route53_record.a.name}"]
}

The above includes a new function, and a new behavior for one of the functions we already defined:

Although I used IPv4 prefixes and addresses throughout my examples above, I believe all of these functions can apply equally to both IPv4 and IPv6 and get the right results as long as the user stays consistently within one addressing scheme across all the functions.


I'm curious as to what those in this discussion think of the above design idea. I expect some more iteration is possible to improve this, so if you have some real-world examples that would be difficult or impossible to express with the above then please share them!

The Terraform team at HashiCorp is currently focused on more general configuration language improvements and so won't be able to work on these functions for now, but it'd still be good to try to get a good design pinned down so that this can eventually be implemented. We'd be open to reviewing community PRs in the mean time, but given the number of moving parts here I think it's best to come to consensus on an approach in the discussion here first, before moving to implementation.

oogali commented 6 years ago

@apparentlymart

Given the address 172.18.0.1 we need to know both that its reverse-DNS record name is 1.0.18.172.in-addr.arpa and that it belongs to 18.172.in-addr.arpa.

I don't think you need to know what zone it belongs to. It should be the caller's responsibility needs to know whether it belongs to 0.18.172.in-addr.arpa or 18.172.in-addr.arpa, as that can vary from environment to environment.

For the 172.16.0.0/12 example, my experience in the past has been DNS administrators generally slice those up into individual /24s and create zones or delegations based on that (e.g. 0.16.172.in-addr.arpa, 1.16.172.in-addr.arpa, 2.16.172.in-addr.arpa, etc).

For subnets smaller than /24 (less than 256 addresses), this is where one would usually employ an RFC2317 implementation. This bit is called out in section 5 of the RFC.

What the RFC lays out is for the DNS administrator to:

(The requirement to create a CNAME for each address is why it's onerous to use this method for larger subnets such as /12s)

As an example, you have a larger aggregate block of 192.168.0.0/24. Within that, you have two subnets assigned to two different customers, and are sized as /25 and /26 respectively.

With RFC2317, what your DNS zone should resemble is:

...
0.168.192.in-addr.arpa. IN NS ns1.apparentlymart.com.

; First customer who is assigned 192.168.0.0/25
0/25.168.192.in-addr.arpa. IN NS ns1.customer1.com.

0.0.168.192.in-addr.arpa. IN CNAME 0.0/25.168.192.in-addr.arpa.
1.0.168.192.in-addr.arpa. IN CNAME 1.0/25.168.192.in-addr.arpa.
...
127.0.168.192.in-addr.arpa. IN CNAME 127.0/25.168.192.in-addr.arpa.

; Second customer who is assigned 192.168.0.128/26
128/26.168.192.in-addr.arpa. IN NS a.ns.customer2.com.
128/26.168.192.in-addr.arpa. IN NS b.ns.customer2.com.

128.0.168.192.in-addr.arpa. IN CNAME 128.128/26.168.192.in-addr.arpa.
129.0.168.192.in-addr.arpa. IN CNAME 129.128/26.168.192.in-addr.arpa.
...
191.0.168.192.in-addr.arpa. IN CNAME 191.128/26.168.192.in-addr.arpa.

Now for an additional wrench in RFC2317... what's laid out there is meant as a suggestion. In my earlier days working at an ISP, I deviated and did delegations directly into the customer's domain zone for convenience sake.

...
0.168.192.in-addr.arpa. IN NS ns1.apparentlymart.com.

0.0.168.192.in-addr.arpa. IN CNAME 192.168.0.0.oogali.tld.
1.0.168.192.in-addr.arpa. IN CNAME 192.168.0.1.oogali.tld.
2.0.168.192.in-addr.arpa. IN CNAME 192.168.0.2.oogali.tld.
...so on and so forth...

And in the customer zone, the "convenience" was not needing a separate zone, and not needing to mentally reverse IP addresses:

...
oogali.tld. IN NS ns1.oogali.tld.

192.168.0.0.oogali.tld. IN PTR reverse.dns.for.a.server.oogali.tld.

Because different people will have different RFC2317 implementations, I don't think you really want to factor RFC2317 too much into your decisioning as it'd make for a messy/painful implementation that is striving to be generic.

I think a reasonable compromise for networks smaller than /24 is for one to use a to-be-designed helper function that takes a single IP address as input, and returns its full reverse DNS name (192.168.0.1 --> 1.0.168.192.in-addr.arpa), and leave it up to the caller to use Terraform's string manipulation functions to change it into whatever is needed (e.g. replace("0.168.192", "0.0/25.168.192")).

vanDonselaar commented 6 years ago

I am using the following script combined with an external data source for reverse ipv6 DNS.

#!/usr/bin/env bash

set -o errexit # exit when a command fails
set -o nounset # exit when using undeclared variables.
set -o pipefail # exit status of the last command that threw a non-zero exit code returns

# jq will ensure that the values are properly quoted
# and escaped for consumption by the shell.
eval "$(jq -r '@sh "IP=\(.ipv6)"')"

# retrieved from https://gist.github.com/lsowen/4447d916fd19cbb7fce4
function reverseIp6 {
    echo "$1" | awk -F: 'BEGIN {OFS=""; }{addCount = 9 - NF; for(i=1; i<=NF;i++){if(length($i) == 0){ for(j=1;j<=addCount;j++){$i = ($i "0000");} } else { $i = substr(("0000" $i), length($i)+5-4);}}; print}' | rev | sed -e "s/./&./g"
}

REVERSE=$(reverseIp6 $IP)"ip6.arpa."

jq -n --arg reverse "$REVERSE" '{"reversed":$reverse}'
data "external" "my_reverse_script" {
    program = ["bash", "${path.module}/files/reverse_ipv6.sh"]

    query = {
        ipv6 = "${aws_instance.my_instance.ipv6_addresses[0]}"
    }
}
resource "aws_route53_record" "my_reverse_record" {
    zone_id = "${aws_route53_zone.my_reverse_zone.id}"
    type = "PTR"
    ttl = 3600
    name = "${data.external.my_reverse_script.result.reversed}"
    records = ["${format("%s.example.com", aws_instance.my_instance.tags.Name)}"]
}

Make sure you have bash and jq installed.

JackBracken commented 5 years ago

I started work on at least adding an rdnshost function as I think that at least makes sense to have. I don't know whether appending .ip6.arpa./.in-addr.arpa. or not is completely correct--I have for now but that's easily changed.

I'm not the most knowledgable about networking so don't feel qualified to weigh in on the discussion about how rest of the API should look or work, but happy enough to help where I can.

LichtiMi commented 5 months ago

There past a while in the meantime. It can be done with terraform built in functions:

format( "%s.in-addr.arpa.", join( ".", reverse( split( ".",var.ipaddr ) ) )

zelch commented 5 months ago

@LichtiMi That's very easy for IPv4 addresses.

It's much more difficult for IPv6 addresses.

LichtiMi commented 5 months ago

@zelch: So true... :)

crw commented 4 months ago

Thank you for your continued interest in this issue.

Terraform version 1.8 launches with support of provider-defined functions. It is now possible to implement your own functions! We would love to see this implemented as a provider-defined function.

Please see the provider-defined functions documentation to learn how to implement functions in your providers. If you are new to provider development, learn how to create a new provider with the Terraform Plugin Framework. If you have any questions, please visit the Terraform Plugin Development category in our official forum.

We hope this feature unblocks future function development and provides more flexibility for the Terraform community. Thank you for your continued support of Terraform!

apparentlymart commented 1 month ago

This seems like a plausible addition to the hashicorp/dns provider, now that such a thing is possible.