mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-30 17:53:21 -04:00 
			
		
		
		
	middleware/httpproxy: Add (#439)
This PR adds a middleware that talks to dns.google.com over HTTPS, meaning all your DNS traffic is encrypted when traversing your ISP and the internet. The `dns.google.com` address is re-resolved every 30 seconds.
This commit is contained in:
		| @@ -15,6 +15,7 @@ import ( | |||||||
| 	_ "github.com/miekg/coredns/middleware/etcd" | 	_ "github.com/miekg/coredns/middleware/etcd" | ||||||
| 	_ "github.com/miekg/coredns/middleware/file" | 	_ "github.com/miekg/coredns/middleware/file" | ||||||
| 	_ "github.com/miekg/coredns/middleware/health" | 	_ "github.com/miekg/coredns/middleware/health" | ||||||
|  | 	_ "github.com/miekg/coredns/middleware/httpproxy" | ||||||
| 	_ "github.com/miekg/coredns/middleware/kubernetes" | 	_ "github.com/miekg/coredns/middleware/kubernetes" | ||||||
| 	_ "github.com/miekg/coredns/middleware/loadbalance" | 	_ "github.com/miekg/coredns/middleware/loadbalance" | ||||||
| 	_ "github.com/miekg/coredns/middleware/log" | 	_ "github.com/miekg/coredns/middleware/log" | ||||||
|   | |||||||
| @@ -94,5 +94,6 @@ var directives = []string{ | |||||||
| 	"etcd", | 	"etcd", | ||||||
| 	"kubernetes", | 	"kubernetes", | ||||||
| 	"proxy", | 	"proxy", | ||||||
|  | 	"httpproxy", | ||||||
| 	"whoami", | 	"whoami", | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								middleware/cache/handler.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								middleware/cache/handler.go
									
									
									
									
										vendored
									
									
								
							| @@ -22,7 +22,7 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) | |||||||
| 		return c.Next.ServeDNS(ctx, w, r) | 		return c.Next.ServeDNS(ctx, w, r) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	do := state.Do() // might need more from OPT record? Like the actual bufsize? | 	do := state.Do() // TODO(): might need more from OPT record? Like the actual bufsize? | ||||||
|  |  | ||||||
| 	if i, ok, expired := c.get(qname, qtype, do); ok && !expired { | 	if i, ok, expired := c.get(qname, qtype, do); ok && !expired { | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								middleware/httpproxy/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								middleware/httpproxy/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | # httpproxy | ||||||
|  |  | ||||||
|  | *httpproxy* proxies DNS request to a proxy using HTTPS (or HTTP/2 - not implemented). Usually this | ||||||
|  |  involves sending a JSON payload over this transport and translating the response back to DNS. The | ||||||
|  |  current supported backend is Google, using the URL: https://dns.google.com . | ||||||
|  |  | ||||||
|  | ## Syntax | ||||||
|  |  | ||||||
|  | In its most basic form, a simple http proxy uses this syntax: | ||||||
|  |  | ||||||
|  | ~~~ | ||||||
|  | httpproxy FROM TO | ||||||
|  | ~~~ | ||||||
|  |  | ||||||
|  | * **FROM** is the base domain to match for the request to be proxied. | ||||||
|  | * **TO** is the destination endpoint to proxy to, accepted values here are `dns.google.com`. | ||||||
|  |  | ||||||
|  | For changing the defaults you can use the expanded syntax: | ||||||
|  |  | ||||||
|  | ~~~ | ||||||
|  | proxy FROM TO { | ||||||
|  |     upstream ADDRESS... | ||||||
|  | } | ||||||
|  | ~~~ | ||||||
|  |  | ||||||
|  | * `upstream` defines upstream resolvers to be used (re-)resolve `dns.google.com` (or other names in the | ||||||
|  |   future) every 30 seconds. When not specified the combo 8.8.8.8, 8.8.4.4 is used. | ||||||
|  |  | ||||||
|  | ## Metrics | ||||||
|  |  | ||||||
|  | If monitoring is enabled (via the *prometheus* directive) then the following metric is exported: | ||||||
|  |  | ||||||
|  | * coredns_httpproxy_request_count_total{zone, proto, family} | ||||||
|  |  | ||||||
|  | ## Examples | ||||||
|  |  | ||||||
|  | Proxy all requests within example.org to Google's dns.google.com. | ||||||
|  |  | ||||||
|  | ~~~ | ||||||
|  | proxy example.org dns.google.com | ||||||
|  | ~~~ | ||||||
|  |  | ||||||
|  | Proxy everything, and re-lookup `dns.google.com` every 30 seconds using the resolvers specified | ||||||
|  | in /etc/resolv.conf. | ||||||
|  |  | ||||||
|  | ~~~ | ||||||
|  | proxy . dns.google.com { | ||||||
|  |     upstream /etc/resolv.conf | ||||||
|  | } | ||||||
|  | ~~~ | ||||||
							
								
								
									
										307
									
								
								middleware/httpproxy/google.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								middleware/httpproxy/google.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,307 @@ | |||||||
|  | package httpproxy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"net" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"sync" | ||||||
|  | 	"sync/atomic" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/coredns/middleware/proxy" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // immediate retries until this duration ends or we get a nil host. | ||||||
|  | var tryDuration = 60 * time.Second | ||||||
|  |  | ||||||
|  | type google struct { | ||||||
|  | 	client   *http.Client | ||||||
|  | 	upstream *simpleUpstream | ||||||
|  | 	addr     *simpleUpstream | ||||||
|  | 	quit     chan bool | ||||||
|  | 	sync.RWMutex | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newGoogle() *google { return &google{client: newClient(ghost), quit: make(chan bool)} } | ||||||
|  |  | ||||||
|  | func (g *google) Exchange(req *dns.Msg) (*dns.Msg, error) { | ||||||
|  | 	v := url.Values{} | ||||||
|  |  | ||||||
|  | 	v.Set("name", req.Question[0].Name) | ||||||
|  | 	v.Set("type", fmt.Sprintf("%d", req.Question[0].Qtype)) | ||||||
|  |  | ||||||
|  | 	start := time.Now() | ||||||
|  |  | ||||||
|  | 	for time.Now().Sub(start) < tryDuration { | ||||||
|  |  | ||||||
|  | 		g.RLock() | ||||||
|  | 		addr := g.addr.Select() | ||||||
|  | 		g.RUnlock() | ||||||
|  |  | ||||||
|  | 		if addr == nil { | ||||||
|  | 			return nil, fmt.Errorf("no healthy upstream http hosts") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		atomic.AddInt64(&addr.Conns, 1) | ||||||
|  |  | ||||||
|  | 		buf, backendErr := g.do(addr.Name, v.Encode()) | ||||||
|  |  | ||||||
|  | 		atomic.AddInt64(&addr.Conns, -1) | ||||||
|  |  | ||||||
|  | 		if backendErr == nil { | ||||||
|  | 			gm := new(googleMsg) | ||||||
|  | 			if err := json.Unmarshal(buf, gm); err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			m, err := toMsg(gm) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			m.Id = req.Id | ||||||
|  | 			return m, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		log.Printf("[WARNING] Failed to connect to HTTPS backend %q: %s", ghost, backendErr) | ||||||
|  |  | ||||||
|  | 		timeout := addr.FailTimeout | ||||||
|  | 		if timeout == 0 { | ||||||
|  | 			timeout = 10 * time.Second | ||||||
|  | 		} | ||||||
|  | 		atomic.AddInt32(&addr.Fails, 1) | ||||||
|  | 		go func(host *proxy.UpstreamHost, timeout time.Duration) { | ||||||
|  | 			time.Sleep(timeout) | ||||||
|  | 			atomic.AddInt32(&host.Fails, -1) | ||||||
|  | 		}(addr, timeout) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil, errUnreachable | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // OnStartup looks up the IP address for "ghost" every 30 seconds. | ||||||
|  | func (g *google) OnStartup() error { | ||||||
|  | 	r := new(dns.Msg) | ||||||
|  | 	r.SetQuestion(dns.Fqdn(ghost), dns.TypeA) | ||||||
|  | 	new, err := g.lookup(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	r.SetQuestion(dns.Fqdn(ghost), dns.TypeAAAA) | ||||||
|  | 	new6, err := g.lookup(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	up, _ := newSimpleUpstream(append(new, new6...)) | ||||||
|  | 	g.Lock() | ||||||
|  | 	g.addr = up | ||||||
|  | 	g.Unlock() | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		tick := time.NewTicker(30 * time.Second) | ||||||
|  |  | ||||||
|  | 		for { | ||||||
|  | 			select { | ||||||
|  | 			case <-tick.C: | ||||||
|  |  | ||||||
|  | 				r.SetQuestion(dns.Fqdn(ghost), dns.TypeA) | ||||||
|  | 				new, err := g.lookup(r) | ||||||
|  | 				if err != nil { | ||||||
|  | 					log.Printf("[WARNING] Failed to lookup A records %q: %s", ghost, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				r.SetQuestion(dns.Fqdn(ghost), dns.TypeAAAA) | ||||||
|  | 				new6, err := g.lookup(r) | ||||||
|  | 				if err != nil { | ||||||
|  | 					log.Printf("[WARNING] Failed to lookup AAAA records %q: %s", ghost, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				up, _ := newSimpleUpstream(append(new, new6...)) | ||||||
|  | 				g.Lock() | ||||||
|  | 				g.addr = up | ||||||
|  | 				g.Unlock() | ||||||
|  | 			case <-g.quit: | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g *google) OnShutdown() error { | ||||||
|  | 	g.quit <- true | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g *google) SetUpstream(u *simpleUpstream) error { | ||||||
|  | 	g.upstream = u | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g *google) lookup(r *dns.Msg) ([]string, error) { | ||||||
|  | 	c := new(dns.Client) | ||||||
|  | 	start := time.Now() | ||||||
|  |  | ||||||
|  | 	for time.Now().Sub(start) < tryDuration { | ||||||
|  | 		host := g.upstream.Select() | ||||||
|  | 		if host == nil { | ||||||
|  | 			return nil, fmt.Errorf("no healthy upstream hosts") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		atomic.AddInt64(&host.Conns, 1) | ||||||
|  |  | ||||||
|  | 		m, _, backendErr := c.Exchange(r, host.Name) | ||||||
|  |  | ||||||
|  | 		atomic.AddInt64(&host.Conns, -1) | ||||||
|  |  | ||||||
|  | 		if backendErr == nil { | ||||||
|  | 			if len(m.Answer) == 0 { | ||||||
|  | 				return nil, fmt.Errorf("no answer section in response") | ||||||
|  | 			} | ||||||
|  | 			ret := []string{} | ||||||
|  | 			for _, an := range m.Answer { | ||||||
|  | 				if a, ok := an.(*dns.A); ok { | ||||||
|  | 					ret = append(ret, net.JoinHostPort(a.A.String(), "443")) | ||||||
|  | 				} | ||||||
|  | 				if a, ok := an.(*dns.AAAA); ok { | ||||||
|  | 					ret = append(ret, net.JoinHostPort(a.AAAA.String(), "443")) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if len(ret) > 0 { | ||||||
|  | 				return ret, nil | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			return nil, fmt.Errorf("no address records in answer section") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		timeout := host.FailTimeout | ||||||
|  | 		if timeout == 0 { | ||||||
|  | 			timeout = 10 * time.Second | ||||||
|  | 		} | ||||||
|  | 		atomic.AddInt32(&host.Fails, 1) | ||||||
|  | 		go func(host *proxy.UpstreamHost, timeout time.Duration) { | ||||||
|  | 			time.Sleep(timeout) | ||||||
|  | 			atomic.AddInt32(&host.Fails, -1) | ||||||
|  | 		}(host, timeout) | ||||||
|  | 	} | ||||||
|  | 	return nil, fmt.Errorf("no healthy upstream hosts") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (g *google) do(addr, json string) ([]byte, error) { | ||||||
|  | 	url := "https://" + addr + "/resolve?" + json | ||||||
|  | 	req, err := http.NewRequest("GET", url, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	req.Host = ghost | ||||||
|  |  | ||||||
|  | 	resp, err := g.client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	buf, err := ioutil.ReadAll(resp.Body) | ||||||
|  | 	resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if resp.StatusCode != 200 { | ||||||
|  | 		return nil, fmt.Errorf("failed to get 200 status code, got %d", resp.StatusCode) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return buf, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func toMsg(g *googleMsg) (*dns.Msg, error) { | ||||||
|  | 	m := new(dns.Msg) | ||||||
|  | 	m.Rcode = g.Status | ||||||
|  | 	m.Truncated = g.TC | ||||||
|  | 	m.RecursionDesired = g.RD | ||||||
|  | 	m.RecursionAvailable = g.RA | ||||||
|  | 	m.AuthenticatedData = g.AD | ||||||
|  | 	m.CheckingDisabled = g.CD | ||||||
|  |  | ||||||
|  | 	m.Question = make([]dns.Question, 1) | ||||||
|  | 	m.Answer = make([]dns.RR, len(g.Answer)) | ||||||
|  | 	m.Ns = make([]dns.RR, len(g.Authority)) | ||||||
|  | 	m.Extra = make([]dns.RR, len(g.Additional)) | ||||||
|  |  | ||||||
|  | 	m.Question[0] = dns.Question{Name: g.Question[0].Name, Qtype: g.Question[0].Type, Qclass: dns.ClassINET} | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	for i := 0; i < len(m.Answer); i++ { | ||||||
|  | 		m.Answer[i], err = toRR(g.Answer[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	for i := 0; i < len(m.Ns); i++ { | ||||||
|  | 		m.Ns[i], err = toRR(g.Authority[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	for i := 0; i < len(m.Extra); i++ { | ||||||
|  | 		m.Extra[i], err = toRR(g.Additional[i]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return m, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func toRR(g googleRR) (dns.RR, error) { | ||||||
|  | 	typ, ok := dns.TypeToString[g.Type] | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, fmt.Errorf("failed to convert type %q", g.Type) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	str := fmt.Sprintf("%s %d %s %s", g.Name, g.TTL, typ, g.Data) | ||||||
|  | 	rr, err := dns.NewRR(str) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to parse %q: %s", str, err) | ||||||
|  | 	} | ||||||
|  | 	return rr, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // googleRR represents a dns.RR in another form. | ||||||
|  | type googleRR struct { | ||||||
|  | 	Name string | ||||||
|  | 	Type uint16 | ||||||
|  | 	TTL  uint32 | ||||||
|  | 	Data string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // googleMsg is a JSON representation of the dns.Msg. | ||||||
|  | type googleMsg struct { | ||||||
|  | 	Status   int | ||||||
|  | 	TC       bool | ||||||
|  | 	RD       bool | ||||||
|  | 	RA       bool | ||||||
|  | 	AD       bool | ||||||
|  | 	CD       bool | ||||||
|  | 	Question []struct { | ||||||
|  | 		Name string | ||||||
|  | 		Type uint16 | ||||||
|  | 	} | ||||||
|  | 	Answer     []googleRR | ||||||
|  | 	Authority  []googleRR | ||||||
|  | 	Additional []googleRR | ||||||
|  | 	Comment    string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	ghost = "dns.google.com" | ||||||
|  | ) | ||||||
							
								
								
									
										5
									
								
								middleware/httpproxy/google_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								middleware/httpproxy/google_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | package httpproxy | ||||||
|  |  | ||||||
|  | // TODO(miek): | ||||||
|  | // Test cert failures - put those in SERVFAIL messages, but attach error code in TXT | ||||||
|  | // Test connecting to a a bad host. | ||||||
							
								
								
									
										32
									
								
								middleware/httpproxy/metrics.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								middleware/httpproxy/metrics.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | package httpproxy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/coredns/middleware" | ||||||
|  |  | ||||||
|  | 	"github.com/prometheus/client_golang/prometheus" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Metrics the httpproxy middleware exports. | ||||||
|  | var ( | ||||||
|  | 	RequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||||||
|  | 		Namespace: middleware.Namespace, | ||||||
|  | 		Subsystem: subsystem, | ||||||
|  | 		Name:      "request_duration_milliseconds", | ||||||
|  | 		Buckets:   append(prometheus.DefBuckets, []float64{50, 100, 200, 500, 1000, 2000, 3000, 4000, 5000, 10000}...), | ||||||
|  | 		Help:      "Histogram of the time (in milliseconds) each request took.", | ||||||
|  | 	}, []string{"zone"}) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // OnStartupMetrics sets up the metrics on startup. | ||||||
|  | func OnStartupMetrics() error { | ||||||
|  | 	metricsOnce.Do(func() { | ||||||
|  | 		prometheus.MustRegister(RequestDuration) | ||||||
|  | 	}) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var metricsOnce sync.Once | ||||||
|  |  | ||||||
|  | const subsystem = "httpproxy" | ||||||
							
								
								
									
										45
									
								
								middleware/httpproxy/proxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								middleware/httpproxy/proxy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | // Package httpproxy is middleware that proxies requests to a HTTPs server doing DNS. | ||||||
|  | package httpproxy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/coredns/middleware" | ||||||
|  | 	"github.com/miekg/coredns/request" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | 	"golang.org/x/net/context" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var errUnreachable = errors.New("unreachable backend") | ||||||
|  |  | ||||||
|  | // Proxy represents a middleware instance that can proxy requests to HTTPS servers. | ||||||
|  | type Proxy struct { | ||||||
|  | 	from string | ||||||
|  | 	e    Exchanger | ||||||
|  |  | ||||||
|  | 	Next middleware.Handler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ServeDNS satisfies the middleware.Handler interface. | ||||||
|  | func (p *Proxy) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { | ||||||
|  | 	start := time.Now() | ||||||
|  | 	state := request.Request{W: w, Req: r} | ||||||
|  |  | ||||||
|  | 	reply, backendErr := p.e.Exchange(r) | ||||||
|  |  | ||||||
|  | 	if backendErr == nil { | ||||||
|  | 		state.SizeAndDo(reply) | ||||||
|  |  | ||||||
|  | 		w.WriteMsg(reply) | ||||||
|  | 		RequestDuration.WithLabelValues(p.from).Observe(float64(time.Since(start) / time.Millisecond)) | ||||||
|  | 		return 0, nil | ||||||
|  | 	} | ||||||
|  | 	RequestDuration.WithLabelValues(p.from).Observe(float64(time.Since(start) / time.Millisecond)) | ||||||
|  |  | ||||||
|  | 	return dns.RcodeServerFailure, errUnreachable | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Name implements the Handler interface. | ||||||
|  | func (p Proxy) Name() string { return "httpproxy" } | ||||||
							
								
								
									
										96
									
								
								middleware/httpproxy/setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								middleware/httpproxy/setup.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | |||||||
|  | package httpproxy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/coredns/core/dnsserver" | ||||||
|  | 	"github.com/miekg/coredns/middleware" | ||||||
|  |  | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyfile" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	caddy.RegisterPlugin("httpproxy", caddy.Plugin{ | ||||||
|  | 		ServerType: "dns", | ||||||
|  | 		Action:     setup, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func setup(c *caddy.Controller) error { | ||||||
|  | 	p, err := httpproxyParse(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return middleware.Error("httpproxy", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler { | ||||||
|  | 		p.Next = next | ||||||
|  | 		return p | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	c.OnStartup(func() error { | ||||||
|  | 		OnStartupMetrics() | ||||||
|  | 		e := p.e.OnStartup() | ||||||
|  | 		if e != nil { | ||||||
|  | 			return middleware.Error("httpproxy", e) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  | 	c.OnShutdown(func() error { | ||||||
|  | 		e := p.e.OnShutdown() | ||||||
|  | 		if e != nil { | ||||||
|  | 			return middleware.Error("httpproxy", e) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func httpproxyParse(c *caddy.Controller) (*Proxy, error) { | ||||||
|  | 	var p = &Proxy{} | ||||||
|  |  | ||||||
|  | 	for c.Next() { | ||||||
|  | 		if !c.Args(&p.from) { | ||||||
|  | 			return p, c.ArgErr() | ||||||
|  | 		} | ||||||
|  | 		to := c.RemainingArgs() | ||||||
|  | 		if len(to) != 1 { | ||||||
|  | 			return p, c.ArgErr() | ||||||
|  | 		} | ||||||
|  | 		switch to[0] { | ||||||
|  | 		case "dns.google.com": | ||||||
|  | 			p.e = newGoogle() | ||||||
|  | 			u, _ := newSimpleUpstream([]string{"8.8.8.8:53", "8.8.4.4:53"}) | ||||||
|  | 			p.e.SetUpstream(u) | ||||||
|  | 		default: | ||||||
|  | 			return p, fmt.Errorf("unknown http proxy %q", to[0]) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for c.NextBlock() { | ||||||
|  | 			if err := parseBlock(&c.Dispenser, p); err != nil { | ||||||
|  | 				return p, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return p, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func parseBlock(c *caddyfile.Dispenser, p *Proxy) error { | ||||||
|  | 	switch c.Val() { | ||||||
|  | 	case "upstream": | ||||||
|  | 		upstreams := c.RemainingArgs() | ||||||
|  | 		if len(upstreams) == 0 { | ||||||
|  | 			return c.ArgErr() | ||||||
|  | 		} | ||||||
|  | 		u, err := newSimpleUpstream(upstreams) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		p.e.SetUpstream(u) | ||||||
|  | 	default: | ||||||
|  | 		return c.Errf("unknown property '%s'", c.Val()) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										68
									
								
								middleware/httpproxy/setup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								middleware/httpproxy/setup_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | |||||||
|  | package httpproxy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestSetupChaos(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		input              string | ||||||
|  | 		shouldErr          bool | ||||||
|  | 		expectedFrom       string // expected from. | ||||||
|  | 		expectedErrContent string // substring from the expected error. Empty for positive cases. | ||||||
|  | 	}{ | ||||||
|  | 		// ok | ||||||
|  | 		{ | ||||||
|  | 			`httpproxy . dns.google.com`, false, "", "", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`httpproxy . dns.google.com { | ||||||
|  | 				upstream 8.8.8.8:53 | ||||||
|  | 			}`, false, "", "", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`httpproxy . dns.google.com { | ||||||
|  | 				upstream resolv.conf | ||||||
|  | 			}`, false, "", "", | ||||||
|  | 		}, | ||||||
|  | 		// fail | ||||||
|  | 		{ | ||||||
|  | 			`httpproxy`, true, "", "Wrong argument count or unexpected line ending after", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`httpproxy . wns.google.com`, true, "", "unknown http proxy", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Write fake resolv.conf for test | ||||||
|  | 	err := ioutil.WriteFile("resolv.conf", []byte("nameserver 127.0.0.1\n"), 0600) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed to write test resolv.conf") | ||||||
|  | 	} | ||||||
|  | 	defer os.Remove("resolv.conf") | ||||||
|  |  | ||||||
|  | 	for i, test := range tests { | ||||||
|  | 		c := caddy.NewTestController("dns", test.input) | ||||||
|  | 		_, err := httpproxyParse(c) | ||||||
|  |  | ||||||
|  | 		if test.shouldErr && err == nil { | ||||||
|  | 			t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Logf("%q", err) | ||||||
|  | 			if !test.shouldErr { | ||||||
|  | 				t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if !strings.Contains(err.Error(), test.expectedErrContent) { | ||||||
|  | 				t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								middleware/httpproxy/tls.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								middleware/httpproxy/tls.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | package httpproxy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Exchanger is an interface that specifies a type implementing a DNS resolver that | ||||||
|  | // uses a HTTPS server. | ||||||
|  | type Exchanger interface { | ||||||
|  | 	Exchange(*dns.Msg) (*dns.Msg, error) | ||||||
|  |  | ||||||
|  | 	SetUpstream(*simpleUpstream) error | ||||||
|  | 	OnStartup() error | ||||||
|  | 	OnShutdown() error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newClient(sni string) *http.Client { | ||||||
|  | 	tls := &tls.Config{ServerName: sni} | ||||||
|  |  | ||||||
|  | 	c := &http.Client{ | ||||||
|  | 		Timeout:   time.Second * timeOut, | ||||||
|  | 		Transport: &http.Transport{TLSClientConfig: tls}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const timeOut = 5 | ||||||
							
								
								
									
										92
									
								
								middleware/httpproxy/upstream.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								middleware/httpproxy/upstream.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | package httpproxy | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"sync/atomic" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/coredns/middleware/pkg/dnsutil" | ||||||
|  | 	"github.com/miekg/coredns/middleware/proxy" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type simpleUpstream struct { | ||||||
|  | 	from   string | ||||||
|  | 	Hosts  proxy.HostPool | ||||||
|  | 	Policy proxy.Policy | ||||||
|  |  | ||||||
|  | 	FailTimeout time.Duration | ||||||
|  | 	MaxFails    int32 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // newSimpleUpstream return a new simpleUpstream initialized with the addresses. | ||||||
|  | func newSimpleUpstream(hosts []string) (*simpleUpstream, error) { | ||||||
|  | 	upstream := &simpleUpstream{ | ||||||
|  | 		Hosts:       nil, | ||||||
|  | 		Policy:      &proxy.Random{}, | ||||||
|  | 		FailTimeout: 10 * time.Second, | ||||||
|  | 		MaxFails:    3, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	toHosts, err := dnsutil.ParseHostPortOrFile(hosts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return upstream, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	upstream.Hosts = make([]*proxy.UpstreamHost, len(toHosts)) | ||||||
|  | 	for i, host := range toHosts { | ||||||
|  | 		uh := &proxy.UpstreamHost{ | ||||||
|  | 			Name:        host, | ||||||
|  | 			Conns:       0, | ||||||
|  | 			Fails:       0, | ||||||
|  | 			FailTimeout: upstream.FailTimeout, | ||||||
|  | 			Unhealthy:   false, | ||||||
|  |  | ||||||
|  | 			CheckDown: func(upstream *simpleUpstream) proxy.UpstreamHostDownFunc { | ||||||
|  | 				return func(uh *proxy.UpstreamHost) bool { | ||||||
|  | 					if uh.Unhealthy { | ||||||
|  | 						return true | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					fails := atomic.LoadInt32(&uh.Fails) | ||||||
|  | 					if fails >= upstream.MaxFails && upstream.MaxFails != 0 { | ||||||
|  | 						return true | ||||||
|  | 					} | ||||||
|  | 					return false | ||||||
|  | 				} | ||||||
|  | 			}(upstream), | ||||||
|  | 		} | ||||||
|  | 		upstream.Hosts[i] = uh | ||||||
|  | 	} | ||||||
|  | 	return upstream, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (u *simpleUpstream) From() string                   { return u.from } | ||||||
|  | func (u *simpleUpstream) Options() proxy.Options         { return proxy.Options{} } | ||||||
|  | func (u *simpleUpstream) IsAllowedPath(name string) bool { return true } | ||||||
|  |  | ||||||
|  | func (u *simpleUpstream) Select() *proxy.UpstreamHost { | ||||||
|  | 	pool := u.Hosts | ||||||
|  | 	if len(pool) == 1 { | ||||||
|  | 		if pool[0].Down() { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		return pool[0] | ||||||
|  | 	} | ||||||
|  | 	allDown := true | ||||||
|  | 	for _, host := range pool { | ||||||
|  | 		if !host.Down() { | ||||||
|  | 			allDown = false | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if allDown { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if u.Policy == nil { | ||||||
|  | 		h := (&proxy.Random{}).Select(pool) | ||||||
|  | 		return h | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	h := u.Policy.Select(pool) | ||||||
|  | 	return h | ||||||
|  | } | ||||||
| @@ -13,7 +13,7 @@ In its most basic form, a simple reverse proxy uses this syntax: | |||||||
| proxy FROM TO | proxy FROM TO | ||||||
| ~~~ | ~~~ | ||||||
|  |  | ||||||
| * **FROM** is the base path to match for the request to be proxied | * **FROM** is the base domain to match for the request to be proxied | ||||||
| * **TO** is the destination endpoint to proxy to | * **TO** is the destination endpoint to proxy to | ||||||
|  |  | ||||||
| However, advanced features including load balancing can be utilized with an expanded syntax: | However, advanced features including load balancing can be utilized with an expanded syntax: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user