blob: 25962bf6efde13aa4cbf45bdfb870adbb6b8c0db [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package nfproxy is a Kubernetes Service IP proxy based exclusively on the Linux nftables interface.
// It uses netfilter's NAT capabilities to accept traffic on service IPs and DNAT it to the respective endpoint.
package nfproxy
import (
"context"
"errors"
"fmt"
"net"
"os"
"time"
"git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
"github.com/sbezverk/nfproxy/pkg/controller"
"github.com/sbezverk/nfproxy/pkg/nftables"
"github.com/sbezverk/nfproxy/pkg/proxy"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
)
type Service struct {
// Traffic in ClusterCIDR is assumed to be originated inside the cluster and will not be SNATed
ClusterCIDR net.IPNet
// A Kubernetes ClientSet with read access to endpoints and services
ClientSet kubernetes.Interface
}
func (s *Service) Run(ctx context.Context) error {
var ipv4ClusterCIDR string
var ipv6ClusterCIDR string
if s.ClusterCIDR.IP.To4() == nil && s.ClusterCIDR.IP.To16() != nil {
ipv6ClusterCIDR = s.ClusterCIDR.String()
} else if s.ClusterCIDR.IP.To4() != nil {
ipv4ClusterCIDR = s.ClusterCIDR.String()
} else {
return errors.New("invalid ClusterCIDR")
}
nfti, err := nftables.InitNFTables(ipv4ClusterCIDR, ipv6ClusterCIDR)
if err != nil {
return fmt.Errorf("failed to initialize nftables with error: %w", err)
}
// Create event recorder to report events into K8s
hostname, err := os.Hostname()
if err != nil {
return fmt.Errorf("failed to get local host name with error: %w", err)
}
eventBroadcaster := record.NewBroadcaster()
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "nfproxy", Host: hostname})
// Create new proxy controller with endpoint slices enabled
// https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/
nfproxy := proxy.NewProxy(nfti, hostname, recorder, true)
// Create special informer which doesn't track headless services
noHeadlessEndpoints, err := labels.NewRequirement(v1.IsHeadlessService, selection.DoesNotExist, nil)
if err != nil {
return fmt.Errorf("failed to create Requirement for noHeadlessEndpoints: %w", err)
}
labelSelector := labels.NewSelector()
labelSelector = labelSelector.Add(*noHeadlessEndpoints)
kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(s.ClientSet, time.Minute*5,
kubeinformers.WithTweakListOptions(func(options *metav1.ListOptions) {
options.LabelSelector = labelSelector.String()
}))
svcController := controller.NewServiceController(nfproxy, s.ClientSet, kubeInformerFactory.Core().V1().Services())
ep := controller.NewEndpointSliceController(nfproxy, s.ClientSet, kubeInformerFactory.Discovery().V1beta1().EndpointSlices())
kubeInformerFactory.Start(ctx.Done())
if err = svcController.Start(ctx.Done()); err != nil {
return fmt.Errorf("error running Service controller: %w", err)
}
if err = ep.Start(ctx.Done()); err != nil {
return fmt.Errorf("error running endpoint controller: %w", err)
}
supervisor.Signal(ctx, supervisor.SignalHealthy)
supervisor.Signal(ctx, supervisor.SignalDone)
return nil
}