发送 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 实现非常熟悉才可以,由此亦可见作者水平之高。

accept_ra 的一个例子

在 IPv6 中,Router Advertisement (简称 RA)是很关键的一个 ICMP 包,stateless autoconf 就是靠 RA 配置 IP 地址的,主机发送Router Solicitation(RS)广播,有点类似于 IPv4 中的 DHCP request,路由器就会回应 RA,类似于 DHCP reply。不同的是,DHCP 是 stateful 的,因为 DHCP 服务器必须维持一个地址表,而 IPv6 的路由器却不需要,所以是无状态的,而且 RA 的广播是周期性的。

以上是技术背景。显然,一般情况下你是要接受这个包的。而我遇到一种情况却不得不禁止掉这个选项。

情况是这样的,两台主机通过一个交换机连接,现在我们修改两台主机以及交换机的 MTU 为 9000,来发送大小为 8000 的 Jumbo Frame,IPv4 一切正常,可是到了 IPv6 下面却不行了,会收到 ICMPV6_PKT_TOOBIG。

在这里,需要指出 IPv6 和 IPv4 一个重要的区别是,包的分片(fragmentation)是在主机上完成的,而不是由 router 完成。wikipedia 上说:

In IPv4, routers perform fragmentation, whereas in IPv6, routers do not fragment, but drop the packets that are larger than the MTU.
而我这里所谓的主机完成是指 PMTU 的计算是由主机完成的:既然 IPv6 的 router 不进行分片,那么主机必须在发送之前知道整个 path 上最小的 MTU,即 PMTU。

好了,既然我们修改了主机的 MTU,那么 PMTU 也需要更新,尤其是 IPv6 路由表中的 MTU。幸运的是,这个是由内核自动完成的,通过 NETDEV_CHANGEMTU 这个异步通知完成。ip -6 r s 也可以看出对应的 MTU 确实变成了 9000,但是,不一会儿我就发现该 MTU 还会自动变会成之前的 1500。这很奇怪,然后我就捕捉一些包看看,发现原来每当收到 RA 的时候这个 MTU 就会变化!不用看代码你也能马上理解,RA 里肯定也包含了一个 router 的 MTU,这个 MTU 是 1500,所以 PMTU 才会跳回到 1500!

可是我们不是已经修改交换机的 MTU 了,为什么它还会发出 RA (MTU=1500) 的广播?连接上交换机一看,jumbo MTU 确实是改成了 9000,但 routing MTU 依旧是 1500,而且无法修改成 9000,毕竟千兆网卡还没有普及。所以现在的问题就是,这个配置其实是对的,8000 字节的包明显在物理上也是可以发送出去的,但 IPv6 就因为交换机的 routing MTU 是 1500 而无法发送,这种情况下我们就不得不禁止掉 accept_ra 这个选项了,地址的获取用 DHCPv6 了。

An overview of Openvswitch implementation

Author: Cong Wang <xiyou.wangcong@gmail.com>

This is NOT a tutorial on how to use openvswitch, this is for developers who want to know the implementation details of openvswitch project, thus, I assume you at least know the basic concepts of openvswitch and know how to use it. If not, see www.openvswitch.org to get some documents or slides.

Let’s start from the user-space part. Openvswitch user-space contains several components: they are a few daemons which actually implements the switch and the flow table, the core of openvswitch, and several utilities to manage the switch, the database, and even talk to the kernel directly.

There are three daemons started by openvswitch service: ovs-vswitchd, which is the core implementation of the switch; ovsdb-server, which manipulates the database of the vswitch configuration and flows; and ovs-brcompatd which keeps the compatibility with the traditional bridges (that is the one you create with ‘brctl’ command) .

Their relationship is shown in the picture below:

Obviously, the most important one is ovs-vswitchd, it implements the switch, it directly talks with kernel via netlink protocol (we will see the details later). And ovs-vsctl is the utility used primarily to manage the switch, which of course needs to talk with ovs-vswitchd. Ovs-vswitchd saves and changes the switch configuration into a database, which is directly managed by ovsdb-server, therefore, ovs-vswitchd will need to talk to ovsdb-server too, via Unix domain socket, in order to retrieve or save the configuration information. This is also why your openvswitch configuration could survive from reboot even you are not using ifcfg* files.

Ovs-brcompatd is omitted here, we are not very interested in it now.

Ovs-vsctl can do lots of things for you, so most of time, you will use ovs-vsctl to manage openvswitch. Ovs-appctl is also available to manage the ovs-vswitchd itself, it sends some internal commands to ovs-vswitchd daemon to change some configurations.

However, sometimes you may need to manage the datapath in the kernel directly by yourself, in this case, assume ovs-vswitchd is not running, you can invoke ovs-dpctl to let ovs-vswitchd to manage the datapath in the kernel space directly, without database.

And, when you need to talk with ovsdb-server directly, to do some database operation, you can run ovsdb-client, or you want to manipulate the database directly without ovsdb-server, ovsdb-tool is handy too.

What’s more, openvswitch can be also administered and monitored by a remote controller. This is why we could define the network by software! sFlow is a protocol for packet sampling and monitoring, while OpenFlow is a protocol to manage the flow table of a switch, bridge, or device. Openvswitch supports both OpenFlow and sFlow. With ovs-ofctl, you can use OpenFlow to connect to the switch and do some monitoring and administering in the remote. sFlowTrend, which is not a part of openvswitch package, is the one that is capable for sFlow.

Now, let’s take a look at the kernel part.

As mentioned previously, the user-space communicates with the kernel-space via netlink protocol, generic netlink is used in this case. So there are several groups of genl commands defined by the kernel, they are used to get/set/add/delete some datapath/flow/vport and execute some actions on a specific packet.

The ones used to control datapatch are:

enum ovs_datapath_cmd {

OVS_DP_CMD_UNSPEC,

OVS_DP_CMD_NEW,

OVS_DP_CMD_DEL,

OVS_DP_CMD_GET,

OVS_DP_CMD_SET

};

and there are corresponding kernel functions which does the job:

ovs_dp_cmd_new()

ovs_dp_cmd_del()

ovs_dp_cmd_get()

ovs_dp_cmd_set()

Similar functions are defined for vport and flow commands too.

Before we talk about the details of the data structures, let’s see how a packet is sent out or received from a port of an ovs bridge. When is packet is send out from the ovs bridge, an internal device defined by openvswitch module, which is viewed, by the kernel, as a struct vport too. The packet will be finally passed to internal_dev_xmit(), which in turn “receives” the packet. Now, the kernel needs to look at the flow table, to see if there is any “cache” for how to forward this packet. This is done by function ovs_flow_tbl_lookup(), which needs a key. The key is extracted by ovs_flow_extract() which briefly collects the details of the packet (L2~L4) and then constructs a unique key for this flow. Assume this is the first packet going out after we create the ovs bridge, so, there is no “cache” in the kernel, and the kernel doesn’t know how to handle this packet! Then it will pass it to the user-space with “upcall” which uses genl too. The user-space daemon, ovs-vswitchd, will check the database and see which is the destination port for this packet, and will response to the kernel with OVS_ACTION_ATTR_OUTPUT to tell kernel which is the port it should forward to, in this case let’s assume it is eth0. and finally a OVS_PACKET_CMD_EXECUTE command is to let the kernel execute the action we just set. That is, the kernel will execute this genl command in function do_execute_actions() and finally forward the packet to the port “eth0” with do_output(). Then it goes to outside!

The receiving side is similar. The openvswitch module registers an rx_handler for the underlying (non-internal) devices, it is netdev_frame_hook(), so once the underlying device receives packets on wire, openvswitch will forward it to user-space to check where it should goes, and what actions it needs to execute on it. For example, if this is a VLAN packet, the VLAN tag should be removed from the packet first, and then forwarded to a right port. The user-space could learn that which is the right port to forward a given packet.

The internal devices are special, when a packet is sent to an internal device, it is be immediately sent up to openvswitch to decide where it should go, instead of really sending it out. There is actually no way out of an internal device directly.

Besides OVS_ACTION_ATTR_OUTPUT, the kernel also defines some other actions:

OVS_ACTION_ATTR_USERSPACE, which tells the kernel to pass the packet to user-space

OVS_ACTION_ATTR_SET: Modify the header of the packet

OVS_ACTION_ATTR_PUSH_VLAN: Insert a vlan tag into the packet

OVS_ACTION_ATTR_POP_VLAN: Remove the vlan tag from the packet

OVS_ACTION_ATTR_SAMPLE: Do sampling

With these commands combined, the user-space could implement some different policies, like a bridge, a bond or a VLAN device etc. GRE tunnel is not currently in upstream, so we don’t care about it now.

So far, you already know how the packets are handled by openvswitch module. There are much more details, especially about the flow and datapath mentioned previously. A flow in kernel is represented as struct sw_flow, and datapath is defined as struct datapath, and the actions on a flow is defined as struct sw_flow_actions, and plus the one we mentioned, struct vport. These structures are the most important ones for openvswitch kernel module, their relationship is demonstrated in this picture:

The most important one needs to mention is each struct sk_buff is associated with a struct sw_flow, which is via a pointer in ovs control block. And the above actions is associated with each flow, every time when a packet passed to openvswitch module, it first needs to lookup the flow table which is contained in a datapath which in turn either contains in a struct vport or in a global linked-list, with the key we mentioned. If a flow is found, the corresponding actions will be executed. Remember that datapath, flow, vport, all could be changed by the user-space with some specific genl command.

As you can see, the kernel part only implements a mechanism and the fast path (except the first packet), and the user-space implements different policies upon the mechanism provided by the kernel, the slow path. The user-space is much more complicated, so I will not cover its details here.

Update: Ben, one of the most active developers for openvswitch, pointed out some mistakes in the previous version, I updated this article as he suggested. Thanks to Ben!

从 IPv4 到 IPv6

稍微熟悉 IPv6 的人都知道,IPv6 相对于 IPv4 并不只是简单地把地址从 32 位扩展到了 128 位,它同时还修复了 IPv4 协议设计中的一些不足。正如地址空间一样,当然设计 IPv4 时的不足已经逐渐凸显,理解这些 IPv4 的不足以及 IPv6 对它的改进对于深入理解 IPv6 至关重要。我在学习 IPv6 的时候就特别想找一个这样的文档,可惜一直没有发现,所以在这里简单总结一下,以方便初学 IPv6 的人。

1. 地址空间的划分

从 32 位一下子扩展到 128 位一个最明显的变化是,你很可能再也无法用脑子记住一个 IP 地址(除了 ::1 这种简单的地址)。玩笑。:-)

言归正传,IPv4 中的 A、B、C 类地址划分在 IPv6 中不存在了,这种死板的分配方式问题多多,本身就属于设计的不合理,连 IPv4 自己都已经舍弃它改用 Classless Inter-Domain Routing 了。所以,IPv6 的一个显著的变化是没了网络掩码(mask),而改用前缀(prefix)了,这样可以完全通过前缀决定网络了。同时,网络掩码的中间可以包含0,可它几乎没有什么用处,但前缀却是直接指定前x位含1,直接避免了含0的可能性,简化了设计。

在功能上,IPv4 中的地址分为三类:单播(unicast)、多播(multicast)和广播(broadcast),而 IPv6 中不再有广播地址,反而多了一个任播( anycast)。wikipedia 上有一幅图可以很好的解释它们的区别:

但是,任播地址是直接从单播地址上拿出来,并没有单独分类,IPv6 协议这么写的:

Anycast addresses are allocated from the unicast address space, using any of the defined unicast address formats. Thus, anycast addresses are syntactically indistinguishable from unicast addresses. When a unicast address is assigned to more than one interface, thus turning it into an anycast address, the nodes to which the address is assigned must be explicitly configured to know that it is an anycast address.
更详细的信息可见 RFC2373。好了,那么单播和多播地址又是如何划分的呢?这一点和 IPv4 一样,也是根据地址中的头几位来区分:

(图片来自《TCP/IP guide》)

从这里你可以看到了,单播地址还有多了一个非常重要的概念:约束(scope)。虽然在 IPv4 中 192.168.. 这种地址被用于私有地址,不应该出现在互联网上,但是,这种保证仅仅取决于路由,而非协议本身。而 IPv6 直接从协议规范上禁止了私有地址(其实叫 link-local 和 site-local )被转发到互联网,它为不同的地址划分了不同的范围,一个范围内的地址只能在这个范围内使用。

如上图,IPv6 规定了三种约束:link-local,site-local,和 global。link-local 的可达范围是本地物理连接的网络,任何路由都不能转发目的地址为这种地址的包(它是给 neighbour discovery 使用的);site-local 的可达范围是整个组织、站点,组织内部的路由可以转发这种包,但是绝对不能把它们转发到互联网上;global 就是可以在互联网上使用的地址。一个设备可以有多个 IPv6 地址,比如一个 link-local 的地址和一个 site-local 的。

多播地址的划分和 IPv4 类似,故此省略。

2. IPv6 头

让我们对比一下 IPv4 和 IPv6 的头部:

(图片来自这里

不难发现,IPv6 的头部结构明显简单了一些:

第一个最明显的改变是没有了 header checksum,这个是没有必要的,因为 checksum 完全可以由上层的 TCP,UDP 协议来做,在 IP 层再做一次是浪费。当然 IPv4 UDP 中的 checksum 可以不做,而到了 IPv6 UDP 中的 checksum 就是必须的了。给 IP 头做 checksum 的另一个缺点是,每次 TTL 改变的时候都需要重新计算 checksum。

第二个改变是没有了 IP options,因此也就没有了 Ip Header Length,因此 IPv6 头部也就是固定的大小,40 个字节。IPv6 之所以这么做一是因为它使用了 next header,使得这个没有必要;二是,这可以提高 IP 头的解析速度。

第三个改变是多了一个 Next Header,正如刚才提到的,如果你对网络比较熟悉的话,你不难发现这个设计的灵感来自于 IPSec,Next Header 是指向下一个头,有点儿类似于单链表。下一个头可以是一个 option(所以IP option没有必要)头,比如 Hop-by-Hop,也可以上一层的协议头,比如 TCP,UDP。这种灵活的设计使得 IPv6 头部可“链接”多个头部。

第四个改变是,IPv6 不再有 Fragment Offset 和 ID,这是因为分片(fragment)的 IPv6 包可以用 Next Header 来表示(知道它强大了吧?),而且 IPv6 协议已经禁止在中间的 router 上进行分片,如果需要分片必须在 host 上完成(PMTU),这可以大大提高路由的速度。其实 IP 包分片性能很差,IPv4 包分片的问题多多:ID 的生成是个问题,在对端把分段的包进行组装又是一个头疼的问题(该预留多少内存?中间少了几个分片怎么办?收到重复的分片怎么办?),所以能尽量避免分片就避免。

3. 邻居发现( Neighbour Discovery

IPv6 相比 IPv4 另一个显著的变化是没有了 ARP 协议,邻居发现协议使用的是 ICMPv6。ARP 协议是一个尴尬的存在,它处于第三层,但又不像 ICMP 协议那样基于 IP,而是和 IP 并列,从它的头部就可以看出:

(图片来自这里

这种设计使得 OSI 分层模式变得含糊。而 IPv6 使用的 ICMPv6 则完全基于 IP 之上,无须设计另外一个并列于 IP 层的协议。正如前面提到,邻居发现协议使用的是 link-local 地址,所以不会造成更大链路范围上的影响。link-local 地址是根据 MAC 地址自动生成的(协议规定的算法),因此有了 link-local 地址就可以马上和路由器进行通信了,因此也就有了 IP 地址自动分配的功能,即类似于 IPv4 的 DHCP,在 IPv6 上这被称为自动配置(Stateless Address Autoconfiguration)。

当然了,因为没有广播地址,邻居发现协议使用的是多播地址。具体协议的细节可以参考 ICMPv6 协议的定义,和 ICMP 并无显著的差别,故省略。

用 scapy 发送 vlan 包的例子

scapy 绝对是你应该学习的一大利器,功能十分强大,把 Python 的优势发挥的淋漓尽致。用它发送定制的数据包比你用C写快好几个数量级。。。

下面是我在测试前面文章中的 vlan 问题时发送带指定 vlan tag 的 arp request,不到10行代码就可以搞定!

[python]

!/usr/bin/python

import sys
from scapy.all import *

mac=”52:54:00:2E:23:92”
eth=Ether(src=mac,type=0x8100)/Dot1Q(vlan=int(sys.argv[1]), prio=5)
arp=ARP(hwtype=0x0001,ptype=0x0800,op=0x0001,hwsrc=mac,psrc=’192.168.122.1’,pdst=’192.168.122.74’)
a=eth/arp
a.show()
sendp(a, iface=”virbr0”)

[/python]