数据报式套接字(SOCK_DGRAM)
无连接的 socket,针对无连接的 UDP 服务
与邮件模型来进行对比
流式套接字(SOCK_STREAM)
面向连接的 socket,针对面向连接的 TCP 服务
与电话模型来进行对比
这两类套接字似乎涵盖了 TCP/IP 应用的全部
TCP 与 UDP 各自有独立的 port 互不影响
一个进程同时可拥有多个 port
不用关心 tcp/ip 协议实现的过程
client
创建 socket 接口
定义 sockaddr_in 变量,其中 ip、port 为目的主机的信息
可发送 0 长度的数据包
server
bind 本地主机的 ip、port 等信息
接收到的数据包中包含来源主机的 ip、port 信息
client
connect 来建立连接
write、read 收发数据
不可发送 0 长度的数据
server
bind 本地主机的 ip、port 等信息
listen 把主动套接字变为被动
accept 会有新的返回值
多进程、线程完成并发
原始套接字(SOCK_RAW)
一种不同于 SOCK_STREAM、SOCK_DGRAM 的套接字,它实现于系统核心
可接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用
开发人员可发送自己组装的数据包到网络上
广泛应用于高级网络编程
网络专家、黑客通常会用此来编写奇特的网络程序
流式套接字只能收发
TCP 协议的数据
数据报套接字只能收发
UDP 协议的数据
原始套接字可以收发
内核没有处理的数据包,因此要访问其他协议
发送的数据需要使用,原始套接字(SOCK_RAW)
/*
*function:
* 创建链路层的原始套接字
*parameter:
* protocol:指定可以接收或发送的数据包类型
* ETH_P_IP: IPV4数据包
* ETH_P_ARP: ARP数据包
* ETH_P_ALL: 任何协议类型的数据包
*return:
* 成功(>0):链路层套接字
* 失败(<0):出错
*/
int socket(PF_PACKET, SOCK_RAW, protocol);
#include
#include
sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
在 TCP/IP 协议栈中的每一层为了能够正确解析出上层的数据包,从而使用一些“协议类型”来标记
组装/拆解 udp 数据包流程
UDP 封包格式
IP 封包格式
Ethernet 封包格式
TCP 封包格式
ICMP 封包格式
ICMP 回显请求和回显应答格式
不同的类型值以及代码值,代表不同的功能
链路层数据格式
#include
#include
#include
#include
int main(int argc, char *argv[])
{
unsigned char buf[1024] = "";
//创建链路层原始套接字
int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
while(1)
{
unsigned char src_mac[18] = "";
unsigned char dst_mac[18] = "";
//获取链路层的数据帧
recvfrom(sock_raw_fd, buf, sizeof(buf), 0, NULL, NULL);
//根据格式解析数据
//从buf里提取目的mac、源mac
sprintf(dst_mac, "%02x:%02x:%02x:%02x:%02x:%02x",
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
sprintf(src_mac, "%02x:%02x:%02x:%02x:%02x:%02x",
buf[6], buf[7], buf[8], buf[9], buf[10], buf[11]);
//打印源MAC、目的MAC
printf("MAC:%s >> %s\n", src_mac, dst_mac);
}
return 0;
}
ARP 的 TYPE 为 0x0806
buf 为 unsinged char
所有数据均为大端
IP 的 TYPE 为 0x0800
buf 为 unsinged char
所有数据均为大端
如下,网上的数据包的组包过程;其解包过程正好相反,首先分析以太网得到 MAC 然后再依次分析,如 IP、PORT
混杂模式
指一台机器的网卡能够接收所有经过它的数据包,而不论其目的地址是否是它
一般计算机网卡都工作在非混杂模式下,如果设置网卡为混杂模式需要 root 权限
linux 下设置
//设置混杂模式
ifconfig eth0 promisc
//取消混杂模式
ifconfig eth0 -promisc
linux 下通过程序设置网卡混杂模式:
struct ifreq ethreq;
strncpy(ethreq.ifr name, "eth0", IFNAMSIZ);
//获取eth0网络接口标志
if(ioctl(sock_raw_fd, SIOCGIFFLAGS, ðreq) != 0)
{
perror("ioctl");
close(sock_raw_fd);
exit(-1);
}
ethreq.ifr_flags |= IFF_PROMISC;
//设置eth0网络接口标志
if(ioctl(sock_raw_fd, SIOCSIFFLAGS, ðreq) != 0)
{
perror("ioctl");
close(sock_raw_fd);
exit(-1);
}
/*
*sock_raw_fd:原始套接字
*msg: 发送的消息(封装好的协议数据)
*sll: 本机网络接口,指发送的数据应该从本机的哪个网卡出去,而不是以前的目的地址
*/
sendto(sock_raw_fd, msg, msg_len, 0, (struct sockaddr *)&sll, sizeof(sll));
#include
struct sockaddr_ll
{
unsigned short int sll_family; //一般为PF_PACKET
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]; //MAC地址
}
只需要对 sll.sll_ifindex 赋值,就可使用
//将网络接口赋值给原始套接字地址结构
struct sockaddr_ll all;
bzero(&sll, sizeof(sll));
sll.sll_ifindex = /* 获取本机的出去接口地址 */
int len = sendto(sock_raw_fd, msg, sizeof(msg), 0, (struct sockaddr *)&sll, sizeof(sll));
#include
int ioctl(int fd, int request, void *);
ioctl 获取接口示例
/* 网络接口地址 */
struct ifreq ethreq;
/* 指定网卡名称 */
strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ);
/* 获取网络接口 */
if( -1 == ioctl(sock_raw_fd, SIOCGIFINDEX, ðreq))
{
perror("ioctl");
close(sock_raw_fd);
exit(-1);
}
struct sockfaddr_ll sll;
bzero(&sll, sizeof(sll));
//给sll赋值
sll.sll_ifindex = ethreq.ifr_ifindex;
//发送
int len = sendto(sock_raw_fd, msg, sizeof(msg), 0, (struct sockaddr *)&sll, sizeof(sll));
ioctl 参数对照表
类别 | request | 说明 | 数据类型 |
接口 | SIOCGIFINDEX | 获取网络接口 | struct ifreq |
SIOCSIFADDR | 设置接口地址 | struct ifreq | |
SIOCGIFADDR | 获取接口地址 | struct ifreq | |
SIOCSIFFLAGS | 设置接口标志 | struct ifreq | |
SIOCGIFFLAGS | 获取接口标志 | struct ifreq |
#include
#define IFNAMSIZ 16
//网络接口地址
struct ifreq ethreq;
//指定网卡名称
strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ);
//获取网络接口
ioctl(sock_raw_fd, SIOCGIFINDEX, ðreq);
ARP(Address Resolution Protocol,地址解析协议)
TCP/IP 协议族中的一个
主要用于查询指定 ip 所对应的的 MAC
请求方使用广播来发送请求
应答方使用单播来回送数据
为了在发送数据的时提高效率 在计算中会有一个 ARP 缓存表,用来暂时存放 ip 所对应的 MAC,在 linux中使用 ARP 即可查看
机器 A 获取机器 B 的 MAC :
向指定 IP 发送 ARP 请求(demo)
int main(int argc, char *argv[])
{
//创建通信用的原始套接字
int sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
//根据各种协议首部格式构造发送数据报
unsigned char send_msg[1024] = {
/****** 组MAC 14 *********/
0xff,0xff,0xff,0xff,0xff,0xff, //dst_mac:FF:FF:FF:FF:FF:FF
0x00,0x0c,0x29,0x75,0xa6,0x51, //src_man:00:0c:29:75:a6:51
0x08,0x06, //类型:0x0806 ARP协议
/******* 组ARP 28 *******/
0x00,0x01,0x08,0x00, //硬件类型1(以太网地址),协议类型0x0800(ip)
0x06,0x04,0x00,0x01, //硬件,协议地址分为6,4 op:(1:arp请求 2:arp应答)
0x00,0x0c,0x29,0x75,0xa6,0x51, //发送端的MAC地址
172, 20, 226, 12, //发送端的IP地址
0x00,0x00,0x00,0x00,0x00,0x00, //目的MAC地址(获取对方MAC,目的MAC置0)
172,20,226,11
};
//数据初始化
struct sockaddr_ll sll; //原始套接字地址结构
struct ifreq ethreq; //网络接口地址
strncpy(ethreq.ifr_name, "eth0", IFNAMSIZ); //指定网卡名称
//将网络接口赋值给原始套接字地址结构
ioctl(sock_raw_fd, SIOCGIFINDEX, (char *)ðreq);
bzero(&sll, sizeof(sll));
sll.sll_ifindex = ethreq.ifr_ifindex;
sendto(sock_raw_fd, send_msg, 42, 0, (struct sockaddr *)&sll, sizeof(sll));
//接收对方的ARP应答
unsigned char recv_msg[1024] = "";
recvfrom(sock_raw_fd, recv_msg, sizeof(recv_msg), 0, NULL, NULL);
if(recv_msg[21] == 2)
{
char resp_mac[18] = ""; //arp响应的MAC
char resp_ip[16] = ""; //arp响应的IP
sprintf(resp_mac, "%02x:%02x:%02x:%02x:%02x:%02x",
recv_msg[22], recv_msg[23],
recv_msg[24], recv_msg[25],
recv_msg[26], recv_msg[27]);
sprintf(resp_ip, "%d.%d.%d.%d",
recv_msg[28], recv_msg[29],
recv_msg[30], recv_msg[31]);
printf("IP:%s - MAC:%s\n",resp_ip, resp_mac);
}
return 0;
}
飞鸽格式:
版本:用户名:主机名:命令字:附加消息
组包过程:
/*
*飞鸽消息格式:
*note:
* msg : udp 报文头中的数据
*/
sprintf(msg, "1:%d:%s:%s:%d:%s", 123, "qfedu", "qfedu", 32, ok);
MAC、IP、UDP 报文头参考前面的数据包详解
#include
#include
#include
#include
#include
#include
#include
unsigned short checksum(unsigned short *buf, int nword);
int main(int argc, char *argv[])
{
int sockfd = 0;
struct sockaddr_in dest_addr;
//组织发送的信息;注意伪头部
char udp_checksum_buf[] = {
/****** 伪头部 开始 ******/
172, 20, 223, 119, //src ip
172, 20, 223, 83, //dst ip
0x00, //默认
17, //udp
0x00, 34, //udp总长度(header lenth + udp data length)
/****** 伪头部-结束 ******/
/****** udp 首部 ******/
0x09, 0x79, //udp src port,2425(feiQ)
0x09, 0x79, //udp dst port,2425(feiQ)
0x00, 34, //udp总长度
0x00, 0x00, //udp data checksum,注意 校验和不计算也是可以的但是必须为0
'1', ':', //1 代表飞秋的版本号
'1', '2', '3', ':', //123 本次发送的飞秋的数据包的报编号
't', 'o', 'm', ':', //tom 发送者姓名
'q', 'f', ':', //sun 发送者主机名
'1', ':', //1 代表上线
'q', 'f', 'e', 'd', 'u' //qfedu 发送者的名字
};
//创建 网络层原始套接字,并且指定将来作用udp相关
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
if(sockfd < 0)
{
perror("socket error");
exit(1);
}
//配置 结构体变量,代表着目的主机的信息
bzero(&dest_addr, sizeof(dest_addr)); //初始化
//套接字域AF_INET(网络套接字)
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(2425);
//设置目的IP
dest_addr.sin_addr.s_addr = inet_addr("172.20.223.83");
//对发送的信息进行校验
*((unsigned short *)&udp_checksum_buf[18]) = htons(checksum((unsigned short *)udp_checksum_buf,
sizeof(udp_checksum_buf)/2));
//发送数据到指定目的
sendto(sockfd, udp_checksum_buf+12, 34, 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
//关闭套接字
close(sockfd);
return 0;
}
/*
*function:
* 对数据进行校验
*note:
* 注意伪头部的问题
*/
unsigned short checksum(unsigned short *buf, int nword)
{
unsigned long sum;
for(sum = 0; nword > 0; nword--)
{
sum += htons(*buf);
buf++;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return ~sum;
}
TCP 数据包(发送一个 SYN 数据包)
#include
#include
#include
#include
#include
#include
#include
unsigned short checksum(unsigned short *buf, int nword);
/*
*function:
* 创建原始套接字,然后手动组织tcp数据包,然后发送
*/
int main(int argc, char *argv[])
{
int sockfd = 0;
struct sockaddr_in dest_addr;
/*组织发送的信息;
*note:
* 伪头部
* 0x50的高4位代表是首部长度,要注意其代表的是4Byte的个数,即0x50代表的首部长度为20(5*4)
* 本次发送的tcp数据包,只有tcp包头,不包含任何tcp数据
*/
char tcp_checksum_buf[] = {
/******* 伪头部 开始 ******/
172, 20, 223, 119, //src ip
172, 20, 223, 83, //dst ip
0x00, //默认
6, //6 为tcp
0x00, 20, //tcp 头部长度
/******* 伪头部 结束 ******/
/******* TCP头部 开始 *****/
0x55, 0x22, //tcp src port
0x00, 80, //tcp dst port,port=80
0x00, 0x00, 0x00, 0x01, //tcp id
0x00, 0x00, 0x00, 0x00, //tcp ack
0x50, 0x02, 0x17, 0x70, //4位首部长度+6位保留+6个标志位(URG/ACK/PSH/PST/SYN/FIN)+16位窗口大小
0x00, 0x00, //16位tcp校验和
0x00, 0x00 //16位紧急指针
/******* TCP头部 结束 ******/
};
//创建 网络层原始套接字,并且指定将来作用tcp相关
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket error");
//exit(1):异常退出
//exit(0):正常退出
exit(1);
}
//配置 结构体变量,代表着目的主机的信息
bzero(&dest_addr, sizeof(dest_addr)); //初始化
//套接字域是AF_INET(网络套接字)
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(80);
//设置目的ip
dest_addr.sin_addr.s_addr = inet_addr("172.20.223.83");
// 对发送的信息进行校验
*((unsigned short *)&tcp_checksum_buf[28]) = htons(checksum((unsigned short *)tcp_checksum_buf,
sizeof(tcp_checksum_buf)/2));
// 发送数据到指定目的
sendto(sockfd, tcp_checksum_buf + 12, 20, 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
//关闭套接字
close(sockfd);
return 0;
}
/*
*function:
* 对数据进行校验
*note:
* 注意伪头部的问题
*/
unsigned short checksum(unsigned short *buf, int nword)
{
unsigned long sum;
for(sum = 0; nword > 0; nword--)
{
sum += htons(*buf);
buf++;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return ~sum;
}