作者:shmily
实验题目:SCANNER简单网络扫描程序实现
实验目的:熟悉并实现网络扫描的基本原理。了解网络扫描的几种常用的方法。
实验环境:linux/windows
实验内容:用C/C++语言(必须用socket函数)编写一个扫描局域网内主机的程序。要求可以显示局域网内的主机名列表,IP地址列表,并可以显示哪些主机开放了哪些端口。
主运行机器:Linux kali 4.19.0-kali1-686-pae #1 SMP Debian 4.19.13-1kali1 (2019-01-03) i686 GNU/Linux
其他机器:Linux ubuntu 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
make
如框图所示,在获取本机ip等信息之后,可以知道当前主机位于哪一个局域网,进而利用ARP广播报文获取该局域网内存活的主机。
扫描存活主机的实现运用了双进程,新开启一个进程向所有主机发送ARP询问报文,主进程用来接收存活主机发来的ARP 应答报文。
得到所有的存活主机之后,将其保存下来,再一一获取他们的主机名称,由于linux系统提供了 gethostbyaddr()函数,可以通过ip地址获取到规范名、别名等信息,因此只需要调用这个函数即可。
获取到主机名称之后,就可以对每台主机进行端口扫描,使用的方法为TCP connect扫描。对于每个 IP 来说,扫描器要扫 65536=2^16 个端口。实验中通过多线程的方式加快其扫描速度,开设 128=2^7 个线程同时扫描,直到最后一个ip的最后一组。
这个文件中包含了可能会用到的头文件,定义了一些全局变量,主要有存活的主机信息、物理地址信息、本地IP、子网掩码和本地MAC和网卡名称。备注中由更具体的说明
#ifndef _NET_GLOBAL_H_
#define _NET_GLOBAL_H_
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#if __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 1
#include
#include
#else
#include
#include
#include
#endif
#include
#include
/* 以太网帧首部长度 */
#define ETHER_HEADER_LEN sizeof(struct ether_header)
/* 整个arp结构长度 */
#define ETHER_ARP_LEN sizeof(struct ether_arp)
/* 以太网 + 整个arp结构长度 */
#define ETHER_ARP_PACKET_LEN ETHER_HEADER_LEN + ETHER_ARP_LEN
/* MAC地址长度 */
#define MAC_ADDR_LEN 6
/* IP地址长度 */
#define IP_ADDR_LEN 4
#define IP_CHAR_MAX_LEN 50
/* 网关地址长度 */
// #define
/* 广播地址 */
#define BROADCAST_ADDR{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
void err_exit(const char *err_msg)
{
perror(err_msg);
exit(1);
}
// 活动的IP
struct hosts_alive
{
struct in_addr ip;
unsigned char mac[MAC_ADDR_LEN];
char name[0x200];
} alive_hosts[0x200];
// 子网中的IP段
struct in_addr ip_list[0x300];
// 数据链路层
struct sockaddr_ll saddr_ll;
// 本地IP、子网掩码和本地MAC
struct in_addr local_ip, subnet_mask;
unsigned char local_mac_addr[MAC_ADDR_LEN];
// 网卡名称
char if_name[0x20];
#endif
获取的本机信息有网卡接口名称、ip地址、mac地址、子网掩码和本机开放端口。其中本机开放端口调用了scan_port()函数,在之后会做说明。
实验中需要将获取到的信息保存在ifreq 结构体
当中。ifreq结构体
定义在/usr/include/net/if.h
,用来配置ip地址,激活接口,配置MTU等接口信息的。其中包含了一个接口的名字和具体内容,是个共用体,有可能是IP地址,广播地址,子网掩码,MAC号,MTU或其他内容。
结构如下:
struct ifreq
{
# define IFHWADDRLEN 6
# define IFNAMSIZ IF_NAMESIZE
union
{
char ifrn_name[IFNAMSIZ]; /* Interface name, e.g. "en0". */
} ifr_ifrn;
union
{
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
struct sockaddr ifru_netmask;
struct sockaddr ifru_hwaddr;
short int ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ]; /* Just fits the size */
char ifru_newname[IFNAMSIZ];
__caddr_t ifru_data;
} ifr_ifru;
};
因此实验中需要定义一个 struct ifreq ifr
存储网络接口信息。
接着,用PF_PACKET选项创建ARP类型的原始套接字。用 ioctl()函数 通过网卡接口名来获取该接口对应的mac地址,ip地址,接口索引。接口索引填充到设备无关的物理层地址结构sockaddr_ll
里面。该结构体位于 /usr/include/netpacket/packet.h
中:
struct sockaddr_ll
{
unsigned short int sll_family;
unsigned short int sll_protocol;
int sll_ifindex;
unsigned short int sll_hatype;
unsigned char sll_pkttype;
unsigned char sll_halen;
unsigned char sll_addr[8];
};
sll_ifindex
是网络(网卡)接口索引,代表从这个接口收发数据包;protocol
是按照网络字节顺序(network byte order),大部分定义在头文件中,设置协议时,例如 htons(ETH_P_ALL),来接收 所有的 数据包。如果要获取从指定以太网接口卡上的数据包时,在struct sockaddr_ll
中指定网络接口卡,绑定(bind)数据包到该interface上。当发送数据包时,指定sll_family, sll_addr, sll_halen, sll_ifindex, sll_protocol
就足够了。其它字段设置为0;sll_hatype
和sll_pkttype
是在接收数据包时使用的; 如果要bind, 只需要使用 sll_protocol
和sll_ifindex
就足够了。
程序中还用到了inet_pton()
和inet_ntop()
函数将ip地址在点分十进制和二进制之间做转换。
GetNetInfo()函数如下:
#include "init_net.h"
void GetNetInfo()
{
/* 网络接口 */
struct ifreq ifr;
/* socket接口 */
int socket_fd;
/* MAC */
unsigned char src_mac_addr[MAC_ADDR_LEN];
/* 源IP */
char src_ip[IP_CHAR_MAX_LEN + 1];
char netmask[IP_CHAR_MAX_LEN + 1];
char buf[ETHER_ARP_PACKET_LEN];
int i;
// 打开 socket,set as promiscuous mode
if ((socket_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1)
err_exit("socket()");
// 填充 0
bzero(&saddr_ll, sizeof(struct sockaddr_ll));
bzero(&ifr, sizeof(struct ifreq));
// 获取网卡接口名称
printf("interface: %s\n", if_name);
memcpy(ifr.ifr_name, if_name, strlen(if_name));
// 获取网卡接口索引
// by ioctl func,index is sent to ifr
if (ioctl(socket_fd, SIOCGIFINDEX, &ifr) == -1)
err_exit("ioctl() get ifindex");
saddr_ll.sll_ifindex = ifr.ifr_ifindex;//if_ifru.ifru_value
saddr_ll.sll_family = PF_PACKET;
// 获取网卡接口IP
// by ioctl func,sddr is senr to ifr
if (ioctl(socket_fd, SIOCGIFADDR, &ifr) == -1)
err_exit("ioctl() get ip");
strcpy(src_ip, inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_addr))->sin_addr));
src_ip[strlen(inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_addr))->sin_addr))] = '\0';
// change to "." format
printf("local ip: %s\n", src_ip);
inet_pton(AF_INET, src_ip, &local_ip);
// change to binary format
// 获取网卡接口MAC地址
if (ioctl(socket_fd, SIOCGIFHWADDR, &ifr) == -1)
err_exit("ioctl() get mac");
memcpy(src_mac_addr, ifr.ifr_hwaddr.sa_data, MAC_ADDR_LEN);
printf("local mac: ");
for (i = 0; i < MAC_ADDR_LEN; ++i)
{
printf("%02x", src_mac_addr[i]);
local_mac_addr[i] = src_mac_addr[i];
if (i < MAC_ADDR_LEN - 1)
putchar(':');
}
putchar('\n');
// 获取网卡接口子网掩码
if (ioctl(socket_fd, SIOCGIFNETMASK, &ifr) == -1)
err_exit("ioctl() get netmask");
strcpy(netmask, inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_netmask))->sin_addr));
netmask[strlen(inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_netmask))->sin_addr))] = '\0';
printf("netmask: %s\n", netmask);
inet_pton(AF_INET, netmask, &subnet_mask);
printf("Port: 22 Open\n");
close(socket_fd);
}
扫描出局域网内的主机用 到了ARP 协议。首先构造ARP包,ARP包需要在数据链路层中传递,因此ARP包需要加一个以太网帧首部。完整的ARP帧结构如下:
ARP报文的结构体定义在 /usr/include/netinet/if_either.h
中
struct ether_arp {
struct arphdr ea_hdr; /* fixed-size header */
u_int8_t arp_sha[ETH_ALEN]; /* sender hardware address */
u_int8_t arp_spa[4]; /* sender protocol address */
u_int8_t arp_tha[ETH_ALEN]; /* target hardware address */
u_int8_t arp_tpa[4]; /* target protocol address */
};
#define arp_hrd ea_hdr.ar_hrd
#define arp_pro ea_hdr.ar_pro
#define arp_hln ea_hdr.ar_hln
#define arp_pln ea_hdr.ar_pln
#define arp_op ea_hdr.ar_op
上面的ether_arp结构
还包含一个arp首部,位于 /usr/include/net/if_arp.h
中:
struct arphdr
{
unsigned short int ar_hrd; /* Format of hardware address. */
unsigned short int ar_pro; /* Format of protocol address. */
unsigned char ar_hln; /* Length of hardware address. */
unsigned char ar_pln; /* Length of protocol address. */
unsigned short int ar_op; /* ARP opcode (command). */
};
ar_hrd
是硬件类型,可供选择的数据在 net/if_arp.h
中,这里我们选择 ARPHRD_ETHER
也就是以太网。ar_pro
是协议类型,在 /urs/include/net/ethernet.h
中,选择 ETHERTYPE_IP
即 IP 协议。ar_hln
和 ar_pln
分别是硬件地址长度和协议地址长度。对于以太网上的 ARP 请求或应答来说分别是 **6 **和 4,我选用了宏来定义它们。ar_op
代表操作类型,位于 /usr/include/net/if_arp.h
,1为请求,2为应答。综上,我们可以得到构造ARP包的函数:
struct ether_arp *fill_arp_packet(const unsigned *local_mac_addr, struct in_addr src_ip, struct in_addr dst_ip)
{
struct ether_arp *arp_packet;
struct in_addr src_in_addr, dst_in_addr;
unsigned char dst_mac_addr[MAC_ADDR_LEN] = BROADCAST_ADDR;
/* ARP package */
arp_packet = (struct ether_arp *)malloc(ETHER_ARP_LEN);
arp_packet->arp_hrd = htons(ARPHRD_ETHER);
arp_packet->arp_pro = htons(ETHERTYPE_IP);
arp_packet->arp_hln = MAC_ADDR_LEN;
arp_packet->arp_pln = IP_ADDR_LEN;
arp_packet->arp_op = htons(ARPOP_REQUEST);
memcpy(arp_packet->arp_sha, local_mac_addr, MAC_ADDR_LEN);
memcpy(arp_packet->arp_tha, dst_mac_addr, MAC_ADDR_LEN);
memcpy(arp_packet->arp_spa, &src_ip, IP_ADDR_LEN);
memcpy(arp_packet->arp_tpa, &dst_ip, IP_ADDR_LEN);
return arp_packet;
}
其中发送者的硬件地址和 IP 地址可以确定,目标 IP 地址可以确定,硬件地址不能确定,系统发送的 ARP 报文会将其自动填充为 0。接下来就利用 pid = fork()
函数开启两个进程,子进程负责发送ARP 报文,父进程负责接收ARP报文。
void GetAliveHosts()
{
pid_t pid; // short type,is process number
int i;
for (i = 0; i < 0x200; ++i) //0x200=256
{
alive_hosts[i].ip.s_addr = 0;
}
if (signal(SIGCHLD, SIG_IGN) == SIG_ERR)
{
perror("signal error");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid == -1)
err_exit("fork error");
if (pid == 0)
/* subprocess send ARP package */
SendARPPacket();
if (pid > 0)
/* parent process get ARP package */
GetARPPacket(pid);
}
发送ARP报文时,需要向其添加以太网首部形成以太网帧,首部的结构体在 /usr/include/net/ethernet.h
中定义:
/* 10Mb/s ethernet header */
struct ether_header
{
uint8_t ether_dhost[ETH_ALEN]; /* destination eth addr */
uint8_t ether_shost[ETH_ALEN]; /* source ether addr */
uint16_t ether_type; /* packet type ID field */
} __attribute__ ((__packed__));
ether_dhost
为以太网目的地址,填充为广播地址。ether_shost
为以太网源地址ether_type
为帧类型,和 ar_pro
类似,实验中选择 ETHERTYPE_ARP
代表ARP 类型。这样我们就可以对子网中的每一个ip段(暴利扫描,从0到255依次询问)构造出整个 ARP 报文了。填好参数后,调用sendto(socket_fd, buf, ETHER_ARP_PACKET_LEN, 0, (struct sockaddr *)&saddr_ll, sizeof(struct sockaddr_ll))
,发送到刚刚的物理地址sockaddr_ll中去,等待父进程的接收。以下是发送报文函数:
/* 扫描所有256个ip段 */
void init_iplist()
{
// get ip addr
int i;
char tmp[19];
int ip4_1 = local_ip.s_addr&0xff, ip4_2 = (local_ip.s_addr&0xff00)>>8, ip4_3 = (local_ip.s_addr&0xff0000)>>16;
for (i = 0; i < 255; ++i)
{
sprintf(tmp, "%u.%u.%u.%u",ip4_1,ip4_2,ip4_3, i + 1);
inet_pton(AF_INET, tmp, &ip_list[i]);
}
ip_list[256].s_addr = 0;
}
void SendARPPacket()
{
int socket_fd;
int i;
struct ether_arp *arp_packet;
struct ether_header *eth_header;
char buf[ETHER_ARP_PACKET_LEN];
unsigned char dst_mac_addr[MAC_ADDR_LEN] = BROADCAST_ADDR;
memset(alive_hosts, 0, sizeof(uint32_t) * 0x200);
if ((socket_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1){
err_exit("socket()");
}
init_iplist();
/*对子网中的每一个ip段*/
for (i = 0; ip_list[i].s_addr != 0; ++i)
{
bzero(buf, ETHER_ARP_PACKET_LEN);
/* 填充以太首部 */
eth_header = (struct ether_header *)buf;
memcpy(eth_header->ether_shost, local_mac_addr, MAC_ADDR_LEN);
memcpy(eth_header->ether_dhost, dst_mac_addr, MAC_ADDR_LEN);
eth_header->ether_type = htons(ETHERTYPE_ARP);
/* 填充ARP包 */
arp_packet = fill_arp_packet((unsigned *)local_mac_addr, local_ip, ip_list[i]);
memcpy(buf + ETHER_HEADER_LEN, arp_packet, ETHER_ARP_LEN);
/* 发送请求 */
usleep(10000);
ssize_t ret_len = sendto(socket_fd, buf, ETHER_ARP_PACKET_LEN, 0, (struct sockaddr *)&saddr_ll, sizeof(struct sockaddr_ll));
}
close(socket_fd);
exit(0);
}
父进程直接调用接收函数 recv(sock_raw_fd, buf, ETHER_ARP_PACKET_LEN, 0)
,之后会收到网卡接收的ARP数据包,判断收到的ARP包操作是否为ARP应答包,也就是检查报文操作码op是否为2。如果是,说明这个数据报的mac地址已被填充,剥去以太首部,取出源mac地址和ip地址即可。将ip用sprint函数以点分十进制写进临时字符串tmp_ip 中,以便直观输出。
**注意:**alive_hosts结构体中存放的ip是网络字节序,而tmp_ip是点分十进制,因此需要 inet_pton()
函数做转换。
完成主机扫描后关闭socket即可。接收函数源码如下:
unsigned char tmp_ip[0x20];
void GetARPPacket(pid_t pid)
{
struct ether_arp *arp_packet;
char buf[ETHER_ARP_PACKET_LEN];
int sock_raw_fd, ret_len, i;
if ((sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1)
err_exit("socket()");
signal(SIGALRM, handler);
alarm(10);
int cnt = 0;
unsigned char tmp_mac[0x20];
if (!setjmp(mark))
{
while (1)
{
bzero(buf, ETHER_ARP_PACKET_LEN);
ret_len = recv(sock_raw_fd, buf, ETHER_ARP_PACKET_LEN, 0);
if (ret_len > 0)
{
/* 剥去以太头部 */
arp_packet = (struct ether_arp *)(buf + ETHER_HEADER_LEN);
/* arp操作码为2代表arp应答 */
if (ntohs(arp_packet->arp_op) == 2)
{
sprintf(tmp_ip, "%u.%u.%u.%u", arp_packet->arp_spa[0], arp_packet->arp_spa[1], arp_packet->arp_spa[2], arp_packet->arp_spa[3]);
inet_pton(AF_INET, tmp_ip, &(alive_hosts[cnt].ip));
printf(" - ARP Reply Package:\n");
printf("from ip:");
for (i = 0; i < IP_ADDR_LEN; i++)
{
printf(".%u", arp_packet->arp_spa[i]);
}
printf("\nfrom mac");
for (i = 0; i < ETH_ALEN; i++)
{
printf(":%02x", arp_packet->arp_sha[i]);
alive_hosts[cnt].mac[i] = arp_packet->arp_sha[i];
}
printf("\n\n");
cnt++;
}
}
}
}
else
{
// close socket
printf("Done, close socket\n");
close(sock_raw_fd);
}
}
将扫描到的每个活跃的主机都存在一个结构体数组 alive_hosts
中。
由于linux系统提供了gethostbyaddr()函数
,可以通过ip地址获取到规范名、别名等信息,因此只需要调用这个函数即可。同样的问题,需要将 alive_hosts 中的网络字节序ip形式转换成点分十进制的字符串,用到了inet_ntoa()函数。
gethostbyaddr()函数的原型为
struct hostent *gethostbyaddr(const char * addr, int len, int type)
返回值是 hostent结构体
,结构体原型如下:
struct hostent
{
char *h_name; //正式主机名
char **h_aliases; //主机别名
int h_addrtype; //主机IP地址类型:IPV4-AF_INET
int h_length; //主机IP地址字节长度,对于IPv4是四字节,即32位
char **h_addr_list; //主机的IP地址列表
};
gethostbyaddr()函数的第一个参数addr是指向网络字节顺序地址的指针。 (**PS: **按理说本可以将alive_hosts[cnt].ip的值直接传进去,但不知道什么原因总是报错Segment fault,因此只能先转化成字符串,再转化成网络字节顺序地址,这样就不会报错。)
将函数解析的结果放在这个结构体的h_name中,再将这个值复制到alive_hosts[cnt].name中,便完成了主机名的获取。这部分源码如下:
struct hostent *phost;
void assist(char temp[])
{
char *ptr, **pptr;
struct in_addr addr;
char str[32] = {0};
ptr = temp;
if (inet_pton(AF_INET, ptr, &addr) <= 0) {
printf("inet_pton error:%s\n", strerror(errno));
exit(0);
}
/* 调用gethostbyaddr获取主机名 */
phost = gethostbyaddr((const char*)&addr, sizeof(addr), AF_INET);
if (phost == NULL) {
printf("gethostbyaddr error:%s\n", strerror(h_errno));
exit(0);
}
}
void GetHostsName()
{
int cnt;
int i;
struct hostent *ht = NULL;
for (cnt = 0;; cnt++)
{
char temp[25];
strcpy(temp,inet_ntoa(alive_hosts[cnt].ip));
if (alive_hosts[cnt].ip.s_addr == 0)
break;
assist(temp);
strcpy(alive_hosts[cnt].name, phost->h_name);
printf("device %d\n", cnt + 1);
printf("ip: %s\n", inet_ntoa(alive_hosts[cnt].ip));
printf("mac: ");
for (i = 0; i < 6; ++i)
{
printf("%02x", alive_hosts[cnt].mac[i]);
if (i < 6 - 1)
putchar(':');
}
putchar('\n');
printf("hostname: %s\n", alive_hosts[cnt].name);
printf("\n");
}
}
遍历 alive_hosts
中的每个存活主机,进行端口扫描,端口扫描的原理很简单,就是建立socket通信,切换不同端口,通过connect()函数,如果成功则代表端口开放,否则端口关闭。首先先定义一个ScanArgs结构体来存放ip+端口信息,每个端口的result的值为正(成功)或为负(失败)。
typedef struct
{
struct in_addr ip;
int port;
int result;
} ScanArgs;
对于每个 IP 来说,扫描器要扫 65536=2^16 个端口。实验中通过多线程的方式加快其扫描速度,开设 128=2^7 个线程同时扫描,这样对于每个ip只要512次循环即可,直到最后一个ip的最后一组。用函数 pthread_join()
来实现等待一个线程的结束,线程间同步的操作,根据 args_list[p_j].result
的值来判断端口是否开放。
void ScanIP()
{
int i;
int p_i, p_j;
for (i =1 ;; ++i)
{
if (alive_hosts[i].ip.s_addr == 0)
break;
printf("device %d:\n",i+1);
printf("IP: %s\n", inet_ntoa(alive_hosts[i].ip));
printf("hostname: %s\n", alive_hosts[i].name);
ScanArgs args_list[MAX_PTH_NUM];
pthread_t pth_list[MAX_PTH_NUM];
for (p_i = 0; p_i < 512; ++p_i)
{
for (p_j = 0; p_j < 128; ++p_j)
{
args_list[p_j].ip.s_addr = alive_hosts[i].ip.s_addr;
args_list[p_j].port = p_i * 128 + p_j + 1;
/*创建线程*/
pthread_create(pth_list + p_j, NULL, ScannerThread, args_list + p_j);
}
for (p_j = 0; p_j < 128; ++p_j)
{
/*线程同步操作*/
pthread_join(pth_list[p_j], NULL);
if (args_list[p_j].result)
{
printf("Port: %5d Open\n", args_list[p_j].port);
}
}
}
printf("\n");
}
}
用pthread_create()函数创建线程时,线程运行函数 ScannerThread()
的内容如下:
void *ScannerThread(void *args)
{
ScanArgs *p = (ScanArgs *)args;
p->result = SYNScan(p->ip, p->port);
}
result的值通过SYNScan()函数获取,首先创建一个socket标识符,将参数传进去。
接收端接收到TCP报文之后存入scoket接收缓冲区,用户进程通过receive读取缓冲区的数据。如果接收缓冲区满了会通知TCP发送端关闭窗口,保证接收端的缓冲区不会被溢出。如果接收缓冲区的数据为空的时候,那么receive调用scoket的read方法就会处于阻塞状态,直到有数据过来。同样,对于写来说,如果发送缓冲区满了,那么调用scoket的write方法就会处于阻塞状态,直到报文送到网络上。
阻塞IO会一直等待,所以非阻塞IO是用来解决 IO线程与scoket之间的解耦问题 (引入事件机制),如果scoket发送缓存区可写的话会通知IO线程进行write,同样如果scoket的接受缓冲区可读的话会通知IO线程进行read。
在一个TCP套接口被设置为非阻塞之后调用connect,connect会立即返回 EINPROGRESS 错误,表示连接操作正在进行中,但是仍未完成;同时TCP的三路握手操作继续进行;在这之后,我们可以调用 select()函数
来检查这个链接是否建立成功:
当连接建立成功时,套接口描述符变成可写;
当连接出错时,套接口描述符变成既可读又可写。
注意:当一个套接口出错时,它会被select调用标记为既可读又可写。当发现套接口描述符可读或可写时,可进一步判断是连接成功还是出错。这里必须将上述第二种情况和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写。
因此,仅从socket可读或可写无法判断socket连接的状态。并且,有可能在调用select之前,连接就已经建立成功,而且对方的数据已经到来.在这种情况下,连接成功时套接口将既可读又可写.这和连接失败时是一样的.这个时候我们还得通过 getsockopt()
函数来读取错误值。
非阻塞connect的实现:
(一)建立socket套接字;
(二)将socket建立为非阻塞,此时socket被设置为非阻塞模式;
(三)建立connect连接,此时socket设置为非阻塞,connect调用后,无论连接是否建立,会立即返回-1。同时将 errno(包含errno.h就可以直接使用)设置为EINPROGRESS,表示此时tcp三次握手仍旧进行,如果errno不是EINPROGRESS,则说明连接错误,程序结束。
(四)设置等待时间。使用select函数等待正在后台连接的connect函数,使用select监听socket描述符是否可读或者可写,如果只可写,说明连接成功,可以进行之后的操作。如果描述符既可读又可写,分为两种情况,第一种情况是socket连接出现错误,第二种情况是connect连接成功,socket读缓冲区得到了远程主机发送的数据。通过调用 getsockopt() 函数返回值来判断是否发生错误。
struct timeval tout;
int SYNScan(struct in_addr ip, int port)
{
tout.tv_sec = 1;
tout.tv_usec = 0;
int fd, res, valopt, retflag = 1;
long arg;
unsigned int temp;
fd_set set;
struct sockaddr_in tgt_addr;
fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
tgt_addr.sin_addr.s_addr = ip.s_addr;
tgt_addr.sin_family = AF_INET;
tgt_addr.sin_port = htons(port);
arg = fcntl(fd, F_GETFL, NULL);
arg |= O_NONBLOCK;
fcntl(fd, F_SETFL, arg);
// Trying to connect with timeout
res = connect(fd, (struct sockaddr *)&tgt_addr, sizeof(tgt_addr));
if (res < 0) {
/*连接操作正在进行中,但是仍未完成*/
if (errno == EINPROGRESS) {
while (1)
{
FD_ZERO(&set);
FD_SET(fd, &set);
res = select(fd + 1, NULL, &set, NULL, &tout);
if (res < 0 && errno != EINTR)
{
retflag = 0;
goto ret;
}
else if (res > 0)
{
// Socket selected for write
temp = sizeof(int);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (void*)(&valopt), &temp) < 0)
{
retflag = 0;
goto ret;
}
// Check the value returned
if (valopt)
{
retflag = 0;
goto ret;
}
break;
}
else
{
retflag = 0;
goto ret;
}
}
}
else
{
retflag = 0;
goto ret;
}
}
ret:
close(fd);
return retflag;
}
使用非阻塞connect的优势:
可以在三路握手的同时做一些其它的处理:connect操作要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网。在这段时间内我们可能有一些其他的处理想要执行。
并且,由于我们使用select来等待连接的完成,因此我们可以给select设置一个时间限制,从而缩短connect的超时时间.在大多数实现中,connect的超时时间在75秒到几分钟之间。有时候应用程序想要一个更短的超时时间,使用非阻塞connect就可以实现。
有了各个子函数实现所有功能后,主函数只需调用它们即可。默认(缺省)情况下,网卡名称为eth0,超时时间**timeout= 2s **,初始网络为本机网络。
#include "global.h"
#include "init_net.h"
#include "get_host.h"
#include "scan_port.h"
int main(int *argc, const char *argv[])
{
// input from shell
int timeout = 2;
// set my interface name
const char default_ifname[] = "eth0";
strncpy(if_name, default_ifname, strlen(default_ifname));
// Get IP and Subnet mask from adapter and get all IP
printf("\n");
printf("\33[;32m============================= Scanner Start ==============================\n\33[0m");
printf("\n");
printf("\33[;33m>> Local Network Infomation\n\33[0m");
GetNetInfo();
printf("\n");
// get all alive hosts.
printf("\33[;33m>> Scan Alive Hosts\n\33[0m");
GetAliveHosts();
printf("\n");
// get all hosts' name.
printf("\33[;33m>> Show Alive Hosts Name\n\33[0m");
GetHostsName();
// TCP SYN scanning, which will get all open ports.
printf("\33[;33m>> Show Hosts and Ports which has open ports\n\33[0m");
ScanIP();
printf("\33[;32m======================= Program End =============================\n\33[0m");
printf("\n");
}
在192.168.163.0/24局域网内共扫描到5台存活主机,其中包含网关、DNS服务器以及自己搭建的主机。
扫描到的5个主机正是程序的运行结果。再对每个主机开放的端口做验证:
与实验结果相同,验证完毕。
《linux原始套接字(1)-arp请求与接收》:https://blog.csdn.net/weijinqian0/article/details/51861499
《linux网络编程——域名转换 》:https://blog.csdn.net/weixin_34279579/article/details/94002991
《UNIX网络编程 非阻塞connect的实现》:http://www.it165.net/os/html/201404/7763.html
《socket实验指导手册》