Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CNAME flattening #3403

Merged
merged 23 commits into from
Jul 3, 2018
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,7 @@
non-go = true
go-tests = true
unused-packages = true

[[constraint]]
name = "github.com/patrickmn/go-cache"
version = "2.1.0"
7 changes: 7 additions & 0 deletions cmd/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,12 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
},
}

defaultResolver := configuration.HostResolverConfig{
CnameFlattening: false,
ResolvConfig: "/etc/resolv.conf",
ResolvDepth: 5,
}

defaultConfiguration := configuration.GlobalConfiguration{
Docker: &defaultDocker,
File: &defaultFile,
Expand Down Expand Up @@ -296,6 +302,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
API: &defaultAPI,
Metrics: &defaultMetrics,
Tracing: &defaultTracing,
HostResolver: &defaultResolver,
}

return &TraefikConfiguration{
Expand Down
8 changes: 8 additions & 0 deletions configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type GlobalConfiguration struct {
API *api.Handler `description:"Enable api/dashboard" export:"true"`
Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"`
Ping *ping.Handler `description:"Enable ping" export:"true"`
HostResolver *HostResolverConfig `description:"Enable CNAME Flattening" export:"true"`
}

// WebCompatibility is a configuration to handle compatibility with deprecated web provider options
Expand Down Expand Up @@ -508,3 +509,10 @@ type LifeCycle struct {
RequestAcceptGraceTimeout flaeg.Duration `description:"Duration to keep accepting requests before Traefik initiates the graceful shutdown procedure"`
GraceTimeOut flaeg.Duration `description:"Duration to give active requests a chance to finish before Traefik stops"`
}

// HostResolverConfig contain configuration for CNAME Flattening
type HostResolverConfig struct {
CnameFlattening bool `description:"A flag to enable/disable CNAME flattening" export:"true"`
ResolvConfig string `description:"resolv.conf used for DNS resolving" export:"true"`
ResolvDepth int `description:"The maximal depth of DNS recursive resolving" export:"true"`
}
29 changes: 29 additions & 0 deletions docs/configuration/commons.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,35 @@ If no units are provided, the value is parsed assuming seconds.
idleTimeout = "360s"
```

## Host Resolver

`hostResolver` are used for request host matching process.

```toml
[hostResolver]

# cnameFlattening is a trigger to flatten request host, assuming it is a CNAME record
#
# Optional
# Default : false
cnameFlattening = true

# resolvConf is dns resolving configuration file, the default is /etc/resolv.conf
#
# Optional
# Default : "/etc/resolv.conf"
resolvConf = "/etc/resolv.conf"

# resolvDepth is the maximum CNAME recursive lookup
#
# Optional
# Default : 5
resolvDepth = 5

```

- To allow serving secure https request and generate the SSL using ACME while `cnameFlattening` is set to `true`. The `acme` configuration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please only write one sentence by line.

for `HTTP-01` challenge and `onDemand` is mandatory. Refer to [ACME configuration](/configuration/acme) for more information.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please only write one sentence by line.


## Override Default Configuration Template

Expand Down
89 changes: 89 additions & 0 deletions hostresolver/hostresolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package hostresolver

import (
"net"
"sort"
"strconv"
"strings"
"time"

"github.com/containous/traefik/log"
"github.com/miekg/dns"
"github.com/patrickmn/go-cache"
)

// HostResolver used for host resolver
type HostResolver struct {
Enabled bool
Cache *cache.Cache
ResolvConfig string
ResolvDepth int
}

// CNAMEResolv used for storing CNAME result
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace for storing by to store

type CNAMEResolv struct {
TTL int
Record string
}

// CNAMEFlatten check if CNAME records is exists, flatten if possible
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CNAMEFlatten check if CNAME records is exists,

Please modify in CNAMEFlatten check if CNAME records exists.

func (hr *HostResolver) CNAMEFlatten(host string) (string, string) {
var result []string
result = append(result, host)
request := host
if hr.Cache == nil {
hr.Cache = cache.New(30*time.Minute, 5*time.Minute)
}
rst, found := hr.Cache.Get(host)
if found {
result = strings.Split(rst.(string), ",")
} else {
var cacheDuration = 0 * time.Second
for i := 0; i < hr.ResolvDepth; i++ {
r := hr.CNAMEResolve(request)
if r != nil {
result = append(result, r.Record)
if i == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why do we override the TTL only if the answer comes from the first level please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the record is like

sub.userA.com TTL IN CNAME host.domain.com
host.domain.com TTL IN CNAME server.domainB.com

Whilst the one we serve is server.domainB.com. The CNAMEFlatten will result sub.userA.com mapped to server.domainB.com, and because it will be cached, then we use the first record highest TTL. The TTL of host.domain.com will be ignored, because if it changed(not pointing to server.domainB.com), then we won't need to serve sub.userA.com anymore.

cacheDuration = time.Duration(r.TTL) * time.Second
}
request = r.Record
} else {
break
}
}
hr.Cache.Add(host, strings.Join(result, ","), cacheDuration)
}
return result[0], result[len(result)-1]
}

// CNAMEResolve resolve CNAME if exists, and return with the highest TTL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you correct the comment please: CNAMEResolve resolves CNAME if exists, and returns with the highest TTL ?

func (hr *HostResolver) CNAMEResolve(host string) *CNAMEResolv {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method seems to be used only on this file.
WDYT to make it private?

config, _ := dns.ClientConfigFromFile(hr.ResolvConfig)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you manage the error please?

c := dns.Client{Timeout: 30 * time.Second}
c.Timeout = 30 * time.Second
m := &dns.Msg{}
m.SetQuestion(dns.Fqdn(host), dns.TypeCNAME)
var result []*CNAMEResolv
for i := 0; i < len(config.Servers); i++ {
r, _, err := c.Exchange(m, net.JoinHostPort(config.Servers[i], config.Port))
if err != nil {
log.Errorf("Failed to resolve host %s with server %s", host, config.Servers[i])
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to contin ue to precces the statement in the loop if there is an error?
WDYT to add a instructioncontinue if err != nil?

if r != nil && len(r.Answer) > 0 {
temp := strings.Fields(r.Answer[0].String())
ttl, _ := strconv.Atoi(temp[1])
tempRecord := &CNAMEResolv{
TTL: ttl,
Record: strings.TrimSuffix(strings.TrimSpace(temp[len(temp)-1]), "."),
}
result = append(result, tempRecord)
}
}
if len(result) > 0 {
sort.Slice(result, func(i, j int) bool {
return result[i].TTL > result[j].TTL
})
return result[0]
}
return nil
}
52 changes: 52 additions & 0 deletions hostresolver/hostresolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package hostresolver

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestCNAMEFlatten(t *testing.T) {
hostResolver := &HostResolver{
Enabled: false,
ResolvConfig: "/etc/resolv.conf",
ResolvDepth: 5,
}

testCase := []struct {
desc string
domain string
expectedDomain string
isCNAME bool
}{
{
desc: "host request is CNAME record",
domain: "www.github.com",
expectedDomain: "github.com",
isCNAME: true,
},
{
desc: "host request is not CNAME record",
domain: "github.com",
expectedDomain: "github.com",
isCNAME: false,
},
}
for _, test := range testCase {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()

reqH, flatH := hostResolver.CNAMEFlatten(test.domain)
if test.isCNAME {
assert.Equal(t, test.domain, reqH)
assert.Equal(t, test.expectedDomain, flatH)
assert.NotEqual(t, test.expectedDomain, reqH)
} else {
assert.Equal(t, test.domain, reqH)
assert.Equal(t, test.expectedDomain, flatH)
assert.Equal(t, test.expectedDomain, reqH)
}
})

}
}
16 changes: 16 additions & 0 deletions integration/fixtures/simple_hostresolver.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
logLevel = "DEBUG"
defaultEntryPoints = ["http"]

[entryPoints]
[entryPoints.http]
address = ":8000"

[api]

[docker]
exposedByDefault = false
domain = "docker.local"
watch = true

[hostResolver]
cnameFlattening = true
35 changes: 35 additions & 0 deletions integration/hostresolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package integration

import (
"net/http"
"time"

"github.com/containous/traefik/integration/try"
"github.com/go-check/check"
checker "github.com/vdemeester/shakers"
)

type HostResolverSuite struct{ BaseSuite }

func (s *HostResolverSuite) SetUpSuite(c *check.C) {
s.createComposeProject(c, "hostresolver")

s.composeProject.Start(c)
s.composeProject.Container(c, "server1")
}

func (s *HostResolverSuite) TestSimpleConfig(c *check.C) {
cmd, display := s.traefikCmd(withConfigFile("fixtures/simple_hostresolver.toml"))
defer display(c)

err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()

req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil)
req.Host = "frontend1.docker.local"

err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.HasBody())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, but this test only checks if the usual behavior is the same if the cnameflattening is enabled but it does not test if this one really works, it isn't?

Is it possible to add a test to check if CNAME flattening works or is it hard to check?

c.Assert(err, checker.IsNil)
}
1 change: 1 addition & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func init() {
check.Suite(&FileSuite{})
check.Suite(&GRPCSuite{})
check.Suite(&HealthCheckSuite{})
check.Suite(&HostResolverSuite{})
check.Suite(&HTTPSSuite{})
check.Suite(&LogRotationSuite{})
check.Suite(&MarathonSuite{})
Expand Down
8 changes: 8 additions & 0 deletions integration/resources/compose/hostresolver.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
server1:
image: emilevauge/whoami
labels:
- traefik.enable=true
- traefik.port=80
- traefik.backend=backend1
- traefik.frontend.entryPoints=http
- traefik.frontend.rule=Host:frontend1.docker.local
21 changes: 16 additions & 5 deletions rules/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import (

"github.com/BurntSushi/ty/fun"
"github.com/containous/mux"
"github.com/containous/traefik/hostresolver"
"github.com/containous/traefik/types"
)

// Rules holds rule parsing and configuration
type Rules struct {
Route *types.ServerRoute
err error
Route *types.ServerRoute
err error
HostResolver *hostresolver.HostResolver
}

func (r *Rules) host(hosts ...string) *mux.Route {
Expand All @@ -26,9 +28,18 @@ func (r *Rules) host(hosts ...string) *mux.Route {
if err != nil {
reqHost = req.Host
}
for _, host := range hosts {
if types.CanonicalDomain(reqHost) == types.CanonicalDomain(host) {
return true
if r.HostResolver != nil && r.HostResolver.Enabled {
reqH, flatH := r.HostResolver.CNAMEFlatten(types.CanonicalDomain(reqHost))
for _, host := range hosts {
if types.CanonicalDomain(reqH) == types.CanonicalDomain(host) || types.CanonicalDomain(flatH) == types.CanonicalDomain(host) {
return true
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT to add a DEBUG log to indicate to user that the CNAMEFlattening did not allow resolving his domain?

} else {
for _, host := range hosts {
if types.CanonicalDomain(reqHost) == types.CanonicalDomain(host) {
return true
}
}
}
return false
Expand Down