Magnolia Tech

いつもコードのことばかり考えている人のために。

考えをまとめる「環境」の作り方

手元でノートに色んなキーワードを書き出したり、雑な図形を描いたり、眺めたりしないと考えがまとまらないタイプで、いつも手元にノートやペンが無いと落ち着かない。 一方で、それなりの量の文書になると、テキストエディタに移ってmarkdown形式で、書いたり、消したり、入れ替えたりしないとまとまらない。

普段と全然違う場所でやってみないとまとまらないとか、最低でも3回くらいやり直さないとまとまらないとか、自分なりの考える環境のパターン、みたいなのがあって、それを意識しておくと、いいよね、という話でした。

たぶんロフトあたりで見つけて買っておいたけど、ずっと使い道のない「会議ノート」。「業務ノート」というのもあるらしい。なんか真面目に使ってみたい。

というわけで、皆さんの考えをまとめる「環境」の作り方をブクマとかで教えてください。

`sbt new`が`invalid privatekey`というエラーになって実行できない

タイトルそのままなのだけど、sbt newというテンプレートからsbtのプロジェクトディレクトリを作るコマンドがinvalid privatekeyを出して実行できなくなっていた。

$ sbt new scala/scala3.g8
...
ssh://git@github.com/scala/scala3.g8.git: invalid privatekey: xxxxxx

(xxxxxxの箇所には、sshのキーらしきものが表示されている)

sbt newを実行する機会は意外と少なくて、せいぜい何かのライブラリのサンプルコードを動かす時くらいなので、最近環境をアップグレードするまで気づかなかった。

issueは簡単に見つかった。

No support for open ssh keys? · Issue #5589 · sbt/sbt · GitHub

JSch / Bugs / #129 "JSchException: invalid privatekey" on OpenSSH 7.8 and above

原因は、openSSHが生成するキーの形式がOpenSSH 7.8から変更になっているそう。

dev.classmethod.jp

sbtは、sbt newをgiter8経由で実行するが、このgiter8が、jschに依存している giter8/Dependencies.scala at develop · foundweekends/giter8 · GitHub

  val jgitJsch             = "org.eclipse.jgit" % "org.eclipse.jgit.ssh.jsch" % "5.13.1.202206130422-r"
  val jsch                 = "com.jcraft" % "jsch.agentproxy.jsch" % "0.0.9"
  val jschSshAgent         = "com.jcraft" % "jsch.agentproxy.sshagent" % "0.0.9"
  val jschConnectorFactory = "com.jcraft" % "jsch.agentproxy.connector-factory" % "0.0.9"

jschは2018年以来、リリースが止まっているので、上記の変更が取り込めていない模様。

http://www.jcraft.com/jsch/ChangeLog

一応forkも有って、そちらは活発にリリースが続いている模様。

github.com

JGit自体も、Apache MINAをサポートしたアドオンをリリースしているので、Giter8がこちらに載せ替えれば良さそうな気がする。

https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit.ssh.apache


というわけで、sshのキー形式をOpenSSH形式からPEM形式に変えてもいいのだけど、めったに実行しないsbt newのためにそんな変更を入れるのも嫌なので、sbtのプロジェクトは既存のものを参考に手作成しました。


2022/9/23 追記

github.com

そのまま放置、というのもアレなので、修正するPRを作って、マージしてもらいました。

依存先を変えることで対処しました。


2022/10/4 追記

上記の修正が取り込まれたgiter8が、sbt側にも取り込まれたので、エラーが出なくなりました。

github.com

`X-Forwarded-For`ヘッダからクライアントのIPアドレスを取得する

2022/7/24: Plack::Middleware::ReverseProxyが、X-Forwarded-Forをサポートしていますよ、と教えていただいたので、記載を修正しました。 @karupanerura++


Scalatra 3.0.0-M1がリリースされた。Scala3対応だ、めでたい。

github.com

最近Scalatraへあまり貢献できていなかったので、いくつかのコンパイル時の警告を消すPRを出した。コードを見直す中で、ふとX-Forwarded-ForヘッダからクライアントのIPアドレスを取得する箇所が目にとまった。

  /**
   * The remote address the client is connected from.
   * This takes the load balancing header X-Forwarded-For into account
   * @return the client ip address
   */
  def remoteAddress: String = header("X-FORWARDED-FOR").flatMap(_.split(",").map(_.trim).filterNot(StringUtils.isBlank).headOption).getOrElse(r.getRemoteAddr)

確かX-で始まるヘッダは非推奨になっていて、別のヘッダが標準化されているんじゃなかったっけ?と、気になって調べてみたので、その覚え書き。

X-Forwarded-Forヘッダとは?

X-Forwarded-ForヘッダはX-で始まることから分かる通り、何かの規格で定義されている訳ではなく、事実上標準的に使われているヘッダの一つ。

MDNに解説が書かれているので、詳しくはリンク先を読んだ方がいいけど、先頭の解説を引用すると、以下のように書かれている。

X-Forwarded-For (XFF) ヘッダーは、 HTTP プロキシーサーバーを通過してウェブサーバーへ接続したクライアントの、送信元 IP アドレスを特定するために事実上の標準となっているヘッダーです。

developer.mozilla.org

類似のヘッダに、X-Forwarded-Hostや、X-Forwarded-Protoが有るが、あまり使われている例は見たことがない。元はSquidというプロキシソフトウェアが始めたものとのこと。

途中でプロキシサーバによるIPアドレスの変換が行われると元のIPアドレスが付加されていく。複数のプロキシを経由した場合は、後ろに足されていくため、複数のIPアドレスが記録される可能性がある(ちなみに、Scalatraでは複数のIPアドレスが記録されているパターンを上手く扱えず、ちょっと前に修正された)。

通常はIPアドレスのみで、ポート番号は付かない実装がほとんどのようだが、RackのチケットにAzureのApplication GatewayX-Forwarded-Forヘッダにポート番号付でIPアドレスを記録するので、そのポート番号を削除するコードがマージされている。

github.com

あと、後述するが、面倒なことに、IPv4IPv6で記法が異なる。

Forwardedヘッダとは?

X-Forwarded-Forヘッダは非標準ということで、RFC 7239 Forwarded HTTP Extensionで同様の機能を持つヘッダ「Forwarded」が標準化されている。

developer.mozilla.org

しかし、ヘッダーの中で更にforと書かれている部分を抜き出す必要があるし、IPv6の場合は引用符と角括弧で囲む必要が有り、ちょいと面倒くさい。

X-Forwarded-For: 123.34.567.89
Forwarded: for=123.34.567.89

X-Forwarded-For: 192.0.2.43, "[2001:db8:cafe::17]"
Forwarded: for=192.0.2.43, for="[2001:db8:cafe::17]"

そもそも実際に、使われているのか、よく分からない。

Rackではちゃんとサポートされているようだ。

      def forwarded_for
        forwarded_priority.each do |type|
          case type
          when :forwarded
            if forwarded_for = get_http_forwarded(:for)
              return(forwarded_for.map! do |authority|
                split_authority(authority)[1]
              end)
            end
          when :x_forwarded
            if value = get_header(HTTP_X_FORWARDED_FOR)
              return(split_header(value).map do |authority|
                split_authority(wrap_ipv6(authority))[1]
              end)
            end
          end
        end

ScalatraにおけるX-Forwarded-Forヘッダーの扱い

ScalatraはServletベースのフレームワークなので、アプリケーションコードからはgetRemoteAddrメソッドを使ってクライアントのIPアドレスを取得する。

getRemoteAddr()
クライアントまたはリクエストを送信した最後のプロキシのインターネットプロトコル(IP)アドレスを返します。

Servletが参考にしたCGI規格ではクライアントのIPアドレスは、REMOTE_ADDRで取得するが、JavaではSnake Caseを使わないのでアンダースコアが省略されている。

Scalatraでは、remoteAddressというメソッドがRequestオブジェクトに用意されていて、これは先ほどのX-Forwarded-Forヘッダーが設定されている場合、getRemoteAddrの値ではなく、”おそらくクライアントのものと思われるIPアドレス"を取得して、設定している。

特にIPv6のことは考慮されていないし、後述する信頼されたIPアドレスのことも考慮されていない、最もシンプルな実装になっている。

では、どう取り扱うのが良いか?

通常は、クライアントのIPアドレスは、リクエストが途中で複数のプロキシを経由すると、その度に末尾に変換前のIPアドレスが付加されるため、X-Forwarded-Forヘッダの先頭に書かれているIPアドレスと解釈される。

つまり、以下の例ではAの箇所に書かれているIPアドレスが最初にリクエストを送信したクライアントのIPアドレスと解釈される(2個目以降のB、Cは単に無視される)。

X-Forwarded-For: A, B, C

通常、インターネット経由などで送られてくるリクエストの場合、わざわざ送信元の情報を外部に開示することは無いため、実際には多段階のプロキシを経由していたとしてもX-Forwarded-Forヘッダが付与されていることは考えづらい。

一方で、MDNのX-Forwarded-Forヘッダの解説によると、わざと不正なX-Forwarded-Forヘッダを付加してリクエストが送られてくる可能性を考慮すべきと書かれている。

X-Forwarded-For

クライアントから最も近い X-Forwarded-For の IP アドレスを選択する場合(信頼できない、かつ、セキュリティに関係の ない 目的で)は、左端から 有効 で プライベート/内部 ではない最初の IP アドレスを選択する必要があります。(なりすましの値は IP アドレスではない可能性があるため "有効" である必要があります。また、 "プライベート/内部アドレスではない" というのはクライアントが彼らの内部ネットワークのプロキシーを使用した可能性があり、その場合 プライベートネットワーク で IP アドレスが追加されている可能性があるためです。)

信頼できる_ 最初の X-Forwarded-For クライアント IP アドレスを選択する場合、追加の設定が要求されます。これらは一般的に2つの方法があります:

  • 信頼できるプロキシーの数: インターネットとサーバーの間のリバースプロキシーの数が設定されている。X-Forwarded-For IP リストは右端から1を引いて位置から検索されます。(例えば、リバースプロキシーが1つの場合は、そのプロキシーはクライアントの IP アドレスを追加するため右端の IP アドレスを使用するべきです。もし、リバースプロキシーが3つある場合、最後の2つの IP アドレスは内部のものになるでしょう。)
  • 信頼できるプロキシーのリスト: 信頼できるリバースプロキシーの IP リストもしくは IP の範囲が設定されている。 X-Forwarded-For IP リストは右端から検索されますが、この時に信頼できるプロキシーリストのアドレスはスキップされます。信頼できるリストに一致しなかった最初のアドレスが目的のアドレスです。

最初の信頼できる X-Forwarded-For IP アドレスは実際のクライアントコンピューターではなく信頼できない中間プロキシーのものかもしれません、しかし、それはセキュリティ用途のための唯一適した IP アドレスです。

少なくとも自分たちが管理するネットワークに接続される際に使ったIPアドレス自体は正しいので、自分たちが管理している(=信頼できる)プロキシの直前のIPアドレスを使っておけば不正に差し込まれたIPアドレスではないと判断できる、という考え方のようです。

そもそも不正なヘッダを送信してくる時点でIPアドレスを取得するよりも別にやることが有るような気もしますが...

PlackでのX-Forwarded-Forの対応と、実装

HTTPプロトコル関係の実装例を見たくなったらまずはPlackか、Rackの実装を見てしまうのですが、PlackではPlack::Middleware::ReverseProxyを使うことでX-Forwarded-Forをサポートしています。

metacpan.org

X-Forwarded-Forに記述されている最後のIPアドレスREMOTE_ADDRに設定してくれます。

    # If we are running as a backend server, the user will always appear
    # as 127.0.0.1. Select the most recent upstream IP (last in the list)
    if ( $env->{'HTTP_X_FORWARDED_FOR'} ) {
        my ( $ip, ) = $env->{HTTP_X_FORWARDED_FOR} =~ /([^,\s]+)$/;
        $env->{REMOTE_ADDR} = $ip;
    }

また、Graham Barrさん作成のPlack::Middleware::XForwardedForを使うと、同じようにX-Forwarded-ForからクライアントのIPアドレスを取得できますが、信用できるプロキシのリストをサブネットマスク付で指定できるようになっている点が異なります。

  if (@forward) {
    my $addr = $env->{REMOTE_ADDR};
    
    if (my $trust = $self->trust) { # $self-trustは、Net::IPの配列リファレンスを持つ
    ADDR: {
        if (my $next = pop @forward) {
          foreach my $netmask (@$trust) {
            my $ip = Net::IP->new($addr) or redo ADDR;
            if ($netmask->overlaps($ip)) {
              $addr = $next;
              redo ADDR;
            }
          }
        }
      }
    }
    else {    # trust everything, so use first in list
      $addr = shift @forward;
    }
    $env->{REMOTE_ADDR} = $addr;
  } 

信頼できるプロキシのリストが与えられない時は、単にX-Forwarded-Forの先頭のIPアドレスREMOTE_ADDRに設定して終わりです。

信頼できるプロキシのリストが与えられた場合は、今度は逆にREMOTE_ADDRX-Forwarded-Forの末尾から先頭という順で、最初に信頼できるIPアドレスに該当しないアドレスを探索していって最初に見つかったアドレスをREMOTE_ADDRを返すようになっています。

仮に、最初からREMOTE_ADDRに信頼されたIPアドレスではないものが登録されていれば、X-Forwarded-Forの内容は一切無視されます。

なお、上記の引用では省きましたが、IPv4射影アドレスとしてのIPv6IPアドレスはサポートされているようですが(それでも記述形式が合っていない気がする...)、通常のIPv6アドレスはサポートされていないようです。

Rackの実装

RackはデフォルトでX-Forwarded-Forをサポートしていて、信頼できるIPアドレスを取り除いて、最初に見つかったIPアドレスを、クライアントのIPアドレスと定義しています(Forwardedヘッダもサポートしていて、そちらを優先して使うようになっています)。

reject_trusted_ip_addressesというメソッドで信頼できるIPアドレスを取り除いて、結果的に見つからない場合は、便宜的にX-Forwarded-Forの先頭をクライアントのIPアドレスとして取り扱う実装になっている。

def ip
    remote_addresses = split_header(get_header('REMOTE_ADDR'))
    external_addresses = reject_trusted_ip_addresses(remote_addresses)


    unless external_addresses.empty?
        return external_addresses.last
    end


    if (forwarded_for = self.forwarded_for) && !forwarded_for.empty?
        # The forwarded for addresses are ordered: client, proxy1, proxy2.
        # So we reject all the trusted addresses (proxy*) and return the
        # last client. Or if we trust everyone, we just return the first
        # address.
        return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
    end


    # If all the addresses are trusted, and we aren't forwarded, just return
    # the first remote address, which represents the source of the request.
    remote_addresses.first
end

def reject_trusted_ip_addresses(ip_addresses)
  ip_addresses.reject { |ip| trusted_proxy?(ip) }
end

信頼できるIPアドレスは、最初からプライベートアドレスがすべて定義されていて、外から定義を与えることはできない。

    trusted_proxies = Regexp.union(
      /\A127#{valid_ipv4_octet}{3}\z/,                          # localhost IPv4 range 127.x.x.x, per RFC-3330
      /\A::1\z/,                                                # localhost IPv6 ::1
      /\Af[cd][0-9a-f]{2}(?::[0-9a-f]{0,4}){0,7}\z/i,           # private IPv6 range fc00 .. fdff
      /\A10#{valid_ipv4_octet}{3}\z/,                           # private IPv4 range 10.x.x.x
      /\A172\.(1[6-9]|2[0-9]|3[01])#{valid_ipv4_octet}{2}\z/,   # private IPv4 range 172.16.0.0 .. 172.31.255.255
      /\A192\.168#{valid_ipv4_octet}{2}\z/,                     # private IPv4 range 192.168.x.x
      /\Alocalhost\z|\Aunix(\z|:)/i,                            # localhost hostname, and unix domain sockets
    )

全部プライベートアドレスの場合は、前述した通り、X-Forwarded-Forの先頭のIPアドレスが返される(よく見ると、localhostという文字列が帰ってくる場合はも有るのか…)。

おわりに

ちなみにGo言語のnet/httpは、X-Forwarded-Forはサポートしていなかった。

全くサポートしていないか、単にX-Forwarded-Forの先頭を返すか、信頼できるIPアドレスを元に丁寧にチェックするか、リバースプロキシ的に一つ前のアドレスを返すか...その4種類の実装が有ることが分かりました。

Rackの実装が整理されていますね(ローカルIPアドレスだけで構成されたローカルネットワークの中で多段プロキシが有ってもちゃんと動作するし、不正なヘッダは読みこないし)。

と、散々調べたものの、さまざまなネットワーク構成が取り得る現代においてWebアプリケーションのレイヤーでクライアントのIPアドレスを参照してアクセス制御するのか?とか、httpsだとそもそも途中の経路でヘッダを差し込めないのでは?とか、Forwardedヘッダって使われているの???とか考え始めると、そこまでフレームワーク側で頑張ることなのか?という気持ちになったので、特にScalatraのコードの見直しはしませんでした。

USB PD EPR(240W)対応のUSB Type-Cケーブル

備忘録

USB PD EPR(240W)対応のUSB Type-CケーブルをELECOMがリリースしている。

USB 4にも対応していて、最大40Gbpsでの通信が可能。つまり、いわゆる全部入りケーブルの新世代。

USB 2.0版で、ケーブル長が2.0mまで、というバージョンも用意されている。

USB 4と、Thunderbolt 4の違いは、下記のブログの解説が詳しい。

hanpenblog.com

っていうか、Type-Cよーわからん問題は、5年くらい経っても結局、ますますよーわからん問題になっている気がする…

紙の技術書を開いたままコードを書く時は、クラスプクリップがおすすめです

紙の技術書を開いたままの状態にして、参考にしながらコードを書きたい時ってありますね。ただ、本を開いた状態にしておくのが結構大変です。ブックスタンドなどもありますが、けっこう大きいし、持ち運びには向いていないです。

そんな時には、ステンレス製のクラスプ クリップがおすすめです。ペンケースにも入るサイズなので持ち運びもできます。

(写真の本は、最近読んでいる『Learning Go』です)

紙の質にもよりますが、150ページくらいまでは止めておけるし、それ以上のページになれば自重で本全体は開いたままの状態になるので、めくれないように手前のページを数ページ軽く挟んで押さえておけばいいだけです。

一番大きなサイズでも275円なので、試しに買えるレベルなのもオススメの理由の一つです。

ブックスタンドみたいに立てかけて見たい場合は向かないですが、ちょっと押さえておきたい、という用途にはいいと思います。

何もしていないのに、壊れました」から「何もしていないから壊れました」に時代は変わりました、という話をしました

と、書いてみたのだけれど、本当にそうなのだろうか。

今までだって本当に”何もしないのに、壊れずに使い続けられていたのか?”

その見える部分が変わった時に違和感を感じているだけだったりしないですか? あの手この手で、全体の挙動を押さえている人を雇用継続することで、維持できていて、そのためには知らず知らずのうちの不断の対応が有ったんじゃないでしょうか?

ということを、真夏のような気温の夜に考えてみました。

ぜひ感想をお待ちしております。

『Learning Go』を読んで、Goに入門している

A Tour of Goにチャレンジしたことがあるくらいで、Goをちゃんと勉強してこなかった。

さすがに、ISUCONの問題がGoが最初に作られている時代に、やっぱりちゃんと学んだ方がいいかなと思って、買ったまま積んであった『Learning Go』を読んでいる。

Go固有の、キャッチーな所を先に説明するようなことはなく、淡々とリテラルから始まり、制御構造や、structgoroutineなどの解説に進んでいくので、あまりつまみ食いしながら読む感じでもなくて、1週間くらいかけて最初から最後まで読む、みたいなスタイルの読み方の方が良いかも。

随所に機能の経緯や、背景、他の機能との関連が差し込まれていて、単に機能解説書になっていない点もよかったです。

”プログラミング自体”への入門本ではないので、最低限一つは他の言語で一通り書けるくらいのスキルはないと読みこなすのは難しいのと、演習問題が無いので、学んだことの確認がしづらい、みたいなところはありますが、他にGoで書かれたコードの事例を見ながら一緒に見ていくとか工夫が有れば、このくらいの記述量で十分ですね。