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.