侧边栏壁纸
  • 累计撰写 244 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论
隐藏侧边栏

容器网络实现

kaixindeken
2021-10-08 / 0 评论 / 0 点赞 / 56 阅读 / 3,963 字

通过几个步骤以实现创建的容器支持网络:

  • 创建一对虚拟网络设备 veth1/veth2
  • 设置 veth1 的 MAC 地址
  • 将 veth1 加到网桥 kaixindeken0 上
  • 激活 veth1
  • 创建子进程
  • 将 veth2 移动到 子进程网络 namespace 中,并重命名为 eth0
  • 等待子进程结束
  • 删除网络设备 veth1 和 veth2

所以我们必须进一步优化我们的 start() 逻辑:

首先我们应该为 docker::container_config 增加网络相关的配置:

包含头文件:

#include <net/if.h>     // if_nametoindex
#include <arpa/inet.h>  // inet_pton
#include "network.h"

增加 docker::container_config 配置:

// docker 容器启动配置
typedef struct container_config {
    std::string host_name;      // 主机名
    std::string root_dir;       // 容器根目录
    std::string ip;             // 容器 IP
    std::string bridge_name;    // 网桥名
    std::string bridge_ip;      // 网桥 IP
} container_config;

然后在 main.cpp 中设置好容器 IP,要加入的网桥名 docker0,以及网桥的 IP:

int main(int argc, char** argv) {
    std::cout << "...start container" << std::endl;
    docker::container_config config;
    config.host_name = "kaixindeken";
    config.root_dir  = "./kaixindeken";
    
    // 配置网络参数
    config.ip        = "192.168.0.100"; // 容器 IP
    config.bridge_name = "docker0";     // 宿主机网桥
    config.bridge_ip   = "192.168.0.1"; // 宿主机网桥 IP
    
    docker::container container(config);
    container.start();
    std::cout << "stop container..." << std::endl;
    return 0;
}

我们再来根据上面的网络设备加载的逻辑重构 start() 方法:

private:
    // 保存容器网络设备, 用于删除
    char *veth1;
    char *veth2;
public:
void start() {
    char veth1buf[IFNAMSIZ] = "kaixindeken0X";
    char veth2buf[IFNAMSIZ] = "kaixindeken0X";
    // 创建一对网络设备, 一个用于加载到宿主机, 另一个用于转移到子进程容器中
    veth1 = lxc_mkifname(veth1buf); // lxc_mkifname 这个 API 在网络设备名字后面至少需要添加一个 "X" 来支持随机创建虚拟网络设备
    veth2 = lxc_mkifname(veth2buf); // 用于保证网络设备的正确创建, 详见 network.c 中对 lxc_mkifname 的实现
    lxc_veth_create(veth1, veth2);

    // 设置 veth1 的 MAC 地址
    setup_private_host_hw_addr(veth1);

    // 将 veth1 添加到网桥中
    lxc_bridge_attach(config.bridge_name.c_str(), veth1);

    // 激活 veth1
    lxc_netdev_up(veth1);

    // 容器创建前的一些配置工作
    auto setup = [](void *args) -> int {
        auto _this = reinterpret_cast<container *>(args);
        _this->set_hostname();
        _this->set_rootdir();
        _this->set_procsys();
        
        // 配置容器内网络
        // ...
        
        _this->start_bash();
        return proc_wait;
    };

    // 使用 clone 创建容器
    process_pid child_pid = clone(setup, child_stack, 
                      CLONE_NEWUTS| // UTS   namespace
                      CLONE_NEWNS|  // Mount namespace
                      CLONE_NEWPID| // PID   namespace
                      CLONE_NEWNET| // Net   namespace
                      SIGCHLD,      // 子进程退出时会发出信号给父进程
                      this);

    // 将 veth2 转移到容器内部, 并命名为 eth0
    lxc_netdev_move_by_name(veth2, child_pid, "eth0");

    waitpid(child_pid, nullptr, 0); // 等待子进程的退出
}
~container() {
    // 退出时,记得删除创建的虚拟网络设备
    lxc_netdev_delete_by_name(veth1);
    lxc_netdev_delete_by_name(veth2);
}

在 clone 里增加了 CLONE_NEWNET 这个参数。

从上面的步骤我们可以看到,创建好网络设备之后、子进程创建期间,需要在容器内部配合外部的网络设备进行相关配置:

  • 激活容器内部的 lo 设备
  • 配置 eth0 的 IP 地址
  • 激活 eth0
  • 设置网关
  • 设置 eth0 的 MAC 地址
private:
void set_network() {

int ifindex = if_nametoindex("eth0");
struct in_addr ipv4;
struct in_addr bcast;
struct in_addr gateway;

// IP 地址转换函数,将 IP 地址在点分十进制和二进制之间进行转换
inet_pton(AF_INET, this->config.ip.c_str(), &ipv4);
inet_pton(AF_INET, "255.255.255.0", &bcast);
inet_pton(AF_INET, this->config.bridge_ip.c_str(), &gateway);
 
// 配置 eth0 IP 地址
lxc_ipv4_addr_add(ifindex, &ipv4, &bcast, 16);

// 激活 lo
lxc_netdev_up("lo");

// 激活 eth0
lxc_netdev_up("eth0");

// 设置网关
lxc_ipv4_gateway_add(ifindex, &gateway);

// 设置 eth0 的 MAC 地址
char mac[18];
new_hwaddr(mac);
setup_hw_addr(mac, "eth0");
}

然后再在容器的 setup 中调用这个方法:

……
_this->set_procsys();
_this->set_network();   // 容器内部配合网络配置
_this->start_bash();
return proc_wait;

因为这时我们已经开始需要使用刚才编译的 network.o 和 nl.o 这两个 gcc 编译的链接文件,因此我们不妨编写一个非常简单的 Makefile:

C = gcc
CXX = g++
C_LIB = network.c nl.c
C_LINK = network.o nl.o
MAIN = main.cpp
LD = -std=c++11
OUT = docker-run

all:
    make container
container:
    $(C) -c $(C_LIB)
    $(CXX) $(LD) -o $(OUT) $(MAIN) $(C_LINK)
clean:
    rm *.o $(OUT)

Makefile 里的命令应该是 Tab 开头而不是空格。

再编译执行,并进入容器,我们就能够通过 ifconfig 查看网络,同时,我们已经能够 ping 通宿主机网络了。

外网访问

想要让容器能够对外网进行访问,我们可以通过 iptables 进行源地址转换,达到网络访问的目的。

源地址转换即内网地址向外访问时,发起访问的内网ip地址转换为指定的ip地址(可指定具体的服务以及相应的端口或端口范围),这可以使内网中使用保留ip地址的主机访问外部网络,即内网的多部主机可以通过一个有效的公网i IP 地址访问外部网络。

首先,我们需要在容器内部配置好 DNS:

vi /etc/resolv.conf # 容器中没有 vim 命令

并写入:

nameserver 114.114.114.114

这还不够,因为宿主机的防火墙会限制这一举措,因此,我们还需要对防火墙进行相关配置:

sudo iptables -t nat -A POSTROUTING -s 192.168.0.1/16 ! -o docker0 -j MASQUERADE

删除这条规则只需将 -A 改为 -D

当这条命令执行后,网桥便能够将来自容器的网络访问转换到外网去,至此便完成了容器网络的访问支持:

现在我们能够访问外网后,不妨给容器安装一些软件,刚才我们在写入 DNS 时候会发现,我们并没有 vim 工具,不妨安装一下这个:

apt-get update
apt-get install vim

0

评论区