A. Kiểm tra
1. Lấy XML profile TTLS do chính Windows tạo ra
Có 2 cách lấy profile mà Windows đang lưu:
- Nhanh nhất:
netsh wlan export profile - Đúng với hướng DllImport của bạn: gọi
WlanGetProfile()từwlanapi.dll.
Microsoft xác nhận WlanGetProfile() trả trực tiếp XML representation của wireless profile; sau khi lấy xong phải gọi WlanFreeMemory() để giải phóng buffer.
Cách 1 — dùng netsh để lấy XML
Nếu máy Windows đã kết nối thành công tới CongDuan87, mở CMD:
netsh wlan show profiles
Bạn sẽ thấy:
Profiles on interface Wi-Fi:
All User Profile : CongDuan87
Sau đó:
mkdir C:\Temp\WifiProfile
netsh wlan export profile name="CongDuan87" folder="C:\Temp\WifiProfile"
Windows sẽ tạo file XML trong:
C:\Temp\WifiProfile\
Tên file thường có dạng:
Wi-Fi-CongDuan87.xml
netsh wlan export profile chính thức hỗ trợ export một WLAN profile ra XML.
Không cần key=clear đối với trường hợp của bạn, vì đây là WPA2-Enterprise/EAP-TTLS chứ không phải Wi-Fi PSK thông thường.
Cách 2 — dùng chính WlanGetProfile() bằng DllImport
Đây mới là cách phù hợp với project của bạn.
Thêm API này vào WlanApi.cs:
[DllImport(
"wlanapi.dll",
CallingConvention = CallingConvention.Winapi,
CharSet = CharSet.Unicode)]
public static extern uint WlanGetProfile(
IntPtr hClientHandle,
ref Guid pInterfaceGuid,
string strProfileName,
IntPtr pReserved,
out IntPtr pstrProfileXml,
ref uint pdwFlags,
out uint pdwGrantedAccess);
Microsoft định nghĩa prototype chính thức của API là:
DWORD WlanGetProfile(
HANDLE hClientHandle,
const GUID *pInterfaceGuid,
LPCWSTR strProfileName,
PVOID pReserved,
LPWSTR *pstrProfileXml,
DWORD *pdwFlags,
DWORD *pdwGrantedAccess
);
Sau đó thêm hàm GetProfileXml()
Trong WlanClient.cs:
public string GetProfileXml(
Guid interfaceGuid,
string profileName)
{
IntPtr xmlPtr = IntPtr.Zero;
uint flags = 0;
uint grantedAccess;
uint result =
WlanApi.WlanGetProfile(
_handle,
ref interfaceGuid,
profileName,
IntPtr.Zero,
out xmlPtr,
ref flags,
out grantedAccess);
WlanApi.ThrowIfFailed(
result,
"WlanGetProfile");
try
{
return Marshal.PtrToStringUni(xmlPtr);
}
finally
{
if (xmlPtr != IntPtr.Zero)
{
WlanApi.WlanFreeMemory(xmlPtr);
}
}
}
Nhớ thêm:
using System.Runtime.InteropServices;
ở đầu WlanClient.cs.
Gọi nó với CongDuan87
Sau khi bạn tạo:
using (var wlan = new WlanClient())
{
...
}
có thể:
string xml =
wlan.GetProfileXml(
interfaceGuid,
"CongDuan87");
Console.WriteLine(xml);
File.WriteAllText(
@"C:\Temp\CongDuan87.xml",
xml,
Encoding.UTF8);
Thêm:
using System.IO;
using System.Text;
Kết quả:
C:\Temp\CongDuan87.xml
sẽ là XML đúng cái Windows đang lưu trong WLAN profile store, thay vì XML chúng ta tự đoán.
Nhưng có một điểm rất quan trọng với EAP-TTLS
WlanGetProfile() lấy WLAN connection profile.
Nó không nhất thiết cho bạn toàn bộ username/password EAP-TTLS.
Ví dụ XML bạn lấy được có thể trông như:
<WLANProfile
xmlns="http://www.microsoft.com/networking/WLAN/profile/v1">
<name>CongDuan87</name>
<SSIDConfig>
<SSID>
<name>CongDuan87</name>
</SSID>
</SSIDConfig>
<connectionType>ESS</connectionType>
<connectionMode>auto</connectionMode>
<MSM>
<security>
<authEncryption>
<authentication>WPA2</authentication>
<encryption>AES</encryption>
<useOneX>true</useOneX>
</authEncryption>
<OneX
xmlns="http://www.microsoft.com/networking/OneX/v1">
<authMode>user</authMode>
<EAPConfig>
...
EAP-TTLS
...
<PAPAuthentication />
...
</EAPConfig>
</OneX>
</security>
</MSM>
</WLANProfile>
Đây chính là profile XML mà WlanSetProfile() nhận vào. Microsoft cũng xác nhận WLAN profile được lưu trong profile store và có thể lấy lại bằng WlanGetProfile().
Tôi khuyên bạn làm theo quy trình này
Đối với CongDuan87, làm như sau:
Bước 1
Không chạy code C# của chúng ta trước.
Trên máy Windows, kết nối CongDuan87 bằng giao diện Windows.
Nếu Windows hỏi:
Username: CongDuan87
Password: ****
nhập vào và kết nối thành công.
Bước 2
Kiểm tra:
netsh wlan show profiles
Phải có:
CongDuan87
Bước 3
Export:
mkdir C:\Temp\CongDuan87
netsh wlan export profile name="CongDuan87" folder="C:\Temp\CongDuan87"
Bước 4
Mở file XML.
Đừng gửi password cho tôi.
Bạn có thể gửi cho tôi toàn bộ XML sau khi xóa/che các thông tin nhạy cảm, đặc biệt là:
<Username>...</Username>
<Password>...</Password>
nếu có.
Tôi cần xem các phần:
<OneX>
<EAPConfig>
...
</EAPConfig>
</OneX>
đặc biệt:
<EapHostConfig>
...
<EapTtls>
<ServerValidation>
...
</ServerValidation>
<Phase2Authentication>
<PAPAuthentication />
</Phase2Authentication>
<Phase1Identity>
...
</Phase1Identity>
</EapTtls>
</EapHostConfig>
Và đây mới là điểm quan trọng nhất
Nếu Windows hiện tại đã kết nối được CongDuan87, thì XML đó là golden reference cho project C# của chúng ta.
Thay vì tự viết:
TtlsXmlBuilder.cs
↓
đoán schema
↓
WlanSetProfile()
ta làm:
Windows GUI
↓
kết nối CongDuan87 thành công
↓
WlanGetProfile()
↓
XML chính xác của Windows
↓
phân tích XML
↓
C# template
↓
WlanSetProfile()
↓
WlanSetProfileEapXmlUserData()
↓
WlanConnect()
Như vậy khả năng gặp lỗi kiểu:
ERROR_BAD_PROFILE
ERROR_INVALID_PARAMETER
WLAN_REASON_CODE_...
EAP authentication failed
sẽ giảm đáng kể.
Bạn hãy chạy netsh wlan export profile name="CongDuan87" folder="C:\Temp\CongDuan87" rồi gửi nội dung file XML đó cho tôi (che password/username nếu muốn). Tôi sẽ dựa đúng XML Windows tạo ra để viết lại TtlsXml.cs cho .NET Framework 4.7.2 + DllImport wlanapi.dll, thay vì tiếp tục đoán schema.
B. Execute
Được. Với trường hợp của bạn:
.NET Framework 4.7.2
Windows 10/11 x64
SSID: CongDuan87
Security: WPA2-Enterprise
EAP: EAP-TTLS
Inner authentication: PAP
Username: CongDuan87
Password: ****
ta có thể gọi trực tiếp Wlanapi.dll bằng DllImport, không cần netsh và không cần NuGet.
Microsoft xác nhận WlanSetProfile, WlanConnect và WlanSetProfileEapXmlUserData là các API của wlanapi.dll; riêng WlanSetProfileEapXmlUserData có cảnh báo rằng EAP-TTLS có thể lỗi nếu chương trình 32-bit chạy trên Windows 64-bit, nên tôi khuyến nghị build x64.
Dưới đây là một source tương đối đầy đủ để bạn copy vào Visual Studio.
1. Cấu trúc project
WifiTtls472
│
├── WifiTtls472.csproj
│
├── Program.cs
│
├── Models
│ └── WifiConfig.cs
│
└── Native
├── WlanApi.cs
├── WlanClient.cs
└── TtlsXml.cs
2. WifiTtls472.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">
Debug
</Configuration>
<Platform Condition=" '$(Platform)' == '' ">
x64
</Platform>
<OutputType>Exe</OutputType>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<RootNamespace>WifiTtls472</RootNamespace>
<AssemblyName>WifiTtls472</AssemblyName>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>latest</LangVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
</Project>
Build platform phải là x64, không chọn Any CPU hoặc x86.
3. Models/WifiConfig.cs
namespace WifiTtls472.Models
{
public class WifiConfig
{
public string Ssid { get; set; }
public string Username { get; set; }
public string Password { get; set; }
// Có thể để null nếu chưa biết RADIUS server.
public string ServerName { get; set; }
// SHA-1 của Root CA.
// Có thể để null khi test.
public string TrustedRootCaSha1 { get; set; }
// Anonymous identity, ví dụ:
// anonymous@example.com
public string AnonymousIdentity { get; set; }
public bool IdentityPrivacy { get; set; }
public bool DisablePrompt { get; set; }
public WifiConfig()
{
IdentityPrivacy = false;
// Khi chưa có CA/server name thì để false để
// Windows có thể hỏi certificate.
DisablePrompt = false;
}
}
}
4. Native/WlanApi.cs
Đây là phần `DllImport(“wlanapi.dll”) mà bạn đang cần.
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace WifiTtls472.Native
{
internal static class WlanApi
{
private const string DLL = "wlanapi.dll";
public const uint ERROR_SUCCESS = 0;
public const uint WLAN_CLIENT_VERSION_LONGHORN = 2;
public const uint WLAN_PROFILE_USER = 0x00000002;
public const uint WLAN_SET_EAPHOST_DATA_ALL_USERS = 0x00000001;
// =========================================================
// WLAN_INTERFACE_STATE
// =========================================================
public enum WLAN_INTERFACE_STATE
{
NotReady = 0,
Connected = 1,
AdHocNetworkFormed = 2,
Disconnecting = 3,
Disconnected = 4,
Associating = 5,
Discovering = 6,
Authenticating = 7
}
// =========================================================
// WLAN_CONNECTION_MODE
// =========================================================
public enum WLAN_CONNECTION_MODE
{
Automatic = 0,
Profile = 1,
TemporaryProfile = 2,
DiscoverySecure = 3,
DiscoveryUnsecure = 4,
Auto = 5,
Invalid = 6
}
// =========================================================
// DOT11_BSS_TYPE
// =========================================================
public enum DOT11_BSS_TYPE
{
Infrastructure = 1,
Independent = 2,
Any = 3
}
// =========================================================
// WLAN_INTERFACE_INFO
// =========================================================
[StructLayout(
LayoutKind.Sequential,
CharSet = CharSet.Unicode)]
public struct WLAN_INTERFACE_INFO
{
public Guid InterfaceGuid;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string strInterfaceDescription;
public WLAN_INTERFACE_STATE isState;
}
// =========================================================
// WLAN_INTERFACE_INFO_LIST_HEADER
// =========================================================
[StructLayout(LayoutKind.Sequential)]
public struct WLAN_INTERFACE_INFO_LIST_HEADER
{
public int dwNumberOfItems;
public int dwIndex;
}
// =========================================================
// WLAN_CONNECTION_PARAMETERS
// =========================================================
[StructLayout(
LayoutKind.Sequential,
CharSet = CharSet.Unicode)]
public struct WLAN_CONNECTION_PARAMETERS
{
public WLAN_CONNECTION_MODE wlanConnectionMode;
[MarshalAs(UnmanagedType.LPWStr)]
public string strProfile;
public IntPtr pDot11Ssid;
public IntPtr pDesiredBssidList;
public DOT11_BSS_TYPE dot11BssType;
public uint dwFlags;
}
// =========================================================
// WlanOpenHandle
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi)]
public static extern uint WlanOpenHandle(
uint dwClientVersion,
IntPtr pReserved,
out uint pdwNegotiatedVersion,
out IntPtr phClientHandle);
// =========================================================
// WlanCloseHandle
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi)]
public static extern uint WlanCloseHandle(
IntPtr hClientHandle,
IntPtr pReserved);
// =========================================================
// WlanFreeMemory
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi)]
public static extern uint WlanFreeMemory(
IntPtr pMemory);
// =========================================================
// WlanEnumInterfaces
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi)]
public static extern uint WlanEnumInterfaces(
IntPtr hClientHandle,
IntPtr pReserved,
out IntPtr ppInterfaceList);
// =========================================================
// WlanSetProfile
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi,
CharSet = CharSet.Unicode)]
public static extern uint WlanSetProfile(
IntPtr hClientHandle,
ref Guid pInterfaceGuid,
uint dwFlags,
string strProfileXml,
string strAllUserProfileSecurity,
[MarshalAs(UnmanagedType.Bool)]
bool bOverwrite,
IntPtr pReserved,
out uint pdwReasonCode);
// =========================================================
// WlanSetProfileEapXmlUserData
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi,
CharSet = CharSet.Unicode)]
public static extern uint WlanSetProfileEapXmlUserData(
IntPtr hClientHandle,
ref Guid pInterfaceGuid,
string strProfileName,
uint dwFlags,
string strEapXmlUserData,
IntPtr pReserved);
// =========================================================
// WlanConnect
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi,
CharSet = CharSet.Unicode)]
public static extern uint WlanConnect(
IntPtr hClientHandle,
ref Guid pInterfaceGuid,
ref WLAN_CONNECTION_PARAMETERS pConnectionParameters,
IntPtr pReserved);
// =========================================================
// WlanDisconnect
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi)]
public static extern uint WlanDisconnect(
IntPtr hClientHandle,
ref Guid pInterfaceGuid,
IntPtr pReserved);
// =========================================================
// WlanDeleteProfile
// =========================================================
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi,
CharSet = CharSet.Unicode)]
public static extern uint WlanDeleteProfile(
IntPtr hClientHandle,
ref Guid pInterfaceGuid,
string strProfileName,
IntPtr pReserved);
// =========================================================
// WlanQueryInterface
// =========================================================
public enum WLAN_INTF_OPCODE
{
AutoconfEnabled = 1,
BackgroundScanEnabled = 2,
MediaStreamingMode = 3,
RadioState = 4,
BssType = 5,
InterfaceState = 6,
CurrentConnection = 7
}
[DllImport(
DLL,
CallingConvention = CallingConvention.Winapi)]
public static extern uint WlanQueryInterface(
IntPtr hClientHandle,
ref Guid pInterfaceGuid,
WLAN_INTF_OPCODE OpCode,
IntPtr pReserved,
out uint pdwDataSize,
out IntPtr ppData,
out uint pWlanOpcodeValueType);
// =========================================================
// Helper
// =========================================================
public static void ThrowIfFailed(
uint error,
string function)
{
if (error != ERROR_SUCCESS)
{
throw new Win32Exception(
unchecked((int)error),
function +
" failed. Win32 error = " +
error);
}
}
}
}
Các API trên tương ứng trực tiếp với WlanOpenHandle, WlanEnumInterfaces, WlanSetProfile, WlanSetProfileEapXmlUserData, WlanConnect của Windows Native Wi-Fi API.
5. Native/TtlsXml.cs
Đây là phần quan trọng nhất.
Microsoft mô tả EAP-TTLS là EAP type 21, AuthorId=311, và PAPAuthentication là lựa chọn phase 2 cho PAP.
using System.Security;
using WifiTtls472.Models;
namespace WifiTtls472.Native
{
internal static class TtlsXml
{
private const string EapHostConfig =
"http://www.microsoft.com/provisioning/EapHostConfig";
private const string EapCommon =
"http://www.microsoft.com/provisioning/EapCommon";
private const string TtlsConnection =
"http://www.microsoft.com/provisioning/EapTtlsConnectionPropertiesV1";
private const string Wlan =
"http://www.microsoft.com/networking/WLAN/profile/v1";
private const string OneX =
"http://www.microsoft.com/networking/OneX/v1";
private const string UserCredentials =
"http://www.microsoft.com/provisioning/EapHostUserCredentials";
private const string TtlsUser =
"http://www.microsoft.com/provisioning/EapTtlsUserPropertiesV1";
// =========================================================
// WLAN PROFILE
// =========================================================
public static string BuildWlanProfile(
WifiConfig config)
{
string ssid =
Xml(config.Ssid);
string serverNames =
string.IsNullOrWhiteSpace(config.ServerName)
? string.Empty
:
"<ServerNames>" +
Xml(config.ServerName) +
"</ServerNames>";
string rootCa =
string.IsNullOrWhiteSpace(
config.TrustedRootCaSha1)
? string.Empty
:
"<TrustedRootCAHash>" +
Xml(NormalizeSha1(
config.TrustedRootCaSha1)) +
"</TrustedRootCAHash>";
string anonymous =
string.IsNullOrWhiteSpace(
config.AnonymousIdentity)
? string.Empty
:
"<AnonymousIdentity>" +
Xml(config.AnonymousIdentity) +
"</AnonymousIdentity>";
return
$@"<?xml version=""1.0"" encoding=""UTF-8""?>
<WLANProfile xmlns=""{Wlan}"">
<name>{ssid}</name>
<SSIDConfig>
<SSID>
<name>{ssid}</name>
</SSID>
</SSIDConfig>
<connectionType>ESS</connectionType>
<connectionMode>auto</connectionMode>
<MSM>
<security>
<authEncryption>
<authentication>WPA2</authentication>
<encryption>AES</encryption>
<useOneX>true</useOneX>
</authEncryption>
<OneX xmlns=""{OneX}"">
<cacheUserData>true</cacheUserData>
<authMode>user</authMode>
<EAPConfig>
<EapHostConfig
xmlns=""{EapHostConfig}"">
<EapMethod>
<Type
xmlns=""{EapCommon}"">
21
</Type>
<VendorId
xmlns=""{EapCommon}"">
0
</VendorId>
<VendorType
xmlns=""{EapCommon}"">
0
</VendorType>
<AuthorId
xmlns=""{EapCommon}"">
311
</AuthorId>
</EapMethod>
<Config>
<EapTtls
xmlns=""{TtlsConnection}"">
<ServerValidation>
{serverNames}
{rootCa}
<DisablePrompt>
{config.DisablePrompt.ToString().ToLowerInvariant()}
</DisablePrompt>
</ServerValidation>
<Phase2Authentication>
<PAPAuthentication />
</Phase2Authentication>
<Phase1Identity>
<IdentityPrivacy>
{config.IdentityPrivacy.ToString().ToLowerInvariant()}
</IdentityPrivacy>
{anonymous}
</Phase1Identity>
</EapTtls>
</Config>
</EapHostConfig>
</EAPConfig>
</OneX>
</security>
</MSM>
</WLANProfile>";
}
// =========================================================
// EAP-TTLS USER CREDENTIAL
// =========================================================
public static string BuildUserCredentials(
string username,
string password)
{
return
$@"<?xml version=""1.0"" encoding=""UTF-8""?>
<EapHostUserCredentials
xmlns=""{UserCredentials}""
xmlns:eapCommon=""{EapCommon}""
xmlns:baseEap=""http://www.microsoft.com/provisioning/BaseEapMethodUserCredentials"">
<EapMethod>
<eapCommon:Type>
21
</eapCommon:Type>
<eapCommon:AuthorId>
311
</eapCommon:AuthorId>
</EapMethod>
<Credentials
xmlns=""{UserCredentials}"">
<EapTtls
xmlns=""{TtlsUser}"">
<Username>
{Xml(username)}
</Username>
<Password>
{Xml(password)}
</Password>
</EapTtls>
</Credentials>
</EapHostUserCredentials>";
}
private static string Xml(
string value)
{
return
SecurityElement.Escape(value)
?? string.Empty;
}
private static string NormalizeSha1(
string value)
{
string clean =
value
.Replace(" ", "")
.Replace(":", "")
.Replace("-", "")
.Trim();
if (clean.Length != 40)
{
throw new System.ArgumentException(
"SHA-1 phải có 40 ký tự.");
}
for (int i = 0;
i < clean.Length;
i++)
{
if (!System.Uri.IsHexDigit(
clean[i]))
{
throw new System.ArgumentException(
"SHA-1 không hợp lệ.");
}
}
var result =
new System.Text.StringBuilder();
for (int i = 0;
i < clean.Length;
i += 2)
{
if (i > 0)
result.Append(" ");
result.Append(
clean.Substring(i, 2)
.ToUpperInvariant());
}
return result.ToString();
}
}
}
Lưu ý: Microsoft schema hiện mô tả trường trusted CA là TrustedRootCAHashes; một số WLAN profile thực tế/legacy của Windows dùng TrustedRootCAHash trong XML. Vì vậy nếu bạn đã có certificate RADIUS, tôi khuyên lấy profile XML được Windows tạo ra trên đúng máy Windows của bạn rồi đối chiếu trước khi khóa certificate. Microsoft xác nhận TTLS có ServerNames, trusted root SHA-1 và DisablePrompt.
6. Native/WlanClient.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using WifiTtls472.Models;
namespace WifiTtls472.Native
{
public sealed class WlanClient : IDisposable
{
private IntPtr _handle;
public WlanClient()
{
uint result =
WlanApi.WlanOpenHandle(
WlanApi.WLAN_CLIENT_VERSION_LONGHORN,
IntPtr.Zero,
out _,
out _handle);
WlanApi.ThrowIfFailed(
result,
"WlanOpenHandle");
}
// =========================================================
// GET INTERFACES
// =========================================================
public List<WlanApi.WLAN_INTERFACE_INFO>
GetInterfaces()
{
IntPtr listPtr;
uint result =
WlanApi.WlanEnumInterfaces(
_handle,
IntPtr.Zero,
out listPtr);
WlanApi.ThrowIfFailed(
result,
"WlanEnumInterfaces");
try
{
var header =
System.Runtime.InteropServices.Marshal
.PtrToStructure<
WlanApi.WLAN_INTERFACE_INFO_LIST_HEADER>(
listPtr);
var interfaces =
new List<
WlanApi.WLAN_INTERFACE_INFO>();
int headerSize =
System.Runtime.InteropServices.Marshal
.SizeOf<
WlanApi.WLAN_INTERFACE_INFO_LIST_HEADER>();
int itemSize =
System.Runtime.InteropServices.Marshal
.SizeOf<
WlanApi.WLAN_INTERFACE_INFO>();
for (int i = 0;
i < header.dwNumberOfItems;
i++)
{
IntPtr itemPtr =
IntPtr.Add(
listPtr,
headerSize +
itemSize * i);
var item =
System.Runtime.InteropServices.Marshal
.PtrToStructure<
WlanApi.WLAN_INTERFACE_INFO>(
itemPtr);
interfaces.Add(item);
}
return interfaces;
}
finally
{
WlanApi.WlanFreeMemory(listPtr);
}
}
// =========================================================
// INSTALL PROFILE
// =========================================================
public void InstallTtlsProfile(
Guid interfaceGuid,
WifiConfig config)
{
string profileXml =
TtlsXml.BuildWlanProfile(
config);
Console.WriteLine(
"Installing WLAN profile...");
uint result =
WlanApi.WlanSetProfile(
_handle,
ref interfaceGuid,
WlanApi.WLAN_PROFILE_USER,
profileXml,
null,
true,
IntPtr.Zero,
out uint reasonCode);
if (result != WlanApi.ERROR_SUCCESS)
{
throw new Exception(
"WlanSetProfile failed. " +
"Error=" + result +
", ReasonCode=" + reasonCode);
}
Console.WriteLine(
"WLAN profile OK.");
// -----------------------------------------------------
// EAP-TTLS username/password
// -----------------------------------------------------
string credentialsXml =
TtlsXml.BuildUserCredentials(
config.Username,
config.Password);
Console.WriteLine(
"Installing EAP-TTLS credentials...");
result =
WlanApi.WlanSetProfileEapXmlUserData(
_handle,
ref interfaceGuid,
config.Ssid,
0,
credentialsXml,
IntPtr.Zero);
WlanApi.ThrowIfFailed(
result,
"WlanSetProfileEapXmlUserData");
Console.WriteLine(
"EAP-TTLS credentials OK.");
}
// =========================================================
// CONNECT
// =========================================================
public void Connect(
Guid interfaceGuid,
string profileName)
{
var parameters =
new WlanApi.WLAN_CONNECTION_PARAMETERS
{
wlanConnectionMode =
WlanApi.WLAN_CONNECTION_MODE.Profile,
strProfile =
profileName,
pDot11Ssid =
IntPtr.Zero,
pDesiredBssidList =
IntPtr.Zero,
dot11BssType =
WlanApi.DOT11_BSS_TYPE.Infrastructure,
dwFlags = 0
};
Console.WriteLine();
Console.WriteLine(
"Connecting to: " +
profileName);
uint result =
WlanApi.WlanConnect(
_handle,
ref interfaceGuid,
ref parameters,
IntPtr.Zero);
WlanApi.ThrowIfFailed(
result,
"WlanConnect");
}
// =========================================================
// DISCONNECT
// =========================================================
public void Disconnect(
Guid interfaceGuid)
{
uint result =
WlanApi.WlanDisconnect(
_handle,
ref interfaceGuid,
IntPtr.Zero);
WlanApi.ThrowIfFailed(
result,
"WlanDisconnect");
}
// =========================================================
// DELETE PROFILE
// =========================================================
public void DeleteProfile(
Guid interfaceGuid,
string profileName)
{
uint result =
WlanApi.WlanDeleteProfile(
_handle,
ref interfaceGuid,
profileName,
IntPtr.Zero);
WlanApi.ThrowIfFailed(
result,
"WlanDeleteProfile");
}
// =========================================================
// GET STATE
// =========================================================
public WlanApi.WLAN_INTERFACE_STATE
GetState(Guid interfaceGuid)
{
IntPtr data;
uint result =
WlanApi.WlanQueryInterface(
_handle,
ref interfaceGuid,
WlanApi.WLAN_INTF_OPCODE.InterfaceState,
IntPtr.Zero,
out _,
out data,
out _);
WlanApi.ThrowIfFailed(
result,
"WlanQueryInterface");
try
{
int state =
System.Runtime.InteropServices.Marshal
.ReadInt32(data);
return
(WlanApi.WLAN_INTERFACE_STATE)state;
}
finally
{
WlanApi.WlanFreeMemory(data);
}
}
// =========================================================
// WAIT CONNECTED
// =========================================================
public async Task<bool>
WaitForConnectionAsync(
Guid interfaceGuid,
int timeoutSeconds)
{
DateTime timeout =
DateTime.Now.AddSeconds(
timeoutSeconds);
while (DateTime.Now < timeout)
{
var state =
GetState(interfaceGuid);
Console.WriteLine(
"State: " + state);
if (state ==
WlanApi.WLAN_INTERFACE_STATE.Connected)
{
return true;
}
await Task.Delay(1000);
}
return false;
}
// =========================================================
// DISPOSE
// =========================================================
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
WlanApi.WlanCloseHandle(
_handle,
IntPtr.Zero);
_handle = IntPtr.Zero;
}
}
}
}
7. Program.cs
Đây là chương trình bạn có thể chạy trực tiếp.
using System;
using System.Collections.Generic;
using WifiTtls472.Models;
using WifiTtls472.Native;
namespace WifiTtls472
{
internal class Program
{
private static void Main(
string[] args)
{
Console.Title =
"WiFi EAP-TTLS - .NET Framework 4.7.2";
Console.WriteLine(
"======================================");
Console.WriteLine(
" WiFi EAP-TTLS");
Console.WriteLine(
" .NET Framework 4.7.2");
Console.WriteLine(
"======================================");
Console.WriteLine();
// -----------------------------------------------------
// Configuration
// -----------------------------------------------------
var config =
new WifiConfig
{
Ssid = "CongDuan87",
Username = "CongDuan87",
Password = ReadPassword(),
// Chưa biết RADIUS server:
ServerName = null,
// Chưa biết Root CA:
TrustedRootCaSha1 = null,
AnonymousIdentity = null,
IdentityPrivacy = false,
// false khi test.
// Production nên true khi đã cấu hình CA.
DisablePrompt = false
};
// -----------------------------------------------------
// Open WLAN API
// -----------------------------------------------------
using (var wlan =
new WlanClient())
{
List<
WlanApi.WLAN_INTERFACE_INFO>
interfaces =
wlan.GetInterfaces();
if (interfaces.Count == 0)
{
Console.WriteLine(
"Không tìm thấy Wi-Fi adapter.");
return;
}
// -------------------------------------------------
// Show adapters
// -------------------------------------------------
Console.WriteLine(
"Wi-Fi adapters:");
Console.WriteLine();
for (int i = 0;
i < interfaces.Count;
i++)
{
var item =
interfaces[i];
Console.WriteLine(
"[" + i + "] " +
item.strInterfaceDescription);
Console.WriteLine(
" GUID: " +
item.InterfaceGuid);
Console.WriteLine(
" State: " +
item.isState);
Console.WriteLine();
}
// -------------------------------------------------
// Select adapter
// -------------------------------------------------
int index = 0;
if (interfaces.Count > 1)
{
Console.Write(
"Chọn adapter [0]: ");
string input =
Console.ReadLine();
if (!string.IsNullOrWhiteSpace(
input))
{
int.TryParse(
input,
out index);
}
}
if (index < 0 ||
index >= interfaces.Count)
{
Console.WriteLine(
"Adapter không hợp lệ.");
return;
}
Guid interfaceGuid =
interfaces[index].InterfaceGuid;
// -------------------------------------------------
// Install profile
// -------------------------------------------------
try
{
wlan.InstallTtlsProfile(
interfaceGuid,
config);
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine(
"INSTALL PROFILE ERROR:");
Console.WriteLine(
ex.Message);
return;
}
// -------------------------------------------------
// Connect
// -------------------------------------------------
try
{
wlan.Connect(
interfaceGuid,
config.Ssid);
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine(
"CONNECT ERROR:");
Console.WriteLine(
ex.Message);
return;
}
// -------------------------------------------------
// Wait
// -------------------------------------------------
Console.WriteLine();
Console.WriteLine(
"Waiting for EAP-TTLS...");
bool connected =
wlan
.WaitForConnectionAsync(
interfaceGuid,
30)
.GetAwaiter()
.GetResult();
Console.WriteLine();
if (connected)
{
Console.ForegroundColor =
ConsoleColor.Green;
Console.WriteLine(
"================================");
Console.WriteLine(
" WIFI CONNECTED!");
Console.WriteLine(
"================================");
Console.ResetColor();
}
else
{
Console.ForegroundColor =
ConsoleColor.Red;
Console.WriteLine(
"================================");
Console.WriteLine(
" WIFI CONNECTION FAILED");
Console.WriteLine(
"================================");
Console.ResetColor();
Console.WriteLine();
Console.WriteLine(
"Kiểm tra:");
Console.WriteLine(
"1. Username/password");
Console.WriteLine(
"2. RADIUS server");
Console.WriteLine(
"3. RADIUS certificate");
Console.WriteLine(
"4. PAP");
Console.WriteLine(
"5. Event Viewer -> EapHost");
}
Console.WriteLine();
Console.WriteLine(
"Press ENTER to exit...");
Console.ReadLine();
}
}
private static string ReadPassword()
{
Console.Write(
"Password: ");
var password =
new System.Text.StringBuilder();
while (true)
{
ConsoleKeyInfo key =
Console.ReadKey(true);
if (key.Key ==
ConsoleKey.Enter)
{
break;
}
if (key.Key ==
ConsoleKey.Backspace)
{
if (password.Length > 0)
{
password.Length--;
Console.Write(
"\b \b");
}
continue;
}
if (!char.IsControl(
key.KeyChar))
{
password.Append(
key.KeyChar);
Console.Write("*");
}
}
Console.WriteLine();
return password.ToString();
}
}
}
8. Luồng thực thi
Source trên thực hiện đúng chuỗi:
WlanOpenHandle()
│
▼
WlanEnumInterfaces()
│
▼
chọn Wi-Fi adapter
│
▼
Build WLAN XML
│
├── WPA2
├── AES
├── 802.1X
└── EAP-TTLS
│
└── PAP
│
▼
WlanSetProfile()
│
▼
Build EAP-TTLS User XML
│
├── Username = CongDuan87
└── Password = ****
│
▼
WlanSetProfileEapXmlUserData()
│
▼
WlanConnect()
│
▼
RADIUS authentication
│
▼
Connected
WlanSetProfileEapXmlUserData chính là API Microsoft cung cấp để gán EAP user credentials vào profile; XML truyền vào phải theo EAPHost User Credentials schema.
9. Một vấn đề tôi muốn sửa so với code trước
Trong câu trả lời trước tôi đã dùng:
<TrustedRootCAHashes>
nhưng profile TTLS Windows thực tế có thể xuất hiện dưới dạng:
<TrustedRootCAHash>
và cấu trúc XML được Windows sinh ra có thể khác tùy context/profile. Ví dụ Microsoft Q&A có một WLAN profile TTLS thực tế với:
<EapTtls xmlns="http://www.microsoft.com/provisioning/EapTtlsConnectionPropertiesV1">
<ServerValidation>
<ServerNames />
<TrustedRootCAHash>...</TrustedRootCAHash>
<DisablePrompt>false</DisablePrompt>
</ServerValidation>
<Phase2Authentication>
<PAPAuthentication />
</Phase2Authentication>
</EapTtls>
Trong khi tài liệu protocol hiện tại mô tả trusted-root setting dưới TrustedRootCAHashes.
Do đó, nếu mục tiêu của bạn là làm cho CongDuan87 kết nối được ngay trên máy thực tế, bước tốt nhất là lấy XML profile TTLS do chính Windows tạo ra, sau đó tôi sẽ chuyển nguyên XML đó thành C# string/builder. Cách này tránh lỗi ERROR_INVALID_PARAMETER, ERROR_BAD_PROFILE hoặc profile cài được nhưng EAPHost không sử dụng credentials.
10. Kiểm tra trước khi chạy
Bạn cần đảm bảo service:
WLAN AutoConfig
đang chạy.
Có thể kiểm tra:
Get-Service WlanSvc
Nếu:
Status : Running
là OK.
Bạn cũng nên build:
Configuration: Release
Platform: x64
chứ không phải x86. Đây đặc biệt quan trọng với TTLS vì Microsoft cảnh báo WlanSetProfileEapXmlUserData có thể gây lỗi kết nối khi gọi từ ứng dụng 32-bit trên Windows 64-bit.
Nếu chạy mà WlanSetProfile() thành công nhưng không authenticate
Đừng vội sửa WlanConnect(). Với EAP-TTLS, phần cần kiểm tra đầu tiên là EAP XML và certificate của RADIUS.
Bạn có thể xem:
Event Viewer
└─ Applications and Services Logs
└─ Microsoft
└─ Windows
└─ EapHost
Nếu bạn gửi cho tôi Windows 10/11 và lỗi cụ thể trong EapHost/Event Viewer, tôi có thể chỉnh chính xác phần TtlsXml.cs cho profile CongDuan87, đặc biệt là RADIUS certificate + ServerNames + TrustedRootCAHash.