kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

centos7.4 上使用 SQL Server 2019

https://docs.microsoft.com/zh-cn/sql/linux/quickstart-install-connect-red-hat?view=sql-server-ver15

安装 SQL Server

https://packages.microsoft.com/rhel/7/mssql-server-2019/

https://packages.microsoft.com/rhel/7/mssql-server-2019/mssql-server-15.0.4073.23-4.x86_64.rpm

下载 Microsoft SQL Server 2019 Red Hat 存储库配置文件:

1
sudo curl -o /etc/yum.repos.d/mssql-server.repo https://packages.microsoft.com/config/rhel/7/mssql-server-2019.repo

运行以下命令以安装 SQL Server:

1
sudo yum install -y mssql-server

包安装完成后,运行 mssql-conf setup,按照提示设置 SA 密码并选择版本。

1
sudo /opt/mssql/bin/mssql-conf setup

请确保为 SA 帐户指定强密码(最少 8 个字符,包括大写和小写字母、十进制数字和/或非字母数字符号)。

完成配置后,验证服务是否正在运行:

1
systemctl status mssql-server

安装 SQL Server 命令行工具

若要创建数据库,则需要使用可在 SQL Server 上运行 Transact-SQL 语句的工具进行连接。 以下步骤将安装 SQL Server 命令行工具:sqlcmd 和 bcp。

下载 Microsoft Red Hat 存储库配置文件。

1
sudo curl -o /etc/yum.repos.d/msprod.repo https://packages.microsoft.com/config/rhel/7/prod.repo

如果安装了早期版本的 mssql-tools,请删除所有旧的 unixODBC 包。

1
sudo yum remove unixODBC-utf16 unixODBC-utf16-devel

运行以下命令,以使用 unixODBC 开发人员包安装 mssql-tools。 有关详细信息,请参阅安装 Microsoft ODBC Driver for SQL Server (Linux)。

1
sudo yum install -y mssql-tools unixODBC-devel

为方便起见,向 PATH 环境变量添加 /opt/mssql-tools/bin/。 这样可以在不指定完整路径的情况下运行这些工具。 运行以下命令以修改登录会话和交互式/非登录会话的路径:

1
2
3
echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bash_profile
echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc
source ~/.bashrc

本地连接

以下步骤使用 sqlcmd 本地连接到新的 SQL Server 实例。

使用 SQL Server 名称 (-S),用户名 (-U) 和密码 (-P) 的参数运行 sqlcmd 。 在本教程中,用户进行本地连接,因此服务器名称为 localhost。 用户名为 SA,密码是在安装过程中为 SA 帐户提供的密码。

1
sqlcmd -S localhost -U SA -P '<YourPassword>'

可以在命令行上省略密码,以收到密码输入提示。

如果以后决定进行远程连接,请指定 -S 参数的计算机名称或 IP 地址,并确保防火墙上的端口 1433 已打开。

如果成功,应会显示 sqlcmd 命令提示符:1>。

创建和查询数据

下面各部分将逐步介绍如何使用 sqlcmd 新建数据库、添加数据并运行简单查询。

新建数据库

以下步骤创建一个名为 TestDB 的新数据库。

在 sqlcmd 命令提示符中,粘贴以下 Transact-SQL 命令以创建测试数据库:

1
CREATE DATABASE TestDB

在下一行中,编写一个查询以返回服务器上所有数据库的名称:

1
SELECT Name from sys.Databases

前两个命令没有立即执行。 必须在新行中键入 GO 才能执行以前的命令:

1
GO

提示

若要详细了解如何编写 Transact-SQL 语句和查询,请参阅教程:编写 Transact-SQL 语句。

插入数据

接下来创建一个新表 Inventory,然后插入两个新行。

在 sqlcmd 命令提示符中,将上下文切换到新的 TestDB 数据库:

1
USE TestDB

创建名为 Inventory 的新表:

1
CREATE TABLE Inventory (id INT, name NVARCHAR(50), quantity INT)

将数据插入新表:

1
INSERT INTO Inventory VALUES (1, 'banana', 150); INSERT INTO Inventory VALUES (2, 'orange', 154);

要执行上述命令的类型 GO:

1
GO

选择数据

现在,运行查询以从 Inventory 表返回数据。

通过 sqlcmd 命令提示符输入查询,以返回 Inventory 表中数量大于 152 的行:

1
SELECT * FROM Inventory WHERE quantity > 152;

执行此命令:

1
GO

退出 sqlcmd 命令提示符

要结束 sqlcmd 会话,请键入 QUIT:

1
QUIT

sqlcmd

1
sqlcmd -U SA  -P password -Q“SQL语句”  执行完退出

备份语句

备份数据库名为test

1
sqlcmd -U SA  -P password -Q“BACKUP DATABASE test TO  DISK='/tmp/test.bak' with format ,init”

使用sqlcmd 命令还原

例如还原数据库test

1
sqlcmd -U SA -P password  -Q "RESTORE DATABASE  test FROM  DISK='/tmp/sqlserver/test.bak' WITH REPLACE"

master 数据库特殊,需要在单用户模式下备份

方法停止sqlserver数据库,已单用户模式启动数据库

1
/opt/mssql/bin/sqlservr  -m"SQLCMD"   

然后在执行还原命令即可

网桥源码

https://blog.csdn.net/NW_NW_NW/article/details/75045966

https://blog.csdn.net/NW_NW_NW/article/details/75090101

https://blog.csdn.net/NW_NW_NW/article/details/75647220

https://blog.csdn.net/NW_NW_NW/article/details/76022117

https://blog.csdn.net/NW_NW_NW/article/details/76153027

https://blog.csdn.net/NW_NW_NW/article/details/76204707

https://blog.csdn.net/NW_NW_NW/article/details/76674232

https://blog.csdn.net/NW_NW_NW/article/details/76710941

桥初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static int __init br_init(void)
{
	int err;
	/*注册协议生成树收包函数*/
	err = stp_proto_register(&br_stp_proto);
	if (err < 0) {
		pr_err("bridge: can't register sap for STP\n");
		return err;
	}
	/*转发数据库初始化*/
	err = br_fdb_init();
	if (err)
		goto err_out;
	/*在/proc目录下生成任何与bridge相关的目录,如果我们想在/proc下生成bridge相关的子目录或子文件*/
	err = register_pernet_subsys(&br_net_ops);
	if (err)
		goto err_out1;
	/*目前好像没有什么实际作用,在内核中所注册的函数为空*/
	err = br_nf_core_init();
	if (err)
		goto err_out2;
	/*注册相关网络设备的事件通知连*/
	err = register_netdevice_notifier(&br_device_notifier);
	if (err)
		goto err_out3;

	/*注册通知连,主要针对桥转发表事件的相关信息*/
	err = register_switchdev_notifier(&br_switchdev_notifier);
	if (err)
		goto err_out4;
	/*进行netlink的初始化*/
	err = br_netlink_init();
	if (err)
		goto err_out5;

	/*用来处理ioctl命令的函数,比如添加和删除网桥*/
	brioctl_set(br_ioctl_deviceless_stub);

#if IS_ENABLED(CONFIG_ATM_LANE)
	br_fdb_test_addr_hook = br_fdb_test_addr;
#endif
	return 0;
}

了解了桥初始化大致要做的事情后,我们再来看看这些初始化或者注册的事情到底干了些什么?

1.注册协议生成树收包函数stp_proto_register

在桥初始化的时候,注册了一个br_stp_proto参数,此参数的具体模样是这样子的

1
2
3
static const struct stp_proto br_stp_proto = {
	.rcv   = br_stp_rcv,
};

br_stp_rcv函数在/net/bridge/br_stp_bpdu.c中主要针对网桥进行协议交换的帧(BPDU)进行配置操作。

2.桥转发数据库初始化br_fdb_init

此函数就是在内存中建立一块slab cache,以存放net_bridge_fdb_entry

其中:net_bridge_fdb_entry是一个结构体,用来转发数据库的记录项网桥所学到对的每个MAC地址都有这样一个记录

3.在proc目录下生成相关文件的注册函数register_pernet_subsys,初始化的时候给这个函数传递了一个参数br_net_ops,这个参数的模样是这样的

1
2
3
static struct pernet_operations br_net_ops = {
	.exit   = br_net_exit,
};

但是在桥初始化的时候,仅仅注册了br_net_exit,这个函数会将桥下面的所有文件全部清空。

4.通知链的相关函数注册register_netdevice_notifier这个注册函数主要针对设备信息的变化,注册参数br_device_notifier,具体如下:

1
2
3
static struct notifier_block br_device_notifier = {
	.notifier_call = br_device_event
};

br_device_event函数是用来当桥上的设备状态或者设备信息发生改变时做相应的处理,该函数在/net/bridge/br.c中

5.注册通知连,主要针对桥转发表事件的相关信息register_switchdev_notifier,传入的参数br_switchdev_notifier详细信息如下:

1
2
3
static struct notifier_block br_switchdev_notifier = {
	.notifier_call = br_switchdev_event,
};

br_switchdev_event,主要针对桥转发表的事件做出相应的处理该函数在/net/bridge/br.c中

6.brioctl_set用来处理ioctl命令的函数,比如添加和删除网桥,br_ioctl_deviceless_stub给回调函数br_ioctl_hook,而br_ioctl_hook在sock_ioctl中

使用,这样通过在应用层调用socket的ioctl函数,就能够进行网桥的添加与删除了,函数用来处理添加和删除网桥的相关操作

以上就是网桥初始化的相关操作。

添加一个桥设备——br_add_bridge

我们先来看一个命令:brctl addbr br1

上节我们提到一个用来处理ioctl命令的函数br_ioctl_deviceless_stub通过调用brioctl_set, 将br_ioctl_deviceless_stub赋值给回调函数br_ioctl_hook,而br_ioctl_hook在sock_ioctl中使用。 这样通过在应用层调用socket的ioctl函数,就能够进行网桥的添加与删除了。

如果我们想增加新的ioctl,用于我们新开放的功能,就可以在该函数里增加新的case即可。

当我们输入上面命令时,就会触发br_ioctl_deviceless_stub函数来响应br_add_bridge函数,当命令执行完成以后, 使用brctl show命令就可以看见我们添加的br1这个网桥设备已经生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user *uarg)
{
	switch (cmd) {
	case SIOCGIFBR:
	case SIOCSIFBR:
		return old_deviceless(net, uarg);

	case SIOCBRADDBR:
	case SIOCBRDELBR:
	{
		char buf[IFNAMSIZ];

		if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
			return -EPERM;

		if (copy_from_user(buf, uarg, IFNAMSIZ))
			return -EFAULT;

		buf[IFNAMSIZ-1] = 0;
		if (cmd == SIOCBRADDBR)
			return br_add_bridge(net, buf);/*添加网桥设备的操作*/

		return br_del_bridge(net, buf);
	}
	}
	return -EOPNOTSUPP;
}

该函数注册在,桥设备添加时候dev->netdev_ops = &br_netdev_ops; 在br_netdev_ops有一个函数指针.ndo_do_ioctl= br_dev_ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
int br_add_if(struct net_bridge *br, struct net_device *dev)
{
	struct net_bridge_port *p;
	int err = 0;
	unsigned br_hr, dev_hr;
	bool changed_addr;

	/* Don't allow bridging non-ethernet like devices, or DSA-enabled
	 * master network devices since the bridge layer rx_handler prevents
	 * the DSA fake ethertype handler to be invoked, so we do not strip off
	 * the DSA switch tag protocol header and the bridge layer just return
	 * RX_HANDLER_CONSUMED, stopping RX processing for these frames.
	 */

	if ((dev->flags & IFF_LOOPBACK) ||
		dev->type != ARPHRD_ETHER || dev->addr_len != ETH_ALEN ||
		!is_valid_ether_addr(dev->dev_addr) ||
		netdev_uses_dsa(dev))
		return -EINVAL;

	/* No bridging of bridges */
	if (dev->netdev_ops->ndo_start_xmit == br_dev_xmit)
		return -ELOOP;

	/* Device is already being bridged  */
	if (br_port_exists(dev))
		return -EBUSY;

	/* No bridging devices that dislike that (e.g. wireless) */
	if (dev->priv_flags & IFF_DONT_BRIDGE)
		return -EOPNOTSUPP;

	/*分配一个新网桥端口并对其初始化*/
	p = new_nbp(br, dev);
	if (IS_ERR(p))
		return PTR_ERR(p);

	/*调用设备通知链,告诉网络有这样一个设备*/
	call_netdevice_notifiers(NETDEV_JOIN, dev);

	/**向设备添加或删除所有多播帧的接收。*/
	err = dev_set_allmulti(dev, 1);
	if (err)
		goto put_back;

	err = kobject_init_and_add(&p->kobj, &brport_ktype, &(dev->dev.kobj),
				   SYSFS_BRIDGE_PORT_ATTR);
	if (err)
		goto err1;
	/*把链路添加到sysfs*/
	err = br_sysfs_addif(p);
	if (err)
		goto err2;

	err = br_netpoll_enable(p);
	if (err)
		goto err3;

	/*注册设备接收帧函数*/
	err = netdev_rx_handler_register(dev, br_handle_frame, p);
	if (err)
		goto err4;

	/*给该端口指派默认优先权*/
	dev->priv_flags |= IFF_BRIDGE_PORT;

	/*向上级设备添加主链路*/
	err = netdev_master_upper_dev_link(dev, br->dev, NULL, NULL);
	if (err)
		goto err5;

	/*禁用网络设备上的大型接收卸载(LRO)。
	必须在RTNL下调用。
	如果接收到的数据包可能转发到另一个接口,
	则需要这样做。*/
	dev_disable_lro(dev);

	list_add_rcu(&p->list, &br->port_list);
	/*更新桥上的端口数,如果有更新,再进一步将其设为混杂模式*/
	nbp_update_port_count(br);
	/* 重新计算dev->features并发送通知(如果已更改)。
	应该调用驱动程序或硬件依赖条件可能会改变影响功能。*/
	netdev_update_features(br->dev);

	br_hr = br->dev->needed_headroom;
	dev_hr = netdev_get_fwd_headroom(dev);
	if (br_hr < dev_hr)
		update_headroom(br, dev_hr);
	else
		netdev_set_rx_headroom(dev, br_hr);

	/*把dev的mac添加到转发数据库中*/
	if (br_fdb_insert(br, p, dev->dev_addr, 0))
		netdev_err(dev, "failed insert local address bridge forwarding table\n");

	/*初始化该桥端口的vlan*/
	err = nbp_vlan_init(p);
	if (err) {
		netdev_err(dev, "failed to initialize vlan filtering on this port\n");
		goto err6;
	}

	spin_lock_bh(&br->lock);
	/*更新网桥id*/
	changed_addr = br_stp_recalculate_bridge_id(br);
	/*设备是否启动,桥是否启动,设备上是否有载波信号(网桥没有载波状态,因为网桥是虚拟设备)*/
	if (netif_running(dev) && netif_oper_up(dev) &&
		(br->dev->flags & IFF_UP))
		/*启动网桥端口*/
		br_stp_enable_port(p);
	spin_unlock_bh(&br->lock);

	br_ifinfo_notify(RTM_NEWLINK, p);
	/*如果网桥的地址改变,则调用通知连相关的函数*/
	if (changed_addr)
		call_netdevice_notifiers(NETDEV_CHANGEADDR, br->dev);
	/*更新网桥mtu*/
	dev_set_mtu(br->dev, br_min_mtu(br));
	br_set_gso_limits(br);
	/*添加一个内核对象*/
	kobject_uevent(&p->kobj, KOBJ_ADD);

	return 0;

err6:
	list_del_rcu(&p->list);
	br_fdb_delete_by_port(br, p, 0, 1);
	nbp_update_port_count(br);
	netdev_upper_dev_unlink(dev, br->dev);

err5:
	dev->priv_flags &= ~IFF_BRIDGE_PORT;
	netdev_rx_handler_unregister(dev);
err4:
	br_netpoll_disable(p);
err3:
	sysfs_remove_link(br->ifobj, p->dev->name);
err2:
	kobject_put(&p->kobj);
	p = NULL; /* kobject_put frees */
err1:
	dev_set_allmulti(dev, -1);
put_back:
	dev_put(dev);
	kfree(p);
	return err;
}

在桥上添加接口的基本步骤,如上,删除桥端口,主要是把建立接口时所做的事情撤销,如添加接口出错时的一些处理。

桥设备及端口的开启和关闭

关于设备的添加删除的基本动作,我们已经知道。 这节,我们看看关于网桥设备以及桥设备上的端口的启动和关闭。

我们说过,在初始化一个桥设备的时候有这样一个操作: dev->netdev_ops = &br_netdev_ops;

br_netdev_ops这个参数,注册了很多函数,其中包括网桥设备的启动和关闭函数

br_dev_open和br_dev_stop,这两个函数的工作主要是初始化桥设备的一些队列和 桥设备上端口的一些启动和关闭动作。

启动和关闭网桥设备

1
2
3
4
5
6
7
8
9
10
11
12
13
static int br_dev_open(struct net_device *dev)
{
	struct net_bridge *br = netdev_priv(dev);
	/*重新更新网桥设备功能*/
	netdev_update_features(dev);
	/*函数启动进行设备传输*/
	netif_start_queue(dev);
	/*启动网桥设备*/
	br_stp_enable_bridge(br);
	/*初始化网桥本身的多播对列*/
	br_multicast_open(br);
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
static int br_dev_stop(struct net_device *dev)
{
	struct net_bridge *br = netdev_priv(dev);
	/*关闭网桥设备*/
	br_stp_disable_bridge(br);
	/*关闭网桥设备的多播队列*/
	br_multicast_stop(br);
	/*关闭设备的传输,任何企图在设备上传输信息的尝试都会被拒绝*/
	netif_stop_queue(dev);

	return 0;
}

启动网桥设备,当启动网桥设备时,先前绑定在该设备上的端口也会跟着启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void br_stp_enable_bridge(struct net_bridge *br)
{
	struct net_bridge_port *p;
	/*锁定网桥*/
	spin_lock_bh(&br->lock);
	if (br->stp_enabled == BR_KERNEL_STP)
		mod_timer(&br->hello_timer, jiffies + br->hello_time);

	/* 当网桥启动时,设置次定时器,1/10秒到期一次 */
	mod_timer(&br->gc_timer, jiffies + HZ/10);
	/*TX配置bpdu*/
	br_config_bpdu_generation(br);

	list_for_each_entry(p, &br->port_list, list) {
		if (netif_running(p->dev) && netif_oper_up(p->dev))
			br_stp_enable_port(p);/*启动网桥设备的每个端口*/
	}
	/*给网桥解锁*/
	spin_unlock_bh(&br->lock);
}

关闭网桥设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void br_stp_disable_bridge(struct net_bridge *br)
{
	struct net_bridge_port *p;

	spin_lock_bh(&br->lock);
	list_for_each_entry(p, &br->port_list, list) {
		if (p->state != BR_STATE_DISABLED)
			br_stp_disable_port(p);/*关闭网桥设备的每个端口*/
	}
	/*重新设置拓扑标识*/
	br->topology_change = 0;
	br->topology_change_detected = 0;
	spin_unlock_bh(&br->lock);

	/*删除在初始化桥设备时的定时器*/
	del_timer_sync(&br->hello_timer);
	del_timer_sync(&br->topology_change_timer);
	del_timer_sync(&br->tcn_timer);
	del_timer_sync(&br->gc_timer);
}

启动和关闭网桥端口

要启动网桥端口,必须满足下列几个条件

1.被管理的相关设备已用管理手段启动

2.被绑定的相关设备有载波状态

3.相关的网桥设备已用管理手段启动

注意:网桥设备上没有载波状态,因为网桥是虚拟设备。

当网桥是以用户空间命令建起来并且先前三个条件都满足时,该网桥端口就可以立即启用了

但是,假设当端口建立时,由于上述三项条件至少有一项不满足无法启动端口时,下面的条件是 每项条件最终满足时启用端口的场合:

1.当被关闭的网桥设备重新启动时,其所有关闭的端口就会启用

2.当被绑定的设备检测到载波状态时,桥程序会收到NETDE_CHANGE通知消息

3.当被关掉的版定设备重启时,桥程序会收到NETDEV_UP的通知消息

如若还不满足,网桥端口就会被关闭

启动网桥上的端口

1
2
3
4
5
6
7
8
9
void br_stp_enable_port(struct net_bridge_port *p)
{
	/*初始化端口*/
	br_init_port(p);
	/*遍历所有端口,为端口指定合适的状态*/
	br_port_state_selection(p->br);
	/*捕捉一个端口变化信息的通知*/
	br_ifinfo_notify(RTM_NEWLINK, p);
}

关闭网桥上的端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void br_stp_disable_port(struct net_bridge_port *p)
{
	struct net_bridge *br = p->br;
	int wasroot;
	/*判断是否是根网桥*/
	wasroot = br_is_root_bridge(br);
	/*分配制定角色*/
	br_become_designated_port(p);
	/*将关闭位置位*/
	br_set_state(p, BR_STATE_DISABLED);
	p->topology_change_ack = 0;
	p->config_pending = 0;

	br_ifinfo_notify(RTM_NEWLINK, p);
	/*删除定时器*/
	del_timer(&p->message_age_timer);
	del_timer(&p->forward_delay_timer);
	del_timer(&p->hold_timer);
	/*更改转发表信息*/
	br_fdb_delete_by_port(br, p, 0, 0);
	br_multicast_disable_port(p);
	/*更改桥的bpdu信息*/
	br_configuration_update(br);
	/*更新所有桥上端口的状态*/
	br_port_state_selection(br);
	/*处理非根网桥到根网桥的转移*/
	if (br_is_root_bridge(br) && !wasroot)
		br_become_root_bridge(br);
}

注意,当网桥端口关闭时,非根网桥可能会变成根网桥

skb桥转发蓝图

需要说明的是:

1.我们先暂时忽略数据包从一开始是怎么从驱动进入到netif_receive_skb的,因为这个暂时不影响我们理解这幅图的流程。

2.由于桥转发的篇幅较大,图中没有标示出,数据包中途被丢弃的情况。约定数据包会发送成功。

现在数据包(skb)已经准备好了装备要闯关了

1.首先skb从驱动经过繁杂的路线走到了netif_receive_skb这个函数中经过一些小波折到达__netif_receive_skb_core中时遇到了第一个十字路口是看了看自己有没有skb->dev->rx_handler(注1)这个装备,如果有,则进入br_handle_frame(注2).如果没有则直接上协议栈。

注1:桥下的设备注册过rx_handler函数,所以数据包会进入桥,br_pass_frame_up函数将原先的设备换成了桥设备, 而桥设备没有注册过rx_handler函数,所以数据包不会二次进入桥。

注2:br_handle_frame我们在前几节提到过,是skb进入桥的入口函数,在br_add_if的时候会注册该函数。

2.skb注定要经历一番劫难,刚进入br_handle_frame又将陷入两难境地,此时有两个入口,这两个是netfilter设下的连个hook点,分别是,NF_BR_PRE_ROUTING,和NF_BR_LOCAL_IN,两种路径,如果数据包只想去本地,则会选择NF_BR_LOCAL_IN入口,然后发往本地,如果暂时还确定不了,则先进入NF_BR_PRE_ROUTING入口.

3.进入NF_BR_PRE_ROUTING入口后,会遇到br_handle_frame_finish函数,这个函数决定了skb的何去何从,(1)如果是转发,则在经过NF_BR_FORWARD钩子点进入转发阶段的障碍,最后在进入NF_BR_POST_ROUTING,以及最终的dev_queue_xmit,实现最终转发。(2)如果发往本地则重新进入NF_BR_LOCAL_IN最后在进入netif_receive_skb,进行转发。skb在经过目前口述的磨练最终得以释放。

4.如果是如果是本地发出的数据包,经过NF_BR_LOCAL_OUT钩子点然后进入最后阶段的NF_BR_POST_ROUTING,进行相应的转发。

桥数据包接收—-br_handle_frame

一个很重要的函数br_handle_frame这个函数的初始注册地点是在桥添加接口的时候,注册在桥某一个接口上

1
2
3
4
5
6
7
int br_add_if(struct net_bridge *br, struct net_device *dev)
{
	........
	/*注册设备接收帧函数*/
	err = netdev_rx_handler_register(dev, br_handle_frame, p);
	........
}

其次,那么__netif_receive_skb_core是怎样让数据包进入桥的呢?我们看看上面提到的netdev_rx_handler_register函数,具体做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int netdev_rx_handler_register(struct net_device *dev,
				   rx_handler_func_t *rx_handler,
				   void *rx_handler_data)
{
	ASSERT_RTNL();

	if (dev->rx_handler)
		return -EBUSY;
	/* Note: rx_handler_data must be set before rx_handler */
	/*将dev->rx_handler_data,指向rx_handler_data(上面的p是桥端口信息)*/
	rcu_assign_pointer(dev->rx_handler_data, rx_handler_data);
	/*将dev->rx_handle指针指向rx_handler*/
	rcu_assign_pointer(dev->rx_handler, rx_handler);

	return 0;
}

看完这个函数,我们就明白了为什么在__netif_receive_skb_core中可以用skb->dev->rx_handle将数据包传入br_handle_frame函数,也就是将数据包传入了桥。

值得注意的是:上面的dev是桥下的设备,不是桥设备,桥设备(比如br0)是没有注册rx_handle这个函数的

好,了解到,桥的注册函数和如何接收数据包后,然后一起来看看br_handle_frame是如何操作的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*
 * Return NULL if skb is handled
 * note: already called with rcu_read_lock
 */
rx_handler_result_t br_handle_frame(struct sk_buff **pskb)
{
	struct net_bridge_port *p;
	struct sk_buff *skb = *pskb;
	const unsigned char *dest = eth_hdr(skb)->h_dest;
	br_should_route_hook_t *rhook;

	if (unlikely(skb->pkt_type == PACKET_LOOPBACK))
		return RX_HANDLER_PASS;
	/*判断是否是有效的mac地址,即不是多播地址也不是全00地址*/
	if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
		goto drop;
	/*判断是否是共享数据包,若果是则clone该数据包*/
	skb = skb_share_check(skb, GFP_ATOMIC);
	if (!skb)
		return RX_HANDLER_CONSUMED;
	/*获取数据包网桥端口的一些信息*/
	p = br_port_get_rcu(skb->dev);
	/*BPDU是网桥之间交流的报文,目标mac是 01:80:C2:00:00:00*/
	if (unlikely(is_link_local_ether_addr(dest))) {
		u16 fwd_mask = p->br->group_fwd_mask_required;

		/*
		 * See IEEE 802.1D Table 7-10 Reserved addresses
		 *
		 * Assignment             Value
		 * Bridge Group Address       01-80-C2-00-00-00
		 * (MAC Control) 802.3        01-80-C2-00-00-01
		 * (Link Aggregation) 802.3   01-80-C2-00-00-02
		 * 802.1X PAE address     01-80-C2-00-00-03
		 *
		 * 802.1AB LLDP       01-80-C2-00-00-0E
		 *
		 * Others reserved for future standardization
		 */
		switch (dest[5]) {
		case 0x00:    /* Bridge Group Address */
			/* If STP is turned off,
			   then must forward to keep loop detection */
			if (p->br->stp_enabled == BR_NO_STP ||
				fwd_mask & (1u << dest[5]))
				goto forward;
			*pskb = skb;
			__br_handle_local_finish(skb);
			return RX_HANDLER_PASS;

		case 0x01:    /* IEEE MAC (Pause) */
			goto drop;

		case 0x0E:    /* 802.1AB LLDP */
			fwd_mask |= p->br->group_fwd_mask;
			if (fwd_mask & (1u << dest[5]))
				goto forward;
			*pskb = skb;
			__br_handle_local_finish(skb);
			return RX_HANDLER_PASS;

		default:
			/* Allow selective forwarding for most other protocols */
			fwd_mask |= p->br->group_fwd_mask;
			if (fwd_mask & (1u << dest[5]))
				goto forward;
		}

		/* Deliver packet to local host only */
		NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, dev_net(skb->dev),
			NULL, skb, skb->dev, NULL, br_handle_local_finish);
		return RX_HANDLER_CONSUMED;
	}

forward:
	switch (p->state) {
	case BR_STATE_FORWARDING:
		/*ebtables获取路由的hook点*/
		rhook = rcu_dereference(br_should_route_hook);
		if (rhook) {/*如果是转发状态,则转发数据包,然后返回*/
			if ((*rhook)(skb)) {
				*pskb = skb;
				return RX_HANDLER_PASS;
			}
			dest = eth_hdr(skb)->h_dest;
		}
		/* fall through */
	case BR_STATE_LEARNING:
		/*目的地址是否是设备链路层地址 */
		if (ether_addr_equal(p->br->dev->dev_addr, dest))
			skb->pkt_type = PACKET_HOST;
		/*将数据包送入数据帧处理函数br_handle_frame_finish*/
		NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING,
			dev_net(skb->dev), NULL, skb, skb->dev, NULL,
			br_handle_frame_finish);
		break;
	default:
drop:
		kfree_skb(skb);
	}
	return RX_HANDLER_CONSUMED;
}

在br_handle_frame主要做一件事,就是将数据包放进那个钩子点。

说明:br_handle_frame函数中有两个hook函数,br_handle_local_finish和br_handle_frame_finish这两个函数只有在netfilter因其他原因没有丢弃或者消化该帧时才会被调用,ebtables也能查看帧。ebtables是一个架构,能提供一些netfilter所没有的提供的额外功能,尤其是,ebtables可以过滤和修改任何类型的帧,而非仅限于那些携带ip封包的帧。

桥数据包处理函数——br_handle_frame_finish

br_handle_frame_finish. 作用:br_handle_frame_finish函数主要是决策将不同类别的数据包做不同的分发路径。

其函数处理的过程如下图所示:

首先判断该数据包是否符合桥转发的条件:

(1)桥端口状态是否是开启状态,如果没有开启则丢掉数据包

(2)是否允许从该桥上转发,如果不允许,则直接返回0

获得桥转发的条件以后,开始判断数据包的类型:

(1)判断此时桥的标志为允许做哪些事情,学习还是扩展

如果学习的标志位被至位,则更新数据转发表。否则继续向下走

(2)根据多播或者广播报文的类型决定数据包的去留

(3)判断此时端口的状态,如果是学习状态,则将数据包丢弃

(要注意的是:桥的端口状态(和上面的flag不冲突,上面的flag表示网桥可以做的事情)state表示网桥端口所处于的状态) 在处理完一些需要预备的事情之后,就要为数据包的转发开始做准备了

(1)网桥设备是否处于混杂模式,如果是则建立副本,为发往本地做个备份

(注意的是,所有网桥端口绑定的设备都会处于混杂模式,因为 网桥运行必须此模式。但除非明确的对其进行配置,否则网桥自己是不会处于混杂模式的)

(2)在次判断广播还是多播地址

广播地址:仅仅设置副本,进行广播转发和发往本地

多播地址:先查多播地址转发表,如果存在,设置副本,进行多播转发,原始数据包指向NULL,如果已经传送至本地,则会释放副本,不进行本地转发,否则重新转发到本地

(3)不是广播或者多播

判断是否本地地址,如果是本地地址,则将原始数据包指向NULL,发往本地。

否则进行数据包转发

桥数据包转发

无论是在发往本地还是转发,有一个函数的功能是不能忽略的,就是br_handle_vlan函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct sk_buff *br_handle_vlan(struct net_bridge *br,
				   struct net_bridge_vlan_group *vg,
				   struct sk_buff *skb)
{
	struct br_vlan_stats *stats;
	struct net_bridge_vlan *v;
	u16 vid;

	/* If this packet was not filtered at input, let it pass */
	if (!BR_INPUT_SKB_CB(skb)->vlan_filtered)
		goto out;

	/* At this point, we know that the frame was filtered and contains
	 * a valid vlan id.  If the vlan id has untagged flag set,
	 * send untagged; otherwise, send tagged.
	 */
	br_vlan_get_tag(skb, &vid);
	/*find vid from vlan group*/
	v = br_vlan_find(vg, vid);
	/* Vlan entry must be configured at this point.  The
	 * only exception is the bridge is set in promisc mode and the
	 * packet is destined for the bridge device.  In this case
	 * pass the packet as is.
	 */
	if (!v || !br_vlan_should_use(v)) {
		if ((br->dev->flags & IFF_PROMISC) && skb->dev == br->dev) {
			goto out;
		} else {
			kfree_skb(skb);
			return NULL;
		}
	}
	/*statistacs the vlan if flow and if the vlan_stats_enabled is true */
	if (br->vlan_stats_enabled) {
		stats = this_cpu_ptr(v->stats);
		u64_stats_update_begin(&stats->syncp);
		stats->tx_bytes += skb->len;
		stats->tx_packets++;
		u64_stats_update_end(&stats->syncp);
	}

	if (v->flags & BRIDGE_VLAN_INFO_UNTAGGED)
		skb->vlan_tci = 0;
out:
	return skb;
}

这个函数的作用很简单就是,数据包是否要带tag,

过程:

在传递进来的vlan group中查找自己所处的vlan

如果该vlan不存在则判断当前模式是否是混杂模式和数据包的设备是否是桥下的设备,选择发包或者丢弃。

如果存在,且vlan是开启的,则统计vlan接口上的数据流量,最后根据vlan出口的标记位进行位运算判断是否要带tag.

然后我们来看一下上节提到的发往本地数据包的处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int br_pass_frame_up(struct sk_buff *skb)
{
	struct net_device *indev, *brdev = BR_INPUT_SKB_CB(skb)->brdev;
	struct net_bridge *br = netdev_priv(brdev);
	struct net_bridge_vlan_group *vg;
	struct pcpu_sw_netstats *brstats = this_cpu_ptr(br->stats);
	/*统计该桥上的流量*/
	u64_stats_update_begin(&brstats->syncp);
	brstats->rx_packets++;
	brstats->rx_bytes += skb->len;
	u64_stats_update_end(&brstats->syncp);

	/*获取该桥上的vlan组*/
	vg = br_vlan_group_rcu(br);
	/* Bridge is just like any other port.  Make sure the
	 * packet is allowed except in promisc modue when someone
	 * may be running packet capture.
	 */
	if (!(brdev->flags & IFF_PROMISC) &&
		!br_allowed_egress(vg, skb)) {
		kfree_skb(skb);
		return NET_RX_DROP;
	}
	/*替换掉数据包中的设备信息改为桥设备*/
	indev = skb->dev;
	skb->dev = brdev;
	/*配置数据包vlan的相关信息*/
	skb = br_handle_vlan(br, vg, skb);
	if (!skb)
		return NET_RX_DROP;
	/*进入NF_BR_LOCAL_IN*/
	return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN,
			   dev_net(indev), NULL, skb, indev, NULL,
			   br_netif_receive_skb);
}

这个函数所做的事情很简单,就是配置vlan的相关信息后,然后发往本地的netfilter钩子函数中 最后重新回到netif_recive_skb.如下函数:

1
2
3
4
5
static int
br_netif_receive_skb(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	return netif_receive_skb(skb);
}

再来看看数据包转发的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb)
{
	struct net_bridge_vlan_group *vg;
	struct net_device *indev;

	if (skb_warn_if_lro(skb)) {
		kfree_skb(skb);
		return;
	}
	/*获取vlan组,这个组中有许多的vlanid,br_handle_vlan函数就是要在这个组中查找自己的vid*/
	vg = nbp_vlan_group_rcu(to);
	/*添加vlan的相关配置*/
	skb = br_handle_vlan(to->br, vg, skb);
	if (!skb)
		return;

	indev = skb->dev;
	skb->dev = to->dev;
	skb_forward_csum(skb);

	NF_HOOK(NFPROTO_BRIDGE, NF_BR_FORWARD,
		dev_net(indev), NULL, skb, indev, skb->dev,
		br_forward_finish);
}

int br_forward_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	return NF_HOOK(NFPROTO_BRIDGE, NF_BR_POST_ROUTING,
			   net, sk, skb, NULL, skb->dev,
			   br_dev_queue_push_xmit);

}

整个数据包转发的过程与转发到本地的过程类似,只不过所进入的netfilter钩子点不同.

整个分析中不包含数据包从本地发出的数据包

Linux 上的抽象网络设备

https://www.ibm.com/developerworks/cn/linux/1310_xiawc_networkdevice/

和磁盘设备类似,Linux 用户想要使用网络功能,不能通过直接操作硬件完成,而需要直接或间接的操作一个 Linux 为我们抽象出来的设备,既通用的 Linux 网络设备来完成。一个常见的情况是,系统里装有一个硬件网卡,Linux 会在系统里为其生成一个网络设备实例,如 eth0,用户需要对 eth0 发出命令以配置或使用它了。更多的硬件会带来更多的设备实例,虚拟的硬件也会带来更多的设备实例。随着网络技术,虚拟化技术的发展,更多的高级网络设备被加入了到了 Linux 中,使得情况变得更加复杂。在以下章节中,将一一分析在虚拟化技术中经常使用的几种 Linux 网络设备抽象类型:Bridge、802.1.q VLAN device、VETH、TAP,详细解释如何用它们配合 Linux 中的 Route table、IP table 简单的创建出本地虚拟网络。 相关网络设备工作原理

Bridge

Bridge(桥)是 Linux 上用来做 TCP/IP 二层协议交换的设备,与现实世界中的交换机功能相似。Bridge 设备实例可以和 Linux 上其他网络设备实例连接,既 attach 一个从设备,类似于在现实世界中的交换机和一个用户终端之间连接一根网线。当有数据到达时,Bridge 会根据报文中的 MAC 信息进行广播、转发、丢弃处理。

图 1.Bridge 设备工作过程

如图所示,Bridge 的功能主要在内核里实现。当一个从设备被 attach 到 Bridge 上时,相当于现实世界里交换机的端口被插入了一根连有终端的网线。这时在内核程序里,netdev_rx_handler_register()被调用,一个用于接受数据的回调函数被注册。以后每当这个从设备收到数据时都会调用这个函数可以把数据转发到 Bridge 上。当 Bridge 接收到此数据时,br_handle_frame()被调用,进行一个和现实世界中的交换机类似的处理过程:判断包的类别(广播/单点),查找内部 MAC 端口映射表,定位目标端口号,将数据转发到目标端口或丢弃,自动更新内部 MAC 端口映射表以自我学习。

Bridge 和现实世界中的二层交换机有一个区别,图中左侧画出了这种情况:数据被直接发到 Bridge 上,而不是从一个端口接受。这种情况可以看做 Bridge 自己有一个 MAC 可以主动发送报文,或者说 Bridge 自带了一个隐藏端口和寄主 Linux 系统自动连接,Linux 上的程序可以直接从这个端口向 Bridge 上的其他端口发数据。所以当一个 Bridge 拥有一个网络设备时,如 bridge0 加入了 eth0 时,实际上 bridge0 拥有两个有效 MAC 地址,一个是 bridge0 的,一个是 eth0 的,他们之间可以通讯。由此带来一个有意思的事情是,Bridge 可以设置 IP 地址。通常来说 IP 地址是三层协议的内容,不应该出现在二层设备 Bridge 上。但是 Linux 里 Bridge 是通用网络设备抽象的一种,只要是网络设备就能够设定 IP 地址。当一个 bridge0 拥有 IP 后,Linux 便可以通过路由表或者 IP 表规则在三层定位 bridge0,此时相当于 Linux 拥有了另外一个隐藏的虚拟网卡和 Bridge 的隐藏端口相连,这个网卡就是名为 bridge0 的通用网络设备,IP 可以看成是这个网卡的。当有符合此 IP 的数据到达 bridge0 时,内核协议栈认为收到了一包目标为本机的数据,此时应用程序可以通过 Socket 接收到它。一个更好的对比例子是现实世界中的带路由的交换机设备,它也拥有一个隐藏的 MAC 地址,供设备中的三层协议处理程序和管理程序使用。设备里的三层协议处理程序,对应名为 bridge0 的通用网络设备的三层协议处理程序,即寄主 Linux 系统内核协议栈程序。设备里的管理程序,对应 bridge0 寄主 Linux 系统里的应用程序。

Bridge 的实现当前有一个限制:当一个设备被 attach 到 Bridge 上时,那个设备的 IP 会变的无效,Linux 不再使用那个 IP 在三层接受数据。举例如下:如果 eth0 本来的 IP 是 192.168.1.2,此时如果收到一个目标地址是 192.168.1.2 的数据,Linux 的应用程序能通过 Socket 操作接受到它。而当 eth0 被 attach 到一个 bridge0 时,尽管 eth0 的 IP 还在,但应用程序是无法接受到上述数据的。此时应该把 IP 192.168.1.2 赋予 bridge0。

另外需要注意的是数据流的方向。对于一个被 attach 到 Bridge 上的设备来说,只有它收到数据时,此包数据才会被转发到 Bridge 上,进而完成查表广播等后续操作。当请求是发送类型时,数据是不会被转发到 Bridge 上的,它会寻找下一个发送出口。用户在配置网络时经常忽略这一点从而造成网络故障。

VLAN device for 802.1.q

VLAN 又称虚拟网络,是一个被广泛使用的概念,有些应用程序把自己的内部网络也称为 VLAN。此处主要说的是在物理世界中存在的,需要协议支持的 VLAN。它的种类很多,按照协议原理一般分为:MACVLAN、802.1.q VLAN、802.1.qbg VLAN、802.1.qbh VLAN。其中出现较早,应用广泛并且比较成熟的是 802.1.q VLAN,其基本原理是在二层协议里插入额外的 VLAN 协议数据(称为 802.1.q VLAN Tag),同时保持和传统二层设备的兼容性。Linux 里的 VLAN 设备是对 802.1.q 协议的一种内部软件实现,模拟现实世界中的 802.1.q 交换机。

图 2 .VLAN 设备工作过程

如图所示,Linux 里 802.1.q VLAN 设备是以母子关系成对出现的,母设备相当于现实世界中的交换机 TRUNK 口,用于连接上级网络,子设备相当于普通接口用于连接下级网络。当数据在母子设备间传递时,内核将会根据 802.1.q VLAN Tag 进行对应操作。母子设备之间是一对多的关系,一个母设备可以有多个子设备,一个子设备只有一个母设备。当一个子设备有一包数据需要发送时,数据将被加入 VLAN Tag 然后从母设备发送出去。当母设备收到一包数据时,它将会分析其中的 VLAN Tag,如果有对应的子设备存在,则把数据转发到那个子设备上并根据设置移除 VLAN Tag,否则丢弃该数据。在某些设置下,VLAN Tag 可以不被移除以满足某些监听程序的需要,如 DHCP 服务程序。举例说明如下:eth0 作为母设备创建一个 ID 为 100 的子设备 eth0.100。此时如果有程序要求从 eth0.100 发送一包数据,数据将被打上 VLAN 100 的 Tag 从 eth0 发送出去。如果 eth0 收到一包数据,VLAN Tag 是 100,数据将被转发到 eth0.100 上,并根据设置决定是否移除 VLAN Tag。如果 eth0 收到一包包含 VLAN Tag 101 的数据,其将被丢弃。上述过程隐含以下事实:对于寄主 Linux 系统来说,母设备只能用来收数据,子设备只能用来发送数据。和 Bridge 一样,母子设备的数据也是有方向的,子设备收到的数据不会进入母设备,同样母设备上请求发送的数据不会被转到子设备上。可以把 VLAN 母子设备作为一个整体想象为现实世界中的 802.1.q 交换机,下级接口通过子设备连接到寄主 Linux 系统网络里,上级接口同过主设备连接到上级网络,当母设备是物理网卡时上级网络是外界真实网络,当母设备是另外一个 Linux 虚拟网络设备时上级网络仍然是寄主 Linux 系统网络。

需要注意的是母子 VLAN 设备拥有相同的 MAC 地址,可以把它当成现实世界中 802.1.q 交换机的 MAC,因此多个 VLAN 设备会共享一个 MAC。当一个母设备拥有多个 VLAN 子设备时,子设备之间是隔离的,不存在 Bridge 那样的交换转发关系,原因如下:802.1.q VLAN 协议的主要目的是从逻辑上隔离子网。现实世界中的 802.1.q 交换机存在多个 VLAN,每个 VLAN 拥有多个端口,同一 VLAN 端口之间可以交换转发,不同 VLAN 端口之间隔离,所以其包含两层功能:交换与隔离。Linux VLAN device 实现的是隔离功能,没有交换功能。一个 VLAN 母设备不可能拥有两个相同 ID 的 VLAN 子设备,因此也就不可能出现数据交换情况。如果想让一个 VLAN 里接多个设备,就需要交换功能。在 Linux 里 Bridge 专门实现交换功能,因此将 VLAN 子设备 attach 到一个 Bridge 上就能完成后续的交换功能。总结起来,Bridge 加 VLAN device 能在功能层面完整模拟现实世界里的 802.1.q 交换机。

Linux 支持 VLAN 硬件加速,在安装有特定硬件情况下,图中所述内核处理过程可以被放到物理设备上完成。

TAP 设备与 VETH 设备

TUN/TAP 设备是一种让用户态程序向内核协议栈注入数据的设备,一个工作在三层,一个工作在二层,使用较多的是 TAP 设备。VETH 设备出现较早,它的作用是反转通讯数据的方向,需要发送的数据会被转换成需要收到的数据重新送入内核网络层进行处理,从而间接的完成数据的注入。

图 3 .TAP 设备和 VETH 设备工作过程

如图所示,当一个 TAP 设备被创建时,在 Linux 设备文件目录下将会生成一个对应 char 设备,用户程序可以像打开普通文件一样打开这个文件进行读写。当执行 write()操作时,数据进入 TAP 设备,此时对于 Linux 网络层来说,相当于 TAP 设备收到了一包数据,请求内核接受它,如同普通的物理网卡从外界收到一包数据一样,不同的是其实数据来自 Linux 上的一个用户程序。Linux 收到此数据后将根据网络配置进行后续处理,从而完成了用户程序向 Linux 内核网络层注入数据的功能。当用户程序执行 read()请求时,相当于向内核查询 TAP 设备上是否有需要被发送出去的数据,有的话取出到用户程序里,完成 TAP 设备的发送数据功能。针对 TAP 设备的一个形象的比喻是:使用 TAP 设备的应用程序相当于另外一台计算机,TAP 设备是本机的一个网卡,他们之间相互连接。应用程序通过 read()/write()操作,和本机网络核心进行通讯。

VETH 设备总是成对出现,送到一端请求发送的数据总是从另一端以请求接受的形式出现。该设备不能被用户程序直接操作,但使用起来比较简单。创建并配置正确后,向其一端输入数据,VETH 会改变数据的方向并将其送入内核网络核心,完成数据的注入。在另一端能读到此数据。

网络设置举例说明

为了更好的说明 Linux 网络设备的用法,下面将用一系列的例子,说明在一个复杂的 Linux 网络元素组合出的虚拟网络里,数据的流向。网络设置简介如下:一个中心 Bridge:bridge0 下 attach 了 4 个网络设备,包括 2 个 VETH 设备,1 个 TAP 设备 tap0,1 个物理网卡 eth0。在 VETH 的另外一端又创建了 VLAN 子设备。Linux 上共存在 2 个 VLAN 网络,既 vlan100 与 vlan200。物理网卡和外部网络相连,并且在它之下创建了一个 VLAN ID 为 200 的 VLAN 子设备。

从 vlan100 子设备发送 ARP 报文

图 4 .ARP from vlan100 child device

如图所示,当用户尝试 ping 192.168.100.3 时,Linux 将会根据路由表,从 vlan100 子设备发出 ARP 报文,具体过程如下:

1) 用户 ping 192.168.100.3

2) Linux 向 vlan100 子设备发送 ARP 信息。

3) ARP 报文被打上 VLAN ID 100 的 Tag 成为 ARP@vlan100,转发到母设备上。

4) VETH 设备将这一发送请求转变方向,成为一个需要接受处理的报文送入内核网络模块。

5) 由于对端的 VETH 设备被加入到了 bridge0 上,并且内核发现它收到一个报文,于是报文被转发到 bridge0 上。

6) bridge0 处理此 ARP@vlan100 信息,根据 TCP/IP 二层协议发现是一个广播请求,于是向它所知道的所有端口广播此报文,其中一路进入另一对 VETH 设备的一端,一路进入 TAP 设备 tap0,一路进入物理网卡设备 eth0。此时在 tap0 上,用户程序可以通过 read()操作读到 ARP@vlan100,eth0 将会向外界发送 ARP@vlan100,但 eth0 的 VLAN 子设备不会收到它,因为此数据方向为请求发送而不是请求接收。

7) VETH 将请求方向转换,此时在另一端得到请求接受的 ARP@vlan100 报文。

8) 对端 VETH 设备发现有数据需要接受,并且自己有两个 VLAN 子设备,于是执行 VLAN 处理逻辑。其中一个子设备是 vlan100,与 ARP@vlan100 吻合,于是去除 VLAN ID 100 的 Tag 转发到这个子设备上,重新成为标准的以太网 ARP 报文。另一个子设备由于 ID 不吻合,不会得到此报文。

9) 此 VLAN 子设备又被 attach 到另一个桥 bridge1 上,于是转发自己收到的 ARP 报文。

10) bridge1 广播 ARP 报文。

11) 最终另外一个 TAP 设备 tap1 收到此请求发送报文,用户程序通过 read()可以得到它。

从 vlan200 子设备发送 ARP 报文

图 5 .ARP from vlan200 child device

和前面情况类似,区别是 VLAN ID 是 200,对端的 vlan200 子设备设置为 reorder_hdr = 0,表示此设备被要求保留收到的报文中的 VLAN Tag。此时子设备会收到 ARP 报文,但是带了 VLAN ID 200 的 Tag,既 ARP@vlan200。

从中心 bridge 发送 ARP 报文

图 5 .ARP from central bridge

当 bridge0 拥有 IP 时,通过 Linux 路由表用户程序可以直接将 ARP 报文发向 bridge0。这时 tap0 和外部网络都能收到 ARP,但 VLAN 子设备由于 VLAN ID 过滤的原因,将收不到 ARP 信息。

从外部网络向物理网卡发送 ARP@vlan200 报文

图 6 .ARP from external network

当外部网络连接在一个支持 VLAN 并且对应端口为 vlan200 时,此情况会发生。此时所有的 VLAN ID 为 200 的 VLAN 子设备都将接受到报文,如果设置 reorder_hdr=0 则会收到带 Tag 的 ARP@vlan200。

从 TAP 设备以 ping 方式发送 ARP

图 7 .ping from TAP device

给 tap0 赋予 IP 并加入路由,此时再 Ping 其对应网段的未知 IP 会产生 ARP 发送请求。需要注意的是此时由于 tap0 上存在的是发送而不是接收请求,因此 ARP 报文不会被转发到桥上,从而什么也不会发生。图中右边画了一个类似情况:从 vlan200 子设备发送 ARP 请求。由于缺少 VETH 设备反转请求方向,因此报文也不会被转发到桥上,而是直接通过物理网卡发往外部网络。

以文件操作方式从 TAP 设备发送报文

图 8 .file operation on TAP device

用户程序指定 tap0 设备发送报文有两种方式:socket 和 file operation。当用 socket_raw 标志新建 socket 并指定设备编号时,可以要求内核将报文从 tap0 发送。但和前面的 ping from tap0 情况类似,由于报文方向问题,消息并不会被转发到 bridge0 上。当用 open()方式打开 tap 设备文件时,情况有所不同。当执行 write()操作时,内核认为 tap0 收到了报文,从而会触发转发动作,bridge0 将收到它。如果发送的报文如图所示,是一个以 A 为目的地的携带 VLAN ID 100 Tag 的单点报文,bridge0 将会找到对应的设备进行转发,对应的 VLAN 子设备将收到没有 VLAN ID 100 Tag 的报文。 Linux 上配置网络设备命令举例

以 Redhat6.2 红帽 Linux 发行版为例,如果已安装 VLAN 内核模块和管理工具 vconfig,TAP/TUN 设备管理工具 tunctl,那么可以用以下命令设置前述网络设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    创建 Bridge:brctl addbr [BRIDGE NAME]
    删除 Bridge:brctl delbr [BRIDGE NAME]
    attach 设备到 Bridge:brctl addif [BRIDGE NAME] [DEVICE NAME]
    从 Bridge detach 设备:brctl delif [BRIDGE NAME] [DEVICE NAME]
    查询 Bridge 情况:brctl show
    创建 VLAN 设备:vconfig add [PARENT DEVICE NAME] [VLAN ID]
    删除 VLAN 设备:vconfig rem [VLAN DEVICE NAME]
    设置 VLAN 设备 flag:vconfig set_flag [VLAN DEVICE NAME] [FLAG] [VALUE]
    设置 VLAN 设备 qos:

vconfig set_egress_map [VLAN DEVICE NAME] [SKB_PRIORITY] [VLAN_QOS]

vconfig set_ingress_map [VLAN DEVICE NAME] [SKB_PRIORITY] [VLAN_QOS]

    查询 VLAN 设备情况:cat /proc/net/vlan/[VLAN DEVICE NAME]
    创建 VETH 设备:ip link add link [DEVICE NAME] type veth
    创建 TAP 设备:tunctl -p [TAP DEVICE NAME]
    删除 TAP 设备:tunctl -d [TAP DEVICE NAME]
    查询系统里所有二层设备,包括 VETH/TAP 设备:ip link show
    删除普通二层设备:ip link delete [DEVICE NAME] type [TYPE]