mirror of
https://github.com/pi-hole/FTL.git
synced 2024-10-26 16:52:18 +02:00
Merge pull request #1557 from pi-hole/new/arp-scan
Add local network scanning feature (ARP)
This commit is contained in:
+2
-2
@@ -116,8 +116,6 @@ set(sources
|
||||
daemon.h
|
||||
datastructure.c
|
||||
datastructure.h
|
||||
dhcp-discover.c
|
||||
dhcp-discover.h
|
||||
dnsmasq_interface.c
|
||||
dnsmasq_interface.h
|
||||
edns0.c
|
||||
@@ -180,6 +178,7 @@ add_executable(pihole-FTL
|
||||
$<TARGET_OBJECTS:lua>
|
||||
$<TARGET_OBJECTS:tre-regex>
|
||||
$<TARGET_OBJECTS:syscalls>
|
||||
$<TARGET_OBJECTS:tools>
|
||||
)
|
||||
if(STATIC STREQUAL "true")
|
||||
set_target_properties(pihole-FTL PROPERTIES LINK_SEARCH_START_STATIC ON)
|
||||
@@ -253,3 +252,4 @@ add_subdirectory(lua)
|
||||
add_subdirectory(lua/scripts)
|
||||
add_subdirectory(tre-regex)
|
||||
add_subdirectory(syscalls)
|
||||
add_subdirectory(tools)
|
||||
|
||||
+29
-11
@@ -33,7 +33,9 @@
|
||||
// LUA dependencies
|
||||
#include "lua/ftl_lua.h"
|
||||
// run_dhcp_discover()
|
||||
#include "dhcp-discover.h"
|
||||
#include "tools/dhcp-discover.h"
|
||||
// run_arp_scan()
|
||||
#include "tools/arp-scan.h"
|
||||
// defined in dnsmasq.c
|
||||
extern void print_dnsmasq_version(const char *yellow, const char *green, const char *bold, const char *normal);
|
||||
|
||||
@@ -163,6 +165,24 @@ void parse_args(int argc, char* argv[])
|
||||
(argc > 1 && strEndsWith(argv[1], ".db")))
|
||||
exit(sqlite3_shell_main(argc, argv));
|
||||
|
||||
// DHCP discovery mode
|
||||
if(argc > 1 && strcmp(argv[1], "dhcp-discover") == 0)
|
||||
{
|
||||
// Enable stdout printing
|
||||
cli_mode = true;
|
||||
exit(run_dhcp_discover());
|
||||
}
|
||||
|
||||
// ARP scanning mode
|
||||
if(argc > 1 && strcmp(argv[1], "arp-scan") == 0)
|
||||
{
|
||||
// Enable stdout printing
|
||||
cli_mode = true;
|
||||
const bool scan_all = argc > 2 && strcmp(argv[2], "-a") == 0;
|
||||
const bool extreme_mode = argc > 2 && strcmp(argv[2], "-x") == 0;
|
||||
exit(run_arp_scan(scan_all, extreme_mode));
|
||||
}
|
||||
|
||||
// start from 1, as argv[0] is the executable name
|
||||
for(int i = 1; i < argc; i++)
|
||||
{
|
||||
@@ -415,14 +435,6 @@ void parse_args(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
// Regex test mode
|
||||
if(strcmp(argv[i], "dhcp-discover") == 0)
|
||||
{
|
||||
// Enable stdout printing
|
||||
cli_mode = true;
|
||||
exit(run_dhcp_discover());
|
||||
}
|
||||
|
||||
// List of implemented arguments
|
||||
if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "help") == 0 || strcmp(argv[i], "--help") == 0)
|
||||
{
|
||||
@@ -495,13 +507,19 @@ void parse_args(int argc, char* argv[])
|
||||
|
||||
printf("%sDebugging and special use:%s\n", yellow, normal);
|
||||
printf("\t%sd%s, %sdebug%s Enter debugging mode\n", green, normal, green, normal);
|
||||
printf("\t%stest%s Don't start pihole-FTL but\n", green, normal);
|
||||
printf("\t instead quit immediately\n");
|
||||
printf("\t%stest%s Don't start pihole-FTL but instead\n", green, normal);
|
||||
printf("\t quit immediately\n");
|
||||
printf("\t%s-f%s, %sno-daemon%s Don't go into daemon mode\n\n", green, normal, green, normal);
|
||||
|
||||
printf("%sOther:%s\n", yellow, normal);
|
||||
printf("\t%sdhcp-discover%s Discover DHCP servers in the local\n", green, normal);
|
||||
printf("\t network\n");
|
||||
printf("\t%sarp-scan %s[-a/-x]%s Use ARP to scan local network for\n", green, cyan, normal);
|
||||
printf("\t possible IP conflicts\n");
|
||||
printf("\t Append %s-a%s to force scan on all\n", cyan, normal);
|
||||
printf("\t interfaces\n");
|
||||
printf("\t Append %s-x%s to force scan on all\n", cyan, normal);
|
||||
printf("\t interfaces and scan 10x more often\n");
|
||||
printf("\t%s-h%s, %shelp%s Display this help and exit\n\n", green, normal, green, normal);
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,47 @@ static const unsigned int capabilityIDs[] = { CAP_CHOWN , CAP_DAC_OVERRIDE ,
|
||||
static const char* capabilityNames[] = {"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_DAC_READ_SEARCH", "CAP_FOWNER", "CAP_FSETID", "CAP_KILL", "CAP_SETGID", "CAP_SETUID", "CAP_SETPCAP", "CAP_LINUX_IMMUTABLE", "CAP_NET_BIND_SERVICE", "CAP_NET_BROADCAST", "CAP_NET_ADMIN", "CAP_NET_RAW", "CAP_IPC_LOCK", "CAP_IPC_OWNER", "CAP_SYS_MODULE", "CAP_SYS_RAWIO", "CAP_SYS_CHROOT", "CAP_SYS_PTRACE", "CAP_SYS_PACCT", "CAP_SYS_ADMIN", "CAP_SYS_BOOT", "CAP_SYS_NICE", "CAP_SYS_RESOURCE", "CAP_SYS_TIME", "CAP_SYS_TTY_CONFIG", "CAP_MKNOD", "CAP_LEASE", "CAP_AUDIT_WRITE", "CAP_AUDIT_CONTROL", "CAP_SETFCAP"};
|
||||
static const unsigned int numCaps = sizeof(capabilityIDs) / sizeof(*capabilityIDs);
|
||||
|
||||
bool check_capability(const unsigned int cap)
|
||||
{
|
||||
// First assume header version 1
|
||||
int capsize = 1; // VFS_CAP_U32_1
|
||||
cap_user_data_t data = NULL;
|
||||
cap_user_header_t hdr = calloc(sizeof(*hdr), capsize);
|
||||
|
||||
// Determine capabilities version used by the current kernel
|
||||
capget(hdr, NULL);
|
||||
|
||||
// Check version
|
||||
if (hdr->version != LINUX_CAPABILITY_VERSION_1)
|
||||
{
|
||||
// If unknown version, use largest supported version (3)
|
||||
// Version 2 is deprecated according to linux/capability.h
|
||||
if (hdr->version != LINUX_CAPABILITY_VERSION_2)
|
||||
{
|
||||
hdr->version = LINUX_CAPABILITY_VERSION_3;
|
||||
capsize = 2; // VFS_CAP_U32_3
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use version 2
|
||||
capsize = 2; // VFS_CAP_U32_2
|
||||
}
|
||||
}
|
||||
|
||||
// Get current capabilities
|
||||
data = calloc(sizeof(*data), capsize);
|
||||
capget(hdr, data);
|
||||
|
||||
// Check if the capability is available
|
||||
const bool available = ((data->permitted & (1 << cap)) && (data->effective & (1 << cap)));
|
||||
|
||||
// Free memory
|
||||
free(hdr);
|
||||
free(data);
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
bool check_capabilities(void)
|
||||
{
|
||||
// First assume header version 1
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#ifndef CAPABILITIES_H
|
||||
#define CAPABILITIES_H
|
||||
|
||||
bool check_capability(const unsigned int cap);
|
||||
bool check_capabilities(void);
|
||||
|
||||
#endif //CAPABILITIES_H
|
||||
|
||||
@@ -32,11 +32,14 @@ ssize_t FTLrecvfrom(int sockfd, void *buf, size_t len, int flags, struct sockadd
|
||||
// Backup errno value
|
||||
const int _errno = errno;
|
||||
|
||||
// Final error checking (may have failed for some other reason then an
|
||||
// EINTR = interrupted system call)
|
||||
if(ret < 0)
|
||||
// Final error checking. May have failed for some other reason then an
|
||||
// EINTR = interrupted system call. In that case, log a warning However,
|
||||
// if the error is EAGAIN, this is not an error, but just a non-blocking
|
||||
// socket that has no data available or we ran into an (expected)
|
||||
// timeout. In that case, do not log a warning
|
||||
if(ret < 0 && errno != EAGAIN)
|
||||
logg("WARN: Could not recvfrom() in %s() (%s:%i): %s",
|
||||
func, file, line, strerror(errno));
|
||||
func, file, line, strerror(errno));
|
||||
|
||||
// Restore errno value
|
||||
errno = _errno;
|
||||
|
||||
@@ -33,8 +33,8 @@ ssize_t FTLsendto(int sockfd, void *buf, size_t len, int flags, const struct soc
|
||||
const int _errno = errno;
|
||||
|
||||
// Final error checking (may have failed for some other reason then an
|
||||
// EINTR = interrupted system call)
|
||||
if(ret < 0)
|
||||
// EINTR = interrupted system call), also ignore EPROTONOSUPPORT (ARP scanning)
|
||||
if(ret < 0 && errno != EPROTONOSUPPORT)
|
||||
logg("WARN: Could not sendto() in %s() (%s:%i): %s",
|
||||
func, file, line, strerror(errno));
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Pi-hole: A black hole for Internet advertisements
|
||||
# (c) 2020 Pi-hole, LLC (https://pi-hole.net)
|
||||
# Network-wide ad blocking via your own hardware.
|
||||
#
|
||||
# FTL Engine
|
||||
# /src/tools/CMakeList.txt
|
||||
#
|
||||
# This file is copyright under the latest version of the EUPL.
|
||||
# Please see LICENSE file for your rights under this license.
|
||||
|
||||
set(tools_sources
|
||||
arp-scan.c
|
||||
arp-scan.h
|
||||
dhcp-discover.c
|
||||
dhcp-discover.h
|
||||
)
|
||||
|
||||
add_library(tools OBJECT ${tools_sources})
|
||||
target_compile_options(tools PRIVATE "${EXTRAWARN}")
|
||||
target_include_directories(tools PRIVATE ${PROJECT_SOURCE_DIR}/src)
|
||||
@@ -0,0 +1,735 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2023 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* FTL Engine
|
||||
* ARP scanning routines
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
// Inspired by https://stackoverflow.com/a/39287433 but heavily modified
|
||||
|
||||
#include "FTL.h"
|
||||
#include "arp-scan.h"
|
||||
#include "log.h"
|
||||
// get_hardware_address()
|
||||
#include "dhcp-discover.h"
|
||||
// sleepms()
|
||||
#include "timers.h"
|
||||
// check_capability()
|
||||
#include "capabilities.h"
|
||||
#include <linux/capability.h>
|
||||
|
||||
#include <linux/if_packet.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/if_arp.h>
|
||||
//htons etc
|
||||
#include <arpa/inet.h>
|
||||
|
||||
// How many threads do we spawn at maximum?
|
||||
// This is also the limit for interfaces
|
||||
// we scan for DHCP activity.
|
||||
#define MAXTHREADS 32
|
||||
|
||||
// How many MAC addresses do we store per IP address?
|
||||
#define MAX_MACS 3
|
||||
|
||||
// How many ARP requests do we send per IP address?
|
||||
#define NUM_SCANS 10
|
||||
|
||||
// How long do we wait for ARP replies in each scan [seconds]?
|
||||
#define ARP_TIMEOUT 1
|
||||
|
||||
// Protocol definitions
|
||||
#define PROTO_ARP 0x0806
|
||||
#define ETH2_HEADER_LEN 14
|
||||
#define HW_TYPE 1
|
||||
#define MAC_LENGTH 6
|
||||
#define IPV4_LENGTH 4
|
||||
#define ARP_REQUEST 0x01
|
||||
#define ARP_REPLY 0x02
|
||||
#define BUF_SIZE 60
|
||||
|
||||
// ARP header struct
|
||||
// See https://en.wikipedia.org/wiki/Address_Resolution_Protocol#Packet_structure
|
||||
#pragma pack(push, 1)
|
||||
struct arp_header {
|
||||
unsigned short hardware_type;
|
||||
unsigned short protocol_type;
|
||||
unsigned char hardware_len;
|
||||
unsigned char protocol_len;
|
||||
unsigned short opcode;
|
||||
unsigned char sender_mac[MAC_LENGTH];
|
||||
unsigned char sender_ip[IPV4_LENGTH];
|
||||
unsigned char target_mac[MAC_LENGTH];
|
||||
unsigned char target_ip[IPV4_LENGTH];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct arp_result {
|
||||
struct device {
|
||||
unsigned char replied[NUM_SCANS];
|
||||
unsigned char mac[MAC_LENGTH];
|
||||
} device[MAX_MACS];
|
||||
};
|
||||
|
||||
struct arp_result_extreme {
|
||||
struct device_extreme {
|
||||
// In extreme mode, we can scan up to 10x more often
|
||||
unsigned char replied[10*NUM_SCANS];
|
||||
unsigned char mac[MAC_LENGTH];
|
||||
} device[MAX_MACS];
|
||||
};
|
||||
|
||||
enum status {
|
||||
STATUS_INITIALIZING = 0,
|
||||
STATUS_SKIPPED_CIDR_MISMATCH,
|
||||
STATUS_SCANNING,
|
||||
STATUS_ERROR,
|
||||
STATUS_COMPLETE
|
||||
} __attribute__ ((packed));
|
||||
|
||||
struct thread_data {
|
||||
bool scan_all :1;
|
||||
bool extreme :1;
|
||||
char iface[IF_NAMESIZE + 1];
|
||||
char ipstr[INET_ADDRSTRLEN];
|
||||
unsigned char mac[MAC_LENGTH];
|
||||
enum status status;
|
||||
int dst_cidr;
|
||||
unsigned int num_scans;
|
||||
unsigned int total_scans;
|
||||
size_t result_size;
|
||||
uint32_t scanned_addresses;
|
||||
const char *error;
|
||||
struct ifaddrs *ifa;
|
||||
union {
|
||||
struct arp_result_extreme *result_extreme;
|
||||
struct arp_result *result;
|
||||
};
|
||||
struct sockaddr_in src_addr;
|
||||
struct sockaddr_in dst_addr;
|
||||
struct sockaddr_in mask;
|
||||
};
|
||||
|
||||
// Sends multiple ARP who-has request on interface ifindex, using source mac src_mac and source ip src_ip.
|
||||
// Iterates over all IP addresses in the range of dst_ip/cidr.
|
||||
static int send_arps(const int fd, const int ifindex, struct thread_data *thread_data)
|
||||
{
|
||||
int err = -1;
|
||||
unsigned char buffer[BUF_SIZE];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
|
||||
// Construct the Ethernet header
|
||||
struct sockaddr_ll socket_address;
|
||||
socket_address.sll_family = AF_PACKET;
|
||||
socket_address.sll_protocol = htons(ETH_P_ARP);
|
||||
socket_address.sll_ifindex = ifindex;
|
||||
socket_address.sll_hatype = htons(ARPHRD_ETHER);
|
||||
socket_address.sll_pkttype = PACKET_BROADCAST;
|
||||
socket_address.sll_halen = MAC_LENGTH;
|
||||
socket_address.sll_addr[6] = 0;
|
||||
socket_address.sll_addr[7] = 0;
|
||||
|
||||
struct ethhdr *send_req = (struct ethhdr *) buffer;
|
||||
struct arp_header *arp_req = (struct arp_header *) (buffer + ETH2_HEADER_LEN);
|
||||
ssize_t ret;
|
||||
|
||||
// Destination is the broadcast address
|
||||
memset(send_req->h_dest, 0xff, MAC_LENGTH);
|
||||
|
||||
// Target MAC is zero (we don't know it)
|
||||
memset(arp_req->target_mac, 0x00, MAC_LENGTH);
|
||||
|
||||
// Source MAC to our own MAC address
|
||||
memcpy(send_req->h_source, thread_data->mac, MAC_LENGTH);
|
||||
memcpy(arp_req->sender_mac, thread_data->mac, MAC_LENGTH);
|
||||
memcpy(socket_address.sll_addr, thread_data->mac, MAC_LENGTH);
|
||||
|
||||
// Protocol type is ARP
|
||||
send_req->h_proto = htons(ETH_P_ARP);
|
||||
|
||||
// Create ARP request
|
||||
arp_req->hardware_type = htons(HW_TYPE);
|
||||
arp_req->protocol_type = htons(ETH_P_IP);
|
||||
arp_req->hardware_len = MAC_LENGTH;
|
||||
arp_req->protocol_len = IPV4_LENGTH;
|
||||
arp_req->opcode = htons(ARP_REQUEST);
|
||||
|
||||
// Copy IP address to arp_req
|
||||
memcpy(arp_req->sender_ip, &thread_data->src_addr.sin_addr.s_addr, sizeof(thread_data->src_addr.sin_addr.s_addr));
|
||||
|
||||
// Loop over all possible IP addresses in the range dst_ip/cidr
|
||||
// We start at 1 because the first IP address has already been set above
|
||||
struct in_addr dst_ip = thread_data->dst_addr.sin_addr;
|
||||
for(unsigned int i = 0; i < (1u << (32 - thread_data->dst_cidr)); i++)
|
||||
{
|
||||
// Fill in target IP address
|
||||
memcpy(arp_req->target_ip, &dst_ip.s_addr, sizeof(dst_ip.s_addr));
|
||||
|
||||
#ifdef DEBUG
|
||||
printf("Sending ARP request for %s@%s\n", inet_ntoa(*dst_ip), iface);
|
||||
#endif
|
||||
|
||||
// Send ARP request
|
||||
ret = sendto(fd, buffer, 42, 0, (struct sockaddr *) &socket_address, sizeof(socket_address));
|
||||
if (ret == -1)
|
||||
{
|
||||
err = errno;
|
||||
thread_data->error = strerror(err);
|
||||
goto out;
|
||||
}
|
||||
|
||||
// Increment IP address
|
||||
dst_ip.s_addr = htonl(ntohl(dst_ip.s_addr) + 1);
|
||||
|
||||
thread_data->scanned_addresses++;
|
||||
}
|
||||
|
||||
err = 0;
|
||||
out:
|
||||
return err;
|
||||
}
|
||||
|
||||
static int create_arp_socket(const int ifindex, const char *iface, const char **error)
|
||||
{
|
||||
// Create socket for ARP communications
|
||||
const int arp_socket = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP));
|
||||
if(arp_socket < 0)
|
||||
{
|
||||
*error = strerror(errno);
|
||||
#ifdef DEBUG
|
||||
printf("Unable to create socket for ARP communications on interface %s: %s\n", iface, *error);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Bind socket to interface
|
||||
struct sockaddr_ll sll;
|
||||
memset(&sll, 0, sizeof(struct sockaddr_ll));
|
||||
sll.sll_family = AF_PACKET;
|
||||
sll.sll_ifindex = ifindex;
|
||||
if (bind(arp_socket, (struct sockaddr*) &sll, sizeof(struct sockaddr_ll)) < 0)
|
||||
{
|
||||
*error = strerror(errno);
|
||||
#ifdef DEBUG
|
||||
printf("Unable to bind socket for ARP communications on interface %s: %s\n", iface, *error);
|
||||
#endif
|
||||
close(arp_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
struct timeval tv;
|
||||
tv.tv_sec = ARP_TIMEOUT;
|
||||
tv.tv_usec = 0;
|
||||
if (setsockopt(arp_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
|
||||
{
|
||||
*error = strerror(errno);
|
||||
#ifdef DEBUG
|
||||
printf("Unable to set timeout for ARP communications on interface %s: %s\n", iface, *error);
|
||||
#endif
|
||||
close(arp_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return arp_socket;
|
||||
}
|
||||
|
||||
static void add_result(struct in_addr *rcv_ip, unsigned char *sender_mac,
|
||||
struct thread_data *thread_data, const unsigned int scan_id)
|
||||
{
|
||||
|
||||
// Check if we have already found this IP address
|
||||
uint32_t i = ntohl(rcv_ip->s_addr) - ntohl(thread_data->dst_addr.sin_addr.s_addr);
|
||||
if(i >= thread_data->result_size)
|
||||
{
|
||||
printf("Received IP address %s out of range for interface %s (%u >= %zu)\n",
|
||||
inet_ntoa(*rcv_ip), thread_data->iface, i, thread_data->result_size);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save MAC address
|
||||
unsigned int j = 0;
|
||||
for(; j < MAX_MACS; j++)
|
||||
{
|
||||
unsigned char *mac = thread_data->extreme ?
|
||||
thread_data->result_extreme[i].device[j].mac :
|
||||
thread_data->result[i].device[j].mac;
|
||||
// Check if received MAC is already stored in result[i].device[j].mac
|
||||
if(memcmp(mac, sender_mac, MAC_LENGTH) == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
// Check if mac is all-zero
|
||||
if(memcmp(mac, "\x00\x00\x00\x00\x00\x00", MAC_LENGTH) == 0)
|
||||
{
|
||||
// Copy MAC address to mac
|
||||
memcpy(mac, sender_mac, MAC_LENGTH);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Memorize that we have received a reply for this IP address
|
||||
thread_data->extreme ?
|
||||
thread_data->result_extreme[i].device[j].replied[scan_id]++ :
|
||||
thread_data->result[i].device[j].replied[scan_id]++;
|
||||
}
|
||||
|
||||
// Read all ARP responses
|
||||
static ssize_t read_arp(const int fd, struct thread_data *thread_data)
|
||||
{
|
||||
ssize_t ret = 0;
|
||||
unsigned char buffer[BUF_SIZE];
|
||||
|
||||
// Read ARP responses
|
||||
while(ret >= 0)
|
||||
{
|
||||
ret = recvfrom(fd, buffer, BUF_SIZE, 0, NULL, NULL);
|
||||
if (ret == -1)
|
||||
{
|
||||
if(errno == EAGAIN)
|
||||
{
|
||||
// Timeout
|
||||
ret = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Error
|
||||
thread_data->error = strerror(errno);
|
||||
printf("recvfrom(): %s", thread_data->error);
|
||||
break;
|
||||
}
|
||||
struct ethhdr *rcv_resp = (struct ethhdr *) buffer;
|
||||
struct arp_header *arp_resp = (struct arp_header *) (buffer + ETH2_HEADER_LEN);
|
||||
if (ntohs(rcv_resp->h_proto) != PROTO_ARP)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
printf("Not an ARP packet");
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
if (ntohs(arp_resp->opcode) != ARP_REPLY)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
printf("Not an ARP reply");
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
#ifdef DEBUG
|
||||
printf("received ARP len=%ld", ret);
|
||||
#endif
|
||||
struct in_addr sender_a;
|
||||
memcpy(&sender_a.s_addr, arp_resp->sender_ip, sizeof(sender_a.s_addr));
|
||||
|
||||
#ifdef DEBUG
|
||||
printf("%-16s %-20s\t%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
thread_data->iface, inet_ntoa(sender_a),
|
||||
arp_resp->sender_mac[0],
|
||||
arp_resp->sender_mac[1],
|
||||
arp_resp->sender_mac[2],
|
||||
arp_resp->sender_mac[3],
|
||||
arp_resp->sender_mac[4],
|
||||
arp_resp->sender_mac[5]);
|
||||
#endif
|
||||
add_result(&sender_a, arp_resp->sender_mac, thread_data, thread_data->num_scans);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Convert netmask to CIDR
|
||||
static int netmask_to_cidr(struct in_addr *addr)
|
||||
{
|
||||
// Count the number of set bits in an unsigned integer
|
||||
return __builtin_popcount(addr->s_addr);
|
||||
}
|
||||
|
||||
static const char *get_hostname(const struct in_addr *addr)
|
||||
{
|
||||
// Get hostname
|
||||
struct hostent *he = gethostbyaddr(&addr->s_addr, sizeof(addr->s_addr), AF_INET);
|
||||
if(he == NULL)
|
||||
return "N/A";
|
||||
|
||||
// Allow at most 24 characters for the hostname
|
||||
static char hostname[25] = { 0 };
|
||||
strncpy(hostname, he->h_name, 24);
|
||||
|
||||
// Return hostname
|
||||
return hostname;
|
||||
}
|
||||
|
||||
static void *arp_scan_iface(void *args)
|
||||
{
|
||||
// Get thread_data pointer
|
||||
struct thread_data *thread_data = (struct thread_data*)args;
|
||||
|
||||
// Get interface details
|
||||
struct ifaddrs *ifa = thread_data->ifa;
|
||||
|
||||
// Get interface name
|
||||
const char *iface = thread_data->iface;
|
||||
|
||||
// Set interface name as thread name
|
||||
prctl(PR_SET_NAME, iface, 0, 0, 0);
|
||||
|
||||
// Get interface netmask
|
||||
memcpy(&thread_data->mask, ifa->ifa_netmask, sizeof(thread_data->mask));
|
||||
|
||||
// Convert subnet to CIDR
|
||||
thread_data->dst_cidr = netmask_to_cidr(&thread_data->mask.sin_addr);
|
||||
|
||||
// Get interface index
|
||||
const int ifindex = if_nametoindex(iface);
|
||||
|
||||
// Scan only interfaces with CIDR >= 24
|
||||
if(thread_data->dst_cidr < 24 && !thread_data->scan_all)
|
||||
{
|
||||
thread_data->status = STATUS_SKIPPED_CIDR_MISMATCH;
|
||||
#ifdef DEBUG
|
||||
printf("Skipped interface %s (%s/%i)\n", iface, thread_data->ipstr, thread_data->dst_cidr);
|
||||
#endif
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
#ifdef DEBUG
|
||||
printf("Scanning interface %s (%s/%i)...\n", iface, thread_data->ipstr, thread_data->dst_cidr);
|
||||
#endif
|
||||
thread_data->status = STATUS_SCANNING;
|
||||
|
||||
// Create socket for ARP communications
|
||||
const int arp_socket = create_arp_socket(ifindex, iface, &thread_data->error);
|
||||
|
||||
// Cannot create socket, likely a permission error
|
||||
if(arp_socket < 0)
|
||||
{
|
||||
thread_data->status = STATUS_ERROR;
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
// Get hardware address of client machine
|
||||
get_hardware_address(arp_socket, iface, thread_data->mac);
|
||||
|
||||
// Define destination IP address by masking source IP with netmask
|
||||
thread_data->dst_addr.sin_addr.s_addr = thread_data->src_addr.sin_addr.s_addr & thread_data->mask.sin_addr.s_addr;
|
||||
|
||||
// Allocate memory for ARP response buffer
|
||||
const size_t arp_result_len = 1 << (32 - thread_data->dst_cidr);
|
||||
thread_data->result_size = arp_result_len;
|
||||
if(thread_data->extreme)
|
||||
{
|
||||
// Allocate extreme memory for ARP response buffer
|
||||
struct arp_result_extreme *result = calloc(arp_result_len, sizeof(struct arp_result_extreme));
|
||||
if(result == NULL)
|
||||
{
|
||||
// Memory allocation failed due to insufficient memory being
|
||||
// available
|
||||
thread_data->status = STATUS_ERROR;
|
||||
thread_data->error = strerror(ENOMEM);
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
thread_data->result_extreme = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Allocate memory for ARP response buffer
|
||||
struct arp_result *result = calloc(arp_result_len, sizeof(struct arp_result));
|
||||
if(result == NULL)
|
||||
{
|
||||
// Memory allocation failed due to insufficient memory being
|
||||
// available
|
||||
thread_data->status = STATUS_ERROR;
|
||||
thread_data->error = strerror(ENOMEM);
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
thread_data->result = result;
|
||||
}
|
||||
|
||||
for(thread_data->num_scans = 0; thread_data->num_scans < thread_data->total_scans; thread_data->num_scans++)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
printf("Still scanning interface %s (%s/%i) %i%%...\n", iface, thread_data->ipstr, thread_data->dst_cidr, 100*scan_id/thread_data->total_scans);
|
||||
#endif
|
||||
// Send ARP requests to all IPs in subnet
|
||||
if(send_arps(arp_socket, ifindex, thread_data) != 0)
|
||||
{
|
||||
thread_data->status = STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
// Read ARP responses
|
||||
if(read_arp(arp_socket, thread_data) != 0)
|
||||
{
|
||||
thread_data->status = STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Close socket
|
||||
if(close(arp_socket) != 0)
|
||||
thread_data->status = STATUS_ERROR;
|
||||
|
||||
if(thread_data->status != STATUS_ERROR)
|
||||
thread_data->status = STATUS_COMPLETE;
|
||||
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
static void print_results(struct thread_data *thread_data)
|
||||
{
|
||||
|
||||
if(thread_data->status == STATUS_SKIPPED_CIDR_MISMATCH)
|
||||
{
|
||||
printf("Skipped interface %s (%s/%i) because of too large network (use -a or -x to force scanning this interface)\n\n",
|
||||
thread_data->iface, thread_data->ipstr, thread_data->dst_cidr);
|
||||
return;
|
||||
}
|
||||
|
||||
if(thread_data->status == STATUS_ERROR)
|
||||
{
|
||||
printf("Error scanning interface %s (%s/%i)%s%s\n\n",
|
||||
thread_data->iface, thread_data->ipstr, thread_data->dst_cidr,
|
||||
thread_data->error ? ": " : "", thread_data->error ? thread_data->error : "");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are any results
|
||||
bool any_replies = false;
|
||||
for(unsigned int i = 0; i < thread_data->result_size; i++)
|
||||
for(unsigned int j = 0; j < MAX_MACS; j++)
|
||||
for(unsigned int k = 0; k < thread_data->total_scans; k++)
|
||||
if(thread_data->extreme)
|
||||
{
|
||||
if(thread_data->result_extreme[i].device[j].replied[k])
|
||||
{
|
||||
any_replies = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(thread_data->result[i].device[j].replied[k])
|
||||
{
|
||||
any_replies = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit early if there are no results
|
||||
if(!any_replies)
|
||||
{
|
||||
printf("No devices replied on interface %s (%s/%i)\n\n",
|
||||
thread_data->iface, thread_data->ipstr, thread_data->dst_cidr);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is at least one result, print header
|
||||
printf("ARP scan on interface %s (%s/%i) finished\n",
|
||||
thread_data->iface, thread_data->ipstr, thread_data->dst_cidr);
|
||||
printf("%-16s %-16s %-24s %-17s %s\n",
|
||||
"IP address", "Interface", "Hostname", "MAC address", "Reply rate");
|
||||
|
||||
// Add our own IP address to the results so IP conflicts can be detected
|
||||
// (our own IP address is not included in the ARP scan)
|
||||
for(unsigned int i = 0; i < thread_data->total_scans; i++)
|
||||
add_result(&thread_data->src_addr.sin_addr, thread_data->mac, thread_data, i);
|
||||
|
||||
// Print results
|
||||
for(unsigned int i = 0; i < thread_data->result_size; i++)
|
||||
{
|
||||
unsigned int j = 0, replied_devices = 0;
|
||||
|
||||
// Print MAC addresses
|
||||
for(j = 0; j < MAX_MACS; j++)
|
||||
{
|
||||
// Check if result[i].mac[j] is all-zero, if so, skip this entry
|
||||
unsigned char *mac = thread_data->extreme ?
|
||||
thread_data->result_extreme[i].device[j].mac :
|
||||
thread_data->result[i].device[j].mac;
|
||||
if(memcmp(mac, "\x00\x00\x00\x00\x00\x00", 6) == 0)
|
||||
break;
|
||||
|
||||
bool replied = false;
|
||||
unsigned char replies = 0u;
|
||||
unsigned char multiple_replies = 0;
|
||||
const unsigned char *rp = thread_data->extreme ?
|
||||
thread_data->result_extreme[i].device[j].replied :
|
||||
thread_data->result[i].device[j].replied;
|
||||
|
||||
// Check if IP address replied
|
||||
for(unsigned int k = 0; k < thread_data->total_scans; k++)
|
||||
{
|
||||
replied |= rp[k] > 0;
|
||||
replies += rp[k] > 0 ? 1 : 0;
|
||||
multiple_replies += rp[k] > 1;
|
||||
}
|
||||
if(!replied)
|
||||
continue;
|
||||
|
||||
// Check if IP address replied multiple times from different MAC address
|
||||
replied_devices++;
|
||||
|
||||
// Convert IP address to string
|
||||
struct in_addr ip = { 0 };
|
||||
ip.s_addr = htonl(ntohl(thread_data->dst_addr.sin_addr.s_addr) + i);
|
||||
inet_ntop(AF_INET, &ip, thread_data->ipstr, INET_ADDRSTRLEN);
|
||||
|
||||
// Print MAC address
|
||||
printf("%-16s %-16s %-24s %02x:%02x:%02x:%02x:%02x:%02x %3u %%\n",
|
||||
thread_data->ipstr, thread_data->iface,
|
||||
get_hostname(&ip),
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
|
||||
replies * 100 / thread_data->total_scans);
|
||||
|
||||
#ifdef DEBUG
|
||||
for(unsigned int k = 0; k < thread_data->total_scans; k++)
|
||||
printf(" %s", rp[k] > 0 ? "X" : "-");
|
||||
#endif
|
||||
if(multiple_replies > 0)
|
||||
printf("INFO: Received multiple replies from %02x:%02x:%02x:%02x:%02x:%02x for %s in %i scan%s\n",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
|
||||
thread_data->ipstr, multiple_replies, multiple_replies > 1 ? "s" : "");
|
||||
}
|
||||
|
||||
// Print warning if we received multiple replies
|
||||
if(replied_devices > 1)
|
||||
printf("WARNING: Received replies for %s from %i devices\n",
|
||||
thread_data->ipstr, replied_devices);
|
||||
}
|
||||
putc('\n', stdout);
|
||||
}
|
||||
|
||||
int run_arp_scan(const bool scan_all, const bool extreme_mode)
|
||||
{
|
||||
// Check if we are capable of sending ARP packets
|
||||
if(!check_capability(CAP_NET_RAW))
|
||||
{
|
||||
puts("Error: Insufficient permissions or capabilities (needs CAP_NET_RAW). Try running as root (sudo)");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
puts("Discovering IPv4 hosts on the network using the Address Resolution Protocol (ARP)...\n");
|
||||
|
||||
// Get interface names for available interfaces on this machine
|
||||
// and launch a thread for each one
|
||||
pthread_t scanthread[MAXTHREADS];
|
||||
pthread_attr_t attr;
|
||||
// Initialize thread attributes object with default attribute values
|
||||
pthread_attr_init(&attr);
|
||||
|
||||
struct ifaddrs *addrs, *tmp;
|
||||
getifaddrs(&addrs);
|
||||
tmp = addrs;
|
||||
|
||||
// Loop until there are no more interfaces available
|
||||
// or we reached the maximum number of threads
|
||||
unsigned int tid = 0;
|
||||
|
||||
struct thread_data thread_data[MAXTHREADS] = {0};
|
||||
|
||||
while(tmp != NULL && tid < MAXTHREADS)
|
||||
{
|
||||
// Create a thread for interfaces of type AF_INET
|
||||
if(tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_INET)
|
||||
{
|
||||
thread_data[tid].ifa = tmp;
|
||||
strncpy(thread_data[tid].iface, tmp->ifa_name, sizeof(thread_data[tid].iface) - 1);
|
||||
|
||||
// Get interface IPv4 address
|
||||
memcpy(&thread_data[tid].src_addr, tmp->ifa_addr, sizeof(thread_data[tid].src_addr));
|
||||
inet_ntop(AF_INET, &thread_data[tid].src_addr.sin_addr, thread_data[tid].ipstr, INET_ADDRSTRLEN);
|
||||
|
||||
thread_data[tid].extreme = extreme_mode;
|
||||
thread_data[tid].scan_all = scan_all || extreme_mode;
|
||||
thread_data[tid].total_scans = extreme_mode ? 10*NUM_SCANS : NUM_SCANS;
|
||||
|
||||
// Always skip the loopback interface
|
||||
if(thread_data[tid].src_addr.sin_addr.s_addr != htonl(INADDR_LOOPBACK))
|
||||
{
|
||||
// Create thread
|
||||
if(pthread_create(&scanthread[tid], &attr, arp_scan_iface, &thread_data[tid] ) != 0)
|
||||
{
|
||||
printf("Unable to launch thread for interface %s, skipping...\n",
|
||||
tmp->ifa_name);
|
||||
}
|
||||
|
||||
// Increase thread ID
|
||||
tid++;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance to the next interface
|
||||
tmp = tmp->ifa_next;
|
||||
}
|
||||
|
||||
// Wait for all threads to finish scanning
|
||||
bool all_done = false;
|
||||
unsigned int progress = 0;
|
||||
while(!all_done)
|
||||
{
|
||||
all_done = true;
|
||||
uint64_t num_scans = 0, total_scans = 0;
|
||||
for(unsigned int i = 0; i < tid; i++)
|
||||
{
|
||||
if(thread_data[i].status == STATUS_INITIALIZING ||
|
||||
thread_data[i].status == STATUS_SCANNING)
|
||||
{
|
||||
// At least one thread is still scanning
|
||||
all_done = false;
|
||||
}
|
||||
if(thread_data[i].status == STATUS_SCANNING ||
|
||||
thread_data[i].status == STATUS_COMPLETE)
|
||||
{
|
||||
// Also add up scans for completed threads
|
||||
num_scans += thread_data[i].scanned_addresses;
|
||||
total_scans += thread_data[i].total_scans * thread_data[i].result_size;
|
||||
}
|
||||
}
|
||||
if(!all_done)
|
||||
{
|
||||
// Calculate progress (total number of scans / total number of addresses)
|
||||
// We add 1 to total_scans to avoid division by zero
|
||||
const unsigned int new_progress = 100 * num_scans / (total_scans + 1);
|
||||
if(new_progress > progress)
|
||||
{
|
||||
// Print progress
|
||||
printf(" %i%% ", new_progress);
|
||||
|
||||
// Update progress
|
||||
progress = new_progress;
|
||||
}
|
||||
|
||||
putc('.', stdout);
|
||||
|
||||
// Flush stdout
|
||||
fflush(stdout);
|
||||
|
||||
// Sleep for 1 second
|
||||
sleepms(1000);
|
||||
}
|
||||
}
|
||||
puts("100%\n");
|
||||
|
||||
// Wait for all threads to join back with us
|
||||
for(unsigned int i = 0; i < tid; i++)
|
||||
pthread_join(scanthread[i], NULL);
|
||||
|
||||
// Destroy the thread attributes object, since we are done with it
|
||||
pthread_attr_destroy(&attr);
|
||||
|
||||
// Free linked-list of interfaces on this client
|
||||
freeifaddrs(addrs);
|
||||
|
||||
// Loop over thread results and print them
|
||||
for(unsigned int i = 0; i < tid; i++)
|
||||
{
|
||||
// Print results
|
||||
print_results(&thread_data[i]);
|
||||
|
||||
// Free allocated memory
|
||||
if(thread_data[i].result != NULL)
|
||||
free(thread_data[i].result);
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2023 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* FTL Engine
|
||||
* ARP scanning prototypes
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
#ifndef ARP_SCAN_H
|
||||
#define ARP_SCAN_H
|
||||
|
||||
int run_arp_scan(const bool scan_all, const bool extreme_mode);
|
||||
|
||||
#endif // ARP_SCAN_H
|
||||
@@ -128,7 +128,7 @@ static int create_dhcp_socket(const char *iname)
|
||||
}
|
||||
|
||||
// determines hardware address on client machine
|
||||
static int get_hardware_address(const int sock, const char *iname, unsigned char *mac)
|
||||
int get_hardware_address(const int sock, const char *iname, unsigned char *mac)
|
||||
{
|
||||
struct ifreq ifr;
|
||||
strncpy((char *)&ifr.ifr_name, iname, sizeof(ifr.ifr_name)-1);
|
||||
@@ -12,5 +12,6 @@
|
||||
#define DHCP_DISCOVER_H
|
||||
|
||||
int run_dhcp_discover(void);
|
||||
int get_hardware_address(const int sock, const char *iname, unsigned char *mac);
|
||||
|
||||
#endif // DHCP_DISCOVER_H
|
||||
Reference in New Issue
Block a user