特别是在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的文档。于是就有了第一版
同时,使用和traefik一样的label设置来读取用户对每个单独容器的设置(是否转发,转发哪些端口)
# 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 & ,就可以使用了!