محسن نوشته

خداحافظ ‍Docker‍، خداحافظ LXC، سلام Taipan!
منتشر شده در: — Aug 19, 2019

توی این پست میخوام راجع به Taipan بنویسم. Taipan میتونه برامون Container بسازه. این Container هم مثل بقیه Container هاست. اما یه تفاوت مهم داره و اونم اینه که قراره به راحتی بفهمیم داره چیکار میکنه. توی پست های قبلی درباره NameSpace ها و اینکه چطور با استفاده از کنسول لینوکس Container بسازیم نوشتم. حالا وقتشه یه برنامه به زبان شیرینC بنویسیم! و با استفاده از system-call های لینوکس Container بسازیم. حواسمون باشه Taipan فقط میخواد به ما کمک کنه تا شناختمون از Container ها ارتقا پیدا کنه و برخلاف عنوان این پست قرار نیست جای Docker یا هیچ ابزار مشابه ی را بگیره!

خوب من یه کد ساده آماده کردم که کمک میکنه مفاهیمی که قراره توی این پست باهاش آشنا بشیم، را راحت درک کنیم. کاربرد این کد فقط آموزش هستش و کد کامل و حرفه ای نیست. برای اینکه پست شلوغ نشه سورس کد را اینجا نمیزارم و فقط بخش های مهمشو مرور و برسی می کنیم. اگه محیط توسعه مبتنی بر GCC دارید دستورات زیر کمکتون میکنه تا سورس Taipan را دریافت و کامپایل کنید. لازمه یادآوری کنم کامپایل و اجرای این کد روی Linux/Mint تست شده و تضمینی برای سایر توزیع ها وجود نداره.

1
2
3
4
5
cd /tmp
git clone https://github.com/mohsenmoqadam/Taipan.git
cd Taipan
gcc taipan.c -o taipan
sudo ./taipan

آخرین دستور بالا یک Container با تنظیمات پیشفرض ایجاد میکنه. شکل زیر این تنظیمات را نمایش میده.

taipan

خوب باید چنتا نکته را در نظر داشته باشیم:

مقادیر پیشفرض Taipan را میشه تغییر داد. برای اینکار چنتا سوئیچ داره:

دستور زیر نحوه استفاده از این سوئیچ را را نشون میده:

1
./taipan --main_ip 192.168.3.1/30 --container_ip 192.168.3.2/30 --container_gw 192.168.3.1 --container_dns 4.2.2.4 --container_rfs ./taipan_rfs --container_cmd 'ping mohsenmoqagam.ir -c 3'

خوب حالا که دیدیم ماجرا از چه قراره وقتشه بریم توی کد و ببینیم Taipan چطور کار میکنه و اینجوری پل بزنیم به هدف انشتار این پست. Taipan فقط 11 تابع داره که پروتوتایپشون توی کد زیر نمایش داده شده:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void print_container_info();
void config_container_user_namespace(int pid);
void config_container_filesystem();
void config_container_network();
int prepare_container_cmd();
int container();
void config_host_network(int cmd_pid);
void write_file(char path[], char line[]);
void do_system_command(char *fmt, ...);
int get_user_defined_options(int argc, char **argv);
int main(int argc, char **argv);

در ادامه بصورت مختصر کاری که هر تابع انجام میده را بهمره کدش میزام.

تابع: ()print_container_info لوگوی Taipan و تنظیماتش Container ایجاد شده را نماشی میده. این تنظیمات توی یه ابجکت سراسری به نام params نگهداری میشن:

 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
void print_container_info(void)
{
  printf(ANSI_COLOR_YELLOW"\n");
  printf("        ---_ ......._-_--.\n");
  printf("     ("ANSI_COLOR_RED"|"ANSI_COLOR_YELLOW"\\ /      / /"ANSI_COLOR_RED"|"ANSI_COLOR_YELLOW" \\  \\\n");
  printf("     /  /     .'  -=-'   `.\n");
  printf("    /  /    .'             )\n");
  printf("  _/  /   .'        _.)   /\n");
  printf(" / o   o        _.-' /  .'\n");
  printf(" \\          _.-'    / .'*|\n");
  printf("  \\______.-'//    .'.' \\*|\n");
  printf("   \\|  \\ | //   .'.' _ |*|\n");
  printf("    `   \\|//  .'.'_ _ _|*|\n");
  printf("     .  .// .'.' | _ _ \\*|\n");
  printf("     \\`-|\\_/ /    \\ _ _ \\*\n");
  printf("      `/'\\__\\/      \\ _ _ \\*\n");
  printf("     /^|            \\ _ _ \\*\n");
  printf("    '  `             \\ _ _ \\\n");
  printf("                      \\_ TAIPAN 1.0.0\n");
  printf(ANSI_COLOR_RESET"\n");
  printf(ANSI_COLOR_BLUE" Container-IP:"ANSI_COLOR_RESET"      %s\n", params.container_ip);
  printf(ANSI_COLOR_BLUE" Container-Gateway:"ANSI_COLOR_RESET" %s\n", params.container_gw);
  printf(ANSI_COLOR_BLUE" Container-DNS:"ANSI_COLOR_RESET"     %s\n", params.container_dns);
  printf(ANSI_COLOR_BLUE" Container-CMD:"ANSI_COLOR_RESET"     %s\n", params.container_cmd);
  printf(ANSI_COLOR_BLUE" Container-RFS:"ANSI_COLOR_RESET"     %s\n", params.container_rfs);
  printf("\n");
}

تابع ()config_container_user_namespace تنظیمات user-namespace را برای Container انجام میده:

 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
void config_container_user_namespace(int pid)
{
  /* @Reference:                                                                                                                                                                                                                              
     Link: http://man7.org/linux/man-pages/man7/user_namespaces.7.html                                                                                                                                                                        
     Section: `Defining user and group ID mappings: writing to uid_map and gid_map`                                                                                                                                                           
     Last part: In the case of gid_map, use of the setgroups(2) system call must first                                                                                                                                                        
                be denied by writing "deny" to the /proc/[pid]/setgroups file (see below)                                                                                                                                                     
                before writing to gid_map.                                                                                                                                                                                                    
  */

  char path[40];
  char line[40];

  uid_t uid = 1000;
  gid_t gid = 1000;

  sprintf(path, "/proc/%d/uid_map", pid);
  sprintf(line, "0 %d 1\n", uid);
  write_file(path, line);

  sprintf(path, "/proc/%d/setgroups", pid);
  sprintf(line, "deny");
  write_file(path, line);

  sprintf(path, "/proc/%d/gid_map", pid);
  sprintf(line, "0 %d 1\n", gid);
  write_file(path, line);
}

تابع: ()config_container_filesystem تنظیمات مربوط به File System را برای Container ایجاد شده انجام میده:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void config_container_filesystem()
{
  const char *old_fs = ".old_fs";

  // Config ROOTFS                                                                                                                                                                                                                            
  mount(params.container_rfs, params.container_rfs, "ext4", MS_BIND, "");
  chdir(params.container_rfs);
  mkdir(old_fs, 0777);
  syscall(SYS_pivot_root, ".", old_fs);
  chdir("/");

  // Config PROCFS                                                                                                                                                                                                                            
  mkdir("/proc", 0555);
  mount("proc", "/proc", "proc", 0, "");

  umount2(old_fs, MNT_DETACH);
}

تابع: ()config_container_network تنظیمات مربوط به Network را برای Container ایجاد شده انجام میده:

1
2
3
4
5
6
7
8
void config_container_network()
{
  do_system_command("ip link set lo up");
  do_system_command("ip link set eth0 up");
  do_system_command("ip addr add %s dev eth0", params.container_ip);
  do_system_command("route add default gw %s eth0", params.container_gw);
  do_system_command("echo nameserver %s > /etc/resolv.conf", params.container_dns);
}

تابع: ()prepare_container_cmd برنامه و سوئیچ هاشو برای اجرا توی ‍‍Container آماده میکنه:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int prepare_container_cmd ()
{
  char *token;
  int index = 0;
  char container_cmd[256];

  memset(container_cmd, 0, sizeof(container_cmd));
  memcpy(container_cmd, params.container_cmd, strlen(params.container_cmd));

  token = strtok (container_cmd, " ");
  params.argv[index] = token;
  while (token != NULL){
    token = strtok (NULL, " ");
    params.argv[++index] = token;
  }
  params.argv[++index] = NULL;

  return 0;
}

تابع: ()container درواقع بدنه پراسس Container ایجاد شده هستش و برنامه ای که قراره توی Container اجرا بشه را ران میکنه:

 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
int container()
{
  char buf[5];

  read(params.fd[0], buf, 5);

  config_container_filesystem();
  if (setgid(0) == -1) {
     printf("Failure: drop superuser privileges.\n");
     exit(EXIT_FAILURE);
  }
  if (setuid(0) == -1) {
    printf("Failure: drop superuser privileges.\n");
    exit(EXIT_FAILURE);
  }

  config_container_network();

  print_container_info();

  if (execvp(params.argv[0], params.argv) == -1) {
    printf("Failure: execute command: %s\n", params.container_cmd);
    exit(EXIT_FAILURE);
  }

  return 1;
}

تابع: ()‍config_host_network تنظیمات مربوط به شبکه و فایروال را روی Host انجام میده:

1
2
3
4
5
6
7
8
9
void config_host_network(int cmd_pid)
{
  do_system_command("ip link add cable type veth peer name eth0");
  do_system_command("ip link set eth0 netns %d", cmd_pid);
  do_system_command("ip link set cable up");
  do_system_command("ip addr add %s dev cable", params.host_ip);
  do_system_command("echo 1 > /proc/sys/net/ipv4/ip_forward");
  do_system_command("iptables -t nat -A POSTROUTING -j MASQUERADE");
}

تابع: ()write_file به ما کمک میکنه راحت توی فایل هایی که لازم داریم بنویسیم:

1
2
3
4
5
6
void write_file(char path[40], char line[40])
{
    FILE *f = fopen(path, "w");
    fwrite(line, 1, strlen(line), f);
    fclose(f);
}

تابع: ()do_system_command به ما کمک میکنه راحت روی Host دستورات مورد نیاز را اجرا کنیم:

1
2
3
4
5
6
7
8
9
void do_system_command(char *fmt, ...)
{
  va_list args;
  char *cmd;
  va_start(args, fmt);
  vasprintf(&cmd, fmt, args);
  va_end(args);
  system(cmd);
}

تابع: ()get_user_defined_options سوئیچ هایی که کاربر تنظیم کرده را توی ابجکت سراسری params بروز میکنه.

 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
int get_user_defined_options(int argc, char **argv)
{
  int opt;
  static const struct option longopts[] =
    {{.name = "main_ip", .has_arg = no_argument, .val = 'm'},
     {.name = "container_ip", .has_arg = no_argument, .val = 'i'},
     {.name = "container_gw", .has_arg = no_argument, .val = 'g'},
     {.name = "container_dns", .has_arg = no_argument, .val = 'd'},
     {.name = "container_cmd", .has_arg = no_argument, .val = 'c'},
     {.name = "container_rfs", .has_arg = no_argument, .val = 'r'},
     {}
    };

  params.argv = argv;
  while ((opt = getopt_long(argc, argv, "migdcr", longopts, NULL)) != -1) {
    switch (opt) {
    case 'm':
      params.host_ip = argv[optind];
      break;
    case 'i':
      params.container_ip = argv[optind];
      break;
    case 'g':
      params.container_gw = argv[optind];
      break;
    case 'd':
      params.container_dns = argv[optind];
      break;
    case 'c':
      params.container_cmd = argv[optind];
      break;
    case 'r':
      params.container_rfs = argv[optind];
      break;
    default:
      // @TODO: write manual                                                                                                                                                                                                                  
      return -1;
    }
  }

  prepare_container_cmd();

  return 0;
}

و نهایتا تابع : ()main هم پراسس اصلی را پیاده سازی میکنه. این پراسس بعنوان پراسس والد تا انتهای کار پراسس Container منتظر میمونه و بعد از مرگ Container رولهایNAT را از IPTable حذف میکنه:

 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
int main(int argc, char **argv)
{
    // Initilize Default Values.                                                                                                                                                                                                              
    memset(&params, 0, sizeof(struct params));
    params.host_ip = "192.168.1.1/30";
    params.container_ip = "192.168.1.2/30";
    params.container_gw = "192.168.1.1";
    params.container_dns = "4.2.2.4";
    params.container_cmd = "sh";
    params.container_rfs = "./taipan_rfs";

    // Get User Defined Options.                                                                                                                                                                                                              
    if (get_user_defined_options(argc, argv) == -1)
      exit(EXIT_FAILURE);

    // Create pipe to communicate between main and container process.                                                                                                                                                                         
    pipe(params.fd);

    // Clone container process.                                                                                                                                                                                                               
    int clone_flags =
      SIGCHLD       |
      CLONE_NEWUTS  |
      CLONE_NEWUSER |
      CLONE_NEWNS   |
      CLONE_NEWNET  |
      CLONE_NEWPID;
    int cmdPid = clone(container,
                       cmd_stack + STACKSIZE,
                       clone_flags,
                       NULL);
    if(cmdPid == -1) {
      printf("Clone Error!\n");
      exit(EXIT_FAILURE);
    }

    // Prepare Pipe                                                                                                                                                                                                                           
    close(params.fd[0]);
    int pipe = params.fd[1];

    config_container_user_namespace(cmdPid);
    config_host_network(cmdPid);

    // Signal to the container process we're done with setup.                                                                                                                                                                                 
    if (write(pipe, "START", 5) != 5){
      printf("Pipe Write Error!\n");
      exit(EXIT_FAILURE);
    }
    if (close(pipe)){
      printf("Pipe Close Error!\n");
      exit(EXIT_FAILURE);
    }

    // Wait for container process.                                                                                                                                                                                                            
    if (waitpid(cmdPid, NULL, 0) == -1){
      printf("Failed to Wait for cmd!\n");
      exit(EXIT_FAILURE);
    }

    // Remove NAT                                                                                                                                                                                                                             
    // @TODO: This command remove all NAT roules.                                                                                                                                                                                             
    do_system_command("iptables -t nat -F");
}

من به دو دلیل برای توابع توضیح مفصل ننوشتم: اول اینکه فکر میکنم کد ها به اندازه کافی بامسما هستن و دوم اینکه اندازه پست طولانی و خسته کننده نشه. اگه در رابطه با کد سوالی داشتید توی شبکه های اجتماعی میتونید منو پیدا و یا توی Email مطرح کنید. با کمال میل پاسخگو هستم.

پیروز باشید.