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 }