-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
CNAME flattening #3403
Changes from 15 commits
a26686f
a1314de
97b75b6
9224ec2
1b914f0
2ea89ce
b0db833
81c7aa9
3bfd0e6
bace07f
cd67799
4b0b352
dbea009
9853a93
0000940
89ff204
25e283f
60ffc16
927095e
2a66649
6fafc3c
f143cfb
6d0922a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
for `HTTP-01` challenge and `onDemand` is mandatory. Refer to [ACME configuration](/configuration/acme) for more information. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please only write one sentence by line. |
||
|
||
## Override Default Configuration Template | ||
|
||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please replace |
||
type CNAMEResolv struct { | ||
TTL int | ||
Record string | ||
} | ||
|
||
// CNAMEFlatten check if CNAME records is exists, flatten if possible | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please modify in |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assuming the record is like
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The method seems to be used only on this file. |
||
config, _ := dns.ClientConfigFromFile(hr.ResolvConfig) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
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 | ||
} |
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) | ||
} | ||
}) | ||
|
||
} | ||
} |
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 |
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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Is it possible to add a test to check if CNAME flattening works or is it hard to check? |
||
c.Assert(err, checker.IsNil) | ||
} |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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 | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYT to add a |
||
} else { | ||
for _, host := range hosts { | ||
if types.CanonicalDomain(reqHost) == types.CanonicalDomain(host) { | ||
return true | ||
} | ||
} | ||
} | ||
return false | ||
|
There was a problem hiding this comment.
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.