Skip to content

Commit

Permalink
feat: add support for TCPRoute, TLSRoute, and UDPRoute (#4)
Browse files Browse the repository at this point in the history
Signed-off-by: Matthew Penner <me@matthewp.io>
  • Loading branch information
matthewpi authored May 30, 2024
1 parent e4859c2 commit 3b5baa1
Show file tree
Hide file tree
Showing 23 changed files with 1,820 additions and 160 deletions.
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

0 comments on commit 3b5baa1

Please sign in to comment.