第1回では、Windows Filtering Platform (WFP) の全体像や、コールアウトドライバー実装に必要な考え方、ALE レイヤの位置づけについて整理しました。
第2回では、いよいよ 実装に踏み出します。
ここでのゴールは、最小構成のカーネルモードのコールアウトドライバーを作成します。
1. 今回作るもの(全体像)
今回扱う構成は、WFP で最もシンプルで最小構成パターンです。
- カーネルモード
- KMDF ベース
- コールアウトドライバー(Fwps*, Fwpm* API)
- ユーザーモード
- 今回は無し
今回は、カーネルモードドライバーとユーザーモードアプリで役割分担を分けずに、ドライバー内で「どの通信をフィルターするか」、「対象の通信がきたら何をするか」をひとつのドライバーで実装します。
2. 開発環境の前提
本記事では、以下の環境を前提とします。
- Windows 11, version 25H2
Visual Studio Subscriptions - Windows 11 version 25H2 - 対応する Visual Studio 2022 + 最新の WDK (10.1.26100.6584)
WDK インストール - テストサイニング有効
テスト署名されたドライバーの読み込みを有効化 - (推奨)仮想マシン環境
3. 最小構成コールアウト ドライバーの作成
まずは、「観察だけを行う」コールアウト ドライバーを作成します。
3.1 DriverEntry の実装
DriverEntry では、主に WDF の初期化、コールアウトの登録を行います。次にその具体的なコードを記載しました。
- WDF (KMDF) の初期化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_UNLOAD EvtDriverUnload;
NTSTATUS
DriverEntry(
DRIVER_OBJECT* driverObject,
UNICODE_STRING* registryPath
)
{
NTSTATUS status;
WDF_DRIVER_CONFIG config;
WDFDRIVER driver;
PWDFDEVICE_INIT pDeviceInit = NULL;
WDFDEVICE device = NULL;
PDEVICE_OBJECT pDeviceObject;
// Windows 8 以降と以前のバージョンの Windows の両方で実行される単一のドライバー バイナリを
// 構築するには POOL_NX_OPTIN オプトイン メカニズムを使用します。
// https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/single-binary-opt-in-pool-nx-optin
ExInitializeDriverRuntime(DrvRtPoolNxOptIn);
// フレームワーク ドライバー オブジェクトの作成
WDF_DRIVER_CONFIG_INIT(&config, WDF_NO_EVENT_CALLBACK);
config.DriverInitFlags |= WdfDriverInitNonPnpDriver;
// コールアウトドライバーでは必ずアンロード ルーチンの提供が必要です
// アンロード ルーチンでコールアウトの登録解除を行わないと ドライバー がアンロードされません
config.EvtDriverUnload = EvtDriverUnload;
status = WdfDriverCreate(
driverObject,
registryPath,
WDF_NO_OBJECT_ATTRIBUTES,
&config,
&driver);
if (!NT_SUCCESS(status))
goto Exit;
// Control Device Object の作成
pDeviceInit = WdfControlDeviceInitAllocate(driver, &SDDL_DEVOBJ_KERNEL_ONLY);
if (!pDeviceInit) {
status = STATUS_INSUFFICIENT_RESOURCES;
goto Exit;
}
WdfDeviceInitSetDeviceType(pDeviceInit, FILE_DEVICE_NETWORK);
WdfDeviceInitSetCharacteristics(pDeviceInit, FILE_DEVICE_SECURE_OPEN, FALSE);
WdfDeviceInitSetCharacteristics(pDeviceInit, FILE_AUTOGENERATED_DEVICE_NAME, TRUE);
status = WdfDeviceCreate(&pDeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &device);
if (!NT_SUCCESS(status))
goto Exit;
// WDM デバイス オブジェクトの取得
pDeviceObject = WdfDeviceWdmGetDeviceObject(device);
WdfControlFinishInitializing(device);
Exit:
if (!NT_SUCCESS(status)) {
if (device != NULL) {
WdfObjectDelete(device);
device = NULL;
}
if (pDeviceInit) {
WdfDeviceInitFree(pDeviceInit);
pDeviceInit = NULL;
}
}
return status;
}; - (必要に応じて)デバイスや内部データ構造の初期化を行います。今回の例では特にないためスキップします。
- 続いて、コールアウトを登録し、フィルターを追加します。これには、次の順序で関数を呼び出します。
FwpsCalloutRegisterを呼び出して、コールアウトをフィルターエンジンに登録します。
FwpmCalloutAddを呼び出して、コールアウトをシステムに追加します。
FwpmFilterAddを呼び出して、フィルターをシステムに追加します。
以下はその一連の処理をまとめたコードです。この例では IPv4 アドレス 10.0.0.1 への接続をインターセプトするようフィルターを構成しています。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136HANDLE g_hEngineHandle = NULL;
HANDLE g_hInjectionHandle = NULL;
UINT32 g_uiCalloutId = 0;
// {5D84A707-9404-46DA-9B5A-4CB2FA2202A5}
DEFINE_GUID(WDKBLOG_WFPSAMPLE_ALE_CONNECT_CALLOUT_V4,
0x5d84a707, 0x9404, 0x46da, 0x9b, 0x5a, 0x4c, 0xb2, 0xfa, 0x22, 0x2, 0xa5);
// {00FB2DFF-9670-435B-B53D-9D5B697C69DE}
DEFINE_GUID(WDKBLOG_WFPSAMPLE_SUBLAYER,
0xfb2dff, 0x9670, 0x435b, 0xb5, 0x3d, 0x9d, 0x5b, 0x69, 0x7c, 0x69, 0xde);
NTSTATUS
RegisterAsCallout(
_In_ PDEVICE_OBJECT pDeviceObject
)
{
NTSTATUS status = STATUS_SUCCESS;
FWPM_SESSION session;
FWPS_CALLOUT sCallout;
FWPM_CALLOUT mCallout;
FWPM_DISPLAY_DATA displayData;
FWPM_SUBLAYER InspectSubLayer;
FWPM_FILTER filter;
FWPM_FILTER_CONDITION filterCondition[1] = { 0 };
// フィルター対象の IP アドレス
LPWSTR terminator;
struct in_addr remoteAddr;
RtlIpv4StringToAddressW(L"10.0.0.1", FALSE, &terminator, &remoteAddr);
// インジェクション ハンドルの取得
// ※今回はパケットのインジェクション処理は行いませんが、将来の拡張に備えてインジェクションハンドルを取得しています
status = FwpsInjectionHandleCreate(AF_INET, FWPS_INJECTION_TYPE_TRANSPORT, &g_hInjectionHandle);
if (!NT_SUCCESS(status))
goto Exit;
// フィルターエンジンへのセッションを開きます。
RtlZeroMemory(&session, sizeof(FWPM_SESSION));
// FWPM_SESSION_FLAG_DYNAMIC を指定すると、FwpmEngineClose 時に
// このセッションで追加したフィルター・コールアウトが自動で削除されます。
// テスト・サンプル用途に適していますが、永続的なフィルターが必要な場合は
// このフラグを指定しません。
session.flags = FWPM_SESSION_FLAG_DYNAMIC;
status = FwpmEngineOpen(
NULL,
RPC_C_AUTHN_WINNT,
NULL,
&session,
&g_hEngineHandle);
if (!NT_SUCCESS(status))
goto Exit;
// Read/Write トランザクションの開始
status = FwpmTransactionBegin(g_hEngineHandle, 0);
if (!NT_SUCCESS(status))
goto Exit;
// フィルターエンジンにコールアウトを登録します
RtlZeroMemory(&sCallout, sizeof(FWPS_CALLOUT));
sCallout.calloutKey = WDKBLOG_WFPSAMPLE_ALE_CONNECT_CALLOUT_V4;
sCallout.classifyFn = WdkBlogALEConnectClassifyFn;
sCallout.notifyFn = WdkBlogALEConnectNotifyFn;
status = FwpsCalloutRegister(pDeviceObject, &sCallout, &g_uiCalloutId);
if (!NT_SUCCESS(status))
goto Exit;
// コールアウト オブジェクトの追加
RtlZeroMemory(&displayData, sizeof(FWPM_DISPLAY_DATA));
displayData.name = L"Transport Inspect ALE Classify Callout";
displayData.description = L"Intercepts outbound connect attempts";
RtlZeroMemory(&mCallout, sizeof(FWPM_CALLOUT));
mCallout.calloutKey = WDKBLOG_WFPSAMPLE_ALE_CONNECT_CALLOUT_V4;
mCallout.displayData = displayData;
mCallout.applicableLayer = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
status = FwpmCalloutAdd(g_hEngineHandle, &mCallout, NULL, NULL);
if (!NT_SUCCESS(status))
goto Exit;
// このコールアウトドライバー用に、独自のサブレイヤーを作成します
// GUID は固定値として定義
// Weight は、他のフィルタと衝突しないよう適切に設定
// サブレイヤーは、「このドライバーの居場所」を決めるための重要な要素です
RtlZeroMemory(&InspectSubLayer, sizeof(FWPM_SUBLAYER));
InspectSubLayer.subLayerKey = WDKBLOG_WFPSAMPLE_SUBLAYER;
InspectSubLayer.displayData.name = L"Transport Inspect Sub-Layer";
InspectSubLayer.displayData.description = L"Sub-Layer for use by Transport Inspect callouts";
InspectSubLayer.flags = 0;
InspectSubLayer.weight = 0;
status = FwpmSubLayerAdd(g_hEngineHandle, &InspectSubLayer, NULL);
if (!NT_SUCCESS(status))
goto Exit;
// フィルターの追加
RtlZeroMemory(&filter, sizeof(FWPM_FILTER));
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
filter.displayData.name = (wchar_t*)L"Inspect filter for TCP connection request";
filter.displayData.description = (wchar_t*)L"Intercepts TCP outbound connect attempt";
filter.action.type = FWP_ACTION_CALLOUT_TERMINATING;
filter.action.calloutKey = WDKBLOG_WFPSAMPLE_ALE_CONNECT_CALLOUT_V4;
filter.filterCondition = &filterCondition[0];
filter.subLayerKey = WDKBLOG_WFPSAMPLE_SUBLAYER;
filter.weight.type = FWP_EMPTY; // auto-weight.
filter.rawContext = 0;
filter.numFilterConditions = 1;
RtlZeroMemory(&filterCondition, sizeof(FWPM_FILTER_CONDITION));
filterCondition[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS;
filterCondition[0].matchType = FWP_MATCH_EQUAL;
filterCondition[0].conditionValue.type = FWP_UINT32;
filterCondition[0].conditionValue.uint32 = RtlUlongByteSwap(remoteAddr.S_un.S_addr);
status = FwpmFilterAdd(g_hEngineHandle, &filter, NULL, NULL);
if (!NT_SUCCESS(status))
goto Exit;
// トランザクションのコミット
status = FwpmTransactionCommit(g_hEngineHandle);
if (!NT_SUCCESS(status))
goto Exit;
Exit:
if (!NT_SUCCESS(status)) {
if (g_uiCalloutId) {
FwpsCalloutUnregisterById(g_uiCalloutId);
g_uiCalloutId = 0;
}
if (g_hEngineHandle != NULL) {
FwpmEngineClose(g_hEngineHandle);
g_hEngineHandle = NULL;
}
if (g_hInjectionHandle != NULL) {
FwpsInjectionHandleDestroy(g_hInjectionHandle);
g_hInjectionHandle = NULL;
}
}
return status;
} DriverEntryにて、追加したRegisterAsCallout関数を呼び出します。1
2
3
4
5
6
7
8
9// WDM デバイス オブジェクトの取得
pDeviceObject = WdfDeviceWdmGetDeviceObject(device);
WdfControlFinishInitializing(device);
// コールアウト ドライバーとして登録し、フィルターの追加を行う
status = RegisterAsCallout(pDeviceObject);
Exit:
if (!NT_SUCCESS(status)) {
3.2 アンロード関数の実装
WFP コールアウト ドライバーでは、アンロード ルーチンの実装が必須です。アンロード ルーチンでコールアウトの登録解除を行います。
登録の解除を行わないと、ドライバーをアンロードできません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_Function_class_(EVT_WDF_DRIVER_UNLOAD)
_IRQL_requires_same_
_IRQL_requires_max_(PASSIVE_LEVEL)
void
EvtDriverUnload(
_In_ WDFDRIVER driverObject
)
{
NTSTATUS status;
UNREFERENCED_PARAMETER(driverObject);
if (g_uiCalloutId) {
status = FwpsCalloutUnregisterById(g_uiCalloutId);
g_uiCalloutId = 0;
}
if (g_hEngineHandle) {
status = FwpmEngineClose(g_hEngineHandle);
g_hEngineHandle = NULL;
}
if (g_hInjectionHandle) {
status = FwpsInjectionHandleDestroy(g_hInjectionHandle);
g_hInjectionHandle = NULL;
}
}
1 | _Function_class_(EVT_WDF_DRIVER_UNLOAD) |
3.3 通知コールアウト (notifyFn) の実装
notifyFn では、フィルターがエンジンに追加されたタイミングや削除されたタイミングで呼び出されます。
まずは、それら追加と削除のタイミングの通知を受信するコードを追加します。
以下の WdkBlogALEConnectNotifyFn は、前述の RegisterAsCallout 内で呼び出している FwpsCalloutRegister で登録している notifyFn です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22NTSTATUS
WdkBlogALEConnectNotifyFn(
_In_ FWPS_CALLOUT_NOTIFY_TYPE notifyType,
_In_ const GUID* filterKey,
_Inout_ const FWPS_FILTER* filter
)
{
UNREFERENCED_PARAMETER(filterKey);
UNREFERENCED_PARAMETER(filter);
switch (notifyType) {
case FWPS_CALLOUT_NOTIFY_ADD_FILTER:
DbgPrint("FWPS_CALLOUT_NOTIFY_ADD_FILTER\n");
break;
case FWPS_CALLOUT_NOTIFY_DELETE_FILTER:
DbgPrint("FWPS_CALLOUT_NOTIFY_DELETE_FILTER\n");
break;
default:
break;
}
return STATUS_SUCCESS;
}
3.4 分類コールアウト (classifyFn) の実装
classifyFn は、WFP によってネットワークデータが分類されるたびに呼び出されます。今回の例では、”10.0.0.1” への TCP 接続要求が発生すると呼び出されます。
最初の段階では、次のような方針で十分です。
- パケットやストリームの内容は変更しない
- 判定結果はそのまま次のフィルタへ渡す (= FWP_ACTION_CONTINUE を返す)
つまり Inline Inspection(観察のみ) です。
この段階では、 - notifyFn, classifyFn が正しく実行されるのか
- 指定したレイヤー FWPM_LAYER_ALE_AUTH_CONNECT_V4 で適切に呼びだされること
を確認することが目的になります。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36void
WdkBlogALEConnectClassifyFn(
_In_ const FWPS_INCOMING_VALUES* inFixedValues,
_In_ const FWPS_INCOMING_METADATA_VALUES* inMetaValues,
_Inout_opt_ void* layerData,
_In_opt_ const void* classifyContext,
_In_ const FWPS_FILTER* filter,
_In_ UINT64 flowContext,
_Inout_ FWPS_CLASSIFY_OUT* classifyOut
)
{
PNET_BUFFER_LIST rawData = (PNET_BUFFER_LIST)layerData;
UNREFERENCED_PARAMETER(inFixedValues);
UNREFERENCED_PARAMETER(inMetaValues);
UNREFERENCED_PARAMETER(classifyContext);
UNREFERENCED_PARAMETER(filter);
UNREFERENCED_PARAMETER(flowContext);
UNREFERENCED_PARAMETER(classifyOut);
// 分類する権限のない場合は処理をスキップ
if ((classifyOut->rights & FWPS_RIGHT_ACTION_WRITE) == 0)
goto Exit;
if (layerData != NULL) {
FWPS_PACKET_INJECTION_STATE packetState;
packetState = FwpsQueryPacketInjectionState(g_hInjectionHandle, rawData, NULL);
if ((packetState == FWPS_PACKET_INJECTED_BY_SELF) ||
(packetState == FWPS_PACKET_PREVIOUSLY_INJECTED_BY_SELF)) {
// 許可またはブロックの決定を次のフィルタに渡す場合 FWP_ACTION_CONTINUE をセットします
classifyOut->actionType = FWP_ACTION_CONTINUE;
}
}
Exit:
return;
}
4. インストール用 INF ファイルの作成
WFP コールアウト ドライバーは、プラグアンドプレイ ドライバーではないため、DefaultInstall セクションを使用してインストールを行います。
以下 INF ファイルの例です。
1 | ; |
5. ドライバーのビルド
Visual Studio でそのままビルドを行うと、定義の重複やリンク エラー等が発生するため、必要な設定を行います。
5.1 Preprocessor Definitions の追加
次の定義を追加します。
1 | NDIS630 |

5.2 Linker / Additional Dependencies の追加
リンクするするライブラリの設定として次を追加します。
1 | $(DDK_LIB_PATH)\ndis.lib |

5.3 Driver Sigining の設定
テスト証明書による署名の設定を行います。
5.4 ビルドの実行
Visual Studio の [Build] メニューからビルドを行います。
ビルドのログが以下のように succeeded となれば、完了です。wdkblogwfpsample.cat, WdkBlogWfpSample.inf, WdkBlogWfpSample.sys の 3 つのファイルが生成されます。
1 | 1> |
6. インストールと動作確認
ビルドしたコールアウト ドライバーをテスト環境にコピーして動作確認まで行います。
6.1 インストール
- テスト環境 (Bcdedit.exe -set TESTSIGNING ON が実施されている環境) にドライバーファイルとテスト証明書をコピーします。
- テスト証明書をルート証明書ストアにインストールします。
証明書のインストールは、WDK の C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0<CPU type> に含まれる CertMgr.exe を使用するか、証明書スナップイン certmgr.msc を使ってインストールします。 - 続いて、ドライバーのインストールは INF ファイルを右クリックして [Install] をクリックします。”The operation completed successfully.” と表示されたら OK です。

- インストール後、ドライバーの開始を行うため、管理者権限で起動したコマンドプロンプトから次のコマンドを入力して、ドライバーを開始します。
1
net start WdkBlogWfpSample

6.2 動作確認
ビルド・インストールしたドライバーが実行されているかどうかをカーネル デバッガー (WinDbg) で確認します。
今回は WdkBlogWfpSample.sys の関数にブレークポイントをセットし、期待通り停止するかどうかでチェックします。
[!NOTE] デバッガーが接続されていない場合は次のドキュメントを参考にデバッガーの接続を行います。
KDNET ネットワーク カーネル デバッグを自動的に設定する
KDNET ネットワーク カーネルのデバッグを手動で設定する
- まず、WinDbg の Break ボタンをクリックして、デバッガーにブレークインします。続いて、Command ウィンドウ内で .reload コマンドを実行し、シンボルをロードします。
1
2
3
4
5
6
714: kd> .reload
Connected to Windows 10 26100 x64 target at (Thu Apr 2 16:45:55.358 2026 (UTC + 9:00)), ptr64 TRUE
Loading Kernel Symbols
...............................................................
Loading User Symbols
Loading unloaded module list
........... - ドライバーが正しくロードされているか確認するため lm (List Loaded Modules) コマンドを次のように実行します。
1
2
3
4
5
6
7
8
9
10
11
12
13
1414: kd> lmDvm WdkBlogWfpSample
Browse full module list
start end module name
fffff807`54cf0000 fffff807`54cf8000 WdkBlogWfpSample (private pdb symbols) Z:\work\NET\WFP\WfpSampleBlog\x64\Debug\WdkBlogWfpSample.pdb
Loaded symbol image file: WdkBlogWfpSample.sys
Image path: \SystemRoot\System32\drivers\WdkBlogWfpSample.sys
Image name: WdkBlogWfpSample.sys
Browse all global symbols functions data Symbol Reload
Timestamp: Thu Apr 2 15:50:41 2026 (69CE11C1)
CheckSum: 0000D573
ImageSize: 00008000
Mapping Form: Loaded
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
Information from resource tables: - bu コマンドでブレークポイントをセットします。
1
bu WdkBlogWfpSample!WdkBlogALEConnectClassifyFn
- 念のため、ブレークポイントが正しくセットされているかどうかを bl コマンドで確認し、g コマンドで実行を再開します。
1
2
314: kd> bl
0 e Disable Clear fffff807`54cf19d0 [Z:\work\NET\WFP\WfpSampleBlog\driver.c @ 41] 0001 (0001) WdkBlogWfpSample!WdkBlogALEConnectClassifyFn
14: kd> g - “10.0.0.1 への接続” を意図的に発生させます。PowerShell を起動して、次のように
Test-NetConnectionコマンドを実行します。1
PS C:\Users\localAdmin> Test-NetConnection -RemoteAddress 10.0.0.1 -Port 80
- WinDbg に戻り、command ウィンドウに次のように表示されていれば、期待通りブレークポイントで停止しています。net stop WdkBlogWfpSample
1
2
3
4
5
6
7
8
9
10
11
12
13
14Breakpoint 0 hit
WdkBlogWfpSample!WdkBlogALEConnectClassifyFn:
fffff807`54cf19d0 4c894c2420 mov qword ptr [rsp+20h],r9
```
実際の WinBbg の画面は次のようになります。
<img src="https://jpwdkblog.github.io/images/wfp_callout/windbg_break.png" width="50%" align="left" border="1"><br clear="left">
- `classfyFn` が呼び出されているところまで確認出来たら、最後に WdkBlogWfpSample.sys の `notifyFn` である `WdkBlogALEConnectNotifyFn` にご自身でブレークポイントをセットして、フィルターの削除で呼び出されるかどうかをチェックしてみましょう。
このコールアウト ドライバーでは、ドライバーのアンロード タイミングで
FwpmEngineClose → 動的セッション終了 → フィルター自動削除 → notifyFn(FWPS_CALLOUT_NOTIFY_DELETE_FILTER)が呼ばれる
という流れの処理が期待されます。そのため、ドライバーをアンロードすることで `notifyFn` が呼び出されます。
コールアウト ドライバーをアンロードさせるためには、次のコマンドを実行します。アンロード処理のタイミングで期待通り `notifyFn` が呼び出されると次の画像のようになります。 <img src="https://jpwdkblog.github.io/images/wfp_callout/windbg_break2.png" width="50%" align="left" border="1"><br clear="left">
7. まとめ
第2回では、WFP コールアウトドライバーの 最小の実装と動作確認 を行いました。
- 今回のコールアウトドライバーでは、「フィルターのポリシー設定」と「
classifyFnとnotifyFnの実装」を行いました。 classifyFnではパケット自体の変更や操作は行わず、次のフィルターを呼び出す FWP_ACTION_CONTINUE を返す実装をしました。- 実際にドライバーをインストールして、
classifyFnとnotifyFnが呼び出される確認を行いました。
次回以降で、今回のコードを使ったカスタマイズ例や、デバッガー以外での動作確認方法などを紹介できればと思います。
変更履歴2026/04/02 created by riwaida
※ 本記事は 「jpwdkblog について」 の留意事項に準じます。
※ 併せて 「ホームページ」 および 「記事一覧」 もご参照いただければ幸いです。