Párhuzamos port scanner: portspy

A werkstatt sorozat ötödik része – egy port scanner ami goroutine-okkal ezernyi portot vizsgál párhuzamosan.

Párhuzamos port scanner: portspy

A portspy a netmapper testvére – de míg a netmapper egy teljes subnetet térképez fel, a portspy egyetlen hostra koncentrál. Cserébe akár 65535 portot is végigscannelhet, és minden nyitott porthoz megmutatja a válaszidőt.

Az ötlet onnan jött, hogy egy szerver auditnál tudni akartam pontosan milyen portok vannak nyitva. Az nmap természetesen megcsinálja, de a portspy annyira egyszerű hogy tíz perc alatt megírtam – és közben a Go párhuzamosítási modelljét gyakoroltam.

Goroutine-ok ezerrel

A portspy lényege egyetlen ciklus:

for port := start; port <= end; port++ {
    wg.Add(1)
    go func(p int) {
        defer wg.Done()
        results <- probePort(host, p, timeout)
    }(port)
}

Ha a tartomány 1-1024, akkor 1024 goroutine indul el egyszerre. Mindegyik egy TCP kapcsolatot próbál nyitni, és az eredményt egy channel-be küldi. A Go runtime elosztja ezeket az operációs rendszer száljaira – neked nem kell thread pool-lal foglalkoznod.

Az egész scan másodpercek alatt lefut, szemben a szekvenciális megoldással ami portonként kivárja a timeout-ot.

A probePort függvény

Minden port vizsgálat egy önálló függvény:

func probePort(host string, port int, timeout time.Duration) Result {
    addr := fmt.Sprintf("%s:%d", host, port)
    start := time.Now()

    conn, err := net.DialTimeout("tcp", addr, timeout)
    dur := time.Since(start)

    if err != nil {
        return Result{Port: port, Open: false, Duration: dur}
    }
    conn.Close()
    return Result{Port: port, Open: true, Duration: dur}
}

A net.DialTimeout a Go standard library része – egy TCP kapcsolatot próbál nyitni adott időlimiten belül. Ha sikerül, a port nyitott. Ha nem, zárt vagy szűrt. A válaszidőt is mérem, mert néha az is informatív – egy 500ms-es válasz más mint egy 2ms-es.

Channel mint eredménygyűjtő

A goroutine-ok nem egy közös slice-ba írnak (az mutex-et igényelne), hanem egy channel-be küldik az eredményt:

results := make(chan Result, end-start+1)

A buffered channel fontos – a méret megegyezik a portok számával, így egyetlen goroutine sem blokkolódik íráskor. A főszál a for r := range results ciklusban gyűjti össze az eredményeket miután a channel bezárul.

Ez tisztább mint a netmapper mutex-es megoldása, és jól mutatja a Go filozófiát: “ne közös memórián kommunikálj, hanem kommunikáción osztozz”.

Tesztelés helyi listener-ekkel

A portspy tesztjei ugyanazt a mintát használják mint a netmapper: ideiglenes TCP listener-eket indítanak a localhost-on:

func startListener(t *testing.T) (int, func()) {
    ln, _ := net.Listen("tcp", "127.0.0.1:0")
    // accept loop háttérben...
    return port, func() { ln.Close() }
}

A :0 port a rendszertől kér egy szabad portot. A teszt tudja hogy pontosan ez az egy port nyitott, és ellenőrzi hogy a scanner megtalálja-e. Több listener-t indítva a rendezettséget is teszteljük – az eredmények mindig port szám szerint növekvő sorrendben jönnek.

Mi a különbség a netmapper-hez képest?

A netmapper két fázisú: először hostokat keres, utána portokat vizsgál. A portspy egyfázisú: egy hostot kap, és azon scanneli a portokat.

A netmapper-ben mutex védi a közös adatstruktúrát. A portspy-ban channel gyűjti az eredményeket. Mindkét megközelítés helyes – a mutex akkor jó ha egy meglévő struktúrát módosítasz, a channel akkor ha eredményeket továbbítasz.

A portspy emellett válaszidőt is mér, ami a netmapper-ben nem volt. Ez hasznos ha nem csak azt akarod tudni hogy nyitott-e a port, hanem azt is hogy milyen gyorsan válaszol.

Mit tanultam?

A portspy megerősítette amit a netmapper-ben tanultam, és egy fontos árnyalatot adott hozzá: a channel-ek és a mutex-ek közötti választást. Ha eredményeket gyűjtesz, a channel tisztább. Ha közös állapotot módosítasz, a mutex egyszerűbb.

A másik tanulság a time.Since használata – válaszidő mérés két sor Go-ban. Bash-ben ehhez date +%s%N trükkök kellenek.

Hogyan próbáld ki?

git clone https://github.com/brtkcs/werkstatt-tools.git
cd werkstatt-tools/portspy
go run main.go -host 10.0.0.1

Vagy a teljes tartomány:

go build -o portspy
./portspy -host 192.168.1.1 -start 1 -end 65535 -timeout 2

A teljes forráskód a werkstatt-tools repóban.

Ez a werkstatt sorozat ötödik része. A netmapper-ben subnet discovery-t és port scannelést kombináltunk. A portspy egyszerűbb és fókuszáltabb – egyetlen host, széles porttartomány, válaszidő méréssel.