Construindo um Port Scanner com Golang Link para o cabeçalho
Esses dias eu quis fazer um projetinho com go, então visitei a programming challenges e busquei por algo, o port scanner chamou minha atenção então decidir pegar ele.
Disclaimer Link para o cabeçalho
O que vamos montar aqui não pode ser usado em servidores nos quais você não possui permissão para executar. Leia essa página para entender melhor
Versão Inicial Link para o cabeçalho
Então primeiro vamos definir uma versão chamada PortScan
que irá receber um parâmetro servidor
o qual será o servidor que iremos escanear e retornar uma lista com as portas disponÃveis no mesmo.
Um número de porta é um inteiro do tipo 16-bit unsigned, portanto seu range é de 0 a 65535, porém 0 é uma porta reservada e não pode ser usada, com isso já sabemos qual a quantidade de portas que teremos de verificar. Nós também precisamos pensar em como vamos verificar se uma porta está disponÃvel, para isso usaremos o pacote net
, ele nos fornece a função Dial
, a qual podemos usar para testar a conexão, mas uma maneira melhor seria usar a DialTimeout
pois nos dá a possibilidade de configurar um timeout para a conexão. Sem mais delongas vamos definir nossa PortScan()
func PortScan(server string) []int {
var available []int
for i := 1; i <= 65535; i++ {
ip := server + ":" + strconv.Itoa(i)
_, err := net.DialTimeout("tcp", ip, time.Duration(300)*time.Millisecond)
if err == nil {
available = append(available, i)
}
}
return available
}
Para esse código funcionar precisamos importar os seguintes pacotes:
net
: O pacote com a função DialTimeout
strconv
: Para converter inteiros para strings e montar a nossa variável com o servidor
time
: Para criar o parâmetro *time.Second
os
: Para recuperar os argumentos da linha de comando (Veremos onde mais a frente)
fmt
: Para exibir os resultados na linha de comando
Agora para nossa função principal
func main() {
fmt.Println("Cheking for available ports...")
ports := PortScan(os.Args[1])
fmt.Println("Ports available: " ,ports)
}
Agora que já temos nosso programa, vamos querer usá-lo, para usar isso você só precisa rodar go run main.go <servidor>
onde o servidor é o endereço de IP que vc quer checar. Vamos usar o comando time
para verificar o tempo que leva para ser executado:
┌─[nivaldogmelo@yggdrasil] - [/port-scanner]
└─[$] time go run port-scanner-no-goroutines.go localhost
Checking for available ports...
Ports available: [4000 40031 42987 57621]
real 20.40s
user 8.74s
sys 12.14s
Observe que elvou um tempo razoável, isso se deve ao fato de que estamos checando uma porta de cada vez, vamos tentar acelerar a execução checando múltiplas portas ao mesmo tempo
Usando Goroutines Link para o cabeçalho
Para aumentar a velocidade do nosso teste podemos checar várias portas ao mesmo tempo. Para isso nós podemos usar go routines, o que são similares a threads em linguagens como Java. Se você não sabe o que uma goroutine é recomendo a leitura do Golang Bot (em inglês) (seções 20-23) para ter uma ideia do que vamos lidar.
A primeira coisa que vamos fazer é importar o pacote sync
para lidar com as goroutines. Agora vamos fazer algumas mudanças na estrutura do código. Primeiro vamos fazer da variável available
uma variável global, pois será manipulada por múltiplas rotinas. Segundo é criar uma struct para definir o job que será executado.
type Job struct {
server string
port int
}
var available []int
var jobs = make(chan Job, 10)
A variável jobs
mantem um canal bufferizado, que é um cnal que vai manter registro do buffer dos jobs que serão executados, nós definimos um canal de tamanho 10, o que significa que pode executar um total de 10 jobs ao mesmo tempo, qualquer outra execução será bloqueada até que os jobs em andamento sejam finalizados.
Agora vamos definir nossa função createWorkerPool()
, a qual irá criar nossos workers para executar os jobs. Basicamente é aqui onde definiremos quantos jobs concorrentes queremos executar.
Para iniciar uma nova goroutine só precisamos executar nossa função com um go
vindo antes. Nós usamos o wg.Add(1)
para adicionar uma nova rotina para executar nossos jobs. Ao final o wg.Wait()
é necessário para que nossa rotina principal espere pelas subsequentes serem finalizadas antes de ir para o próximo passo. O go worker(&wg)
precisa usar um ponteiro de forma que use o mesmo WaitGroup
criado pela função, caso contrário sempre iria iniciar um novo e as tarefas seriam executadas em outras goroutines e nosso grupo de espera original (wg) nunca seria finalizado
func createWorkerPool(noOfWorkers int) {
var wg sync.WaitGroup
for i := 0; i < noOfWorkers; i++ {
wg.Add(1)
go worker(&wg)
}
wg.Wait()
}
Agora vamos criar a função worker()
para executar nosso job.
func worker(wg *sync.WaitGroup) {
for job := range jobs{
ip := job.server + ":" + strconv.Itoa(job.port)
_, err := net.DialTimeout("tcp", ip, time.Duration(300)*time.Millisecond)
if err == nil {
available = append(available, job.port)
}
}
wg.Done()
}
Now we handle the ip assembly in the worker function, with parameters that we’ll receive from the jobs
buffer, composed by variables with the Job
struct type, which contains a server and a
port. The net.DialTimeout will be executed as in our previous PortScan()
and to finish the function we need to pass a wg.Done()
, indicating the goroutine that the task is completed.
As we introduced these changes, we’ll have to change our original PortScan()
. Now the function will be called with an additional parameter, a channel which will be written once all the jobs
are executed. For each port we’ll build a Job
type variable and send it to our jobs list. At the end we’ll close the jobs channel since all jobs have been assigned and no other job will be
written to the channel. At the end we pass the true
value to the done
channel, to indicate we’ve finished the execution of all our goroutines.
func PortScan(done chan bool, server string) {
for i := 1; i <= 65535; i++ {
job := Job{server, i}
jobs <- job
}
close(jobs)
done <- true
}
To end our implementation, we make the needed change at our main()
func main() {
fmt.Println("Cheking for available ports...")
done := make(chan bool)
go PortScan(done, os.Args[1])
noOfWorkers := 10
createWorkerPool(noOfWorkers)
<-done
fmt.Println("Ports available: " , available)
}
First we create our done
channel, then we create a goroutine to run our PortScan
getting the parameter that we’ll pass at the command line. Then we set a number of workers and
create a worker pool of this size, for the sake of this demo we’ll go with 100 workers. Then we wait for the done
channel to return a true
value. So we only need to need to print
the available ports.
Ok so we’ve done a few changes on our code, but what have we achieved with this, so let’s use our time
command to measure the performance:
┌─[nivaldogmelo@yggdrasil] - [/port-scanner]
└─[$] time go run port-scanner-goroutines.go localhost
Checking for available ports...
Ports available: [4000 40031 42987 57621]
real 4.37s
user 4.93s
sys 5.21s
Now we’ve reached a smaller time
Final considerations Link para o cabeçalho
When hiting a remote server be careful with the number of workers used, because some routers limit the number of concurrent threads, so some ports will be skipped
And that’s it, i hope you guys enjoyed, if you have any questions you can send me an email or reach me through any of my social media accounts