上篇讲了virtio-blk简易驱动,这篇来实验virtio-net驱动。喜欢自己鼓捣协议栈的,有virtio-net驱动之后,就可以开始了。同样,有了网络之后,内核就可以跟外界交互了,可以实验各种简易网络服务,比如memcache,httpserver等。
virtio-net需要2个或以上virtio队列,一个接收队列,一个发送队列,可能还有一个控制队列。
接收队列用于接收网卡收到的包。因为是被动接收的,设置队列的时候只需要告诉virtio内存地址跟大小即可。
static void virtio_net_refill_rx_queue(void)
{
struct addr_size phys[1];
struct packet *p;
while ((in_rx < BUF_PACKETS / 2) && !STAILQ_EMPTY(&free_list)) {
/* peek */
p = STAILQ_FIRST(&free_list);
/* remove */
STAILQ_REMOVE_HEAD(&free_list, next);
phys[0].vp_addr = p->pdata;
phys[0].vp_size = MAX_PACK_SIZE;
phys[0].vp_flags = VRING_DESC_F_WRITE;
virtio_to_queue(to_virtio_dev_t(&pci_vn), RX_Q, phys, 1, p);
in_rx++;
}
if (in_rx == 0 && STAILQ_EMPTY(&free_list)) {
pci_w(("warning: rx queue underflow!"));
virtio_net_stats.ets_fifoUnder++;
}
}
virtio_net_refill_rx_queue用来设置接收队列,有个阈值,BUF_PACKETS / 2,当接收队列可用描述符数量少于阈值的时候,就填充进去。
填充就一项即可,接收包内存地址p->pdata,大小为MAX_PACK_SIZE,设置只写属性,即告诉virtio这个是输出的。
static void virtio_net_check_queues(void)
{
struct packet *p;
size_t len;
/* Put the received packets into the recv list */
while (virtio_from_queue(to_virtio_dev_t(&pci_vn), RX_Q, (void **)&p, &len)
== 0) {
pci_d("virtio_from_RX_queue:%lx,len:%xn", p, len);
p->len = len;
pci_d("vhdr:%lx,phdr:%lx,vdata:%lx,pdata:%lx,len:%dn",
p->vhdr, p->phdr, p->vdata, p->pdata, p->len);
dump_mem(p->vdata,len);
}
}
virtio_net_check_queues从接收队列接收包数据,然后打印内存。运行之后发现会有几个包收到。
virtio_from_RX_queue:ffffffff8005da48,len:107
vhdr:ffffffff8005c1d6,phdr:5c1d6,vdata:ffffffff800555f6,pdata:555f6,len:263
addr:ffffffff800555f6:
~800555f6 - 00 00 00 00 00 00 00 00 00 00 33 33 00 00 00 fb ..........33....
~80055606 - be 13 e8 8e 9a ce 86 dd 60 02 53 8d 00 c7 11 ff ........`.S.....
~80055616 - fe 80 00 00 00 00 00 00 bc 13 e8 ff fe 8e 9a ce ................
~80055626 - ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 fb ................
~80055636 - 14 e9 14 e9 00 c7 6b 84 00 00 84 00 00 00 00 04 ......k.........
~80055646 - 00 00 00 00 04 68 61 32 33 0b 5f 75 64 69 73 6b .....ha23._udisk
~80055656 - 73 2d 73 73 68 04 5f 74 63 70 05 6c 6f 63 61 6c s-ssh._tcp.local
~80055666 - 00 00 10 80 01 00 00 11 94 00 01 00 04 68 61 32 .............ha2
~80055676 - 33 c0 22 00 1c 80 01 00 00 00 78 00 10 fe 80 00 3.".......x.....
~80055686 - 00 00 00 00 00 bc 13 e8 ff fe 8e 9a ce 01 65 01 ..............e.
~80055696 - 63 01 61 01 39 01 65 01 38 01 65 01 66 01 66 01 c.a.9.e.8.e.f.f.
~800556a6 - 66 01 38 01 65 01 33 01 31 01 63 01 62 01 30 01 f.8.e.3.1.c.b.0.
~800556b6 - 30 01 30 01 30 01 30 01 30 01 30 01 30 01 30 01 0.0.0.0.0.0.0.0.
~800556c6 - 30 01 30 01 30 01 30 01 38 01 65 01 66 03 69 70 0.0.0.0.8.e.f.ip
~800556d6 - 36 04 61 72 70 61 00 00 0c 80 01 00 00 00 78 00 6.arpa........x.
~800556e6 - 02 c0 34 c0 0c 00 21 80 01 00 00 00 78 00 08 00 ..4.........x...
~800556f6 - 00 00 00 00 16 c0 34 ......4
前面10个字节属于virtio-net的头,后面是以太网的数据。
目的mac:33:33:00:00:00:fb
源mac:be:13:e8:8e:9a:ce
协议类型:86 dd(ipv6)
运行ifconfig,可以看到tap0
tap0 Link encap:Ethernet HWaddr be:13:e8:8e:9a:ce
inet6 addr: fe80::bc13:e8ff:fe8e:9ace/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:19 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:2942 (2.9 KB)
源mac跟tap0的mac对应。
ipv6包第一个字节是协议版本,0x60,高4位0x6表示协议版本6(ipv6),低4位是其他内容。
对于大端跟小端,在低4位还是高4位不一样。对应x86,协议号在高4位。
可以看到payload Length为16位,0x00 0xc7
整个包长度为0x107,virtio-net头10字节,ethnet协议头14字节(mac地址2*6,协议类型2字节),ipv6头40字节。加起来刚好。
0x107 = 0xc7 + 10 + 14+ 40
可以看到源ip地址为:fe 80 00 00 00 00 00 00 bc 13 e8 ff fe 8e 9a ce
跟ifconfig tap0的ipv6地址对应:inet6 addr: fe80::bc13:e8ff:fe8e:9ace
目的ip,ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 fb
FF开头的为组播地址,相当于ipv4的广播。
因此自动收到的几个包是ipv6的组播包。
Next Header为0x11,表示是UDP包。
可以看到端口号为14e9 14e9,即5353,为DNS端口。00 c7为包长度,同ipv6头里面的payload Length,payload Length包括ipv6扩展头跟payload长度,这里没有扩展头所以一致。6b 84为校验和。
现在我们来写个ipv6 udp包的校验和检测,看看我们自己写的校验和跟包里面的是否一致。因为接下来我们要进行发包,为了测试,就发ipv6 udp包,需要计算校验和。
u32 check_sum(u32 sum,void *buf, int len)
{
int num = 0;
uchar *p = (uchar *)buf;
if ((NULL == p) || (0==len)) return sum;
while (len > 1) {
sum += ((ushort)p[num]<<8&0xff00)|((ushort)p[num+1]&0xff);
len -= 2;
num += 2;
}
if (len>0) {
sum += ((ushort)p[num]<<8)&0xffff;
num++;
}
while (sum >> 16) {
sum = (sum >> 16) + (sum & 0xffff);
}
return sum;
}
check_sum为校验和计算函数,16位进行累加,换算成本机字节序再计算,如何和有溢出,超过16位,溢出部分加到低位。最后取反。
计算的时候需要添加伪头部,即源ip地址,目的ip地址,以及协议号。
u16 *pw = (u16 *) p->vdata+5;
pci_d("%lx,%lx,%lxn", pw[5], pw[6], pw[7]);
uchar *pc = (uchar *) p->vdata+10;
pci_d("To:%x:%x:%x:%x:%x:%x,From:%x:%x:%x:%x:%x:%x: len:%x,type:%xn",
pc[0], pc[1], pc[2], pc[3], pc[4], pc[5], pc[6], pc[7], pc[8],
pc[9], pc[10], pc[11],pw[9],pw[6]);
if (pw[6]==0xdd86)pci_d("ipv6n");
if (pc[20]==0x11)pci_d("udpn");
if (pw[6]==0xdd86 && pc[20]==0x11){
ushort oldsum = pw[30];
pw[30]=0;
u32 sum = 0;
sum+=len-64;
sum = check_sum(sum,&pw[11],32);
sum += 0x11;
ushort calsum = (ushort)check_sum(sum,&pw[27],len-64);
pci_d("oldsum:%x,calsum:%xn",oldsum,~calsum&0xffff);
}
pw,pc指向以太网包头。
计算sum时,先添加udp长度,为virtio-net输出的len,减去64字节(10字节为virtio头,14字节为以太网包头,40字节为ipv6包头),然后计算源ip地址跟目的ip地址,2个16字节,然后加上0x11,UDP协议号。再计算UDP内容部分。
打印出来为:
virtio_from_RX_queue:ffffffff8005da48,len:107
vhdr:ffffffff8005c1d6,phdr:5c1d6,vdata:ffffffff800555f6,pdata:555f6,len:263
addr:ffffffff800555f6:
~800555f6 - 00 00 00 00 00 00 00 00 00 00 33 33 00 00 00 fb ..........33....
~80055606 - 96 c9 06 8f 74 a9 86 dd 60 0e c2 65 00 c7 11 ff ....t...`..e....
~80055616 - fe 80 00 00 00 00 00 00 94 c9 06 ff fe 8f 74 a9 ..............t.
~80055626 - ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 fb ................
~80055636 - 14 e9 14 e9 00 c7 bd 23 00 00 84 00 00 00 00 04 .......#........
~80055646 - 00 00 00 00 04 68 61 32 33 0b 5f 75 64 69 73 6b .....ha23._udisk
~80055656 - 73 2d 73 73 68 04 5f 74 63 70 05 6c 6f 63 61 6c s-ssh._tcp.local
~80055666 - 00 00 10 80 01 00 00 11 94 00 01 00 04 68 61 32 .............ha2
~80055676 - 33 c0 22 00 1c 80 01 00 00 00 78 00 10 fe 80 00 3.".......x.....
~80055686 - 00 00 00 00 00 94 c9 06 ff fe 8f 74 a9 01 39 01 ...........t..9.
~80055696 - 61 01 34 01 37 01 66 01 38 01 65 01 66 01 66 01 a.4.7.f.8.e.f.f.
~800556a6 - 66 01 36 01 30 01 39 01 63 01 34 01 39 01 30 01 f.6.0.9.c.4.9.0.
~800556b6 - 30 01 30 01 30 01 30 01 30 01 30 01 30 01 30 01 0.0.0.0.0.0.0.0.
~800556c6 - 30 01 30 01 30 01 30 01 38 01 65 01 66 03 69 70 0.0.0.0.8.e.f.ip
~800556d6 - 36 04 61 72 70 61 00 00 0c 80 01 00 00 00 78 00 6.arpa........x.
~800556e6 - 02 c0 34 c0 0c 00 21 80 01 00 00 00 78 00 08 00 ..4.........x...
~800556f6 - 00 00 00 00 16 c0 34 ......4
flags:0,gso_type:0,hdr_len:0,gso_size:0,csum_start:0,csum_offset:0
0,fb00000033330000,dd86a9748f06c996,ff11c70065c20e60,80fe,a9748ffeff06c994,2ff
a974,dd86,e60
To:33:33:0:0:0:fb,From:96:c9:6:8f:74:a9: len:c700,type:dd86
ipv6
udp
oldsum:23bd,calsum:bd23
字节序换成网络字节序,就都是23bd了。
接下来来实践发包。
libs/net/udp.c
void udp_set_data(struct udp_packet *p,void *data, size_t len)
{
memcpy(p->data, data, len);
p->udp.len = htons(len + sizeof(p->udp));
p->ip.ip_len = htons(len +sizeof(p->udp) + (p->ip.ip_hl<<2));
ip_set_checksum(&p->ip);
ip_set_udp_checksum(&p->ip);
}
void udp6_set_data(struct udp6_packet *p,void *data, size_t len)
{
memcpy(p->data, data, len);
p->udp.len = htons(len + sizeof(p->udp));
p->ip6.payload_len = p->udp.len;
ip_set_udp6_checksum(&p->ip6);
}
udp_set_data, udp6_set_data分别为ipv4跟ipv6的udp包设置数据,然后计算包长度,设置校验和。
校验和相关函数在libs/net/ip.c
__used static void init_udp_packet(char *destip,char *destmac)
{
struct udp_packet *p = ptest_udp;
eth_set_hdr(&p->eth, destmac, pci_vn._config.mac,ETH_IP_PROTO);
char myip[4]={192,168,122,98};
ip_init_hdr(&p->ip,destip,myip,0x11);
p->udp.source = htons(4444);
p->udp.dest = htons(4444);
udp_set_data(p,"Hello Yaos!", 12);
//printk("ip.len:%dn",ntohs(p->ip.ip_len));
dump_mem(p,ntohs(p->ip.ip_len)+sizeof(p->eth));
}
__used static void init_udp6_packet(char *destip,char *destmac)
{
struct udp6_packet *p = ptest_udp6;
eth_set_hdr(&p->eth, destmac, pci_vn._config.mac,ETH_P_IPV6);
char myip[16]={0,0,0,0,0,0,0,0,0,0,0xff,0xff,192,168,122,98};
ip6_init_hdr(&p->ip6,destip,myip,0x11);
p->udp.source = htons(4444);
p->udp.dest = htons(4444);
udp6_set_data(p,"Hello Yaos! From IPV6", 22);
//printk("ip.len:%dn",ntohs(p->ip.ip_len));
dump_mem(p,ntohs(p->udp.len)+sizeof(p->ip6)+sizeof(p->eth));
}
init_udp_packet 和 init_udp6_packet这2个函数分别初始化一个ipv4的udp包跟一个ipv6的udp包。
启动一个定时器,在定时器里面发包:
__used static void vn_timeout(u64 nowmsec,void *param)
{
set_timeout_nsec(5000000000, vn_timeout,param);
pci_d("vntimeout:now msec:%dn", nowmsec/1000000UL);
init_udp6_packet(host_ipv6,host_mac);
virtio_net_send_packet(ptest_udp6,ntohs(ptest_udp6->udp.len)+sizeof(ptest_udp6->eth)
+sizeof(ptest_udp6->ip6));
char toip4[4]={192,168,122,1};
init_udp_packet(toip4,host_mac);
struct udp_packet *p = ptest_udp;
virtio_net_send_packet(p,ntohs(p->ip.ip_len)+sizeof(p->eth));
}
这里指定了ip地址:192.168.122.98,现在还没有通过DHCP获取IP,也不支持ARP。
就通过手动指定ARP的方式:
arp -s 192.168.122.98 52:54:0:12:34:56
test/client为ipv4的udp发送包,在内核运行之后,可以运行client发包测试。
./test/client 192.168.122.98 4444
运行过arp -s 192.168.122.98 52:54:0:12:34:56之后
To:52:54:0:12:34:56,From:b6:ac:3:b6:32:e9: len:a38,type:8,prot:40,port:5c11,len:2a
addr:ffffffff80057284:
~80057284 - 52 54 00 12 34 56 b6 ac 03 b6 32 e9 08 00 45 00 RT..4V....2...E.
~80057294 - 00 2a 38 0a 40 00 40 11 8d 04 c0 a8 7a 01 c0 a8 .*8.@.@.....z...
~800572a4 - 7a 62 c8 9a 11 5c 00 16 bb 11 68 65 6c 6c 6f 20 zb.......hello.
~800572b4 - 69 27 6d 20 68 65 72 65 i'm.here
oldip:48d,newip:48d,oldudp:11bb,newudp:11bb
如果没有运行arp,结果为:
To:ff:ff:ff:ff:ff:ff,From:b6:ac:3:b6:32:e9: len:406,type:608,prot:0,port:0,len:800
这个是ARP包。
内核往外发包,可以通过tcpdump来查看
tcpdump -i tap0 -XX -xx -nn -c4
以上命令抓取tap0接口的4个包,注意要等内核运行之后再抓包。内核运行指挥tap0才会启动。
tcpdump -i tap0 -XX -xx -nn -c4
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tap0, link-type EN10MB (Ethernet), capture size 262144 bytes
23:58:54.572160 IP6 ::ffff:192.168.122.98.4444 > fe80::ec60:d1ff:feb3:91bc.4444: UDP,
length 22
0x0000: ee60 d1b3 91bc 5254 0012 3456 86dd 6000 .`....RT..4V..`.
0x0010: 0000 001e 11ff 0000 0000 0000 0000 0000 ................
0x0020: ffff c0a8 7a62 fe80 0000 0000 0000 ec60 ....zb.........`
0x0030: d1ff feb3 91bc 115c 115c 001e ea36 4865 ...........6He
0x0040: 6c6c 6f20 5961 6f73 2120 4672 6f6d 2049 llo.Yaos!.From.I
0x0050: 5056 3600 PV6.
23:58:54.572173 IP 192.168.122.98.4444 > 192.168.122.1.4444: UDP, length 12
0x0000: ee60 d1b3 91bc 5254 0012 3456 0800 4500 .`....RT..4V..E.
0x0010: 0028 0000 4000 ff11 0610 c0a8 7a62 c0a8 .(..@.......zb..
0x0020: 7a01 115c 115c 0014 5992 4865 6c6c 6f20 z.....Y.Hello.
0x0030: 5961 6f73 2100 Yaos!.
23:58:59.582392 IP6 ::ffff:192.168.122.98.4444 > fe80::ec60:d1ff:feb3:91bc.4444: UDP,
length 22
0x0000: ee60 d1b3 91bc 5254 0012 3456 86dd 6000 .`....RT..4V..`.
0x0010: 0000 001e 11ff 0000 0000 0000 0000 0000 ................
0x0020: ffff c0a8 7a62 fe80 0000 0000 0000 ec60 ....zb.........`
0x0030: d1ff feb3 91bc 115c 115c 001e ea36 4865 ...........6He
0x0040: 6c6c 6f20 5961 6f73 2120 4672 6f6d 2049 llo.Yaos!.From.I
0x0050: 5056 3600 PV6.
23:58:59.582408 IP 192.168.122.98.4444 > 192.168.122.1.4444: UDP, length 12
0x0000: ee60 d1b3 91bc 5254 0012 3456 0800 4500 .`....RT..4V..E.
0x0010: 0028 0000 4000 ff11 0610 c0a8 7a62 c0a8 .(..@.......zb..
0x0020: 7a01 115c 115c 0014 5992 4865 6c6c 6f20 z.....Y.Hello.
0x0030: 5961 6f73 2100 Yaos!.
test/server为udp 服务端测试程序,可以运行./test/server 192.168.122.1 4444进行测试收包。
./test/server 192.168.122.1 4444
create socket.
bind address to socket.
receive from 192.168.122.98: buffer:Hello Yaos!
receive from 192.168.122.98: buffer:Hello Yaos!
receive from 192.168.122.98: buffer:Hello Yaos!
gcc -o server udpserver.c
gcc -o client udpclient.c
进行编译。
运行本例:
git clone https://github.com/saneee/x86_64_kernel.git
cd 0021
make qemu
测试收包用test/udpclient.c,发包用test/udpserver.c,或者用tcpdump
BUG:
arch/x86_64/entry64.S: movq $init_per_cpu__init_stack+4096,%rax
初始化的时候固定了4K栈,修改INIT_STACK_SIZE不会改变初始化栈大小,需要修正。