├── fnetfilter
│ ├── Makefile
│ └── main.c
#include "../include/common.h"
#define MAXBUF 4098
#define MAXARGS 16
static char *args[MAXARGS] = {0};
static int argcnt = 0;
int arg_quiet = 0;
static char *default_filter =
"*filter\n"
":INPUT DROP [0:0]\n"
":FORWARD DROP [0:0]\n"
":OUTPUT ACCEPT [0:0]\n"
"-A INPUT -i lo -j ACCEPT\n"
"-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT\n"
"# echo replay is handled by -m state RELATED/ESTABLISHED above\n"
"#-A INPUT -p icmp --icmp-type echo-reply -j ACCEPT\n"
"-A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT\n"
"-A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT\n"
"-A INPUT -p icmp --icmp-type echo-request -j ACCEPT \n"
"# disable STUN\n"
"-A OUTPUT -p udp --dport 3478 -j DROP\n"
"-A OUTPUT -p udp --dport 3479 -j DROP\n"
"-A OUTPUT -p tcp --dport 3478 -j DROP\n"
"-A OUTPUT -p tcp --dport 3479 -j DROP\n"
"COMMIT\n";
static void usage(void) {
printf("Usage:\n");
printf("\tfnetfilter netfilter-command destination-file\n");
}
static void err_exit_cannot_open_file(const char *fname) {
fprintf(stderr, "Error fnetfilter: cannot open %s\n", fname);
exit(1);
}
static void copy(const char *src, const char *dest) {
FILE *fp1 = fopen(src, "r");
if (!fp1)
err_exit_cannot_open_file(src);
FILE *fp2 = fopen(dest, "w");
if (!fp2)
err_exit_cannot_open_file(dest);
char buf[MAXBUF];
while (fgets(buf, MAXBUF, fp1))
fprintf(fp2, "%s", buf);
fclose(fp1);
fclose(fp2);
}
static void process_template(char *src, const char *dest) {
char *arg_start = strchr(src, ',');
assert(arg_start);
*arg_start = '\0';
arg_start++;
if (*arg_start == '\0') {
fprintf(stderr, "Error fnetfilter: you need to provide at least one argument\n");
exit(1);
}
// extract the arguments from command line
char *token = strtok(arg_start, ",");
while (token) {
if (argcnt == MAXARGS) {
fprintf(stderr, "Error fnetfilter: only up to %u arguments are supported\n", (unsigned) MAXARGS);
exit(1);
}
// look for abnormal things
int len = strlen(token);
if (strcspn(token, "\\&!?\"'<>%^(){};,*[]") != (size_t)len) {
fprintf(stderr, "Error fnetfilter: invalid argument in netfilter command\n");
exit(1);
}
args[argcnt] = token;
argcnt++;
token = strtok(NULL, ",");
}
#if 0
{
printf("argcnt %d\n", argcnt);
int i;
for (i = 0; i < argcnt; i++)
printf("%s\n", args[i]);
}
#endif
// open the files
FILE *fp1 = fopen(src, "r");
if (!fp1)
err_exit_cannot_open_file(src);
FILE *fp2 = fopen(dest, "w");
if (!fp2)
err_exit_cannot_open_file(dest);
int line = 0;
char buf[MAXBUF];
while (fgets(buf, MAXBUF, fp1)) {
line++;
char *ptr = buf;
while (*ptr != '\0') {
if (*ptr != '$')
fputc(*ptr, fp2);
else {
// parsing
int index = 0;
int rv = sscanf(ptr, "$ARG%d", &index) ;
if (rv != 1) {
fprintf(stderr, "Error fnetfilter: invalid template argument on line %d\n", line);
exit(1);
}
// print argument
if (index < 1 || index > argcnt) {
fprintf(stderr, "Error fnetfilter: $ARG%d on line %d was not defined\n", index, line);
exit(1);
}
fprintf(fp2, "%s", args[index - 1]);
// march to the end of argument
ptr += 4;
while (isdigit(*ptr))
ptr++;
ptr--;
}
ptr++;
}
}
fclose(fp1);
fclose(fp2);
}
int main(int argc, char **argv) {
#if 0
{
system("cat /proc/self/status");
int i;
for (i = 0; i < argc; i++)
printf("*%s* ", argv[i]);
printf("\n");
}
#endif
char *quiet = getenv("FIREJAIL_QUIET");
if (quiet && strcmp(quiet, "yes") == 0)
arg_quiet = 1;
if (argc > 1 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") ==0)) {
usage();
return 0;
}
if (argc != 2 && argc != 3) {
usage();
return 1;
}
warn_dumpable();
char *destfile = (argc == 3)? argv[2]: argv[1];
char *command = (argc == 3)? argv[1]: NULL;
//printf("command %s\n", command);
//printf("destfile %s\n", destfile);
// destfile is a real filename
reject_meta_chars(destfile, 0);
// handle default config (command = NULL, destfile)
if (command == NULL) {
// create a default filter file
FILE *fp = fopen(destfile, "w");
if (!fp)
err_exit_cannot_open_file(destfile);
fprintf(fp, "%s\n", default_filter);
fclose(fp);
}
else {
if (strrchr(command, ','))
process_template(command, destfile);
else
copy(command, destfile);
}
return 0;
}
这是一个iptables的防火墙规则脚本,下面是每一行的解释:
*filter
:这是定义过滤表的开始,iptables有多个表,filter是默认表,主要用于过滤数据包。:INPUT DROP [0:0]
:设置默认策略,所有进入(INPUT)的数据包默认丢弃(DROP),除非有规则明确允许。[0:0]
是计数器,表示匹配到这条规则的数据包和字节数,初始为0。:FORWARD DROP [0:0]
:所有转发(FORWARD)的数据包默认丢弃。:OUTPUT ACCEPT [0:0]
:所有出去(OUTPUT)的数据包默认接受(ACCEPT)。-A INPUT -i lo -j ACCEPT
:允许来自本地回环(lo)接口的数据包。-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
:允许所有已经建立连接或者相关的数据包。-A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
:允许ICMP目的地不可达的数据包。-A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
:允许ICMP超时的数据包。-A INPUT -p icmp --icmp-type echo-request -j ACCEPT
:允许ICMP回显请求的数据包,即允许ping请求。-A OUTPUT -p udp --dport 3478 -j DROP
:禁止向外发送目的端口为3478的UDP数据包。-A OUTPUT -p udp --dport 3479 -j DROP
:禁止向外发送目的端口为3479的UDP数据包。-A OUTPUT -p tcp --dport 3478 -j DROP
:禁止向外发送目的端口为3478的TCP数据包。-A OUTPUT -p tcp --dport 3479 -j DROP
:禁止向外发送目的端口为3479的TCP数据包。COMMIT
:提交以上规则,使其生效。这个脚本的主要作用是默认拒绝所有进入和转发的数据包,但允许所有出去的数据包和已经建立的连接。同时,它还允许ICMP和本地回环接口的数据包,但禁止了向外发送到某些特定端口的数据包。这些特定端口通常用于STUN协议,用于NAT穿透。所以这个脚本可能是为了禁止STUN协议而编写的。
static void usage(void) {
:声明一个名为usage的静态函数,它没有参数也没有返回值。printf("Usage:\n");
:在标准输出(通常是终端)上打印字符串"Usage:"和换行符。printf("\tfnetfilter netfilter-command destination-file\n");
:在标准输出上打印制表符、字符串"fnetfilter netfilter-command destination-file"和换行符。这行代码用于显示命令的使用方式。}
:结束usage函数。static void err_exit_cannot_open_file(const char *fname) {
:声明一个名为err_exit_cannot_open_file的静态函数,它接受一个指向字符常量的指针作为参数,并且没有返回值。fprintf(stderr, "Error fnetfilter: cannot open %s\n", fname);
:使用fprintf函数将错误信息(包含文件名fname)写入标准错误输出(通常是终端)。exit(1);
:调用exit函数,使程序立即终止,返回值为1,通常表示出现了错误。}
:结束err_exit_cannot_open_file函数。static void copy(const char *src, const char *dest) {
:声明一个名为copy的静态函数,它接受两个指向字符常量的指针作为参数,并且没有返回值。FILE *fp1 = fopen(src, "r");
:使用fopen函数打开名为src的文件以读取模式。如果文件无法打开,则返回NULL。if (!fp1) err_exit_cannot_open_file(src);
:检查fp1是否为NULL。如果是,则调用err_exit_cannot_open_file函数并传入src作为参数,该函数会打印错误消息并退出程序。FILE *fp2 = fopen(dest, "w");
:使用fopen函数打开名为dest的文件以写入模式。如果文件无法打开,则返回NULL。if (!fp2) err_exit_cannot_open_file(dest);
:检查fp2是否为NULL。如果是,则调用err_exit_cannot_open_file函数并传入dest作为参数,该函数会打印错误消息并退出程序。char buf[MAXBUF];
:定义一个大小为MAXBUF的字符数组buf,用于存储从源文件中读取的内容。while (fgets(buf, MAXBUF, fp1)) fprintf(fp2, "%s", buf);
:使用fgets函数从fp1(源文件)中读取一行内容到buf数组中,直到到达文件末尾或读取到错误。然后使用fprintf函数将buf中的内容写入fp2(目标文件)。这个循环会一直重复,直到读取完源文件的所有内容。fclose(fp1);
:使用fclose函数关闭fp1文件流。fclose(fp2);
:使用fclose函数关闭fp2文件流。}
:结束copy函数。当然,我会尽力解释这个函数中的每一行代码。这个函数是用C语言编写的,主要用于处理模板文件。
static void process_template(char *src, const char *dest) {
这是函数的声明,函数名为 process_template
,它接受两个参数,src
和 dest
,分别代表源文件和目标文件的路径。
char *arg_start = strchr(src, ',');
这行代码在 src
字符串中查找第一个逗号 ,
的位置,并将其地址赋值给 arg_start
。
assert(arg_start);
这行代码使用 assert
函数来确保 arg_start
不为 NULL
。如果 arg_start
为 NULL
,则程序会打印错误信息并终止执行。
*arg_start = '\0';
这行代码将 arg_start
指向的字符替换为结束符 \0
,从而将 src
字符串分割为两部分。
arg_start++;
这行代码将 arg_start
指针向后移动一位,使其指向参数列表的开始位置。
if (*arg_start == '\0') {
fprintf(stderr, "Error fnetfilter: you need to provide at least one argument\n");
exit(1);
}
这段代码检查是否提供了至少一个参数。如果没有提供任何参数,程序会打印错误信息并退出。
当然,让我们通过一个具体的例子来解释这个过程。
假设我们有一个字符串
src
,它的内容是"template,arg1,arg2,arg3"
。这个字符串包含一个模板名"template"
和三个参数"arg1"
,"arg2"
,"arg3"
,它们之间用逗号,
分隔。当我们执行
char *arg_start = strchr(src, ',');
这行代码时,arg_start
指针会指向src
中第一个逗号,
的位置。所以,arg_start
现在指向的是字符串",arg1,arg2,arg3"
。然后,当我们执行
*arg_start = '\0';
这行代码时,逗号,
被替换为了结束符\0
。所以,src
现在变成了两个字符串:"template"
和"arg1,arg2,arg3"
。最后,当我们执行
arg_start++;
这行代码时,arg_start
指针向后移动了一位,跳过了刚刚插入的结束符\0
,从而指向了"arg1,arg2,arg3"
。所以,通过这两行代码,我们将
src
字符串分割为了两部分,并且得到了参数列表的开始位置。
char *token = strtok(arg_start, ",");
这行代码使用 strtok
函数来分割参数列表,分隔符为逗号 ,
。
while (token) {
这是一个 while
循环,只要 token
不为 NULL
,循环就会继续。
if (argcnt == MAXARGS) {
fprintf(stderr, "Error fnetfilter: only up to %u arguments are supported\n", (unsigned) MAXARGS);
exit(1);
}
这段代码检查参数的数量是否超过了最大限制 MAXARGS
。如果超过了,程序会打印错误信息并退出。
int len = strlen(token);
if (strcspn(token, "\\&!?\"'<>%^(){};,*[]") != (size_t)len) {
fprintf(stderr, "Error fnetfilter: invalid argument in netfilter command\n");
exit(1);
}
这段代码检查参数是否包含任何非法字符。如果包含,程序会打印错误信息并退出。
strcspn
是C编程语言中的一个函数,用于查找第一个字符串(称为“源字符串”)中不包含第二个字符串(称为“拒绝字符串”)中的任何字符的初始子串的长度。
strcspn
函数的语法如下:size_t strcspn(const char *s1, const char *s2);
其中:
s1
是源字符串。s2
是拒绝字符串。- 函数返回
s1
中不包含s2
中任何字符的初始子串的长度。例如,考虑以下代码:
#include
#include int main() { char str1[] = "Hello, World!"; char str2[] = "World"; size_t result = strcspn(str1, str2); printf("不含str2中字符的str1的初始子串的长度为: %zu\n", result); return 0; } 在这个例子中,
strcspn
函数用于查找不包含str2
中任何字符的str1
的初始子串的长度。程序的输出将是:不含str2中字符的str1的初始子串的长度为: 6
这是因为
str1
的前6个字符("Hello, ")不在str2
中出现。
args[argcnt] = token;
argcnt++;
这两行代码将参数添加到参数数组 args
中,并将参数计数器 argcnt
加一。
token = strtok(NULL, ",");
}
这行代码获取下一个参数。while
循环在此结束。
strtok
是C编程语言中的一个函数,用于将字符串分割成一个个标记(token)。这个函数在处理分隔符时非常有用,例如逗号、空格或其他特定字符。在你的代码片段中:
token = strtok(NULL, ",");
这是对
strtok
函数的第二次或后续调用。第一次调用strtok
需要提供要分割的原始字符串和作为分隔符的字符数组。例如:char str[] = "apple,banana,grape"; token = strtok(str, ",");
在这个例子中,
str
是要分割的原始字符串,,
是分隔符。首次调用strtok
后,它会返回第一个标记(在这种情况下是"apple"),并将内部状态信息保存下来,以便后续调用。后续的
strtok
调用应该传入NULL
作为第一个参数,表示要继续处理上次调用留下的剩余字符串。第二个参数仍然是分隔符字符数组。所以:token = strtok(NULL, ",");
这行代码表示从上次分割的位置开始,继续查找以逗号分隔的下一个标记。这次调用可能会返回"banana",然后再下一次调用会返回"grape",直到没有更多的标记可以找到为止。
FILE *fp1 = fopen(src, "r");
if (!fp1)
err_exit_cannot_open_file(src);
这两行代码尝试打开源文件。如果无法打开,程序会打印错误信息并退出。
FILE *fp2 = fopen(dest, "w");
if (!fp2)
err_exit_cannot_open_file(dest);
这两行代码尝试打开目标文件。如果无法打开,程序会打印错误信息并退出。
int line = 0;
char buf[MAXBUF];
while (fgets(buf, MAXBUF, fp1)) {
这是一个 while
循环,用于逐行读取源文件。
line++;
这行代码将行号加一。
char *ptr = buf;
while (*ptr != '\0') {
这是一个 while
循环,用于处理当前行的每一个字符。
if (*ptr != '$')
fputc(*ptr, fp2);
这两行代码检查当前字符是否为 $
。如果不是,就将其写入到目标文件中。
else {
int index = 0;
int rv = sscanf(ptr, "$ARG%d", &index) ;
if (rv != 1) {
fprintf(stderr, "Error fnetfilter: invalid template argument on line %d\n", line);
exit(1);
}
这段代码尝试解析 $ARG
标记。如果解析失败,程序会打印错误信息并退出。
这段代码的作用是在处理模板文件时解析和提取
$ARGn
形式的参数占位符。
- 初始化
index
变量为0,这个变量将用于存储提取的参数索引值。- 使用
sscanf()
函数尝试从ptr
指针指向的字符串中解析格式为$ARG%d
的参数。&index
是一个输出参数,将存储解析出的整数值。sscanf()
函数返回实际成功赋值的参数数量。在这里,我们期望它返回1,表示成功解析了一个整数。- 如果
rv
不等于1,说明没有成功解析出一个整数或者解析了多个整数,这与我们期望的$ARGn
格式不符。在这种情况下,程序通过fprintf()
函数向标准错误输出打印一条错误消息,指出在哪一行出现了无效的模板参数,并使用exit(1)
函数终止程序执行,表示出现了错误。这段代码的主要目的是确保模板文件中的参数占位符
$ARGn
符合预期的格式,以便后续正确替换为实际的参数值。
if (index < 1 || index > argcnt) {
fprintf(stderr, "Error fnetfilter: $ARG%d on line %d was not defined\n", index, line);
exit(1);
}
这段代码检查 $ARG
标记的索引是否有效。如果无效,程序会打印错误信息并退出。
fprintf(fp2, "%s", args[index - 1]);
这行代码将 $ARG
标记替换为相应的参数,并写入到目标文件中。
ptr += 4;
while (isdigit(*ptr))
ptr++;
ptr--;
}
ptr++;
}
}
这段代码将指针移动到当前行的下一个字符。
fclose(fp1);
fclose(fp2);
}
这两行代码关闭源文件和目标文件,函数在此结束。希望这个解释对你有所帮助!
这段代码是用C语言编写的,主要功能是在给定命令和目标文件的情况下,根据特定条件处理它们。以下是逐行解释:
int main(int argc, char **argv)
:这是程序的主入口点,参数argc
表示命令行参数的数量,argv
是一个包含所有参数的字符指针数组。
#if 0
:这是一个预处理器指令,用于注释掉下面的代码块(在}
之前)。
3-4. system("cat /proc/self/status");
:运行系统命令cat /proc/self/status
以显示进程状态信息。
5-6. int i; for (i = 0; i < argc; i++)
:声明一个整数变量i
并使用它遍历argc
中的每个参数。
printf("*%s* ", argv[i]);
:打印出每个参数,将其括在星号(*)之间,并在末尾添加空格。
printf("\n");
:换行。
#endif
:结束预处理器指令。
char *quiet = getenv("FIREJAIL_QUIET");
:从环境变量中获取FIREJAIL_QUIET
的值,并将其赋给字符指针quiet
。
if (quiet && strcmp(quiet, "yes") == 0) arg_quiet = 1;
:如果quiet
不为空且等于字符串"yes",则将arg_quiet
设置为1。
12-14. 检查命令行参数是否包含帮助选项(如-h、–help或-?),如果是,则输出帮助信息并返回0。
15-16. 如果参数数量不是2或3,则输出错误消息并返回1。
warn_dumpable()
函数。void warn_dumpable(void) { if (getuid() != 0 && prctl(PR_GET_DUMPABLE, 0, 0, 0, 0) == 1 && getenv("FIREJAIL_PLUGIN")) { fprintf(stderr, "Error: dumpable process\n"); // best effort to provide detailed debug information // cannot use process name, it is just a file descriptor number char path[BUFLEN]; ssize_t len = readlink("/proc/self/exe", path, BUFLEN - 1); if (len < 0) return; path[len] = '\0'; // path can refer to a sandbox mount namespace, use basename only const char *base = gnu_basename(path); struct stat s; if (stat("/proc/self/exe", &s) == 0 && s.st_uid != 0) fprintf(stderr, "Change owner of %s executable to root\n", base); else if (access("/proc/self/exe", R_OK) == 0) fprintf(stderr, "Remove read permission on %s executable\n", base); } }
此文件来自 src\lib\common.c
这段代码是一个C语言函数,名为
warn_dumpable
,其功能是检查当前进程是否可被核心转储(dumpable),并根据需要提供调试信息和警告。函数执行以下操作:
- 使用
getuid
函数检查当前进程的用户ID是否为0(即root用户)。如果不是,则继续后续检查。- 调用
prctl
函数,参数为PR_GET_DUMPABLE
,用于获取当前进程的dumpable状态。如果返回值为1,表示进程是可dumpable的。- 检查环境变量
FIREJAIL_PLUGIN
是否存在。如果存在,则继续后续警告和调试信息输出。- 输出错误信息到标准错误流(stderr):“Error: dumpable process”。
- 努力提供详细的调试信息:
a. 使用readlink
函数从/proc/self/exe
读取当前进程的可执行文件路径,并将其存储在path
缓冲区中。
b. 如果读取失败,则返回。
c. 使用GNU的basename
函数从path
中提取出基名(不含路径的部分),并存储在base
变量中。- 使用
stat
函数获取/proc/self/exe
的文件状态信息,并检查其所有者用户ID是否为0(即root用户)。如果不是,则输出警告信息,建议将可执行文件的所有者改为root用户。- 如果上述
stat
函数调用失败或所有者已经是root用户,使用access
函数检查当前用户是否有读取/proc/self/exe
的权限。如果有,则输出警告信息,建议移除对可执行文件的读取权限。总的来说,这个函数主要用于在非root用户运行的进程中检测dumpable状态,并在必要时提供警告和调试信息,以增强系统的安全性。
======================================================================================
"是否可被核心转储"指的是一个进程或者系统在遇到严重错误或崩溃时,是否允许操作系统生成核心转储文件。核心转储文件是一个包含进程在崩溃时刻内存状态的详细快照,包括程序数据、堆栈信息、打开的文件描述符、内存映射等。如果一个进程是"可被核心转储"的,那么当它因为某种原因异常终止时,操作系统会自动或根据配置生成一个核心转储文件。这个文件对于调试和分析程序崩溃的原因非常有用,因为它提供了崩溃瞬间程序内部状态的详细视图。
相反,如果一个进程不可被核心转储,那么在它崩溃时,操作系统不会生成核心转储文件。这可能是由于安全考虑、资源限制或者是为了防止敏感信息泄露。
在Linux和其他类Unix系统中,可以通过
prctl()
系统调用的PR_SET_DUMPABLE
和PR_GET_DUMPABLE
选项来设置和查询进程的dumpable状态。默认情况下,新创建的进程通常是可被核心转储的,但这个行为可以根据需要进行修改。
destfile
和command
变量。19-20. 禁止在目标文件名中使用元字符。
void reject_meta_chars(const char *fname, int globbing) { assert(fname); reject_cntrl_chars(fname); const char *reject = "\\&!?\"<>%^{};,*[]"; if (globbing) reject = "\\&!\"<>%^{};,"; // file globbing ('*?[]') is allowed const char *c = strpbrk(fname, reject); if (c) { fprintf(stderr, "Error: \"%s\" is an invalid filename: rejected character: \"%c\"\n", fname, *c); exit(1); } }
此文件来自 src\lib\common.c
21-23. 如果没有提供命令(即command == NULL
),则创建一个默认的过滤器文件。
24-26. 如果提供了命令,则根据需要调用process_template()
或copy()
函数来处理命令和目标文件。
这个程序的作用是处理和配置Netfilter(Linux中的网络包过滤系统)的规则。它通过以下方式实现这一功能:
default_filter
变量中)。$ARGn
标记。处理后的规则将写入目标文件。在处理过程中,程序会进行一些错误检查,如确保文件可以打开和读取,检查提供的参数是否有效,以及确保 $ARGn
标记在模板中正确使用。如果发生错误,程序将打印错误消息并退出。
以下是一些不同的输入情况及其结果:
- 只提供目标文件名:
fnetfilter /etc/netfilter/rules.conf
在这种情况下,程序将创建一个名为
rules.conf
的文件,并将默认的Netfilter规则写入该文件。默认规则如下:*filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -i lo -j ACCEPT -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT -A INPUT -p icmp --icmp-type echo-request -j ACCEPT -A OUTPUT -p udp --dport 3478 -j DROP -A OUTPUT -p udp --dport 3479 -j DROP -A OUTPUT -p tcp --dport 3478 -j DROP -A OUTPUT -p tcp --dport 3479 -j DROP COMMIT
- 提供Netfilter命令和目标文件名,命令中不包含逗号:
fnetfilter "iptables -A INPUT -p tcp --dport 80 -j ACCEPT" /etc/netfilter/rules.conf
在这种情况下,程序将把"
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
"这条命令直接复制到rules.conf
文件中。
- 提供Netfilter命令和目标文件名,命令中包含逗号(模板形式):
fnetfilter "iptables -A INPUT -p $ARG1 --dport $ARG2 -j ACCEPT" ,tcp,80 /etc/netfilter/rules.conf
在这种情况下,程序将解析命令模板并用提供的参数替换
$ARGn
标记。因此,rules.conf
文件将包含以下规则:iptables -A INPUT -p tcp --dport 80 -j ACCEPT
注意,这里的逗号后紧跟参数列表,参数之间用逗号分隔。在这个例子中,
$ARG1
被替换为tcp
,$ARG2
被替换为80
。