Logo
Overview

traefik折腾第二期

July 29, 2025

特别是在CTF比赛的时候,我们经常能看到靶机下发容器,然后容器顶着个自己的独立域名的情况。但是当我们dig或者nslookup看的时候这个ip又是同一套。这是为什么呢?

答案是用了泛域名:在root dns server上有这样一个配置*.host.domain,这样一个记录,它会把所有满足通配符的内容全部解析到一个服务器上。然后就可以走你的转发服务来处理内容了。

今天下午突发奇想,有没有可能通过一些简单配置,让我的服务也能做到这样,于是就试了试。

traefik

首先,我是不可能手写一套转发服务器拿出去用的。抛开性能不谈,我真的能用自己的简单编码技巧来完成一套非常复杂的server么?答案显然是不行,所以在现有多种solution中选择,耶无非就apache httpd,nginx这两个比较传统的,caddy,稍微新一点,以及现在k8s系统中都在使用的traefik可以用了。apache直接排除,我不是很喜欢用这个(看起来就好重啊)。nginx能找到docker-gen这个方案,caddy也是有对应的方案的,而traefik则是原生支持,这个一看就好用!那就它吧。

可以直接参考这篇文章配一套,这里略过不讲。

手动实现的部分

经过我一番调研,这个traefik原生支持的模板里只有docker容器的名字之类的,没有我非常想要的这个container id,所以决定自己手搓。

大概想了下,需要搓的也就这个逻辑:Docker事件监测(onStart:检查容器配置,手动修改配置)

这里我们用docker-py库来获得python的docker支持,用pyyaml生成yaml的文档。于是就有了第一版

# monitor.py

import docker
import yaml
import os
import time

OUTPUT_FILE = "./traefik-dynamic/routers.yaml"
ENTRYPOINT = "web"
DOMAIN_SUFFIX = "hk.domain"

client = docker.from_env()

def get_container_info(container):
    try:
        data = container.attrs
        short_id = container.id[:12]
        labels = data.get("Config", {}).get("Labels", {})
        network_settings = data.get("NetworkSettings", {})
        ip = list(network_settings.get("Networks", {}).values())[0].get("IPAddress")

        port = labels.get("traefik.port", "80")

        return {
            "id": short_id,
            "ip": ip,
            "port": int(port)
        }
    except Exception as e:
        print(f"[ERROR] Failed to inspect container {container.id}: {e}")
        return None

def write_dynamic_config(containers_info):
    routers = {}
    services = {}

    for info in containers_info:
        rid = f"router-{info['id']}"
        sid = f"service-{info['id']}"
        host = f"{info['id']}.{DOMAIN_SUFFIX}"

        routers[rid] = {
            "rule": f"Host(`{host}`)",
            "service": sid,
            "entryPoints": [ENTRYPOINT]
        }

        services[sid] = {
            "loadBalancer": {
                "servers": [
                    {"url": f"http://{info['ip']}:{info['port']}"}
                ]
            }
        }

    full_config = {
        "http": {
            "routers": routers,
            "services": services
        }
    }

    os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
    with open(OUTPUT_FILE, "w") as f:
        yaml.dump(full_config, f)
    print(f"[INFO] Wrote {len(containers_info)} entries to {OUTPUT_FILE}")

def main():
    print("[INFO] Starting Traefik dynamic config monitor...")

    running_containers = {}
    for container in client.containers.list():
        info = get_container_info(container)
        if info:
            running_containers[info["id"]] = info
    write_dynamic_config(running_containers.values())

    for event in client.events(decode=True):
        if event.get("Type") != "container":
            continue

        action = event.get("Action")
        cid = event.get("id")[:12]

        if action == "start":
            try:
                container = client.containers.get(cid)
                info = get_container_info(container)
                if info:
                    running_containers[cid] = info
                    print(f"[EVENT] Started: {cid}")
            except:
                pass
        elif action in ["die", "stop", "destroy"]:
            if cid in running_containers:
                print(f"[EVENT] Stopped: {cid}")
                running_containers.pop(cid, None)

        write_dynamic_config(running_containers.values())

if __name__ == "__main__":
    main()

同时,使用和traefik一样的label设置来读取用户对每个单独容器的设置(是否转发,转发哪些端口)

# monitor.py

import docker
import yaml
import os
import json
import hashlib

OUTPUT_FILE = "./traefik-dynamic/routers.yaml"
ENTRYPOINT = "web"
DOMAIN_SUFFIX = "hk.domain"
LABEL_KEY = "traefik.expose"
LABEL_VALUE = "true"
LABEL_PORT = "traefik.port"

client = docker.from_env()
last_config_hash = None


def get_container_info(container):
    try:
        data = container.attrs
        labels = data.get("Config", {}).get("Labels", {})

        if labels.get(LABEL_KEY, "false") != LABEL_VALUE:
            return None

        short_id = container.id[:12]
        networks = data.get("NetworkSettings", {}).get("Networks", {})
        ip = list(networks.values())[0].get("IPAddress")

        port = labels.get(LABEL_PORT, "80")

        return {
            "id": short_id,
            "ip": ip,
            "port": int(port)
        }
    except Exception as e:
        print(f"[ERROR] Failed to inspect container {container.id}: {e}")
        return None


def generate_config(containers_info):
    routers = {}
    services = {}

    for info in containers_info:
        rid = f"router-{info['id']}"
        sid = f"service-{info['id']}"
        host = f"{info['id']}.{DOMAIN_SUFFIX}"

        routers[rid] = {
            "rule": f"Host(`{host}`)",
            "service": sid,
            "entryPoints": [ENTRYPOINT]
        }

        services[sid] = {
            "loadBalancer": {
                "servers": [
                    {"url": f"http://{info['ip']}:{info['port']}"}
                ]
            }
        }

    return {
        "http": {
            "routers": routers,
            "services": services
        }
    }


def config_changed(new_config):
    global last_config_hash
    new_hash = hashlib.sha256(json.dumps(new_config, sort_keys=True).encode()).hexdigest()
    if new_hash != last_config_hash:
        last_config_hash = new_hash
        return True
    return False


def write_dynamic_config(config):
    os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
    with open(OUTPUT_FILE, "w") as f:
        yaml.dump(config, f)
    print(f"[INFO] Updated config with {len(config['http']['routers'])} routers")


def sync_all_containers():
    containers_info = []
    for container in client.containers.list():
        info = get_container_info(container)
        if info:
            containers_info.append(info)
    return containers_info


def main():
    print("[INFO] Starting Traefik dynamic monitor...")

    running_containers = {}

    # 初始同步
    for container in client.containers.list():
        info = get_container_info(container)
        if info:
            running_containers[info["id"]] = info

    current_config = generate_config(running_containers.values())
    if config_changed(current_config):
        write_dynamic_config(current_config)

    # 实时监听
    for event in client.events(decode=True):
        if event.get("Type") != "container":
            continue

        action = event.get("Action")
        cid = event.get("id")[:12]

        changed = False

        if action == "start":
            try:
                container = client.containers.get(cid)
                info = get_container_info(container)
                if info:
                    running_containers[cid] = info
                    print(f"[EVENT] Start: {cid}")
                    changed = True
            except:
                pass

        elif action in ["die", "stop", "destroy"]:
            if cid in running_containers:
                print(f"[EVENT] Stop: {cid}")
                running_containers.pop(cid, None)
                changed = True

        if changed:
            current_config = generate_config(running_containers.values())
            if config_changed(current_config):
                write_dynamic_config(current_config)


if __name__ == "__main__":
    main()

接着你也可以加一些自定义的配置什么的,其他配置也都可以用!

运行

运行的话,需要把config挂到/etc/traefik/config的目录下,然后nohup python monitor.py & ,就可以使用了!

comment

留言 / 评论

如果暂时没有看到评论,请点击下方按钮重新加载。