aboutsummaryrefslogtreecommitdiff
path: root/middleware
diff options
context:
space:
mode:
authorGravatar Miek Gieben <miek@miek.nl> 2016-11-26 17:57:22 +0000
committerGravatar GitHub <noreply@github.com> 2016-11-26 17:57:22 +0000
commit96222927a3309570b7ac7c618a28731f52845544 (patch)
treebcd3efd7c666c204b709890243e80cb5d8ee96ff /middleware
parentf8b93322659fee6d0456d2da920d92b35cae9273 (diff)
downloadcoredns-96222927a3309570b7ac7c618a28731f52845544.tar.gz
coredns-96222927a3309570b7ac7c618a28731f52845544.tar.zst
coredns-96222927a3309570b7ac7c618a28731f52845544.zip
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.
Diffstat (limited to 'middleware')
-rw-r--r--middleware/cache/handler.go2
-rw-r--r--middleware/httpproxy/README.md50
-rw-r--r--middleware/httpproxy/google.go307
-rw-r--r--middleware/httpproxy/google_test.go5
-rw-r--r--middleware/httpproxy/metrics.go32
-rw-r--r--middleware/httpproxy/proxy.go45
-rw-r--r--middleware/httpproxy/setup.go96
-rw-r--r--middleware/httpproxy/setup_test.go68
-rw-r--r--middleware/httpproxy/tls.go32
-rw-r--r--middleware/httpproxy/upstream.go92
-rw-r--r--middleware/proxy/README.md2
11 files changed, 729 insertions, 2 deletions
diff --git a/middleware/cache/handler.go b/middleware/cache/handler.go
index aae7206e2..77a0cea48 100644
--- a/middleware/cache/handler.go
+++ b/middleware/cache/handler.go
@@ -22,7 +22,7 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
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 {
diff --git a/middleware/httpproxy/README.md b/middleware/httpproxy/README.md
new file mode 100644
index 000000000..026fbdc71
--- /dev/null
+++ b/middleware/httpproxy/README.md
@@ -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
+}
+~~~
diff --git a/middleware/httpproxy/google.go b/middleware/httpproxy/google.go
new file mode 100644
index 000000000..78b5ea864
--- /dev/null
+++ b/middleware/httpproxy/google.go
@@ -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"
+)
diff --git a/middleware/httpproxy/google_test.go b/middleware/httpproxy/google_test.go
new file mode 100644
index 000000000..bd435a6ff
--- /dev/null
+++ b/middleware/httpproxy/google_test.go
@@ -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.
diff --git a/middleware/httpproxy/metrics.go b/middleware/httpproxy/metrics.go
new file mode 100644
index 000000000..c3822e523
--- /dev/null
+++ b/middleware/httpproxy/metrics.go
@@ -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"
diff --git a/middleware/httpproxy/proxy.go b/middleware/httpproxy/proxy.go
new file mode 100644
index 000000000..6b1243dff
--- /dev/null
+++ b/middleware/httpproxy/proxy.go
@@ -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" }
diff --git a/middleware/httpproxy/setup.go b/middleware/httpproxy/setup.go
new file mode 100644
index 000000000..01094c908
--- /dev/null
+++ b/middleware/httpproxy/setup.go
@@ -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
+}
diff --git a/middleware/httpproxy/setup_test.go b/middleware/httpproxy/setup_test.go
new file mode 100644
index 000000000..71d631220
--- /dev/null
+++ b/middleware/httpproxy/setup_test.go
@@ -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)
+ }
+ }
+ }
+}
diff --git a/middleware/httpproxy/tls.go b/middleware/httpproxy/tls.go
new file mode 100644
index 000000000..9651ac1c6
--- /dev/null
+++ b/middleware/httpproxy/tls.go
@@ -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
diff --git a/middleware/httpproxy/upstream.go b/middleware/httpproxy/upstream.go
new file mode 100644
index 000000000..f12de51fb
--- /dev/null
+++ b/middleware/httpproxy/upstream.go
@@ -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
+}
diff --git a/middleware/proxy/README.md b/middleware/proxy/README.md
index 837f166fc..e59a5b060 100644
--- a/middleware/proxy/README.md
+++ b/middleware/proxy/README.md
@@ -13,7 +13,7 @@ In its most basic form, a simple reverse proxy uses this syntax:
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
However, advanced features including load balancing can be utilized with an expanded syntax: