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

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

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

traefik

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

Docker 环境下使用 Traefik 3 的最佳实践:快速上手

Traefik 最近终于发布了大版本升级后的第一个修正版本,或许是时候正式迁移程序到新版本了。

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

手动实现的部分

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

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

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

Python
# monitor.pyimport dockerimport yamlimport osimport timeOUTPUT_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 Nonedef 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设置来读取用户对每个单独容器的设置(是否转发,转发哪些端口)

Python
# monitor.pyimport dockerimport yamlimport osimport jsonimport hashlibOUTPUT_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 = Nonedef 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 Nonedef 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 Falsedef 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_infodef 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 & ,就可以使用了!