blob: 38a68d9a5d2c2e5c01a755fbfc6fe4bdced9c504 [file] [log] [blame]
Serge Bazanski6b649f82021-05-24 15:09:25 +02001// kubenat implements a data source for undoing NAT on hosts running
2// Kubernetes/containerd workloads.
3//
4// It parses the kernel conntrack NAT translation table to figure out the IP
5// address of the pod that was making the connection.
6//
7// It then uses the containerd API to figure out what pod runs under what IP
8// address.
9//
10// Both conntrack and containerd access is cached and only updated when needed.
11// This means that as long as a TCP connection is open, identd will be able to
12// respond about its information without having to perform any OS/containerd
13// queries.
14//
15// Unfortunately, there is very little in terms of development/test harnesses
16// for kubenat. You will have to have a locally running containerd, or do some
17// mounts/forwards from a remote host.
18package kubenat
19
20import (
21 "context"
22 "errors"
23 "fmt"
24 "net"
25 "time"
26
27 "github.com/cenkalti/backoff"
28 "github.com/golang/glog"
29)
30
31// Resolver is the main interface for kubenat. It runs background processing to
32// update conntrack/containerd state, and resolves Tuple4s into PodInfo.
33type Resolver struct {
34 conntrackPath string
35 criPath string
36
37 translationC chan *translationReq
38 podInfoC chan *podInfoReq
39}
40
41// Tuple4 is a 4-tuple of a TCP connection. Local describes the machine running
42// this code, not the listen/connect 'ends' of TCP.
43type Tuple4 struct {
44 RemoteIP net.IP
45 RemotePort uint16
46 LocalIP net.IP
47 LocalPort uint16
48}
49
50func (t *Tuple4) String() string {
51 local := net.JoinHostPort(t.LocalIP.String(), fmt.Sprintf("%d", t.LocalPort))
52 remote := net.JoinHostPort(t.RemoteIP.String(), fmt.Sprintf("%d", t.RemotePort))
53 return fmt.Sprintf("L: %s R: %s", local, remote)
54}
55
56// PodInfo describes a Kubernetes pod which terminates a given Tuple4 connection.
57type PodInfo struct {
58 // PodIP is the IP address of the pod within the pod network.
59 PodIP net.IP
60 // PodTranslatedPort is the port on the PodIP corresponding to the Tuple4
61 // that this PodInfo was requested for.
62 PodTranslatedPort uint16
63 // KubernetesNamespace is the kubernetes namespace in which this pod is
64 // running.
65 KubernetesNamespace string
66 // Name is the name of the pod, as seen by kubernetes.
67 Name string
68}
69
70// NewResolver startss a resolver with a given path to /paroc/net/nf_conntrack
71// and a CRI gRPC domain socket.
72func NewResolver(ctx context.Context, conntrackPath, criPath string) (*Resolver, error) {
73 r := Resolver{
74 conntrackPath: conntrackPath,
75 criPath: criPath,
76
77 translationC: make(chan *translationReq),
78 podInfoC: make(chan *podInfoReq),
79 }
80 // TODO(q3k): bubble up errors from the translation worker into here?
81 go r.runTranslationWorker(ctx)
82 // The pod worker might fail on CRI connectivity issues, so we attempt to
83 // restart it with a backoff if needed.
84 go func() {
85 bo := backoff.NewExponentialBackOff()
86 bo.MaxElapsedTime = 0
87 bo.Reset()
88 for {
89 err := r.runPodWorker(ctx)
90 if err == nil || errors.Is(err, ctx.Err()) {
91 glog.Infof("podWorker exiting")
92 return
93 }
94 glog.Errorf("podWorker failed: %v", err)
95 wait := bo.NextBackOff()
96 glog.Errorf("restarting podWorker in %v", wait)
97 time.Sleep(wait)
98 }
99 }()
100
101 return &r, nil
102}
103
104// ResolvePod returns information about a running pod for a given TCP 4-tuple.
105// If the 4-tuple or pod cannot be resolved, an error will be returned.
106func (r *Resolver) ResolvePod(ctx context.Context, t *Tuple4) (*PodInfo, error) {
107 // TODO(q3k): expose translation/pod not found errors as package-level
108 // vars, or use gRPC statuses?
109 podAddr, err := r.translate(ctx, t)
110 if err != nil {
111 return nil, fmt.Errorf("translate: %w", err)
112 }
113 if podAddr == nil {
114 return nil, fmt.Errorf("translation not found")
115 }
116 podInfo, err := r.getPodInfo(ctx, podAddr.localIP)
117 if err != nil {
118 return nil, fmt.Errorf("getPodInfo: %w", err)
119 }
120 if podInfo == nil {
121 return nil, fmt.Errorf("pod not found")
122 }
123
124 return &PodInfo{
125 PodIP: podAddr.localIP,
126 PodTranslatedPort: podAddr.localPort,
127 KubernetesNamespace: podInfo.namespace,
128 Name: podInfo.name,
129 }, nil
130}