使用 Golang 玩转 Bridge 与 NetNamespace 互联
❝本文转自掘金,原文:https://juejin.cn/post/7074894564879761416,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang
❝前置知识:希望您对 Linux Namespace 有所了解,以及一些基础的网络知识。
简介
先简略的介绍下 Bridge
,它是一个虚拟的网络设备,linux 内核已经实现了该网络设备的功能。所以我们可以直接通过命令就能创建出 Bridge。很多人为了更好的理解 Bridge 这个网络设备都会将它类比成交换机
,但其实这种理解会很容易误导且对其他网络设备的理解会形成障碍。其实 Bridge 它就是一种协议或者是一种类型,进行实例化后形成二层网络设备
对象,而网络设备分为二层三层设备。Net Namespace
是实现网络虚拟化的重要功能,它能创建多个隔离的网络空间,它们有独自的网络栈信息。不管是虚拟机还是容器,运行的时候仿佛自己就在独立的网络中。什么是 Linux Namespace ?[1]最后同步一个概念 二层交换
:使用 MAC 地址进行通信;三层交换
:使用 IP 地址进行通信,IP 就需要网关(下一跳地址);
Linux 环境实验
❝实验环境 Centos7.9 (3.10.0-1160.el7.x86_64)
NetNamespace 间通信
图示,这种做法只是为了展示 namespace 之间通信,生产上很少出现这样的场景;
Console (实验)
# 创建 net namespace, ns0 和 ns1
$ ip netns add ns0
$ ip netns add ns1
$ ip netns list
ns1 (id: 1)
ns0 (id: 0)
# 创建 veth 以太对设备,这是一个成对出现的设备,用于在两个 net namespace 中传输数据
# 创建一个 veth0 和 veth1 并将 veth1 绑定在 ns1 中
$ ip link add veth0 type veth peer name veth1 netns ns1
# 将 veth0 绑定到 ns0
$ ip link set veth0 netns ns0
# 查看 ns0 设备 , 两个设备都为 DOWN
$ ip -n ns0 link
1: lo: <LOOPBACK,UP,LOWER_UP$ mtu 65536 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
11: veth0@if10: <NO-CARRIER,BROADCAST,MULTICAST,UP$ mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
link/ether 2e:73:f7:b1:52:c2 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# 为两个 veth 设置地址,并启用 lo,veth
$ ip -n ns0 addr add 10.0.1.1/24 dev veth0
$ ip -n ns0 link set veth0 up
$ ip -n ns0 link set lo up
$ ip -n ns1 addr add 10.0.1.2/24 dev veth1
$ ip -n ns1 link set veth1 up
$ ip -n ns1 link set lo up
# 最后做 ping 测试
$ ip netns exec ns1 ping 10.0.1.1
PING 10.0.1.1 (10.0.1.1) 56(84) bytes of data.
64 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=0.232 ms
$ ip netns exec ns0 ping 10.0.1.2
PING 10.0.1.2 (10.0.1.2) 56(84) bytes of data.
64 bytes from 10.0.1.2: icmp_seq=1 ttl=64 time=0.293 ms
Bridge、NetNamespace、Localhost 通信
这种做法是一种常态做法,在 K8s 和 Docker 中都是常见的场景;下面会顺带解析二层 mac 交换与三层 IP 交换;架构图示 :
Console(实验),图中有 3 个 namespace,下面实验只建立两个做示范:
# 安装 bridge 查看工具
$ yum install -y bridge-utils
# 创建 bridge (创建 bridge 默认创建一个 vlan1),绑定一个 IP 后启用它。
$ ip link add bridge0 type bridge
$ ip addr add 10.0.1.254/24 dev bridge0
$ ip link set bridge0 up
# 创建 net0 namespace,并创建 veth(和第一个实验类似)
$ ip netns add net0
# 创建 veth 将 eth0 放入 net0
$ ip link add veth0 type veth peer name eth0 netns net0
# 将 veth0 绑定到 bridge0 上
$ ip link set veth0 master bridge0
# 为 eth0 绑定 ip 地址
$ ip -n net0 addr add 10.0.1.1/24 dev eth0 brd +
# 启动设备
$ ip -n net0 link set dev eth0 up
$ ip -n net0 link set dev lo up
$ ip link set dev veth0 up
# 查看 net0 设备情况
$ ip -n net0 link
1: lo: <LOOPBACK,UP,LOWER_UP$ mtu 65536 qdisc noqueue state UNKNOWN group default 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
...
2: eth0@if18: <BROADCAST,MULTICAST,UP,LOWER_UP$ mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 46:ed:ec:f0:63:07 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.0.1.1/24 scope global eth0
...
# 这里在说一个知识点 eth0@if18 的 if18 对应的是主 namespace 下 ip link 的 18 号设备,如果你删除外面的这个以太对设备,那么 net0 里面的也会一并删除。
$ ip link
...
18: veth0@if2: <BROADCAST,MULTICAST,UP,LOWER_UP$ mtu 1500 qdisc noqueue master bridge0 state UP mode DEFAULT group default qlen 1000
link/ether 0e:d5:0e:0c:e7:73 brd ff:ff:ff:ff:ff:ff link-netnsid 0
...
# 下面是创建 net1 的操作
$ ip netns add net1
$ ip link add veth1 type veth peer name eth1 netns net1
$ ip link set veth1 master bridge0
$ ip -n net1 addr add 10.0.1.2/24 dev eth0 brd +
$ ip -n net1 link set dev eth1 up
$ ip -n net1 link set dev lo up
$ ip link set dev veth1 up
# 查看现在 bridge0 中拥有的 veth 设备数量,可以看到是 2 个
$ brctl show bridge0
bridge name bridge id STP enabled interfaces
bridge0 8000.0ed50e0ce773 no veth0
veth1
下面进行 bridge0 内 netns 互通测试,可以看到是没有问题。
$ ip netns exec net0 ping 10.0.1.2
PING 10.0.1.2 (10.0.1.2) 56(84) bytes of data.
64 bytes from 10.0.1.2: icmp_seq=1 ttl=64 time=0.061 ms
64 bytes from 10.0.1.2: icmp_seq=2 ttl=64 time=0.080 ms
...
# 这是通过二层 mac 寻址(二层交换),可以看看在 linux 上如何查看他们
# net0 上来记录对端的 mac 地址以及 ip 地址
$ ip netns exec net0 ip neigh
10.0.1.2 dev eth0 lladdr 56:01:64:48:c3:87 STALE
10.0.1.254 dev eth0 lladdr 0e:d5:0e:0c:e7:73 STALE
# 可以看到 net1 的 mac 地址 56:01:64:48:c3:87
$ ip -n net1 link
...
2: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP$ mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 56:01:64:48:c3:87 brd ff:ff:ff:ff:ff:ff link-netnsid 0
那我们继续测试 netns 与 LocalHost 主机是否互通,我这里的主机地址是 192.168.0.3。
$ ip netns exec net0 ping 192.168.0.3
connect: 网络不可达
OMG,可以看到 netns 到达主机是不通。因为 net0 所在网段是 10.0.1.0/24 这个网段,而我的主机网段是 192.168.0.0/24,二层广播域也是不通的所以无法进行二层交换。(还有一点是 net0 是通过 bridge0 连接主机,而 bridge0 本身再创建的时候默认划分了一个 vlan1。
由此我们应该想到就是走三层交换,即通过路由寻址。
# 分别给给 net0、net1 加上默认路由 , 所有流量下一跳都走 10.0.1.254 由 eth0 出。
$ ip netns exec net0 ip route add default via 10.0.1.254 dev eth0
$ ip netns exec net1 ip route add default via 10.0.1.254 dev eth0
# 查看 net0 的路由
$ ip netns exec net0 route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.0.1.254 0.0.0.0 UG 0 0 0 eth0
10.0.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
# 还需要一条在主机上回包的路由,去往 10.0.1.0/24 网段的流量走 10.0.1.254 由 bridge0 这个设备出
# 先删除创建 bridge 时系统自动创建的一条默认路由
$ ip route del 10.0.1.0/24
$ ip route add 10.0.1.0/24 via 10.0.1.254 dev bridge0
# 查看主机的路由
$ route -n
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.0.1 0.0.0.0 UG 100 0 0 eth0
10.0.1.0 10.0.1.254 255.255.255.0 UG 0 0 0 bridge0
192.168.0.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
# 再测试下
$ ip netns exec net0 ping 192.168.0.3
PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.061 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.080 ms
❝PS: 写路由时由哪个设备出是很重要的,由于不同的网络设备对数据包会做不同的封装。
Bridge、NetNamespace、RemoteHost 通信
上个小节写了实现了前面三种,这次只需要实现 Remotehost 通信即可。如果理解上面一节的三层交换含义,那么就能得知跨主机通信也是需要三层交换。
实验
架构图
* 主机 A: 192.168.0.3;net0 网段:10.0.1.0/24
* 主机 B: 192.168.0.2;net0 网段:10.0.2.0/24
* 默认网关 : 192.168.0.1
* 实验内容: 两台机器的 net0 namespace 间互通,相当于 10.0.1.1 需要 ping 通 10.0.1.2;以及 10.0.1.1 ping 通 192.168.0.2
构建出 net0 到本机的互通 (192.168.0.3)
# 和上一个实验是一样的流程配置即可
构建出 net0 到本机的互通 (192.168.0.2)
# bridge #
$ ip link add dev bridge0 type bridge
$ ip link set dev bridge0 up
$ ip addr add 10.0.2.254/24 dev bridge0
# net0 #
$ ip netns add net0
$ ip link add dev veth0 type veth peer name eth0 netns net0
# brd + 是设置 10.0.2.255 为广播地址
$ ip -n net0 addr add 10.0.2.1/24 dev eth0 brd +
$ ip -n net0 link set dev eth0 up
$ ip -n net0 link set dev lo up
$ ip link set dev veth0 master bridge0
$ ip link set dev veth0 up
# 查看 veth0 bridge0 都是 up
$ ip link
# 配置 net0 默认路由
$ ip netns exec net0 ip route add default via 10.0.2.254 dev eth0
# 配置主机路由
$ ip route del 10.0.2.0/24
$ ip route add 10.0.2.0/24 via 10.0.2.254 dev bridge0
# ping 通测试
$ ip netns exec net0 ping 192.168.0.2
到此两台机器的 net0 都是可以 ping 通主机的出口 IP,下面正式进行跨主机联通;
net0 (10.0.1.1) - GW (192.168.0.,1)
通过 iptables 进行伪装发送即可,这样的做法即可以把网络包发送到网关和 ping 通主机,但是还不能解决跨主机的 namespace 通信。
# 这里其实有两种做法
# 。
# 192.168.0.3
$ echo 1 $ /proc/sys/net/ipv4/ip_forward
$ iptables -t nat -A POSTROUTING -s 10.0.1.0/24 -j MASQUERADE
# 192.168.0.2
$ echo 1 $ /proc/sys/net/ipv4/ip_forward
$ iptables -t nat -A POSTROUTING -s 10.0.2.0/24 -j MASQUERADE
# 现在就可以测试 ping
$ ip netns exec net0 ping 192.168.0.2
net0 (10.0.1.1) - net0 (10.0.2.1)
其实最简单的办法就是再两个机器上都各自写一条路由,做三层交换即可
# 192.168.0.3
$ ip route del 10.0.1.0/24
# 直接为目标 10.0.2.0 的网段指定下一跳
$ ip route 10.0.2.0/24 via 192.168.0.2 dev eth0
# 192.168.0.2
$ ip route del 10.0.2.0/24
# 直接为目标 10.0.1.0 的网段指定下一跳
$ ip route 10.0.1.0/24 via 192.168.0.3 dev eth0
# 测试 ping 主机
$ ip netns exec net0 ping 192.168.0.2
# 测试 net0 (10.0.2.1)
$ ip netns exec net0 ping 10.0.2.1
代码演示
这里使用用了 3 个库来完成代码的开发 netlink
、netns
和 go-iptables
,废了一番功夫倒腾出来了,整个程序是幂等的,也就是可以多次运行结果都是一样的;
安装库
$ go get "github.com/coreos/go-iptables/iptables"
$ go get "github.com/vishvananda/netlink"
代码
netfilter_study.go,最终允许是分别在两台主机上运行的,实现了 2.3.1 的效果,代码都比较简单结合注释和末尾会有一些难点解析。
package main
import (
"fmt"
"github.com/coreos/go-iptables/iptables"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
"log"
"net"
"os"
"os/exec"
"runtime"
)
/*
#实验环境#
主机 A:
HOST: 192.168.0.3
ns0: 10.0.1.1/24
bridge0: 10.0.1.254/24
主机 B:
HOST: 192.168.0.2
ns0: 10.0.2.1/24
bridge0: 10.0.2.254/24
#实现目标#
10.0.1.1 -$ 192.168.0.2
10.0.1.1 -$ 10.0.2.1
*/
var (
IsHostA = false
)
const (
EnvName = "IS_HOST_A"
// 无需修改
NSName = "ns0"
BridgeName = "bridge0"
VEth0 = "veth0"
NsEth0 = "nseth0"
// 如果需要在自己的机器上实现,需要改动 HostA 和 HostB 变量即可
HostA = "192.168.0.3/24"
HostANS0 = "10.0.1.1/24"
HostABridge0 = "10.0.1.254/24"
HostB = "192.168.0.2/24"
HostBNS0 = "10.0.2.1/24"
HostBBridge0 = "10.0.2.254/24"
)
func Error(e error) {
if e != nil {
log.Fatalln(e)
}
}
type DoFunc func(oNs, nNs *netns.NsHandle) error
func SetupBridge() *netlink.Bridge {
log.Println("SetupBridge...runing")
linkDev, _ := netlink.LinkByName(BridgeName)
if linkDev != nil {
if _, ok := linkDev.(*netlink.Bridge); ok {
Error(netlink.LinkDel(linkDev))
}
}
br0 := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: BridgeName,
MTU: 1500,
TxQLen: -1,
},
}
// 添加
Error(netlink.LinkAdd(br0))
// 启动
Error(netlink.LinkSetUp(br0))
// 设置 ip
hb := HostABridge0
if !IsHostA {
hb = HostBBridge0
}
ipv4Net, err := netlink.ParseIPNet(hb)
Error(err)
Error(netlink.AddrAdd(br0, &netlink.Addr{
IPNet: ipv4Net,
}))
log.Println("SetupBridge...done")
return br0
}
func SetupNetNamespace() *netns.NsHandle {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
log.Println("SetupNetNamespace...running")
origns, _ := netns.Get()
defer origns.Close()
_, err := netns.GetFromName(NSName)
Error(netns.Set(origns))
if err == nil {
log.Printf("%s net ns is exists. Delete netns %s\n", NSName, NSName)
cmd := exec.Command(
"sh",
"-c",
fmt.Sprintf("/usr/sbin/ip netns del %s", NSName))
Error(cmd.Run())
}
// 由于程序上启动 netns 是需要附着于进程上的,所以这里直接使用 ip netns 来创建 net namespace
cmd := exec.Command("sh", "-c", fmt.Sprintf("/usr/sbin/ip netns add %s", NSName))
Error(cmd.Run())
ns, err := netns.GetFromName(NSName)
Error(err)
log.Println("SetupNetNamespace...done")
return &ns
}
func SetupVEthPeer(br *netlink.Bridge, ns *netns.NsHandle) {
log.Println("SetupVEth...running")
oBrVEth, _ := netlink.LinkByName(VEth0)
if oBrVEth != nil {
log.Printf("%s is exists, it will be delete. \n", VEth0)
Error(netlink.LinkDel(oBrVEth))
}
log.Printf("Create vethpeer %s peer name %s\n", VEth0, NsEth0)
vethPeer := &netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: VEth0,
},
PeerName: NsEth0,
}
Error(netlink.LinkAdd(vethPeer))
log.Printf("Set %s to %s \n", vethPeer.PeerName, NSName)
// 获取 ns 的 veth
nsVeth, err := netlink.LinkByName(vethPeer.PeerName)
Error(err)
// 设置 ns 的 veth
Error(netlink.LinkSetNsFd(nsVeth, int(*ns)))
log.Printf("Set %s master to %s \n", vethPeer.Name, BridgeName)
// 获取 host 的 veth
brVeth, err := netlink.LinkByName(vethPeer.Name)
Error(err)
// 设置 bridge 的 veth
Error(netlink.LinkSetMaster(brVeth, br))
log.Printf("Set up %s \n", vethPeer.Name)
Error(netlink.LinkSetUp(brVeth))
Error(NsDo(func(oNs, nNs *netns.NsHandle) error {
// 设置 IP 地址
hn := HostANS0
if !IsHostA {
hn = HostBNS0
}
log.Printf("Addr add ip to %s \n", vethPeer.Name)
ipv4Net, err := netlink.ParseIPNet(hn)
Error(err)
Error(netlink.AddrAdd(nsVeth, &netlink.Addr{
IPNet: ipv4Net,
}))
log.Printf("Set up %s \n", vethPeer.PeerName)
// 启动设备
Error(netlink.LinkSetUp(nsVeth))
log.Println("SetupVEth...done")
return nil
}))
}
func SetupNsDefaultRoute() {
Error(NsDo(func(oNs, nNs *netns.NsHandle) error {
// add default gate way
log.Println("SetupNsDefaultRouter... running")
log.Printf("Add net namespace %s default gateway.", NSName)
gwAddr := HostBBridge0
if IsHostA {
gwAddr = HostABridge0
}
gw, err := netlink.ParseIPNet(gwAddr)
Error(err)
defaultRoute := &netlink.Route{
Dst: nil,
Gw: gw.IP,
}
Error(netlink.RouteAdd(defaultRoute))
log.Println("SetupNsDefaultRouter... done")
return nil
}))
}
func SetupIPTables() {
log.Println("Setup IPTables for ns transfer packet to remote host...running")
// 读取本地 iptables,默认是 ipv4
ipt, err := iptables.New()
Error(err)
// iptables -t nat -A POSTROUTING -s 10.0.1.0/24 -j MASQUERADE
nsAddr := HostBNS0
if IsHostA {
nsAddr = HostANS0
}
_, srcNet, err := net.ParseCIDR(nsAddr)
Error(err)
rule := []string{"-s", srcNet.String(), "-j", "MASQUERADE"}
// 先进行清除后再添加,保持简易幂等
_ = ipt.Delete("nat", "POSTROUTING", rule...)
Error(ipt.Append("nat", "POSTROUTING", rule...))
log.Println("Setup IPTables done")
}
func SetupRouteNs2Ns() {
log.Println("Setup route HostA(net0) <==$ HostB(net0)... running")
// 10.0.1.0/24 via 192.168.0.3 dev eth0
gwAddr := HostA
dstAddr := HostANS0
if IsHostA {
gwAddr = HostB
dstAddr = HostBNS0
}
_, dstNet, err := net.ParseCIDR(dstAddr)
Error(err)
gwNet, err := netlink.ParseIPNet(gwAddr)
Error(err)
// 先删除后增加
_ = netlink.RouteDel(&netlink.Route{
LinkIndex: netlink.NewLinkAttrs().Index,
Dst: dstNet,
})
Error(netlink.RouteAdd(&netlink.Route{
LinkIndex: netlink.NewLinkAttrs().Index,
Dst: dstNet,
Gw: gwNet.IP,
}))
log.Println("Setup route HostA(net0) <==$ HostB(net0) done")
}
func NsDo(doFunc DoFunc) error {
// 进入 netns, 使用线程锁固定当前 routine 不会其他线程执行,避免 namespace 执行出现异常。
// 因为如果在执行过程中切换了线程,可能找不到已经建立好的 namespace。
runtime.LockOSThread()
defer runtime.UnlockOSThread()
log.Printf("Switch net namespace to %s \n", NSName)
originNs, err := netns.Get()
Error(err)
// 切换回原始 ns
defer func() {
log.Println("Switch net namespace to origin")
Error(netns.Set(originNs))
}()
ns, err := netns.GetFromName(NSName)
Error(err)
Error(netns.Set(ns))
return doFunc(&originNs, &ns)
}
func main() {
if os.Getenv(EnvName) == "1" {
IsHostA = true
}
ns := SetupNetNamespace()
bridge := SetupBridge()
SetupVEthPeer(bridge, ns)
SetupNsDefaultRoute()
SetupIPTables()
SetupRouteNs2Ns()
log.Println("Config Finished.")
}
从名字上看基本和之前在命令的使用是一致的,创建 namespace、bridge 和 vethpeer 然后进行 veth 绑定,最后配置路由和 iptables。 唯一有个疑点是在 NsDo
这个函数,这个函数是一个必包函数,主要为了锁定线程;在 go 里,每个操作系统线程都可能对应不同的 network namespace,但由于 go 线程调度机制的特点,在我们的代码逻辑开始执行的时候,并不能够保证当前的 network namespace 就是我们创建的或者是我们开始时候的 namespace,这样会导致我们不知道当前身处什么 space 下。(涉及知识点:GO 调度模型(GPM)
,Linux Process Namespace
)
运行
在 HostA 上运行:export IS_HOST_A=1; go run netfilter_study.go
。
在 HostB 上运行:export IS_HOST_A=0; go run netfilter_study.go
。
运行后效果,看到 Config Finished 即可。
$ export IS_HOST_A=1; go run netlink_study.go
2022/03/14 17:23:58 SetupNetNamespace...running
2022/03/14 17:23:58 ns0 net ns is exists. Delete netns ns0
2022/03/14 17:23:58 SetupNetNamespace...done
2022/03/14 17:23:58 SetupBridge...runing
2022/03/14 17:23:58 SetupBridge...done
2022/03/14 17:23:58 SetupVEth...running
2022/03/14 17:23:58 veth0 is exists, it will be delete.
2022/03/14 17:23:58 Create vethpeer veth0 peer name nseth0
2022/03/14 17:23:58 Set nseth0 to ns0
2022/03/14 17:23:58 Set veth0 master to bridge0
2022/03/14 17:23:58 Set up veth0
2022/03/14 17:23:58 Switch net namespace to ns0
2022/03/14 17:23:58 Addr add ip to veth0
2022/03/14 17:23:58 Set up nseth0
2022/03/14 17:23:58 SetupVEth...done
2022/03/14 17:23:58 Switch net namespace to origin
2022/03/14 17:23:58 Switch net namespace to ns0
2022/03/14 17:23:58 SetupNsDefaultRouter... running
2022/03/14 17:23:58 Add net namespace ns0 default gateway.
2022/03/14 17:23:58 SetupNsDefaultRouter... done
2022/03/14 17:23:58 Switch net namespace to origin
2022/03/14 17:23:58 Setup IPTables for ns transfer packet to remote host...running
2022/03/14 17:23:58 Setup IPTables done
2022/03/14 17:23:58 Setup route HostA(net0) <==$ HostB(net0)... running
2022/03/14 17:23:58 Setup route HostA(net0) <==$ HostB(net0) done
2022/03/14 17:23:58 Config Finished.
HostA 上运行测试命令
$ ip netns exec ns0 ping 10.0.2.1
写在最后
之前我个人对 linux 网络设备
的理解是误解,总以一种物理性质来理解它,比如一个网卡设备我很难理解为什么它还有子接口?如果将它以一种设备对象或软件化的去理解它就非常好理解。一个网卡设备默认有个主接口,那么相对应就可以创建子借口,再者想到网卡是一个多路复用和多路分解的网络设备,这样一来就容易消化了。网络设备的 类型
,需要明确知道那种类型是二层还是三层。好比实体化二层交换机只会包含二层类型的设备与协议, 三层交换机(核心交换)就包含了二层三层设备与协议。
引用链接
[1]什么是 Linux Namespace ?: https://juejin.cn/post/6901477931265196039
你可能还喜欢
点击下方图片即可阅读
云原生是一种信仰 🤘
关注公众号
后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!
点击 "阅读原文" 获取更好的阅读体验!
发现朋友圈变“安静”了吗?