Windows 系統管理代理 (SNMP Extension Agent) 透過 inetmib1.dll 公開 MIB-II 物件,包含 ARP 表 (ipNetToMediaTable)。本文示範載入 DLL、初始化 SNMP 擴展代理、呼叫 SnmpExtensionQuery 進行 GETNEXT 遍歷讀取 ARP 條目,以及 SET 操作新增/修改/刪除條目,封裝為 CARP 類可直接整合。
ipNetToMediaEntry:
ipNetToMediaIfIndex INTEGER -- 介面索引 (1.3.6.1.2.1.4.22.1.1)
ipNetToMediaPhysAddress PhysAddress -- MAC 位址 (1.3.6.1.2.1.4.22.1.2)
ipNetToMediaNetAddress IpAddress -- IP 位址 (1.3.6.1.2.1.4.22.1.3)
ipNetToMediaType INTEGER -- 類型: 1=Other, 2=Invalid(刪除), 3=Dynamic, 4=Static (1.3.6.1.2.1.4.22.1.4)
INDEX { ipNetToMediaIfIndex, ipNetToMediaNetAddress }
// 讀取用 3 個 OID
UINT OID_READ[3][10] = {
{1,3,6,1,2,1,4,22,1,1}, // IfIndex
{1,3,6,1,2,1,4,22,1,2}, // PhysAddress (MAC)
{1,3,6,1,2,1,4,22,1,4} // Type
};
// 寫入用 4 個 OID (含 NetAddress)
UINT OID_WRITE[4][10] = {
{1,3,6,1,2,1,4,22,1,1}, // IfIndex
{1,3,6,1,2,1,4,22,1,2}, // PhysAddress
{1,3,6,1,2,1,4,22,1,3}, // NetAddress (IP)
{1,3,6,1,2,1,4,22,1,4} // Type
};
CARP 核心實作CARP::CARP()
{
hMIBLibrary = LoadLibrary(_T("inetmib1.dll"));
if (hMIBLibrary)
{
pfnSnmpExtensionInit = (PFNSNMPEXTENSIONINIT)GetProcAddress(hMIBLibrary, "SnmpExtensionInit");
pfnSnmpExtensionQuery = (PFNSNMPEXTENSIONQUERY)GetProcAddress(hMIBLibrary, "SnmpExtensionQuery");
if (pfnSnmpExtensionInit && pfnSnmpExtensionQuery)
{
HANDLE hTrapEvent; AsnObjectIdentifier aoiView;
bInitialized = pfnSnmpExtensionInit(0, &hTrapEvent, &aoiView);
}
}
}
GetEntriesint CARP::GetEntries(ARP_TABLE* pTable, int maxCount, int adapterIndex)
{
if (!bInitialized) return 0;
SnmpVarBindList vbl[3]; SnmpVarBind vb[3];
AsnObjectIdentifier oid[3];
AsnInteger32 errStatus[3], errIndex[3];
// 綁定 3 個 OID
for (int i=0; i<3; ++i) {
vbl[i].len = 1; vbl[i].list = &vb[i];
SnmpUtilOidCpy(&vb[i].name, &oid[i]); // oid[i] 從 OID_READ 填入
}
int count = 0;
do {
// 重置錯誤碼
for (int i=0; i<3; ++i) errStatus[i] = errIndex[i] = 0;
// GETNEXT 遍歷
if (!pfnSnmpExtensionQuery(SNMP_PDU_GETNEXT, &vbl[0], &errStatus[0], &errIndex[0])) break;
if (!pfnSnmpExtensionQuery(SNMP_PDU_GETNEXT, &vbl[1], &errStatus[1], &errIndex[1])) break;
if (!pfnSnmpExtensionQuery(SNMP_PDU_GETNEXT, &vbl[2], &errStatus[2], &errIndex[2])) break;
if (errStatus[0]!=SNMP_ERRORSTATUS_NOERROR || errStatus[1]!=SNMP_ERRORSTATUS_NOERROR || errStatus[2]!=SNMP_ERRORSTATUS_NOERROR) break;
// 檢查 OID 是否仍在 ipNetToMediaTable 子樹下
if (SnmpUtilOidNCmp(&vb[0].name, &oid[0], oid[0].idLength)) break;
if (SnmpUtilOidNCmp(&vb[1].name, &oid[1], oid[1].idLength)) break;
if (SnmpUtilOidNCmp(&vb[2].name, &oid[2], oid[2].idLength)) break;
// 比對介面索引
if (vb[0].list->value.asnValue.number == adapterIndex)
{
// 從 OID 尾端解析 IP 位址 (最後 4 個 sub-id)
ULONG* ipIds = vb[1].list->name.ids + vb[1].list->name.idLength - 4;
pTable[count].ip[0] = (BYTE)ipIds[0]; // 注意:原文碼有硬編碼偏移 44/48/52/56,此處改為通用解析
pTable[count].ip[1] = (BYTE)ipIds[1];
pTable[count].ip[2] = (BYTE)ipIds[2];
pTable[count].ip[3] = (BYTE)ipIds[3];
// MAC 位址從 PhysAddress 取得
BYTE* mac = (BYTE*)vb[1].list->value.asnValue.string.stream;
if (mac) memcpy(pTable[count].mac, mac, 6);
pTable[count].type = vb[2].list->value.asnValue.number; // 1~4
if (pTable[count].type >= 1 && pTable[count].type <= 4) count++;
}
} while (count < maxCount);
// 釋放 OID 記憶體
for (int i=2; i>=0; --i) SnmpUtilOidFree(&vb[i].name);
return count;
}
EditEntry (新增/修改/刪除)BOOL CARP::EditEntry(BYTE ip[4], BYTE mac[6], ULONG type, int adapterIndex)
{
if (!bInitialized) return FALSE;
SnmpVarBindList vbl; SnmpVarBind vb[4];
AsnObjectIdentifier oid[4];
AsnInteger32 errStatus, errIndex;
for (int i=0; i<4; ++i) {
vb[i].name = oid[i]; // 從 OID_WRITE 填入
switch (i) {
case 0: // IfIndex
vb[i].value.asnType = ASN_INTEGER;
vb[i].value.asnValue.number = adapterIndex; break;
case 1: // PhysAddress (MAC)
vb[i].value.asnType = ASN_OCTETSTRING;
vb[i].value.asnValue.string.stream = (BYTE*)mac;
vb[i].value.asnValue.string.length = 6;
vb[i].value.asnValue.string.dynamic = FALSE; break;
case 2: // NetAddress (IP)
vb[i].value.asnType = ASN_IPADDRESS;
vb[i].value.asnValue.string.stream = (BYTE*)ip;
vb[i].value.asnValue.string.length = 4;
vb[i].value.asnValue.string.dynamic = FALSE; break;
case 3: // Type
vb[i].value.asnType = ASN_INTEGER;
vb[i].value.asnValue.number = type; break; // 2=刪除, 3=動態, 4=靜態
}
SnmpUtilOidCpy(&vb[i].name, &oid[i]);
}
vbl.len = 4; vbl.list = vb;
errStatus = errIndex = 0;
BOOL ok = pfnSnmpExtensionQuery(SNMP_PDU_SET, &vbl, &errStatus, &errIndex) &&
(errStatus == SNMP_ERRORSTATUS_NOERROR);
for (int i=3; i>=0; --i) SnmpUtilOidFree(&vb[i].name);
return ok;
}
CARP arp;
ARP_TABLE table[100];
int n = arp.GetEntries(table, 100, 1); // 介面索引 1 (通常為第一張實體 NIC)
for (int i=0; i
同樣透過 inetmib1.dll 存取 MIB-II 其他表:
ipRouteTable (1.3.6.1.2.1.4.21) — 路由表ifTable (1.3.6.1.2.1.2.2) — 介面統計tcpConnTable (1.3.6.1.2.1.6.13) — TCP 連線udpTable (1.3.6.1.2.1.7.5) — UDP 端口完整 OID 樹見原文附錄 (RFC 1213 節錄)。
| 議題 | 說明 |
|---|---|
| 權限 | 修改 ARP/路由表需管理員權限 |
| SNMP 服務 | 需啟用「SNMP 服務」 (Windows 功能) 並設定 Community Name (預設 public) |
| 介面索引 | 可透過 GetAdaptersInfo 或 iphlpapi 取得正確 IfIndex |
| 動態條目老化 | 動態 ARP 會隨時間老化消失,靜態條目永久保留直到手動刪除 |
| 現代替代 | GetIpNetTable/SetIpNetEntry (IPHLPAPI) 更直接、無需 SNMP 服務 |
透過 SNMP 擴展代理操作 ARP 表展示了 Windows 管理儀器的底層機制。雖現代開發推薦使用 IPHLPAPI (GetIpNetTable 等),但理解 MIB-II、OID、SNMP PDU (GETNEXT/SET) 流程對網管系統開發、跨平台協定實作極有價值。
本文配套原始碼含 CARP 類完整實作與測試主程式。