├── .gitignore ├── README.md ├── cloudformation-template.json ├── ec2_cache.go ├── ec2_server.go ├── main.go └── upstart └── aws-name-server.conf /.gitignore: -------------------------------------------------------------------------------- 1 | /aws-name-server 2 | /aws-name-server-linux 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A DNS server that serves up your ec2 instances by name. 2 | 3 | Usage 4 | ===== 5 | 6 | ``` 7 | aws-name-server --domain aws.bugsnag.com \ 8 | --aws-region us-east-1 \ 9 | --aws-access-key-id \ 10 | --aws-secret-access-key 11 | ``` 12 | 13 | This will serve up DNS records for the following: 14 | 15 | * `.aws.bugsnag.com` all your EC2 instances tagged with Name=<name> 16 | * `..aws.bugsnag.com` the nth instances tagged with Name=<name> 17 | * `.role.aws.bugsnag.com` all your EC2 instances tagged with Role=<role> 18 | * `..role.aws.bugsnag.com` the nth instances tagged with Role=<role> 19 | * `.aws.bugsnag.com` all your EC2 instances by instance id. 20 | * `..aws.bugsnag.com` all your EC2 instances by instance id. 21 | 22 | It uses CNAMEs so that instances will resolve to internal IP addresses if you query from inside AWS, 23 | and external IP addresses if you query from the outside. 24 | 25 | Quick start 26 | =========== 27 | 28 | There's a long-winded [Setup guide](#setup), but if you already know your way 29 | around EC2, you'll need to: 30 | 31 | 1. Open up port 53 (UDP and TCP) on your security group. 32 | 2. Boot an instance with an IAM Role with `ec2:DescribeInstances` permission. (or use an IAM user and 33 | configure `aws-name-server` manually). 34 | 3. Install `aws-name-server`. 35 | 4. Setup your NS records correctly. 36 | 37 | Parameters 38 | ========== 39 | 40 | ### `--domain` 41 | 42 | This is the domain you wish to serve. i.e. `aws.example.com`. It is the 43 | only required parameter. 44 | 45 | ### `--hostname` 46 | 47 | The publically resolvable hostname of the current machine. This defaults 48 | sensibly, so you only need to set this if you see a warning in the logs. 49 | 50 | ### `--aws-access-key-id` and `--aws-secret-access-key` 51 | 52 | An Amazon key pair with permission to run `ec2:DescribeInstances`. This defaults to 53 | the IAM role of the machine running `aws-name-server` or to the values of the environment 54 | variables `$AWS_ACCESS_KEY_ID` and `$AWS_SECRET_ACCESS_KEY` (or `$AWS_ACCESS_KEY` and `$AWS_SECRET_KEY`). 55 | 56 | ### `--aws-region` 57 | 58 | This defaults to the region in which `aws-name-server` is running, or `us-east-1`. 59 | 60 | Setup 61 | ===== 62 | 63 | These instructions assume you're going to launch a new EC2 instance to run 64 | `aws-name-server`. If you want to run it on an existing server, adapt the 65 | instructions to suit. 66 | 67 | ### 1. Create an IAM role 68 | 69 | [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) 70 | let you give EC2 instances permission to access the AWS API. We will need our 71 | dns machine to run `ec2:DescribeInstances`. 72 | 73 | 1. Log into the AWS web console and navigate to IAM. 74 | 2. Create a new role called *iam-role-aws-name-server* 75 | 3. Select the *Amazon EC2* role type. 76 | 4. Create a *Custom Policy* called *describe-instances-only* with the content: 77 | 78 | ``` 79 | { 80 | "Version": "2012-10-17", 81 | "Statement": [{ 82 | "Action": ["ec2:DescribeInstances"], 83 | "Effect": "Allow", 84 | "Resource": "*" 85 | }] 86 | } 87 | ``` 88 | 89 | ### 2. Create a security group 90 | 91 | [Security groups](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html) 92 | describe what traffic is allowed to get to your instance. DNS servers use UDP port 53 and TCP port 53. 93 | 94 | 1. Log into the AWS web console and navigate to EC2. 95 | 2. Create a new security group called *aws-name-server* 96 | 3. Configure it to have: 97 | 98 | ``` 99 | # Type # Protocol # Port # Source 100 | SSH TCP 22 My IP x.x.x.x/32 101 | DNS UDP 53 Anywhere 0.0.0.0/0 102 | Custom TCP 53 Anywhere 0.0.0.0/0 103 | ``` 104 | 105 | This will let you ssh in to the DNS server, and let anyone run DNS queries. 106 | 107 | ### 3. Launch an instance 108 | 109 | I recommend running 64bit HVM-based EBS-backed Ubuntu 14.04 on a `t2.micro` 110 | ([ami-acff23c4](https://console.aws.amazon.com/ec2/home?region=us-east-1#launchAmi=ami-acff23c4)). You 111 | can use whatever distro you like the most. 112 | 113 | 1. Log into the AWS web console and navigate to EC2. 114 | 2. Click "Launch Instance" 115 | 3. Select your favourite AMI (e.g. *ami-acff23c4*). 116 | 3. Select your favourite cheap instance type (e.g. *t2.micro*) (If you don't have VPCs yet, choose *t1.micro* instead) 117 | 4. Set IAM role to *iam-role-aws-name-server* 118 | 5. Skip through disks (the default is fine) 119 | 6. Skip through tags (though if you set Name=dns1 and Role=dns you can test the server :) 120 | 7. Select an existing security group `sg-aws-name-server`. 121 | 8. Launch! 122 | 123 | ### 4. Install the binary 124 | 125 | 1. Download the [latest version](http://gobuild.io/download/github.com/ConradIrwin/aws-name-server/master). 126 | 127 | ``` 128 | wget http://gobuild.io/github.com/ConradIrwin/aws-name-server/master/linux/amd64 -O aws-name-server.zip 129 | unzip aws-name-server.zip 130 | ``` 131 | 132 | 2. Move the binary into /usr/bin. 133 | 134 | ``` 135 | sudo cp aws-name-server /usr/bin 136 | sudo chmod +x /usr/bin/aws-name-server 137 | ``` 138 | 139 | 3. (optional) Set the capabilities of aws-name-server so it doesn't need to run as root. 140 | 141 | ``` 142 | # the cap_net_bind_service capability allows this program to bind to ports below 1024 143 | # when it us run as a non-root user. 144 | sudo setcap cap_net_bind_service=+ep /usr/bin/aws-name-server 145 | ``` 146 | 147 | ### 5. Configure upstart. 148 | 149 | If you use upstart (the default process manager under ubuntu) you can use the provided upstart 150 | script. You'll need to change the script to reflect your hostname: 151 | 152 | 1. Open upstart/aws-name-server.conf and change --domain=internal to --domain <your-domain> 153 | 2. `sudo cp upstart/aws-name-server.conf /etc/init/` 154 | 3. `sudo initctl start aws-name-server` 155 | 156 | ### 6. Configure NS Records 157 | 158 | To add your DNS server into the global DNS tree, you need to add an `NS` record 159 | from the parent domain to your new server. 160 | 161 | Let's say you currently have DNS for `example.com`, and you're running 162 | `aws-name-server` on the machine `ec2-12-34-56-78.compute-1.amazonaws.com`. In 163 | the admin page for `example.com`s DNS add a new record of the form: 164 | 165 | ``` 166 | # name # ttl # value 167 | aws.example.com 300 IN NS ec2-12-34-56-78.compute-1.amazonaws.com 168 | ``` 169 | 170 | The TTL can be whatever you want, I like 5 minutes because it's not too long to wait if I make a mistake. 171 | 172 | The value should be a hostname for your server that is directly resolvable (i.e. not a CNAME). The public 173 | hostnames that Amazon gives instances are perfect for this. 174 | 175 | Troubleshooting 176 | =============== 177 | 178 | There's a lot that can go wrong, so troubleshooting takes a while. 179 | 180 | ### Did it start? 181 | 182 | First try looking in the logs (`/var/log/upstart/aws-name-server.log` if you're 183 | using upstart). If there's nothing there, then try `/var/log/syslog`. 184 | 185 | ### Is it running? 186 | Try running `dig dns1.aws.example.com @localhost` while ssh'd into the machine. 187 | It should return a `CNAME` record. If not, look in the logs, the chances are 188 | the DNS server is not running. This happens if your EC2 credentials are wrong. 189 | 190 | ### Is the security group configured correctly? 191 | Assuming you can make DNS lookups to localhost, try running 192 | `dig dns1.aws.example.com @ec2-12-34-56-78.compute-1.amazonaws.com` from your 193 | laptop. If you don't get a reply, double check the security group config. 194 | 195 | ### Are the NS records set up correctly? 196 | Assuming you can make DNS lookups correctly when pointing dig at the DNS 197 | server, try running `dig NS aws.example.com`. If this doesn't return anything, 198 | you probably need to update your `NS` records. If you've already done this, you 199 | might need to wait a few minutes for caches to clear. 200 | 201 | ### Are you getting a warning about NS records in the logs but everything seems fine? 202 | This happens when the `--hostname` parameter has been set or auto-detected to 203 | something different from what you've configured the `NS` records to be. This 204 | may cause hard-to-debug issues, so you should set `--hostname` correctly. 205 | -------------------------------------------------------------------------------- /cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Runs an instance of aws-name-server", 4 | "Parameters": { 5 | "InstanceType": { 6 | "Description": "EC2 instance type", 7 | "Type": "String", 8 | "Default": "t2.micro" 9 | }, 10 | "KeyName": { 11 | "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instances", 12 | "Type": "String" 13 | }, 14 | "Ami": { 15 | "Description": "The EC2 AMI (64bit HVM-based EBS-backed Ubuntu 14.04 recommended)", 16 | "Type": "String", 17 | "Default": "ami-acff23c4" 18 | }, 19 | "DnsPrefix": { 20 | "Description": "Prefix for the NS record (.)", 21 | "Type": "String", 22 | "Default": "aws" 23 | }, 24 | "DnsZone": { 25 | "Description": "Route53-hosted zone to use for the NS record (.)", 26 | "Type": "String" 27 | }, 28 | "SubnetId": { 29 | "Description": "The Subnet ID for the instance", 30 | "Type": "String" 31 | }, 32 | "VpcId": { 33 | "Description": "VPC associated with the provided subnet", 34 | "Type": "String" 35 | }, 36 | "AdminSecurityGroup": { 37 | "Description": "Existing security group that should be granted administrative access (e.g., 'sg-123456')", 38 | "Type": "String" 39 | }, 40 | "GoPackage": { 41 | "Description": "The go package to build", 42 | "Type": "String", 43 | "Default": "github.com/ConradIrwin/aws-name-server" 44 | } 45 | }, 46 | "Resources": { 47 | "Role": { 48 | "Type": "AWS::IAM::Role", 49 | "Properties": { 50 | "AssumeRolePolicyDocument": { 51 | "Statement": [ 52 | { 53 | "Effect": "Allow", 54 | "Principal": { 55 | "Service": [ 56 | "ec2.amazonaws.com" 57 | ] 58 | }, 59 | "Action": [ 60 | "sts:AssumeRole" 61 | ] 62 | } 63 | ] 64 | }, 65 | "Path": "/", 66 | "Policies": [ 67 | { 68 | "PolicyName": "Ec2DescribeInstances", 69 | "PolicyDocument": { 70 | "Statement": [ 71 | { 72 | "Effect": "Allow", 73 | "Action": "ec2:DescribeInstances", 74 | "Resource": "*" 75 | } 76 | ] 77 | } 78 | } 79 | ] 80 | } 81 | }, 82 | "InstanceProfile": { 83 | "Type": "AWS::IAM::InstanceProfile", 84 | "Properties": { 85 | "Path": "/", 86 | "Roles": [ 87 | { 88 | "Ref": "Role" 89 | } 90 | ] 91 | } 92 | }, 93 | "NameServer": { 94 | "Type": "AWS::EC2::Instance", 95 | "Properties": { 96 | "KeyName": { 97 | "Ref": "KeyName" 98 | }, 99 | "ImageId": { 100 | "Ref": "Ami" 101 | }, 102 | "IamInstanceProfile": { 103 | "Ref": "InstanceProfile" 104 | }, 105 | "InstanceType": { 106 | "Ref": "InstanceType" 107 | }, 108 | "NetworkInterfaces": [ 109 | { 110 | "AssociatePublicIpAddress": "true", 111 | "DeviceIndex": "0", 112 | "SubnetId": { 113 | "Ref": "SubnetId" 114 | }, 115 | "GroupSet": [ 116 | { 117 | "Ref": "ServerSecurityGroup" 118 | }, 119 | { 120 | "Ref": "AdminSecurityGroup" 121 | } 122 | ] 123 | } 124 | ], 125 | "UserData": { 126 | "Fn::Base64": { 127 | "Fn::Join": [ 128 | "", 129 | [ 130 | "#!/bin/bash -ex\n", 131 | "apt-get update\n", 132 | "apt-get install -y golang git unattended-upgrades\n", 133 | "# Automatic security updates\n", 134 | "cat > /etc/apt/apt.conf.d/20auto-upgrades < /etc/init/aws-name-server.conf\n", 142 | "# upstart script for aws-name-server\n", 143 | "description \"AWS Name Server\"\n\n", 144 | "start on filesystem or runlevel [2345]\n", 145 | "stop on runlevel [!2345]\n\n", 146 | "respawn\n", 147 | "respawn limit 10 5\n", 148 | "setuid nobody\n", 149 | "setgid nogroup\n\n", 150 | "exec /usr/local/bin/aws-name-server --domain ", { "Ref": "DnsPrefix" }, ".", { "Ref": "DnsZone" }, 151 | "\nEOF\n\n", 152 | "initctl start aws-name-server\n" 153 | ] 154 | ] 155 | } 156 | } 157 | } 158 | }, 159 | "ServerSecurityGroup": { 160 | "Type": "AWS::EC2::SecurityGroup", 161 | "Properties": { 162 | "GroupDescription": "Enable SSH and Registry access", 163 | "VpcId": { 164 | "Ref": "VpcId" 165 | }, 166 | "SecurityGroupIngress": [ 167 | { 168 | "IpProtocol": "tcp", 169 | "FromPort": "53", 170 | "ToPort": "53", 171 | "CidrIp": "0.0.0.0/0" 172 | }, 173 | { 174 | "IpProtocol": "udp", 175 | "FromPort": "53", 176 | "ToPort": "53", 177 | "CidrIp": "0.0.0.0/0" 178 | } 179 | ] 180 | } 181 | }, 182 | "DnsRecord": { 183 | "Type": "AWS::Route53::RecordSet", 184 | "Properties": { 185 | "HostedZoneName": { 186 | "Fn::Join": [ 187 | "", 188 | [ 189 | { 190 | "Ref": "DnsZone" 191 | }, 192 | "." 193 | ] 194 | ] 195 | }, 196 | "Comment": "AWS Name Server", 197 | "Name": { 198 | "Fn::Join": [ 199 | "", 200 | [ 201 | { 202 | "Ref": "DnsPrefix" 203 | }, 204 | ".", 205 | { 206 | "Ref": "DnsZone" 207 | }, 208 | "." 209 | ] 210 | ] 211 | }, 212 | "Type": "NS", 213 | "TTL": "300", 214 | "ResourceRecords": [ 215 | { 216 | "Fn::GetAtt": [ 217 | "NameServer", 218 | "PublicDnsName" 219 | ] 220 | } 221 | ] 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /ec2_cache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/mitchellh/goamz/aws" 6 | "github.com/mitchellh/goamz/ec2" 7 | "log" 8 | "net" 9 | "regexp" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // The length of time to cache the results of ec2-describe-instances. 16 | // This value is exposed as the TTL of the DNS record (down to a minimum 17 | // of 10 seconds). 18 | const TTL = 5 * time.Minute 19 | 20 | // LookupTag represents the type of tag we're caching by. 21 | type LookupTag uint8 22 | 23 | const ( 24 | // LOOKUP_NAME for when tag:Name= 25 | LOOKUP_NAME LookupTag = iota 26 | // LOOKUP_ROLE for when tag:Role= 27 | LOOKUP_ROLE 28 | ) 29 | 30 | // Key is used to cache results in O(1) lookup structures. 31 | type Key struct { 32 | LookupTag 33 | string 34 | } 35 | 36 | // Record represents the DNS record for one EC2 instance. 37 | type Record struct { 38 | CName string 39 | PublicIP net.IP 40 | PrivateIP net.IP 41 | ValidUntil time.Time 42 | } 43 | 44 | // EC2Cache maintains a local cache of ec2-describe-instances data. 45 | // It refreshes every TTL. 46 | type EC2Cache struct { 47 | region aws.Region 48 | accessKey string 49 | secretKey string 50 | records map[Key][]*Record 51 | mutex sync.RWMutex 52 | } 53 | 54 | // NewEC2Cache creates a new EC2Cache that uses the provided 55 | // EC2 client to lookup instances. It starts a goroutine that 56 | // keeps the cache up-to-date. 57 | func NewEC2Cache(regionName, accessKey, secretKey string) (*EC2Cache, error) { 58 | 59 | region, ok := aws.Regions[regionName] 60 | if !ok { 61 | return nil, fmt.Errorf("unknown AWS region: %s", regionName) 62 | } 63 | 64 | cache := &EC2Cache{ 65 | region: region, 66 | accessKey: accessKey, 67 | secretKey: secretKey, 68 | records: make(map[Key][]*Record), 69 | } 70 | 71 | if err := cache.refresh(); err != nil { 72 | return nil, err 73 | } 74 | 75 | go func() { 76 | for _ = range time.Tick(1 * time.Minute) { 77 | err := cache.refresh() 78 | if err != nil { 79 | log.Println("ERROR: " + err.Error()) 80 | } 81 | } 82 | }() 83 | 84 | return cache, nil 85 | } 86 | 87 | // setRecords updates the cache with a new set of Records 88 | func (cache *EC2Cache) setRecords(records map[Key][]*Record) { 89 | cache.mutex.Lock() 90 | defer cache.mutex.Unlock() 91 | cache.records = records 92 | } 93 | 94 | func (cache *EC2Cache) Instances() (*ec2.InstancesResp, error) { 95 | auth, err := aws.GetAuth(cache.accessKey, cache.secretKey) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | return ec2.New(auth, cache.region).Instances(nil, nil) 101 | } 102 | 103 | // allow _ in DNS name 104 | var SANE_DNS_NAME = regexp.MustCompile("^[\\w-]+$") 105 | var SANE_DNS_REPL = regexp.MustCompile("[^\\w-]+") 106 | 107 | func sanitize(tag string) string { 108 | out := strings.ToLower(tag) 109 | if SANE_DNS_NAME.MatchString(out) { 110 | return out 111 | } 112 | return SANE_DNS_REPL.ReplaceAllString(out, "-") 113 | } 114 | 115 | func (cache *EC2Cache) refresh() error { 116 | result, err := cache.Instances() 117 | validUntil := time.Now().Add(TTL) 118 | 119 | if err != nil { 120 | return err 121 | } 122 | 123 | records := make(map[Key][]*Record) 124 | count := 0 125 | 126 | for _, reservation := range result.Reservations { 127 | for _, instance := range reservation.Instances { 128 | count++ 129 | record := Record{} 130 | if instance.PublicIpAddress != "" { 131 | record.PublicIP = net.ParseIP(instance.PublicIpAddress) 132 | } 133 | if instance.PrivateIpAddress != "" { 134 | record.PrivateIP = net.ParseIP(instance.PrivateIpAddress) 135 | } 136 | if instance.DNSName != "" { 137 | record.CName = instance.DNSName + "." 138 | } 139 | record.ValidUntil = validUntil 140 | for _, tag := range instance.Tags { 141 | if tag.Key == "Name" { 142 | name := sanitize(tag.Value) 143 | records[Key{LOOKUP_NAME, name}] = append(records[Key{LOOKUP_NAME, name}], &record) 144 | } 145 | if tag.Key == "Role" { 146 | role := sanitize(tag.Value) 147 | records[Key{LOOKUP_ROLE, role}] = append(records[Key{LOOKUP_ROLE, role}], &record) 148 | } 149 | } 150 | 151 | // Lookup servers by instance id 152 | records[Key{LOOKUP_NAME, instance.InstanceId}] = append(records[Key{LOOKUP_NAME, instance.InstanceId}], &record) 153 | 154 | } 155 | } 156 | cache.setRecords(records) 157 | return nil 158 | } 159 | 160 | // Lookup a node in the Cache either by Name or Role. 161 | func (cache *EC2Cache) Lookup(tag LookupTag, value string) []*Record { 162 | cache.mutex.RLock() 163 | defer cache.mutex.RUnlock() 164 | 165 | return cache.records[Key{tag, value}] 166 | } 167 | 168 | func (cache *EC2Cache) Size() int { 169 | cache.mutex.RLock() 170 | defer cache.mutex.RUnlock() 171 | 172 | return len(cache.records) 173 | } 174 | 175 | func (record *Record) TTL(now time.Time) time.Duration { 176 | if now.After(record.ValidUntil) { 177 | return 10 * time.Second 178 | } 179 | return record.ValidUntil.Sub(now) 180 | } 181 | -------------------------------------------------------------------------------- /ec2_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "log" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type EC2Server struct { 12 | domain string 13 | hostname string 14 | cache *EC2Cache 15 | } 16 | 17 | type response struct { 18 | *dns.Msg 19 | } 20 | 21 | func NewEC2Server(domain string, hostname string, cache *EC2Cache) *EC2Server { 22 | 23 | if !strings.HasSuffix(domain, ".") { 24 | domain += "." 25 | } 26 | if !strings.HasSuffix(hostname, ".") { 27 | hostname += "." 28 | } 29 | 30 | server := &EC2Server{ 31 | domain: domain, 32 | hostname: hostname, 33 | cache: cache, 34 | } 35 | 36 | dns.HandleFunc(server.domain, server.handleRequest) 37 | 38 | return server 39 | } 40 | 41 | func (s *EC2Server) listenAndServe(port string, net string) { 42 | server := &dns.Server{Addr: port, Net: net} 43 | if err := server.ListenAndServe(); err != nil { 44 | if strings.Contains(err.Error(), "permission denied") { 45 | log.Printf(CAPABILITIES) 46 | } 47 | log.Fatalf("%s", err) 48 | } 49 | } 50 | 51 | func (s *EC2Server) handleRequest(w dns.ResponseWriter, request *dns.Msg) { 52 | r := new(dns.Msg) 53 | r.SetReply(request) 54 | r.Authoritative = true 55 | 56 | for _, msg := range request.Question { 57 | log.Printf("%v %#v %v (id=%v)", dns.TypeToString[msg.Qtype], msg.Name, w.RemoteAddr(), request.Id) 58 | 59 | answers := s.Answer(msg) 60 | if len(answers) > 0 { 61 | r.Answer = append(r.Answer, answers...) 62 | 63 | } else { 64 | r.Ns = append(r.Ns, s.SOA(msg)) 65 | } 66 | } 67 | 68 | w.WriteMsg(r) 69 | } 70 | 71 | func (s *EC2Server) Answer(msg dns.Question) (answers []dns.RR) { 72 | 73 | if msg.Qtype == dns.TypeNS { 74 | if msg.Name == s.domain { 75 | answers = append(answers, &dns.NS{ 76 | Hdr: dns.RR_Header{Name: msg.Name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300}, 77 | Ns: s.hostname, 78 | }) 79 | } 80 | return answers 81 | } 82 | 83 | if msg.Qtype == dns.TypeSOA { 84 | if msg.Name == s.domain { 85 | answers = append(answers, s.SOA(msg)) 86 | } 87 | return answers 88 | } 89 | 90 | for _, record := range s.Lookup(msg) { 91 | ttl := uint32(record.TTL(time.Now()) / time.Second) 92 | 93 | if record.CName != "" { 94 | answers = append(answers, &dns.CNAME{ 95 | Hdr: dns.RR_Header{Name: msg.Name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: ttl}, 96 | Target: record.CName, 97 | }) 98 | } else if msg.Qtype == dns.TypeA { 99 | 100 | if record.PublicIP != nil { 101 | answers = append(answers, &dns.A{ 102 | Hdr: dns.RR_Header{Name: msg.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: ttl}, 103 | A: record.PublicIP, 104 | }) 105 | } else { 106 | answers = append(answers, &dns.A{ 107 | Hdr: dns.RR_Header{Name: msg.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: ttl}, 108 | A: record.PrivateIP, 109 | }) 110 | } 111 | } 112 | } 113 | 114 | return answers 115 | } 116 | 117 | func (s *EC2Server) Lookup(msg dns.Question) []*Record { 118 | parts := strings.Split(strings.TrimSuffix(msg.Name, "."+s.domain), ".") 119 | 120 | nth := 0 121 | tag := LOOKUP_NAME 122 | 123 | // handle role lookup, e.g. web.role.internal 124 | if len(parts) > 1 { 125 | if parts[len(parts)-1] == "role" { 126 | tag = LOOKUP_ROLE 127 | parts = parts[:len(parts)-1] 128 | } 129 | } 130 | 131 | // handle nth lookup, e.g. 1.web.internal 132 | if len(parts) > 1 { 133 | if i, err := strconv.Atoi(parts[0]); err == nil && i > 0 { 134 | nth = i 135 | parts = parts[1:] 136 | } 137 | } 138 | 139 | if len(parts) != 1 || parts[0] == "" { 140 | log.Printf("ERROR: badly formed: %s %#v", msg.Name, parts) 141 | return nil 142 | } 143 | 144 | results := s.cache.Lookup(tag, parts[0]) 145 | 146 | if nth != 0 { 147 | if nth > len(results) { 148 | results = results[0:0] 149 | } else { 150 | results = results[nth-1 : nth] 151 | } 152 | } 153 | 154 | return results 155 | } 156 | 157 | func (s *EC2Server) SOA(msg dns.Question) dns.RR { 158 | return &dns.SOA{ 159 | Hdr: dns.RR_Header{Name: s.domain, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 60}, 160 | Ns: s.hostname, 161 | Mbox: "me.cirw.in.", 162 | Serial: uint32(time.Now().Unix()), 163 | Refresh: 86400, 164 | Retry: 7200, 165 | Expire: 86400, 166 | Minttl: 60, 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "time" 10 | 11 | "github.com/mitchellh/goamz/aws" 12 | ) 13 | 14 | const USAGE = `Usage: aws-name-server --domain 15 | [ --hostname 16 | --aws-region us-east-1 17 | --aws-access-key-id 18 | --aws-secret-access-key ] 19 | 20 | aws-name-server --domain internal.example.com will serve DNS requests for: 21 | 22 | .internal.example.com — all ec2 instances tagged with Name= 23 | .role.internal.example.com — all ec2 instances tagged with Role= 24 | ..internal.example.com — th instance tagged with Name= 25 | ..role.internal.example.com — th instance tagged with Role= 26 | 27 | For more details see https://github.com/ConradIrwin/aws-name-server` 28 | 29 | const CAPABILITIES = `FATAL 30 | 31 | You need to give this program permission to bind to port 53. 32 | 33 | Using capabilities (recommended): 34 | $ sudo setcap cap_net_bind_service=+ep "$(which aws-name-server)" 35 | 36 | Just run it as root (not recommended): 37 | $ sudo aws-name-server 38 | 39 | ` 40 | 41 | func main() { 42 | domain := flag.String("domain", "", "the domain heirarchy to serve (e.g. aws.example.com)") 43 | hostname := flag.String("hostname", "", "the public hostname of this server (e.g. ec2-12-34-56-78.compute-1.amazonaws.com)") 44 | help := flag.Bool("help", false, "show help") 45 | 46 | region := flag.String("aws-region", "us-east-1", "The AWS Region") 47 | accessKey := flag.String("aws-access-key-id", "", "The AWS Access Key Id") 48 | secretKey := flag.String("aws-secret-access-key", "", "The AWS Secret Key") 49 | 50 | flag.Parse() 51 | 52 | if *domain == "" { 53 | fmt.Println(USAGE) 54 | log.Fatalf("missing required parameter: --domain") 55 | } else if *help { 56 | fmt.Println(USAGE) 57 | os.Exit(0) 58 | } 59 | 60 | hostnameFuture := getHostname() 61 | 62 | cache, err := NewEC2Cache(*region, *accessKey, *secretKey) 63 | if err != nil { 64 | log.Fatalf("FATAL: %s", err) 65 | } 66 | 67 | if *hostname == "" { 68 | *hostname = <-hostnameFuture 69 | } 70 | 71 | server := NewEC2Server(*domain, *hostname, cache) 72 | 73 | log.Printf("Serving %d DNS records for *.%s from %s port 53", cache.Size(), server.domain, server.hostname) 74 | 75 | go checkNSRecordMatches(server.domain, server.hostname) 76 | 77 | go server.listenAndServe(":53", "udp") 78 | server.listenAndServe(":53", "tcp") 79 | } 80 | 81 | func getHostname() chan string { 82 | result := make(chan string) 83 | go func() { 84 | 85 | // This can be slow on non-EC2-instances 86 | if hostname, err := aws.GetMetaData("public-hostname"); err == nil { 87 | result <- string(hostname) 88 | return 89 | } 90 | 91 | if hostname, err := os.Hostname(); err == nil { 92 | result <- hostname 93 | return 94 | } 95 | 96 | result <- "localhost" 97 | }() 98 | return result 99 | } 100 | 101 | // checkNSRecordMatches does a spot check for DNS misconfiguration, and prints a warning 102 | // if using it for DNS is likely to be broken. 103 | func checkNSRecordMatches(domain, hostname string) { 104 | 105 | time.Sleep(1 * time.Second) 106 | 107 | results, err := net.LookupNS(domain) 108 | 109 | if err != nil { 110 | log.Printf("|WARN| No working NS records found for %s", domain) 111 | log.Printf("|WARN| You can still test things using `dig example.%s @%s`, but you won't be able to resolve hosts directly.", domain, hostname) 112 | log.Printf("|WARN| See https://github.com/ConradIrwin/aws-name-server for instructions on setting up NS records.") 113 | return 114 | } 115 | 116 | matched := false 117 | 118 | for _, record := range results { 119 | if record.Host == hostname { 120 | matched = true 121 | } 122 | } 123 | 124 | if !matched { 125 | log.Printf("|WARN| The NS record for %s points to: %s", domain, results[0].Host) 126 | log.Printf("|WARN| But --hostname is: %s", hostname) 127 | log.Printf("|WARN| These hostnames must match if you want DNS to work properly.") 128 | log.Printf("|WARN| See https://github.com/ConradIrwin/aws-name-server for instructions on NS records.") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /upstart/aws-name-server.conf: -------------------------------------------------------------------------------- 1 | # upstart script for aws-name-server 2 | description "AWS Name Server" 3 | 4 | start on filesystem or runlevel [2345] 5 | stop on runlevel [!2345] 6 | 7 | respawn 8 | respawn limit 10 5 9 | setuid nobody 10 | setgid nogroup 11 | 12 | exec /usr/bin/aws-name-server-linux --domain ____YOUR_DOMAIN_HERE___ 13 | 14 | --------------------------------------------------------------------------------