github.com/jlmeeker/kismatic@v1.10.1-0.20180612190640-57f9005a1f1a/pkg/inspector/check/tcp.go (about)

     1  package check
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"log"
     7  	"net"
     8  	"os/exec"
     9  	"regexp"
    10  	"strings"
    11  	"time"
    12  )
    13  
    14  // TCPPortClientCheck verifies that a given port on a remote node
    15  // is accessible through the network
    16  type TCPPortClientCheck struct {
    17  	// IPAddress is the IP of the remote node
    18  	IPAddress string
    19  	// PortNumber is the target service port
    20  	PortNumber int
    21  	// Timeout is the maximum amount of time the check will
    22  	// wait when connecting to the server before bailing out
    23  	Timeout time.Duration
    24  }
    25  
    26  // Check returns true if the TCP connection is established and the server
    27  // returns the expected response. Otherwise, returns false and an error message
    28  func (c *TCPPortClientCheck) Check() (bool, error) {
    29  	timeout := c.Timeout
    30  	if timeout == 0 {
    31  		timeout = 5 * time.Second
    32  	}
    33  	conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.IPAddress, c.PortNumber), timeout)
    34  	if err != nil {
    35  		return false, fmt.Errorf("Port %d on host %q is unreachable. Error was: %v", c.PortNumber, c.IPAddress, err)
    36  	}
    37  	conn.Close()
    38  	return true, nil
    39  }
    40  
    41  // TCPPortServerCheck ensures that the given port is free, or bound to the right
    42  // process. In the case that it is free, it stands up a TCP server that can be
    43  // used to check TCP connectivity to the host using TCPPortClientCheck
    44  type TCPPortServerCheck struct {
    45  	PortNumber     int
    46  	ProcName       string
    47  	started        bool
    48  	closeListener  func() error
    49  	listenerClosed chan interface{}
    50  }
    51  
    52  // Check returns true if the port is free, or taken by the expected process.
    53  func (c *TCPPortServerCheck) Check() (bool, error) {
    54  	ln, err := net.Listen("tcp", fmt.Sprintf(":%d", c.PortNumber))
    55  	if err != nil && strings.Contains(err.Error(), "address already in use") {
    56  		return portTakenByProc(c.PortNumber, c.ProcName)
    57  	}
    58  	if err != nil {
    59  		return false, fmt.Errorf("error listening on port %d: %v", c.PortNumber, err)
    60  	}
    61  	c.closeListener = ln.Close
    62  	// Setup go routine for accepting connections
    63  	c.listenerClosed = make(chan interface{})
    64  	go func(closed <-chan interface{}) {
    65  		for {
    66  			conn, err := ln.Accept()
    67  			if err != nil {
    68  				select {
    69  				case <-closed:
    70  					// don't log the error, as we have closed the server and the error
    71  					// is related to that.
    72  					return
    73  				default:
    74  					log.Println(fmt.Sprintf("error occurred accepting request: %v", err))
    75  					continue
    76  				}
    77  			}
    78  			// Setup go routine that behaves as an echo server
    79  			go func(c net.Conn) {
    80  				io.Copy(c, c)
    81  				c.Close()
    82  			}(conn)
    83  		}
    84  	}(c.listenerClosed)
    85  	c.started = true
    86  	return true, nil
    87  }
    88  
    89  // Close the TCP server if it was started. Otherwise this is a noop.
    90  func (c *TCPPortServerCheck) Close() error {
    91  	if c.started {
    92  		close(c.listenerClosed)
    93  		return c.closeListener()
    94  	}
    95  	return nil
    96  }
    97  
    98  // Returns true if the port is taken by a process with the given name.
    99  func portTakenByProc(port int, procName string) (bool, error) {
   100  	// Use `ss` (sockstat) to find the process listening on the given port.
   101  	// Sample output:
   102  	// ~# ss -tpln state listening src :6443  | strings
   103  	// Recv-Q Send-Q Local Address:Port               Peer Address:Port
   104  	// 0      128              :::6443                         :::*                   users:(("kube-apiserver",pid=21199,fd=59))
   105  	//
   106  	// ~# ss -tpln state listening src :80  | strings
   107  	// Recv-Q Send-Q Local Address:Port               Peer Address:Port
   108  	// 0      128               *:80                            *:*                   users:(("nginx",pid=30729,fd=10),("nginx",pid=30728,fd=10),("nginx",pid=30721,fd=10))
   109  	//
   110  	cmd := exec.Command("ss", "-tpln", "state", "listening", "src", fmt.Sprintf(":%d", port))
   111  	out, err := cmd.Output()
   112  	if err != nil {
   113  		return false, fmt.Errorf("error running ss: %v", err)
   114  	}
   115  	lines := strings.Split(string(out), "\n")
   116  	if len(lines) < 2 {
   117  		return false, fmt.Errorf("expected ss to return at least 2 lines, but returned %d", len(lines))
   118  	}
   119  	boundProc, err := getProcNameFromTCPSockStatLine(lines[1])
   120  	if err != nil {
   121  		return false, err
   122  	}
   123  	return boundProc == procName, nil
   124  }
   125  
   126  // given an entry returned by sockstat (ss), return the name of the process using the port.
   127  // assumes ss was run with flags: -tpln
   128  func getProcNameFromTCPSockStatLine(line string) (string, error) {
   129  	ssFields := strings.Fields(line)
   130  	// The fifth field includes information about the process using the port
   131  	if len(ssFields) != 5 {
   132  		return "", fmt.Errorf("unexpected output returned from ss command. output was: %s", line)
   133  	}
   134  	// users:(("nginx",pid=30729,fd=10),("nginx",pid=30728,fd=10),("nginx",pid=30721,fd=10))
   135  	usersField := ssFields[4]
   136  
   137  	// This regular expression contains a single capturing group that will fish
   138  	// out the process name from the `ss` output. In the case that `ss` returns
   139  	// a list with multiple users, the left-most user will be matched.
   140  	re := regexp.MustCompile(`^users:\(\("([^"]+)",pid=\d+,fd=\d+\)`)
   141  	matched := re.FindSubmatch([]byte(usersField))
   142  	if len(matched) < 2 {
   143  		return "", fmt.Errorf("unable to determine the process from ss line %q", usersField)
   144  	}
   145  	// We are interested in the subexpression (capturing group). The first item
   146  	// in the matched list is the match of the full regexp, not the capturing
   147  	// group.
   148  	return string(matched[1]), nil
   149  }