概要
ネットワーク上の機器を「名前解決」や「サービス発見」で自動的に見つける仕組みとして、mDNS(Multicast DNS)とSSDP(Simple Service Discovery Protocol)は広く使われています。
mDNSは、DNSサーバーなしでローカルネットワーク内のホスト名解決を実現するプロトコルです。RFC 6762として標準化され、*.localドメインを使って機器を名前で参照できます。AppleのBonjourサービス、LinuxのAvahiデーモン、ESP32のmDNSライブラリなどが実装しており、IoT機器がmy-sensor.localのような人間が読めるアドレスで参照可能になります。
SSDPは、UPnP(Universal Plug and Play)の発見レイヤーとして定義されたプロトコルです。機器がネットワークに参加するとマルチキャストで自身の存在を通知し、クライアントはマルチキャスト問い合わせで機器一覧を取得できます。SmarTVやルーター、Chromecast、スマートスピーカーなどで広く使われています。
IoT機器において、これらのプロトコルは「デバイスのIPアドレスを事前に知らなくてもサービスを利用できる」ユーザー体験を実現する重要な基盤技術です。
歴史・背景
mDNSの歴史
1990年代のLAN環境では、NetBIOS(Windows)やAppleTalk(Mac)が独自の名前解決を提供していましたが、互換性がなく複雑でした。IETF ZeroconfワーキンググループはIPv4のリンクローカルアドレス(169.254.x.x)自動設定、mDNS、DNS-SD(DNS-Based Service Discovery)を合わせた「ゼロ設定ネットワーキング」の仕様を策定し、2013年にRFC 6762(mDNS)・RFC 6763(DNS-SD)として標準化しました。
Appleは2002年のMac OS X 10.2でBonjourとして実装し、iTunes・プリンタ共有・AirPrintなどに活用しました。2016年以降、ESP-IDFが組み込みmDNSを標準搭載し、IoT機器での利用が大幅に増加しました。
SSDPの歴史
SSDPは1999年にMicrosoftとHPが策定したUPnP仕様の一部として登場しました。Windows XP以降はUPnPをOSが標準サポートし、家庭内ルーター・NAS・スマートTVなどに普及しました。2016年にGoogles ChromecastやAmazon Echoがネットワーク発見にSSDPを採用し、スマートホーム機器でも普及しています。
技術仕様
mDNS の仕組み
mDNSはDNSプロトコル(RFC 1035)をマルチキャストで使用します。
| 項目 | 通常DNS | mDNS |
|---|---|---|
| DNSサーバー | 必要 | 不要 |
| ドメイン | 任意 | .local |
| 通信方式 | ユニキャスト(1対1) | マルチキャスト(1対多) |
| アドレス(IPv4) | — | 224.0.0.251 |
| アドレス(IPv6) | — | FF02::FB |
| ポート | 53 | 5353 |
mDNS 名前解決の流れ
デバイスA ネットワーク上の全機器
| |
|-- mDNS Query(multicast) ---------->|
| "Who is esp32-sensor.local?" |
| |
| esp32-sensor (デバイスB)が応答
|<-- mDNS Response ------------------|
| "esp32-sensor.local = 192.168.1.42"
DNS-SD(サービス発見)
mDNSと組み合わせて使うDNS-SDは、ホスト名だけでなく「サービスの種類」で機器を発見できます。
# サービスタイプ(PTRレコード)の問い合わせ
_http._tcp.local. PTR my-sensor._http._tcp.local.
# サービスインスタンスの詳細(SRVレコード)
my-sensor._http._tcp.local. SRV 0 0 80 my-sensor.local.
# テキスト情報(TXTレコード)
my-sensor._http._tcp.local. TXT "version=1.2.0" "model=TempSensor"
標準的なサービスタイプ
| サービスタイプ | 用途 |
|---|---|
_http._tcp.local. | Webサーバー |
_https._tcp.local. | HTTPSサーバー |
_mqtt._tcp.local. | MQTTブローカー |
_coap._udp.local. | CoAPサーバー |
_printer._tcp.local. | プリンター |
_airplay._tcp.local. | AirPlay |
SSDP の仕組み
SSDPはHTTPのテキスト構造を使いますが、マルチキャストUDPで送信します。
| 項目 | 値 |
|---|---|
| マルチキャストアドレス | 239.255.255.250 |
| ポート | 1900 |
| プロトコル | UDP |
| メッセージ形式 | HTTP-like テキスト |
SSDPメッセージタイプ
| メッセージ | 説明 |
|---|---|
| NOTIFY(ssdp:alive) | 機器参加通知 |
| NOTIFY(ssdp:byebye) | 機器離脱通知 |
| M-SEARCH | 機器検索要求(クライアント発行) |
| HTTP 200 OK | M-SEARCHへの応答 |
M-SEARCH リクエスト
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 3
ST: ssdp:all
NOTIFY(参加通知)
NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=1800
LOCATION: http://192.168.1.100:8080/description.xml
NT: urn:schemas-upnp-org:device:MediaRenderer:1
NTS: ssdp:alive
SERVER: Linux/5.15 UPnP/1.1 MyDevice/1.0
USN: uuid:550e8400-e29b-41d4-a716-446655440000
動作原理
ESP32でのmDNS実装
#include "mdns.h"
void mdns_setup(void) {
// mDNS初期化
ESP_ERROR_CHECK(mdns_init());
// ホスト名設定(esp32-sensor.local でアクセス可能になる)
ESP_ERROR_CHECK(mdns_hostname_set("esp32-sensor"));
// インスタンス名(人間が読める名前)
ESP_ERROR_CHECK(mdns_instance_name_set("温度センサー ESP32"));
// HTTPサービスを登録
ESP_ERROR_CHECK(mdns_service_add(
"ESP32 Web Server", // インスタンス名
"_http", // サービスタイプ
"_tcp", // プロトコル
80, // ポート
NULL, // TXTレコード
0 // TXTレコード数
));
// TXTレコード付きでMQTTサービスを登録
mdns_txt_item_t mqtt_txt[] = {
{"version", "1.2.0"},
{"device_type", "temperature_sensor"},
{"manufacturer", "kobesoft"}
};
ESP_ERROR_CHECK(mdns_service_add(
"ESP32 MQTT Client",
"_mqtt",
"_tcp",
1883,
mqtt_txt,
3
));
ESP_LOGI(TAG, "mDNS起動完了: esp32-sensor.local");
}
Raspberry Pi での mDNS(Avahi)
# Avahi サービスファイルを作成
cat > /etc/avahi/services/iot-gateway.service << EOF
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">IoT Gateway on %h</name>
<service>
<type>_http._tcp</type>
<port>8080</port>
<txt-record>version=2.1.0</txt-record>
<txt-record>model=RPi4-Gateway</txt-record>
</service>
<service>
<type>_mqtt._tcp</type>
<port>1883</port>
</service>
</service-group>
EOF
systemctl restart avahi-daemon
# 確認
avahi-browse -a # ネットワーク上の全サービスを表示
avahi-browse _http._tcp # HTTPサービスのみ
Python でのmDNSサービス発見
from zeroconf import ServiceBrowser, Zeroconf, ServiceInfo
import socket
class IoTDeviceListener:
def __init__(self):
self.devices = {}
def add_service(self, zc, type_, name):
info = zc.get_service_info(type_, name)
if info:
ip = socket.inet_ntoa(info.addresses[0])
port = info.port
props = {k.decode(): v.decode() for k, v in info.properties.items()}
self.devices[name] = {
"ip": ip,
"port": port,
"properties": props
}
print(f"発見: {name} @ {ip}:{port}")
print(f" 属性: {props}")
def remove_service(self, zc, type_, name):
if name in self.devices:
print(f"離脱: {name}")
del self.devices[name]
def update_service(self, zc, type_, name):
pass
# HTTPサービスを探す
zeroconf = Zeroconf()
listener = IoTDeviceListener()
browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)
import time
time.sleep(5) # 5秒間スキャン
print(f"発見されたデバイス: {len(listener.devices)}")
for name, info in listener.devices.items():
print(f" {name}: http://{info['ip']}:{info['port']}/")
zeroconf.close()
PythonでのSSDPスキャン
import socket
import struct
SSDP_ADDR = "239.255.255.250"
SSDP_PORT = 1900
SSDP_MX = 3
def discover_upnp_devices():
"""SSDPでUPnP機器を検索"""
search_msg = (
"M-SEARCH * HTTP/1.1\r\n"
f"HOST: {SSDP_ADDR}:{SSDP_PORT}\r\n"
'MAN: "ssdp:discover"\r\n'
f"MX: {SSDP_MX}\r\n"
"ST: ssdp:all\r\n"
"\r\n"
).encode()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(SSDP_MX + 1)
# マルチキャストTTLを設定
ttl = struct.pack('b', 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
sock.sendto(search_msg, (SSDP_ADDR, SSDP_PORT))
devices = []
try:
while True:
data, addr = sock.recvfrom(1024)
response = data.decode(errors='ignore')
# LOCATION行を抽出
for line in response.split('\r\n'):
if line.startswith('LOCATION:'):
location = line.split(':', 1)[1].strip()
devices.append({
"ip": addr[0],
"location": location
})
print(f"発見: {addr[0]} -> {location}")
except socket.timeout:
pass
sock.close()
return devices
devices = discover_upnp_devices()
用途・ユースケース
スマートホーム機器の自動設定
Google HomeやApple HomeKitはmDNS/SSDPで対応機器を自動発見し、アプリ上に一覧表示します。ユーザーはIPアドレスを設定することなく機器を追加できます。
工場内IoTゲートウェイの発見
工場に新たなセンサーノードを設置した際、ゲートウェイがmDNSで自動発見し、設定なしでデータ収集を開始します。
# ゲートウェイ:新規センサーを自動発見して登録
class SensorAutoDiscovery:
def __init__(self, gateway_db):
self.db = gateway_db
def add_service(self, zc, type_, name):
info = zc.get_service_info(type_, name)
if not info:
return
ip = socket.inet_ntoa(info.addresses[0])
props = {k.decode(): v.decode() for k, v in info.properties.items()}
if props.get("device_type") == "temperature_sensor":
# センサーをデータベースに自動登録
self.db.register_sensor(
name=name,
ip=ip,
port=info.port,
model=props.get("model", "unknown"),
firmware=props.get("version", "unknown")
)
print(f"新センサー自動登録: {name} @ {ip}")
開発環境でのデバッグ
# ネットワーク上のmDNSサービスを一覧表示(macOS)
dns-sd -B _http._tcp local
# 特定サービスの詳細を確認
dns-sd -L "ESP32 Web Server" _http._tcp local
# Linux(avahi-utils)
avahi-browse -r _http._tcp
# ESP32のIPを名前で解決
ping esp32-sensor.local
curl http://esp32-sensor.local/api/status
Chromecast/スマートTV発見
import pychromecast
# SSDPでChromecastを自動発見
chromecasts, browser = pychromecast.get_chromecasts()
for cast in chromecasts:
print(f"Chromecast: {cast.name} @ {cast.host}:{cast.port}")
実装・開発のポイント
主要ライブラリ
| 環境 | mDNS ライブラリ | SSDP ライブラリ |
|---|---|---|
| ESP32(C) | ESP-IDF mdns | カスタム実装 |
| Raspberry Pi(Python) | zeroconf | ssdpy, upnpclient |
| macOS | ネイティブ(Bonjour) | python-ssdp |
| Linux | Avahi | python-ssdp |
| Windows | mDNS(Windows 10+) | UPnP標準API |
ファイアウォール設定
mDNSとSSDPはマルチキャストを使うため、ファイアウォールで明示的に許可が必要な場合があります。
# Linuxファイアウォール(ufw)でmDNS許可
sudo ufw allow 5353/udp # mDNS
# SSDPの許可
sudo ufw allow 1900/udp # SSDP
# 特定のインターフェイスのみ(セキュリティのため推奨)
sudo ufw allow in on eth0 to 224.0.0.251 port 5353 # mDNS
sudo ufw allow in on eth0 to 239.255.255.250 port 1900 # SSDP
セキュリティの注意点
SSDPはセキュリティ機構を持たないため、以下のリスクがあります。
- SSDP増幅攻撃:インターネットに公開されたSSDPサービスがDDoS攻撃の踏み台になる事例(2014年Spamhausへの攻撃等)
- 情報漏洩:SSDPのNOTIFYメッセージに機器の詳細情報が含まれる
対策として、SSDPはLANセグメント内に限定し、インターネット側ファイアウォールでブロックします。
# WAN側でSSDPをブロック(重要)
iptables -A INPUT -i eth0 -p udp --dport 1900 -j DROP # eth0がWAN側の場合
ESP32 mDNS のトラブルシューティング
// mDNSが動作しない場合のデバッグ
// 1. Wi-Fi接続後にmDNSを初期化する(順序が重要)
void on_wifi_connected(void) {
ESP_ERROR_CHECK(mdns_init());
ESP_ERROR_CHECK(mdns_hostname_set("esp32-sensor"));
// ここでサービスを登録
}
// 2. NVS(不揮発メモリ)の初期化を先に行う
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
// → WiFi初期化 → 接続 → mDNS初期化
// 3. ホスト名の文字種制限(小文字英数字とハイフンのみ)
// "ESP32_Sensor" は無効 → "esp32-sensor" に修正
他技術との比較
| 項目 | mDNS | SSDP | DNS(通常) | DHCP |
|---|---|---|---|---|
| サーバー要否 | 不要 | 不要 | 必要 | 必要(通常) |
| 名前解決 | ○(.local) | △(URLから) | ◎ | × |
| サービス発見 | ○(DNS-SD) | ◎ | △ | × |
| 通信方式 | マルチキャスト | マルチキャスト | ユニキャスト | ブロードキャスト |
| RFC標準 | ○(6762/6763) | △(UPnP内) | ○ | ○ |
| セキュリティ | △ | × | △ | △ |
ESP32やRaspberry PiでIoTシステムを構築する際、mDNSを使うことで各デバイスを固定IPなしで名前アクセスできるようになり、開発・運用の利便性が大幅に向上します。Wi-Fiネットワーク内での機器自動発見として、CoAPのリソース発見(/.well-known/core)とも組み合わせて使われます。