ヘキサゴナルアーキテクチャとレイヤードアーキテクチャの違いとマイクロサービス

ヘキサゴナルアーキテクチャレイヤードアーキテクチャのざっくりとした違いとヘキサゴナルアーキテクチャの利点を説明する。

ヘキサゴナルアーキテクチャは主に六角形で描かれる、多角形の全辺が外部とのインターフェイスであり、内部にさらにアプリケーションレイヤーやドメインコンテキストといった1つ以上のレイヤーを持つアーキテクチャである。 層構造はレイヤードアーキテクチャと同じであり、要はレイヤードアーキテクチャを最下層を軸に一回転させて全方位に対して層構造の性格が変わらないようにしただけである。

ヘキサゴナルアーキテクチャは実装においてはレイヤードアーキテクチャと同じだが、設計を行う上でいくつかの認知的効果がある。

複数インターフェイスを表現しやすい

レイヤードアーキテクチャではインターフェイスは直線または長方形1つのインターフェイスレイヤーで表現されるため複数インターフェイスを定義するにはこの直線や長方形を細切れに分割していかなければならず複数インターフェイスを表現しにくかった。

ヘキサゴナルアーキテクチャでは各面によって異なるインターフェイスを表現できるため複数インターフェイスを設計しやすくインターフェイス複数持つ設計に積極的になれる。

インターフェイスビジネスロジックの分離が促進される

レイヤードアーキテクチャではインターフェイスレイヤーにビジネスロジックなど他のレイヤーの責務が混入するしやすく異なるインターフェイスを追加する障害となっていた。

ヘキサゴナルアーキテクチャは全周囲からアクセスが行われることが視覚的に明白であるためサービスがどのインターフェイスにおいても等しく機能しなければならないという設計要件を効果的に認識させることができる。開発者は常にいずれかひとつの面だけでなくすべての面からの入力に対して等しく機能する内部レイヤーを作らなければならないというイメージを持って開発することになるため抽象化が促進されインターフェイスレイヤーが適切に構築されるようになる。

なお内部レイヤーについてはインターフェイスと同じく六角形で表現する例がよくあるが円形で表現することを勧める。内部レイヤーは基本的にすべてのインターフェイスに対して同じ共通のAPIを提供すべきであり、インターフェイスごとにAPIを変える設計というのはナンセンスだからである。

複数サービス間の連携を表現しやすい

レイヤードアーキテクチャは上下の方向の概念があるため複数アーキテクチャを連携のためにそのまま並べようとすると上下へアクセスの端点を接続できる配置を意識しなければならない煩雑さがある。これはサービス同士の関係がピラミッド型の厳格な階層構造である場合は都合がいいがネットワーク型である場合は向いていない。

ヘキサゴナルアーキテクチャは全周囲にアクセスの端点を持てるためネットワーク状のサービス連携を表現しやすい。

なお通信を行うレイヤーがインターフェイスであるかドメインまたはインフラであるかの差異を多角形の上下または左右で表現する例がよくあるが端点を外周のインターフェイスに持つか内部レイヤーに持つかで表現することを勧める。このような図は互いの位置関係を気にすることなく依存関係を表現することができるからである。たとえばサービスAの内部レイヤーとサービスBのインターフェイスが線で結ばれているならば位置関係に関係なくAはBに依存している。

マイクロサービスと相性がいい

上記のような複数サービス間の連携の表現しやすさはマイクロサービスの設計と相性がよい。マイクロサービスの連携を設計するときは見た目だけでもヘキサゴナルアーキテクチャで表したほうがいいだろう。実体は後から実装すればよい。

まとめ

要はレイヤードアーキテクチャを正しく実装するためのより正確で効果的な視覚表現がヘキサゴナルアーキテクチャである。レイヤードアーキテクチャと比べて特に変わったことをしているわけではない。

TypeScriptで形式的証明・交差型編 ~ 状態付きの型による高信頼領域の構築

TypeScriptで交差型を使って静的型に状態的な型を動的に付与していきます。 これによりサニタイズ済/未サニタイズエンコード済/未エンコード、不変/可変、通常文脈/エラー文脈、Truthy/Falsyといった状態を複合的に扱えるようになります。 さっそく試してみましょう。

この記事はTypeScript アドベントカレンダー2015 2日目の記事です。

サニタイズ済みである文字列のみを受け取る型を作ってみます。

declare class Sanitized {
    private id;
}
function sanitize<T>(data: T): Sanitized&T {
    return <Sanitized&T>data; // any processing
}

var plain = '';
var secure: Sanitized&string = plain; // type error
var secure: Sanitized&string = sanitize(plain); // ok

たったこれだけです。たったこれだけで信頼境界の内側と外側を分離できるのです。もはやただの文字列は安全な文字列を要求する型に入れることができなくなりました。

見た目が通常の型宣言とよく似ているためアノテーションとしても利用できます。それでいて、この手法は型のみで実現され実装に影響を及ぼさないためまったく何のオーバーヘッドもありません。

信頼境界を越えるときに安全な文字列に変換し、境界内では安全な文字列だけ使うように制限すればプログラムが確実に安全なデータだけを使っていることが保証されるようになります。

苦労して経路を網羅しなくても受け取り側が型を見て不正なデータを拒否するので、境界内が要件を満たさない不正なデータで汚染されることはありません。

信頼境界内は常に安全なデータだけで満たされているクリーンな領域となります。

このような状態付きの型を定義する方法は他にもいくつかありますが、ここで紹介した交差型を使用した方法はその中でも汎用性が高く開発者の負担が小さいものです。今日からでも使い始められるでしょう。

上記のコードは最低限の機能しかないので、以下の条件を追加してもう少し実用的にしてみます。

  • 二重に状態を付与しようとした場合はvoidとなる。
export declare class Sanitized {
    private SANITIZED;
}

export function type<T>(target: Sanitized&T): void
export function type<T>(target: T): Sanitized&T
export function type<T>(target: T): Sanitized&T {
    return <Sanitized&T>target;
}

export function extract<T>(target: Sanitized&T): T {
    return <T>target;
}

プリミティブ型の次はデータ型にも対応できるようオブジェクトに状態を持たせてみましょう。 すいません動くコードロストして再実装できなくてロストテクノロジーになりました。以下のコード実装できた方いたら教えてください(泣)。 データ型については信頼できるデータのみを保持する信頼境界内用の公称型を用意して信頼性を確保してください。

declare class Sanitized {
    private id;
}
function sanitize<T>(data: T): Sanitized&T {
    return <Sanitized&T>data; // any processing
}

class Data<T> {
    constructor(public value: T) {
    }
    public sanitize(this: Data<Sanitized>): void
    public sanitize(): Data<Sanitized&T>
    public sanitize(): Data<Sanitized&T> {
        return new Data(sanitize(this.value));
    }
}

var plain = new Data('');
var secure: Data<Sanitized&string> = plain; // type error
var secure: Data<Sanitized&string> = plain.sanitize(); // ok
var secure: Data<Sanitized&string> = plain.sanitize().sanitize(); // type error

これまでは信頼境界内の信頼性を境界通過時の検査のみで担保・依存し、ひとたび境界内に入ればあとはフリーパスになって不正なデータが混入しても検出できないという状態に甘んじていたこともありましたが、型に状態を持てるようにすることで要求される状態を持たない値が境界内で誤って使われようとしても要件を満たせず拒否される信頼性の高い領域を構築できます。

これまで境界線という線で確保していた信頼性と安全性を境界内の空間全体というより高い次元で確保できるようになったのです。より応用的には個々のデータのうち特に信頼性を求める場合は専用に公称型のデータ型を用意することでさらに確実な操作を行い、それ以外のデータは状態の要件により信頼性を確保するといったアプローチが望ましいでしょう。

今回の内容は単純な状態遷移をモデルへ部分的に適用しただけなので形式的証明と呼べるほどのものではありませんでしたが*1、実装に高水準の信頼性を与える方法として形式的証明と同様非常に効果的なものです。

外部と通信を行うアプリケーションは規模の大小にかかわらずサニタイズと正規化を厳しく要求されるため本稿で紹介したように形式的に信頼性を保証する方法が非常に重要となってきますので、これを期にぜひ自身のプロダクトの信頼性と品質の向上に取り組んでみてください。

TypeScriptアドベントカレンダー2015、需要と余裕があれば次回は状態遷移モデルの形式的証明を行う並行処理および並列処理の形式的証明(状態遷移編)です。予定は未定。

*1:形式的証明は未証明部分で破綻していては意味がないので原則として証明対象の全機能を証明します

TypeScriptで形式的証明・直和型編 ~ じゃんけんゲーム最強トーナメント

TypeScriptで形式的証明を取り入れたプログラミングを実践します。

形式的証明とは、仕様またはモデルが数理論理学的に正しいものであり実装可能であることの形式手法による証明です。 本稿では形式手法のうち、型システムを利用した軽量形式手法による形式的証明を扱います。

この記事はTypeScript アドベントカレンダー2015 1日目の記事です。

TypeScriptアドベントカレンダー2015!!

f:id:falsandtru:20151130044653j:plain

去年あれだけいた戦士たちはどこへ行ったのか…

さて、形式的証明を実感するために1つのコードモジュールを作ってみましょう。

じゃんけんの結果をグー・チョキ・パーの3つの型を使って型推論だけで計算し、不正な入力を実行前に検出する、形式的妥当性の証明されたコードモジュールです。

まず前提となるグー・チョキ・パーの3つの型を作ります。

class Gu {
    private identity;
}
class Choki {
    private identity;
}
class Pa {
    private identity;
}

TypeScriptの型は構造的部分型だから型を区別できないと思っていましたか? TypeScriptには型を一意に識別する方法も用意されており、構造的部分型と公称的部分型の両方をサポートしています。

アクセシビリティ修飾子にprivateまたはprotectedを指定するとそのメンバーが他で定義された同名のメンバーに代入できない一意な識別子となります。

これによりTypeScriptでも公称型または公称的部分型である任意の代数的データ型とその直和型を作ることができるのです(数学的には共通部分のない型同士に限られますが実用上は気にする必要はないでしょう)。

また、TypeScriptのUnion typesは直和型そのものではありませんが直和型としての使い方もできるのでTypeScriptでも直和型の概念に基づいたプログラミングが可能です。

次にグーとチョキならグーが勝つといった論理(規則・仕様)を作ります。

interface Rule<T, U> {
  (a: T, b: U): T;
  (b: U, a: T): T;
}
interface Fight<T, U, V> extends Rule<T, U>, Rule<U, V>, Rule<V, T> {
}
var fight: Fight<Gu, Choki, Pa> = (a: Options, b: Options): any => void 0;
type Options = Gu|Choki|Pa;

最後にこれらの仕様の妥当性確認を行うバリデーションコードを書いて正しく機能しているか確認します。

<Gu>fight(new Gu, new Choki);
<Pa>fight(new Gu, new Pa);
...

これで完成です。さっそく試してみましょう。

var correctWinner: Gu = fight(new Gu, new Choki);
var incorrectWinner: Choki|Pa = fight(new Gu, new Choki); // type error

ちゃんと動いてますね。型だけで結果を得られるので問題によってはアルゴリズムを実装せずに正しい答えを得ることすら可能です。

var winner: Gu = fight(new Gu, new Choki) || new Gu;

型を見てから結果をハードコーディング余裕でした。テストでカンニングして途中計算を書かずに答えだけ書くようなものですね。

でもこれだけではつまらないのでもっとサイズを大きくしてみましょう。じゃんけんトーナメントを開催してみます。

グー・チョキ・パーの各インスタンスが個々の選手(の最初の手)となります。選手を区別したい場合はコンストラクタが初期化パラメータを受け取るようにして選手名などで選手を区別できるようにしましょう。このように実体を区別する必要があるときはさっきのようなずるはできません。

// 2回戦
fight(
    fight(new Gu, new Choki),
    fight(new Choki, new Pa)
); // Gu
// 3回戦
fight(
    fight(
        fight(new Gu, new Choki),
        fight(new Choki, new Pa)
    ),
    fight(
        fight(new Gu, new Pa),
        fight(new Choki, new Gu)
    )
); // Pa

問題なくトーナメントが成立することが証明されていますね。

とはいえ…

fight(
    fight(new Gu, new Pa),
    fight(new Pa, new Gu)
); // type error

Oops! エラーになりました。ドローになり勝ち負けが決まらない場合を考慮していなかったのです。

形式的証明を行っても非網羅的定義までフォローしてくれるとは限りません。Haskellなど一部のプログラミング言語は定義の網羅性を検証してくれますが状態遷移がすべての組み合わせにおいて破綻なく循環することまでは検証してくれません。形式的証明用の言語やツールであればおおむね網羅的な検証を実施する機能を備えていますが、通常のプログラミング言語の型システムを利用するだけではそうした本格的な検証までは行えないのです。あくまでソースコード中で具体的に示された組み合わせの中での検証となり、入力する型を誤った実装や実装不可能な仕様やモデルを作らないための検証です。また、いずれにしても根本的定義ミスには無力なので過信は禁物です。

しかし、トーナメントに欠陥があり成立しないことは事前に判明しました。それも実装アルゴリズムに関係なくテストケースの範囲外であってもです(形式的に正しいことを確認する妥当性確認は行いましたが、実装の正しさを検証する、通常のテストに該当する正当性検証は行っていません。なにしろ実装が空ですから。)。トーナメント中に発覚して興行を中止せざるをえなくなり、大ブーイングを浴びながら出資者からの法的責任追及と集団訴訟に怯えるよりよほどましです。これこそが形式手法と形式的証明の威力です。この欠陥の判明を受けてトーナメントを成立させるためにトーナメントが不成立となる組み合わせを認めないか、ドローの解決方法を定義するかは主催者しだいです。

以上のように、このケースでは形式的証明を導入したことで大きな成果を上げることができました。定義と運用の範囲内で動作可能であることが数理論理学的に証明され、逆に不可能である場合はそれを検出してコンパイルの段階で型エラーとして出力し、期待通り正しく機能したわけです。

宣言的で簡潔な型による抽象化された検証と証明は、アルゴリズムの実装可能性という尺度からソースコード上の仕様ないしモデルの評価を可能にし*1、手続き的になりやすく複雑な実装アルゴリズムの部分的テストよりも高水準な信頼性の指標となります*2

静的型付き言語は静的型により動的型付き言語よりも早い段階で実装の正しさを検査して品質と信頼性を高めてきましたが、形式的証明はさらに早く、実装前の段階で抽象化された仕様やモデルを検証しているのです。

このように形式的証明はコードの品質と信頼性の向上に大きく役立つものです。毎年新しいフレームワークを覚えては使い捨てるより形式手法を1つ覚えるほうがよほどにスキルとコード品質が向上します。形式的証明はこのさき10年でも20年でも生きつづける安定した技術分野です。みなさんもぜひ使ってみてください。

TypeScriptアドベントカレンダー2015、次回は交差型を使って型に状態を動的に付与する形式的証明(交差型編)です。お楽しみに。

本稿に掲載したコードは以下のリンク先で形式的動作を確認できます。

じゃんけんゲーム

*1:副作用だらけの言語でも型の世界では形式的証明に適した純粋関数プログラミングを実現できるのです。

*2:形式的証明は入出力の型以上の実装の正しさをテストしないので実装のテストも依然として必要です。

gitで流行のコミットメッセージとベーシックなコミットメッセージのフォーマットまとめ

最近気になってたchore(package): bump versionGoogleフォーマットとBump versionなベーシックフォーマットのまとめ。

Googleフォーマット

chore(package): bump version

Angular.jsなど主にJavaScriptのメジャーリポジトリで人気のフォーマット。

github.com

azuが概要をまとめているのでこれを引用する。

feat(ngInclude): add template url parameter to events

The `src` (i.e. the url of the template to load) is now provided to the
`$includeContentRequested`, `$includeContentLoaded` and `$includeContentError`
events.

Closes #8453
Closes #8454
                         scope        commit title

        commit type       /                /      
                \        |                |
                 feat(ngInclude): add template url parameter to events

        body ->  The 'src` (i.e. the url of the template to load) is now provided to the
                 `$includeContentRequested`, `$includeContentLoaded` and `$includeContentError`
                 events.

 referenced  ->  Closes #8453
 issues          Closes #8454

Karmaにキーワードの一覧などがまとまっている。

<type>(<scope>): <subject>

<body>

<footer>

Allowed <type> values:

  • feat (new feature for the user, not a new feature for build script)
  • fix (bug fix for the user, not a fix to a build script)
  • docs (changes to the documentation)
  • style (formatting, missing semi colons, etc; no production code change)
  • refactor (refactoring production code, eg. renaming a variable)
  • test (adding missing tests, refactoring tests; no production code change)
  • chore (updating grunt tasks etc; no production code change)

Example <scope> values:

  • init
  • runner
  • watcher
  • config
  • web-server
  • proxy
  • etc.

The <scope> can be empty (eg. if the change is a global or difficult to assign to a single component), in which case the parentheses are omitted. In smaller projects such as Karma plugins, the <scope> is empty.

あらかじめコミットメッセージ(タイトル)に使用するキーワードが絞り込まれててタイトルに悩まず手早くコミットしていけるのがよい。 スコープの絞り込みも便利。

github.com github.com

コミットログからチェンジログを生成するconventional-changelogをあわせて使うとさらに強力。

https://github.com/ajoslin/conventional-changelog/blob/master/conventions/angular.mdgithub.com

ベーシックフォーマット(Gitフォーマット)

Bump version

本家本元Gitの由緒正しいフォーマット。

github.com

フォーマットのルールがまとまったよい翻訳記事があるのでこれを引用する。

  1. タイトルの後は1行空けて本文を書く
  2. タイトルを50字以内におさめる
  3. タイトルの文頭を大文字にする
  4. タイトルの文末にピリオドを付けない
  5. タイトルは命令形で記述する
  6. 本文は1行あたり72字以内におさめる
  7. 本文ではどのようにではなく何をとなぜを説明する

ルールのいくつかはGoogleフォーマットにも当てはまる基礎基本。

postd.cc

自分が使っているフォーマット

chore: bump version

しばらく使ってみたところ変更のスコープは一意に特定することも一貫させることも困難であり公式のリポジトリでも省略することが多い。 このため基本的に定型部分は操作種別の分類・表明によりコミットの意図と影響範囲を明確にするのみにとどめ、具体的なスコープの絞り込みは行わないものとした。 ただしプロダクトが独立的な機能の集合である場合はコミットの文脈情報が不足するためその機能をスコープとして明示する。

なおバージョンアップのコミットメッセージは本稿の例ではバージョンナンバーを記述しないものを使用しているが、自分はLinuxとGitのリポジトリにならって明記している。

空間ナビゲーション vs リンクマップ

自分が思い描くところの理想に近い空間ナビゲーションを実装してひと段落ついた。

chrome.google.com

開発に着手する前、自分はVimiumのような網羅的なリンクマップより空間ナビゲーションのほうが直感的で使いやすいと思っていた。しかし実際に使ってみると理想的なナビゲーションとなるケースでなおリンクにたどり着くまでがもどかしい。

Googleの検索結果の3件目を開くキータイプを数えると以下のようになる。

s->s->s->Enterの4タイプ

または

(s->)e->xの2から3タイプ

前者は検索結果間にノイズとなるリンクがある場合さらにタイプ数が増える。サイトによってはこのノイズのせいで目的のリンクになかなかたどり着けない。これを解決するために後者のように補助的なリンクマップ機能をつけているが緊急回避的なものである。

後者は補助的かつ限定的なリンクマップなのでこれをメインに使用するくらいならタイプ数的にも最初からVimiumを使ったほうがいい。

対してVimiumはすべてのリンクが一律に

f->x(x)の2から3タイプ

である。意図しないリンクまで網羅される点が視覚的にわずらわしかったが空間ナビゲーションがこれを超える効率でリンクを選択できるページ上の範囲はごく一部だ。

よって空間ナビゲーションがリンクマップより優れたリンクオープナーとして使うには

  • 偶然空間ナビゲーションがメインのリンクから開始されるページであり
  • そのページとそこでの使い方をユーザーが記憶しておかなければならない

という偶然とマンパワーにたよった不合理な方法によらなければならない。

というのが、このあいだまでの状況だったのだがeeeとEEEコマンドを実装したところ使用感が格段に向上した。

画面内にタグリストのようなテキストリンク集がなければeかeeですべてのリンクを網羅できるしEで外周から攻めることもできるので普段見ている過半数のサイトでもどかしさを感じることがなくなった。

Vimiumがリンクを開くためにfのあと2つのキーを読んで打たなければならないのに対して何も考えずにeを1回から数回叩いて1キー押すだけなのは思考的負担がかなり軽くて使いやすい。

かくして網羅的リンクマップより使いやすい空間ナビゲーションが、部分的リンクマップの力により一応達成された。

Vivaldiのキーボードコマンドによる操作の手引きと評価

Vivaldi TP3

キーボードコマンド操作手引き

操作 推奨コマンド ブラウザ共通 Vivaldi Vimium
タブ開く t,Ctrl+T Ctrl+T - t
タブ閉じる x,Ctrl+W Ctrl+W - x
タブ復元 X,Ctrl+Z Ctrl+Shift+T Ctrl+Z X
次のタブ K,Ctrl+Tab Ctrl+Tab 1,4 K
前のタブ J,Ctrl+Shift+Tab Ctrl+Shift+Tab 2,3 J
新規ウィンドウ Ctrl+N Ctrl+N -
ウェブ検索 t,O,Ctrl+Q Ctrl+L Ctrl+Q O
タブ検索 T - Ctrl+Q T
履歴検索 O Ctrl+H Ctrl+Q O
ページ検索 /,Ctrl+F Ctrl+F - /
リンク選択 WASD,f - WASD f
テキストボックスフォーカス gi - - gi
フォーカス解除 Ctrl+[ - - Ctrl+[
スクロール j - - j
スクロール逆 k - - k
ページスクロール d Space - d
ページスクロール逆 u Shift+Space - u
戻る Z Alt+Left Z H
進む X Alt+Right X L
拡大 0 Ctrl+= 0
縮小 9 Ctrl+- 9
等倍 6 Ctrl+0 6
URLコピー yy - - yy
URLペースト移動 P - - P
ブックマーク Ctrl+D Ctrl+D -

ヒント

  • 空間ナビのWASDはVimiumをiキーでInsert modeにすればキーバインドが競合しない
  • Ctrl+[でフォーカス解除できる

欠けてるビルトインコマンド

  • スクロール
  • テキストボックス選択
  • フォーカス解除
  • クイックコマンドタブ絞込み
  • クイックコマンド履歴絞込み
  • クイックコマンドブクマ絞込み
  • クイックコマンドメモ絞込み
  • ネルフォーカス
  • パネル操作
  • タブスタック作成
  • タブスタック解除
  • 履歴画面表示

評価

差別化項目の多くが拡張で代替可能である製品としての欠陥がある。 拡張開発者がその気になればChromium上でVivaldiの操作上の利点のほとんどを再現できる。 さらに現状ではビルトインが拡張より劣っている。 ビルトインのほうが好ましいとはいえ少なくともビルトインが拡張の同等以上か拡張との相性がChromium以上でなければ使われない。

以下問題点

Vivaldiのウリのクイックコマンドだが実はタブを検索できる以外Ctrl+Lでアドレスバーから検索するのと変わらない

ウェブ検索がサジェストが使えないぶん完全なキーワード入力を要求されタイプ数が増えるので圧倒的に不便

タブ検索は組み込みとしてはVivaldi固有だがタブだけ絞り込めないので拡張のVimiumのほうが使いやすい

組み込みの履歴検索のショートカットが上書きされて使用不能

空間ナビゲーションが唯一の希望だけどこれも拡張で簡単に実装できるので危ういオリジナリティ

右クリックメニューに翻訳がない(鯖構築してると英語以外にロシア語やイスラエル語もぶつかることがある)

縦タブとパネル以外VivaldiにあってChromiumで実現不可能なブラウザ機能がない

ぶっちゃけ縦タブもタブ検索時に全件リストアップされれば常時表示されてなくてもいいのでキーボーダーには不要

結論

機能的には拡張作られるだけで吹き飛ばされかねない危ういブラウザ。 Vimiumに空間ナビを追加できるブラウザとしては悪くない。 まずVimiumありきで作って最終的にすべてビルトインでできるようにするロードマップがベスト。 クロスプラットフォームのメーラとしてもいい選択肢になるはず。 でもやっぱりサジェスト使えなくてタイプ数増えるのがブラウザコンセプト的にありえない。新規タブでGoogle開けばサジェスト使えるはずだが変なページ開いて検索できない(正式版で設定できるようになるれば問題ない)。 メジャーなChromium系ブラウザとしての役割も担えるのでコンセプトが無視されてもそこそこ使われて死にはしないと思う。

iptablesをフィルタを組み合わせてロールベースで設定管理する高機能スクリプト「iptables-firewall」

iptables-firewallはIPホワイトリスト、国別制限、Firewall、IDS/IPSなどの各種自動生成されるフィルタを用途ごとに自由に組み合わせてルールセットを作成し、任意のロールとして設定・運用することで厳格かつ柔軟なアクセス制御機構を実装するスクリプトです。

導入

GitHub - falsandtru/iptables-firewall: iptables rule generating and management script.

=======================
  PACKET FLOW EXAMPLE
=======================

== config ==
  ROLES=(SSH)
  SSH=(BLOCK_COUNTRY "file{1,2}|TRACK_PROWLER|DROP" LOCAL_COUNTRY FIERWALL IPF "IPS|LOG...|DROP")
  ...
  MAP=("${MAP[@]}" "INPUT -p tcp --dport 60022 -j SSH")
  MAP=("${MAP[@]}" "INPUT -j TRAP_PORTSCAN")
  MAP=("${MAP[@]}" "FORWARD -j TRAP_PORTSCAN")


== apply ==
            INTERNET
        ______ V ______________________________________________________    _______
INPUT  |               | TCP UDP ICMP                                  |  |       |
       |   TCP 60022   |       TRAP_PORTSCAN  ( --> TRACK_PROWLER  )  --->| POLICY|
       |               |                                               |  |       |
       |====== | ======|===============================================|  |_______|
       |====== V ======|===============================================|   _______
Layer1 |                                                               |  |       |
       |                          BLOCK_COUNTRY                       --->|       |
       |______________________________ V ______________________________|  |       |
Layer2 | Rule1         | Rule101       | Rule201       | Rule202       |  |       |
       |     file1    -->    file2    -->TRACK_PROWLER-->    DROP     --->|       |
       |______ V ______|______ V ______|______ V ______|_______________|  | BLOCK |
Layer3 |                                                               |  |       |
       |                          LOCAL_COUNTRY                       --->|       |
       |______________________________ V ______________________________|  |       |
Layer4 |                                                               |  |       |
       |                            FIERWALL  ( --> TRACK_ATTACKER )  --->|       |
       |______________________________ V ______________________________|  |       |
Layer5 |                                                               |  |       |
       |                              IPF  ( ANTI_PROWLER/ATTACKER )  --->|       |
       |______________ V ______________________________________________|  |       |
Layer6 |                               |               |               |  |       |
       |            IDS/IPS           -->     LOG     -->     DROP    --->|       |
       |                               |               |               |  |       |
       |============== | ==============================================|  |_______|
       |============== V ==============================================|
SERVICE|                                                               |
       |                      === SSH SERVICE ===                      |
       |_______________________________________________________________|

特徴

ロールベースコントロール

ロールにより適用するルールをまとめられるため、用途にあわせてルールを設定する現実の実務モデルに則した運用を手軽に実施できます。

初期設定ではGLOBAL/LOCAL/CONNECTION/SYSTEM/NETWORK/AUTH/PRIVATE/CUSTOMER/PUBLICロールが定義されています。

# TESTロールを作成
ROLES=(TEST)
...
......
# TESTロールを適用
MAP=("${MAP[@]}" "INPUT -p tcp --dport 8080 -j TEST")

マルチレイヤフィルタリング

ロールに設定するルールセットは、ホワイトリスト、国別制限、Firewall、IDS/IPSなどから選択した各種フィルタを組み合わせた順に適用していくマルチレイヤフィルタとして動作します。

# TESTロールにルールを設定
TEST=(whitelist/private LOCAL_COUNTRY FIREWALL IPF IPS ACCEPT)
# 1. whitelist/private
# ファイルに記述されたIPのみ通過させ、ほかは遮断する。
#
# 2. LOCAL_COUNTRY
# 許可した国のIPのみ通過させ、ほかは遮断する。
#
# 3. FIREWALL
# Firewallを適用し接続を検疫する。
#
# 4. IPF
# 攻撃行為または不審行為のあったIPを遮断する。
#
# 5. IPS
# 指定のパケットをIPSへ渡し処理を終える。
#
# 6. ACCEPT
# 渡されなかった残りのパケットをすべて許可し処理を終える。
#

機能

ホワイトリスト

IPを記載したファイルから記載されているIPのみを通過させるフィルタを生成し、ホワイトリストによる厳格なアクセス制限を行います。

# 1. whitelist/private
# ファイルに記載されたIPのみ通過させ、ほかは遮断する。
#

国別フィルタ(特定国禁止/日本国内限定等制限)

アクセス可能な国を日本国内のみ等特定の国のみに制限、および特定の国からの全アクセスを禁止遮断します。IPと国との対応は割り当ての実施組織である地域レジストリから自動的に取得し適用されます。

# 許可 日本
LOCAL_COUNTRY_CODE="JP"

# 拒否 中国|香港|マカオ|韓国|北朝鮮
BLOCK_COUNTRY_CODE="CN|HK|MO|KR|KP"
# 2. LOCAL_COUNTRY
# 許可した国のIPのみ通過させ、ほかは遮断する。。
#

Firewall

各種Firewall機能を利用できます。

# 3. FIREWALL
# Firewallを適用し接続を検疫する。
#

PortscanTrap

公開していないポートにアクセスを試みたIPをIPFにより遮断するIPとして追跡します。

# 4. IPF
# 攻撃行為または不審行為のあったIPを遮断する。
#

IDS/IPS

IDSまたはIPSを導入している場合、これに処理を引き渡します。

# 5. IPS
# 指定のパケットをIPSへ渡し処理を終える。
#
# 6. ACCEPT
# 渡されなかった残りのパケットをすべて許可し処理を終える。
#

運用例

iptables-firewallはサーバーを利用する際に要求されるセキュリティ機能の確保に寄与します。

ポートスキャンの追跡

iptables-firewallはさくらのVPSでテスト運用されていますが、このサーバーは管理者自身しか利用していないにもかかわらず、ユニキャストによるポートスキャンを約10分に1回、1日に100回以上受けています。

Nov 17 19:59:37 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=178.33.x.x DST=133.242.x.x LEN=44 TOS=0x00 PREC=0x00 TTL=48 ID=38796 PROTO=TCP SPT=80 DPT=28612 WINDOW=16384 RES=0x00 ACK SYN URGP=0 
Nov 17 20:00:01 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=104.192.x.x DST=133.242.x.x LEN=28 TOS=0x08 PREC=0x00 TTL=234 ID=16288 PROTO=UDP SPT=40790 DPT=53413 LEN=8 
Nov 17 20:07:08 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=114.38.x.x DST=133.242.x.x LEN=60 TOS=0x00 PREC=0x00 TTL=50 ID=23942 DF PROTO=TCP SPT=59270 DPT=23 WINDOW=5808 RES=0x00 SYN URGP=0 
Nov 17 20:07:11 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=114.38.x.x DST=133.242.x.x LEN=60 TOS=0x00 PREC=0x00 TTL=50 ID=23943 DF PROTO=TCP SPT=59270 DPT=23 WINDOW=5808 RES=0x00 SYN URGP=0 
Nov 17 20:07:17 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=114.38.x.x DST=133.242.x.x LEN=60 TOS=0x00 PREC=0x00 TTL=50 ID=23944 DF PROTO=TCP SPT=59270 DPT=23 WINDOW=5808 RES=0x00 SYN URGP=0 
Nov 17 20:08:33 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=31.148.x.x DST=133.242.x.x LEN=40 TOS=0x10 PREC=0x00 TTL=245 ID=54321 PROTO=TCP SPT=46530 DPT=9064 WINDOW=65535 RES=0x00 SYN URGP=0 
Nov 17 20:18:34 www kernel: [IPTABLES INVALID] : IN=eth0 OUT= MAC=xx SRC=66.150.x.x DST=133.242.x.x LEN=76 TOS=0x00 PREC=0x00 TTL=50 ID=12979 PROTO=ICMP TYPE=3 CODE=3 [SRC=133.242.x.x DST=66.150.x.x LEN=48 TOS=0x00 PREC=0x00 TTL=125 ID=27360 PROTO=UDP SPT=27005 DPT=27015 LEN=28 ] 
Nov 17 20:33:11 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=66.114.x.x DST=133.242.x.x LEN=60 TOS=0x00 PREC=0x00 TTL=50 ID=9599 DF PROTO=TCP SPT=3618 DPT=23 WINDOW=5840 RES=0x00 SYN URGP=0 
Nov 17 20:33:17 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=66.114.x.x DST=133.242.x.x LEN=60 TOS=0x00 PREC=0x00 TTL=50 ID=9600 DF PROTO=TCP SPT=3618 DPT=23 WINDOW=5840 RES=0x00 SYN URGP=0 
Nov 17 20:35:12 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=178.33.x.x DST=133.242.x.x LEN=44 TOS=0x00 PREC=0x00 TTL=48 ID=50103 PROTO=TCP SPT=80 DPT=25596 WINDOW=16384 RES=0x00 ACK SYN URGP=0 
Nov 17 20:41:09 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=5.196.x.x DST=133.242.x.x LEN=44 TOS=0x00 PREC=0x00 TTL=46 ID=30348 PROTO=TCP SPT=443 DPT=55323 WINDOW=16384 RES=0x00 ACK SYN URGP=0 
Nov 17 20:43:19 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=111.249.x.x DST=133.242.x.x LEN=40 TOS=0x00 PREC=0x00 TTL=111 ID=256 DF PROTO=TCP SPT=12200 DPT=21320 WINDOW=8192 RES=0x00 SYN URGP=0 
Nov 17 20:43:41 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=8.8.x.x DST=133.242.x.x LEN=44 TOS=0x00 PREC=0x00 TTL=39 ID=48886 PROTO=TCP SPT=53 DPT=51501 WINDOW=42900 RES=0x00 ACK SYN URGP=0 

ポートスキャンからクラッキング

このような一見穏やかに見えるポートスキャンであっても、ポートを発見すると途端に秒間数回から数十回の総当たり攻撃や辞書攻撃を行いサーバーへ侵入しようとしてくるため放置すると非常に危険です。過去に8080番ポートでサーバー監視ツールを使用していた際に攻撃を受けました。

ホワイトリストの作成が困難な要件でのセキュリティ対応

サービスを少ない資本で運営している場合、こうした攻撃を避けようとSSHポートなどの重要機能へ接続可能なIPを制限しようとしてもプロバイダレベルまででしかIPの範囲を絞り込めず、社員の外出先や自宅からの接続を許可したい場合はそれすらも困難となります。

攻撃の遮断およびポートスキャンからのポート隠蔽

このような要件においても、国別フィルタで接続を日本国内に限定するとともに、ポートスキャンや攻撃を行ったIPを遮断するファイアウォールのオプション機能であるFW_INTRUDERによりポートを隠蔽することができます。また、攻撃を受けた場合でも攻撃を検知して以降の攻撃とアクセスを遮断します(※いずれも0-1023番以外のポートである必要がある)。

# e.g.
# 公開していないポートへのポートスキャンといった不審行為や攻撃を検知してIPを追跡
Nov 17 20:43:19 www kernel: [IPTABLES PORTSCAN] : IN=eth0 OUT= MAC=xx SRC=111.249.x.x DST=133.242.x.x LEN=40 TOS=0x00 PREC=0x00 TTL=111 ID=256 DF PROTO=TCP SPT=12200 DPT=21320 WINDOW=8192 RES=0x00 SYN URGP=0 
# 追跡中の不審なIPからの重要機能へのアクセスを遮断して公開ポートを保護・隠蔽
Nov 17 20:43:19 www kernel: [IPTABLES INTRUDER] : IN=eth0 OUT= MAC=xx SRC=111.249.x.x DST=133.242.x.x LEN=40 TOS=0x00 PREC=0x00 TTL=111 ID=256 DF PROTO=TCP SPT=12200 DPT=10022 WINDOW=8192 RES=0x00 SYN URGP=0 

サーバーのネットワークセキュリティ向上に

以上のように、iptables-firewallは実用的なネットワークセキュリティの導入を容易にします。