fcmc 0.9.2

A CLI tool to check whether CTF challenge meta.toml configs are valid, and optionally test Docker setup
Documentation
use std::{collections::HashMap, path::PathBuf};

use bollard::{
    Docker,
    network::CreateNetworkOptions,
    query_parameters::{
        CreateContainerOptions, CreateContainerOptionsBuilder, StartContainerOptions,
    },
    secret::{ContainerCreateBody, EndpointIpamConfig, Ipam},
};
use clap::builder::Str;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct GameBoxMeta {
    pub name: String,
    pub author: String,
    pub category: String,
    pub description: String,
    pub gamebox: GameBoxConfig,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct GameBoxConfig {
    pub username: String,
    pub image_tag: String,
    pub break_point: f32,
    pub fix_point: f32,
    pub down_point: f32,
    pub first_bouns: f32,
}

impl GameBoxMeta {
    pub fn from_toml_str(toml: &str) -> Result<Self, toml::de::Error> {
        toml::from_str(toml)
    }

    // 用户名 & 密码
    pub async fn create_and_start(
        &self,
        docker: &Docker,
        identifier: &str,
        net_bridge: Option<String>,
        container_ip: Option<String>,
        ctf_password: String,
    ) -> Result<(), Box<dyn std::error::Error>> {
        // docker run --rm -d --network=br-awd --ip 172.20.2.100 --name ctf-target-team2 floatctf/ctf-target
        let container_name = format!("{}", identifier);
        let options = CreateContainerOptionsBuilder::new()
            .name(&container_name)
            .build();
        let network_name = net_bridge.unwrap_or_else(|| "bridge".to_string());
        let config = ContainerCreateBody {
            image: Some(self.gamebox.image_tag.clone()),
            env: Some(vec![
                format!("{}={}", "CTF_USER", self.gamebox.username),
                format!("{}={}", "CTF_PASSWORD", ctf_password),
            ]),
            host_config: Some(bollard::models::HostConfig {
                network_mode: network_name.clone().into(),
                ..Default::default()
            }),
            networking_config: Some(bollard::models::NetworkingConfig {
                endpoints_config: Some(std::collections::HashMap::from([(
                    network_name,
                    bollard::models::EndpointSettings {
                        ipam_config: Some(EndpointIpamConfig {
                            ipv4_address: container_ip,
                            ..Default::default()
                        }),
                        ..Default::default()
                    },
                )])),
                ..Default::default()
            }),
            ..Default::default()
        };

        let container = docker.create_container(Some(options), config).await?;
        docker
            .start_container(&container.id, None::<StartContainerOptions>)
            .await?;

        // connect network
        Ok(())
    }

    pub async fn generate_basic_template(name: &str, output_dir: &str) -> anyhow::Result<()> {
        use std::fs;
        use std::path::Path;

        // 创建挑战目录
        let gamebox_dir = Path::new(output_dir).join(name);
        fs::create_dir_all(&gamebox_dir)?;

        // 创建src目录
        let src_dir = gamebox_dir.join("src");
        fs::create_dir_all(&src_dir)?;

        let meta_content = r#"name = "awd-base"
author = "fb0sh@outlook.com"
category = "Web"
description = "awd-base"


[gamebox]
username = "floatctf"
image_tag = "floatctf/awd-base:gamebox-web_v1.0.0"
break_point = 100.0 # 直接转给攻击者 每轮
fix_point = 100.0 # 裁判的防御 分数 每轮 裁判不扣分
down_point = 200.0 # 宕机扣分 每轮
first_bouns = 0.2
# 攻击得分,被攻击的扣分,裁判的攻击防守不扣分,宕机扣分
"#;
        fs::write(gamebox_dir.join("meta.toml"), meta_content)?;

        let entrypoint_content = r#"#!/bin/bash
set -e

# 1. 动态同步账户 (根据 Bollard 传来的 ENV)
USERNAME=${CTF_USER:-"floatctf"}
if [ "$USERNAME" != "floatctf" ]; then
    usermod -l "$USERNAME" floatctf || useradd -m -s /bin/bash "$USERNAME"
fi

# 2. 设置密码
if [ -n "$CTF_PASSWORD" ]; then
    echo "$USERNAME:$CTF_PASSWORD" | chpasswd
fi

# 3. 权限修正 (AWD 灵魂操作)
chown -R "$USERNAME":"$USERNAME" /var/www/html

# 4. 启动 Apache (后台) 并启动 SSH (前台接管)
rm -f /var/run/apache2/apache2.pid
service apache2 start

echo "[FloatCTF] Base Gamebox is UP. Good luck, hackers!"
exec "$@"
"#;
        fs::write(src_dir.join("entrypoint.sh"), entrypoint_content)?;

        let dockefile_content = r#"# 基础镜像 slim
FROM ubuntu:24.04

# 避免交互式安装
ENV DEBIAN_FRONTEND=noninteractive
ENV NeedRestartPriority=0

# 更新源并安装 SSH + 基础工具
RUN apt-get update && apt-get install -y --no-install-recommends \
    openssh-server \
    sudo \
    curl \
    wget \
    vim \
    iproute2 \
    net-tools \
    git \
    bash-completion \
    iputils-ping \
    procps \
    apache2 \
    php \
    libapache2-mod-php \
    php-curl \
    watch \
    && rm -rf /var/lib/apt/lists/*

# 创建 SSH 数据目录
RUN mkdir /var/run/sshd

# 创建 ctf 用户并设置密码
RUN useradd -m -s /bin/bash floatctf

# SSH 配置:允许 ctf 用户密码登录,禁止 root 登录
RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config \
    && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config

ENV APACHE_RUN_USER=www-data
ENV APACHE_RUN_GROUP=www-data
ENV APACHE_LOG_DIR=/var/log/apache2


COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# 默认启动 SSH
CMD ["/usr/sbin/sshd", "-D"]
"#;
        fs::write(src_dir.join("Dockerfile"), dockefile_content)?;

        println!("Gamebox 模板已生成: {}", gamebox_dir.display());
        Ok(())
    }

    pub async fn generate_template(name: &str, output_dir: &str) -> anyhow::Result<()> {
        use std::fs;
        use std::path::Path;

        // 创建挑战目录
        let gamebox_dir = Path::new(output_dir).join(name);
        fs::create_dir_all(&gamebox_dir)?;

        // 创建src目录
        let src_dir = gamebox_dir.join("src");
        fs::create_dir_all(&src_dir)?;

        let meta_content = format!(
            r#"name = "{}"
author = "your_email"
category = "Web"
description = "hello floatctf"


[gamebox]
username = "floatctf"
image_tag = "floatctf/hello-floatctf:gamebox-web_v1.0"
break_point = 100.0 # 直接转给攻击者 每轮
fix_point = 100.0 # 裁判的防御 分数 每轮 裁判不扣分
down_point = 200.0 # 宕机扣分 每轮
first_bouns = 0.2
# 攻击得分,被攻击的扣分,裁判的攻击防守不扣分,宕机扣分
"#,
            name
        );
        fs::write(gamebox_dir.join("meta.toml"), meta_content)?;

        let dockerfile_content = r#"FROM floatctf/awd-base:gamebox-web_v1.0.0

COPY index.php /var/www/html/index.php

RUN chown -R floatctf:floatctf /var/www/html
"#;
        fs::write(src_dir.join("Dockerfile"), dockerfile_content)?;

        let index_php_content = r#"<?php
// 获取用户输入的 URL
$url = $_GET['url'];
if (isset($url)) {
    // 1. 初始化 curl
    $ch = curl_init();

    // 2. 设置配置
    curl_setopt($ch, CURLOPT_URL, $url);           // 设置目标 URL
    curl_setopt($ch, CURLOPT_HEADER, 0);           // 不返回 header
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);   // 将结果返回成字符串而非直接输出

    // 3. 执行请求
    $result = curl_exec($ch);

    // 4. 关闭连接并输出结果
    curl_close($ch);
    echo $result;
} else {
    echo "Please usage: ?url=http://cn.bing.com";
}
?>
"#;
        fs::write(src_dir.join("index.php"), index_php_content)?;
        println!("Gamebox 模板已生成: {}", gamebox_dir.display());
        Ok(())
    }

    pub async fn build_image(&self, docker: &Docker, src_dir: &PathBuf) -> anyhow::Result<()> {
        let image_tag = self.gamebox.image_tag.clone();
        crate::build_image(&docker, &image_tag, src_dir).await?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[tokio::test]
    async fn test_create_and_start() {
        let docker = Docker::connect_with_local_defaults().unwrap();
        let meta = GameBoxMeta {
            name: "hello-floatctf".to_string(),
            author: "fb0sh@outlook.com".to_string(),
            category: "Web".to_string(),
            description: "hello floatctf".to_string(),
            gamebox: GameBoxConfig {
                username: "floatctf".to_string(),
                image_tag: "floatctf/hello-floatctf:gamebox-web_v1.0.0".to_string(),
                break_point: 100.0,
                fix_point: 100.0,
                down_point: 200.0,
                first_bouns: 0.2,
            },
        };
        crate::remove_and_create_bridge_net(
            &docker,
            "br-awd".to_string(),
            "172.20.0.0/16".to_string(),
        )
        .await
        .unwrap();
        meta.create_and_start(
            &docker,
            "gamebox-hello-floatctf",
            "br-awd".to_string().into(),
            "172.20.1.100".to_string().into(),
            "floatctf".to_string(),
        )
        .await
        .unwrap();
        tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
        crate::stop_and_remove(&docker, "gamebox-hello-floatctf")
            .await
            .unwrap();

        meta.create_and_start(
            &docker,
            "gamebox-hello-floatctf",
            "br-awd".to_string().into(),
            "172.20.1.100".to_string().into(),
            "floatctf".to_string(),
        )
        .await
        .unwrap();
    }
}