Diagnóstico de rede
Ferramentas de diagnóstico de rede são programas usados para verificar diferentes aspectos da comunicação entre dispositivos e serviços. Elas permitem testar se uma conexão pode ser estabelecida, medir quanto tempo uma operação demora, verificar a resposta de um serviço e investigar possíveis problemas no caminho percorrido pelos pacotes. Em Go, podemos desenvolver essas ferramentas usando principalmente o pacote net e outros recursos da biblioteca padrão.
Cada teste fornece informações sobre uma parte específica da comunicação. Conseguir estabelecer uma conexão TCP indica que o destino aceitou aquela conexão, mas não garante que o serviço esteja funcionando corretamente. Da mesma maneira, enviar um datagrama UDP sem receber um erro não confirma que o pacote chegou ao servidor, porque o protocolo não fornece uma confirmação automática de entrega.
Para interpretar corretamente os resultados, precisamos entender o comportamento de cada protocolo e os limites das operações realizadas. Também precisamos definir timeouts, tratar erros e controlar a quantidade de conexões simultâneas, evitando que a ferramenta permaneça bloqueada ou consuma recursos excessivamente. Nos exemplos, vamos começar com testes locais e avançar para ferramentas que permitem diagnosticar outros destinos.
Testando se uma porta TCP está aberta
Uma porta TCP é um identificador numérico usado pelo sistema operacional para encaminhar conexões aos serviços que estão escutando na máquina. Quando um programa cria um servidor TCP, ele associa um socket a determinado endereço e porta, permitindo que outros processos tentem estabelecer conexões. Podemos testar essa comunicação tentando nos conectar ao endereço e à porta desejados.
Esse procedimento é conhecido como TCP connect scan, quando aplicado à verificação de portas, porque usa uma tentativa normal de conexão TCP. Se a conexão for estabelecida, podemos afirmar que o destino aceitou aquela conexão naquele momento. Entretanto, isso não significa que o serviço tenha respondido corretamente a um protocolo de aplicação, como HTTP ou SMTP.
Se a conexão falhar, precisamos examinar o erro retornado antes de interpretar o resultado. Uma conexão recusada pode indicar que não existe um serviço aceitando conexões naquele endereço e porta, enquanto um timeout pode estar relacionado a filtros de rede, problemas de conectividade ou ausência de resposta. Por isso, uma ferramenta de diagnóstico precisa apresentar o resultado sem transformar qualquer falha em uma conclusão definitiva sobre a porta.
Testando uma conexão com net.DialTimeout
O pacote net fornece a função net.DialTimeout, que permite tentar estabelecer uma conexão com determinado endereço e protocolo, definindo um limite máximo de tempo. Ela recebe o tipo de rede, o endereço de destino e uma duração, retornando um objeto net.Conn quando a conexão é estabelecida ou um erro quando a operação falha.
Podemos criar um programa que tenta estabelecer uma conexão TCP com a porta 8080 da própria máquina. Esse primeiro exemplo apenas verifica o resultado da conexão, sem enviar dados para o serviço que estiver escutando.
package main
import (
"fmt"
"net"
"time"
)
func main() {
// Define o endereço e a porta que serão testados.
destino := "127.0.0.1:8080"
// Tenta estabelecer uma conexão TCP em até dois segundos.
conn, err := net.DialTimeout(
"tcp",
destino,
2*time.Second,
)
if err != nil {
fmt.Println("Falha na conexão:", err)
return
}
// Fecha a conexão após confirmar o estabelecimento.
defer conn.Close()
fmt.Println("Conexão TCP estabelecida:", destino)
}
A função net.DialTimeout recebe "tcp" para indicar o protocolo usado na comunicação. O endereço 127.0.0.1:8080 representa a máquina local e a porta que será testada, enquanto 2*time.Second define um limite de dois segundos para a tentativa. Quando a operação é concluída com sucesso, recebemos uma conexão que precisa ser fechada quando não for mais necessária.
Se existir um serviço TCP aceitando conexões na porta informada, o programa apresentará uma mensagem semelhante a esta:
Conexão TCP estabelecida: 127.0.0.1:8080
Se não existir um serviço aceitando a conexão, o programa poderá receber um erro indicando que a tentativa foi recusada. Entretanto, a mensagem exata depende do sistema operacional e da condição encontrada, por isso não devemos depender exclusivamente do texto do erro para identificar o problema.
O resultado positivo confirma somente que a conexão TCP foi estabelecida. Um servidor HTTP, por exemplo, pode aceitar a conexão e ainda retornar um erro quando receber uma requisição inválida. Para verificar se o serviço realmente executa sua função, precisamos enviar dados seguindo o protocolo esperado e interpretar a resposta recebida.
Criando uma ferramenta TCP com flags
Quando desenvolvemos uma ferramenta CLI, normalmente queremos informar o destino diretamente pelo terminal. Podemos usar o pacote flag para receber o endereço, a porta e o timeout, permitindo executar diferentes testes sem modificar o código. Também precisamos validar os argumentos antes de estabelecer a conexão, evitando trabalhar com portas inválidas ou tempos de espera inadequados.
Vamos criar uma ferramenta chamada tcpcheck, que recebe essas configurações e tenta estabelecer uma conexão. O programa usará net.JoinHostPort para montar corretamente o endereço de destino, permitindo trabalhar com nomes de domínio e endereços IPv4 ou IPv6 sem precisar concatenar manualmente a porta.
package main
import (
"flag"
"fmt"
"net"
"os"
"strconv"
"time"
)
func main() {
// Define os parâmetros da ferramenta.
host := flag.String(
"host",
"127.0.0.1",
"Endereço que será testado",
)
porta := flag.Int(
"port",
8080,
"Porta TCP",
)
timeout := flag.Duration(
"timeout",
2*time.Second,
"Tempo máximo da conexão",
)
flag.Parse()
// Verifica os parâmetros recebidos.
if *host == "" || *porta < 1 || *porta > 65535 {
fmt.Fprintln(
os.Stderr,
"Erro: host ou porta inválidos.",
)
os.Exit(2)
}
if *timeout <= 0 {
fmt.Fprintln(
os.Stderr,
"Erro: timeout deve ser maior que zero.",
)
os.Exit(2)
}
// Monta o endereço respeitando IPv4 e IPv6.
destino := net.JoinHostPort(
*host,
strconv.Itoa(*porta),
)
// Tenta estabelecer a conexão TCP.
conn, err := net.DialTimeout(
"tcp",
destino,
*timeout,
)
if err != nil {
fmt.Fprintln(
os.Stderr,
"Falha na conexão:",
err,
)
os.Exit(1)
}
// Fecha a conexão após o teste.
defer conn.Close()
fmt.Println("Conexão estabelecida:", destino)
}
O programa recebe o endereço pela flag -host, a porta por -port e o limite de tempo por -timeout. Depois de interpretar os argumentos, verifica se os valores são adequados para a operação. A função strconv.Itoa converte a porta numérica para string, enquanto net.JoinHostPort monta o endereço completo no formato esperado pelo pacote net.
Podemos executar a ferramenta com os valores padrão:
go run main.go
Também podemos informar outro endereço, outra porta e um timeout diferente:
go run main.go -host example.com -port 443 -timeout 3s
Nesse caso, a ferramenta tentará estabelecer uma conexão TCP com example.com na porta 443, aguardando no máximo três segundos. Se a conexão for aceita, o programa apresentará uma mensagem de sucesso. Caso contrário, retornará um erro que poderá representar diferentes condições, como conexão recusada, falha de resolução ou tempo limite excedido.
Quando o host representa um domínio, o tempo da tentativa também pode incluir operações necessárias para resolver o nome e obter endereços IP. Portanto, o timeout não representa exclusivamente o período gasto na comunicação TCP. Se quisermos medir apenas a tentativa para um endereço já conhecido, podemos informar diretamente um IP e eliminar a etapa de resolução do domínio.
Interpretando erros de conexão TCP
Uma tentativa de conexão pode falhar por motivos diferentes, e o pacote net permite examinar essas condições por meio dos erros retornados. O tipo net.OpError, por exemplo, contém informações sobre a operação que falhou, a rede usada e os endereços envolvidos. Podemos usar errors.As para identificar esse tipo, mesmo quando o erro está encapsulado por outras estruturas.
Também podemos reconhecer alguns erros específicos usando errors.Is, como syscall.ECONNREFUSED, que representa uma condição de conexão recusada quando o sistema operacional fornece esse erro. Entretanto, não devemos assumir que todos os sistemas retornam exatamente os mesmos códigos em todas as situações. O pacote pode encapsular erros diferentes dependendo da plataforma e do tipo de operação.
func interpretarErro(err error) {
if err == nil {
return
}
// Verifica se a conexão foi recusada.
if errors.Is(err, syscall.ECONNREFUSED) {
fmt.Println("Conexão recusada.")
return
}
// Verifica se o erro representa um timeout.
var erroRede net.Error
if errors.As(err, &erroRede) && erroRede.Timeout() {
fmt.Println("Tempo limite excedido.")
return
}
// Apresenta outros erros encontrados.
fmt.Println("Erro de conexão:", err)
}
Esse trecho pressupõe que os pacotes errors, fmt, net e syscall foram importados. A função verifica primeiro se o erro corresponde a uma conexão recusada e depois consulta a interface net.Error, que possui o método Timeout. Quando nenhuma condição conhecida é encontrada, o erro original é apresentado para permitir uma análise mais detalhada.
Uma conexão recusada costuma indicar que a tentativa chegou a um ponto capaz de rejeitá-la, mas isso não comprova necessariamente que não existe nenhum serviço relacionado à porta. Dispositivos intermediários também podem rejeitar conexões, dependendo de suas configurações. Da mesma maneira, um timeout não comprova que a porta está filtrada, porque também pode ocorrer por problemas de conectividade ou ausência de resposta.
Para ferramentas de diagnóstico, normalmente é melhor apresentar o resultado como conexão estabelecida, conexão recusada ou tempo limite excedido, mantendo a interpretação próxima do que realmente foi observado. Classificações como aberta, fechada ou filtrada exigem cuidado, porque representam conclusões sobre o comportamento da rede e não apenas o retorno de uma função.
Testando envio e resposta via UDP
UDP é um protocolo de transporte orientado a datagramas, que permite enviar mensagens sem estabelecer uma conexão antes da transmissão. Diferentemente do TCP, ele não possui um mecanismo automático de confirmação de entrega ou retransmissão. Isso significa que enviar um datagrama normalmente não permite determinar se existe um serviço escutando na porta de destino.
Para realizar um teste UDP mais útil, precisamos enviar uma mensagem que o serviço remoto reconheça e aguardar uma resposta. Essa comunicação depende do protocolo de aplicação, porque não existe uma mensagem universal que todos os serviços UDP sejam obrigados a responder. Um servidor DNS, por exemplo, espera uma mensagem DNS válida, enquanto um servidor próprio pode trabalhar com comandos de texto.
Vamos começar criando um servidor UDP simples, responsável por receber a mensagem PING e responder com PONG. Depois, desenvolveremos um cliente que envia essa mensagem e aguarda a resposta dentro de um intervalo definido. Isso permitirá verificar a comunicação nos dois sentidos usando um protocolo de teste conhecido.
Criando um servidor UDP para teste
O pacote net fornece a função net.ListenPacket, que permite criar um socket para receber datagramas. Esse socket pode receber mensagens de diferentes clientes, informando o endereço de origem de cada datagrama. Podemos usar o método ReadFrom para receber os bytes e WriteTo para responder ao remetente.
Vamos criar um servidor que escuta na porta 9001 da interface local. Quando receber exatamente a mensagem PING, ele responderá com PONG. Para outras mensagens, devolverá COMANDO_INVALIDO, permitindo observar como a resposta depende do conteúdo enviado pelo cliente.
package main
import (
"fmt"
"net"
"os"
)
func main() {
// Cria um socket UDP na interface local.
conn, err := net.ListenPacket(
"udp4",
"127.0.0.1:9001",
)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
defer conn.Close()
fmt.Println("Servidor UDP:", conn.LocalAddr())
// Reserva espaço para receber datagramas.
buffer := make([]byte, 2048)
for {
// Recebe o datagrama e identifica sua origem.
n, origem, err := conn.ReadFrom(buffer)
if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao receber:",
err,
)
return
}
// Consulta somente os bytes recebidos.
mensagem := string(buffer[:n])
fmt.Printf(
"%s enviou: %q\n",
origem,
mensagem,
)
resposta := "COMANDO_INVALIDO"
if mensagem == "PING" {
resposta = "PONG"
}
// Envia a resposta para o remetente.
_, err = conn.WriteTo(
[]byte(resposta),
origem,
)
if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao responder:",
err,
)
}
}
}
A função net.ListenPacket cria um socket UDP associado ao endereço 127.0.0.1:9001, enquanto ReadFrom aguarda os datagramas enviados para essa porta. Quando uma mensagem chega, o método retorna a quantidade de bytes recebidos e o endereço de origem. O programa interpreta esses bytes como texto e verifica se o conteúdo corresponde ao comando esperado.
Quando a mensagem é PING, o servidor envia PONG para o endereço retornado pela leitura. Isso permite que o cliente confirme que recebeu uma resposta associada à operação realizada. Entretanto, essa confirmação pertence ao protocolo de teste que implementamos, e não ao UDP, porque o próprio transporte não fornece automaticamente esse comportamento.
O servidor continuará recebendo datagramas até que sua execução seja interrompida ou aconteça algum erro. Como usamos apenas uma goroutine para receber e responder, as mensagens são processadas sequencialmente. Essa abordagem é suficiente para o teste, mas aplicações que executam operações demoradas podem precisar encaminhar o processamento para outras goroutines.
Criando um cliente UDP com timeout
Para enviar datagramas UDP, podemos usar net.DialTimeout com o protocolo "udp". Entretanto, precisamos considerar que essa chamada não estabelece uma conexão com handshake, como acontece no TCP. Ela apenas prepara um socket associado ao destino informado, permitindo enviar e receber datagramas por meio dos métodos Write e Read.
Como UDP não garante uma resposta, precisamos configurar um prazo máximo para a leitura. Podemos usar SetDeadline, que define um limite para as operações de entrada e saída, evitando que o programa permaneça aguardando indefinidamente. Depois de enviar PING, o cliente tentará receber PONG dentro do intervalo configurado.
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
destino := "127.0.0.1:9001"
// Prepara um socket UDP associado ao destino.
conn, err := net.DialTimeout(
"udp",
destino,
2*time.Second,
)
if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao preparar socket:",
err,
)
return
}
defer conn.Close()
// Define um prazo para envio e recebimento.
err = conn.SetDeadline(
time.Now().Add(2 * time.Second),
)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
// Envia a mensagem reconhecida pelo servidor.
_, err = conn.Write([]byte("PING"))
if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao enviar datagrama:",
err,
)
return
}
// Reserva espaço para a resposta.
buffer := make([]byte, 1024)
// Aguarda um datagrama de resposta.
n, err := conn.Read(buffer)
if err != nil {
fmt.Fprintln(
os.Stderr,
"Falha ao receber resposta:",
err,
)
return
}
// Verifica se o servidor respondeu corretamente.
resposta := string(buffer[:n])
if resposta != "PONG" {
fmt.Println("Resposta inesperada:", resposta)
return
}
fmt.Println("Resposta UDP recebida:", resposta)
}
A função net.DialTimeout prepara o socket para enviar datagramas ao destino informado, mas seu sucesso não significa que existe um servidor UDP aceitando mensagens naquela porta. Depois da preparação, usamos SetDeadline para estabelecer um limite de dois segundos e enviamos PING com o método Write. A operação de envio pode terminar normalmente mesmo que o destinatário não receba o datagrama.
O método Read aguarda uma resposta e armazena os bytes recebidos no buffer. Se o servidor estiver executando e responder dentro do intervalo, o programa poderá receber PONG e confirmar que houve comunicação de aplicação nos dois sentidos. O teste também verifica o conteúdo retornado, porque receber qualquer datagrama não significa que a operação solicitada foi executada corretamente.
Quando não existe um servidor respondendo, a operação pode terminar por timeout ou retornar outro erro de rede. Em determinados sistemas, erros ICMP recebidos pela pilha de rede podem ser associados ao socket UDP e aparecer como erros de leitura. Por isso, não devemos assumir que todas as tentativas sem resposta terminarão necessariamente por timeout.
Executando o teste UDP
Podemos salvar o código do servidor como servidor_udp.go e o código do cliente como cliente_udp.go. Os arquivos podem ficar no mesmo diretório, desde que sejam executados individualmente, porque ambos possuem uma função main. Primeiro, iniciamos o servidor em um terminal e mantemos sua execução ativa.
go run servidor_udp.go
O servidor apresentará uma mensagem semelhante a esta:
Servidor UDP: 127.0.0.1:9001
Em outro terminal, executamos o cliente:
go run cliente_udp.go
Se o servidor receber a mensagem e responder corretamente, a saída do cliente será:
Resposta UDP recebida: PONG
No terminal do servidor, aparecerá uma mensagem identificando o endereço de origem e o conteúdo recebido. A porta de origem normalmente será escolhida pelo sistema operacional e poderá mudar entre execuções. O resultado positivo indica que o cliente recebeu a resposta esperada, mas continua representando apenas o comportamento observado durante aquela execução.
Também podemos encerrar o servidor e executar novamente o cliente para observar como o teste se comporta sem um serviço respondendo. Dependendo do sistema operacional, a leitura poderá retornar um timeout ou outro erro relacionado à comunicação. Essa diferença ajuda a entender por que UDP exige mais cuidado ao classificar portas como abertas ou fechadas.
Medindo tempo de conexão
Medir o tempo de uma conexão permite observar quanto uma operação demora para ser concluída. Esse tipo de informação é útil para identificar mudanças de comportamento, comparar destinos e investigar problemas relacionados ao estabelecimento de conexões. Entretanto, precisamos definir exatamente qual intervalo está sendo medido para evitar interpretar o resultado como uma característica mais ampla da rede.
Em um teste TCP, podemos medir o tempo decorrido entre o início de uma tentativa de conexão e seu resultado. Esse intervalo pode incluir resolução DNS, operações do sistema operacional e o estabelecimento da conexão TCP. Ele não representa necessariamente o tempo de resposta do protocolo de aplicação, porque ainda não enviamos uma mensagem HTTP ou outro comando depois que a conexão foi estabelecida.
Em Go, o pacote time fornece a função time.Now, que registra o instante atual, e time.Since, que calcula a duração desde determinado instante. Podemos combinar essas funções com net.DialTimeout para medir quanto tempo uma tentativa de conexão demora, apresentando o resultado mesmo quando a operação termina com erro.
Medindo uma tentativa TCP
Podemos registrar o instante imediatamente antes de chamar net.DialTimeout e calcular a duração assim que a função retornar. Dessa maneira, teremos o tempo gasto pela operação observada pelo programa. Como a medição acontece na aplicação, seu resultado também pode incluir pequenas variações relacionadas ao agendamento do processo e à execução do runtime.
Vamos criar um programa que mede uma conexão TCP com um endereço local. O tempo será apresentado em milissegundos, embora o tipo time.Duration permita representar intervalos com maior precisão.
package main
import (
"fmt"
"net"
"time"
)
func main() {
destino := "127.0.0.1:8080"
// Registra o instante inicial da tentativa.
inicio := time.Now()
// Tenta estabelecer uma conexão TCP.
conn, err := net.DialTimeout(
"tcp",
destino,
2*time.Second,
)
// Calcula o tempo gasto até o resultado.
duracao := time.Since(inicio)
if err != nil {
fmt.Println("Erro:", err)
fmt.Println("Duração:", duracao)
return
}
defer conn.Close()
fmt.Println("Conexão estabelecida.")
fmt.Println("Duração:", duracao)
}
A função time.Now registra o início da operação, enquanto time.Since(inicio) calcula o tempo decorrido até o momento em que a conexão terminou. O resultado é um valor do tipo time.Duration, que pode representar nanossegundos, microssegundos, milissegundos e outras unidades de tempo. A formatação padrão desse tipo escolhe uma representação adequada ao intervalo armazenado.
Se a conexão for estabelecida, teremos um resultado semelhante a este:
Conexão estabelecida.
Duração: 1.234ms
O valor apresentado representa apenas um exemplo, porque o tempo real depende das condições da máquina, do destino e da rede. Mesmo em conexões locais, diferentes execuções podem apresentar resultados distintos. Quando estamos comparando serviços externos, também precisamos considerar alterações de roteamento, congestionamento e outros fatores que podem afetar a tentativa.
Uma medição de conexão TCP não corresponde diretamente ao RTT, que significa Round-Trip Time e representa o intervalo necessário para uma informação percorrer o caminho até outro ponto e para uma resposta retornar. Embora o estabelecimento TCP envolva a troca de mensagens, o tempo medido por DialTimeout também inclui outras etapas. Para medir o comportamento de uma aplicação, precisamos enviar uma solicitação e aguardar uma resposta definida pelo protocolo.
Realizando várias medições
Uma única medição pode não representar adequadamente o comportamento de uma conexão. Pequenas variações de agendamento, utilização da rede e processamento do servidor podem alterar o tempo observado. Por isso, ferramentas de diagnóstico frequentemente executam várias tentativas e apresentam informações como quantidade de sucessos, menor tempo, maior tempo e duração média.
Podemos criar um programa que executa cinco tentativas TCP e armazena somente as durações das conexões estabelecidas. O programa também contará as falhas, porque uma média calculada apenas com tentativas bem-sucedidas não representa os casos em que a conexão não pôde ser realizada.
package main
import (
"fmt"
"net"
"time"
)
func main() {
const tentativas = 5
destino := "127.0.0.1:8080"
var soma time.Duration
var menor time.Duration
var maior time.Duration
sucessos := 0
falhas := 0
for i := 1; i <= tentativas; i++ {
// Registra o início de cada tentativa.
inicio := time.Now()
conn, err := net.DialTimeout(
"tcp",
destino,
2*time.Second,
)
duracao := time.Since(inicio)
if err != nil {
falhas++
fmt.Printf(
"Tentativa %d: falha (%v)\n",
i,
err,
)
continue
}
// Libera a conexão após a medição.
conn.Close()
sucessos++
soma += duracao
// Atualiza os limites observados.
if sucessos == 1 || duracao < menor {
menor = duracao
}
if duracao > maior {
maior = duracao
}
fmt.Printf(
"Tentativa %d: %s\n",
i,
duracao,
)
}
fmt.Println("Sucessos:", sucessos)
fmt.Println("Falhas:", falhas)
if sucessos > 0 {
// Calcula a média apenas das conexões concluídas.
media := soma / time.Duration(sucessos)
fmt.Println("Menor tempo:", menor)
fmt.Println("Maior tempo:", maior)
fmt.Println("Tempo médio:", media)
}
}
O programa executa cinco tentativas e registra o tempo de cada operação. Quando uma conexão é estabelecida, sua duração é adicionada à soma e comparada com os menores e maiores valores observados. Quando ocorre um erro, a falha é contabilizada separadamente, evitando incluir resultados sem conexão estabelecida no cálculo da média.
A média é calculada dividindo a soma das durações pela quantidade de tentativas bem-sucedidas. Entretanto, esse valor não representa o comportamento completo da operação quando existem muitas falhas, porque os tempos de erro não entram no cálculo. Por isso, precisamos apresentar a quantidade de sucessos e falhas junto às estatísticas, permitindo interpretar os resultados de maneira adequada.
Também precisamos considerar que diferentes tentativas podem estabelecer novas conexões usando portas locais distintas. Se estivermos medindo um domínio, sua resolução e a escolha do endereço de destino também poderão influenciar o resultado. Para comparações mais controladas, podemos usar diretamente um endereço IP e registrar informações suficientes para identificar qual destino foi realmente consultado.
Diferença entre tempo de conexão e tempo de resposta
O tempo de conexão representa quanto a aplicação demorou para estabelecer a comunicação TCP, enquanto o tempo de resposta envolve o envio de uma solicitação e o recebimento de uma resposta. Um servidor pode aceitar conexões rapidamente, mas demorar para processar comandos ou consultar outros serviços. Nesse caso, uma ferramenta que mede apenas DialTimeout não identificará diretamente o atraso no processamento da aplicação.
Para medir uma resposta HTTP, por exemplo, podemos registrar o instante antes de chamar client.Do e calcular a duração depois de receber a resposta. Entretanto, precisamos decidir se a medição termina quando os headers chegam ou depois que todo o corpo foi lido. Essas operações representam intervalos diferentes, especialmente quando o servidor transmite grandes quantidades de dados.
Em UDP, o tempo de resposta também precisa considerar o protocolo usado pela aplicação. Podemos medir o intervalo entre enviar PING e receber PONG, mas esse resultado representa apenas a operação definida pelo nosso servidor de teste. Não podemos enviar qualquer conteúdo para uma porta UDP e interpretar a ausência de resposta como uma medição válida do tempo de comunicação.
Criando um scanner simples de portas
Um scanner de portas é uma ferramenta que executa testes em diferentes portas de um destino para identificar quais aceitam conexões ou apresentam determinados comportamentos. Em TCP, podemos criar um scanner simples usando tentativas normais de conexão, sem precisar construir manualmente pacotes do protocolo. Cada tentativa permite verificar se foi possível estabelecer uma conexão naquele endereço e porta.
Quando precisamos testar várias portas, podemos percorrer um intervalo e executar uma tentativa para cada número. Entretanto, fazer todas as operações sequencialmente pode aumentar o tempo total, principalmente quando algumas conexões demoram para retornar. Podemos usar goroutines para executar diferentes tentativas durante o mesmo período, desde que exista um limite de concorrência para controlar o consumo de recursos.
Os exemplos deste capítulo começam com endereços locais e intervalos pequenos. A execução de scanners em sistemas de terceiros deve acontecer somente quando existe autorização para realizar os testes. Mesmo um scanner que usa conexões TCP normais pode gerar registros, acionar controles de segurança ou consumir recursos do serviço consultado.
Criando um scanner sequencial
A implementação mais simples percorre um intervalo de portas e chama net.DialTimeout para cada uma delas. Se a conexão for estabelecida, podemos registrar a porta como acessível naquele momento e fechar imediatamente o socket. Quando a tentativa falha, podemos apresentar o erro ou apenas registrar que não foi possível estabelecer a conexão.
Vamos testar as portas de 8000 até 8010 da máquina local. O programa terá um timeout de 300 milissegundos por tentativa, permitindo que operações demoradas sejam interrompidas antes de bloquear todo o processamento por muito tempo.
package main
import (
"fmt"
"net"
"strconv"
"time"
)
func main() {
host := "127.0.0.1"
// Percorre um pequeno intervalo de portas.
for porta := 8000; porta <= 8010; porta++ {
// Monta o endereço de destino.
destino := net.JoinHostPort(
host,
strconv.Itoa(porta),
)
// Tenta estabelecer uma conexão TCP.
conn, err := net.DialTimeout(
"tcp",
destino,
300*time.Millisecond,
)
if err != nil {
fmt.Printf(
"Porta %d: sem conexão\n",
porta,
)
continue
}
// Fecha a conexão estabelecida.
conn.Close()
fmt.Printf(
"Porta %d: conexão estabelecida\n",
porta,
)
}
}
O laço percorre as portas informadas e tenta estabelecer uma conexão TCP para cada destino. Quando a operação termina com sucesso, o programa fecha a conexão e registra a porta como acessível. Quando ocorre um erro, apresenta sem conexão, evitando concluir automaticamente que a porta está fechada ou filtrada.
Essa implementação é simples, mas todas as tentativas acontecem sequencialmente. Se cada uma demorasse o timeout completo de 300 milissegundos, o tempo acumulado aumentaria conforme a quantidade de portas. Para intervalos maiores, podemos executar diferentes tentativas concorrentemente, mantendo um limite para evitar iniciar conexões demais ao mesmo tempo.
Criando um scanner concorrente
Podemos criar um scanner concorrente usando um worker pool, que representa um conjunto fixo de goroutines responsáveis por processar tarefas. Em vez de criar uma goroutine para cada porta, o programa envia os números para um channel e permite que os workers executem as tentativas conforme ficam disponíveis. Isso controla a quantidade de conexões simultâneas sem impedir que diferentes portas sejam testadas durante o mesmo período.
Vamos criar uma ferramenta que recebe o host, o início e o fim do intervalo, o número de workers e o timeout. Para manter o exemplo controlado, vamos limitar a consulta a no máximo 100 portas por execução e permitir entre um e 32 workers. Esses limites são decisões da ferramenta, não restrições impostas pelo protocolo TCP ou pelo Go.
package main
import (
"flag"
"fmt"
"net"
"os"
"strconv"
"sync"
"time"
)
type Resultado struct {
Porta int
Erro error
}
func testarPorta(
host string,
porta int,
timeout time.Duration,
) Resultado {
// Monta o endereço da conexão.
destino := net.JoinHostPort(
host,
strconv.Itoa(porta),
)
// Tenta estabelecer uma conexão TCP.
conn, err := net.DialTimeout(
"tcp",
destino,
timeout,
)
if err != nil {
return Resultado{
Porta: porta,
Erro: err,
}
}
// Libera a conexão após confirmar seu estabelecimento.
conn.Close()
return Resultado{
Porta: porta,
}
}
func main() {
// Define os parâmetros do scanner.
host := flag.String(
"host",
"127.0.0.1",
"Endereço que será testado",
)
inicio := flag.Int(
"inicio",
8000,
"Primeira porta",
)
fim := flag.Int(
"fim",
8010,
"Última porta",
)
workers := flag.Int(
"workers",
4,
"Quantidade de conexões simultâneas",
)
timeout := flag.Duration(
"timeout",
300*time.Millisecond,
"Tempo máximo por conexão",
)
flag.Parse()
// Valida o endereço e o intervalo solicitado.
if *host == "" ||
*inicio < 1 ||
*fim > 65535 ||
*inicio > *fim ||
*fim-*inicio+1 > 100 {
fmt.Fprintln(
os.Stderr,
"Erro: host ou intervalo inválidos.",
)
os.Exit(2)
}
// Valida os limites de concorrência e tempo.
if *workers < 1 || *workers > 32 || *timeout <= 0 {
fmt.Fprintln(
os.Stderr,
"Erro: workers ou timeout inválidos.",
)
os.Exit(2)
}
// Cria os channels de tarefas e resultados.
tarefas := make(chan int)
resultados := make(chan Resultado)
var wg sync.WaitGroup
// Inicia uma quantidade fixa de trabalhadores.
for i := 0; i < *workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Processa as portas recebidas.
for porta := range tarefas {
resultado := testarPorta(
*host,
porta,
*timeout,
)
resultados <- resultado
}
}()
}
go func() {
// Envia as portas para os workers.
for porta := *inicio; porta <= *fim; porta++ {
tarefas <- porta
}
// Informa que não existem novas tarefas.
close(tarefas)
}()
go func() {
// Aguarda todos os workers terminarem.
wg.Wait()
// Encerra o envio de resultados.
close(resultados)
}()
// Armazena os resultados para apresentá-los em ordem.
verificacoes := make(map[int]error)
for resultado := range resultados {
verificacoes[resultado.Porta] = resultado.Erro
}
// Apresenta as portas na ordem numérica.
for porta := *inicio; porta <= *fim; porta++ {
err := verificacoes[porta]
if err != nil {
fmt.Printf(
"TCP/%d: sem conexão (%v)\n",
porta,
err,
)
continue
}
fmt.Printf(
"TCP/%d: conexão estabelecida\n",
porta,
)
}
}
A função testarPorta concentra a tentativa TCP e retorna uma estrutura Resultado, que contém o número da porta e um possível erro. Quando a conexão é estabelecida, ela é fechada imediatamente e o resultado é devolvido sem erro. Isso permite separar o processamento da conexão da apresentação das informações, mantendo a lógica do scanner organizada.
Na função main, o programa interpreta as flags e valida os limites informados. O intervalo não pode ultrapassar 100 portas por execução, enquanto a quantidade de workers precisa ficar entre um e 32. Essas verificações impedem que uma execução incorreta inicie uma quantidade de operações muito maior do que aquela planejada para esta ferramenta didática.
O channel tarefas transporta os números das portas, enquanto resultados recebe as informações produzidas pelos workers. Cada trabalhador executa uma conexão de cada vez, permitindo que a quantidade máxima de tentativas simultâneas seja controlada pelo número de goroutines criadas. Quando todas as tarefas foram distribuídas, o channel de entrada é fechado para informar que não existem novas portas a processar.
Depois que todos os workers terminam, uma goroutine auxiliar fecha o channel de resultados. A função principal recebe os valores e os armazena em um mapa, permitindo apresentá-los posteriormente na ordem numérica das portas. Isso evita depender da ordem de conclusão das goroutines, que pode variar conforme o tempo de resposta de cada tentativa.
Também precisamos considerar que o scanner mantém um resultado para cada porta consultada. Como limitamos a execução a 100 portas, esse armazenamento é pequeno e adequado ao exemplo. Em ferramentas que processam intervalos maiores, normalmente precisamos considerar estratégias diferentes para armazenar ou apresentar os resultados sem manter todas as informações na memória.
Executando o scanner
Podemos executar o scanner com os parâmetros padrão, que consultam as portas 8000 até 8010 da máquina local. Como os workers são criados dentro do programa, não precisamos iniciar processos adicionais para executar as tentativas concorrentes. O resultado dependerá dos serviços que estiverem escutando nas portas testadas.
go run main.go
Também podemos escolher um intervalo diferente, mantendo o destino na interface local:
go run main.go \
-host 127.0.0.1 \
-inicio 9000 \
-fim 9010 \
-workers 4 \
-timeout 500ms
Nesse exemplo, a ferramenta verificará as portas de 9000 até 9010, permitindo até quatro tentativas simultâneas. Cada conexão terá um limite de 500 milissegundos, mas as tentativas poderão terminar antes desse prazo. Depois que todas forem concluídas, os resultados serão apresentados na ordem numérica.
A saída poderá conter mensagens semelhantes a estas:
TCP/9000: conexão estabelecida
TCP/9001: sem conexão (connect: connection refused)
TCP/9002: sem conexão (connect: connection refused)
As mensagens representam apenas um exemplo e podem variar conforme o sistema operacional e os serviços disponíveis. Uma porta marcada como acessível aceitou uma conexão TCP durante o teste, enquanto sem conexão indica apenas que aquela tentativa falhou. Para identificar o serviço e verificar seu funcionamento, precisaríamos executar operações adicionais seguindo o protocolo esperado.
Cuidados ao executar scanners
Mesmo quando usamos conexões TCP normais, um scanner pode produzir efeitos no sistema consultado. Alguns serviços registram cada conexão recebida, enquanto mecanismos de monitoramento podem identificar várias tentativas em sequência como uma atividade incomum. Por isso, precisamos executar esses testes somente em máquinas e redes que temos autorização para diagnosticar.
Também não devemos assumir que aumentar o número de workers sempre melhora o desempenho. Uma quantidade excessiva de conexões simultâneas pode consumir recursos locais, pressionar os serviços consultados e produzir mais erros. O limite de concorrência precisa considerar a capacidade da máquina, o comportamento do destino e a finalidade do diagnóstico.
Outro cuidado é que o scanner apresentado usa conexões TCP completas, que podem ser visíveis para a aplicação remota. Ele não implementa técnicas de envio manual de segmentos TCP nem tenta identificar serviços por respostas específicas. Isso mantém a implementação simples e adequada para aprender como Go trabalha com tentativas de conexão e concorrência controlada.
Entendendo limitações de UDP
O diagnóstico de portas UDP possui limitações diferentes das encontradas em TCP. Como UDP não estabelece conexões com handshake, não existe uma etapa equivalente à confirmação de que um servidor aceitou uma conexão. O envio de um datagrama pode terminar normalmente mesmo quando não existe nenhuma aplicação escutando na porta de destino.
Isso significa que um teste UDP não deve classificar uma porta como aberta apenas porque a operação de envio foi concluída. Para obter uma confirmação positiva, normalmente precisamos enviar uma mensagem válida para o protocolo esperado e receber uma resposta. Quando nenhuma resposta chega, diferentes condições podem explicar o resultado, impedindo uma conclusão definitiva apenas com essa observação.
Uma ferramenta de diagnóstico UDP precisa distinguir o resultado da operação local de envio do resultado da comunicação com a aplicação remota. Essas etapas são diferentes porque o sistema operacional pode aceitar o datagrama para transmissão sem receber qualquer confirmação de entrega ou processamento.
Por que enviar um datagrama não confirma a porta
Quando usamos net.Dial ou net.DialTimeout com UDP, o Go pode preparar um socket associado ao destino sem trocar mensagens com o servidor remoto. Isso acontece porque UDP não possui um mecanismo de estabelecimento de conexão equivalente ao TCP. Portanto, receber um objeto net.Conn não significa que existe um serviço disponível na porta informada.
O mesmo cuidado vale para o método Write. Quando ele termina sem erro, sabemos que a operação local de envio foi aceita conforme o comportamento do socket e do sistema operacional. Entretanto, isso não garante que o datagrama atravessou toda a rede, chegou ao destino ou foi recebido por uma aplicação.
Podemos demonstrar esse comportamento tentando enviar um datagrama para uma porta local em que não existe um servidor UDP. Dependendo do sistema operacional, o envio poderá ser concluído normalmente, mesmo sem uma aplicação disponível para processar a mensagem.
package main
import (
"fmt"
"net"
"time"
)
func main() {
// Prepara um socket UDP para o destino informado.
conn, err := net.DialTimeout(
"udp",
"127.0.0.1:65000",
time.Second,
)
if err != nil {
fmt.Println("Erro ao preparar socket:", err)
return
}
defer conn.Close()
// Tenta enviar um datagrama UDP.
_, err = conn.Write([]byte("TESTE"))
if err != nil {
fmt.Println("Erro no envio:", err)
return
}
// O envio concluído não comprova a entrega.
fmt.Println("Datagrama enviado localmente.")
}
O programa prepara um socket UDP e tenta enviar a mensagem TESTE para a porta 65000. Se a operação de escrita terminar normalmente, apresentará uma mensagem indicando que o datagrama foi enviado localmente. Isso não representa uma confirmação da existência de um servidor, porque nenhuma resposta de aplicação foi recebida.
A porta escolhida é apenas um exemplo e pode estar ocupada em determinada máquina. Também precisamos considerar que alguns sistemas podem informar erros relacionados à comunicação UDP em momentos diferentes, principalmente quando recebem mensagens ICMP sobre problemas de entrega. Por isso, nem mesmo o comportamento de uma porta sem serviço precisa ser idêntico em todas as plataformas.
O papel das mensagens ICMP
ICMP, que significa Internet Control Message Protocol, é um protocolo usado para transportar determinadas mensagens de controle e diagnóstico relacionadas à comunicação IP. Ele permite informar condições como destino inalcançável e tempo de vida excedido. Essas mensagens não fazem parte do fluxo de dados da aplicação, mas podem ajudar a identificar problemas encontrados durante o encaminhamento dos pacotes.
Quando um datagrama UDP chega a um destino que não possui uma aplicação escutando na porta solicitada, o sistema remoto pode enviar uma mensagem ICMP indicando que a porta está inalcançável. Entretanto, esse comportamento não é garantido em todas as situações, porque firewalls, políticas de rede e configurações do sistema podem impedir ou limitar essas respostas.
Por isso, receber uma indicação ICMP de porta inalcançável pode fornecer uma informação útil sobre o resultado da tentativa. Já a ausência dessa indicação não permite concluir que a porta esteja aberta, porque a mensagem pode ter sido bloqueada ou simplesmente não ter sido enviada. Essa diferença explica por que scanners UDP precisam interpretar os resultados com mais cautela.
Ausência de resposta e protocolos de aplicação
Muitos serviços UDP não respondem a mensagens desconhecidas ou inválidas. Um servidor DNS, por exemplo, espera datagramas que representem consultas DNS corretamente estruturadas. Se enviarmos apenas a string PING para uma porta DNS, o serviço poderá ignorar o conteúdo ou apresentar outro comportamento definido por sua implementação, sem que isso indique necessariamente indisponibilidade.
Para testar um serviço UDP com maior confiança, precisamos conhecer o protocolo de aplicação e construir uma mensagem que ele consiga interpretar. Se o servidor devolver uma resposta válida, teremos evidência de que a comunicação funcionou naquele momento. Entretanto, mesmo uma resposta positiva não garante que todas as funcionalidades do serviço estejam disponíveis ou funcionando corretamente.
Quando desenvolvemos ferramentas genéricas, normalmente precisamos informar claramente o que foi testado. Uma mensagem como sem resposta UDP descreve melhor o resultado observado do que porta fechada, porque não transforma uma ausência de informação em uma conclusão. Essa distinção é importante para evitar diagnósticos incorretos, principalmente em redes com filtros ou regras específicas de segurança.
Outras limitações de UDP
UDP também não garante que os datagramas sejam recebidos na mesma ordem em que foram enviados. Quando uma aplicação envia várias mensagens, algumas podem chegar fora de ordem ou não chegar ao destino. Por isso, ferramentas que dependem de várias solicitações precisam considerar identificadores, timeouts e mecanismos de associação entre cada pergunta e sua resposta.
Outro cuidado envolve o tamanho dos datagramas. Uma mensagem muito grande pode exigir fragmentação IP, aumentando o impacto de perdas durante a transmissão. Além disso, quando o buffer de recebimento é menor do que o datagrama recebido, parte do conteúdo pode ser descartada. Para diagnósticos simples, normalmente trabalhamos com mensagens pequenas e limites conhecidos.
Por fim, o endereço de origem de um datagrama UDP não representa sozinho uma prova confiável de identidade. Dependendo do ambiente, mensagens UDP podem sofrer alterações relacionadas a NAT ou outros mecanismos de rede, e também existem situações em que endereços podem ser falsificados. Quando a aplicação precisa autenticar o remetente, devemos trabalhar com mecanismos próprios de segurança, em vez de confiar apenas no endereço recebido.
Traceroute em Go
Traceroute é uma técnica de diagnóstico usada para descobrir parte do caminho percorrido por pacotes IP até determinado destino. Em vez de testar apenas se um endereço pode ser alcançado, ela tenta identificar os roteadores intermediários que participam do encaminhamento. Isso permite observar em quais pontos do caminho existem respostas e medir o tempo aproximado de retorno das mensagens de diagnóstico.
O funcionamento tradicional do traceroute depende de um campo presente no cabeçalho IP. No IPv4, esse campo é chamado de TTL, que significa Time To Live e funciona como um limite para a quantidade de saltos que o pacote pode percorrer. Cada roteador que encaminha o pacote reduz esse valor, e quando ele chega a zero, o roteador descarta o pacote e normalmente pode enviar uma mensagem ICMP indicando que o tempo foi excedido.
Ao enviar pacotes com valores de TTL progressivamente maiores, podemos tentar descobrir diferentes pontos do caminho. Um pacote com TTL igual a um normalmente expira no primeiro roteador, enquanto um pacote com TTL igual a dois pode alcançar o segundo. O procedimento continua até que o destino responda, algum erro interrompa a operação ou o limite máximo de saltos seja alcançado.
Entendendo os saltos da rede
Um salto, também chamado de hop, representa uma etapa de encaminhamento entre roteadores IP. Quando um pacote atravessa um roteador, seu TTL é reduzido, permitindo limitar quantas etapas de encaminhamento podem acontecer antes que ele seja descartado. Esse mecanismo também ajuda a impedir que pacotes permaneçam circulando indefinidamente em situações de erro de roteamento.
Considere um caminho em que um computador precisa atravessar três roteadores para alcançar determinado servidor. Se enviarmos um pacote com TTL igual a um, ele normalmente será descartado pelo primeiro roteador. Com TTL igual a dois, poderá alcançar o segundo roteador, permitindo identificar outro ponto do caminho por meio da resposta ICMP correspondente.
Cliente
|
| TTL = 1
v
Roteador 1 ---- ICMP Time Exceeded
|
| TTL = 2
v
Roteador 2 ---- ICMP Time Exceeded
|
| TTL = 3
v
Roteador 3 ---- ICMP Time Exceeded
|
v
Servidor de destino
Nesse exemplo, o TTL precisa aumentar conforme queremos alcançar pontos mais distantes. Quando um roteador descarta o pacote porque o limite chegou a zero, ele pode devolver uma mensagem ICMP do tipo Time Exceeded. A ferramenta recebe essa mensagem e consulta seu endereço de origem, permitindo associar determinado salto ao roteador que respondeu.
Entretanto, o traceroute não garante que todos os roteadores responderão. Alguns dispositivos podem ignorar mensagens ICMP, limitar sua frequência ou aplicar regras que impedem a entrega da resposta. Além disso, o caminho de retorno pode ser diferente do caminho de ida, por isso o tempo medido não representa exclusivamente o atraso do roteador identificado.
Diferentes métodos de traceroute
Existem diferentes maneiras de implementar traceroute. Uma abordagem utiliza datagramas UDP enviados com valores crescentes de TTL, enquanto outra usa mensagens ICMP Echo Request, semelhantes às usadas por ferramentas de ping. Também existem implementações que usam sondas TCP, permitindo observar o comportamento de caminhos em que determinados protocolos são filtrados.
No Linux, a implementação tradicional do comando traceroute costuma usar datagramas UDP destinados a portas que provavelmente não possuem serviços escutando. Quando o pacote alcança o destino, o sistema remoto pode responder com uma mensagem ICMP indicando porta inalcançável. Essa resposta permite que a ferramenta identifique que chegou ao destino final.
No Windows, o comando tracert tradicionalmente usa mensagens ICMP Echo Request com TTL progressivamente maior. Quando essas mensagens alcançam o destino, ele pode responder com ICMP Echo Reply, permitindo encerrar a descoberta do caminho. Como os métodos usam protocolos diferentes, também podem produzir resultados distintos quando existem firewalls ou políticas de roteamento específicas.
Usando traceroute pelo terminal
Antes de implementar o traceroute diretamente em Go, podemos observar como ele funciona usando ferramentas disponíveis no sistema operacional. No Linux e no macOS, normalmente usamos o comando traceroute, que permite informar o destino e algumas opções relacionadas ao número máximo de saltos e ao tempo de espera.
traceroute example.com
Podemos usar a opção -n para evitar a resolução reversa dos endereços encontrados, permitindo apresentar diretamente os IPs dos roteadores. Também podemos informar -m para limitar a quantidade máxima de saltos que serão consultados.
traceroute -n -m 12 example.com
No Windows, usamos o comando tracert, que possui opções próprias. A opção /d evita a resolução dos nomes dos roteadores, enquanto /h define a quantidade máxima de saltos.
tracert /d /h 12 example.com
O resultado normalmente apresenta uma sequência de saltos, seus endereços e os tempos observados durante as tentativas. Entretanto, a quantidade de sondas por salto e o formato da saída dependem da ferramenta e de suas opções. Também é comum encontrar asteriscos quando não existe uma resposta dentro do intervalo configurado.
1 192.168.1.1 1.2 ms
2 198.51.100.1 10.5 ms
3 * *
4 203.0.113.10 18.3 ms
Esses endereços e tempos representam apenas um exemplo ilustrativo. Os blocos 198.51.100.0/24 e 203.0.113.0/24 são reservados para documentação, por isso não representam necessariamente roteadores reais encontrados em uma consulta. O asterisco indica que a ferramenta não recebeu uma resposta válida no prazo esperado, e não que aquele ponto da rede esteja obrigatoriamente indisponível.
Também precisamos considerar que o traceroute apresenta os roteadores que responderam às sondas, não necessariamente todos os dispositivos que encaminharam os pacotes. Firewalls, balanceamento de carga e diferentes caminhos de roteamento podem modificar os resultados entre execuções. Por isso, a saída deve ser interpretada como uma observação do comportamento da rede naquele momento.
Executando o traceroute do sistema pelo Go
Uma maneira simples de criar uma ferramenta CLI de traceroute em Go é executar o comando nativo do sistema operacional. Essa abordagem não implementa diretamente o protocolo, mas permite integrar o diagnóstico a uma aplicação Go sem precisar trabalhar com raw sockets ou mensagens ICMP manualmente. Para isso, podemos usar o pacote os/exec, que permite iniciar processos externos e acompanhar sua execução.
Como os nomes e as opções dos comandos são diferentes entre os sistemas, podemos usar runtime.GOOS para identificar a plataforma. No Windows, executaremos tracert, enquanto Linux e macOS usarão traceroute. Também criaremos um contexto com timeout para impedir que o processo externo permaneça executando indefinidamente.
package main
import (
"context"
"fmt"
"os"
"os/exec"
"runtime"
"time"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(
os.Stderr,
"Uso: tracecli <destino>",
)
os.Exit(2)
}
destino := os.Args[1]
// Limita o tempo total do comando.
ctx, cancel := context.WithTimeout(
context.Background(),
45*time.Second,
)
defer cancel()
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
// Usa o comando nativo do Windows.
cmd = exec.CommandContext(
ctx,
"tracert",
"/d",
"/h",
"12",
destino,
)
case "linux", "darwin":
// Usa o traceroute disponível no sistema.
cmd = exec.CommandContext(
ctx,
"traceroute",
"-n",
"-m",
"12",
destino,
)
default:
fmt.Fprintln(
os.Stderr,
"Sistema operacional não suportado.",
)
os.Exit(2)
}
// Encaminha a saída diretamente para o terminal.
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Executa o comando externo e aguarda sua conclusão.
if err := cmd.Run(); err != nil {
fmt.Fprintln(
os.Stderr,
"Erro no traceroute:",
err,
)
os.Exit(1)
}
}
O programa recebe o destino como argumento posicional e usa runtime.GOOS para selecionar o comando correspondente ao sistema operacional. A função exec.CommandContext prepara a execução do processo externo e associa seu ciclo de vida ao contexto informado. Quando o prazo de 45 segundos termina, o mecanismo pode interromper o processo iniciado, evitando uma espera indefinida.
Os campos Stdout e Stderr encaminham as mensagens do processo externo para os mesmos fluxos usados pelo programa Go. Isso permite visualizar o resultado do traceroute conforme o comando é executado, sem precisar armazenar toda a saída em memória. O método Run inicia o processo e aguarda sua conclusão, retornando um erro quando a execução não termina normalmente.
A função exec.CommandContext recebe o nome do comando e seus argumentos separadamente, sem interpretar diretamente uma linha de shell. Isso evita problemas comuns de montagem de comandos quando o destino contém caracteres especiais. Entretanto, ainda precisamos validar os parâmetros recebidos e considerar que a ferramenta externa possui suas próprias regras de interpretação dos argumentos.
Essa implementação depende da existência do executável correspondente no sistema. No Linux, traceroute pode precisar ser instalado separadamente, enquanto no Windows o comando tracert normalmente já está disponível. Além disso, o programa apenas executa a ferramenta nativa, por isso seu comportamento, formato de saída e necessidades de permissão continuam dependendo da implementação do sistema operacional.
Implementando traceroute com ICMP em Go
Também podemos implementar o mecanismo de traceroute diretamente em Go, criando mensagens ICMP e modificando o TTL dos pacotes enviados. Entretanto, essa operação exige acesso a recursos de rede de nível mais baixo do que aqueles usados nas conexões TCP e UDP comuns. O pacote net não fornece sozinho todas as estruturas necessárias para construir e interpretar mensagens ICMP.
O módulo golang.org/x/net possui os pacotes icmp e ipv4, que permitem trabalhar com mensagens ICMP e opções do protocolo IPv4. Esses pacotes são mantidos no ecossistema Go, mas não fazem parte da biblioteca padrão instalada diretamente com a linguagem. Por isso, precisamos adicioná-los ao projeto antes de executar o exemplo.
Vamos criar uma implementação didática de traceroute usando ICMP Echo Request sobre IPv4. O programa enviará uma sonda por salto, aumentando o TTL até alcançar o destino ou atingir o limite configurado. A implementação também verificará se determinadas respostas pertencem à sonda enviada, evitando interpretar automaticamente qualquer mensagem ICMP recebida como resultado da consulta.
Primeiro, criamos um módulo para o projeto e adicionamos as dependências necessárias.
go mod init exemplo/traceroute
Depois, adicionamos os pacotes responsáveis pelas mensagens ICMP e pelo controle de IPv4.
go get golang.org/x/net/icmp golang.org/x/net/ipv4
O comando go get registra as dependências necessárias no módulo. Como esses pacotes são externos à biblioteca padrão, suas versões e requisitos mínimos de Go dependem da versão selecionada pelo gerenciador de módulos. Para projetos que precisam de reprodutibilidade, devemos manter os arquivos go.mod e go.sum junto ao código-fonte.
No arquivo main.go, podemos implementar o envio das sondas e a interpretação das respostas.
package main
import (
"errors"
"fmt"
"net"
"os"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
// Verifica se a resposta corresponde à sonda enviada.
func corresponde(
mensagem *icmp.Message,
id int,
sequencia int,
) bool {
switch mensagem.Type {
case ipv4.ICMPTypeEchoReply:
// Confere o identificador da resposta Echo Reply.
eco, ok := mensagem.Body.(*icmp.Echo)
return ok &&
eco.ID == id &&
eco.Seq == sequencia
case ipv4.ICMPTypeTimeExceeded:
// Obtém o pacote original incluído no erro ICMP.
corpo, ok := mensagem.Body.(*icmp.TimeExceeded)
if !ok {
return false
}
// Interpreta o cabeçalho IPv4 original.
cabecalho, err := ipv4.ParseHeader(corpo.Data)
if err != nil {
return false
}
// Exige espaço suficiente para o cabeçalho ICMP original.
if cabecalho.Len < 20 ||
len(corpo.Data) < cabecalho.Len+8 {
return false
}
// Interpreta a mensagem ICMP original.
original, err := icmp.ParseMessage(
1,
corpo.Data[cabecalho.Len:],
)
if err != nil {
return false
}
if original.Type != ipv4.ICMPTypeEcho {
return false
}
// Confirma o identificador e a sequência da sonda.
eco, ok := original.Body.(*icmp.Echo)
return ok &&
eco.ID == id &&
eco.Seq == sequencia
}
return false
}
func executar(destino string) error {
// Resolve o endereço IPv4 do destino.
ips, err := net.LookupIP(destino)
if err != nil {
return fmt.Errorf("resolver destino: %w", err)
}
var ip net.IP
for _, candidato := range ips {
if candidato.To4() != nil {
ip = candidato.To4()
break
}
}
if ip == nil {
return errors.New("destino não possui IPv4 resolvido")
}
// Abre um socket ICMP bruto para IPv4.
socket, err := net.ListenPacket(
"ip4:icmp",
"0.0.0.0",
)
if err != nil {
return fmt.Errorf("abrir socket ICMP: %w", err)
}
defer socket.Close()
// Permite configurar o TTL dos pacotes enviados.
pacote := ipv4.NewPacketConn(socket)
// Define um identificador para as sondas.
id := os.Getpid() & 0xffff
// Prepara um buffer para receber respostas ICMP.
buffer := make([]byte, 1500)
fmt.Printf("Traceroute para %s (%s)\n", destino, ip)
const maxSaltos = 20
for ttl := 1; ttl <= maxSaltos; ttl++ {
// Cria uma mensagem Echo Request com sequência única.
mensagem := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: id,
Seq: ttl,
Data: []byte("TRACE"),
},
}
// Converte a mensagem para bytes ICMP.
dados, err := mensagem.Marshal(nil)
if err != nil {
return fmt.Errorf("montar ICMP: %w", err)
}
// Define o TTL da sonda atual.
if err := pacote.SetTTL(ttl); err != nil {
return fmt.Errorf("configurar TTL: %w", err)
}
inicio := time.Now()
// Envia a sonda para o destino.
_, err = pacote.WriteTo(
dados,
nil,
&net.IPAddr{IP: ip},
)
if err != nil {
return fmt.Errorf("enviar sonda: %w", err)
}
// Define o prazo para receber uma resposta.
prazo := time.Now().Add(2 * time.Second)
if err := pacote.SetReadDeadline(prazo); err != nil {
return fmt.Errorf("configurar prazo: %w", err)
}
respondeu := false
chegou := false
for {
// Recebe uma mensagem ICMP.
n, _, origem, err := pacote.ReadFrom(buffer)
if err != nil {
var erroRede net.Error
if errors.As(err, &erroRede) &&
erroRede.Timeout() {
break
}
return fmt.Errorf("receber ICMP: %w", err)
}
// Interpreta a mensagem recebida.
resposta, err := icmp.ParseMessage(
1,
buffer[:n],
)
if err != nil {
continue
}
// Ignora mensagens de outras sondas.
if !corresponde(resposta, id, ttl) {
continue
}
duracao := time.Since(inicio)
fmt.Printf(
"%2d %-18s %s\n",
ttl,
origem.String(),
duracao,
)
respondeu = true
// Echo Reply indica que o destino respondeu.
if resposta.Type == ipv4.ICMPTypeEchoReply {
chegou = true
}
break
}
// Apresenta saltos sem resposta correspondente.
if !respondeu {
fmt.Printf("%2d *\n", ttl)
}
// Encerra quando o destino responde.
if chegou {
return nil
}
}
return errors.New(
"destino não respondeu dentro do limite de saltos",
)
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(
os.Stderr,
"Uso: traceroute <destino>",
)
os.Exit(2)
}
if err := executar(os.Args[1]); err != nil {
fmt.Fprintln(os.Stderr, "Erro:", err)
os.Exit(1)
}
}
O programa começa resolvendo o destino para um endereço IPv4. Como esta implementação trabalha especificamente com ICMPv4, precisamos escolher um endereço compatível com essa família. Depois, usamos net.ListenPacket("ip4:icmp", "0.0.0.0") para abrir um socket ICMP bruto, que permite enviar e receber mensagens desse protocolo por meio dos recursos disponibilizados pelo sistema operacional.
A função ipv4.NewPacketConn cria um objeto que permite configurar opções relacionadas ao IPv4, incluindo o TTL dos pacotes enviados. Dentro do laço, usamos SetTTL para aumentar o limite a cada sonda, começando em um e avançando até o máximo de vinte saltos. Dessa maneira, tentamos receber respostas dos roteadores encontrados progressivamente ao longo do caminho.
Cada sonda é representada por uma mensagem icmp.Message do tipo Echo Request. O campo ID contém um identificador derivado do processo, enquanto Seq recebe o número do salto atual. Esses valores ajudam a associar as respostas recebidas às sondas enviadas, evitando que o programa interprete automaticamente mensagens ICMP que pertencem a outras operações.
A função corresponde examina o tipo da mensagem recebida. Quando encontra Echo Reply, verifica o identificador e o número de sequência. Quando recebe Time Exceeded, tenta interpretar a parte do pacote original incluída na mensagem ICMP, procurando os mesmos valores de identificação. Isso é necessário porque uma mensagem Time Exceeded não possui diretamente os campos de uma resposta Echo Reply.
O prazo de leitura é definido em dois segundos para cada salto. Durante esse intervalo, o programa pode receber mensagens ICMP que não correspondem à sonda atual, por isso continua aguardando até encontrar uma resposta compatível ou ultrapassar o tempo permitido. Quando nenhuma resposta correspondente é encontrada, o salto é apresentado com um asterisco.
Quando o destino responde com Echo Reply, o programa considera que a sonda alcançou seu objetivo e encerra a execução. Entretanto, essa implementação é didática e possui limitações importantes. Ela envia apenas uma sonda por salto, não implementa todos os tipos de erro ICMP e pode deixar de identificar respostas em situações nas quais os dados originais necessários para a associação não estão disponíveis.
Também precisamos considerar que a associação por identificador e sequência não substitui uma validação completa do caminho e de todos os campos da mensagem. Um traceroute mais completo normalmente precisa tratar diferentes respostas ICMP, múltiplas sondas, balanceamento de carga e outros comportamentos específicos das redes. Por isso, a implementação apresentada demonstra o mecanismo fundamental, mas não pretende substituir todas as funcionalidades de ferramentas especializadas.
Compilando e executando o traceroute em Go
Como o exemplo usa raw sockets, sua execução pode exigir permissões adicionais. Em sistemas Linux, a criação desses sockets normalmente exige a capacidade CAP_NET_RAW, enquanto no macOS o acesso a raw sockets também costuma depender de privilégios elevados. Essas permissões são diferentes das necessárias para abrir conexões TCP e sockets UDP comuns.
Podemos compilar o programa usando o comando go build, gerando um executável antes de iniciar o diagnóstico.
go build -o traceroute .
Em um sistema que exige privilégios elevados para abrir o socket ICMP, a execução poderá precisar ser feita por uma conta autorizada. No Linux ou macOS, um exemplo seria executar o binário compilado com sudo, desde que isso seja permitido e realmente necessário no ambiente.
sudo ./traceroute example.com
Executar programas com privilégios elevados aumenta o impacto de possíveis falhas ou comportamentos inesperados. Por isso, devemos revisar o código antes de conceder essas permissões e evitar usar privilégios administrativos quando uma alternativa menos privilegiada atende à necessidade. Para diagnósticos comuns, muitas vezes é mais conveniente usar a ferramenta nativa do sistema operacional.
Também precisamos lembrar que a implementação usa IPv4 e pode não funcionar da mesma maneira em todas as plataformas. O exemplo foi baseado nas interfaces disponibilizadas pelos pacotes golang.org/x/net/icmp e golang.org/x/net/ipv4, mas o suporte efetivo a raw sockets depende do sistema operacional. No Windows, normalmente precisamos considerar interfaces próprias do sistema para implementar diagnósticos ICMP com maior compatibilidade.
Limitações práticas de traceroute em Linux, Windows e macOS
Embora o conceito de traceroute seja semelhante entre os sistemas operacionais, suas implementações podem usar protocolos diferentes e disponibilizar opções distintas. Isso significa que o mesmo destino pode produzir resultados diferentes quando consultado por ferramentas executadas em Linux, Windows ou macOS. Essas diferenças não indicam necessariamente problemas de rede, porque o método usado para descobrir os saltos também influencia as respostas recebidas.
Além do protocolo escolhido, precisamos considerar o suporte do sistema operacional a sockets, ICMP e alterações de TTL. Algumas operações são disponibilizadas por APIs comuns, enquanto outras dependem de interfaces específicas ou exigem privilégios adicionais. Quando desenvolvemos ferramentas portáveis em Go, essas diferenças precisam ser consideradas antes de escolher a abordagem de implementação.
Traceroute no Linux
No Linux, o comando traceroute normalmente oferece diferentes métodos para descobrir o caminho até um destino. A implementação tradicional usa datagramas UDP destinados a portas que provavelmente não possuem serviços escutando, aumentando o TTL para identificar os roteadores intermediários. Também existem opções para usar ICMP Echo Request ou sondas TCP, permitindo adaptar o diagnóstico às condições da rede.
Podemos executar uma consulta básica usando:
traceroute -n example.com
Em implementações que oferecem suporte às opções correspondentes, podemos solicitar sondas ICMP usando -I ou sondas TCP usando -T.
traceroute -I example.com
traceroute -T example.com
Esses métodos podem produzir resultados diferentes porque firewalls e dispositivos intermediários podem aplicar políticas específicas para UDP, TCP e ICMP. Uma rede pode permitir determinadas mensagens e bloquear outras, fazendo com que alguns saltos apareçam em uma consulta e não em outra. Por isso, comparar métodos pode ajudar na investigação, mas não permite afirmar automaticamente que determinado roteador está indisponível.
O Linux também pode permitir determinadas operações de traceroute sem privilégios administrativos, dependendo do método usado. A implementação tradicional com datagramas UDP pode funcionar sem raw sockets, enquanto métodos que precisam receber mensagens ICMP diretamente podem exigir recursos adicionais. Também existem mecanismos específicos para sockets ICMP de eco, cuja disponibilidade depende da configuração do sistema.
Traceroute no Windows
No Windows, o comando nativo é tracert, que utiliza mensagens ICMP Echo Request ou ICMPv6 para descobrir os saltos. Ele aumenta progressivamente o TTL ou o Hop Limit das sondas e interpreta as respostas dos roteadores intermediários. Diferentemente do traceroute tradicional baseado em UDP, esse comportamento pode produzir resultados distintos quando a rede filtra determinados protocolos.
Podemos executar uma consulta sem resolver os nomes dos roteadores usando:
tracert /d example.com
Também podemos informar o número máximo de saltos e o tempo máximo de espera por resposta. No exemplo seguinte, limitamos a consulta a doze saltos e definimos um segundo de espera para cada sonda.
tracert /d /h 12 /w 1000 example.com
O Windows disponibiliza mecanismos próprios para realizar operações ICMP, incluindo APIs como IcmpSendEcho. Essas interfaces podem ser mais adequadas para ferramentas desenvolvidas especificamente para a plataforma, principalmente quando queremos evitar depender diretamente do comportamento de raw sockets. Entretanto, elas exigem integração com APIs do sistema operacional, o que aumenta a complexidade de uma implementação que também precisa funcionar em Linux e macOS.
O comando tracert normalmente pode ser executado por usuários comuns, sem exigir privilégios administrativos. Isso não significa que qualquer programa tenha permissão para criar raw sockets arbitrários, porque essas operações seguem regras próprias do Windows. Portanto, devemos diferenciar as permissões necessárias para usar a ferramenta nativa das permissões exigidas por uma implementação personalizada.
Traceroute no macOS
No macOS, o comando traceroute também está disponível em instalações comuns do sistema. Sua implementação tradicional trabalha com sondas UDP, permitindo observar o caminho até determinado destino sem precisar construir manualmente as mensagens do protocolo. Algumas opções podem variar em relação às implementações encontradas no Linux, por isso devemos consultar a documentação correspondente à versão instalada.
Podemos executar uma consulta básica usando:
traceroute -n example.com
Também podemos limitar a quantidade máxima de saltos:
traceroute -n -m 12 example.com
O macOS possui mecanismos de permissões para operações de rede de baixo nível, incluindo a criação de raw sockets. Programas que tentam acessar esses recursos diretamente podem precisar de privilégios elevados, dependendo do tipo de socket e das restrições aplicadas ao processo. Entretanto, a ferramenta nativa pode possuir mecanismos próprios para realizar determinadas operações sem exigir que todo o programa seja executado como administrador.
Quando desenvolvemos uma ferramenta Go que precisa funcionar em macOS e Linux, podemos aproveitar os pacotes golang.org/x/net/icmp e golang.org/x/net/ipv4 para determinadas operações. Mesmo assim, precisamos testar o comportamento real em cada plataforma, porque o suporte a opções de socket e o formato das informações recebidas podem variar conforme o sistema operacional.
Por que os resultados podem ser diferentes
Uma diferença importante é que diferentes métodos de traceroute podem atravessar caminhos distintos. Em redes que usam balanceamento de carga, roteadores podem encaminhar pacotes conforme informações como endereço de origem, destino, protocolo e portas. Isso significa que sondas UDP, TCP e ICMP não precisam seguir obrigatoriamente o mesmo caminho, mesmo quando possuem o mesmo destino final.
Também existem dispositivos que não respondem às mensagens ICMP usadas pelo traceroute. Um roteador pode encaminhar normalmente os pacotes destinados a outros serviços e, ao mesmo tempo, ignorar sondas de diagnóstico ou limitar a quantidade de respostas enviadas. Portanto, um salto apresentado com asteriscos não comprova que existe uma falha naquele ponto da rede.
Outro cuidado envolve a medição de tempo. O valor apresentado para um salto representa o intervalo observado entre o envio da sonda e o recebimento da resposta correspondente, incluindo o caminho de ida, o processamento da mensagem e o caminho de retorno. Como essas etapas podem variar, o tempo de um roteador intermediário não deve ser interpretado isoladamente como uma medida precisa do atraso de encaminhamento.
Permissões necessárias para raw socket e ICMP
Raw sockets são recursos de rede que permitem acessar ou construir pacotes em um nível mais próximo do protocolo IP, sem depender exclusivamente das abstrações oferecidas por conexões TCP e sockets UDP comuns. Eles são úteis para implementar determinadas ferramentas de diagnóstico, protocolos e mecanismos de comunicação que precisam trabalhar diretamente com cabeçalhos ou mensagens de controle.
Como essas operações permitem um controle maior sobre o tráfego de rede, os sistemas operacionais normalmente aplicam restrições de acesso. Um programa que cria conexões TCP comuns não precisa necessariamente das mesmas permissões exigidas para abrir um raw socket ICMP. Por isso, uma ferramenta pode conseguir executar verificações de portas normalmente e falhar quando tenta iniciar uma operação de traceroute implementada diretamente com ICMP.
Permissões no Linux
No Linux, a criação de raw sockets normalmente exige a capacidade CAP_NET_RAW no contexto apropriado. Capacidades são mecanismos que permitem conceder permissões específicas a processos, separando determinadas operações privilegiadas do acesso administrativo completo. Isso permite, em alguns casos, executar recursos de rede de baixo nível sem conceder todas as permissões associadas ao usuário root.
Quando um processo não possui as permissões necessárias, uma tentativa de criar um raw socket pode retornar um erro de permissão, frequentemente associado a EPERM. Em Go, esse erro aparece como resultado da função responsável pela criação do socket. Podemos tratá-lo e apresentar uma mensagem explicando que a operação exige permissões adicionais.
package main
import (
"errors"
"fmt"
"net"
"os"
"syscall"
)
func main() {
// Tenta abrir um socket ICMP bruto.
conn, err := net.ListenPacket(
"ip4:icmp",
"0.0.0.0",
)
if err != nil {
// Identifica uma falha de permissão.
if errors.Is(err, syscall.EPERM) ||
errors.Is(err, syscall.EACCES) {
fmt.Fprintln(
os.Stderr,
"Permissão insuficiente para raw socket.",
)
return
}
fmt.Fprintln(
os.Stderr,
"Erro ao criar socket:",
err,
)
return
}
defer conn.Close()
fmt.Println("Socket ICMP criado.")
}
O programa tenta abrir um socket ICMP bruto usando net.ListenPacket. Se a operação falhar, verifica se o erro corresponde a uma condição conhecida de permissão e apresenta uma mensagem específica. Isso permite diferenciar problemas de acesso de outras falhas, como uma opção não suportada pelo sistema ou uma configuração de rede incompatível.
Em ambientes Linux que permitem atribuir capacidades diretamente a executáveis, podemos conceder CAP_NET_RAW a um programa específico em vez de executar todo o processo como root. Entretanto, essa capacidade fornece acesso a operações de rede privilegiadas e deve ser concedida somente quando realmente necessária. Também precisamos considerar que contêineres e outros mecanismos de isolamento podem impor restrições adicionais.
O Linux também possui mecanismos de sockets ICMP de eco que podem funcionar sem privilégios administrativos, dependendo da configuração de grupos permitidos. Entretanto, esses sockets oferecem funcionalidades mais restritas do que raw sockets completos e não substituem automaticamente todos os recursos necessários para implementar um traceroute. A disponibilidade depende da versão do kernel, das permissões e da interface usada pela aplicação.
Permissões no Windows
No Windows, o acesso a raw sockets segue regras próprias do sistema operacional e da API Winsock. Algumas operações podem exigir privilégios administrativos, enquanto outras possuem restrições específicas mesmo quando o processo está sendo executado com permissões elevadas. Isso significa que não devemos assumir que o comportamento de um raw socket no Linux será reproduzido automaticamente no Windows.
Para operações comuns de diagnóstico ICMP, o Windows disponibiliza APIs específicas, como IcmpSendEcho, que podem ser usadas para enviar solicitações de eco e receber respostas sem construir manualmente toda a comunicação por raw sockets. O comando ping e a ferramenta tracert também disponibilizam funcionalidades prontas que normalmente podem ser executadas por usuários comuns.
Quando desenvolvemos uma aplicação Go que precisa trabalhar diretamente com essas APIs, normalmente precisamos criar uma integração específica para Windows. Isso pode envolver pacotes próprios da plataforma e tratamento de diferentes estruturas de dados, porque essas interfaces não possuem necessariamente a mesma organização das funções disponibilizadas pelo pacote net.
Permissões no macOS
No macOS, raw sockets também são considerados recursos de rede privilegiados. Programas que precisam abrir determinados sockets de baixo nível podem exigir permissões administrativas, dependendo da operação e das restrições aplicadas ao processo. Essas regras fazem parte dos mecanismos de segurança do sistema e não podem ser ignoradas apenas porque o programa foi desenvolvido em Go.
Para ferramentas de diagnóstico, normalmente é melhor usar interfaces que não exigem privilégios elevados quando elas oferecem as informações necessárias. O comando nativo traceroute, por exemplo, pode trabalhar com métodos e mecanismos próprios da plataforma. Isso evita conceder permissões adicionais a um programa que precisa apenas apresentar informações básicas sobre o caminho da rede.
Quando o acesso a raw sockets for realmente necessário, devemos limitar as operações executadas com privilégios elevados e encerrar os recursos assim que deixarem de ser necessários. Também precisamos considerar que permissões e comportamentos podem mudar entre versões do sistema, por isso uma implementação deve ser testada no ambiente em que será executada.
Diferença entre raw socket e socket comum
Sockets TCP e UDP comuns são criados usando interfaces que permitem ao sistema operacional administrar grande parte das operações dos protocolos. Em uma conexão TCP, por exemplo, o sistema controla o estabelecimento da conexão, a retransmissão de segmentos e a organização do fluxo de bytes. Isso permite que a aplicação trabalhe com Read e Write sem precisar construir manualmente os cabeçalhos TCP.
Raw sockets disponibilizam um nível de acesso diferente, permitindo que determinadas informações de protocolos sejam manipuladas mais diretamente. Isso é útil para construir mensagens ICMP e trabalhar com características específicas dos pacotes IP. Entretanto, também exige maior conhecimento da estrutura dos protocolos e atenção às diferenças entre os sistemas operacionais.
| Operação | Recurso usado | Permissão típica |
|---|---|---|
| Estabelecer conexão TCP | Socket TCP comum | Usuário comum |
| Enviar datagrama UDP | Socket UDP comum | Usuário comum |
| Escutar uma porta TCP alta | Socket TCP comum | Usuário comum |
| Enviar ICMP Echo por API específica | Interface própria do sistema | Depende da plataforma e da configuração |
| Criar raw socket ICMP | Raw socket | Pode exigir privilégios elevados |
| Construir pacotes IP diretamente | Raw socket e APIs específicas | Normalmente exige permissões adicionais |
Essas permissões representam comportamentos comuns, mas não são garantias universais. Ambientes com políticas de segurança restritivas, contêineres, sandboxes ou regras administrativas podem impedir operações normalmente disponíveis para usuários comuns. Também existem diferenças relacionadas às portas escolhidas, principalmente em sistemas que restringem a associação de serviços a portas consideradas privilegiadas.
Por isso, uma ferramenta de diagnóstico deve tratar falhas de permissão como erros esperados e apresentar mensagens que expliquem qual operação não pôde ser realizada. Sempre que possível, também deve oferecer alternativas menos privilegiadas, permitindo executar os testes necessários sem exigir acesso administrativo para todas as funcionalidades.
Cuidados com privilégios e segurança
Executar ferramentas de rede com permissões elevadas aumenta a responsabilidade sobre o código que será executado. Um programa com acesso a raw sockets pode realizar operações que não estão disponíveis para processos comuns, por isso devemos limitar suas funcionalidades e validar os argumentos recebidos. Também precisamos evitar conceder privilégios administrativos apenas para contornar erros que poderiam ser resolvidos usando outra abordagem.
Quando uma ferramenta precisa trabalhar com raw sockets, normalmente é melhor separar as operações privilegiadas das demais funcionalidades. Uma aplicação pode executar testes TCP e UDP comuns sem permissões especiais e solicitar recursos adicionais somente quando o usuário escolher uma operação que realmente dependa deles. Essa organização reduz a quantidade de código executado com privilégios elevados.
Também precisamos considerar que diagnósticos de rede podem gerar tráfego em sistemas externos. Ferramentas como scanners de portas e traceroute devem ser executadas em ambientes autorizados, com limites de tempo, quantidade de tentativas e concorrência adequados. Esses cuidados ajudam a evitar consumo desnecessário de recursos e reduzem a possibilidade de interpretar falhas provocadas pela própria ferramenta como problemas existentes na rede.