diff --git a/internal/caddy/ingress/annotations.go b/internal/caddy/ingress/annotations.go index 3f9f720..c711cdb 100644 --- a/internal/caddy/ingress/annotations.go +++ b/internal/caddy/ingress/annotations.go @@ -12,6 +12,7 @@ const ( permanentRedirectAnnotation = "permanent-redirect" permanentRedirectCodeAnnotation = "permanent-redirect-code" temporaryRedirectAnnotation = "temporal-redirect" + trustedProxies = "trusted-proxies" ) func getAnnotation(ing *v1.Ingress, rule string) string { diff --git a/internal/caddy/ingress/reverseproxy.go b/internal/caddy/ingress/reverseproxy.go index 0d6f7b9..3e5a41c 100644 --- a/internal/caddy/ingress/reverseproxy.go +++ b/internal/caddy/ingress/reverseproxy.go @@ -2,6 +2,7 @@ package ingress import ( "fmt" + "net/netip" "strings" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -26,6 +27,7 @@ func (p ReverseProxyPlugin) IngressHandler(input converter.IngressMiddlewareInpu path := input.Path ing := input.Ingress backendProtocol := strings.ToLower(getAnnotation(ing, backendProtocol)) + trustedProxiesAnnotation := strings.ToLower(getAnnotation(ing, trustedProxies)) // TODO :- // when setting the upstream url we should bypass kube-dns and get the ip address of @@ -41,11 +43,22 @@ func (p ReverseProxyPlugin) IngressHandler(input converter.IngressMiddlewareInpu } } + var err error + var parsedProxies []string + if trustedProxiesAnnotation != "" { + trustedProxies := strings.Split(trustedProxiesAnnotation, ",") + parsedProxies, err = parseTrustedProxies(trustedProxies) + if err != nil { + return nil, err + } + } + handler := reverseproxy.Handler{ TransportRaw: caddyconfig.JSONModuleObject(transport, "protocol", "http", nil), Upstreams: reverseproxy.UpstreamPool{ {Dial: clusterHostName}, }, + TrustedProxies: parsedProxies, } handlerModule := caddyconfig.JSONModuleObject( @@ -58,6 +71,28 @@ func (p ReverseProxyPlugin) IngressHandler(input converter.IngressMiddlewareInpu return input.Route, nil } +// Copied from https://github.com/caddyserver/caddy/blob/21af88fefc9a8239a024f635f1c6fdd9defd7eb7/modules/caddyhttp/reverseproxy/reverseproxy.go#L270-L286 +func parseTrustedProxies(trustedProxies []string) (parsedProxies []string, err error) { + for _, trustedProxy := range trustedProxies { + trustedProxy = strings.TrimSpace(trustedProxy) + if strings.Contains(trustedProxy, "/") { + ipNet, err := netip.ParsePrefix(trustedProxy) + if err != nil { + return nil, fmt.Errorf("failed to parse IP: %q", trustedProxy) + } + parsedProxies = append(parsedProxies, ipNet.String()) + } else { + ipAddr, err := netip.ParseAddr(trustedProxy) + if err != nil { + return nil, fmt.Errorf("failed to parse IP: %q", trustedProxy) + } + ipNew := netip.PrefixFrom(ipAddr, ipAddr.BitLen()) + parsedProxies = append(parsedProxies, ipNew.String()) + } + } + return parsedProxies, nil +} + func init() { converter.RegisterPlugin(ReverseProxyPlugin{}) } diff --git a/internal/caddy/ingress/reverseproxy_test.go b/internal/caddy/ingress/reverseproxy_test.go new file mode 100644 index 0000000..e9f9d6c --- /dev/null +++ b/internal/caddy/ingress/reverseproxy_test.go @@ -0,0 +1,168 @@ +package ingress + +import ( + "encoding/json" + "os" + "testing" + + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/ingress/pkg/converter" + "github.com/stretchr/testify/require" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestTrustedProxesConvertToCaddyConfig(t *testing.T) { + rpp := ReverseProxyPlugin{} + + tests := []struct { + name string + annotations map[string]string + expectedConfigPath string + }{ + { + name: "ipv4 trusted proxies", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "192.168.1.0, 10.0.0.1", + }, + expectedConfigPath: "test_data/reverseproxy_trusted_proxies_ipv4.json", + }, + { + name: "ipv4 trusted proxies wit subnet", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "192.168.1.0/16,10.0.0.1/8", + }, + expectedConfigPath: "test_data/reverseproxy_trusted_proxies_ipv4_subnet.json", + }, + { + name: "ipv6 trusted proxies", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::1, 2001:db8::5", + }, + expectedConfigPath: "test_data/reverseproxy_trusted_proxies_ipv6.json", + }, + { + name: "ipv6 trusted proxies", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::1/36,2001:db8::5/60", + }, + expectedConfigPath: "test_data/reverseproxy_trusted_proxies_ipv6_subnet.json", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := converter.IngressMiddlewareInput{ + Ingress: &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: test.annotations, + Namespace: "namespace", + }, + }, + Path: networkingv1.HTTPIngressPath{ + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "svcName", + Port: networkingv1.ServiceBackendPort{Number: 80}, + }, + }, + }, + Route: &caddyhttp.Route{}, + } + + route, err := rpp.IngressHandler(input) + require.NoError(t, err) + + expectedCfg, err := os.ReadFile(test.expectedConfigPath) + require.NoError(t, err) + + cfgJson, err := json.Marshal(&route) + require.NoError(t, err) + + require.JSONEq(t, string(expectedCfg), string(cfgJson)) + }) + } +} + +func TestMisconfiguredTrustedProxiesConvertToCaddyConfig(t *testing.T) { + rpp := ReverseProxyPlugin{} + + tests := []struct { + name string + annotations map[string]string + expectedError string + }{ + { + name: "invalid ipv4 trusted proxy", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "999.999.999.999", + }, + expectedError: `failed to parse IP: "999.999.999.999"`, + }, + { + name: "invalid ipv4 with subnet trusted proxy", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "999.999.999.999/32", + }, + expectedError: `failed to parse IP: "999.999.999.999/32"`, + }, + { + name: "invalid subnet for ipv4 trusted proxy", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "10.0.0.0/100", + }, + expectedError: `failed to parse IP: "10.0.0.0/100"`, + }, + { + name: "invalid ipv6 trusted proxy", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::g", + }, + expectedError: `failed to parse IP: "2001:db8::g"`, + }, + { + name: "invalid ipv6 with subnet trusted proxy", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::g/128", + }, + expectedError: `failed to parse IP: "2001:db8::g/128"`, + }, + { + name: "invalid subnet for ipv6 trusted proxy", + annotations: map[string]string{ + "caddy.ingress.kubernetes.io/trusted-proxies": "2001:db8::/200", + }, + expectedError: `failed to parse IP: "2001:db8::/200"`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := converter.IngressMiddlewareInput{ + Ingress: &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: test.annotations, + Namespace: "namespace", + }, + }, + Path: networkingv1.HTTPIngressPath{ + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "svcName", + Port: networkingv1.ServiceBackendPort{Number: 80}, + }, + }, + }, + Route: &caddyhttp.Route{}, + } + + route, err := rpp.IngressHandler(input) + require.EqualError(t, err, test.expectedError) + + cfgJson, err := json.Marshal(&route) + require.NoError(t, err) + + require.JSONEq(t, string(cfgJson), "null") + }) + } +} diff --git a/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv4.json b/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv4.json new file mode 100644 index 0000000..7ccbe8a --- /dev/null +++ b/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv4.json @@ -0,0 +1,19 @@ +{ + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http" + }, + "trusted_proxies": [ + "192.168.1.0/32", + "10.0.0.1/32" + ], + "upstreams": [ + { + "dial": "svcName.namespace.svc.cluster.local:80" + } + ] + } + ] +} diff --git a/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv4_subnet.json b/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv4_subnet.json new file mode 100644 index 0000000..9b7a776 --- /dev/null +++ b/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv4_subnet.json @@ -0,0 +1,19 @@ +{ + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http" + }, + "trusted_proxies": [ + "192.168.1.0/16", + "10.0.0.1/8" + ], + "upstreams": [ + { + "dial": "svcName.namespace.svc.cluster.local:80" + } + ] + } + ] +} diff --git a/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv6.json b/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv6.json new file mode 100644 index 0000000..112bd0e --- /dev/null +++ b/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv6.json @@ -0,0 +1,19 @@ +{ + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http" + }, + "trusted_proxies": [ + "2001:db8::1/128", + "2001:db8::5/128" + ], + "upstreams": [ + { + "dial": "svcName.namespace.svc.cluster.local:80" + } + ] + } + ] +} diff --git a/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv6_subnet.json b/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv6_subnet.json new file mode 100644 index 0000000..4c0e759 --- /dev/null +++ b/internal/caddy/ingress/test_data/reverseproxy_trusted_proxies_ipv6_subnet.json @@ -0,0 +1,19 @@ +{ + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http" + }, + "trusted_proxies": [ + "2001:db8::1/36", + "2001:db8::5/60" + ], + "upstreams": [ + { + "dial": "svcName.namespace.svc.cluster.local:80" + } + ] + } + ] +}