Síťový skener pracující na vrstvách L2 a L3 - najde aktivní zařízení v zadané podsíti a vrátí jejich MAC a IP adresy, ať už jde o IPv4 nebo IPv6. Je to čistě CLI nástroj, výsledky se vypisují přímo do terminálu. Kvůli práci s raw sockety potřebuje běžet s sudo.
Každé zařízení připojené k síti má IP adresu, ale nepřiděluje se libovolně - adresní prostor je rozkrájený na podsítě. Zápis 192.168.1.0/24 říká, že prvních 24 bitů adresy určuje síť a zbylých 8 konkrétní zařízení v ní. Z toho plyne 256 adres, z nichž první patří síti a poslední broadcastu, takže na zařízení zbývá 254.
Není to nic exotického: vlastní podsíť má doma každý. Notebook, telefon i televize dostaly od routeru adresu se stejnými prvními 24 bity a liší se posledním číslem - proto spolu můžou mluvit napřímo.
Právě tohle rozdělení je důvod, proč skener kombinuje dva druhy protokolů. Zařízení uvnitř téže podsítě se domlouvají přímo přes MAC adresy na linkové vrstvě (L2). Cokoliv mimo ni musí přes router a řeší se až na síťové vrstvě (L3).
Pro skener je podsíť vymezením práce: „projdi 10.0.1.0/24“ znamená vyzkoušet 254 adres. U IPv6 platí totéž, jen v jiném měřítku - běžná domácí podsíť /64 obsahuje přes 18 trilionů adres. Projít je nelze ani teoreticky, takže se u IPv6 skenují jen malé rozsahy nebo konkrétní adresy.
Smysl kombinace: každý protokol vidí něco jiného. ARP a NDP dají MAC adresu a jsou spolehlivé, ale nedosáhnou za hranici segmentu. ICMP dosáhne dál, zato se dá snadno odfiltrovat. Zkoušet obojí naráz dá úplnější obrázek - a ve výstupu je pak vidět, který z nich uspěl. Proto taky ScanResult drží čtyři nezávislé příznaky místo jediného „našel / nenašel“.
Jeden příkaz, jedna podsíť, seznam nalezených hostů. Tenhle výstup je skutečný záznam z testování na domácí síti:
sudo ./ipk-L2L3-scan -i enp0s3 -s 10.0.1.48/24Scanning ranges:
10.0.1.0/24 254
10.0.1.1 arp FAIL, icmpv4 FAIL
10.0.1.11 arp OK (be-87-5a-ad-ea-55), icmpv4 OK
10.0.1.18 arp OK (60-cf-84-62-68-59), icmpv4 FAIL
10.0.1.19 arp OK (c8-8a-d8-0a-cb-2c), icmpv4 OK
10.0.1.39 arp OK (70-54-b4-8d-73-7b), icmpv4 OK
10.0.1.48 arp FAIL, icmpv4 OK
10.0.1.138 arp OK (24-e4-ce-60-de-d1), icmpv4 OK10.0.1.18 je fyzický stroj, na kterém běžela testovací VM (proto selhává ICMP), 10.0.1.48 je VM samotná (proto selhává ARP) - obojí odpovídá očekávání, ne chybě.
Dvě abstrakce dělají hlavní logiku prakticky protokolově nezávislou - a právě na nich stojí čitelnost celého projektu.
Parsování podsítě vrací buď IPv4Subnet, nebo IPv6Subnet, ale volající kód pracuje jen s abstraktní Subnet - nemusí nikde větvit podle verze IP.
class Subnet {
public:
virtual ~Subnet() = default;
virtual std::string toString() const = 0;
virtual uint64_t hostCount() const = 0;
virtual std::unique_ptr<IPGenerator> getGenerator() const = 0;
static std::unique_ptr<Subnet> parse(const std::string& input);
// ...vrací IPv4Subnet nebo IPv6Subnet, volající to nemusí řešit
};ARP, ICMPv4, NDP a ICMPv6 mají naprosto odlišné pakety, ale všechny stojí za stejným rozhraním: pošli požadavek, přijmi odpověď, řekni jestli umíš danou IP verzi.
class NetworkScanner {
public:
virtual ~NetworkScanner() = default;
virtual void sendRequest(const std::string& ip) = 0;
virtual void receiveResponse(std::unordered_map<std::string, ScanResult>& results) = 0;
virtual int getSocket() const = 0;
virtual bool supports(const std::string& ip) const = 0;
};
// implementují: ARPScanner, ICMPv4Scanner, NDPScanner, ICMPv6ScannerVýsledek: main() neví nic o ARP/ICMP/NDP paketech, jenom skládá dohromady podsítě, skenery a výsledky.
int main(int argc, char** argv) {
std::signal(SIGINT, handle_signal);
std::signal(SIGTERM, handle_signal);
Arguments args = processArguments(argc, argv);
optionalDisplayHelp(args);
optionalDisplayInterfaces(args);
std::vector<std::unique_ptr<Subnet>> subnets = getSubnets(args);
displayScanningRanges(subnets);
std::unordered_map<std::string, ScanResult> results;
std::vector<std::unique_ptr<NetworkScanner>> scanners = initializeScanners(args);
sendAllRequestsLazy(subnets, scanners, results);
collectResponses(scanners, results, args.timeout);
displayResultsLazy(subnets, results);
return static_cast<int>(ExitCode::SUCCESS);
}Podsíť /16 má 65 534 adres, /8 přes šestnáct milionů. Kdyby si program nejdřív vyrobil seznam všech, zaplatil by za to pamětí i čekáním dřív, než odešle první paket.
class IPGenerator {
public:
virtual ~IPGenerator() = default;
virtual bool hasNext() const = 0;
virtual std::string next() = 0;
};Rozhraní má jen dvě metody a vydává adresy po jedné, takže program nikdy nedrží víc než jednu. Implementace jsou dvě: IPv4Generator inkrementuje 32bitové číslo, IPv6Generator musí řešit přenos přes celé 16bajtové pole.
Použije se dvakrát - při rozesílání požadavků i při výpisu výsledků. Odtud názvy sendAllRequestsLazy() a displayResultsLazy().
Naivní postup - pošli dotaz, počkej na odpověď, jdi na další adresu - je při skenování k ničemu. Při timeoutu 500 ms na každou z 254 adres by průchod jedné /24 trval přes dvě minuty, i kdyby většina zařízení odpovídala okamžitě.
Program to obrací: nejdřív rozešle požadavky na všechny adresy, teprve pak sbírá odpovědi. Sokety jsou neblokující a čeká se na nich přes select(), takže jedno vlákno obsluhuje všechny protokoly najednou. Každý protokol má vlastní soket - proto má rozhraní NetworkScanner metodu getSocket().
Odpovědi tak chodí v libovolném pořadí a zapisují se do mapy výsledků podle IP adresy. U ICMP k tomu slouží sekvenční číslo požadavku, které se přes mapu seq_to_ip_ páruje zpátky na adresu.
Rozeslat desetitisíce paketů tak rychle, jak to jen jde, není dobrý nápad. Přeteče odesílací buffer, aktivní prvky můžou provoz vyhodnotit jako útok a část paketů se cestou ztratí. Výsledkem jsou falešně negativní odpovědi - zařízení tam je, ale sken ho nenajde.
Program proto odesílání přiškrcuje jednoduchým omezovačem. Je to vědomý kompromis: o něco pomalejší sken výměnou za výsledek, kterému se dá věřit.
U nástroje spouštěného z příkazové řádky je návratový kód často jediné, co se o jeho osudu dozví volající skript. Místo magických čísel je proto v projektu výčtový typ:
enum class ExitCode {
SUCCESS = 0,
ARGUMENT_PARSING_ERROR = 3,
INTERFACE_LISTING_ERROR = 4,
SUBNET_PARSING_ERROR = 5,
SOCKET_ERROR = 6,
INTERFACE_ERROR = 7,
};Fatální chyby se hlásí přes std::exit(static_cast<int>(code)), takže je i v kódu na první pohled vidět, co přesně selhalo.
| Složka | Zodpovědnost |
|---|---|
| /args | Parsování a validace přepínačů příkazové řádky |
| /core | Orchestrace — inicializace skenerů a podsítí, rozeslání požadavků, sběr odpovědí, výpis výsledků |
| /scanner | Implementace jednotlivých protokolů: sestavení paketu, odeslání, rozpoznání platné odpovědi |
| /utils | Podsítě, generátory adres, struktura výsledku, návratové kódy, obsluha signálů a kontrola systémových volání |
| /tests | Jednotkové testy (doctest) |
| /extern | doctest jako jediná externí závislost |
Dělení kopíruje tok programu: /args zjistí co, /utils to přeloží na konkrétní adresy, /scanner se ptá sítě a /core to celé řídí. Jediná složka, která ví o raw socketech a hlavičkách paketů, je /scanner.
| Třída | Zodpovědnost |
|---|---|
| ArgumentParser | Projde argv, naplní strukturu Arguments a ověří, že dává smysl |
| Subnet | Abstraktní podsíť — společné rozhraní a tovární metoda parse() |
| IPv4Subnet / IPv6Subnet | Kontrola prefixu, normalizace zápisu, výpočet rozsahu a počtu hostů |
| IPGenerator | Abstraktní zdroj adres vydávaných po jedné |
| IPv4Generator / IPv6Generator | Inkrementace 32bitového čísla, resp. 16bajtového pole s přenosem |
| NetworkScanner | Kontrakt skeneru: supports, sendRequest, receiveResponse, getSocket |
| ARPScanner / NDPScanner | Zjištění MAC adresy na druhé vrstvě (IPv4 / IPv6) |
| ICMPv4Scanner / ICMPv6Scanner | Ověření dostupnosti na třetí vrstvě, párování odpovědí přes sekvenční číslo |
| ScanResult | Výsledek pro jednu IP: čtyři nezávislé příznaky a MAC adresa |
Tři abstraktní rozhraní a k nim devět konkrétních tříd - to je celý objektový návrh. Přidat pátý protokol znamená napsat jednu třídu dědící z NetworkScanner a zaregistrovat ji v initializeScanners(); nikde jinde se nesahá.
Automatizované testy (doctest, spuštěné přes make test) pokrývají parsování argumentů, podsítí, generátory IP adres, tovární metodu i orchestraci skeneru pomocí mock objektů.
Ruční testování proběhlo na reálné domácí síti - výsledky skeneru byly porovnány s nmap, ping6 a systémovou tabulkou ip neigh a odpovídaly si, včetně hraničních případů jako lokální VM nebo hostitelský stroj.