2012/11

发送 IGMP 包的 Perl 脚本

因为 Fedora 上的 scapy (2.2.0)还不支持 IGMP,所以我用 Perl 写了一个发送 IGMP 的脚本。最初代码来自这里,原脚本只能发送 IGMP query,我添加了 IGMP leave 和一些命令行选项。用法如下:

% ./igmp.pl -q 192.168.10.2 224.0.0.22
% ./igmp.pl -l 224.8.8.8 192.168.10.2 224.0.0.22

具体代码如下:

[perl]

! /usr/bin/perl -w

Adapted from : http://code.google.com/p/perl-igmp-querier/

use strict;
use POSIX;
use Socket qw(inet_pton AF_INET SOCK_RAW);
use Getopt::Long qw(:config no_auto_abbrev);
my $leave;
my $leave_addr;
my $query = 1;
my $help;

sub forgepkt {

my $src_host = shift;
my $dst_host = shift;

my $zero_cksum = 0;
my $igmp_proto = 2;
my $igmp_type = $query? ‘11’: ‘17’; #11 query, 17 leave#
my $igmp_mrt = ‘64’;
my $igmp_pay = $query? “” : $leave_addr; # 0 for query
my $igmp_chk = 0;
my $igmp_len = 0;

my ($igmp_pseudo) = pack(‘H2H2va4’,$igmp_type,$igmp_mrt,$igmp_chk,$igmp_pay);
$igmp_chk = &checksum($igmp_pseudo);
$igmp_pseudo = pack(‘H2H2va4’,$igmp_type,$igmp_mrt,$igmp_chk,$igmp_pay);
$igmp_len = length($igmp_pseudo);

my $ip_ver = 4;
my $ip_len = 6;
my $ip_ver_len = $ip_ver . $ip_len;
my $ip_tos = 00;
my ($ip_tot_len) = $igmp_len + 20 + 4;
my $ip_frag_id = 11243;
my $ip_frag_flag = “010”;
my $ip_frag_oset = “0000000000000”;
my $ip_fl_fr = $ip_frag_flag . $ip_frag_oset;
my $ip_ttl = 1;
my $ip_opts = ‘94040000’; # router alert

my ($head) = pack(‘H2H2nnB16C2n’,
$ip_ver_len,$ip_tos,$ip_tot_len,$ip_frag_id,
$ip_fl_fr,$ip_ttl,$igmp_proto);
my ($addresses) = pack(‘a4a4’,$src_host,$dst_host);
my ($pkt) = pack(‘aaH8a*’,$head,$addresses,$ip_opts,$igmp_pseudo);

return $pkt;
}

sub checksum {
my ($msg) = @_;
my ($len_msg,$num_short,$short,$chk);
$len_msg = length($msg);
$num_short = $len_msg / 2;
$chk = 0;
foreach $short (unpack(“S$num_short”, $msg)) {
$chk += $short;
}
$chk += unpack(“C”, substr($msg, $len_msg - 1, 1)) if $len_msg % 2;
$chk = ($chk >> 16) + ($chk & 0xffff);
return(~(($chk >> 16) + $chk) & 0xffff);
}

sub help {
my ($exitcode) = @_;

    print < $leave,
'q|query'       => $query,
'h|help'        => $help,

) or help(0);

help(1) if ($#ARGV < 1);

if ($leave) {
$query = 0;
$leave_addr = inet_pton(AF_INET, $leave);
}

my $src = $ARGV[0];
my $dst = $ARGV[1]; # 224.0.0.22, igmp all-hosts

socket(RAW, AF_INET, SOCK_RAW, 255) || die $!;
setsockopt(RAW, 0, 1, 1);

my $src_host = (gethostbyname($src))[4];
my $dst_host = (gethostbyname($dst))[4];
my ($packet) = forgepkt($src_host,$dst_host);
my ($dest) = pack('Sna4x8', AF_INET, 0, $dst_host);

Send general query packet twice for reliability

send(RAW,$packet,0,$dest);
send(RAW,$packet,0,$dest);

[/perl]

两个精彩的比喻

我们知道,计算机中有很多概念并不容易理解,有些时候一个好的比喻能胜过很多句解释。下面两个是我看到的两个很精彩的比喻,拿出来和大家分享一下。

第一比喻是关于吞吐量(throughput)和延迟(latency)的。如果你要搞网络性能优化,这两个概念是你必须要知道的,它们看似简单实则不是。我相信包括我在内的很多人都曾经认为大的吞吐量就意味着低延迟,高延迟就意味着吞吐量变小。下面的比喻可以解释这种观点根本不对。该比喻来自这里,我来做个大体意译(非逐字翻译)。

我们可以把网络发送数据包比喻成去街边的 ATM 取钱。每一个人从开始使用 ATM 到取钱结束整个过程都需要一分钟,所以这里的延迟是60秒,那吞吐量呢?当然是 1/60 人/秒。现在银行升级了他们的 ATM 机操作系统,每个人只要30秒就可以完成取款了!延迟是 30秒,吞吐量是 1/30 人/秒。很好理解,可是前面的问题依然存在对不对?别慌,看下面。

因为这附近来取钱的人比较多,现在银行决定在这里增加一台 ATM 机,一共有两台 ATM 机了。现在,一分钟可以让4个人完成取钱了,虽然你去排队取钱时在 ATM 机前还是要用 30 秒!也就是说,延迟没有变,但吞吐量增大了!可见,吞吐量可以不用通过减小延迟来提高。

好了,现在银行为了改进服务又做出了一个新的决定:每个来取钱的客户在取完钱之后必须在旁边填写一个调查问卷,用时也是30秒。那么,现在你去取钱的话从开始使用 ATM 到完成调查问卷离开的时间又是 60 秒了!换句话说,延迟是60秒。而吞吐量根本没变!一分钟之内还是可以进来4个人!可见,延迟增加了,而吞吐量没有变。

从这个比喻中我们可以看出,延迟测量的是每个客户(每个应用程序)感受到的时间长短,而吞吐量测量的是整个银行(整个操作系统)的处理效率,是两个完全不同的概念。用作者的原话说是:

In short, the throughput is a function of how many stages are in parallel while latency is a function of how many are in series when there are multiple stages in the processing. The stage with the lowest throughput determines the overall throughput.
正如银行为了让客户满意不光要提高自身的办事效率外,还要尽量缩短客户在银行办事所花的时间一样,操作系统不光要尽量让网络吞吐量大,而且还要让每个应用程序发送数据的延迟尽量小。这是两个不同的目标。

另外一个比喻是解释信号量(semaphore)和互斥锁(mutex)的区别。该比喻最初来自这里,我先翻译一下,然后对它做个改进。

互斥锁是一把公共厕所的钥匙。一个人使用厕所的时候可以拿到这把钥匙,用完之后把这把钥匙交给排队的下一个人。

信号量是没有人使用的厕所的钥匙数量,所有厕所的钥匙都一样。比如有4个厕所有相同的钥匙和锁。信号量的值就是钥匙的数量,一开始是4。当进来一个人的时候数量就是少一个,如果4个厕所都满了,信号量就成0了,出去一个人就增加1,并把钥匙交给排队的下一个人。

这个比喻并不是太好,尤其是它无法解释 二元(binary)信号量和互斥锁的区别!我把这个比喻做了改进。互斥锁的比喻还是和上面一样,需要指出的是,当你拿到那把钥匙的时候你就是它的拥有者(owner),别人是无法打开厕所门的。

而信号量到底是什么呢?它就是一个大的公共厕所,里面有若干个位置,外面的大门口有一个可以翻动牌子写着“已满”和“可用”,当里面还有空的位置的时候,进去的人不用翻动这个牌子,直到没有位置时最后一个进去的人必须把它设成“已满”,这时后面的人必须排队等候,然后出去的人必须把牌子翻到“可用”,如果需要的话。

很好理解对嘛?那么它怎么解释二元信号量呢?也就是当这个厕所里面只能容纳一个人的时候,每个人进去的时候都要把门口的牌子翻到“已满”,出去的时候翻到“可用”。它和互斥锁的区别马上就可以看出来了,翻动的牌子在外面可以被别人翻的,而锁住的锁只有拿钥匙的人才可以开!

当然了,信号量之所以翻译成“信号”,还是有道理的,因为它(厕所门口的牌子)标示的是资源(厕所空位)的状态,而互斥锁就是锁,它实实在在地锁住了资源。这在生产者消费者的情况下区别更明显。

深入理解 VXLAN

VXLAN 是非常新的一个 tunnel 技术,它是一个 L2 tunnel。Linux 内核的 upstream 中也刚刚加入 VXLAN 的实现。相比 GRE tunnel 它有着很的扩展性,同时解决了很多其它问题。

一,GRE tunnel 的不足

网络很多介绍 VXLAN 的文章都没有直接告诉你相比较 GRE tunnel,VXLAN 的优势在哪里,或者说 GRE tunnel 的不足在哪里。为了更好的了解 VXLAN,我们有必要看一下 GRE tunnel 的不足。

在我前面写的介绍 GRE tunnel 的文章中,其实并不容易看出 GRE tunnel 的不足之处。根本原因是图中给出的例子不太好,只有两个网络的话 GRE tunnel 的不足凸显不出来,让我们看看有三个网络的情况如何用 GRE tunnel 互联,如下图所示:

这下子就很明显了,要让这三个网络互联,我们需要建立三个 GRE tunnel。如果网络数量再增长,那么需要的 tunnel 数量更多。换句话说,GRE tunnel 的扩展性太差,从根本上讲还是因为它只是一个 point to point 的 tunnel。

二,VLAN 的不足

其实 VLAN 在某种程度上也可以看作一个 L2 over L2 的 tunnel,只不过它多了一个新的 VLAN header,这其中有12 bit 是 VLAN tag。所以 VLAN 的第一个不足之处就是它最多只支持 4096 个 VLAN 网络(当然这还要除去几个预留的),对于大型数据中心的来说,这个数量是远远不够的。

第二个不足就是,VLAN 这个所谓的 tunnel 是基于 L2 的,所以很难跨越 L2 的边界,在很大程度上限制了网络的灵活性。同时,VLAN 操作需手工介入较多,这对于管理成千上万台机器的管理员来说是难以接受的。

三,VXLAN 的引入

VXLAN 是 Virtual eXtensible LANs 的缩写,所以顾名思义,它是对 VLAN 的一个扩展,但又不仅限于此。

从数量上讲,它确实把 12 bit 的 VLAN tag 扩展成了 24 bit,所以至少暂时够用的了。从实现上讲,它是 L2 over UDP,它利用了 UDP 同时也是 IPv4 的单播和多播,可以跨 L3 边界,很巧妙地解决了 GRE tunnel 和 VLAN 存在的不足,让组网变得更加灵活。

四,VXLAN 的实现

VXLAN 的配置可以参考内核文档 Documentation/networking/vxlan.txt,本人目前还没有环境测试,所以只能做一些代码分析了。

Linux 内核中对 VXLAN 的实现是在 drivers/net/vxlan.c 源文件中,是由 Stephen Hemminger (iproute2 的维护者)完成的。代码质量相当高,所以可读性也很好,强烈推荐阅读一下。

看代码之前先看 VXLAN 的头是一个怎样的结构,如下图所示(图片来自参考资料4):

好了,现在我们可以看代码了。先看发送端,vxlan_xmit() 函数。首先需要说的是发送之前内核会检查目的地址,如果是L2 multicast,那么应该发送到 VXLAN group 组播地址,否则,如果 MAC 地址是已知的,直接单播到对应的 IP;如果未知,则广播到组播地址。代码如下,比文档还要好读。:-)

[c]
static __be32 vxlan_find_dst(struct vxlan_dev vxlan, struct sk_buff skb)
{
const struct ethhdr eth = (struct ethhdr ) skb->data;
const struct vxlan_fdb *f;

    if (is_multicast_ether_addr(eth-&gt;h_dest))
            return vxlan-&gt;gaddr;

    f = vxlan_find_mac(vxlan, eth-&gt;h_dest);
    if (f)
            return f-&gt;remote_ip;
    else
            return vxlan-&gt;gaddr;

}
[/c]

剩下的基本上就是一层一层的往外添加头了,依次添加 VXLAN header,UDP header,IP header:
[c]
//…
vxh = (struct vxlanhdr ) __skb_push(skb, sizeof(vxh));
vxh->vx_flags = htonl(VXLAN_FLAGS);
vxh->vx_vni = htonl(vxlan->vni <dest = htons(vxlan_port);
uh->source = htons(src_port);

    uh-&gt;len = htons(skb-&gt;len);
    uh-&gt;check = 0;

    __skb_push(skb, sizeof(*iph));
    skb_reset_network_header(skb);
    iph             = ip_hdr(skb);
    iph-&gt;version    = 4;
    iph-&gt;ihl        = sizeof(struct iphdr) &gt;&gt; 2;
    iph-&gt;frag_off   = df;
    iph-&gt;protocol   = IPPROTO_UDP;
    iph-&gt;tos        = vxlan_ecn_encap(tos, old_iph, skb);
    iph-&gt;daddr      = dst;
    iph-&gt;saddr      = fl4.saddr;
    iph-&gt;ttl        = ttl ? : ip4_dst_hoplimit(&amp;rt-&gt;dst);

    vxlan_set_owner(dev, skb);

[/c]

正如 GRE tunnel,比较复杂的地方是在接收端。因为 VXLAN 利用了 UDP,所以它在接收的时候势必需要有一个 UDP server 在监听某个端口,这个是在 VXLAN 初始化的时候完成的,即 vxlan_init_net() 函数:

[c]
static __net_init int vxlan_init_net(struct net net)
{
struct vxlan_net
vn = net_generic(net, vxlan_net_id);
struct sock *sk;
struct sockaddr_in vxlan_addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
};
int rc;
unsigned h;

/* Create UDP socket for encapsulation receive. */
rc = sock_create_kern(AF_INET, SOCK_DGRAM, IPPROTO_UDP, &amp;vn-&gt;sock);
if (rc sock-&gt;sk;
sk_change_net(sk, net);

vxlan_addr.sin_port = htons(vxlan_port);

rc = kernel_bind(vn-&gt;sock, (struct sockaddr *) &amp;vxlan_addr,
         sizeof(vxlan_addr));
if (rc sock = NULL;
    return rc;
}

/* Disable multicast loopback */
inet_sk(sk)-&gt;mc_loop = 0;

/* Mark socket as an encapsulation socket. */
udp_sk(sk)-&gt;encap_type = 1;
udp_sk(sk)-&gt;encap_rcv = vxlan_udp_encap_recv;
udp_encap_enable();

for (h = 0; h vni_list[h]);

return 0;

}
[/c]

由此可见内核内部创建 socket 的 API 是sock_create_kern(),bind() 对应的是 kernel_bind()。注意到这里实现了一个hook,vxlan_udp_encap_recv(),这个正是接收端的主要代码。

发送端是一层一层往外填,那么接收端一定就是一层一层外里剥:

[c]
/ pop off outer UDP header /
__skb_pull(skb, sizeof(struct udphdr));

/* Need Vxlan and inner Ethernet header to be present */
if (!pskb_may_pull(skb, sizeof(struct vxlanhdr)))
    goto error;

/* Drop packets with reserved bits set */
vxh = (struct vxlanhdr *) skb-&gt;data;
if (vxh-&gt;vx_flags != htonl(VXLAN_FLAGS) ||
    (vxh-&gt;vx_vni &amp; htonl(0xff))) {
    netdev_dbg(skb-&gt;dev, "invalid vxlan flags=%#x vni=%#xn",
           ntohl(vxh-&gt;vx_flags), ntohl(vxh-&gt;vx_vni));
    goto error;
}

__skb_pull(skb, sizeof(struct vxlanhdr));

/* Is this VNI defined? */
vni = ntohl(vxh-&gt;vx_vni) &gt;&gt; 8;
vxlan = vxlan_find_vni(sock_net(sk), vni);
if (!vxlan) {
    netdev_dbg(skb-&gt;dev, "unknown vni %dn", vni);
    goto drop;
}

if (!pskb_may_pull(skb, ETH_HLEN)) {
    vxlan-&gt;dev-&gt;stats.rx_length_errors++;
    vxlan-&gt;dev-&gt;stats.rx_errors++;
    goto drop;
}

[/c]

在重新入栈之前还要做一些准备工作:

[c]
/ Re-examine inner Ethernet packet /
oip = ip_hdr(skb);
skb->protocol = eth_type_trans(skb, vxlan->dev);

/* Ignore packet loops (and multicast echo) */
if (compare_ether_addr(eth_hdr(skb)-&gt;h_source,
               vxlan-&gt;dev-&gt;dev_addr) == 0)
    goto drop;

if (vxlan-&gt;learn)
    vxlan_snoop(skb-&gt;dev, oip-&gt;saddr, eth_hdr(skb)-&gt;h_source);

__skb_tunnel_rx(skb, vxlan-&gt;dev);
skb_reset_network_header(skb);
skb-&gt;ip_summed = CHECKSUM_NONE;

[/c]

另外需要特别指出的是:1) 加入和离开组播地址是在 vxlan_open() 和 vxlan_stop() 中完成的;2) Linux 内核已经把 bridge 的 L2 learn 功能给抽出来了,所以 VXLAN 也实现了对 L2 地址的学习和转发:

[c]
static const struct net_device_ops vxlan_netdev_ops = {
.ndo_init = vxlan_init,
.ndo_open = vxlan_open,
.ndo_stop = vxlan_stop,
.ndo_start_xmit = vxlan_xmit,
.ndo_get_stats64 = vxlan_stats64,
.ndo_set_rx_mode = vxlan_set_multicast_list,
.ndo_change_mtu = eth_change_mtu,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = eth_mac_addr,
.ndo_fdb_add = vxlan_fdb_add,
.ndo_fdb_del = vxlan_fdb_delete,
.ndo_fdb_dump = vxlan_fdb_dump,
};
[/c]

附注:openvswitch 中的 VXLAN 的实现:http://openvswitch.org/pipermail/dev/2011-October/012051.html

参考资料:

1. http://tools.ietf.org/html/draft-mahalingam-dutt-dcops-vxlan-02
2. http://blogs.cisco.com/datacenter/digging-deeper-into-vxlan/
3. http://www.yellow-bricks.com/2012/11/02/vxlan-use-cases/
4. http://www.borgcube.com/blogs/2011/11/vxlan-primer-part-1/
5. http://www.borgcube.com/blogs/2012/03/vxlan-primer-part-2-lets-get-physical/
6. http://it20.info/2012/05/typical-vxlan-use-case/

在 openvswitch 上配置 GRE tunnel

如果你是用 openvswitch 内置的 GRE tunnel,那么配置很简单,基本上就一条命令:

ovs-vsctl add-port br0 gre0 -- set interface gre0 type=gre options:remote_ip=192.168.1.10

本文想谈的显然不是这个。因为 upstream 内核(指 Linus tree)中的 openvswitch 是不支持 GRE tunnel 的,那我们如何在 upstream 内核中上使用 GRE tunnel 呢?

其实也不难,我们可以创建一个普通的 GRE tunnel,然后把它添加到 openvswitch bridge 中去就可以了。至少这在理论上是可行的,而实际操作中却有一些问题,你可以动手试一试。我们假设网络环境如下图所示:

我们的目标是让 HOST1 上面的两个 VM 和 HOST2 上面的两个 VM 通过 GRE tunnel 实现通信。因为两边的配置是对称的,所以下面只说明 HOST2 上是如何配置的,HOST1 上以此类推即可。

在这个环境中,一个很可能的错误是把 HOST2 上的 uplink,即 eth0 也加入到 openvswitch 的 bridge 中,这是不对的,需要加入仅仅的是 GRE tunnel 设备,即 gre1 (你当然也可以把它命名为其它名字)。

剩下的一个最重要的问题是,GRE tunnel 是无法回应 ARP 的,因为它是一个 point to point 的设备(ip addr add 192.168.2.1/24 peer 192.168.1.1/24 dev gre1),所以很明显设置了 NOARP。这个问题是这里的关键。因为这个的缘故,即使你在 VM1 上也无法 ping HOST2 上的 gre1。所以这里需要一个技巧,就是要给 bridge 本身配置一个 IP 地址,然后让 bridge 做一个 ARP proxy

所以最后在 HOST2 上面的配置如下:

[root@host2 ~]# ip tunnel show
gre0: gre/ip  remote any  local any  ttl inherit  nopmtudisc
gre1: gre/ip  remote 10.16.43.214  local 10.16.43.215  ttl inherit
[root@host2 ~]# ip r s
192.168.2.0/24 dev ovsbr0  proto kernel  scope link  src 192.168.2.4
192.168.1.0/24 dev gre1  scope link
192.168.122.0/24 dev virbr0  proto kernel  scope link  src 192.168.122.1
10.16.40.0/21 dev eth0  proto kernel  scope link  src 10.16.43.215
169.254.0.0/16 dev eth0  scope link  metric 1005
default via 10.16.47.254 dev eth0
[root@host2 ~]# ovs-vsctl show
71f0f455-ccc8-4781-88b2-4b663dd48c5f
    Bridge "ovsbr0"
        Port "vnet0"
            Interface "vnet0"
        Port "ovsbr0"
            Interface "ovsbr0"
                type: internal
        Port "vnet1"
            Interface "vnet1"
        Port "gre1"
            Interface "gre1"
    ovs_version: "1.7.0"

[root@host2 ~]# ip addr ls gre1 && ip addr ls ovsbr0
17: gre1:  mtu 1476 qdisc noqueue state UNKNOWN
    link/gre 10.16.43.215 peer 10.16.43.214
    inet 192.168.2.1/24 scope global gre1
    inet 192.168.2.1 peer 192.168.1.1/24 scope global gre1
13: ovsbr0:  mtu 1500 qdisc noqueue state UNKNOWN
    link/ether 66:d7:ae:42:db:44 brd ff:ff:ff:ff:ff:ff
    inet 192.168.2.4/24 brd 192.168.2.255 scope global ovsbr0
    inet6 fe80::64d7:aeff:fe42:db44/64 scope link
       valid_lft forever preferred_lft forever

[root@host2 ~]# cat /proc/sys/net/ipv4/conf/ovsbr0/proxy_arp
1

HOST2 上面的 VM1 只需添加一个路由即可,配置如下:
[root@localhost ~]# ip r s
192.168.2.0/24 dev eth1  proto kernel  scope link  src 192.168.2.2
192.168.1.0/24 dev eth1  scope link  src 192.168.2.2
[root@localhost ~]# ping 192.168.1.2
PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data.
64 bytes from 192.168.1.2: icmp_seq=1 ttl=62 time=561 ms
64 bytes from 192.168.1.2: icmp_seq=2 ttl=62 time=0.731 ms
64 bytes from 192.168.1.2: icmp_seq=3 ttl=62 time=0.669 ms
64 bytes from 192.168.1.2: icmp_seq=4 ttl=62 time=0.765 ms

--- 192.168.1.2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3823ms
rtt min/avg/max/mdev = 0.669/140.860/561.275/242.726 ms

深入理解 GRE tunnel

我以前写过一篇介绍 tunnel 的文章,只是做了大体的介绍。里面多数 tunnel 是很容易理解的,因为它们多是一对一的,换句话说,是直接从一端到另一端。比如 IPv6 over IPv4 的 tunnel,也就是 SIT,它的原理如下图所示:

显然,除了端点的 host A 和 host B之外,中间经过的任何设备都是看不到里面的 IPv6 的头,对于它们来说,经过 sit 发出的包和其它的 IPv4 的包没有任何区别。

GRE tunnel 却不一样了,它的原理从根本上和 sit,ipip 这样的 tunnel 就不一样。除了外层的 IP 头和内层的 IP 头之间多了一个 GRE 头之外,它最大的不同是,tunnel 不是建立在最终的 host 上,而是在中间的 router 上!换句话说,对于端点 host A 和 host B 来说,该 tunnel 是透明的(对比上面的 sit tunnel)。这是网上很多教程里没有直接告诉你的。理解这一点非常关键,正是因为它这么设计的,所以它才能解决 ipip tunnel 解决不了的问题。

所以,经过 GRE tunnel 发送的包(从 host A 发送到 host B)大体过程是这样子的(仍然借用了 LARTC 中的例子):

我们可以看出,从 host A 发出的包其实就是一个很普通的 IP 包,除了目的地址不直接可达外。该 GRE tunnel 的一端是建立在 router A上,另一段是建立在 router B上,所以添加外部的 IP 头是在 router A 上完成的,而去掉外面的 IP 头是在 router B上完成的,两个端点的 host 上几乎什么都不用做(除了配置路由,把发送到 10.0.2.0 的包路由到 router A)!

这么设计的好处也就很容易看出来了,ipip tunnel 是端对端的,通信也就只能是点对点的,而 GRE tunnel 却可以进行多播。

现在让我们看看Linux内核是怎么实现的,我们必须假设 router A 和 B 上都是运行的 Linux,而 host A 和 B上运行什么是无所谓的。

发送过程是很简单的,因为 router A 上配置了一条路由规则,凡是发往 10.0.2.0 网络的包都要经过 netb 这个 tunnel 设备,在内核中经过 forward 之后就最终到达这个 GRE tunnel 设备的 ndo_start_xmit(),也就是 ipgre_tunnel_xmit() 函数。这个函数所做的事情无非就是通过 tunnel 的 header_ops 构造一个新的头,并把对应的外部 IP 地址填进去,最后发送出去。

稍微难理解的是接收过程,即 router B 上面进行的操作。这里需要指出的一点是,GRE tunnel 自己定义了一个新的 IP proto,也就是 IPPROTO_GRE。当 router B 收到从 router A 过来的这个包时,它暂时还不知道这个是 GRE 的包,它首先会把它当作普通的 IP 包处理。因为外部的 IP 头的目的地址是该路由器的地址,所以它自己会接收这个包,把它交给上层,到了 IP 层之后才发现这个包不是 TCP,UDP,而是 GRE,这时内核会转交给 GRE 模块处理。

GRE 模块注册了一个 hook:

[c]
static const struct gre_protocol ipgre_protocol = {
.handler = ipgre_rcv,
.err_handler = ipgre_err,
};
[/c]

所以真正处理这个包的函数是 ipgre_rcv() 。ipgre_rcv() 所做的工作是:通过外层IP 头找到对应的 tunnel,然后剥去外层 IP 头,把这个“新的”包重新交给 IP 栈去处理,像接收到普通 IP 包一样。到了这里,“新的”包处理和其它普通的 IP 包已经没有什么两样了:根据 IP 头中目的地址转发给相应的 host。注意:这里我所谓的“新的”包其实并不新,内核用的还是同一个copy,只是skbuff 里相应的指针变了。

以上就是Linux 内核对 GRE tunnel 的整个处理过程。另外,GRE 的头如下所示(图片来自这里):

IP header in GRE tunnel

GRE header

How TSQ works

通常看到 TCP Small Queue (简称 TSQ)之后第一问题是,既然我们已经有 tcp_wmem 了,为何还需要一个新的 sysctl?

这个问题是理解 TSQ 的关键,其实 tcp_wmem 仅仅是从 TCP socket 层限制队列(或 buffer)中最多可以 queue 多少数据包,而实际上,一个包从 TCP 层发出到最后到达网卡,中间还经历了很多个 queue,TCP socket 只是其中一层罢了。而 TSQ 用了一个非常聪明的技巧来限制在所有这些 queue 中的包,从而降低 latency。

TSQ 的代码中最关键的一个地方是它实现了一个新的 skb destructor,也就是 tcp_wfree(),看它的定义:

[c]
static void tcp_wfree(struct sk_buff skb)
{
struct sock
sk = skb->sk;
struct tcp_sock *tp = tcp_sk(sk);

    if (test_and_clear_bit(TSQ_THROTTLED, &amp;tp-&gt;tsq_flags) &amp;&amp;
        !test_and_set_bit(TSQ_QUEUED, &amp;tp-&gt;tsq_flags)) {
            unsigned long flags;
            struct tsq_tasklet *tsq;

            /* Keep a ref on socket.
             * This last ref will be released in tcp_tasklet_func()
             */
            atomic_sub(skb-&gt;truesize - 1, &amp;sk-&gt;sk_wmem_alloc);

            /* queue this socket to tasklet queue */
            local_irq_save(flags);
            tsq = &amp;__get_cpu_var(tsq_tasklet);
            list_add(&amp;tp-&gt;tsq_node, &amp;tsq-&gt;head);
            tasklet_schedule(&amp;tsq-&gt;tasklet);
            local_irq_restore(flags);
    } else {
            sock_wfree(skb);
    }

}
[/c]

我们知道在Linux内核网络子系统中,kfree_skb() 是内核丢包的地方,而 skb 的 destructor 就是在丢包时被调用的,用来清理和该 skb 相关的东西。TSQ 实现新的 destructor 就是想在包被丢弃的时候做一些动作,也就是如果条件合适(设置了 TSQ_THROTTLED,没有设置 TSQ_QUEUED),那么就把它加入进 TSQ。

而在下一个 softIRQ 时,相应的 tasklet 就会调度,从而触发 tcp_tasklet_func():

[c]
static void tcp_tasklet_func(unsigned long data)
{
struct tsq_tasklet tsq = (struct tsq_tasklet )data;
LIST_HEAD(list);
unsigned long flags;
struct list_head q, n;
struct tcp_sock tp;
struct sock
sk;

    local_irq_save(flags);
    list_splice_init(&amp;tsq-&gt;head, &amp;list);
    local_irq_restore(flags);

    list_for_each_safe(q, n, &amp;list) {
            tp = list_entry(q, struct tcp_sock, tsq_node);
            list_del(&amp;tp-&gt;tsq_node);

            sk = (struct sock *)tp;
            bh_lock_sock(sk);

            if (!sock_owned_by_user(sk)) {
                    tcp_tsq_handler(sk);
            } else {
                    /* defer the work to tcp_release_cb() */
                    set_bit(TCP_TSQ_DEFERRED, &amp;tp-&gt;tsq_flags);
            }
            bh_unlock_sock(sk);

            clear_bit(TSQ_QUEUED, &amp;tp-&gt;tsq_flags);
            sk_free(sk);
    }

}
[/c]

这里对加入到 TSQ 队列中的 socket 进行处理,对于没有 owner 的 socket(不是由用户使用的socket),直接发送;否则就是推迟到 tcp_release_cb() 中发送,即 release_sock() 的时候。

现在,我们从总体上可以看出 TSQ 的用意了:当发送的包超过 sysctl_tcp_limit_output_bytes 时,就会发生抖动(throttle),这时就会把包推迟发送,推迟到什么时候呢?当然是该队列有空闲的时候!那么什么时候有空闲呢?包被丢弃的时候!内核发送的包是在即将被丢弃的时候(忽略tasklet,用tasklet仅仅是因为skb destructor 中不能进行发送),用户层发送的包则是关闭 socket 的时候,这种时候 TSQ 会有新的空间,所以可以重新入队。

可见,TSQ 的聪明之处在于,它虽然有自己的所谓队列,但并没有计算该队列的长度,而是巧妙地利用了内核中几个关键点来判断何时可以入队!这当然需要对 TCP 实现非常熟悉才可以,由此亦可见作者水平之高。