Magnolia Tech

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

『初めてのGo言語―他言語プログラマーのためのイディオマティックGo実践ガイド』、『Learning Go: An Idiomatic Approach to Real-World Go Programming』の邦訳が出版される

『Learning Go: An Idiomatic Approach to Real-World Go Programming』の邦訳が出版される。

『初めてのGo言語―他言語プログラマーのためのイディオマティックGo実践ガイド』という署名になっているけど、サブタイトルがいいな。原著と全然違うんだけど、本書を読んだことがある人なら、「あーこっちの方が合っているなー」と思えるはず。

原著が出た時にすぐに買ったのに、積んであったのを最近ISUCONのためにようやく読んだ。

blog.magnolia.tech

既に何らかのプログラミング言語を習得している人が読むことが前提で、プログラミング言語が備える機能の概念ではなく、機能を設計の背景含めて流れるように解説している感じが割と好きな構成だった。過去に読んだプログラミング言語の入門本の中では一番理解しやすかったかも。

というわけで、既に何らかの言語を習得している人がGo言語を学ぶ時にはお薦めの一冊ですね。

『チームトポロジー』と、『How Do Committees Invent?』を読み直して「コンウェイの法則」について考え直した

コンウェイの法則」というのがある。

Melvin Conwayが『How Do Committees Invent?』という論文にて発表した考え方を指す言葉。筆者自身による論文の紹介文に書かれている、以下のくだりが端的にその内容を表している。

Any organization that designs a system (defined more broadly here than just information systems) will inevitably produce a design whose structure is a copy of the organization's communication structure.

システムを設計する組織(ここでは情報システムだけでなく、より広い意味で定義する)は、必然的に組織のコミュニケーション構造をコピーした構造を持つ設計を生み出すことになる。

「組織をつくる人」は、それぞれの階層に於いて、その課せられた目的を達成するために自分がコントロールし易いように成果や役割を分割し、それに合わせた「組織」を作り上げていく。当たり前だけど、「組織をつくる人」の視点から見れば、それらの分割された成果は合成可能であり、その統合によって目的が達せられる。

初めてこのコンウェイの法則を聞いたとき、「それは組織をつくる人の意図通りの結果なのだから、当たり前では?」とか、「縦割りの組織に所属している人からの悲鳴?」とか、「組織を外から見ている人の皮肉?」とか色々な考えが巡った......つまり、「組織をつくる人」「組織に所属する人」「組織を外から評価する人」…誰の視点からのメッセージなのか、よく分からなかった。

一方で、「逆コンウェイ戦略」と呼ばれる考え方がある。

組織の構造がシステムの構造を規定するのであれば、作るべきシステムの構造に合わせて組織を作る考え方だ。これは…確かにその通り…というか、元から組織は目的に合わせて作られるべきだし、目的に合っていなければスクラップ&ビルドされるべき……後から見ればいくらでも言えるだけなのだけれど。

ちなみに、元の論文をよく読むと、最後にちゃんと書いてある。

Primarily, we have found a criterion for the structuring of design organizations: a design effort should be organized according to the need for communication.

それは、「デザインはコミュニケーションの必要性に応じて組織化されるべき」という、デザイン組織の構造化の基準を見出したことである。

This criterion creates problems because the need to communicate at any time depends on the system concept in effect at that time. Because the design which occurs first is almost never the best possible, the prevailing system concept may need to change. Therefore, flexibility of organization is important to effective design.

この基準では、その時々のシステムコンセプトによって通信の必要性が変わるという問題が発生します。なぜなら、その時点のシステムコンセプトによって、いつでもコミュニケーションが必要だからです。したがって、効果的な設計を行うためには、組織の柔軟性が重要である。

わざわざ「逆コンウェイ戦略」とか言わなくても、大事なことはちゃんと書かれている。逆じゃなくない?


で、じゃあその考え方をうまく取り入れて組織を作っていくにはどうしたらいいか?「コンウェイの法則」をベースにした具体的な組織構造の作り方が『チームトポロジー』に書かれている。

現代的なソフトウェアアーキテクチャの特性を背景に、チームの構成や、人数、コミュニケーション設計の方法などが解説

もちろんそのまま取り入れればいいというものでもないし、謎のカタカナ用語が多すぎて(「イネイブリングチーム」とか、「コンプリケイテッド・サブシステムチーム」とか...)、そのまま発声するにはちょっと躊躇するけど、「具体性」という意味では非常に参考になる事例が多く掲載されていて、最初に書いた疑問である「この法則は、誰が理解すべきなのか?」ということに対しては「組織をデザインする人」ということがようやく理解できた。

個人的には、「ソフトウェアの境界のサイズをチームの認知付加に合わせる」という考え方が非常に納得感が有って、ここを失敗すると組織の生産性や品質は格段に下がってしまう、と感じている。


コンウェイの法則」以外にも、色々なソフトウェアエンジニアにとって有益な考え方や、視点がざっとまとまっている『達人プログラマ』もお薦めです。一つ一つのことを深く掘り下げるより、ざっと「こういう視点がある」ということを掴むのに、お薦めの1冊です。

ネットワーク知識のリフレッシュのために最新の『マスタリングTCP/IP 入門編 第6版』を読んでみた

最初にネットワークの基礎を学んでから随分時間も経ってしまったので、ネットワーク知識のリフレッシュのために『マスタリングTCP/IP 入門編 第6版』を買ってきた。2022年に入って、10年ぶりに改版された第6版が出版されたので、購入のタイミング的には丁度良かった。

QUICや、Wi-Fi 6など、今時のキーワードも網羅している。一方で10BASE2や、10BASE5などの、もうめったに見られない技術も出てきて、さすがに入門書からは消しても良かったのでは?と思ったり......

でもすっかり忘れてた知識を、学び直すことができたので良かった。


いろいろなネットワークの教科書を読んでみると、OSIの7階層モデルが出てきて、物理層から始まる下位レイヤーから、より上位のアプリケーションのレイヤーに向かって解説が進んでいくのだけど、上位の層から下位の層に向けて学んでいく方が良いのか、下位の層から上位の層に向けて学んでいく方が良いのか、ということを考えた。

ネットワークを構築する人の視点だと前者なのだろうし、サーバサイドのアプリケーションを構築する人の視点だと後者の方が良い気がする。ウェブアプリケーションを書く人からすると、物理層のこととか、知識として知っていてもいいけど、まずはそこじゃないよねって話もあるだろうし、クラウドだと、昔のようにインフラサイドの人に、物理スイッチや、物理ルータの設置や設定をお願いすることなく、自分で設定してしまって完結することもあるだろうし。

ウェブアプリケーションを書く人のために、上位のレイヤーから、下位のレイヤーに向けて自分の興味に合わせて掘り下げていく順序の入門書が有ってもいいよなーって思った。


『マスタリングTCP/IP』シリーズ、過去に色んな本が出ているけど、その後アップデートされているものばかりでもないので、発行日とかはよく確認しておいた方がよさそう。

最近アップデートされたのは、入門編以外だと、セキュリティ編が新しくなっている。

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

手元でノートに色んなキーワードを書き出したり、雑な図形を描いたり、眺めたりしないと考えがまとまらないタイプで、いつも手元にノートやペンが無いと落ち着かない。 一方で、それなりの量の文書になると、テキストエディタに移って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年くらい経っても結局、ますますよーわからん問題になっている気がする…