/* * meow_mdns.c — 极简 IPv4-only mDNS 应答器(MeowOS / R3 Pro II) * * 目标: 让设备在纯 IPv4 下应答 .local (A 记录) 与 _ssh._tcp.local (PTR/SRV/TXT), * 供主机 avahi-resolve .local 解析到设备 IP。 * * 为什么不用 mDNSResponder/SAResponder: 两者均需 IPv6(socket AF_INET6 在内核无 IPv6 时 fatal), * 且完整版 mdnsd 是 IPC 服务型守护进程,自身不公布主机名 A 记录;SAResponder 只注册单一服务。 * 本实现为自研,零 IPv6 依赖,线程/信号安全,优雅退。 * * 用法: meow_mdns [-v] # 前台运行; -v 打印收到的查询/源地址 * 环境: MEOW_MDNS_HOST 缺省 = gethostname() 第一标签 (设备为"ingenic") * MEOW_MDNS_ADDR 缺省 = 第一块非回环 UP 接口的 IPv4 (可为 wlan0 10.109.49.50) * MEOW_MDNS_IF 缺省 = 自动; 设定接口名, join 组播绑定到该接口 * MEOW_MDNS_PORT 缺省 = 5353; 可覆写(通常仅宿主单测用) * * 构建: 宿主机 cc -O2 -o meow_mdns meow_mdns.c * 交叉 zig-mipsel-cc -Os -static -o meow_mdns meow_mdns.c * * 协议要点(RFC 6762): * - join 224.0.0.251:5353 (IP_ADD_MEMBERSHIP), SO_REUSEADDR(+SO_REUSEPORT) * - 应答所有收到的查询(单播/组播), 回发到查询源地址(单播), 规避组播回程丢失 * - 应答头 flags = 0x8400 (QR=1, AA=1), 回显查询的 Question 段 * - 记录均带 TTL 120; 唯一记录(A/SRV/TXT)置 cache-flush 位 0x8000, PTR 不置 * - QNAME 大小写不敏感; 应答中名字不压缩(合法, 由 avahi 解析) * - 忽略 QR=1 包(避免自响应), 忽略非查询包 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MDNS_PORT 5353 #define MDNS_GROUP "224.0.0.251" #define TTL_DEFAULT 120 #define QCLASS_IN 1 #define QFLUSH 0x8000 #define TYPE_A 1 #define TYPE_PTR 12 #define TYPE_TXT 16 #define TYPE_SRV 33 #define TYPE_ANY 255 #define MAX_Q 16 /* 每包最多解析的 question 数 */ #define MAX_NAME 256 static volatile sig_atomic_t g_running = 1; static int g_cacheflush = 0; /* 唯一记录(A/SRV/TXT)是否置 cache-flush 位 (MEOW_MDNS_CF) */ static void on_signal(int sig) { (void)sig; g_running = 0; } static void to_lower(char *s) { for (; *s; s++) *s = (char)tolower((unsigned char)*s); } /* 把 "a.b.c" 编码为 DNS 名字(无压缩) ==> abc<0>。返回长度, -1 失败 */ static int encode_name(uint8_t *buf, size_t buflen, const char *name) { size_t pos = 0; const char *p = name; if (!name || !*name) return -1; while (*p) { const char *dot = strchr(p, '.'); size_t len = dot ? (size_t)(dot - p) : strlen(p); if (len == 0 || len > 63) return -1; if (pos + 1 + len >= buflen) return -1; buf[pos++] = (uint8_t)len; memcpy(buf + pos, p, len); pos += len; if (!dot) break; p = dot + 1; } if (pos + 1 >= buflen) return -1; buf[pos++] = 0; return (int)pos; } /* 解析 DNS 名字(支持 0xC0 压缩指针)。返回解码长度; 把 *off 移到名字结束处 */ static int parse_name(const uint8_t *msg, size_t msglen, size_t *off, char *out, size_t outlen) { size_t pos = *off, olen = 0, jumps = 0; int jumped = 0; while (1) { if (pos >= msglen) return -1; uint8_t c = msg[pos]; if (c == 0) { pos++; break; } if ((c & 0xC0) == 0xC0) { if (pos + 1 >= msglen) return -1; size_t ptr = ((c & 0x3F) << 8) | msg[pos + 1]; pos += 2; if (!jumped) *off = pos; jumped = 1; if (++jumps > 64) return -1; pos = ptr; continue; } if ((c & 0xC0) != 0) return -1; /* 非法标签 */ pos++; if (olen > 0) { if (olen + 1 >= outlen) return -1; out[olen++] = '.'; } if (pos + c > msglen) return -1; if (olen + c >= outlen) return -1; memcpy(out + olen, msg + pos, c); olen += c; pos += c; } if (!jumped) *off = pos; out[olen] = 0; return (int)olen; } /* 小写入工具 */ static size_t w16(uint8_t *b, size_t o, uint16_t v){ b[o]=v>>8; b[o+1]=v&0xFF; return o+2; } static size_t w32(uint8_t *b, size_t o, uint32_t v){ b[o]=v>>24; b[o+1]=v>>16; b[o+2]=v>>8; b[o+3]=v&0xFF; return o+4; } struct question { char name[MAX_NAME]; /* 小写 QNAME */ uint16_t qtype; uint16_t qclass; size_t raw_off; /* question 原始字节在消息中的起点 */ size_t raw_len; /* question 原始字节长度(用于回显) */ }; /* 编码一个 DNS 名字到 resp[*ro..];若能在 resp[0..*ro) 中找到完全相同的名字序列, * 则改写为压缩指针(RFC 1035 §4.1.4)。返回 0 成功, -1 失败。 */ static int put_name(uint8_t *resp, size_t *ro, size_t cap, const char *name) { uint8_t tmp[256]; int tl = encode_name(tmp, sizeof(tmp), name); if (tl < 0) return -1; for (size_t i = 0; i + (size_t)tl <= *ro && i < 0x4000; i++) { if (resp[i] == tmp[0] && memcmp(resp + i, tmp, (size_t)tl) == 0) { if (*ro + 2 > cap) return -1; resp[(*ro)++] = (uint8_t)(0xC0 | (i >> 8)); resp[(*ro)++] = (uint8_t)(i & 0xFF); return 0; } } if (*ro + (size_t)tl > cap) return -1; memcpy(resp + *ro, tmp, (size_t)tl); *ro += (size_t)tl; return 0; } /* 追加一条 answer 记录。owner=记录名(自带大小写, 小写即可), type, unique, * rdata 指针+长度。返回 0 成功 */ static int put_answer(uint8_t *resp, size_t *ro, size_t cap, const char *owner, uint16_t type, int unique, const uint8_t *rdata, uint16_t rdlen) { int nl = put_name(resp, ro, cap, owner); if (nl < 0 || (size_t)(*ro + 2 + 2 + 4 + 2 + rdlen) > cap) return -1; *ro = w16(resp, *ro, type); *ro = w16(resp, *ro, (uint16_t)(QCLASS_IN | ((unique && g_cacheflush) ? QFLUSH : 0))); *ro = w32(resp, *ro, TTL_DEFAULT); size_t rdlen_pos = *ro; *ro += 2; /* 占位 */ if (rdlen) { memcpy(resp + *ro, rdata, rdlen); *ro += rdlen; } /* 回填 rdlen(注意:*ro 已越过 rdata,不能用 w16 的返回值覆盖 *ro) */ resp[rdlen_pos] = (uint8_t)(rdlen >> 8); resp[rdlen_pos + 1] = (uint8_t)(rdlen & 0xFF); return 0; } int main(int argc, char **argv) { int verbose = 0; for (int i = 1; i < argc; i++) if (!strcmp(argv[i], "-v")) verbose = 1; uint16_t port = MDNS_PORT; const char *ep = getenv("MEOW_MDNS_PORT"); if (ep && *ep) port = (uint16_t)strtoul(ep, NULL, 10); if (port == 0) port = MDNS_PORT; int qd_echo = 1; /* 是否回显问题段 (QDCOUNT) */ const char *eq = getenv("MEOW_MDNS_QD"); if (eq && *eq) qd_echo = atoi(eq); g_cacheflush = 0;/* 唯一记录(A/SRV/TXT)是否置 cache-flush 位 */ const char *ecf = getenv("MEOW_MDNS_CF"); if (ecf && *ecf) g_cacheflush = atoi(ecf); int reply_mcast = 1;/* 组播查询是否同时回一条组播应答; 0=只单播回源 */ const char *emc = getenv("MEOW_MDNS_MCAST"); if (emc && *emc) reply_mcast = atoi(emc); int set_mcastif = 0;/* 是否显式设置 IP_MULTICAST_IF (0=走内核默认, 同 zeroconf, 更稳) */ const char *emi = getenv("MEOW_MDNS_MIF"); if (emi && *emi) set_mcastif = atoi(emi); /* ---- 确定主机名 ---- */ char host[MAX_NAME]; const char *eh = getenv("MEOW_MDNS_HOST"); if (eh && *eh) { snprintf(host, sizeof(host), "%s", eh); } else { if (gethostname(host, sizeof(host) - 1) != 0 || !host[0]) snprintf(host, sizeof(host), "meowos"); host[sizeof(host) - 1] = 0; char *d = strchr(host, '.'); if (d) *d = 0; /* 只取第一标签 */ } if (!*host) snprintf(host, sizeof(host), "meowos"); to_lower(host); /* ---- 确定 IP ---- */ char hostA[MAX_NAME]; /* "host.local" */ snprintf(hostA, sizeof(hostA), "%s.local", host); char inst[MAX_NAME]; /* "host._ssh._tcp.local" */ snprintf(inst, sizeof(inst), "%s._ssh._tcp.local", host); char svc[MAX_NAME] = "_ssh._tcp.local"; /* 地址全程保持网络字节序 (struct in_addr) */ struct in_addr ipin = {0}; const char *ea = getenv("MEOW_MDNS_ADDR"); if (ea && *ea) { if (inet_pton(AF_INET, ea, &ipin) != 1) ipin.s_addr = 0; } if (!ipin.s_addr) { /* 遍历接口取第一块非回环 UP 的 IPv4(sin_addr 已是网络序) */ struct ifaddrs *ifa, *i; if (getifaddrs(&ifa) == 0) { for (i = ifa; i; i = i->ifa_next) { if (!i->ifa_addr || i->ifa_addr->sa_family != AF_INET) continue; if (!(i->ifa_flags & IFF_UP) || (i->ifa_flags & IFF_LOOPBACK)) continue; ipin = ((struct sockaddr_in *)i->ifa_addr)->sin_addr; break; } freeifaddrs(ifa); } } if (!ipin.s_addr) { fprintf(stderr, "meow_mdns: no IPv4 address (set MEOW_MDNS_ADDR)\n"); return 2; } char ips[32]; inet_ntop(AF_INET, &ipin, ips, sizeof(ips)); printf("meow_mdns: host=%s.local addr=%s swc=%s\n", host, ips, svc); fflush(stdout); /* ---- 打开 UDP 套接字 ---- */ int s = socket(AF_INET, SOCK_DGRAM, 0); if (s < 0) { perror("socket"); return 2; } int one = 1; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); /* 可能失败, 忽略 */ /* 接收时携带目的地址与接口 (用于判断查询是组播还是单播, 再决定应答方向) */ setsockopt(s, IPPROTO_IP, IP_PKTINFO, &one, sizeof(one)); struct sockaddr_in bindaddr; memset(&bindaddr, 0, sizeof(bindaddr)); bindaddr.sin_family = AF_INET; bindaddr.sin_port = htons(port); bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(s, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) < 0) { perror("bind"); return 2; } /* ---- 加入组播组 ---- */ struct ip_mreq mreq; memset(&mreq, 0, sizeof(mreq)); inet_pton(AF_INET, MDNS_GROUP, &mreq.imr_multiaddr); mreq.imr_interface.s_addr = ipin.s_addr; /* 绑定到设备 IP */ if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { perror("IP_ADD_MEMBERSHIP"); /* 不视为致命: 仍可应答单播查询 */ } unsigned char ttl = 255; const char *ettl = getenv("MEOW_MDNS_TTL"); if (ettl && *ettl) ttl = (unsigned char)atoi(ettl); setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)); /* IP_MULTICAST_LOOP 保持默认(1): 允许本机其它 socket(如 avahi)收到组播应答; * 自身收到的应答会是 QR=1, 已被下方 flags&0x8000 过滤, 不会自响应。 */ /* 让 recv 有超时以便检查 g_running */ struct timeval tv = { 1, 0 }; setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); signal(SIGINT, on_signal); signal(SIGTERM, on_signal); signal(SIGPIPE, SIG_IGN); struct in_addr mcast_addr; inet_pton(AF_INET, MDNS_GROUP, &mcast_addr); /* 找出持有设备 IP 的接口索引 (组播应答固定从该接口发出, 避免 rcv_ifindex 落到 lo/错接口) */ int dev_ifindex = 0; { struct ifaddrs *ifa, *i; if (getifaddrs(&ifa) == 0) { for (i = ifa; i; i = i->ifa_next) { if (i->ifa_addr && i->ifa_addr->sa_family == AF_INET && ((struct sockaddr_in *)i->ifa_addr)->sin_addr.s_addr == ipin.s_addr) { dev_ifindex = (int)if_nametoindex(i->ifa_name); if (dev_ifindex) break; } } freeifaddrs(ifa); } } printf("meow_mdns: listening on %s:%d (multicast %s)\n", MDNS_GROUP, port, MDNS_GROUP); if (verbose) printf("meow_mdns: ip=%s dev_ifindex=%d\n", ips, dev_ifindex); fflush(stdout); char pkt[2048]; struct sockaddr_in src; while (g_running) { /* recvmsg + IP_PKTINFO: 拿到查询的目的地址与到达接口, 判断组播/单播 */ memset(&src, 0, sizeof(src)); unsigned char ctrl[256]; struct iovec iov; struct msghdr msg; memset(&msg, 0, sizeof(msg)); msg.msg_name = &src; msg.msg_namelen = sizeof(src); iov.iov_base = pkt; iov.iov_len = sizeof(pkt); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl); ssize_t n = recvmsg(s, &msg, 0); if (n < 0) { if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; if (!g_running) break; perror("recvmsg"); continue; } if (n < 12) continue; uint32_t destip = mcast_addr.s_addr; /* 取不到就按组播处理 */ for (struct cmsghdr *cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) { if (cm->cmsg_level == IPPROTO_IP && cm->cmsg_type == IP_PKTINFO) { struct in_pktinfo *pi = (struct in_pktinfo *)CMSG_DATA(cm); if (pi->ipi_addr.s_addr) destip = pi->ipi_addr.s_addr; } } uint8_t *m = (uint8_t *)pkt; size_t ml = (size_t)n; uint16_t flags = (uint16_t)((m[2] << 8) | m[3]); if (flags & 0x8000) continue; /* 是应答/响应, 忽略 */ uint16_t qd = (uint16_t)((m[4] << 8) | m[5]); if (qd == 0 || qd > MAX_Q) continue; struct question qs[MAX_Q]; size_t off = 12; int nq = 0; for (int qi = 0; qi < qd; qi++) { size_t qstart = off; if (parse_name(m, ml, &off, qs[nq].name, sizeof(qs[nq].name)) < 0) { nq = -1; break; } if (off + 4 > ml) { nq = -1; break; } qs[nq].qtype = (uint16_t)((m[off] << 8) | m[off + 1]); qs[nq].qclass = (uint16_t)((m[off + 2] << 8) | m[off + 3]); off += 4; qs[nq].raw_off = qstart; qs[nq].raw_len = off - qstart; to_lower(qs[nq].name); nq++; } if (nq < 0) continue; if (verbose) { char srcip[64]; inet_ntop(AF_INET, &src.sin_addr, srcip, sizeof(srcip)); printf("Q from %s:%d, %d question(s)\n", srcip, ntohs(src.sin_port), nq); for (int qi = 0; qi < nq; qi++) printf(" q[%d] %s type=%u class=%u\n", qi, qs[qi].name, qs[qi].qtype, qs[qi].qclass); fflush(stdout); } /* ---- 构建应答 ---- * RFC 6762: 应答 QDCOUNT 通常为 0, 但部分实现(avahi)接受、部分要求回显问题。 * MEOW_MDNS_QD 控制是否回显; MEOW_MDNS_CF 控制唯一记录 cache-flush 位。 */ uint8_t resp[2048]; size_t ro = 0, ancount = 0; size_t head_ro = ro; ro += 12; /* 预占 header */ uint16_t qdcount = 0; if (qd_echo) { for (int qi = 0; qi < nq; qi++) { if (ro + qs[qi].raw_len > sizeof(resp)) { ro = 0; ancount = 0; break; } memcpy(resp + ro, m + qs[qi].raw_off, qs[qi].raw_len); ro += qs[qi].raw_len; } qdcount = (uint16_t)nq; } /* 依据每个 question 的 (qname, qtype) 追加相应记录 */ for (int qi = 0; qi < nq && ro; qi++) { const char *qn = qs[qi].name; uint16_t qt = qs[qi].qtype; int wantA = (qt == TYPE_A || qt == TYPE_ANY); int wantPTR = (qt == TYPE_PTR || qt == TYPE_ANY); int wantSRV = (qt == TYPE_SRV || qt == TYPE_ANY); int wantTXT = (qt == TYPE_TXT || qt == TYPE_ANY); /* host.local -> A */ if (wantA && !strcmp(qn, hostA)) { uint8_t rd[4]; memcpy(rd, &ipin.s_addr, 4); /* 网络序, 直接作为 A rdata */ if (put_answer(resp, &ro, sizeof(resp), hostA, TYPE_A, 1, rd, 4) == 0) ancount++; } /* _ssh._tcp.local -> PTR 实例 */ if (wantPTR && !strcmp(qn, svc)) { uint8_t rd[256]; int rl = encode_name(rd, sizeof(rd), inst); if (rl > 0 && put_answer(resp, &ro, sizeof(resp), svc, TYPE_PTR, 0, rd, (uint16_t)rl) == 0) ancount++; } /* host._ssh._tcp.local -> SRV + TXT */ if ((wantSRV || wantTXT) && !strcmp(qn, inst)) { if (wantSRV) { uint8_t rd[256]; size_t rp = 0; rp = w16(rd, rp, 0); rp = w16(rd, rp, 0); /* priority=weight=0 */ rp = w16(rd, rp, 22); /* SSH port */ int hl = encode_name(rd + rp, sizeof(rd) - rp, hostA); if (hl > 0 && put_answer(resp, &ro, sizeof(resp), inst, TYPE_SRV, 1, rd, (uint16_t)(rp + hl)) == 0) ancount++; } if (wantTXT) { uint8_t rd[2] = { 0x00 }; /* 空 TXT */ if (put_answer(resp, &ro, sizeof(resp), inst, TYPE_TXT, 1, rd, 1) == 0) ancount++; } } /* _services._dns-sd._udp.local -> PTR _ssh._tcp.local (便于 avahi-browse) */ if (wantPTR && !strcmp(qn, "_services._dns-sd._udp.local")) { uint8_t rd[256]; int rl = encode_name(rd, sizeof(rd), svc); if (rl > 0 && put_answer(resp, &ro, sizeof(resp), "_services._dns-sd._udp.local", TYPE_PTR, 0, rd, (uint16_t)rl) == 0) ancount++; } } if (ro == 0) continue; /* 构建失败或没有记录, 放弃 */ if (ancount == 0) continue; /* 没有我们需要应答的记录 */ /* 组装 header */ memset(resp + head_ro, 0, 12); resp[head_ro] = 0x00; resp[head_ro+1] = 0x00; /* ID */ resp[head_ro+2] = 0x84; resp[head_ro+3] = 0x00; /* QR+AA, no error */ resp[head_ro+4] = (uint8_t)(qdcount >> 8); resp[head_ro+5] = (uint8_t)qdcount; resp[head_ro+6] = (uint8_t)(ancount >> 8); resp[head_ro+7] = (uint8_t)ancount; resp[head_ro+8] = 0; resp[head_ro+9] = 0; /* ANCOUNT of NS */ resp[head_ro+10] = 0; resp[head_ro+11] = 0; /* ARCOUNT */ /* 选择应答方向: * 查询到达组播组 -> 应答走组播(RFC 6762: 组播查询期待组播应答, avahi 才会接受), * 并冗余单播一份给查询源(组播被热点丢弃时兜底)。 * 查询到达单播 -> 直接单播回源。 */ int to_mcast = (destip == mcast_addr.s_addr); struct sockaddr_in dst; memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; if (to_mcast && reply_mcast) { /* 组播应答固定从持有设备 IP 的接口发出 */ unsigned char mloop = 1; /* IP_MULTICAST_IF 用设备接口的 IPv4 地址(网络序), 而非接口索引 */ if (set_mcastif && ipin.s_addr) setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, &ipin.s_addr, sizeof(ipin.s_addr)); setsockopt(s, IPPROTO_IP, IP_MULTICAST_LOOP, &mloop, sizeof(mloop)); dst.sin_port = htons(MDNS_PORT); dst.sin_addr = mcast_addr; ssize_t sn = sendto(s, resp, ro, 0, (struct sockaddr *)&dst, sizeof(dst)); if (verbose) printf(" -> mcast sendto ret=%zd errno=%s\n", sn, errno ? strerror(errno) : "ok"); } /* 单播回源 (始终发送; 也作为组播应答的兜底/冗余) */ dst.sin_port = src.sin_port; dst.sin_addr = src.sin_addr; ssize_t su = sendto(s, resp, ro, 0, (struct sockaddr *)&dst, sizeof(dst)); (void)su; if (verbose) { printf(" -> replied %zu bytes to %s%s:%d (AN %zu)\n", ro, to_mcast ? "mcast(224.0.0.251)" : "", inet_ntop(AF_INET, &src.sin_addr, ips, sizeof(ips)), ntohs(src.sin_port), ancount); fflush(stdout); } } /* 退出: 离开组播组 */ if (setsockopt(s, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) == 0) close(s); printf("meow_mdns: exiting\n"); return 0; }