Skip to main content
added 266 characters in body
Source Link
janos
  • 113.1k
  • 15
  • 154
  • 396

(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.)

(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.)

Source Link
janos
  • 113.1k
  • 15
  • 154
  • 396

Port pinger command line tool

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:

  1. telnet is not available in all systems, for example in recent versions of Windows.
  2. 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 using telnet is not so easily scriptable.

To solve these issues, I started a simple command line tool in . 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)
    }
}