前回は、Docker Desktop を Linux Container Mode で利用した際の構成についてまとめた。
Docker Desktop の復習と、Windows Container に入門: Docker Desktop + Linux Container 復習編
Docker Desktop の復習と、Windows Container に入門: Windows Server Container 理論編
Docker Desktop の復習と、Windows Container に入門: Windows Hyper-V Container, LCOW 理論編
Docker Desktop の復習と、Windows Container に入門: 実践編
今回は、いよいよ Windows Container についての概要と、Windows Server Container の理論についてまとめる。
また、実際にどの様に動作するのか、具体的にも迫れるだけ迫ってみたい。
前準備
門をくぐる前に、準備しなければいけないことがある。
Mode の切り替え
まずは、Linux Container Mode からの切り替え。
Docker Desktop の場合、タスクバーのコンテキストメニューから簡単に切り替えることができる。
念の為、一度 Docker Desktop の Restart をしておくと良い。
調査ツール導入
Windows の調査する場合、Sysinternals を入れておくと便利。
scoop で簡単に入るのでオススメ。
PS> scoop install sysinternals
● Process Explorer
全 Process についての基本的な情報を閲覧できる基本ツール。
● Process Monitor
Process による File 操作, Registry 操作, Network, Event などの全ログを確認できるツール。
● Object Manager namespace viewer
Windows Container の要である Object Namespace を閲覧するためのツール。
前準備終わり
Windows Container とは
そもそも Windows Container とは、Windows の NT Kernel で動作する Container のこと と、少なくとも自分は分類している。
Docker Desktop for Win の Linux Container Mode も、Linux Container on Windows も、名前に Windows と入っていても実際には Linux Kernel で動作している Linux Container だ。
Docker Engine
Docker Engine ( Daemon ) にも、Windows Platform 向けと Linux Platform 向けがある。
関係性は以下となっている。
Kernel | Docker Engine Platform | |
---|---|---|
Windows Server Container | NT Kernel | Windows |
Windows Hyper-V Container | NT Kernel | Windows |
LCOW | Linux Kernel | Windows |
Linux Container | Linux Kernel | Linux |
Document
Windows Container は兎に角ドキュメントが少なく、その少ない情報から推測していくしかない。
網羅的に書かれている資料としては、以下が参考になると思う。
コンテナの種類
Windows Container で扱う Container の種類を見ていく。
まず、Windows Container には、Process Isolation
と Hyper-V Isolation
という 2 つの分離レベルがある。
分離レベル : Process Isolation
Kernel を Host と共有し、Container は 1 つの Process として動作する方式。
Windows Server Container, Windows Process Container とも呼ばれる。
同じ方式を取っている Container Runtime としては runC や rkt 等がある。
- 利点
- Process なので、Memory Footprint が小さい
- Hyper-V が不要
- 欠点
- kernel を共有するため、悪意ある Container からの Kernel Exploit リスクがある
--isolation process
と指定することで切り替えが可能
PS> docker run -it --isolation process mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
分離レベル : Hyper-V Isolation
Container 毎に軽量 VM ( UtilityVM ) を立ち上げ、その上に Container を建てる方式。
Windows Hyper-V Container とも呼ばれる。
https://docs.microsoft.com/ja-jp/virtualization/windowscontainers/manage-containers/hyperv-container
似た方式を取っている Container Runtime としては Kata Containers 等がある。
- 利点
- kernel レベルで分離されるので、Host への Kernel Exploit は原理上起こりえない
- 欠点
- Memory Footprint がでかい
- Hyper-V が有効になっている必要がある
--isolation hyperv
と指定することで切り替えが可能
PS> docker run -it --isolation hyperv mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
番外 : LCOW ( Linux Containers on Windows )
これは Linux Container であるが、Windows Container Ecosystem では重要な役割を担う。
LCOW とは、Windows Docker Engine から Linux Container を立ち上げるという機能。
原理としては、Windows Hyper-V containers と同じで、軽量 VM ( LinuxKit for lcow ) を立ち上げて、その上で Linux Container を立ち上げる。
https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/linux-containers
Windows Hyper-V containers の原理を考えれば、当然これは出来るだろうと想像していたが、Windows Container 用に Container Image を作り直す必要がない、というのは思っていた以上に重要な機能だったとしみじみ感じている。
- 利点
- Windows Container 用に Container Image 作り直す必要がなく、既存の資産が利用できる
- 欠点
- Linux Container Mode との Image Cache の共有ができない
- まぁ、面倒と容量の問題
- Linux Container Mode との Image Cache の共有ができない
--platform linux
と指定することで切り替えが可能
PS> docker run -it --platform -p 80:80 linux nginx
インターフェイス
次は、Docker Engine から Kernel まで間にある、様々な Interface を見ていく。
その前に復習として、Linux Docker Engine の場合は、以下のような構成になっている。
HCS ( Host Compute Service )
Windows Container を構成するいくつかの低レベル機能の操作を抽象化した Interface を提供するサービス。
https://docs.microsoft.com/en-US/virtualization/windowscontainers/deploy-containers/containerd
https://blogs.technet.microsoft.com/virtualization/2017/01/27/introducing-the-host-compute-service-hcs/
Windows Container を Docker Engine と統合する際、cgroups や Namespaces 等の Interface をそれぞれ模すような API を提供するという選択肢もあったが、仕様安定性や応用性を考慮してかそれを選ばず、新たに安定的で使いやすい抽象 API を提供するサービスを構築することにした。
HCS は、Job Object や Silos、UnionFS 等の操作をする API を提供するサービス。
実態は vmcompute.dll
である。
また、直接 C API を呼び出さなくても利用できるよう、Go Wrapper である hcsshim や C# Wrapper がある。
現在、Windows Docker Engine には hcsshim が組み込まれている。
HCN ( Host Compute Network )
https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/architecture
HCN は、元々 HNS ( Host Network Service ) と呼ばれていた機能で、仮想 Switch や Firewall, Endpoint 設定等の提供するサービス。
実態は computenetwork.dll
と思われる。
HCS と同様 hcsshim で呼び出しができる。
https://github.com/Microsoft/hcsshim/tree/master/hcn
OCI, CRI
Linux の構成と見比べれば一目瞭然だが、上記構成では Docker Engine ( の中の hcsshim ) から直接 HCS, HCN を呼び出している為、OCI ( Open Container Initiative ) や CRI ( Container Runtime Interface ) に対応できない。
そこで、runhcs という OCI に準拠した実装を用意している。
将来は、Windows Platform containerd + runhcs の構成になる模様。
明言されていないが、Linux 版 containerd は Networking 辺りも担当しているので、Windows 版 containerd が HCN に対応していくと予想。
プロセス分離原理
まずは、Windows process container がどの様に Host 環境から分離されているのかを見ていく。
構成技術の比較
技術的について網羅的にまとめられている以下を参考に見ていく。
Windows container security - Docker, Inc.
まずは、Linux Process Container との比較。
Windows container security - Docker, Inc. |
Linux Container |
Windows Process Container |
|
---|---|---|
Resource Limitation (CPU,Memory,IO) |
cgroups | Job Objects |
Syscall Filtering | seccomp | Win32k Blacklist |
Sandboxing | Capability | AppContainer |
Change Root | pivot_root | Silos |
Registry | × | Silos |
UnionFS | aufs, overlayfs, ... | wcifs |
Process | Namespaces | Silos |
Network | Network Namespaces | Silos? |
聞き慣れない要素が多いので、1 つずつ見ていく。
Job Objects
Windows container security - Docker, Inc. |
Process を Group 化し管理できる機能。
https://docs.microsoft.com/en-us/windows/desktop/ProcThread/job-objects
デフォルトでは Child Process は同じ Job に属するので、Process Tree の一部 Branch をまとめて管理するという使い方ができる。
- Process を Group 単位で操作
- Group 毎の Resource Limitation
- Execution time
- CPU affinity
- Memory Usage
- Priority
- Number of process
- Job 内の Process 死亡を検知
Job object 機能自体は古くからあるものだが、Anniversary Update により、作成された Job が JID ( Job ID ) という識別子を持つように変更された。
Silo
Windows container security - Docker, Inc. |
Job Objects を拡張し、リソース ( NT Object ) を Namespace 毎に分離する機能も持たせたもの。
元々 Windows は 1 つの KernelMode と 1 つの UserMode しか持っていなかったが、Windows Server 2016 以降は複数の UserMode を持てるようになった。Silos は Host の UserMode とは別に、Windows Container という特殊な UserMode を作っているという事らしい。
Silo は、Namespace 作成後、JID 経由で Job Object に assign される。
( 下図は WinObj のキャプチャ。 2136
3572
が JID )
Silo の操作は基本的には vmcompute.dll
からしかできない様になっている。
- Namespace による分離
- NT Object
- Registry
- Network
- volume mount
Document がない !!!
多分、分離機能の中で最も重要な要素であるはずなのに、とにかく情報がない。
探した中で最も詳しく書かれている文書は、分厚い Windows Internals だった ( 以下は Google Books の Preview )。
Windows Internals, Part 1: System architecture, processes, threads, memory management, and more - Google Books
その他、以下動画や資料などで情報を補完した。
- Windows Server & Docker - The Internals Behind Bringing Docker & Containers to Windows - Black Belt - Youtube
- 同動画スライド - slideshare
- Deep dive into Windows Server Containers and Docker – Part 2 – Underlying implementation of Windows Server Containers
- コンテナー - Windows Server コンテナーで Windows 開発者が Docker を使えるように - MSDN Magazine
Syscall Filtering
Windows container security - Docker, Inc. |
Container 内からの Syscall を制限する機能。
全ての Syscall が対象ではなく、Win32k.sys のみらしい。
脆弱性が度々発覚し kernel exploit の Target となり易いからと考えられる。
詳細な原理についての説明は見つけられなかった。
見つけた中でこれらに一番近いものとして、Edge の Win32k Syscall Filtering がある。
Win32k Syscall Filtering
Win32k Syscall API は 約 1400 あるが、その中で Edge が動作するのに必要最低限の Syscall API のみアクセスを許可するフィルタ機能。
https://www.slideshare.net/PeterHlavaty/rainbow-over-the-windows-more-colors-than-you-could-expecthttps://blogs.technet.microsoft.com/iftekhar/2017/08/28/threat-mitigation-in-windows-10/
https://improsec.com/tech-blog/win32k-system-call-filtering-deep-dive
ただし、これは Edge にしか搭載していないとあり、Windows Container に使われているという情報は見つけられなかった。
Sandboxing ( Capability ACL )
Windows container security - Docker, Inc. |
この文脈としての Sandboxing とは、Capability ACL について言及していると考えられる。
Linux の capability の場合、root の持つ権能を分割し、個別に Process に Add/Drop する事ができる。
アプリケーションの実行環境として Container を使う分には不要な機能だが、例えば Container を管理する Container を立ち上げる場合などでは必須な機能となる。
これも詳細な原理については説明は無かったが、AppContainer というキーワードがあり、これに関連すると考えられる。
AppContainer
アプリケーション単位で実行環境を Sandboxing する機能。主に Windows Store アプリを安全に動作させる事を目指し導入された。
AppContainer は Integrity : AppContainer
という Low よりも強く操作が制限された状態で起動しながらも、マニフェストファイルで宣言した Capability がそれぞれ別途付与される。これにより、Capability の細やかな管理を可能としている。
与えられた Capability は、専用の特殊な Group ( Flags: Capability
, Name: APPLICATION_PACKAGE_AUTHORITY\XXXXXXX
( SID: S-1-15-3-XXXXX ) ) への所属をもって管理される。
wcifs ( Windows Container Isolation FileSystem )
Windows container security - Docker, Inc. |
Windows で Union FS Like な Layered Filesystem を実現する FS Filter Driver。
https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts
Windows へ向けた多くのアプリケーションは FileSystem が NTFS であることを期待し、新しい Filesystem を追加するのは困難であった。
その為、wcifs はベースは NTFS で、新たに Filter Driver のみ追加し、擬似的にそれを実現しているようだ。
Filter Driver の実体は、wcifs.sys
。
- NTFS と認識
- Layer Capabilities
- 下位層は Reparse Points ( NTFS の SymLink ) により参照
- 最上位層として、Virtual Hard Disk を利用
- Copy on Write
- 変更を Filter がキャッチし、最上位層である Virtual Hard Disk に書き込む
動作確認 : Windows process container
では、実際の環境を見ていく。
PS> $ docker info
# Containers: 0
# Running: 0
# Paused: 0
# Stopped: 4
# Images: 4
# Server Version: 18.09.2
# Storage Driver: windowsfilter (windows) lcow (linux)
# Windows:
# LCOW:
# Logging Driver: json-file
# Plugins:
# Volume: local
# Network: ics l2bridge l2tunnel nat null overlay transparent
# Log: awslogs etwlogs fluentd gelf json-file local logentries splunk syslog
# Swarm: inactive
# Default Isolation: hyperv
# Kernel Version: 10.0 17763 (17763.1.amd64fre.rs5_release.180914-1434)
# Operating System: Windows 10 Pro Version 1809 (OS Build 17763.316)
# OSType: windows
# Architecture: x86_64
# CPUs: 4
# Total Memory: 15.82GiB
# Name: SG04-NB-038
# ID:
# Docker Root Dir: C:\ProgramData\Docker
# Debug Mode (client): false
# Debug Mode (server): true
# File Descriptors: -1
# Goroutines: 26
# System Time: 2019-02-23T00:31:45.5057749+09:00
# EventsListeners: 1
# Registry: https://index.docker.io/v1/
# Labels:
# Experimental: true
# Insecure Registries:
# 127.0.0.0/8
# Live Restore Enabled: false
# Product License: Community Engine
以下で Docker から Windows Container を立ち上げ、その様子を観察していく。
PS> docker run -it --isolation process mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
Services
Windows は Linux とは違い、直接 Syscall せずに DLL を介して kernel Mode にアクセスする。
しかし、これら DLL が依存する System Service が User Mode に存在する為、Container はこれら System Service を丸ごと含む必要がある。
docker run ~
すると、smss.exe
の下に新たに smss.exe
ができ、また csrss.exe
と wininit.exe
Tree が新たに起動されるのが確認できる。
Windows Internals によると、Silo Namespace 作成後、
- Job Object に関連付けられた Smss を作成
- Smss が Session 0 の初期化処理として、
Wininit.exe
,Csrss.exe
を起動 -
Wininit.exe
がservices.exe
,Lsass.exe
を起動し、自動起動サービスが立ち上がっていく -
CExecSvc.exe
サービスが、Docker run
で指定されたコマンドを実行する
という順番で起動していくとのこと。
Linux Docker の docker run --init
みたいなイメージかな。
Object
WinObj で Object の変化を見てみる。
Container Job Object
\
の中に Container_<<container_id>>
という Job Object が追加されている。
これは、Process に関連付けられた Job Name と一致する。
PS> docker inspect 19a | wsl jq '.[0].Id'
# "19a639be74b7a2569e37d26fef02637039ddce3c3408b61c23f9a7d1f1f6bee1"
Silo Namespace
\Silos\
という Directory 以下に 4 桁数字の Directory が追加される。
この数字が JID になるようだ。
-
\Device\
-
\Global\Device
への SymLink- アクセスできる Device が絞られている
-
NamedPipe
,MountPointmanager
,Mailslot
,MQAC
だけ SymLink ではなく、Namespace 内に分離されている
-
-
\GLOBAL??\
- ここにある Object は Userspace からアクセスできる
-
\\.
で呼び出す ( ex.\\.\pipe\
→\GLOBAL??\pipe
)
-
- ここにある Object は Userspace からアクセスできる
-
\DosDevice
- MS-DOS Device Object
-
Global??\
への SymLink - COM Port や Drive を示す際に使われる Alias ?
-
\Driver
,Filesystem
,Registry
-
Global??\
への SymLink - Host と同じものを利用
-
-
\GLOBAL??\C:
- Drive Letter Object
- Volume に Drive Letter を割り当てるとできる
- HarddiskVolume への SymLink
-
\DosDevice\C:
→\GLOBAL??\C:
→\Device\HarddiskVolume4
-
-
\GLOBAL??\Volume{<GUID>}
- Volume Object
- HarddiskVolume への SymLink
-
\DosDevice\Volume{...}
→\GLOBAL??\Volume{...}
→\Device\HarddiskVolume4
-
-
\SystemRoot
- Windows System の Root Object
-
Global??\C:\windows
への SymLink
● Kernel Object
Kernel レベルで見てみる。
今回は LiveKD を使って以下で WinDbg を起動する。
PS> livekd.exe -k "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe"
Silo 一覧は、!Silo
で見られる。
Silo の情報はこんな感じ。
ちなみに、Host のは !Silo -g Host
で見られる。
Globals はこんな感じ。
Container Template File
Windows Container を作る時、wsc.def
( Windows Server Container ? ) という定義を元に作成される。
%SystemRoot%\System32\Containers\wsc.def
にある。
あくまで Template なので、Container の立ち上げ方次第では設定値は変わるはず。
Object Section
<container>
<namespace>
<ob shadow="false">
<symlink name="FileSystem" path="\FileSystem" scope="Global" />
<symlink name="PdcPort" path="\PdcPort" scope="Global" />
<symlink name="SeRmCommandPort" path="\SeRmCommandPort" scope="Global" />
<symlink name="Registry" path="\Registry" scope="Global" />
<symlink name="Driver" path="\Driver" scope="Global" />
<objdir name="BaseNamedObjects" clonesd="\BaseNamedObjects" shadow="false"/>
<objdir name="GLOBAL??" clonesd="\GLOBAL??" shadow="false">
<!-- Valid links to \Device -->
<symlink name="WMIDataDevice" path="\Device\WMIDataDevice" scope="Local" />
<symlink name="UNC" path="\Device\Mup" scope="Local" />
<symlink name="Tcp" path="\Device\Tcp" scope="Local" />
<symlink name="MountPointManager" path="\Device\MountPointManager" scope="Local" />
<symlink name="Nsi" path="\Device\Nsi" scope="Local" />
<symlink name="fsWrap" path="\Device\FsWrap" scope="Local" />
<symlink name="NDIS" path="\Device\Ndis" scope="Local" />
<symlink name="TermInptCDO" path="\Device\TermInptCDO" scope="Local" />
</objdir>
<objdir name="Device" clonesd="\Device" shadow="false">
<symlink name="Afd" path="\Device\Afd" scope="Global" />
<symlink name="ahcache" path="\Device\ahcache" scope="Global" />
<symlink name="CNG" path="\Device\CNG" scope="Global" />
<symlink name="ConDrv" path="\Device\ConDrv" scope="Global" />
<symlink name="DeviceApi" path="\Device\DeviceApi" scope="Global" />
<symlink name="DfsClient" path="\Device\DfsClient" scope="Global" />
<symlink name="DxgKrnl" path="\Device\DxgKrnl" scope="Global" />
<symlink name="FsWrap" path="\Device\FsWrap" scope="Global" />
<symlink name="Ip" path="\Device\Ip" scope="Global" />
<symlink name="Ip6" path="\Device\Ip6" scope="Global" />
<symlink name="KsecDD" path="\Device\KsecDD" scope="Global" />
<symlink name="LanmanDatagramReceiver" path="\Device\LanmanDatagramReceiver" scope="Global" />
<symlink name="LanmanRedirector" path="\Device\LanmanRedirector" scope="Global" />
<symlink name="MailslotRedirector" path="\Device\MailslotRedirector" scope="Global" />
<symlink name="Mup" path="\Device\Mup" scope="Global" />
<symlink name="Ndis" path="\Device\Ndis" scope="Global" />
<symlink name="Nsi" path="\Device\Nsi" scope="Global" />
<symlink name="Null" path="\Device\Null" scope="Global" />
<symlink name="PcwDrv" path="\Device\PcwDrv" scope="Global" />
<symlink name="RawIp" path="\Device\RawIp" scope="Global" />
<symlink name="RawIp6" path="\Device\RawIp6" scope="Global" />
<symlink name="Tcp" path="\Device\Tcp" scope="Global" />
<symlink name="Tcp6" path="\Device\Tcp6" scope="Global" />
<symlink name="Tdx" path="\Device\Tdx" scope="Global" />
<symlink name="Udp" path="\Device\Udp" scope="Global" />
<symlink name="Udp6" path="\Device\Udp6" scope="Global" />
<symlink name="VolumesSafeForWriteAccess" path="\Device\VolumesSafeForWriteAccess" scope="Global" />
<symlink name="VRegDriver" path="\Device\VRegDriver" scope="Global" />
<symlink name="WMIDataDevice" path="\Device\WMIDataDevice" scope="Global" />
<symlink name="TermInptCDO" path="\Device\TermInptCDO" scope="Global" />
<symlink name="RdpVideoMiniport0" path="\Device\RdpVideoMiniport0" scope="Global" />
</objdir>
<objdir name="NLS" clonesd="\NLS" shadow="false"/>
<objdir name="UMDFCommunicationPorts" clonesd="\UMDFCommunicationPorts" shadow="false"/>
</ob>
...
\FileSystem
, \PdcPort
, \SeRmCommandPort
, \Registry
, \Driver
は \Global\*
に直接 Link されているようだ。
\BaseNamedObjects
と \GLOBAL??
は Clone されているので別物。
Job Section
<container>
<namespace>
....
<job>
<systemroot path="C:\Windows" />
</job>
....
Job に SystemRoot を設定する必要性が不明。
Mountmgr Section
<container>
<namespace>
....
<mountmgr>
</mountmgr>
....
Default では特に何もしない。
NamedPipe Section
<container>
<namespace>
....
<namedpipe>
</namedpipe>
....
これも Default では特に何もしない。
Registry Section
<container>
<namespace>
....
<registry>
<symlink
key="$SiloHivesRoot$\Silo_$SiloName$_Security\SAM"
target="\Registry\Machine\SAM\SAM"
/>
<symlink
key="$SiloHivesRoot$\Silo_$SiloName$_User\S-1-5-18"
target="\Registry\User\.Default"
/>
<symlink
key="$SiloHivesRoot$\Silo_$SiloName$_System\CurrentControlSet"
target="\Registry\Machine\SYSTEM\ControlSet001"
/>
<symlink
key="$SiloHivesRoot$\Silo_$SiloName$_System\ControlSet001\Hardware Profiles\Current"
target="\Registry\Machine\System\ControlSet001\Hardware Profiles\0001"
/>
<hivestack hive="machine">
</hivestack>
<hivestack hive="security">
</hivestack>
<hivestack hive="system">
</hivestack>
<hivestack hive="software">
</hivestack>
<hivestack hive="sam">
</hivestack>
<hivestack hive="user">
</hivestack>
<hivestack hive="defaultuser">
</hivestack>
<RedirectionNode
ContainerPath="\Registry\MACHINE"
HostPath="$SiloHivesRoot$\Silo_$SiloName$_Machine"
access_mask="0xffffffff"
/>
<RedirectionNode
ContainerPath="\Registry\MACHINE\Hardware"
HostPath="\Registry\MACHINE\Hardware"
access_mask="0x83020019"
TrustedHive="true"
/>
<RedirectionNode
ContainerPath="\Registry\MACHINE\SOFTWARE"
HostPath="$SiloHivesRoot$\Silo_$SiloName$_Software"
access_mask="0xffffffff"
TrustedHive="true"
/>
<RedirectionNode
ContainerPath="\Registry\MACHINE\SYSTEM"
HostPath="$SiloHivesRoot$\Silo_$SiloName$_System"
access_mask="0xffffffff"
TrustedHive="true"
/>
<RedirectionNode
ContainerPath="\Registry\MACHINE\SYSTEM\ControlSet001\Control\Nsi"
HostPath="\Registry\MACHINE\SYSTEM\ControlSet001\Control\Nsi"
access_mask="0x83020019"
/>
<RedirectionNode
ContainerPath="\Registry\MACHINE\SYSTEM\ControlSet001\Control\SystemInformation"
HostPath="\Registry\MACHINE\SYSTEM\ControlSet001\Control\SystemInformation"
access_mask="0x83020019"
/>
<RedirectionNode
ContainerPath="\Registry\MACHINE\SAM"
HostPath="$SiloHivesRoot$\Silo_$SiloName$_Sam"
access_mask="0xffffffff"
TrustedHive="true"
/>
<RedirectionNode
ContainerPath="\Registry\MACHINE\Security"
HostPath="$SiloHivesRoot$\Silo_$SiloName$_Security"
access_mask="0xffffffff"
TrustedHive="true"
/>
<RedirectionNode
ContainerPath="\Registry\USER"
HostPath="$SiloHivesRoot$\Silo_$SiloName$_User"
access_mask="0xffffffff"
/>
<RedirectionNode
ContainerPath="\Registry\USER\.DEFAULT"
HostPath="$SiloHivesRoot$\Silo_$SiloName$_DefaultUser"
access_mask="0xffffffff"
/>
</registry>
....
Registry は後ほど詳細を見ていく。
Resource Limitation
Job Object の持つ Resource 制限機能を利用している。
試しに Memory Limitation をかけてみる。
https://docs.microsoft.com/en-us/windows/desktop/ProcThread/job-objects#job-limits-and-notifications
PS> docker run -it --isolation process -m "100m" mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
PS> docker inspect dd | wsl jq '.[0].HostConfig.Memory'
# 104857600
この Container の Job Object を ProcessExplorer で見てみると、Memory Limit がかかっているのが確認できる。
Process
● Container 内
Container 内から見えるのは、Job Object に関連付けられた smss
が立ち上げた子プロセスに限られる。
(CONTAINER)> tasklist /SVC
# Image Name PID Services
# ========================= ======== ============================================
# System Idle Process 0 N/A
# System 4 N/A
# smss.exe 11804 N/A
# csrss.exe 18240 N/A
# wininit.exe 17392 N/A
# services.exe 12668 N/A
# lsass.exe 8328 SamSs
# svchost.exe 11272 DcomLaunch, LSM, SystemEventsBroker
# svchost.exe 16444 RpcEptMapper, RpcSs
# fontdrvhost.exe 14512 N/A
# svchost.exe 12728 gpsvc, iphlpsvc, ProfSvc, Schedule, SENS,
# UserManager, UsoSvc, Winmgmt
# svchost.exe 14768 EventSystem, nsi
# CExecSvc.exe 17812 cexecsvc
# svchost.exe 15756 Dhcp, EventLog, TimeBrokerSvc,
# WinHttpAutoProxySvc
# svchost.exe 6204 CryptSvc, Dnscache, LanmanWorkstation, WinR
# conhost.exe 16212 N/A
# powershell.exe 16728 N/A
# svchost.exe 15860 CoreMessagingRegistrar
# svchost.exe 12604 DiagTrack
# svchost.exe 18144 SysMain
# msdtc.exe 11284 MSDTC
# tasklist.exe 10076 N/A
# WmiPrvSE.exe 18920 N/A
唯一、smss
自身と System
, System Idle Process
が見えている。Kernel Mode を共有しているので当然か。
不思議なのが、Container の smss
を立ち上げているのが、PID 4 の方の smss
で、User Mode が Nest しているんだなぁと。
以下のようにはならないんだ。
System
├─ smss
( PID : 4 )
└─ smss
( PID : 11804 )
● Container 外
さて、外からどう見えるかと言うと、Process Explorer で見えている事からも分かる通り、丸見えとなる。
これは、Linux Process Container と同じ挙動だ。
PS> tasklist /SVC | findstr 16728
# powershell.exe 16728 N/A
● 分離原理
正直、これは分からない。
資料などを見ていると、Process Table を Silo が分離しているという説明が多い。
● Kernel Object
Kernel レベルで見てみる。
ServerCore Container を 1 つ立ち上げ、起動した powershell.exe を調べる。
powershell.exe
に Job が関連付けられているのが分かる。
Job を調べると、Silo
Flag が付けられている。
その Silo は Server Silo Type で、RootDirectry が \Silos\1192
と分かる。
◆ 結論
答えはわからないが、 Process 単位では Job ( Server Silo ) との関連を持っているので、その辺でゴニョゴニョしてるのかなぁ。
Syscall Filtering
足がかりが全く無い。関係ありそうな情報としては、以下などが見つかったが。
https://improsec.com/tech-blog/win32k-system-call-filtering-deep-dive
http://redplait.blogspot.com/2016/11/w32pservicetablefilter-from-windows-10.html
参考サイト の通りにやってみたが、MicrosoftEdgeCP.exe
の _EPROCESS は EnableFilteredWin32kAPIs
Field を持っていなかった。
W32pServiceTableFilter
や W32pArgumentTableFilter
辺りなのかなぁとは思いつつ、これ以上 Deep な世界に行く能力もなく、ここらへんで断念。
◆ 結論
詳細な動作原理が分からなかった。
そもそも、Docker の --security-opt
に Windows で使えそうなセキュリティオプションがなく、カスタマイズのしようが無い。
https://docs.docker.com/engine/reference/run/#security-configuration
Sandboxing for capability
● AppContainer
まずは、AppContainer についてざっと見てみる。
Store App である Calculator の場合。
インターネット接続 ( S-1-15-3-1 )
Capability が付与されていると分かる。
Calculator マニフェストファイル を確認すると、確かに Capability が付与されている。
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/2010/manifest" xmlns:build="http://schemas.microsoft.com/developer/appx/2012/build" IgnorableNamespaces="build">
...
<Capabilities>
<Capability Name="internetClient" />
</Capabilities>
...
</Package>
設定できる Capabilities は、以下に一覧されている。
https://docs.microsoft.com/ja-jp/windows/uwp/packaging/app-capability-declarations
確かにこれを応用すれば、Linux の Capability と同じ様な機能を実現できそうだ。
● Windows Container
次は Windows Container を立ち上げて見てみる。
意外にも、Container 内の powershell.exe の Integrity は High で、UAC 昇格後と同等の相当高いレベル。
Group にも、Capability に関するものが何もない。
もしかして、全く違う機構で動いているんだろうか。
● Docker Option
よく分からないので、--cap-add=SYS_ADMIN
オプションを付けて起動してみるが、Integrity も Group も変化が無かった。
PS> docker run -d --isolation=process --cap-add=SYS_ADMIN mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
PS> docker inspect 633 | wsl jq '.[0].HostConfig.CapAdd'
# [
# "SYS_ADMIN"
# ]
じゃあ、今度は思い付きで --cap-drop=internetClient
とやってみる。
PS> docker run -it --isolation=process --cap-drop=internetClient mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
PS> docker inspect 633 | wsl jq '.[0].HostConfig.CapDrop'
# [
# "internetClient"
# ]
(CONTAINER)> curl https://httpbin.org/get
# {
# "args": {},
# "headers": {
# "Accept": "*/*",
# "Host": "httpbin.org",
# "User-Agent": "curl/7.55.1"
# },
# ...
多分間違ってるのに起動はしてしまうんだ。当然効果はない。
そもそも、Docker 公式ドキュメントの --cap-add
--cap-drop
には Add/Drop Linux capabilities と記述があり 、付けても意味がないのかも。
◆ 結論
分からなかった。
そもそも、Windows Container の Capability について語られているものがこの資料しか見つからない。
動作原理はおろか、本当に動いているのかどうかすら分からなかった。Windows はお呼びでない可能性もある。
発表資料にある Capability-based Access Control
とは、一体何だったのだろう。
Change Root
振る舞いを明確に解説した情報が見つけられなかったので、Object の設定から調べた。
新たに JID 2136
の Container を立ち上げたとする。
\Silos\2136\GLOBAL??\C:
と \Silos\2136\GLOBAL??\Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b}
の Link は HarddiskVolume68
を指している。
HarddiskVolume68
は、Disk 2 の Partition 2 上に mount されている。( Volume と Disk の関連を確実に見つけるなら、この方法で で )
これを diskpart
で確認すると、Disk 2 は C:\ProgramData\Docker\windowsfilter\d67daa4ef88...\sandbox.vhdx
にある Virtual Hard Disk であると分る。
PS> diskpart
DISKPART> list vdisk
# 仮想ディスク ### ディスク ### 状態 種類 ファイル
# ---------------- ------------ -------------------- ---------- --------
# 仮想ディスク 0 ディスク 2 アタッチされたディスクは開いていません 拡張可能 C:\ProgramData\Docker\windowsfilter\d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286\sandbox.vhdx
# 仮想ディスク 1 ディスク 1 アタッチされたディスクは開いていません 拡張可能 C:\ProgramData\Docker\windowsfilter\ff205a90d5d80582a0a73df0b388ea4fb63367d2155e3102325b194d9b124acb\sandbox.vhdx
Disk 2 の実体のある d67daa4ef88....
フォルダは、先程立ち上げた Container の ID と一致する。
つまり、Container が立ち上がると、新たに windowsfilter
フォルダ下に Container ID
フォルダが作成され、そこに新たな Virtual Hard Disk が作られる。
Container の Silo Namespace にある ~\GLOBAL??\C:
と ~\GLOBAL??\Volume{...}
の参照 先を、先程作成された Virtual Hard Disk に向けることで、pivot_root ( chroot ) と同等な File System Sandbox 機能を実現していると考えられる。
◆ 結論
これら情報を照らし合わせると、
- Container 起動
-
C:\ProgramData\Docker\windowsfilter
以下に Container ID と同名のフォルダ作成 - その中に Virtual Hard Disk ( 差分 Disk ( 詳細後述 ) ) を作成、Volume として利用可能な状態とする
- Silo Namespace 内の
\Global??\C:
や\Global??\Volume{...}
の参照先を 3 の Volume に - Container の Boot 時には、
\SystemRoot
の参照先である\Global??\C:\windows
から Windows を立ち上げる
と予測できる。
実験
試しに、Container を一旦止めて、Host から閲覧可能かやってみる。
Container を止めると Virtual Hard Disk は止まってしまうので、attach から始める。
PS> diskpart
DISKPART > list vdisk
# 仮想ディスク ### ディスク ### 状態 種類 ファイル
# ---------------- ------------ -------------------- ---------- --------
# 仮想ディスク 0 ディスク --- 追加済み 不明 C:\ProgramData\Docker\windowsfilter\d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286\sandbox.vhdx
DISKPART > select vdisk file="C:\ProgramData\Docker\windowsfilter\d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286\sandbox.vhdx"
DISKPART > attach vdisk
# 100% 完了しました
# DiskPart により、仮想ディスク ファイルがアタッチされました。
DISKPART > list vol
# Volume ### Ltr Label Fs Type Size Status Info
# ---------- --- ----------- ---- ---------- ------- --------- --------
# ...
# Volume 6 NTFS Partition 19 GB 正常
DISKPART > select volume 6
# ボリューム 6 が選択されました。
DISKPART > assign letter=x
# DiskPart はドライブ文字またはマウント ポイントを正常に割り当てました。
UnionFS
Virtual Hard Disk レベルで切り替えがされていることは分かった。
しかし、Docker の特徴でもある、差分毎の Image Layer や Copy on Write 等については、どの様に実現するのだろうか。
通常、Linux Container の場合、overlayfs や aufs の様な Union filesystem を利用する。
https://docs.docker.com/storage/storagedriver/overlayfs-driver/
しかし、NTFS は union filesystem には対応できない。
Windows Container の Storage Driver は、lcow (linux) windowsfilter (windows)
になっている。
どうやらこの windowsfilter
が wcifs の事のようだ。
● Image Layer
Docker Image を pull すると、C:\ProgramData\Docker\windowsfilter
以下に 2 つのフォルダができる。
PS> docker pull mcr.microsoft.com/windows/nanoserver:1809
# 1809: Pulling from windows/nanoserver
# ...
PS> ls C:\ProgramData\Docker\windowsfilter | ft -Property Name,Attributes -HideTableHeaders
# 0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196 Directory
# e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038 Directory
これらは、展開された Image の各 Layer のファイル差分 ( Snapshot ? ) となっている。
例えば nanoserver:1809
は、2 つの Layer で構成されているのが分かる。
PS> docker image inspect mcr.microsoft.com/windows/nanoserver:1809 | wsl jq '.[0].GraphDriver'
# {
# "Data": {
# "dir": "C:\\ProgramData\\Docker\\windowsfilter\\0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196"
# },
# "Name": "windowsfilter"
# }
PS> cat C:\ProgramData\Docker\windowsfilter\0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196\layerchain.json
# ["C:\\ProgramData\\Docker\\windowsfilter\\e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038"]
PS> cat C:\ProgramData\Docker\windowsfilter\e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038\layerchain.json
# null
実験
C:\ProgramData\Docker\windowsfilter\<<Image Layer ID>>\Files
が File 実体のはず。
という事で、Host の Explorer から直接ここを変更した場合、Container にどういった影響があるのか実験してみる。
・Layer 2 ( 上位 Layer ) を変更
C:\
直下への変更はなぜか伝わらなかった。
加えた変更が伝わるフォルダと、伝わらないフォルダがある。
伝わらないフォルダは空フォルダであった。
・Layer 1 ( 下位 Layer ) に変更
やはり C:\
直下への変更は伝わらない。
Layer 2 で伝わらなかった空フォルダへの変更のみ伝わった。
これら結果から、以下が予想される。
-
C:\
だけは特殊なフォルダ - フォルダ単位で参照しに行く Layer を記憶している ?
- もしくは、上位が空なら下位に聞く、という事か ?
- 下位 Layer に参照しに行くフォルダは、上位 Layer では空になっている
- という事は、上位 Layer で空フォルダが無くなる = フォルダ削除 ?
● Merged Layer
まずは Container を立ち上げる。
取り敢えず、mount 状況を確認する。
PS> docker run -it mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
(CONTAINER)> mountvol C: /L
# \\?\Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b}\
windowsfilter
フォルダに、新たに Merged Layer を格納するフォルダが追加されている。
PS> docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS # PORTS NAMES
# 0934e4f05940 mcr.microsoft.com/windows/nanoserver:1809 "cmd.exe" 32 seconds ago Up 28 seconds brave_jackson
PS> ls C:\ProgramData\Docker\windowsfilter | ft -Property Name,Attributes -HideTableHeaders
# 0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519 Directory
# 0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196 Directory
# e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038 Directory
PS> cat C:\ProgramData\Docker\windowsfilter\0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519\layerchain.json | wsl jq '.'
# [
# "C:\\ProgramData\\Docker\\windowsfilter\\0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196",
# "C:\\ProgramData\\Docker\\windowsfilter\\e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038"
# ]
Merged Layer の中身は、Storage の項で見たとおり Virtual Hard Disk になる。
PS> ls C:\ProgramData\Docker\windowsfilter\0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519 | ft -Property Name,Attributes -HideTableHeaders
# layerchain.json Archive
# sandbox.vhdx Archive
Container の中で、ファイルを編集してみる。
編集された影響は、C:\ProgramData\Docker\windowsfilter\<<Image Layer ID>>\Files
へは及んでいない。
Copy on Write が実現できている様だ。
(CONTAINER)> dir
# 09/15/2018 04:14 PM 5,510 License.txt
# 02/26/2019 04:35 AM <DIR> Users
# 02/26/2019 04:35 AM <DIR> Windows
(CONTAINER)> echo 'hoge' > test.txt
(CONTAINER)> dir
# 02/26/2019 04:38 AM 8 hoge.txt
# 09/15/2018 04:14 PM 5,510 License.txt
# 02/26/2019 04:35 AM <DIR> Users
# 02/26/2019 04:35 AM <DIR> Windows
実験
まずは、この sandbox.vhdx
がどこから来たのか調べてみると、どうやら 差分 Disk だったらしい。
親フォルダの e0a98d172d8...
は、Base とした Image の最下層 Layer を展開したフォルダであった。
PS> diskpart
DISKPART > select vdisk file="C:\ProgramData\Docker\windowsfilter\0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519\sandbox.vhdx"
# DiskPart により、仮想ディスク ファイルが選択されました。
DISKPART > detail vdisk
# デバイスの種類 ID: 3 (不明)
# ベンダー ID: {EC984AEC-A0F9-47E9-901F-71415A66345B} (Microsoft Corporation)
# 状態: 追加済み
# 仮想サイズ: 20 GB
# 物理サイズ: 23 MB
# ファイル名: C:\ProgramData\Docker\windowsfilter\0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519\sandbox.vhdx
# 子: はい
# 親ファイル名: C:\ProgramData\Docker\windowsfilter\e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038\blank-base.vhdx
# 関連付けられたディスク番号: 見つかりません。
また、Copy on Write がどう実現されているのかを確認しよう。
Container 内部で write.log
というファイルを作成すると、\Device\HarddiskVolume12\write.log
に書き込まれているのが確認できた。
どうやら Write は通常取り書き込まれているようで、つまり Read の時にその参照先を切り替える事で UnionFS を実現しているようだ。
● Data Volume Layer, Bind Mount
Data Volume は C:\ProgramData\Docker\volumes
内に設置される。
PS> docker volume create cache
# cache
PS> docker volume inspect cache | wsl jq '.[0].Mountpoint'
# "C:\\ProgramData\\Docker\\volumes\\cache\\_data"
PS> ls C:\ProgramData\Docker\volumes | ft -Property Name,Attributes -HideTableHeaders
# cache Directory
# metadata.db Archive
PS> docker run -it -v "cache:c:\temp" mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
(CONTAINER)> dir
# 09/15/2018 04:14 PM 5,510 License.txt
# 02/26/2019 05:57 AM <DIR> temp
# 02/26/2019 05:57 AM <DIR> Users
# 02/26/2019 05:57 AM <DIR> Windows
PS> docker inspect be13f4b02824 | wsl jq '.[0].Mounts'
# [
# {
# "Type": "volume",
# "Name": "cache",
# "Source": "C:\\ProgramData\\Docker\\volumes\\cache\\_data",
# "Destination": "c:\\temp",
# "Driver": "local",
# "Mode": "",
# "RW": true,
# "Propagation": ""
# }
# ]
Bind Mount も同様。
PS> docker run -it -v "c:\src:c:\src" mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
(CONTAINER)> dir
# 09/15/2018 04:14 PM 5,510 License.txt
# 02/26/2019 05:57 AM <DIR> temp
# 02/26/2019 05:57 AM <DIR> Users
# 02/26/2019 05:57 AM <DIR> Windows
PS> docker inspect be13f4b02824 | wsl jq '.[0].Mounts'
# [
# {
# "Type": "bind",
# "Source": "c:\\src",
# "Destination": "c:\\src",
# "Mode": "",
# "RW": true,
# "Propagation": ""
# }
# ]
● wcifs
File Read の参照先を切り替えているのが wcifs.sys
だ。
https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/anti-virus-optimization-for-windows-containers
上記リンクより、Redirect には reparse points ( NTFS における Simlink ) を利用していると。
書き込みが起こった際には、書き込みは Sanbox 内に行い、reparse points をそっちに向けることで Copy on Write を実現しているらしい。
上記を透過的に行っているのが wcifs.sys
になる。
まず、wcifs の設定を確認するため、HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\wcifs
を見てみると、FltMgr
の管理する Filter であることが分かる。
また、Instance は Host に 1 つと、Container 毎に 1 つづつ存在している。
以下は、Container を 2 つ立ち上げている状態。wcifs Outer Instance
は利用していない模様。
PS> fltmc instances -f wcifs
# wcifs フィルターのインスタンス:
# ボリューム名 階層 インスタンス名 フレーム Vl 状態
# ------------------------------------- ------------ ---------------------- ----- --------
# C: 189900 wcifs Instance 0
# 189900 wcifs Instance 0
# 189900 wcifs Instance 0
# overlayfs は、こんな感じに mount 時に指定する
$ mount -t overlay overlay -o lowerdir=/volumes/layer_0:/volumes/layer_1,upperdir=/volumes/layer_3,workdir=/volumes/layer_merged
それがどこなのかは分からなかった。これ以上は Kernel Debug でもしないと分からなそう。
◆ 結論
という事で、これら情報を照らし合わせると、
- Docker Image を Pull
-
C:\ProgramData\Docker\windowsfilter
以下に Layer 毎にフォルダを作成 -
C:\ProgramData\Docker\windowsfilter\\<<GUID>>\\Files
に Layer のファイル展開 -
docker volume create <<dir_name>>
した場合は、C:\ProgramData\Docker\volumes
以下に<dir_name>
フォルダを作成 - Container 起動
- Change Root の手順
- 何らかの方法 で、Layer 情報を wcifs に伝える or wcifs が参照するデータ ? ファイル ? Object ? に設定する
- Container 内で File Write が発生すると、sandbox.vhdk 上に変更が書き込まれ reparse points 先をそっちに変更
- Container 内で File Read が発生すると、reparse points 先を読み込む
と予測できる。
Registry
Registry も Windows においては重要なデータである。
App-V 1703 Virtual Registry and Containers?
● Registry とは
そもそも Registry は、Hive というファイルを HKLM
, HKCU
や HKLM\SECURITY
等に mount して 1 つの大きな Tree Structure を構築したもの。
以下表を見ると分かる通り、mount, SymLink, 疑似 FS と NTFS よりも Linux のそれに近い。
実体 | 参照 | |
---|---|---|
HKEY_CLASSES_ROOT| |
HKLM\SOFTWARE\Classes と HKCU\Software\Classes を merge した仮想 Key |
|
HKEY_CURRENT_CONFIG| | HKLM\SYSTEM\CurrentControlSet\Hardware Profiles\Current |
|
HKEY_USERS\<SID> |C:\Users\<USERNAME>\NTUSER.DAT
|
||
HKEY_CURRENT_USER| | ログイン中の HKEY_USERS\<SID>\
|
|
HKEY_LOCAL_MACHINE\* | C:\Windows\System32\config\* |
|
HKEY_LOCAL_MACHINE\HARDWARE | Hardware 情報を Registry として閲覧できる。 Linux の /dev みたいなもの。 |
つまり、Container は HKEY_LOCAL_MACHINE と HKEY_USERS を管理すれば良いという事になる。
● 実践
という事で、まずは Process Explorer で確認してみたが、普通にアクセスしているようにしか見えない。
どういう事 ?
● Hive ファイル
よく分からないので、新たに Container を立ち上げてその過程を Process Monitor で確認すると、\Registry\WC\Silo ~
というレコードが沢山出てきた ( 通常は、HKCU\…
や HKLM\…
になる )。
このレコードの最初の出処まで遡って見てみると、\Registry\WC\Silo9dd9eeab-...-b0acd579bc17system
等が RegLoadKey されていた。
その備考欄には Hive Path: Volume{9dd9eea3-...}\WcSandboxState\Hives\software_Delta
とある。
9dd9eea3-...
という GUID は立ち上げた Container の sandbox.vhdx
の事で、これはつまり Container 内の Hive File を読み込んだんだ と分かる。
しかし、一体これらはどこで定義されているのか。
Windows は起動時に読み込まれる Hive の List を HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\hivelist
に定義しているので、そこを確認してみると 、偶然 \REGISTRY\WC\Silo ~
という Value をいくつも見つけた。
・\REGISTRY\WC\Silo + <_Random GUID_> + [ machine | user ]
Value : \Device\HarddiskVolume4\Windows\System32\containers\machine_user
HarddiskVolume4
は Host の C:
Volume の事。
つまり Host の C:\Windows\System32\containers\machine_user
の事。
・\REGISTRY\WC\Silo + <_Random GUID_> + [ defaultuser | sam | security | software | system ]
Value : \Device\HarddiskVolume76\WcSandboxState\Hives\ + [ defaultuser | sam | security | software | system ] + _Delta
Container の Virtual Hard Disk 上にある Hive File を指している。
・\REGISTRY\WC\Silo + <_Another Random GUID_> + [ defaultuser | sam | security | software | system ]
Value : \Device\HarddiskVolume4\ProgramData\Docker\windowsfilter\0b497555b76d5...\Hives\ + [ defaultuser | sam | security | software | system ] + _Base
0b497555b76d5...
は Container の Base Image の最上位 Layer を展開したフォルダ名。
また、同じフォルダには XXX_Delta
も存在している。
実験
Hive ファイルが増えてややこしくなてきたので、少しまとめたい。
その為に、試しに各所の Software の Hive 復元してみよう。
Hive の復元は、regedit の Import 機能で適当な場所に展開する。
Container から Hive ファイルを抜き出す為に、Virtual Hard Disk に Drive Letter ( 今回は Q ) を与えて、Explorer 経由で取ってくる。
Container ID | b6478d22cc7443... |
Base Image Top Layer | 0b497555b76d5... |
Base Image Bottom Layer | e0a98d172d860... |
・Base Hive in Top Image
場所 : C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Hives\Software_Base
・Delta Hive in Top Image
場所 : C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Hives\Software_Delta
・Base Hive in Bottom Image
場所 : C:\ProgramData\Docker\windowsfilter\e0a98d172d860...\Hives\Software_Base
上記 3 つをそれぞれ復元し、比較すると、Base Hive in Top Image = Base Hive in Bottom Image + Delta Hive in Top Image
と分かった。
+ |
= |
・System Hive in Container
場所 : Q:\Windows\System32\config\SOFTWARE
参照元 : C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Files\Windows\System32\config\SOFTWARE
Windows は起動時に smss
が C:\Windows\System32\config\
以下の Hive を読み込んで Registry を構成する。
サイズを見て『もしや…』と思い C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Hives\Software_Base
と比較すると一致した。どうやら同じもののようだ。
PS> reg compare "HKEY_CURRENT_USER\RestoreFromContainer\SOFTWARE" "HKEY_CURRENT_USER\RestoreFromWindowsfilter\SOFTWARE" /s
# 比較の結果: 一致
# この操作を正しく終了しました。
・Delta Hive in Container
場所 : Q:\WcSandboxState\Hives\Software_Delta
参照元 : ?
ProcessMonitor のログで RegLoadKey されていた Hive。
Q:WcSandboxState
は読み込み権限が無いので、適当に権限追加して開けるようにする ( Container 起動中は取ってこれないので一旦止める必要がある )。
上記 2 つと、Container 内から見える Registry を比較すると、Container Registry = System Hive in Container + Delta Hive in Container
と分かった。
+ |
= |
|
もし Base Hive in Top Image が下層 Layer の変更を全て merge した Snapshot であるとするなら、あとは Container 内で Copy on Write が実現できれば良い。
この Delta ファイルが Copy on Write の Copy と考えれば辻褄が合う。
● Registry Virtualization
実は、Registry には既に Copy on Write を実現している機能が存在する。
Windows Vista 以降、以下機能が追加されている。
- UAC 昇格前のアクセスを deny するのではなく、仮想の Registry Tree に Copy on Write する機能
- ただし、仮想化が許されるのは
HKEY_LOCAL_MACHINE\Software
以下のみ ?
完全に同じものではないようだが、似た原理は利用しているのかもしれない。
◆ 結論
これら情報を照らし合わせると、
- Container 起動
- Container 内にある
C:\WcSandboxState\Hives\*
を\REGISTRY\WC\Silo + <Random GUID>
に mount - Container 内にある
C:\Windows\System32\config\*
を\REGISTRY\WC\Silo + <Other Random GUID>
に mount - Container 内で Registry Write が発生すると、
\REGISTRY\WC\Silo + <Random GUID>
以下に書き込まれる - Container 内で Registry Read が発生すると、
\REGISTRY\WC\Silo + <Random GUID>
と\REGISTRY\WC\Silo + <Other Random GUID>
を merge した Virtual Registry から読み込む
と予測できる。
Network
次は、Network がどうなるか。
Network については、資料がとても充実している。
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/architecture
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-drivers-topologies
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-isolation-security
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/advanced#dhcp-ip-assignment-not-supported-with-l2bridge-networks
- https://blogs.technet.microsoft.com/virtualization/2016/05/05/windows-container-networking/
- https://docs.microsoft.com/en-us/windows-server/networking/sdn/manage/connect-container-endpoints-to-a-tenant-virtual-network
Network Namespace の実現方法は置いといて、まずは各 Docker Network の種類からみていく。
● nat
Host 上に仮想 NAT を置き、Container はその NAT 配下の Subnet に参加するという構成。
Container 間通信 | ○ |
外部 Outbound 通信 | ○ |
外部 Inbound 通信 | × |
Docker Desktop を導入すると、Host に vEthernet (nat)
という 仮想 NIC が作成される。
PS> Get-NetAdapter | ? {$_.Name -eq "vEthernet (nat)"} | ft -Property Name,MacAddress,DeviceID,DeviceName,InterfaceIndex,InterfaceName,InterfaceType,Virtual
# Name MacAddress DeviceID DeviceName InterfaceIndex InterfaceName InterfaceType Virtual
# ---- ---------- -------- ---------- -------------- ------------- ------------- -------
# vEthernet (nat) 00-15-5D-4E-59-28 {9AAC8FFE-AD59-456E-A61F-F805BDD8E456} \Device\{9AAC8FFE-AD59-456E-A61F-F805BDD8E456} 81 ethernet_32779 6 True
PS> Get-NetIPAddress | ? {$_.InterfaceIndex -eq 81} | ft -Property IPAddress,InterfaceAlias,AddressFamily,PrefixLength,IPAddress,Type
# IPAddress InterfaceAlias AddressFamily PrefixLength IPAddress Type
# --------- -------------- ------------- ------------ --------- ----
# fe80::3064:109f:dea2:c5be%81 vEthernet (nat) IPv6 64 fe80::3064:109f:dea2:c5be%81 Unicast
# 172.26.16.1 vEthernet (nat) IPv4 20 172.26.16.1 Unicast
PS> Get-NetNat
#
PS> Get-NetNatSession
# NatName Protocol InternalSourceAddress InternalSourcePort InternalDestinationAddress InternalDestinationPort ExternalSourceAddress ExternalSourcePort ExternalDestinationAddress ExternalDestinationPort
# ------- -------- --------------------- ------------------ -------------------------- ----------------------- --------------------- ------------------ -------------------------- -----------------------
# ICS9AAC8FFE-AD59-456E-A61F-F805BDD8E456 1 172.26.30.77 1 8.8.8.8 1 192.168.100.5 1000 8.8.8.8 1000
# ...
WinNAT であるとするなら Get-NetNat
で表示されるはずだが、なぜか空っぽだった。
しかし Get-NetNatSession
にレコードはあるという謎挙動。
まぁ置いといて。
ExternalSourceAddress : 192.168.100.5
は Host の Default Root なので、NAPT されている事が分かる。
Docker からは以下のように認識される。
PS> docker network inspect 92320d040238
# [
# {
# "Name": "nat",
# "Id": "92320d0402389650219d42455fd7ac317de0eb6411bad5a52bb84965ce90558a",
# "Created": "2019-03-08T22:16:48.0365891+09:00",
# "Scope": "local",
# "Driver": "nat",
# "EnableIPv6": false,
# "IPAM": {
# "Driver": "windows",
# "Options": null,
# "Config": [
# {
# "Subnet": "0.0.0.0/0"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": {
# "d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286": {
# "Name": "wonderful_mahavira",
# "EndpointID": "2b416d30dd2957f57762790f14fcc03b238895231aac77e33cddfc6908fd7cf4",
# "MacAddress": "00:15:5d:4e:59:7d",
# "IPv4Address": "172.26.30.77/16",
# "IPv6Address": ""
# },
# ....
# },
# "Options": {
# "com.docker.network.windowsshim.hnsid": "D637C6A7-6604-4888-9FD3-3372668AACB5",
# "com.docker.network.windowsshim.networkname": "nat"
# },
# "Labels": {}
# }
# ]
次は、Container 内から確認する。
Container を立ち上げると、
PS> docker run -it --isolation process mcr.microsoft.com/windows/servercore:1809 powershell.exe
PS> docker inspect d67 | wsl jq '.[0].NetworkSettings'
# {
# "Bridge": "",
# "SandboxID": "d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286",
# "HairpinMode": false,
# "LinkLocalIPv6Address": "",
# "LinkLocalIPv6PrefixLen": 0,
# "Ports": {},
# "SandboxKey": "d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286",
# "SecondaryIPAddresses": null,
# "SecondaryIPv6Addresses": null,
# "EndpointID": "",
# "Gateway": "",
# "GlobalIPv6Address": "",
# "GlobalIPv6PrefixLen": 0,
# "IPAddress": "",
# "IPPrefixLen": 0,
# "IPv6Gateway": "",
# "MacAddress": "",
# "Networks": {
# "nat": {
# "IPAMConfig": null,
# "Links": null,
# "Aliases": null,
# "NetworkID": "92320d0402389650219d42455fd7ac317de0eb6411bad5a52bb84965ce90558a",
# "EndpointID": "2b416d30dd2957f57762790f14fcc03b238895231aac77e33cddfc6908fd7cf4",
# "Gateway": "172.26.16.1",
# "IPAddress": "172.26.30.77",
# "IPPrefixLen": 16,
# "IPv6Gateway": "",
# "GlobalIPv6Address": "",
# "GlobalIPv6PrefixLen": 0,
# "MacAddress": "00:15:5d:4e:59:7d",
# "DriverOpts": null
# }
# }
# }
(CONTAINER)> Get-NetAdapter | ft -Property Name,MacAddress,DeviceID,DeviceName,InterfaceIndex,InterfaceName,InterfaceType,Virtual
# Name MacAddress DeviceID DeviceName InterfaceIndex InterfaceName InterfaceType Virtual
# ---- ---------- -------- ---------- -------------- ------------- ------------- -------
# vEthernet (Ethernet) 00-15-5D-4E-59-7D {06956E35-47F3-4029-8708-796120B3E527} 87 iftype0_0 0
(CONTAINER)> Get-NetIPAddress | ? {$_.InterfaceIndex -eq 87} | ft -Property IPAddress,InterfaceIndex,InterfaceAlias,AddressFamily,PrefixLength,IPA
# IPAddress InterfaceIndex InterfaceAlias AddressFamily PrefixLength IPA
# --------- -------------- -------------- ------------- ------------ ---
# fe80::24cd:a697:b875:b711%87 87 vEthernet (Ethernet) IPv6 64
# 172.26.30.77 87 vEthernet (Ethernet) IPv4 20
(CONTAINER)> Get-NetIPConfiguration | ? {$_.InterfaceIndex -eq 87} | ft -Property InterfaceIndex,@{Expression={$_.IPv4Address}},@{Expression={$_.IPv4DefaultGateway.NextHop}}
# InterfaceIndex $_.IPv4Address $_.IPv4DefaultGateway.NextHop
# -------------- -------------- -----------------------------
# 87 172.26.30.77 172.26.16.1
(CONTAINER)> Get-NetRoute -AddressFamily IPv4
# ifIndex DestinationPrefix NextHop RouteMetric ifMetric PolicyStore
# ------- ----------------- ------- ----------- -------- -----------
# 87 255.255.255.255/32 0.0.0.0 256 5000 ActiveStore
# 86 255.255.255.255/32 0.0.0.0 256 75 ActiveStore
# 87 224.0.0.0/4 0.0.0.0 256 5000 ActiveStore
# 86 224.0.0.0/4 0.0.0.0 256 75 ActiveStore
# 87 172.26.31.255/32 0.0.0.0 256 5000 ActiveStore
# 87 172.26.30.77/32 0.0.0.0 256 5000 ActiveStore
# 87 172.26.16.0/20 0.0.0.0 256 5000 ActiveStore
# 86 127.255.255.255/32 0.0.0.0 256 75 ActiveStore
# 86 127.0.0.1/32 0.0.0.0 256 75 ActiveStore
# 86 127.0.0.0/8 0.0.0.0 256 75 ActiveStore
# 87 0.0.0.0/0 172.26.16.1 256 5000 ActiveStore
(CONTAINER)> Get-Netneighbor -State Stale
# ifIndex IPAddress LinkLayerAddress State PolicyStore
# ------- --------- ---------------- ----- -----------
# 87 fe80::c9e1:deb8:fbf1:9125 00-15-5D-4E-58-D0 Stale ActiveStore
# 87 172.26.27.1 00-15-5D-4E-58-D0 Stale ActiveStore
# 87 172.26.16.1 00-15-5D-4E-59-28 Stale ActiveStore
(CONTAINER)> Get-NetFirewallRule
# Get-NetFirewallRule : There are no more endpoints available from the endpoint mapper.
# ...
Network Adapter や Routing Table, Arp Table が独立していることは分かった。
Firewall はエラーが出て確認できなかった。
Linux と違うのは、Windows Container は Network namespace を超えて Switch に直接接続できるところか。
Linux は veth のペアをそれぞれの Nemaspace に置くことで接続していた。
接続された Container 間はブリッジされアクセス可能で、Host や Internet への Outbound 通信は NAPT 経由で可能だ。
外部から Container への Inbound 通信は NAPT 超えでもしない限りできない。
Port Forwarding
次に、Port Forwarding について。
IIS の Image を Pull して、アクセスできるか試してみる。
PS> docker pull mcr.microsoft.com/windows/servercore/iis
# Using default tag: latest
# latest: Pulling from windows/servercore/iis
# 65014b3c3121: Already exists
# d48f50035439: Extracting [=====> ] 74.09MB/620.8MB
# ...
PS> docker run -d --isolation process -p 8080:80 mcr.microsoft.com/windows/servercore/iis:latest
# 8f1055998ab5daff3d4e8d91f47e7742bf78b7e7b54d975b8d9e280088f7a5df
PS> docker inspect 8f105599 | wsl jq '.[0].NetworkSettings.Ports'
# {
# "80/tcp": [
# {
# "HostIp": "0.0.0.0",
# "HostPort": "8080"
# }
# ]
# }
動作原理を探ってみる。
netsh の Port Proxy 辺りかなと思っていたが、
PS> netsh interface portproxy show v4tov4
#
PS> netstat -aon | findstr 8080
#
どうやら違うようだ。
じゃあ NAT の Port Forwarding かな、と思い見てみたが、
PS> Get-NetNatStaticMapping
#
何もない。
ICS (詳細後述) も同様に Port Forwarding 機能を持っているが、これも設定されていなかった。
https://superuser.com/questions/1241347/does-ics-allow-port-forwarding
その後も色々調べてみて、まさかと思い Routing Table を見てみたら、
PS> route print
# ...
# 固定ルート:
# ネットワーク アドレス ネットマスク ゲートウェイ アドレス メトリック
# 0.0.0.0 0.0.0.0 172.24.48.1 既定
# ...
怪しいやつを発見。
vEthernet (nat)
が Gateway の乗っ取りをしているのかな。
ちなみに、Port Forward している Container を落とすと、このエントリは消える。
因みに Port Forwarding している Port にアクセスすると、NAT Session ができる。
$ curl http://192.168.100.5:8080
# <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
# <html xmlns="http://www.w3.org/1999/xhtml">
# <head>
# ...
PS> Get-NetNatSession
# NatName : ICS190A9C7A-6FC1-47F5-B6A7-8BBE2CB1FDD3
# InternalRoutingDomainId : {b1062982-2b18-4b4f-b3d5-a78ddb9cdd49}
# CreationTime : 2019/03/10 22:16:17 午後
# Protocol : 6
# InternalSourceAddress : 172.24.58.96
# InternalSourcePort : 80
# InternalDestinationAddress : 192.168.100.104
# InternalDestinationPort : 52542
# ExternalSourceAddress : 192.168.100.5
# ExternalSourcePort : 8080
# ExternalDestinationAddress : 192.168.100.104
# ExternalDestinationPort : 52542
NAT 名が 『ICS ~』 になっているのが気になるが…
ポツポツと要素は見えてきたが、それらがいまいち繋がらない。
もう少し掘り下げられそうなので、またあとで調べる。
● transparent
Host 上に作成された仮想 Switch に、Host 上の物理 NIC と Container 上の仮想 NIC をどちらも指すことで、Host の属する Subnet に参加させる構成。
いわゆる bridge 接続に近い構成が作れる。
Container 間通信 | ○ |
外部 Outbound 通信 | ○ |
外部 Inbound 通信 | ○ |
まずは、Network を作成する。
PS> docker network create -d transparent new_transparent
d80d77762f4fdf92f386cc9d0d40d273f0b225c9bbf3e1adfcef76e1c87dd8a5
PS> docker network inspect d80d77762f4f
# [
# {
# "Name": "new_transparent",
# "Id": "d80d77762f4fdf92f386cc9d0d40d273f0b225c9bbf3e1adfcef76e1c87dd8a5",
# "Created": "2019-03-12T23:13:07.251347+09:00",
# "Scope": "local",
# "Driver": "transparent",
# "EnableIPv6": false,
# "IPAM": {
# "Driver": "windows",
# "Options": {},
# "Config": [
# {
# "Subnet": "0.0.0.0/0"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": {},
# "Options": {
# "com.docker.network.windowsshim.hnsid": "565C95F0-4D7B-40CA-849F-B70AB3FAB484"
# },
# "Labels": {}
# }
# ]
Network Adapter には変化は無いが、Hyper-V には新たに仮想スイッチが追加されている。
しかし、VirtualBox の Host-Only に接続されていて、大丈夫か ? と思ったら案の定、Link Local Address が割り振られている。
PS> docker run -it --isolation process --net new_transparent mcr.microsoft.com/windows/servercore:1809 powershell.exe
(CONTAINER)> Get-NetIPAddress -AddressFamily IPv4 | ft
# ifIndex IPAddress PrefixLength PrefixOrigin SuffixOrigin AddressState PolicyStore
# ------- --------- ------------ ------------ ------------ ------------ -----------
# 49 169.254.130.120 16 WellKnown Link Preferred ActiveStore
# 48 127.0.0.1 8 WellKnown WellKnown Preferred ActiveStore
Bind NIC
どうやら、物理 NIC が複数ある場合、com.docker.network.windowsshim.interface
という Option で指定する必要がある らしい。
PS> docker network rm new_transparent
PS> docker network create -d transparent -o com.docker.network.windowsshim.interface="イーサネット" new_transparent
# d80d77762f4fdf92f386cc9d0d40d273f0b225c9bbf3e1adfcef76e1c87dd8a5
PS> docker run -it --isolation process --net new_transparent mcr.microsoft.com/windows/servercore:1809 powershell.exe
(CONTAINER)> Get-NetIPAddress -AddressFamily IPv4 | ft
# ifIndex IPAddress PrefixLength PrefixOrigin SuffixOrigin AddressState PolicyStore
# ------- --------- ------------ ------------ ------------ ------------ -----------
# 63 192.168.100.19 24 Dhcp Dhcp Preferred ActiveStore
# 62 127.0.0.1 8 WellKnown WellKnown Preferred ActiveStore
DHCP から IP も取れている。
動作原理としては、Hyper-V の 『管理オペレーティングシステムにネットワークアダプタの共有を許可する』 という機能を利用しているようだ。
この機能を有効にすると、
- 新たに仮想スイッチが作成される :
<Docker Network ID>
( 上図で言うと 3b6b8ca... ) - 物理 NIC が
Hyper-V Extensible Virtual Switch
という状態になる- 仮想スイッチ
<Docker Network ID>
の 1 ポートとして振る舞う
- 仮想スイッチ
- Host 用に仮想 NIC が1つ作られる :
vEthernet (イーサネット)
-
vEthernet (イーサネット)
が仮想スイッチ<Docker Network ID>
に接続される - Container を立ち上げると仮想 NIC が作られ、仮想スイッチ
<Docker Network ID>
に接続される
となっている。
この『管理オペレーティング…』機能の詳細は こちらの記事 が参考になる。
Host も Container も上位 Router の管理する Network に直接所属し、Container 間通信はもちろん、別 Subnet との通信もこの上位 Router によって管理され、可能となる。
VLAN
作成時に VLAN の Tag 指定 もでき、それを使えば Network を分離することもできる。
PS> docker network create -d transparent -o com.docker.network.windowsshim.vlanid=11 -o com.docker.network.windowsshim.interface="イーサネット" vlan_11
PS> docker network create -d transparent -o com.docker.network.windowsshim.vlanid=12 -o com.docker.network.windowsshim.interface="イーサネット" vlan_12
この際、物理 NIC Port は Trunk mode となり、全ての Tag を通す Port となる。
● l2bridge
主に SDN ( Software Defined Network ) での利用を目的とした Driver。
transparent と似た構成を取るが、仮想 Switch に VFP ( Virtual Filtering Platform ) という Extension が含まれている のが特徴。
この VFP により、
- Mac Address Rewrite 機能
- Container の Inbound/Outbound フレームヘッダの MAC Address を、Host の
vEthernet (イーサネット)
と同じものに書き換え
- Container の Inbound/Outbound フレームヘッダの MAC Address を、Host の
- Network Controller による Network policy のリモート制御
- Port ACLs
- Encapsulation
- QoS 制御
等の機能拡張が行われているらしい。
https://blogs.technet.microsoft.com/virtualization/2016/05/05/windows-container-networking/
https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-isolation-security
SDN 環境を用意するのは辛いので、取り敢えず Local で作って、分かる範囲で調べる。
参考リンク によると、DHCP に対応していないようで、 subnet と gateway を明示する必要があるらしい。
PS> docker network create -d l2bridge -o com.docker.network.windowsshim.interface="イーサネット" --subnet=192.168.100.0/24 --gateway=192.168.100.254 l2b
# 11df7738042939bd3c10109ea8315304a914d8811e74f05916d52ec4dc94a20f
PS> docker network inspect 11df7738042
# [
# {
# "Name": "l2b",
# "Id": "11df7738042939bd3c10109ea8315304a914d8811e74f05916d52ec4dc94a20f",
# "Created": "2019-03-13T00:32:10.8494309+09:00",
# "Scope": "local",
# "Driver": "l2bridge",
# "EnableIPv6": false,
# "IPAM": {
# "Driver": "windows",
# "Options": {},
# "Config": [
# {
# "Subnet": "192.168.100.0/24",
# "Gateway": "192.168.100.254"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": {},
# "Options": {
# "com.docker.network.windowsshim.hnsid": "5C46A2B6-666C-4467-A06F-140262C92027",
# "com.docker.network.windowsshim.interface": "イーサネット"
# },
# "Labels": {}
# }
# ]
Container を立ち上げる。
PS> docker run -it --isolation process --net l2b mcr.microsoft.com/windows/servercore:1809 powershell.exe
(CONTAINER)> Get-NetIPAddress -AddressFamily IPv4 | ft
# ifIndex IPAddress PrefixLength PrefixOrigin SuffixOrigin AddressState PolicyStore
# ------- --------- ------------ ------------ ------------ ------------ -----------
# 45 192.168.100.207 24 Manual Manual Preferred ActiveStore
# 44 127.0.0.1 8 WellKnown WellKnown Preferred ActiveStore
この時に作られた仮想 Switch を Hyper-V Manager から見てみると『Microsoft Azure VFP Switch Extension』が有効になっているのが分かる。
MAC Address Rewrite
どのように MAC Address が書き換わるのかを確認する。
Container を参加させた Subnet 内の適当な PC へ Packet を送信し、その時の Arp Table の状態を確認してみる。
PS> Get-NetAdapter | ? { $_.Name -eq "イーサネット" } | fl -Property MacAddress
# MacAddress : AD-5C-E2-55-79-1A
PS> ping 192.168.100.150
# 192.168.100.150 に ping を送信しています 32 バイトのデータ:
# 192.168.100.150 からの応答: バイト数 =32 時間 =12ms TTL=118
# 192.168.100.150 からの応答: バイト数 =32 時間 =9ms TTL=118
# ...
PS> Get-NetNeighbor | ? { $_.IPAddress -eq "192.168.100.150" }
# ifIndex IPAddress LinkLayerAddress State PolicyStore
# ------- --------- ---------------- ----- -----------
# 25 192.168.100.150 D4-31-8E-5A-B2-1C Reachable ActiveStore
PS> Get-NetAdapter | fl -Property MacAddress
# MacAddress : 00-15-5D-66-7D-5E
PS> ping 192.168.100.150
# Pinging 192.168.100.150 with 32 bytes of data:
# Reply from 192.168.100.150: bytes=32 time=1ms TTL=64
# Reply from 192.168.100.150: bytes=32 time<1ms TTL=64
# ...
PS> Get-NetNeighbor | ? { $_.IPAddress -eq "192.168.100.150" }
# ifIndex IPAddress LinkLayerAddress State PolicyStore
# ------- --------- ---------------- ----- -----------
# 45 192.168.100.150 D4-31-8E-5A-B2-1C Reachable ActiveStore
$ ip link show eth0 # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
# link/ether d4:31:8e:5a:b2:ic brd ff:ff:ff:ff:ff:ff
$ ip n | grep -e 192.168.100.5 -e 192.168.100.207
# 192.168.100.207 dev eth0 lladdr ad:5c:e2:55:79:1a STALE
# 192.168.100.5 dev eth0 lladdr ad:5c:e2:55:79:1a STALE
どちらも Host の MAC Address になっている。
正直、MAC Address を書き換える必要性は分からないが、Port ACLs 辺りに関係するのだろうか。
Kubernetes
l2bridge は、Kubernetes の L3 Routing Topology や Host Gateway Mode などで利用される。
https://kubernetes.io/docs/getting-started-guides/windows/#upstream-l3-routing-topology
https://kubernetes.io/docs/getting-started-guides/windows/#host-gateway-topology
● l2tunnel
基本的には l2bridge と同じだが、同一 Host & 同一 Subnet の場合に、l2bridge は Container 間で Bridge 通信を行うが、l2tunnel は全てのパケットを一度必ず Host の物理 NIC に送るらしい。
この場合の違いとして、l2bridge は Network policy に引っかからないが、l2tunnel は Network policy が適用される。
https://docs.microsoft.com/en-US/windows-server/networking/sdn/manage/connect-container-endpoints-to-a-tenant-virtual-network
● overlay
Docker Swarm や Kubernetes 向けの Driver。
Swarm Mode
まずは Docker を Swarm Mode に切り替える。
https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/swarm-mode
PS> docker swarm init --advertise-addr=192.168.100.9 --listen-addr 192.168.100.9:2377
# Swarm initialized: current node (bgh6nt297dyjay8e5pkf6c5pn) is now a manager.
#
# To add a worker to this swarm, run the following command:
# docker swarm join --token SWMTKN-1-3g8mm9neiee9y4tpj23azhmi964s7pbb33czbe2f7h6jwdiz2t-hkhb33as2k9i3usr823xnj3z5 192.168.100.9:2377
#
# To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
PS> docker node ls
# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
# bgh6nt297dyjay8e5pkf6c5pn * HOST_MACHINE Ready Active Leader 18.09.3
PS> docker info --format '{{json .}}' | wsl jq '.Swarm'
# {
# "NodeID": "bgh6nt297dyjay8e5pkf6c5pn",
# "NodeAddr": "192.168.100.9",
# "LocalNodeState": "active",
# "ControlAvailable": true,
# "Error": "",
# "RemoteManagers": [
# {
# "NodeID": "bgh6nt297dyjay8e5pkf6c5pn",
# "Addr": "192.168.10.9:2377"
# }
# ],
# "Nodes": 1,
# "Managers": 1,
# "Cluster": {
# "ID": "czxk5zcetu8i8axfedhpg4wrx",
# "Version": {
# "Index": 9
# },
# "CreatedAt": "2019-03-13T21:08:00.8401934Z",
# "UpdatedAt": "2019-03-13T21:08:01.4015433Z",
# "Spec": {
# "Name": "default",
# "Labels": {},
# "Orchestration": {
# "TaskHistoryRetentionLimit": 5
# },
# "Raft": {
# "SnapshotInterval": 10000,
# "KeepOldSnapshots": 0,
# "LogEntriesForSlowFollowers": 500,
# "ElectionTick": 10,
# "HeartbeatTick": 1
# },
# "Dispatcher": {
# "HeartbeatPeriod": 5000000000
# },
# "CAConfig": {
# "NodeCertExpiry": 7776000000000000
# },
# "TaskDefaults": {},
# "EncryptionConfig": {
# "AutoLockManagers": false
# }
# },
# "TLSInfo": {
# "TrustRoot": "...",
# "CertIssuerSubject": "...",
# "CertIssuerPublicKey": "..."
# },
# "RootRotationInProgress": false,
# "DefaultAddrPool": [
# "10.0.0.0/8"
# ],
# "SubnetSize": 24
# }
すると勝手に ingress
という Docker Network が作られる。こいつは Get-NetAdapter
にも Get-VMSwitch
にも現れない。
PS> docker network inspect ingress
# [
# {
# "Name": "ingress",
# "Id": "vvbuty8emqhj8pjdeisgz60t1",
# "Created": "2019-03-13T21:08:00.8401934Z",
# "Scope": "swarm",
# "Driver": "overlay",
# "EnableIPv6": false,
# "IPAM": {
# "Driver": "default",
# "Options": null,
# "Config": [
# {
# "Subnet": "10.255.0.0/16",
# "Gateway": "10.255.0.1"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": true,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": null,
# "Options": {
# "com.docker.network.driver.overlay.vxlanid_list": "4096"
# },
# "Labels": null
# }
# ]
今は無視して、Overlay Driver の network を独自に作る。
ingress 同様、Get-NetAdapter
にも Get-VMSwitch
にも現れないので実体は不明。
PS> docker network create --driver=overlay other_overlay
wi88on1q4q0l0nsca5uie488l
PS> docker network inspect wi88on1q4q0l0nsca5uie488l
# [
# {
# "Name": "other_overlay",
# "Id": "wi88on1q4q0l0nsca5uie488l",
# "Created": "2019-03-13T21:23:35.0489805Z",
# "Scope": "swarm",
# "Driver": "overlay",
# "EnableIPv6": false,
# "IPAM": {
# "Driver": "default",
# "Options": null,
# "Config": [
# {
# "Subnet": "10.0.0.0/24",
# "Gateway": "10.0.0.1"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": null,
# "Options": {
# "com.docker.network.driver.overlay.vxlanid_list": "4097"
# },
# "Labels": null
# }
# ]
IIS を Service として追加する。
PS> docker service create --name=web --endpoint-mode dnsrr --network=ingress mcr.microsoft.com/windows/servercore/iis:latest
# Error response from daemon: rpc error: code = InvalidArgument desc = Service cannot be explicitly attached to the ingress network "ingress"
なんか、ingress
は自由に使えないっぽいので、自作の方を使う。
PS> docker service create --name=web --isolation=process --endpoint-mode=dnsrr --network=other_overlay mcr.microsoft.com/windows/servercore/iis:latest
# jzp3mme7gtsar9hxj3k28t6hc
# overall progress: 0 out of 1 tasks
# 1/1: hnsCall failed in Win32: The parameter is incorrect. (0x57)
#
# kill by Ctrl+C
PS> docker service ls
# ID NAME MODE REPLICAS IMAGE PORTS
# jzp3mme7gtsa web replicated 0/1 mcr.microsoft.com/windows/servercore/iis:latest
PS> docker service ps web
# docker service ps web
# ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
# jisa93cfywpm web.1 mcr.microsoft.com/windows/servercore/iis:latest Ready Pending less than a second ago
# oiz4kx611k0p \_ web.1 mcr.microsoft.com/windows/servercore/iis:latest HOST_MACHINE Shutdown Rejected 2 seconds ago "hnsCall failed in Win32: The …"
# ms9ikb3tur0t \_ web.1 mcr.microsoft.com/windows/servercore/iis:latest HOST_MACHINE Shutdown Rejected 7 seconds ago "hnsCall failed in Win32: The …"
# 2wg539hb1xhc \_ web.1 mcr.microsoft.com/windows/servercore/iis:latest HOST_MACHINE Shutdown Rejected 13 seconds ago "hnsCall failed in Win32: The …"
# xrn0ntwpup47 \_ web.1 mcr.microsoft.com/windows/servercore/iis:latest HOST_MACHINE Shutdown Rejected 18 seconds ago "hnsCall failed in Win32: The …"
# thdieu0w7074 \_ web.1 mcr.microsoft.com/windows/servercore/iis:latest HOST_MACHINE Shutdown Rejected 20 seconds ago "hnsCall failed in Win32: The …"
PS> docker service rm web
今度は謎のエラーが出て永遠に終わらず、強制終了しても Replicas が 1 にならない。Option を変えても、自作の overlay
を使う限り、同じエラーが出続ける。公開ポートモード でやっても同様。
とにかく、overlay network を利用しようとするとエラーが出るらしい。
[00:40:46.380][WindowsDaemon ][Info ] debug: releasing IPv4 pools from network ingress (soghasemncn07kmzqtbsgew0z)
[00:40:46.381][WindowsDaemon ][Info ] debug: ReleaseAddress(LocalDefault/10.255.0.0/16, 10.255.0.1)
[00:40:46.383][WindowsDaemon ][Info ] debug: Released address PoolID:LocalDefault/10.255.0.0/16, Address:10.255.0.1 Sequence:App: ipam/default/data, ID: LocalDefault/10.255.0.0/16, DBIndex: 0x0, Bits: 65536, Unselected: 65533, Sequence: (0xc0000000, 1)->(0x0, 2046)->(0x1, 1)->end Curr:0
[00:40:46.384][WindowsDaemon ][Info ] debug: ReleasePool(LocalDefault/10.255.0.0/16)
[00:40:46.385][WindowsDaemon ][Error ] fatal task error [task.id=gy02uficooig3h0zuq7zxajmx error=hnsCall failed in Win32: The parameter is incorrect. (0x57) service.id=3b25cd9gd87khs77wjbhj8gkm module=node/agent/taskmanager node.id=k1vkebradiz1er039xb5u9vbk]
調べてみると、同様の現象が報告されている。未解決。
- Fail to start tasks/services in Docker Swarm: hnsCall failed in Win32: The parameter is incorrect - stackoverflow
- How to troubleshoot docker on windows : hnsCall failed in Win32: The parameter is incorrect. (0x57) - DOCKER COMMUNITY FORUMS
エラーの内容的に、libnetwork
, hnsproxy.dll
辺りの API Call に齟齬があるとするなら、今は諦めるしかないか。
● ics
参考リンク に情報は無いが、Default で作成される謎の Driver。
PS> docker network ls
# NETWORK ID NAME DRIVER SCOPE
# 521a6bc421a4 Default Switch ics local
# bc9eed69c95b nat nat local
# 919a6b63caa3 none null local
そもそも ICS ( Internet Connection Sharing ) とは Windows が元々持っている機能。
ICS 設定された NIC が NAT のような役割をすることで、LAN 内の他の PC が ICS 設定した PC を介して Internet へアクセスすることが出来るというもの。
Default Switch
の実体は何か探すと、仮想 Switch に同名のものがある。
Default Switch
は Hyper-V 導入時作成されて、『規定のスイッチ』として設定されている。
また、Default Switch
には、vEthernet ( Default Switch )
という仮想 NIC が刺さっている。
Default Switch
network の Gateway がこの仮想 NIC を指すので、おそらく間違いない。
PS> Get-NetIPConfiguration | ? {$_.InterfaceAlias -eq "vEthernet (Default Switch)"} | fl -Property IPv4Address
# IPv4Address : {172.17.120.129}
PS> docker network inspect "Default Switch" | wsl jq '.[0].IPAM.Config'
# [
# {
# "Subnet": "172.17.120.128/28",
# "Gateway": "172.17.120.129"
# }
# ]
では、どのように設定されているのか、 ICS 設定を見てみると … ICS になってない !
まさかと思い、Container を立ち上げて、NAT 的動きをしていないか調べてみると、
PS> docker run -it --isolation process --net "Default Switch" mcr.microsoft.com/windows/servercore:1809 powershell.exe
(CONTAINER)> ping 8.8.8.8
PS> Get-NetNatSession
# NatName : ICSb28ea085-cad9-4498-9e07-30fe2d83e5bc
# InternalRoutingDomainId : {b1062982-2b18-4b4f-b3d5-a78ddb9cdd49}
# CreationTime : 2019/03/10 5:42:13 午前
# Protocol : 1
# InternalSourceAddress : 172.18.1.12
# InternalSourcePort : 1
# InternalDestinationAddress : 8.8.8.8
# InternalDestinationPort : 1
# ExternalSourceAddress : 192.168.100.5
# ExternalSourcePort : 1000
# ExternalDestinationAddress : 8.8.8.8
# ExternalDestinationPort : 1000
ん ? やっぱり nat なの ?
おそらく、Hyper-V は以前は仮想 NAT が作れなかったので、NAT Like な Alternative として ICS を利用していたが、現在は仮想 NAT が作れるので Default Switch が NAT として作られるようになったのではないだろうか ( 適当 )。
※ 追記
そもそも最初から Default Switch は NAT だったらしいです。
Docker Network がそれをなぜ ics
と認識しているのかは依然として謎。
参考: 謎: Windows 10 ver 1809 の Default Switch の NAT サブネットが起動ごとに変わる件 - 山市良のえぬなんとかわーるど
その他
Environment
System 環境変数は HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
にあり、Registry の分離がなされていれば自然と分離されるようだ。
User 環境変数は HKU\<<SID>>\Environment
にあり、これも同様と考えられる。
感想
Windows について、無駄に詳しくなった。
しかし、Windows は Linux とは違い、最終的には闇の中 になってしまうのは、しょうがないのかなぁ。
次回は、Hyper-V Isolation と LCOW をまとめる。
おまけ
Windows : Volume GUID から、乗っている Disc を探す方法
diskpart
はなぜか Volume の GUID を出してくれないので、代わりに diskext
で確認する。
PS> diskext
# ...
# Volume: \\?\Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b}\
# Mounted at: <unmounted>
# Extent [1]:
# Disk: 2
# Offset: 135266304
# Length: 21339553280
# ...
これで、Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b}
に対応する Disk が 2 であることが分かる。
あとは、diskpart
で Disk 2 について調べると良い。
参考
https://www.slideshare.net/Docker/windows-container-security
https://www.slideshare.net/Docker/windows-server-and-docker-the-internals-behind-bringing-docker-and-containers-to-windows-by-taylor-brown-and-john-starks
https://blogs.msdn.microsoft.com/microsoft_press/2017/08/30/free-ebook-introduction-to-windows-containers/
https://www.slideshare.net/Docker/windows-container-security
https://www.slideshare.net/kazukitakai/windows-server-2019-container
https://www.slideshare.net/firewood/ss-40143470
https://yamanxworld.blogspot.com/
https://www.itmedia.co.jp/author/208420/