Checking if some port is up on the network is a very common task when working with remote services. A common way to check is using telnet
, but I have two practical issues with that:
telnet
is not available in all systems, for example in recent versions of Windows.- When connection is successful with
telnet
, an interactive shell may start, which you have to exit by pressing Control] followed by Controld. It's OK, but not nearly as easy as running a command that simply exits with 0 on success and non-zero on failure. For the same reason, this method usingtelnet
is not so easily scriptable.
(In case you're wondering, I excluded nmap
as an alternative, because it can be used for far more than pinging ports. As far as I know, it's not recommended to have it lying around, unless you're a security expert and it's your job to use it on a daily basis.)
To solve these issues, I started a simple command line tool in go. The source code is on GitHub. I'm still a beginner of this language, I welcome any and all comments about the implementation, testing, project organization, or anything else.
The main module, portping.go
:
package main
import (
"net"
"fmt"
"regexp"
)
var pattern_getsockopt = regexp.MustCompile(`getsockopt: (.*)`)
var pattern_other = regexp.MustCompile(`^dial tcp: (.*)`)
func Ping(host string, port int) error {
addr := fmt.Sprintf("%s:%d", host, port)
conn, err := net.Dial("tcp", addr)
if err == nil {
conn.Close()
}
return err
}
func PingN(host string, port int, count int, c chan error) {
for i := 0; i < count; i++ {
c <- Ping(host, port)
}
}
func FormatResult(err error) string {
if err == nil {
return "success"
}
s := err.Error()
if result := pattern_getsockopt.FindStringSubmatch(s); result != nil {
return result[1]
}
if result := pattern_other.FindStringSubmatch(s); result != nil {
return result[1]
}
return s
}
Unit tests for the main module, portping_test.go
:
package main
import (
"testing"
"fmt"
"net"
"log"
"strings"
)
const testHost = "localhost"
// TODO hopefully unused. Better ideas?
const testPort = 1234
const knownNonexistentHost = "nonexistent.janosgyerik.com"
func acceptN(host string, port int, count int) {
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
log.Fatal(err)
}
defer ln.Close()
for i := 0; i < count; i++ {
conn, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
conn.Close()
}
}
func assertPingResult(host string, port int, t*testing.T, expected bool, pattern string) {
err := Ping(host, port)
addr := fmt.Sprintf("%s:%d", host, port)
log.Printf("port ping %s -> %v", addr, err)
actual := err == nil
if expected != actual {
var openOrClosed string
if expected {
openOrClosed = "open"
} else {
openOrClosed = "closed"
}
t.Errorf("%s:%d should be %s", host, port, openOrClosed)
}
if pattern != "" {
errstr := err.Error()
if !strings.Contains(errstr, pattern) {
t.Errorf("the result was expected to contain %s, but was: %s", pattern, errstr)
}
}
}
func assertPingSuccess(host string, port int, t*testing.T) {
assertPingResult(host, port, t, true, "")
}
func assertPingFailure(host string, port int, t*testing.T, pattern string) {
assertPingResult(host, port, t, false, pattern)
}
func assertPingNSuccessCount(host string, port int, t*testing.T, pingCount int, expectedSuccessCount int) {
c := make(chan error)
go PingN(host, port, pingCount, c)
addr := fmt.Sprintf("%s:%d", host, port)
successCount := 0
for i := 0; i < pingCount; i++ {
err := <-c
log.Printf("port ping %s [%d] -> %v", addr, i + 1, err)
if err == nil {
successCount++
}
}
if expectedSuccessCount != successCount {
t.Errorf("expected %d successful pings, but got only %d", expectedSuccessCount, successCount)
}
}
func Test_ping_open_port(t*testing.T) {
go acceptN(testHost, testPort, 1)
assertPingSuccess(testHost, testPort, t)
// for sanity: acceptN should have shut down already
assertPingFailure(testHost, testPort, t, "connection refused")
}
func Test_ping_unopen_port(t*testing.T) {
assertPingFailure(testHost, testPort, t, "connection refused")
}
func Test_ping_nonexistent_host(t*testing.T) {
assertPingFailure(knownNonexistentHost, testPort, t, "no such host")
}
func Test_ping_negative_port(t*testing.T) {
assertPingFailure(testHost, -1, t, "invalid port")
}
func Test_ping_too_high_port(t*testing.T) {
assertPingFailure(testHost, 123456, t, "invalid port")
}
func Test_ping5_all_success(t*testing.T) {
pingCount := 3
go acceptN(testHost, testPort, pingCount)
assertPingNSuccessCount(testHost, testPort, t, pingCount, pingCount)
}
func Test_ping5_all_fail(t*testing.T) {
pingCount := 5
successCount := 0
assertPingNSuccessCount(testHost, testPort, t, pingCount, successCount)
}
func Test_ping5_partial_success(t*testing.T) {
successCount := 3
go acceptN(testHost, testPort, successCount)
pingCount := 5
assertPingNSuccessCount(testHost, testPort, t, pingCount, successCount)
}
func assertFormatResult(host string, port int, t*testing.T, expected string) {
actual := FormatResult(Ping(host, port))
if expected != actual {
t.Errorf("expected '%s' but got '%s'", expected, actual)
}
}
func Test_format_result_success(t*testing.T) {
go acceptN(testHost, testPort, 1)
assertFormatResult(testHost, testPort, t, "success")
}
func Test_format_result_connection_refused(t*testing.T) {
assertFormatResult(testHost, testPort, t, "connection refused")
}
func Test_format_result_invalid_port_m1(t*testing.T) {
port := -1
assertFormatResult(testHost, port, t, fmt.Sprintf("invalid port %d", port))
}
func Test_format_result_invalid_port_123456(t*testing.T) {
port := 123456
assertFormatResult(testHost, port, t, fmt.Sprintf("invalid port %d", port))
}
func Test_format_result_nonexistent_host(t*testing.T) {
host := knownNonexistentHost
assertFormatResult(host, testPort, t, fmt.Sprintf("lookup %s: no such host", host))
}
The command line interface, main.go
:
package main
import (
"flag"
"fmt"
"os"
"strconv"
)
// TODO
// flags: --tcp, --udp; default is tcp
// flag: -W timeout
// flag: -v verbose; default=false
// drop default count, print forever, until cancel with Control-C, and print stats
const defaultCount = 5
func exit() {
flag.Usage()
os.Exit(1)
}
type Params struct {
host string
port int
count int
}
func parseArgs() Params {
flag.Usage = func() {
fmt.Printf("Usage: %s [options] host port\n\n", os.Args[0])
flag.PrintDefaults()
}
countPtr := flag.Int("c", defaultCount, "stop after count connections")
flag.Parse()
if len(flag.Args()) < 2 {
exit()
}
host := flag.Args()[0]
port, parseErr := strconv.Atoi(flag.Args()[1])
if parseErr != nil {
exit()
}
return Params{
host: host,
port: port,
count: *countPtr,
}
}
func main() {
params := parseArgs()
host := params.host
port := params.port
count := params.count
addr := fmt.Sprintf("%s:%d", host, port)
fmt.Printf("Starting to ping %s ...\n", addr)
c := make(chan error)
go PingN(host, port, count, c)
allSuccessful := true
for i := 0; i < count; i++ {
// TODO add time
err := <-c
if err != nil {
allSuccessful = false
}
fmt.Printf("%s [%d] -> %s\n", addr, i + 1, FormatResult(err))
}
// TODO print summary
// --- host:port ping statistics ---
// n connections attempted, m successful, x% failed
// round-trip min/avg/max/stddev = a/b/c/d ms
if !allSuccessful {
os.Exit(1)
}
}