跳至主要內容

【OJ】⑦基于Docker构建代码沙箱

holic-x...大约 30 分钟项目oj-platform

【OJ】⑦基于Docker构建代码沙箱

​ 先配置好虚拟机环境,Docker在虚拟机环境上操作(也可使用远程服务器,如果是构建项目测试的话建议使用虚拟机,以免玩崩)

Docker核心

1.Docker基本概念

为什么要使用Docker技术?

为了提升系统的安全性,把不同的程序和宿主机进行隔离,是的某个程序(应用)的执行不会影响到系统本身

Docker 技术可以实现程序和宿主机的隔离

什么是容器?(可以把一个容器理解为一个新的电脑(定制化的操作系统))

​ 理解为对一系列应用程序、服务和环境的封装,从而把程序运行在一个隔离的、密闭的、隐私的空间内,对外整体提供服务。

Docker基本概念

术语说明
镜像用来创建容器的安装包,可以理解为给电脑安装操作系统的系统镜像
容器通过镜像来创建的一套运行环境,一个容器里可以运行多个程序,可以理解为一个电脑实例
Dockerfile制作镜像的文件,可以理解为制作镜像的一个清单
镜像仓库存放镜像的仓库,用户可以从仓库下载现成的镜像,也可以把做好的镜像放到仓库里

【Dockerfile】 == build ==》【Docker Image】 == run ==》【Docker Container】

Docker实现原理:❓Docker能实现哪些资源的隔离

1)Docker 运行在 Linux 内核上

2)CGroups:实现了容器的资源隔离,底层是 Linux CGroup 命令,能够控制进程使用的资源

3)Network 网络:实现容器的网络隔离,docker 容器内部的网络互不影响

4)Namespaces 命名空间:可以把进程隔离在不同的命名空间下,每个容器他都可以有自己的命名空间,不同的命名空间下的进程互不影响

5)Storage 存储空间:容器内的文件是相互隔离的,也可以去使用宿主机的文件

image-20240504113410620

Docker compose:是一种同时启动多个容器的集群操作工具(容器管理工具),一般情况下,开发者仅做了解即可,实际使用 docker compose 时去百度配置文件

镜像仓库:类似Maven仓库、前端NPM仓库,是一个存放镜像的仓库,用户可以从仓库下载现成的镜像,也可以把做好的镜像放到仓库中

官方镜像仓库:docker hubopen in new window:一个官方的docker镜像仓库

image-20240505101811518

2.Docker安装(基于Ubuntu)

一般情况下,不建议在 Windows 系统上安装。Windows 本身就自带了一个虚拟机叫 WSL,不推荐;推荐使用虚拟机安装Docker

使用免费的VMWare Workstation Playeropen in new window软件,安装虚拟机、Docker环境和远程开发环境

此处选用Ubuntu,提供了一个Ubuntu Desktop,对新手比较友好dovkerd

Docker常用操作

1)查看Docker版本信息
# 查看Docker版本
Docker -v
Docker version 24.0.5, build 24.0.5-0ubuntu1~22.04.1
2)查看命令用法
# 查看命令用法
docker --help

# 查看具体子命令的用法
docker run --help

3)从远程仓库拉取现成的镜像

​ 从官方仓库docker hub搜索现成的镜像(例如hello-world),拉取镜像

# 从远程仓库拉取现成的镜像
docker pull [options] name[:tag|@digest]

# 参考示例
docker pull hello-world

sudo docker pull hello-world

image-20240505102510906

执行指令如果提示没有权限:说明linux当前登录用户没有使用该系统的一些权限(需要修改权限),或者通过sudo执行

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/images/create?fromImage=hello-world&tag=latest": dial unix /var/run/docker.sock: connect: permission denied
4)根据镜像创建容器实例
# 根据镜像创建容器实例
docker create [options] image [command] [arg...]

# 参考示例(启动实例,得到容器实例containerId)
sudo docker create hello-world
// 执行指令会得到一个容器实例Id
2d72aa8448a8cb02f2e5938f7fd9833741816642c90c17bdbd824a3a3aab68ca
5)查看容器状态
# 查看容器状态
sudo docker ps
# 运行结果:会显示当前启动的容器信息(如果没有启动则无)
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES



# 查看所有容器状态
sudo docker ps -a(添加-a参数指定查看所有的容器状态)
# 运行结果(会展示所有容器信息,CONTSINERID会展示前缀信息)
容器ID、创建容器使用的镜像、创建容器指定的命令、创建时间、创建状态、容器名称(如果没有指定,则系统默认随机分配)
CONTAINER ID   IMAGE         COMMAND    CREATED              STATUS                    PORTS     NAMES
2d72aa8448a8   hello-world   "/hello"   About a minute ago   Created                             amazing_khorana
038dc7fbdf64   hello-world   "/hello"   20 hours ago         Exited (0) 20 hours ago             gifted_mclaren
0d2e50090cf5   hello-world   "/hello"   20 hours ago         Exited (0) 20 hours ago             happy_benz
e9c040898ee4   hello-world   "/hello"   20 hours ago         Exited (0) 20 hours ago             dazzling_hypatia
6)启动容器
# 启动容器
docker start [options] container [container...]
sudo docker start [容器名称]
sudo docker start [容器ID]

# 参考示例
sudo docker start amazing_khorana   // 以容器名称启动

sudo docker start 038dc7fbdf64      // 以容器ID启动

image-20240505103815009

7)查看日志
# 查看日志
docker logs [options] container

sudo docker logs [容器名称]
sudo docker logs [容器ID]

# 参考示例
sudo docker logs amazing_khorana

image-20240505104020590

8)删除容器实例
# 删除容器实例
docker rm [options] container [container...]

# 参考示例
sudo docker rm amazing_khorana

image-20240505104106292

9)删除镜像
# 删除镜像
docker rmi [optiions] image [image...]

sudo docker rmi hello-world       // 如果镜像还绑定了容器则不允许删除
sudo docker rmi hello-world -f    // 强制删除,如果镜像绑定了容器则删除镜像,保留容器

# 参考示例
sudo docker rmi hello-world -f

一般不建议直接删除容器、镜像

image-20240505104615797

10)推送镜像
# 推送镜像
docker push image

Java操作Docker

使用 Docker-Java (Java操作Docker的API):https://github.com/docker-java/docker-java

官方快速入门:https://github.com/docker-java/docker-java/blob/main/docs/getting_started.md,根据官方入门文档完成测试

先引入依赖:

		<!-- 引入Docker操作相关依赖 -->
		<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java -->
        <dependency>
            <groupId>com.github.docker-java</groupId>
            <artifactId>docker-java</artifactId>
            <version>3.3.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java-transport-httpclient5 -->
        <dependency>
            <groupId>com.github.docker-java</groupId>
            <artifactId>docker-java-transport-httpclient5</artifactId>
            <version>3.3.0</version>
        </dependency>

DockerClientConfig:用于定义一些初始化的 DockerClient 的配置(类比 MySQL 的连接配置、线程数的配置等)

DockerHttpClient:用于向 Docker 守护进程(操作 Docker 的接口)发送请求的客户端,低层封装(不推荐使用),你要自己构建请求参数(简单地理解成JDBC)

DockerClient(推荐):这个才是真正的和 Docker 守护进程交互的、最方便的 SDK,高层封装,对DockerHttpClient 在进行了一层封装(理解成MyBatis),提供了线程的增删改查

Linux Docker远程开发(oj-code-sandbox)

1)方式1:远程部署(本地开发,同步代码到远程服务器)

方式1:远程部署(本地代码开发同步到linux远程服务器路径:其核心是本地开发,然后同步文件)

​ 使用IDEA Development 将代码上传到Linux,然后使用JetBrains远程开发完全连接linux实时开发,构建思路:

  • 添加远程开发配置
  • 将本地文件夹和linux远程服务器的文件夹进行映射
  • 同步文件(手动同步、自动同步)
  • 确认linux中是否已经同步

远程开发配置

​ 配置同步:Tools=》Development=》Configuration (创建一个远程配置)=》添加选择sftp协议=》输入服务器名称(Ubuntu)

​ 然后在相应配置中添加一个SSH连接配置,让软件知道应该去访问哪个linux服务器。点击设置添加一个SSH配置,输入虚拟机的IP、用户名、密码

image-20240505110147943

​ 配置Mapping映射,同步本地文件和远程服务器文件

​ Tools=》Development=》Configuration :选择刚创建好的服务配置,然后点击Mappings选项卡进行配置,需要先理解每个参数的含义

  • Local path:本机项目代码存放位置
  • Deployment path:希望将代码同步到linux服务器的目录位置
  • Web path:/

​ 进入到linux 虚拟机目录(如果虚拟机创建了用户,例如创建了用户noob,则可在/home下有一个noob目录专门用于存放这个用户相关的所有文件),在里面创建一个文件夹专门用于存储项目代码(即同步部署的目标路径)

image-20240505110617098

​ 🚀手动同步:配置完成,右键选择项目RemoteDemo,选择Deployment=》Sync with Deployed to Ubuntu (一键同步),然后就会自动列举出当前没有同步的文件内容。点击一键同步即可完成同步操作,随后去linux服务器上查看代码是否同步完成(基于手动同步操作)

image-20240505110702346

​ 🚀自动同步:Tools=》Development=》Automatic Upload(自动上传配置),点击该配置则开启自动上传,随后在项目中任意位置创建一个文件,测试是否自动同步即可

​ 🤡:基于这种方式可能每次都要手动或者自动同步一下然后再启动、部署,应择优选择适合自己的方案,也可以选择基于idea的远程开发模式(直接操作服务器代码)

​ 如果要把远程服务器的代码同步到本地,则可通过Remote Host =》 打开对应目录,右键选择Download Here(会提示是否覆盖当前目录)

image-20240505174042225

​ 或者选择本地项目,右键选择download from 选择配置好的远程服务器,然后下载同步到本地即可

2)方式2:远程开发(直接通过本地操作远程服务器代码)

​ 方式2:远程开发模式,通过idea的remote development模式进行开发,本地相当于操作远程服务器一样完成远程服务器上的代码开发,其所有的操作都是基于远程服务器的代码。可以理解为玩网页游戏,本地不会存储客户端软件,直接通过网页端操作

操作步骤

​ File =》Remote Development =》新建一个SSH连接,然后配置远程服务器信息,选择远程服务器依赖的环境和项目目录(IDEA版本、代码目录)

​ 例如此处,项目代码目录可以选择前面方式1中已经同步过去的文件夹(需要注意不要两边都开启同步,即不要两种方式混着用,如果是以服务器那边的为参考则后续不要用方式1误操作把本地的内容给同步过去导致直接冲掉了服务器的代码内容,因为服务器的代码版本是没有做版本跟踪的,一旦被冲掉就无法挽回)

image-20240505112221200

​ 等待项目加载完成,会开启一个新的远程服务开发的窗口,可以通过这个窗口直接操作远程代码(此处可以考虑在远程服务器上对这个项目进行代码版本管理)

image-20240505112327353

​ 如果无法启动程序,则需要修改settings=》Build,Exception,Development:Compile=》进程VM选项:-Djdk.lang.Process.launchMechanism=vfork

可能是使用了比较新的IDEA版本导致,需要配置一下,此外还需注意要配置一下远程开发项目环境(例如Maven依赖等),然后启动一下项目,尝试是否可以正常访问

image-20240505114151358

​ 完成上述步骤,就可以直接操作远程服务器代码完成开发

远程开发:编写案例操作Docker

​ 如果启动失败(连接Docker失败):

  • 增加权限
  • 重启虚拟机、重启开发环境、重启程序

权限设定:查看用户组

# 查看用户组
cat /etc/group

# 查看用户组(过滤选择docker,查看docker所属用户组)
cat /etc/group | grep 'docker'

# 如果当前用户没有访问docker的权限,则加入用户组(使用gpasswd命令添加)
sudo gpasswd -a ${USER} docker      // 此处${USER}表示获取当前登录用户名,也可直接用常量代替例如:sudo gpasswd -a noob docker

image-20240505121040184

Docker连接失败:重启一下远程开发环境

image-20240505121729290

1)拉取镜像
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.PullImageCmd;
import com.github.dockerjava.api.model.PullResponseItem;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.PullImageResultCallback;

public class DockerDemo {
    public static void main(String[] args) throws InterruptedException {
        // 获取默认的 Docker Client
        DockerClient dockerClient = DockerClientBuilder.getInstance().build();
//        PingCmd pingCmd = dockerClient.pingCmd();
//        pingCmd.exec();
        String image = "nginx:latest";
        PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);

        PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
            @Override
            public void onNext(PullResponseItem item) {
                System.out.println("下载镜像:" + item.getStatus());
                super.onNext(item);
            }
        };
        pullImageCmd
                .exec(pullImageResultCallback)
                .awaitCompletion();
        System.out.println("下载完成");
    }
}

image-20240505122347114

​ 回到linux,查看拉取的镜像信息

# 查看镜像
docker images

image-20240505122534563

2)创建容器
CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
        CreateContainerResponse createContainerResponse = containerCmd
                .withCmd("echo", "Hello Docker") // 在创建容器的时候可以附加一些给创建容器时自动执行的命令(例如此处指定命令输出Hello Docker)
                .exec();
        System.out.println(createContainerResponse);

image-20240505123220528

3)查看容器状态
ListContainersCmd listContainersCmd = dockerClient.listContainersCmd();
        List<Container> containerList = listContainersCmd.withShowAll(true).exec();
        for (Container container : containerList) {
            System.out.println(container);
        }

image-20240505123435959

4)启动容器
// qizhong containerId,为容器创建成功生成的ID
dockerClient.startContainerCmd(containerId).exec();

image-20240505123711005

5)查看日志
// 5.查看日志
        LogContainerResultCallback logContainerResultCallback = new LogContainerResultCallback() {
            @Override
            public void onNext(Frame item) {
                System.out.println("日志:" + new String(item.getPayload()));
                super.onNext(item);
            }
        };

        // 阻塞等待日志输出
        dockerClient.logContainerCmd(containerId)
                .withStdErr(true)
                .withStdOut(true)
                .exec(logContainerResultCallback)
                .awaitCompletion(); // 阻塞等待日志输出

​ 此处需注意,如果日志没有输出,则考虑下述情况(记得阻塞等待日志输出awaitCompletion

image-20240505130518530

image-20240505130606930

6)删除容器
// 删除容器
dockerClient.removeContainerCmd(containerId).exec();
7)删除镜像
// 删除镜像
dockerClient.removeImageCmd(image).exec();

​ 镜像删除时候如果存在容器(有些容器还在使用这个镜像),则无法直接删除,如果要强制删除,则可加入.withForce()

image-20240505131041845

Docker实现代码沙箱

实现流程:docker 负责运行 Java 程序,并且得到结果

1)把用户的代码保存为文件

2)编译代码,得到 class 文件

3)把编译好的文件上传到容器环境内

4)在容器中执行代码,得到输出结果

5)收集整理输出结果

6)文件清理

7)错误处理,提升程序健壮性

todo:扩展:模版方法设计模式,定义同一套实现流程,让不同的子类去负责不同流程中的具体实现。执行步骤一样,每个步骤的实现方式不一样。

准备工作

​ 复制一份JavaNativeCodeSandbox.java,构建新的JavaDockerCodeSandbox,保留前两步(保存代码文件、编译代码,尽量保证编程可以跨平台运行,可以在linux也可以在windows项目运行),清理掉一些冗余的内容(例如黑白名单等),参考:

public class JavaDockerCodeSandbox implements CodeSandbox {

    // 全局文件存储文件夹定义
    private static final String GLOBAL_CODE_DIR_NAME = "tmpCode";

    // 存储的文件名定义
    private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";

    // 设置超时控制时长
    private static final long TIME_OUT = 5000L;

    // 自定义安全管理器存储路径
    private static final String SECURITY_MANAGER_PATH = "E:\\workspace\\Git\\github\\PROJECT\\noob\\oj-platform\\oj-code-sandbox\\src\\main\\resources\\security";

    // 自定义安全管理器类名
    private static final String SECURITY_MANAGER_CLASS_NAME = "MySecurityManager";

    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {

        List<String> inputList = executeCodeRequest.getInputList();
        String code = executeCodeRequest.getCode();
        String language = executeCodeRequest.getLanguage();

        // 1)把用户的代码保存为文件
        String userDir = System.getProperty("user.dir");
        String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
        // 判断全局代码目录是否存在,没有则新建
        if (!FileUtil.exist(globalCodePathName)) {
            FileUtil.mkdir(globalCodePathName);
        }

        // 把用户的代码隔离存放
        String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
        String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
        File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);


        // 2)编译代码,得到 class 文件
        String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
        try {
            Process compileProcess = Runtime.getRuntime().exec(compileCmd);
            // 自定义ProcessUtils获取控制台输出(通过 exitValue 判断程序是否正常返回,通过 inputStream 和 errorStream 获取控制台输出)
            // 方式1:通过args方式输入参数(项目核心)
            ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
            System.out.println(executeMessage);
        } catch (Exception e) {
            return getErrorResponse(e);
        }



        // todo 封装响应结果
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        // 6)错误处理,提升程序健壮性(通过自定义封装getErrorResponse错误处理方法,当程序抛出异常时,直接返回错误响应)参考getErrorResponse方法

        // 返回执行代码结果响应
        return executeCodeResponse;
    }


    /**
     * 获取错误响应
     *
     * @param e
     * @return
     */
    private ExecuteCodeResponse getErrorResponse(Throwable e) {
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        executeCodeResponse.setOutputList(new ArrayList<>());
        executeCodeResponse.setMessage(e.getMessage());
        // 表示代码沙箱错误
        executeCodeResponse.setStatus(2);
        executeCodeResponse.setJudgeInfo(new JudgeInfo());
        return executeCodeResponse;
    }
}

1.创建容器,上传编译文件

自定义容器的两种方式:

  • 在已有镜像的基础上再扩充:比如拉取线程的 Java 环境(包含 jdk),再把编译后的文件复制到容器里。适合新项目,跑通流程
  • 完全自定义容器:适合比较成熟的项目,比如封装多个语言的环境和实现

思考:每个测试用例都单独创建一个容器,每个容器只执行一次 Java 命令吗?浪费性能,所以需要创建一个 可交互 的容器,能接受多次输入并且输出

// 创建容器的时候,可以指定文件路径(Volumn)映射,作用是把本地的文件同步到容器中,可以让容器访问。也可以叫容器挂载目录
HostConfig hostConfig = new HostConfig();
hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app")));// 容器映射,一般不要放在根目录

// 此处userCodeParentPath为用户代码存放位置,对应映射到linux服务器的/app下,因此实际Docker执行的是/app/xxx/Main.java文件内容 

​ 编写完创建容器的逻辑,然后执行main方法测试一下镜像能否正常拉取,linux通过docker images指令查看

noob@noob-virtual-machine:~$ docker images
REPOSITORY   TAG        IMAGE ID       CREATED       SIZE
nginx        latest     7383c266ef25   11 days ago   188MB
openjdk      8-alpine   a3562aa0b991   4 years ago   105MB
public class JavaDockerCodeSandbox implements CodeSandbox {

    // 全局文件存储文件夹定义
    private static final String GLOBAL_CODE_DIR_NAME = "tmpCode";

    // 存储的文件名定义
    private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";

    // 设置超时控制时长
    private static final long TIME_OUT = 5000L;

    // 自定义安全管理器存储路径
    private static final String SECURITY_MANAGER_PATH = "E:\\workspace\\Git\\github\\PROJECT\\noob\\oj-platform\\oj-code-sandbox\\src\\main\\resources\\security";

    // 自定义安全管理器类名
    private static final String SECURITY_MANAGER_CLASS_NAME = "MySecurityManager";

    // 定义初始化
    private static final Boolean FIRST_INIT = true;

    public static void main(String[] args) {

        JavaDockerCodeSandbox javaNativeCodeSandbox = new JavaDockerCodeSandbox();
        ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest();
        executeCodeRequest.setInputList(Arrays.asList("1 2", "1 3"));
        String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
        executeCodeRequest.setCode(code);
        executeCodeRequest.setLanguage("java");
        ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
        System.out.println(executeCodeResponse);

    }


    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {

        List<String> inputList = executeCodeRequest.getInputList();
        String code = executeCodeRequest.getCode();
        String language = executeCodeRequest.getLanguage();

        // 1)把用户的代码保存为文件
        String userDir = System.getProperty("user.dir");
        String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
        // 判断全局代码目录是否存在,没有则新建
        if (!FileUtil.exist(globalCodePathName)) {
            FileUtil.mkdir(globalCodePathName);
        }

        // 把用户的代码隔离存放
        String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
        String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
        File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);


        // 2)编译代码,得到 class 文件
        String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
        try {
            Process compileProcess = Runtime.getRuntime().exec(compileCmd);
            // 自定义ProcessUtils获取控制台输出(通过 exitValue 判断程序是否正常返回,通过 inputStream 和 errorStream 获取控制台输出)
            // 方式1:通过args方式输入参数(项目核心)
            ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
            System.out.println(executeMessage);
        } catch (Exception e) {
            return getErrorResponse(e);
        }

        // 3)创建容器,把编译好的文件上传到容器环境内
        // 获取默认的 Docker Client
        DockerClient dockerClient = DockerClientBuilder.getInstance().build();
        String image = "openjdk:8-alpine";
        if(FIRST_INIT){
            // 初始化镜像(不用每次都去拉取镜像,只需要初始化一次)
            PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
            PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
                @Override
                public void onNext(PullResponseItem item) {
                    System.out.println("下载镜像:" + item.getStatus());
                    super.onNext(item);
                }
            };
            try {
                pullImageCmd
                        .exec(pullImageResultCallback)
                        .awaitCompletion();
            } catch (InterruptedException e) {
                System.out.println("拉取镜像异常");
                throw new RuntimeException(e);
            }
            System.out.println("下载完成");
        }

        // 创建容器
        CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
        HostConfig hostConfig = new HostConfig();
        hostConfig.withMemory(100 * 1000 * 1000L); // 限制内存
        hostConfig.withMemorySwap(0L);
        hostConfig.withCpuCount(1L); // 限制CPU
        hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app"))); // 容器映射,一般不要放在根目录
        CreateContainerResponse createContainerResponse = containerCmd
                .withHostConfig(hostConfig)
                .withNetworkDisabled(true)
                .withReadonlyRootfs(true)
                .withAttachStdin(true)
                .withAttachStderr(true)
                .withAttachStdout(true)
                .withTty(true)
                .exec();
        System.out.println(createContainerResponse);
        String containerId = createContainerResponse.getId();

        // todo 封装响应结果
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        // 6)错误处理,提升程序健壮性(通过自定义封装getErrorResponse错误处理方法,当程序抛出异常时,直接返回错误响应)参考getErrorResponse方法

        // 返回执行代码结果响应
        return executeCodeResponse;
    }


    /**
     * 获取错误响应
     *
     * @param e
     * @return
     */
    private ExecuteCodeResponse getErrorResponse(Throwable e) {
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        executeCodeResponse.setOutputList(new ArrayList<>());
        executeCodeResponse.setMessage(e.getMessage());
        // 表示代码沙箱错误
        executeCodeResponse.setStatus(2);
        executeCodeResponse.setJudgeInfo(new JudgeInfo());
        return executeCodeResponse;
    }
}

​ 查看容器状态:此时会发现容器状态为Created(创建),控制台

noob@noob-virtual-machine:~$ docker ps -a
CONTAINER ID   IMAGE              COMMAND                   CREATED             STATUS                         PORTS     NAMES
cfc50fee1d0b   openjdk:8-alpine   "/bin/sh"                 8 minutes ago       Created                                  recursing_torvalds
6d71f09b603f   openjdk:8-alpine   "/bin/sh"                 11 minutes ago      Created                                  unruffled_bohr
8e10b5f4d26d   nginx:latest       "/docker-entrypoint.…"   13 minutes ago      Created                                  recursing_boyd
fe2b3f4c98f7   nginx:latest       "/docker-entrypoint.…"   51 minutes ago      Exited (0) 51 minutes ago                competent_mclean

​ 测试容器启动,发现容器无法正常启动,报错提示:Exception in thread "main" com.github.dockerjava.api.exception.InternalServerErrorException: Status 500: Decoding seccomp profile failed: invalid character 'å' looking for beginning of value

image-20240505142227762

​ 如果遇到容器启动报错500(服务器内部错误),按照提示信息去跟踪错误:参考上图错误提示,这个错误表明Docker在尝试启动一个容器时遇到了问题。Status 500是一个通用的HTTP状态码,表示服务器内部错误。具体到Docker,这通常意味着Docker守护进程(daemon)遇到了一个预料之外的错误。在这个错误消息中,"Decoding seccomp profile failed: invalid character"暗示Docker在解码Seccomp(安全计算模式)配置文件时遇到了格式错误或无效字符。

​ Seccomp是一种用来限制容器进程可调用的系统调用的机制。Docker使用Seccomp来提供容器的隔离性和安全性。

解决方法:

1)检查Docker版本:确保你的Docker版本是最新的,或者至少是一个支持你当前操作的版本

2)检查容器配置:如果你在启动容器时指定了特定的Seccomp配置文件,请检查该文件的格式是否正确,没有无效字符

3)重启Docker服务:有时候,简单的重启Docker服务可以解决临时的问题

4)查看Docker日志:检查Docker守护进程的日志文件,通常位于/var/log/docker.log或通过journalctl -u docker.service命令查看,以获取更多错误信息

5)如果问题依然存在,考虑在Docker社区或者GitHub仓库提交issue,提供错误报告和相关日志文件,以便开发者们可以查看并修复问题

在进行任何修改前,请确保备份重要数据和配置,以防需要恢复到原始状态

​ 调整完上述错误,再次查看容器状态:此时会发现通过openjdk:8-alpine镜像创建的状态不是退出EXit...,这是因为通过Java代码控制创建的是一个tty(对应containerCmd.withTty(true))可交互的命令行程序(对应COMMAND:/bin/sh),它相当于会在后台启动一个命令行工具

CONTAINER ID   IMAGE              COMMAND                   CREATED              STATUS                         PORTS     NAMES
d08b5c400e7f   openjdk:8-alpine   "/bin/sh"                 About a minute ago   Up About a minute                        loving_payne
3f637103930a   openjdk:8-alpine   "/bin/sh"                 7 minutes ago        Up 7 minutes                             nostalgic_clarke
40b76c3266f9   openjdk:8-alpine   "/bin/sh"                 11 minutes ago       Created                                  elated_williamson
fe2b3f4c98f7   nginx:latest       "/docker-entrypoint.…"   About an hour ago    Exited (0) About an hour ago             competent_mclean

2.启动容器,执行代码

Docker执行命令(操作已启动容器)

docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

Options:
  -d, --detach               Detached mode: run command in the background
      --detach-keys string   Override the key sequence for detaching a container
  -e, --env list             Set environment variables
      --env-file list        Read in a file of environment variables
  -i, --interactive          Keep STDIN open even if not attached
      --privileged           Give extended privileges to the command
  -t, --tty                  Allocate a pseudo-TTY
  -u, --user string          Username or UID (format: "<name|uid>[:<group|gid>]")
  -w, --workdir string       Working directory inside the container

参考示例:例如此处拉取了JDK8镜像,并创建了容器,然后使用它来执行代码

docker exec [容器名称] java -cp /app Main 1 3

# 以上述创建好的容器为参考,选择一个容器执行代码
docker exec loving_payne java -cp /app Main 1 3
结果:4

​ 注意,要把命令按照空格拆分,作为一个数组传递,否则可能会被识别为一个字符串,而不是多个参数。完善步骤4)中的内容,实现启动容器,执行指定代码

		// 4)启动容器,执行代码
        dockerClient.startContainerCmd(containerId).exec();

        // 创建命令:docker exec [容器名称] java -cp /app Main 1 3
        for (String inputArgs : inputList) {
            String[] inputArgsArray = inputArgs.split(" ");
            String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray);
            ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
                    .withCmd(cmdArray)
                    .withAttachStderr(true)
                    .withAttachStdin(true)
                    .withAttachStdout(true)
                    .exec();
            System.out.println("创建执行命令:" + execCreateCmdResponse);
            String execId = execCreateCmdResponse.getId();
            ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {
                @Override
                public void onNext(Frame frame) {
                    //
                    StreamType streamType = frame.getStreamType();
                    if (StreamType.STDERR.equals(streamType)) {
                        System.out.println("输出错误结果:" + new String(frame.getPayload()));
                    } else {
                        System.out.println("输出结果:" + new String(frame.getPayload()));
                    }
                    super.onNext(frame);
                }
            };
            try {
                // 将回调结果传递过来,并且阻塞等待日志输出
                dockerClient.execStartCmd(execId)
                        .exec(execStartResultCallback)
                        .awaitCompletion();
            } catch (InterruptedException e) {
                System.out.println("程序执行异常");
                throw new RuntimeException(e);
            }
        }

​ 编写完成,启动查看控制台信息确认命令是否可以正常执行。尽量复用之前的ExecuteMessage模式,在异步接口中填充正常信息和异常信息,进一步优化打印执行信息,定义一个List<ExecuteMessage> executeMessageList存放每条命令执行结果消息。此处需要注意一点,在内部类中使用外部的变量不能直接使用普通变量,可以根据idea提示将他调整为引用类型即可

image-20240505145328573

🚀获取程序执行时间:和Java原生一样,使用StopWatch在执行前后统计时间

stopWatch.start();
dockerClient.execStartCmd(execId)
        .exec(execStartResultCallback)
        .awaitCompletion(TIME_OUT, TimeUnit.MILLISECONDS);
stopWatch.stop();
time = stopWatch.getLastTaskTimeMillis();

image-20240505150115546

🚀获取程序占用内存:

程序占用的内存每个时刻都在变化,不可能获取到所有时间点的内存。因此此处设定:定义一个周期,定期的获取内存

参考示例:

// 获取占用的内存
StatsCmd statsCmd = dockerClient.statsCmd(containerId);
ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {
    @Override
    public void onNext(Statistics statistics) {
        System.out.println("内存占用:" + statistics.getMemoryStats().getUsage());
        maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
    }

    @Override
    public void onStart(Closeable closeable) {

    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onComplete() {

    }

    @Override
    public void close() throws IOException {

    }
});
statsCmd.exec(statisticsResultCallback);



--------------------------------------------------------
// 记得关闭(否则可能一直输出)
statsCmd.close();

注意程序执行完毕之后要关闭统计命令:statsCmd.close()

此处多次测试结果,发现测试每次输出的结果不太一样,有时候是一条、有时候是两条,可能和异步执行操作有关

image-20240505151834154

3.封装响应结果、清理文件(和原生实现基本一致)

	    // 5)封装响应结果(和此前的原生Java方式完全一致)
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        List<String> outputList = new ArrayList<>();
        // 取用时最大值,便于判断是否超时
        long maxTime = 0;
        for (ExecuteMessage executeMessage : executeMessageList) {
            String errorMessage = executeMessage.getErrorMessage();
            if (StrUtil.isNotBlank(errorMessage)) {
                executeCodeResponse.setMessage(errorMessage);
                // 用户提交的代码执行中存在错误
                executeCodeResponse.setStatus(3);
                break;
            }
            outputList.add(executeMessage.getMessage());
            Long time = executeMessage.getTime();
            if (time != null) {
                maxTime = Math.max(maxTime, time);
            }
        }
        // 正常运行完成
        if (outputList.size() == executeMessageList.size()) {
            executeCodeResponse.setStatus(1);
        }
        executeCodeResponse.setOutputList(outputList);
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setTime(maxTime);
        // todo 要借助第三方库来获取内存占用,非常麻烦,此处不做实现
        // judgeInfo.setMemory();
        executeCodeResponse.setJudgeInfo(judgeInfo);

        // 6)文件清理
        if (userCodeFile.getParentFile() != null) {
            boolean del = FileUtil.del(userCodeParentPath);
            System.out.println("删除" + (del ? "成功" : "失败"));
        }

最终基于Docker构建初稿参考如下:

package com.noob.oj.codesandbox;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.noob.oj.codesandbox.model.ExecuteCodeRequest;
import com.noob.oj.codesandbox.model.ExecuteCodeResponse;
import com.noob.oj.codesandbox.model.ExecuteMessage;
import com.noob.oj.codesandbox.model.JudgeInfo;
import com.noob.oj.codesandbox.utils.ProcessUtils;

import org.springframework.util.StopWatch;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

/**
 * @ClassName JavaDockerCodeSandbox
 * @Description JavaDocker沙箱实现
 * @Author holic-x
 * @Date 2024/5/3 19:08
 */
public class JavaDockerCodeSandbox implements CodeSandbox {

    // 全局文件存储文件夹定义
    private static final String GLOBAL_CODE_DIR_NAME = "tmpCode";

    // 存储的文件名定义
    private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";

    // 设置超时控制时长
    private static final long TIME_OUT = 5000L;

    // 自定义安全管理器存储路径
    private static final String SECURITY_MANAGER_PATH = "E:\\workspace\\Git\\github\\PROJECT\\noob\\oj-platform\\oj-code-sandbox\\src\\main\\resources\\security";

    // 自定义安全管理器类名
    private static final String SECURITY_MANAGER_CLASS_NAME = "MySecurityManager";

    // 定义初始化
    private static final Boolean FIRST_INIT = true;

    public static void main(String[] args) {

        JavaDockerCodeSandbox javaNativeCodeSandbox = new JavaDockerCodeSandbox();
        ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest();
        executeCodeRequest.setInputList(Arrays.asList("1 2", "1 3"));
        String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
        executeCodeRequest.setCode(code);
        executeCodeRequest.setLanguage("java");
        ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
        System.out.println(executeCodeResponse);

    }


    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {

        List<String> inputList = executeCodeRequest.getInputList();
        String code = executeCodeRequest.getCode();
        String language = executeCodeRequest.getLanguage();

        // 1)把用户的代码保存为文件
        String userDir = System.getProperty("user.dir");
        String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
        // 判断全局代码目录是否存在,没有则新建
        if (!FileUtil.exist(globalCodePathName)) {
            FileUtil.mkdir(globalCodePathName);
        }

        // 把用户的代码隔离存放
        String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
        String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
        File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);


        // 2)编译代码,得到 class 文件
        String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
        try {
            Process compileProcess = Runtime.getRuntime().exec(compileCmd);
            // 自定义ProcessUtils获取控制台输出(通过 exitValue 判断程序是否正常返回,通过 inputStream 和 errorStream 获取控制台输出)
            // 方式1:通过args方式输入参数(项目核心)
            ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
            System.out.println(executeMessage);
        } catch (Exception e) {
            return getErrorResponse(e);
        }

        // 3)创建容器,把编译好的文件上传到容器环境内
        // 获取默认的 Docker Client
        DockerClient dockerClient = DockerClientBuilder.getInstance().build();
        String image = "openjdk:8-alpine";
        if (FIRST_INIT) {
            // 初始化镜像(不用每次都去拉取镜像,只需要初始化一次)
            PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
            PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
                @Override
                public void onNext(PullResponseItem item) {
                    System.out.println("下载镜像:" + item.getStatus());
                    super.onNext(item);
                }
            };
            try {
                pullImageCmd
                        .exec(pullImageResultCallback)
                        .awaitCompletion();
            } catch (InterruptedException e) {
                System.out.println("拉取镜像异常");
                throw new RuntimeException(e);
            }
            System.out.println("下载完成");
        }

        // 创建容器
        CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
        HostConfig hostConfig = new HostConfig();
        hostConfig.withMemory(100 * 1000 * 1000L); // 限制内存
        hostConfig.withMemorySwap(0L);
        hostConfig.withCpuCount(1L); // 限制CPU
//        hostConfig.withSecurityOpts(Arrays.asList("seccomp=安全管理配置字符串"));
        hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app"))); // 容器映射,一般不要放在根目录
        CreateContainerResponse createContainerResponse = containerCmd
                .withHostConfig(hostConfig)
                .withNetworkDisabled(true)
                .withReadonlyRootfs(true)
                .withAttachStdin(true)
                .withAttachStderr(true)
                .withAttachStdout(true)
                .withTty(true)
                .exec();
        System.out.println(createContainerResponse);
        String containerId = createContainerResponse.getId();

        // 4)启动容器,执行代码
        dockerClient.startContainerCmd(containerId).exec();

        // 定义执行结果消息列表
        List<ExecuteMessage> executeMessageList = new ArrayList<>();
        // 创建命令:docker exec [容器名称] java -cp /app Main 1 3
        for (String inputArgs : inputList) {
            // 定义计时器
            StopWatch stopWatch = new StopWatch();
            long time = 0L;

            String[] inputArgsArray = inputArgs.split(" ");
            String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray);
            ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
                    .withCmd(cmdArray)
                    .withAttachStderr(true)
                    .withAttachStdin(true)
                    .withAttachStdout(true)
                    .exec();
            System.out.println("创建执行命令:" + execCreateCmdResponse);

            ExecuteMessage executeMessage = new ExecuteMessage();
            final String[] message = {null};
            final String[] errorMessage = {null};

            String execId = execCreateCmdResponse.getId();
            ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {
                @Override
                public void onNext(Frame frame) {
                    //
                    StreamType streamType = frame.getStreamType();
                    if (StreamType.STDERR.equals(streamType)) {
                        errorMessage[0] = new String(frame.getPayload());
                        System.out.println("输出错误结果:" + errorMessage[0]);
                    } else {
                        message[0] = new String(frame.getPayload());
                        System.out.println("输出结果:" + message[0]);
                    }
                    super.onNext(frame);
                }
            };

            final long[] maxMemory = {0L};
            // 获取占用的内存
            StatsCmd statsCmd = dockerClient.statsCmd(containerId);
            ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {

                @Override
                public void onNext(Statistics statistics) {
                    System.out.println("内存占用:" + statistics.getMemoryStats().getUsage());
                    maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
                }

                @Override
                public void close() throws IOException {

                }

                @Override
                public void onStart(Closeable closeable) {

                }

                @Override
                public void onError(Throwable throwable) {

                }

                @Override
                public void onComplete() {

                }
            });
            statsCmd.exec(statisticsResultCallback);

            try {
                stopWatch.start();
                // 将回调结果传递过来,并且阻塞等待日志输出
                dockerClient.execStartCmd(execId)
                        .exec(execStartResultCallback)
                        .awaitCompletion();
                stopWatch.stop();
                time = stopWatch.getLastTaskTimeMillis();

                // 关闭(否则可能一直输出)
                statsCmd.close();

            } catch (InterruptedException e) {
                System.out.println("程序执行异常");
                throw new RuntimeException(e);
            }

            // 设置执行信息
            executeMessage.setMessage(message[0]);
            executeMessage.setErrorMessage(errorMessage[0]);
            executeMessage.setTime(time);
            executeMessage.setMemory(maxMemory[0]);
            // 将执行信息加入列表
            executeMessageList.add(executeMessage);
        }

        // 5)封装响应结果(和此前的原生Java方式完全一致)
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        List<String> outputList = new ArrayList<>();
        // 取用时最大值,便于判断是否超时
        long maxTime = 0;
        for (ExecuteMessage executeMessage : executeMessageList) {
            String errorMessage = executeMessage.getErrorMessage();
            if (StrUtil.isNotBlank(errorMessage)) {
                executeCodeResponse.setMessage(errorMessage);
                // 用户提交的代码执行中存在错误
                executeCodeResponse.setStatus(3);
                break;
            }
            outputList.add(executeMessage.getMessage());
            Long time = executeMessage.getTime();
            if (time != null) {
                maxTime = Math.max(maxTime, time);
            }
        }
        // 正常运行完成
        if (outputList.size() == executeMessageList.size()) {
            executeCodeResponse.setStatus(1);
        }
        executeCodeResponse.setOutputList(outputList);
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setTime(maxTime);
        // todo 要借助第三方库来获取内存占用,非常麻烦,此处不做实现
        // judgeInfo.setMemory();
        executeCodeResponse.setJudgeInfo(judgeInfo);

        // 6)文件清理
        if (userCodeFile.getParentFile() != null) {
            boolean del = FileUtil.del(userCodeParentPath);
            System.out.println("删除" + (del ? "成功" : "失败"));
        }

        // 7)错误处理,提升程序健壮性(通过自定义封装getErrorResponse错误处理方法,当程序抛出异常时,直接返回错误响应)参考getErrorResponse方法

        // 返回执行代码结果响应
        return executeCodeResponse;
    }

    /**
     * 获取错误响应
     *
     * @param e
     * @return
     */
    private ExecuteCodeResponse getErrorResponse(Throwable e) {
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        executeCodeResponse.setOutputList(new ArrayList<>());
        executeCodeResponse.setMessage(e.getMessage());
        // 表示代码沙箱错误
        executeCodeResponse.setStatus(2);
        executeCodeResponse.setJudgeInfo(new JudgeInfo());
        return executeCodeResponse;
    }
}

Docker代码沙箱优化

​ 参考基于Java原生方式构建,针对各种异常代码攻击来优化代码实现

1.超时控制

实现思路

  • 设置容器的超时控制
  • 判断是否超时

实现说明

执行容器时,可以增加超时参数控制值:awaitCompletion的TIMEOUT参数配置(如果执行超时则退掉)

dockerClient.execStartCmd(execId)
                        .exec(execStartResultCallback)
                        .awaitCompletion(TIME_OUT, TimeUnit.MILLISECONDS);

但是,这种方式无论超时与否,都会往下执行,无法判断是否超时

​ 解决方案:**可以定义一个标志,如果程序执行完成,把标志设置为false。**即假设初始状态是超时的,如果执行回调完成则说明没有超时(因为如果超时了会受到上面的超时控制限制,如果是正常完成则会通过回调函数把这个超时参数给“纠正”过来,进而可以得到一个可以判断是否超时的参数(类似于设置它是否完成概念))

示例代码如下:

// 判断是否超时
final boolean[] timeout = {true};
String execId = execCreateCmdResponse.getId();
ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {

    @Override
    public void onComplete() {
        // 如果执行完成,则表示没超时
        timeout[0] = false;
        super.onComplete();
    }

    ...
};

2.内存资源

​ 创建容器的时候可以指定内容限制,通过hostConfig的withMemory方法设置,hostConfig.withMemory(100 * 1000 * 1000L);

CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
HostConfig hostConfig = new HostConfig();
hostConfig.withMemory(100 * 1000 * 1000L);
hostConfig.withMemorySwap(0L);
hostConfig.withCpuCount(1L);
CreateContainerResponse createContainerResponse = containerCmd
        .withHostConfig(hostConfig)
        .exec();

3.限制网络资源

创建容器的时候可以设置网络配置为管理:

CreateContainerResponse createContainerResponse = containerCmd
                .withHostConfig(hostConfig)
                .withNetworkDisabled(true)

4.权限管理

Docker 容器已经做了系统层面的隔离,比较安全,但不能保证绝对安全

  • 结合 Java 安全管理器去使用
  • 限制用户不能向 root 根目录写文件
CreateContainerResponse createContainerResponse = containerCmd
        .withHostConfig(hostConfig)
        .withNetworkDisabled(true)
        .withReadonlyRootfs(true)
  • Linux 自带的一些安全管理措施 seccomp(Security Compute Mode),是一个用于 Linux 内核的安全功能,它允许你限制进程可以执行的系统调用,从而减少潜在的攻击面和提高容器的安全性。通过配置 seccomp,可以控制容器内进程可以使用的系统调用类型和参数

    hostConfig.withSecurityOpts(Arrays.asList("seccomp=安全管理配置字符串"));
    
# 示例 seccomp 配置文件 profile.json
{
  "defaultAction": "SCMP_ACT_ALLOW""syscalls" : [
    {
      "name": "write",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "read",
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

​ 在 hostConfig 中开启安全机制:可以将profile.json放在resources下,然后通过ResourceUtil读取文件内容,然后通过hostConfig进行配置拼接(或者直接把json字符串拼接到字符串后面)

String profileConfig = ResourceUtil.readUtf8Str("profile.json");
hostConfig.withSecurityOpts(Arrays.asList("seccomp=" + profileConfig));
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3