/

1問目

問題文

LinuxカーネルでXenが動作するアーキテクチャとして最も近いものを答えてください。

  • KVM
  • Docker
  • VirtualBox
  • Hyper-V

解説

  • KVM
    • ネイティブ型ハイパーバイザ
    • モノリシックカーネルアーキテクチャ
  • Docker
    • アプリケーションコンテナ
  • VirtualBox
    • ホスト型ハイパーバイザ
  • Hyper-V
    • ネイティブ型ハイパーバイザ
    • マイクロカーネルアーキテクチャ
  • OpenVZ
    • OSコンテナ

Docker, OpenVZと言ったソフトはコンテナ仮想化であり、カーネルを全ての仮想マシンで共有してユーザ空間を隔離する技術です。

VirtualBoxはQemuと同じように、OSの上でソフトウェアを起動し、OSの機能を利用して仮想化を行う技術です。

KVM・Xen・Hyper-Vと言ったソフトは、仮想化とその管理の機能を含めたOS制御全ての機能をカーネルに含めたアーキテクチャです。

このうち、KVMでは管理カーネル上でその他のプログラムが動作しますが、
Xenではdom0と呼ばれる管理用の仮想マシンが作成され、その上でカーネルに対して仮想化サーバーの起動などを命令します。
Hyper-VはXenと同様に、管理を行うための仮想マシンが作成され、その上で管理用のプログラムやGUIが動作しています。

Xenでは、管理用の仮想マシンをDomain-0(dom0)、その他の仮想マシンをDomain-U(domU)と呼び、
Hyper-Vではそれぞれ親パーティション (Root Partition, Parent Partition)、子パーティション (Child Partition)と呼びます。

講評

調べればわかる問題ということもあり、問題の正答率は高かったです。
誤答を見ると、同じLinux OS上で動作するということからか「KVM」を選択するチームも見られました。
仮想化を扱う際にはその仮想化の動作原理を知るとトラブルシューティングの際に便利なことがあります。

2問目

問題文

Xenのトラブルシューティングを行う際に見るべきファイル・実行するべきコマンドを箇条書きで答えよ。
なおこの問題はファイル・コマンドの出力結果から見ることの出来る情報の種類や重要度により点数が増減する。

解説

Xenでは、ログファイルを /var/log/xen に保存しています。
また、Xenの管理を行うプログラムに xm (以前のバージョンは xl )があります。
Xenのシステムのトラブルシューティングはこれらのファイル・コマンドで充分に行えます。
またその他にも /var/lib/xen/, /var/xen/dump/ に内部で生成されるダンプやデータベースが、
/etc/xenに設定ファイルが、
/proc/xenでXenのカーネル情報が取得できます。

参考: 第32章 Xen para-virtualized ドライバーのトラブルシューティング – Red Hat Customer Portal

その他、この問題では言及していないものとして、現在稼働中の物を確認するためのコマンドとしてのxentop
libvirtを使用している場合はそれに関連するディレクトリ、
ネットワークに関連するトラブルとしては ネットワークのパケットキャプチャなど、仮想マシンのトラブルシューティングでは見るべき部分が多数あります。

今回の問題はXen本体のトラブルを解決するために必要なファイル・コマンド群双方に言及している場合に最高点数を付与しています。
ファイルのみ・コマンドのみの場合は取得できる情報から30%〜70%で付与しています。
その他のトラブルについて解決するためのコマンド類を記述しているチームには、問題の最高点を上限として加点をしています。

講評

問題文と自由記述の欄から、回答に苦労したチームが多かったのではと思います。
問題文に「見るべきファイル・実行するべきコマンド」と明記しており、ファイルとコマンド双方を回答してくれることを想定していたのですが、コマンドのみ・ログファイルのディレクトリのみの回答も複数ありました。
中には20以上のファイル・コマンドを回答として提出したチームもありました。
情報の中からトラブルの原因を見つけ出し解決に漕ぎ着ける為に、トラブルごとに見るべき項目は異なります。
その際に「どの情報が必要か」を考え、その時々で適切なコマンドの実行・ファイルの閲覧を行うことが大切です。
この時に「どのコマンドで何が取得できる」という事を知っておく事はプラスになります。

3問目

問題文

Xenの仮想化方式にPVとHVMがありますが、それぞれの違いについて説明してください。

解説

まずPVですが、これはCPUの機能である仮想化支援がなくてもVMを動作させることができる方式です。ただし、VMを動作させるにはPVに対応するドライバが必要なので、そのドライバを別途インストールする必要があります。また、Xenに対応したゲストOSを用いることで、ハードウェアを触らずに高速動作しますが、メモリマッピングといった直接ハードウェアを触る部分で遅延が発生する欠点があります。
HVMはPVと違い、使用するCPUに仮想化支援の機能が必要です(intelならVT-x, AMDならAMD-V)。HVMはqemuを使用してBIOSや様々なコントローラをエミュレートするため、PVのように専用ドライバをインストールする必要がありません。仮想化支援の機能を使うのでハードウェアを触る処理が高速に行える特徴を持ちます。

講評

XenはAmazonが運営しているAWSに採用されています。そのためか、解答にAWS特有の名称などを用いて説明しているチームが複数ありました。
全体的に見てしっかりと説明しているチームが多かったなと思います。

4問目

問題文

PVとHVMを組み合わせたPVHがりますが、この仮想化方式についてPVとHVMを使って説明してください。

解説

3問目の解説にもある通り、PVはXenに対応したゲストOSを用いることで、ハードウェアを触らずに高速動作しますが、メモリマッピングといった直接ハードウェアを触る部分で遅延が発生します。PVの欠点であるハードウェアを触る処理を、HVMで使われている仮想化支援の機能を使って高速にできるのがPVHとなります。

講評

3問目より4問目の方が回答率が高かったです。

 /

問題文

Kubernetes クラスタ環境を移行後、Kubernetes上のWordPressにアクセスできなくなりました。
原因をつきとめ、修正してください。

情報

Kubernetesクラスタの移行手順書は存在しません。アドレスレンジに以下の変更があったことのみ分かっています。

Address Range: 172.16.0.0/24 -> 192.168.0.0/24
Cluster Address Range: 192.168.0.0/24 -> 10.254.0.0/24

  • Address Range: Kubernetes Node に振られるアドレスのレンジ
  • Cluster Address Range: Kubernetes Service に振られるアドレスのレンジ

ゴール

VNC 踏み台サーバ上のブラウザにて「192.168.0.1:30080」を入力することで、WordPressの画面が表示される

トラブルの概要

Kubernetes Pod の名前解決に失敗するため、wordpress Pod のセットアップに失敗する。その結果、外部からWordPressへのhttpアクセスに失敗する。

解説

当問題の環境では、kube-apiserver が掴んでいる証明書の Subject Alternative Names (SANs) が誤っていることが原因で、Pod からsvc/kubernetesへhttpsでの通信が失敗します。そのため svc/kubernetes との通信が必要である kube-dns Pod を正常に動作させることが出来ず、wordpress Pod から mysql.default.svc.cluster.local:3306 への通信における名前解決に失敗するため、wordpress Pod のセットアップに失敗するという問題が生じます。

当問題は、kube-apiserver が正常な SANs を持つ証明書を掴むよう、証明書を再生成することで解決します。

なおKubernetesは、kube-apiserverの --tls-cert-file オプションにサーバ証明書、 --tls-private-key-file オプションに秘密鍵、kube-controller-managerの --root-ca-file オプションにCA証明書を指定することで Podからsvc/kubernetesへのhttps通信が可能となります。

回答例

まず、現在のPodの稼働状況を確認します。

# kubectl get pod
NAME READY STATUS RESTARTS AGE
mysql-755f57f594-mc9cr 1/1 Running 1 2h
wordpress-dc9bb949d-9glcx 1/1 Running 4 2h
wordpress-dc9bb949d-sntj9 1/1 Running 4 2h

アクセス先のPodである pod/wordpress のログを確認します。

# kubectl logs wordpress-dc9bb949d-9glcx
WordPress not found in /var/www/html - copying now...
Complete! WordPress has been successfully copied to /var/www/html

Warning: mysqli::__construct(): php_network_getaddresses: getaddrinfo failed: Temporary failure in name resolution in Standard input code on line 22

Warning: mysqli::__construct(): (HY000/2002): php_network_getaddresses: getaddrinfo failed: Temporary failure in name resolution in Standard input code on line 22

MySQL Connection Error: (2002) php_network_getaddresses: getaddrinfo failed: Temporary failure in name resolution
... (省略)

以上のログより名前解決に失敗していることがわかります。/root/manifests/wordpress/deployment-wordpress.yaml の中身を見ると、WORDPRESS_DB_HOST 環境変数に mysql.default.svc.cluster.local というドメイン名が渡されているため、当箇所における名前解決に失敗しているものであると考えられます。

Kubernetesクラスタ内のServiceリソースの名前解決は kube-system ネームスペース内の kube-dns Pod が行います。

kube-dns Pod の稼働状況を確認します。

# kubectl get pod -n kube-system
NAME READY STATUS RESTARTS AGE
kube-dns-7c7877989-8frxc 2/3 Running 1 1m

kube-dns Pod のログを確認します。

# kubectl -n kube-system logs kube-dns-7c7877989-8frxc kubedns
...(省略)
E0729 13:29:49.132142 1 reflector.go:201] k8s.io/dns/pkg/dns/dns.go:192: Failed to list *v1.Service: Get https://10.254.0.1:443/api/v1/services?resourceVersion=0: x509: certificate is valid for 172.16.0.1, 192.168.0.1, not 10.254.0.1
E0729 13:29:49.139524 1 reflector.go:201] k8s.io/dns/pkg/dns/dns.go:189: Failed to list *v1.Endpoints: Get https://10.254.0.1:443/api/v1/endpoints?resourceVersion=0: x509: certificate is valid for 172.16.0.1, 192.168.0.1, not 10.254.0.1
...(省略)

以上のエラーログより、kube-apiserverの持つ証明書のSANsに10.254.0.1が存在しないことがわかります。

サーバ証明書に対応する秘密鍵である /etc/pki/tls/kube01/server.key を用いてCSRファイルを再生成します。以下はopensslを用いた例です。

# cat << 'EOF' > /etc/pki/tls/kube01/csr.conf
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C = JP
ST = dummy
L = dummy
O = dummy
OU = dummy
CN = dummy

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = kubernetes
DNS.2 = kubernetes.default
DNS.3 = kubernetes.default.svc
DNS.4 = kubernetes.default.svc.cluster
DNS.5 = kubernetes.default.svc.cluster.local
DNS.6 = kube01
IP.1 = 192.168.0.1
IP.2 = 10.254.0.1

[ v3_ext ]
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment
extendedKeyUsage=serverAuth,clientAuth
subjectAltName=@alt_names
EOF
# openssl req -new -key server.key -out server.csr -config csr.conf

CSRファイルに対し、CA証明書である/etc/pki/tls/kube01/ca.crt にて署名を行うことでCRTファイルを生成します。

# openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 10000 \
-extensions v3_ext -extfile csr.conf

kube-apiserver.service を再起動します。

# systemctl restart kube-apiserver.service

Pod を明示的に再起動させるため、Pod を再作成します。

# kubectl delete -f /root/manifests/kube-dns/kube-dns.yml &amp;amp;amp;&amp;amp;amp; kubectl create -f /root/manifests/kube-dns/kube-dns.yml
# kubectl delete -f /root/manifests/wordpress/wordpress.yaml &amp;amp;amp;&amp;amp;amp; kubectl create -f /root/manifests/wordpress/wordpress.yaml

講評

Kubernetesは利用者が簡単にコンテナのオーケストレーションを行える反面、内部の仕組みは複雑なものになっています。そのためKubernetesについて学ぶ際は、GCP などのクラウドサービスにてKuberntesを利用する他、Kubernetesを実際に構築してみることに是非チャレンジすると良いと思います!

問題文

[トラブル1] ZabbixからSNMPで監視ができない。。。

自宅の検証環境でZabbixからVyosをSNMPで監視したいのだがSNMPのホストをZabbixに登録しても監視ができない。原因を突き止めトラブル対応をし、ZabbixからVyosを監視できるようにしてください。
踏み台サーバにログインをして以下のアクセス情報を使ってトラブルシュートすることができる。

情報

接続情報

  • Zabbix
    IPアドレス: 192.168.0.5
    SSH-ID: admin
    SSH-パスワード: fG9bycDJ
    ZabbixコントロールパネルURL: https://192.168.0.5:8080 (VNCサーバからFirefoxを使ってアクセスすることができます)
    ZabbixログインID: admin
    Zabbixパスワード: zabbix

  • Vyos
    IPアドレス:192.168.0.10
    SSH-ID: admin
    SSH-パスワード: fG9bycDJ
    SNMPのcommunity: ictsc2018-pre1

ゴール

Zabbixのコントロールのパネルホスト一覧からVyosのSNMPの監視のところが赤色から緑色なればゴールです

[トラブル2] Grafanaが起動できない。。。

自宅の検証環境をZabbixではグラフの可視化がいけてないのでGrafanaというOSSをdocker-composeで構築・可視化をしていたのだがGrafanaのアップデートが来ていたのでアップデートしたら起動できなくなってしまった。原因をつきとめてGrafanaのコントロールパネルを見れるようにしてください。

情報

- docker-compose.ymlはadminユーザのホームディレクトリにあるdocker-composeフォルダにあります
- バージョンアップ前はdocker-compose.yml.old、バージョンアップ後はdocker-compose.ymlです
- docker-compose up -dするとエラーが吐かれて起動できません。
- バージョンアップ前のdocker-compose.ymlを試しに使うと起動できます
- ~/docker-compose/.volume/ にGrafanaコンテナで使うデータが入っています。このデータは必ずアップデート後も引き継いでください

接続情報

  • Grafanaサーバ
    IPアドレス:192.168.0.6
    SSH-ID: admin
    SSH-PW: fG9bycDJ
    GrafanaのコントロールパネルURL: 192.168.0.6 (VNCサーバからFirefoxを使ってアクセスすることができます)
    GrafanaID: admin
    GrafanaPW: grafana

ゴール

Grafanaが無事起動ができ踏み台のVNCサーバのWebブラウザからGrafanaのコントロールパネルのログイン画面が見れればゴールです。

禁止事項

- ~/docker-compose/.volumeを削除する・アップデート後のコンテナで使わないというのは禁止です。
  必ずGrafanaコンテナにマウントしてデータを引き継いでください。

トラブルの概要

[トラブル1]

これはVyos側の snmp communitysnmp portが間違っている単純な設定ミスのトラブルでした。

[トラブル2]

このトラブルはこのGrafanaというOSSの特有のトラブルとなっております。トラブル自体はとても簡単でGrafanaの公式ドキュメントの Installing using Docker を読まれた方はすぐに解けたのではないでしょうか。
公式ドキュメント

解説

[トラブル1]

よくみるとVyos側のcommunity側の文字が ictc2018-pre1 でとなっており問題文で指定されたcommunity名と違います。ちなみにZabbix側の設定は ictsc2018-pre1 となっており正しいものとなっています。
またVyosのSNMPのportの設定が 162 と指定されおり、これも修正する必要があります。

[トラブル2]

このトラブルはGrafanaのバージョン5.1以降からはUIDが変更されております。ですので5.1以前に作成されたファイルはそれ以降のバージョンでは正しいアクセス権を持たないので使えません。
これを解決するには新しいコンテナ作成時にUIDを変えてしまうという手段があります。
user-id-changes

解答例

[トラブル1]

admin@vyos# delete  service snmp community ictc2018-pre1
admin@vyos# set service snmp community ictsc2018-pre1 authorization ro
admin@vyos# delete service snmp listen-address
admin@vyos# set service snmp listen-address 192.168.0.10 port 161
admin@vyos# commit
admin@vyos# save

[トラブル2]

docker-compose.ymlのGrafanaのサービスのところに以下を追記する

    user: &quot;104&quot;

講評

全49チーム中
トラブル1だけ解けたチーム: 5チーム
トラブル2だけ解けたチーム: 0チーム
トラブル1,2両方解けたチーム: 9チーム

トラブル1はVyosを使ったことある人なら簡単に解けたかと思います。
SNMP?Vyos?Zabbix?なにそれ思った方は是非一度自分で調べて自分で構築することをおすすめします。
どんどん自分だけの検証環境を作って試してみるといいかもしれません。
トラブル自体はとてもとても簡単なので解けなかったチームはしっかり復習しましょう。

トラブル2を解けたチームはログを見る・ドキュメントを調べる・ぐぐるという基本的なことがしっかりできていた印象が解答から感じられました。
解けたチームはしっかりとエビデンスやドキュメントのURLを貼って報告してくれてました。
またあるチームはGithubのissueまで調べたというのもありました。

 /

1問目

問題文

以下のDockerコンテナに関する説明のうち、正しいものを選択してください。

a. Dockerコンテナ内で動かすことができるプロセス数の上限は1である
b. Dockerコンテナ内部のUIDを0以外に指定できる
c. Dockerコンテナではデフォルトでinit(systemdなど)が実行される
d. “scratch”と呼ばれるイメージを使うと、ホストのOSと同じOSが入ったDockerコンテナを起動できる

解説

この問題はDockerコンテナに関する知識問題でした。

a. Dockerコンテナ内で動かすことができるプロセス数の上限は1である

誤りです。Dockerコンテナ内ではulimitなどに違反しない限りいくつでもプロセスを立ち上げられます。(=フォークできる)

b. Dockerコンテナ内部のUIDを0以外に指定できる

正解です。Dockerコンテナの中では、root以外にも新しいユーザー追加して利用することができます。

c. Dockerコンテナではデフォルトでinit(systemdなど)が実行される

誤りです。Dockerコンテナの場合、通常initは起動せず単体のプロセスが実行されます。LXCなどでは、デフォルトでinitが実行されます。

d. “scratch”と呼ばれるイメージを使うと、ホストのOSと同じOSが入ったDockerコンテナを起動できる

誤りです。”scratch”は何も入っていない真っ白なイメージです。golangのバイナリを置いたり、マルチステージビルドと組み合わせて活用すると、イメージの容量を削減できます。

回答

b. Dockerコンテナ内部のUIDを0以外に指定できる

採点基準

正解の選択肢をすべて選んだ場合にのみ満点の70点を与えました。

講評

49チーム中、回答率は95%(47チーム)、正答率は65%(32チーム)でした。

Dockerを触る上では欠かせない基礎知識なので、ぜひ身につけてもらいたいなと思いました。

また、Docker以外のコンテナ技術では正しい選択肢を織り交ぜたので、自分で比較しながら他のコンテナも触ってもらえると嬉しいです。

2問目

問題文

Docker Swarm に関する以下の記述のうち、正しいものを選択してください。(複数選択可)

a. Docker Swarm ではサービスのイメージタグに latest を指定すると、常に最新のイメージを用いてコンテナがデプロイされる
b. Docker Swarm では標準で IPIP トンネリングを用いてノードを跨いだコンテナ間の通信を提供する
c. Docker Swarm ではマネージャー(クラスタを管理するノード)と非マネージャー(コンテナを実行するノード)の役割を同一のホストに割り当てることができる
d. Docker Swarm では非マネージャーの数より多い数のレプリカを指定したサービスを作成できる

概要

この問題は コンテナに関するツールである Docker において、複数台のノードでのクラスタリング機能を提供する Docker の Swarm mode に関する知識問題です。

クラスタリング/オーケストレーション環境としては他にも Kubernetes や Mesos が有名ですが、ここでは Docker 本体にバンドルされている Docker Swarm を対象としました。

この機能を使ったことがある人はほとんどいないという想定であったため、ドキュメントを読んで解答ができるようにしています。

解説

a. Docker Swarm ではサービスのイメージタグに latest を指定すると、常に最新のイメージを用いてコンテナがデプロイされる

誤りです。Docker Swarm では、サービスのコンテナ数を指定された値に保つように維持しますが、このとき、ノードに latest タグが付与された古いイメージがあるときには、最新のイメージを pull せずにそれを用いて起動します。

latest タグは、コンテナレジストリ上で最新のイメージを参照するためにしばしば用いられます。

より具体的な例を上げて説明しましょう。Docker Hub の ruby イメージでは、 Ruby 2.5.1 のリリース後に ruby:latestruby:2.5.0 ではなく ruby:2.5.1 と等しくなるように変更されました。(ソース)
このとき、 Ruby 2.5.1 のリリース前に ruby:latest イメージを pull していたノードでは、ruby:latest イメージとして Ruby 2.5.0 のイメージを保持しているため、 Docker Hub の ruby:latest が更新されても新たに pull をし直すことはありません。
一方、それまで ruby:latest を持っていなかったノードでコンテナが実行される時には、新たにイメージが pull されます。そのため、同じイメージタグを指定しているにも関わらず、実行されるイメージが異なり、問題が生じることがあります。

こういった問題を回避するためには、しばしば latest タグを用いないという対策が取られます。ちなみに、 Docker Swarm でタグは更新しないけどイメージの pull は強制したい! という場合には、 docker service update --force --image [image_name]:latest というように、 --force フラグと --image を組み合わせて用いることで強制的にリポジトリのイメージをチェックさせることができます。

b. Docker Swarm では標準で IPIP トンネリングを用いてノードを跨いだコンテナ間の通信を提供する

誤りです。Docker Swarm では標準でノードを跨いだコンテナ間の通信に VXLAN を用います。
ドキュメントの記述では以下のようにあります。

UDP port 4789 for overlay network traffic

IANA のポート割当でこのポート番号を検索すると、 4789/udp は VXLAN 向けに予約されていることが分かります。

これだけの情報では実際に Docker Swarm が VXLAN を用いているのかの判断はできないですが、実際にDocker Swarm でクラスタを作り tcpdump で確認する、「Docker Swarm IPIP」「Docker Swarm VXLAN」で調べてみることにより解答できるはずです。

c. Docker Swarm ではマネージャー(クラスタを管理するノード)と非マネージャー(コンテナを実行するノード)の役割を同一のホストに割り当てることができる

正しいです。

Docker Swarm ではノードに対してマネージャー、またはワーカーの役割が割り当てられますが、コンテナの配置は役割とは関係なく ACTIVE 状態のノードに対してコンテナを配置します。

問題文を一見すると、マネージャーと非マネージャーがあるのだから、マネージャーにコンテナが配置できないと思ってしまいがちですが、これは誤りです。

あるノードにコンテナを配置しないためには、 docker node update --availability drain [node-name] コマンドにより DRAIN 状態にノードを変更する必要があります。

参考になるドキュメントは Drain a node on the swarm です。また、ドキュメント中にて例示している docker node ls の結果では、 manager のノードが ACTIVE 状態になっており、コンテナを配置できる状態になっていることが分かります。

d. Docker Swarm では非マネージャーの数より多い数のレプリカを指定したサービスを作成できる

正しいです。

Docker Swarm では、 docker service create コマンドに --replicas n オプションを付けることで指定したコンテナ数を維持させることが可能です。
このとき、3台のノードから構成されるクラスタでレプリカ数が4のサービスを作成できるか? というのが問題の本意になります。

実際これは可能であり、特にこれを制約するような文章はドキュメントに記載されていません。StackOverFlow の質問 でもこれに関する解答があります。このとき、少なくとも1台のノードには同じサービスのコンテナが2つ以上実行されることになります。

問題とは関係がありませんが、一般的にVMやコンテナにおいて、こういった配置に関する設定は Affinity/Anti-Affinity と呼ばれる設定で制御できます。OpenStack では Affinity Policy として、Kubernetes では Node/Pod Affinity としてこれらの機能が実装されています。ちなみに Docker Swarm では実装されていません。

解答

  • c, d

採点基準

正解の選択肢をすべて選んだ場合にのみ満点の70点を与えました。

講評

回答数は46件/49チーム (93.9%), 正答数は12件(26.1%)でした。

Docker Swarm はあまり使用されているケースを見ることがなく、日本語のドキュメントも少ないため混乱してしまった方も多いかと思います。英語のドキュメントは比較的豊富であり、そちらを参考にして頂ければと思いました。

Docker におけるイメージビルドに用いる docker build コマンドでも有数にハマりやすい(主観)ポイントを問題にしたものです。


3問目

問題文

以下に示す環境でイメージをビルドしたとき、期待される docker run an_image ls -l / | grep hoge コマンドの結果は選択肢のうちどれでしょう。

# ls -l
total 16
-rw-r--r--    1 root     root            66 Aug 16 10:09 Dockerfile
-rw-r--r--    1 root     root          3072 Aug 16 10:09 hoge.tar.gz
# tar tvf hoge.tar.gz
drwxr-xr-x guest/users         0 2018-03-03 23:55:03 hoge/
-rw-r--r-- guest/users         0 2016-02-27 05:03:30 hoge/a
-rw-r--r-- guest/users         0 2016-08-28 20:32:45 hoge/b
# curl https://some_host/hoge.tar.gz &gt; remote_hoge.tar.gz
# tar tvf remote_hoge.tar.gz
drwxr-xr-x root/root         0 2018-03-03 10:16:31 hoge/
-rw-r--r-- root/root         0 2017-03-07 10:00:00 hoge/a
-rw-r--r-- root/root         0 2017-08-26 15:37:42 hoge/b
# cat Dockerfile
FROM alpine:3.6
ADD hoge.tar.gz https://some_host/hoge.tar.gz /
# docker build -t an_image .

1.

drwxr-xr-x    1 root     root          4096 Aug 16 10:09 hoge

2.

drwxr-xr-x    1 guest    users         4096 Aug 16 10:09 hoge

3.

-rw-------    1 root     root          3072 Aug 16 10:09 hoge.tar.gz

4.

-rw-------    1 guest    users         3072 Aug 16 10:09 hoge.tar.gz

5.

drwxr-xr-x    1 guest    users         4096 Aug 16 10:09 hoge
-rw-------    1 root     root          3072 Aug 16 10:09 hoge.tar.gz

6.

drwxr-xr-x    1 root     root          4096 Aug 16 10:09 hoge
-rw-------    1 guest    users         3072 Aug 16 10:09 hoge.tar.gz

解説

tar コマンド等は環境を示すために用いており、実際にはアーカイブの中身は重要ではありません。肝となるのは docker build -t an_image . の挙動です。

Dockerfile において、 ADDCOPY と同様に、ビルド時にコンテナ外のファイルをコンテナに追加するために用いられます。

ADD の挙動は分かりづらいですが、しっかりドキュメントされています。長いですね。

解答のキーとなるのは以下の2箇所です。

  1. リモートのファイルはダウンロードされる

    If is a URL and does end with a trailing slash, then the filename is inferred from the URL and the file is downloaded to /.

    抄訳すると以下のようになります。

    <src> つまり hoge.tar.gz もしくは https://some_host/hoge.tar.gz が URL であり、かつ <dest> の末尾が / で終わるとき (今回は / のため当てはまる) ときには、ファイル名は URL を基にし、 <dest>/<URLのファイル名部分> へダウンロードされる。

    このケースに当てはまるのは https://some_host/hoge.tar.gz です。つまり、 https://some_host/hoge.tar.gz はダウンロードされ、 /hoge.tar.gz へ保存されます。

  2. ローカルのアーカイブは展開されるがリモートのアーカイブは展開されない

    If is a local tar archive in a recognized compression format (identity, gzip, bzip2 or xz) then it is unpacked as a directory. Resources from remote URLs are not decompressed.

    抄訳すると以下のようになります。

    <src> がローカルの tar アーカイブであり、認識可能な圧縮形式であるときには、それはディレクトリへ展開されます。ただし、<src> がリモートのURLである場合には展開されません。

    つまり、これはローカルの hoge.tar.gz は展開されるが、 https://some_host/hoge.tar.gz は展開されないということを言っています。

ドキュメントを基にすると、少なくとも一方の hoge.tar.gz は展開され、もう一方はされていないことが分かります。つまり、選択肢を 5 もしくは 6 に絞ることができます。

実は 5 になるのか 6 になるのかはドキュメントを読むだけでは判然としません。ただし、 ADD コマンドが取得したリモートのファイルがなんの操作もなくguest ユーザに chown されることはないだろう、ということは hoge.tar.gzroot ユーザのものなはずだ、といった推測などを基に 5 を選ぶことは可能かと思います。

実際には Docker の実行環境さえあればこの問題は簡単に(10分以下で)再現可能です。 some_host の代わりに、ローカルで Web サーバを立て、適当なアーカイブで実験してみると良いでしょう。
競技時間中であれば、 Docker 実技問題のホストで実験することも可能でした。

正解

5 です。

採点基準

正答したチームへ70点を与えました。

講評

回答数は43件/49チーム (87.8%), 正答数は13件(30.2%)でした。

選択問題では、とりあえず何かを選べば正解するかもしれません。分からなくても回答してみるのも手ではないでしょうか。

予選後のアンケートにおいて Docker の筆記問題は難しかったという回答が多く見られましたが、主にこの問題、ないしは Docker Swarm に関する問題が原因になっていると考えています。
どちらの問題も、ドキュメントを読めば正答できるように作られています。 Docker のドキュメントは文章量が多いため、的確な場所をいかに早く見つけられるかというところが分かれ目になったのではないかと考えています。

 /

問題文

このネットワークでServerからRouterにpingを実行した際に、172.16.0.254からは応答がありましたが、10.0.0.254からは応答がありませんでした。
原因を突きとめ、問題を解決してください。

ゴール

Serverから10.0.0.254にpingが通ること

トラブルの概要

  • Routerへのpingがdocker0の方にルーティングされてしまい、想定していない所にパケットが流れていってしまっている。

解説

Dockerは、dockerdが起動する際にdocker0というブリッジを作成します。このブリッジはコンテナがインターネットに接続する際に使われるブリッジになります。

今回、docker0のIPアドレスは10.0.0.1/16になっています。そのため、10.0.0.0/16宛のパケットはdocker0から送信されることになります。そのせいで、10.0.0.254からpingが返ってこないように見えていました。

解決するためには、10.0.0.0/16宛のパケットがVyOS側に届けば十分なので、docker0のブリッジを削除すれば十分なのですが、そうしてしまうとDockerが使えなくなってしまいます。docker0はローカルでのみ使われること(外部からそこに直接IPアドレスを指定してアクセスすること)は基本的にないので、docker0のサブネットを使われていないサブネットにすることで解決します。

docker0のサブネットを変更するためには、systemdのUnitファイルを編集してdockerdの起動時の引数を指定するか、/etc/docker/daemon.conf内で指定することで変更できます。ですが、一般的にはsystemdのデフォルトの引数は変更しない方が良いので、/etc/docker/daemon.jsonの設定を書き換えた方が良いでしょう。

解答例

ServerにSSHに、ping 10.0.0.254を実行してみても応答が返ってこないことが確認できた。

ip routeを実行してみると、10.0.0.254はデフォルトゲートウェイではなくdocker0にルーティングされていることが分かる。そのため、/etc/docker/daemon.jsonを以下のように書き換えた。

{
    "bip": "10.1.0.1/16"
}

その後、以下のコマンドでDockerを再起動した。すると、10.0.0.254からpingが返ってくる事が確認できた。

採点基準

10.0.0.254からpingが返ってくるようであれば250点与えていますが、本来出来ていたことができなくなっていた場合、若干の減点があります。

講評

この問題は作問者が実際にICTSC8の本戦で遭遇したトラブルを再現した問題でした。

いくつかのチームで、Dockerを停止させたりstatic routeを張ったりしてpingを通すチームが見られました。問題を解決するという意味では問題がないので点数を与えていますが、そのままではDockerの動作に影響を与える可能性があります。そのため、コンテナは動いていませんでしたが、多少の減点があると思います。

Routerに手を加えているチームも何チームか見られました。その解法の場合、そもそもpingが通らなかったり、インターネットへの疎通性が失われてしまっていました。問題文でRouterに問題がありそうな書き方をしましたが、問題が発生した際には、pingの送信元に近い方から検証していき、順番に障害を取り除いていくほうが簡単だと思います(tracerouteやtcpdumpなどを使えば、Serverに問題があることが分かると思います)。

ほとんど模範解答通りだったのですが、bipをグローバルIPアドレスにしている方もいました。こうしてしまうと、Routerにはpingが通るようになりますが、インターネットの一部空間へ疎通できなくなってしまいます。その空間が未割り当てなら点数の減点幅を抑えようかと考えましたが、Intelが確保していたので、手動でstatic route張ったときと同じ程度に減点しています。

https://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xhtml

余談ですが、ipコマンド使ってあげてください。アドレスの確認はもちろん、ルーティングテーブルの確認や仮想ネットワークデバイスの作成・削除も同じコマンドからできるのでいろいろと楽になると思います。

https://qiita.com/miyu/items/0cac69b6810dbbc56a9b

 /

問題文

「WebRTC」

この問題はトラブルが複数に分かれています。
その場合、回答本文に「どちらのトラブルについての回答・質問か」を明記してください。
ある社内のコミュニケーションツールとして、WebRTCを利用したテキストチャット・ビデオ共有ツールを導入しています。
あなたはこの社員と協力し、以下のトラブルを解決することになりました。

情報

  • この問題で使用するブラウザは「Google Chrome」と「Firefox」のみです。
  • これら以外のブラウザでは問題回答ができませんので、このブラウザで動作確認をしてください。
  • 参加者は自身のPCからVNCサーバーにHTTPSでWebRTCサービスにアクセスしてください。
  • WebRTCサービスではHTTPS通信に自己署名証明書を使用をしております。ブラウザからアクセスした際に証明書の警告がされます。

問題1 Firefoxで動作しない。

Firefoxにてビデオチャットが動作しないトラブルが発生しています。
このトラブルが発生する原因を調べ、原因の報告、Firefoxにて動作するよう修正を行ってください。

問題2 テキストチャットが動作しない。

クライアント同士が接続後にWebRTC上の通信でテキストチャットを行おうとしたが動作しないトラブルが発生しています。
このトラブルが発生する原因を調べ、原因の報告、テキストチャットが動作するよう修正を行ってください。

サーバーへのアクセス情報

踏み台サーバーから以下のサーバーにアクセスすることができます。

1. WebRTC Server
Address: 192.168.0.100
User: admin
Password: vcFkyv3u
WebRTCのExpress Serverは systemd にて管理
systemctl start ictsc-chat で起動

ゴール

問題1. Firefoxで動作しない

Firefoxでビデオチャットを動作するようにする

問題2. テキストチャットが動作しない。

正しくテキストチャットが動作するようにする

トラブルの概要

[問題1]

~/server/assets/main.jsにて古いAPIが使用されている為に発生するトラブルです。

[問題2]

offer SDPにData Channelの情報が含まれない為に発生するトラブルです。

解説

[問題1]

今回のプログラムをFirefoxで動かすと、コンソールに
TypeError: navigator.getUserMedia is not a function[詳細]
と表示されている事が確認できます。
メディアストリームを取得するAPIで Navigator.getUserMedia が使用されていますが、現在では非推奨となっており、Firefoxでは予選開催日(8月25日)現在で未対応であるため発生するトラブルです。
このAPIの代替APIであり、FirefoxとGoogle Chrome双方で対応しているMediaDevices.getUserMedia を使用するように修正することでこの問題を解決できます。

[問題2]

RTCPeerConnection.createDataChannel
RTCPeerConnection.createOffer 後に行った場合、
offer SDPに Data Channel の情報が乗らず、
Data Channelでの通信がクライアント同士で行われない故に発生するトラブルです。

RTCPeerConnection.createOffer にてoffer SDPを生成する前に
RTCPeerConnection.createDataChannelを使用するように修正することでこの問題を解決できます。

今回のトラブルでは、

クライアント同士が接続後にWebRTC上の通信でテキストチャットを行おうとしたが動作しないトラブルが発生しています。

上記の問題文にある通り、WebRTC上の通信にてテキストチャットを行えるよう修正する問題ですので、シグナリングサーバーのプログラムである ~/server/app.js ファイルを修正しての回答は減点しております。

解答例

[問題1]

~/server/assets/main.js ファイル の21行目にある
navigator.getUserMedia APIを使用している行の記述を変更します。

  • 変更前
    navigator
      .getUserMedia(
        {
          video: true,
          audio: false
        },
        stream => {
          lms = stream;
          const video = addVideo("local");
          video.srcObject = lms;
          video.play();
          socket.send({ type: "call" });
        },
        e => console.error(e)
      );
  • 変更後
    navigator.mediaDevices.getUserMedia({ video: true, audio: false })
        .then(stream => {
          lms = stream;
          const video = addVideo("local");
          video.srcObject = lms;
          video.play();
          socket.send({ type: "call" });
        })
        .catch(e => console.error(e));

[問題2]

~/server/assets/main.js 114, 115行目

const channel = peer.createDataChannel("datachannel");
channel.onmessage = handleRTCData(id);


const offer = await peer.createOffer();の前に移動。

  • 変更前
...
const offer = await peer.createOffer();
await peer.setLocalDescription(new RTCSessionDescription(offer));
sendData({ type: "sdp", data: offer }, id);

const channel = peer.createDataChannel("datachannel");
channel.onmessage = handleRTCData(id);

...
  • 変更後
...
const channel = peer.createDataChannel("datachannel");
channel.onmessage = handleRTCData(id);

const offer = await peer.createOffer();
await peer.setLocalDescription(new RTCSessionDescription(offer));
sendData({ type: "sdp", data: offer }, id);
...

講評

この問題の作成を担当した杉山です。第一予選お疲れ様でした!
WebRTC問題の結果になります。

配点: 500点
問1: 30%
問2: 70%
回答チーム数: 12
問1正解チーム数: 3
問2正解チーム数: 0
※回答によっては部分点として配点しています。

ICTSCでソースコードを書き換える問題は今までに出題されたことがありますが、フロントエンド側の問題としては初めてでした。

問1に関しては、エラーログをコンソールで見れば問題箇所はすぐにわかりますので、そこからMDNなどのサイトを確認すればすぐに修正できたかと思います。
問2に関しては、
1. ~/server/assets/main.js にて sendData 関数を確認し、Data Channelが有効な場合 user.channel.send が呼ばれることを確認
2. SDP negotiationをデバッグし、offer SDPにData Channelの情報が含まれていないことを確認
3. RTCPeerConnection.createOffer にてoffer SDPを生成した後に RTCPeerConnection.createDataChannelにてData Channelが生成されていることを確認
上記の手順を踏めば問題を修正できたかと思います。

今回の問題ではWebRTCを題材とした問題を出題しましたが、想定していたよりもWebRTCの問題の部分に触れてくれるチームが少なかったと感じています。

WebRTCは様々な技術が内部で使用されている魅力的な技術なので、是非調べて挑戦してみてください!

ソースコード

~/server/app.js

const express = require('express');
const app = express();
const http = require("http").Server(app);
const io = require("socket.io")(http);
const PORT = 8080;

app.use(express.static("assets"));

io.on("connection", (socket) => {
  let roomName = null;
  socket.on("enter", (x) => {
    roomName = x;
    socket.join(roomName);
  });

  socket.on("message", (message) => {
    message.from = socket.id;

    if (message.type != "call" && message.type != "sdp" && message.type != "candidate" && message.type != "bye") {
      return;
    }

    if (message.sendTo) {
      socket.to(message.sendTo).json.emit("message", message);
      return;
    }

    if (roomName) socket.broadcast.to(roomName).emit("message", message);
    else socket.broadcast.emit("message", message);
  });

  socket.on("disconnect", () => {
    if (roomName) socket.broadcast.to(roomName).emit("message", { from: socket.id, type: "bye"});
    else socket.broadcast.emit("message", { from: socket.id, type: "bye"});
  });
});

http.listen(PORT);

~/server/assets/index.html

<!doctype html>
<html>
<head>
  <title>ICTSC Chat</title>
  <meta charset="utf-8">
  <link rel="stylesheet" href="main.css" type="text/css" media="all">
</head>
<body>
  <main>
    <div class="chat-widget">
      <div class="control-box">
        <input type="text" id="room-name" placeholder="Room name"
               inputmode="latin" size=15 maxlength=10>
        <button id="connect-button">
          Connect
        </button>
      </div>
      <div class="message-box" id="message-box">
      </div>
      <div class="chat-box">
        <input type="text" id="message" placeholder="Message text"
               inputmode="latin" size=40 maxlength=120 disabled>
        <button id="send-button" disabled>
          Send
        </button>
      </div>
    </div>
    <div class="webrtc-media" id="webrtc-media"></div>
  </main>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.slim.js"></script>
  <script src="./main.js"></script>
</body>
</html>

~/server/assets/main.js

const socket = io.connect(location.origin);

const connectButton = document.getElementById("connect-button");
const sendButton = document.getElementById("send-button");
const messageInputBox = document.getElementById("message");
const messageBox = document.getElementById("message-box");
const roomName = document.getElementById("room-name");
const webrtcMedia = document.getElementById("webrtc-media");

let users = [];
let lms = null; // localmediastream

const states = {
  get connected() {
    return this._connected;
  },
  // handler for state change
  async connect() {
    this._connected = true;
    socket.emit("enter", roomName.value ? roomName.value : "_default");
    navigator
      .getUserMedia(
        {
          video: true,
          audio: false
        },
        stream => {
          lms = stream;
          const video = addVideo("local");
          video.srcObject = lms;
          video.play();
          socket.send({ type: "call" });
        },
        e => console.error(e)
      );
    connectButton.innerText = "Disconnect";
    roomName.disabled = true;
    sendButton.disabled = false;

    messageInputBox.value = "";
    messageInputBox.disabled = false;
  },
  disconnect() {
    this._connected = false;
    connectButton.innerText = "Connect";
    roomName.disabled = false;
    sendButton.disabled = true;

    messageInputBox.value = "";
    messageInputBox.disabled = true;

    delAllVideo();

    if (users.length !== 0) {
      socket.send({ type: "bye" });
      users.forEach(user => {
        user.channel && user.channel.close();
        user.peer.close();
      });
      users = [];
    }
    lms = null;
  }
}

const createPeer = id => {
  const peer = new RTCPeerConnection({
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
  });

  peer.onicecandidate = event => sendData({ type: "candidate", data: event.candidate }, id);
  peer.ontrack = e => e.streams[0] && addRemoteVideo(id, e.streams[0]);

  return peer;
};

// automatically choose socket or datachannel and send
const sendData = (data, id) => {
  const user = users.find(x => x.id === id);

  if (user && user.channel && user.channel.readyState === "open") {
    user.channel.send(JSON.stringify(data));
  } else {
    data.sendTo = id;
    socket.send(data);
  }
};

const handleSocketData = data => {
  handleData(data.from, data);
};

const handleRTCData = id => message => {
  handleData(id, JSON.parse(message.data));
};

// generic handler for socket and datachannel
const handleData = async (id,  obj) => {
  if (!states.connected) return;
  const type = obj.type;
  const data = obj.data;

  if (type === "call") {
    const peer = createPeer(id);

    for (const track of lms.getVideoTracks()) {
      peer.addTrack(track, lms);
    }

    const offer = await peer.createOffer();
    await peer.setLocalDescription(new RTCSessionDescription(offer));
    sendData({ type: "sdp", data: offer }, id);

    const channel = peer.createDataChannel("datachannel");
    channel.onmessage = handleRTCData(id);

    users = users.concat({
      id,
      channel,
      peer
    });
  } else if (type === "sdp") {
    const sdp = data;
    // new RTC connection
    if (sdp.type === "offer") {
      const peer = createPeer(id);
      const user = { id, peer };

      peer.ondatachannel = async event => {
        const channel = event.channel;
        const label = channel.label;

        channel.onmessage = handleRTCData(id);

        users = users.map(x => {
          if (x.id === id) {
            x.channel = channel;
          }
          return x;
        });
      };

      for (const track of lms.getVideoTracks()) {
        peer.addTrack(track, lms);
      }
      await peer.setRemoteDescription(new RTCSessionDescription(sdp));
      const answer = await peer.createAnswer();
      await peer.setLocalDescription(new RTCSessionDescription(answer))
      sendData({ type: "sdp", data: answer }, user.id);

      users = users.concat(user);
    } else if (sdp.type == "answer") {
      const user = users.find(x => x.id === id);
      user.peer.setRemoteDescription(new RTCSessionDescription(sdp));
    }
  } else if (type === "candidate") {
    const user = users.find(x => x.id === id);
    const candidate = data;
    if (user && candidate) user.peer.addIceCandidate(candidate);
  } else if (type === "chat") {
    handleMessage(id, data);
  } else if (type === "bye") {
    const user = users.find(x => x.id === id);
    if (user) {
      user.channel && user.channel.close();
      user.peer.close();
      users = users.filter(x => x.id !== id);
      delVideo(`video-${id}`);
    }
  } else {
    console.error(`unhandled data:${type}`, data);
  }
};

// media chat handler
const addRemoteVideo = (id, stream) => {
  const video = addVideo(`video-${id}`);
  stream.onremovetrack = () => {
    delVideo(`video-${id}`);
  };
  video.srcObject = stream;
  video.play();
};

const addVideo = id => {
  let video = document.getElementById(id);
  if (video) return video;
  video = document.createElement("video");
  video.id = id;
  video.width = 160;
  webrtcMedia.appendChild(video);
  return video;
};

const delVideo = id => {
  const video = document.getElementById(id);
  if (!video) return null;
  if (video) return webrtcMedia.removeChild(video);
};

const delAllVideo = () => {
  while (webrtcMedia.firstChild)
    webrtcMedia.removeChild(webrtcMedia.firstChild);
}

// chat message handler
const handleMessage = (id, message) => {
  const el = document.createElement("div");
  el.className = "message received-message";
  const nameEl = document.createElement("span");
  const balloonEl = document.createElement("p");
  nameEl.textContent = id;
  balloonEl.textContent = message;
  el.appendChild(nameEl);
  el.appendChild(balloonEl);
  const needsScroll =
    messageBox.scrollTop + messageBox.clientHeight === messageBox.scrollHeight;
  messageBox.appendChild(el);
  if (needsScroll)
    messageBox.scrollTop = messageBox.scrollHeight - messageBox.clientHeight;
};

const appendMyMessage = message => {
  const el = document.createElement("div");
  el.className = "message my-message";
  const balloonEl = document.createElement("p");
  balloonEl.textContent = message;
  el.appendChild(balloonEl);
  messageBox.appendChild(el);
  messageBox.scrollTop = messageBox.scrollHeight - messageBox.clientHeight;
};

// add event handlers for each button
connectButton.addEventListener("click", () => {
  if (!states.connected)
    states.connect();
  else
    states.disconnect();
});

sendButton.addEventListener(
  "click",
  () => {
    const message = messageInputBox.value;
    if (message) {
      for (const user of users)
        sendData({ type: "chat", data: message }, user.id);

      appendMyMessage(message);
      messageInputBox.value = "";
      messageInputBox.focus();
    }
  },
  false
);

socket.on("message", handleSocketData);

~/server/assets/main.css

html {
  height: 100%;
}

body {
  margin: 0;
  font-family: "Lucida Grande", "Arial", sans-serif;
  font-size: 16px;
  display: flex;
  height: 100%;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: linear-gradient(to top, #bbd0d5, #d1dbdd);
}

button + button {
  margin-left: 8px;
}

button {
  border: none;
  display: inline-block;
  padding: 7px 20px;
  border-radius: 25px;
  text-decoration: none;
  color: #FFF;
  background-image: linear-gradient(45deg, #FFC107 0%, #ff8b5f 100%);
  transition: all .4s ease-out;
  cursor: pointer;
  box-shadow: 1px 1px 1px rgba(0,0,0,.3);
}

button:active {
  background-image: linear-gradient(45deg, #FFC107 0%, #f76a35 100%);
}

button:disabled {
  color: #eee;
  background: #bbb;
  cursor: default;
  box-shadow: none;
}

main {
  background-color: #fafbfd;
  border-radius: 8px;
  box-shadow: 3px 3px 20px 9px rgba(0, 0, 0, .3);
  display: flex;
  height: 480px;
}

video {
  margin: 12px 24px;
}

.chat-widget {
  padding: 24px;
  width: 400px;
  display: flex;
  flex-direction: column;
}

.message-box {
  flex-grow: 1;
  overflow-x: hidden;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding-top: 16px;
}

.control-box {
  display: flex;
  justify-content: flex-end;
  min-height: 34px;
  border-bottom: 1px solid #ccc;
  padding-bottom: 12px;
}

.control-box > input {
  margin-right: auto;
}

.chat-box {
  display: flex;
  border-top: 1px solid #ccc;
  padding-top: 12px;
  min-height: 32px;
  position: relative;
}

.chat-box > input {
  flex-grow: 1;
  padding-right: 75px;
}

.chat-box > button {
  position: absolute;
  right: 0;
  height: 33px;
}

input {
  display: inline-block;
  padding: 10px 0 10px 15px;
  font-weight: 400;
  color: #377D6A;
  background: #efefef;
  border: 0;
  border-radius: 16px;
  outline: 0;
  transition: all .3s ease-out;
}

input:focus,
input:active {
  color: #377D6A;
  background: #fff;
}

.message {
  display: flex;
  width: fit-content;
  font-size: 14px;
  min-height: min-content;
}

.message > p {
  min-width: 40px;
  max-width: 230px;
  margin: 0;
  margin-bottom: 12px;
  position: relative;
  display: inline-block;
  padding: 0 10px;
  width: auto;
  height: fit-content;
  line-height: 28px;
  border-radius: 40px;
  z-index: 1;
  word-break: break-all;
}

.message > p:before {
  content: "";
  position: absolute;
  z-index: -1;
  display: block;
  width: 22px;
  height: 22px;
  border-radius: 0 30px 0 30px;
}

.message > p:after {
  content: "";
  position: absolute;
  display: block;
  width: 22px;
  height: 22px;
  border-radius: 0 30px 0 30px;
  background: #fafbfd;
  z-index: -1;
}

.my-message {
  align-self: flex-end;
  margin-right: 16px;
}

.my-message > p {
  color: #F6F6F6;
  background: #651fff;
}

.my-message > p:before {
  bottom: 0px;
  right: -8px;
  background: #651fff;
}

.my-message > p:after {
  bottom: 1px;
  right: -18px;
  transform: rotate(30deg);
}

.received-message {
  align-self: flex-start;
  margin-left: 16px;
  flex-direction: column;
}

.received-message > p {
  color: #F6F6F6;
  background: #4caf50;
}

.received-message > p:before {
  bottom: 0px;
  left: -8px;
  background: #4caf50;
  transform: rotate(90deg);
}

.received-message > p:after {
  bottom: 1px;
  left: -18px;
  transform: rotate(60deg);
}

.received-message > span {
  font-size: 11px;
  margin-left: 8px;
  margin-bottom: 4px;
  color: #777;
}

.webrtc-media {
  overflow: auto;
  display: flex;
  flex-direction: column;
  padding: 24px 0;
}

#local {
  order: -1;
  border: 1px solid #4caf50;
}
 /

問題文

とある部署に配属されたあなたは上司に以下のように言われました。

Webサーバ落ちてるんだけど。
僕さ、あんまりLinuxとかよくわかんないんだよね。
前任の人がいい感じにしてくれたんだけど、壊しちゃってさ。
まぁ、いい感じにしてくれ!

上記のトラブルを解決してください。

ゴール

http://192.168.0.1 が200 OKを返すようにすること。

トラブルの概要

  • nginxが起動していない。
  • 80/tcpのパケットがフィルタリングされている。

この2つによって応答が返ってこなくなってしまった。

解説

Webサーバから何の応答が返ってこない場合、次の様な理由が考えられます。

  • Clientのファイアウォールの設定が適切でない。

Clientは踏み台サーバになっていて特別にフィルタリングをするような設定は入れられていないので、この点に関しては問題はありません。

  • ClientとServer間の通信経路に異常がある。

ClientとServerは直接つながっているため問題はありません。これはsshが正常に使えることからも判断ができます。

  • Serverのファイアウォールの設定が適切でない。

Serverでiptables -Lを実行しても1つもルールが入っていないので問題はなさそうです。

  • Server上でWebサーバが動いていない。

lsofnetstatなどのコマンドを用いて、80/tcpをlistenしているプロセスを調べてみても、そのようなプロセスは存在しません。なので、この点は問題がありそうです。

ですが、ここで1度考えてみましょう。もしファイアウォールに何もルールがなく、80/tcpをlistenしているプロセスもないのであれば、以下のようなメッセージが表示されるでしょう。

curl: (7) Failed to connect to 192.168.0.1 port 80: Connection refused

ですが、今回、踏み台サーバからwgetなどでリクエストを送信してもすぐに応答は返ってこなかったと思います。これは、Linuxがiptablesで表示されないような方法でパケットをフィルタリングしているからです。Serverでは、nftablesを用いてパケットをフィルタリングしていました。なので、現在のnftablesのファイアウォールの設定を表示させてみます。

nft list ruleset

すると、以下のような出力が得られます。

    table inet filter {
        chain input {
            type filter hook input priority 0; policy drop;
            icmp type echo-request accept
            tcp dport ssh accept
            ct state established accept
        }

        chain forward {
            type filter hook forward priority 0; policy accept;
        }

        chain output {
            type filter hook output priority 0; policy accept;
        }
    }

このinputチェインを見てみると、80/tcpはACCEPTされていないことが分かります。

ここまでの考察を整理すると、Serverは以下の事柄が原因で正常にレスポンスが返せていなかった事が分かります。

  • 80/tcpをlistenしているプロセスがいない。
  • 80/tcpがnftablesによってフィルタリングされている。

この2つの問題を解決すれば正解になります。

解答例

Serverに入り、lsof -i :80で80/tcpをlistenしているプロセスを調べてみても、どのプロセスも80/tcpをlistenしていなかった。なので、/etc等を調べてnginxを起動・有効化した。

systemctl enable nginx
systemctl start nginx

その次に、nft list rulesetをしてみると、httpがdropされていることが分かった。

なので、nft add rule inet filter input tcp dport http acceptを実行して、ルールを追加した。

nft list ruleset
nft add rule inet filter input tcp dport http accept

これだけではルールが永続化されていないので、以下のコマンドを実行した。

nft list ruleset > /etc/nftables.conf

採点基準

nginxを起動させたことに言及していれば50点。ちゃんと192.168.0.1から応答が返ってくることが確認できた場合、満点です。ですが、本来動いていたnftablesを停止させた場合、減点しています。

講評

少しこの問題はエスパー的な能力が無いと解くのが難しい問題だったかもしれません。解答提出率も20%と割と低かったです。まず、「Webサーバが何なのか」という問題と「誰がフィルタリングしているのか」という問題がありました。Linuxに触れたことが多い人間でないと、nginxやnftablesに目がいかなかったかも知れません。

個人的には、「iptables -Lを見て、ルールが入っていないのに誰かがfilteringしている」というのは初見だとびっくりする人もいるんじゃないのかなと思いました。私はよく、代替のソフトや技術を調べる際に「alternative」をつけて検索することが多いのですが、「iptables alternative」と検索するとすぐにnftablesが出てくるので、焦らずに調べれば解くことができたんじゃないかなと思います。

 /

1問目

問題文

あなたはWebサービスを作っています。

このサーバーではnginxが0.0.0.0:80で通信待ち受けしています。このnginxを用いてlocalhost:8080で動いているサービスにproxyするようにしたいのですが、何故か正常に動作しません。

原因を特定し、正常に動作するために必要な手順を説明してください。

なお、nginxは適切に設定がなされているものとします。また、iptablesのポリシーを変更するのは禁止します。

root@ubuntu:/etc/nginx/sites-enabled# systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
   Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
   Active: active (running) since Sun 2018-07-29 11:48:05 JST; 18min ago
     Docs: man:nginx(8)
  Process: 1394 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
  Process: 1393 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
 Main PID: 1395 (nginx)
    Tasks: 3 (limit: 2327)
   CGroup: /system.slice/nginx.service
           ├─1395 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
           ├─1396 nginx: worker process
           └─1397 nginx: worker process
root@ubuntu:/etc/nginx/sites-enabled# ps ax | grep 8080
 2750 pts/0    S      0:00 python3 -m http.server 8080
 2762 pts/0    S+     0:00 grep --color=auto 8080
root@ubuntu:/etc/nginx/sites-enabled# iptables -t filter -L -v
Chain INPUT (policy DROP 46 packets, 4344 bytes)
 pkts bytes target     prot opt in     out     source               destination         
 1220 89088 ACCEPT     tcp  --  any    any     anywhere             anywhere             tcp dpt:ssh
    0     0 ACCEPT     tcp  --  any    any     anywhere             anywhere             tcp dpt:http
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain OUTPUT (policy ACCEPT 772 packets, 90484 bytes)
 pkts bytes target     prot opt in     out     source               destination         
root@ubuntu:/etc/nginx/sites-enabled# iptables -t nat -L -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
root@ubuntu:/etc/nginx/sites-enabled# iptables -t mangle -L -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
root@ubuntu:/etc/nginx/sites-enabled# iptables -t raw -L -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination    

トラブルの概要

nginxとPythonで作成されたWebアプリケーション(以下アプリケーション)がうまく通信できていない。

解説

リクエストが処理される流れを前から追って問題を調べてみましょう。

  • Serverに80/tcpのパケットが送信される。

iptablesのfilterテーブルのINPUTチェインを見るとhttpはACCEPTされているので問題ありません。

  • Linuxからnginxにデータが渡される
  • nginxがlocalhost:8080にproxyする

「nginxは適切に設定されている」とあるので問題ありません。

  • nginxがアプリケーションにリクエストを投げる
  • Serverに8080/tcpのパケットが送信される(内部で)

iptablesのfilterテーブルのINPUTチェインを見ても8080/tcpはACCEPTされていないので問題があります。

  • アプリケーションがnginxにレスポンスを返す
  • nginxにデータが送り返される

nginxがアプリケーションにデータを送る際に、nginxはephemeralなポートを使ってアプリケーションと通信します。アプリケーションはnginxにデータを送り返しますが、iptablesのfilterテーブルのINPUTチェインを見てもephemeralなポートはACCEPTされていないので問題があります。

  • nginxがレスポンスを返す

iptablesのfilterテーブルのOUTPUTチェインを見ても何もルールがないので問題ありません。

これまでの考察を整理すると、nginxとアプリケーションがiptablesの設定により正常に通信できていないことが分かります。

解答例

iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT

もしくは

iptables -A INPUT -i lo -j ACCEPT

採点基準

200 OKが返ってくるような設定ならば100点を与えています。
ESTABLISHEDのルールが抜けている場合は50点を与えています。

講評

簡単に点数が取れるだろうと思っていたのですが思ったよりも解けているチームは多くなかったです。

当初、8080/tcpとESTABLISHEDをACCEPTする答えが来ると想定していましたが、loをACCEPTする解答が大半を占めていました。

ちなみに、8080/tcpをACCEPTする解答を送信してきたチームはすべてESTABLISHEDをACCEPTするのを忘れていました。ESTABLISHEDは忘れやすいですし、TCPのESTABLISHEDとは無関係なので、少し厄介なのかなと思います。

2,3問目

問題文

ここにGoで書かれたコードがあります。このプログラムはIPv4のIPアドレスをキーにして通信を破棄することができます。
このコードを参考に、後述する問いに答えてください。

package main

import (
    &quot;fmt&quot;
    &quot;os&quot;

    &quot;github.com/AkihiroSuda/go-netfilter-queue&quot;
    &quot;github.com/google/gopacket/layers&quot;
)

const NFQUEUE_NUM_ID = 10
const MAX_PACKET_IN_QUEUE = 300

const EXCLUDE_IN_IP = &quot;192.168.0.2&quot;
const EXCLUDE_IN_Port = &lt;1&gt;

func isSelectedExcludeInIP(packet *netfilter.NFPacket, target string) {
    if target == EXCLUDE_IN_IP {
        packet.SetVerdict(netfilter.NF_DROP)
        fmt.Println(&quot;Drop is IP&quot;)
    }
}
func isSelectedExcludeInPort(packet *netfilter.NFPacket, target string) {
    if target == EXCLUDE_IN_Port {
        packet.SetVerdict(netfilter.NF_DROP)
        fmt.Println(&quot;Drop is Port&quot;)
    }
}

func main() {
    var err error

    nfq, err := netfilter.NewNFQueue(NFQUEUE_NUM_ID, MAX_PACKET_IN_QUEUE, netfilter.NF_DEFAULT_PACKET_SIZE)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer nfq.Close()
    packets := nfq.GetPackets()
    for true {
        select {
        case packet := &lt;-packets:
            ethernetLayer := packet.Packet.Layer(layers.LayerTypeEthernet)
            ipLayer := packet.Packet.Layer(layers.LayerTypeIPv4)
            tcpLayer := packet.Packet.Layer(layers.LayerTypeTCP)
            if ipLayer != nil {
                ip, _ := ipLayer.(*layers.IPv4)
                isSelectedExcludeInIP(&amp;packet, ip.SrcIP.String())
            } else if &lt;2&gt;{
                &lt;3&gt;
            }else {
                packet.SetVerdict(netfilter.NF_ACCEPT)
            }
        }
    }
}

1.このプログラムを用いてパケットを制御することができます。
go run ファイル名 でプログラムを起動し、パケットの制御を行えるのですが、それに加えてiptablesでqueueの設定をしなくてはいけません。
前述したコードを用いてIP通信を制御する際にはどのようなコマンドでqueueの設定をすればよいでしょうか。
コマンドはiptablesから始めて記述してください。
また、startだけではなく、stopの時も空行を挟んで同様に示してください。

2.突然ですがあなたは学内ネットワークの管理者になりました。
そこではsshなんて通しちゃダメという謎の言葉があるドキュメントがあり、あなたはそれに従う必要が出てきました。
前述したコードには<1>,<2>,<3>という穴があります。
その部分を選択肢から一つ選んで埋めてください。

  • A
    • <1>:22
    • <2>:tcpLayer != nil
    • <3>:isSelecteExcludedInMACAddr(&packet, ethernetPacket.SrcMAC.String())
  • B
    • <1>:80
    • <2>:ethernetLayer != nil
    • <3>:isSelecteExcludedInMACAddr(&packet, ethernetPacket.SrcMAC.String())
  • C
    • <1>:23
    • <2>:tcpLayer != nil
    • <3>:isSelectedExcludeInIP(&packet, ip.SrcIP.String())
  • D
    • <1>:22
    • <2>:tcpLayer != nil
    • <3>:isSelectedExcludeInPort(&packet, tcp.SrcPort.String())

概要

  • netfilter queueの実行の仕方
  • sshのブロックを行う方法

解説

2問目

queueというのはiptablesのnetfilterでユーザー側にデータを渡すための待ち行列のことで、queue番号というのはその待ち行列に対するタグ付けした番号です。
この問題のキモとして、入力としてQUEUE番号を何に指定しているのかということを理解している必要があります。
この場合はconst NFQUEUE_NUM_ID = 10 より10にタグ付けされているのでNFQUEUE --queue-num 10ということになります。

3問目

  • <1>
    SSHのデフォルトポートである22番ポートを指定する
  • <2>
    TCPのコネクションが確立されているかの判定を行う。tcpLayer!=nilならば、それはTCPのレイヤーでの通信挙動を取得できているということになる。
  • <3>
    TCPのデータを抽出し、それをチェックする関数を呼び出す。

解答例

2問目

A.

//start: sudo iptables -A OUTPUT -j NFQUEUE --queue-num 10
//end  : sudo iptables -D OUTPUT -j NFQUEUE --queue-num 10

3問目

  • <1>
    22
  • <2>
    tcpLayer != nil
  • <3>
tcp, _ := tcpLayer.(*layers.TCP)
isSelectedExcludeInPort(&amp;packet, tcp.SrcPort.String())

採点基準

2問目

  • queue番号を10ということを言及できている 30点
  • 実行と解除のコマンドを示すことができている 30点
  • きちんと実行できる 40点

3問目

正答であれば満点

講評

2問目では誘導でキーワードを調べる機会として、
3問目では実際にコードに触れ、どういう挙動になるのか簡単に知ってもらうという意図からの問題でした。
3問目は選択式ということもあり正答率が半数を超えて高く、点数源にしてもらえていてよかったです。しかしながら2問目は解いているチームが半数ぐらいで驚きました。

4,5問目

問題文

4問目

ネットワークのパケットをtcpdumpでキャプチャするときの基礎として「フィルタリングの条件式構文」が挙げられます。
式は一つかそれ以上の要素で構成され、要素は通常は一つかそれ以上の修飾子によって構成されています。

これらの具体例を挙げると、
– 192.168.0.1の場合だけを見たい場合はtcpdump net 192.168.0.1
– ポート21~23だけを見たい場合はtcpdump portrange 21-23

のようになり、任意の条件式を使うことができます。

この前提から、以下の問題に答えてください。

  • tcpdumpでinterface eth1から取得して、HTTP GETとPOSTのみを表示するという条件でフィルタリングして表示してください。
  • また、tcpdumpでinterface eth0から取得して、宛先アドレスは192.168.2.200または192.168.1.100、TCPでポート番号80という条件でフィルタリングを行い表示してください。

5問目

フィルタリングの技術としてBPF(Berkeley Packet Filter)というものがあります。
BPFはレジスタマシーンとして命令を受理して動作が行われます。 これを踏まえると
tcpdumpはフィルタ条件式のパーサーとそれをコンパイルすることのできるある種のドメイン言語ともいうことができ、
最近ではJITコンパイルを行うものも存在します。
tcpdumpがコンパイラと同じということは、パースしたフィルタ条件式から変換されたバイトコードを吐くという事です。

例えば
tcpdump -p -ni en0 -d "ip and udp"
を実行すると以下のようなバイトコードが出力されます。

(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 5
(002) ldb      [23]
(003) jeq      #0x11            jt 4    jf 5
(004) ret      #262144
(005) ret      #0

軽く説明すると、0x0800がイーサネットフレーム上のIPv4を示していて、0x11がUDPということを示しています。
これらがマッチした場合に最後の004に飛んで無事フィルターにマッチしたものが出力されるという動作をします。
※本文では005と書きましたが正しくは004です

上記の前提から、以下のバイトコードの示している条件式を答えてください。

(000) ldh      [12]
(001) jeq      #0x86dd          jt 16 jf 2
(002) jeq      #0x800           jt 3  jf 16
(003) ldb      [23]
(004) jeq      #0x6             jt 5  jf 16
(005) ldh      [20]
(006) jset     #0x1fff          jt 16 jf 7
(007) ldxb     4*([14]&amp;0xf)
(008) ldh      [x + 14]
(009) jeq      #0x50            jt 12 jf 10
(010) ldh      [x + 16]
(011) jeq      #0x50            jt 12 jf 16
(012) ld       [30]
(013) jeq      #0xc0a802c8      jt 15 jf 14
(014) jeq      #0xc0a80164      jt 15 jf 16
(015) ret      #262144
(016) ret      #0

トラブルの概要

  • パケットフィルタリングを行うのに行うべき条件を正しく理解して表現できるか

解説

4問目

1.tcpdump -i eth1 'tcp [32:4] = 0x47455420 or tcp[32:4] = 0x504f5354'
が答えです(一例)

tcpdump -i eth1でinterface eth1から取得しています。

次にtcp[32:4]が何を示しているかについて説明します。
これはTCPヘッダーの32バイトから4バイトの参照ということを示しています。
この参照先ではTCPヘッダーのうち、オプションを含めない領域の20バイトに加えてTCPのコネクションが成立している時に付くオプションのことを指しています。

主にTCPは以下のオプションパラメータがあります。
– mss : 最大セグメント長 Max-segment-size
– wscale : ウィンドウスケール
– sackOK : Selective Acknowledgmentが有効
– TS val ecr : valはタイムスタンプ ecrはecho reply 参照 rfc7323
– nop : パディング No operation provides padding around other options
– eol : オプションリストの終わり

options [nop,nop,TS val ecr ]

というような想定されるパターンが帰ってくる時には、一般的にオプションは12バイトということが一般に知られています。
ということでTCPヘッダーのオプションを含めないサイズ20バイト+オプションで12バイトということで32バイトとなります。

そこからHTTPリクエストは、次のように開始されます。例としてあげたこれの先頭にGETという文字列が存在します。これは先頭の3バイトについてあります。

GET / HTTP / 1.1 \ r \ n

これをもとにGETとPOSTのみを表示すると言う条件でフィルタリングについて考えると

&gt;&gt;&gt; import binascii
&gt;&gt;&gt; binascii.hexlify(b&#039;GET&#039;) 
b&#039;474554&#039;
&gt;&gt;&gt; binascii.hexlify(b&#039;POST&#039;) 
b&#039;504f5354&#039;

ということがわかりました。またなぜ「4」という指定なのかというのは試しに他の3などを入れるとわかるのですが

tcpdump: data size must be 1, 2, or 4

というエラーが出力される事が物語っているので興味のある人は理由を考えてみると面白いでしょう。

2.tcpdump -i eth0 '((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))'が答えです(一例)

-i eth0 でインターフェイスとしてeth0を指定しています。
'((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))'ではtcpport 80のものでかつ宛先IPが192.168.2.200192.168.1.100のどちらかであるものという条件式になっています。

5問目

((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))
が答えなのですが皆さんは解けましたでしょうか。根気よく調べながら追っていけば簡単に解ける問題だったと思います。

バイトコードは以下の方法で確認できます。(環境によってinterfaceは変えてください。)
sudo tcpdump -p -ni en0 -d '((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))'

では簡単にですが実際にバイトコードを追ってみましょう。

(000) ldh      [12]
(001) jeq      #0x86dd          jt 16   jf 2
(002) jeq      #0x800           jt 3    jf 16

(000)でオフセットのロードをした後、
(001)と(002)ではIPv4とIPv6であることを確認しています。
jeqなのでマッチしたら002に飛ぶという動きをしています。
これでTCPである前提条件のIPが使われているというがわかりました。

(003) ldb      [23]
(004) jeq      #0x6             jt 5    jf 16

(003)で 12バイトのオフセットのロードをした後、
(004)で次のヘッダーが6であるかのチェックをしています.
これは6==(tcp) ということがわかります。

(005) ldh      [20]
(006) jset     #0x1fff          jt 16   jf 7
(007) ldxb     4*([14]&amp;0xf)
(008) ldh      [x + 14]
(009) jeq      #0x50            jt 12   jf 10
(010) ldh      [x + 16]
(011) jeq      #0x50            jt 12   jf 16

(005)は(flags + frag offset)というサイズのロードをしていて
(006)は0x1fff == 0001 1111 1111 1111 というフラグメントオフセットが真かどうかのマッチをしています。

(007)はかなり厳つい感じですが
x == 4*ipヘッダの長さということを示しています。つまりTCPヘッターの基本サイズです
なのでx = 20バイトと考えてあげることができます。
(008)ではハーフワードをパケットオフセットx + 14にロードしています。この場合はパケットオフセットが20のため、20+14 で 34になります。
(009)からは、上でパケットオフセットの位置を整えたため単純なマッチで(途中にロードを含んでいますが)ポート番号のチェックをしています。
ここでは0x50番ポート、つまり80番ポートであることのチェックをしています。

(012) ld       [30]
(013) jeq      #0xc0a802c8      jt 15   jf 14
(014) jeq      #0xc0a80164      jt 15   jf 16
(015) ret      #262144
(016) ret      #0

(012)で30をロードしていますが、これは宛先アドレスへの移動をしています。
その先の(013),(014)で宛先アドレスの判定を行っています。
この16進数がIPアドレスを示しており、以下のように変換されます。
0xc0a802c8->192.168.2.200
0xc0a801640->192.168.1.100

最後に、前述までのバイトコードで頻繁に出てきた16は失敗した時に送られるnon-matchを返すコードのアドレス(016)で、最終的に成功した時は15というmatchを返すコードのアドレス(015)です。
条件式の結果により、どちらかのアドレスにジャンプすることでパケットの分類が完了します。

というような手順でパケットを読み解くための条件式を導き出すことができます。

解答例

4問目

1.tcpdump -i eth1 'tcp [32:4] = 0x47455420 or tcp[32:4] = 0x504f5354'
2.tcpdump -i eth0 '((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))'

5問目

((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))

採点基準

4問目

  • 一つ目はPOSTとGETを示したら10+10点, 全て示して動いたら30点
  • 二つ目は全て示して50点

で合わせて100点

5問目

  • IPv4 IPv6ということを読み解き言及している 50点
  • Port 80ということを読み解き言及している 50点
  • Destination Hostについて言及している 50点
  • 上記を満たした上で実行して動くということが完全に示せている 50点

講評

まずは問題文の訂正です。

これらがマッチした場合に最後の005に飛んで無事フィルターにマッチしたものが出力されるという動作をします。

と問題文の簡単な説明の時に書きましたが、正しくは004です。
間接的にですが間違った言及をしたことをお詫び申し上げます。

4問目ではどういうシンタックスなのかを調べる機会として、
5問目では本質的にどういう挙動になるのか簡単に触れてもらうという意図で作成した問題でした。

4問目の2つ目は比較的簡単に作ったので得点源に・・・と思っていましたが、実際に出題されると意外なことに4問目の1つ目が割と解かれていた印象でした。
個人的に面白かった点として、どこから参考にしたのか基本的に問題を解いた方は判を押したように tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420' and ~~~ のような記法で書いていた方がほとんどでした。
気になって調べたところ、某緑色の技術記事を投稿するサイトが日本語記事でトップに出てくるようで、そちらと同じ記法であることから、そのサイトを参考にしたのだろうと思いながら採点を行いました。

5問目は一見点数を取らせないような問題に見せていますが、実はよく読めば解ける問題です。解けるだろうと前述した「4問目の2つ目」がそのまま答えでした。
条件が複雑なものが全問に記載されていたこともあり、未回答も目立ちましたが割とアプローチをした跡があった事から参加者たちのレベルの高さが伺えます。
やはりきちんと解析をしてみるというのは大切だと感じました。これはいくつかのチームが解いてくれていましたが満点を取ったのは2チームだけでした(拍手)

ぜひ自分のチームは5問目にアプローチしたなーと思った方は回答に合わせてtcpdumpに-dをつけて挙動を見てみてくださいね!

 /

問題文

あなたの元にWebサーバーの構築に関するトラブルシューティングの依頼が届きました。
ロードバランサーにアクセスをすると、正常なレスポンスが返ってこないことがあるようです。
ロードバランサーによる振り分け先のWebサーバーはセキュリティ上触ることはできませんが、
ロードバランサーへの接続情報は知っています。
このロードバランサーの設定を変更し、エラー表示がなくなるよう修正してください。

情報

ルーター・サーバーへのアクセス情報

踏み台VNCサーバから以下のサーバにアクセスすることができます。

  1. Load Balancer
    Address: 192.168.0.1
    User: admin
    Password: 8NrV6CZei

ゴール

ロードバランサの設定を修正し、エラーが表示されなくなるようにする

復旧措置

ペナルティーによる減点はありません。

解説

この問題はNginxのupstream機能を使ったロードバランシングの問題でした。

まず、状況を確認するために、ロードバランサーにhttp GETリクエストを送ってみると、稀に500レスポンスが返ってくることがあります。
今回の問題では、実際にレスポンスを返しているアプリケーションサーバーには手を付けることができないため、ロードバランサーの設定を変更することでエラーが表示されないようにしなければなりません。
もう少し、詳しく状況を確認してみましょう。ロードバランサーの設定ファイルを確認してみると、upstream機能によって、192.168.0.11と192.168.0.12の2台にリクエストが振り分けられていることがわかります。
実際にcurlなどを用いてレスポンスを確認してみると、このうち192.168.0.11の方は500レスポンスは出さず、192.168.0.12で稀に500レスポンスが返ってくるというという状況にあることが確認できます。

192.168.0.12で稀に500レスポンスが返ってくるという状況であることから、パッシブヘルスチェックを用いて500レスポンスが返るときに、failoverされるように設定すると問題が解決できます。

解答例

upstreamによってリクエストが192.168.0.11と192.168.0.12に振り分けられていることがわかる。
それぞれをcurlコマンドによって確認したところ、192.168.0.12のサーバーのみから稀にエラーが返ってきた。
/etc/nginx/nginx.confproxy_pass http://ictsc-web; の次の行に

proxy_next_upstream error timeout http_500;

と追記することでNginxのパッシブヘルスチェックが行われ、500レスポンスが返ってきた際にフェイルオーバーすることができるようになる。
設定の編集後、 sudo systemctl restart nginx でNginxを再起動する。

採点基準

採点の際に、upstreamのserver 192.168.0.12;を削除する、という解答が多く見られましたが、これでは振り分け先が1つになってしまい、ロードバランサの役割をなしていないと考えたため30点の減点をしました。
また、192.168.0.12のサーバーをstandby状態にしておくという解答も見られました。今回の問題では192.168.0.11のサーバーではエラーが起こることはなく表面上問題が解決されたように見えてしまいますが、仮に192.168.0.11のサーバーでエラーが起こった際に、確実に稀にエラーが返ってくるように設定してある 192.168.0.12のサーバーにフェイルオーバーされてしまうため、50点の減点ということにしました。

講評

この問題への解答率は44.90%、解答があったなかで部分点を含め点数をつけたチームは86.36%、満点をつけたチームは31.82%でした。
他の問題に比べると難易度が低く設定されていたため、解答がなかったチームでも挑戦をしてみればなにか取っ掛かりが得られたのではないかと考えています。個人的には、解答率が予想より低く残念だと感じました。

 /

問題文

社内にVyOSを導入することが決まった。
そこで、社内のインフラを構築する前に以下の図の様なテスト環境で検証を行った。
VyOSにDNSのキャッシュサーバとDHCPサーバを起動し、下に接続されているUbuntuにIPアドレスを割り当てた。
しかし、Ubuntuから外部のサーバにアクセスしようとしたが接続できずICMPを送っても応答がない。ところが、VyOSからICMPを外部のサーバに送ると応答が返ってくる。
なので、Ubuntuから外にアクセスできる様に解決してほしい。ただし、Ubuntuに直接接続することはできず、VyOSからはアクセスできるようになっている。

スタート

  • Ubuntuから外部のサーバにアクセスできない
  • VyOSからはアクセスが可能

ゴール

  • Ubuntuから外部にアクセスできる

情報

サーバ IP アドレス アカウント パスワード
vyos 192.168.20.80 admin PxZsMqycN4a4nzlk1Xg7
client 192.168.20.2 admin kopvVL3cT2tkALu4nQnD

トラブルの概要

この問題ではVyOSがNATをして下に接続されているUbuntu Server(以下clientとする)が外とアクセスできるような構成になっていました。しかし、何らかトラブルでclientが外と全く疎通が取れない状態でした。VyOSから8.8.8.8にpingをすると応答が帰ってくるという状態でした。

解説

問題文を読んだだけで大抵の人はVyOS側に問題があることに気づいたかと思われます。答えはVyOSのNATの設定に不備がありclientが外に繋げられなかったということです。

解答例

NATの設定を確認するためにshow natを打つと以下のように表示されます。

nat {
source {
rule 1 {
outbound-interface eth1
source {
address 192.168.20.0/26
}
translation {
address masquerade
}
}
}
}

今回の場合、VyOSが対外に接続しているインターフェースはeth0なのにoutbound-interface eth1となっています。これが原因な訳でこれを消すか上書きしてあげることで疎通が取れるようになるはずです。以下のような手順で上書きをしてみます。

# set nat source rule 1 outbound-interface eth0
# commit
# save

この後に8.8.8.8に対してpingをすると以下のように接続できていることがわかります。

$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=12.8 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=11.8 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=61 time=10.4 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=61 time=9.57 ms

採点基準

  • 外部に疎通できないことを指摘
  • 解決方法の提示
  • そのあとに疎通できることを提示

講評

この問題はウォームアップ問題として作成しました。なのでほとんどのチームが解答してくれました。実はVyOSは一回も触ったことがありませんでした。だそうにも難しくすることができないので実際に自分がミスしたところを出せばいいのではと思い出題しました。

最近のコメント