import socket
import struct
import argparse
import random
import string
import time
import threading
from collections import defaultdict
" python mdns_responder.py --iface_ip 192.168.0.100 "
MDNS_ADDR = "224.0.0.251"
MDNS_PORT = 5353
class DNSError(Exception):
"""DNS错误基类"""
pass
def generate_random_service_id(length=8):
"""生成8位随机服务ID(大写字母+数字)"""
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
def generate_random_hostname_suffix(length=8):
"""生成随机主机名后缀(数字)"""
return ''.join(random.choices(string.digits, k=length))
def parse_dns_name(data: bytes, offset: int) -> tuple:
"""
解析DNS名称(支持压缩指针)
返回: (域名, 新偏移量)
"""
labels = []
traversed = set()
while offset < len(data):
length = data[offset]
offset += 1
if length == 0: # 结束标记
break
# 处理压缩指针
if length & 0xC0 == 0xC0:
if offset >= len(data):
raise DNSError("压缩指针不完整")
ptr_offset = ((length & 0x3F) << 8) | data[offset]
offset += 1
if ptr_offset in traversed:
break
traversed.add(ptr_offset)
if ptr_offset >= len(data):
break
name_part, _ = parse_dns_name(data, ptr_offset)
labels.append(name_part)
break
# 处理普通标签
end = offset + length
if end > len(data):
break
labels.append(data[offset:end].decode('utf-8', 'ignore'))
offset = end
return '.'.join(labels), offset
def build_dns_name(name: str, compress_dict: dict, base_offset: int) -> bytes:
"""
构建DNS名称(支持压缩)
compress_dict: {域名: 偏移量}
base_offset: 当前数据起始偏移
"""
encoded = b''
parts = name.split('.')
for i in range(len(parts)):
partial = '.'.join(parts[i:])
# 应用压缩
if len(partial) > 3 and partial in compress_dict:
ptr = compress_dict[partial]
if ptr < 0x4000:
encoded += struct.pack('!H', 0xC000 | ptr)
return encoded
# 添加标签
label = parts[i]
if not 1 <= len(label) <= 63:
raise DNSError(f"无效标签长度: {len(label)}")
encoded += bytes([len(label)]) + label.encode('utf-8')
# 注册压缩点
current = '.'.join(parts[i:])
pos = base_offset + len(encoded) - len(label) - 1
if len(current) > 3 and current not in compress_dict:
compress_dict[current] = pos
encoded += b'\x00'
return encoded
def create_service_response(transaction_id: int,
service_type: str,
instance_name: str,
host_name: str,
ip: str,
port: int) -> bytes:
"""
创建单服务响应包
"""
compress_dict = {}
parts = []
current_offset = 12 # DNS头部长12字节
# === DNS头部 ===
flags = 0x8400 # QR=1, AA=1
qdcount = 0 # 无查询部分
ancount = 3 # PTR + SRV + A
header = struct.pack("!HHHHHH", transaction_id, flags, qdcount, ancount, 0, 0)
parts.append(header)
# === PTR记录 ===
ptr_name = build_dns_name(service_type, compress_dict, current_offset)
ptr_data = build_dns_name(instance_name, compress_dict, current_offset + len(ptr_name) + 10)
ptr_record = (
ptr_name +
struct.pack("!HHIH", 12, 1, 4500, len(ptr_data)) + # TYPE=PTR, CLASS=IN, TTL=120
ptr_data
)
parts.append(ptr_record)
current_offset += len(ptr_record)
# === SRV记录 ===
srv_name = build_dns_name(instance_name, compress_dict, current_offset)
srv_data = struct.pack("!HHH", 0, 0, port) # 优先级, 权重, 端口
srv_data += build_dns_name(host_name, compress_dict, current_offset + len(srv_name) + 10 + len(srv_data))
srv_record = (
srv_name +
struct.pack("!HHIH", 33, 1, 120, len(srv_data)) + # TYPE=SRV
srv_data
)
parts.append(srv_record)
current_offset += len(srv_record)
# === A记录 ===
a_name = build_dns_name(host_name, compress_dict, current_offset)
a_data = socket.inet_aton(ip)
a_record = (
a_name +
struct.pack("!HHIH", 1, 1, 120, 4) + # TYPE=A
a_data
)
parts.append(a_record)
return b''.join(parts)
def create_pair_response(transaction_id: int,
airplay_instance: str,
raop_instance: str,
host_name: str,
ip: str) -> bytes:
"""
创建服务对响应包(AirPlay + RAOP)
"""
compress_dict = {}
parts = []
current_offset = 12
# === DNS头部 ===
flags = 0x8400
qdcount = 0
ancount = 5 # 2 PTR + 2 SRV + 1 A
header = struct.pack("!HHHHHH", transaction_id, flags, qdcount, ancount, 0, 0)
parts.append(header)
# === AirPlay PTR ===
ptr_ap_name = build_dns_name("_airplay._tcp.local", compress_dict, current_offset)
ptr_ap_data = build_dns_name(airplay_instance, compress_dict, current_offset + len(ptr_ap_name) + 10)
ptr_ap_record = ptr_ap_name + struct.pack("!HHIH", 12, 1, 120, len(ptr_ap_data)) + ptr_ap_data
parts.append(ptr_ap_record)
current_offset += len(ptr_ap_record)
# === AirPlay SRV ===
srv_ap_name = build_dns_name(airplay_instance, compress_dict, current_offset)
srv_ap_data = struct.pack("!HHH", 0, 0, 5000)
srv_ap_data += build_dns_name(host_name, compress_dict, current_offset + len(srv_ap_name) + 10 + len(srv_ap_data))
srv_ap_record = srv_ap_name + struct.pack("!HHIH", 33, 1, 120, len(srv_ap_data)) + srv_ap_data
parts.append(srv_ap_record)
current_offset += len(srv_ap_record)
# === RAOP PTR ===
ptr_raop_name = build_dns_name("_raop._tcp.local", compress_dict, current_offset)
ptr_raop_data = build_dns_name(raop_instance, compress_dict, current_offset + len(ptr_raop_name) + 10)
ptr_raop_record = ptr_raop_name + struct.pack("!HHIH", 12, 1, 120, len(ptr_raop_data)) + ptr_raop_data
parts.append(ptr_raop_record)
current_offset += len(ptr_raop_record)
# === RAOP SRV ===
srv_raop_name = build_dns_name(raop_instance, compress_dict, current_offset)
srv_raop_data = struct.pack("!HHH", 0, 0, 7000)
srv_raop_data += build_dns_name(host_name, compress_dict, current_offset + len(srv_raop_name) + 10 + len(srv_raop_data))
srv_raop_record = srv_raop_name + struct.pack("!HHIH", 33, 1, 120, len(srv_raop_data)) + srv_raop_data
parts.append(srv_raop_record)
current_offset += len(srv_raop_record)
# === 共享A记录 ===
a_name = build_dns_name(host_name, compress_dict, current_offset)
a_data = socket.inet_aton(ip)
a_record = a_name + struct.pack("!HHIH", 1, 1, 120, 4) + a_data
parts.append(a_record)
return b''.join(parts)
class MDNSResponder:
def __init__(self, iface_ip: str):
"""mDNS响应器初始化"""
self.iface_ip = iface_ip
self.services = [] # 所有服务对
self.service_map = defaultdict(list) # 服务类型到实例的映射
self.running = False
# 创建socket
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 多播设置
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(iface_ip))
try:
self.sock.bind(("", MDNS_PORT))
mreq = struct.pack("!4s4s", socket.inet_aton(MDNS_ADDR), socket.inet_aton(iface_ip))
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
print(f"[mDNS] Socket bound to {MDNS_ADDR}:{MDNS_PORT}")
except Exception as e:
raise RuntimeError(f"Socket初始化失败: {e}")
def generate_services(self, start_ip: str, count: int):
"""生成服务对(每对包含AirPlay和RAOP服务)"""
base_ip, start_num = start_ip.rsplit('.', 1)
start_num = int(start_num)
for i in range(count):
# 生成递增IP地址
ip = f"{base_ip}.{start_num + i}"
# 生成唯一服务ID(8位字符)
service_id = generate_random_service_id()
# 生成主机名(主机名后缀为8位数字)
host_suffix = generate_random_hostname_suffix()
host_name = f"host-{host_suffix}.local"
# AirPlay实例名
airplay_instance = f"{service_id}._airplay._tcp.local"
# RAOP实例名(遵循Apple规范)
raop_instance = f"{service_id}@AirPlay._raop._tcp.local"
# 添加到服务列表
self.services.append({
"ip": ip,
"host_name": host_name,
"airplay_instance": airplay_instance,
"raop_instance": raop_instance
})
# 添加到服务映射
self.service_map["_airplay._tcp.local"].append(
(airplay_instance, host_name, ip, 5000)
)
self.service_map["_raop._tcp.local"].append(
(raop_instance, host_name, ip, 7000)
)
print(f"[生成服务] 已创建 {count} 对服务")
def start(self):
"""启动mDNS响应器"""
self.running = True
print(f"[mDNS响应器] 在 {self.iface_ip} 上启动")
while self.running:
try:
data, addr = self.sock.recvfrom(2048)
if len(data) < 12:
continue
# 处理数据包
self.handle_packet(data, addr)
except Exception as e:
if self.running: # 防止关闭时的报错
print(f"处理报文错误: {e}")
def handle_packet(self, data: bytes, addr: tuple):
"""处理接收到的mDNS查询(支持多问题查询)"""
# 解析头部
transaction_id = struct.unpack("!H", data[0:2])[0]
flags = struct.unpack("!H", data[2:4])[0]
qdcount = struct.unpack("!H", data[4:6])[0] # 问题数量
# 只处理查询请求
if flags & 0x8000:
return
offset = 12 # DNS头部后开始解析问题
services_to_respond = set() # 收集需要响应的服务类型
# 循环读取所有问题
for _ in range(qdcount):
try:
# 解析域名
qname, new_offset = parse_dns_name(data, offset)
# 读取类型和类(各2字节)
if new_offset + 4 > len(data):
break
qtype, qclass = struct.unpack("!HH", data[new_offset:new_offset+4])
offset = new_offset + 4
# 只处理PTR查询(qtype=12)且类为IN(qclass=1)
if qtype != 12 or qclass != 1:
continue
service_type = qname.rstrip('.').lower()
if service_type in self.service_map:
services_to_respond.add(service_type)
except DNSError as e:
# 跳过当前问题,继续下一个
continue
# 对每个需要响应的服务类型,发送响应
for service_type in services_to_respond:
print(f"[查询] 来自 {addr[0]} 查询 {service_type}")
for instance_info in self.service_map[service_type]:
instance_name, host_name, ip, port = instance_info
try:
response = create_service_response(
transaction_id,
service_type,
instance_name,
host_name,
ip,
port
)
self.sock.sendto(response, (MDNS_ADDR, MDNS_PORT))
except Exception as e:
print(f"构建响应错误: {e}")
def stop(self):
"""停止响应器"""
self.running = False
self.sock.close()
print("[mDNS响应器] 已停止")
class ServiceAnnouncer:
def __init__(self, iface_ip: str, services: list):
"""服务广播器初始化"""
self.iface_ip = iface_ip
self.services = services
self.running = False
# 创建广播socket
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(iface_ip))
def start(self):
"""启动服务广播"""
self.running = True
print("[广播器] 开始周期性广播服务")
broadcast_count = 0
while self.running:
try:
broadcast_count += 1
broadcast_time = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"\n[广播 #{broadcast_count}] 开始于 {broadcast_time}")
# 广播每个服务对
for idx, service in enumerate(self.services):
transaction_id = random.randint(0, 0xFFFF)
try:
response = create_pair_response(
transaction_id,
service["airplay_instance"],
service["raop_instance"],
service["host_name"],
service["ip"]
)
self.sock.sendto(response, (MDNS_ADDR, MDNS_PORT))
print(f" 已广播服务对 #{idx+1}/{len(self.services)}: {service['ip']}")
except Exception as e:
print(f" 广播错误: {e}")
# 等待下次广播
sleep_interval = 120 # 2分钟
for _ in range(sleep_interval):
if not self.running:
break
time.sleep(1)
except Exception as e:
print(f"广播错误: {e}")
def stop(self):
"""停止广播"""
self.running = False
self.sock.close()
print("[广播器] 已停止")
def main():
parser = argparse.ArgumentParser(description="AirPlay & RAOP mDNS 批量响应器")
parser.add_argument("--iface_ip", required=True, help="网络接口IP")
parser.add_argument("--start_ip", default="192.168.0.2", help="起始IP地址 (默认: 192.168.0.2)")
parser.add_argument("--count", type=int, default=100, help="服务对数量 (默认: 100)")
args = parser.parse_args()
# 创建响应器
responder = MDNSResponder(args.iface_ip)
# 生成服务
responder.generate_services(args.start_ip, args.count)
# 启动响应线程
responder_thread = threading.Thread(target=responder.start, daemon=True)
responder_thread.start()
print("服务已启动,按Ctrl+C退出...")
try:
# 主线程等待中断信号
while True:
time.sleep(1)
except KeyboardInterrupt:
print("正在停止...")
# 设置停止标志(如果类中有)
responder.running = False
# 也可以调用stop方法,但注意线程可能阻塞在recvfrom或sleep中
# 所以设置标志后,还需要中断这些阻塞(例如关闭socket)
responder.stop()
# 等待线程结束(如果有必要)
responder_thread.join(timeout=1.0)
print("已退出")
if __name__ == "__main__":
main()