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:
parent
f8b9332265
commit
96222927a3
13 changed files with 731 additions and 2 deletions
|
@ -15,6 +15,7 @@ import (
|
|||
_ "github.com/miekg/coredns/middleware/etcd"
|
||||
_ "github.com/miekg/coredns/middleware/file"
|
||||
_ "github.com/miekg/coredns/middleware/health"
|
||||
_ "github.com/miekg/coredns/middleware/httpproxy"
|
||||
_ "github.com/miekg/coredns/middleware/kubernetes"
|
||||
_ "github.com/miekg/coredns/middleware/loadbalance"
|
||||
_ "github.com/miekg/coredns/middleware/log"
|
||||
|
|
|
@ -94,5 +94,6 @@ var directives = []string{
|
|||
"etcd",
|
||||
"kubernetes",
|
||||
"proxy",
|
||||
"httpproxy",
|
||||
"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)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
|
|
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
|
||||
~~~
|
||||
|
||||
* **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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue