VaultでDynamic Secretsを試してみる
目次
- 初めに
- 環境情報
- Dynamic Secretsについて
- AWS Secrets Engineの準備
- Roleを定義する
- IAM Userを作成する
- まとめ
1. 初めに
VaultではDynamic Secretsという機能がありますので、今回はこちらの機能について触れていきたいと思います。 今回もHashiCorp Learnにそって進めていき、補足情報などを備忘録として残していこうと思います。
Vault Curriculum - HashiCorp Learn
2. 環境情報
3. Dynamic Secretsについて
まずはDynamic Secretsがどういうものなのかについて説明したいと思います。通常のSecretはCLIなどで追加をしていました。
例えば kv
の場合ですと下記のようにしてSecretを追加します。
$ vault write kv/hello target=world
ただこうして追加されたSecretは他のユーザから見られてしまうので、セキュリティ的によくないですね。
そこでDynamic Secretsを使うことでこの問題が解決されます。
Dynamic Secretsは動的にSecretを作成する方法です。Secretを動的に作成し使用後すぐに消すことも可能ですし、TTLを指定することができます。また、動的に作成され作成したものは後から見ることができないのでセキュアに扱うことができます。
文字だけで説明してもDynamic Secretの良さが伝わらないので実際にコマンドと一緒に確認していきたいと思います。
HashiCorp LearnではAWS Secrets Engineを使っています。これはAWSへアクセスるためのクレデンシャルを作成してくれるものになります。 ここも詳しくは実際のコマンドを見ながら確認していきましょう。
4. AWS Secrets Engineの準備
まずは AWS Secrets Engineをenableにしましょう
$ vault secrets enable -path=aws aws Success! Enabled the aws secrets engine at: aws/ $ vault secrets list Path Type Accessor Description ---- ---- -------- ----------- aws/ aws aws_df70dbb8 n/a cubbyhole/ cubbyhole cubbyhole_18f07d22 per-token private secret storage identity/ identity identity_5170f551 identity store secret/ kv kv_2fd4ecf6 key/value secret storage sys/ system system_4a94a9f3 system endpoints used for control, policy and debugging
これでenableになってるのが確認できました。ちなみにDescriptionが n/a
になっているのはDescriptionを何も設定してないっということです。これはオプションで設定することができます。
$ vault secrets enable -path=aws -description="aws secrets engine" aws Success! Enabled the aws secrets engine at: aws/
AWS Secrets Engineは上記でも記述した通り、動的にAWSへのアクセス権限を作成します。
次にAWS Secrets Engineのconfig設定をしていきます。
$ vault write aws/config/root access_key=XXXXXXXXXXXXXXXXXXXX secret_key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX region=ap-northeast-1 Success! Data written to: aws/config/root
access_keyとsecret_keyはVaultからAWSへアクセスするときに使用するものになります。
権限は以下のものを利用しました。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "iam:DeleteAccessKey", "iam:AttachUserPolicy", "iam:DeleteUserPolicy", "iam:DeleteUser", "iam:ListUserPolicies", "iam:CreateUser", "iam:CreateAccessKey", "iam:RemoveUserFromGroup", "iam:ListGroupsForUser", "iam:PutUserPolicy", "iam:ListAttachedUserPolicies", "iam:DetachUserPolicy", "iam:ListAccessKeys" ], "Resource": "arn:aws:iam::*:user/*" } ] }
5. Roleを定義する
Roleを定義していきます。Roleの定義をVaultに保存していきます。 今回追加するRoleは以下のものになります。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt1426528957000", "Effect": "Allow", "Action": ["ec2:*"], "Resource": ["*"] } ] }
このRoleはすべてのEC2へすべてのAction権限があるPolicyになります。 ではこれをVaultに保存します。
$ vault write aws/roles/my-role \ credential_type=iam_user \ policy_document=-<<EOF { "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt1426528957000", "Effect": "Allow", "Action": [ "ec2:*" ], "Resource": [ "*" ] } ] } EOF $ vault read aws/roles/my-role Key Value --- ----- credential_type iam_user default_sts_ttl 0s max_sts_ttl 0s permissions_boundary_arn n/a policy_arns <nil> policy_document {"Version":"2012-10-17","Statement":[{"Sid":"Stmt1426528957000","Effect":"Allow","Action":["ec2:*"],"Resource":["*"]}]} role_arns <nil> user_path n/a
ここれで credential_type
に iam_user
を指定しました。
これはIAM Userを作成するRoleとして定義しており、Policyはインラインポリシーとして定義されます。
iam_user
以外にも下記のようなものがあります。
タイプ | 説明 |
---|---|
assumed_role | sts:AssumeRole |
federation_token | sts:GetFederationToken |
それぞれの細かい説明は省きます。詳しく知りたい方は下記リンクを参照してください。
AWS - Secrets Engines - Vault by HashiCorp
初めてのAssumeRole | Developers.IO
6. IAM Userを作成する
では先ほど作成したRoleでIAM Userを作成しようと思います。
IAM Userを作成するには aws/creds/[作成したRoleの名前]
パスを読むことで作成されます。上記で my-role
でRoleを作成したので今回は aws/creds/my-role
を読むことで作成されます。
$ vault read aws/creds/my-role Key Value --- ----- lease_id aws/creds/my-role/nDPEvmAlgRUBdNx7ChuRuhRU lease_duration 768h lease_renewable true access_key YYYYYYYYYYYYYYYYYYYY secret_key YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY security_token <nil>
実際にAWSコンソールの画面で見てみると新規にIAM Userが作成されているのが確認できます。また出力された そしてもう一度実行すると新規でIAM Userが作られて新しい出力を得ることができます。
また -format=json
を指定するだけで出力はJSON形式でも出力ができます。
$ vault read -format=json aws/creds/my-role { "request_id": "d73d780e-a5cc-e30f-df5d-ae7e72e832f3", "lease_id": "aws/creds/my-role/eNmGkM06f3yzv9ZgvZVUx6Uh", "lease_duration": 2764800, "renewable": true, "data": { "access_key": "ZZZZZZZZZZZZZZZZZZZZ", "secret_key": "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ", "security_token": null }, "warnings": null }
最後に削除処理です。IAM Userを削除するときはIAM Userを作成したときに出力された lease_id
引数にして削除します。
$ vault lease revoke aws/creds/my-role/nDPEvmAlgRUBdNx7ChuRuhRU All revocation operations queued successfully!
AWSコンソールからも削除されているのが確認できます。
7. まとめ
今回はAWS IAM Userを動的に作成し、access keyとsecret keyの取得までできるようになりました。
HashiCorp Learnをそって進めるだけでここまでできるので気になる方はぜひやってみてください。 AssumeRoleなどのほうが利便性も良さそうなので機会があるときにやってみようと思います。
Vault初めてみる : Secretの基礎
目次
- 初めに
- 環境情報
- vault serverの起動
- Secretの保存
- Secret Engineについて触れておく
- 終わりに
1. 初めに
Vaultの勉強でHashiCorp Learnをやってみました。その時にやった内容や補足で調べたことなどを備忘録として残していきたいと思います。
また、HashiCorp Learnのサイトは下記リンクから確認できます。コマンド等は本家のサイトとほぼ、同様になっています。
Vault Curriculum - HashiCorp Learn
このブログよりも細かい説明もHashiCorp Learnに記載されていますので、気になる方は合わせてご確認ください。
今回はVault事態の説明は省いていますので、気になる方は下記サイトなどを参考にしてください。
HashiCorp Vaultの基礎知識と導入 | Developers.IO
2. 環境情報
3. Vault Serverの起動
まずはVault Serverを起動させてみます。HashiCorp Learnでは dev modeでServerを起動させていますので、サンプルにそって実行させてみましょう。 Serverはセキュアなものではないので本番での利用は推奨されてませんので気を付けましょう。
$vault server -dev ==> Vault server configuration: Api Address: http://127.0.0.1:8200 Cgo: disabled Cluster Address: https://127.0.0.1:8201 Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled") Log Level: info Mlock: supported: true, enabled: false Recovery Mode: false Storage: inmem Version: Vault v1.3.0 Version Sha: 4083c36aa0630faafb7c04be62c4940299880bc9+CHANGES WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory and starts unsealed with a single unseal key. The root token is already authenticated to the CLI, so you can immediately begin using Vault. You may need to set the following environment variable: $ export VAULT_ADDR='http://127.0.0.1:8200' The unseal key and root token are displayed below in case you want to seal/unseal the Vault or re-authenticate. Unseal Key: UoL9YBIsRrIxBFbfO+cfpmEHyUyzL8DH57CXWm7aI4E= Root Token: s.VGnMIQk4ogkD2Yn42O4GT0Zr Development mode should NOT be used in production installations!
Serverが実行されるとフォワグラウンドで実行されます。ですので別のターミナルを使って続きをやっていきましょう。 ここでServerを起動したときに出力されたServerのエンドポイントとRoot Token情報新しく立ち上げたターミナルの環境変数として登録します。
export VAULT_ADDR='http://127.0.0.1:8200' export VAULT_DEV_ROOT_TOKEN_ID="s.VGnMIQk4ogkD2Yn42O4GT0Zr"
VAULT_ADDRは出力された文字内に環境変数の設定まできれいに記載されています。
VAULT_DEV_ROOT_TOKEN_IDは Root Token
の値になります。
環境変数を設定した後にステータスを確認してみると下記のような結果になります。
$ vault status Key Value --- ----- Seal Type shamir Initialized true Sealed false Total Shares 1 Threshold 1 Version 1.3.0 Cluster Name vault-cluster-9bdecbfc Cluster ID e619f604-2e79-b963-55e0-c3f73e1e860c HA Enabled false
これでCLIでVault Serverを操作する準備ができました。では次にSecretを保存してみましょう。
4. Secretの保存
まずSecretとはどういうものかというと、APIキーやユーザ/パスワードなどの機密情報のことです。
そしてこのSecretを管理するのがSecrets Engineになります。
HashiCorp Learnでは kv
っというSecrets Engineを使っていますので、まずはその挙動を見てみましょう。
$ vault kv put secret/hello foo=world Key Value --- ----- created_time 2019-12-22T08:38:13.005825481Z deletion_time n/a destroyed false version 1
ここでは foo=world
っというペアで secret/hello
っというパスに保存をしました。パスっというのは各Secrets Engineで独自のパスとプロパティを定義されています。
dev modeで起動しているServerでは kv
はデフォルトでenableになっており、パスは /secret
を使うようになっています。
ちなみに、 kv
はversion 1とversion 2があり、デフォルトでenableになっているのはversion 2のほうになります。
サンプルにそってもう一つ、ペアを登録してみましょう。
$ vault kv put secret/hello foo=world excited=yes Key Value --- ----- created_time 2019-12-22T08:47:51.195186928Z deletion_time n/a destroyed false version 2
では、登録したSecretを確認してみましょう。
$ vault kv get secret/hello ====== Metadata ====== Key Value --- ----- created_time 2019-12-22T08:47:51.195186928Z deletion_time n/a destroyed false version 2 ===== Data ===== Key Value --- ----- excited yes foo world
無事に登録されているのが確認できました。
ちなみに vault kv put
処理は追加ではなく、上書となるので注意が必要です。
kv
で保存されたSecretで特定のKeyのものだけを取り出すこともできます。
$ vault kv get -field=excited secret/hello yes
最後に、不要となったSecretを削除する処理は下記のようになります。
$ vault kv delete secret/hello Success! Data deleted (if it existed) at: secret/hello
ここでの delete
処理は secret/hello
の削除ではなく、 secret/hello
の中にあるSecretのみ削除となります。
ですので下記のように secret/hello
をgetしたときに情報が返ってきます。
$ vault kv get secret/hello ====== Metadata ====== Key Value --- ----- created_time 2019-12-22T10:24:36.600075751Z deletion_time 2019-12-22T10:32:10.159743478Z destroyed false version 2
5. Secret Engineについて触れておく
先ほどの secret/hello
はSecrets Engine独自のパスとプロパティっていう記載しました。
もう少し掘り下げると、「パスによってSecrets EngineがなにかをVaultへ指示している」っということになります。
リクエストのパスによってSecrets Engineが特定されるようになるので、各Secrets Engineによって独自のパスになるということです。
パスとSecrets Engineの対応を見る方法は下記のコマンドで確認できます。
$ vault secrets list Path Type Accessor Description ---- ---- -------- ----------- cubbyhole/ cubbyhole cubbyhole_18f07d22 per-token private secret storage identity/ identity identity_5170f551 identity store secret/ kv kv_2fd4ecf6 key/value secret storage sys/ system system_4a94a9f3 system endpoints used for control, policy and debugging
これで kv
のパスが secret/
になっているのが確認できます。
では kv
のSecrets Engineを別のパスで一つ追加してみたいと思います。
$ vault secrets enable -path=kv kv Success! Enabled the kv secrets engine at: kv/ $ vault secrets list Path Type Accessor Description ---- ---- -------- ----------- cubbyhole/ cubbyhole cubbyhole_18f07d22 per-token private secret storage identity/ identity identity_5170f551 identity store kv/ kv kv_30fe82bc n/a secret/ kv kv_2fd4ecf6 key/value secret storage sys/ system system_4a94a9f3 system endpoints used for control, policy and debugging
kv/
っというパスで kv
Secret Engineが追加されました。
ではSecretを追加していきたいと思います。
$ vault write kv/my-secret value="s3c(eT" Success! Data written to: kv/my-secret $ vault write kv/hello target=world Success! Data written to: kv/hello $ vault write kv/airplane type=boeing class=787 Success! Data written to: kv/airplane $ vault list kv Keys ---- airplane hello my-secret
最後にSecrets Engineを無効にします。
$ vault secrets disable kv/
6. 終わりに
HashiCorp Learnを参考にVault Serverを立ち上げてSecretの保存、Secrets Engineの概要について勉強してきました。 引き続きHashiCorp Learnを続けていきたいと思います。
Elixirで簡単なProxyサーバを自作した
どうもこんにちは。ここ最近Erlangを書いてたり、AWS、OpenStackで遊んでいましたが久々にElixirを書いています。 今回はElixirでProxyサーバを自作しているので、どのような実装になっているか備忘録的に書いていきます。
動作環境
全体的な流れ
Proxyサーバを実装するにあたって、全体的な流れは以下のようにしています。
- Supervisorにリクエストを待ち受けるworkerを複数件登録
- workerでリクエストを待ち受ける
- バックエンド側(HTTPサーバ)へのコネクションを確立させる
- フロントとバックエンド、それぞれのソケットで受け取ったパケットの送受信をする
- フロントかバックエンド側どちらかのソケットがクローズしたら、もう片方もクローズする
workerはSupervisorに登録していき、それぞれのworkerでパケットの処理をしていきます。 また、コネクションごとにworkerは一つとなり、コネクション数ごとにworkerが増えていく想定となっています。
Supervisorにリクエストを待ち受けるworkerを複数件登録
では、これから実装内容を見ていきます。リクエストを待ち受けるworkerを複数件事前に作成し、Supervisorに登録していきます。
def init(listen) do # backend hosts = [ %Exdeath.ProxyNode{ host: {192, 168, 33, 61}, port: 8010, } ] spawn(__MODULE__, :start_workers, [listen, hosts]) {:ok, { {:one_for_one, 6, 60}, []} } end def start_workers(listen, hosts) do for _ <- 1..100, do: start_worker(listen, hosts) end def start_worker(listen, hosts) do pid = make_ref() child_spec = %{ id: pid, start: {Exdeath.Proxy, :start_link, [listen, hosts, pid]}, restart: :temporary, shutdown: :brutal_kill, type: :worker } Supervisor.start_child(__MODULE__, child_spec) end
では簡単に、要点ごとに説明してきたいと思います。
spawn(__MODULE__, :start_workers, [listen, hosts])
まずは start_workers
を別プロセスとして実行します。
hsots
はバックエンド側になるサーバをリストとして登録しています。バックエンド側のサーバを複数件登録し、LB知るようにしようと思っていますが、
今回はLBの実装は省略して進めたいと思います。
def start_workers(listen, hosts) do for _ <- 1..100, do: start_worker(listen, hosts) end
start_workers
でSupervisorに登録する件数分、ループ(リスト内包表記)を回します。今回は100件登録されています。
def start_worker(listen, hosts) do pid = make_ref() child_spec = %{ id: pid, start: {Exdeath.Proxy, :start_link, [listen, hosts, pid]}, restart: :temporary, shutdown: :brutal_kill, type: :worker } Supervisor.start_child(__MODULE__, child_spec) end
実際のSupervisorへの登録処理です。Exdeath.Proxy
をworkerとして起動させています。
pidには make_ref()
で取得した値を入れ、それをworkerの名前として登録します(後で出てきます)。
ここで、listenも workerの起動時の引数として渡していますが、これは gen_tcp
で listen しているソケットになります。
workerでリクエストを待ち受ける
Supervisorで登録までできたので、ここから個別のworkerでの処理を見ていきましょう。
use GenServer def start_link(listen, hosts, pid) do GenServer.start_link(__MODULE__, [listen, hosts], name: {:global, pid}) end def init([listen, hosts]) do GenServer.cast(self(), :accept) {:ok, %{listen: listen, front: nil, back: nil, hosts: hosts}} end def handle_cast(:accept, %{listen: listen}=state) do with {:ok, socket} <- :gen_tcp.accept(listen) do :gen_tcp.controlling_process(socket, self()) {:noreply, %{state| front: socket}} else {:error, :eaddrinuse} -> {:stop, {:shutdown, :eaddrinuse}} {:error, reason} -> {:stop, {:shutdown, reason}} error -> {:stop, {:shutdown, error}} end end
workerはGenServerを利用しています。GenServerで実行するとパケットが送られてきたときに handle_info
のメソッドで処理できるので、このようにしています。
def start_link(listen, hosts, pid) do GenServer.start_link(__MODULE__, [listen, hosts], name: {:global, pid}) end
ここでGenServerとして実行し、リンクしています。ここで先ほどSupervisorで作ったpidをnameにセットします。
def init([listen, hosts]) do GenServer.cast(self(), :accept) {:ok, %{listen: listen, front: nil, back: nil, hosts: hosts}} end
次に初期化処理ですが、ここで GenServer.cast(self(), :accept)
を実行し、リクエストの待ち受けを非同期で行えるようにします。
def handle_cast(:accept, %{listen: listen}=state) do with {:ok, socket} <- :gen_tcp.accept(listen) do :gen_tcp.controlling_process(socket, self()) {:noreply, %{state| front: socket}} else {:error, :eaddrinuse} -> {:stop, {:shutdown, :eaddrinuse}} {:error, reason} -> {:stop, {:shutdown, reason}} error -> {:stop, {:shutdown, error}} end end
メインとなるリクエストを待ち受けてる状態となります。このworkerがリクエストを取得するとフロント側のソケットが開き、そのソケットからのメッセージをこのプロセス(self)が受け付けられるよう :gen_tcp.controlling_process(socket, self())
を実行します。
バックエンド側(HTTPサーバ)へのコネクションを確立させる
フロント側のコネクションを確立させたので、次はバックエンド側となります。
def handle_info({:tcp, socket, packet}, %{front: socket, back: nil, hosts: [host| _]}=state) do {:ok, backend_socket} = set_connect(host) end(backend_socket, packet) {:noreply, %{state| back: proxy_host}} end def set_connect(proxy) do with {:ok, conn} <- :gen_tcp.connect(proxy.host, proxy.port, [:binary, packet: 0]) do {:ok, %{proxy| conn: conn}} else error -> error end end def send(socket, packet) do :gen_tcp.send(socket, packet) end
バックエンドのコネクションの確立にはフロント側からパケットを受け取った時に行うようにしています。
def set_connect(proxy) do with {:ok, conn} <- :gen_tcp.connect(proxy.host, proxy.port, [:binary, packet: 0]) do {:ok, %{proxy| conn: conn}} else error -> error end end
ここでTCPのコネクション確立のためバックエンドのサーバへリクエストを送ります。
def send(proxy, packet) do :gen_tcp.send(proxy.conn, packet) end
先ほどのバックエンドとのコネクションで確立したソケットに対して、パケットを送信します。 これで、フロントから受け取ったパケットをバックエンドのサーバへ送信することができるようになりました。
フロントとバックエンド、それぞれのソケットで受け取ったパケットの送受信をする
最後にそれぞれのソケット(フロントとバックエンド)で受け取ったパケットを送信するようにします。
def handle_info({:tcp, socket, packet}, %{front: socket, back: back}=state) do send(back, packet) {:noreply, state} end def handle_info({:tcp, socket, packet}, %{front: front, back: %{conn: socket}}=state) do send(front, packet) {:noreply, state} end
ここではパターンマッチを使ってフロントから来たパケットなのか、バックエンドから来たパケットなのかを判断し、それぞれ別方向へパケットを送信します。
フロントかバックエンド側、どちらかのソケットがクローズしたらもう片方もクローズする
最後にソケットのクローズ処理です。
def handle_info({:tcp_closed, socket}, %{front: nil, back: socket}=state) do {:stop, :shutdown, state} end def handle_info({:tcp_closed, socket}, %{front: front, back: %{conn: socket}}=state) do :gen_tcp.close(front) {:stop, :shutdown, state} end
これも、パケットの送信同様、パターンマッチでどっちのソケットがクローズしたかを判断し、もう片方のソケットをクローズするようにします。
まとめ
Proxy サーバはErlangでも実装したことがあり、その実装を参考にElixirでも実装してみました。
Erlangで簡易的なTCP Proxyを作った - ネットワークプログラマを目指して
Proxyを使えばいろいろと応用的な実装もできそうなので次はそのことについてまとめていきたいと思います。
ErlangでHTTPリクエストをパースする
どうもこんにちは。前回に引き続きErlangでProxyを作っています。HTTP ProxyでURIごとにルーティングをさせる。さらにProxyを経由してHTTPリクエストをするので「X-Forwarded-For」をHeaderに追加させる必要があります。そのためHTTPリクエストの簡単なパース処理を実装しました。その内容をまとめていきます。
前回の内容
動作環境
Erlangで文字列処理
まずはErlangでの文字列処理について。そもそもErlangでの文字列は他の言語の文字とは少し違い、リスト、バイナリになります。Erlangで文字列を表現するときは以下のようになります。
> [97, 98, 99]. "abc" > is_list("abc"). true > is_binary("abc"). false > is_binary(<<"abc">>). true
is_list("abc").
の結果がtrue
っということは"abc"
はリストになります。つまり文字列はリストで表現されてます。
そしてもう一つの表現方法としてバイナリ文字列として文字列を表現することもできます。
Erlangでのリストとバイナリ、2つの文字列の違いはメモリ空間の使い方が大きく違います。リストは単方向リストとなりますがバイナリ文字列は一つの塊としてメモリに保存されます。
HTTPリクエストのフォーマット
HTTPのパケットは基本的に文字列になります。ヘッダー情報もすべて文字列となるので文字列を操作する必要があります。 まずはHTTPリクエストの内容を見てみましょう。
<<"GET /hello?hoe=test HTTP/1.1\r\nHost: 127.0.0.1:5555\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n">>
これはErlangでキャプチャしたものになります。バイナリ文字としてパケットを取得しています。各要素について見ていきましょう。
GET
はHTTPリクエストのメソッドですね。
/hello?hoe=test
っというURIでリクエストが来ています。
HTTP/1.1
はHTTPのバージョンになります。
そして残りの部分がHeader部分になります。 これをErlangでパースします。
パース処理
全体的なパース処理を見てみましょう。
parse(Buffer) -> {ok, Method, Res0} = parse_method(Buffer, <<>>), {ok, Uri, Res1} = parse_uri(Res0, <<>>), {ok, Ver, Res2} = parse_version(Res1), {ok, Header} = parse_header(Res2, []), #{method => Method, uri => Uri, version => Ver, header => Header}.
こんな感じでパース処理を実装しました。それぞれのパース処理は以下のようになります。
parse_method
: メソッド部分のパース
parse_uri
: URI部分のパース
parse_version
: バージョン部分のパース
parse_header
: ヘッダー部分のパース
そしてすべての結果をマップとして返します。 特に実装は複雑にはしてなく、エラー処理等は省いています。
メソッド部分
まずは
{ok, Method, Res0} = parse_method(Buffer, <<>>),
の部分を見ていきます。
parse_method(<<?BLANK, Res/binary>>, Method) when ?IS_HTTP_METHOD(Method) -> {ok, Method, Res}; parse_method(<<C, Res/binary>>, Method) -> parse_method(Res, <<Method/binary, C>>).
これだけで実装しました。
?BLANK
はdefineで空白文字を定義しておきます。
-define(BLANK, $\s).
$\s
はアスキーコードで32を表してます。アスキーコードの32は空白文字になるので空白文字が出るまで第二引数のバイナリに先頭の文字を追加していきます。
そして、空白文字が出たときに第二引数の文字、つまりメソッドがちゃんとHTTPのメソッドなのか IS_HTTP_METHOD
で確認します。
-define(GET, <<"GET">>). -define(POST, <<"POST">>). -define(PUT, <<"PUT">>). -define(DELETE, <<"DELETE">>). -define(IS_HTTP_METHOD(H), (H =:= ?GET) or (H =:= ?POST) or (H =:= ?PUT) or (H =:= ?DELETE)).
IS_HTTP_METHOD
を define
で定義し、ガードの中で使っています。
これでメソッドが正しいかどうかチェックをしています。
URIのパース処理
先程のメソッドと同じようにURIもパースします。
parse_uri(<<?BLANK, Res/binary>>, Uri) -> {ok, Uri, Res}; parse_uri(<<C, Res/binary>>, Uri) -> parse_uri(Res, <<Uri/binary, C>>).
バージョンのパース処理
バージョンだけは長さが固定長になっているのでもっと楽に取り出しましょう。
parse_version(<<"HTTP/1.0\r\n", Res/binary>>) -> {ok, http_1_0, Res}; parse_version(<<"HTTP/1.1\r\n", Res/binary>>) -> {ok, http_1_1, Res}; parse_version(<<"HTTP/2.0\r\n", Res/binary>>) -> {ok, http_2_0, Res}; parse_version(Res) -> {error, not_match_http_version, Res}.
そのままパターンマッチで処理をします。 それぞれアトムを定義して、後ほどこれをいい感じに処理をできるようにします。
ヘッダーのパース処理
今回で一番めんどくさいヘッダーのパース処理です。 では、ヘッダーのパース処理を見ていきましょう。
parse_header(<<$\r, $\n, $\r, $\n>>, Headers) -> {ok, Headers}; parse_header(<<$\r, $\n, _/binary>>, Headers) -> {ok, Headers}; parse_header(<<Res0/binary>>, Headers) -> {ok, Header, Res} = parse_header_field(Res0, <<>>), parse_header(Res, [Header| Headers]). parse_header_field(<<>>, _) -> {error, not_header_field}; parse_header_field(<<$:, Res0/binary>>, Field) -> {ok, Value, Res} = parse_header_value(Res0, <<>>), {ok, {Field, string:trim(Value)}, Res}; parse_header_field(<<C, Res/binary>>, Field) -> parse_header_field(Res, <<Field/binary, C>>). parse_header_value(<<>>, _) -> {error, not_header_value}; parse_header_value(<<$\r, $\n, Res/binary>>, Value) -> {ok, Value, Res}; parse_header_value(<<C, Res/binary>>, Value) -> parse_header_value(Res, <<Value/binary, C>>).
全体のヘッダーのパースをするparse_header
と各ヘッダー項目をパースするためフィールドと値を取り出す関数を別々にして実装しています。
parse_header_field
でヘッダーのフィールド部分を取り出し、
parse_header_value
でフィールドの値を取り出します。
最終的なパースした結果はこうなります。
#{header => [{<<"Accept">>,<<"*/*">>}, {<<"User-Agent">>,<<"curl/7.58.0">>}, {<<"Host">>,<<"127.0.0.1:5555">>}], method => <<"GET">>,uri => <<"/hello?hoe=test">>,version => http_1_1}
ヘッダー部分もフィールドと値でタプルの配列として取れましたし、メソッド、URI、バージョンも無事にマップとして取り出せました。
まとめ
Erlangで簡易的なTCP Proxyを作った
どうもこんにちは。久々に記事書きます。 サボりにサボってやく一年ぶりぐらい(正確には11ヶ月ぶりぐらい)でしょうか。「ちゃんと更新しないとなぁ」っと思いつつこれぐらいに月日が立ってしまったのでこれからは真面目に更新しようと思います!
ErlangでのProxy
では早速本題に入っていきたいと思います。最近趣味でElixir、Erlangを触ることが多く、よく書いてます。そこでErlangでHTTPのProxyを作りたいと思い作り始めたのでました。まずは簡易的なTCPのProxyを作ったのでまとめたいと思います。
動作環境
実装内容
rebar3でアプリケーションの作成
まずはrebar3
でアプリケーションを作成します。
rebar3 new app sample_proxy
作成したら、まずはメインとなるProxyのプロセスを作成します。
Proxyプロセスの初期化
まずはProxyプロセスの初期化処理を行っていきます。
start_link(Receive, Destination) -> init([Receive, Destination]). init([Receive, Destination]) -> {ok, ListenSocket} = gen_tcp:listen(Receive, [binary, {active, false}, {reuseaddr, true}, {backlog, 64}] ), spawn_link(?MODULE, accept, [ListenSocket, Destination]), {ok, []}.
ここで、TCPサーバで待ち受けポートでlistenします。そしてここで新しくプロセスを作成します。 (start_linkを中継している理由はgen_serverとかとインターフェイスを揃えるためです) Receiveにはフロント側となるクライアントからのリクエストを待ち受けるポートを指定します。Destinationには転送先となるサーバ情報を渡します。
TCPリクエストのaccept
では新しく生成したプロセス内を見ていきます。
accept(ListenSocket, Destination) -> case gen_tcp:accept(ListenSocket) of {ok, Socket} -> BackId = spawn_link(?MODULE, proxy_accept, [Destination, self()]), loop(Socket, BackId); {error, closed} -> ok; {error, Reson} -> io:format("fail accept ~p~n", [Reson]) end, spawn(?MODULE, accept, [ListenSocket, Destination]).
gen_tcp:accept
でListenSocketでlistenしているポートに対してアクセスが来るまで待機します。ここでリクエストが来るとcaseブロック内の処理が実行されます。
{ok, Socket}となると正しいリクエストとなり、転送先のサーバへ接続をするプロセスを生成します。
その後はループ処理。実際のメイン処理となります。
転送先のサーバと接続するプロセス
転送先へ接続するためのプロセスをaccept後に生成しているのでその中身を見ていきたいと思います。
proxy_accept([{host, Host}, {port, Port}], FrontId) -> {ok, Con} = gen_tcp:connect(Host, Port, [binary, {packet, 0}]), loop(Con, FrontId).
転送先のホストとポートを指定し、gen_tcp:connect
で接続を行います。
その後に、接続したコネクション情報とacceptしたプロセスIDをもとにループ処理へ入ります。
リクエストを待受、送信する
ではここからがメイン、loop
関数の中を見ていきたいと思います。
loop(Socket, SendPid) -> inet:setopts(Socket, [{active, once}]), receive {tcp, Socket, Message} -> SendPid ! {recv, Message}, loop(Socket, SendPid); {tcp_closed, Socket} -> gen_tcp:close(Socket); {tcp_error, Socket, Reson} -> io:format("Handle error ~p on ~p~n", [Reson, Socket]); {recv, Message} -> gen_tcp:send(Socket, Message), loop(Socket, SendPid) end.
第一引数に待ち受けるソケット情報、第二引数に転送先となるプロセスIDを渡します。 転送先となるプロセスIDはクライアント側の場合は転送のサーバプロセスID、転送先のサーバプロセスの場合はクライアント側のプロセスID。これで相互にメッセージのやり取りするIDがわかるということです。
receive
でメッセージが来るまで待ち受けます。
{tcp, Socket, Message}
で来た場合はこちらが予測していたメッセージなのでそのまま転送先のプロセスにメッセージを送ります。
{recv, Message}
で上記のメッセージを受け取ります。これはクライアントプロセスだろうが転送先サーバのプロセスだろうが同じことですね。ここで受け取ったメッセージをgen_tcp:send
でお互いの接続している先へメッセージを送信します。
リザルト
では実際に試してみたいと思います。まずはProxyの起動から
> sample_proxy:start_link(5555, [{host, {127, 0, 0, 1}}, {port, 8010}]),
5555ポートで待ち受け、転送先は127.0.0.1の8010ポートへ転送します。 次に2つのターミナルを立ち上げ、片方は転送先のサーバとしてHTTPサーバとして起動させます。 もう片方はクライアントとして使います。 転送先のサーバは「Hello \nWorld \nCunked!」を返します。 ではやってみましょう。
$ curl http://127.0.0.1:5555 Hello World Cunked!
はい!無事に通りました。
最後に
これぐらいならサクサクっと作れましたし、TCPの中継だけなのでそこまで手間はかからなかったです。ただエラー処理やコネクション処理などもろもろしてないのでそこら辺はおいおい実装していきます。そして何よりHTTPのProxyを作りたいのでこれからURLベースでの転送、複数台の転送サーバを持ったときの挙動などを実装していきたいと思います。
golangでUDPを使ってみた
環境情報
go version : 1.8
os : kubuntu 16
やったこと
UDPサーバの実装
UDPはステートレスなプロトコルなので、セッション情報を持たず一方的に送り付けるプロトコルです。 なのでTCPと比べて比較的に実装は楽ですね。(まぁプログラムのコード量自体はそんなに大差ないですが)
UDPで使う主要なメソッド
func net.ResolveUDPAddr(net, addr string) (*UDPAddr, error) func net.ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, error) func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, error) func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, error)
net.ResolveUDPAddrはTCPと一緒で名前解決してくれるメソッドです。サーバサイドではローカルのIPを指定して待ち受けるポートを指定します。
net.ListenUDPで実際にパケットが来るのを待ち受け状態になります。
ReadFromUDPでパケットが来た場合に内容を読み込み、WriteToUDPで実際にパケットを送ります。
例:
func main() { udpAddr, err := net.ResolveUDPAddr("udp", ":1201") if err != nil { fmt.Fprintf(so.Stderr, "Fatal Error ", err.Error()) os.Exit(1) } conn, err := net.ListenUDP("udp", udpAddr) if err != nil { fmt.Fprintf(so.Stderr, "Fatal Error ", err.Error()) os.Exit(1) } for { handleClient(conn) } } func handleClient(conn *net.UDPConn) { var buf [512]byte _, addr, err := conn.ReadFromUDP(buf[0:]) if err != nil { fmt.Fprintf(so.Stderr, "Fataaaaal Error ", err.Error()) os.Exit(1) } daytime := time.Now().String() conn.WirteToUDP([]byte(daytime), addr) }
UDPクライアントの実装
クライアントはほとんどサーバと一緒です。違いはクライアント側でリッスンせずに、パケットを送り付けるぐらいですかね。
func net.ResolveUDPAddr(net, addr string) (*UDPAddr, error) func net.DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error)
ResolveUDPAddrはサーバと一緒でUDPで送るIPとポートでマッピングを行います。ただサーバと違うのは送りたい先のIPとポートを指定します
DialUDPで実際にパケットを送る先のサーバを設定します
例:
func main() { udpAddr, err := net.ResolveUDPAddr("udp", "localhost:1201") if err != nil { fmt.Fprintf(so.Stderr, "Fataaaaal Error ", err.Error()) os.Exit(1) } conn, err != net.DialUDP("udp", nil, udpAddr) if err != nil { fmt.Fprintf(so.Stderr, "Fataaaaal Error ", err.Error()) os.Exit(1) } _, err = conn.Write([]byte("anything") if err != nil { fmt.Fprintf(so.Stderr, "Fataaaaal Error ", err.Error()) os.Exit(1) } var buf [512]byte n, err := conn.Read(buf[0:]) if err != nil { fmt.Fprintf(so.Stderr, "Fataaaaal Error ", err.Error()) os.Exit(1) } fmt.Println(string(buf[0:n]) }
以上、UDPのサーバとクライアントの実装でした!
golangでTCPを使ってみた
環境情報
go version : 1.8
os : kubuntu 16
やったこと
- LookupPort
- TCP Client
LookupPort
現在、実行してるサービスのポートを調べることができます。
net.LookupPort(network, service string) (int, error)
serviceは実行しているサービス、例えば"telent"、"http"、"ssh"とか
exsample
func main() { port, err := net.LookupPort("tcp", "http") if err != nil { fmt.Println("Error : ", err.Error()) os.Exit(2) } fmt.Println("Service port : ", port) }
TCP Client
いよいよTCPの話です。
まずはTCP関連でよく使う構造体から
type TCPAddr struct { IP IP Port int Zone string }
IPとPortとZoneを内包しています。 ZoneはIPv6で使用するZoneになります。IPとPortはそのままの意味かな。
この構造体を作るためには下記のメソッドを使います
net.ResolveTCPAddr(net, addr string) (*TCPAddr, err)
netにはネットワークタイプを(tcp, tcp4, tcp6)、addrにはアドレスとポートを入れます。 addrのフォーマットは「IPアドレスorホスト名:ポート」になります 例:www.google.com:80
次にTCPAddr構造体を使って実際にサーバとつなぎ合わせます
net.DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)
netにはResolveTCPAddr同様でtcp、tcp4、tcp6のどれかを入れ、laddr、raddrにはTCPAddrを入れます。 それぞれladdrはローカル、raddrはリモートを表しています。 なので接続したいサーバのTCPAddrはraddrに入れます。(laddrには基本的にはnilが入ります)
ここで取得したTCPConnを使い実際のメッセージのやり取りを行います。 このTCPConnにたいして、write、readを行いリモートのサーバと通信を行います。
(c *TCPConn) Write(b []byte) (int, error)
bには送るメッセージを入れます(そのまんま)
最後にサーバからレスポンスを受け取り、読み込みます
ioutil.ReadAll(io.Reader) ([]byte, error)
io.Readerから読み込みます。これはサーバからレスポンスをすべて受け取るまでブロックして、すべて受け取ってから読み込みを開始します。
これで一通りTCPの通信の流れができます。 TCPクライアントのサンプルとしてHTTP通信をしたいと思います
exsample
func main() { tcpAddr, err := net.ResolveTCPAddr("tcp4", "www.google.com:80") if err != nil { fmt.Println("net resolve TCP Addr error ") os.Exit(1) } conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Println("net dial tcp error ") os.Exit(1) } _, err = conn.Write([]byte{"HEAD / HTTP/1.0 \r\n\r\n) if err != nil { fmt.Println("conn write error ") os.Exit(1) } result, err := ioutil.ReadAll(conn) if err != nil { fmt.Println("ioutil read all error ") os.Exit(1) } fmt.Println(string(result)) }
これでTCP通信ができるようになりました(やーー!!)