背景

最近要上线一个应用,需要修改时间相关的配置

照常打包上线后,在容器内使用date -s 2077-08-02修改时间

发现宿主机的TLS握手失败,宿主机的时间也被修改了

这个时候才想起容器是共享宿主机内核的,并且是共享同一个内核时钟的

我们起个Debian容器实验下(需要开privileged模式

1
docker run -d --name debian debian:12-slim --priviliage sleep infinity

k8s的Pod也是类似的,不过我们这里为了方便就用Docker了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
  name: debian-privileged-pod
spec:
  containers:
  - name: debian-container
    image: debian:12-slim
    securityContext:
      # 设置为 true,等同于 Docker 的 --privileged 标志
      privileged: true
    command: ["sleep"]
    args: ["infinity"]

先前已经校准了时间,现在我在容器内执行

1
date -s 2077-08-02

发现连宿主机的时间也被修改了

简单的eBPF程序

在这里,我简单写个eBPF程序来监控容器内的时间修改操作

先装好ebpf相关软件包

1
sudo apt-get install -y linux-headers-$(uname -r) libbpf-dev clang llvm bpftool
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用 clang 编译 eBPF 程序
CC = clang
# 编译选项
CFLAGS = -g -O2 -Wall

all: vmlinux.h tracer.o monitor

# 生成 vmlinux.h 文件
vmlinux.h:
        sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

# 编译 eBPF 内核对象文件
tracer.o: tracer.c vmlinux.h
        $(CC) $(CFLAGS) -target bpf -D__TARGET_ARCH_x86 -I. -c tracer.c -o tracer.o

# 编译用户空间程序
monitor: monitor.c
        gcc $(CFLAGS) -I. -o monitor monitor.c -lbpf

clean:
        rm -f tracer.o monitor vmlinux.h

.PHONY: all clean
  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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// monitor.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

// 事件结构体 - 必须与tracer.c中的定义一致
struct event {
    __u64 timestamp;
    __u32 pid;
    char comm[16];
    char function[32];
    __s64 time_sec;
    __s32 ret_code;
    __u8 event_type;
    __s32 cap_num;
};

static volatile int running = 1;

void signal_handler(int sig) {
    running = 0;
}

// capability编号到名称的映射
const char* cap_to_string(int cap) {
    switch (cap) {
        case 0: return "CAP_CHOWN";
        case 1: return "CAP_DAC_OVERRIDE";
        case 2: return "CAP_DAC_READ_SEARCH";
        case 3: return "CAP_FOWNER";
        case 4: return "CAP_FSETID";
        case 5: return "CAP_KILL";
        case 6: return "CAP_SETGID";
        case 7: return "CAP_SETUID";
        case 8: return "CAP_SETPCAP";
        case 9: return "CAP_LINUX_IMMUTABLE";
        case 10: return "CAP_NET_BIND_SERVICE";
        case 11: return "CAP_NET_BROADCAST";
        case 12: return "CAP_NET_ADMIN";
        case 13: return "CAP_NET_RAW";
        case 14: return "CAP_IPC_LOCK";
        case 15: return "CAP_IPC_OWNER";
        case 16: return "CAP_SYS_MODULE";
        case 17: return "CAP_SYS_RAWIO";
        case 18: return "CAP_SYS_CHROOT";
        case 19: return "CAP_SYS_PTRACE";
        case 20: return "CAP_SYS_PACCT";
        case 21: return "CAP_SYS_ADMIN";
        case 22: return "CAP_SYS_BOOT";
        case 23: return "CAP_SYS_NICE";
        case 24: return "CAP_SYS_RESOURCE";
        case 25: return "CAP_SYS_TIME";
        case 26: return "CAP_SYS_TTY_CONFIG";
        case 27: return "CAP_MKNOD";
        case 28: return "CAP_LEASE";
        case 29: return "CAP_AUDIT_WRITE";
        case 30: return "CAP_AUDIT_CONTROL";
        case 31: return "CAP_SETFCAP";
        case 32: return "CAP_MAC_OVERRIDE";
        case 33: return "CAP_MAC_ADMIN";
        case 34: return "CAP_SYSLOG";
        case 35: return "CAP_WAKE_ALARM";
        case 36: return "CAP_BLOCK_SUSPEND";
        case 37: return "CAP_AUDIT_READ";
        case 38: return "CAP_PERFMON";
        case 39: return "CAP_BPF";
        case 40: return "CAP_CHECKPOINT_RESTORE";
        default: return "UNKNOWN_CAP";
    }
}

// 格式化时间戳
void format_timestamp(__u64 timestamp, char *buf, size_t size) {
    time_t seconds = timestamp / 1000000000;
    __u64 nanoseconds = timestamp % 1000000000;
    struct tm *tm_info = localtime(&seconds);
    
    snprintf(buf, size, "%02d:%02d:%02d.%03llu",
             tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec,
             nanoseconds / 1000000);
}

// 事件处理回调函数
static int handle_event(void *ctx, void *data, size_t data_sz) {
    struct event *e = data;
    char timestamp_str[32];
    
    format_timestamp(e->timestamp, timestamp_str, sizeof(timestamp_str));
    
    printf("[%s] PID=%-6d COMM=%-15s FUNC=%-25s ",
           timestamp_str, e->pid, e->comm, e->function);
    
    switch (e->event_type) {
        case 0: // 系统调用入口
            printf("ENTER");
            if (e->time_sec > 0) {
                time_t new_time = e->time_sec;
                struct tm *tm_info = localtime(&new_time);
                printf(" time=%lld (%04d-%02d-%02d %02d:%02d:%02d)",
                       (long long)e->time_sec,
                       tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday,
                       tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec);
            }
            break;
            
        case 1: // 系统调用出口
            printf("EXIT ret=%d", e->ret_code);
            if (e->ret_code == 0) {
                printf(" SUCCESS");
            } else {
                printf(" FAILED");
            }
            break;
            
        case 2: // capability检查
            printf("CAP_CHECK cap=%d (%s)", e->cap_num, cap_to_string(e->cap_num));
            break;
            
        default:
            printf("UNKNOWN_EVENT_TYPE=%d", e->event_type);
            break;
    }
    
    printf("\n");
    return 0;
}

int main(int argc, char **argv) {
    struct bpf_object *obj = NULL;
    struct bpf_map *map;
    struct ring_buffer *rb = NULL;
    struct bpf_link **links = NULL;
    int link_count = 0;
    int err;
    
    // 设置信号处理器
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
    
    // 设置libbpf错误输出
    libbpf_set_print(NULL);
    
    // 打开BPF对象文件
    obj = bpf_object__open_file("tracer.o", NULL);
    if (libbpf_get_error(obj)) {
        fprintf(stderr, "ERROR: Failed to open BPF object file\n");
        return 1;
    }
    
    // 加载BPF程序
    err = bpf_object__load(obj);
    if (err) {
        fprintf(stderr, "ERROR: Failed to load BPF object: %d\n", err);
        goto cleanup;
    }
    
    // 计算程序数量并分配links数组
    struct bpf_program *prog;
    bpf_object__for_each_program(prog, obj) {
        link_count++;
    }
    
    links = calloc(link_count, sizeof(struct bpf_link *));
    if (!links) {
        fprintf(stderr, "ERROR: Failed to allocate memory for links\n");
        err = -ENOMEM;
        goto cleanup;
    }
    
    // 修复:正确处理bpf_program__attach返回的指针
    int i = 0;
    bpf_object__for_each_program(prog, obj) {
        links[i] = bpf_program__attach(prog);
        if (libbpf_get_error(links[i])) {
            fprintf(stderr, "ERROR: Failed to attach program %s\n", 
                    bpf_program__name(prog));
            links[i] = NULL;
            err = -1;
            goto cleanup;
        }
        i++;
    }
    
    // 查找events map
    map = bpf_object__find_map_by_name(obj, "events");
    if (!map) {
        fprintf(stderr, "ERROR: Failed to find events map\n");
        err = -1;
        goto cleanup;
    }
    
    // 创建ring buffer
    rb = ring_buffer__new(bpf_map__fd(map), handle_event, NULL, NULL);
    if (!rb) {
        fprintf(stderr, "ERROR: Failed to create ring buffer\n");
        err = -1;
        goto cleanup;
    }
    
    printf("Kernel Time & Capability Monitor started. Press Ctrl+C to exit.\n");
    printf("Monitoring settimeofday and capability checks...\n");
    printf("================================================================================\n");
    
    // 主事件循环
    while (running) {
        err = ring_buffer__poll(rb, 100); // 100ms timeout
        if (err == -EINTR) {
            break;
        }
        if (err < 0) {
            fprintf(stderr, "Error polling ring buffer: %d\n", err);
            break;
        }
    }
    
cleanup:
    // 清理links
    if (links) {
        for (int i = 0; i < link_count; i++) {
            if (links[i]) {
                bpf_link__destroy(links[i]);
            }
        }
        free(links);
    }
    
    if (rb) ring_buffer__free(rb);
    if (obj) bpf_object__close(obj);
    printf("\nMonitor stopped.\n");
    return err < 0 ? 1 : 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
 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
// tracer.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

// 手动定义需要的结构体
struct user_timeval {
    __s64 tv_sec;
    __s64 tv_usec;
};

// 事件数据结构
struct event {
    __u64 timestamp;
    __u32 pid;
    char comm[16];
    char function[32];
    __s64 time_sec;
    __s32 ret_code;
    __u8 event_type;
    __s32 cap_num;
};

// Ring buffer
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

// 复制字符串的辅助函数
static __always_inline void copy_string(char *dst, const char *src, int max_len) {
    #pragma unroll
    for (int i = 0; i < max_len - 1; i++) {
        dst[i] = src[i];
        if (src[i] == '\0') break;
    }
    dst[max_len - 1] = '\0';
}

// settimeofday 入口探针
SEC("tracepoint/syscalls/sys_enter_settimeofday")
int trace_settimeofday_enter(struct trace_event_raw_sys_enter *ctx)
{
    struct event *e;
    __u64 pid_tgid = bpf_get_current_pid_tgid();
    __u32 pid = pid_tgid >> 32;
    
    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    
    e->timestamp = bpf_ktime_get_ns();
    e->pid = pid;
    e->event_type = 0;
    e->ret_code = 0;
    e->cap_num = 0;
    e->time_sec = 0;
    
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    copy_string(e->function, "sys_settimeofday", sizeof(e->function));
    
    // 读取用户空间的timeval结构
    if (ctx->args[0]) {
        struct user_timeval tv;
        if (bpf_probe_read_user(&tv, sizeof(tv), (void*)ctx->args[0]) == 0) {
            e->time_sec = tv.tv_sec;
        }
    }
    
    bpf_ringbuf_submit(e, 0);
    return 0;
}

// settimeofday 出口探针
SEC("tracepoint/syscalls/sys_exit_settimeofday")
int trace_settimeofday_exit(struct trace_event_raw_sys_exit *ctx)
{
    struct event *e;
    __u64 pid_tgid = bpf_get_current_pid_tgid();
    __u32 pid = pid_tgid >> 32;
    
    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    
    e->timestamp = bpf_ktime_get_ns();
    e->pid = pid;
    e->event_type = 1;
    e->ret_code = ctx->ret;
    e->time_sec = 0;
    e->cap_num = 0;
    
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    copy_string(e->function, "sys_settimeofday_exit", sizeof(e->function));
    
    bpf_ringbuf_submit(e, 0);
    return 0;
}

// capability检查探针
SEC("kprobe/capable")
int trace_capable(struct pt_regs *ctx)
{
    struct event *e;
    __u64 pid_tgid = bpf_get_current_pid_tgid();
    __u32 pid = pid_tgid >> 32;
    int cap;
    
    cap = (int)PT_REGS_PARM1(ctx);
    
    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    
    e->timestamp = bpf_ktime_get_ns();
    e->pid = pid;
    e->event_type = 2;
    e->ret_code = 0;
    e->time_sec = 0;
    e->cap_num = cap;
    
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    copy_string(e->function, "capable_check", sizeof(e->function));
    
    bpf_ringbuf_submit(e, 0);
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

我的ebpf程序主要干了几件事

  1. 监控 settimeofday 系统调用 通过 tracepoint 追踪 sys_enter_settimeofdaysys_exit_settimeofday,捕获所有修改系统时间的操作(比如我用到的 date -s)。

  2. 监控 capable 权限检查 通过 kprobe 追踪内核的 capable 函数,捕获所有进程在请求 Linux capability 权限时的情况,比如 CAP_SYS_ADMIN、CAP_SYS_TIME 等。

代码主要分为两个部分:内核空间的 tracer.c 和用户空间的 monitor.c

tracer.c 负责在内核态收集时间并且通过 ring buffer 发送事件到用户空间。

monitor.c 就在用户态接收这些事件并打印出来。

make编译后运行

1
2
3
sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
make
sudo ./monitor

这里我们在Debian容器执行命令来修改时间

1
docker run --rm -it debian date -s "2077-01-01"

输入如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ ./monitor
Kernel Time & Capability Monitor started. Press Ctrl+C to exit.
Monitoring settimeofday and capability checks...
================================================================================
[08:14:17.552] PID=6306   COMM=monitor         FUNC=capable_check         CAP_CHECK cap=39 (CAP_BPF)
[08:14:20.498] PID=971    COMM=dockerd         FUNC=capable_check         CAP_CHECK cap=21 (CAP_SYS_ADMIN)
...
[08:14:20.573] PID=6363   COMM=runc            FUNC=capable_check         CAP_CHECK cap=21 (CAP_SYS_ADMIN)
...
[08:14:20.599] PID=6376   COMM=runc:[2:INIT]   FUNC=capable_check         CAP_CHECK cap=27 (CAP_MKNOD)
...
[08:14:20.678] PID=6376   COMM=date            FUNC=capable_check         CAP_CHECK cap=21 (CAP_SYS_ADMIN)
...
[08:16:11.361] PID=905    COMM=ntpd            FUNC=capable_check         CAP_CHECK cap=25 (CAP_SYS_TIME)
...
[08:16:48.285] PID=7271   COMM=iptables        FUNC=capable_check         CAP_CHECK cap=12 (CAP_NET_ADMIN)
...

(为简洁起见,以上为日志节选)

从这个日志中,我们可以清晰地看到容器内外的时间管理和权限检查过程。

分析

  1. 容器的启动与权限检查

在[08:14:20]这个时间点,我们看到dockerd (PID=971) 这个Docker守护进程,进行了一系列的capable_check

它检查了CAP_SYS_ADMIN(一个权限超高的能力,swap和mount也是此管理)和CAP_DAC_READ_SEARCH(文件读和搜索权限)。这是Docker在为启动新容器做准备工作。

然后,runc(一个底层的容器运行时工具)被调用,它同样检查了CAP_SYS_ADMIN等权限。runc:[2:INIT]进程代表正在初始化的容器内部的"init"进程,

它甚至检查了CAP_MKNOD权限,用于创建设备节点。

这说明,容器的创建过程,本质上是宿主机上的进程(dockerd, runc)在进行一系列需要特定内核权限的操作。

  1. 容器内修改时间的尝试

日志中最关键的一行出现在[08:14:20.678]:

1
PID=6376   COMM=date   FUNC=capable_check   CAP_CHECK cap=21 (CAP_SYS_ADMIN)

看到这里的COMM=date表明,在PID为6376的进程内部,有程序尝试执行date命令来修改系统时间。

然而,它并没有直接去请求CAP_SYS_TIME(专门用于修改系统时间的权限),而是触发了对CAP_SYS_ADMIN的检查。

这是因为在默认情况下,Docker容器是不允许修改宿主机时间的这种尝试会被内核的权限机制所拦截。

如果容器拥有CAP_SYS_ADMIN或者CAP_SYS_TIME,这种对时间的修改也会直接影响到宿主机和其他所有容器,这正是共享内核的“副作用”之一。

  1. 时间管理大师(笑):NTPD

在[08:16:11.361],能看到这次修改时间的关键部分:

1
PID=905    COMM=ntpd   FUNC=capable_check   CAP_CHECK cap=25 (CAP_SYS_TIME)

ntpd是网络时间协议进程服务,它的核心职责就是通过网络同步和校准系统时间(连接ntp服务器校准时间也是靠它)。

他调用capable_check检查了CAP_SYS_TIME权限,这表明ntpd检测是否有权限去修改系统时间。

可以预见

  • 宿主机通过ntpd等服务来保证自身时间的准确性。

  • 所有容器共享宿主机的内核时钟,它们看到的时间,就是宿主机的时间。

  • 默认情况下,容器内不允许修改时间,以防止对整个系统的干扰。

总结

容器的设计初衷是为了隔离应用环境,但它们仍然共享宿主机的内核和时钟。

为了宿主机的安全,要保证最小权限原则,非必要不要给容器分配过高的权限。

不过也感谢容器的这种设计,让我可以通过ebpf来检测容器内的调用内核的情况。

参考

https://man7.org/linux/man-pages/man7/capabilities.7.html