diff --git a/Dockerfile b/Dockerfile index 7060551..d297e39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,10 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -FROM golang:1.24.1-alpine3.21 AS builder +FROM golang:1.24.1-alpine3.21 AS base -RUN apk add --no-cache clang llvm bpftool libbpf-dev +RUN apk add --no-cache clang llvm bpftool libbpf-dev iptables +ENV CGO_ENABLED=0 +ENV GOOS=linux +FROM base AS builder WORKDIR /app COPY go.mod go.sum ./ diff --git a/api/api.go b/api/api.go index 228e90a..eef06c2 100644 --- a/api/api.go +++ b/api/api.go @@ -35,7 +35,11 @@ const ( RoutingModeDefault = RoutingModeBPF RoutingModeBPF = "eBPF" RoutingModeLoopback = "Loopback" + RoutingModeIPTables = "IPTables" GKEAnnotationServiceAccount = GroupGKE + "/gcp-service-account" GKELabelNodeEnabled = GroupGKE + "/gke-metadata-server-enabled" + + GKEMetadataServerAddressDefault = "169.254.169.254" + GKEMetadataServerPortDefault = 80 ) diff --git a/go.mod b/go.mod index 6a47113..97eaab5 100644 --- a/go.mod +++ b/go.mod @@ -14,13 +14,14 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 - github.com/vishvananda/netlink v1.3.0 + github.com/vishvananda/netlink v1.3.1-0.20250206174618-62fb240731fa golang.org/x/oauth2 v0.28.0 google.golang.org/api v0.226.0 k8s.io/api v0.32.3 k8s.io/apimachinery v0.32.3 k8s.io/client-go v0.32.3 k8s.io/klog/v2 v2.130.1 + k8s.io/kubernetes v1.32.3 sigs.k8s.io/controller-runtime v0.20.3 ) diff --git a/go.sum b/go.sum index ea4a4f6..c1d8e61 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= @@ -170,8 +172,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= -github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netlink v1.3.1-0.20250206174618-62fb240731fa h1:iAhToRwOrdk+pKzclvLM7nKZhsg8f7dVrgkFccDUbUw= +github.com/vishvananda/netlink v1.3.1-0.20250206174618-62fb240731fa/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -286,6 +288,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/kubernetes v1.32.3 h1:2A58BlNME8NwsMawmnM6InYo3Jf35Nw5G79q46kXwoA= +k8s.io/kubernetes v1.32.3/go.mod h1:GvhiBeolvSRzBpFlgM0z/Bbu3Oxs9w3P6XfEgYaMi8k= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.20.3 h1:I6Ln8JfQjHH7JbtCD2HCYHoIzajoRxPNuvhvcDbZgkI= diff --git a/internal/iptables/iptables.go b/internal/iptables/iptables.go new file mode 100644 index 0000000..f65c5eb --- /dev/null +++ b/internal/iptables/iptables.go @@ -0,0 +1,87 @@ +package iptables + +import ( + "fmt" + "net/netip" + "strconv" + + "github.com/matheuscscp/gke-metadata-server/api" + + "k8s.io/kubernetes/pkg/util/iptables" + "k8s.io/utils/exec" +) + +func LoadAndAttach(emulatorAddr netip.Addr, emulatorPort int) func() (func() error, error) { + return func() (func() error, error) { + // Create the following iptables rules: + // iptables -t nat -A OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j DNAT --to-destination + // iptables -A FORWARD -d -p tcp --dport -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT + + // This rule essentially rewrites the destination of packets targeting the + // GKE metadata server with the ip:port address of the emulator, i.e. it + // effectively modifies the destination fields of matching packets. + ipTables := iptables.New(exec.New(), iptables.ProtocolIPv4) + + // match conditions + natTableArgs := []string{ + "-d", api.GKEMetadataServerAddressDefault, + "-p", "tcp", + "--dport", strconv.Itoa(api.GKEMetadataServerPortDefault), + + // action taken + "-j", "DNAT", + "--to-destination", emulatorAddr.String(), + } + + filterTableArgs := []string{ + "-d", emulatorAddr.String(), + "-p", "tcp", + "--dport", strconv.Itoa(emulatorPort), + "-m", "state", "--state", "NEW,ESTABLISHED,RELATED", // new or established connections + // action taken + "-j", "ACCEPT", + } + + close := func() error { + err1 := ipTables.DeleteRule( + iptables.Table(iptables.TableNAT), + iptables.ChainOutput, + natTableArgs..., + ) + err2 := ipTables.DeleteRule( + iptables.Table(iptables.TableFilter), + iptables.ChainForward, + filterTableArgs..., + ) + + if err1 != nil { + return err1 + } + if err2 != nil { + return err2 + } + return nil + } + + _, err := ipTables.EnsureRule( + iptables.Append, + iptables.TableNAT, // NAT rules are applied before routing + iptables.ChainOutput, // output chain is for locally generated traffic + natTableArgs..., + ) + if err != nil { + return nil, fmt.Errorf("error adding DNAT rule: %w", err) + } + + // This rule ensures that packets destined to the emulator IP and port + // are accepted to be forwarded by the host, i.e. prevents the host from + // dropping matching packets. + _, err = ipTables.EnsureRule( + iptables.Append, + iptables.TableFilter, // filter table is for access control (should packets be forwarded or dropped?) + iptables.ChainForward, // forward chain is for packets that are being routed, i.e. not destined to the local host + filterTableArgs..., + ) + return close, err + } +} diff --git a/internal/routing/routing.go b/internal/routing/routing.go index 56ab314..78923e6 100644 --- a/internal/routing/routing.go +++ b/internal/routing/routing.go @@ -27,6 +27,7 @@ import ( "net/netip" "github.com/matheuscscp/gke-metadata-server/api" + "github.com/matheuscscp/gke-metadata-server/internal/iptables" "github.com/matheuscscp/gke-metadata-server/internal/loopback" "github.com/matheuscscp/gke-metadata-server/internal/redirect" @@ -44,6 +45,8 @@ func LoadAndAttach(node *corev1.Node, emulatorIP netip.Addr, emulatorPort int) ( loadAndAttach = redirect.LoadAndAttach(emulatorIP, emulatorPort) case api.RoutingModeLoopback: loadAndAttach = loopback.LoadAndAttach + case api.RoutingModeIPTables: + loadAndAttach = iptables.LoadAndAttach(emulatorIP, emulatorPort) default: return "", nil, fmt.Errorf("invalid routing mode: %s", mode) }