404频道

学习笔记

控制器向上提供接口,用来供应用程序调用,此接口成为北向接口;控制器向下调用接口,控制网络设备,此接口成为南向接口。

OpenFlow是控制器和网络设备之间互通的南向协议,OpenvSwitch 用于创建软件的虚拟交换机。

原理

https://static001.geekbang.org/resource/image/d8/14/d870e5bfcad8ec45d146c3226cdccb14.jpg

用户态进程

  • ovsdb:本地数据库,存储ovs的配置信息
  • vswitchd:ovs-ofctl用来跟该命令通讯,下发流表规则

install OpenvSwitch on CentOS 7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 安装依赖
yum install openssl-devel python-sphinx gcc make python-devel openssl-devel kernel-devel graphviz kernel-debug-devel autoconf automake rpm-build redhat-rpm-config libtool python-twisted-core python-zope-interface PyQt4 desktop-file-utils libcap-ng-devel groff checkpolicy selinux-policy-devel gcc-c++ python-six unbound unbound-devel -y

mkdir -p ~/rpmbuild/SOURCES && cd ~/rpmbuild/SOURCES
# 下载ovs源码
wget https://www.openvswitch.org/releases/openvswitch-2.12.0.tar.gz

tar zvxf openvswitch-2.12.0.tar.gz
# 构建rpm包
rpmbuild -bb --nocheck openvswitch-2.12.0/rhel/openvswitch-fedora.spec

# 安装rpm包
yum localinstall /root/rpmbuild/RPMS/x86_64/openvswitch-2.12.0-1.el7.x86_64.rpm

systemctl start openvswitch.service

在编译的时候有如下报错:

1
2
3
  File "/usr/lib64/python2.7/site-packages/jinja2/sandbox.py", line 22, in <module>
from markupsafe import EscapeFormatter
ImportError: cannot import name EscapeFormatter

是因为markupsafe的版本不对导致的,解决方法为安装合适的版本:

1
2
pip uninstall markupsafe
pip install markupsafe==0.23

vlan实验

1
2
3
# 创建虚拟交换机ovs_br
ovs-vsctl add-br ovs_br
ovs-vsctl add-port ovs_br first_port

flow table试验 1

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
# 创建namespace
ip netns add ns1
ip netns add ns2

# 创建veth pair设备
ip link add veth1 type veth peer name veth1_br
ip link add veth2 type veth peer name veth2_br

# 设置veth pair设备的namespace
ip link set veth1 netns ns1
ip link set veth2 netns ns2

# 创建OVS网桥
ovs-vsctl add-br ovs1

# 将veth pair设备另一端绑到网桥ovs1
ovs-vsctl add-port ovs1 veth1_br
ovs-vsctl add-port ovs1 veth2_br

# 启动veth pair
ip netns exec ns1 ip link set veth1 up
ip netns exec ns2 ip link set veth2 up
ip link set veth1_br up
ip link set veth2_br up

# 设置veth1和veth2的ip地址
ip netns exec ns1 ip addr add 192.168.1.100 dev veth1
ip netns exec ns2 ip addr add 192.168.1.200 dev veth2

# 配置路由
ip netns exec ns1 route add -net 192.168.1.0 netmask 255.255.255.0 dev veth1
ip netns exec ns2 route add -net 192.168.1.0 netmask 255.255.255.0 dev veth2

可以使用如下命令来查看刚才的操作:

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
# 可以看到刚才创建的网桥
$ ovs-vsctl list-br
ovs1

# 查看网桥的端口
$ ovs-vsctl list-ports ovs1
veth1_br
veth2_br

# 查看网桥的状态
$ ovs-vsctl show
4ec35070-a763-4748-878a-c3784b5938a4
Bridge "ovs1"
Port "veth1_br"
Interface "veth1_br"
Port "ovs1"
Interface "ovs1"
type: internal
Port "veth2_br"
Interface "veth2_br"
ovs_version: "2.12.0"

# 查看interface的状态,跟port是一一对应的
$ ovs-vsctl list interface veth1_br
...
mac : []
mac_in_use : "32:13:6b:99:91:2b"
mtu : 1500
mtu_request : []
name : "veth1_br"
ofport : 1 # ovs port编号
...

接下来测试一下网络的连通性是没问题的。

1
ip netns exec ns1 ping 192.168.1.200

查看当前流表,可以看到有一条默认的规则,该条规则用来实现交换机的基本动作。

1
2
$ ovs-ofctl dump-flows ovs1
cookie=0x0, duration=1428.955s, table=0, n_packets=22, n_bytes=1676, priority=0 actions=NORMAL

将上述规则删除,再执行ping命令发现已经不通。说明该默认规则会将流量在端口之间进行转发。

1
ovs-ofctl del-flows ovs1

新增加如下两条规则,用来表示将port 1的流量转发到port 3,将port 3的流量转发到port 1。其中的1和3分别为port编号,使用ovs-vsctl list interface veth1_br命令中的ofport可以看到。

1
2
3
4
5
6
ovs-ofctl add-flow ovs1 "priority=1,in_port=1,actions=output:3"
ovs-ofctl add-flow ovs1 "priority=2,in_port=3,actions=output:1"

$ ovs-ofctl dump-flows ovs1
cookie=0x0, duration=69.378s, table=0, n_packets=4, n_bytes=280, priority=1,in_port="veth1_br" actions=output:"veth2_br"
cookie=0x0, duration=69.063s, table=0, n_packets=4, n_bytes=280, priority=2,in_port="veth2_br" actions=output:"veth1_br"

再执行ping命令,发现可以ping通了。

重新增加一条优先级更高的规则,将port 1的数据drop掉。此时再ping发现已经不通了。

1
ovs-ofctl add-flow ovs1 "priority=3,in_port=1,actions=drop"

多table

接下来清理掉规则,并将规则重新写入到table1中,默认规则是写入到table0中的

1
2
3
ovs-ofctl del-flows ovs1
ovs-ofctl add-flow ovs1 "table=1,priority=1,in_port=1,actions=output:3"
ovs-ofctl add-flow ovs1 "table=1,priority=2,in_port=3,actions=output:1"

此时再执行ping命令,发现网络是不通的。因为table0中没有匹配成功,包被drop掉了。

再增加如下规则,即将table 0的规则发送到table 1处理,此时可以ping通。

1
ovs-ofctl add-flow ovs1 "table=0,actions=goto_table=1"

group table

执行ovs-ofctl del-flows ovs1重新清理掉规则,执行下面命令查看group table内容,可以看到内容为空。

1
2
# ovs-ofctl -O OpenFlow13 dump-groups ovs1
OFPST_GROUP_DESC reply (OF1.3) (xid=0x2):

执行如下命令,完成数据包从table0 -> group table -> table1的过程,真正数据处理在table1中。

1
2
3
4
5
6
7
8
9
10
# 创建一个group table,其作用为将数据包发送到table 1
ovs-ofctl add-group ovs1 "group_id=1,type=select,bucket=resubmit(,1)"

# 将port 1和3 的数据发往group table 1
ovs-ofctl add-flow ovs1 "table=0,in_port=1,actions=group:1"
ovs-ofctl add-flow ovs1 "table=0,in_port=3,actions=group:1"

# table 1为真正要处理数据的逻辑
ovs-ofctl add-flow ovs1 "table=1,priority=1,in_port=1,actions=output:3"
ovs-ofctl add-flow ovs1 "table=1,priority=2,in_port=3,actions=output:1"

此时再执行ping命令,发现是可以ping通的。

清理操作

1
2
3
4
5
6
# 删除网桥
ovs-vsctl del-br ovs1
ip link delete veth1_br
ip link delete veth2_br
ip netns del ns1
ip netns del ns2

常用操作

  • ovs-appctl fdb/show ovs1: 查看mac地址表
  • ovs-ofctl show ovs1: 可以查看网桥的端口号
  • ovs-vsctl set bridge ovs1 stp_enable=false: 开启网桥的生成树协议
  • ovs-appctl ofproto/trace ovs1 in_port=1,dl_dst=7a:42:0a:ca:04:65: 可用来验证一个包到达网桥后的处理流程

reference

kube-proxy默认使用iptables规则来做k8s集群内部的负载均衡,本文通过例子来分析创建的iptabels规则。

主要的自定义链涉及到:

  • KUBE-SERVICES: 访问集群内服务的CLusterIP数据包入口,根据匹配到的目标ip+port将数据包分发到相应的KUBE-SVC-xxx链上。一个Service对应一条规则。由OUTPUT链调用。
  • KUBE-NODEPORTS: 用来匹配nodeport端口号,并将规则转发到KUBE-SVC-xxx。一个NodePort类型的Service一条。在KUBE-SERVICES链的最后被调用
  • KUBE-SVC-xxx:相当于是负载均衡,将流量利用random模块均分到KUBE-SEP-xxx链上。
  • KUBE-SEP-xxx:通过dnat规则将连接的目的地址和端口号做dnat,从Service的ClusterIP或者NodePort转换为后端的pod ip
  • KUBE-MARK-MASQ: 使用mark命令,对数据包设置标记0x4000/0x4000。在KUBE-POSTROUTING链上有MARK标记的数据包进行一次MASQUERADE,即SNAT,会用节点ip替换源ip地址。

环境准备

创建nginx deployment

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
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx-svc
version: nginx
name: nginx
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx-svc
template:
metadata:
labels:
app: nginx-svc
version: nginx
spec:
containers:
- image: 'nginx:1.9.0'
name: nginx
ports:
- containerPort: 443
protocol: TCP
- containerPort: 80
protocol: TCP

创建service对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
namespace: default
spec:
ports:
- name: '80'
port: 8000
protocol: TCP
targetPort: 80
nodePort: 30080
selector:
app: nginx-svc
sessionAffinity: None
type: NodePort

环境信息如下:

  • 容器网段:172.20.0.0/16
  • Service ClusterIP cidr: 192.168.0.0/20
  • k8s版本:

提交后创建出来的信息如下:

  • Service ClusterIP:192.168.103.148
  • nginx pod的两个ip地址:172.16.3.3 172.16.4.4

从宿主机上访问ClusterIP

从本机请求ClusterIP的数据包会经过iptables的链:OUTPUT -> POSTROUTING

要想详细知道iptabels的执行情况,可以通过iptables的trace功能。如何开启trace功能可以参考:http://kuring.me/post/iptables/。

image

执行 iptables -nvL OUTPUT -t nat 可以看到如下的iptables规则命令

1
2
pkts bytes target         prot opt in     out     source               destination         
17M 1150M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */

执行 iptables -nvL KUBE-SERVICES -t nat 可以查看自定义链的具体内容,里面包含了多条规则,其中跟当前Service相关的规则如下。

1
2
pkts bytes target                     prot opt in     out     source               destination
1 60 KUBE-SVC-Y5VDFIEGM3DY2PZE tcp -- * * 0.0.0.0/0 192.168.103.148 /* default/nginx-svc:80 cluster IP */ tcp dpt:8000

执行 iptables -nvL KUBE-SVC-Y5VDFIEGM3DY2PZE -t nat 查看自定义链的具体规则

1
2
3
pkts  bytes target                     prot opt in     out     source               destination
0 0 KUBE-SEP-IFV44I3EMZAL3LH3 all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/nginx-svc:80 */ statistic mode random probability 0.50000000000
1 60 KUBE-SEP-6PNQETFAD2JPG53P all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/nginx-svc:80 */

上述规则会按照特定的概率将流量均等的执行自定义链的规则,两个自定义的链的规则跟endpoint相关,执行 iptables -nvL KUBE-SEP-IFV44I3EMZAL3LH3 -t nat可查看endpoint级别的iptabels规则。dnat操作会修改数据包的目的地址和端口,从clusterip+service port修改为访问pod ip+pod端口。

1
2
3
pkts bytes target          prot opt in     out     source               destination
0 0 KUBE-MARK-MASQ all -- * * 172.16.3.3 0.0.0.0/0 /* default/nginx-svc:80 */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/nginx-svc:80 */ tcp to:172.16.3.3:80

会在dnat操作之前为对数据包执行打标签操作。KUBE-MARK-MASQ 自定义链为对数据包打标记的自定义规则,执行 iptables -nvL KUBE-MARK-MASQ -t nat

1
2
pkts bytes target     prot opt in     out     source               destination         
1 60 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000

接下来看一下POSTROUTING链上的规则,iptables -nvL POSTROUTING -t nat

1
2
pkts bytes target            prot opt in     out     source               destination         
205K 13M KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */

继续看一下KUBE-POSTROUTING链的内容,iptables -nvL KUBE-POSTROUTING -t nat,其中最后一条的MASQUERADE指令的操作实际上为SNAT操作。

1
2
3
4
5
Chain KUBE-POSTROUTING (1 references)
pkts bytes target prot opt in out source destination
6499 398K RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 mark match ! 0x4000/0x4000
1 60 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK xor 0x4000
1 60 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */

即从本机访问service clusterip的数据包,在output链上经过了dnat操作,在postrouting链上经过了snat操作后,最终会发往目标pod。pod在处理完请求后,回的数据包最终会经过nat的逆过程返回到本机。

外部访问nodeport

从外部访问本机的nodeport数据包会经过iptables的链:PREROUTING -> FORWARD -> POSTROUTING

image

nodeport都是被外部访问的情况,入口位于PREROUTING链上。执行 iptables -nvL PREROUTING -t nat

1
2
pkts bytes target         prot opt in     out     source               destination         
349K 21M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */

在KUBE-SERVICES链的最后一条规则为跳转到KUBE-NODEPORTS链

1
4079  246K KUBE-NODEPORTS  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

执行iptables -nvL KUBE-NODEPORTS -t nat, 查看KUBE-NODEPORTS链

1
2
3
pkts bytes target                     prot opt in     out     source               destination         
0 0 KUBE-MARK-MASQ tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/nginx-svc:80 */ tcp dpt:30080
0 0 KUBE-SVC-Y5VDFIEGM3DY2PZE tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/nginx-svc:80 */ tcp dpt:30080

其中KUBE-MARK-MASQ链只有一条规则,即打上0x4000的标签。

1
2
pkts bytes target     prot opt in     out     source               destination         
0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000

自定义链KUBE-SVC-Y5VDFIEGM3DY2PZE的内容如下,跟clusterip的规则是重叠的:

1
2
3
pkts bytes target     prot opt in     out     source               destination         
0 0 KUBE-SEP-IFV44I3EMZAL3LH3 all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/nginx-svc:80 */ statistic mode random probability 0.50000000000
0 0 KUBE-SEP-6PNQETFAD2JPG53P all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/nginx-svc:80 */

KUBE-SEP-IFV44I3EMZAL3LH3的内容为,会经过一次DNAT操作:

1
2
3
pkts bytes target          prot opt in     out     source               destination         
0 0 KUBE-MARK-MASQ all -- * * 172.16.3.3 0.0.0.0/0 /* default/nginx-svc:80 */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/nginx-svc:80 */ tcp to:172.16.3.3:80

在经过了PREROUTING链后,接下来会判断目的ip地址不是本机的ip地址,接下来会经过FORWARD链。在FORWARD链上,仅做了一件事情,就是将前面大了0x4000的数据包允许转发。

1
2
3
4
pkts bytes target              prot opt in     out     source               destination         
0 0 KUBE-FORWARD all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding rules */
0 0 KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate NEW /* kubernetes service portals */
0 0 KUBE-EXTERNAL-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate NEW /* kubernetes externally-visible service portals */

KUBE-FORWARD的内容如下:

1
2
3
4
5
pkts bytes target     prot opt in     out     source               destination         
0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate INVALID
0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding rules */ mark match 0x4000/0x4000
0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding conntrack pod source rule */ ctstate RELATED,ESTABLISHED
0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes forwarding conntrack pod destination rule */ ctstate RELATED,ESTABLISHED

跟clusterip一样,会在POSTROUTING阶段匹配mark为0x4000/0x4000的数据包,并进行一次MASQUERADE转换,将ip包替换为宿主上的ip地址。

加入这里不做MASQUERADE,流量发到目的的pod后,pod回包时目的地址为发起端的源地址,而发起端的源地址很可能是在k8s集群外部的,此时pod发回的包是不能回到发起端的。NodePort跟ClusterIP的最大不同就是NodePort的发起端很可能是在集群外部的,从而这里必须做一层SNAT转换。

在上述分析中,访问NodePort类型的Service会经过snat,从而服务端的pod不能获取到正确的客户端ip。可以设置Service的spec.externalTrafficPolicy为Local,此时iptables规则只会将ip包转发给运行在这台宿主机上的pod,而不需要经过snat。pod回包时,直接回复源ip地址即可,此时源ip地址是可达的,因为源ip地址跟宿主机是可达的。如果所在的宿主机上没有pod,那么此时流量就不可以转发,此为限制。

使用LoadBalancer类型访问的情况

externalTrafficPolicy为local

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-A KUBE-SERVICES -d 10.149.30.186/32 -p tcp -m comment --comment "acs-system/nginx-ingress-lb-cloudbiz:http loadbalancer IP" -m tcp --dport 80 -j KUBE-FW-76HLDRT5IPNSMPF5
-A KUBE-FW-76HLDRT5IPNSMPF5 -m comment --comment "acs-system/nginx-ingress-lb-cloudbiz:http loadbalancer IP" -j KUBE-XLB-76HLDRT5IPNSMPF5
-A KUBE-FW-76HLDRT5IPNSMPF5 -m comment --comment "acs-system/nginx-ingress-lb-cloudbiz:http loadbalancer IP" -j KUBE-MARK-DROP

# 10.149.112.0/23为pod网段
-A KUBE-XLB-76HLDRT5IPNSMPF5 -s 10.149.112.0/23 -m comment --comment "Redirect pods trying to reach external loadbalancer VIP to clusterIP" -j KUBE-SVC-76HLDRT5IPNSMPF5
-A KUBE-XLB-76HLDRT5IPNSMPF5 -m comment --comment "Balancing rule 0 for acs-system/nginx-ingress-lb-cloudbiz:http" -j KUBE-SEP-XZXLBWOKJBSJBGVU

-A KUBE-SVC-76HLDRT5IPNSMPF5 -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-XZXLBWOKJBSJBGVU
-A KUBE-SVC-76HLDRT5IPNSMPF5 -j KUBE-SEP-GP4UCOZEF3X7PGLR

-A KUBE-SEP-XZXLBWOKJBSJBGVU -s 10.149.112.45/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-XZXLBWOKJBSJBGVU -p tcp -m tcp -j DNAT --to-destination 10.149.112.45:80
-A KUBE-SEP-GP4UCOZEF3X7PGLR -s 10.149.112.46/32 -j KUBE-MARK-MASQ
-A KUBE-SEP-GP4UCOZEF3X7PGLR -p tcp -m tcp -j DNAT --to-destination 10.149.112.46:80

缺点

  1. iptables规则特别乱,一旦出现问题非常难以排查
  2. 由于iptables规则是串行执行,算法复杂度为O(n),一旦iptables规则多了后,性能将非常差。
  3. iptables规则提供的负载均衡功能非常有限,不支持较为复杂的负载均衡算法。

docker bridge是默认的网络模式

容器访问外网

默认情况下,容器即可访问外网。

启动一个容器:docker run -d nginx

容器中访问外网的请求如www.baidu.com,内核协议栈根据路由信息,会选择默认路由,将请求发送到容器中的eth0网卡,目的mac地址为网关172.17.0.1的mac地址。

1
2
3
4
5
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0

eth0网卡接收到数据包后,会将数据包转发到veth pair的另外一端,即宿主机网络中的veth6b173fd设备。

veth6b173fd设备是挂在网桥上的,会将数据包转发到网桥br0,br0即为网关172.17.0.1。

br0接收到数据包后,会将数据包转发给内核协议栈。

宿主机上的/proc/sys/net/ipv4/ip_forward为1,表示转发功能开启,即目的ip不是本机的会根据路由规则进行转发,而不是丢弃。

仅在宿主机上开启了ip_forward后,包即使转发了,还是无法回来的,因为包中的源ip地址为172.17.0.1,是私有网段的ip地址。需要做一次SNAT才可以,docker会在iptabels的nat表中的postrouting链中增加SNAT规则,下面规则的意思是源地址为172.17.0.0/16的会做一次地址伪装,即SNAT。

1
2
3
4
5
# iptables -nL -t nat

Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0

在eth0网卡上抓包,可以发现源ip已经是eth0的网卡ip地址。

端口映射

命令格式:-p ${host_port}:${container_port}

启动一个容器:docker run -d -p 8080:80 nginx

查看本地的iptables规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ iptables -nL -t nat
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 172.17.0.2 172.17.0.2 tcp dpt:80

Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
## prerouting链引用,外面发往本机的8080端口的数据包,会dnat为172.17.0.2:80
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80

丢包问题

SNAT在并发比较高的情况下,会存在少量的丢包现象,具体原因跟conntrack模式的实现有关。conntrack在SNAT端口的分配和插入conntrack表之间有个延时,如果在这中间存在冲突的话会导致插入失败,从而出现丢包的问题。

该问题没有根治的解决办法,能大大缓解的解决办法为使用iptabels的–random-fully选项,SNAT选择端口为随机,大大降低出现冲突的概率。

ipip协议为在ip协议报文的基础上继续封装ip报文,基于tun设备实现,是一种点对点的通讯技术。

install

ipip需要内核模块ipip的支持

1
2
3
4
5
$ modprobe ipip
$ lsmod | grep ipip
ipip 13465 0
tunnel4 13252 1 ipip
ip_tunnel 25163 1 ipip

实战

两台主机:172.16.5.126(host1)和172.16.5.127(host2)

在host1上创建tun1设备,执行如下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 用来创建tun1设备,并ipip协议的外层ip,目的ip为172.16.5.127, 源ip为172.16.5.126
ip tunnel add tun1 mode ipip remote 172.16.5.127 local 172.16.5.126
# 给tun1设备增加ip地址,并设置tun1设备的对端ip地址为10.10.200.10
ip addr add 10.10.100.10 peer 10.10.200.10 dev tun1
ip link set tun1 up

$ ifconfig tun1
tun1: flags=209<UP,POINTOPOINT,RUNNING,NOARP> mtu 1480
inet 10.10.100.10 netmask 255.255.255.255 destination 10.10.200.10
tunnel txqueuelen 1000 (IPIP Tunnel)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

# 增加一条路由,所有到达10.10.200.10的请求会经过设备tun1
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.16.7.253 0.0.0.0 UG 0 0 0 eth0
10.10.200.10 0.0.0.0 255.255.255.255 UH 0 0 0 tun1
172.16.0.0 0.0.0.0 255.255.248.0 U 0 0 0 eth0

同样在host2上创建tun1设备:

1
2
3
ip tunnel add tun1 mode ipip remote 172.16.5.126 local 172.16.5.127
ip addr add 10.10.200.10 peer 10.10.100.10 dev tun1
ip link set tun1 up

并分别在host1和host2上打开ip_forward功能

1
echo 1 >  /proc/sys/net/ipv4/ip_forward

然后在host1上ping 10.10.200.10,可以ping通。

在host1的tun1上抓包,可以看到正常的ping包。

在host1的eth1上抓包,可以看到已经是ipip的数据包了。

tun1.pcap

清理现场分别在两台主机上执行

1
ip link delete tun0

ref

Linux虚拟网络设备tap/tun

tap/tun常用于隧道通讯,通过一个字符设备来实现用户态和内核态的通讯,字符设备一端连接着用户空间,一端连接着内核空间。

与物理网卡的最大不同是,tap/tun的数据源来自于用户态的程序,而物理网卡的数据源来自于物理链路。

对应的字符设备文件位置:

  • tap: /dev/tap0
  • tun: /dev/net/tun

当应用程序打开字符设备文件时,驱动程序会创建并注册相应的虚拟设备接口,以tunX或tapX命名。应用程序关闭设备文件时,驱动程序会删除tunX和tapX网络虚拟设备,并删除建立起来的路由信息。

两个设备的不同点:

  • tap是一个二层网络设备,只能处理二层的以太网帧,可以与物理网卡做桥接
  • tun是一个点对点的三层网络设备,只能处理处理三层的IP数据包,无法与物理网卡做桥接,可以通过三层交换方式与物理网卡连通。Linux下的隧道协议基于该tun设备实现,如ipip、gre。
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
                ┌──────────────┐
│ │
│ APP │
│ │
└───────┬──────┘






┌────────────▼──────────┐
│ │
─ ─ ─ ─ ─ ─│ /dev/net/tun ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│ │
└────────────┬──────────┘





┌───────▼──────┐ ┌──────────────┐
│ │ │ │
│ tunX ├────────────────▶│Network Stack │
│ │ │ │
└──────────────┘ └──────────────┘

tun设备应用举例

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
          ┌──────────────┐         ┌──────────────┐
│ │ │ │
│ APP A │ │ APP B │◀┐
│ │ │ │ │
└───────┬──────┘ └───────┬──────┘ │
│ │ │
│ │ │
1│ │ │
│ 5│ │
│ │ │
─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ┼ ─ ─ ─ ─ ─
│ │ │
│ │ 4│
│ │ │
┌────────────▼────────────────────────▼─────┐ │
│ │ │
│ Network Stack │ │
│ │ │
└────────────┬───────────────────────┬──────┘ │
│ │ │
6│ 3│ │
│ │ │
┌───────▼──────┐ ┌─▼─────────┴──┐
│ │ │ │
10.1.1.11 │ eth0 │ │ tun0 │ 192.168.1.11
│ │ │ │
└───────┬──────┘ └──────────────┘

7│



10.1.1.100 / 192.168.1.100

应用程序A要发送数据到其他物理机192.168.1.100,由于物理网络环境下只有10.1.1.11和10.1.1.100是相互连通的,192.168.1.11和192.168.1.100是不通的,为了192.168.1.11和192.168.1.100能够进行通讯,需要将数据包进行一次封装。

应用程序B是通过打开字符设备文件/dev/net/tun0的方式来打开网络设备

流程如下:

  1. A构造数据包,目的ip为192.168.1.100,并发送给协议栈
  2. 协议栈根据数据包中的ip地址,匹配路由规则,要从tun0出去
  3. 内核协议栈将数据包发送给tun0网络设备
  4. tun0发送应用程序B打开,于是将数据发送给应用程序B
  5. B收到数据包后,在用户态构造一个新的数据包,源IP为eth0的IP 10.1.1.11,目的IP为配置的对端10.1.1.100,并封装原来的数据包
  6. 协议栈根据当前数据包的IP地址选择路由,将数据包发送给eth0

reference

网卡的混杂模式是指网卡将其接收的所有流量都交给cpu。非混杂模式下,网卡仅接收目的mac地址是自己mac地址的单播,以及多播和广播包,可以看出混杂模式是工作在二层的。

通过ifconfig eth0的方式,如果输出中包含了PROMISC字段,说明网卡处于混杂模式。但是如果ifconfig 命令的输出中未包含PROMISC字段,并不能说明网卡处于非混杂模式下。

可以通过查看 cat /sys/class/net/bond0/flags 的输出得知,如果置位了0x100,说明处于混杂模式。

veth pair是一对虚拟的网络设备,两个网络设备彼此连接。常用于两个network namespace之间的连接,如果在同一个命名空间下有很多的限制。

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
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ network protocol │
│ │
│ │
└────────────────────▲─────────────────────────▲──────────────────────▲────────┘
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
┌─────▼────┐ ┌─────▼────┐ ┌─────▼────┐
│ │ │ │ │ │
│ eth0 │ │ veth0 ◀───────────▶ veth1 │
│ │ │ │ │ │
└─────▲────┘ └──────────┘ └──────────┘







physical network

实战

veth设备的ping测试

1. 只给一个veth设备配置ip的情况测试

给veth0配置ip 192.168.100.10,可以看到主机的路由表中增加了目的地为192.168.100.0的记录

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
[root@localhost vagrant]# ip link add veth0 type veth peer name veth1
[root@localhost vagrant]# ip addr add 192.168.100.10/24 dev veth0
[root@localhost vagrant]# ip addr add 192.168.100.11/24 dev veth1
## 因为veth创建完后默认不启用,此时还没有路由
[root@localhost vagrant]# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default gateway 0.0.0.0 UG 100 0 0 eth0
10.0.2.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
192.168.33.0 0.0.0.0 255.255.255.0 U 101 0 0 eth1

## 启用veth0后增加路由
[root@localhost vagrant]# ip link set veth0 up
[root@localhost vagrant]# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default gateway 0.0.0.0 UG 100 0 0 eth0
10.0.2.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
192.168.33.0 0.0.0.0 255.255.255.0 U 101 0 0 eth1
192.168.100.0 0.0.0.0 255.255.255.0 U 0 0 0 veth0

## 启用veth1后居然又增加了一条路由信息
[root@localhost vagrant]# ip link set veth1 up
[root@localhost vagrant]# ip addr
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
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 52:54:00:26:10:60 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0
valid_lft 86214sec preferred_lft 86214sec
inet6 fe80::5054:ff:fe26:1060/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:98:06:20 brd ff:ff:ff:ff:ff:ff
inet 192.168.33.11/24 brd 192.168.33.255 scope global noprefixroute eth1
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fe98:620/64 scope link
valid_lft forever preferred_lft forever
4: veth1@veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether e2:15:95:0a:1f:da brd ff:ff:ff:ff:ff:ff
inet 192.168.100.11/24 scope global veth1
valid_lft forever preferred_lft forever
inet6 fe80::e015:95ff:fe0a:1fda/64 scope link
valid_lft forever preferred_lft forever
5: veth0@veth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether b2:2c:f6:e4:74:c5 brd ff:ff:ff:ff:ff:ff
inet 192.168.100.10/24 scope global veth0
valid_lft forever preferred_lft forever
inet6 fe80::b02c:f6ff:fee4:74c5/64 scope link
valid_lft forever preferred_lft forever
[root@localhost vagrant]# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default gateway 0.0.0.0 UG 100 0 0 eth0
10.0.2.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
192.168.33.0 0.0.0.0 255.255.255.0 U 101 0 0 eth1
192.168.100.0 0.0.0.0 255.255.255.0 U 0 0 0 veth0
192.168.100.0 0.0.0.0 255.255.255.0 U 0 0 0 veth1

默认情况下arp表如下:

1
2
3
4
5
6
# arp
Address HWtype HWaddress Flags Mask Iface
localhost.localdomain (incomplete) veth0
192.168.33.1 ether 0a:00:27:00:00:00 C eth1
gateway ether 52:54:00:12:35:02 C eth0
10.0.2.3 ether 52:54:00:12:35:03 C eth0

使用ping命令ping -I veth0 192.168.100.11 -c 2,默认情况下veth1和veth0会接收到arp报文,但并没有arp的响应报文。这是因为默认情况下有些arp内核参数的限制。执行如下命令解决arp的限制。

1
2
3
4
5
echo 1 > /proc/sys/net/ipv4/conf/veth1/accept_local
echo 1 > /proc/sys/net/ipv4/conf/veth0/accept_local
echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/veth0/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/veth1/rp_filter

veth pair设备的删除

1
2
# 删除veth0后会自动删除veth1
$ ip link delete veth0

container与host veth pair的关系

veth pair的其中一个设备位于container中备位于container中,另外一个设备位于host network namespace中,如何知道container中的eth0和host network namesapce中的veth设备的对应关系呢?

原理为veth pair设备都有一个ifindex和iflink值,,容器中的eth0设备的ifindex值跟host network namespace中的对应veth pair设备的iflink值相等,反之亦然。

方法一

获取iflink值:cat /sys/class/net/eth0/iflink

也可用此方法获取ifindex值:cat /sys/class/net/eth0/ifindex

方法二

1
2
3
$ ip link show eth0
3: eth0@if18: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 96:5f:80:a3:a3:01 brd ff:ff:ff:ff:ff:ff

其中的3为eth0的ifindex。18为eth0的iflink,即对应的veth pair的另外一个设备的ifindex。

host network namespace中找到对应ifindex值的veth pair设备

1
2
3
4
5
$ ip addr 
18: veth0e09999e@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UP group default
link/ether de:b0:74:89:e8:3e brd ff:ff:ff:ff:ff:ff link-netnsid 4
inet6 fe80::dcb0:74ff:fe89:e83e/64 scope link
valid_lft forever preferred_lft forever

其中的18为ifindex,3为对应的veth pair的ifindex。

reference

距离上次的知识分享系列已经过去了半年之久,难以想象。在该系列的开始我就说过该系列是不定期的,果不食言,只是这次的不定期有些久😓。该系列会继续下去,但节奏仍然是不定期的,但应该不会间隔半年之巨了。

题图来自首钢工业遗址公园,首钢于2010年完成了搬迁到唐山市的工作,位于石景山的工厂被废弃。2019年国庆节前夕以公园的形式部分对外开放,跟普通公园不同的地方在于保留了很多之前的工厂建筑及大型生产机械,足够硬核,非常原汁原味。

2022年要举办的冬奥会也非常明智的将场地选择在了该公园,预计将来会有很多的场馆位于该公园内且对外开放。

国内很多的地市都有一些类似的建筑,比如90年代下岗潮之前的一些国企工厂,我知道的济南的机床厂就有很多个,但很多这些倒闭关门的工厂后来的建筑及地皮都给卖掉了,建筑物也直接给拆掉了,殊不知其衍生价值也是有不少的。位于北京酒仙桥的798就是个非常好的改造案例,798园区的建筑稍加改造,给很多艺术工作室提供了非常好的办公场地,也是城市中的一个亮眼的名片。

资源

1.MessagePack

json作为一种常见的数据序列化方案,存在占用空间过多、反序列化过于消耗cpu的问题。MessagePack是一种基于二进制的高效轻量的数据序列化方案,支持数据的压缩,支持丰富的编程语言。在上图中,可以看出原27字节的json数据转换为MessagePack后仅占用了18字节。

另外,据今日头条的压测,要比Thrift的二进制序列化方案更高效一些。

2.GoAst Viewer

https://raw.githubusercontent.com/yuroyoro/goast-viewer/master/goast-viewer.png

Golang中的ast、parser、token包可用来对golang的源码进行语法分析,并构建出AST树。GoAst Viewer支持在线输入Golang源码来构建AST树。

3.KubeEdge

https://github.com/kubeedge/kubeedge/blob/master/docs/images/kubeedge_arch.png

IoT目前正在大力发展,边缘设备的计算能力在逐渐增强,同时处理的数据量的需求正在快速增加,而数据中心的数据处理能力、网络带宽、扩展能力并没有太多的增强,未来势必会将部分计算能力下放到边缘设备,以降低数据中心的成本。

华为开源的KubeEdge为基于Kubernetes的边缘计算平台,支持边缘集群的编排和管理。

4.Mycat

http://mycat.io/index_files/mycat2.jpg

国内开源的关系型数据库中间件,支持MySQL、Oracle等常见的关系型数据库。关系型数据库单表过大导致性能下降后的解决思路往往是分库分表,分库分表后需要增加中间件层来解决多个数据库多张表的数据增删改查问题,而Mycat是一个不错的解决方案。

当然Mycat的功能不仅限于此,比如支持跨库的两张表join、Mycat-eye可以来监控Mycat等,更多功能请参照官方网站。

5.WeChat Format

一款转换markdown格式的文档为微信公众号排版的工具,排版比较精美,推荐一试。

6.GitNote

https://gitnoteapp.com/imgs/gitnote.png

一款基于git的笔记管理软件,可以将笔记存放到Github中,支持多种图床插件,支持多个平台(还没有移动端),支持富文本编辑和Markdown编辑。由于笔记是可以同步Github上的,可以做到永久保存和版本控制,而且笔记的存放目录和格式不受该工具的影响,可以说是完全没有侵入性,脱离了该工具仍然可以通过直接编辑git项目的方式来发布笔记。

7.Vlang开源啦

V语言宣布开源,从V语言的特性上看到了很多Golang的影子,并未看到耳目一新的特性。

8.krew

一款kubectl的plunin管理工具,mac平台下有brew包管理工具,随着kubectl的plugin机制的成熟,plugin管理工具应运而生。

9.cert-manager

运行在k8s上的证书管理工具,可以签发证书,基于CRD实现。

Let’s Encrypt提供了免费的tls证书,但证书有有效期限制,过期后需要手工重新申请证书,cert-manager可以做到从Let’s Encrypt自动申请证书,并过期后重新申请证书。

10.cmatrix

https://github.com/abishekvashok/cmatrix/raw/master/data/img/capture_orig.gif?raw=true

一款黑客帝国效果的命令行工具,除了炫酷也没啥其他用途了。

11.boxes

boxes为一款有趣的命令行工具,可以显示很多神奇效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
          __   _,--="=--,_   __
/ \." .-. "./ \
/ ,/ _ : : _ \/` \
\ `| /o\ :_: /o\ |\__/
`-'| :="~` _ `~"=: |
\` (_) `/
.-"-. \ | / .-"-.
.---{ }--| /,.-'-.,\ |--{ }---.
) (_)_)_) \_/`~-===-~`\_/ (_(_(_) (
( Different all twisty a )
) of in maze are you, (
( passages little. )
) (
'---------------------------------------'

12.rally

一款elasticsearch的压测工具。

13.tekton

https://tekton.dev/img/logos/tekton-horizontal-color.png

Google开源的一款基于Kubernetes的应用发布框架,Google在云原生生态中出品一般质量都比较高,主要用来做CI/CD。

14.kubectl-debug

一款基于kubectl插件的debug工具,基础镜像使用nicolaka/netshoot(内置了大量的网络排查工具),可用于kubernetes集群中快速定位问题。值得一提的是,该工具的初版是作者在参加pingcap面试时的小作业。

15.Monocular

Rancher出品的一款基于管理helm chart的ui工具。

16.gitmoji

github上的开源项目中经常会看到一些git commit message中包含了moji表情,而且有越来越多的趋势,这些moji表情不紧紧是好玩,而且还非常生动形象的表达了commit message的含义,并且非常醒目,但这些moji表情可不应该是滥用的。该网站记录了一些常用的moji表情在git commit中的含义。

精彩文章

1.Monitoring and Tuning the Linux Networking Stack: Receiving Data

本文讲解了一个数据包到达网卡后是怎么一步步从网卡 -> 操作系统 -> 应用程序,并讲解了Linux中的实现方式。

绝大多数的工程师对于这一块的知识是较为模糊的,建议一读。

视频

https://mp.weixin.qq.com/s?__biz=MzU3OTc1Njk4MQ==&mid=2247486851&idx=1&sn=d0322f6d1a59c21e977488d9701d0476&chksm=fd607b59ca17f24fb07f60b8f488e602ac7e75302c999c206938682387e1d3cac44feb67b208&mpshare=1&scene=1&srcid=%23rd

半年多前比较火的视频,但我还是经常会想起来,给大家重温一下。

视频中为杭州一小伙深夜骑车逆行被交警拦下后情绪崩溃,失声痛哭。小伙每晚加班到十一二点,一方面女朋友在催着给送药匙,另一方面公司还在催着赶回公司,再加上被交警拦下,最终来自三方面的催促导致积压在小伙内心长久以来的压力爆发而情绪失控。隔着屏幕都能感受到小伙长期以来的压力,我猜想如果给他一些自由的时间,他一定会选择独自一人到一个安静的地方过上一段时间与世隔绝的生活。

生活本不易,在觉大多数的成年人生活中没有简单二字,祝愿各位生活如意!

Linux内核会存在一些严重的bug,导致内核crash,会在/var/crash目录下产生类似”127.0.0.1-2019-09-30-21:33:38“这种的文件夹,里面包含了vmcore文件,该文件对于debug 内核crash的原因非常有帮助。

本文在CentOS 7下操作。

执行yum install crash来安装crash

另外还需要两个rpm包:kernel-debuginfo-3.10.0-957.el7.x86_64.rpm 和 kernel-debuginfo-common-x86_64-3.10.0-957.el7.x86_64.rpm,需要关注下操作系统的内核版本,这两个rpm包可以通过搜索引擎找到。

下到包后即可执行rpm -ivh *.rpm的方式来安装rpm包。

在机器上执行crash /usr/lib/debug/lib/modules/3.10.0-957.el7.x86_64/vmlinux /var/crash/xx/vmcore进行debug,可以输入bt命令来查看栈信息。

go mod从1.11开始已经成为了go的默认包管理工具,本文记录go mod的一些使用经验。

要想使用go mod,需要将go升级到1.11或者更高版本。

在没有go mod之前,项目源码必须是放在GOPATH目录下的,有了go mod之后项目即可以放在GOPATH目录下,也可以放在非GOPATH的目录下,在GOPATH目录下在执行时需要指定环境变量GO111MODULE=on,具体的写法可以是GO111MODULE=on go mod init

由于众所周知的原因,go的包相对还是比较难下载的,很多情况下还是需要vendor目录存在的,并将vendor目录中的包一并提交到代码库中。可以使用go mod vendor命令来完成,执行该命令后会将本地下载的包copy到vendor目录下。

坑1 提示unknown revision

1
2
3
4
# GO111MODULE=on go get gitlab.aa-inc.com/bb@v2
go: finding gitlab.aa-inc.com/bb v2
go: finding gitlab.aa-inc.com v2
go get gitlab.aa-inc.com/bb@v2: unknown revision v2

在获取单个包的时候提示unknown revision错误,后发现是go get默认是使用的https协议,而不是git协议,而git仓库的https协议不支持导致的,解决办法为:

1
git config --global url."git@gitlab.aa-inc.com:".insteadOf "https://gitlab.aa-inc.com/"

参考文档

0%