Last update: 2026-09-20
TCP Port Scanning in Go
package tcputil
import (
"bufio"
"context"
"errors"
"io"
"net"
"os"
"strings"
"syscall"
"time"
)
type PortStatus string
const (
PortStatusUnknown PortStatus = "unknown" // Ex: context was cancelled before handshake could be performed.
PortStatusOpen PortStatus = "open" // Explicitly open (= received SYN-ACK packet).
PortStatusClosed PortStatus = "closed" // Explicitly closed (= received RST packet).
PortStatusHostUnreachable PortStatus = "host unreachable" // No known route to that specific host.
PortStatusNetworkUnreachable PortStatus = "network unreachable" // No route to the entire network/subnet.
PortStatusTimeout PortStatus = "timeout" // No response was received.
)
func DialErrorPortStatus(err error) PortStatus {
switch {
default:
return PortStatusUnknown
case errors.Is(err, syscall.ECONNREFUSED):
return PortStatusClosed
case errors.Is(err, syscall.EHOSTUNREACH):
return PortStatusHostUnreachable
case errors.Is(err, syscall.ENETUNREACH):
return PortStatusNetworkUnreachable
case errors.Is(err, syscall.ETIMEDOUT), errors.Is(err, os.ErrDeadlineExceeded), errors.Is(err, context.DeadlineExceeded):
return PortStatusTimeout
}
}
func ScanPort(ctx context.Context, addr string) (status PortStatus, banner string, err error) {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", addr)
status = DialErrorPortStatus(err)
if err != nil && status != PortStatusUnknown {
return status, "", nil
} else if err != nil {
return status, "", err
}
defer conn.Close()
deadline, _ := ctx.Deadline()
conn.SetReadDeadline(deadline)
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-done:
case <-ctx.Done():
conn.Close()
}
}()
banner, _ = bufio.NewReader(io.LimitReader(conn, 255)).ReadString('\n')
return PortStatusOpen, strings.TrimSpace(banner), nil
}