TELECUBE/EXPRESS WORK/(STATION BOOTH)を使った

忙しくしていたらあっという間に4月らしく、学会発表やら就活やらが無事終了してた。
大学生・大学院生の夏休みは2ヶ月近くあって遊び放題などと一昔前まで言われていたらしい。今思い返すと大学一年のときはそこそこのんびりしてたと思うが、その後は新型コロナウイルスで家に閉じこもってゴニョゴニョしてたし、ACPIの悪口を言っていたら職を頂いてワチャワチャしていた。それ以来大学の長期休みは講義がないのでその分何かに追われる時間が増えるだけになった。趣味として睡眠と資産運用を嗜むようになり、寝る前にお布団で資産評価額が変動しているのを見てニコニコしながら眠りにつくのが最近の日課である。金がかかるのかかからないのかわからない趣味である。

さて、最近の駅にボックス型の個室が置いてある。テレワークで普及したらしく、いつでもどこでも働ける素晴らしいボックスである。ある方曰く「日本の社畜精神を表した激ヤバボックス」らしい。
私はLTE対応ノートPCを持ち歩いているため、新幹線の中でもメールは返信するし、比叡山の山頂でも事務手続きするし、南小谷駅の乗り換え待ちで突然要求されたレポートも書き上げる人間である。と言ってもこれらは椅子があればどうとでもなるので個室の必要はない。個室が必要なのは会議の時くらいだろうか。

3月の最終週という年度も末の末の時に、個室を利用する機会があった。就活である。
就活では関わらせていただいたほとんどの方は大変親切で、私の進路についてしっかりと聞いてくださり、親身に話してくださったので大変感謝している。申し訳なく内々定を辞退した会社もあるがとても良い会社なので今後も御縁があることを祈っている。
しかし、面談日程は調整に困った。とある企業の最終面談は現地であり、大阪に向かう必要があったのだが、別のもう一社のオンライン面接がその日にしか選択肢がなかった。「次の選択肢から都合のつく日程を選んでください」という文言で一つしかなかった。大阪の現地面接の1時間後である。年度末だから忙しいのはお互い様だろうから非難するつもりはないが、とにかく困った。

ホテルのデイユースやネットカフェなどを検討したが、空きがなかったり駅から遠かったりした。そこで話題のなんたらボックスを使うことに決めた。

現地での面接が終わって急いで駅に向かったが次の列車がなかなか来ない。列車を待っている間に大阪駅・新大阪駅のなんたらボックスの空きを確認した所、これが意外に空いていない。列車に乗ってからも空きを調べて回った所、JR西日本の運営するTELECUBEが一席空いているのを発見した。

大急ぎで京都線の中で予約処理を行い新大阪駅について目的のボックスを探した。
予約したのは「TELECUBE JR西日本 新大阪駅在来線改札内 (個室ブース)」の一つであり、改札内にあった。急いでいたので写真を取り忘れたがリンクを辿ると公式サイトから見られるだろう。

中の様子は以下の動画のような感じである。(退出前に撮影した。)

遮音性にはまあまあ優れており、直接個室に向かって話しかけられなければ何を言っているかは聞こえない。このままで行けそうだ。面接開始まであと10分だし、Wi-Fiに接続して準備をすれば始まるだろう。
そう思ってPCでWi-Fiに繋ぎTeamsへの接続を試みるが繋がらない。スマホでも繋ぐが携帯回線に切り替わったりWi-Fiになったりで安定しない。恐る恐る回線速度を測ってみたら以下の通りである。

あと5分で面談が始まると言う時にこれを見せられて冷静でいられるだろうか。確かにWebサイトにはLTE回線を使用しているとあった。しかしこれでは3G回線ではないか。少なくともテレワークではまともに使えたものではない。Softbank回線は持ったことがないが、これが速度制限無しでの新大阪駅の実効値なら改善されない限り回線契約はしないな…と思った。

スマホでテザリングをしようにもOCNのNTT DOCOMO回線も先程から怪しい。恐らく出て2Mbpsくらいであろう。耐えられない事は無いが何らかの制限はかかるだろう。

しかし、このノートパソコンにはIIJmioのA回線(KDDI Au回線)のSIMカードが刺さっている。
なんでか知らないが、MousePro-L140PUはNTT DOCOMOの回線を使おうとすると、HSDPA?(UMTS表記)しか繋がらず、安定もしないため、単なる思いつきでKDDI Au回線ならどうだろうかとIIJmioを契約したところ、バッチリLTEを掴んだためそのまま運用していた。

藁にもすがる思いでIIJmio回線を有効化し、Teamsに接続し面談は始まった。
結果として以下のように安定した通信品質を担保してくれた。最大で10Mbpsほどのスループットが必要となった場面があったが問題なく動作してくれた。面談は1時間でその間に画面共有も行ったが特に問題なかった。通信量は1.5GBで6GBの枠を持っていたため速度制限にも引っかかることもなく無事終了した。

これのおかげで就職活動は成功したのだから、もうIIJとKDDI様様である。Auは英雄である(激寒駄洒落)。LTEモジュール付きのノートパソコンを持っていたことにこれほど感謝したこともないだろう。スマホのテザリングでも行けたかもしれないが、安定性の点において少し不安があった。

さて、その3日後にまた新大阪に行く予定があったので駅のホームで集中的にメールを返信するために今度はJR東海の提供するEXPRESS WORKに20分だけ滞在した。開発自体はTELECUBEがしたようだ。

中の作りの撮影は忘れたが、椅子が前回使用したものよりも質素で座りにくかった以外は特に問題なかった。気になっていたのはやはりWi-Fiの通信速度である。

こちらはKDDI Au回線であった。文句なしのハイスピードである。あの時、ここが使えればTeamsも余裕だったろうに…。やはり時代はAuなのだろうか。
今後もし個室を使う際にはEXPRESS WORKを使おうと思う。

TODO: STATION BOOTH

あけましておめでとうございます。

今年は曇天のため、初日の出は見れませんでした。本日の予定は終了です。

去年はコード書きだしたり、仕事したり、休んだり、東北行ったり、出張行ったり色々しました。気分も緩やかに回復しました。

今年は色々決める年だったり、締めの年のため頑張っていきたいです。

PCI BARの値とACPIの_CRS関数

この投稿は、Kernel/VM探検隊@北陸 Part 6で発表を行った「AArch64 ThinHypervisor開発記録」の「PCI Base Address Registerは絶対アドレスじゃ無かった話」を詳しく説明したものです。


PCIの各デバイスには、PCI Configuration Spaceというものがあって、そこにはBase Address Register(BAR)が0~5まで6つあります。

PCIデバイスを操作するためには、BARに記載されているアドレスにアクセスする必要があり、最近のデバイスは殆どMMIO(Memory Mapped I/O)となっていて、メモリアドレスが記載されています。
最近では、64bit AddressがよほどでBAR 0/1を用いてBAR0に下位32bit、BAR1に上位32bitが記述されていると思います。

基本的にBARに書かれているアドレスがそのままMMIOのアドレスになると思うのですが、まれにこれが相対アドレスで記載されている場合があります。
今回は、そのケースについて追っていきます。

背景

ハイパーバイザで実際のPCI Bridgeの下に寄生する形でVirtIO Network Deviceを生やす実装をしていました。
具体的には、PCI Configuration Spaceを走査していきデバイスがないBus:Device.Functionを取得し、対応するPCI Configuration SpaceへのアクセスをトラップするようにしてハイパーバイザでVirtIO Network用のPCI Configurationを提供する、という実装をしていました。
VirtIO Networkが実際に動作するためには、BARに記載されるMMIOでVirtQueueの処理などをしないといけないのですが、Linuxは起動時にBase Addressを変更して整理します。このためBARに新たな値が書き込まれた場合にはこの値をもとにトラップするメモリアドレスを変更しないといけません。
今回は64bitアドレスなのでBAR0/BAR1を使用しているのですが、おかしな挙動に悩まされました。
以下は、Linuxとハイパーバイザの起動時のシリアル出力なのですが、Linuxが割り当てたと主張するアドレスと実際にPCI Configuration SpaceのBARに書き込まれる値が異なるのです。
(Hypervisor: と書かれている行はハイパーバイザが出力した行です。)

[    1.454562] pci 0000:00:01.0: BAR 0: assigned [mem 0xe000200000-0xe000200fff 64bit]
Hypervisor: BAR0: 0x200004
Hypervisor: BAR1: 0x0
Hypervisor: New Base Address: 0x200000

Linuxは 0xe000200000 をアサインしたと表示しているのに、実際にBARに書き込まれるアドレスは、 0x200000 となっています。(BAR0の下位3bitはフラグで0x04は64bit BARであることを示しています。)
このため、ハイパーバイザは 0x200000 をMMIO領域と設定してアクセスをトラップするのに対し、Linuxは 0xe000200000 にアクセスしに行くのでデバイスの制御ができず、起動に失敗します。
どうしてこのようなことになるのか、挙動を追います。

なお今回使用するLinuxはGNU/Linux 5.4です。

PCI Base Addressの割り当て

PCI Base Address RegisterにMMIOのアドレスを書き込んでいるのは、drivers/pci/setup-res.cpci_std_update_resource で行っています。pci_std_update_resourceの今回関係ある部分を概要を抽出すると以下のようになります。

static void pci_std_update_resource(struct pci_dev *dev, int resno)
{
	struct pci_bus_region region;
	u32 new;
	int reg;
	struct resource *res = dev->resource + resno;

	pcibios_resource_to_bus(dev->bus, &region, res);
	new = region.start;
	new |= res->flags & ~PCI_BASE_ADDRESS_MEM_MASK;

	reg = PCI_BASE_ADDRESS_0 + 4 * resno;

	pci_write_config_dword(dev, reg, new);

	if (res->flags & IORESOURCE_MEM_64) {
		new = region.start >> 16 >> 16;
		pci_write_config_dword(dev, reg + 4, new);
	}
}

この関数では、 配列となっている dev->resourceresno 番目のエントリを取り出し、これを pcibios_resource_to_bus に与えて、返ってきた region.start をBAR0/1に書き込んでいます。

次に pcibios_resource_to_bus を見てみます。

void pcibios_resource_to_bus(struct pci_bus *bus, struct pci_bus_region *region,
			     struct resource *res)
{
	struct pci_host_bridge *bridge = pci_find_host_bridge(bus);
	struct resource_entry *window;
	resource_size_t offset = 0;

	resource_list_for_each_entry(window, &bridge->windows) {
		if (resource_contains(window->res, res)) {
			offset = window->offset;
			break;
		}
	}

	region->start = res->start - offset;
	region->end = res->end - offset;
}

region->start = res->start - offset; が今回注目すべき点なのですが、 offset というのが気になります。この値は、 bridge->windows を順番に調べていき、 resource_contains(window->res, res) が真を返した際に、 window->offset の値がセットされます。 resource_contains は、 window->res->start <= res->start && window->res->end >= res->end の際に true を返します。

さて、 window->offset はどこでセットされるのでしょうか。
調べたところ、 acpi_dev_new_resource_entry で設定されるようです。

static acpi_status acpi_dev_new_resource_entry(struct resource_win *win,
					       struct res_proc_context *c)
{
	struct resource_entry *rentry;

	rentry = resource_list_create_entry(NULL, 0);
	if (!rentry) {
		c->error = -ENOMEM;
		return AE_NO_MEMORY;
	}
	*rentry->res = win->res;
	rentry->offset = win->offset;
	resource_list_add_tail(rentry, c->list);
	c->count++;
	return AE_OK;
}

rentry->offset = win->offset; がそれです。この関数は、すぐ下の acpi_dev_process_resource で呼ばれています。 acpi_dev_process_resource の処理概要は以下の通りです。

static acpi_status acpi_dev_process_resource(struct acpi_resource *ares,
					     void *context)
{
	struct res_proc_context *c = context;
	struct resource_win win;

	memset(&win, 0, sizeof(win));

	if (acpi_dev_resource_memory(ares, res)
	    || acpi_dev_resource_io(ares, res)
	    || acpi_dev_resource_address_space(ares, &win)
	    || acpi_dev_resource_ext_address_space(ares, &win))
		return acpi_dev_new_resource_entry(&win, c);

	return AE_OK;
}

ares を各関数に渡してパースが成功したら acpi_dev_new_resource_entry を呼び出すようです。
ここまでの予想で、 win->offset に値が入っているだろうと予想しているので、 acpi_dev_resource_address_spaceacpi_dev_resource_ext_address_space のどちらかが true を返すだろうと予想します。とりあえず、 acpi_dev_resource_address_space を見てみます。

bool acpi_dev_resource_address_space(struct acpi_resource *ares,
				     struct resource_win *win)
{
	struct acpi_resource_address64 addr;

	win->res.flags = 0;
	if (ACPI_FAILURE(acpi_resource_to_address64(ares, &addr)))
		return false;

	return acpi_decode_space(win, (struct acpi_resource_address *)&addr,
				 &addr.address);
}

win への値の格納は acpi_decode_space でやっているようです。 acpi_dev_resource_ext_address_spaceacpi_decode_space を呼び出しています。

acpi_decode_space のうち win->offset に関わる部分を抜粋します。

static bool acpi_decode_space(struct resource_win *win,
			      struct acpi_resource_address *addr,
			      struct acpi_address64_attribute *attr)
{
	// ...
	/*
	 * For bridges that translate addresses across the bridge,
	 * translation_offset is the offset that must be added to the
	 * address on the secondary side to obtain the address on the
	 * primary side. Non-bridge devices must list 0 for all Address
	 * Translation offset bits.
	 */
	if (addr->producer_consumer == ACPI_PRODUCER)
		offset = attr->translation_offset;
	win->offset = offset;
	// ...
}

コメントで、translate addressesについて書いており、PCIブリッジを跨ぐ際にアドレス変換を行うブリッジは attr->translation_offset がセットされているようです。
ここから、VirtIO Network Deviceが寄生していたPCI Host Bridgeはこの translation_offset がセットされていると予想できます。

さて acpi_dev_process_resource はどこから呼ばれているのでしょうか。
呼び出し一覧を見ると、 __acpi_dev_get_resources から呼び出されている気がします。
この関数は、acpi_dev_get_resources の内部関数です。 __acpi_dev_get_resources の抜粋は以下の通りです。

static int __acpi_dev_get_resources(struct acpi_device *adev,
				    struct list_head *list,
				    int (*preproc)(struct acpi_resource *, void *),
				    void *preproc_data, char *method)
{
	struct res_proc_context c;
	acpi_status status;

	c.list = list;
	c.preproc = preproc;
	c.preproc_data = preproc_data;
	c.count = 0;
	c.error = 0;
	status = acpi_walk_resources(adev->handle, method,
				     acpi_dev_process_resource, &c);

	return c.count;
}

acpi_dev_process_resourceacpi_walk_resources のコールバック関数として渡されています。 acpi_walk_resources は ACPICAの関数で、リソースのリストから一つずつエントリを取り出して、コールバック関数を呼び出します。

acpi_dev_get_resources のコメントには、 “Evaluate the _CRS method for the given device node and process its output”とあり、渡されたデバイスの_CRS関数を評価するようです。

この関数を呼び出していそうなのは、 acpi_pci_probe_root_resources だと予想します。
これに関してのドキュメントが、 https://docs.kernel.org/PCI/acpi-info.html に存在します。
ここから必要な部分を読み取ります。

まず、”For example, there’s no standard hardware mechanism for enumerating PCI host bridges, so the ACPI namespace must describe each host bridge, the method for accessing PCI config space below it, the address space windows the host bridge forwards to PCI (using _CRS), and the routing of legacy INTx interrupts (using _PRT).”という記述に注目します。要約すると、「PCI host bridgeを検出する標準的なハードウェアメカニズムは存在せず、ACPI名前空間において_CRSを評価することでアドレス空間の情報を取得できる。」という感じでしょうか。
PCI Configuration Spaceにアクセスする方法として、ECAMがあり、これはMCFGテーブルを読み取ることでアクセス可能だと理解してました。これについては、”Static tables like MCFG, HPET, ECDT, etc., are not mechanisms for reserving address space. The static tables are for things the OS needs to know early in boot, before it can parse the ACPI namespace. “とあり、MCFGなどの静的テーブルはあくまで起動初期に暫定的に使用する事を目的としているようです。
PCI host bridgeについて、”PCI host bridges are PNP0A03 or PNP0A08 devices.”とあり、_HIDがこれであるデバイスを見つけてくれば良いということになります。

さて、今回使用しているデバイスのDSDT/SSDTをiaslでディスアセンブルし、”PNP0A08″を探してみた結果、以下のようなデバイスが見つかりました。(必要な部分のみ抜粋)

    Scope (_SB)
    {
        Device (PCI0)
        {
            Name (_HID, EisaId ("PNP0A08") /* PCI Express Bus */)  // _HID: Hardware ID
            Name (_CID, EisaId ("PNP0A03") /* PCI Bus */)  // _CID: Compatible ID
            Name (_SEG, Zero)  // _SEG: PCI Segment
            Name (_BBN, Zero)  // _BBN: BIOS Bus Number

            Method (_CRS, 0, Serialized)  // _CRS: Current Resource Settings
            {
                Name (RBUF, ResourceTemplate ()
                {
                    WordBusNumber (ResourceProducer, MinFixed, MaxFixed, PosDecode,
                        0x0000,             // Granularity
                        0x0000,             // Range Minimum
                        0x00FD,             // Range Maximum
                        0x0000,             // Translation Offset
                        0x00FE,             // Length
                        ,, )
                    QWordMemory (ResourceProducer, PosDecode, MinFixed, MaxFixed, Cacheable, ReadWrite,
                        0x0000000000000000, // Granularity
                        0x0000000000000000, // Range Minimum
                        0x000000007FFFFFFF, // Range Maximum
                        0x000000E000000000, // Translation Offset
                        0x0000000080000000, // Length
                        ,, , AddressRangeMemory, TypeStatic)
                    QWordMemory (ResourceProducer, PosDecode, MinFixed, MaxFixed, Prefetchable, ReadWrite,
                        0x0000000000000000, // Granularity
                        0x000000E200000000, // Range Minimum
                        0x000000FFFFFFFFFF, // Range Maximum
                        0x0000000000000000, // Translation Offset
                        0x0000001E00000000, // Length
                        ,, , AddressRangeMemory, TypeStatic)
                })
                Return (RBUF) /* \_SB_.PCI0._CRS.RBUF */
            }
}

_CRS関数では、3つのリソースが記述されています。
_CRS関数の返す値についての説明は、Device Configuration — ACPI Specification 6.4 documentation にあります。
QWordMemory の項を見ると、”Address Translation offset”というものがあり、_CRSの返すエントリの2番目を見ると、”0x000000E000000000, // Translation Offset”とあり、これが巡り巡って、 offset の値になっていると考えると不可解な挙動も納得できます。

検証

今までのコードリーディングで立てた仮説を検証するためにLinux Kernelに pr_info を加えて値の確認をしてみます。

まずは、 pci_std_update_resource を以下のように書き換えます。

        pr_info("%s: res->start = 0x%llx\n", __func__, res->start);
        pcibios_resource_to_bus(dev->bus, &region, res);
        pr_info("%s: region.start = 0x%llx\n", __func__, region.start);
        new = region.start;

次に、 pcibios_resource_to_bus を以下のように書き換えます。

        resource_list_for_each_entry(window, &bridge->windows) {
                if (resource_contains(window->res, res)) {
                        pr_info("%s: window->offset: 0x%llx\n", __func__, window->offset);
                        offset = window->offset;
                        break;
                }
        }

更に、 acpi_dev_new_resource_entry にも pr_info を追加します。

        *rentry->res = win->res;
        pr_info("%s: win->offset: 0x%llx\n", __func__, win->offset);
        rentry->offset = win->offset;

acpi_dev_resource_address_spaceacpi_dev_resource_ext_address_space にも呼ばれた痕跡が残るようにします。

bool acpi_dev_resource_address_space(struct acpi_resource *ares,
				     struct resource_win *win)
{
	struct acpi_resource_address64 addr;

	win->res.flags = 0;
	if (ACPI_FAILURE(acpi_resource_to_address64(ares, &addr)))
		return false;
        pr_info("%s: Called\n", __func__);
	return acpi_decode_space(win, (struct acpi_resource_address *)&addr,
}
bool acpi_dev_resource_ext_address_space(struct acpi_resource *ares,
                                         struct resource_win *win)
{
        struct acpi_resource_extended_address64 *ext_addr;

        win->res.flags = 0;
        if (ares->type != ACPI_RESOURCE_TYPE_EXTENDED_ADDRESS64)
                return false;

        ext_addr = &ares->data.ext_address64;
        pr_info("%s: Called\n", __func__);
        return acpi_decode_space(win, (struct acpi_resource_address *)ext_addr,
                                 &ext_addr->address);
}

最後に、 acpi_pci_probe_root_resources も予想したデバイスが評価されているか表示するようにします。

int acpi_pci_probe_root_resources(struct acpi_pci_root_info *info)
{
        int ret;
        struct list_head *list = &info->resources;
        struct acpi_device *device = info->bridge;
        struct resource_entry *entry, *tmp;
        unsigned long flags;

        flags = IORESOURCE_IO | IORESOURCE_MEM | IORESOURCE_MEM_8AND16BIT;
        pr_info("%s: probe %s\n", __func__, info->name);
        // ...
}

これらの変更を適用したLinux Kernelをビルドして差し替えて起動してみます。
まずは、PCI Root Busを認識している付近のBoot Messageを見てみます。

[   24.084991] acpi PNP0A08:00: ECAM area [mem 0xe100000000-0xe10fdfffff] reserved by PNP0C02:00
[   24.093530] acpi PNP0A08:00: ECAM at [mem 0xe100000000-0xe10fdfffff] for [bus 00-fd]
[   24.101266] acpi_pci_probe_root_resources: probe PCI Bus 0000:00
[   24.201701] acpi_dev_resource_address_space: Called
[   24.206568] acpi_dev_new_resource_entry: win->offset: 0xe000000000
[   24.212736] acpi_dev_resource_address_space: Called
[   24.217603] acpi_dev_new_resource_entry: win->offset: 0x0
[   24.223076] PCI host bridge to bus 0000:00
[   24.227166] pci_bus 0000:00: root bus resource [mem 0xe000000000-0xe07fffffff window] (bus address [0x00000000-0x7fffffff])
[   24.238283] pci_bus 0000:00: root bus resource [mem 0xe200000000-0xffffffffff pref window]
[   24.246535] pci_bus 0000:00: root bus resource [bus 00-fd]

acpi_pci_probe_root_resourcesacpi_dev_resource_address_spaceacpi_dev_new_resource_entry の順番で呼ばれているようですね。Translation Offsetの値が取り出され、resourceが作成されているようです。
次に、実際にVirtIO Network DeviceのBARに値が書き込まれている所を見てみます。

[   24.462843] pci 0000:00:01.0: BAR 0: assigned [mem 0xe000200000-0xe000200fff 64bit]
[   24.470487] pci_std_update_resource: res->start = 0xe000200000
[   24.476308] pcibios_resource_to_bus: window->offset: 0xe000000000
[   24.482389] pci_std_update_resource: region.start = 0x200000

予想通り、Translation Offsetの値がここに伝播し、 region.start の値が変更されています。
検証の結果、予想どおりの挙動を示している事を確認できました。

結論

PCIのBase Address Registerの値を読み書きする際はDSDT/SSDTのAMLをよく読んで、Translation Offsetの値に注意しましょう。

追伸

ACPI被害者の会は随時会員を募集しております。
ACPIのあんな挙動やこんな挙動で悩まされた方は是非、ブログ記事などで「ACPI被害者の会」と言うワードと共に投稿してみてください。

セキュリティキャンプ2023 参加記(講師)

ドッタンバッタン大騒ぎして結局今までろくにまとめられなかったセキュリティキャンプの参加記。
感想も交えて詳しく書けたらいいなと思っています。

セキュリティキャンプ2017参加記

今年もやってきました、あの夏、と言いたいところですが今年は待ちに待った4年ぶりのオフライン開催です。今回は幸運にも講師として参加させていただくことができました。(責任重大)
毎回のごとく、参加記録をまとめたいと思います。
これまでの記録
セキュリティキャンプ2017参加記
セキュリティキャンプ2019参加記(チューター)
セキュリティ・ネクストキャンプ2020参加記(チューター)

朝から適当な食事をすまし、体調を壊しました。アホです。

1日目

京王八王子から分倍河原、分倍河原から徒歩という恐らく誰も移動しないルートできました。

講師控え室に移動してから昼ご飯になりました。

相変わらず、おいしいご飯で体調は一気に良くなりした。
ご飯を食べた後は、受講生の方は名詞交換会をしていました。
13:00から開講式がありました。
相変わらず、有り難いお話ばかりで、受講生の方はとても心に響いたと思います。

開講式の後はLT大会、グループワークがありました。

グループワークの後は夕食と、スイーツ食べながら名刺交換会がありました。
おいしい夕食をモグモグ食べているとスイーツ食べる時間が無くなって、名詞交換せずにモグモグ食べていました、いいのかそれで…

交流会用のスイーツ

お部屋は相変わらずいい部屋でした。

スイーツを食べた後は、早速皆さんでOS開発を取り組みました。1時間半もあったのですが、あっという間に終了。
初日から疲れて23:00に就寝

2日目

朝一番乗り、またも確保しました。
チュータ時にやらかした経験を踏まえ、カーテンを開けて寝るようにしたら日の出と共に目が覚めてしまいました。
ご飯を食べたら早速講義開始です。

講義開始前

皆さん真剣に開発されており、有能なチュータさんのパワーも借りながらデバッグをしていきます。
他の講義の受講生の皆さんがたまに覗きに来られて、活発に名刺交換会が行われていました。
あっという間にお昼ご飯です。

昼ごはん後、もくもくと開発をしていると、あっという間に夜御飯です。
夜御飯はすごい中華でした。

ご飯の後は、企業説明の時間です。
セキュリティキャンプに協賛して下さっている企業のお話です。
ガチガチの企業説明ではなく、会社で実際に対応した内応などを説明されているところが多く、受講生の方は楽しく聞けたと思います。
私も大学院生なのでひっそりと聞いてました。
一番びっくりしたのは、2017年に受講生と一緒に参加した方が協賛企業枠で参加されていて、久しぶりの再会があったことです。

最後の8:30から9:00のホームルームでは、同じくOS自作ゼミの講師のhikalium先生が、講師である私をチュータと間違えまくるので、OS自作ゼミのチュータである1010さんと、名刺ストラップ(講師は緑色、チュータは橙色)と無線機(チュータと一部の講師が持っている)を交換するなどのコスプレをしてました。
他の受講生・チュータの皆様曰く、似合っているらしい、どうして…

そんなわけで2日目もあっという間に終わりました。

3日目

今日は社会見学ですが講師は(一部を除いて)付いていくことが出来ません。大人しく待機です。
IPAの設備を観ることができるらしく、2017年参加時には見たことがないのでとても羨ましいです。
受講生がいない間に洗濯を行います。

B1Fの洗濯機

洗濯の間は、同じ講師の方や協賛企業の方と近況報告を行いました。

その後、持ってきていたTinker Board2というシングルボードコンピュータを維持ろうと思ってたのですが、開発環境が置いてある自宅サーバにつながらなくなるしTinker Board2は起動しなくなるし、さんざんでした…(Tinker Board2は復活した。)

受講生の皆さんが社会見学から戻ってこられる前に講師陣は昼食を食べます。
ラーメン、美味しかったです。

ご飯を食べて待っていると、受講生の方が戻ってこられて早速開発に取り組まれておりました。
頭をフル回転させてできる限りのサポートをしているとあっという間に夕食です。

夜ご飯の後も皆さんすぐに開発に戻り、もくもくと作業されていました。
皆さんの熱意を見ると、なんだか受講生だったころを思い出して懐かしくなりました。

途中、他のコースやNOCの見学に行き、それぞれのコースもとても濃い内容の開発を行われている事を目の当たりにしました。

そんなこんなであっという間に9時、開発終了です。
因みに今日は講師兼チュータという謎の役職がつきました、このネタ気に入ってきました。

4日目

前日寝付きが悪かったですが、またも一番乗りを獲得しました。

開発最終日ということもあり、開発コース受講生の皆さんには目標を達成するために熱が入ります。
皆さん真剣にデバッグを行い、こちらもアップアップになりながら対応します。自分にもっとデバッグ能力があればすっと解決できたのになぁと思いつつネットワークの設定がうまくいかなかったり、元からあるprintfの実装がいい加減なのに気付くのが遅れたり、様々なバグがありました。
そんなこんなであっという間に昼食です。

開発コースの方は急いで食事を終わらせて、開発に戻られている方も多かったです。
開発に熱がこもり熱中しちゃう気持ち、受講生のころを思い出して懐かしく思います。
ラストスパートの開発を終え、「こんな状態じゃ終われない、今日の夜は寝ずにデバッグだぁ」とおっしゃっていた方も多かったです。夜遅くまでやりすぎて寝過ごさないようにね…(思い出すチュータのトラウマ)。
16:00からYトラック内で成果発表会を行いました。皆さん、自分がセキュリティキャンプで実現した事を活き活きと語り、とても輝いていました。
成果発表会の後は、夕食です。

夕飯の後は、LT発表会です。自分も少し発表しましたが、皆さん個性的な発表でとても楽しめました。
LT大会の後はグループワークです。
グループワークでは、それぞれの目的を達成するためにどうするべきかチームで熱心に取り組んでいました。グループワークではなりたての講師なのに色々アドバイスを求められて上機嫌で答えてしまいました、恥ずかしい…
グループワークの後は30分のホームルームで見学に来られる方がいらっしゃいました。
皆さん、OS自作について興味深々で是非ともOS自作をやってほしいと布教しました。
そんなこんなで4日目もあっという間に終了です。
…と思ったら名刺交換会が自然発生し、皆さんの熱気で気温が上がるくらい密集した名刺交換が1時間近く行われました。11:00におやすみです。

5日目

最後の朝食です。6:20に行ったのに4番乗りでした。みんな早い。

ご飯の後は、グループワークです。前半のほうは講義部屋の撤収作業をしていたので聞けなかったですが、後半から聞いても皆さん継続的にセキュリティに取り組もうという心持ちが見られてとても温かい気持ちになりました。セキュリティキャンプの繋がりはのちの人生で役に立つので是非大切にしてもらいたいです。
グループワーク後は急いで移動して集合写真を撮影します。
全国大会・ネクストキャンプ・ジュニアキャンプ・ALLの受講生のみ、講師込みでつぎつぎに写真を取っていきます。皆さんいい笑顔でセキュリティキャンプを楽しめた事がわかります。

撮影後は専門コースの成果発表です。
受講生時代から専門コースとの関わりは薄かったので、毎回新鮮な気持ちで聞いてます。

AクラスはIoTセキュリティコースで、セキュリティがおろそかになりがちなC言語などのプログラミングでいかにセキュリティを担保するかを病院や車載ネットワークを例に検証したそうです。
IoT機器には脆弱性が残っているとのことで、それが社会に与える影響がいかに大きいかを実感しました。ファイルが削除されてもFAT32などでは単に削除マークがついているだけで復元ができるなどOS自作と馴染み深い話もありました。

Bクラスは、Webセキュリティクラスで広さと深さをどちらも追求した講義をされているそうです。
仮想的な企業を立ち上げてその会社になりすましメールを送って権限を得ようとする試みをやって、Webでどのようにセキュリティを担保するか講義とハンズオン両方で理解を深めたそうです。

Cクラスでは、Linux Kernelのエクスプロイトや脅威度の数値化などをローレイヤーミドルレイヤーの解析をウイルスの検知をされていたそうです。オンラインの時期に参加された方が発表されてましたが、毎年講義の内容が変わっており一筋縄ではうまくいかなかったと笑顔で話していました。デジタルフォレンジックなどは攻撃者がいかにシステムに侵入したかを調べる上で幅広いレイヤーの知識が必要だというのが発表者の知識から感じました。

Dクラスでは、ハードウェア・ソフトェアと法律・倫理という二つの観点からAIとセキュリティについて勉強したそうです。近年急速に発達する生成系AIをどう扱うかを議論したり、画像にノイズを加えてAIの判定を間違えさせるという技術よりの話までAIを隅から隅まで研究しておりこれからの時代を生きるのに重要な事を学ばれているな感じました。AIでもAttack And Defenceをされており情報セキュリティはどこの分野にも存在するということを実感しました。中にはPythonを分からない中でAIをいじるためにPythonを勉強した方がいらっしゃって受講生の皆さんの熱意を感じました。セキュリティキャンプを通じてより精力的に取り組もうと言う気持ちになるのはとても良いことだと思います。

ジュニアキャンプでは、自分たちの作りたいアプリをセキュアに作るというプロジェクトを開発していました。自分が中学生の頃にはとても難しくて利用できなかったPHPフレームワークやNode.JSなどを使われていて最近の中学生の凄さに感心しました。しかもその上にしっかりとセキュリティを意識した設計になっており、脱帽モノでした。

ネクストキャンプでは、世界でユニークな事を専門とする講師陣による濃い講義が行われたようです。GPUを低レイヤーの観点から一からプログラムを設計し、高速なプログラムを設計したそうです。GPUのSIMDの最適化は私も勉強途中なので是非受けてみたいものです。GPUの次はCPUを作り上げるという講義で機械語を作る作業などは低レイヤーをやっている身としては共感できました。テスト駆動開発やモブプログラミングなど普段1人で開発しているときは経験しにくい開発スタイルについて学べた様でより柔軟な考え方ができるようになったと思います。

午前中の成果発表はこれで終わりで、セキュリティキャンプ最後の食事が始まります。

午後からは、我々開発コースの成果発表です。
トップバッターはOS自作ゼミです。NICドライバを書かれていた受講生の方が発表されてました。自作OSのデモもやって大盛況で終わりとても大盛り上がりでした。
CPU自作ゼミの方もRISC-Vのカスタム命令を実装し、ユニークな実装をしていました。
分散合意ゼミではRaftという分散合意アルゴリズムの実装されていました。分散合意はコードレベルでは非同期処理で、その中で同期処理を行うのでとても難しいのだと感じました。
CPU+コンパイラ自作ゼミでは、CPUとコンパイラを並行して作るというなかなかハードな事をされていました。Cコンパイラで吐き出したコードをCPUで実行し、結果を得られるのはとても楽しいだろうなと思いました。

次はXトラックでIoT機器の分解・改造をやっているようです。
リバースエンジニアリングゼミでは、デバイスを解体して、基板の読み込みをしたりファームウェアを解析してました、毎年熱い講義です。
電子回路・プリント基板を作ろうゼミでは、JTAG Debuggerを作るというハードウェア自作を行うゼミでした。お米より小さな部品を基板に配置する作業など、はたから聞いているととても難しそうに聞こえます。
ハードウェア魔改造ゼミは恒例の走るルータです。ハードウェアからソフトウェアまで全て弄るのは夢があり自分も一度やってみたいものです。今回は加速度センサを使って制御していて利便性(?)も上がっていました。
無線通信ハッキングゼミでは電波法令に気をつけながら電波を攪乱するジャミング攻撃をしたり、未知の信号波を解析してどういうプロトコルなのかを検証していました。

Zトラックでは、様々な講義が開講されています。
Rust製Linux向けアンチウィルス実装ゼミでは、マルウェアを解析するために実行してログを取得し、解析するコードを作成していました。挙動が正しいか怪しいかを区別する作業に苦心しているようでした。
アイデアを形にするWindowsアンチウイルス実装ゼミでは、Windowsの仕組みを受講生自身が試行錯誤して理解していたそうです。Windows APIは難しいイメージが多く、前のトラックと同じく正常な動作か怪しい挙動かを判定するのに苦労しているようでした。
EDR強化ゼミでは実際にWIndowsPCに攻撃してもらい、それを検知して分析して対応する講義をされていたそうです。検知にはシグマルールというものを作成し、それが上手く動いた時の爽快感が良かったそうです。
ブロックチェーンゲームを作って学ぶWebセキュリティゼミでは、Ethereum VMなどを用いてゲームを作り、ゲームにクリアできたらトークンや、報酬をゲットできるものらしいです。ゲームを作りながらそれをブロックチェーンに結びつけるという作業はとても大変だったらしく、受講生の方はさぞかし熱中できたでしょう。
OSINTによるランサムウェア攻撃者の攻撃戦術特定ゼミでは、検知ルールを用いてどんな製品が狙われたかを検知する講義をされているようです。

Lトラックは暗号系の講義が開講されています。
暗号化通信ゼミでは、学校のネットワークなどSSHなどを通さないネットワークのVPNをTLSでトンネリングするソフトウェアを作成されていました。また、既に破られた暗号を自分の手で破っている方もいました。
暗号のままで計算しようゼミでは、準同型暗号を実際に計算してNAND計算ができる事を確認しているようでした。NANDができれば任意の計算ができます。高速化についても研究されたそうです。
Cコンパイラゼミでは、ゼロからCコンパイラをアセンブリで書くという熱意ある方がいらっしゃいました。残念ながら配列などは実装できなかったですが、アセンブリが分からない中でそこまで作り上げたのはすごいことだと思います。
分散型アプリケーション脆弱性解析ゼミでは、ブロックチェーンへの攻撃を解析する、というゼミのようです。攻撃を解析するためにEthereum VMを実装されており、コンパイラ脆弱性を解析されているとのことでした。
TEEの活用と攻撃実践ゼミでは、Intel SGXを使って信頼可能な実行環境を作るという低レイヤーゼミでした。SGXではEDLという独自の言語で記述しないといけない部分があり大変そうだなと思いました。実際に攻撃も実践しており、攻防どちらも経験しているようでした。

NOCからはNOCの概要と今回の通信の概要が説明されました。

短い休憩時間を挟んで閉講式が始まります。
閉講式ではIPAの方から自身の経験を踏まえた熱いエールを頂き、講師でも心に来ました。また修了証書と激励文の授与が行われていました。受講生代表の皆さんの一言スピーチは心に響きました。
あとはセキュリティキャンプのグループの紹介などがあり、終了宣言を以て終わりました。

感想

初めての講師ということで受講生の皆さんのサポートができるか不安でした。そのためチュータの経験を生かして受講生の皆さんがどういうバグでつまっているかを確認しながらデバッグ作業の補助をしました。受講生の方の中には私が経験したことあるバグで詰まる方がいらっしゃったり、私のやったことのない事をされており、何をしようとしているか理解するのに精一杯だったこともあります。Yコースの講師の方々からチュータと間違えられることも多かったですが、自分自身受講生の皆さんにはチュータみたいに扱ってもらえたらなと思っていたので割と良かったと思っています。終わってみて正直受講生の皆さんが満足できたか不安だったのですが、皆さんが楽しかったとおっしゃっていたので、こちらも嬉しくなりました。ですが、改めて自分の技術力の不足を感じました。これからも進捗を出さないとなと思いました。来年も講師ができるかわかりませんが、より受講生の皆さんをサポートできるようになって参加できたらなと思います。

3回とも違う身分での参加

Open Build System(OBS)で依存関係のソースのリポジトリを指定する

普段、サーバでNAXSIというWeb Application Firewallを使ってるので、せっかくだからディストリビューション開発もくもく会に参加したので、勢いでOBSにリポジトリを作ってみました。 作ったリポジトリがhttps://build.opensuse.org/package/show/home:Manami:nginx-modules/nginx-module-naxsiです。

この際に困ったのが、Nginxのバージョンです。Nginxには公式のリポジトリで提供されている安定版とOBSのServer:httpで提供されている最新版があって、Server:httpではNginxのいろいろなモジュールが公開されていることから、自分は後者を使っています。ところが、普通にSpecファイルでBuildRequiresを書くと安定版のNginxを使用されます。それだと実際にインストールしようとした時にバージョンが違うとコンフリクトを起こすので困ります。そこで、特定のリポジトリから提供されているパッケージを使用してほしいのですが設定にやや戸惑ったので記録しておきます。

まず、リポジトリのページに行き、”Repositories”に移動し、”You can configure individual flags for this package here. The repositories are inherited from the project (Repository Name)”の(Repository Name)のリンクをクリックします。
すると、以下の様に追加したターゲットのRepository pathsが表示されているので、各た0ゲットの”+”ボタンをクリックします。

そこで、リポジトリ名をいれるところがでるので、”server:http”とか”Virtualization”などいれます。Repositoryはターゲットのバージョンに合わせて選びます。

これで、Acceptを押せば、追加されるので、追加されたパスの横にある”↑”ボタンを押して優先度を上げてやれば完了です。

残念ながらプロジェクト毎の調整はできないみたいですが、OBSにはサブプロジェクトというものがあるそうです。(クロスディストロの方々に教えていただきました。)

“Your Home Project”をクリックして、”Overview”の欄にある”Subprojects”をクリックして、右側の”Actions on this page”にある、”Create subproject”をクリックして適宜作ってください。

あと、先にビルドしちゃうとこの変更に伴う再ビルドはされないみたいなのでファイルのアップデートやRelease番号のインクリメントなどで対応してみてください。

Ethernetで一対一でデバイスと通信

ルータなどを介さずにIoTデバイスと直接やり取りする場合に必要な設定などを記録しておきます。
具体的に言うと、本来は IoT Device <—> Router <—> PCと通信するものをIoT Device <–> PCと通信する場合に必要に設定です。環境はopenSUSEを想定しています。

まずdhcp-serverをインストールします。その後設定を以下のように書き換えます。
なお、今回IoT Deviceに割り当てるIPアドレスレンジは192.168.0.2 ~ 192.168.0.100までとし、PC側のIPv4アドレスは192.168.0.1とします。また使用するネットワークインターフェイスの名前は”enp1s0f1″とします。また必要に応じてfirewalldは落としておいてください。

“/etc/dhcpd.conf”に以下のように設定します。

option subnet-mask 255.255.255.0;
subnet 192.168.0.0 netmask 255.255.255.0 {
        range 192.168.0.2 192.168.0.100;
}

次に”/etc/sysconfig/dhcpd”に使用するネットワークインターフェイスを指定します。

DHCPD_INTERFACE="enp1s0f1"

IPコマンドでIPアドレスレンジを割り当ててからDHCPサーバを起動します。

sudo ip addr flush dev enp1s0f1
sudo ip addr add 192.168.0.1/24 dev enp1s0f1
sudo systemctl start dhcpd

IoT DeviceがARPに対応していてIPアドレスの問い合わせのブロードキャストに対して適切に応答してくれる場合はいいんですが、無視してしまうようなデバイスの場合IPアドレス直指定で通信してもPCの方がARPパケットを飛ばしまくって結局通信できない、ということになります。
その場合、無理やりパケットがIoT Deviceに到達するように調整します。

IoT DeviceとのDHCPでのやり取り(Discover・Request・ACK)の中でIPアドレスとMACアドレスがわかるはずなのでそれを記録しておいてください。もしくはデバイスに記載されている場合はそちらを参照してください。
ipコマンド群を実行する前にIoTデバイスのIPアドレスとMACアドレスを変数として以下のように定義しておいてください。

IOT_IP_ADDRESS=192.168.0.2
IOT_MAC_ADDRESS=00:00:5E:00:53:11

DHCPによるIoT DeviceのIPアドレス取得が完了したの見計らってDHCPサーバを停止し、ipコマンドでネットワークの調整を行います。以下のコマンドを実行していきます。

sudo systemctl stop dhcpd
sudo ip addr flush dev enp1s0f1
sudo ip addr add 192.168.0.1/24 dev enp1s0f1
sudo ip route add ${IOT_IP_ADDRESS} dev enp1s0f1
sudo ip neigh add ${IOT_IP_ADDRESS} dev enp1s0f1 lladdr ${IOT_MAC_ADDRESS}

これでARPを飛ばさずにMACアドレスとIPアドレス直指定でIoT Deviceと通信できるようになるはずです。

openSUSEデスクトップ環境設定Etc

久しぶりにopenSUSEのほうをいじったら動かなくなったので色々動かなくなっていたので設定し直した時の備忘録

KDEでIBusが起動しない

~/.config/autostart/にibus-autostart.desktopなどで以下を作成

[Desktop Entry]
Comment[ja_JP]=Start IBus daemon
Comment=Start IBus daemon
Exec=/usr/bin/ibus-daemon -rxR
GenericName[ja_JP]=IBus Daemon
GenericName=IBus Daemon
Icon=ibus-setup
MimeType=
Name[ja_JP]=IBus
Name=IBus
Path=
StartupNotify=false
Terminal=false
TerminalOptions=
Type=Application
Version=1.0
X-DBUS-ServiceName=
X-DBUS-StartupType=
X-GNOME-Autostart-Delay=10
X-KDE-StartupNotify=false
X-KDE-SubstituteUID=false
X-KDE-Username=
X-KDE-autostart-after=panel

手癖で/usr/bin/ibus-daemon -drxRとしていたけど、それはコマンドラインから起動させる場合であってautostartでは”-d”はいらない。

PulseAudioじゃなくてPipeWireを使う

ヘッドホンのコーディックがHFPしか選択できず、音質に耐えられなかったのでごにょごにょしてたら、最近はPulseAudioじゃ無くてPipeWireを使うらしいというわけで以下を実行

Remove: pulseaudio pulseaudio-lang pulseaudio-module-* alsa-plugins-pulse mpg123-pulse pulseaudio-bash-completion pulseaudio-setup pulseaudio-zsh-completion system-user-pulse pulseaudio-utils

Install: pipewire pipewire-moudles-*  pipewire-alsa pipewire-pulseaudio  bluez-auto-enable-devices libdac2 libpulse0 libsbc1 gstreamer-plugin-pipewire

そんでもって一応以下のコマンドを実行

sudo mkdir -p /etc/pipewire/media-session.d/ && sudo touch /etc/pipewire/media-session.d/with-pulseaudio

再起動すると結構豊富にコーディックプロファイルが選べるようになっていた。

アセンブリでラベルを関数として認識させる

OS自作などでアセンブリでなにかの処理を書いた後に、これをCやRustなどから呼び出すことがあると思います。
このようなことをしている時に稀にリンカが超巨大エラーメッセージを吐いてズッコケることがありませんか?私はあります。
さて、このエラーをチンタラ解読するとアセンブリで書いた関数が再配置で呼び出し元の関数と離れすぎて相対ジャンプできないんだけど、みたいなことが書いてあります。例えば以下のようなエラーを吐いている場合などです。

relocation R_X86_64_PLT32 out of range: -549755221402 is not in [-2147483648, 2147483647]

「じゃあアセンブリの関数を呼び出し元の近くに配置してくれればええやんけ…」と思ってもやってくれないので、どうにかするしかないわけです。

手始めにstripなどされてないバイナリをobjdump -xでアセンブリのラベルがどう認識されているかを調べてみます。
アセンブリで

.section .text
hoge: 
        mov rax, rdi
        mul rdi
        ret

などと書いたとします。
すると、objdump -xでは
ffffff8000100000 l .text 0000000000 hoge
などと表示されます。
ここでCやRustで書かれた関数を見ると
ffffff8000200000 l F .text 000000003f foo
という感じの表示になっています。

さて、違いを見比べるとアセンブリで書いた関数は「Fのマークがない」「サイズが0」という点が異なるとわかります。特にサイズが0だと言うことはリロケーションができそうになさそうです。これで関数の再配置ができなくてリンカがコケるのではないかと思うわけです。
ここでアセンブリのラベルを高級言語での関数と同等の表示がされるようにしてみます。

まずは、「F」のマークをつけたいと思います。おそらく「Function」の略でしょうが、これは単に.textセクションに置くだけではつかないようです。
ここでELF形式で出力する場合は、.typeという擬似命令を使用できます。hogeの上に.typeを追加してみます。

.section .text
.type    hoge, %function
hoge: 
        mov rax, rdi
        mul rdi
        ret

これで再度objdumpの結果を見てみると
ffffff8000100000 l F .text 0000000000 hoge
などと表示されているはずです。ひとまずは関数として認識させられたようです。

問題は関数のサイズをどう認識させるかです。これがないと、どこまでがhogeなのかがリンカからわかりません。
ネットを探し回っていると.sizeという擬似命令を見つけました。
これは
.size label_name, size
label_nameのサイズをsizeであると指定する命令のようです。
これでsizeにhogeのretまでのバイト数を明記すれば良さそうです。
ただ、毎度計算するのは面倒なので”.“を使って、楽をします。

.section .text
.type    hoge, %function
hoge: 
        mov rax, rdi
        mul rdi
        ret
.size   hoge, . - hoge

.“は現在のアドレスを示すので、関数の先頭のアドレスを示すラベルとの引き算で関数のサイズが計算できます。

これでビルドしてobjdumpの結果を見ると、
ffffff8000100000 l F .text 0000000007 hoge
というふうにサイズもしっかり記載されてます。
これでhogeがリロケーションされ呼び出し元の近くに配置してリンカのエラーがなくなることもあります。(別のことが原因の場合もある)

この記事の元ツイート: https://twitter.com/PG_MANA_/status/1550674356929724416

Mini Web ServerをC言語で書いた

自作OSでopenとかreadのSystemCallを実装していましたが、何か面白いものを作りたくなったので、最近の自作OSでは一つの目標(?)とされているWebサーバを動かすことを目標とすることにしました。
とりあえず使用するシステムコールを探るためにC言語で超簡易版のWeb Serverを書いてみました。

使用するSystemCallを最小限にするためにfopenやmallocなどは使用せず、open/readなどのSystemCallに近い関数のみ使用するようにしました。本当はmallocは既にOS側でメモリ管理ができているので使いたかったのですがこの世のLinux向けのlibc実装はメモリを確保する際にbrkやmprotectを使用するらしく、現在の自分のOSでは紆余曲折あってLinux ABI互換で、これらの実装がやや面倒だったので(頑張ればできないことはないが)、とりあえず端折ることにしました。

コードは以下のとおりです。

#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

//#define DEBUG

#ifdef DEBUG
#include <arpa/inet.h>
#endif

#define HTTP_VERSION "HTTP/1.1"
#define GET_METHOD "GET"
#define SERVER_HEADER_ENTRY "Server: Mini Web Server"
#define HTTP_200 HTTP_VERSION " 200 OK"
#define HTTP_404 HTTP_VERSION " 404 Not Found"
#define HTTP_500 HTTP_VERSION " 500 Internal Server Error"

int main() {
  puts("Mini Web Server");

  int receive_socket = socket(AF_INET, SOCK_STREAM, 0);
  if (receive_socket < 0) {
    fprintf(stderr, "Failed to open the socket(ret: %d)\n", receive_socket);
    return 1;
  }

  struct sockaddr_in host;
  host.sin_family = AF_INET;
  host.sin_port = htons(8080);
  host.sin_addr.s_addr = INADDR_ANY;
  if (bind(receive_socket, (struct sockaddr *)&host, sizeof(host)) < 0) {
    fprintf(stderr, "Failed to bind\n");
    return 1;
  }

  if (listen(receive_socket, 10) < 0) {
    fprintf(stderr, "Failed to listen the socket.\n");
    return 1;
  }

  while (1) {
    struct sockaddr_in client_address;
    socklen_t client_address_length;
    int client_socket =
        accept(receive_socket, (struct sockaddr *)&client_address,
               &client_address_length);
#ifdef DEBUG
    printf("Client: {\"Address\": \"%s\", \"Port\": %d}\n",
           inet_ntoa(client_address.sin_addr), client_address.sin_port);
#endif
    char buffer[0x1000];
    buffer[sizeof(buffer) - 1] = '\0';

    int received = recv(client_socket, buffer, sizeof(buffer), 0);
    if (received < 0) {
      fprintf(stderr, "Failed to receive data(ret: %d)\n", received);
      return 1;
    }

    /* Check method */
    if (received <= (sizeof(GET_METHOD) - 1) ||
        strncmp(buffer, GET_METHOD, sizeof(GET_METHOD) - 1) != 0) {
      close(client_socket);
      continue;
    }
    size_t pointer = sizeof(GET_METHOD) - 1;

    if (buffer[pointer] != ' ' || buffer[pointer + 1] != '/') {
      close(client_socket);
      continue;
    }
    pointer += 1;
    /* remove "/"(root) */
    pointer += 1;
    char *file_name = buffer + pointer;
    size_t file_name_size = 0;
    for (; (pointer + file_name_size) < received &&
           buffer[pointer + file_name_size] != ' ';
         file_name_size++)
      ;
    pointer += file_name_size + 1;

    if (received <= (pointer + sizeof(HTTP_VERSION) - 1) ||
        strncmp(buffer + pointer, HTTP_VERSION, sizeof(HTTP_VERSION) - 1) !=
            0) {
      close(client_socket);
      continue;
    }
    if (file_name_size == 0) {
      const char index_name[] = "index.htm";
      strcpy(file_name, index_name);
      file_name_size = sizeof(index_name) - 1;
    }
    file_name[file_name_size] = '\0';
#ifdef DEBUG
    printf("URL: %s\n", file_name);
#endif
    /* Create Response */
    int fd = open(file_name, O_RDONLY);
    if (fd < 0) {
      const char not_found_text[] =
          HTTP_404 "\r\n" SERVER_HEADER_ENTRY "\r\n\r\nFile is not found.";
      send(client_socket, not_found_text, sizeof(not_found_text) - 1, 0);
    } else {
      size_t size = lseek(fd, 0, SEEK_END);
      lseek(fd, 0, SEEK_SET);
      if (size > sizeof(buffer)) {
        const char error_text[] =
            HTTP_500 "\r\n" SERVER_HEADER_ENTRY
                     "\r\n\r\nFile size is exceeded the buffer size.";
        send(client_socket, error_text, sizeof(error_text) - 1, 0);
      } else {
        sprintf(buffer,
                HTTP_200 "\r\n" SERVER_HEADER_ENTRY
                         "\r\nContent-Length: %zu\r\n\r\n",
                size);
        send(client_socket, buffer, strlen(buffer), 0);
        read(fd, buffer, size);
        send(client_socket, buffer, size, 0);
      }
    }
    close(client_socket);
  }
  return 0;
}

straceで呼ばれているシステムコールを確認すると起動処理以外では、

  • wrtiev(puts, fprintf)
  • socket
  • bind
  • listen
  • accept
  • recvfrom
  • open
  • lseek
  • sendto
  • read
  • close

のみとなってます。

とりあえずLinuxでビルドしたところまあまあ動いているようのなのでこれが動くように頑張ります。

と言っても、まずはネットワークデバイスのドライバ書くところから始めないといけないのですが…

追記: 一応動きました。 https://twitter.com/PG_MANA_/status/1533812030582292481

MouseProのLTEモジュール(Telit LN940)をLinuxで使う(ATコマンド編)

旅先や出先で大活躍のLTEモジュール付きノートパソコンですが、妙に電波のつかみが悪いので更に調査していたところ別の方法で認識させることができたので記録しておきます。

環境

  • MousePro-NB510HL
  • openSUSE Tumbleweed
  • KDE Plasma Desktop

注意

前回の方法(MouseProのLTEモジュール(Telit LN940)をLinuxで使う)をすでに適用されている場合は一旦取り消して再起動してください。
具体的には以下のコマンドを実行してください。

sudo rm /etc/udev/rules.d/15-lte.rules
sudo systemctl disable wwan-sleep-hook
sudo rm /etc/systemd/system/wwan-sleep-hook.service

また今回の方法はモジュールのファームウェアの設定を変更します。そのため場合によってはモジュールが動作不能になる恐れがあります。以下の内容は全て自己責任で行ってください。

手順

まずはlibqmiやmodemmanagerをインストールします。
ModemManagerは起動させずに停止させたままにします。
(sudo systemctl stop ModemManager)

次に/dev/ttyUSB2に接続します。(screenコマンドがない場合はインストールしてください)

sudo screen /dev/ttyUSB2 115200

接続したらATE1と入力し改行します。(アルファベットは大文字で)、リターンキーを押すまでエコーバックはないのでお気をつけください。この作業でエコーバックを有効にします。成功すればOKと返ってきます。

(ATE1と入力し改行)
OK

次にATIと入力し改行してみます。成功すれば各情報とOKが返ってきます。(改行)はリターンキーのことを指します。

ATI(改行)

Manufacturer: QUALCOMM INCORPORATED
Model: 4105
Revision: (ファームウェアのリビジョン番号)
+GCAP: +CGSM

OK

モデルが4105であることを確認し次の作業に進みます。
AT^SETMODE?で現在のファームウェアの状態を確認します。成功すれば現在のファームウェアのモードとOKが返ってきます。マニュアルによれば0がNormal Mode、1がExtension Mode、2がLegacy Modeだそうです。

AT^SETMODE?(改行)
^SETMODE: 0

OK

次にモードを1(Extension Mode)に変更します。
AT^SETMODE=1でモードを変更します。成功すればOKが返ってきます。

AT^SETMODE=1
OK

ここまで来たらCtrl+a, k, yの順に入力しscreenを終了し、最後に再起動します。


再起動後ModemManagerを起動するとKDEのネットワーク設定でLTEモジュールを認識していることを確認します。
あとはMouseProのLTEモジュール(Telit LN940)をLinuxで使うを参考にKDEの設定を行ってください。
まれに接続が成功してもDNSの名前解決ができない場合があります。(curl https://1.1.1.1/ は接続できるけど curl http://example.com はできないなど)
この際は、resolv.confにPublic DNSを追記してやると接続できるようになる場合があります。

sudo sh -c "echo nameserver 1.1.1.1 >> /etc/resolv.conf"

以上の方法でusb_switchを使う場合よりより安定してつながるようになりました。
もし、不具合が発生した場合は上記の手順をAT^SETMODE=0に変更して行えば元に戻るはずです。