diff --git a/pkg/controller/cache.go b/pkg/controller/cache.go index 2c676cfdb..e55714ad6 100644 --- a/pkg/controller/cache.go +++ b/pkg/controller/cache.go @@ -50,6 +50,18 @@ func (c *cache) GetEndpoints(service *api.Service) (*api.Endpoints, error) { return &ep, err } +func (c *cache) GetTerminatingPods(service *api.Service) ([]*api.Pod, error) { + pods, err := c.listers.Pod.GetTerminatingServicePods(service) + if err != nil { + return []*api.Pod{}, err + } + podRef := make([]*api.Pod, len(pods)) + for i := range pods { + podRef[i] = &pods[i] + } + return podRef, err +} + func (c *cache) GetPod(podName string) (*api.Pod, error) { sname := strings.Split(podName, "/") if len(sname) != 2 { diff --git a/pkg/converters/ingress/annotations/backend.go b/pkg/converters/ingress/annotations/backend.go index f0c8c9791..951f5238a 100644 --- a/pkg/converters/ingress/annotations/backend.go +++ b/pkg/converters/ingress/annotations/backend.go @@ -18,6 +18,8 @@ package annotations import ( "fmt" + "net" + "regexp" "strconv" "strings" @@ -47,7 +49,7 @@ func (c *updater) buildBackendAffinity(d *backData) { } d.backend.Cookie.Name = name d.backend.Cookie.Strategy = strategy - d.backend.Cookie.Key = d.ann.CookieKey + d.backend.Cookie.Dynamic = d.ann.SessionCookieDynamic } func (c *updater) buildBackendAuthHTTP(d *backData) { @@ -80,7 +82,14 @@ func (c *updater) buildBackendAuthHTTP(d *backData) { c.logger.Warn("userlist on %v for basic authentication is empty", d.ann.Source) } } - d.backend.HreqValidateUserlist(userlist) + d.backend.Userlist.Name = userlist.Name + realm := "localhost" // HAProxy's backend name would be used if missing + if strings.Index(d.ann.AuthRealm, `"`) >= 0 { + c.logger.Warn("ignoring auth-realm with quotes on %v", d.ann.Source) + } else if d.ann.AuthRealm != "" { + realm = d.ann.AuthRealm + } + d.backend.Userlist.Realm = realm } func (c *updater) buildBackendAuthHTTPExtractUserlist(source, secret, users string) ([]hatypes.User, []error) { @@ -167,6 +176,10 @@ func (c *updater) buildBackendBlueGreen(d *backData) { deployWeights = append(deployWeights, dw) } for _, ep := range d.backend.Endpoints { + if ep.Weight == 0 { + // Draining endpoint, remove from blue/green calc + continue + } hasLabel := false if pod, err := c.cache.GetPod(ep.TargetRef); err == nil { for _, dw := range deployWeights { @@ -261,3 +274,138 @@ func (c *updater) buildBackendBlueGreen(d *backData) { } } } + +var ( + corsOriginRegex = regexp.MustCompile(`^(https?://[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`) + corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`) + corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`) +) + +func (c *updater) buildBackendCors(d *backData) { + if !d.ann.CorsEnable { + return + } + d.backend.Cors.Enabled = true + if d.ann.CorsAllowOrigin != "" && corsOriginRegex.MatchString(d.ann.CorsAllowOrigin) { + d.backend.Cors.AllowOrigin = d.ann.CorsAllowOrigin + } else { + d.backend.Cors.AllowOrigin = "*" + } + if corsHeadersRegex.MatchString(d.ann.CorsAllowHeaders) { + d.backend.Cors.AllowHeaders = d.ann.CorsAllowHeaders + } else { + d.backend.Cors.AllowHeaders = + "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + } + if corsMethodsRegex.MatchString(d.ann.CorsAllowMethods) { + d.backend.Cors.AllowMethods = d.ann.CorsAllowMethods + } else { + d.backend.Cors.AllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS" + } + d.backend.Cors.AllowCredentials = d.ann.CorsAllowCredentials + if d.ann.CorsMaxAge > 0 { + d.backend.Cors.MaxAge = d.ann.CorsMaxAge + } else { + d.backend.Cors.MaxAge = 86400 + } + if corsHeadersRegex.MatchString(d.ann.CorsExposeHeaders) { + d.backend.Cors.ExposeHeaders = d.ann.CorsExposeHeaders + } +} + +var ( + oauthHeaderRegex = regexp.MustCompile(`^[A-Za-z0-9-]+:[A-Za-z0-9-_]+$`) +) + +func (c *updater) buildOAuth(d *backData) { + if d.ann.OAuth == "" { + return + } + if d.ann.OAuth != "oauth2_proxy" { + c.logger.Warn("ignoring invalid oauth implementation '%s' on %v", d.ann.OAuth, d.ann.Source) + return + } + uriPrefix := "/oauth2" + headers := []string{"X-Auth-Request-Email:auth_response_email"} + if d.ann.OAuthURIPrefix != "" { + uriPrefix = d.ann.OAuthURIPrefix + } + if d.ann.OAuthHeaders != "" { + headers = strings.Split(d.ann.OAuthHeaders, ",") + } + uriPrefix = strings.TrimRight(uriPrefix, "/") + namespace := d.ann.Source.Namespace + backend := c.findBackend(namespace, uriPrefix) + if backend == nil { + c.logger.Error("path '%s' was not found on namespace '%s'", uriPrefix, namespace) + return + } + headersMap := make(map[string]string, len(headers)) + for _, header := range headers { + if len(header) == 0 { + continue + } + if !oauthHeaderRegex.MatchString(header) { + c.logger.Warn("invalid header format '%s' on %v", header, d.ann.Source) + continue + } + h := strings.Split(header, ":") + headersMap[h[0]] = h[1] + } + d.backend.OAuth.Impl = d.ann.OAuth + d.backend.OAuth.BackendName = backend.ID + d.backend.OAuth.URIPrefix = uriPrefix + d.backend.OAuth.Headers = headersMap +} + +func (c *updater) findBackend(namespace, uriPrefix string) *hatypes.Backend { + for _, host := range c.haproxy.Hosts() { + for _, path := range host.Paths { + if strings.TrimRight(path.Path, "/") == uriPrefix && path.Backend.Namespace == namespace { + return path.Backend + } + } + } + return nil +} + +var ( + rewriteURLRegex = regexp.MustCompile(`^[^"' ]+$`) +) + +func (c *updater) buildRewriteURL(d *backData) { + if d.ann.RewriteTarget == "" { + return + } + if !rewriteURLRegex.MatchString(d.ann.RewriteTarget) { + c.logger.Warn("rewrite-target does not allow white spaces or single/double quotes on %v", d.ann.Source) + return + } + d.backend.RewriteURL = d.ann.RewriteTarget +} + +func (c *updater) buildWAF(d *backData) { + if d.ann.WAF == "" { + return + } + if d.ann.WAF != "modsecurity" { + c.logger.Warn("ignoring invalid WAF mode: %s", d.ann.WAF) + return + } + d.backend.WAF = d.ann.WAF +} + +func (c *updater) buildWhitelist(d *backData) { + if d.ann.WhitelistSourceRange == "" { + return + } + var cidrlist []string + for _, cidr := range strings.Split(d.ann.WhitelistSourceRange, ",") { + if _, _, err := net.ParseCIDR(cidr); err != nil { + c.logger.Warn("skipping invalid cidr '%s' in whitelist config on %v", cidr, d.ann.Source) + } else { + cidrlist = append(cidrlist, cidr) + } + } + d.backend.Whitelist = cidrlist +} diff --git a/pkg/converters/ingress/annotations/backend_test.go b/pkg/converters/ingress/annotations/backend_test.go index 681e33679..121617543 100644 --- a/pkg/converters/ingress/annotations/backend_test.go +++ b/pkg/converters/ingress/annotations/backend_test.go @@ -18,6 +18,7 @@ package annotations import ( "reflect" + "strconv" "strings" "testing" @@ -71,14 +72,14 @@ func TestAffinity(t *testing.T) { }, // 6 { - ann: types.BackendAnnotations{Affinity: "cookie", SessionCookieStrategy: "prefix"}, - expCookie: hatypes.Cookie{Name: "INGRESSCOOKIE", Strategy: "prefix"}, + ann: types.BackendAnnotations{Affinity: "cookie", SessionCookieStrategy: "prefix", SessionCookieDynamic: true}, + expCookie: hatypes.Cookie{Name: "INGRESSCOOKIE", Strategy: "prefix", Dynamic: true}, expLogging: "", }, // 7 { - ann: types.BackendAnnotations{Affinity: "cookie", CookieKey: "ha"}, - expCookie: hatypes.Cookie{Name: "INGRESSCOOKIE", Strategy: "insert", Key: "ha"}, + ann: types.BackendAnnotations{Affinity: "cookie", SessionCookieDynamic: false}, + expCookie: hatypes.Cookie{Name: "INGRESSCOOKIE", Strategy: "insert", Dynamic: false}, expLogging: "", }, } @@ -98,13 +99,12 @@ func TestAffinity(t *testing.T) { func TestAuthHTTP(t *testing.T) { testCase := []struct { - namespace string - ingname string - ann types.BackendAnnotations - secrets ing_helper.SecretContent - expUserlists []*hatypes.Userlist - expHTTPRequests []*hatypes.HTTPRequest - expLogging string + namespace string + ingname string + ann types.BackendAnnotations + secrets ing_helper.SecretContent + expUserlists []*hatypes.Userlist + expLogging string }{ // 0 { @@ -134,25 +134,32 @@ func TestAuthHTTP(t *testing.T) { }, // 5 { - namespace: "ns1", - ingname: "i1", - ann: types.BackendAnnotations{AuthType: "basic", AuthSecret: "mypwd"}, - secrets: ing_helper.SecretContent{"ns1/mypwd": {"auth": []byte{}}}, - expUserlists: []*hatypes.Userlist{&hatypes.Userlist{Name: "ns1_mypwd"}}, - expHTTPRequests: []*hatypes.HTTPRequest{{}}, - expLogging: "WARN userlist on ingress 'ns1/i1' for basic authentication is empty", + ann: types.BackendAnnotations{AuthType: "basic", AuthSecret: "mypwd", AuthRealm: `"a name"`}, + secrets: ing_helper.SecretContent{"default/mypwd": {"auth": []byte("usr1::clear1")}}, + expUserlists: []*hatypes.Userlist{&hatypes.Userlist{Name: "default_mypwd", Users: []hatypes.User{ + {Name: "usr1", Passwd: "clear1", Encrypted: false}, + }}}, + expLogging: "WARN ignoring auth-realm with quotes on ingress 'default/ing1'", }, // 6 { - ann: types.BackendAnnotations{AuthType: "basic", AuthSecret: "basicpwd"}, - secrets: ing_helper.SecretContent{"default/basicpwd": {"auth": []byte("fail")}}, - expUserlists: []*hatypes.Userlist{&hatypes.Userlist{Name: "default_basicpwd"}}, - expHTTPRequests: []*hatypes.HTTPRequest{{}}, + namespace: "ns1", + ingname: "i1", + ann: types.BackendAnnotations{AuthType: "basic", AuthSecret: "mypwd"}, + secrets: ing_helper.SecretContent{"ns1/mypwd": {"auth": []byte{}}}, + expUserlists: []*hatypes.Userlist{&hatypes.Userlist{Name: "ns1_mypwd"}}, + expLogging: "WARN userlist on ingress 'ns1/i1' for basic authentication is empty", + }, + // 7 + { + ann: types.BackendAnnotations{AuthType: "basic", AuthSecret: "basicpwd"}, + secrets: ing_helper.SecretContent{"default/basicpwd": {"auth": []byte("fail")}}, + expUserlists: []*hatypes.Userlist{&hatypes.Userlist{Name: "default_basicpwd"}}, expLogging: ` WARN ignoring malformed usr/passwd on secret 'default/basicpwd', declared on ingress 'default/ing1': missing password of user 'fail' line 1 WARN userlist on ingress 'default/ing1' for basic authentication is empty`, }, - // 7 + // 8 { ann: types.BackendAnnotations{AuthType: "basic", AuthSecret: "basicpwd"}, secrets: ing_helper.SecretContent{"default/basicpwd": {"auth": []byte(` @@ -161,10 +168,9 @@ nopwd`)}}, expUserlists: []*hatypes.Userlist{&hatypes.Userlist{Name: "default_basicpwd", Users: []hatypes.User{ {Name: "usr1", Passwd: "clearpwd1", Encrypted: false}, }}}, - expHTTPRequests: []*hatypes.HTTPRequest{{}}, - expLogging: "WARN ignoring malformed usr/passwd on secret 'default/basicpwd', declared on ingress 'default/ing1': missing password of user 'nopwd' line 3", + expLogging: "WARN ignoring malformed usr/passwd on secret 'default/basicpwd', declared on ingress 'default/ing1': missing password of user 'nopwd' line 3", }, - // 8 + // 9 { ann: types.BackendAnnotations{AuthType: "basic", AuthSecret: "basicpwd"}, secrets: ing_helper.SecretContent{"default/basicpwd": {"auth": []byte(` @@ -172,8 +178,7 @@ usrnopwd1: usrnopwd2:: :encpwd3 ::clearpwd4`)}}, - expUserlists: []*hatypes.Userlist{&hatypes.Userlist{Name: "default_basicpwd"}}, - expHTTPRequests: []*hatypes.HTTPRequest{{}}, + expUserlists: []*hatypes.Userlist{&hatypes.Userlist{Name: "default_basicpwd"}}, expLogging: ` WARN ignoring malformed usr/passwd on secret 'default/basicpwd', declared on ingress 'default/ing1': missing password of user 'usrnopwd1' line 2 WARN ignoring malformed usr/passwd on secret 'default/basicpwd', declared on ingress 'default/ing1': missing password of user 'usrnopwd2' line 3 @@ -181,7 +186,7 @@ WARN ignoring malformed usr/passwd on secret 'default/basicpwd', declared on ing WARN ignoring malformed usr/passwd on secret 'default/basicpwd', declared on ingress 'default/ing1': missing username line 5 WARN userlist on ingress 'default/ing1' for basic authentication is empty`, }, - // 9 + // 10 { ann: types.BackendAnnotations{AuthType: "basic", AuthSecret: "basicpwd"}, secrets: ing_helper.SecretContent{"default/basicpwd": {"auth": []byte(` @@ -191,8 +196,7 @@ usr2::clearpwd2`)}}, {Name: "usr1", Passwd: "encpwd1", Encrypted: true}, {Name: "usr2", Passwd: "clearpwd2", Encrypted: false}, }}}, - expHTTPRequests: []*hatypes.HTTPRequest{{}}, - expLogging: "", + expLogging: "", }, } @@ -210,13 +214,9 @@ usr2::clearpwd2`)}}, d := c.createBackendData(test.namespace, test.ingname, &test.ann) u.buildBackendAuthHTTP(d) userlists := u.haproxy.Userlists() - httpRequests := d.backend.HTTPRequests if len(userlists)+len(test.expUserlists) > 0 && !reflect.DeepEqual(test.expUserlists, userlists) { t.Errorf("userlists config %d differs - expected: %+v - actual: %+v", i, test.expUserlists, userlists) } - if len(httpRequests)+len(test.expHTTPRequests) > 0 && !reflect.DeepEqual(test.expHTTPRequests, httpRequests) { - t.Errorf("httprequest config %d differs - expected: %+v - actual: %+v", i, test.expHTTPRequests, httpRequests) - } c.logger.CompareLogging(test.expLogging) c.teardown() } @@ -244,11 +244,20 @@ func TestBlueGreen(t *testing.T) { ep := []*hatypes.Endpoint{} if targets != "" { for _, targetRef := range strings.Split(targets, ",") { + targetWeight := strings.Split(targetRef, "=") + target := targetRef + weight := 1 + if len(targetWeight) == 2 { + target = targetWeight[0] + if w, err := strconv.ParseInt(targetWeight[1], 10, 0); err == nil { + weight = int(w) + } + } ep = append(ep, &hatypes.Endpoint{ IP: "172.17.0.11", Port: 8080, - Weight: 1, - TargetRef: targetRef, + Weight: weight, + TargetRef: target, }) } } @@ -481,6 +490,13 @@ INFO-V(3) blue/green balance label 'v=3' on ingress 'default/ing1' does not refe expWeights: []int{256, 1, 1, 1, 1}, expLogging: "", }, + // 29 + { + ann: buildAnn("v=1=50,v=2=50", "deploy"), + endpoints: buildEndpoints("pod0101-01,pod0101-02,pod0102-01,pod0102-02=0"), + expWeights: []int{1, 1, 2, 0}, + expLogging: "", + }, } for i, test := range testCase { @@ -501,3 +517,239 @@ INFO-V(3) blue/green balance label 'v=3' on ingress 'default/ing1' does not refe c.teardown() } } + +func TestOAuth(t *testing.T) { + testCases := []struct { + ann types.BackendAnnotations + backend string + oauthExp hatypes.OAuthConfig + logging string + }{ + // 0 + { + ann: types.BackendAnnotations{}, + oauthExp: hatypes.OAuthConfig{}, + }, + // 1 + { + ann: types.BackendAnnotations{OAuth: "none"}, + logging: "WARN ignoring invalid oauth implementation 'none' on ingress 'default/app'", + }, + // 2 + { + ann: types.BackendAnnotations{OAuth: "oauth2_proxy"}, + logging: "ERROR path '/oauth2' was not found on namespace 'default'", + }, + // 3 + { + ann: types.BackendAnnotations{OAuth: "oauth2_proxy"}, + backend: "default:back:/oauth2", + oauthExp: hatypes.OAuthConfig{ + Impl: "oauth2_proxy", + BackendName: "default_back_8080", + URIPrefix: "/oauth2", + Headers: map[string]string{"X-Auth-Request-Email": "auth_response_email"}, + }, + }, + // 4 + { + ann: types.BackendAnnotations{OAuth: "oauth2_proxy", OAuthURIPrefix: "/auth"}, + backend: "default:back:/auth", + oauthExp: hatypes.OAuthConfig{ + Impl: "oauth2_proxy", + BackendName: "default_back_8080", + URIPrefix: "/auth", + Headers: map[string]string{"X-Auth-Request-Email": "auth_response_email"}, + }, + }, + // 5 + { + ann: types.BackendAnnotations{OAuth: "oauth2_proxy", OAuthHeaders: "X-Auth-New:attr_from_lua"}, + backend: "default:back:/oauth2", + oauthExp: hatypes.OAuthConfig{ + Impl: "oauth2_proxy", + BackendName: "default_back_8080", + URIPrefix: "/oauth2", + Headers: map[string]string{"X-Auth-New": "attr_from_lua"}, + }, + }, + // 6 + { + ann: types.BackendAnnotations{OAuth: "oauth2_proxy", OAuthHeaders: "space before:attr"}, + backend: "default:back:/oauth2", + oauthExp: hatypes.OAuthConfig{ + Impl: "oauth2_proxy", + BackendName: "default_back_8080", + URIPrefix: "/oauth2", + Headers: map[string]string{}, + }, + logging: "WARN invalid header format 'space before:attr' on ingress 'default/app'", + }, + // 7 + { + ann: types.BackendAnnotations{OAuth: "oauth2_proxy", OAuthHeaders: "no-colon"}, + backend: "default:back:/oauth2", + oauthExp: hatypes.OAuthConfig{ + Impl: "oauth2_proxy", + BackendName: "default_back_8080", + URIPrefix: "/oauth2", + Headers: map[string]string{}, + }, + logging: "WARN invalid header format 'no-colon' on ingress 'default/app'", + }, + // 8 + { + ann: types.BackendAnnotations{OAuth: "oauth2_proxy", OAuthHeaders: "more:colons:unsupported"}, + backend: "default:back:/oauth2", + oauthExp: hatypes.OAuthConfig{ + Impl: "oauth2_proxy", + BackendName: "default_back_8080", + URIPrefix: "/oauth2", + Headers: map[string]string{}, + }, + logging: "WARN invalid header format 'more:colons:unsupported' on ingress 'default/app'", + }, + // 9 + { + ann: types.BackendAnnotations{ + OAuth: "oauth2_proxy", + OAuthHeaders: ",,X-Auth-Request-Email:auth_response_email,,X-Auth-New:attr_from_lua,", + }, + backend: "default:back:/oauth2", + oauthExp: hatypes.OAuthConfig{ + Impl: "oauth2_proxy", + BackendName: "default_back_8080", + URIPrefix: "/oauth2", + Headers: map[string]string{ + "X-Auth-Request-Email": "auth_response_email", + "X-Auth-New": "attr_from_lua", + }, + }, + }, + } + for i, test := range testCases { + c := setup(t) + d := c.createBackendData("default", "app", &test.ann) + if test.backend != "" { + b := strings.Split(test.backend, ":") + backend := c.haproxy.AcquireBackend(b[0], b[1], "8080") + c.haproxy.AcquireHost("app.local").AddPath(backend, b[2]) + } + c.createUpdater().buildOAuth(d) + if !reflect.DeepEqual(test.oauthExp, d.backend.OAuth) { + t.Errorf("oauth on %d differs - expected: %+v - actual: %+v", i, test.oauthExp, d.backend.OAuth) + } + c.logger.CompareLogging(test.logging) + c.teardown() + } +} + +func TestRewriteURL(t *testing.T) { + testCases := []struct { + input string + expected string + logging string + }{ + // 0 + { + input: ``, + expected: ``, + }, + // 1 + { + input: `/"/`, + expected: ``, + logging: `WARN rewrite-target does not allow white spaces or single/double quotes on ingress 'default/app'`, + }, + // 2 + { + input: `/app`, + expected: `/app`, + }, + } + + for i, test := range testCases { + c := setup(t) + d := c.createBackendData("default", "app", &types.BackendAnnotations{RewriteTarget: test.input}) + c.createUpdater().buildRewriteURL(d) + if d.backend.RewriteURL != test.expected { + t.Errorf("rewrite on %d differs - expected: %v - actual: %v", i, test.expected, d.backend.RewriteURL) + } + c.logger.CompareLogging(test.logging) + c.teardown() + } +} + +func TestWAF(t *testing.T) { + testCase := []struct { + waf string + expected string + logging string + }{ + { + waf: "", + expected: "", + logging: "", + }, + { + waf: "none", + expected: "", + logging: "WARN ignoring invalid WAF mode: none", + }, + { + waf: "modsecurity", + expected: "modsecurity", + logging: "", + }, + } + for i, test := range testCase { + c := setup(t) + d := c.createBackendData("default", "app", &types.BackendAnnotations{WAF: test.waf}) + c.createUpdater().buildWAF(d) + if d.backend.WAF != test.expected { + t.Errorf("WAF on %d differs - expected: %v - actual: %v", i, test.expected, d.backend.WAF) + } + c.logger.CompareLogging(test.logging) + c.teardown() + } +} + +func TestWhitelist(t *testing.T) { + testCase := []struct { + cidrlist string + expected []string + logging string + }{ + // 0 + { + cidrlist: "10.0.0.0/8,192.168.0.0/16", + expected: []string{"10.0.0.0/8", "192.168.0.0/16"}, + }, + // 1 + { + cidrlist: "10.0.0.0/8,192.168.0/16", + expected: []string{"10.0.0.0/8"}, + logging: `WARN skipping invalid cidr '192.168.0/16' in whitelist config on ingress 'default/app'`, + }, + // 2 + { + cidrlist: "10.0.0/8,192.168.0/16", + expected: []string{}, + logging: ` +WARN skipping invalid cidr '10.0.0/8' in whitelist config on ingress 'default/app' +WARN skipping invalid cidr '192.168.0/16' in whitelist config on ingress 'default/app'`, + }, + } + for i, test := range testCase { + c := setup(t) + d := c.createBackendData("default", "app", &types.BackendAnnotations{WhitelistSourceRange: test.cidrlist}) + c.createUpdater().buildWhitelist(d) + if !reflect.DeepEqual(d.backend.Whitelist, test.expected) { + if len(d.backend.Whitelist) > 0 || len(test.expected) > 0 { + t.Errorf("whitelist on %d differs - expected: %v - actual: %v", i, test.expected, d.backend.Whitelist) + } + } + c.logger.CompareLogging(test.logging) + c.teardown() + } +} diff --git a/pkg/converters/ingress/annotations/global.go b/pkg/converters/ingress/annotations/global.go index 92f397bb7..38f6e80e0 100644 --- a/pkg/converters/ingress/annotations/global.go +++ b/pkg/converters/ingress/annotations/global.go @@ -18,6 +18,7 @@ package annotations import ( "fmt" + "regexp" "strings" ) @@ -99,6 +100,7 @@ func (c *updater) buildGlobalSSL(d *globalData) { d.global.SSL.DHParam.DefaultMaxSize = d.config.SSLDHDefaultMaxSize d.global.SSL.Engine = d.config.SSLEngine d.global.SSL.ModeAsync = d.config.SSLModeAsync + d.global.SSL.HeadersPrefix = d.config.SSLHeadersPrefix } func (c *updater) buildGlobalModSecurity(d *globalData) { @@ -108,9 +110,26 @@ func (c *updater) buildGlobalModSecurity(d *globalData) { d.global.ModSecurity.Timeout.Processing = d.config.ModsecurityTimeoutProcessing } +var ( + forwardRegex = regexp.MustCompile(`^(add|ignore|ifmissing)$`) +) + +func (c *updater) buildGlobalForwardFor(d *globalData) { + if forwardRegex.MatchString(d.config.Forwardfor) { + d.global.ForwardFor = d.config.Forwardfor + } else { + if d.config.Forwardfor != "" { + c.logger.Warn("Invalid forwardfor value option on configmap: '%s'. Using 'add' instead", d.config.Forwardfor) + } + d.global.ForwardFor = "add" + } +} + func (c *updater) buildGlobalCustomConfig(d *globalData) { if d.config.ConfigGlobal != "" { d.global.CustomConfig = strings.Split(strings.TrimRight(d.config.ConfigGlobal, "\n"), "\n") + } + if d.config.ConfigGlobals.ConfigDefaults != "" { d.global.CustomDefaults = strings.Split(strings.TrimRight(d.config.ConfigGlobals.ConfigDefaults, "\n"), "\n") } } diff --git a/pkg/converters/ingress/annotations/global_test.go b/pkg/converters/ingress/annotations/global_test.go new file mode 100644 index 000000000..a298e14a9 --- /dev/null +++ b/pkg/converters/ingress/annotations/global_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2019 The HAProxy Ingress Controller Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package annotations + +import ( + "testing" + + "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress/types" +) + +func TestForwardFor(t *testing.T) { + testCases := []struct { + conf string + expected string + logging string + }{ + // 0 + { + conf: "", + expected: "add", + logging: "", + }, + // 1 + { + conf: "non", + expected: "add", + logging: "WARN Invalid forwardfor value option on configmap: 'non'. Using 'add' instead", + }, + // 2 + { + conf: "add", + expected: "add", + logging: "", + }, + // 3 + { + conf: "ignore", + expected: "ignore", + logging: "", + }, + // 4 + { + conf: "ifmissing", + expected: "ifmissing", + logging: "", + }, + } + for i, test := range testCases { + c := setup(t) + u := c.createUpdater() + d := c.createGlobalData(&types.Config{ + ConfigGlobals: types.ConfigGlobals{ + Forwardfor: test.conf, + }, + }) + u.buildGlobalForwardFor(d) + if d.global.ForwardFor != test.expected { + t.Errorf("ForwardFor differs on %d: expected '%s' but was '%s'", i, test.expected, d.global.ForwardFor) + } + c.logger.CompareLogging(test.logging) + c.teardown() + } +} diff --git a/pkg/converters/ingress/annotations/host.go b/pkg/converters/ingress/annotations/host.go index bed6ace48..086adc5e1 100644 --- a/pkg/converters/ingress/annotations/host.go +++ b/pkg/converters/ingress/annotations/host.go @@ -29,7 +29,6 @@ func (c *updater) buildHostAuthTLS(d *hostData) { d.host.TLS.CAHash = cafile.SHA1Hash d.host.TLS.CAVerifyOptional = verify == "optional" || verify == "optional_no_ca" d.host.TLS.CAErrorPage = d.ann.AuthTLSErrorPage - d.host.TLS.AddCertHeader = d.ann.AuthTLSCertHeader } else { c.logger.Error("error building TLS auth config: %v", err) } @@ -49,7 +48,7 @@ func (c *updater) buildHostSSLPassthrough(d *hostData) { c.logger.Warn("ignoring path '%s' from '%s': ssl-passthrough only support root path", path.Path, d.ann.Source) } } - if d.ann.SSLPassthroughHTTPPort != 0 { + if d.ann.SSLPassthroughHTTPPort != "" { httpBackend := c.haproxy.FindBackend(rootPath.Backend.Namespace, rootPath.Backend.Name, d.ann.SSLPassthroughHTTPPort) d.host.HTTPPassthroughBackend = httpBackend } diff --git a/pkg/converters/ingress/annotations/updater.go b/pkg/converters/ingress/annotations/updater.go index 700d48e81..fe17be33d 100644 --- a/pkg/converters/ingress/annotations/updater.go +++ b/pkg/converters/ingress/annotations/updater.go @@ -73,15 +73,20 @@ func (c *updater) UpdateGlobalConfig(global *hatypes.Global, config *ingtypes.Co global.Syslog.Endpoint = config.SyslogEndpoint global.Syslog.Format = config.SyslogFormat global.Syslog.Tag = config.SyslogTag + global.Syslog.HTTPLogFormat = config.HTTPLogFormat + global.Syslog.HTTPSLogFormat = config.HTTPSLogFormat + global.Syslog.TCPLogFormat = config.TCPLogFormat global.MaxConn = config.MaxConnections - global.DrainSupport = config.DrainSupport - global.DrainSupportRedispatch = config.DrainSupportRedispatch + global.DrainSupport.Drain = config.DrainSupport + global.DrainSupport.Redispatch = config.DrainSupportRedispatch + global.Cookie.Key = config.CookieKey global.LoadServerState = config.LoadServerState global.StatsSocket = "/var/run/haproxy-stats.sock" c.buildGlobalProc(data) c.buildGlobalTimeout(data) c.buildGlobalSSL(data) c.buildGlobalModSecurity(data) + c.buildGlobalForwardFor(data) c.buildGlobalCustomConfig(data) } @@ -106,10 +111,20 @@ func (c *updater) UpdateBackendConfig(backend *hatypes.Backend, ann *ingtypes.Ba } // TODO check ModeTCP with HTTP annotations backend.BalanceAlgorithm = ann.BalanceAlgorithm + backend.HSTS.Enabled = ann.HSTS + backend.HSTS.MaxAge = ann.HSTSMaxAge + backend.HSTS.Preload = ann.HSTSPreload + backend.HSTS.Subdomains = ann.HSTSIncludeSubdomains backend.MaxConnServer = ann.MaxconnServer backend.ProxyBodySize = ann.ProxyBodySize backend.SSLRedirect = ann.SSLRedirect + backend.SSL.AddCertHeader = ann.AuthTLSCertHeader c.buildBackendAffinity(data) c.buildBackendAuthHTTP(data) c.buildBackendBlueGreen(data) + c.buildBackendCors(data) + c.buildOAuth(data) + c.buildRewriteURL(data) + c.buildWAF(data) + c.buildWhitelist(data) } diff --git a/pkg/converters/ingress/annotations/updater_test.go b/pkg/converters/ingress/annotations/updater_test.go index 6cde98ace..5ee484716 100644 --- a/pkg/converters/ingress/annotations/updater_test.go +++ b/pkg/converters/ingress/annotations/updater_test.go @@ -73,3 +73,10 @@ func (c *testConfig) createBackendData(namespace, name string, ann *types.Backen ann: ann, } } + +func (c *testConfig) createGlobalData(config *types.Config) *globalData { + return &globalData{ + global: &hatypes.Global{}, + config: config, + } +} diff --git a/pkg/converters/ingress/defaults.go b/pkg/converters/ingress/defaults.go index bf80b06e6..f7b3e4a1a 100644 --- a/pkg/converters/ingress/defaults.go +++ b/pkg/converters/ingress/defaults.go @@ -28,13 +28,14 @@ const ( func createDefaults() *types.Config { return &types.Config{ ConfigDefaults: types.ConfigDefaults{ - BalanceAlgorithm: "roundrobin", - CookieKey: "Ingress", - HSTS: true, + BalanceAlgorithm: "roundrobin", + CookieKey: "Ingress", + HSTS: true, HSTSIncludeSubdomains: false, HSTSMaxAge: "15768000", HSTSPreload: false, ProxyBodySize: "", + SessionCookieDynamic: true, SSLRedirect: true, TimeoutClient: "50s", TimeoutClientFin: "50s", @@ -53,9 +54,9 @@ func createDefaults() *types.Config { BindIPAddrHTTP: "*", BindIPAddrStats: "*", BindIPAddrTCP: "*", + ConfigDefaults: "", ConfigFrontend: "", ConfigGlobal: "", - ConfigDefaults: "", DNSAcceptedPayloadSize: 8192, DNSClusterDomain: "cluster.local", DNSHoldObsolete: "0s", diff --git a/pkg/converters/ingress/helper_test/cachemock.go b/pkg/converters/ingress/helper_test/cachemock.go index b14139355..11dcc82d9 100644 --- a/pkg/converters/ingress/helper_test/cachemock.go +++ b/pkg/converters/ingress/helper_test/cachemock.go @@ -33,6 +33,7 @@ type SecretContent map[string]map[string][]byte type CacheMock struct { SvcList []*api.Service EpList map[string]*api.Endpoints + TermPodList map[string][]*api.Pod PodList map[string]*api.Pod SecretTLSPath map[string]string SecretCAPath map[string]string @@ -62,6 +63,15 @@ func (c *CacheMock) GetEndpoints(service *api.Service) (*api.Endpoints, error) { return nil, fmt.Errorf("could not find endpoints for service '%s'", serviceName) } +// GetTerminatingPods ... +func (c *CacheMock) GetTerminatingPods(service *api.Service) ([]*api.Pod, error) { + serviceName := service.Namespace + "/" + service.Name + if pods, found := c.TermPodList[serviceName]; found { + return pods, nil + } + return []*api.Pod{}, nil +} + // GetPod ... func (c *CacheMock) GetPod(podName string) (*api.Pod, error) { if pod, found := c.PodList[podName]; found { diff --git a/pkg/converters/ingress/ingress.go b/pkg/converters/ingress/ingress.go index 460d4de4b..d7f1ec152 100644 --- a/pkg/converters/ingress/ingress.go +++ b/pkg/converters/ingress/ingress.go @@ -18,10 +18,12 @@ package ingress import ( "fmt" + "strconv" "strings" api "k8s.io/api/core/v1" extensions "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/util/intstr" "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress/annotations" ingtypes "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress/types" @@ -50,7 +52,7 @@ func NewIngressConverter(options *ingtypes.ConverterOptions, haproxy haproxy.Con } haproxy.ConfigDefaultX509Cert(options.DefaultSSLFile.Filename) if options.DefaultBackend != "" { - if backend, err := c.addBackend(options.DefaultBackend, 0, &ingtypes.BackendAnnotations{}); err == nil { + if backend, err := c.addBackend(options.DefaultBackend, "", &ingtypes.BackendAnnotations{}); err == nil { haproxy.ConfigDefaultBackend(backend) } else { c.logger.Error("error reading default service: %v", err) @@ -154,7 +156,7 @@ func (c *converter) syncAnnotations() { } } -func (c *converter) addDefaultHostBackend(fullSvcName string, svcPort int, ingFrontAnn *ingtypes.HostAnnotations, ingBackAnn *ingtypes.BackendAnnotations) error { +func (c *converter) addDefaultHostBackend(fullSvcName, svcPort string, ingFrontAnn *ingtypes.HostAnnotations, ingBackAnn *ingtypes.BackendAnnotations) error { if fr := c.haproxy.FindHost("*"); fr != nil { if fr.FindPath("/") != nil { return fmt.Errorf("path / was already defined on default host") @@ -182,7 +184,7 @@ func (c *converter) addHost(hostname string, ingAnn *ingtypes.HostAnnotations) * return host } -func (c *converter) addBackend(fullSvcName string, svcPort int, ingAnn *ingtypes.BackendAnnotations) (*hatypes.Backend, error) { +func (c *converter) addBackend(fullSvcName, svcPort string, ingAnn *ingtypes.BackendAnnotations) (*hatypes.Backend, error) { svc, err := c.cache.GetService(fullSvcName) if err != nil { return nil, err @@ -190,17 +192,20 @@ func (c *converter) addBackend(fullSvcName string, svcPort int, ingAnn *ingtypes ssvcName := strings.Split(fullSvcName, "/") namespace := ssvcName[0] svcName := ssvcName[1] - if svcPort == 0 { + if svcPort == "" { // if the port wasn't specified, take the first one // from the api.Service object - // TODO named port - svcPort = svc.Spec.Ports[0].TargetPort.IntValue() + svcPort = svc.Spec.Ports[0].TargetPort.String() } - backend := c.haproxy.AcquireBackend(namespace, svcName, svcPort) + epport := findServicePort(svc, svcPort) + if epport.String() == "" { + return nil, fmt.Errorf("port not found: '%s'", svcPort) + } + backend := c.haproxy.AcquireBackend(namespace, svcName, epport.String()) ann, found := c.backendAnnotations[backend] if !found { // New backend, configure endpoints and svc annotations - if err := c.addEndpoints(svc, svcPort, backend); err != nil { + if err := c.addEndpoints(svc, epport, backend); err != nil { c.logger.Error("error adding endpoints of service '%s': %v", fullSvcName, err) } // Initialize with service annotations, giving precedence @@ -214,17 +219,42 @@ func (c *converter) addBackend(fullSvcName string, svcPort int, ingAnn *ingtypes // Merging Ingress annotations skipped, _ := utils.UpdateStruct(c.globalConfig.ConfigDefaults, ingAnn, ann) if len(skipped) > 0 { - c.logger.Info("skipping backend '%s/%s:%d' annotation(s) from %v due to conflict: %v", + c.logger.Info("skipping backend '%s/%s:%s' annotation(s) from %v due to conflict: %v", backend.Namespace, backend.Name, backend.Port, ingAnn.Source, skipped) } return backend, nil } +func findServicePort(svc *api.Service, servicePort string) intstr.IntOrString { + for _, port := range svc.Spec.Ports { + if port.Name == servicePort { + return port.TargetPort + } + } + for _, port := range svc.Spec.Ports { + if port.TargetPort.String() == servicePort { + return port.TargetPort + } + } + svcPortNumber, err := strconv.ParseInt(servicePort, 10, 0) + if err != nil { + return intstr.FromString("") + } + for _, port := range svc.Spec.Ports { + if port.Port == int32(svcPortNumber) { + return port.TargetPort + } + } + return intstr.FromString("") +} + func (c *converter) addHTTPPassthrough(fullSvcName string, ingFrontAnn *ingtypes.HostAnnotations, ingBackAnn *ingtypes.BackendAnnotations) { // a very specific use case of pre-parsing annotations: // need to add a backend if ssl-passthrough-http-port assigned - if ingFrontAnn.SSLPassthrough && ingFrontAnn.SSLPassthroughHTTPPort != 0 { - c.addBackend(fullSvcName, ingFrontAnn.SSLPassthroughHTTPPort, ingBackAnn) + if ingFrontAnn.SSLPassthrough && ingFrontAnn.SSLPassthroughHTTPPort != "" { + if _, err := c.addBackend(fullSvcName, ingFrontAnn.SSLPassthroughHTTPPort, ingBackAnn); err != nil { + c.logger.Warn("skipping http port config of ssl-passthrough: %v", err) + } } } @@ -240,23 +270,40 @@ func (c *converter) addTLS(namespace, secretName string) ingtypes.File { return c.options.DefaultSSLFile } -func (c *converter) addEndpoints(svc *api.Service, servicePort int, backend *hatypes.Backend) error { +func (c *converter) addEndpoints(svc *api.Service, svcPort intstr.IntOrString, backend *hatypes.Backend) error { endpoints, err := c.cache.GetEndpoints(svc) if err != nil { return err } // TODO ServiceTypeExternalName // TODO ServiceUpstream - annotation nao documentada - // TODO DrainSupport + // TODO svcPort.IntValue() doesn't work if svc.targetPort is a pod's named port for _, subset := range endpoints.Subsets { for _, port := range subset.Ports { - if int(port.Port) == servicePort && port.Protocol == api.ProtocolTCP { + ssport := int(port.Port) + if ssport == svcPort.IntValue() && port.Protocol == api.ProtocolTCP { for _, addr := range subset.Addresses { - backend.NewEndpoint(addr.IP, servicePort, addr.TargetRef.Namespace+"/"+addr.TargetRef.Name) + backend.NewEndpoint(addr.IP, ssport, addr.TargetRef.Namespace+"/"+addr.TargetRef.Name) + } + if c.globalConfig.DrainSupport { + for _, addr := range subset.NotReadyAddresses { + ep := backend.NewEndpoint(addr.IP, ssport, addr.TargetRef.Namespace+"/"+addr.TargetRef.Name) + ep.Weight = 0 + } } } } } + if c.globalConfig.DrainSupport { + pods, err := c.cache.GetTerminatingPods(svc) + if err != nil { + return err + } + for _, pod := range pods { + ep := backend.NewEndpoint(pod.Status.PodIP, svcPort.IntValue(), pod.Namespace+"/"+pod.Name) + ep.Weight = 0 + } + } return nil } @@ -282,8 +329,8 @@ func (c *converter) readAnnotations(source *ingtypes.Source, annotations map[str return frontAnn, backAnn } -func readServiceNamePort(backend *extensions.IngressBackend) (string, int) { +func readServiceNamePort(backend *extensions.IngressBackend) (string, string) { serviceName := backend.ServiceName - servicePort := backend.ServicePort.IntValue() + servicePort := backend.ServicePort.String() return serviceName, servicePort } diff --git a/pkg/converters/ingress/ingress_test.go b/pkg/converters/ingress/ingress_test.go index d366b58ce..db1fd5f6e 100644 --- a/pkg/converters/ingress/ingress_test.go +++ b/pkg/converters/ingress/ingress_test.go @@ -26,6 +26,7 @@ import ( extensions "k8s.io/api/extensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/scheme" ing_helper "github.com/jcmoraisjr/haproxy-ingress/pkg/converters/ingress/helper_test" @@ -82,6 +83,75 @@ func TestSyncDefaultSvcNotFound(t *testing.T) { ERROR error reading default service: service not found: 'system/default'`) } +func TestSyncSvcPortNotFound(t *testing.T) { + c := setup(t) + defer c.teardown() + + c.createSvc1Auto() + c.Sync(c.createIng1("default/echo", "echo.example.com", "/", "echo:non")) + + c.compareConfigFront(` +- hostname: echo.example.com + paths: [] +`) + + c.compareConfigBack(` +- id: _default_backend + endpoints: + - ip: 172.17.0.99 + port: 8080 +`) + + c.compareLogging(` +WARN skipping backend config of ingress 'default/echo': port not found: 'non' +`) +} + +func TestSyncSvcNamedPort(t *testing.T) { + c := setup(t) + defer c.teardown() + + c.createSvc1("default/echo", "httpsvc:1001:8080", "172.17.1.101") + c.Sync( + c.createIng1("default/echo1", "echo1.example.com", "/", "echo:httpsvc"), + c.createIng1("default/echo2", "echo2.example.com", "/", "echo:1001"), + c.createIng1("default/echo3", "echo3.example.com", "/", "echo:8080"), + c.createIng1("default/echo4", "echo4.example.com", "/", "echo:9000"), + ) + + c.compareConfigFront(` +- hostname: echo1.example.com + paths: + - path: / + backend: default_echo_8080 +- hostname: echo2.example.com + paths: + - path: / + backend: default_echo_8080 +- hostname: echo3.example.com + paths: + - path: / + backend: default_echo_8080 +- hostname: echo4.example.com + paths: [] +`) + + c.compareConfigBack(` +- id: default_echo_8080 + endpoints: + - ip: 172.17.1.101 + port: 8080 +- id: _default_backend + endpoints: + - ip: 172.17.0.99 + port: 8080 +`) + + c.compareLogging(` +WARN skipping backend config of ingress 'default/echo4': port not found: '9000' +`) +} + func TestSyncSingle(t *testing.T) { c := setup(t) defer c.teardown() @@ -181,6 +251,50 @@ func TestSyncInvalidEndpoint(t *testing.T) { ERROR error adding endpoints of service 'default/echo': could not find endpoints for service 'default/echo'`) } +func TestSyncDrainSupport(t *testing.T) { + c := setup(t) + defer c.teardown() + + svc, ep := c.createSvc1("default/echo", "8080", "172.17.1.101,172.17.1.102") + svcName := svc.Namespace + "/" + svc.Name + ss := &ep.Subsets[0] + addr := ss.Addresses + ss.Addresses = []api.EndpointAddress{addr[0]} + ss.NotReadyAddresses = []api.EndpointAddress{addr[1]} + pod := c.createPod1("default/echo-xxxxx", "172.17.1.103") + c.cache.TermPodList[svcName] = []*api.Pod{pod} + + c.SyncDef( + map[string]string{"drain-support": "true"}, + c.createIng1("default/echo", "echo.example.com", "/", "echo:8080"), + ) + + c.compareConfigFront(` +- hostname: echo.example.com + paths: + - path: / + backend: default_echo_8080 +`) + c.compareConfigBack(` +- id: default_echo_8080 + endpoints: + - ip: 172.17.1.101 + port: 8080 + - ip: 172.17.1.102 + port: 8080 + drain: true + - ip: 172.17.1.103 + port: 8080 + drain: true +- id: _default_backend + endpoints: + - ip: 172.17.0.99 + port: 8080 +`) + + c.compareLogging(``) +} + func TestSyncRootPathLast(t *testing.T) { c := setup(t) defer c.teardown() @@ -907,6 +1021,67 @@ func TestSyncAnnBackDefault(t *testing.T) { INFO skipping backend 'default/echo5:8080' annotation(s) from ingress 'default/echo5' due to conflict: [balance-algorithm]`) } +func TestSyncAnnPassthrough(t *testing.T) { + c := setup(t) + defer c.teardown() + + svc, ep := c.createSvc1("default/echo", "http:8080", "172.17.1.101") + svcPort := api.ServicePort{ + Name: "https", + Port: 8443, + TargetPort: intstr.FromInt(8443), + } + epPort := api.EndpointPort{ + Name: "https", + Port: 8443, + Protocol: api.ProtocolTCP, + } + svc.Spec.Ports = append(svc.Spec.Ports, svcPort) + ep.Subsets[0].Ports = append(ep.Subsets[0].Ports, epPort) + c.Sync( + c.createIng1Ann("default/echo1", "echo1.example.com", "/", "echo:8443", + map[string]string{ + "ingress.kubernetes.io/ssl-passthrough": "true", + "ingress.kubernetes.io/ssl-passthrough-http-port": "8080", + }), + c.createIng1Ann("default/echo2", "echo2.example.com", "/", "echo:8443", + map[string]string{ + "ingress.kubernetes.io/ssl-passthrough": "true", + "ingress.kubernetes.io/ssl-passthrough-http-port": "9000", + }), + ) + + c.compareConfigFront(` +- hostname: echo1.example.com + paths: + - path: / + backend: default_echo_8443 +- hostname: echo2.example.com + paths: + - path: / + backend: default_echo_8443 +`) + + c.compareConfigBack(` +- id: default_echo_8080 + endpoints: + - ip: 172.17.1.101 + port: 8080 +- id: default_echo_8443 + endpoints: + - ip: 172.17.1.101 + port: 8443 +- id: _default_backend + endpoints: + - ip: 172.17.0.99 + port: 8080 +`) + + c.compareLogging(` +WARN skipping http port config of ssl-passthrough: port not found: '9000' +`) +} + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * BUILDERS @@ -932,8 +1107,9 @@ func setup(t *testing.T) *testConfig { decode: scheme.Codecs.UniversalDeserializer().Decode, hconfig: haproxy.CreateInstance(logger, &ha_helper.BindUtilsMock{}, haproxy.InstanceOptions{}).Config(), cache: &ing_helper.CacheMock{ - SvcList: []*api.Service{}, - EpList: map[string]*api.Endpoints{}, + SvcList: []*api.Service{}, + EpList: map[string]*api.Endpoints{}, + TermPodList: map[string][]*api.Pod{}, SecretTLSPath: map[string]string{ "system/ingress-default": "/tls/tls-default.pem", }, @@ -978,24 +1154,30 @@ func (c *testConfig) SyncDef(config map[string]string, ing ...*extensions.Ingres conv.Sync(ing) } -func (c *testConfig) createSvc1Auto() *api.Service { +func (c *testConfig) createSvc1Auto() (*api.Service, *api.Endpoints) { return c.createSvc1("default/echo", "8080", "172.17.0.11") } -func (c *testConfig) createSvc1AutoAnn(ann map[string]string) *api.Service { - svc := c.createSvc1Auto() +func (c *testConfig) createSvc1AutoAnn(ann map[string]string) (*api.Service, *api.Endpoints) { + svc, ep := c.createSvc1Auto() svc.SetAnnotations(ann) - return svc + return svc, ep } -func (c *testConfig) createSvc1Ann(name, port, endpoints string, ann map[string]string) *api.Service { - svc := c.createSvc1(name, port, endpoints) +func (c *testConfig) createSvc1Ann(name, port, endpoints string, ann map[string]string) (*api.Service, *api.Endpoints) { + svc, ep := c.createSvc1(name, port, endpoints) svc.SetAnnotations(ann) - return svc + return svc, ep } -func (c *testConfig) createSvc1(name, port, endpoints string) *api.Service { +func (c *testConfig) createSvc1(name, port, endpoints string) (*api.Service, *api.Endpoints) { sname := strings.Split(name, "/") + sport := strings.Split(port, ":") + if len(sport) < 2 { + sport = []string{"", port, port} + } else if len(sport) < 3 { + sport = []string{sport[0], sport[1], sport[1]} + } svc := c.createObject(` apiVersion: v1 @@ -1005,8 +1187,9 @@ metadata: namespace: ` + sname[0] + ` spec: ports: - - port: ` + port + ` - targetPort: ` + port).(*api.Service) + - name: ` + sport[0] + ` + port: ` + sport[1] + ` + targetPort: ` + sport[2]).(*api.Service) c.cache.SvcList = append(c.cache.SvcList, svc) @@ -1019,7 +1202,8 @@ metadata: subsets: - addresses: [] ports: - - port: ` + port + ` + - name: ` + sport[0] + ` + port: ` + sport[2] + ` protocol: TCP`).(*api.Endpoints) addr := []api.EndpointAddress{} @@ -1036,7 +1220,22 @@ subsets: ep.Subsets[0].Addresses = addr c.cache.EpList[name] = ep - return svc + return svc, ep +} + +func (c *testConfig) createPod1(name, ip string) *api.Pod { + pname := strings.Split(name, "/") + + pod := c.createObject(` +apiVersion: v1 +kind: Pod +metadata: + name: ` + pname[1] + ` + namespace: ` + pname[0] + ` +status: + podIP: ` + ip).(*api.Pod) + + return pod } func (c *testConfig) createSecretTLS1(secretName string) { @@ -1195,8 +1394,9 @@ func (c *testConfig) compareConfigDefaultFront(expected string) { type ( endpointMock struct { - IP string - Port int + IP string + Port int + Drain bool `yaml:",omitempty"` } backendMock struct { ID string @@ -1211,7 +1411,7 @@ func convertBackend(habackends ...*hatypes.Backend) []backendMock { for _, b := range habackends { endpoints := []endpointMock{} for _, e := range b.Endpoints { - endpoints = append(endpoints, endpointMock{IP: e.IP, Port: e.Port}) + endpoints = append(endpoints, endpointMock{IP: e.IP, Port: e.Port, Drain: e.Weight == 0}) } backends = append(backends, backendMock{ ID: b.ID, diff --git a/pkg/converters/ingress/types/annotations.go b/pkg/converters/ingress/types/annotations.go index 9a4bf0925..2628a0373 100644 --- a/pkg/converters/ingress/types/annotations.go +++ b/pkg/converters/ingress/types/annotations.go @@ -20,14 +20,13 @@ package types type HostAnnotations struct { Source Source `json:"-"` AppRoot string `json:"app-root"` - AuthTLSCertHeader bool `json:"auth-tls-cert-header"` AuthTLSErrorPage string `json:"auth-tls-error-page"` AuthTLSVerifyClient string `json:"auth-tls-verify-client"` AuthTLSSecret string `json:"auth-tls-secret"` ServerAlias string `json:"server-alias"` ServerAliasRegex string `json:"server-alias-regex"` SSLPassthrough bool `json:"ssl-passthrough"` - SSLPassthroughHTTPPort int `json:"ssl-passthrough-http-port"` + SSLPassthroughHTTPPort string `json:"ssl-passthrough-http-port"` TimeoutClient string `json:"timeout-client"` TimeoutClientFin string `json:"timeout-client-fin"` } @@ -38,18 +37,19 @@ type BackendAnnotations struct { Affinity string `json:"affinity"` AuthRealm string `json:"auth-realm"` AuthSecret string `json:"auth-secret"` + AuthTLSCertHeader bool `json:"auth-tls-cert-header"` AuthType string `json:"auth-type"` BalanceAlgorithm string `json:"balance-algorithm"` BlueGreenBalance string `json:"blue-green-balance"` BlueGreenDeploy string `json:"blue-green-deploy"` BlueGreenMode string `json:"blue-green-mode"` ConfigBackend string `json:"config-backend"` - CookieKey string `json:"cookie-key"` CorsAllowCredentials bool `json:"cors-allow-credentials"` CorsAllowHeaders string `json:"cors-allow-headers"` CorsAllowMethods string `json:"cors-allow-methods"` CorsAllowOrigin string `json:"cors-allow-origin"` CorsEnable bool `json:"cors-enable"` + CorsExposeHeaders string `json:"cors-expose-headers"` CorsMaxAge int `json:"cors-max-age"` HSTS bool `json:"hsts"` HSTSIncludeSubdomains bool `json:"hsts-include-subdomains"` @@ -70,7 +70,7 @@ type BackendAnnotations struct { SecureBackends bool `json:"secure-backends"` SecureCrtSecret string `json:"secure-crt-secret"` SecureVerifyCASecret string `json:"secure-verify-ca-secret"` - SessionCookieDynamic string `json:"session-cookie-dynamic"` + SessionCookieDynamic bool `json:"session-cookie-dynamic"` SessionCookieName string `json:"session-cookie-name"` SessionCookieStrategy string `json:"session-cookie-strategy"` SSLRedirect bool `json:"ssl-redirect"` diff --git a/pkg/converters/ingress/types/config.go b/pkg/converters/ingress/types/config.go index ab938f92d..a4338a523 100644 --- a/pkg/converters/ingress/types/config.go +++ b/pkg/converters/ingress/types/config.go @@ -25,6 +25,7 @@ type ConfigDefaults struct { HSTSMaxAge string `json:"hsts-max-age"` HSTSPreload bool `json:"hsts-preload"` ProxyBodySize string `json:"proxy-body-size"` + SessionCookieDynamic bool `json:"session-cookie-dynamic"` SSLRedirect bool `json:"ssl-redirect"` TimeoutClient string `json:"timeout-client"` TimeoutClientFin string `json:"timeout-client-fin"` diff --git a/pkg/converters/ingress/types/interfaces.go b/pkg/converters/ingress/types/interfaces.go index db60ab13f..f2ec1ae90 100644 --- a/pkg/converters/ingress/types/interfaces.go +++ b/pkg/converters/ingress/types/interfaces.go @@ -24,6 +24,7 @@ import ( type Cache interface { GetService(serviceName string) (*api.Service, error) GetEndpoints(service *api.Service) (*api.Endpoints, error) + GetTerminatingPods(service *api.Service) ([]*api.Pod, error) GetPod(podName string) (*api.Pod, error) GetTLSSecretPath(secretName string) (File, error) GetCASecretPath(secretName string) (File, error) diff --git a/pkg/haproxy/config.go b/pkg/haproxy/config.go index 2add7602d..7911303c2 100644 --- a/pkg/haproxy/config.go +++ b/pkg/haproxy/config.go @@ -29,13 +29,14 @@ import ( type Config interface { AcquireHost(hostname string) *hatypes.Host FindHost(hostname string) *hatypes.Host - AcquireBackend(namespace, name string, port int) *hatypes.Backend - FindBackend(namespace, name string, port int) *hatypes.Backend + AcquireBackend(namespace, name, port string) *hatypes.Backend + FindBackend(namespace, name, port string) *hatypes.Backend ConfigDefaultBackend(defaultBackend *hatypes.Backend) ConfigDefaultX509Cert(filename string) AddUserlist(name string, users []hatypes.User) *hatypes.Userlist FindUserlist(name string) *hatypes.Userlist - BuildFrontendGroup() (*hatypes.FrontendGroup, error) + FrontendGroup() *hatypes.FrontendGroup + BuildFrontendGroup() error DefaultHost() *hatypes.Host DefaultBackend() *hatypes.Backend Global() *hatypes.Global @@ -46,6 +47,7 @@ type Config interface { } type config struct { + fgroup *hatypes.FrontendGroup bindUtils hatypes.BindUtils mapsTemplate *template.Config mapsDir string @@ -122,7 +124,7 @@ func (c *config) sortBackends() { }) } -func (c *config) AcquireBackend(namespace, name string, port int) *hatypes.Backend { +func (c *config) AcquireBackend(namespace, name, port string) *hatypes.Backend { if backend := c.FindBackend(namespace, name, port); backend != nil { return backend } @@ -132,7 +134,7 @@ func (c *config) AcquireBackend(namespace, name string, port int) *hatypes.Backe return backend } -func (c *config) FindBackend(namespace, name string, port int) *hatypes.Backend { +func (c *config) FindBackend(namespace, name, port string) *hatypes.Backend { for _, b := range c.backends { if b.Namespace == namespace && b.Name == name && b.Port == port { return b @@ -141,7 +143,7 @@ func (c *config) FindBackend(namespace, name string, port int) *hatypes.Backend return nil } -func createBackend(namespace, name string, port int) *hatypes.Backend { +func createBackend(namespace, name, port string) *hatypes.Backend { return &hatypes.Backend{ ID: buildID(namespace, name, port), Namespace: namespace, @@ -151,8 +153,8 @@ func createBackend(namespace, name string, port int) *hatypes.Backend { } } -func buildID(namespace, name string, port int) string { - return fmt.Sprintf("%s_%s_%d", namespace, name, port) +func buildID(namespace, name, port string) string { + return fmt.Sprintf("%s_%s_%s", namespace, name, port) } func (c *config) ConfigDefaultBackend(defaultBackend *hatypes.Backend) { @@ -187,17 +189,26 @@ func (c *config) FindUserlist(name string) *hatypes.Userlist { return nil } -func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { +func (c *config) FrontendGroup() *hatypes.FrontendGroup { + return c.fgroup +} + +func (c *config) BuildFrontendGroup() error { + // tested thanks to instance_test templating tests + // ideas to make a nice test or a nice refactor are welcome if len(c.hosts) == 0 { - return nil, fmt.Errorf("cannot create frontends without hosts") + return fmt.Errorf("cannot create frontends without hosts") } frontends, sslpassthrough := hatypes.BuildRawFrontends(c.hosts) + fgroupMaps := hatypes.CreateMaps() fgroup := &hatypes.FrontendGroup{ Frontends: frontends, HasSSLPassthrough: len(sslpassthrough) > 0, - HTTPFrontsMap: c.mapsDir + "/http-front.map", - RedirectMap: c.mapsDir + "/redirect.map", - SSLPassthroughMap: c.mapsDir + "/sslpassthrough.map", + Maps: fgroupMaps, + HTTPFrontsMap: fgroupMaps.AddMap(c.mapsDir + "/_global_http_front.map"), + HTTPRootRedirMap: fgroupMaps.AddMap(c.mapsDir + "/_global_http_root_redir.map"), + HTTPSRedirMap: fgroupMaps.AddMap(c.mapsDir + "/_global_https_redir.map"), + SSLPassthroughMap: fgroupMaps.AddMap(c.mapsDir + "/_global_sslpassthrough.map"), } if fgroup.HasTCPProxy() { // More than one HAProxy's frontend or bind, or using ssl-passthrough config, @@ -205,23 +216,21 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { var i int for _, frontend := range frontends { for _, bind := range frontend.Binds { - var bindName string + i++ + bindName := fmt.Sprintf("_socket%03d", i) if len(bind.Hosts) == 1 { - bindName = bind.Hosts[0].Hostname bind.TLS.TLSCert = c.defaultX509Cert bind.TLS.TLSCertDir = bind.Hosts[0].TLS.TLSFilename } else { - i++ - bindName = fmt.Sprintf("_socket%03d", i) x509dir, err := c.createCertsDir(bindName, bind.Hosts) if err != nil { - return nil, err + return err } bind.TLS.TLSCert = c.defaultX509Cert bind.TLS.TLSCertDir = x509dir } bind.Name = bindName - bind.Socket = fmt.Sprintf("unix@/var/run/front_%s.sock", bindName) + bind.Socket = fmt.Sprintf("unix@/var/run/%s.sock", bindName) bind.AcceptProxy = true } } @@ -236,7 +245,7 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { } else { x509dir, err := c.createCertsDir(bind.Name, bind.Hosts) if err != nil { - return nil, err + return err } frontends[0].Binds[0].TLS.TLSCert = c.defaultX509Cert frontends[0].Binds[0].TLS.TLSCertDir = x509dir @@ -244,141 +253,127 @@ func (c *config) BuildFrontendGroup() (*hatypes.FrontendGroup, error) { } for _, frontend := range frontends { mapsPrefix := c.mapsDir + "/" + frontend.Name - frontend.HostBackendsMap = mapsPrefix + "_host.map" - frontend.SNIBackendsMap = mapsPrefix + "_sni.map" - frontend.TLSInvalidCrtErrorList = mapsPrefix + "_inv_crt.list" - frontend.TLSInvalidCrtErrorPagesMap = mapsPrefix + "_inv_crt_redir.map" - frontend.TLSNoCrtErrorList = mapsPrefix + "_no_crt.list" - frontend.TLSNoCrtErrorPagesMap = mapsPrefix + "_no_crt_redir.map" - frontend.VarNamespaceMap = mapsPrefix + "_k8s_ns.map" + frontend.Maps = hatypes.CreateMaps() + frontend.HostBackendsMap = frontend.Maps.AddMap(mapsPrefix + "_host.map") + frontend.RootRedirMap = frontend.Maps.AddMap(mapsPrefix + "_root_redir.map") + frontend.SNIBackendsMap = frontend.Maps.AddMap(mapsPrefix + "_sni.map") + frontend.TLSInvalidCrtErrorList = frontend.Maps.AddMap(mapsPrefix + "_inv_crt.list") + frontend.TLSInvalidCrtErrorPagesMap = frontend.Maps.AddMap(mapsPrefix + "_inv_crt_redir.map") + frontend.TLSNoCrtErrorList = frontend.Maps.AddMap(mapsPrefix + "_no_crt.list") + frontend.TLSNoCrtErrorPagesMap = frontend.Maps.AddMap(mapsPrefix + "_no_crt_redir.map") + frontend.VarNamespaceMap = frontend.Maps.AddMap(mapsPrefix + "_k8s_ns.map") for _, bind := range frontend.Binds { - bind.UseServerList = mapsPrefix + "_bind_" + bind.Name + ".list" + bind.Maps = hatypes.CreateMaps() + bind.UseServerList = bind.Maps.AddMap(c.mapsDir + "/" + bind.Name + ".list") } } - type mapEntry struct { - Key string - Value string - } - var sslpassthroughMap []mapEntry - var redirectMap []mapEntry - var httpFront []mapEntry + // Some maps use yes/no answers instead of a list with found/missing keys + // This approach avoid overlap: + // 1. match with path_beg/map_beg, /path has a feature and a declared /path/sub doesn't have + // 2. *.host.domain wildcard/alias/alias-regex has a feature and a declared sub.host.domain doesn't have yesno := map[bool]string{true: "yes", false: "no"} for _, sslpassHost := range sslpassthrough { rootPath := sslpassHost.FindPath("/") if rootPath == nil { - return nil, fmt.Errorf("missing root path on host %s", sslpassHost.Hostname) + return fmt.Errorf("missing root path on host %s", sslpassHost.Hostname) } - sslpassthroughMap = append(sslpassthroughMap, mapEntry{ - Key: sslpassHost.Hostname, - Value: rootPath.BackendID, - }) - redirectMap = append(redirectMap, mapEntry{ - Key: sslpassHost.Hostname + "/", - Value: yesno[sslpassHost.HTTPPassthroughBackend == nil], - }) + fgroup.SSLPassthroughMap.AppendHostname(sslpassHost.Hostname, rootPath.BackendID) + fgroup.HTTPSRedirMap.AppendHostname(sslpassHost.Hostname+"/", yesno[sslpassHost.HTTPPassthroughBackend == nil]) if sslpassHost.HTTPPassthroughBackend != nil { - httpFront = append(httpFront, mapEntry{ - Key: sslpassHost.Hostname + "/", - Value: sslpassHost.HTTPPassthroughBackend.ID, - }) - } else { - fgroup.HasRedirectHTTPS = true + fgroup.HTTPFrontsMap.AppendHostname(sslpassHost.Hostname+"/", sslpassHost.HTTPPassthroughBackend.ID) } } for _, f := range frontends { - var hostBackendsMap []mapEntry - var sniBackendsMap []mapEntry - var invalidCrtList []mapEntry - var invalidCrtMap []mapEntry - var noCrtList []mapEntry - var noCrtMap []mapEntry - var varNamespaceMap []mapEntry for _, host := range f.Hosts { for _, path := range host.Paths { // TODO use only root path if all uri has the same conf - redirectMap = append(redirectMap, mapEntry{ - Key: host.Hostname + path.Path, - Value: yesno[path.Backend.SSLRedirect], - }) - entry := mapEntry{ - Key: host.Hostname + path.Path, - Value: path.BackendID, + fgroup.HTTPSRedirMap.AppendHostname(host.Hostname+path.Path, yesno[path.Backend.SSLRedirect]) + base := host.Hostname + path.Path + var aliasName, aliasRegex string + // TODO warn in logs about ignoring alias name due to hostname colision + if host.Alias.AliasName != "" && c.FindHost(host.Alias.AliasName) == nil { + aliasName = host.Alias.AliasName + path.Path + } + if host.Alias.AliasRegex != "" { + aliasRegex = host.Alias.AliasRegex + path.Path } + back := path.BackendID if host.HasTLSAuth() { - sniBackendsMap = append(sniBackendsMap, entry) + f.SNIBackendsMap.AppendHostname(base, back) + f.SNIBackendsMap.AppendAliasName(aliasName, back) + f.SNIBackendsMap.AppendAliasRegex(aliasRegex, back) + path.Backend.SSL.HasTLSAuth = true } else { - hostBackendsMap = append(hostBackendsMap, entry) + f.HostBackendsMap.AppendHostname(base, back) + f.HostBackendsMap.AppendAliasName(aliasName, back) + f.HostBackendsMap.AppendAliasRegex(aliasRegex, back) } - if path.Backend.SSLRedirect { - fgroup.HasRedirectHTTPS = true - } else { - httpFront = append(httpFront, entry) + if !path.Backend.SSLRedirect { + fgroup.HTTPFrontsMap.AppendHostname(base, back) } + var ns string if host.VarNamespace { - entry.Value = path.Backend.Namespace + ns = path.Backend.Namespace } else { - entry.Value = "-" + ns = "-" } - varNamespaceMap = append(varNamespaceMap, entry) + f.VarNamespaceMap.AppendHostname(base, ns) } if host.HasTLSAuth() { - var entry mapEntry - entry.Key = host.Hostname - invalidCrtList = append(invalidCrtList, entry) + f.TLSInvalidCrtErrorList.AppendHostname(host.Hostname, "") if !host.TLS.CAVerifyOptional { - noCrtList = append(noCrtList, entry) + f.TLSNoCrtErrorList.AppendHostname(host.Hostname, "") } - if host.TLS.CAErrorPage != "" { - entry.Value = host.TLS.CAErrorPage - invalidCrtMap = append(invalidCrtMap, entry) + page := host.TLS.CAErrorPage + if page != "" { + f.TLSInvalidCrtErrorPagesMap.AppendHostname(host.Hostname, page) if !host.TLS.CAVerifyOptional { - noCrtMap = append(noCrtMap, entry) + f.TLSNoCrtErrorPagesMap.AppendHostname(host.Hostname, page) } } } + // TODO wildcard/alias/alias-regex hostname can overlap + // a configured domain which doesn't have rootRedirect + if host.RootRedirect != "" { + fgroup.HTTPRootRedirMap.AppendHostname(host.Hostname, host.RootRedirect) + f.RootRedirMap.AppendHostname(host.Hostname, host.RootRedirect) + } } for _, bind := range f.Binds { - var useServerList []mapEntry for _, host := range bind.Hosts { - useServerList = append(useServerList, mapEntry{Key: host.Hostname}) + bind.UseServerList.AppendHostname(host.Hostname, "") } - if err := c.mapsTemplate.WriteOutput(useServerList, bind.UseServerList); err != nil { - return nil, err - } - } - if err := c.mapsTemplate.WriteOutput(hostBackendsMap, f.HostBackendsMap); err != nil { - return nil, err } - if err := c.mapsTemplate.WriteOutput(sniBackendsMap, f.SNIBackendsMap); err != nil { - return nil, err - } - if err := c.mapsTemplate.WriteOutput(invalidCrtList, f.TLSInvalidCrtErrorList); err != nil { - return nil, err - } - if err := c.mapsTemplate.WriteOutput(invalidCrtMap, f.TLSInvalidCrtErrorPagesMap); err != nil { - return nil, err + } + if err := writeMaps(fgroup.Maps, c.mapsTemplate); err != nil { + return err + } + for _, f := range frontends { + if err := writeMaps(f.Maps, c.mapsTemplate); err != nil { + return err } - if err := c.mapsTemplate.WriteOutput(noCrtList, f.TLSNoCrtErrorList); err != nil { - return nil, err + for _, bind := range f.Binds { + if err := writeMaps(bind.Maps, c.mapsTemplate); err != nil { + return err + } } - if err := c.mapsTemplate.WriteOutput(noCrtMap, f.TLSNoCrtErrorPagesMap); err != nil { - return nil, err + } + c.fgroup = fgroup + return nil +} + +func writeMaps(maps *hatypes.HostsMaps, template *template.Config) error { + for _, hmap := range maps.Items { + if err := template.WriteOutput(hmap.Match, hmap.MatchFile); err != nil { + return err } - if err := c.mapsTemplate.WriteOutput(varNamespaceMap, f.VarNamespaceMap); err != nil { - return nil, err + if len(hmap.Regex) > 0 { + if err := template.WriteOutput(hmap.Regex, hmap.RegexFile); err != nil { + return err + } } } - if err := c.mapsTemplate.WriteOutput(sslpassthroughMap, fgroup.SSLPassthroughMap); err != nil { - return nil, err - } - if err := c.mapsTemplate.WriteOutput(redirectMap, fgroup.RedirectMap); err != nil { - return nil, err - } - if err := c.mapsTemplate.WriteOutput(httpFront, fgroup.HTTPFrontsMap); err != nil { - return nil, err - } - fgroup.HasHTTPHost = len(httpFront) > 0 - return fgroup, nil + return nil } func (c *config) createCertsDir(bindName string, hosts []*hatypes.Host) (string, error) { diff --git a/pkg/haproxy/config_test.go b/pkg/haproxy/config_test.go index dba68dcfe..3beddb4ec 100644 --- a/pkg/haproxy/config_test.go +++ b/pkg/haproxy/config_test.go @@ -24,11 +24,11 @@ import ( func TestEmptyFrontend(t *testing.T) { c := createConfig(&ha_helper.BindUtilsMock{}, options{}) - if _, err := c.BuildFrontendGroup(); err == nil { + if err := c.BuildFrontendGroup(); err == nil { t.Error("expected error creating empty frontend") } c.AcquireHost("empty") - if _, err := c.BuildFrontendGroup(); err != nil { + if err := c.BuildFrontendGroup(); err != nil { t.Errorf("error creating frontends: %v", err) } } @@ -58,11 +58,11 @@ func TestBuildID(t *testing.T) { testCases := []struct { namespace string name string - port int + port string expected string }{ { - "default", "echo", 8080, "default_echo_8080", + "default", "echo", "8080", "default_echo_8080", }, } for _, test := range testCases { @@ -86,13 +86,13 @@ func TestEqual(t *testing.T) { if !c1.Equals(c2) { t.Error("c1 and c2 should be equals (default cert)") } - b1 := c1.AcquireBackend("d", "app1", 8080) - c1.AcquireBackend("d", "app2", 8080) + b1 := c1.AcquireBackend("d", "app1", "8080") + c1.AcquireBackend("d", "app2", "8080") if c1.Equals(c2) { t.Error("c1 and c2 should not be equals (backends on one side)") } - c2.AcquireBackend("d", "app2", 8080) - b2 := c2.AcquireBackend("d", "app1", 8080) + c2.AcquireBackend("d", "app2", "8080") + b2 := c2.AcquireBackend("d", "app1", "8080") if !c1.Equals(c2) { t.Error("c1 and c2 should be equals (with backends)") } @@ -106,8 +106,8 @@ func TestEqual(t *testing.T) { if !c1.Equals(c2) { t.Error("c1 and c2 should be equals (with hosts)") } - _, err1 := c1.BuildFrontendGroup() - _, err2 := c2.BuildFrontendGroup() + err1 := c1.BuildFrontendGroup() + err2 := c2.BuildFrontendGroup() if err1 != nil { t.Errorf("error building c1: %v", err1) } diff --git a/pkg/haproxy/instance.go b/pkg/haproxy/instance.go index 6bd5fde55..d0201d278 100644 --- a/pkg/haproxy/instance.go +++ b/pkg/haproxy/instance.go @@ -117,6 +117,11 @@ func (i *instance) Update() { i.logger.InfoV(2, "new configuration is empty") return } + if err := i.curConfig.BuildFrontendGroup(); err != nil { + i.logger.Error("error building configuration group: %v", err) + i.clearConfig() + return + } if i.curConfig.Equals(i.oldConfig) { i.logger.InfoV(2, "old and new configurations match, skipping reload") i.clearConfig() @@ -141,7 +146,6 @@ func (i *instance) Update() { i.logger.Error("error reloading server:\n%v", err) return } - i.clearConfig() i.logger.Info("HAProxy successfully reloaded") } @@ -164,8 +168,9 @@ func (i *instance) reload() error { } out, err := exec.Command(i.options.ReloadCmd, i.options.ReloadStrategy, i.options.HAProxyConfigFile).CombinedOutput() if len(out) > 0 { - return fmt.Errorf(string(out)) - } else if err != nil { + i.logger.Warn("output from haproxy:\n%v", string(out)) + } + if err != nil { return err } return nil diff --git a/pkg/haproxy/instance_test.go b/pkg/haproxy/instance_test.go index a2d17c524..cf11d2dd6 100644 --- a/pkg/haproxy/instance_test.go +++ b/pkg/haproxy/instance_test.go @@ -32,6 +32,177 @@ import ( "github.com/jcmoraisjr/haproxy-ingress/pkg/types/helper_test" ) +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * BACKEND TESTCASES + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +func TestBackends(t *testing.T) { + testCases := []struct { + doconfig func(g *hatypes.Global, b *hatypes.Backend) + path []string + srvsuffix string + expected string + }{ + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.Cookie.Name = "ingress-controller" + b.Cookie.Strategy = "insert" + }, + srvsuffix: "cookie s1", + expected: ` + cookie ingress-controller insert indirect nocache httponly`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.Cookie.Name = "Ingress" + b.Cookie.Strategy = "prefix" + b.Cookie.Dynamic = true + }, + expected: ` + cookie Ingress prefix dynamic + dynamic-cookie-key "Ingress"`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.Cors.Enabled = true + b.Cors.AllowOrigin = "*" + b.Cors.AllowHeaders = + "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + b.Cors.AllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS" + b.Cors.MaxAge = 86400 + }, + expected: ` + http-request use-service lua.send-response if METH_OPTIONS + http-response set-status 204 reason "No Content" if METH_OPTIONS + http-response set-header Content-Type "text/plain" if METH_OPTIONS + http-response set-header Content-Length "0" if METH_OPTIONS + http-response set-header Access-Control-Allow-Origin "*" if METH_OPTIONS + http-response set-header Access-Control-Allow-Methods "GET, PUT, POST, DELETE, PATCH, OPTIONS" if METH_OPTIONS + http-response set-header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" if METH_OPTIONS + http-response set-header Access-Control-Max-Age "86400" if METH_OPTIONS + http-response set-header Access-Control-Allow-Origin "*" + http-response set-header Access-Control-Allow-Methods "GET, PUT, POST, DELETE, PATCH, OPTIONS" + http-response set-header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.HSTS.Enabled = true + b.HSTS.MaxAge = 15768000 + b.HSTS.Preload = true + b.HSTS.Subdomains = true + }, + expected: ` + http-response set-header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" if { ssl_fc }`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + g.ForwardFor = "add" + }, + expected: ` + http-request set-header X-Original-Forwarded-For %[hdr(x-forwarded-for)] if { hdr(x-forwarded-for) -m found } + http-request del-header x-forwarded-for + option forwardfor`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.RewriteURL = "/" + }, + path: []string{"/app"}, + expected: ` + reqrep ^([^:\ ]*)\ /app/?(.*)$ \1\ /\2`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.RewriteURL = "/other" + }, + path: []string{"/app"}, + expected: ` + reqrep ^([^:\ ]*)\ /app(.*)$ \1\ /other\2`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.RewriteURL = "/other/" + }, + path: []string{"/app", "/app/sub"}, + expected: ` + reqrep ^([^:\ ]*)\ /app/sub(.*)$ \1\ /other/\2 + reqrep ^([^:\ ]*)\ /app(.*)$ \1\ /other/\2`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.Whitelist = []string{"10.0.0.0/8", "192.168.0.0/16"} + }, + expected: ` + http-request deny if !{ src 10.0.0.0/8 192.168.0.0/16 }`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.Whitelist = []string{"10.0.0.0/8", "192.168.0.0/16"} + b.ModeTCP = true + }, + expected: ` + tcp-request content reject if !{ src 10.0.0.0/8 192.168.0.0/16 }`, + }, + { + doconfig: func(g *hatypes.Global, b *hatypes.Backend) { + b.OAuth.Impl = "oauth2_proxy" + b.OAuth.BackendName = "system_oauth_4180" + b.OAuth.URIPrefix = "/oauth2" + b.OAuth.Headers = map[string]string{"X-Auth-Request-Email": "auth_response_email"} + }, + expected: ` + http-request set-header X-Real-IP %[src] + http-request lua.auth-request system_oauth_4180 /oauth2/auth + http-request redirect location /oauth2/start?rd=%[path] if !{ path_beg /oauth2/ } !{ var(txn.auth_response_successful) -m bool } + http-request set-header X-Auth-Request-Email %[var(txn.auth_response_email)] if { var(txn.auth_response_email) -m found }`, + }, + } + for _, test := range testCases { + c := setup(t) + + if len(test.path) == 0 { + test.path = []string{"/"} + } + if test.srvsuffix != "" { + test.srvsuffix = " " + test.srvsuffix + } + + var h *hatypes.Host + var b *hatypes.Backend + + b = c.config.AcquireBackend("d1", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS1} + h = c.config.AcquireHost("d1.local") + for _, p := range test.path { + h.AddPath(b, p) + } + test.doconfig(c.config.Global(), b) + + var mode string + if b.ModeTCP { + mode = "tcp" + } else { + mode = "http" + } + + c.instance.Update() + c.checkConfig(` +<<global>> +<<defaults>> +backend d1_app_8080 + mode ` + mode + test.expected + ` + server s1 172.17.0.11:8080 weight 100` + test.srvsuffix + ` +<<backends-default>> +<<frontends-default>> +`) + + c.logger.CompareLogging(defaultLogging) + c.teardown() + } +} + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * TEMPLATES @@ -42,25 +213,36 @@ func TestInstanceEmpty(t *testing.T) { c := setup(t) defer c.teardown() - template := ` + c.config.AcquireHost("empty").AddPath(c.config.AcquireBackend("default", "empty", "8080"), "/") + c.instance.Update() + + c.checkConfig(` global daemon - quiet - stats socket %s level admin expose-fd listeners - maxconn 0 + stats socket /var/run/haproxy.sock level admin expose-fd listeners + maxconn 2000 + hard-stop-after 15m lua-load /usr/local/etc/haproxy/lua/send-response.lua lua-load /usr/local/etc/haproxy/lua/auth-request.lua - tune.ssl.default-dh-param 0 + ssl-dh-param-file /var/haproxy/tls/dhparam.pem + ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256 + ssl-default-bind-options no-sslv3 defaults log global - maxconn 0 + maxconn 2000 option redispatch option dontlognull option http-server-close option http-keep-alive - timeout client %s - timeout connect %s - timeout server %s + timeout client 50s + timeout client-fin 50s + timeout connect 5s + timeout http-keep-alive 1m + timeout http-request 5s + timeout queue 5s + timeout server 50s + timeout server-fin 50s + timeout tunnel 1h backend default_empty_8080 mode http backend _error404 @@ -75,27 +257,29 @@ backend _error496 mode http errorfile 400 /usr/local/etc/haproxy/errors/496.http http-request deny deny_status 400 -frontend _front__http +frontend _front_http mode http bind :80 - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/http-front.map,_nomatch) - use_backend %%[var(req.backend)] unless { var(req.backend) _nomatch } + http-request set-var(req.base) base,regsub(:[0-9]+/,/) + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } default_backend _error404 -frontend https-front_empty +frontend _front001 mode http bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem - http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_empty_host.map,_nomatch) - use_backend %%[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + http-request set-var(req.hostbackend) base,lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + <<tls-del-headers>> + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } default_backend _error404 -` - - c.config.AcquireHost("empty").AddPath(c.config.AcquireBackend("default", "empty", 8080), "/") - c.instance.Update() - c.checkConfigFull(fmt.Sprintf(template, "--", "--", "--", "--")) +`) - c.checkMap("http-front.map", ` + c.checkMap("_global_http_front.map", ` empty/ default_empty_8080`) - c.checkMap("https-front_empty_host.map", ` + c.checkMap("_global_https_redir.map", ` +empty/ no`) + c.checkMap("_front001_host.map", ` empty/ default_empty_8080`) c.logger.CompareLogging(defaultLogging) @@ -105,22 +289,21 @@ func TestInstanceDefaultHost(t *testing.T) { c := setup(t) defer c.teardown() - c.configGlobal() - def := c.config.AcquireBackend("default", "default-backend", 8080) + def := c.config.AcquireBackend("default", "default-backend", "8080") def.Endpoints = []*hatypes.Endpoint{endpointS0} c.config.ConfigDefaultBackend(def) var h *hatypes.Host var b *hatypes.Backend - b = c.config.AcquireBackend("d1", "app", 8080) + b = c.config.AcquireBackend("d1", "app", "8080") h = c.config.AcquireHost("*") h.AddPath(b, "/") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} h.VarNamespace = true - b = c.config.AcquireBackend("d2", "app", 8080) + b = c.config.AcquireBackend("d2", "app", "8080") h = c.config.AcquireHost("d2.local") h.AddPath(b, "/app") b.SSLRedirect = true @@ -129,6 +312,8 @@ func TestInstanceDefaultHost(t *testing.T) { c.instance.Update() c.checkConfig(` +<<global>> +<<defaults>> backend d1_app_8080 mode http server s1 172.17.0.11:8080 weight 100 @@ -137,28 +322,39 @@ backend d2_app_8080 server s1 172.17.0.11:8080 weight 100 backend _default_backend mode http - server s0 172.17.0.99:8080 weight 100`, ` -frontend _front__http + server s0 172.17.0.99:8080 weight 100 +<<backend-errors>> +frontend _front_http mode http bind :80 http-request set-var(req.base) base,regsub(:[0-9]+/,/) - redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/redirect.map,_nomatch) yes } + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } use_backend d1_app_8080 -frontend https-front_d2.local +frontend _front001 mode http bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem - http-request set-var(txn.namespace) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d2.local_k8s_ns.map,-) - http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d2.local_host.map,_nomatch) + http-request set-var(req.base) base,lower,regsub(:[0-9]+/,/) + http-request set-var(req.hostbackend) var(req.base),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + http-request set-var(txn.namespace) var(req.base),map_beg(/etc/haproxy/maps/_front001_k8s_ns.map,-) + <<tls-del-headers>> use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } use_backend d1_app_8080 `) - c.checkMap("https-front_d2.local_k8s_ns.map", ` -d2.local/app d2`) - c.checkMap("https-front_d2.local_host.map", ` -d2.local/app d2_app_8080`) - c.checkMap("redirect.map", ` -d2.local/app yes`) + c.checkMap("_global_http_front.map", ` +`) + c.checkMap("_global_https_redir.map", ` +d2.local/app yes +`) + c.checkMap("_front001_k8s_ns.map", ` +d2.local/app d2 +`) + c.checkMap("_front001_host.map", ` +d2.local/app d2_app_8080 +`) c.logger.CompareLogging(defaultLogging) } @@ -167,15 +363,14 @@ func TestInstanceSingleFrontendSingleBind(t *testing.T) { c := setup(t) defer c.teardown() - c.configGlobal() - def := c.config.AcquireBackend("default", "default-backend", 8080) + def := c.config.AcquireBackend("default", "default-backend", "8080") def.Endpoints = []*hatypes.Endpoint{endpointS0} c.config.ConfigDefaultBackend(def) var h *hatypes.Host var b *hatypes.Backend - b = c.config.AcquireBackend("d1", "app", 8080) + b = c.config.AcquireBackend("d1", "app", "8080") h = c.config.AcquireHost("d1.local") h.AddPath(b, "/") b.SSLRedirect = true @@ -184,7 +379,7 @@ func TestInstanceSingleFrontendSingleBind(t *testing.T) { h.TLS.TLSFilename = "/var/haproxy/ssl/certs/d1.pem" h.TLS.TLSHash = "1" - b = c.config.AcquireBackend("d2", "app", 8080) + b = c.config.AcquireBackend("d2", "app", "8080") h = c.config.AcquireHost("d2.local") h.AddPath(b, "/app") b.SSLRedirect = true @@ -194,6 +389,8 @@ func TestInstanceSingleFrontendSingleBind(t *testing.T) { c.instance.Update() c.checkConfig(` +<<global>> +<<defaults>> backend d1_app_8080 mode http server s1 172.17.0.11:8080 weight 100 @@ -202,31 +399,42 @@ backend d2_app_8080 server s1 172.17.0.11:8080 weight 100 backend _default_backend mode http - server s0 172.17.0.99:8080 weight 100`, ` -frontend _front__http + server s0 172.17.0.99:8080 weight 100 +<<backend-errors>> +frontend _front_http mode http bind :80 http-request set-var(req.base) base,regsub(:[0-9]+/,/) - redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/redirect.map,_nomatch) yes } + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } default_backend _default_backend -frontend _front_001 +frontend _front001 mode http bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem crt /var/haproxy/certs/_public - http-request set-var(txn.namespace) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_k8s_ns.map,-) - http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_host.map,_nomatch) + http-request set-var(req.base) base,lower,regsub(:[0-9]+/,/) + http-request set-var(req.hostbackend) var(req.base),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + http-request set-var(txn.namespace) var(req.base),map_beg(/etc/haproxy/maps/_front001_k8s_ns.map,-) + <<tls-del-headers>> use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } default_backend _default_backend `) - c.checkMap("_front_001_k8s_ns.map", ` -d1.local/ d1 -d2.local/app -`) - c.checkMap("_front_001_host.map", ` -d1.local/ d1_app_8080 -d2.local/app d2_app_8080`) - c.checkMap("redirect.map", ` + c.checkMap("_global_http_front.map", ` +`) + c.checkMap("_global_https_redir.map", ` d1.local/ yes -d2.local/app yes`) +d2.local/app yes +`) + c.checkMap("_front001_host.map", ` +d1.local/ d1_app_8080 +d2.local/app d2_app_8080 +`) + c.checkMap("_front001_k8s_ns.map", ` +d1.local/ d1 +d2.local/app - +`) c.checkCerts(` certdirs: @@ -242,19 +450,19 @@ func TestInstanceSingleFrontendTwoBindsCA(t *testing.T) { c := setup(t) defer c.teardown() - c.configGlobal() - def := c.config.AcquireBackend("default", "default-backend", 8080) + def := c.config.AcquireBackend("default", "default-backend", "8080") def.Endpoints = []*hatypes.Endpoint{endpointS0} c.config.ConfigDefaultBackend(def) var h *hatypes.Host var b *hatypes.Backend - b = c.config.AcquireBackend("d", "app", 8080) + b = c.config.AcquireBackend("d", "app", "8080") h = c.config.AcquireHost("d1.local") h.AddPath(b, "/") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} + b.SSL.AddCertHeader = true h.TLS.CAFilename = "/var/haproxy/ssl/ca/d1.local.pem" h.TLS.CAHash = "1" h.TLS.CAErrorPage = "http://d1.local/error.html" @@ -266,72 +474,96 @@ func TestInstanceSingleFrontendTwoBindsCA(t *testing.T) { c.instance.Update() c.checkConfig(` +<<global>> +<<defaults>> backend d_app_8080 mode http + http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)] + http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn] + http-request set-header X-SSL-Client-SHA1 %{+Q}[ssl_c_sha1,hex] + http-request set-header X-SSL-Client-Cert %{+Q}[ssl_c_der,base64] server s1 172.17.0.11:8080 weight 100 backend _default_backend mode http - server s0 172.17.0.99:8080 weight 100`, ` + server s0 172.17.0.99:8080 weight 100 +<<backend-errors>> listen _front__tls mode tcp bind :443 tcp-request inspect-delay 5s tcp-request content accept if { req.ssl_hello_type 1 } - ## _front_001 - use-server _server_d1.local if { req.ssl_sni -i -f /etc/haproxy/maps/_front_001_bind_d1.local.list } - server _server_d1.local unix@/var/run/front_d1.local.sock send-proxy-v2 weight 0 - use-server _server_d2.local if { req.ssl_sni -i -f /etc/haproxy/maps/_front_001_bind_d2.local.list } - server _server_d2.local unix@/var/run/front_d2.local.sock send-proxy-v2 weight 0 + ## _front001/_socket001 + use-server _server_socket001 if { req.ssl_sni -i -f /etc/haproxy/maps/_socket001.list } + server _server_socket001 unix@/var/run/_socket001.sock send-proxy-v2 weight 0 + ## _front001/_socket002 + use-server _server_socket002 if { req.ssl_sni -i -f /etc/haproxy/maps/_socket002.list } + server _server_socket002 unix@/var/run/_socket002.sock send-proxy-v2 weight 0 # TODO default backend -frontend _front__http +frontend _front_http mode http bind :80 http-request set-var(req.base) base,regsub(:[0-9]+/,/) - redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/redirect.map,_nomatch) yes } + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } default_backend _default_backend -frontend _front_001 +frontend _front001 mode http - bind unix@/var/run/front_d1.local.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d1.local.pem verify optional ca-ignore-err all crt-ignore-err all - bind unix@/var/run/front_d2.local.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d2.local.pem verify optional ca-ignore-err all crt-ignore-err all - http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_host.map,_nomatch) + bind unix@/var/run/_socket001.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d1.local.pem verify optional ca-ignore-err all crt-ignore-err all + bind unix@/var/run/_socket002.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d2.local.pem verify optional ca-ignore-err all crt-ignore-err all + http-request set-var(req.hostbackend) base,lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + <<tls-del-headers>> http-request set-header x-ha-base %[ssl_fc_sni]%[path] - http-request set-var(req.snibackend) hdr(x-ha-base),regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_sni.map,_nomatch) - acl tls-invalid-crt ssl_c_ca_err gt 0 - acl tls-invalid-crt ssl_c_err gt 0 + http-request set-var(req.snibackend) hdr(x-ha-base),lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front001_sni.map,_nomatch) acl tls-has-crt ssl_c_used - http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_no_crt_redir.map,_internal) if !tls-has-crt - http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_inv_crt_redir.map,_internal) if tls-invalid-crt + acl tls-need-crt ssl_fc_sni -i -f /etc/haproxy/maps/_front001_no_crt.list + acl tls-has-invalid-crt ssl_c_ca_err gt 0 + acl tls-has-invalid-crt ssl_c_err gt 0 + acl tls-check-crt ssl_fc_sni -i -f /etc/haproxy/maps/_front001_inv_crt.list + http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,lower,map(/etc/haproxy/maps/_front001_no_crt_redir.map,_internal) if !tls-has-crt tls-need-crt + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,lower,map(/etc/haproxy/maps/_front001_inv_crt_redir.map,_internal) if tls-has-invalid-crt tls-check-crt http-request redirect location %[var(req.tls_nocrt_redir)] code 303 if { var(req.tls_nocrt_redir) -m found } !{ var(req.tls_nocrt_redir) _internal } http-request redirect location %[var(req.tls_invalidcrt_redir)] code 303 if { var(req.tls_invalidcrt_redir) -m found } !{ var(req.tls_invalidcrt_redir) _internal } - use_backend _error496 if { var(req.tls_nocrt_redir) _internal } { ssl_fc_sni -i -f /etc/haproxy/maps/_front_001_no_crt.list } - use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } { ssl_fc_sni -i -f /etc/haproxy/maps/_front_001_inv_crt.list } + use_backend _error496 if { var(req.tls_nocrt_redir) _internal } + use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } use_backend %[var(req.snibackend)] unless { var(req.snibackend) _nomatch } default_backend _default_backend `) - c.checkMap("_front_001_inv_crt_redir.map", ` -d1.local http://d1.local/error.html`) - c.checkMap("_front_001_inv_crt.list", ` + c.checkMap("_socket001.list", ` d1.local -d2.local`) - c.checkMap("_front_001_no_crt_redir.map", ` -d1.local http://d1.local/error.html`) - c.checkMap("_front_001_no_crt.list", ` -d1.local -d2.local`) - c.checkMap("_front_001_host.map", ` `) - c.checkMap("_front_001_sni.map", ` -d1.local/ d_app_8080 -d2.local/ d_app_8080`) - c.checkMap("_front_001_bind_d1.local.list", ` -d1.local`) - c.checkMap("_front_001_bind_d2.local.list", ` -d2.local`) - c.checkMap("redirect.map", ` + c.checkMap("_socket002.list", ` +d2.local +`) + c.checkMap("_global_http_front.map", ` +`) + c.checkMap("_global_https_redir.map", ` d1.local/ yes -d2.local/ yes`) +d2.local/ yes +`) + c.checkMap("_front001_host.map", ` +`) + c.checkMap("_front001_sni.map", ` +d1.local/ d_app_8080 +d2.local/ d_app_8080 +`) + c.checkMap("_front001_no_crt.list", ` +d1.local +d2.local +`) + c.checkMap("_front001_inv_crt.list", ` +d1.local +d2.local +`) + c.checkMap("_front001_no_crt_redir.map", ` +d1.local http://d1.local/error.html +`) + c.checkMap("_front001_inv_crt_redir.map", ` +d1.local http://d1.local/error.html +`) c.logger.CompareLogging(defaultLogging) } @@ -340,18 +572,16 @@ func TestInstanceTwoFrontendsThreeBindsCA(t *testing.T) { c := setup(t) defer c.teardown() - c.configGlobal() - def := c.config.AcquireBackend("default", "default-backend", 8080) + def := c.config.AcquireBackend("default", "default-backend", "8080") def.Endpoints = []*hatypes.Endpoint{endpointS0} c.config.ConfigDefaultBackend(def) var h *hatypes.Host var b *hatypes.Backend - b = c.config.AcquireBackend("d", "appca", 8080) + b = c.config.AcquireBackend("d", "appca", "8080") h = c.config.AcquireHost("d1.local") h.AddPath(b, "/") - b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} h.Timeout.Client = "1s" h.TLS.CAFilename = "/var/haproxy/ssl/ca/d1.local.pem" @@ -378,7 +608,7 @@ func TestInstanceTwoFrontendsThreeBindsCA(t *testing.T) { h.TLS.CAHash = "1" h.TLS.CAErrorPage = "http://d22.local/error.html" - b = c.config.AcquireBackend("d", "app", 8080) + b = c.config.AcquireBackend("d", "app", "8080") h = c.config.AcquireHost("d3.local") h.AddPath(b, "/") b.Endpoints = []*hatypes.Endpoint{endpointS21} @@ -390,124 +620,148 @@ func TestInstanceTwoFrontendsThreeBindsCA(t *testing.T) { c.instance.Update() c.checkConfig(` +<<global>> +<<defaults>> backend d_app_8080 mode http server s21 172.17.0.121:8080 weight 100 backend d_appca_8080 mode http + http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)] if { ssl_fc } + http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn] if { ssl_fc } + http-request set-header X-SSL-Client-SHA1 %{+Q}[ssl_c_sha1,hex] if { ssl_fc } server s1 172.17.0.11:8080 weight 100 backend _default_backend mode http - server s0 172.17.0.99:8080 weight 100`, ` + server s0 172.17.0.99:8080 weight 100 +<<backend-errors>> listen _front__tls mode tcp bind :443 tcp-request inspect-delay 5s tcp-request content accept if { req.ssl_hello_type 1 } - ## _front_001 - use-server _server__socket001 if { req.ssl_sni -i -f /etc/haproxy/maps/_front_001_bind__socket001.list } - server _server__socket001 unix@/var/run/front__socket001.sock send-proxy-v2 weight 0 - use-server _server__socket002 if { req.ssl_sni -i -f /etc/haproxy/maps/_front_001_bind__socket002.list } - server _server__socket002 unix@/var/run/front__socket002.sock send-proxy-v2 weight 0 - ## https-front_d1.local - use-server _server_d1.local if { req.ssl_sni -i -f /etc/haproxy/maps/https-front_d1.local_bind_d1.local.list } - server _server_d1.local unix@/var/run/front_d1.local.sock send-proxy-v2 weight 0 + ## _front001/_socket001 + use-server _server_socket001 if { req.ssl_sni -i -f /etc/haproxy/maps/_socket001.list } + server _server_socket001 unix@/var/run/_socket001.sock send-proxy-v2 weight 0 + ## _front002/_socket002 + use-server _server_socket002 if { req.ssl_sni -i -f /etc/haproxy/maps/_socket002.list } + server _server_socket002 unix@/var/run/_socket002.sock send-proxy-v2 weight 0 + ## _front002/_socket003 + use-server _server_socket003 if { req.ssl_sni -i -f /etc/haproxy/maps/_socket003.list } + server _server_socket003 unix@/var/run/_socket003.sock send-proxy-v2 weight 0 # TODO default backend -frontend _front__http +frontend _front_http mode http bind :80 http-request set-var(req.base) base,regsub(:[0-9]+/,/) - http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/http-front.map,_nomatch) - redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/redirect.map,_nomatch) yes } + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } default_backend _default_backend -frontend _front_001 +frontend _front001 mode http - bind unix@/var/run/front__socket001.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem crt /var/haproxy/certs/_socket001 ca-file /var/haproxy/ssl/ca/d2.local.pem verify optional ca-ignore-err all crt-ignore-err all - bind unix@/var/run/front__socket002.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem - timeout client 2s - http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_host.map,_nomatch) + bind unix@/var/run/_socket001.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d1.local.pem verify optional ca-ignore-err all crt-ignore-err all + timeout client 1s + http-request set-var(req.hostbackend) base,lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + <<tls-del-headers>> http-request set-header x-ha-base %[ssl_fc_sni]%[path] - http-request set-var(req.snibackend) hdr(x-ha-base),regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front_001_sni.map,_nomatch) - acl tls-invalid-crt ssl_c_ca_err gt 0 - acl tls-invalid-crt ssl_c_err gt 0 - acl tls-has-crt ssl_c_used - http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_no_crt_redir.map,_internal) if !tls-has-crt - http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/_front_001_inv_crt_redir.map,_internal) if tls-invalid-crt - http-request redirect location %[var(req.tls_nocrt_redir)] code 303 if { var(req.tls_nocrt_redir) -m found } !{ var(req.tls_nocrt_redir) _internal } + http-request set-var(req.snibackend) hdr(x-ha-base),lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front001_sni.map,_nomatch) + acl tls-has-invalid-crt ssl_c_ca_err gt 0 + acl tls-has-invalid-crt ssl_c_err gt 0 + acl tls-check-crt ssl_fc_sni -i -f /etc/haproxy/maps/_front001_inv_crt.list + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,lower,map(/etc/haproxy/maps/_front001_inv_crt_redir.map,_internal) if tls-has-invalid-crt tls-check-crt http-request redirect location %[var(req.tls_invalidcrt_redir)] code 303 if { var(req.tls_invalidcrt_redir) -m found } !{ var(req.tls_invalidcrt_redir) _internal } - use_backend _error496 if { var(req.tls_nocrt_redir) _internal } { ssl_fc_sni -i -f /etc/haproxy/maps/_front_001_no_crt.list } - use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } { ssl_fc_sni -i -f /etc/haproxy/maps/_front_001_inv_crt.list } + use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } use_backend %[var(req.snibackend)] unless { var(req.snibackend) _nomatch } default_backend _default_backend -frontend https-front_d1.local +frontend _front002 mode http - bind unix@/var/run/front_d1.local.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d1.local.pem verify optional ca-ignore-err all crt-ignore-err all - timeout client 1s - http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d1.local_host.map,_nomatch) + bind unix@/var/run/_socket002.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem crt /var/haproxy/certs/_socket002 ca-file /var/haproxy/ssl/ca/d2.local.pem verify optional ca-ignore-err all crt-ignore-err all + bind unix@/var/run/_socket003.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem + timeout client 2s + http-request set-var(req.hostbackend) base,lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front002_host.map,_nomatch) + <<tls-del-headers>> http-request set-header x-ha-base %[ssl_fc_sni]%[path] - http-request set-var(req.snibackend) hdr(x-ha-base),regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d1.local_sni.map,_nomatch) - acl tls-invalid-crt ssl_c_ca_err gt 0 - acl tls-invalid-crt ssl_c_err gt 0 - http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,map(/etc/haproxy/maps/https-front_d1.local_inv_crt_redir.map,_internal) if tls-invalid-crt + http-request set-var(req.snibackend) hdr(x-ha-base),lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front002_sni.map,_nomatch) + acl tls-has-crt ssl_c_used + acl tls-need-crt ssl_fc_sni -i -f /etc/haproxy/maps/_front002_no_crt.list + acl tls-has-invalid-crt ssl_c_ca_err gt 0 + acl tls-has-invalid-crt ssl_c_err gt 0 + acl tls-check-crt ssl_fc_sni -i -f /etc/haproxy/maps/_front002_inv_crt.list + http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,lower,map(/etc/haproxy/maps/_front002_no_crt_redir.map,_internal) if !tls-has-crt tls-need-crt + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,lower,map(/etc/haproxy/maps/_front002_inv_crt_redir.map,_internal) if tls-has-invalid-crt tls-check-crt + http-request redirect location %[var(req.tls_nocrt_redir)] code 303 if { var(req.tls_nocrt_redir) -m found } !{ var(req.tls_nocrt_redir) _internal } http-request redirect location %[var(req.tls_invalidcrt_redir)] code 303 if { var(req.tls_invalidcrt_redir) -m found } !{ var(req.tls_invalidcrt_redir) _internal } - use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } { ssl_fc_sni -i -f /etc/haproxy/maps/https-front_d1.local_inv_crt.list } + use_backend _error496 if { var(req.tls_nocrt_redir) _internal } + use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } use_backend %[var(req.snibackend)] unless { var(req.snibackend) _nomatch } default_backend _default_backend `) - c.checkMap("http-front.map", ` -d3.local/ d_app_8080 -d4.local/ d_app_8080`) - c.checkMap("_front_001_inv_crt_redir.map", ` -d21.local http://d21.local/error.html -d22.local http://d22.local/error.html + c.checkMap("_socket001.list", ` +d1.local `) - c.checkMap("_front_001_inv_crt.list", ` + c.checkMap("_socket002.list", ` d21.local -d22.local`) - c.checkMap("_front_001_no_crt_redir.map", ` -d22.local http://d22.local/error.html +d22.local `) - c.checkMap("_front_001_no_crt.list", ` -d22.local`) - c.checkMap("_front_001_host.map", ` -d3.local/ d_app_8080 -d4.local/ d_app_8080`) - c.checkMap("_front_001_sni.map", ` -d21.local/ d_appca_8080 -d22.local/ d_appca_8080`) - c.checkMap("_front_001_bind__socket001.list", ` -d21.local -d22.local`) - c.checkMap("_front_001_bind__socket002.list", ` + c.checkMap("_socket003.list", ` d3.local -d4.local`) - c.checkMap("https-front_d1.local_inv_crt_redir.map", ` -d1.local http://d1.local/error.html`) - c.checkMap("https-front_d1.local_inv_crt.list", ` -d1.local`) - c.checkMap("https-front_d1.local_no_crt_redir.map", ` +d4.local `) - c.checkMap("https-front_d1.local_host.map", ` + c.checkMap("_global_http_front.map", ` +d1.local/ d_appca_8080 +d21.local/ d_appca_8080 +d22.local/ d_appca_8080 +d3.local/ d_app_8080 +d4.local/ d_app_8080 `) - c.checkMap("https-front_d1.local_sni.map", ` -d1.local/ d_appca_8080`) - c.checkMap("https-front_d1.local_bind_d1.local.list", ` -d1.local`) - c.checkMap("redirect.map", ` -d21.local/ yes -d22.local/ yes + c.checkMap("_global_https_redir.map", ` +d1.local/ no +d21.local/ no +d22.local/ no d3.local/ no d4.local/ no -d1.local/ yes +`) + c.checkMap("_front001_host.map", ` +`) + c.checkMap("_front001_sni.map", ` +d1.local/ d_appca_8080 +`) + c.checkMap("_front001_inv_crt.list", ` +d1.local`) + c.checkMap("_front001_inv_crt_redir.map", ` +d1.local http://d1.local/error.html +`) + c.checkMap("_front002_host.map", ` +d3.local/ d_app_8080 +d4.local/ d_app_8080 +`) + c.checkMap("_front002_sni.map", ` +d21.local/ d_appca_8080 +d22.local/ d_appca_8080 +`) + c.checkMap("_front002_no_crt.list", ` +d22.local +`) + c.checkMap("_front002_inv_crt.list", ` +d21.local +d22.local +`) + c.checkMap("_front002_no_crt_redir.map", ` +d22.local http://d22.local/error.html +`) + c.checkMap("_front002_inv_crt_redir.map", ` +d21.local http://d21.local/error.html +d22.local http://d22.local/error.html `) c.checkCerts(` certdirs: -- dir: /var/haproxy/certs/_socket001 +- dir: /var/haproxy/certs/_socket002 certs: - /var/haproxy/ssl/certs/d.pem`) @@ -518,35 +772,36 @@ func TestInstanceSomePaths(t *testing.T) { c := setup(t) defer c.teardown() - c.configGlobal() - def := c.config.AcquireBackend("default", "default-backend", 8080) + def := c.config.AcquireBackend("default", "default-backend", "8080") def.Endpoints = []*hatypes.Endpoint{endpointS0} c.config.ConfigDefaultBackend(def) var h *hatypes.Host var b *hatypes.Backend - b = c.config.AcquireBackend("d", "app0", 8080) + b = c.config.AcquireBackend("d", "app0", "8080") h = c.config.AcquireHost("d.local") h.AddPath(b, "/") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} - b = c.config.AcquireBackend("d", "app1", 8080) + b = c.config.AcquireBackend("d", "app1", "8080") h.AddPath(b, "/app") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS1} - b = c.config.AcquireBackend("d", "app2", 8080) + b = c.config.AcquireBackend("d", "app2", "8080") h.AddPath(b, "/app/sub") b.Endpoints = []*hatypes.Endpoint{endpointS21, endpointS22} - b = c.config.AcquireBackend("d", "app3", 8080) + b = c.config.AcquireBackend("d", "app3", "8080") h.AddPath(b, "/sub") b.Endpoints = []*hatypes.Endpoint{endpointS31, endpointS32, endpointS33} c.instance.Update() c.checkConfig(` +<<global>> +<<defaults>> backend d_app0_8080 mode http server s1 172.17.0.11:8080 weight 100 @@ -564,65 +819,74 @@ backend d_app3_8080 server s33 172.17.0.133:8080 weight 100 backend _default_backend mode http - server s0 172.17.0.99:8080 weight 100`, ` -frontend _front__http + server s0 172.17.0.99:8080 weight 100 +<<backend-errors>> +frontend _front_http mode http bind :80 http-request set-var(req.base) base,regsub(:[0-9]+/,/) - http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/http-front.map,_nomatch) - redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/redirect.map,_nomatch) yes } + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } default_backend _default_backend -frontend https-front_d.local +frontend _front001 mode http bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem - http-request set-var(req.hostbackend) base,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/https-front_d.local_host.map,_nomatch) + http-request set-var(req.hostbackend) base,lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + <<tls-del-headers>> use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } default_backend _default_backend `) - c.checkMap("https-front_d.local_host.map", ` + c.checkMap("_global_http_front.map", ` d.local/sub d_app3_8080 d.local/app/sub d_app2_8080 -d.local/app d_app1_8080 -d.local/ d_app0_8080`) - c.checkMap("redirect.map", ` +`) + c.checkMap("_global_https_redir.map", ` d.local/sub no d.local/app/sub no d.local/app yes -d.local/ yes`) +d.local/ yes +`) + c.checkMap("_front001_host.map", ` +d.local/sub d_app3_8080 +d.local/app/sub d_app2_8080 +d.local/app d_app1_8080 +d.local/ d_app0_8080 +`) c.logger.CompareLogging(defaultLogging) } -func TestSSLPassthrough(t *testing.T) { +func TestInstanceSSLPassthrough(t *testing.T) { c := setup(t) defer c.teardown() - c.configGlobal() - var h *hatypes.Host var b *hatypes.Backend - b = c.config.AcquireBackend("d2", "app", 8080) + b = c.config.AcquireBackend("d2", "app", "8080") h = c.config.AcquireHost("d2.local") h.AddPath(b, "/") b.SSLRedirect = true b.Endpoints = []*hatypes.Endpoint{endpointS31} h.SSLPassthrough = true - b = c.config.AcquireBackend("d3", "app-ssl", 8443) + b = c.config.AcquireBackend("d3", "app-ssl", "8443") h = c.config.AcquireHost("d3.local") h.AddPath(b, "/") b.Endpoints = []*hatypes.Endpoint{endpointS41s} h.SSLPassthrough = true - b = c.config.AcquireBackend("d3", "app-http", 8080) + b = c.config.AcquireBackend("d3", "app-http", "8080") b.Endpoints = []*hatypes.Endpoint{endpointS41h} h.HTTPPassthroughBackend = b c.instance.Update() c.checkConfig(` +<<global>> +<<defaults>> backend d2_app_8080 mode http server s31 172.17.0.131:8080 weight 100 @@ -632,40 +896,579 @@ backend d3_app-http_8080 backend d3_app-ssl_8443 mode http server s41s 172.17.0.141:8443 weight 100 -backend _error404 - mode http - errorfile 400 /usr/local/etc/haproxy/errors/404.http - http-request deny deny_status 400`, ` +<<backends-default>> listen _front__tls mode tcp bind :443 tcp-request inspect-delay 5s tcp-request content accept if { req.ssl_hello_type 1 } ## ssl-passthrough - tcp-request content set-var(req.backend) req.ssl_sni,lower,map(/etc/haproxy/maps/sslpassthrough.map,_nomatch) + tcp-request content set-var(req.backend) req.ssl_sni,lower,map(/etc/haproxy/maps/_global_sslpassthrough.map,_nomatch) use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } # TODO default backend -frontend _front__http +frontend _front_http mode http bind :80 http-request set-var(req.base) base,regsub(:[0-9]+/,/) - http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/http-front.map,_nomatch) - redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/redirect.map,_nomatch) yes } + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } default_backend _error404`) - c.checkMap("sslpassthrough.map", ` + c.checkMap("_global_sslpassthrough.map", ` d2.local d2_app_8080 d3.local d3_app-ssl_8443`) - c.checkMap("http-front.map", ` + c.checkMap("_global_http_front.map", ` d3.local/ d3_app-http_8080`) - c.checkMap("redirect.map", ` + c.checkMap("_global_https_redir.map", ` d2.local/ yes d3.local/ no`) c.logger.CompareLogging(defaultLogging) } +func TestInstanceRootRedirect(t *testing.T) { + c := setup(t) + defer c.teardown() + + var h *hatypes.Host + var b *hatypes.Backend + + b = c.config.AcquireBackend("d1", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS1} + b.SSLRedirect = false + h = c.config.AcquireHost("d1.local") + h.AddPath(b, "/") + h.RootRedirect = "/app" + + b = c.config.AcquireBackend("d2", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS21} + b.SSLRedirect = true + h = c.config.AcquireHost("d2.local") + h.AddPath(b, "/app1") + h.AddPath(b, "/app2") + h.RootRedirect = "/app1" + + c.instance.Update() + + c.checkConfig(` +<<global>> +<<defaults>> +backend d1_app_8080 + mode http + server s1 172.17.0.11:8080 weight 100 +backend d2_app_8080 + mode http + server s21 172.17.0.121:8080 weight 100 +<<backends-default>> +frontend _front_http + mode http + bind :80 + http-request set-var(req.base) base,regsub(:[0-9]+/,/) + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + http-request set-var(req.host) hdr(host),lower,regsub(:[0-9]+/,/) + http-request set-var(req.rootredir) var(req.host),map(/etc/haproxy/maps/_global_http_root_redir.map,_nomatch) + http-request redirect location %[var(req.rootredir)] if { path / } !{ var(req.rootredir) _nomatch } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + default_backend _error404 +frontend _front001 + mode http + bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem + http-request set-var(req.hostbackend) base,lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + http-request set-var(req.host) hdr(host),lower,regsub(:[0-9]+/,/) + http-request set-var(req.rootredir) var(req.host),map(/etc/haproxy/maps/_front001_root_redir.map,_nomatch) + http-request redirect location %[var(req.rootredir)] if { path / } !{ var(req.rootredir) _nomatch } + <<tls-del-headers>> + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + default_backend _error404 +`) + + c.checkMap("_global_http_front.map", ` +d1.local/ d1_app_8080 +`) + c.checkMap("_global_https_redir.map", ` +d1.local/ no +d2.local/app2 yes +d2.local/app1 yes +`) + c.checkMap("_global_http_root_redir.map", ` +d1.local /app +d2.local /app1 +`) + c.checkMap("_front001_host.map", ` +d1.local/ d1_app_8080 +d2.local/app2 d2_app_8080 +d2.local/app1 d2_app_8080 +`) + c.checkMap("_front001_root_redir.map", ` +d1.local /app +d2.local /app1 +`) + + c.logger.CompareLogging(defaultLogging) +} + +func TestInstanceAlias(t *testing.T) { + c := setup(t) + defer c.teardown() + + var h *hatypes.Host + var b *hatypes.Backend + + b = c.config.AcquireBackend("d1", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS1} + h = c.config.AcquireHost("d1.local") + h.AddPath(b, "/") + h.Alias.AliasName = "*.d1.local" + + b = c.config.AcquireBackend("d2", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS21} + h = c.config.AcquireHost("d2.local") + h.AddPath(b, "/") + h.Alias.AliasName = "sub.d2.local" + h.Alias.AliasRegex = "^[a-z]+\\.d2\\.local$" + + b = c.config.AcquireBackend("d3", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS31} + h = c.config.AcquireHost("d3.local") + h.AddPath(b, "/") + h.Alias.AliasRegex = ".*d3\\.local$" + + c.instance.Update() + c.checkConfig(` +<<global>> +<<defaults>> +backend d1_app_8080 + mode http + server s1 172.17.0.11:8080 weight 100 +backend d2_app_8080 + mode http + server s21 172.17.0.121:8080 weight 100 +backend d3_app_8080 + mode http + server s31 172.17.0.131:8080 weight 100 +<<backends-default>> +frontend _front_http + mode http + bind :80 + http-request set-var(req.base) base,regsub(:[0-9]+/,/) + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + default_backend _error404 +frontend _front001 + mode http + bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem + http-request set-var(req.base) base,lower,regsub(:[0-9]+/,/) + http-request set-var(req.hostbackend) var(req.base),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + http-request set-var(req.hostbackend) var(req.base),map_reg(/etc/haproxy/maps/_front001_host_regex.map,_nomatch) if { var(req.hostbackend) _nomatch } + <<tls-del-headers>> + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + default_backend _error404 +`) + + c.checkMap("_global_https_redir.map", ` +d1.local/ no +d2.local/ no +d3.local/ no +`) + c.checkMap("_global_http_front.map", ` +d1.local/ d1_app_8080 +d2.local/ d2_app_8080 +d3.local/ d3_app_8080 +`) + c.checkMap("_front001_host.map", ` +d1.local/ d1_app_8080 +d2.local/ d2_app_8080 +sub.d2.local/ d2_app_8080 +d3.local/ d3_app_8080 +`) + c.checkMap("_front001_host_regex.map", ` +^[^.]+\.d1\.local/ d1_app_8080 +^[a-z]+\.d2\.local$/ d2_app_8080 +.*d3\.local$/ d3_app_8080 +`) + + c.logger.CompareLogging(defaultLogging) + +} + +func TestUserlist(t *testing.T) { + type list struct { + name string + users []hatypes.User + } + testCase := []struct { + lists []list + listname string + realm string + config string + }{ + { + lists: []list{ + { + name: "default_usr", + users: []hatypes.User{ + {Name: "usr1", Passwd: "clear1", Encrypted: false}, + }, + }, + }, + listname: "default_usr", + config: ` +userlist default_usr + user usr1 insecure-password clear1`, + }, + { + lists: []list{ + { + name: "default_auth", + users: []hatypes.User{ + {Name: "usr1", Passwd: "clear1", Encrypted: false}, + {Name: "usr2", Passwd: "xxxx", Encrypted: true}, + }, + }, + }, + listname: "default_auth", + realm: "usrlist", + config: ` +userlist default_auth + user usr1 insecure-password clear1 + user usr2 password xxxx`, + }, + { + lists: []list{ + { + name: "default_auth1", + users: []hatypes.User{ + {Name: "usr1", Passwd: "clear1", Encrypted: false}, + }, + }, + { + name: "default_auth2", + users: []hatypes.User{ + {Name: "usr2", Passwd: "xxxx", Encrypted: true}, + }, + }, + }, + listname: "default_auth1", + realm: "multi list", + config: ` +userlist default_auth1 + user usr1 insecure-password clear1 +userlist default_auth2 + user usr2 password xxxx`, + }, + } + for _, test := range testCase { + c := setup(t) + + var h *hatypes.Host + var b *hatypes.Backend + + b = c.config.AcquireBackend("d1", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS1} + h = c.config.AcquireHost("d1.local") + h.AddPath(b, "/") + + for _, list := range test.lists { + c.config.AddUserlist(list.name, list.users) + } + b.Userlist.Name = test.listname + b.Userlist.Realm = test.realm + + var realm string + if test.realm != "" { + realm = fmt.Sprintf(` realm "%s"`, test.realm) + } + + c.instance.Update() + c.checkConfig(` +<<global>> +<<defaults>>` + test.config + ` +backend d1_app_8080 + mode http + http-request auth` + realm + ` if !{ http_auth(` + test.listname + `) } + server s1 172.17.0.11:8080 weight 100 +<<backends-default>> +<<frontends-default>> +`) + c.logger.CompareLogging(defaultLogging) + c.teardown() + } +} + +func TestModSecurity(t *testing.T) { + testCases := []struct { + waf string + endpoints []string + backendExp string + modsecExp string + }{ + { + waf: "modsecurity", + endpoints: []string{}, + backendExp: ``, + modsecExp: ``, + }, + { + waf: "", + endpoints: []string{"10.0.0.101:12345"}, + backendExp: ``, + modsecExp: ` + server modsec-spoa0 10.0.0.101:12345`, + }, + { + waf: "modsecurity", + endpoints: []string{"10.0.0.101:12345"}, + backendExp: ` + filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf + http-request deny if { var(txn.modsec.code) -m int gt 0 }`, + modsecExp: ` + server modsec-spoa0 10.0.0.101:12345`, + }, + { + waf: "modsecurity", + endpoints: []string{"10.0.0.101:12345", "10.0.0.102:12345"}, + backendExp: ` + filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf + http-request deny if { var(txn.modsec.code) -m int gt 0 }`, + modsecExp: ` + server modsec-spoa0 10.0.0.101:12345 + server modsec-spoa1 10.0.0.102:12345`, + }, + } + for _, test := range testCases { + c := setup(t) + + var h *hatypes.Host + var b *hatypes.Backend + + b = c.config.AcquireBackend("d1", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS1} + b.WAF = test.waf + h = c.config.AcquireHost("d1.local") + h.AddPath(b, "/") + c.config.Global().ModSecurity.Endpoints = test.endpoints + + c.instance.Update() + + var modsec string + if test.modsecExp != "" { + modsec = ` +backend spoe-modsecurity + mode tcp + timeout connect 5s + timeout server 5s` + test.modsecExp + } + c.checkConfig(` +<<global>> +<<defaults>> +backend d1_app_8080 + mode http` + test.backendExp + ` + server s1 172.17.0.11:8080 weight 100 +<<backends-default>> +<<frontends-default>>` + modsec) + + c.logger.CompareLogging(defaultLogging) + c.teardown() + } +} + +func TestInstanceWildcardHostname(t *testing.T) { + c := setup(t) + defer c.teardown() + + var h *hatypes.Host + var b *hatypes.Backend + + b = c.config.AcquireBackend("d1", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS1} + b.SSLRedirect = true + h = c.config.AcquireHost("d1.local") + h.AddPath(b, "/") + h = c.config.AcquireHost("*.app.d1.local") + h.AddPath(b, "/") + h = c.config.AcquireHost("*.sub.d1.local") + h.AddPath(b, "/") + h.TLS.CAFilename = "/var/haproxy/ssl/ca/d1.local.pem" + h.TLS.CAHash = "1" + h.TLS.CAVerifyOptional = true + h.TLS.CAErrorPage = "http://sub.d1.local/error.html" + + b = c.config.AcquireBackend("d2", "app", "8080") + b.Endpoints = []*hatypes.Endpoint{endpointS21} + b.SSLRedirect = false + h = c.config.AcquireHost("*.d2.local") + h.AddPath(b, "/") + h.RootRedirect = "/app" + h.Timeout.Client = "10s" + + c.instance.Update() + c.checkConfig(` +<<global>> +<<defaults>> +backend d1_app_8080 + mode http + http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)] + http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn] + http-request set-header X-SSL-Client-SHA1 %{+Q}[ssl_c_sha1,hex] + server s1 172.17.0.11:8080 weight 100 +backend d2_app_8080 + mode http + server s21 172.17.0.121:8080 weight 100 +<<backends-default>> +listen _front__tls + mode tcp + bind :443 + tcp-request inspect-delay 5s + tcp-request content accept if { req.ssl_hello_type 1 } + ## _front001/_socket001 + use-server _server_socket001 if { req.ssl_sni -i -f /etc/haproxy/maps/_socket001.list } + server _server_socket001 unix@/var/run/_socket001.sock send-proxy-v2 weight 0 + ## _front001/_socket002 + use-server _server_socket002 if { req.ssl_sni -i -f /etc/haproxy/maps/_socket002.list } + server _server_socket002 unix@/var/run/_socket002.sock send-proxy-v2 weight 0 + ## _front002/_socket003 + use-server _server_socket003 if { req.ssl_sni -i -f /etc/haproxy/maps/_socket003.list } + server _server_socket003 unix@/var/run/_socket003.sock send-proxy-v2 weight 0 + ## _front001/_socket001 wildcard + use-server _server_socket001_wildcard if { req.ssl_sni -i -m reg -f /etc/haproxy/maps/_socket001_regex.list } + server _server_socket001_wildcard unix@/var/run/_socket001.sock send-proxy-v2 weight 0 + ## _front001/_socket002 wildcard + use-server _server_socket002_wildcard if { req.ssl_sni -i -m reg -f /etc/haproxy/maps/_socket002_regex.list } + server _server_socket002_wildcard unix@/var/run/_socket002.sock send-proxy-v2 weight 0 + ## _front002/_socket003 wildcard + use-server _server_socket003_wildcard if { req.ssl_sni -i -m reg -f /etc/haproxy/maps/_socket003_regex.list } + server _server_socket003_wildcard unix@/var/run/_socket003.sock send-proxy-v2 weight 0 + # TODO default backend +frontend _front_http + mode http + bind :80 + http-request set-var(req.base) base,regsub(:[0-9]+/,/) + http-request set-var(req.redir) var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) + http-request redirect scheme https if { var(req.redir) yes } + http-request redirect scheme https if { var(req.redir) _nomatch } { var(req.base),map_reg(/etc/haproxy/maps/_global_https_redir_regex.map,_nomatch) yes } + http-request set-var(req.host) hdr(host),lower,regsub(:[0-9]+/,/) + http-request set-var(req.rootredir) var(req.host),map(/etc/haproxy/maps/_global_http_root_redir.map,_nomatch) + http-request set-var(req.rootredir) var(req.host),map_reg(/etc/haproxy/maps/_global_http_root_redir_regex.map,_nomatch) if { var(req.rootredir) _nomatch } + http-request redirect location %[var(req.rootredir)] if { path / } !{ var(req.rootredir) _nomatch } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) + http-request set-var(req.backend) var(req.base),map_reg(/etc/haproxy/maps/_global_http_front_regex.map,_nomatch) if { var(req.backend) _nomatch } + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + default_backend _error404 +frontend _front001 + mode http + bind unix@/var/run/_socket001.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem + bind unix@/var/run/_socket002.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem ca-file /var/haproxy/ssl/ca/d1.local.pem verify optional ca-ignore-err all crt-ignore-err all + http-request set-var(req.base) base,lower,regsub(:[0-9]+/,/) + http-request set-var(req.hostbackend) var(req.base),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + http-request set-var(req.hostbackend) var(req.base),map_reg(/etc/haproxy/maps/_front001_host_regex.map,_nomatch) if { var(req.hostbackend) _nomatch } + <<tls-del-headers>> + http-request set-header x-ha-base %[ssl_fc_sni]%[path] + http-request set-var(req.snibase) hdr(x-ha-base),lower,regsub(:[0-9]+/,/) + http-request set-var(req.snibackend) var(req.snibase),map_beg(/etc/haproxy/maps/_front001_sni.map,_nomatch) + http-request set-var(req.snibackend) var(req.snibase),map_reg(/etc/haproxy/maps/_front001_sni_regex.map,_nomatch) if { var(req.snibackend) _nomatch } + acl tls-has-invalid-crt ssl_c_ca_err gt 0 + acl tls-has-invalid-crt ssl_c_err gt 0 + acl tls-check-crt ssl_fc_sni -i -f /etc/haproxy/maps/_front001_inv_crt.list + acl tls-check-crt ssl_fc_sni -i -m reg -f /etc/haproxy/maps/_front001_inv_crt_regex.list + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,lower,map(/etc/haproxy/maps/_front001_inv_crt_redir.map,_internal) if tls-has-invalid-crt tls-check-crt + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,lower,map_reg(/etc/haproxy/maps/_front001_inv_crt_redir_regex.map,_internal) if { var(req.tls_invalidcrt_redir) _internal } + http-request redirect location %[var(req.tls_invalidcrt_redir)] code 303 if { var(req.tls_invalidcrt_redir) -m found } !{ var(req.tls_invalidcrt_redir) _internal } + use_backend _error495 if { var(req.tls_invalidcrt_redir) _internal } + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + use_backend %[var(req.snibackend)] unless { var(req.snibackend) _nomatch } + default_backend _error404 +frontend _front002 + mode http + bind unix@/var/run/_socket003.sock accept-proxy ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem + timeout client 10s + http-request set-var(req.base) base,lower,regsub(:[0-9]+/,/) + http-request set-var(req.hostbackend) var(req.base),map_beg(/etc/haproxy/maps/_front002_host.map,_nomatch) + http-request set-var(req.hostbackend) var(req.base),map_reg(/etc/haproxy/maps/_front002_host_regex.map,_nomatch) if { var(req.hostbackend) _nomatch } + http-request set-var(req.host) hdr(host),lower,regsub(:[0-9]+/,/) + http-request set-var(req.rootredir) var(req.host),map(/etc/haproxy/maps/_front002_root_redir.map,_nomatch) + http-request set-var(req.rootredir) var(req.host),map_reg(/etc/haproxy/maps/_front002_root_redir_regex.map,_nomatch) if { var(req.rootredir) _nomatch } + http-request redirect location %[var(req.rootredir)] if { path / } !{ var(req.rootredir) _nomatch } + <<tls-del-headers>> + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + default_backend _error404 +`) + + c.checkMap("_socket001.list", ` +d1.local +`) + c.checkMap("_socket002.list", ` +`) + c.checkMap("_socket003.list", ` +`) + c.checkMap("_socket001_regex.list", ` +^[^.]+\.app\.d1\.local$ +`) + c.checkMap("_socket002_regex.list", ` +^[^.]+\.sub\.d1\.local$ +`) + c.checkMap("_socket003_regex.list", ` +^[^.]+\.d2\.local$ +`) + c.checkMap("_global_http_front.map", ` +`) + c.checkMap("_global_http_front_regex.map", ` +^[^.]+\.d2\.local/ d2_app_8080 +`) + c.checkMap("_global_https_redir.map", ` +d1.local/ yes +`) + c.checkMap("_global_https_redir_regex.map", ` +^[^.]+\.app\.d1\.local/ yes +^[^.]+\.sub\.d1\.local/ yes +^[^.]+\.d2\.local/ no +`) + c.checkMap("_global_http_root_redir.map", ` +`) + c.checkMap("_global_http_root_redir_regex.map", ` +^[^.]+\.d2\.local$ /app +`) + c.checkMap("_front001_host.map", ` +d1.local/ d1_app_8080 +`) + c.checkMap("_front001_host_regex.map", ` +^[^.]+\.app\.d1\.local/ d1_app_8080 +`) + c.checkMap("_front001_sni.map", ` +`) + c.checkMap("_front001_sni_regex.map", ` +^[^.]+\.sub\.d1\.local/ d1_app_8080 +`) + c.checkMap("_front001_inv_crt.list", ` +`) + c.checkMap("_front001_inv_crt_regex.list", ` +^[^.]+\.sub\.d1\.local$ +`) + c.checkMap("_front001_inv_crt_redir.map", ` +`) + c.checkMap("_front001_inv_crt_redir_regex.map", ` +^[^.]+\.sub\.d1\.local$ http://sub.d1.local/error.html +`) + c.checkMap("_front002_host.map", ` +`) + c.checkMap("_front002_host_regex.map", ` +^[^.]+\.d2\.local/ d2_app_8080 +`) + c.checkMap("_front002_root_redir.map", ` +`) + c.checkMap("_front002_root_redir_regex.map", ` +^[^.]+\.d2\.local$ /app +`) + + c.logger.CompareLogging(defaultLogging) +} + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * BUILDERS @@ -717,7 +1520,7 @@ func setup(t *testing.T) *testConfig { }) instance.curConfig = config config.ConfigDefaultX509Cert("/var/haproxy/ssl/certs/default.pem") - return &testConfig{ + c := &testConfig{ t: t, logger: logger, bindUtils: bindUtils, @@ -726,6 +1529,8 @@ func setup(t *testing.T) *testConfig { tempdir: tempdir, configfile: configfile, } + c.configGlobal() + return c } func (c *testConfig) teardown() { @@ -737,9 +1542,11 @@ func (c *testConfig) teardown() { func (c *testConfig) configGlobal() { global := c.config.Global() + global.Cookie.Key = "Ingress" global.MaxConn = 2000 global.SSL.Ciphers = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256" global.SSL.DHParam.Filename = "/var/haproxy/tls/dhparam.pem" + global.SSL.HeadersPrefix = "X-SSL" global.SSL.Options = "no-sslv3" global.StatsSocket = "/var/run/haproxy.sock" global.Timeout.Client = "50s" @@ -754,45 +1561,6 @@ func (c *testConfig) configGlobal() { global.Timeout.Tunnel = "1h" } -var globalConfig = ` -global - daemon - quiet - stats socket /var/run/haproxy.sock level admin expose-fd listeners - maxconn 2000 - hard-stop-after 15m - lua-load /usr/local/etc/haproxy/lua/send-response.lua - lua-load /usr/local/etc/haproxy/lua/auth-request.lua - ssl-dh-param-file /var/haproxy/tls/dhparam.pem - ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256 - ssl-default-bind-options no-sslv3 -defaults - log global - maxconn 2000 - option redispatch - option dontlognull - option http-server-close - option http-keep-alive - timeout client 50s - timeout client-fin 50s - timeout connect 5s - timeout http-keep-alive 1m - timeout http-request 5s - timeout queue 5s - timeout server 50s - timeout server-fin 50s - timeout tunnel 1h` - -var errorPages = ` -backend _error495 - mode http - errorfile 400 /usr/local/etc/haproxy/errors/495.http - http-request deny deny_status 400 -backend _error496 - mode http - errorfile 400 /usr/local/etc/haproxy/errors/496.http - http-request deny deny_status 400` - var endpointS0 = &hatypes.Endpoint{ Name: "s0", IP: "172.17.0.99", @@ -858,12 +1626,82 @@ func _yamlMarshal(in interface{}) string { return string(out) } -func (c *testConfig) checkConfig(backend, frontend string) { - c.checkConfigFull(globalConfig + backend + errorPages + frontend) -} - -func (c *testConfig) checkConfigFull(expected string) { +func (c *testConfig) checkConfig(expected string) { actual := strings.Replace(c.readConfig(c.configfile), c.tempdir, "/etc/haproxy/maps", -1) + replace := map[string]string{ + "<<global>>": `global + daemon + stats socket /var/run/haproxy.sock level admin expose-fd listeners + maxconn 2000 + hard-stop-after 15m + lua-load /usr/local/etc/haproxy/lua/send-response.lua + lua-load /usr/local/etc/haproxy/lua/auth-request.lua + ssl-dh-param-file /var/haproxy/tls/dhparam.pem + ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256 + ssl-default-bind-options no-sslv3`, + "<<defaults>>": `defaults + log global + maxconn 2000 + option redispatch + option dontlognull + option http-server-close + option http-keep-alive + timeout client 50s + timeout client-fin 50s + timeout connect 5s + timeout http-keep-alive 1m + timeout http-request 5s + timeout queue 5s + timeout server 50s + timeout server-fin 50s + timeout tunnel 1h`, + "<<backend-errors>>": `backend _error495 + mode http + errorfile 400 /usr/local/etc/haproxy/errors/495.http + http-request deny deny_status 400 +backend _error496 + mode http + errorfile 400 /usr/local/etc/haproxy/errors/496.http + http-request deny deny_status 400`, + "<<backends-default>>": `backend _error404 + mode http + errorfile 400 /usr/local/etc/haproxy/errors/404.http + http-request deny deny_status 400 +<<backend-errors>>`, + " <<tls-del-headers>>": ` http-request del-header X-SSL-Client-CN + http-request del-header X-SSL-Client-DN + http-request del-header X-SSL-Client-SHA1 + http-request del-header X-SSL-Client-Cert`, + "<<frontends-default>>": `frontend _front_http + mode http + bind :80 + http-request set-var(req.base) base,regsub(:[0-9]+/,/) + http-request redirect scheme https if { var(req.base),map_beg(/etc/haproxy/maps/_global_https_redir.map,_nomatch) yes } + <<tls-del-headers>> + http-request set-var(req.backend) var(req.base),map_beg(/etc/haproxy/maps/_global_http_front.map,_nomatch) + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + default_backend _error404 +frontend _front001 + mode http + bind :443 ssl alpn h2,http/1.1 crt /var/haproxy/ssl/certs/default.pem + http-request set-var(req.hostbackend) base,lower,regsub(:[0-9]+/,/),map_beg(/etc/haproxy/maps/_front001_host.map,_nomatch) + <<tls-del-headers>> + use_backend %[var(req.hostbackend)] unless { var(req.hostbackend) _nomatch } + default_backend _error404`, + } + for { + changed := false + for old, new := range replace { + after := strings.Replace(expected, old, new, -1) + if after != expected { + changed = true + } + expected = after + } + if !changed { + break + } + } c.compareText("haproxy.cfg", actual, expected) } diff --git a/pkg/haproxy/types/backend.go b/pkg/haproxy/types/backend.go index 6c43850a5..47efa9bc0 100644 --- a/pkg/haproxy/types/backend.go +++ b/pkg/haproxy/types/backend.go @@ -37,12 +37,19 @@ func (b *Backend) NewEndpoint(ip string, port int, targetRef string) *Endpoint { return endpoint } -// HreqValidateUserlist ... -func (b *Backend) HreqValidateUserlist(userlist *Userlist) { - // TODO implement - b.HTTPRequests = append(b.HTTPRequests, &HTTPRequest{}) -} - -func (h *HTTPRequest) String() string { - return fmt.Sprintf("%+v", *h) +// AddPath ... +func (b *Backend) AddPath(path string) { + for _, p := range b.Paths { + if p == path { + // add only unique paths + return + } + } + // host's paths that references this backend + // used on RewriteURL config + b.Paths = append(b.Paths, path) + // reverse order in order to avoid overlap of sub-paths + sort.Slice(b.Paths, func(i, j int) bool { + return b.Paths[i] > b.Paths[j] + }) } diff --git a/pkg/haproxy/types/backend_test.go b/pkg/haproxy/types/backend_test.go new file mode 100644 index 000000000..d4bb3063a --- /dev/null +++ b/pkg/haproxy/types/backend_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2019 The HAProxy Ingress Controller Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "reflect" + "testing" +) + +func TestAddPath(t *testing.T) { + testCases := []struct { + input []string + expected []string + }{ + { + input: []string{"/"}, + expected: []string{"/"}, + }, + { + input: []string{"/app", "/app"}, + expected: []string{"/app"}, + }, + { + input: []string{"/app", "/root"}, + expected: []string{"/root", "/app"}, + }, + { + input: []string{"/app", "/root", "/root"}, + expected: []string{"/root", "/app"}, + }, + { + input: []string{"/app", "/root", "/app"}, + expected: []string{"/root", "/app"}, + }, + { + input: []string{"/", "/app", "/root"}, + expected: []string{"/root", "/app", "/"}, + }, + } + for _, test := range testCases { + b := &Backend{} + for _, p := range test.input { + b.AddPath(p) + } + if !reflect.DeepEqual(b.Paths, test.expected) { + t.Errorf("backend.Paths differs - actual: %v - expected: %v", b.Paths, test.expected) + } + } +} diff --git a/pkg/haproxy/types/frontend.go b/pkg/haproxy/types/frontend.go index af6460988..891b44939 100644 --- a/pkg/haproxy/types/frontend.go +++ b/pkg/haproxy/types/frontend.go @@ -20,8 +20,87 @@ import ( "fmt" "reflect" "sort" + "strings" ) +// AppendHostname ... +func (hm *HostsMap) AppendHostname(base, value string) { + // always use case insensitive match + base = strings.ToLower(base) + isHostnameOnly := !strings.Contains(base, "/") + if strings.HasPrefix(base, "*.") { + // *.example.local + key := "^" + strings.Replace(base, ".", "\\.", -1) + key = strings.Replace(key, "*", "[^.]+", 1) + if isHostnameOnly { + // match eol if only the hostname is provided + // if has /path, need to match the begining of the string, a la map_beg() converter + key = key + "$" + } + hm.Regex = append(hm.Regex, &HostsMapEntry{ + Key: key, + Value: value, + }) + } else { + // sub.example.local + hm.Match = append(hm.Match, &HostsMapEntry{ + Key: base, + Value: value, + }) + // Hostnames are already in alphabetical order but Alias are not + // Sort only hostname maps which uses ebtree search via map converter + if isHostnameOnly { + sort.Slice(hm.Match, func(i, j int) bool { + return hm.Match[i].Key < hm.Match[j].Key + }) + } + } +} + +// AppendAliasName ... +func (hm *HostsMap) AppendAliasName(base, value string) { + if base != "" { + hm.AppendHostname(base, value) + } +} + +// AppendAliasRegex ... +func (hm *HostsMap) AppendAliasRegex(base, value string) { + if base != "" { + hm.Regex = append(hm.Regex, &HostsMapEntry{ + Key: base, + Value: value, + }) + } +} + +// HasRegex ... +func (hm *HostsMap) HasRegex() bool { + return len(hm.Regex) > 0 +} + +// HasHost ... +func (hm *HostsMap) HasHost() bool { + return len(hm.Regex) > 0 || len(hm.Match) > 0 +} + +// CreateMaps ... +func CreateMaps() *HostsMaps { + return &HostsMaps{} +} + +// AddMap ... +func (hm *HostsMaps) AddMap(filename string) *HostsMap { + matchFile := filename + regexFile := strings.Replace(filename, ".", "_regex.", 1) + hmap := &HostsMap{ + MatchFile: matchFile, + RegexFile: regexFile, + } + hm.Items = append(hm.Items, hmap) + return hmap +} + // HasTCPProxy ... func (fg *FrontendGroup) HasTCPProxy() bool { // short-circuit saves: @@ -63,7 +142,7 @@ func (f *Frontend) HasNoCrtErrorPage() bool { // HasTLSMandatory ... func (f *Frontend) HasTLSMandatory() bool { for _, host := range f.Hosts { - if !host.TLS.CAVerifyOptional { + if host.HasTLSAuth() && !host.TLS.CAVerifyOptional { return true } } @@ -115,12 +194,8 @@ func BuildRawFrontends(hosts []*Host) (frontends []*Frontend, sslpassthrough []* // naming frontends var i int for _, frontend := range frontends { - if len(frontend.Hosts) == 1 { - frontend.Name = "https-front_" + frontend.Hosts[0].Hostname - } else { - i++ - frontend.Name = fmt.Sprintf("_front_%03d", i) - } + i++ + frontend.Name = fmt.Sprintf("_front%03d", i) } // sorting frontends sort.Slice(frontends, func(i, j int) bool { diff --git a/pkg/haproxy/types/frontend_test.go b/pkg/haproxy/types/frontend_test.go index cd88d713f..5e957a47a 100644 --- a/pkg/haproxy/types/frontend_test.go +++ b/pkg/haproxy/types/frontend_test.go @@ -23,6 +23,49 @@ import ( yaml "gopkg.in/yaml.v2" ) +func TestAppendHostname(t *testing.T) { + testCases := []struct { + hostname string + expectedMatch string + expectedRegex string + }{ + // 0 + {hostname: "Example.Local", expectedMatch: "example.local"}, + // 1 + {hostname: "example.local/", expectedMatch: "example.local/"}, + // 2 + {hostname: "*.Example.Local", expectedRegex: "^[^.]+\\.example\\.local$"}, + // 3 + {hostname: "*.example.local/", expectedRegex: "^[^.]+\\.example\\.local/"}, + // 4 + {hostname: "*.example.local/path", expectedRegex: "^[^.]+\\.example\\.local/path"}, + } + for i, test := range testCases { + hm := &HostsMap{} + hm.AppendHostname(test.hostname, "backend") + if test.expectedMatch != "" { + if len(hm.Match) != 1 || len(hm.Regex) != 0 { + t.Errorf("item %d, expected len(match)==1 and len(regex)==0, but was '%d' and '%d'", i, len(hm.Match), len(hm.Regex)) + continue + } + if hm.Match[0].Key != test.expectedMatch { + t.Errorf("item %d, expected key '%s', but was '%s'", i, test.hostname, hm.Match[0].Key) + continue + } + } else { + //regex + if len(hm.Match) != 0 || len(hm.Regex) != 1 { + t.Errorf("item %d, expected len(match)==0 and len(regex)==1, but was '%d' and '%d'", i, len(hm.Match), len(hm.Regex)) + continue + } + if hm.Regex[0].Key != test.expectedRegex { + t.Errorf("item %d, expected key '%s', but was '%s'", i, test.expectedRegex, hm.Regex[0].Key) + continue + } + } + } +} + func TestBuildFrontendEmpty(t *testing.T) { frontends, _ := BuildRawFrontends([]*Host{}) if len(frontends) > 0 { @@ -50,7 +93,7 @@ func TestBuildFrontend(t *testing.T) { hosts: []*Host{h10_1, h10_2}, expected: []*Frontend{ { - Name: "_front_001", + Name: "_front001", Timeout: timeout10, Hosts: []*Host{h10_1, h10_2}, Binds: []*BindConfig{ @@ -66,7 +109,7 @@ func TestBuildFrontend(t *testing.T) { hosts: []*Host{h10_1, h20_1, h10_2}, expected: []*Frontend{ { - Name: "_front_001", + Name: "_front001", Timeout: timeout10, Hosts: []*Host{h10_1, h10_2}, Binds: []*BindConfig{ @@ -76,7 +119,7 @@ func TestBuildFrontend(t *testing.T) { }, }, { - Name: "https-front_h3.local", + Name: "_front002", Timeout: timeout20, Hosts: []*Host{h20_1}, Binds: []*BindConfig{ @@ -92,7 +135,7 @@ func TestBuildFrontend(t *testing.T) { hosts: []*Host{h10CA1_1, h10CA2_1, h10CA2_2}, expected: []*Frontend{ { - Name: "_front_001", + Name: "_front001", Timeout: timeout10, Hosts: []*Host{h10CA1_1, h10CA2_1, h10CA2_2}, Binds: []*BindConfig{ @@ -113,7 +156,7 @@ func TestBuildFrontend(t *testing.T) { hosts: []*Host{h10_1, h10_2, h10CA2_1, h10CA2_2}, expected: []*Frontend{ { - Name: "_front_001", + Name: "_front001", Timeout: timeout10, Hosts: []*Host{h10_1, h10_2, h10CA2_1, h10CA2_2}, Binds: []*BindConfig{ diff --git a/pkg/haproxy/types/host.go b/pkg/haproxy/types/host.go index 6f984f253..5cf7b2ac6 100644 --- a/pkg/haproxy/types/host.go +++ b/pkg/haproxy/types/host.go @@ -38,6 +38,8 @@ func (h *Host) AddPath(backend *Backend, path string) { Backend: backend, BackendID: backend.ID, }) + backend.AddPath(path) + // reverse order in order to avoid overlap of sub-paths sort.Slice(h.Paths, func(i, j int) bool { return h.Paths[i].Path > h.Paths[j].Path }) diff --git a/pkg/haproxy/types/types.go b/pkg/haproxy/types/types.go index 076341375..7690ed667 100644 --- a/pkg/haproxy/types/types.go +++ b/pkg/haproxy/types/types.go @@ -18,18 +18,19 @@ package types // Global ... type Global struct { - Procs ProcsConfig - Syslog SyslogConfig - MaxConn int - Timeout TimeoutConfig - SSL SSLConfig - ModSecurity ModSecurityConfig - DrainSupport bool - DrainSupportRedispatch bool - LoadServerState bool - StatsSocket string - CustomConfig []string - CustomDefaults []string + Procs ProcsConfig + Syslog SyslogConfig + MaxConn int + Timeout TimeoutConfig + SSL SSLConfig + ModSecurity ModSecurityConfig + Cookie CookieConfig + DrainSupport DrainConfig + ForwardFor string + LoadServerState bool + StatsSocket string + CustomConfig []string + CustomDefaults []string } // ProcsConfig ... @@ -45,9 +46,12 @@ type ProcsConfig struct { // SyslogConfig ... type SyslogConfig struct { - Endpoint string - Format string - Tag string + Endpoint string + Format string + HTTPLogFormat string + HTTPSLogFormat string + Tag string + TCPLogFormat string } // TimeoutConfig ... @@ -59,11 +63,12 @@ type TimeoutConfig struct { // SSLConfig ... type SSLConfig struct { - DHParam DHParamConfig - Ciphers string - Options string - Engine string - ModeAsync bool + DHParam DHParamConfig + Ciphers string + Options string + Engine string + ModeAsync bool + HeadersPrefix string } // DHParamConfig ... @@ -78,6 +83,17 @@ type ModSecurityConfig struct { Timeout ModSecurityTimeoutConfig } +// CookieConfig ... +type CookieConfig struct { + Key string +} + +// DrainConfig ... +type DrainConfig struct { + Drain bool + Redispatch bool +} + // ModSecurityTimeoutConfig ... type ModSecurityTimeoutConfig struct { Hello string @@ -85,15 +101,36 @@ type ModSecurityTimeoutConfig struct { Processing string } +// HostsMapEntry ... +type HostsMapEntry struct { + Key string + Value string +} + +// HostsMap ... +type HostsMap struct { + Match []*HostsMapEntry + MatchFile string + Regex []*HostsMapEntry + RegexFile string +} + +// HostsMaps ... +type HostsMaps struct { + Items []*HostsMap +} + // FrontendGroup ... type FrontendGroup struct { - Frontends []*Frontend - HasHTTPHost bool - HasRedirectHTTPS bool + Frontends []*Frontend + // HasSSLPassthrough bool - HTTPFrontsMap string - RedirectMap string - SSLPassthroughMap string + // + Maps *HostsMaps + HTTPFrontsMap *HostsMap + HTTPRootRedirMap *HostsMap + HTTPSRedirMap *HostsMap + SSLPassthroughMap *HostsMap } // Frontend ... @@ -102,15 +139,17 @@ type Frontend struct { Binds []*BindConfig Hosts []*Host // - ConvertLowercase bool - HostBackendsMap string - SNIBackendsMap string - Timeout HostTimeoutConfig - TLSInvalidCrtErrorList string - TLSNoCrtErrorList string - TLSInvalidCrtErrorPagesMap string - TLSNoCrtErrorPagesMap string - VarNamespaceMap string + Timeout HostTimeoutConfig + // + Maps *HostsMaps + HostBackendsMap *HostsMap + RootRedirMap *HostsMap + SNIBackendsMap *HostsMap + TLSInvalidCrtErrorList *HostsMap + TLSInvalidCrtErrorPagesMap *HostsMap + TLSNoCrtErrorList *HostsMap + TLSNoCrtErrorPagesMap *HostsMap + VarNamespaceMap *HostsMap } // BindConfig ... @@ -119,9 +158,11 @@ type BindConfig struct { Socket string Hosts []*Host // - AcceptProxy bool - TLS BindTLSConfig - UseServerList string + AcceptProxy bool + TLS BindTLSConfig + // + Maps *HostsMaps + UseServerList *HostsMap } // BindTLSConfig ... @@ -177,7 +218,6 @@ type HostTimeoutConfig struct { // HostTLSConfig ... type HostTLSConfig struct { - AddCertHeader bool CAErrorPage string CAFilename string CAHash string @@ -191,23 +231,30 @@ type Backend struct { ID string Namespace string Name string - Port int + Port string Endpoints []*Endpoint // AgentCheck AgentCheck BalanceAlgorithm string Cookie Cookie + Cors Cors CustomConfig []string HealthCheck HealthCheck - HTTPRequests []*HTTPRequest + HSTS HSTS MaxConnServer int MaxQueueServer int ModeTCP bool + OAuth OAuthConfig + Paths []string ProxyBodySize string + RewriteURL string SendProxyProtocol string SSL SSLBackendConfig SSLRedirect bool Timeout BackendTimeoutConfig + Userlist UserlistConfig + WAF string + Whitelist []string } // Endpoint ... @@ -237,13 +284,23 @@ type HealthCheck struct { RiseCount string } +// OAuthConfig ... +type OAuthConfig struct { + Impl string + BackendName string + URIPrefix string + Headers map[string]string +} + // SSLBackendConfig ... type SSLBackendConfig struct { - IsSecure bool - CertFilename string - CertHash string - CAFilename string - CAHash string + HasTLSAuth bool + AddCertHeader bool + IsSecure bool + CertFilename string + CertHash string + CAFilename string + CAHash string } // BackendTimeoutConfig ... @@ -257,15 +314,36 @@ type BackendTimeoutConfig struct { Tunnel string } +type UserlistConfig struct { + Name string + Realm string +} + // Cookie ... type Cookie struct { Name string Strategy string - Key string + Dynamic bool +} + +// Cors ... +type Cors struct { + Enabled bool + // + AllowCredentials bool + AllowHeaders string + AllowMethods string + AllowOrigin string + ExposeHeaders string + MaxAge int } -// HTTPRequest ... -type HTTPRequest struct { +// HSTS ... +type HSTS struct { + Enabled bool + MaxAge int + Subdomains bool + Preload bool } // Userlist ... diff --git a/rootfs/etc/haproxy/modsecurity/spoe-modsecurity.tmpl b/rootfs/etc/haproxy/modsecurity/spoe-modsecurity.tmpl index 08a1f98e4..2f0fdd405 100644 --- a/rootfs/etc/haproxy/modsecurity/spoe-modsecurity.tmpl +++ b/rootfs/etc/haproxy/modsecurity/spoe-modsecurity.tmpl @@ -6,7 +6,7 @@ # # This file is automatically updated, do not edit # # # -{{- $modsec := .Global.ModSecurity -}} +{{- $modsec := .Global.ModSecurity }} [modsecurity] spoe-agent modsecurity-agent messages check-request @@ -17,4 +17,4 @@ spoe-agent modsecurity-agent use-backend spoe-modsecurity spoe-message check-request args unique-id method path query req.ver req.hdrs_bin req.body_size req.body - event on-frontend-http-request + event on-backend-http-request diff --git a/rootfs/etc/haproxy/template/haproxy.tmpl b/rootfs/etc/haproxy/template/haproxy.tmpl index baaa02c01..6b0a49c53 100644 --- a/rootfs/etc/haproxy/template/haproxy.tmpl +++ b/rootfs/etc/haproxy/template/haproxy.tmpl @@ -7,10 +7,10 @@ # # # {{- $cfg := . }} +{{- $fgroup := $cfg.FrontendGroup }} {{- $global := $cfg.Global }} global daemon - quiet {{- if gt $global.Procs.Nbproc 1 }} nbproc {{ $global.Procs.Nbproc }} {{- end }} @@ -63,11 +63,11 @@ defaults load-server-state-from-file global {{- end }} maxconn {{ $global.MaxConn }} -{{- if $global.DrainSupport }} +{{- if $global.DrainSupport.Drain }} option persist - {{- if $global.DrainSupportRedispatch }} +{{- if $global.DrainSupport.Redispatch }} option redispatch - {{- end }} +{{- end }} {{- else }} option redispatch {{- end }} @@ -167,16 +167,144 @@ backend {{ $backend.ID }} timeout tunnel {{ $timeout.Tunnel }} {{- end }} +{{- /*------------------------------------*/}} +{{- /* MODE TCP */}} +{{- /*------------------------------------*/}} +{{- if $backend.ModeTCP }} + +{{- /*------------------------------------*/}} +{{- if $backend.Whitelist }} + tcp-request content reject if !{ src{{ range $cidr := $backend.Whitelist }} {{ $cidr }}{{ end }} } +{{- end }} + +{{- /*------------------------------------*/}} +{{- /* MODE HTTP */}} +{{- /*------------------------------------*/}} +{{- else }}{{/*** if $backend.ModeTCP ***/}} + +{{- /*------------------------------------*/}} +{{- if $backend.Whitelist }} + http-request deny if !{ src{{ range $cidr := $backend.Whitelist }} {{ $cidr }}{{ end }} } +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $backend.Userlist.Name }} + http-request auth + {{- if $backend.Userlist.Realm }} realm "{{ $backend.Userlist.Realm }}"{{ end }} + {{- "" }} if !{ http_auth({{ $backend.Userlist.Name }}) } +{{- end }} + +{{- /*------------------------------------*/}} +{{- if and (eq $backend.WAF "modsecurity") $global.ModSecurity.Endpoints }} + filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf + http-request deny if { var(txn.modsec.code) -m int gt 0 } +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $backend.SSL.HasTLSAuth }} + http-request set-header {{ $global.SSL.HeadersPrefix }}-Client-CN %{+Q}[ssl_c_s_dn(cn)]{{ if not $backend.SSLRedirect }} if { ssl_fc }{{ end }} + http-request set-header {{ $global.SSL.HeadersPrefix }}-Client-DN %{+Q}[ssl_c_s_dn]{{ if not $backend.SSLRedirect }} if { ssl_fc }{{ end }} + http-request set-header {{ $global.SSL.HeadersPrefix }}-Client-SHA1 %{+Q}[ssl_c_sha1,hex]{{ if not $backend.SSLRedirect }} if { ssl_fc }{{ end }} +{{- if $backend.SSL.AddCertHeader }} + http-request set-header {{ $global.SSL.HeadersPrefix }}-Client-Cert %{+Q}[ssl_c_der,base64]{{ if not $backend.SSLRedirect }} if { ssl_fc }{{ end }} +{{- end }} +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $backend.Cors.Enabled }} + http-request use-service lua.send-response if METH_OPTIONS +{{- end }} + +{{- /*------------------------------------*/}} +{{- if eq $global.ForwardFor "add" }} + http-request set-header X-Original-Forwarded-For %[hdr(x-forwarded-for)] if { hdr(x-forwarded-for) -m found } + http-request del-header x-forwarded-for + option forwardfor +{{- else if eq $global.ForwardFor "ifmissing" }} + option forwardfor if-none +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $backend.OAuth.Impl }} +{{- $oauth := $backend.OAuth }} +{{- if eq $oauth.Impl "oauth2_proxy" }} + http-request set-header X-Real-IP %[src] + http-request lua.auth-request {{ $oauth.BackendName }} {{ $oauth.URIPrefix }}/auth + http-request redirect location {{ $oauth.URIPrefix }}/start?rd=%[path] if !{ path_beg {{ $oauth.URIPrefix }}/ } !{ var(txn.auth_response_successful) -m bool } +{{- range $header, $attr := $oauth.Headers }} + http-request set-header {{ $header }} %[var(txn.{{ $attr }})] if { var(txn.{{ $attr }}) -m found } +{{- end }} +{{- end }} +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $backend.Cookie.Name }} +{{- $cookie := $backend.Cookie }} + cookie {{ $cookie.Name }} {{ $cookie.Strategy }} + {{- if eq $cookie.Strategy "insert" }} indirect nocache httponly{{ end }} + {{- if $cookie.Dynamic }} dynamic{{ end }} +{{- if $cookie.Dynamic }} + dynamic-cookie-key "{{ $global.Cookie.Key }}" +{{- end }} +{{- end }} + {{- /*------------------------------------*/}} {{- range $snippet := $backend.CustomConfig }} {{ $snippet }} {{- end }} +{{- /*------------------------------------*/}} +{{- if $backend.RewriteURL }} +{{- range $path := $backend.Paths }} +{{- if eq $backend.RewriteURL "/" }} + reqrep ^([^:\ ]*)\ {{ $path }}/?(.*)$ \1\ {{ $backend.RewriteURL }}\2 +{{- else }} + reqrep ^([^:\ ]*)\ {{ $path }}(.*)$ \1\ {{ $backend.RewriteURL }}{{ if hasSuffix $path "/" }}/{{ end }}\2 +{{- end }} +{{- end }} +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $backend.HSTS.Enabled }} +{{- $hsts := $backend.HSTS }} + http-response set-header Strict-Transport-Security "max-age={{ $hsts.MaxAge }} + {{- if $hsts.Subdomains }}; includeSubDomains{{ end }} + {{- if $hsts.Preload }}; preload{{ end }}" + {{- if not $backend.SSLRedirect }} if { ssl_fc }{{ end }} +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $backend.Cors.Enabled }} +{{- $cors := $backend.Cors }} + http-response set-status 204 reason "No Content" if METH_OPTIONS + http-response set-header Content-Type "text/plain" if METH_OPTIONS + http-response set-header Content-Length "0" if METH_OPTIONS + http-response set-header Access-Control-Allow-Origin "{{ $cors.AllowOrigin }}" if METH_OPTIONS + http-response set-header Access-Control-Allow-Methods "{{ $cors.AllowMethods }}" if METH_OPTIONS + http-response set-header Access-Control-Allow-Headers "{{ $cors.AllowHeaders }}" if METH_OPTIONS +{{- if $cors.AllowCredentials }} + http-response set-header Access-Control-Allow-Credentials "{{ $cors.AllowCredentials }}" if METH_OPTIONS +{{- end }} + http-response set-header Access-Control-Max-Age "{{ $cors.MaxAge }}" if METH_OPTIONS + http-response set-header Access-Control-Allow-Origin "{{ $cors.AllowOrigin }}" + http-response set-header Access-Control-Allow-Methods "{{ $cors.AllowMethods }}" + http-response set-header Access-Control-Allow-Headers "{{ $cors.AllowHeaders }}" +{{- if $cors.AllowCredentials }} + http-response set-header Access-Control-Allow-Credentials "{{ $cors.AllowCredentials }}" +{{- end }} +{{- if $cors.ExposeHeaders }} + http-response set-header Access-Control-Expose-Headers "{{ $cors.ExposeHeaders }}" +{{- end }} +{{- end }} + +{{- end }}{{/*** if $backend.ModeTCP ***/}} + {{- /*------------------------------------*/}} {{- range $ep := $backend.Endpoints }} server {{ $ep.Name }} {{ $ep.IP }}:{{ $ep.Port }} {{- if $ep.Disabled }} disabled{{ end }} {{- "" }} weight {{ $ep.Weight }} + {{- if and (not $backend.ModeTCP) ($backend.Cookie.Name) (not $backend.Cookie.Dynamic) }} cookie {{ $ep.Name }}{{ end }} {{- template "backend" map $backend }} {{- end }} {{- end }} @@ -235,7 +363,6 @@ backend _error496 # # FRONTENDS # # # -{{- $fgroup := $cfg.BuildFrontendGroup }} {{- $frontends := $fgroup.Frontends }} {{- if $fgroup.HasTCPProxy }} @@ -246,21 +373,61 @@ backend _error496 listen _front__tls mode tcp bind :443 + +{{- /*------------------------------------*/}} +{{- if $global.Syslog.Endpoint }} +{{- if eq $global.Syslog.HTTPSLogFormat "default" }} + option tcplog +{{- else if $global.Syslog.HTTPSLogFormat }} + log-format {{ $global.Syslog.HTTPSLogFormat }} +{{- else }} + no log +{{- end }} +{{- end }} + +{{- /*------------------------------------*/}} tcp-request inspect-delay 5s tcp-request content accept if { req.ssl_hello_type 1 } + +{{- /*------------------------------------*/}} {{- if $fgroup.HasSSLPassthrough }} ## ssl-passthrough - tcp-request content set-var(req.backend) req.ssl_sni,lower,map({{ $fgroup.SSLPassthroughMap }},_nomatch) + tcp-request content set-var(req.backend) + {{- "" }} req.ssl_sni,lower,map({{ $fgroup.SSLPassthroughMap.MatchFile }},_nomatch) + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } +{{- end }} + +{{- /*------------------------------------*/}} +{{- range $frontend := $frontends }} +{{- range $bind := $frontend.Binds }} + ## {{ $frontend.Name }}/{{ $bind.Name }} + use-server _server{{ $bind.Name }} if { req.ssl_sni -i -f {{ $bind.UseServerList.MatchFile }} } + server _server{{ $bind.Name }} {{ $bind.Socket }} send-proxy-v2 weight 0 +{{- end }} +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $fgroup.SSLPassthroughMap.HasRegex }} + ## ssl-passthrough wildcard +{{- /*** TODO is map_reg() converter running on every request? ***/}} + tcp-request content set-var(req.backend) + {{- "" }} req.ssl_sni,lower,map_reg({{ $fgroup.SSLPassthroughMap.RegexFile }},_nomatch) + {{- "" }} if { var(req.backend) _nomatch } + +{{- /*------------------------------------*/}} use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } {{- end }} {{- range $frontend := $frontends }} - ## {{ $frontend.Name }} {{- range $bind := $frontend.Binds }} - use-server _server_{{ $bind.Name }} if - {{- "" }} { req.ssl_sni -i -f {{ $bind.UseServerList }} } - server _server_{{ $bind.Name }} {{ $bind.Socket }} send-proxy-v2 weight 0 +{{- if $bind.UseServerList.HasRegex }} + ## {{ $frontend.Name }}/{{ $bind.Name }} wildcard + use-server _server{{ $bind.Name }}_wildcard if { req.ssl_sni -i -m reg -f {{ $bind.UseServerList.RegexFile }} } + server _server{{ $bind.Name }}_wildcard {{ $bind.Socket }} send-proxy-v2 weight 0 +{{- end }} {{- end }} {{- end }} + +{{- /*------------------------------------*/}} # TODO default backend {{- end }} @@ -268,32 +435,59 @@ listen _front__tls # # # HTTP frontend # -frontend _front__http +frontend _front_http mode http bind :80 {{- /*------------------------------------*/}} -{{- $hasredirect := $fgroup.HasRedirectHTTPS }} -{{- $hashttp := $fgroup.HasHTTPHost }} -{{- if $hasredirect }} - http-request set-var(req.base) base,regsub(:[0-9]+/,/) -{{- if $hashttp }} - http-request set-var(req.backend) var(req.base),map_beg({{ $fgroup.HTTPFrontsMap }},_nomatch) +{{- if $global.Syslog.Endpoint }} +{{- if $global.Syslog.HTTPLogFormat }} + log-format {{ $global.Syslog.HTTPLogFormat }} +{{- else }} + option httplog +{{- end }} {{- end }} -{{- else if $hashttp }} - http-request set-var(req.backend) base,regsub(:[0-9]+/,/),map_beg({{ $fgroup.HTTPFrontsMap }},_nomatch) + +{{- /*------------------------------------*/}} + http-request set-var(req.base) base,regsub(:[0-9]+/,/) + +{{- /*------------------------------------*/}} +{{- if $fgroup.HTTPSRedirMap.HasRegex }} + http-request set-var(req.redir) + {{- "" }} var(req.base),map_beg({{ $fgroup.HTTPSRedirMap.MatchFile }},_nomatch) + http-request redirect scheme https if { var(req.redir) yes } + http-request redirect scheme https if { var(req.redir) _nomatch } + {{- "" }} { var(req.base),map_reg({{ $fgroup.HTTPSRedirMap.RegexFile }},_nomatch) yes } +{{- else }} + http-request redirect scheme https if { var(req.base),map_beg({{ $fgroup.HTTPSRedirMap.MatchFile }},_nomatch) yes } {{- end }} {{- /*------------------------------------*/}} -{{- if $hasredirect }} - redirect scheme https if - {{- "" }} { var(req.base),map_beg({{ $fgroup.RedirectMap }},_nomatch) yes } +{{- if $fgroup.HTTPRootRedirMap.HasHost }} + http-request set-var(req.host) hdr(host),lower,regsub(:[0-9]+/,/) + http-request set-var(req.rootredir) + {{- "" }} var(req.host),map({{ $fgroup.HTTPRootRedirMap.MatchFile }},_nomatch) +{{- if $fgroup.HTTPRootRedirMap.HasRegex }} + http-request set-var(req.rootredir) + {{- "" }} var(req.host),map_reg({{ $fgroup.HTTPRootRedirMap.RegexFile }},_nomatch) if { var(req.rootredir) _nomatch } +{{- end }} + http-request redirect location %[var(req.rootredir)] if { path / } !{ var(req.rootredir) _nomatch } {{- end }} {{- /*------------------------------------*/}} -{{- if $hashttp }} - use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } + http-request del-header {{ $global.SSL.HeadersPrefix }}-Client-CN + http-request del-header {{ $global.SSL.HeadersPrefix }}-Client-DN + http-request del-header {{ $global.SSL.HeadersPrefix }}-Client-SHA1 + http-request del-header {{ $global.SSL.HeadersPrefix }}-Client-Cert + +{{- /*------------------------------------*/}} + http-request set-var(req.backend) var(req.base),map_beg({{ $fgroup.HTTPFrontsMap.MatchFile }},_nomatch) +{{- if $fgroup.HTTPFrontsMap.HasRegex }} + http-request set-var(req.backend) + {{- "" }} var(req.base),map_reg({{ $fgroup.HTTPFrontsMap.RegexFile }},_nomatch) + {{- "" }} if { var(req.backend) _nomatch } {{- end }} + use_backend %[var(req.backend)] unless { var(req.backend) _nomatch } {{- template "defaultbackend" map $cfg }} @@ -327,41 +521,106 @@ frontend {{ $frontend.Name }} {{- if $frontend.Timeout.ClientFin }} timeout client-fin {{ $frontend.Timeout.ClientFin }} {{- end }} + +{{- /*------------------------------------*/}} +{{- if $global.Syslog.Endpoint }} +{{- if $global.Syslog.HTTPLogFormat }} + log-format {{ $global.Syslog.HTTPLogFormat }} +{{- else }} + option httplog +{{- end }} +{{- end }} + +{{- /*------------------------------------*/}} +{{- if or $frontend.HostBackendsMap.HasRegex $frontend.HasVarNamespace }} + http-request set-var(req.base) base,lower,regsub(:[0-9]+/,/) + http-request set-var(req.hostbackend) + {{- "" }} var(req.base),map_beg({{ $frontend.HostBackendsMap.MatchFile }},_nomatch) +{{- else }} + http-request set-var(req.hostbackend) base,lower,regsub(:[0-9]+/,/) + {{- "" }},map_beg({{ $frontend.HostBackendsMap.MatchFile }},_nomatch) +{{- end }} +{{- if $frontend.HostBackendsMap.HasRegex }} + http-request set-var(req.hostbackend) + {{- "" }} var(req.base),map_reg({{ $frontend.HostBackendsMap.RegexFile }},_nomatch) + {{- "" }} if { var(req.hostbackend) _nomatch } +{{- end }} + +{{- /*------------------------------------*/}} +{{- if $frontend.RootRedirMap.HasHost }} + http-request set-var(req.host) hdr(host),lower,regsub(:[0-9]+/,/) + http-request set-var(req.rootredir) + {{- "" }} var(req.host),map({{ $frontend.RootRedirMap.MatchFile }},_nomatch) +{{- if $frontend.RootRedirMap.HasRegex }} + http-request set-var(req.rootredir) + {{- "" }} var(req.host),map_reg({{ $frontend.RootRedirMap.RegexFile }},_nomatch) if { var(req.rootredir) _nomatch } +{{- end }} + http-request redirect location %[var(req.rootredir)] if { path / } !{ var(req.rootredir) _nomatch } +{{- end }} + +{{- /*------------------------------------*/}} {{- if $frontend.HasVarNamespace }} - http-request set-var(txn.namespace) base - {{- if $frontend.ConvertLowercase }},lower{{ end }} - {{- "" }},regsub(:[0-9]+/,/) - {{- "" }},map_beg({{ $frontend.VarNamespaceMap }},-) + http-request set-var(txn.namespace) + {{- "" }} var(req.base),map_beg({{ $frontend.VarNamespaceMap.MatchFile }},-) +{{- if $frontend.VarNamespaceMap.HasRegex }} + http-request set-var(txn.namespace) + {{- "" }} var(req.base),map_reg({{ $frontend.VarNamespaceMap.RegexFile }},-) + {{- "" }} if { var(txn.namespace) - } +{{- end }} {{- end }} {{- /*------------------------------------*/}} - http-request set-var(req.hostbackend) base - {{- if $frontend.ConvertLowercase }},lower{{ end }} - {{- "" }},regsub(:[0-9]+/,/) - {{- "" }},map_beg({{ $frontend.HostBackendsMap }},_nomatch) + http-request del-header {{ $global.SSL.HeadersPrefix }}-Client-CN + http-request del-header {{ $global.SSL.HeadersPrefix }}-Client-DN + http-request del-header {{ $global.SSL.HeadersPrefix }}-Client-SHA1 + http-request del-header {{ $global.SSL.HeadersPrefix }}-Client-Cert {{- /*------------------------------------*/}} {{- if $frontend.HasTLSAuth }} -{{- /* missing concat converter, fix after 1.9 */}} +{{- /*** TODO missing concat converter, fix after 1.9 ***/}} http-request set-header x-ha-base %[ssl_fc_sni]%[path] - http-request set-var(req.snibackend) hdr(x-ha-base) - {{- if $frontend.ConvertLowercase }},lower{{ end }} - {{- "" }},regsub(:[0-9]+/,/) - {{- "" }},map_beg({{ $frontend.SNIBackendsMap }},_nomatch) +{{- if $frontend.SNIBackendsMap.HasRegex }} + http-request set-var(req.snibase) hdr(x-ha-base),lower,regsub(:[0-9]+/,/) + http-request set-var(req.snibackend) var(req.snibase) + {{- "" }},map_beg({{ $frontend.SNIBackendsMap.MatchFile }},_nomatch) + http-request set-var(req.snibackend) var(req.snibase) + {{- "" }},map_reg({{ $frontend.SNIBackendsMap.RegexFile }},_nomatch) if { var(req.snibackend) _nomatch } +{{- else }} + http-request set-var(req.snibackend) hdr(x-ha-base),lower,regsub(:[0-9]+/,/) + {{- "" }},map_beg({{ $frontend.SNIBackendsMap.MatchFile }},_nomatch) +{{- end }} {{- $mandatory := $frontend.HasTLSMandatory }} - acl tls-invalid-crt ssl_c_ca_err gt 0 - acl tls-invalid-crt ssl_c_err gt 0 {{- if $mandatory }} acl tls-has-crt ssl_c_used - http-request set-var(req.tls_nocrt_redir) ssl_fc_sni - {{- if $frontend.ConvertLowercase }},lower{{ end }} - {{- "" }},map({{ $frontend.TLSNoCrtErrorPagesMap }},_internal) - {{- "" }} if !tls-has-crt -{{- end }} - http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni - {{- if $frontend.ConvertLowercase }},lower{{ end }} - {{- "" }},map({{ $frontend.TLSInvalidCrtErrorPagesMap }},_internal) - {{- "" }} if tls-invalid-crt + acl tls-need-crt ssl_fc_sni -i -f {{ $frontend.TLSNoCrtErrorList.MatchFile }} +{{- if $frontend.TLSNoCrtErrorList.HasRegex }} + acl tls-need-crt ssl_fc_sni -i -m reg -f {{ $frontend.TLSNoCrtErrorList.RegexFile }} +{{- end }} +{{- end }} + acl tls-has-invalid-crt ssl_c_ca_err gt 0 + acl tls-has-invalid-crt ssl_c_err gt 0 + acl tls-check-crt ssl_fc_sni -i -f {{ $frontend.TLSInvalidCrtErrorList.MatchFile }} +{{- if $frontend.TLSInvalidCrtErrorList.HasRegex }} + acl tls-check-crt ssl_fc_sni -i -m reg -f {{ $frontend.TLSInvalidCrtErrorList.RegexFile }} +{{- end }} +{{- if $mandatory }} + http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,lower + {{- "" }},map({{ $frontend.TLSNoCrtErrorPagesMap.MatchFile }},_internal) + {{- "" }} if !tls-has-crt tls-need-crt +{{- if $frontend.TLSNoCrtErrorPagesMap.HasRegex }} + http-request set-var(req.tls_nocrt_redir) ssl_fc_sni,lower + {{- "" }},map_reg({{ $frontend.TLSNoCrtErrorPagesMap.RegexFile }},_internal) + {{- "" }} if { var(req.tls_nocrt_redir) _internal } +{{- end }} +{{- end }} + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,lower + {{- "" }},map({{ $frontend.TLSInvalidCrtErrorPagesMap.MatchFile }},_internal) + {{- "" }} if tls-has-invalid-crt tls-check-crt +{{- if $frontend.TLSInvalidCrtErrorPagesMap.HasRegex }} + http-request set-var(req.tls_invalidcrt_redir) ssl_fc_sni,lower + {{- "" }},map_reg({{ $frontend.TLSInvalidCrtErrorPagesMap.RegexFile }},_internal) + {{- "" }} if { var(req.tls_invalidcrt_redir) _internal } +{{- end }} {{- if and $mandatory $frontend.HasNoCrtErrorPage }} http-request redirect location %[var(req.tls_nocrt_redir)] code 303 if {{- "" }} { var(req.tls_nocrt_redir) -m found } !{ var(req.tls_nocrt_redir) _internal } @@ -373,11 +632,9 @@ frontend {{ $frontend.Name }} {{- if $mandatory }} use_backend _error496 if {{- "" }} { var(req.tls_nocrt_redir) _internal } - {{- "" }} { ssl_fc_sni -i -f {{ $frontend.TLSNoCrtErrorList }} } {{- end }} use_backend _error495 if {{- "" }} { var(req.tls_invalidcrt_redir) _internal } - {{- "" }} { ssl_fc_sni -i -f {{ $frontend.TLSInvalidCrtErrorList }} } {{- end }} {{- /*------------------------------------*/}} @@ -402,4 +659,28 @@ frontend {{ $frontend.Name }} {{- else }} default_backend _error404 {{- end }} +{{- end }} + + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # +# # SUPPORT +# # +# + +{{- if $global.ModSecurity.Endpoints }} + + # # # # # # # # # # # # # # # # # # # +# # +# ModSecurity Agent +# +backend spoe-modsecurity + mode tcp + timeout connect 5s + timeout server 5s +{{- range $i, $endpoint := $global.ModSecurity.Endpoints }} + server modsec-spoa{{ $i }} {{ $endpoint }} +{{- end }} + {{- end }}