IoTプロトコル

mDNS / SSDP

機器を自動発見する仕組み。

概要

ネットワーク上の機器を「名前解決」や「サービス発見」で自動的に見つける仕組みとして、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)をマルチキャストで使用します。

項目通常DNSmDNS
DNSサーバー必要不要
ドメイン任意.local
通信方式ユニキャスト(1対1)マルチキャスト(1対多)
アドレス(IPv4)224.0.0.251
アドレス(IPv6)FF02::FB
ポート535353

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 OKM-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)zeroconfssdpy, upnpclient
macOSネイティブ(Bonjour)python-ssdp
LinuxAvahipython-ssdp
WindowsmDNS(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" に修正

他技術との比較

項目mDNSSSDPDNS(通常)DHCP
サーバー要否不要不要必要必要(通常)
名前解決○(.local)△(URLから)×
サービス発見○(DNS-SD)×
通信方式マルチキャストマルチキャストユニキャストブロードキャスト
RFC標準○(6762/6763)△(UPnP内)
セキュリティ×

ESP32Raspberry PiでIoTシステムを構築する際、mDNSを使うことで各デバイスを固定IPなしで名前アクセスできるようになり、開発・運用の利便性が大幅に向上します。Wi-Fiネットワーク内での機器自動発見として、CoAPのリソース発見(/.well-known/core)とも組み合わせて使われます。

関連用語

参考リンク