特别是在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 & ,就可以使用了!