無線LANのSSIDをC#からAPIで取得するコードを書いてみた。
1. 背景
アプリから無線LANのSSIDを取得したいときがあるが、標準の.NET FrameworkにもWinRTにもそれが可能なAPIは用意されていない。C++には
Native Wifi APIがあるが、このマネージド実装の
Managed Wifi APIは開発が止まって久しく、かといってP/Invokeの宣言を起こすのも大変そうなので敬遠していた。実用上はNetshの出力をパースすれば大体足りるし。
それが、ふとMSDNでこんなエントリを見かけた。
「おおっ」と思いつつダウンロードしたが、実体部分はC++のライブラリで、それをC#から使うものだった(タイトルどおりではある)。悔しかったので少し探したところ、幾つかサンプルが挙げられていた。
これらとMSDNを合わせ見た結果、必要なピースは揃ってそうだったので書いてみた。
2. 利用可能なネットワークのSSIDを取得する
手順としては以下のようになる。
- WlanOpenHandleでハンドルを取得する。このdwClientVersionはOSによって変わるが、Vista以降は2。
- WlanEnumInterfacesでPCにある無線LANインターフェイス(無線LANアダプター)の情報を取得する。
- WlanGetAvailableNetworkListで各インターフェイスから見える無線LANネットワークの情報を示すWLAN_AVAILABLE_NETWORKを取得する。これは各インターフェイスの感度にも左右されるが、インターフェイスが複数あれば当然、単数でも試した限りでは重複があり得る。
- WLAN_AVAILABLE_NETWORKからSSIDを取得する。
public static IEnumerable<string> GetAvailableNetworkSsids()
{
var clientHandle = IntPtr.Zero;
var interfaceList = IntPtr.Zero;
var availableNetworkList = IntPtr.Zero;
try
{
uint negotiatedVersion;
if (WlanOpenHandle(
2, // Client version for Windows Vista and Windows Server 2008
IntPtr.Zero,
out negotiatedVersion,
out clientHandle) != ERROR_SUCCESS)
yield break;
if (WlanEnumInterfaces(
clientHandle,
IntPtr.Zero,
out interfaceList) != ERROR_SUCCESS)
yield break;
var interfaceInfoList = new WLAN_INTERFACE_INFO_LIST(interfaceList);
Debug.WriteLine("Interface count: {0}", interfaceInfoList.dwNumberOfItems);
foreach (var interfaceInfo in interfaceInfoList.InterfaceInfo)
{
if (WlanGetAvailableNetworkList(
clientHandle,
interfaceInfo.InterfaceGuid,
WLAN_AVAILABLE_NETWORK_INCLUDE_ALL_MANUAL_HIDDEN_PROFILES,
IntPtr.Zero,
out availableNetworkList) != ERROR_SUCCESS)
continue;
var networkList = new WLAN_AVAILABLE_NETWORK_LIST(availableNetworkList);
foreach (var network in networkList.Network)
{
Debug.WriteLine("Interface: {0}, SSID: {1}, Quality: {2}",
interfaceInfo.strInterfaceDescription,
network.dot11Ssid,
network.wlanSignalQuality);
yield return network.dot11Ssid.ToString();
}
}
}
finally
{
if (availableNetworkList != IntPtr.Zero)
WlanFreeMemory(availableNetworkList);
if (interfaceList != IntPtr.Zero)
WlanFreeMemory(interfaceList);
if (clientHandle != IntPtr.Zero)
WlanCloseHandle(clientHandle, IntPtr.Zero);
}
}
P/Invokeの宣言はレポジトリの方を参照。
3. 接続中のネットワークのSSIDを取得する
手順は2.までは上と同じ。
- WlanOpenHandleでハンドルを取得する。
- WlanEnumInterfacesでPCにある無線LANインターフェイスの情報を取得する。
- WlanQueryInterfaceで各インターフェイスの現在の接続状況を示すWLAN_CONNECTION_ATTRIBUTESを取得する。このためにはOpCodeにwlan_intf_opcode_current_connectionを指定する。
- WLAN_CONNECTION_ATTRIBUTESのisStateで接続中かどうか判別できるので、接続中ならwlanAssociationAttributesのWLAN_ASSOCIATION_ATTRIBUTESからSSIDを取得する。
public static IEnumerable<string> GetConnectedNetworkSsids()
{
var clientHandle = IntPtr.Zero;
var interfaceList = IntPtr.Zero;
var queryData = IntPtr.Zero;
try
{
uint negotiatedVersion;
if (WlanOpenHandle(
2, // Client version for Windows Vista and Windows Server 2008
IntPtr.Zero,
out negotiatedVersion,
out clientHandle) != ERROR_SUCCESS)
yield break;
if (WlanEnumInterfaces(
clientHandle,
IntPtr.Zero,
out interfaceList) != ERROR_SUCCESS)
yield break;
var interfaceInfoList = new WLAN_INTERFACE_INFO_LIST(interfaceList);
Debug.WriteLine("Interface count: {0}", interfaceInfoList.dwNumberOfItems);
foreach (var interfaceInfo in interfaceInfoList.InterfaceInfo)
{
uint dataSize;
if (WlanQueryInterface(
clientHandle,
interfaceInfo.InterfaceGuid,
WLAN_INTF_OPCODE.wlan_intf_opcode_current_connection,
IntPtr.Zero,
out dataSize,
ref queryData,
IntPtr.Zero) != ERROR_SUCCESS) // If not connected to a network, ERROR_INVALID_STATE will be returned.
continue;
var connection = (WLAN_CONNECTION_ATTRIBUTES)Marshal.PtrToStructure(queryData, typeof(WLAN_CONNECTION_ATTRIBUTES));
if (connection.isState != WLAN_INTERFACE_STATE.wlan_interface_state_connected)
continue;
var association = connection.wlanAssociationAttributes;
Debug.WriteLine("Interface: {0}, SSID: {1}, BSSID: {2}, Signal: {3}",
interfaceInfo.strInterfaceDescription,
association.dot11Ssid,
association.dot11Bssid,
association.wlanSignalQuality);
yield return association.dot11Ssid.ToString();
}
}
finally
{
if (queryData != IntPtr.Zero)
WlanFreeMemory(queryData);
if (interfaceList != IntPtr.Zero)
WlanFreeMemory(interfaceList);
if (clientHandle != IntPtr.Zero)
WlanCloseHandle(clientHandle, IntPtr.Zero);
}
}
なお、3.のWlanQueryInterfaceはそのインターフェイスが接続中でないときはERROR_INVALID_STATEを返してくる。
4. まとめ
Windows 7とWindows 8.1、ついでにWindows 10 TPで確認したところ問題なさそうだったので、とりあえずこれで行けると思う。
しかしAPIで取得できるとコマンド出力をパースするよりわずかでも速いし、気持ちすっきりするのがいい。まあP/Invokeのマーシャリングは結構泥臭いけど。
[追記] UTF-8のSSID
SSIDというとASCII文字列と思われがちだが、IEEE 802.11の規格を読むとSSIDは0-32のOctet stringとされていて、平たくいうと長さ0-32のバイト配列とされている。これは現在はUTF-8も使えることになっていて、当然日本語を使うこともできる(それで駄洒落を作るのが少しネタになっていた)。
Native Wifi APIでSSIDの情報を格納するのは
DOT11_SSIDだが、この説明でも"The SSID that is specified by the ucSSID member is not a null-terminated ASCII string."と釘が刺されている。
このDOT11_SSIDに対応するP/Invoke用の構造体を初め以下のように書いていた。
[StructLayout(LayoutKind.Sequential)]
private struct DOT11_SSID
{
public uint uSSIDLength;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string ucSSID;
public override string ToString()
{
if ((ucSSID == null) || (ucSSID.Length < (int)uSSIDLength))
return null;
return ucSSID.Substring(0, (int)uSSIDLength);
}
}
これには問題があって、uSSIDLengthの示す長さを勘違いしていることもさることながら、実際に試してみるとUnmanagedType.ByValTStrでは正しくマーシャリングされず、「の」が「縺ョ」のように文字化けを起こす。これを回避するには、ucSSIDをまずはバイト配列にマーシャリングした上で、これから文字列に変換すればうまく行った。
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] ucSSID;
public override string ToString()
{
return Encoding.UTF8.GetString(ucSSID, 0, (int)uSSIDLength);
}
さらに、規格に忠実にバイト配列としても取り出せるようにまとめ直せば、こんな感じ。
[StructLayout(LayoutKind.Sequential)]
private struct DOT11_SSID
{
public uint uSSIDLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] ucSSID;
public byte[] ToBytes()
{
return (ucSSID != null)
? ucSSID.Take((int)uSSIDLength).ToArray()
: null;
}
public override string ToString()
{
return (ucSSID != null)
? Encoding.UTF8.GetString(ToSsidBytes())
: null;
}
}
ちなみに、Windows 8.1のNetshの出力を見るとUnmanagedType.ByValTStrを使ったときと同じように文字化けする。一方、設定のネットワークから見ると文字化けしないので、NetshのUTF-8対応が遅れているようで、Windows 10 TPでも解消してないらしい。
ということで、UTF-8対応ができるという点が、現状APIによる方法が明確に優れている点になる。
[追記] ライブラリ公開
ライブラリとしてまとめたので、コードをなるべく合わせるよう修正した。