/

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 > 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をつけて挙動を見てみてくださいね!

 /

問題文

顧客が増えてきたため、今まで単一ノードで動作していた通信販売サービスを複数台構成にし、前段に1種類の負荷分散アプライアンスを導入したい。また、成長株なので社長はいくらでも投資してくれると言っている。よって、コストについては考えなくて良い。この時、導入する負荷分散アプライアンスの動作形式として最も適切だと考えられるのは、以下の4つのうちどれか。

トラブルの概要 (必須)

なし

解説

  • 負荷分散サーバにSSLアクセラレータを搭載し、セッション単位で分散を行う。
  • HTTPのcookieをベースにセッション単位で分散を行う。
  • IPアドレスベースでノード単位の分散を行う。
  • SSLセッションIDをベースにセッション単位で分散を行う。
    この4つの選択肢が与えられていました。

まず、通信販売サービスは、HTTPSを用いている可能性が高い、もしくは今後導入する可能性が高いと考えられるため、HTTPのcookieは通常のLBでは触れない可能性が高くなります。SSLセッション毎に分散をすると、サービスのセッションが各サーバで同期されていない場合ユーザ側は頻繁にサービスのセッションが切れてしまう可能性が考えられます。IPアドレスベースで分散するのと、SSLアクセラレータを搭載し、HTTPのcookie等のセッション情報をベースに分散するのでは、コストを考えない場合、SSLアクセラレータを搭載しHTTPのセッション情報をもとに分散するほうがより適切に分散ができると考えられます。

講評

4択問題なので、ほぼすべてのチームが回答していました。実際にはコスト等を考えると、L3やL4LBが現実解だと思います。最近はDPDKを使ってL4LBやるのが流行っているらしいです。

 /

問題文

あなたの元に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%でした。
他の問題に比べると難易度が低く設定されていたため、解答がなかったチームでも挑戦をしてみればなにか取っ掛かりが得られたのではないかと考えています。個人的には、解答率が予想より低く残念だと感じました。

 /
カテゴリー

ICTSC2018 1次予選の予選環境にアクセスするためには、踏み台サーバを経由してアクセスする必要があります。踏み台サーバへは、SSHもしくはVNCを用いて接続することが出来ます。

macOS

SSHを用いる場合

ターミナルを開き、以下のコマンドを実行します。

$ ssh -D [proxy-port] -p [port] [user]@[remote addr]

VNCを用いる場合

インストール方法

homebrewを用いてインストールする場合は以下のコマンドを実行してください。

$ brew cask install vnc-viewer

公式サイトを用いる場合はこちらからダウンロードしてください。

接続方法

Spotlightなどで「VNC Viewer」を開き、上部のテキストボックスにホスト名とポート番号を入力してEnterを押します。

その後、ユーザ名とパスワードを入力してログインします。

Windows

SSHを用いる場合

こちらでは、SSH接続するため環境をMobaXterm、ブラウザはfirefoxで説明させていただきます。
※MobaXterm未インストールの方はコチラから無料の方をダウンロードしてください。
※firefox未インストールの方はコチラからダウンロードしてください。

接続方法

  • MobaXtermの設定
    • Dynamic port forwardingを用いた新しいtunnelingを追加します。
    • 使用するポート番号と、接続先のipアドレスユーザー名ポート番号を入力してください。
  • firefoxの設定
    • ネットワークプロキシの接続から手動でプロキシを設定します。
    • その際、SOCKSの欄にipアドレスポート番号を設定してください。

VNCを用いる場合

※UltraVNC未インストールの方はこちらからダウンロードしてください。

  1. 「VNC Server」の項目に接続先ip addressを入力します
  2. 右側にあるConnectを押して接続完了です。
 /
カテゴリー

こんにちは、バックボーンなどインフラを担当した川原です。

参加者の皆さん、参加してくださり誠にありがとうございました。今回も30分のサーバの電源断、1日目のWi-Fiの不調など、皆さんにご不便をお掛けしてしまい申し訳ありませんでした。特にWi-Fiにつきましては、アンケートでも厳しい意見が多かったためバックボーン担当として重く受け止めています。ただ、Wi-Fiの調子は人数が増えてみないとわからないことも多いです。年々、改善を繰り返しながら取り組んでおりますので今後とも率直な意見をくださりながら参加して楽しんでいただければと思います。

また、機材提供・協力をはじめとした協賛企業の皆様、毎度のことながら我々学生の無茶な要望を受け入れてくださり誠にありがとうございます。今回も参加者へのインフラを提供する傍ら、運営学生も貴重な体験をすることができました。

さて、堅苦しい話はここまでとして今回も大規模なインフラを組んで皆さんに提供しました。LTなどでも話しましたが、改めてブログにまとめることで多くの人にネットワークやサーバに興味を持って欲しいと思っています。

今回のインフラは何人かで担当しているので、担当した各人がブログへ投稿することになっています。この記事では最初の記事として全体の総括をしていきたいと思います。

個別の解説は以下のとおりです.

バックボーンについて

我々ICTSC運営において、バックボーンとは全チームが問題を解くために使うインフラのことを言います。バックボーン担当は作問など大会開催に必要なタスクをこなす傍ら、興味のある人が思い思いのインフラを構築していくことになります。

バックボーンの目的は以下です。

  • 参加者が問題を快適に解ける環境を提供する
  • ICTSC運営が問題を提供しやすい環境を提供する

つまり、1に参加者、2に参加者、3くらいに問題作成者のことを考えて作るわけです。勘違いすると安定感に欠けた目的のわからないものになってしまうので、気をつけなくてはいけません。また、どんな問題でも出題できるように準備をしています。インフラ的に難しいからこの問題はボツで…ということは極力ないようにします。

問題作成は運営発足から行われるものなので、運営発足からHotstage(本番の大会会場で本番の機材を使いながら準備する期間)中のインフラもある程度安定させなければ、よい品質の問題を用意することができなくなってしまいます。そのあたりの差し引きをしながら本番環境を構築していくのもバックボーン構築の醍醐味です。

今回のバックボーンテーマについて

最近のICTSCのバックボーンはテーマを決めて構築を行っています。第7回は安定感、第8回は仮想化でした。

今回は… 冗長化!!

ということで構築されたバックボーンネットワークは以下のように、今回のテーマに則ってネットワーク機材をすべて冗長化しています。

各機材の冗長構成については各担当の者がそれぞれ本ブログに投稿することになっているので楽しみにしていてください。概要については当日の発表資料を以下に用意しました。

当日発生したトラブルについて

どれだけ準備しようがトラブルは起きてしまうものです。当日起きてしまったトラブルをまとめていきます。

1日目 11:30頃 サーバ電源断

1日目 11:30頃にサーバの全台が電源断するという障害が発生しました。昼食が早まったことで解答不可能になった時間は比較的少なくなりましたが、ご迷惑をお掛けしてしまい誠に申し訳ございませんでした。

原因

我々の不手際によって協賛の皆さんが半分のサーバを接続している電源系統でPCの充電をしてしまったことによる過負荷でおきたものであると考えられます。また、その後私が生きていたサーバのコンセントを抜くというミスオペを犯してしまいました。

改善策

改善策としては、サーバは冗長化が行えていなかったためサーバの電源をそのような危険性がある電源からではなくアクセスしにくい電源を選択するなどがあります。ネットワーク機器は最悪電源が落ちても2系統とっていましたし、再起動も比較的高速に行えます。何よりサーバの電源断はHDDなどが故障する可能性もあります。

ミスオペに関してはダブルチェックなどを行うなどがあります。ただ、復旧を一丸となって行えたのでとても良かったと思います。私を責めずに冷静に私の指示に従ってくれたみんなには感謝です。

1日目 懇親会

Wi-Fiの調子が悪いという話を多くの方から聞きました。懇親会後、原因の調査と修正を行いました。2日目の開始時にも軽くお伝えしましたが、ストレスを感じさせてしまい申し訳ありませんでした。

原因

2.4Ghz帯の電波が強すぎたことによって、多くのクライアントが2.4Ghz帯で接続し混線したものと考えられます。

改善策

2.4Ghz帯の電波を弱める、5Ghz帯でクライアントが接続できているか監視するなどの対策を行いました。我々の感知している範囲では2日目は快適に使用できていたと聞いています。

しかし、競技時間の半分を不快な環境で回答していたことについては反省会でも話題になりました。次回以降はWi-Fiの調子などを早めに聞く機会を設け、早めに対策が行えるように努力します。みなさんもストレスを感じた場合は気軽に問い合わせ下さい。バックボーン側に原因がある場合は早急に対応いたします。

また、Wi-Fiの調子は当日の人数が入らないとわからないことも多いです。次回以降の運営からも当日アナウンスがあるとは思いますが、Wi−Fiが不調の場合は有線接続の使用をご検討ください。

2日目 13時頃

一部グローバルIP(Googleなど)への接続ができなくなるという障害が発生しました。障害は3分ほど継続した後、収束しました。

原因

以上の障害発生時のメトリクスを見ても、障害発生時間帯にルート数が5000経路ほど減少していました。また、こちらの設定ミスか上流からのトラフィックを受信しているのに、こちらからトラフィックが流れていませんでした。障害発生から5分以内にメトリクスを把握し上流であるHomeNOC様に確認したところ、一部障害が発生していたとお聞きしました。

しかし、このようなことが発生してもよいように上流は冗長化していましたが、うまく動作しませんでした。冗長化が動作しなかった原因としては、IPv4のフルルートを受け取っていた影響でルートの収束まで時間がかかってしまったようです。準備期間中や開催後に接続断テストを行った際には収束まで約10分の時間を要しました。

原因究明にはメトリクスによる可視化が重要であることを実感できた障害でもありました。

改善策

なぜ、収束までにここまでの時間が発生するのかは我々も理解しきれていません。勉強してきます。

フルルートの運用をしてみないとわからないことは多く存在します。このような貴重な機会をくださったHomeNOC様に感謝しております。

おまけ: ベンチマーク

今回、テンションがおかしくなってしまった運営学生は10Gbps NICをヤフオクで9本程度落札してしまいました。それをNAVTサーバに搭載し、バックボーンに組み込んでいました。大会終了後、バックボーンネットワークがどれほどの帯域を出せるのかベンチマークを取りました。

実験環境

18:13ごろから18:23ごろまでOpenstackのVM 10台から、別のVM 10台までiperf3を実行しました。以下のような経路を通り、通信を行いました。

その後、18:28以降に同様の経路でOpenstackのVM 15台から、別のVM 15台までiperf3を実行していました。

結果

わかりにくいですが、18:13ごろから18:23ごろまで 5 Gbps 、 18:28以降は 8 Gbps の流量が出ていることが右下のTraffic of servertransmit traffics of servers のグラフからわかります。このグラフは、Openstackのホストサーバの受信と送信の流量を示しています。

タイトルが消えてしまっていますが右上のグラフがNAVT 2台の流量合計です。このグラフからも 8 Gbps まで出ていることがわかります。

真ん中の Traffics between Nexus5548 and QFX5100 のグラフはNexus5548とQFX5100の間の4本の流量をそれぞれ示しており、上半分が送信、下半分が受信になっています。このグラフだけ単位が Bytes per sec になっていることに注意してください。

考察

楽しかった!!

LACPのハッシュアルゴリズムの偏りによって、 1 Gbps * 2 を出せるサーバが確率的に 1 Gbps しか出てないことがあり、15台のVM同士の通信では 8 Gbps までしか実験を行うことができませんでした。右下の Traffic of servertransmit traffics of servers のグラフからOpenstackのVM同士の合計トラフィックが 8 Gbps しか出てないことがわかります。今回のバックボーンのキャパシティは 20 Gbps であったため、今回のベンチマークはバックボーンのベンチマークというよりサーバのベンチマークになってしまいました。特に左下の CPU idle のグラフでは各ホストのcpu0の idle が大幅に下回っており、サーバで 10 Gbps を処理をする際割り込みが多く発生することが大変であるという意味がわかりました。

Traffics between Nexus5548 and QFX5100 のグラフから Nexus5548 <==> QFX5100 間の通信のロードバランスは非常に美しく4つのインターフェイスに対して等コストバランシングができていることがわかりました。 Nexus5548 ==> QFX5100Nexus5548 <== QFX5100 両方向で等コストバランシングされていることから両方の機種がよい性能を持っていることがわかりました。

NAVTは 10 Gbps * 2 のNICを刺したサーバの2台構成で 8 Gbps は余裕であることがわかりました。 :clap:

まとめ

バックボーン担当は以上の通り様々な技術的挑戦をしながら参加者に快適に問題を解いてもらうことを目標にバックボーンを構築しています。興味を持った方は是非これから続く記事も読んでいただき、わからないところを自分で調べることで知識を深めていただければと思います。

また、機材提供をしていただける皆様!!面白い機材、お待ちしております!!

改めて、参加者・関係者の皆様ありがとうございました。次回も是非参加をよろしくお願いいたします!!

 /
カテゴリー

問題文

仮想機密伝送路課の検証環境ラボ(以下、ラボという)のWANルータ(Router1)をリプレースすることになった。

リプレースの要件としては基本的にリプレース前のネットワーク機器から設定をコンバートするだけなのだが、
今回新たな要件として本番環境で動いているwebサーバにラボから接続出来るようにしてほしい言われた。

ただ本番環境とラボは同じセグメントを使用しており、そして擬似的にwebサーバをラボにあるように見せる様に複数回NATをして対処してくれと要望があった。
またラボのクライアントからwebに繋げるipアドレスはRouter1のNATで192.168.14.200のアドレスを利用してwebページを見れるようにしてくれとも言われている。

そのためラボにおいてある別のルータ(Router2)から本番環境のWANルータ(Router3)にプロバイダーサービスのl2vpnを利用してRouter2とRouter3を繋げ、
3段階NATをすることによってwebサーバを見れるようにNATの設定を仮想機密伝送路課の担当者が行った。

しかしNATの設定後、ラボのPCからWebサーバ(192.168.14.200)に対しての通信で以下の現象が発生した。
– ラボのPCからwebサーバ(192.168.14.200)に向かってpingを打つと返ってくる
– Webページを見ることが出来ない

担当者「192.168.14.200に対してpingが返ってくるんだからwebページが見れないのはサーバ屋の問題だ!」

諸君にはRouter2と、webサーバに接続してトラブルシュートをし原因を突き止めてwebページが見れるようにして欲しい。
今回動いているwebサーバは公開用のipアドレスとは別にマネージメントのipアドレスがあるのでそこからsshすることができる。

制約

  • 設定を変更できるのはwebサーバとRouter2(1841)のみである
  • Router2(1841)のデフォルトルートを変えてはいけない

スタート

webサーバ(natip 192.168.14.200)に対してpingは通るのにwebページが見れない。

ゴール

webサーバ(natip 192.168.14.200)にpingも通り、またapacheのテストページを見ることが出来る。

情報

  • webサーバマネージメントssh情報
  • 192.168.14.200はweb公開用なのでicmpとhttpしか許可をしていない
  • 手元のPCは2960-Bの4番ポートに接続してください。dhcpが振ってきます。
  • Router2のアクセス情報
  • Router1は ip nat outside source static 192.168.14.70 192.168.14.200の設定が入っている。
  • Router3は ip nat inside source static 192.168.14.70 192.168.30.130の設定が入っている
  • インターフェースのinside,outsideは図に書いています。
  • 問題文に プロバイダーサービスのl2vpnを利用して とありますが今回は直接結線をしています。
  • この問題にvrfは関係ありません

トラブルの概要

pingが通るのにwebが見れないということは一見サーバ側の問題かなと思うかもしれませんが、その割には手元機材の情報があったり、webサーバは特に問題が無いように見えます。
この問題実はpingを返しているのはWebサーバではなく、NATルータです。

解説

この問題ではサーバ側の不備は一切なくRouter2が原因です。
cisco機器で内部と外部を同セグに見せるようなNATをする場合にありがちな設定ミスです。
NATの設定に不備があるため、NATされたアドレスがルーティングテーブルに乗らずRouter2がNATのアドレスを持ってしまいRouter2がpingの応答をしてしまっています。
なのでNATのルーティングを正しく書けば解決します。

解答例

Router2

- ip route 192.168.14.130 255.255.255.255 FastEthernet0/1.1647
+ ip nat outside source static 192.168.14.130 192.168.14.70 add-route
+ ip route 192.168.14.130 255.255.255.255 192.168.14.131

採点基準

NATに問題があることの指摘、及び設定変更してwebが見れる→満点

講評

この問題が解放されたのが遅かった為解答したチームはありませんでした。
pingが届くのにwebが見れないと聞いたらみなさんはとりあえずサーバに問題があるのでは?と考え問題の無いサーバ側を調べていたかと思われます。
複雑にNATを使用した場合(例えば同セグに見せるようなNAT)ping返答しても設定ミスでルータが返してあることがたまにあるのでみなさん気を付けてください。

最近のコメント