1
16
17 package nosnat
18
19 import (
20 "fmt"
21 "io"
22 "net/http"
23 "os"
24 "strings"
25
26 "github.com/spf13/cobra"
27 "k8s.io/component-base/logs"
28 netutils "k8s.io/utils/net"
29 )
30
31
32 var CmdNoSnatTest = &cobra.Command{
33 Use: "no-snat-test",
34 Short: "Creates the /checknosnat and /whoami endpoints",
35 Long: `Serves the following endpoints on the given port (defaults to "8080").
36
37 - /whoami - returns the request's IP address.
38 - /checknosnat - queries "ip/whoami" for each provided IP ("/checknosnat?ips=ip1,ip2"),
39 and if all the response bodies match the "POD_IP" environment variable, it will return a 200 response, 500 otherwise.`,
40 Args: cobra.MaximumNArgs(0),
41 Run: main,
42 }
43
44 var port string
45
46 func init() {
47 CmdNoSnatTest.Flags().StringVar(&port, "port", "8080", "The port to serve /checknosnat and /whoami endpoints on.")
48 }
49
50
51
52
53
54
55 type masqTester struct {
56 Port string
57 }
58
59 func main(cmd *cobra.Command, args []string) {
60 m := &masqTester{
61 Port: port,
62 }
63
64 logs.InitLogs()
65 defer logs.FlushLogs()
66
67 if err := m.Run(); err != nil {
68 fmt.Fprintf(os.Stderr, "%v\n", err)
69 os.Exit(1)
70 }
71 }
72
73 func (m *masqTester) Run() error {
74
75
76 pip, ok := os.LookupEnv("POD_IP")
77 if !ok {
78 return fmt.Errorf("POD_IP env var was not present in the environment")
79 }
80 nip, ok := os.LookupEnv("NODE_IP")
81 if !ok {
82 return fmt.Errorf("NODE_IP env var was not present in the environment")
83 }
84
85
86 if netutils.ParseIPSloppy(pip) == nil {
87 return fmt.Errorf("POD_IP env var contained %q, which is not an IP address", pip)
88 }
89 if netutils.ParseIPSloppy(nip) == nil {
90 return fmt.Errorf("NODE_IP env var contained %q, which is not an IP address", nip)
91 }
92
93
94 http.HandleFunc("/whoami", whoami)
95 http.HandleFunc("/checknosnat", mkChecknosnat(pip, nip))
96
97
98 return http.ListenAndServe(":"+m.Port, nil)
99 }
100
101 type handler func(http.ResponseWriter, *http.Request)
102
103 func joinErrors(errs []error, sep string) string {
104 strs := make([]string, len(errs))
105 for i, err := range errs {
106 strs[i] = err.Error()
107 }
108 return strings.Join(strs, sep)
109 }
110
111
112 func mkChecknosnat(pip string, nip string) handler {
113
114 return func(w http.ResponseWriter, req *http.Request) {
115 errs := []error{}
116 ips := strings.Split(req.URL.Query().Get("ips"), ",")
117 for _, ip := range ips {
118 if err := check(ip, pip, nip); err != nil {
119 errs = append(errs, err)
120 }
121 }
122 if len(errs) > 0 {
123 w.WriteHeader(500)
124 fmt.Fprintf(w, "%s", joinErrors(errs, ", "))
125 return
126 }
127 w.WriteHeader(200)
128 }
129 }
130
131
132 func whoami(w http.ResponseWriter, req *http.Request) {
133 fmt.Fprintf(w, "%s", req.RemoteAddr)
134 }
135
136
137 func check(ip string, pip string, nip string) error {
138 url := fmt.Sprintf("http://%s/whoami", ip)
139 resp, err := http.Get(url)
140 if err != nil {
141 return err
142 }
143 defer resp.Body.Close()
144 body, err := io.ReadAll(resp.Body)
145 if err != nil {
146 return err
147 }
148 rips := strings.Split(string(body), ":")
149 if rips == nil || len(rips) == 0 {
150 return fmt.Errorf("Invalid returned ip %q from %q", string(body), url)
151 }
152 rip := rips[0]
153 if rip != pip {
154 if rip == nip {
155 return fmt.Errorf("Returned ip %q != my Pod ip %q, == my Node ip %q - SNAT", rip, pip, nip)
156 }
157 return fmt.Errorf("Returned ip %q != my Pod ip %q or my Node ip %q - SNAT to unexpected ip (possible SNAT through unexpected interface on the way into another node)", rip, pip, nip)
158 }
159 return nil
160 }
161
View as plain text