1
0
Fork 0
home-dns/main.go

164 lines
3.8 KiB
Go

package main
import (
"io/ioutil"
"log"
"net/http"
"os"
"os/signal"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/robfig/cron/v3"
yaml "gopkg.in/yaml.v2"
)
// Record defines a single record to upsert and the schedule for updates
type Record struct {
Cron string
Name string
TTL int64
Zone string
}
// Config defines the source endpoint and a set of records to update
type Config struct {
Records []Record
Source string
}
// FindPublicAddress fetches the public address from an external HTTP/S endpoint
func FindPublicAddress(url string) ([]string, error) {
ipResp, ipError := http.Get(url)
if ipError != nil {
log.Printf("error getting public address: %s", ipError.Error())
return nil, ipError
}
defer ipResp.Body.Close()
body, bodyError := ioutil.ReadAll(ipResp.Body)
if bodyError != nil {
log.Printf("error getting response body: %s", bodyError.Error())
return nil, bodyError
}
return []string{
string(body),
}, nil
}
// LoadConfig loads the config data (source and records) from a YAML file
func LoadConfig(path string) (*Config, error) {
data, readError := ioutil.ReadFile(path)
if readError != nil {
log.Printf("error reading config: %s", readError.Error())
return nil, readError
}
dest := &Config{}
yamlError := yaml.Unmarshal(data, dest)
if yamlError != nil {
log.Printf("error parsing config: %s", yamlError.Error())
return nil, yamlError
}
return dest, nil
}
// MapResourceValues converts address strings to route53's special record type
func MapResourceValues(values []string) []*route53.ResourceRecord {
records := make([]*route53.ResourceRecord, len(values))
for i, v := range values {
records[i] = &route53.ResourceRecord{
Value: aws.String(v),
}
}
return records
}
// UpdateRecord upserts a single record in a zone
func UpdateRecord(conf *Config, r Record) {
// create a session
sess, sessError := session.NewSession()
if sessError != nil {
log.Printf("error creating aws session: %s", sessError.Error())
return
}
// get current ip
values, valueError := FindPublicAddress(conf.Source)
if valueError != nil {
log.Printf("error fetching external address: %s", valueError.Error())
return
}
log.Printf("updating '%s' to '%s'", r.Name, values)
// prepare the update
updates := &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{
{
Action: aws.String("UPSERT"),
ResourceRecordSet: &route53.ResourceRecordSet{
Name: aws.String(r.Name),
TTL: aws.Int64(r.TTL),
Type: aws.String("A"),
ResourceRecords: MapResourceValues(values),
},
},
},
},
HostedZoneId: aws.String(r.Zone),
}
// apply
svc := route53.New(sess)
updateOutput, updateError := svc.ChangeResourceRecordSets(updates)
if updateError != nil {
log.Printf("error updating route53 records: %s", updateError.Error())
return
}
log.Printf("updated route53 records: %v", updateOutput)
}
// ScheduleJob adds a new record and update job to the cron pool
func ScheduleJob(conf *Config, c *cron.Cron, r Record) {
log.Printf("scheduling cron job for %s", r.Name)
c.AddFunc(r.Cron, func() {
log.Printf("executing cron job for %s", r.Name)
UpdateRecord(conf, r)
})
}
func main() {
if len(os.Args) < 2 {
log.Printf("not enough arguments: %s config.yml", os.Args[0])
return
}
confPath := os.Args[1]
log.Printf("loading config from '%s'", confPath)
conf, confError := LoadConfig(confPath)
if confError != nil {
log.Printf("error loading config: %s", confError.Error())
return
}
// schedule cron jobs
c := cron.New()
for _, r := range conf.Records {
ScheduleJob(conf, c, r)
}
c.Start()
stop := make(chan os.Signal)
signal.Notify(stop, os.Interrupt, os.Kill)
<-stop
c.Stop()
}