use std::collections::HashMap;
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>> {
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?;
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)?;
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)?;
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(())
}
}
#[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();
}
}