此文原创,始发于 我的简书博客,特此说明
什么是bridge(网桥)?
bridge是一种技术,可以把一个linux设备上的两块网卡桥接在一起,如何对外表现为一个大的网卡接口,这样做有很多用途
比如你有两台设备,但是又没有路由器,那么把他们桥接在一起,可以共享其中一台的网络,这样两台都可以上网,这两台设备也可以是vm,不一定是物理设备
还有一种用途,就是监控两个设备的网络流量,比如用wireshark来监控他们间的流量
总的来讲,桥接就是将一台计算机插入到已经与较大网络(例如Internet)建立连接的另一台计算机上,然后让这台桥接上去的计算机可以使用联网计算机的连接。
哪些软件使用了桥接技术
bridge在vm(virtual machine)的领域非常有用,比如docker, k8s, multipass等。
docker可以帮忙创建很多独立的虚拟容器,那这些容器怎么上网,相互之间怎么通信呢,docker提供多种途径,其中一种就是bridge。
查看设备上有哪些bridge,可以用brctl命令,如果你的没有这个命令,可用apt或者yum安装 sudo apt install bridge-utils
查看bridge
brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242a8576763 no
上面这个是docker的bridge(如果你设备上装有docker的话 就可以看到)
k8s的bridge,k8s也可以通过bridge来通信
bridge name bridge id STP enabled interfaces
cbr0 8000.125a96a00d6c no veth461c2da3
veth4cd7f98e
veth62b53b2e
vethb0c3d926
k8s创建名为cbr0
的网桥,并为每个 pod 创建了一个 veth 对,每个 pod 的主机端都连接到cbr0
。 这个 veth 对的 pod 端会被分配一个 IP 地址,该 IP 地址隶属于节点所被分配的 IP 地址范围内。节点的 IP 地址范围则通过配置或控制器管理器来设置。cbr0
被分配一个 MTU,该 MTU 匹配主机上已启用的正常接口的最小 MTU ( 来自k8s的官方说明)
multipass的bridge,它也是用bridge来通信
bridge name bridge id STP enabled interfaces
mpqemubr0 8000.065897b80a53 no mpqemubr0-dummy
tap-166f18d0005
tap-1e3511f1cea
tap-2cc8b34e9b0
tap-313bf980cb9
tap-416f9717aeb
tap-4524e0d94ea
tap-5abadd4a100
tap-62fe19d7992
tap-c1b397bb0d
创建一个bridge
brctl addbr br0
上面命令创建一个名为br0的桥
接下来我们可以把已有的网卡接口绑定到这个桥上,在这之前可以看看我们有有哪些网卡接口
可以用这个命令查看
ip addr show
假设上面查出来,有eth0和eth1两个网卡接口,下面我们把他们用命令绑定到一起
brctl addif br0 eth0 eth1 # eth0和eth1的顺序不重要,不影响结果
这是我们再来绑定的结果
brctl show
bridge name bridge id STP enabled interfaces
br0 8000.001ec952d26b yes eth0
eth1
就是说eth0和eth1绑定到了br0这个桥上了
上面命令是临时创建这种关系,如果要永久保留下来,需要在/etc/network/interfaces这个配置文件里面修改
sudo vim /etc/network/interfaces
# The loopback network interface
auto lo
iface lo inet loopback
# 上面这两行是系统默认有的, 意思是定义一个回环设备,并且自动启动
# Set up interfaces manually, avoiding conflicts with, e.g., network manager
iface eth0 inet manual # 定义eth0接口 手动启动
iface eth1 inet manual # 定义eth1接口 手动启动
# Bridge setup
iface br0 inet dhcp # 定义br0这个接口,并且从dhcp获取ip
bridge_ports eth0 eth1 # 绑定上面定义那两个接口进来
# 如果希望给这个bridge配置静态ip,可以用下面这个段配置代替上面的bridge配置
#iface br0 inet static
# bridge_ports eth0 eth1
# address 192.168.1.2
# broadcast 192.168.1.255
# netmask 255.255.255.0
# gateway 192.168.1.1
上面个并设置的接口是手动启动,要通过下面的命令来启动
ifup br0
桥接无线网卡和以太网卡
有很多无线路由都会拒绝没有认证过的帧,那么我们的以太网卡虽然能通过bridge借用无线网卡连接网络,但是由于它发的帧并没有经过无线路由的认证(因为以太网卡发出去的frames里面MAC地址不是无线网卡向无线路由注册过的地址),所以会被拒绝掉。
这个时候要借助一种叫做etable的程序,ebtables本质上类似于iptables,不同之处在于它在OSI模型的数据链路层的MAC子层而不是网络层上运行。这允许更改所有帧的源MAC地址。
要实现这个功能,分两步走
- 配置bridge-utils
2. 配置etable的规则
配置bridge-utils
也是在/etc/network/interfaces里面配置
pre-up iwconfig wlan0 essid $YOUR_ESSID # wifi的essid(一般和ssid相同)
bridge_hw $MAC_ADDRESS_OF_YOUR_WIRELESS_CARD # 无线网卡地址
- 这个配置的意思是启动一个叫wlan0的无线网卡连接到essid上
2. 设置以太网网卡的MAC地址为无线网卡的MAC地址
以上这些配置命令的说明可以从 bridge-tuils的文档里面找到
配置etable的规则
先安装etable
apt install ebtables
配置规则(etable的规则和iptables差不多)
# 这条规则是将所有发送到AP的帧的源MAC地址设置为网桥的MAC地址
ebtables -t nat -A POSTROUTING -o wlan0 -j snat --to-src $MAC_OF_BRIDGE --snat-arp --snat-target ACCEPT
# 下面这两条配规则,要求你得知道网桥后面的每台计算机的MAC和IP
ebtables -t nat -A PREROUTING -p IPv4 -i wlan0 --ip-dst $IP -j dnat --to-dst $MAC --dnat-target ACCEPT
ebtables -t nat -A PREROUTING -p ARP -i wlan0 --arp-ip-dst $IP -j dnat --to-dst $MAC --dnat-target ACCEPT
每次手工输入比较麻烦,也可以通过脚本来弄,参考
docker的bridge
docker容器的bridge比我们上面认识到的bridge要复杂
首先docker的提供给容器用的网络模式有好多种:
- none: 没有任何的网卡接口,除了容器自身的loobpack设备lo,通常这种容器用来连接定制的网络驱动
- host: 给独立的容器直接使用宿主机的网络,这种可能会有冲突,容器间没有隔离。
- overlay:覆盖网络,这种网络可以用在swarm集群和独立容器(如networking=host)或者多个docker守护进程上,把他们通过覆盖来相互通信。
- macvlan: 这个涉及到vlan这种虚拟局域网的技术,通过macvlan可以直接给容器分配MAC网卡地址,对于物理网络来说,它就是一个物理的网卡设备(实际上是一个虚拟网卡),容器的守护进程通过这个网卡地址准确的把流量路由给这个容器,相当于容器直接连接到我们的物理网络
- third-party network plugin: 第三方网络插件
- bridge:docker的bridge,这个是我们要个讲的
看了网上很多关于docker bridge的介绍,上面这个示意图比较正确,(来自这里)
- 首先docker会在宿主机里面创建一个docker0的linux bridge
- 然后在创建容器的时候使用veth pair对的技术,会对应在宿主虚拟创建一个虚拟网卡,然后这个网卡和容器里面的网卡一一对应起来,docker的守护(deamon)进程会正确的把发给虚拟网卡的数据路由给对应的容器(一一对应),同时,所有的虚拟网卡会绑定到docker0 这个linux bridge里面(见上面关于linux bridge的介绍),这样的话容器之间就可以相互通信了
那么宿主机又是怎么样把外部的请求正确的发给veth呢,这是因为在docker0 bridge前面还有一层NAT的技术,通过网络地址端口转换的技术,把发给特定端口的数据,转发给容器,既然如此,那么就必须绑定docker对外暴露什么端口,这就是我们平时定义docker的时候的端口bind干的事
docker run -p 80:8080 nginx # 把容器的8080端口绑定到宿主机的80端口
如上面这个,把容器的8080端口绑定到宿主机的80端口,那么在进行NAT路由的时候,就可以准确找到容器对应的veth:xx,然后veth又对应到容器的eth0:xx,docker deamon就可以把数组准确的路由进容器里。
如何查看veth pair的对应关系
查找veth的关系可以通过ip命令来查
1.在容器里面执行:
> ip a
1: lo: mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
55: eth0@if56: mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
容器里面编号为55的接口eth0是@if56
2. 在宿主机里面也执行ip a
> ip a | grep veth
56: veth243b8f2@if55: mtu 1500 qdisc noqueue master docker0 state UP group default
宿主机里面56编号的这个接口是@if55
两者刚好是对应关系,这样看比较麻烦,可以用github上这个项目,一个命令列出对应关系
[root@dockervisor-1 ~]# dockerveth
CONTAINER ID VETH NAMES
60d27ce962ff vethe353e93 hopeful_bhaskara
d07a2979e69a vethe4c3cee silly_meitner
1e8656e195ba veth1ce04be thirsty_meitner
在宿主机里面查看DNAT路由规则
下面这条就是我的主机上建立的一个8081到容器80端口的DNAT规则,宿主机就是根据这条规则准确的把数据路由到172.17.0.2这台容器
> sudo iptables -S -t nat | grep 8081
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8081 -j DNAT --to-destination 172.17.0.2:80
这个时候,这台容器是插入到docker0这个桥的,docker0的ip是172.17.0.1
不推荐在生成环境使用docker0 bridge
官方不推荐在生成使用docker0这个bridge,取而代之,建议在创建容器的时候使用--network来自动自己创建的network bridge.
- 先在宿主机创建自定义的bridge
docker network create cluster1-net-bridge
2. 创建容器的时候指定bridge
docker run --name nginx -p 80:8080 -d nginx --network cluster1-net-bridge
docker run --name mongo -d mongo:tag --network cluster1-net-bridge
为什么官方会这样推荐呢,原因是:
- docker0在所有的容器里面共享配置,包括MTU和防火墙规则等,对于不同的集群我们可能希望可隔离开来
- docker0默认不能通过容器的名称来通信,而自己创建的桥,如cluster1-net-bridge,可以可以允许通过名称来通信,也就是自己创建的bridge具备dns解析功能,而docker0如果需要有这种功能需要用--link来在创建容器的时候指定,如果有复杂的容器关系,那会非常难以维护。
- 自定义桥可以热插拔,在生产状态下改变
--link 这个参数在官方文档中已经被标记为过期的参数, 不再建议使用
如上面的两个容器,如果不指定—network,而是用默认的docker0,那么
在nginx里面 ping mongo是不行的,而如果挂到cluster-net-bridge这个桥,ping mongo 是可以的
也就是我可以在nginx这个容器里面直接通过 "mongo" 这个名称来连接mongo服务,而不需要通过子网的ip来连.那么这种默认的约定在运维的时候会非常方便,再结合使用swam和docker-compose.yaml一起来启动,维护会更方便。
创建docker bridge network
接下来我们试一下创建两个docker bridge network, 然后创建新的容器来加入到这两个network.
docker network create -d bridge --subnet 192.168.5.0/24 --gateway 192.168.5.1 test_bridge1
docker network create -d bridge --subnet 192.168.6.0/24 --gateway 192.168.6.1 test_bridge2
用途 brctl 看看宿主机连的桥
bridge name bridge id STP enabled interfaces
br-6edc31410314 8000.02423e98f193 no
br-c9608a22917c 8000.0242206cc645 no
用docker network ls 看
> docker network ls
brctlNETWORK ID NAME DRIVER SCOPE
6edc31410314 test_bridge1 bridge local
c9608a22917c test_bridge2 bridge local
network Id对应的就是上面bridge name的id部分
用docker network inspect 一个看看, 其实br-6edc31410314就是Id前面部分
> docker network inspect test_bridge1
[
{
"Name": "test_bridge1",
"Id": "6edc31410314709178acb728cd448b17f32f2f6a7646299f0c67369329e81a64",
"Created": "2020-03-08T20:33:25.480769489+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "192.168.5.0/24",
"Gateway": "192.168.5.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
一个容器如何连到多个桥接网络
现在我们创建4个新的容器分别加入到这两个network.
# box 1
docker run --name box1 -p 8082:80 -it --rm --network=test_bridge1 busybox sh
# box 2
docker run --name box2 -p 8083:80 -it --rm --network=test_bridge1 busybox sh
# box 3
docker run --name box3 -p 8084:80 -it --rm --network=test_bridge2 busybox sh
# box 4
docker run --name box4 -p 8085:80 -it --rm --network=test_bridge2 busybox sh
container依次从network获取的dhcp获取到ip,依次为:
box 1: 192.168.5.2
box 2: 192.168.5.3
box 3: 192.168.6.2
box 4: 192.168.6.3
那么box 1和box 2是相互通的,box 3和box 4互通
在box 1 ping box 2的ip,可以通信
/ # ping 192.168.5.3
PING 192.168.5.3 (192.168.5.3): 56 data bytes
64 bytes from 192.168.5.3: seq=0 ttl=64 time=0.211 ms
在box 1 ping 的容器的名称 box2,也可以通信
/ # ping box2
PING box2 (192.168.5.3): 56 data bytes
64 bytes from 192.168.5.3: seq=0 ttl=64 time=0.177 ms
64 bytes from 192.168.5.3: seq=1 ttl=64 time=0.172 ms
在box1 ping box3的ip, 不可以通信
/ # ping 192.168.6.2
PING 192.168.6.2 (192.168.6.2): 56 data bytes
如果box1 要和box3要通信怎么办?把box 3加入到box 1的bridge
docker network connect test_bridge1 box3
再来看看box 3的网卡, box 3多了一个网卡,ip是192.168.5.4
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:C0:A8:06:02
inet addr:192.168.6.2 Bcast:192.168.6.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:172 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:28211 (27.5 KiB) TX bytes:0 (0.0 B)
eth1 Link encap:Ethernet HWaddr 02:42:C0:A8:05:04
inet addr:192.168.5.4 Bcast:192.168.5.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:43 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:6368 (6.2 KiB) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
box3和box1通过test_bridge1也桥接起来了,可以通过brctl看看
➜ bin brctl show
bridge name bridge id STP enabled interfaces
br-6edc31410314 8000.02423e98f193 no veth4003153
veth8e06926
vethc2edaec
br-c9608a22917c 8000.0242206cc645 no veth5ff4051
veth62200c6
可以看到,bridge1桥接了3个网卡,bridge2桥接了2个网卡
/ # ping box3
PING box3 (192.168.5.4): 56 data bytes
64 bytes from 192.168.5.4: seq=0 ttl=64 time=0.210 ms
64 bytes from 192.168.5.4: seq=1 ttl=64 time=0.206 ms
64 bytes from 192.168.5.4: seq=2 ttl=64 time=0.138 ms
这样 box1就可以ping 通 box3了
了解了以上这些原理之后,那么我们可以设计一个这样的网络,把前前后台的容器网络隔离开来,保证安全性
扩展阅读:
bridged-network