Compare commits

...

3 commits

Author SHA1 Message Date
896c733c81
Merge branch 'master' of github.com:traefik/traefik
All checks were successful
Build & Push / build-and-push (push) Successful in 9m12s
2024-09-15 23:47:59 +05:30
Ludovic Fernandez
ac1dad3d14
Add support for custom CA certificates by certificate resolver 2024-09-09 17:24:04 +02:00
Romain
e222d5cb2f
Add support for backend protocol selection in HTTP and GRPC routes 2024-09-09 10:08:08 +02:00
15 changed files with 705 additions and 223 deletions

View file

@ -709,6 +709,109 @@ certificatesResolvers:
# ... # ...
``` ```
### `caCertificates`
_Optional, Default=[]_
The `caCertificates` option specifies the paths to PEM encoded CA Certificates that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list.
```yaml tab="File (YAML)"
certificatesResolvers:
myresolver:
acme:
# ...
caCertificates:
- path/certificates1.pem
- path/certificates2.pem
# ...
```
```toml tab="File (TOML)"
[certificatesResolvers.myresolver.acme]
# ...
caCertificates = [ "path/certificates1.pem", "path/certificates2.pem" ]
# ...
```
```bash tab="CLI"
# ...
--certificatesresolvers.myresolver.acme.caCertificates="path/certificates1.pem,path/certificates2.pem"
# ...
```
??? note "LEGO Environment Variable"
It can be defined globally by using the environment variable `LEGO_CA_CERTIFICATES`.
This environment variable is neither a fallback nor an override of the configuration option.
### `caSystemCertPool`
_Optional, Default=false_
The `caSystemCertPool` option defines if the certificates pool must use a copy of the system cert pool.
```yaml tab="File (YAML)"
certificatesResolvers:
myresolver:
acme:
# ...
caSystemCertPool: true
# ...
```
```toml tab="File (TOML)"
[certificatesResolvers.myresolver.acme]
# ...
caSystemCertPool = true
# ...
```
```bash tab="CLI"
# ...
--certificatesresolvers.myresolver.acme.caSystemCertPool=true
# ...
```
??? note "LEGO Environment Variable"
It can be defined globally by using the environment variable `LEGO_CA_SYSTEM_CERT_POOL`.
`LEGO_CA_SYSTEM_CERT_POOL` is ignored if `LEGO_CA_CERTIFICATES` is not set or empty.
This environment variable is neither a fallback nor an override of the configuration option.
### `caServerName`
_Optional, Default=""_
The `caServerName` option specifies the CA server name that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list.
```yaml tab="File (YAML)"
certificatesResolvers:
myresolver:
acme:
# ...
caServerName: "my-server"
# ...
```
```toml tab="File (TOML)"
[certificatesResolvers.myresolver.acme]
# ...
caServerName = "my-server"
# ...
```
```bash tab="CLI"
# ...
--certificatesresolvers.myresolver.acme.caServerName="my-server"
# ...
```
??? note "LEGO Environment Variable"
It can be defined globally by using the environment variable `LEGO_CA_SERVER_NAME`.
`LEGO_CA_SERVER_NAME` is ignored if `LEGO_CA_CERTIFICATES` is not set or empty.
This environment variable is neither a fallback nor an override of the configuration option.
## Fallback ## Fallback
If Let's Encrypt is not reachable, the following certificates will apply: If Let's Encrypt is not reachable, the following certificates will apply:

View file

@ -57,9 +57,18 @@ Activate API directly on the entryPoint named traefik. (Default: ```false```)
`--certificatesresolvers.<name>`: `--certificatesresolvers.<name>`:
Certificates resolvers configuration. (Default: ```false```) Certificates resolvers configuration. (Default: ```false```)
`--certificatesresolvers.<name>.acme.cacertificates`:
Specify the paths to PEM encoded CA Certificates that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list.
`--certificatesresolvers.<name>.acme.caserver`: `--certificatesresolvers.<name>.acme.caserver`:
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```) CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
`--certificatesresolvers.<name>.acme.caservername`:
Specify the CA server name that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list.
`--certificatesresolvers.<name>.acme.casystemcertpool`:
Define if the certificates pool must use a copy of the system cert pool. (Default: ```false```)
`--certificatesresolvers.<name>.acme.certificatesduration`: `--certificatesresolvers.<name>.acme.certificatesduration`:
Certificates' duration in hours. (Default: ```2160```) Certificates' duration in hours. (Default: ```2160```)

View file

@ -57,9 +57,18 @@ Activate API directly on the entryPoint named traefik. (Default: ```false```)
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>`: `TRAEFIK_CERTIFICATESRESOLVERS_<NAME>`:
Certificates resolvers configuration. (Default: ```false```) Certificates resolvers configuration. (Default: ```false```)
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CACERTIFICATES`:
Specify the paths to PEM encoded CA Certificates that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list.
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CASERVER`: `TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CASERVER`:
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```) CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CASERVERNAME`:
Specify the CA server name that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list.
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CASYSTEMCERTPOOL`:
Define if the certificates pool must use a copy of the system cert pool. (Default: ```false```)
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CERTIFICATESDURATION`: `TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CERTIFICATESDURATION`:
Certificates' duration in hours. (Default: ```2160```) Certificates' duration in hours. (Default: ```2160```)

View file

@ -441,6 +441,9 @@
storage = "foobar" storage = "foobar"
keyType = "foobar" keyType = "foobar"
certificatesDuration = 42 certificatesDuration = 42
caCertificates = ["foobar", "foobar"]
caSystemCertPool = true
caServerName = "foobar"
[certificatesResolvers.CertificateResolver0.acme.eab] [certificatesResolvers.CertificateResolver0.acme.eab]
kid = "foobar" kid = "foobar"
hmacEncoded = "foobar" hmacEncoded = "foobar"
@ -461,6 +464,9 @@
storage = "foobar" storage = "foobar"
keyType = "foobar" keyType = "foobar"
certificatesDuration = 42 certificatesDuration = 42
caCertificates = ["foobar", "foobar"]
caSystemCertPool = true
caServerName = "foobar"
[certificatesResolvers.CertificateResolver1.acme.eab] [certificatesResolvers.CertificateResolver1.acme.eab]
kid = "foobar" kid = "foobar"
hmacEncoded = "foobar" hmacEncoded = "foobar"

View file

@ -483,6 +483,11 @@ certificatesResolvers:
kid: foobar kid: foobar
hmacEncoded: foobar hmacEncoded: foobar
certificatesDuration: 42 certificatesDuration: 42
caCertificates:
- foobar
- foobar
caSystemCertPool: true
caServerName: foobar
dnsChallenge: dnsChallenge:
provider: foobar provider: foobar
delayBeforeCheck: 42s delayBeforeCheck: 42s
@ -505,6 +510,11 @@ certificatesResolvers:
kid: foobar kid: foobar
hmacEncoded: foobar hmacEncoded: foobar
certificatesDuration: 42 certificatesDuration: 42
caCertificates:
- foobar
- foobar
caSystemCertPool: true
caServerName: foobar
dnsChallenge: dnsChallenge:
provider: foobar provider: foobar
delayBeforeCheck: 42s delayBeforeCheck: 42s

View file

@ -213,6 +213,8 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
features.SupportHTTPRoutePathRedirect, features.SupportHTTPRoutePathRedirect,
features.SupportHTTPRouteResponseHeaderModification, features.SupportHTTPRouteResponseHeaderModification,
features.SupportTLSRoute, features.SupportTLSRoute,
features.SupportHTTPRouteBackendProtocolH2C,
features.SupportHTTPRouteBackendProtocolWebSocket,
), ),
}) })
require.NoError(s.T(), err) require.NoError(s.T(), err)

View file

@ -6,9 +6,13 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http"
"net/url" "net/url"
"os"
"reflect" "reflect"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -43,6 +47,10 @@ type Configuration struct {
EAB *EAB `description:"External Account Binding to use." json:"eab,omitempty" toml:"eab,omitempty" yaml:"eab,omitempty"` EAB *EAB `description:"External Account Binding to use." json:"eab,omitempty" toml:"eab,omitempty" yaml:"eab,omitempty"`
CertificatesDuration int `description:"Certificates' duration in hours." json:"certificatesDuration,omitempty" toml:"certificatesDuration,omitempty" yaml:"certificatesDuration,omitempty" export:"true"` CertificatesDuration int `description:"Certificates' duration in hours." json:"certificatesDuration,omitempty" toml:"certificatesDuration,omitempty" yaml:"certificatesDuration,omitempty" export:"true"`
CACertificates []string `description:"Specify the paths to PEM encoded CA Certificates that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list." json:"caCertificates,omitempty" toml:"caCertificates,omitempty" yaml:"caCertificates,omitempty"`
CASystemCertPool bool `description:"Define if the certificates pool must use a copy of the system cert pool." json:"caSystemCertPool,omitempty" toml:"caSystemCertPool,omitempty" yaml:"caSystemCertPool,omitempty" export:"true"`
CAServerName string `description:"Specify the CA server name that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list." json:"caServerName,omitempty" toml:"caServerName,omitempty" yaml:"caServerName,omitempty" export:"true"`
DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge." json:"dnsChallenge,omitempty" toml:"dnsChallenge,omitempty" yaml:"dnsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge." json:"dnsChallenge,omitempty" toml:"dnsChallenge,omitempty" yaml:"dnsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." json:"httpChallenge,omitempty" toml:"httpChallenge,omitempty" yaml:"httpChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." json:"httpChallenge,omitempty" toml:"httpChallenge,omitempty" yaml:"httpChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
TLSChallenge *TLSChallenge `description:"Activate TLS-ALPN-01 Challenge." json:"tlsChallenge,omitempty" toml:"tlsChallenge,omitempty" yaml:"tlsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` TLSChallenge *TLSChallenge `description:"Activate TLS-ALPN-01 Challenge." json:"tlsChallenge,omitempty" toml:"tlsChallenge,omitempty" yaml:"tlsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
@ -261,6 +269,11 @@ func (p *Provider) getClient() (*lego.Client, error) {
config.Certificate.KeyType = GetKeyType(ctx, p.KeyType) config.Certificate.KeyType = GetKeyType(ctx, p.KeyType)
config.UserAgent = fmt.Sprintf("containous-traefik/%s", version.Version) config.UserAgent = fmt.Sprintf("containous-traefik/%s", version.Version)
config.HTTPClient, err = p.createHTTPClient()
if err != nil {
return nil, fmt.Errorf("creating HTTP client: %w", err)
}
client, err := lego.NewClient(config) client, err := lego.NewClient(config)
if err != nil { if err != nil {
return nil, err return nil, err
@ -340,6 +353,64 @@ func (p *Provider) getClient() (*lego.Client, error) {
return p.client, nil return p.client, nil
} }
func (p *Provider) createHTTPClient() (*http.Client, error) {
tlsConfig, err := p.createClientTLSConfig()
if err != nil {
return nil, fmt.Errorf("creating client TLS config: %w", err)
}
return &http.Client{
Timeout: 2 * time.Minute,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
TLSClientConfig: tlsConfig,
},
}, nil
}
func (p *Provider) createClientTLSConfig() (*tls.Config, error) {
if len(p.CACertificates) > 0 || p.CAServerName != "" {
certPool, err := lego.CreateCertPool(p.CACertificates, p.CASystemCertPool)
if err != nil {
return nil, fmt.Errorf("creating cert pool with custom certificates: %w", err)
}
return &tls.Config{
ServerName: p.CAServerName,
RootCAs: certPool,
}, nil
}
// Compatibility layer with the lego.
// https://github.com/go-acme/lego/blob/834a9089f143e3407b3f5c8b93a0e285ba231fe2/lego/client_config.go#L24-L34
// https://github.com/go-acme/lego/blob/834a9089f143e3407b3f5c8b93a0e285ba231fe2/lego/client_config.go#L97-L113
serverName := os.Getenv("LEGO_CA_SERVER_NAME")
customCACertsPath := os.Getenv("LEGO_CA_CERTIFICATES")
if customCACertsPath == "" && serverName == "" {
return nil, nil
}
useSystemCertPool, _ := strconv.ParseBool(os.Getenv("LEGO_CA_SYSTEM_CERT_POOL"))
certPool, err := lego.CreateCertPool(strings.Split(customCACertsPath, string(os.PathListSeparator)), useSystemCertPool)
if err != nil {
return nil, fmt.Errorf("creating cert pool: %w", err)
}
return &tls.Config{
ServerName: serverName,
RootCAs: certPool,
}, nil
}
func (p *Provider) initAccount(ctx context.Context) (*Account, error) { func (p *Provider) initAccount(ctx context.Context) (*Account, error) {
if p.account == nil || len(p.account.Email) == 0 { if p.account == nil || len(p.account.Email) == 0 {
var err error var err error
@ -424,8 +495,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
if len(route.TLS.Domains) > 0 { if len(route.TLS.Domains) > 0 {
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains) domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
for i := range len(domains) { for _, domain := range domains {
domain := domains[i]
safe.Go(func() { safe.Go(func() {
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName) dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
if err != nil { if err != nil {
@ -461,8 +531,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
if len(route.TLS.Domains) > 0 { if len(route.TLS.Domains) > 0 {
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains) domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
for i := range len(domains) { for _, domain := range domains {
domain := domains[i]
safe.Go(func() { safe.Go(func() {
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName) dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
if err != nil { if err != nil {

View file

@ -0,0 +1,61 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: HTTPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-multi-protocols
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- path:
type: Exact
value: /bar
backendRefs:
- name: whoami-h2c
port: 80
weight: 1
kind: Service
group: ""
- name: whoami-ws
port: 80
weight: 1
kind: Service
group: ""
- name: whoami-wss
port: 80
weight: 1
kind: Service
group: ""

View file

@ -7,9 +7,11 @@ metadata:
spec: spec:
ports: ports:
- name: web2 - name: web2
protocol: TCP
port: 8000 port: 8000
targetPort: web2 targetPort: web2
- name: web - name: web
protocol: TCP
port: 80 port: 80
targetPort: web targetPort: web
selector: selector:
@ -48,9 +50,11 @@ metadata:
spec: spec:
ports: ports:
- name: web2 - name: web2
protocol: TCP
port: 8000 port: 8000
targetPort: web2 targetPort: web2
- name: web - name: web
protocol: TCP
port: 80 port: 80
targetPort: web targetPort: web
selector: selector:
@ -89,6 +93,7 @@ metadata:
spec: spec:
ports: ports:
- name: web - name: web
protocol: TCP
port: 8080 port: 8080
targetPort: web targetPort: web
selector: selector:
@ -317,3 +322,105 @@ status:
ingress: ingress:
- hostname: foo.bar - hostname: foo.bar
- ip: 1.2.3.4 - ip: 1.2.3.4
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: whoami-h2c
namespace: default
labels:
kubernetes.io/service-name: whoami-h2c
addressType: IPv4
ports:
- name: h2c
protocol: TCP
port: 80
endpoints:
- addresses:
- 10.10.0.13
conditions:
ready: true
---
apiVersion: v1
kind: Service
metadata:
name: whoami-h2c
namespace: default
spec:
ports:
- protocol: TCP
port: 80
name: h2c
appProtocol: kubernetes.io/h2c
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: whoami-ws
namespace: default
labels:
kubernetes.io/service-name: whoami-ws
addressType: IPv4
ports:
- name: ws
protocol: TCP
port: 80
endpoints:
- addresses:
- 10.10.0.14
conditions:
ready: true
---
apiVersion: v1
kind: Service
metadata:
name: whoami-ws
namespace: default
spec:
ports:
- protocol: TCP
port: 80
name: ws
appProtocol: kubernetes.io/ws
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: whoami-wss
namespace: default
labels:
kubernetes.io/service-name: whoami-wss
addressType: IPv4
ports:
- name: wss
protocol: TCP
port: 80
endpoints:
- addresses:
- 10.10.0.15
conditions:
ready: true
---
apiVersion: v1
kind: Service
metadata:
name: whoami-wss
namespace: default
spec:
ports:
- protocol: TCP
port: 80
name: wss
appProtocol: kubernetes.io/wss

View file

@ -2,7 +2,6 @@ package gateway
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
@ -260,23 +259,16 @@ func (p *Provider) loadGRPCBackendRef(route *gatev1.GRPCRoute, backendRef gatev1
ObservedGeneration: route.Generation, ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(), LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol), Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s/%s/%s port is required", group, kind, namespace, backendRef.Name), Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s/%s/%s: port is required", group, kind, namespace, backendRef.Name),
} }
} }
portStr := strconv.FormatInt(int64(port), 10) portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr) serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, err := p.loadGRPCServers(namespace, backendRef) lb, errCondition := p.loadGRPCServers(namespace, route, backendRef)
if err != nil { if errCondition != nil {
return serviceName, nil, &metav1.Condition{ return serviceName, nil, errCondition
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err),
}
} }
return serviceName, &dynamic.Service{LoadBalancer: lb}, nil return serviceName, &dynamic.Service{LoadBalancer: lb}, nil
@ -319,72 +311,49 @@ func (p *Provider) loadGRPCMiddlewares(conf *dynamic.Configuration, namespace, r
return middlewareNames, nil return middlewareNames, nil
} }
func (p *Provider) loadGRPCServers(namespace string, backendRef gatev1.GRPCBackendRef) (*dynamic.ServersLoadBalancer, error) { func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef) (*dynamic.ServersLoadBalancer, *metav1.Condition) {
if backendRef.Port == nil { backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
return nil, errors.New("port is required for Kubernetes Service reference")
}
service, exists, err := p.client.GetService(namespace, string(backendRef.Name))
if err != nil { if err != nil {
return nil, fmt.Errorf("getting service: %w", err) return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: %s", namespace, backendRef.Name, err),
} }
if !exists {
return nil, errors.New("service not found")
} }
var svcPort *corev1.ServicePort if svcPort.Protocol != corev1.ProtocolTCP {
for _, p := range service.Spec.Ports { return nil, &metav1.Condition{
if p.Port == int32(*backendRef.Port) { Type: string(gatev1.RouteConditionResolvedRefs),
svcPort = &p Status: metav1.ConditionFalse,
break ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only TCP protocol is supported", namespace, backendRef.Name),
} }
} }
if svcPort == nil {
return nil, fmt.Errorf("service port %d not found", *backendRef.Port)
}
endpointSlices, err := p.client.ListEndpointSlicesForService(namespace, string(backendRef.Name)) if svcPort.AppProtocol != nil && *svcPort.AppProtocol != appProtocolH2C {
if err != nil { return nil, &metav1.Condition{
return nil, fmt.Errorf("getting endpointslices: %w", err) Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only kubernetes.io/h2c appProtocol is supported", namespace, backendRef.Name),
} }
if len(endpointSlices) == 0 {
return nil, errors.New("endpointslices not found")
} }
lb := &dynamic.ServersLoadBalancer{} lb := &dynamic.ServersLoadBalancer{}
lb.SetDefaults() lb.SetDefaults()
addresses := map[string]struct{}{} for _, ba := range backendAddresses {
for _, endpointSlice := range endpointSlices {
var port int32
for _, p := range endpointSlice.Ports {
if svcPort.Name == *p.Name {
port = *p.Port
break
}
}
if port == 0 {
continue
}
for _, endpoint := range endpointSlice.Endpoints {
if endpoint.Conditions.Ready == nil || !*endpoint.Conditions.Ready {
continue
}
for _, address := range endpoint.Addresses {
if _, ok := addresses[address]; ok {
continue
}
addresses[address] = struct{}{}
lb.Servers = append(lb.Servers, dynamic.Server{ lb.Servers = append(lb.Servers, dynamic.Server{
URL: fmt.Sprintf("h2c://%s", net.JoinHostPort(address, strconv.Itoa(int(port)))), URL: fmt.Sprintf("h2c://%s", net.JoinHostPort(ba.Address, strconv.Itoa(int(ba.Port)))),
}) })
} }
}
}
return lb, nil return lb, nil
} }

View file

@ -260,23 +260,16 @@ func (p *Provider) loadService(route *gatev1.HTTPRoute, backendRef gatev1.HTTPBa
ObservedGeneration: route.Generation, ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(), LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol), Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s port is required", group, kind, namespace, backendRef.Name), Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: port is required", group, kind, namespace, backendRef.Name),
} }
} }
portStr := strconv.FormatInt(int64(port), 10) portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr) serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, err := p.loadHTTPServers(namespace, backendRef) lb, errCondition := p.loadHTTPServers(namespace, route, backendRef)
if err != nil { if errCondition != nil {
return serviceName, nil, &metav1.Condition{ return serviceName, nil, errCondition
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err),
}
} }
return serviceName, &dynamic.Service{LoadBalancer: lb}, nil return serviceName, &dynamic.Service{LoadBalancer: lb}, nil
@ -372,74 +365,39 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe
return filterFunc(string(extensionRef.Name), namespace) return filterFunc(string(extensionRef.Name), namespace)
} }
func (p *Provider) loadHTTPServers(namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) { func (p *Provider) loadHTTPServers(namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, *metav1.Condition) {
if backendRef.Port == nil { backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
return nil, errors.New("port is required for Kubernetes Service reference")
}
service, exists, err := p.client.GetService(namespace, string(backendRef.Name))
if err != nil { if err != nil {
return nil, fmt.Errorf("getting service: %w", err) return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s: %s", namespace, backendRef.Name, err),
} }
if !exists {
return nil, errors.New("service not found")
} }
var svcPort *corev1.ServicePort protocol, err := getProtocol(svcPort)
for _, p := range service.Spec.Ports {
if p.Port == int32(*backendRef.Port) {
svcPort = &p
break
}
}
if svcPort == nil {
return nil, fmt.Errorf("service port %d not found", *backendRef.Port)
}
endpointSlices, err := p.client.ListEndpointSlicesForService(namespace, string(backendRef.Name))
if err != nil { if err != nil {
return nil, fmt.Errorf("getting endpointslices: %w", err) return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s: %s", namespace, backendRef.Name, err),
} }
if len(endpointSlices) == 0 {
return nil, errors.New("endpointslices not found")
} }
lb := &dynamic.ServersLoadBalancer{} lb := &dynamic.ServersLoadBalancer{}
lb.SetDefaults() lb.SetDefaults()
protocol := getProtocol(*svcPort) for _, ba := range backendAddresses {
addresses := map[string]struct{}{}
for _, endpointSlice := range endpointSlices {
var port int32
for _, p := range endpointSlice.Ports {
if svcPort.Name == *p.Name {
port = *p.Port
break
}
}
if port == 0 {
continue
}
for _, endpoint := range endpointSlice.Endpoints {
if endpoint.Conditions.Ready == nil || !*endpoint.Conditions.Ready {
continue
}
for _, address := range endpoint.Addresses {
if _, ok := addresses[address]; ok {
continue
}
addresses[address] = struct{}{}
lb.Servers = append(lb.Servers, dynamic.Server{ lb.Servers = append(lb.Servers, dynamic.Server{
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(address, strconv.Itoa(int(port)))), URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ba.Address, strconv.Itoa(int(ba.Port)))),
}) })
} }
}
}
return lb, nil return lb, nil
} }
@ -702,13 +660,29 @@ func createURLRewrite(filter *gatev1.HTTPURLRewriteFilter, pathMatch gatev1.HTTP
}, nil }, nil
} }
func getProtocol(portSpec corev1.ServicePort) string { func getProtocol(portSpec corev1.ServicePort) (string, error) {
if portSpec.Protocol != corev1.ProtocolTCP {
return "", errors.New("only TCP protocol is supported")
}
if portSpec.AppProtocol == nil {
protocol := "http" protocol := "http"
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
protocol = "https" protocol = "https"
} }
return protocol, nil
}
return protocol switch ap := *portSpec.AppProtocol; ap {
case appProtocolH2C:
return "h2c", nil
case appProtocolWS:
return "http", nil
case appProtocolWSS:
return "https", nil
default:
return "", fmt.Errorf("unsupported application protocol %s", ap)
}
} }
func mergeHTTPConfiguration(from, to *dynamic.Configuration) { func mergeHTTPConfiguration(from, to *dynamic.Configuration) {

View file

@ -36,6 +36,7 @@ import (
const ( const (
providerName = "kubernetesgateway" providerName = "kubernetesgateway"
controllerName = "traefik.io/gateway-controller" controllerName = "traefik.io/gateway-controller"
groupCore = "core" groupCore = "core"
@ -48,6 +49,10 @@ const (
kindTCPRoute = "TCPRoute" kindTCPRoute = "TCPRoute"
kindTLSRoute = "TLSRoute" kindTLSRoute = "TLSRoute"
kindService = "Service" kindService = "Service"
appProtocolH2C = "kubernetes.io/h2c"
appProtocolWS = "kubernetes.io/ws"
appProtocolWSS = "kubernetes.io/wss"
) )
// Provider holds configurations of the provider. // Provider holds configurations of the provider.
@ -854,6 +859,79 @@ func (p *Provider) allowedNamespaces(gatewayNamespace string, routeNamespaces *g
return nil, fmt.Errorf("unsupported RouteSelectType: %q", *routeNamespaces.From) return nil, fmt.Errorf("unsupported RouteSelectType: %q", *routeNamespaces.From)
} }
type backendAddress struct {
Address string
Port int32
}
func (p *Provider) getBackendAddresses(namespace string, ref gatev1.BackendRef) ([]backendAddress, corev1.ServicePort, error) {
if ref.Port == nil {
return nil, corev1.ServicePort{}, errors.New("port is required for Kubernetes Service reference")
}
service, exists, err := p.client.GetService(namespace, string(ref.Name))
if err != nil {
return nil, corev1.ServicePort{}, fmt.Errorf("getting service: %w", err)
}
if !exists {
return nil, corev1.ServicePort{}, errors.New("service not found")
}
var svcPort *corev1.ServicePort
for _, p := range service.Spec.Ports {
if p.Port == int32(*ref.Port) {
svcPort = &p
break
}
}
if svcPort == nil {
return nil, corev1.ServicePort{}, fmt.Errorf("service port %d not found", *ref.Port)
}
endpointSlices, err := p.client.ListEndpointSlicesForService(namespace, string(ref.Name))
if err != nil {
return nil, corev1.ServicePort{}, fmt.Errorf("getting endpointslices: %w", err)
}
if len(endpointSlices) == 0 {
return nil, corev1.ServicePort{}, errors.New("endpointslices not found")
}
uniqAddresses := map[string]struct{}{}
backendServers := make([]backendAddress, 0)
for _, endpointSlice := range endpointSlices {
var port int32
for _, p := range endpointSlice.Ports {
if svcPort.Name == *p.Name {
port = *p.Port
break
}
}
if port == 0 {
continue
}
for _, endpoint := range endpointSlice.Endpoints {
if endpoint.Conditions.Ready == nil || !*endpoint.Conditions.Ready {
continue
}
for _, address := range endpoint.Addresses {
if _, ok := uniqAddresses[address]; ok {
continue
}
uniqAddresses[address] = struct{}{}
backendServers = append(backendServers, backendAddress{
Address: address,
Port: port,
})
}
}
}
return backendServers, *svcPort, nil
}
func supportedRouteKinds(protocol gatev1.ProtocolType, experimentalChannel bool) ([]gatev1.RouteGroupKind, []metav1.Condition) { func supportedRouteKinds(protocol gatev1.ProtocolType, experimentalChannel bool) ([]gatev1.RouteGroupKind, []metav1.Condition) {
group := gatev1.Group(gatev1.GroupName) group := gatev1.Group(gatev1.GroupName)

View file

@ -2452,6 +2452,104 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) {
TLS: &dynamic.TLSConfiguration{}, TLS: &dynamic.TLSConfiguration{},
}, },
}, },
{
desc: "Simple HTTPRoute, with appProtocol service",
paths: []string{"services.yml", "httproute/with_app_protocol_service.yml"},
groupKindBackendFuncs: map[string]map[string]BuildBackendFunc{
traefikv1alpha1.GroupName: {"TraefikService": func(name, namespace string) (string, *dynamic.Service, error) {
// func should never be executed in case of cross-provider reference.
return "", nil, errors.New("BOOM")
}},
},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-http-multi-protocols-my-gateway-web-0-1c0cf64bde37d9d0df06": {
EntryPoints: []string{"web"},
Service: "default-http-multi-protocols-my-gateway-web-0-1c0cf64bde37d9d0df06-wrr",
Rule: "Host(`foo.com`) && Path(`/bar`)",
Priority: 100008,
RuleSyntax: "v3",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-http-multi-protocols-my-gateway-web-0-1c0cf64bde37d9d0df06-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-h2c-80",
Weight: ptr.To(1),
},
{
Name: "default-whoami-ws-80",
Weight: ptr.To(1),
},
{
Name: "default-whoami-wss-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-h2c-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "h2c://10.10.0.13:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"default-whoami-ws-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.14:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
"default-whoami-wss-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "https://10.10.0.15:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
} }
for _, test := range testCases { for _, test := range testCases {

View file

@ -2,7 +2,6 @@ package gateway
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
@ -252,87 +251,45 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
portStr := strconv.FormatInt(int64(port), 10) portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr) serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, err := p.loadTCPServers(namespace, backendRef) lb, errCondition := p.loadTCPServers(namespace, route, backendRef)
if err != nil { if errCondition != nil {
return serviceName, nil, &metav1.Condition{ return serviceName, nil, errCondition
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err),
}
} }
return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil
} }
func (p *Provider) loadTCPServers(namespace string, backendRef gatev1.BackendRef) (*dynamic.TCPServersLoadBalancer, error) { func (p *Provider) loadTCPServers(namespace string, route *gatev1alpha2.TCPRoute, backendRef gatev1.BackendRef) (*dynamic.TCPServersLoadBalancer, *metav1.Condition) {
if backendRef.Port == nil { backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef)
return nil, errors.New("port is required for Kubernetes Service reference")
}
service, exists, err := p.client.GetService(namespace, string(backendRef.Name))
if err != nil { if err != nil {
return nil, fmt.Errorf("getting service: %w", err) return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s: %s", namespace, backendRef.Name, err),
} }
if !exists {
return nil, errors.New("service not found")
} }
var svcPort *corev1.ServicePort if svcPort.Protocol != corev1.ProtocolTCP {
for _, p := range service.Spec.Ports { return nil, &metav1.Condition{
if p.Port == int32(*backendRef.Port) { Type: string(gatev1.RouteConditionResolvedRefs),
svcPort = &p Status: metav1.ConditionFalse,
break ObservedGeneration: route.GetGeneration(),
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s: only TCP protocol is supported", namespace, backendRef.Name),
} }
} }
if svcPort == nil {
return nil, fmt.Errorf("service port %d not found", *backendRef.Port)
}
endpointSlices, err := p.client.ListEndpointSlicesForService(namespace, string(backendRef.Name))
if err != nil {
return nil, fmt.Errorf("getting endpointslices: %w", err)
}
if len(endpointSlices) == 0 {
return nil, errors.New("endpointslices not found")
}
lb := &dynamic.TCPServersLoadBalancer{} lb := &dynamic.TCPServersLoadBalancer{}
addresses := map[string]struct{}{} for _, ba := range backendAddresses {
for _, endpointSlice := range endpointSlices {
var port int32
for _, p := range endpointSlice.Ports {
if svcPort.Name == *p.Name {
port = *p.Port
break
}
}
if port == 0 {
continue
}
for _, endpoint := range endpointSlice.Endpoints {
if endpoint.Conditions.Ready == nil || !*endpoint.Conditions.Ready {
continue
}
for _, address := range endpoint.Addresses {
if _, ok := addresses[address]; ok {
continue
}
addresses[address] = struct{}{}
lb.Servers = append(lb.Servers, dynamic.TCPServer{ lb.Servers = append(lb.Servers, dynamic.TCPServer{
// TODO determine whether the servers needs TLS, from the port? Address: net.JoinHostPort(ba.Address, strconv.Itoa(int(ba.Port))),
Address: net.JoinHostPort(address, strconv.Itoa(int(port))),
}) })
} }
}
}
return lb, nil return lb, nil
} }

View file

@ -3,6 +3,7 @@ package gateway
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -10,6 +11,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider" "github.com/traefik/traefik/v3/pkg/provider"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types" ktypes "k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
@ -251,21 +253,49 @@ func (p *Provider) loadTLSService(route *gatev1alpha2.TLSRoute, backendRef gatev
portStr := strconv.FormatInt(int64(port), 10) portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr) serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, err := p.loadTCPServers(namespace, backendRef) lb, errCondition := p.loadTLSServers(namespace, route, backendRef)
if err != nil { if errCondition != nil {
return serviceName, nil, &metav1.Condition{ return serviceName, nil, errCondition
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err),
}
} }
return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil
} }
func (p *Provider) loadTLSServers(namespace string, route *gatev1alpha2.TLSRoute, backendRef gatev1.BackendRef) (*dynamic.TCPServersLoadBalancer, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef)
if err != nil {
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonBackendNotFound),
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s: %s", namespace, backendRef.Name, err),
}
}
if svcPort.Protocol != corev1.ProtocolTCP {
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s: only TCP protocol is supported", namespace, backendRef.Name),
}
}
lb := &dynamic.TCPServersLoadBalancer{}
for _, ba := range backendAddresses {
lb.Servers = append(lb.Servers, dynamic.TCPServer{
// TODO determine whether the servers needs TLS, from the port?
Address: net.JoinHostPort(ba.Address, strconv.Itoa(int(ba.Port))),
})
}
return lb, nil
}
func hostSNIRule(hostnames []gatev1.Hostname) string { func hostSNIRule(hostnames []gatev1.Hostname) string {
rules := make([]string, 0, len(hostnames)) rules := make([]string, 0, len(hostnames))
uniqHostnames := map[gatev1.Hostname]struct{}{} uniqHostnames := map[gatev1.Hostname]struct{}{}