diff options
Diffstat (limited to 'plugin/azure/azure.go')
-rw-r--r-- | plugin/azure/azure.go | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/plugin/azure/azure.go b/plugin/azure/azure.go new file mode 100644 index 000000000..f3b2fad40 --- /dev/null +++ b/plugin/azure/azure.go @@ -0,0 +1,293 @@ +package azure + +import ( + "context" + "fmt" + "net" + "sync" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/pkg/upstream" + "github.com/coredns/coredns/request" + + azuredns "github.com/Azure/azure-sdk-for-go/profiles/latest/dns/mgmt/dns" + "github.com/miekg/dns" +) + +type zone struct { + id string + z *file.Zone + zone string +} + +type zones map[string][]*zone + +// Azure is the core struct of the azure plugin. +type Azure struct { + Next plugin.Handler + Fall fall.F + zoneNames []string + client azuredns.RecordSetsClient + upstream *upstream.Upstream + zMu sync.RWMutex + zones zones +} + +// New validates the input DNS zones and initializes the Azure struct. +func New(ctx context.Context, dnsClient azuredns.RecordSetsClient, keys map[string][]string, up *upstream.Upstream) (*Azure, error) { + zones := make(map[string][]*zone, len(keys)) + zoneNames := make([]string, 0, len(keys)) + for resourceGroup, inputZoneNames := range keys { + for _, zoneName := range inputZoneNames { + _, err := dnsClient.ListAllByDNSZone(context.Background(), resourceGroup, zoneName, nil, "") + if err != nil { + return nil, err + } + // Normalizing zoneName to make it fqdn if required. + zoneNameFQDN := dns.Fqdn(zoneName) + if _, ok := zones[zoneNameFQDN]; !ok { + zoneNames = append(zoneNames, zoneNameFQDN) + } + zones[zoneNameFQDN] = append(zones[zoneNameFQDN], &zone{id: resourceGroup, zone: zoneName, z: file.NewZone(zoneName, "")}) + } + } + return &Azure{ + client: dnsClient, + zones: zones, + zoneNames: zoneNames, + upstream: up, + }, nil +} + +// Run updates the zone from azure. +func (h *Azure) Run(ctx context.Context) error { + if err := h.updateZones(ctx); err != nil { + return err + } + go func() { + for { + select { + case <-ctx.Done(): + log.Infof("Breaking out of Azure update loop: %v", ctx.Err()) + return + case <-time.After(1 * time.Minute): + if err := h.updateZones(ctx); err != nil && ctx.Err() == nil { + log.Errorf("Failed to update zones: %v", err) + } + } + } + }() + return nil +} + +func (h *Azure) updateZones(ctx context.Context) error { + errs := make([]string, 0) + for zName, z := range h.zones { + for i, hostedZone := range z { + recordSet, err := h.client.ListByDNSZone(ctx, hostedZone.id, hostedZone.zone, nil, "") + if err != nil { + errs = append(errs, fmt.Sprintf("failed to list resource records for %v from azure: %v", hostedZone.zone, err)) + } + newZ := updateZoneFromResourceSet(recordSet, zName) + newZ.Upstream = h.upstream + h.zMu.Lock() + (*z[i]).z = newZ + h.zMu.Unlock() + } + } + + if len(errs) != 0 { + return fmt.Errorf("errors updating zones: %v", errs) + } + return nil + +} + +func updateZoneFromResourceSet(recordSet azuredns.RecordSetListResultPage, zName string) *file.Zone { + newZ := file.NewZone(zName, "") + + for _, result := range *(recordSet.Response().Value) { + resultFqdn := *(result.RecordSetProperties.Fqdn) + resultTTL := uint32(*(result.RecordSetProperties.TTL)) + if result.RecordSetProperties.ARecords != nil { + for _, A := range *(result.RecordSetProperties.ARecords) { + a := dns.A{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: resultTTL}, + A: net.ParseIP(*(A.Ipv4Address))} + newZ.Insert(&a) + } + } + + if result.RecordSetProperties.AaaaRecords != nil { + for _, AAAA := range *(result.RecordSetProperties.AaaaRecords) { + aaaa := dns.AAAA{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypeAAAA, + Class: dns.ClassINET, + Ttl: resultTTL}, + AAAA: net.ParseIP(*(AAAA.Ipv6Address))} + newZ.Insert(&aaaa) + } + } + + if result.RecordSetProperties.MxRecords != nil { + for _, MX := range *(result.RecordSetProperties.MxRecords) { + mx := dns.MX{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypeMX, + Class: dns.ClassINET, + Ttl: resultTTL}, + Preference: uint16(*(MX.Preference)), + Mx: dns.Fqdn(*(MX.Exchange))} + newZ.Insert(&mx) + } + } + + if result.RecordSetProperties.PtrRecords != nil { + for _, PTR := range *(result.RecordSetProperties.PtrRecords) { + ptr := dns.PTR{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypePTR, + Class: dns.ClassINET, + Ttl: resultTTL}, + Ptr: dns.Fqdn(*(PTR.Ptrdname))} + newZ.Insert(&ptr) + } + } + + if result.RecordSetProperties.SrvRecords != nil { + for _, SRV := range *(result.RecordSetProperties.SrvRecords) { + srv := dns.SRV{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypeSRV, + Class: dns.ClassINET, + Ttl: resultTTL}, + Priority: uint16(*(SRV.Priority)), + Weight: uint16(*(SRV.Weight)), + Port: uint16(*(SRV.Port)), + Target: dns.Fqdn(*(SRV.Target))} + newZ.Insert(&srv) + } + } + + if result.RecordSetProperties.TxtRecords != nil { + for _, TXT := range *(result.RecordSetProperties.TxtRecords) { + txt := dns.TXT{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypeTXT, + Class: dns.ClassINET, + Ttl: resultTTL}, + Txt: *(TXT.Value)} + newZ.Insert(&txt) + } + } + + if result.RecordSetProperties.NsRecords != nil { + for _, NS := range *(result.RecordSetProperties.NsRecords) { + ns := dns.NS{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypeNS, + Class: dns.ClassINET, + Ttl: resultTTL}, + Ns: *(NS.Nsdname)} + newZ.Insert(&ns) + } + } + + if result.RecordSetProperties.SoaRecord != nil { + SOA := result.RecordSetProperties.SoaRecord + soa := dns.SOA{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypeSOA, + Class: dns.ClassINET, + Ttl: resultTTL}, + Minttl: uint32(*(SOA.MinimumTTL)), + Expire: uint32(*(SOA.ExpireTime)), + Retry: uint32(*(SOA.RetryTime)), + Refresh: uint32(*(SOA.RefreshTime)), + Serial: uint32(*(SOA.SerialNumber)), + Mbox: dns.Fqdn(*(SOA.Email)), + Ns: *(SOA.Host)} + newZ.Insert(&soa) + } + + if result.RecordSetProperties.CnameRecord != nil { + CNAME := result.RecordSetProperties.CnameRecord.Cname + cname := dns.CNAME{ + Hdr: dns.RR_Header{ + Name: resultFqdn, + Rrtype: dns.TypeCNAME, + Class: dns.ClassINET, + Ttl: resultTTL}, + Target: dns.Fqdn(*CNAME)} + newZ.Insert(&cname) + } + } + return newZ +} + +// ServeDNS implements the plugin.Handler interface. +func (h *Azure) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + + zName := plugin.Zones(h.zoneNames).Matches(qname) + if zName == "" { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + z, ok := h.zones[zName] // ok true if we are authoritive for the zone. + if !ok || z == nil { + return dns.RcodeServerFailure, nil + } + + m := new(dns.Msg) + m.SetReply(r) + m.Authoritative = true + var result file.Result + for _, hostedZone := range z { + h.zMu.RLock() + m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(ctx, state, qname) + h.zMu.RUnlock() + + // record type exists for this name (NODATA). + if len(m.Answer) != 0 || result == file.NoData { + break + } + } + + if len(m.Answer) == 0 && result != file.NoData && h.Fall.Through(qname) { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + + switch result { + case file.Success: + case file.NoData: + case file.NameError: + m.Rcode = dns.RcodeNameError + case file.Delegation: + m.Authoritative = false + case file.ServerFailure: + return dns.RcodeServerFailure, nil + } + + w.WriteMsg(m) + return dns.RcodeSuccess, nil +} + +// Name implements plugin.Handler.Name. +func (h *Azure) Name() string { return "azure" } |