深入解析:如何通过网络命名空间跟踪单个进程的网络活动(C/C++代码实现)
在 Linux 系统中,网络命名空间(Network Namespaces)是一种强大的功能,它允许系统管理员和开发者隔离网络资源,使得每个命名空间都拥有独立的网络协议栈。这种隔离机制不仅用于容器技术如 Docker,也是网络安全和性能分析的重要工具。是一个利用 Linux 网络命名空间来跟踪单个应用程序网络活动的开源工具,它将跟踪结果保存为 pcap 文件,以便后续使用 Wireshark 或 tshark 等工具进行分析。
网络命名空间的基本概念
在监控和分析网络流的过程中,理解和应用网络命名空间的基本概念是至关重要的。以下是一些关键点,可以帮助你更好地掌握这一技术:
- 基本概念
- 定义:网络命名空间(Network Namespace)是一种内核功能,允许系统创建多个隔离的网络环境,每个网络命名空间都有自己独立的网络栈,包括网络设备、IP地址、路由表等。
- 作用:通过使用网络命名空间,可以在同一台机器上运行多个进程组,每组进程都认为自己拥有独立的网络资源,从而实现网络资源的隔离和管理。
- 创建和管理网络命名空间
- 创建命令:可以使用
ip netns add <namespace_name>
命令来创建一个新的网络命名空间。 - 查看和管理:通过
ip netns list
命令可以列出所有已创建的网络命名空间,而ip netns delete <namespace_name>
则用于删除一个命名空间。
- 将网络接口添加到命名空间
- 虚拟以太网对(veth pair):使用
ip link add veth0 type veth peer name veth1
创建一个虚拟以太网对,然后使用ip link set veth1 netns <namespace_name>
将其中一个接口添加到指定的网络命名空间。 - 配置 IP 地址:在添加接口到命名空间后,需要为这些接口配置 IP 地址并启用它们,例如
ip addr add 192.168.1.1/24 dev veth1
和ip link set dev veth1 up
。
- 跨命名空间通信
- 使用桥接(bridge):为了实现不同网络命名空间之间的通信,可以创建一个桥接设备,并将各个命名空间中的虚拟接口连接到这个桥上。
- 配置路由:确保各个网络命名空间能够正确路由数据包,可以通过配置静态路由或使用动态路由协议来实现这一点。
- 监控和分析
- 工具使用:利用
tcpdump
和Wireshark
等工具可以在特定网络命名空间中捕获和分析流量。 - 脚本自动化:编写脚本定期捕获和保存网络数据,以便后续分析。
将进程关联到特定网络命名空间
要将进程关联到特定的网络命名空间,可以使用以下步骤:
-
首先,确保你已经创建了一个网络命名空间。你可以使用
ip netns add <namespace_name>
命令来创建一个命名空间。例如,要创建一个名为"my_namespace"的命名空间,可以运行以下命令:ip netns add my_namespace
-
接下来,你需要将一个网络接口(如veth对)添加到该命名空间中。首先,创建一个虚拟以太网对,然后将其连接到命名空间。以下是创建和连接虚拟以太网对的命令示例:
ip link add veth0 type veth peer name veth1 ip link set veth1 netns my_namespace
-
在命名空间内部,你需要配置网络接口并启用它。进入命名空间的命令是
ip netns exec <namespace_name> /bin/bash
。一旦你进入了命名空间,你可以执行以下命令来配置网络接口:ip addr add 192.168.1.2/24 dev veth1 ip link set dev veth1 up
-
现在,你可以在命名空间内启动你的进程。你可以使用
ip netns exec <namespace_name> <command>
来执行命令。例如,要在命名空间内启动一个名为"my_process"的进程,可以运行以下命令:ip netns exec my_namespace my_process
-
最后,你可以使用
ps
命令查看命名空间内的进程列表。要查看特定命名空间中的进程,可以使用ip -n <namespace_name> ps
命令。例如,要查看名为"my_namespace"的命名空间中的进程,可以运行以下命令:ip -n my_namespace ps
通过以上步骤,你可以将进程关联到特定的网络命名空间,并在该命名空间内进行监控和分析。
网络命名空间的配置
1.创建虚拟网络接口
为了使被跟踪的进程能够与外界通信,我们需要在根命名空间和新的命名空间之间创建一对虚拟网络接口(veth对)。然后,我们可以将其中一个接口配置为新命名空间的默认网关。
2.使用iptables进行NAT
为了允许命名空间中的进程访问外部网络,我们可以使用iptables设置网络地址转换(NAT)。这样,所有从命名空间发出的流量都会被自动转换到根命名空间的IP地址。
3.处理DNS解析
在网络命名空间中,DNS解析可能会遇到问题,因为默认的DNS服务器可能无法访问。nsntrace提供了一个选项–use-public-dns,可以覆盖命名空间内的resolv.conf文件,使用公共DNS服务器进行查询。
深入解析:如何通过网络命名空间跟踪单个进程的网络活动(C/C++代码实现)
利用Linux的网络命名空间来跟踪单个进程的网络活动。它通过创建一个新的网络命名空间,在这个隔离的环境中启动目标进程,并使用libpcap库来捕获该进程产生的所有网络数据包,然后将这些数据包保存到pcap文件中,以便后续分析
int nsntrace_capture_start(const char *iface,
const char *filter,
FILE *fp);
void nsntrace_capture_stop();
unsigned long nsntrace_capture_packet_count();
void nsntrace_capture_flush();
char *nsntrace_capture_default_device();
int nsntrace_capture_check_device(char *iface);
int nsntrace_net_init(pid_t ns_pid,
const char *device,
struct nsntrace_if_info *info);
int nsntrace_net_deinit(pid_t ns_pid,
const char *device,
struct nsntrace_if_info *info);
int nsntrace_net_ns_init(int use_public_dns,
struct nsntrace_if_info *info);
int nsntrace_net_ip_forward_enabled();
int nsntrace_net_get_if_info(pid_t pid,
struct nsntrace_if_info *info);
...
const int nsntrace_signals[] = {
SIGHUP,
SIGINT,
SIGABRT,
SIGTERM,
SIGQUIT,
SIGSEGV,
};
static void
_nsntrace_handle_signals(void (*handler)(int))
{
struct sigaction action = { 0 };
int s;
action.sa_handler = handler;
for (s = 0; s < sizeof(nsntrace_signals) / sizeof(int); s++) {
sigaction(nsntrace_signals[s], &action, NULL);
}
}
static int _nsntrace_unlink_cb(const char *fpath,
const struct stat *sb,
int typeflag,
struct FTW *ftwbuf)
{
int rv = remove(fpath);
if (rv)
perror(fpath);
return rv;
}
static void
_nsntrace_remove_rundir(pid_t pid)
{
char path[PATH_MAX] = { 0, };
snprintf(path, PATH_MAX, "%s/%d", NSNTRACE_RUN_DIR, getppid());
nftw(path, _nsntrace_unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
}
static void _nsntrace_remove_ns()
{
char netns_path[PATH_MAX];
snprintf(netns_path, sizeof(netns_path), "%s/nsntrace-%d", NETNS_RUN_DIR, getpid());
umount2(netns_path, MNT_DETACH);
if (unlink(netns_path) < 0) {
perror("unlink");
}
_nsntrace_remove_rundir(getppid());
}
static void
_nsntrace_cleanup_ns()
{
kill(child_pid, SIGTERM);
waitpid(child_pid, NULL, 0);
_nsntrace_remove_ns();
fprintf(where, "Finished capturing %lu packets.\n",
nsntrace_capture_packet_count());
nsntrace_capture_flush();
}
static void
_nsntrace_cleanup_ns_signal_callback()
{
_nsntrace_cleanup_ns();
exit(EXIT_SUCCESS);
}
static void
_nsntrace_cleanup() {
wait(NULL);
}
static void
_nsntrace_set_name()
{
...
/* 如果基本netns目录不存在,则创建它 */
if ((ret = mkdir(NETNS_RUN_DIR, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)) < 0) {
if (errno != EEXIST) {
goto out;
}
}
snprintf(netns_path, PATH_MAX, "%s/nsntrace-%d", NETNS_RUN_DIR, getpid());
fd = open(netns_path, O_RDONLY|O_CREAT|O_EXCL, 0);
if (fd < 0) {
ret = fd;
goto out;
}
close(fd);
ret = mount(PROC_SELF_NETNS_PATH, netns_path, "none", MS_BIND, NULL);
out:
if (ret < 0) {
fprintf(stderr, "failed to set namespace name\n");
}
}
static void
_nsntrace_start_tracer(struct nsntrace_options *options,
struct nsntrace_if_info *info)
{
int ret;
/*
* 如果outfile是一个单破折号(“-”),用户希望我们输出到STDOUT
*/
FILE *fp;
if (options->outfile[0] == '-' && options->outfile[1] == '\0') {
where = stderr;
fp = stdout;
} else {
where = stdout;
fp = fopen(options->outfile, "w");
if (!fp) {
perror("fopen");
exit(EXIT_FAILURE);
}
}
fprintf(where, "Starting network trace of '%s' on interface %s.\n"
"Your IP address in this trace is %s.\n"
"Use ctrl-c to end at any time.\n\n",
options->args[0], options->device, info->ns_ip);
ret = nsntrace_capture_start(info->ns_if, options->filter, fp);
if (ret != 0) {
exit(ret);
}
}
static void
_nsntrace_start_tracee(struct nsntrace_options *options)
{
...
if (sscanf(options->user, "%i", &uid) == 1) {
pwd = getpwuid(uid);
} else {
pwd = getpwnam(options->user);
}
if (!pwd) {
fprintf(stderr,"Cannot find user '%s'\n",
options->user);
exit(EXIT_FAILURE);
}
uid = pwd->pw_uid;
gid = pwd->pw_gid;
setenv("HOME", pwd->pw_dir, 1);
setenv("USER", pwd->pw_name, 1);
setenv("USERNAME", pwd->pw_name, 1);
if (initgroups(options->user, gid) < 0) {
fprintf(stderr, "Failed to initialize groups\n");
exit(EXIT_FAILURE);
}
} else {
uid = getuid();
gid = getgid();
}
if (setgid(gid) < 0) {
fprintf(stderr, "Unable to set process group ID\n");
exit(EXIT_FAILURE);
}
if (setuid(uid) < 0) {
fprintf(stderr, "Unable to set process user ID\n");
exit(EXIT_FAILURE);
}
/*
* 只在STDOUT上输出PCAP数据
*/
if (options->outfile[0] == '-' && options->outfile[1] == '\0') {
dup2(STDERR_FILENO, STDOUT_FILENO);
}
/* 启动要跟踪的应用程序 */
if (execvp(options->args[0], options->args) < 0) {
fprintf(stderr, "Unable to start '%s'\n", options->args[0]);
exit(EXIT_FAILURE);
}
}
static int
netns_main(void *arg) {
...
if (nsntrace_net_ns_init(options->use_public_dns, &common->if_info) < 0) {
fprintf(stderr, "failed to setup network environment\n");
return EXIT_FAILURE;
}
_nsntrace_handle_signals(_nsntrace_cleanup_ns_signal_callback);
_nsntrace_start_tracer(common->options, &common->if_info);
_nsntrace_set_name();
child_pid = fork();
if (child_pid < 0) {
return EXIT_FAILURE;
} else if (child_pid > 0) { /* parent - tracer */
struct timespec timeout = { 0 };
waitpid(child_pid, &status, 0);
if (WIFEXITED(status)) {
ret = WEXITSTATUS(status);
} else {
ret = EXIT_FAILURE;
}
/*
* 睡眠,以便处理所有数据包。
*/
timeout.tv_sec = 2; /* 2 seconds timeout */
nanosleep(&timeout, NULL);
nsntrace_capture_stop();
/* 退出捕获循环,清理 */
_nsntrace_cleanup_ns();
exit(ret);
} else {
_nsntrace_start_tracee(common->options);
}
return ret;
}
...
static void
_nsntrace_parse_options(struct nsntrace_options *options,
int argc, char **argv)
{
int c;
opterr = 0;
while ((c = getopt_long(argc, argv, short_opt, long_opt, NULL)) > 0) {
switch(c) {
case -1:
case 0:
break;
case 'o':
options->outfile = strdup(optarg);
break;
case 'd':
options->device = strdup(optarg);
break;
case 'u':
options->user = strdup(optarg);
break;
case 'f':
options->filter = strdup(optarg);
break;
case PUBLIC_DNS:
options->use_public_dns = 1;
break;
case 'h':
_nsntrace_usage();
exit(EXIT_SUCCESS);
break;
default:
fprintf(stderr, "Invalid option '%c'\n", c);
_nsntrace_usage();
exit(EXIT_FAILURE);
}
}
if (!options->device) {
options->device = nsntrace_capture_default_device();
}
if (!options->outfile) {
options->outfile = strdup(DEFAULT_OUTFILE);
}
options->args = argv + optind; /* 解析选项后的参数 */
if (!options->args[0]) {
_nsntrace_usage();
exit(EXIT_FAILURE);
}
}
/*
创建基于pid的运行目录
*/
static void
_nsntrace_mkrundir()
{
char path[PATH_MAX] = { 0, };
if (mkdir(NSNTRACE_RUN_DIR, 0644) < 0) {
if (errno != EEXIST) {
perror("mkdir");
}
}
snprintf(path, PATH_MAX, "%s/%d", NSNTRACE_RUN_DIR, getpid());
if (mkdir(path, 0644) < 0) {
if (errno != EEXIST) {
perror("mkdir");
}
}
}
int
main(int argc, char **argv)
{
...
_nsntrace_parse_options(&options, argc, argv);
/* geteuid()返回有效的用户ID,如果是root,则返回0 */
if (geteuid() != 0) {
fprintf(stderr,
"You need root privileges to run this application\n");
exit(EXIT_FAILURE);
}
if (!nsntrace_net_ip_forward_enabled()) {
fprintf(stderr, "Please enable IP forwarding:\n");
if( NULL == getenv("SUDO_UID") ) {
fprintf(stderr, "# sysctl net.ipv4.ip_forward=1\n");
} else {
fprintf(stderr, "$ sudo sysctl net.ipv4.ip_forward=1\n");
}
exit(EXIT_FAILURE);
}
_nsntrace_mkrundir();
if (nsntrace_net_get_if_info(getpid(), &common.if_info) < 0) {
ret = EXIT_FAILURE;
goto out;
}
/* 在一个新的网络名称空间中创建一个新进程 */
pid = clone(netns_main, child_stack + STACK_SIZE,
CLONE_NEWNET | SIGCHLD, &common);
if (pid < 0) {
perror("clone");
ret = EXIT_FAILURE;
goto out;
}
...
/* 在此处等待,直到跟踪的进程存在或用户中止 */
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
ret = WEXITSTATUS(status);
} else {
ret = EXIT_FAILURE;
}
out:
if (pid != 0) {
nsntrace_net_deinit(pid, options.device, &common.if_info);
}
_nsntrace_remove_rundir(getpid());
return ret;
}
If you need the complete source code, please add the WeChat number (c17865354792)
$ sudo my_nsntrace -d eth1 wget www.google.com
Starting network trace of 'wget' on interface eth1.
Your IP address in this trace is 172.16.42.255.
Use ctrl-c to end at any time.
--2024-07-15 12:12:17-- http://www.google.com/
Location: http://www.google.se/?gfe_rd=cr&ei=AbeIV5zZHcaq8wfTlrjgCA [following]
--2024-07-15 12:12:17-- http://www.google.se/?gfe_rd=cr&ei=AbeIV5zZHcaq8wfTlrjgCA
Length: unspecified [text/html]
Saving to: ‘index.html’
index.html [ <=> ] 10.72K --.-KB/s in 0.001s
2024-07-15 12:12:17 (15.3 MB/s) - ‘index.html’ saved [10980]
Finished capturing 42 packets.
$ tshark -r my_nsntrace.pcap -Y 'http.response or http.request'
16 0.998839 172.16.42.255 -> 195.249.146.104 HTTP 229 GET http://www.google.com/ HTTP/1.1
20 1.010671 195.249.146.104 -> 172.16.42.255 HTTP 324 HTTP/1.1 302 Moved Temporarily (text/html)
22 1.010898 172.16.42.255 -> 195.249.146.104 HTTP 263 GET http://www.google.se/?gfe_rd=cr&ei=AbeIV5zZHcaq8wfTlrjgCA HTTP/1.1
31 1.051006 195.249.146.104 -> 172.16.42.255 HTTP 71 HTTP/1.1 200 OK (text/html)
使用tshark进行实时捕捉
$ sudo my_nsntrace -f tcp -o - wget www.google.com 2> /dev/null | tshark -r -
1 0.000000 172.16.42.255 → 142.250.74.36 TCP 74 51088 → 80 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM=1 TSval=1362504482 TSecr=0 WS=128
2 0.014010 142.250.74.36 → 172.16.42.255 TCP 74 80 → 51088 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1430 SACK_PERM=1 TSval=2846449454 Secr=1362504482 WS=256
3 0.014078 172.16.42.255 → 142.250.74.36 TCP 66 51088 → 80 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=1362504496 TSecr=2846449454
4 0.014221 172.16.42.255 → 142.250.74.36 HTTP 207 GET / HTTP/1.1
5 0.033935 142.250.74.36 → 172.16.42.255 TCP 66 80 → 51088 [ACK] Seq=1 Ack=142 Win=66816 Len=0 TSval=2846449475 TSecr=1362504496
6 0.093989 142.250.74.36 → 172.16.42.255 TCP 1484 HTTP/1.1 200 OK [TCP segment of a reassembled PDU]
7 0.094022 172.16.42.255 → 142.250.74.36 TCP 66 51088 → 80 [ACK] Seq=142 Ack=1419 Win=63360 Len=0 TSval=1362504576 TSecr=2846449532
8 0.096447 142.250.74.36 → 172.16.42.255 TCP 2902 HTTP/1.1 200 OK [TCP segment of a reassembled PDU]
9 0.096478 172.16.42.255 → 142.250.74.36 TCP 66 51088 → 80 [ACK] Seq=142 Ack=4255 Win=62208 Len=0 TSval=1362504578 TSecr=2846449532
10 0.099871 142.250.74.36 → 172.16.42.255 HTTP 9626 Continuation[Packet size limited during capture]
11 0.099936 172.16.42.255 → 142.250.74.36 TCP 66 51088 → 80 [ACK] Seq=142 Ack=13815 Win=56320 Len=0 TSval=1362504582 TSecr=2846449532
12 0.100743 172.16.42.255 → 142.250.74.36 TCP 66 51088 → 80 [FIN, ACK] Seq=142 Ack=13815 Win=64128 Len=0 TSval=1362504583 TSecr=2846449532
13 0.113167 142.250.74.36 → 172.16.42.255 TCP 66 80 → 51088 [FIN, ACK] Seq=13815 Ack=143 Win=66816 Len=0 TSval=2846449554 TSecr=1362504583
14 0.113190 172.16.42.255 → 142.250.74.36 TCP 66 51088 → 80 [ACK] Seq=143 Ack=13816 Win=64128 Len=0 TSval=1362504595 TSecr=2846449554
my_nsntrace 应用程序使用 clone 系统调用创建一个新的网络命名空间(CLONE_NEWNET),并在该命名空间中启动目标进程,同时使用 libpcap 开始跟踪。这样可以确保所有跟踪到的数据包都来自该进程。
由于进程在命名空间中是隔离的,无法直接访问其他网络,因此 my_nsntrace 通过创建虚拟网络接口来解决这个问题。其中一个虚拟网络接口保留在根网络命名空间中,另一个放在新创建的命名空间中。然后,将根命名空间中的虚拟设备设置为跟踪命名空间中虚拟设备的默认网关。
为了确保能够访问目标网络,my_nsntrace 使用 iptables 和 NAT 技术将虚拟设备的所有流量转发到默认的网络接口。这样,就可以在不影响系统其他部分的情况下,捕获单个进程与默认网络通信时的数据包。
总结
通过网络命名空间跟踪单个进程的网络活动是一种高级网络监控技术,它允许系统管理员和网络安全专家深入分析特定应用程序或服务的网络行为。通过创建隔离的网络环境并将目标进程移入该环境,我们可以精确地捕捉到该进程生成的所有网络流量,而不受其他进程的干扰。
在实际操作中,这通常涉及使用ip netns命令来管理网络命名空间,利用nsenter工具将进程附加到特定的网络命名空间,并在该命名空间内部署如tcpdump或iftop等网络嗅探工具以捕获数据包。通过这种方式,我们能够获得关于进程通信模式、数据流大小、连接尝试等详细信息,从而为进一步的分析提供坚实的基础。
We also undertake the development of program requirements here. If necessary, please follow the WeChat official account 【程序猿编码】and contact me