Linux 中的 Tun/Tap 介绍

TUN/TAP 设备

在计算机中TUN与TAP是操作系统内核中的虚拟网络设备。不同于硬件设备这些虚拟的网络设备全部用软件实现,但提供了与硬件设备完全相同的功能。

我们先了解一下物理设备的工作原理

所有主机物理网卡收到的数据包时,会先将其交给内核的 Network Stack 处理,然后通过 Socket API 通知给用户态的用户程序。

Linux 中 Tun/Tap 驱动程序为应用程序提供了两种交互方式:

  • 虚拟网络接口和字符设备/dev/net/tun。写入字符设备/dev/net/tun的数据会发送到虚拟网络接口中;
  • 发送到虚拟网络接口中的数据也会出现在该字符设备上;

我们再看下 tun 设备的工作原理

用户态应用往字符设备 /dev/tunX 写数据时,写入的数据都会出现在TUN虚拟设备上,当内核发送一个包给 TUN 虚拟设备时,通过读这个字符设备 /dev/tunX 同样可以拿到包的内容。

用户态应用程序写数据到 tun/tap 设备后进入内核态,内核态通过TCP协议复制到用户态,最后数据再次复制到内核态并通过物理网卡转发出去,期间共经历了三次用户态与内核态的复制操作,相比传统的一次复制操作来说,开销还是比较大的,因此性能会有一定的下降,这正是它的缺点。

TAP 设备与 TUN 设备工作方式完全相同,区别在于:

  • TUN 设备的 /dev/tunX 文件收发的是 IP 层数据包,只能工作在 IP 层,无法与物理网卡做 bridge,但是可以通过三层交换(如 ip_forward)与物理网卡连通。
  • TAP 设备的 /dev/tapX 文件收发的是 MAC 层数据包,拥有 MAC 层功能,可以与物理网卡做 bridge,支持 MAC 层广播

应用场景

tun/tap 的最主要应用场景就是vpn。
基实现原理就是用到隧道技术,将无法直接发送的包通先封成允许通过的合法数据包,然后经过隧道的方式传递给对方,对方收到数据包再解包成原始数据包,再继续传递下去,直到接收端收到,然后响应并原路返回。
以下图为例

  1. 应用进程(用户态)发起一个请求时,数据包并不是直接通过eth0网卡流出去,而是将请求数据包写入一个 TUN 字符设备,此时字符设备的数据会被发到虚拟网卡上(进入内核态)。根据TUN设置的特点,凡是写到这个设备的数据都可以在设备的另一端被应用程序读出的原理,应用程序客户端VPN(Port:28001)不断的从TUN 设备里将数据包读出来,然后再经过物理的网卡 eth0(IP1) 网卡流出,这一步就是一个普通的应用程序的客户端发起一个请求的过程。
  2. 流出的数据包通过 eth0 (IP2)被服务端VPN(Port:38001)接收到,然后再将收到的数据包以同样的方式写入 TUN 设备,此时进入内核态,经过 TCP/IP 协议栈,则再次将数据包经过物理网卡eth0(IP2)出去,经过交换同机或路由器直到最终到达目的主机(目的主机非本机)。
  3. 然后目的主机将响应按原来的线路返回给发起请求应用程序。

总结

  • 数据包在整个流程中,需要进行一些封包解包的操作, 这个操作由设备驱动完成
  • 如果数据包目的地不是VPN(Port:38001)当前所在主机的话,则需要向数据流向其它机器,此时务必修改IPTABLES的来源地址进行,即需要做SNAT,否则响应数据包将无法原路返回给客户端。

使用方法

我们先介绍一下在 Linux 中是如何对 TUN/TAP 虚拟设备进行操作创建管理和删除的,但对于如何充分利用这些设备必须通过编写程序代码来实现,在后面会给出网友整理出来的演示代码。

命令行输入 ip help 查看 ip 命令是否支持 tun/tap 工具,支持的话就会显示 tuntap 选项:

# ip help
Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }
       ip [ -force ] -batch filename
where  OBJECT := { link | addr | addrlabel | route | rule | neigh | ntable |
                   tunnel | tuntap | maddr | mroute | mrule | monitor | xfrm |
                   netns | l2tp | tcp_metrics | token }

不支持就请升级或下载最新的 iproute2 工具包,也可以使用类似的 tunctl 工具。

先查看一下 ip tuntap 的基本用法

# ip tuntap help
Usage: ip tuntap { add | del | show | list | lst | help } [ dev PHYS_DEV ]
    [ mode { tun | tap } ] [ user USER ] [ group GROUP ]
    [ one_queue ] [ pi ] [ vnet_hdr ] [ multi_queue ] [ name NAME ]

Where:    USER  := { STRING | NUMBER }
    GROUP := { STRING | NUMBER }
  1. 创建 tap/tun 设备:
# ip tuntap add dev tap0 mod tap # 创建 tap 
# ip tuntap add dev tun0 mod tun # 创建 tun

# ifconfig -a

新添加的虚拟网卡默认是 DOWN 状态.
对于 tun 类型的虚拟网卡,它的MAC地址全是 0,这个是正常的

  1. 激活虚拟网卡
# ip link set tun0 up
# ip link set tap0 up

对它的操作与普通网卡的命令是一样的

  1. 分配IP
# ip addr add 10.0.0.1/24 dev tun0

此时 PING 10.0.0.1 是可以通的。

# ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.031 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.036 ms
64 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=0.031 ms
64 bytes from 10.0.0.1: icmp_seq=4 ttl=64 time=0.037 ms
  1. 删除 tap/tun 设备:
# ip tuntap del dev tap0 mod tap # 删除 tap
# ip tuntap del dev tun0 mod tun # 删除 tun

代码演示

上面我们手动创建了虚拟网卡,但没有办法测试网卡的使用效果,下面是一段实现虚拟网卡读取的演示代码,代码摘自:https://segmentfault.com/a/1190000009249039

#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <linux/if_tun.h>
#include<stdlib.h>
#include<stdio.h>

int tun_alloc(int flags)
{

    struct ifreq ifr;
    int fd, err;
    char *clonedev = "/dev/net/tun";

    if ((fd = open(clonedev, O_RDWR)) < 0) {
        return fd;
    }

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = flags;

    if ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) {
        close(fd);
        return err;
    }

    printf("Open tun/tap device: %s for reading...n", ifr.ifr_name);

    return fd;
}

int main()
{

    int tun_fd, nread;
    char buffer[1500];

    /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
     *        IFF_TAP   - TAP device
     *        IFF_NO_PI - Do not provide packet information
     */
    tun_fd = tun_alloc(IFF_TUN | IFF_NO_PI);

    if (tun_fd < 0) {
        perror("Allocating interface");
        exit(1);
    }

    while (1) {
        nread = read(tun_fd, buffer, sizeof(buffer));
        if (nread < 0) {
            perror("Reading from interface");
            close(tun_fd);
            exit(1);
        }

        printf("Read %d bytes from tun/tap devicen", nread);
    }
    return 0;
}

演示

#--------------------------第一个shell窗口----------------------
#将上面的程序保存成tun.c,然后编译
dev@debian:~$ gcc tun.c -o tun

#启动tun程序,程序会创建一个新的tun设备,
#程序会阻塞在这里,等着数据包过来
dev@debian:~$ sudo ./tun
Open tun/tap device tun1 for reading...
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device

#--------------------------第二个shell窗口----------------------
#启动抓包程序,抓经过tun1的包
# tcpdump -i tun1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun1, link-type RAW (Raw IP), capture size 262144 bytes
19:57:13.473101 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 1, length 64
19:57:14.480362 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 2, length 64
19:57:15.488246 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 3, length 64
19:57:16.496241 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 4, length 64

#--------------------------第三个shell窗口----------------------
#./tun启动之后,通过ip link命令就会发现系统多了一个tun设备,
#在我的测试环境中,多出来的设备名称叫tun1,在你的环境中可能叫tun0
#新的设备没有ip,我们先给tun1配上IP地址
dev@debian:~$ sudo ip addr add 192.168.3.11/24 dev tun1

#默认情况下,tun1没有起来,用下面的命令将tun1启动起来
dev@debian:~$ sudo ip link set tun1 up

#尝试ping一下192.168.3.0/24网段的IP,
#根据默认路由,该数据包会走tun1设备,
#由于我们的程序中收到数据包后,啥都没干,相当于把数据包丢弃了,
#所以这里的ping根本收不到返回包,
#但在前两个窗口中可以看到这里发出去的四个icmp echo请求包,
#说明数据包正确的发送到了应用程序里面,只是应用程序没有处理该包
dev@debian:~$ ping -c 4 192.168.3.12
PING 192.168.3.12 (192.168.3.12) 56(84) bytes of data.

--- 192.168.3.12 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3023ms

参考资料

用 Goalng 开发 OPA 策略

Open Policy Agent 简称OPA是一个开源的通用策略引擎,可在整个堆栈中实现统一的、上下文感知的策略实施。OPA 已经成为了云原生计算基金会 (CNCF) 领域的毕业项目,已经在 Kubernetes / Istio 等多个知名项目里使用 。

OPA的核心思想就是策略即代码。

它使用Rego语言开发,Rego 的灵感来自 Datalog,它是一种易于理解、已有数十年的历史的查询语言。Rego 扩展了 Datalog 以支持 JSON 等文档模型。对于它的详细介绍请参考官方文档 https://www.openpolicyagent.org/docs/latest/policy-language/#what-is-rego,这里不再介绍,本方主要介绍如何使用Golang 来开发一个opa策略。

概述

OPA 将 策略决策策略执行 分离,当您的软件需要做出策略决策时,它会查询 OPA 并提供结构化数据(例如 JSON)作为输入。 OPA 接受任意结构化数据作为输入。

对于它的输入一般称为 input, 可以为任意类型,输出也一样可以为任意类型,即可以输出布尔值 truefalse,也可以输出一个 JSON 字符串对象。

Continue reading

一文看懂Golang 定时器源码

计时器分 Timer 和 Ticker 两种,它们底层基本是一样的,两差的区别请参考 https://blog.haohtml.com/archives/19859, 这里我们的介绍对象是 Timer 。

golang timer

计时器结构体

https://github.com/golang/go/blob/go1.17.6/src/time/sleep.go#L84-L98

 // NewTimer creates a new Timer that will send
 // the current time on its channel after at least duration d.
 func NewTimer(d Duration) *Timer {
     c := make(chan Time, 1)
     t := &Timer{
         C: c,
         r: runtimeTimer{
             when: when(d),
             f:    sendTime,
             arg:  c,
         },
     }
     startTimer(&t.r)
     return t
 }

通过调用 NewTimer() 函数创建一个 Timer,首先创建一个长度为1的有缓冲channel,再创建一个Timer的结构体,并将 channel 置于 Timer 结构体内。

注意这里的 runtimeTimer.f 字段是一个函数 sendTime ,其实现如下

func sendTime(c interface{}, seq uintptr) {
	// Non-blocking send of time on c.
	// Used in NewTimer, it cannot block anyway (buffer).
	// Used in NewTicker, dropping sends on the floor is
	// the desired behavior when the reader gets behind,
	// because the sends are periodic.
	select {
	case c.(chan Time) <- Now():
	default:
	}
}

当 sendTime 函数主要用在 newTimer() 时,它以无阻塞的方式将当前时间 Now() 发送到 c 通道里。如果用在 newTicker() 时,如果读取落后,会将发送丢弃,它是周期性的。

我们给出 Timer 的结构体声明。

 type Timer struct {
  C <-chan Time
  r runtimeTimer
 }

一共两个字段,为了理解方面我们称 runtimeTimer 为 timer 值。

我们再看一下其中的 runtimeTimer 结构体的声明

 // Interface to timers implemented in package runtime.
 // Must be in sync with ../runtime/time.go:/^type timer
 type runtimeTimer struct {
  pp       uintptr
  when     int64
  period   int64
  f        func(interface{}, uintptr) // NOTE: must not be closure
  arg      interface{}
  seq      uintptr
  nextwhen int64
  status   uint32
 }

对于 runnerTimer结构体要与在 runtime/time.go 文件中的 timer 结构体保持同步。

结构体字段说明

  • pp 指针类型,这里指 GPM 中的 P。如果这个计时器 timer 在一个heap 上,它在哪个 P 的堆上
  • when 表示唤醒执行的时间,表示什么时间开始执行
  • period 周期,一定是大于 0; when+period 表示下次唤醒执行的时间
  • f 执行函数,不允许为匿名函数,最好为非阻塞函数
  • arg 上面f函数的参数
  • seq 同 arg,其在 runOneTimer 函数中的调用方式为 f(arg, seq)
  • nextwhen 下次运行的时间,其值只有在 timerModifiedXX status 状态下才设置
  • status 状态,其定义的的可用值有10种,定义在 runtime/time.go,我们下面对这些状态进行了介绍。

每次开启一个goroutine 执行 f(arg, now),基中when表示执行的时间,而 when+period 表示下次执行的时间。(这时有点疑问,对调用的函数参数,f的第二个参数是 now, 但后面介绍的时候第二个参数却是 seq)

通过查看 https://github.com/golang/go/blob/go1.17.6/src/runtime/time.go#L41-L116 可知以下几点:

Continue reading

了解eBPF技术

eBPF 的全称“扩展的伯克利数据包过滤器 (Extended Berkeley Packet Filter)” 来看,它是一种数据包过滤技术,是从 BPF (Berkeley Packet Filter) 技术扩展而来的。

BPF 提供了一种在 内核事件 和 用户程序 事件发生时安全注入代码的机制,这就让非内核开发人员也可以对内核进行控制。随着内核的发展,BPF 逐步从最初的数据包过滤扩展到了网络、内核、安全、跟踪等,而且它的功能特性还在快速发展中,这种扩展后的 BPF 被简称为 eBPF(早期的 BPF 被称为经典 BPF,简称 cBPF)。实际上,现代内核所运行的都是 eBPF,如果没有特殊说明,内核和开源社区中提到的 BPF 等同于 eBPF 。

使用场景及分类

根据 eBPF 的功能和使用场景,主要分类三类:

跟踪

从内核和程序的运行状态中提取跟踪信息,来了解当前系统正在发生什么。

跟踪类 eBPF 程序主要用于从系统中提取跟踪信息,进而为监控、排错、性能优化等提供数据支撑。

其中 BCC 工具集中包含的绝大部分工具也都属于这个类型。

网络

对网络数据包进行过滤和处理,以便了解和控制网络数据包的收发过程。

网络类 eBPF 程序主要用于对网络数据包进行过滤和处理,进而实现网络的观测、过滤、流量控制以及性能优化等各种丰富的功能。根据事件触发位置的不同,网络类 eBPF 程序又可以分为 XDP(eXpress Data Path,高速数据路径)程序、TC(Traffic Control,流量控制)程序、套接字程序以及 cgroup 程序。

如著名的开源项目 cilium ,主要用到了ebpf 中的 XDP (eXpress Data Path,高速数据路径)技术。

安全

第三类是除跟踪和网络之外的其他类型,包括安全控制、BPF 扩展等等。

插桩技术

eBPF支持多种事件源,可以在整个软件栈中提供能见度,其实现目前主要通过两种技术,分别为 动态插桩静态插桩,有时候只用其中一种方式是无法实现我们的需求,这时就需要两者相互配合使用了。

动态插桩: kprobes 和 uprobes

动态插桩方式可以做到在程序运行期间,动态的插入观察点,而软件的运行不会受到任何影响,在这点做到了零开销。

kprobes 一般指内核态级别的函数插桩,而 uprobes 则指用户态级别的函数插桩。

动态插桩技术一般需要在 内核函数应用函数开始位置结束位置进行插桩。例如

#!/usr/local/bin/bpftrace

// this program times vfs_read()

kprobe:vfs_read
{
@start[tid] = nsecs;
}

kretbrobe:vfs_read
/@start[tid]/
{
$duration_us = (nsecs - @start[tid]) / 1000;
@us = hist($duration_us);
delete(@start[tid]));
}

这里kprobe:vfs_read 表示是内核函数bfs_read的起始插桩,而kretbroke:vfs_read则是函数的结果插桩,通过对这两个地方分别插桩就可以计算出当前函数的执行时间。

被插桩的函数在整个软件栈中有成千上万个,所以我们可以在任意关注的地方进行插桩,并编写自己的代码实现想要的功能。

动态插桩技术有一点不好的地方就是随着软件版本的迭代变更,被插桩的函数有可能会被重命名或者被移除,这时候会导致一些BPF工具没有办法直接使用,要想继续使用这些BPF工具只能跟着将其进行调整,这个问题十分令人头疼;除此之外还有一个问题就是编译器可能会将一些函数进行 inline 化,这时候会导致这些函数无法使用 kprobesuprobes 动态插桩。

静态插桩: tracepoint 和 USDT

静态插桩会将一些稳定的事件名字编码到软件代码中,由开发者自行维护。BPF跟踪工具支持内核的静态插桩技术,也支持用户态的静态定义跟踪插桩 USDT(user level statically defined tracing)。

不过静态插桩技术也存在一定的问题,那就是会增加开发者的维护成本,同时静态插桩点数量一般很有限。

所有如果我们要开发 BPF工具的话,一般都推荐优先使用静态插桩技术,如果仍无法满足需求的话,再考虑使用动态插桩技(kprobes 或 uprobes)

开发工具

直接通过BPF指令编写BPF程序是件非常繁琐的一件事,因此出现了一些高级语言支持的BPF前端工具,主流开发工具主要是 BCCbpftrade

bcc、bpftrace 与bpf
bcc、bpftrace 与bpf

BCC

BBC(BPF编译器集合,BPF Compiler Collection) 是最早的开发BPF的开发框架。它提供了一个编写内核BPF程序的C语言环境,同时还提供了其它高级语言如Python、Lua 或 C++环境来实现用户端接口,它也是 libbcclibbpf 库的前身,这两个库提供了使用BPF程序对事件进行观测的库函数。

用户可以直接在系统上 安装BCC 即可,不用手动亲自编码代码就可以直接使用自带的一些工具,这些命令的使用场景及用法请参考官方文档 https://github.com/iovisor/bcc/blob/master/docs/tutorial.md

sudo apt-get install bpfcc-tools linux-headers-$(uname -r)

工具集将安装在 /sbin (/usr/sbin in Ubuntu 18.04) 目录,他们的扩展名为 -bpfcc .

$ ls /sbin | grep bpfcc
argdist-bpfcc
bashreadline-bpfcc
bindsnoop-bpfcc
biolatency-bpfcc
biolatpcts-bpfcc
biosnoop-bpfcc
biotop-bpfcc
bitesize-bpfcc
bpflist-bpfcc
btrfsdist-bpfcc
btrfsslower-bpfcc
cachestat-bpfcc
cachetop-bpfcc
capable-bpfcc
cobjnew-bpfcc
compactsnoop-bpfcc
cpudist-bpfcc
cpuunclaimed-bpfcc
criticalstat-bpfcc
dbslower-bpfcc
dbstat-bpfcc
dcsnoop-bpfcc
dcstat-bpfcc
deadlock-bpfcc
dirtop-bpfcc
drsnoop-bpfcc
execsnoop-bpfcc
exitsnoop-bpfcc
ext4dist-bpfcc
ext4slower-bpfcc
filelife-bpfcc
fileslower-bpfcc
filetop-bpfcc
funccount-bpfcc
funcinterval-bpfcc
funclatency-bpfcc
funcslower-bpfcc
gethostlatency-bpfcc
hardirqs-bpfcc
inject-bpfcc
javacalls-bpfcc
javaflow-bpfcc
javagc-bpfcc
javaobjnew-bpfcc
javastat-bpfcc
javathreads-bpfcc
killsnoop-bpfcc
klockstat-bpfcc
llcstat-bpfcc
mdflush-bpfcc
memleak-bpfcc
mountsnoop-bpfcc
mysqld_qslower-bpfcc
netqtop-bpfcc
nfsdist-bpfcc
nfsslower-bpfcc
nodegc-bpfcc
nodestat-bpfcc
offcputime-bpfcc
offwaketime-bpfcc
oomkill-bpfcc
opensnoop-bpfcc
perlcalls-bpfcc
perlflow-bpfcc
perlstat-bpfcc
phpcalls-bpfcc
phpflow-bpfcc
phpstat-bpfcc
pidpersec-bpfcc
profile-bpfcc
pythoncalls-bpfcc
pythonflow-bpfcc
pythongc-bpfcc
pythonstat-bpfcc
readahead-bpfcc
reset-trace-bpfcc
rubycalls-bpfcc
rubyflow-bpfcc
rubygc-bpfcc
rubyobjnew-bpfcc
rubystat-bpfcc
runqlat-bpfcc
runqlen-bpfcc
runqslower-bpfcc
shmsnoop-bpfcc
slabratetop-bpfcc
sofdsnoop-bpfcc
softirqs-bpfcc
solisten-bpfcc
sslsniff-bpfcc
stackcount-bpfcc
statsnoop-bpfcc
swapin-bpfcc
syncsnoop-bpfcc
syscount-bpfcc
tclcalls-bpfcc
tclflow-bpfcc
tclobjnew-bpfcc
tclstat-bpfcc
tcpaccept-bpfcc
tcpconnect-bpfcc
tcpconnlat-bpfcc
tcpdrop-bpfcc
tcplife-bpfcc
tcpretrans-bpfcc
tcprtt-bpfcc
tcpstates-bpfcc
tcpsubnet-bpfcc
tcpsynbl-bpfcc
tcptop-bpfcc
tcptracer-bpfcc
threadsnoop-bpfcc
tplist-bpfcc
trace-bpfcc
ttysnoop-bpfcc
vfscount-bpfcc
vfsstat-bpfcc
wakeuptime-bpfcc
xfsdist-bpfcc
xfsslower-bpfcc
zfsdist-bpfcc
zfsslower-bpfcc

目前大概有120多个命令,可以直接执行这些命令

sudo opensnoop-bpfcc

相关文档

开发教程 https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md
参考指南 https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md

如果要用 BCC 开发其它程序的话,一般是通过  TRACEPOINT_PROBE(category, event)  来定义一个跟踪点处理函数。

对我们要跟踪的短时进程问题来说,也就是下面这两个跟踪点:

// 定义sys_enter_execve跟踪点处理函数.
TRACEPOINT_PROBE(syscalls, sys_enter_execve)
{
    //待添加处理逻辑
}

// 定义sys_exit_execve跟踪点处理函数.
TRACEPOINT_PROBE(syscalls, sys_exit_execve)
{
    //待添加处理逻辑
}

其中 syscalls 是分类,而 sys_enter_execve 是跟踪点。它其实就是下面即将介绍的 bpftrace 中的 tracepoint:syscalls:sys_enter_execve 跟踪点。

bpftrace

bpftrace是一个新兴的前端,其源代码非常简洁,它同样也是基于 libbcclibbpf 库构建的。

bpftrace 原理

bpftrace 会把你开发的脚本借助 BCC 编译加载到内核中执行,再通过 BPF 映射获取执行的结果。

对于 bpftrace 安装很简单

# Ubuntu 19.04
sudo apt-get install -y bpftrace

# RHEL8/CentOS8
sudo dnf install -y bpftrace

安装好 bpftrace 之后,你就可以执行  bpftrace -l  来查询 内核插桩跟踪点

# 查询所有 内核插桩 和 跟踪点
sudo bpftrace -l

# 使用通配符查询所有的系统调用 跟踪点
sudo bpftrace -l 'tracepoint:syscalls:*'

# 使用通配符查询所有名字包含"execve"的跟踪点
sudo bpftrace -l '*execve*'

对于 跟踪点 来说,你还可以加上  -v  参数查询函数的入口参数或返回值。

# 查询execve入口参数格式
$ sudo bpftrace -lv tracepoint:syscalls:sys_enter_execve
tracepoint:syscalls:sys_enter_execve
    int __syscall_nr
    const char * filename
    const char *const * argv
    const char *const * envp

# 查询execve返回值格式
$ sudo bpftrace -lv tracepoint:syscalls:sys_exit_execve
tracepoint:syscalls:sys_exit_execve
    int __syscall_nr
    long ret

而由于内核函数属于不稳定的 API,在 bpftrace 中只能通过  arg0、arg1  这样的参数来访问,具体的参数格式还需要参考内核源代码( https://www.kernel.org/ ),注意内核的版本号差异。

相关文档

bpftrace一行教程 https://github.com/iovisor/bpftrace/blob/master/docs/developers.md
参考指南 https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md

对比

BCC 与 bpftrace 两者具有互补性,bpftrace在编写功能强大的单行程序或短小的脚本方便十分理解;BCC则更适合开发大型复杂的脚本和作为后台进程使用,同时它还可以调用其它的库。比如目前很多用Python来开发BCC程序,它们使用Python的 argparse 库来提供复杂的命令行参数运行。

BCC 和 bpftrace 它们并不属于内核代码仓库的项目,而是托管在 Github 上一个名为 IO Visor的Linux基金会。

推荐阅读:https://www.brendangregg.com/ebpf.html

参考资料

Golang常见编译参数

在执行 go build 命令的时候,经常需要添加一些参数,或许是为了调试,也或许是为了生成最终部署二进制文件。

在编译特定包时需要传递参数,格式应遵守“包名=参数列表”,如

go build -gcflags -gcflags='log=-N -l' main.go

-gcflags

go build 可以用 -gcflagsgo编译器传入参数,也就是传给 go tool compile 的参数,因此可以用 go tool compile –help 查看所有可用的参数。

其中 -m 可以检查代码的编译优化情况,包括逃逸情况和函数是否内联。

-ldflags

go build用 -ldflags 给go链接器传入参数,实际是给go tool link的参数,可以用go tool link –help查看可用的参数。

常用-X来指定版本号等编译时才决定的参数值。例如代码中定义var buildVer string,然后在编译时用go build -ldflags “-X main.buildVer=1.0” … 来赋值。注意-X只能给string类型变量赋值。


Golang中的 CGO_ENABLED 环境变量

Golang中的编译参数

开发中经常使用 go build 命令来编译我们的程序源码,然后将生成二进制文件直接部署,极其方便。

对于 go build 有一些参数,对于针对程序源码进行一些编译优化,下面我们对经常使用的一些参数来介绍一下。

环境变量

环境变量需要在go命令前面设置,如果多个变量的话,中间需要用“空格”分隔。下面我们介绍一个非常常见到的一些环境变量

$ CGO_ENABLED=1 GOARCH=amd64 GOOS=linux go build -o myserver main.go

除了这里给出的这几个变量外,还有一些其它变量,如 GODEBUG、GOFLAGS、GOPROXY 等,所有支持环境变量都可以在 https://github.com/golang/go/blob/a88575d662a7e8e4fbb31bf139bcffc063e2a734/src/cmd/go/internal/help/helpdoc.go#L485 里找到,有兴趣的话可以看看他们的作用。

这里重点介绍一下 CGO_ENABLED 环境变量对我们程序的影响。 CGO_ENABLED是用来控制golang 编译期间是否支持调用 cgo 命令的开关,其值为1或0,默认情况下值为1,可以用 go env 查看默认值。

如果你的程序里调用了cgo 命令,此参数必须设置为1,否则将编译时出错。这里直接用文档 https://go.dev/blog/cgo 中的一个例子验证。

package main

// #include <stdio.h>
// #include <stdlib.h>
//
// static void myprint(char* s) {
//   printf("%sn", s);
// }
import "C"
import "unsafe"

func main() {
	cs := C.CString("Hello from stdio")
	C.myprint(cs)
	C.free(unsafe.Pointer(cs))
}

然后我们执行一下 启用 CGO_ENABLED=1 的情况

root@ubuntu:/home/sxf/gotest# CGO_ENABLED=1 go run main.go
root@ubuntu:/home/sxf/gotest# ./main
Hello from stdio

可以看到输出正常,这里也可以省略不写变量,因为默认情况为启用CGO状态

启用 CGO_ENABLED=0 的情况

root@ubuntu:/home/sxf/gotest# CGO_ENABLED=0 go build main.go
go: no Go source files

可以看到编译失败,验证了我们上面说的情况。

这里提示找不到go源文件,不清楚底层是如何判断这一情况的。

那么,如果我们一个程序里未调用cgo,在编译时指定 CGO_ENABLED 不同值话,又会发生什么呢?编译的二进制有何区别呢?

root@ubuntu:/home/sxf/gotest# CGO_ENABLED=1 go build -o cgo_main main.go
root@ubuntu:/home/sxf/gotest# CGO_ENABLED=0 go build -o no_cgo_main main.go
root@ubuntu:/home/sxf/gotest# ls -al
total 3804
drwxrwxr-x  2 sxf  sxf     4096 Nov 25 10:22 .
drwxr-x--- 47 sxf  sxf     4096 Nov 19 12:48 ..
-rwxr-xr-x  1 root root 1937311 Nov 25 10:22 cgo_main
-rw-rw-r--  1 sxf  sxf      119 Oct 29 09:07 go.mod
-rw-rw-r--  1 sxf  sxf      241 Oct 29 09:07 go.sum
-rw-rw-r--  1 sxf  sxf       72 Nov 25 10:21 main.go
-rwxr-xr-x  1 root root 1937311 Nov 25 10:22 no_cgo_main

可以看到两种情况都可能编译成功,且两者生成的二进制文件是完全一样的。由此总结出可以看出如果程序里未调用 cgo 的话,此变量值并没有影响的。

那么问题来了,为什么这么多项目里编译的时候都明确指定了此环境变量的值呢,主要是编译器在编译时会根据不同的情况使用不同的编译方法。 当CGO_ENABLED=1,进行编译时会将文件中引用libc的库(比如常用的net包),以动态链接的方式生成目标文件。 当CGO_ENABLED=0,进行编译时则会把在目标文件中未定义的符号(外部函数)一起链接到可执行文件中。

不论哪种方式,都可以使用静态连接编译

参考

理解 firewalld/ufw 与iptables、netfilter 的关系

iptables 作为 Linux/Unix 下一款优秀的防火墙软件,在安全方面发挥着极其重要的作用,作为系统管理员来讲一点也不陌生。不过对于一些新手来说,复杂性是一个门槛,Linux厂商为了解决这个问题,于是推出了新的管理工具,如 Centos 下的 Firewalld 和 Ubuntu 下的ufw, 他们对新手十分友好,只需要几个很简单的命令即可实现想要的功能,再不也必为记不住iptables中的四表五键而烦恼了。
那么,是不是有了 firewalld 和 ufw就不需要iptables了呢?并不是的。

首先我们要清楚firewalld、ufw 与iptables的关系,可以理解为两者只是对iptables其进行了一层封装,它们在用户交互方面做了非常多的改进,使其对用户更加友好,不需要再记住原来那么多命令了。

而目前对于一些系统管理员来讲,大概率还是会直接使用 iptables,主要原因是灵活性,当然也有一定的历史原因。对比前面两个管理工具,他们也存在一定的问题,如只能对单条规则进行管理,详细参考相关文档。


另外对于 firewalld 还有图形界面。


除了这三个还有一个 netfilter 的东西,它又是什么呢?

firewalld/ufw 自身并不具备防火墙的功能,而是和 iptables 一样需要通过内核的 netfilter 来实现,也就是说 firewalld 和 iptables 一样,他们的作用都是用于维护规则,而真正使用规则干活的是内核的netfilter。所以iptables服务和firewalld服务都不是真正的防火墙,只是用来定义防火墙规则功能的管理工具,通过iptables将定义好的规则交给内核中的netfilter(网络过滤器来读取)从而实现真正的防火墙功能。不过由于用户一般操作的都是iptables,所以称其为防火墙也并没有什么不妥的。

总结一下,Netfilter/Iptables 是Linux系统自带的防火墙,Iptables管理规则,Netfilter则是规则的执行者,它们一起实现Linux下安全防护。

以上就是他们四者的关系。

参考资料

https://www.cnblogs.com/kevingrace/p/6265113.html

利用代理拉取docker镜像

在日常开发中经常会遇到有些镜像在 gcr.io 仓库,其仓库是google提供的,由于国内网络环境的复杂性是无法拉取到这些镜像的,这时候就需要我们想一些办法来实现拉取了。

这里给出了两种解决方法,一种是直接使用代理,这种可以直接拉取远程镜像到本地。另一种是通过中转的方法,先找一个可以直接拉取到镜像的网络,先将存储到本地,然后再转镜像上传到三方国内可以访问的镜像,如我们最常用镜像 hub.docker.com。

代理方法

使用代理方法的时候,如果通过直接设置 http_proxy 和 https_proxy 这两个环境变量是不可行的。主要原因是 docker 并不会使用它们,需要为 docker daemon 服务的设置代理才可以。

设置docker服务代理

sudo mkdir -p /etc/systemd/system/docker.service.d/
sudo vim /etc/systemd/system/docker.service.d/http-proxy.conf

将以下内容写入 http-proxy.conf 文件

[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="ALL_PROXY=socks5://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp,.docker.io,.docker.com"

上面代理地址是本机开启的代理服务监听端口,如果代理服务在局域网内的其它机器上的话,需要更换为其 ip 地址和端口号。环境变量 NO_PROXY 表示不使用代理的域名或IP。

重启 docker 服务

root@ubuntu:~# systemctl daemon-reload
root@ubuntu:~# systemctl restart docker

验证设置

root@ubuntu:~# systemctl show --property=Environment docker
Environment=HTTP_PROXY=http://127.0.0.1:7890 HTTPS_PROXY=http://127.0.0.1:7890 ALL_PROXY=socks5://127.0.0.1:7890 NO_PROXY=localhost,127.0.0.1,docker-registry.example.com,.corp,.docker.io,.docker.com

也可以使用命令 docker info 验证

root@ubuntu:~# docker info
...
 Debug Mode: false
 HTTP Proxy: http://127.0.0.1:7890
 HTTPS Proxy: http://127.0.0.1:7890
 No Proxy: localhost,127.0.0.1,docker-registry.example.com,.corp,.docker.io,.docker.com
...

拉取镜像

root@ubuntu:~# docker pull gcr.io/google_containers/pause-amd64:3.0
3.0: Pulling from google_containers/pause-amd64
a3ed95caeb02: Pull complete 
f11233434377: Pull complete 
Digest: sha256:163ac025575b775d1c0f9bf0bdd0f086883171eb475b5068e7defa4ca9e76516
Status: Downloaded newer image for gcr.io/google_containers/pause-amd64:3.0
gcr.io/google_containers/pause-amd64:3.0

此时使用命令 docker images 命令查看,会看到镜像下载成功,大小为747K。

中转方法

对于中转方法这里推荐使用 https://labs.play-with-docker.com,国内用户可以直接访问。方法比较简单,这里只给出几个用到的命令,不再详细介绍。

首先拉取镜像到本地,然后用docker tag 命令修改标签,并上传到 hub.docker.com 的个人账号仓库下

# docker pull gcr.io/google_containers/pause-amd64:3.0
# docker tag gcr.io/google_containers/pause-amd64:3.0 cfanbo/gcr.io_google_containers_pause-amd64:3.0 
# 登录 hub.docker.log
# docker login
# docker push cfanbo/gcr.io_google_containers_pause-amd64:3.0

本地从 hub.docker.com 拉取镜像, 再改为原来的名称+标签名

# docker pull cfanbo/gcr.io_google_containers_pause-amd64:3.0
# docker tag cfanbo/gcr.io_google_containers_pause-amd64:3.0 gcr.io/google_containers/pause-amd64:3.0

此时镜像已经拉取成功,当前本地存在两个完全一样的镜像,可以将原来使用的临时中转镜像删除。

参考文档

k8s解决证书过期问题

在k8s中的时间会提示证书过期问题,如

# kubectl get nodes
Unable to connect to the server: x509: certificate has expired or is not yet valid

这里我们介绍一下续期方法。

注意:当前集群通过 kubeadm 命令创建。

kubeadm 安装得证书默认为 1 年,注意原证书文件必须保留在服务器上才能做延期操作,否则就会重新生成,集群可能无法恢复

准备

这里先查看一下测试集群的证书过期时间

# kubeadm certs check-expiration
[check-expiration] Reading configuration from the cluster...
[check-expiration] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Aug 30, 2022 03:18 UTC   324d                                    no      
apiserver                  Aug 30, 2022 03:18 UTC   324d            ca                      no      
apiserver-etcd-client      Aug 30, 2022 03:18 UTC   324d            etcd-ca                 no      
apiserver-kubelet-client   Aug 30, 2022 03:18 UTC   324d            ca                      no      
controller-manager.conf    Aug 30, 2022 03:18 UTC   324d                                    no      
etcd-healthcheck-client    Aug 30, 2022 03:18 UTC   324d            etcd-ca                 no      
etcd-peer                  Aug 30, 2022 03:18 UTC   324d            etcd-ca                 no      
etcd-server                Aug 30, 2022 03:18 UTC   324d            etcd-ca                 no      
front-proxy-client         Aug 30, 2022 03:18 UTC   324d            front-proxy-ca          no      
scheduler.conf             Aug 30, 2022 03:18 UTC   324d                                    no      

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Aug 28, 2031 03:18 UTC   9y              no      
etcd-ca                 Aug 28, 2031 03:18 UTC   9y              no      
front-proxy-ca          Aug 28, 2031 03:18 UTC   9y              no  

可以看到过期时间为 2022-08-30。

Continue reading

istio在虚拟机vm下的安装方法

建议参考官方文档 https://istio.io/latest/zh/docs/setup/install/virtual-machine/ ,这里提醒大家对于命令中文版部分命令与英文版不一致,请以 英文版 为准。

对于istio在vm上的安装教程主要分为三部分。首先是在k8s的master节点生成vm连接主节点的一些配置信息,其实是在vm上应用这些配置信息,最后也就是验证连接是否成功。

本篇主要介绍“单网络”的情况, 对于”多网络“请自行参考官方文档。

vm环境准备

生成vm通讯配置信息

这里主要介绍一些新手迷惑的部分。如环境变量设置及vm注册的方式

设置环境变量

在设置变量时,对于”单网络“来讲 CLUSTER_NETWORK 和 VM_NETWORK 保留空值即可。如我这里设置如下

$ VM_APP="myapp"
$ VM_NAMESPACE="vm"
$ WORK_DIR="/root/myapp"
$ SERVICE_ACCOUNT="vm-sa"
$ CLUSTER_NETWORK=""
$ VM_NETWORK=""
$ CLUSTER="Kubernetes"

每个环境变量的解释:
VM_APP 表示vm上应用的名称
VM_NAMESPACE 表示应用所在的namespace
WORK_DIR 生成vm配置信息保留的目录,任何位置即可
SERVICE_ACCOUNT 服务运行的账号 ,即yaml文件中的 ServiceAccount 字段
CLUSTER 集群名称,默认为 Kubernetes 即可。

Continue reading