middleware/proxy: multiple enhancements (#145)

Add port 53 in the proxy host if not specified.
Check if the host is actually an IP address (v4 or v6)
Remove the http headers and other TODOs
This commit is contained in:
Miek Gieben 2016-04-30 15:54:41 +01:00
parent 14b84ce02b
commit e635b4e773
7 changed files with 86 additions and 62 deletions

View file

@ -19,12 +19,12 @@ However, advanced features including load balancing can be utilized with an expa
~~~ ~~~
proxy from to... { proxy from to... {
policy random | least_conn | round_robin policy random | least_conn | round_robin
fail_timeout duration fail_timeout duration
max_fails integer max_fails integer
health_check path:port [duration] health_check path:port [duration]
except ignored_names... except ignored_names...
tcp ecs [v4 address/mask] [v6 address/mask] (TODO)
} }
~~~ ~~~
@ -35,8 +35,8 @@ proxy from to... {
* `max_fails` is the number of failures within fail_timeout that are needed before considering a backend to be down. If 0, the backend will never be marked as down. Default is 1. * `max_fails` is the number of failures within fail_timeout that are needed before considering a backend to be down. If 0, the backend will never be marked as down. Default is 1.
* `health_check` will check path (on port) on each backend. If a backend returns a status code of 200-399, then that backend is healthy. If it doesn't, the backend is marked as unhealthy for duration and no requests are routed to it. If this option is not provided then health checks are disabled. The default duration is 10 seconds ("10s"). * `health_check` will check path (on port) on each backend. If a backend returns a status code of 200-399, then that backend is healthy. If it doesn't, the backend is marked as unhealthy for duration and no requests are routed to it. If this option is not provided then health checks are disabled. The default duration is 10 seconds ("10s").
* `ignored_names...` is a space-separated list of paths to exclude from proxying. Requests that match any of these paths will be passed thru. * `ignored_names...` is a space-separated list of paths to exclude from proxying. Requests that match any of these paths will be passed thru.
* `tcp` use TCP for all upstream queries, otherwise it depends on the transport of the incoming * `ecs` add EDNS0 client submit metadata to the outgoing query. This can be optionally be followed
query. TODO(miek): implement. by an IPv4 and/or IPv6 address. If none is specified the server's addresses are used.
## Policies ## Policies

View file

@ -4,7 +4,6 @@ package proxy
// style as the proxy. // style as the proxy.
import ( import (
"net/http"
"sync/atomic" "sync/atomic"
"time" "time"
@ -17,22 +16,20 @@ func New(hosts []string) Proxy {
p := Proxy{Next: nil, Client: Clients()} p := Proxy{Next: nil, Client: Clients()}
upstream := &staticUpstream{ upstream := &staticUpstream{
from: "", from: "",
proxyHeaders: make(http.Header), Hosts: make([]*UpstreamHost, len(hosts)),
Hosts: make([]*UpstreamHost, len(hosts)), Policy: &Random{},
Policy: &Random{}, FailTimeout: 10 * time.Second,
FailTimeout: 10 * time.Second, MaxFails: 1,
MaxFails: 1,
} }
for i, host := range hosts { for i, host := range hosts {
uh := &UpstreamHost{ uh := &UpstreamHost{
Name: host, Name: host,
Conns: 0, Conns: 0,
Fails: 0, Fails: 0,
FailTimeout: upstream.FailTimeout, FailTimeout: upstream.FailTimeout,
Unhealthy: false, Unhealthy: false,
ExtraHeaders: upstream.proxyHeaders, // TODO(miek): fixer the fix
CheckDown: func(upstream *staticUpstream) UpstreamHostDownFunc { CheckDown: func(upstream *staticUpstream) UpstreamHostDownFunc {
return func(uh *UpstreamHost) bool { return func(uh *UpstreamHost) bool {
if uh.Unhealthy { if uh.Unhealthy {

View file

@ -3,14 +3,13 @@ package proxy
import ( import (
"errors" "errors"
"net/http"
"sync/atomic" "sync/atomic"
"time" "time"
"golang.org/x/net/context"
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/context"
) )
var errUnreachable = errors.New("unreachable backend") var errUnreachable = errors.New("unreachable backend")
@ -36,6 +35,8 @@ type Upstream interface {
Select() *UpstreamHost Select() *UpstreamHost
// Checks if subpdomain is not an ignored. // Checks if subpdomain is not an ignored.
IsAllowedPath(string) bool IsAllowedPath(string) bool
// Options returns the options set for this upstream
Options() Options
} }
// UpstreamHostDownFunc can be used to customize how Down behaves. // UpstreamHostDownFunc can be used to customize how Down behaves.
@ -48,7 +49,6 @@ type UpstreamHost struct {
Fails int32 Fails int32
FailTimeout time.Duration FailTimeout time.Duration
Unhealthy bool Unhealthy bool
ExtraHeaders http.Header
CheckDown UpstreamHostDownFunc CheckDown UpstreamHostDownFunc
WithoutPathPrefix string WithoutPathPrefix string
} }
@ -71,7 +71,6 @@ var tryDuration = 60 * time.Second
// ServeDNS satisfies the middleware.Handler interface. // ServeDNS satisfies the middleware.Handler interface.
func (p Proxy) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { func (p Proxy) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
for _, upstream := range p.Upstreams { for _, upstream := range p.Upstreams {
// allowed bla bla bla TODO(miek): fix full proxy spec from caddy
start := time.Now() start := time.Now()
// Since Select() should give us "up" hosts, keep retrying // Since Select() should give us "up" hosts, keep retrying
@ -81,8 +80,7 @@ func (p Proxy) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (
if host == nil { if host == nil {
return dns.RcodeServerFailure, errUnreachable return dns.RcodeServerFailure, errUnreachable
} }
// TODO(miek): PORT! reverseproxy := ReverseProxy{Host: host.Name, Client: p.Client, Options: upstream.Options()}
reverseproxy := ReverseProxy{Host: host.Name, Client: p.Client}
atomic.AddInt64(&host.Conns, 1) atomic.AddInt64(&host.Conns, 1)
backendErr := reverseproxy.ServeDNS(w, r, nil) backendErr := reverseproxy.ServeDNS(w, r, nil)

View file

@ -1,5 +1,10 @@
package proxy package proxy
// Also test these inputs:
//.:1053 {
//proxy . ::1 2001:4860:4860::8844 8.8.8.8:54 [2001:4860:4860::8845]:53
//}
/* /*
func init() { func init() {
tryDuration = 50 * time.Millisecond // prevent tests from hanging tryDuration = 50 * time.Millisecond // prevent tests from hanging

View file

@ -8,8 +8,9 @@ import (
) )
type ReverseProxy struct { type ReverseProxy struct {
Host string Host string
Client Client Client Client
Options Options
} }
func (p ReverseProxy) ServeDNS(w dns.ResponseWriter, r *dns.Msg, extra []dns.RR) error { func (p ReverseProxy) ServeDNS(w dns.ResponseWriter, r *dns.Msg, extra []dns.RR) error {
@ -17,12 +18,11 @@ func (p ReverseProxy) ServeDNS(w dns.ResponseWriter, r *dns.Msg, extra []dns.RR)
reply *dns.Msg reply *dns.Msg
err error err error
) )
state := middleware.State{W: w, Req: r}
// We forward the original request, no need to fiddle with EDNS0 opt sizes. switch {
if state.Proto() == "tcp" { case middleware.Proto(w) == "tcp":
reply, err = middleware.Exchange(p.Client.TCP, r, p.Host) reply, err = middleware.Exchange(p.Client.TCP, r, p.Host)
} else { default:
reply, err = middleware.Exchange(p.Client.UDP, r, p.Host) reply, err = middleware.Exchange(p.Client.UDP, r, p.Host)
} }

View file

@ -1,6 +1,7 @@
package proxy package proxy
import ( import (
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@ -19,11 +20,9 @@ var (
) )
type staticUpstream struct { type staticUpstream struct {
from string from string
// TODO(miek): allows use to added headers Hosts HostPool
proxyHeaders http.Header // TODO(miek): kill these Policy Policy
Hosts HostPool
Policy Policy
FailTimeout time.Duration FailTimeout time.Duration
MaxFails int32 MaxFails int32
@ -34,6 +33,10 @@ type staticUpstream struct {
} }
WithoutPathPrefix string WithoutPathPrefix string
IgnoredSubDomains []string IgnoredSubDomains []string
options Options
}
type Options struct {
Ecs []*net.IPNet // EDNS0 CLIENT SUBNET address (v4/v6) to add in CIDR notaton.
} }
// NewStaticUpstreams parses the configuration input and sets up // NewStaticUpstreams parses the configuration input and sets up
@ -42,12 +45,11 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
var upstreams []Upstream var upstreams []Upstream
for c.Next() { for c.Next() {
upstream := &staticUpstream{ upstream := &staticUpstream{
from: "", from: "",
proxyHeaders: make(http.Header), Hosts: nil,
Hosts: nil, Policy: &Random{},
Policy: &Random{}, FailTimeout: 10 * time.Second,
FailTimeout: 10 * time.Second, MaxFails: 1,
MaxFails: 1,
} }
if !c.Args(&upstream.from) { if !c.Args(&upstream.from) {
@ -57,6 +59,15 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
if len(to) == 0 { if len(to) == 0 {
return upstreams, c.ArgErr() return upstreams, c.ArgErr()
} }
for _, host := range to {
h, _, err := net.SplitHostPort(host)
if err != nil {
h = host
}
if x := net.ParseIP(h); x == nil {
return upstreams, fmt.Errorf("not an IP address: `%s'", h)
}
}
for c.NextBlock() { for c.NextBlock() {
if err := parseBlock(&c, upstream); err != nil { if err := parseBlock(&c, upstream); err != nil {
@ -67,12 +78,11 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
upstream.Hosts = make([]*UpstreamHost, len(to)) upstream.Hosts = make([]*UpstreamHost, len(to))
for i, host := range to { for i, host := range to {
uh := &UpstreamHost{ uh := &UpstreamHost{
Name: host, Name: defaultHostPort(host),
Conns: 0, Conns: 0,
Fails: 0, Fails: 0,
FailTimeout: upstream.FailTimeout, FailTimeout: upstream.FailTimeout,
Unhealthy: false, Unhealthy: false,
ExtraHeaders: upstream.proxyHeaders,
CheckDown: func(upstream *staticUpstream) UpstreamHostDownFunc { CheckDown: func(upstream *staticUpstream) UpstreamHostDownFunc {
return func(uh *UpstreamHost) bool { return func(uh *UpstreamHost) bool {
if uh.Unhealthy { if uh.Unhealthy {
@ -107,6 +117,10 @@ func (u *staticUpstream) From() string {
return u.from return u.from
} }
func (u *staticUpstream) Options() Options {
return u.options
}
func parseBlock(c *parse.Dispenser, u *staticUpstream) error { func parseBlock(c *parse.Dispenser, u *staticUpstream) error {
switch c.Val() { switch c.Val() {
case "policy": case "policy":
@ -153,12 +167,6 @@ func parseBlock(c *parse.Dispenser, u *staticUpstream) error {
} }
u.HealthCheck.Interval = dur u.HealthCheck.Interval = dur
} }
case "proxy_header":
var header, value string
if !c.Args(&header, &value) {
return c.ArgErr()
}
u.proxyHeaders.Add(header, value)
case "without": case "without":
if !c.NextArg() { if !c.NextArg() {
return c.ArgErr() return c.ArgErr()
@ -173,6 +181,12 @@ func parseBlock(c *parse.Dispenser, u *staticUpstream) error {
ignoredDomains[i] = strings.ToLower(dns.Fqdn(ignoredDomains[i])) ignoredDomains[i] = strings.ToLower(dns.Fqdn(ignoredDomains[i]))
} }
u.IgnoredSubDomains = ignoredDomains u.IgnoredSubDomains = ignoredDomains
case "ecs":
ips := c.RemainingArgs()
if len(ips) > 0 {
}
default: default:
return c.Errf("unknown property '%s'", c.Val()) return c.Errf("unknown property '%s'", c.Val())
} }
@ -247,3 +261,11 @@ func (u *staticUpstream) IsAllowedPath(name string) bool {
} }
return true return true
} }
func defaultHostPort(s string) string {
_, _, e := net.SplitHostPort(s)
if e == nil {
return s
}
return net.JoinHostPort(s, "53")
}

View file

@ -53,13 +53,15 @@ func (s *State) RemoteAddr() string {
return s.W.RemoteAddr().String() return s.W.RemoteAddr().String()
} }
// Proto gets the protocol used as the transport. This // Proto gets the protocol used as the transport. This will be udp or tcp.
// will be udp or tcp. func (s *State) Proto() string { return Proto(s.W) }
func (s *State) Proto() string {
if _, ok := s.W.RemoteAddr().(*net.UDPAddr); ok { // Proto gets the protocol used as the transport. This will be udp or tcp.
func Proto(w dns.ResponseWriter) string {
if _, ok := w.RemoteAddr().(*net.UDPAddr); ok {
return "udp" return "udp"
} }
if _, ok := s.W.RemoteAddr().(*net.TCPAddr); ok { if _, ok := w.RemoteAddr().(*net.TCPAddr); ok {
return "tcp" return "tcp"
} }
return "udp" return "udp"