k8s.io/kubernetes@v1.29.3/test/images/agnhost/no-snat-test/main.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    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  // CmdNoSnatTest is used by agnhost Cobra.
    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  // ip = target for /whoami query
    51  // rip = returned ip
    52  // pip = this pod's ip
    53  // nip = this node's ip
    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  	// pip is the current pod's IP and nip is the current node's IP
    75  	// pull the pip and nip out of the env
    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  	// validate that pip and nip are ip addresses.
    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  	// register handlers
    94  	http.HandleFunc("/whoami", whoami)
    95  	http.HandleFunc("/checknosnat", mkChecknosnat(pip, nip))
    96  
    97  	// spin up the server
    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  // Builds checknosnat handler, using pod and node ip of current location
   112  func mkChecknosnat(pip string, nip string) handler {
   113  	// Queries /whoami for each provided ip, resp 200 if all resp bodies match this Pod's ip, 500 otherwise
   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  // Writes the req.RemoteAddr into the response, req.RemoteAddr is the address of the incoming connection
   132  func whoami(w http.ResponseWriter, req *http.Request) {
   133  	fmt.Fprintf(w, "%s", req.RemoteAddr)
   134  }
   135  
   136  // Queries ip/whoami and compares response to pip, uses nip to differentiate SNAT from other potential failure modes
   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  }