diff --git a/images/node/system-container/service.template b/images/node/system-container/service.template index 124251dfbf04..d8909c0a4078 100644 --- a/images/node/system-container/service.template +++ b/images/node/system-container/service.template @@ -12,11 +12,6 @@ Type=notify EnvironmentFile=/etc/sysconfig/$NAME EnvironmentFile=/etc/sysconfig/$NAME-dep -ExecStartPre=/usr/bin/cp /etc/origin/node/node-dnsmasq.conf /etc/dnsmasq.d/ -ExecStartPre=/usr/bin/dbus-send --system --dest=uk.org.thekelleys.dnsmasq /uk/org/thekelleys/dnsmasq uk.org.thekelleys.SetDomainServers array:string:/in-addr.arpa/127.0.0.1,/${DNS_DOMAIN}/127.0.0.1 -ExecStopPost=/usr/bin/rm /etc/dnsmasq.d/node-dnsmasq.conf -ExecStopPost=/usr/bin/dbus-send --system --dest=uk.org.thekelleys.dnsmasq /uk/org/thekelleys/dnsmasq uk.org.thekelleys.SetDomainServers array:string: - ExecStartPre=/bin/bash -c 'export -p > /run/$NAME-env' ExecStart=$EXEC_START ExecStop=$EXEC_STOP diff --git a/pkg/dns/dnsmasq.go b/pkg/dns/dnsmasq.go new file mode 100644 index 000000000000..b5511e0662b3 --- /dev/null +++ b/pkg/dns/dnsmasq.go @@ -0,0 +1,97 @@ +package dns + +import ( + "fmt" + "sync" + "time" + + godbus "github.com/godbus/dbus" + "github.com/golang/glog" + + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + utilwait "k8s.io/apimachinery/pkg/util/wait" + utildbus "k8s.io/kubernetes/pkg/util/dbus" +) + +const ( + // dnsmasqRetryInterval is the duration between attempts to register and listen to DBUS. + dnsmasqRetryInterval = 2 * time.Second + // dnsmasqRefreshInterval is the maximum time between refreshes of the current dnsmasq configuration. + dnsmasqRefreshInterval = 30 * time.Second + dbusDnsmasqPath = "/uk/org/thekelleys/dnsmasq" + dbusDnsmasqInterface = "uk.org.thekelleys.dnsmasq" +) + +type dnsmasqMonitor struct { + // dnsIP is the IP address this DNS server is reachable at from dnsmasq + dnsIP string + // dnsDomain is the domain name for this DNS server that dnsmasq should forward to + dnsDomain string + // lock controls sending a dnsmasq refresh + lock sync.Mutex +} + +func (m *dnsmasqMonitor) Start() error { + conn, err := utildbus.New().SystemBus() + if err != nil { + return fmt.Errorf("cannot connect to DBus: %v", err) + } + if err := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, fmt.Sprintf("type='signal',path='%s',interface='%s'", dbusDnsmasqPath, dbusDnsmasqInterface)).Store(); err != nil { + return fmt.Errorf("unable to add a match rule to the system DBus: %v", err) + } + // allow this to fail if dnsmasq hasn't started yet + if err := m.refresh(conn); err != nil { + utilruntime.HandleError(fmt.Errorf("unable to set dnsmasq on startup: %v", err)) + } + go m.run(conn, utilwait.NeverStop) + return nil +} + +func (m *dnsmasqMonitor) run(conn utildbus.Connection, stopCh <-chan struct{}) { + ch := make(chan *godbus.Signal, 20) + defer func() { + utilruntime.HandleCrash() + // unregister the handler + conn.Signal(ch) + }() + conn.Signal(ch) + + // watch for dnsmasq restart + go utilwait.Until(func() { + for s := range ch { + if s.Path != dbusDnsmasqPath { + continue + } + switch s.Name { + case "uk.org.thekelleys.dnsmasq.Up": + glog.V(2).Infof("dnsmasq restarted, refreshing server configuration") + if err := m.refresh(conn); err != nil { + utilruntime.HandleError(fmt.Errorf("unable to refresh dnsmasq status on dnsmasq startup: %v", err)) + } + } + } + }, dnsmasqRetryInterval, stopCh) + + // no matter what, always keep trying to refresh dnsmasq + go utilwait.Until(func() { + if err := m.refresh(conn); err != nil { + utilruntime.HandleError(fmt.Errorf("unable to periodically refresh dnsmasq status: %v", err)) + } + }, dnsmasqRefreshInterval, stopCh) + + <-stopCh +} + +// refresh invokes dnsmasq with the requested configuration +func (m *dnsmasqMonitor) refresh(conn utildbus.Connection) error { + m.lock.Lock() + defer m.lock.Unlock() + addresses := []string{ + fmt.Sprintf("/in-addr.arpa/%s", m.dnsIP), + fmt.Sprintf("/%s/%s", m.dnsDomain, m.dnsIP), + } + glog.V(5).Infof("Instructing dnsmasq to set the following servers: %v", addresses) + return conn.Object(dbusDnsmasqInterface, dbusDnsmasqPath). + Call("uk.org.thekelleys.SetDomainServers", 0, addresses). + Store() +} diff --git a/pkg/dns/server.go b/pkg/dns/server.go index 06a14fb77c64..f5e9a2e61d1f 100644 --- a/pkg/dns/server.go +++ b/pkg/dns/server.go @@ -1,6 +1,9 @@ package dns import ( + "net" + "strings" + "github.com/golang/glog" "github.com/skynetservices/skydns/metrics" @@ -41,6 +44,8 @@ func NewServer(config *server.Config, services ServiceAccessor, endpoints Endpoi // ListenAndServe starts a DNS server that exposes services and values stored in etcd (if etcdclient // is not nil). It will block until the server exits. func (s *Server) ListenAndServe() error { + monitorDnsmasq(s.Config) + resolver := NewServiceResolver(s.Config, s.Services, s.Endpoints, openshiftFallback) resolvers := server.FirstBackend{resolver} if len(s.MetricsName) > 0 { @@ -53,6 +58,33 @@ func (s *Server) ListenAndServe() error { return dns.Run() } +// monitorDnsmasq attempts to start the dnsmasq monitoring goroutines to keep dnsmasq +// in sync with this server. It will take no action if the current config DnsAddr does +// not point to port 53 (dnsmasq does not support alternate upstream ports). It will +// convert the bind address from 0.0.0.0 to the BindNetwork appropriate listen address. +func monitorDnsmasq(config *server.Config) { + if host, port, err := net.SplitHostPort(config.DnsAddr); err == nil && port == "53" { + if ip := net.ParseIP(host); ip != nil && ip.IsUnspecified() { + if config.BindNetwork == "ipv6" { + host = "::1" + } else { + host = "127.0.0.1" + } + } + monitor := &dnsmasqMonitor{ + dnsIP: host, + dnsDomain: strings.TrimSuffix(config.Domain, "."), + } + if err := monitor.Start(); err != nil { + glog.Warningf("Unable to start dnsmasq monitor: %v", err) + } else { + glog.V(2).Infof("Monitoring dnsmasq to point cluster queries to %s", host) + } + } else { + glog.Warningf("Unable to keep dnsmasq up to date, %s must point to port 53", config.DnsAddr) + } +} + func openshiftFallback(name string, exact bool) (string, bool) { if name == "openshift.default.svc" { return "kubernetes.default.svc.", true