LoginSignup
65
53

More than 5 years have passed since last update.

Docker Desktop の復習と、Windows Container に入門: Windows Server Container 理論編

Last updated at Posted at 2019-03-18

前回は、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 IsolationHyper-V Isolation という 2 つの分離レベルがある。

分離レベル : Process Isolation

Kernel を Host と共有し、Container は 1 つの Process として動作する方式。
Windows Server Container, Windows Process Container とも呼ばれる。

同じ方式を取っている Container Runtime としては runCrkt 等がある。

  • 利点
    • 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 の共有ができない
      • まぁ、面倒と容量の問題

--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 である hcsshimC# 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

その他、以下動画や資料などで情報を補完した。

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 の細やかな管理を可能としている。
screenshot_32.png

与えられた Capability は、専用の特殊な Group ( Flags: Capability, Name: APPLICATION_PACKAGE_AUTHORITY\XXXXXXX ( SID: S-1-15-3-XXXXX ) ) への所属をもって管理される。
image.png

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.exewininit.exe Tree が新たに起動されるのが確認できる。
image.png
image.png
Windows Internals によると、Silo Namespace 作成後、

  1. Job Object に関連付けられた Smss を作成
  2. Smss が Session 0 の初期化処理として、Wininit.exe, Csrss.exe を起動
  3. Wininit.exeservices.exe, Lsass.exe を起動し、自動起動サービスが立ち上がっていく
  4. 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"

screenshot_84.png
screenshot_85.png

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 )
  • \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 で見られる。
screenshot_16.png
Silo の情報はこんな感じ。
screenshot_19.png
ちなみに、Host のは !Silo -g Host で見られる。
screenshot_18.png
Globals はこんな感じ。
screenshot_21.png

Container Template File

Windows Container を作る時、wsc.def ( Windows Server Container ? ) という定義を元に作成される。
%SystemRoot%\System32\Containers\wsc.def にある。
あくまで Template なので、Container の立ち上げ方次第では設定値は変わるはず。

Object Section

wsc.def
<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

wsc.def
<container>
    <namespace>
        ....
        <job>
            <systemroot path="C:\Windows" />
        </job>
        ....

Job に SystemRoot を設定する必要性が不明。

Mountmgr Section

wsc.def
<container>
    <namespace>
        ....
        <mountmgr>
        </mountmgr>
        ....

Default では特に何もしない。

NamedPipe Section

wsc.def
<container>
    <namespace>
        ....
        <namedpipe>
        </namedpipe>
        ....

これも Default では特に何もしない。

Registry Section

wsc.def
<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 がかかっているのが確認できる。
screenshot_53.png

Process

● Container 内

Container 内から見えるのは、Job Object に関連付けられた smss が立ち上げた子プロセスに限られる。
screenshot_14.png

(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 を共有しているので当然か。
screenshot_15.png
不思議なのが、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 が関連付けられているのが分かる。
screenshot_06.png
Job を調べると、Silo Flag が付けられている。
screenshot_07.png
その Silo は Server Silo Type で、RootDirectry が \Silos\1192 と分かる。
screenshot_10.png

◆ 結論

答えはわからないが、 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 を持っていなかった。
screenshot_29.png

W32pServiceTableFilterW32pArgumentTableFilter 辺りなのかなぁとは思いつつ、これ以上 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 が付与されていると分かる。
image.png
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 昇格後と同等の相当高いレベル。
screenshot_35.png
Group にも、Capability に関するものが何もない。
screenshot_36.png
もしかして、全く違う機構で動いているんだろうか。

● 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 を指している。
screenshot_59.png
screenshot_65.png
HarddiskVolume68 は、Disk 2 の Partition 2 上に mount されている。( Volume と Disk の関連を確実に見つけるなら、この方法で で )
image.png
これを 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 機能を実現していると考えられる。
docker-Page-13.png

◆ 結論

これら情報を照らし合わせると、

  1. Container 起動
  2. C:\ProgramData\Docker\windowsfilter 以下に Container ID と同名のフォルダ作成
  3. その中に Virtual Hard Disk ( 差分 Disk ( 詳細後述 ) ) を作成、Volume として利用可能な状態とする
  4. Silo Namespace 内の \Global??\C:\Global??\Volume{...} の参照先を 3 の Volume に
  5. Container の Boot 時には、\SystemRoot の参照先である \Global??\C:\windows から Windows を立ち上げる

と予測できる。

:alembic: 実験 :microscope:

試しに、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 はドライブ文字またはマウント ポイントを正常に割り当てました。

screenshot_72.png
普通に中が見れた。ちなみに NTFS として認識される。

UnionFS

Virtual Hard Disk レベルで切り替えがされていることは分かった。
しかし、Docker の特徴でもある、差分毎の Image Layer や Copy on Write 等については、どの様に実現するのだろうか。
docker-Page-12 (1).png

通常、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

docker-Page-15.png

:alembic: 実験 :microscope:

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 を格納するフォルダが追加されている。

OtherTerminal
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

docker-Page-15.png

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

:alembic: 実験 :microscope:

まずは、この 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 に書き込まれているのが確認できた。
screenshot_92.png
どうやら 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": ""
#   }
# ]

docker-Page-16.png

● 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 であることが分かる。
screenshot_86.png

また、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 の様に どの層をどの順番で重ねるか を指定する必要があるはずだが、

# overlayfs は、こんな感じに mount 時に指定する
$ mount -t overlay overlay -o lowerdir=/volumes/layer_0:/volumes/layer_1,upperdir=/volumes/layer_3,workdir=/volumes/layer_merged

それがどこなのかは分からなかった。これ以上は Kernel Debug でもしないと分からなそう。

◆ 結論

という事で、これら情報を照らし合わせると、

  1. Docker Image を Pull
  2. C:\ProgramData\Docker\windowsfilter 以下に Layer 毎にフォルダを作成
  3. C:\ProgramData\Docker\windowsfilter\\<<GUID>>\\Files に Layer のファイル展開
  4. docker volume create <<dir_name>> した場合は、C:\ProgramData\Docker\volumes 以下に <dir_name> フォルダを作成
  5. Container 起動
  6. Change Root の手順
  7. 何らかの方法 で、Layer 情報を wcifs に伝える or wcifs が参照するデータ ? ファイル ? Object ? に設定する
  8. Container 内で File Write が発生すると、sandbox.vhdk 上に変更が書き込まれ reparse points 先をそっちに変更
  9. Container 内で File Read が発生すると、reparse points 先を読み込む

と予測できる。

Registry

Registry も Windows においては重要なデータである。
App-V 1703 Virtual Registry and Containers?

● Registry とは

そもそも Registry は、Hive というファイルを HKLM, HKCUHKLM\SECURITY 等に mount して 1 つの大きな Tree Structure を構築したもの。
以下表を見ると分かる通り、mount, SymLink, 疑似 FS と NTFS よりも Linux のそれに近い。

実体 参照
HKEY_CLASSES_ROOT\ HKLM\SOFTWARE\ClassesHKCU\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_MACHINEHKEY_USERS を管理すれば良いという事になる。

● 実践

という事で、まずは Process Explorer で確認してみたが、普通にアクセスしているようにしか見えない。
screenshot_75.png
どういう事 ?

● Hive ファイル

よく分からないので、新たに Container を立ち上げてその過程を Process Monitor で確認すると、\Registry\WC\Silo ~ というレコードが沢山出てきた ( 通常は、HKCU\…HKLM\… になる )。
screenshot_78.png
このレコードの最初の出処まで遡って見てみると、\Registry\WC\Silo9dd9eeab-...-b0acd579bc17system 等が RegLoadKey されていた。
image.png

その備考欄には 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 をいくつも見つけた。
screenshot_79.png

\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 も存在している。
screenshot_97.png

:alembic: 実験 :microscope:

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 と分かった。

screenshot_94.png
+
screenshot_95.png
=
screenshot_99.png

・System Hive in Container
場所 : Q:\Windows\System32\config\SOFTWARE
参照元 : C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Files\Windows\System32\config\SOFTWARE

Windows は起動時に smssC:\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 以降、以下機能が追加されている。

Registry Virtualization

  • UAC 昇格前のアクセスを deny するのではなく、仮想の Registry Tree に Copy on Write する機能
  • ただし、仮想化が許されるのは HKEY_LOCAL_MACHINE\Software 以下のみ ?

完全に同じものではないようだが、似た原理は利用しているのかもしれない。

◆ 結論

これら情報を照らし合わせると、

  1. Container 起動
  2. Container 内にある C:\WcSandboxState\Hives\*\REGISTRY\WC\Silo + <Random GUID> に mount
  3. Container 内にある C:\Windows\System32\config\*\REGISTRY\WC\Silo + <Other Random GUID> に mount
  4. Container 内で Registry Write が発生すると、\REGISTRY\WC\Silo + <Random GUID> 以下に書き込まれる
  5. Container 内で Registry Read が発生すると、\REGISTRY\WC\Silo + <Random GUID>\REGISTRY\WC\Silo + <Other Random GUID> を merge した Virtual Registry から読み込む

と予測できる。

Network

次は、Network がどうなるか。
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 ができる。

192.168.100.104
$ 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 (イーサネット) と同じものに書き換え
  • 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』が有効になっているのが分かる。
screenshot_42.png

MAC Address Rewrite

どのように MAC Address が書き換わるのかを確認する。
Container を参加させた Subnet 内の適当な PC へ Packet を送信し、その時の Arp Table の状態を確認してみる。

Host
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
Container
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
192.168.100.150
$ 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 を利用しようとするとエラーが出るらしい。

log.txt
[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]

調べてみると、同様の現象が報告されている。未解決。

エラーの内容的に、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/

65
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
53