Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add support for TCPRoute, TLSRoute, and UDPRoute #4

Merged
merged 4 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ Support for missing resources is planned but not yet implemented.
- [x] [GatewayClass](https://gateway-api.sigs.k8s.io/api-types/gatewayclass/)
- [x] [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/)
- [x] [ReferenceGrant](https://gateway-api.sigs.k8s.io/api-types/referencegrant/)
- [ ] [BackendLBPolicy](https://gateway-api.sigs.k8s.io/geps/gep-1619/)
- [x] [BackendTLSPolicy](https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/)
- [x] [HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/)
- [ ] [GRPCRoute](https://gateway-api.sigs.k8s.io/api-types/grpcroute/)
- [ ] [TLSRoute](https://gateway-api.sigs.k8s.io/concepts/api-overview/#tlsroute)
- [ ] [TCPRoute](https://gateway-api.sigs.k8s.io/concepts/api-overview/#tcproute-and-udproute)
- [ ] [UDPRoute](https://gateway-api.sigs.k8s.io/concepts/api-overview/#tcproute-and-udproute)
- [x] [TLSRoute](https://gateway-api.sigs.k8s.io/concepts/api-overview/#tlsroute)
- [x] [TCPRoute](https://gateway-api.sigs.k8s.io/concepts/api-overview/#tcproute-and-udproute)
- [x] [UDPRoute](https://gateway-api.sigs.k8s.io/concepts/api-overview/#tcproute-and-udproute)

The [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) resource is not
supported and support is not planned, sorry.
Expand Down
96 changes: 60 additions & 36 deletions internal/caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
caddyv2 "github.com/caddyserver/gateway/internal/caddyv2"
"github.com/caddyserver/gateway/internal/caddyv2/caddyhttp"
"github.com/caddyserver/gateway/internal/caddyv2/caddytls"
"github.com/caddyserver/gateway/internal/layer4"
)

// Config represents the configuration for a Caddy server.
Expand All @@ -31,10 +32,9 @@ type Config struct {

// Apps is the configuration for "apps" on a Caddy server.
type Apps struct {
HTTP *caddyhttp.App `json:"http,omitempty"`
TLS *caddytls.TLS `json:"tls,omitempty"`
// TODO: replace the layer4 package with our own definitions.
// Layer4 *layer4.App `json:"layer4,omitempty"`
HTTP *caddyhttp.App `json:"http,omitempty"`
TLS *caddytls.TLS `json:"tls,omitempty"`
Layer4 *layer4.App `json:"layer4,omitempty"`
}

// Input is provided to us by the Gateway Controller and is used to
Expand All @@ -56,16 +56,16 @@ type Input struct {

Client client.Client

httpServers map[string]*caddyhttp.Server
// layer4Servers map[string]*layer4.Server
config *Config
loadPems []caddytls.CertKeyPEMPair
httpServers map[string]*caddyhttp.Server
layer4Servers map[string]*layer4.Server
config *Config
loadPems []caddytls.CertKeyPEMPair
}

// Config generates a JSON config for use with a Caddy server.
func (i *Input) Config() ([]byte, error) {
i.httpServers = map[string]*caddyhttp.Server{}
// i.layer4Servers = map[string]*layer4.Server{}
i.layer4Servers = map[string]*layer4.Server{}
i.config = &Config{
Admin: &caddyv2.AdminConfig{Listen: ":2019"},
Apps: &Apps{},
Expand All @@ -87,8 +87,6 @@ func (i *Input) Config() ([]byte, error) {
Body: "unable to route request\n",
Headers: http.Header{
"Caddy-Instance": {"{system.hostname}"},
// TODO: remove
// "Trace-ID": {"{http.vars.trace_id}"},
},
},
},
Expand All @@ -104,11 +102,11 @@ func (i *Input) Config() ([]byte, error) {
GracePeriod: caddyv2.Duration(15 * time.Second),
}
}
//if len(i.layer4Servers) > 0 {
// i.config.Apps.Layer4 = &layer4.App{
// Servers: i.layer4Servers,
// }
//}
if len(i.layer4Servers) > 0 {
i.config.Apps.Layer4 = &layer4.App{
Servers: i.layer4Servers,
}
}
if len(i.loadPems) > 0 {
i.config.Apps.TLS = &caddytls.TLS{
Certificates: &caddytls.Certificates{
Expand All @@ -123,32 +121,26 @@ func (i *Input) Config() ([]byte, error) {
func (i *Input) handleListener(l gatewayv1.Listener) error {
switch l.Protocol {
case gatewayv1.HTTPProtocolType:
break
return i.handleHTTPListener(l)
case gatewayv1.HTTPSProtocolType:
break
// If TLS mode is not Terminate, then ignore the listener. We cannot do HTTP routing while
// doing TLS passthrough as we need to decrypt the request in order to route it.
if l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode != gatewayv1.TLSModeTerminate {
return nil
}
return i.handleHTTPListener(l)
case gatewayv1.TLSProtocolType:
break
return i.handleLayer4Listener(l)
case gatewayv1.TCPProtocolType:
// TODO: implement
return nil
return i.handleLayer4Listener(l)
case gatewayv1.UDPProtocolType:
// TODO: implement
return nil
return i.handleLayer4Listener(l)
default:
return nil
}
}

// Defaults to Terminate which is fine, we do need to handle Passthrough
// differently.
if l.TLS != nil && l.TLS.Mode != nil && *l.TLS.Mode == gatewayv1.TLSModePassthrough {
//server, err := i.getTLSServer(l)
//if err != nil {
// return err
//}
//i.layer4Servers[string(l.Name)] = server
return nil
}

func (i *Input) handleHTTPListener(l gatewayv1.Listener) error {
key := strconv.Itoa(int(l.Port))
s, ok := i.httpServers[key]
if !ok {
Expand Down Expand Up @@ -176,8 +168,6 @@ func (i *Input) handleListener(l gatewayv1.Listener) error {
Body: "{http.error.status_code} {http.error.status_text}\n\n{http.error.message}\n",
Headers: http.Header{
"Caddy-Instance": {"{system.hostname}"},
// TODO: remove
// "Trace-ID": {"{http.vars.trace_id}"},
},
},
},
Expand All @@ -195,6 +185,40 @@ func (i *Input) handleListener(l gatewayv1.Listener) error {
return nil
}

func (i *Input) handleLayer4Listener(l gatewayv1.Listener) error {
proto := "tcp"
if l.Protocol == gatewayv1.UDPProtocolType {
proto = "udp"
}
key := proto + "/" + strconv.Itoa(int(l.Port))
s, ok := i.layer4Servers[key]
if !ok {
s = &layer4.Server{
Listen: []string{proto + "/:" + strconv.Itoa(int(l.Port))},
}
}

var (
server *layer4.Server
err error
)
switch l.Protocol {
case gatewayv1.TLSProtocolType:
server, err = i.getTLSServer(s, l)
case gatewayv1.TCPProtocolType:
server, err = i.getTCPServer(s, l)
case gatewayv1.UDPProtocolType:
server, err = i.getUDPServer(s, l)
default:
return nil
}
if err != nil {
return err
}
i.layer4Servers[key] = server
return nil
}

func isRouteForListener(gw *gatewayv1.Gateway, l gatewayv1.Listener, rNS string, rs gatewayv1.RouteStatus) bool {
for _, p := range rs.Parents {
if !gateway.MatchesControllerName(p.ControllerName) {
Expand Down
9 changes: 1 addition & 8 deletions internal/caddy/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,7 @@ func (i *Input) getHTTPServer(s *caddyhttp.Server, l gatewayv1.Listener) (*caddy

terminal := false
matchers := []caddyhttp.Match{}
handlers := []caddyhttp.Handler{
// TODO: option to enable tracing
//&tracing.Tracing{
// // TODO: see if there is a placeholder for a low-cardinality route.
// // Like if one of the caddyfile matchers has a specific path.
// SpanName: "{http.request.method}",
//},
}
handlers := []caddyhttp.Handler{}

// Match hostnames if any are specified.
if len(hr.Spec.Hostnames) > 0 {
Expand Down
83 changes: 83 additions & 0 deletions internal/caddy/tcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Penner

package caddy

import (
"net"
"strconv"

gateway "github.com/caddyserver/gateway/internal"
"github.com/caddyserver/gateway/internal/layer4"
"github.com/caddyserver/gateway/internal/layer4/l4proxy"
corev1 "k8s.io/api/core/v1"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

func (i *Input) getTCPServer(s *layer4.Server, l gatewayv1.Listener) (*layer4.Server, error) {
routes := []*layer4.Route{}
for _, tr := range i.TCPRoutes {
if !isRouteForListener(i.Gateway, l, tr.Namespace, tr.Status.RouteStatus) {
continue
}

handlers := []layer4.Handler{}
for _, rule := range tr.Spec.Rules {
// We only support a single backend ref as we don't support weights for layer4 proxy.
if len(rule.BackendRefs) != 1 {
continue
}

bf := rule.BackendRefs[0]
bor := bf.BackendObjectReference
if !gateway.IsService(bor) {
continue
}

// Safeguard against nil-pointer dereference.
if bor.Port == nil {
continue
}

// Get the service.
//
// TODO: is there a more efficient way to do this?
// We currently list all services and forward them to the input,
// then iterate over them.
//
// Should we just use the Kubernetes client instead?
var service corev1.Service
for _, s := range i.Services {
if s.Namespace != gateway.NamespaceDerefOr(bor.Namespace, tr.Namespace) {
continue
}
if s.Name != string(bor.Name) {
continue
}
service = s
break
}
if service.Name == "" {
// Invalid service reference.
continue
}

handlers = append(handlers, &l4proxy.Handler{
Upstreams: l4proxy.UpstreamPool{
&l4proxy.Upstream{
Dial: []string{net.JoinHostPort(service.Spec.ClusterIP, strconv.Itoa(int(*bor.Port)))},
},
},
})
}

// Add the route.
routes = append(routes, &layer4.Route{
Handlers: handlers,
})
}

// Update the routes on the server.
s.Routes = append(s.Routes, routes...)
return s, nil
}
Loading
Loading