From 5a413a0cb474cae011a591bbaad6ced4baf3d20f Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 7 Jun 2020 23:12:18 -0500 Subject: [PATCH] Progress in chaos --- ldap.go | 42 ++++++---- setup.go | 231 ++++++++++++++----------------------------------------- sync.go | 150 ++++++++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 188 deletions(-) create mode 100644 sync.go diff --git a/ldap.go b/ldap.go index 9d75259..e861268 100644 --- a/ldap.go +++ b/ldap.go @@ -10,14 +10,15 @@ package ldap import ( "context" - "crypto/tls" "errors" - "fmt" "strings" + "sync" + "time" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/etcd/msg" "github.com/coredns/coredns/plugin/pkg/fall" + "github.com/coredns/coredns/plugin/pkg/upstream" "github.com/coredns/coredns/request" "github.com/miekg/dns" @@ -26,17 +27,34 @@ import ( // Ldap is an ldap plugin to serve zone entries from a ldap backend. type Ldap struct { - Next plugin.Handler - Fall fall.F - Zones []string - Client *ldap.Client - clientConfig map[string] + Next plugin.Handler + Fall fall.F + Zones []string + Upstream *upstream.Upstream + Client ldap.Client + + searchRequest *ldap.SearchRequest + ldapURL string + pagingLimit uint32 + syncInterval time.Duration + username string + password string + sasl bool + zMu sync.RWMutex } // New returns an initialized Ldap with defaults. func New(zones []string) *Ldap { k := new(Ldap) k.Zones = zones + k.pagingLimit = 0 + // SearchRequest defaults + k.searchRequest = new(ldap.SearchRequest) + k.searchRequest.DerefAliases = ldap.NeverDerefAliases // TODO: Reason + k.searchRequest.Scope = ldap.ScopeWholeSubtree // search whole subtree + k.searchRequest.SizeLimit = 500 // TODO: Reason + k.searchRequest.TimeLimit = 500 // TODO: Reason + k.searchRequest.TypesOnly = false // TODO: Reason return k } @@ -48,17 +66,13 @@ var ( // InitClient initializes a Ldap client. func (l *Ldap) InitClient() (err error) { - l.Client, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) + l.Client, err = ldap.DialURL(l.ldapURL) if err != nil { log.Fatal(err) + return err } defer l.Client.Close() - - // Reconnect with TLS - err = l.Client.StartTLS(&tls.Config{InsecureSkipVerify: true}) - if err != nil { - log.Fatal(err) - } + return nil } // Services implements the ServiceBackend interface. diff --git a/setup.go b/setup.go index dae0bb5..e6d2cf2 100644 --- a/setup.go +++ b/setup.go @@ -1,20 +1,13 @@ package ldap import ( - "context" - "errors" - "fmt" "strconv" - "strings" "sync" - "time" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/metrics" - "github.com/coredns/coredns/plugin/pkg/dnsutil" clog "github.com/coredns/coredns/plugin/pkg/log" - "github.com/coredns/coredns/plugin/pkg/parse" "github.com/coredns/coredns/plugin/pkg/upstream" "github.com/caddyserver/caddy" @@ -38,13 +31,11 @@ func setup(c *caddy.Controller) error { return plugin.Error(pluginName, err) } - err = l.InitLdapCache(context.Background()) + err = l.InitClient() if err != nil { return plugin.Error(pluginName, err) } - l.RegisterLdapCache(c) - // add prometheus metrics on startup c.OnStartup(func() error { // add plugin-global metric once @@ -65,30 +56,6 @@ func setup(c *caddy.Controller) error { var once sync.Once -// RegisterLdapCache registers LdapCache start and stop functions with Caddy -func (l *Ldap) RegisterLdapCache(c *caddy.Controller) { - c.OnStartup(func() error { - go l.APIConn.Run() - - timeout := time.After(5 * time.Second) - ticker := time.NewTicker(100 * time.Millisecond) - for { - select { - case <-ticker.C: - if k.APIConn.HasSynced() { - return nil - } - case <-timeout: - return nil - } - } - }) - - c.OnShutdown(func() error { - return l.APIConn.Stop() - }) -} - func ldapParse(c *caddy.Controller) (*Ldap, error) { var ( ldap *Ldap @@ -102,7 +69,7 @@ func ldapParse(c *caddy.Controller) (*Ldap, error) { } i++ - l, err = ParseStanza(c) + ldap, err = ParseStanza(c) if err != nil { return ldap, err } @@ -112,16 +79,7 @@ func ldapParse(c *caddy.Controller) (*Ldap, error) { // ParseStanza parses a ldap stanza func ParseStanza(c *caddy.Controller) (*Ldap, error) { - ldap := New([]string{""}) - ldap.autoPathSearch = searchFromResolvConf() - - opts := dnsControlOpts{ - initEndpointsCache: true, - ignoreEmptyService: false, - } - ldap.opts = opts - zones := c.RemainingArgs() if len(zones) != 0 { @@ -136,153 +94,78 @@ func ParseStanza(c *caddy.Controller) (*Ldap, error) { } } - ldap.primaryZoneIndex = -1 - for i, z := range ldap.Zones { - if dnsutil.IsReverse(z) > 0 { - continue - } - ldap.primaryZoneIndex = i - break - } - - if ldap.primaryZoneIndex == -1 { - return nil, errors.New("non-reverse zone name must be used") - } - ldap.Upstream = upstream.New() for c.NextBlock() { switch c.Val() { // RFC 4516 URL - case "endpoint_pod_names": - args := c.RemainingArgs() - if len(args) > 0 { + case "ldap_url": + c.NextArg() + ldap.ldapURL = c.Val() + continue + case "paging_limit": + c.NextArg() + pagingLimit, err := strconv.Atoi(c.Val()) + if err != nil { return nil, c.ArgErr() } - ldap.endpointNameMode = true + ldap.pagingLimit = pagingLimit continue - case "pods": - args := c.RemainingArgs() - if len(args) == 1 { - switch args[0] { - case podModeDisabled, podModeInsecure, podModeVerified: - ldap.podMode = args[0] + case "search_request": + for c.NextBlock() { + switch c.Val() { + case "base_dn": + c.NextArg() // ou=ae-dir + ldap.searchRequest.BaseDN = c.Val() + case "filter": + c.NextArg() // (objectClass=aeNwDevice) + ldap.searchRequest.Filter = c.Val() + case "attributes": + ldap.searchRequest.Attributes = c.RemainingArgs() // aeFqdn ipHostNumber default: - return nil, fmt.Errorf("wrong value for pods: %s, must be one of: disabled, verified, insecure", args[0]) + return nil, c.Errf("unknown search request property '%s'", c.Val()) } - continue } - return nil, c.ArgErr() - case "namespaces": - args := c.RemainingArgs() - if len(args) > 0 { - for _, a := range args { - ldap.Namespaces[a] = struct{}{} - } - continue - } - return nil, c.ArgErr() - case "endpoint": - args := c.RemainingArgs() - if len(args) > 0 { - // Multiple endpoints are deprecated but still could be specified, - // only the first one be used, though - ldap.APIServerList = args - if len(args) > 1 { - log.Warningf("Multiple endpoints have been deprecated, only the first specified endpoint '%s' is used", args[0]) - } - continue - } - return nil, c.ArgErr() - case "tls": // cert key cacertfile - args := c.RemainingArgs() - if len(args) == 3 { - ldap.APIClientCert, ldap.APIClientKey, ldap.APICertAuth = args[0], args[1], args[2] - continue - } - return nil, c.ArgErr() - case "labels": - args := c.RemainingArgs() - if len(args) > 0 { - labelSelectorString := strings.Join(args, " ") - ls, err := meta.ParseToLabelSelector(labelSelectorString) - if err != nil { - return nil, fmt.Errorf("unable to parse label selector value: '%v': %v", labelSelectorString, err) - } - ldap.opts.labelSelector = ls - continue - } - return nil, c.ArgErr() - case "namespace_labels": - args := c.RemainingArgs() - if len(args) > 0 { - namespaceLabelSelectorString := strings.Join(args, " ") - nls, err := meta.ParseToLabelSelector(namespaceLabelSelectorString) - if err != nil { - return nil, fmt.Errorf("unable to parse namespace_label selector value: '%v': %v", namespaceLabelSelectorString, err) - } - ldap.opts.namespaceLabelSelector = nls - continue - } - return nil, c.ArgErr() + continue + case "username": + c.NextArg() + ldap.username = c.Val() + case "password": + c.NextArg() + ldap.password = c.Val() + case "sasl": + c.NextArg() + ldap.sasl = true case "fallthrough": ldap.Fall.SetZonesFromArgs(c.RemainingArgs()) - case "ttl": - args := c.RemainingArgs() - if len(args) == 0 { - return nil, c.ArgErr() - } - t, err := strconv.Atoi(args[0]) - if err != nil { - return nil, err - } - if t < 0 || t > 3600 { - return nil, c.Errf("ttl must be in range [0, 3600]: %d", t) - } - ldap.ttl = uint32(t) - case "transfer": - tos, froms, err := parse.Transfer(c, false) - if err != nil { - return nil, err - } - if len(froms) != 0 { - return nil, c.Errf("transfer from is not supported with this plugin") - } - ldap.TransferTo = tos - case "noendpoints": - if len(c.RemainingArgs()) != 0 { - return nil, c.ArgErr() - } - ldap.opts.initEndpointsCache = false - case "ignore": - args := c.RemainingArgs() - if len(args) > 0 { - ignore := args[0] - if ignore == "empty_service" { - ldap.opts.ignoreEmptyService = true - continue - } else { - return nil, fmt.Errorf("unable to parse ignore value: '%v'", ignore) - } - } - case "kubeconfig": - args := c.RemainingArgs() - if len(args) == 2 { - config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - &clientcmd.ClientConfigLoadingRules{ExplicitPath: args[0]}, - &clientcmd.ConfigOverrides{CurrentContext: args[1]}, - ) - ldap.ClientConfig = config - continue - } - return nil, c.ArgErr() default: return nil, c.Errf("unknown property '%s'", c.Val()) } } - - if len(ldap.Namespaces) != 0 && ldap.opts.namespaceLabelSelector != nil { - return nil, c.Errf("namespaces and namespace_labels cannot both be set") + // validate non-default ldap values ... + if ldap.ldapURL == "" || &ldap.ldapURL == nil { + return nil, c.ArgErr() + } + if ldap.searchRequest.BaseDN == "" { + return nil, c.ArgErr() + } + if ldap.searchRequest.Filter == "" { + return nil, c.ArgErr() + } + if len(ldap.searchRequest.Attributes) != 2 { + return nil, c.ArgErr() + } + // if only one of password and username set + if (&ldap.username == nil) != (&ldap.password == nil) { + return nil, c.ArgErr() + } + // if both username/password and sasl are set + if &ldap.username != nil && &ldap.sasl != nil { + return nil, c.ArgErr() + } + // if neither username/password nor sasl are set + if &ldap.username == nil && &ldap.sasl == nil { + return nil, c.ArgErr() } return ldap, nil diff --git a/sync.go b/sync.go new file mode 100644 index 0000000..32e4247 --- /dev/null +++ b/sync.go @@ -0,0 +1,150 @@ +package ldap + +import ( + "context" + "fmt" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +// Run updates the zone from ldap. +func (l *Ldap) Run(ctx context.Context) error { + if err := l.updateZones(ctx); err != nil { + return err + } + go func() { + for { + select { + case <-ctx.Done(): + log.Infof("Breaking out of Ldap update loop: %v", ctx.Err()) + return + case <-time.After(l.syncInterval * time.Second): + if err := l.updateZones(ctx); err != nil && ctx.Err() == nil { + log.Errorf("Failed to update zones: %v", err) + } + } + } + }() + return nil +} + +func (l *Ldap) updateZones(ctx context.Context) error { + var err error + var zoneFile file.Zone + + valuePairs, err := getValuePairs() + for _, z := range l.Zones { + zoneFile = file.NewZone(z, "") + zoneFile.Upstream = l.Upstream + l.zMu.Lock() + (*z[i]).z = zoneFile + l.zMu.Unlock() + } + if err != nil { + return fmt.Errorf("error updating zones: %v", err) + } + return nil + +} + +func (l *Ldap) getValuePairs() (valuePairs *[][]string, err error) { + searchResult, err := l.Client.SearchWithPaging(l.searchRequest, l.pagingLimit) + if err != nil { + return nil, fmt.Errorf("error fetching data from ldap server: %w", err) + } + +} + +func updateZoneFromPublicResourceSet(recordSet publicdns.RecordSetListResultPage, zName string) *file.Zone { + zoneFile := 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))} + zoneFile.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))} + zoneFile.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))} + zoneFile.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))} + zoneFile.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))} + zoneFile.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)} + zoneFile.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)} + zoneFile.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)} + zoneFile.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)} + zoneFile.Insert(cname) + } + } + return zoneFile +}