それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

Xbox ゲーミングバーで動画が保存されない。

video フォルダに保存されない。

ビデオ・フォルダに保存されるものだと思いこんでした。

設定-> Gaming -> captiure

ここで示されるフォルダに保存される。

この設定を変えれば、保存先を変えられそうですが。

そこまで頻繁に使うものでもないので、フォルダを開けばいい。

そして、初期設定が ユーザの一時フォルダだから特にに気にしないことにする。

laravel のNotificationをデータベースに入れる

Notificationをデータベースに入れる

Notificationはジョブキュー的に処理されるし、別にメール通知とは限らない。

そもそもイベント・リスナみたいなもの。イベントが発火してリスナが受け取るのと同じように、通知が発生して通知連絡のクラスが起動する。

今回は、Notificationをデータベースにキューイングして構造を見ておこうと思う。

実験環境を準備する。

プロジェクトを作る

composer create-project laravel/laravel database-notification-sample
cd database-notification-sample
sed -i config/app.php -e 's|UTC|Asia/Tokyo|'
sed -i .env -e 's/DB_/#DB_/' -e '/mysql/i DB_CONNECTION=sqlite'
php artisan migrate

通知をやり取りするテーブルを作成する。

php artisan notifications:table
php artisan migrate

クラスを作る。送出側

まずは、通知を送る側クラスを作る。

通知の本体を作る

php artisan make:notification MyNotification

このような感じで作られる。

app/Notifications/MyNotification.php

<?php

namespace App\Notifications;

use App\Models\MyModel;
use Illuminate\Notifications\Notification;

class MyNotification extends Notification {

  public function __construct( protected MyModel $model) { }

  public function via(){
    return ['database'];
  }
  public function toArray(){
    return ['testing' => 'Test'];
  }
}

通知を送る側を作る

通知を送る側は、コマンドとして作り、任意のタイミングで実行できるようにする。

php artisan make:command SendNotification

app/Console/Commands/SendNotification.php

<?php

namespace App\Console\Commands;

use App\Models\MyModel;
use Illuminate\Console\Command;
use App\Notifications\MyNotification;
use Illuminate\Support\Facades\Notification;

class SendNotification extends Command {
  
  protected $signature = 'app:send-notification';
  
  public function handle() {
    $model = MyModel::first();
    Notification::send($model,new MyNotification($model));
  }
}

サンプルのモデルを作る

通知は、モデルに対し行われる。なので、モデルを用意する。

php artisan make:model MyModel -m

app/Models/MyModel.php

use Notifiable; で通知を受け取るフラグを立てる。

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;

class MyModel extends Model {
  use Notifiable;
}

データベースに通知を置く

データベースに通知を置くのが本題なので、通知を設置するテーブルを用意する。

Seederを作る

php artisan make:seeder MyModelSeeder

Seeder を実行する。

php artisan migrate
php artisan db:seed --class MyModelSeeder

database/seeders/MyModelSeeder.php

seeder は次のようにして、いくつかモデルのレコードを用意した。

<?php

namespace Database\Seeders;

use App\Models\MyModel;
use Illuminate\Database\Seeder;

class MyModelSeeder extends Seeder {
  
  public function run():void {
    foreach (range(0, 10) as $idx) {
      ( new MyModel() )->save();
    }
  }
}   

通知を送る。

コマンドからモデルへ通知を送信する。

php artisan app:send-notification

通知を送出後にDBのテーブルを確認する。

id type notifiable_type notifiable_id data read_at created_at updated_at
a3df8a96-f7ae-44ca-bf55-ba94662128f1 App\Notifications\MyNotification App\Models\MyModel 1 {"testing":"Test"} 2024-02-13 14:39:13 2024-02-13 14:39:13

通知を見ればわかるが、どのモデルのID番号へ、通知を送る。そしてReadフラグ(日付)をつけて区別している。これで、データベース通知の仕様が見えてくる

model <- notification 

通知を読み取る。

通知の処理をする起動ポイントを作る。

php artisan make:command ReadNotification

app/Console/Commands/ReadNotification.php

<?php

namespace App\Console\Commands;

use App\Models\MyModel;
use Illuminate\Console\Command;

class ReadNotification extends Command {
  
  protected $signature = 'app:read-notification';
  
  public function handle() {
    /** @var MyModel $model */
    foreach (MyModel::all() as $model) {
      /** @var \Illuminate\Notifications\DatabaseNotification $notification */
      foreach ($model->notifications as $notification) {
        dump($notification->type);
        dump($notification->data);
      }
    }
  }
}

通知の読み取りを実行する

実行結果

# app/Console/Commands/ReadNotification.php:18
"App\Notifications\MyNotification" 
# app/Console/Commands/ReadNotification.php:19
array:1 [
  "testing" => "Test"
] 

覚えること。

DatabaseNotificationは、notificationsテーブルmodelとモデル・リレーションしている。

mophyを使ってる。

Notificationはモデルへ通知するのが基本。

ReadAtをnull / not null を使ってフラグとして管理している。

マニュアルを一読すると、「イベント」のように見えるけど、イベントとはぜんぜん違う仕組みだとわかる。

どこにもキューが出てこないし、queue:workも出てこない。

通知(notification)を消すには、通常のデータベースと同じ処理になる。

DatabaseNotificationは次のように定義されている

class DatabaseNotification extends Model

xxXXXedと書かれるので、イベントのように見えるがイベントではないし、Notificationのチャンネルがあるわけでもない。よってこれは、ただの1対多のモデル・リレーションである。

モデルに「メッセージ(Data)」を紐づけて送っている。モデルにイベントを通知すると考えると、データベース通知は、やや過剰な仕様であり、Dataにデータを紐付ける程度のものである。

サンプルやドキュメントで「InvoicePaid」「XXXCreated」みたいな通知クラスをみかける。まるでイベント名であるが、イベント・リスナの枠組みではないとわかる。メール・Slackと、データベース通知は、2つはまったく違う。Slack・Mainではキューも使っている。一方でデータベース通知はテーブルに書くだけであり、キューの枠組みではないとわかる。これは初見殺しで、ややこしい。

この違いは、use ShouldQueue;use Queueable; の有無である。データベース通知をキューにいれるかは、コーディングで明確に書く必要がある。ただ、データベース通知ではキューをは必要がないので特に書かれない。

2024-05-30

下書きから更新

Laravelのイベント・リスナをDB利用で非同期に分離動作する。

Laravelのイベント・リスナを分割する。

データベースを使ってイベント・リスナを分割する。

データベース(SQL)を使って、イベントをキューに登録し、queue:work でリスナがイベントを拾って動作させる。

データベースにイベント発生を登録してジョブキューとワーカーにする。イベントハンドラとイベントを動作タイミングを分離してレスポンスを高速化したり、後でいいものは後ろに回す。

プロジェクトを作る

まず、プロジェクトを作成して実験環境を整える。

composer create-project laravel/laravel database-listener-sample
cd database-listener-sample
sed -i config/app.php -e 's|UTC|Asia/Tokyo|'
sed -i .env -e 's/DB_/#DB_/' -e '/mysql/i DB_CONNECTION=sqlite'
php artisan migrate
## キューをDBに入れる。
sed -i .env -e 's/QUEUE_CONNECTION=sync/QUEUE_CONNECTION=database/'

イベントとリスナを作る

ここで作ったリスナが、php artisan queue:work でリスナを受け取って実行する。

また、イベントを発火させるコマンドを作る。

php artisan make:event MyEvent
php artisan make:listener MyListener
php artisan make:command AddJob

コマンドからイベントを発火させる部分

ここでは、手動でイベントを発火させるコードを記入しておく。

<?php

namespace App\Console\Commands;

use App\Events\MyEvent;
use App\Listeners\MyListener;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Event;

class AddJob extends Command {  
  protected $signature = 'app:add-job';
  public function handle() {
    Event::listen(MyEvent::class,MyListener::class);
    Event::dispatch(new MyEvent());
  }
}

イベント

ここでは、特に何も書かない。イベントとしてクラスを定義するだけ

<?php
namespace App\Events;

use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;

class MyEvent {  
  use Dispatchable, InteractsWithSockets, SerializesModels;
}

リスナ定義

リスナはイベント発火時に動作をする。イベントはデータベースから引き取るためにconnection を入れる。

<?php
namespace App\Listeners;

use App\Events\MyEvent;
use Illuminate\Contracts\Queue\ShouldQueue;

class MyListener implements ShouldQueue {
  
  public $connection = 'database';
  public function __construct() {}
  
  public function handle( MyEvent $event ):void {
    dump([$this, $event]);
  }
}

DBの作成

キューをためておくテーブルを作る。

php artisan queue:table
php artisan migrate
## または php artisan migrate:fresh

準備完了

ここまでで、キューテーブルをつくり、テーブルでイベントの受け渡しをするListernを作り、イベントを発火させるコマンドを作った。

実験

コマンドを実行して、イベント発火する

php artisan app:add-job

ここでは、イベントを発火させるだけ。

ジョブの登録を確認。

イベントはジョブとしてキューに貯まるはずので、それを確認する。

sqlite3 database/database.sqlite 'select * from jobs;'
-- Loading resources from /Users/takuya/.sqliterc
id  queue    payload                               attempts  reserved_at  available_at  created_at
--  -------  ------------------------------------  ----  -----------  ------------  ----------
1   default  {..."App\\Listeners\\MyListener",...  0                      1707798204    1707798204

リスナを動作させる。

キューに溜まったイベントをリスナで動かす。

php artisan queue:work

コマンド実行すると、ワーカーが、キューを読み込んで、リスナに処理をやらせてくれる。

INFO  Processing jobs from the [default] queue.

  2024-02-13 03:46:10 App\Listeners\MyListener ......... RUNNING
  2024-02-13 03:46:10 App\Listeners\MyListener ......... 8.69ms DONE

あとは、画面を見ながら、イベントを発火せてリスナが処理をするのを眺めていれば、処理機構の枠組みがわかってくる。

覚えるポイント

キューを使ってイベント・リスナの処理を分割するときのポイント。

リスナをデータベースに入れる。

$connection = 'database' を使ってイベント発火し、リスナに割り当てる(dispatch)するとき、実行せずにデータベースに放り込む。

<?php
class MyListener {
    public $connection = 'database';
}

リスナを「動作可能にする」

laravel ではClass::handle()Class::__invoke()を用意すれば、クラスを「実行可能」と定義できる。

<?php
class MyListener implements ShouldQueue {
    public function handle( ...);
}

イベント発火の方法

次はほぼ同じ。

Event::dispatch(new MyEvent());
event(new MyEvent());

イベントとリスナを紐付ける方法

サービスプロバイダで定義してもいいし、Event::listen を使ってもいい。

サービスプロバイダを使うときはEventServiceProviderにかくか、AppServiceProvider#boot()にかく。

Evnet:listen はFacedeに定義されている。\Illuminate\Support\Facades\Event がクラス名である。

キューをデータベースに入れる。

sed -i .env \
  -e 's/QUEUE_/#QUEUE_/' \
  -e '/CONNECTION=sync/i QUEUE_CONNECTION=database'

envに書かずに、動かすには、自分で書くしかない。

データベースに入れ設定を先に書いておかないと、queue:work によるキュー動かないので注意。

2024-05-30

下書きから更新。

SQLiteでcurrent_time

sqlite で current_time

注意点がある。文字列が主体のsqliteなので、暗黙的文字列キャストが入ったり、datetimeにはタイムゾーンが付与されずUTCである点の2点に配慮すること。

current_timestamp がついているカラムがあって。

sqlite3 database/database.sqlite 'select deleted_at from jobs';
-- Loading resources from /home/takuya/.sqliterc
deleted_at
2024-03-16 06:26:50
2024-03-16 07:26:50
2024-03-16 08:26:51
2024-03-16 08:26:51
2024-03-16 02:42:12

datetime を入れた場合

更新していると、ついついそのまま入れたくなるが、これは、UTCである。

update jobs set deleted_at = datetime() where name = "awxlXViQcg"
 ```

## CURRENT_TIMESTAMP はUTC

SELECT CURRENT_TIMESTAMP ; CURRENT_TIMESTAMP 2024-05-30 03:28:33



## localtime を参照する

SELECT time(current_time, 'localtime') local_time, current_time; local_time|current_time 12:27:22|03:27:22

## 現在の標準時帯に変換する。

select datetime(current_timestamp, 'localtime'); datetime(current_timestamp, 'localtime') 2024-05-30 12:31:52

特に指定しないといい感じに、UTCをLocal変換してあげないといけない

SQLite はちゃんとUTC時刻を扱えるので、カラムにローカルタイムを入れないように注意しないといけない。

## 参考資料

- [https://www.sqlite.org/lang_datefunc.html]
- [https://www.sqlitetutorial.net/sqlite-date-functions/sqlite-current_time/]





## 2024-05-30 追記

ローカルタイムについて記載

php でクラスが呼び出す関数をスタブにしたいとき require を工夫すればいいが、laravelでは面倒が起きる

php でクラスが呼び出す関数をスタブにしたいとき

コードテストを書いていて、クラスが呼び出すクラスのメソッドをダミー化してたいとき、スタブを使う。

スタブを作るには、クラスが呼び出すクラスのメソッドを書き換える必要がある。phpでは動的なクラス定義変更ができない。

結論 require_once を弄くればいい。

require_once されているファイルを動的に差し替えればテストが可能。

当たり前なのだけど、気づかないときは気づかない。ちょっと盲点かもしれない。

mockery でやる

また、その実現のために、Mockery が使える。

<?php
// モックでスタブを作る
$mock = Mockery::mock('alias:'.MyChecker::class);
$mock->shouldReceive('is_src_exists')->andReturn(faklse);

たとえば、次のようなクラスがあって。クラスが例外を吐くことをテストしたい。とする

MyChecker::is_src_exists というメソッドを定義して。

<?php
class MyChecker {
  public static function is_src_exists( $path ) {
    return realpath($path) && is_readable($path);
  }
}

MyFileClassクラスの例外送出テストしたいとする。

<?php
class MyFileClass {
  public static function move($src,$dst){
    if (false == MyChecker::is_src_exists($src) ){
      throw new RuntimeException("src not found");
    }
    rename($src,$dst);
  }
}

テスト対象はMyFileClass である。MyChecker::is_src_exists を呼び出すMyFileClassがテスト対象である。

上記のクラスをテストして、例外が起きることをテストしたい

<?php 
class MyFileClassTest extends TestCase{
  public function test_file_move_function_raise_exception(){
    $this->expectException(RuntimeException::class);
    MyFileClass::move('/no_exists','/tmp');
  }
}

php では、動的に関数を上書きできない。runkitを使えばできなくもないが、環境整備も面倒だ。

動的に上書きできないし、フラグや引数やENVをソースコードに書くのも避けたい。 環境変数の切り分けコードをテストのために追記すると、コード全体の見通しが著しく悪化する。

だったら、かわりに、require_once をうまく使えばいい。

<?php 
require_once 'mock_is_src_exists.php';
class MyFileClassTest extends TestCase{ // 以下略

mock_is_src_exists.php

<?php 
// スタブとして代用するクラス定義
class MyChecker {
  
  public static function is_src_exists( $path ) {
    return false;
  }
}

テストしたいモックオブジェクトを作るのではなく、モックオブジェクトの定義を本来の名前でファイルに書き出して、それをrequire してしまえば、解決するのである。

require_onceを使った解決

// mock としてのロードをできるファイルを作る
function create_mock() {
  $temp = tempnam();
  file_put_contents($temp, <<<'EOS'
    <?php 
    class MyChecker{
      publick static function is_src_exists($path){ 
        return false;
      }
    }
  EOS);
  require_once $temp;
  register_shutdown_function(fn()=>@unlink($temp);
}
// テスト前に実行する。
create_mock();

ただ、どうしても煩雑になる。

どうやら、これを快適にやってくれるのが、Mockey 関連であるようで、 phpunit が使っているものらしい。

*1

Mockery を使ったショートカット

require をもっと便利に動的に作れたら便利だろう。それがMockeyらしい。

冒頭で出てきた、mockey のサンプルコードがそれに当たる。

<?php
// モックでスタブを作る
$mock = Mockery::mock('alias:'.MyChecker::class);
$mock->shouldReceive('is_src_exists')->andReturn(faklse);

Mockery で alias:MyClassoverload:MyClass などしたらrequire を変えていい感じにスタブができる。

ただし、php はrequireされたクラスをアンロードできないので。別の箇所でロード済みだと名前が被ってエラーになる。

Could not load mock MyChecker::class, class already exists

そこで、phpunit をprocessIsolation で動かして、テストごとにファイルロードをリセットしてから実行するようにする。

アノテーションを使ってphpunit に指示を出す。

@runTestsInSeparateProcesses@preserveGlobalState disabled を使えばいい。

<?php
use PHPUnit\Framework\TestCase
class MyFileClassTest extends TestCase {
  /**
  * @runTestsInSeparateProcesses
  * @preserveGlobalState disabled
  */
  public function test_file_move_function_raise_exception() {
    // 例外が起きることをテストする
    $this->expectException(RuntimeException::class);
    // モックでスタブを作る
    $mock = Mockery::mock('alias:'.MyChecker::class);
    $mock->shouldReceive('is_src_exists')->andReturn(faklse);
    // メソッドを呼び出すクラスをテストして、例外を送出させる。
    MyFileClass::move('/no_exists', '/tmp');
  }
}

これで無事にモック(スタブ)を使って、クラスが使ってる関数やクラスのメソッドを上書きして、テストコードを実行することができるわけだ。

phpでクラスが呼び出しているクラスの中にある関数やメソッドにダミー値を返却(スタブ)して、作ったクラスの条件判定をすべて網羅してテストしたい。

ネットワークや外部依存が増えた

現代では、プログラミングがオフラインの自己環境で解決しない時代。ネットワークや外部リソースにほぼ必ず依存する。

例えば、DNSHTTPSなど外部のコンテンツを取得する関数に固定値を返却させてテストコードが現実世界に依存しないように記述したい。たとえばDNSのAレコードを変更するツールを作っているとして、コードのテストのために、実際のDNSレコードを書き換えに行くなどは、非現実的である。別の手段として、テスト用にDNSコンテンツサーバーとDNSゾルバを組み込んだDocker環境を作り管理するとか変態的な手間がかかる。どれも正気なテスト実現方法とは言えない。考えるだけでおぞましい。だからモック(スタブ)が必要である。

動的変更は、型付や高速化と相反する

そこで、ある程度メソッドが「固定値」を返すようにスタブを作れば、テストコードのプログラミングが圧倒的に楽になるわけです。

Pythonrubyなど「ゆるい」言語は何でも書き換えられるので、メソッドを上書きしてしまえばいいわけですが。ある程度の「型付」言語はそれを許してくれないし、厳密なコードから離れてしまう。PHPでやろうとすると、クラスを動的に書き換える必要がある。しかし、PHPはそれを許してくれない。高速化の足かせになるだろうし型チェックがききづらくなる。runkitという手もあるがそれもちょっとと思える。緩さと厳密さの板挟みの痛し痒しである。

laravel の場合

laravel の場合、サービスプロバイダでインスタンスを作るという役割を上手に活用することで、モックを作ることができるのだが。

<?php

namespace Tests\Feature\Rules;

use Mockery;
use Tests\TestCase;
use Mockery\MockInterface;

class CheckSrcRuleSeperateTest extends TestCase {
  
  public function test_check_src_rule_has_enough_space(){
    $this->instance(
      MyChecker::class,
      Mockery::mock(MyChecker::class, function (MockInterface $mock) {
        $mock->shouldReceive('is_src_exists')->andReturn(true);
        $mock->shouldReceive('storage_has_enough_space')->andReturn(false);
      })
    );
    // テストコード
    $rule = ['path' => new CheckSrcFile()];
    $v = $this->app['validator']->make(['path' =>test_data('60min.mp4') ], $rule);
  }
}

ただし、「サービスプロバイダ」でインスタンスなので、staticなものをスタブすることはできない。

また、use Tests\TestCase;でテストで使ってるIlluminate\Foundation\Testing\TestCaseuse CreatesApplication している箇所があり、app()でたくさん登録している。

そういう経緯からかphpunit の processIsolation が動かない。

つまり、laravel でh static なものは、スタブ作成が不能

laravel では、static メソッドをスタブにしたテスト不可能

まとめるとこういうことになる。

スタブ作成にはMockery をつかう
-> Mockery は require を使う
-> Mockery は require のために、processIsolation が必要
-> laravel では artisan test から app()を使ってる
-> laravel では app()を使うので、processIsolation はサポート不可能
-> laravel では mock はオブジェクトに限ってサポートされている
-> mockオブジェクトは、サービスプロバイダを経由する必要がある。
-> static::method() はスタブできない。

なので、static::method()をスタブ化したテストはlaravelではかけない。

とくに、laravel 公式で「processIsolationは動かない」と書いてあって絶望するなど。

まぁめんどくさいですよね。phpの限界だろうね。

*1:mockey を使っているとrequire関連のエラーが出まくるので、requireを動的にやっていると想像した。コードは見ていません。

luksをLVMで使うと Devices have inconsistent logical block sizes (512 and 4096).になった

luks のディスクを追加しようとしてエラー

vgextend mydata /edev/mapper/crypt02

エラーが出た

Devices have inconsistent logical block sizes (512 and 4096).

エラーはセクタサイズです。

はて?ファイルシステムでフォーマットしてないのにセクタサイズとは?

調べたらLUKSにはセクタサイズがあった

セクタサイズを調べる。

sudo cryptsetup luksDump /dev/sdd
Data segments:
  0: crypt
        offset: 16777216 [bytes]
        length: (whole device)
        cipher: aes-xts-plain64
        sector: 512 [bytes]

比べてみると・・・

 sudo cryptsetup luksDump /dev/sde
Data segments:
  0: crypt
        offset: 16777216 [bytes]
        length: (whole device)
        cipher: aes-xts-plain64
        sector: 4096 [bytes]

Keyslots:

ああ、ずっと昔から、luks を使ってるからデフォルト値を変えたの忘れている。

そうか、昔作ったLVMは、gitなど細かいデータのバックアップが多いからセクタサイズを小さくしたんだった。

ブロックデバイスのセクタを見ても解決しない。

luksDumpで見て、セクタサイズが小さいことがわかるが。

sudo cryptsetup luksDump /dev/sde
Data segments:
  0: crypt
        offset: 16777216 [bytes]
        length: (whole device)
        cipher: aes-xts-plain64
        sector: 512 [bytes]

その他のデバイス経由だとわからない

バイス マッピングのツリー確認

バイスマッピングのツリーは次のようになっているので。

lsblk /dev/sde
NAME                 SIZE FSAVAIL FSUSE% FSTYPE
sde                      7.3T                      crypto_LUKS
└─crypt-disk10  7.3T                       LVM2_member
    └─data           12.7T    1.6T    87% ext4

セクタを確認

それぞれ、デバイスを見ても、ファイルシステムをみても4096が返ってくるだけった。

$ sudo blockdev --getpbsz /dev/sde
4096
$ sudo blockdev --getpbsz /dev/mapper/data
4096

私は、暗号化ディスクを512として、その上のext4 のセクタを4096にするというムダをやっていた。

luksFormatのやり直し

セクタサイズが異なる場合はluksFomat からやり直しです。

sudo cryptsetup luksFormat --sector 512 /dev/sde
```


### ともかくセクタに注意

luksにもセクタサイズがあります。

nft のコマンドの使い方の例。NFTテーブル操作ガイドを作ろう

nft 使い方まとめ

nft の使い方は、本当にややこしい。覚えることが多くてる辛いので、調べたことをまとめ直す。

ルール閲覧

"コマンド" "役割"
nft list ruleset 全部見る
nft- a list ruleset 管理番号を含める

全体を見渡すコマンドがコレ。ruleではなくruleset

テーブル

"コマンド" "役割"
nft list tables テーブル一覧
nft add table TABLE テーブル追加
nft create table TABLE テーブル追加(既存チェック有り)
nft list table TABLE テーブル中身表示

TABLE<?family> <name> で定義される。

<?family> <name>inet mytableのように、アドレスファミリを付ける場合とつけない場合がある。作成時にアドレスファミリをつけたら以降は全部つけて識別する。作成時の省略なら以後は省略して作業する。

<?family> に使えるのは、inet | ip | ip6 | arp | netdev | bridgeであるが、パケット転送に限ればip, ip6, inet である。inetip,ip6の両方を含む。

add と create の違い

$ nft add    table ip mytable 
$ nft add    table ip mytable 
$ nft add    table ip mytable # 何度もaddでも既存があればスキップ
$ nft create table ip mytable # create は既存があればエラー

チェイン

"コマンド" "役割"
nft list chain TABLE CHAIN チェイン一覧
nft insert chain TABLE CHAIN '{ <rule> }' チェインの先頭にルールを追加
nft add chain TABLE CHAIN '{ <rule> }' チェインの末尾にルールを追加

nft -a list chain inet fw4  srcnat_VmNet

チェインにルールを追加する例

nft -a list chain inet fw4  srcnat_VmNet
nft add chain inet fw4 srcnat_VmNet { oifname "eth2" ip saddr 192.168.11.0/0 counter packets 0 bytes 0 masquerade comment "!fw4: my-sample" \; }

ルール

"コマンド" "役割"
nft insert rule TABLE CHAIN <rule> チェインの先頭にルールを追加
nft add rule TABLE CHAIN <rule> チェインの末尾にルールを追加
nft delete rule TABLE CHAIN handle <ID> ハンドルを指定して消す。
nft add table TABLE { flags dormant \; } ルールを一時休止

ルールの削除は、今のところ、ルールの削除は、Handle <ID>をつかって指定する。

指定したルールの後ろに追加する方法は現在発見されていない。ルールを作り直すほうが確実。

ルール削除はHandle ID

ルール削除は次の通り

nft delete rule TABLE CHAIN handle `<ID>` 

コメント活用したIDの探索

書いたルールがどのハンドルになっているか探すのが大変煩わしい、そこでコメントを使う方法を考えた。

コメントを利用した削除例

例えば、こんな感じに頑張る。

## 追加して
nft add rule inet fw4 srcnat oif eth0 accept comment mycomment      
## コメントを探す
nft -a list chain inet fw4 srcnat | grep mycomment
## コメントから handle を取り出し
HNDL_ID=$(nft -a list chain inet fw4 srcnat | grep mycomment | grep -oP '(?<=handle )\d+')
## ID指定で消す
[ -n $HNDL_ID ] && nft delete rule inet fw4 srcnat handle $HNDL_ID

いずれは、次のような記述も可能になると思うが、まだ無理っぽい。

nft delete rule inet fw4 srcnat oif eth0 accept comment mycomment      

コメントを用いたルールの削除と、JSON出力を組み合わせて頑張ろう

テーブルの追加削除

テーブル存在確認と作成と削除

## 作成  ( add または create )
nft add table ip mytable
## 確認
nft list table ip mytable
## 削除
nft delete table ip mytable

追加の失敗例 :既存がある。

nft create table ip mytable
Error: Could not process rule: File exists
create table ip mytable
                ^^^^^^^

追加の失敗例: 文法エラー

nft create table ip mytable chain
Error: syntax error, unexpected chain, expecting end of file or newline or semicolon
create table ip mytable chain
                        ^^^^^

チェインの追加と削除

## 作成 ( add または create )
nft add chain ip mytable srcnat
## 確認
nft list chain ip mytable srcnat
## 削除
nft delete table ip mytable

srcnat は、予約語なので注意

テーブル名は自由に決められるが、予約語がある。詳しくは、man nftCHAINSを参照すること

予約語については公式ページも参考になる。Priority_within_hook

MASQUERADE を作る例

テーブルにチェインを追加

nft add table ip mytable
nft add chain ip mytable srcnat 
nft add chain ip mytable srcnat_vpn
nft add rule ip mytable srcnat  oifname "eth2" jump srcnat_vpn comment jump-to-my-vpn
nft add rule ip mytable srcnat_vpn ip saddr 192.168.11.0/24  masquerade comment '"srcnat to my vpn';

いくつか、ポイントを解説。

meta nfproto ipv4 oifname "eth2" と書きたいところだが、ip mytable と、v4 限定のテーブルを書いているので、不要である。

コメントで、クォートしている。次のようにコメント記入しているcomment '"srcnat to my vpn'、コレは、bashにより、展開されるので、確実に文字列を引き渡す必要があるため。

確認

$ nft list table ip mytable
table ip mytable {
    chain srcnat {
      oifname "eth2" jump srcnat_vpn comment "jump-to-my-vpn"
  }
  chain srcnat_vpn {
    ip saddr 192.168.11.0/24 masquerade comment "srcnat to my vpn"
  }
}

動作チェック方法

counterを入れてマッチしたパケットの個数をカウントすればいい

nft add rule ip mytable srcnat  oifname "eth2" conter jump srcnat_vpn

counter を入れると、バイト数とパケット数がカウントされる。

丸暗記ポイント追加と削除

追加と削除が、ぱっと見てわかりにくい。

発展途上のコマンドのためサブコマンドが混乱を招きやすい体系である。留意しながら丸暗記しないと大変である。もしかしたら手作業による管理がnftに想定されてないのかもしれない。溜息が出るほどとっつきにくい。

追加と削除がわかりにくいので、とくに致命的じゃないかなって思う。

心配なのは、いま苦労して覚えても、数年後にはガラッとコマンド体系が改められそうと懸念している。懸念でるほど煩雑だ。

ルールの追加がわかりにくい

ルールの追加は add chain table chain { <rule> } 、ルールの削除は delete rule table chain handle <ID>

と思いきや、ルール追加は、add rule table chain <rule> でも行ける。

ルールの削除がわかりにくい

そして、削除はdel では無い。deleteである。add と来たら、ついついdelと打ちがち。しかしdelは存在しない。

愚痴をこぼすと、nftablesをnft って省略するのに、delete をdel って省略したらだめってのはひどい。nft は某用語なので検索しづらい。delを認めないのであれば、同じように nft コマンドではなく、nftables コマンドにしてほしい。

[add | insert] , [add | create] のペアがわかりにくい

テーブルとチェインは、add / create で、ルールはadd / insertになる。

初見殺しである。カジュアル利用の私達は、そんな違いをいちいち覚えてられない。めんどくさい。

MWANのためにデフォルトGWを複数にしたらい問題が生じたので単純な冗長化をした

mwan パッケージで冗長化しようとした。

OpenWRT+mwan3でWAN回線を冗長化する

コレを読んでたら、なんだ簡単じゃん。って思ったので試したのですが。

デフォルトGWを複数にしたら問題が生じた

Global IPを取得する機能

こういうのがいくつかの場所で使われている。

. /lib/functions/network.sh
network_flush_cache
network_find_wan WAN_IF
network_get_ipaddr IP_ADDR "${WAN_IF}"

OpenWrt の内部では、WANを探すのにuse defaut gateway ( routing 0.0.0.0/0 )のフラグの解釈結果を探索し、ルートに0.0.0.0/0 を設定しているインターフェイスを探索し、みつかったら探索終了というスクリプトがあり、各所で使われている。つまり2個め以降は探索されない。

そのため、mwanを使うと、どこかのプログラムで齟齬が生じる可能性がある。

このことから、mwan 機能はいくつかのパッケージに影響をあたえ誤作動を起こす。

だから、マルチなデフォルトGWをLuciで作るとなると怖い。

MWANは、何も考えずに使うと、ちょっとやばい気がする。とくにDDNS関連が崩壊する。

そこで、Luciで作らずに、デフォルトGWを増やす

MWANの役割

MWANの役割は、上流GWをクライアントごとに切り替えて、マルチホーミング(死語?)な上流を構成して、負荷軽減を図る

または、上流がダウンしたときに呼び回線に自動的に切り替えるというものだ。

VRRPのようなスジのわるい実装とは違い、ポリシールーティングと ip route add default metric を使っているシンプルな実装。上流GWをpingで監視してダウンしたら、パケットを切り替えるという感じ。

たぶんポリシールーティングでやってると思う。

冗長化だけなら、MWANいらなくね?

冒頭の記事を読んで試して思ったのだが、負荷分散ならともかく、冗長化ならGWだけでよくね。 要は、メインの回線が死んだときに予備回線に切り替わればいいわけだから。

metric 指定してGWを2つ入れればいい。

root@OpenWrt:~# ip route
default via 221.110.204.8 dev pppoe-ybb proto static metric 100
default via 192.168.55.5 dev eth2 proto static metric 200

mwan にある死活監視やセッションの切り替えのような便利なものはなくなってしまうので、ストリーミングは切れてしまうと思うが。ダウンしたときでもちゃんと繋がるのは良い。

pppoe や map-e の再起動が手軽になった。

いままでは、家族に気を使いながら、再起動や、pppoeのIP再取得を行っていたが、全然気にせずにバンバン落とせる。気楽でいいわコレ。

ストリーミングといってもyoutube 程度ならhttpセッションが再開するだけなのでほとんど気にならない。ゲームなどの連続の通信は駄目出し、MWANを使ってても駄目だと思う。ニンテンドースイッチのようなUDPならなおさら無理だと思うが、通常ブラウジング程度ならダウンしたことに気づかない。

設定

主回線のインターフェイスのGWメトリックを指定して

予備回線のインターフェイスのGWメトリックをを指定する

こっちは、uci/ luci で行うとほかのパッケージに影響するのでコマンドから流す。

ip route add default via 192.168.55.5 dev eth2 proto static metric 200

後は、192.168.55.5 宛に送るときにmasquerade するようにする。

masquerade は nft コマンドから流してもいいい、Luciから作ってもいい。

nft の例

nft add rule inet fw4 srcnat meta nfproto ipv4 oifname "eth2" jump srcnat_opnsense comment "takuya:to-opnsense-masq"
nft add rule inet fw4 srcnat_opnsense  oifname "eth2" ip saddr 192.168.10.0/24 counter masquerade comment "takuya:to-opnsense-masq"

後は、起動時に、自動的に予備回線宛のdefault gw 設定を登録するようにする。

/etc/hotplug.d/iface/97-ifup-InnerVM.sh

#!/usr/bin/env sh


function add_default_route(){

  ip route add default via 192.168.55.5 dev eth2 proto static metric 200

}

function google_home_route_to_vm(){
  /etc/config/custom/radiko-route/radiko-route-to-opnsense.sh add
}
function google_home_route_to_default(){
  /etc/config/custom/radiko-route/radiko-route-to-opnsense.sh del
}


function main(){
  ##
  TARGET_ACTION=ifup
  TARGET_INTERFACE=InnerVM
  TARGET_DEVICE=eth2
  ###

  [ "$INTERFACE" = "$TARGET_INTERFACE"  ] && {
    logger "iface $TARGET_INTERFACE / $TARGET_DEVICE $ACTION detected, do hotplug actions."
    case $ACTION in
      ifup)
        add_default_route
        google_home_route_to_vm
        ;;
      ifdown)
        google_home_route_to_default
        ;;
        *)
        ;;
    esac


  }
}


main

google home は、メイン回線のオンオフでストリーミングが止まるので、最初から、予備回線を通すことにした。

コレで、冗長化が終わる。

あとはダウンして試す。

メイン回線をダウンさせて、様子を見る。

ifdown my-main-isp
ping 1.1.1.1

ちゃんと切り替わってるのが面白い。

まとめ

フェイルオーバーなら、mwan3 は機能が多すぎてややこしいが、GUIで完結するので初心者にはいいかも。

ロードバランサーなら、ポリシールーティングを書くのがめっちゃめんどくさいので、mwan3 を使ったら幸せになる。

ただの冗長化なら、linuxではdefault ゲートウェイのmetric 機能で十分だった。

参考資料

openwrt で hotplug dhcp を調べる

openwrt で hotplug dhcp を調べる

DHCP に何が書かれるか、どういうタイミングでホットプラグが呼ばれるのか全然わからないので調べることにした

調査用のファイルを作成

cat << "EOF" > /etc/hotplug.d/dhcp/00-logger
logger -t hotplug $(env)
EOF

調査する

logread -f 

ネットワークを再起動する

service network restart

hotplug のdhcp は公式ページにも記載がない。

公式ページのHotplugの章を読んでみたが、記載がない

OpenWrt Wiki / Hotplug

実行結果が次の通り

調査してみた結果次のような変数が得られた

Wed May 15 17:21:04 2024 user.notice hotplug: ACTION=update IPADDR=10.0.2.105 SHLVL=1 HOME=/ MACADDR=44:07:0b:5a:bc:b3 TERM=linux BOOT_IMAGE=/boot/vmlinuz PATH=/usr/sbin:/usr/bin:/sbin:/bin PWD=/
Wed May 15 17:21:04 2024 user.notice hotplug: ACTION=add HOSTNAME=Google-Home-Mini IPADDR=10.0.2.103 SHLVL=1 HOME=/ MACADDR=38:8b:59:4a:11:fe TERM=linux BOOT_IMAGE=/boot/vmlinuz PATH=/usr/sbin:/usr/bin:/sbin:/bin PWD=/
Wed May 15 17:21:04 2024 user.notice hotplug: ACTION=update IPADDR=10.0.2.105 SHLVL=1 HOME=/ MACADDR=44:07:0b:5a:bc:b3 TERM=linux BOOT_IMAGE=/boot/vmlinuz PATH=/usr/sbin:/usr/bin:/sbin:/bin PWD=/

つまり、次の変数が取れている。

ACTION=add HOSTNAME=Google-Home-Mini IPADDR=10.0.2.105 SHLVL=1 HOME=/ MACADDR=44:07:0b:5a:bc:b3

また、UPDATEのときは、ホスト名がないことがわかる。

ACTION=update IPADDR=10.0.2.105 SHLVL=1 MACADDR=44:07:0b:5a:bc:b3 

変数まとめ

変数 役割
ACTION update DHCPの更新リクエス
ACTION add DHCPの取得リクエス
IPADDR 0.0.0.0 紐づけたアドレス
MACADDR 00:00:00:00 紐づけたMACアドレス
SHLVL int シェルのネスト

SHLVL はシェルのネスト度合いなので、hotplug がhotplug を呼び出して重複起動しないために重要なので掲載。

DHCPのホットプラグまとめ

コレを使えば、特定のDHCPクライアントが来たときに、ルーティングテーブルを書くような離れ業(荒業)が可能になる。

例えば、未登録のMACADDRはログイン画面に飛ばすとかね。

openwrt のifup / ifdown の使い方

openwrt のifup / ifdown の使い方

使い方の例

ifdown wan86
ifup wan6

インターフェイスがポイント

ifdown <interface name>が書式です。

インターフェイス名はLuciの画面にでてくる名前です。eth0 のようなデバイス名称ではないことに注意する。

インターフェイス名称の取り方

cat /etc/config/network  | grep -oP '(?<=config interface ).+'

uci で確認してもいいけど、grep すれば大丈夫だね

eth1 / eth2 とかの名前は、netdev で、その上にIP空間を載せてるのが interface って感じですね。

通常のBSD/Linuxのifdownに比べ、より階層が浅い。 ドメインが微妙に違う。

public dns のv6アドレスを通信拒否する。

exgress な udp/53 をブロックすれば、通信を改竄詐称盗聴できる時代は終わった。従来どおりやるにはdot / doh / dos を止めないと・・・それには打つ手はない。

v6 の public DNSIPアドレスをブロックする。

ただ、public dnsを止めるだけでも相当の効果はあると考えられる。

v6時代のpublic dns をブロックしてDNSをある程度制御することにする。

public dns のv6アドレスの一覧をURLで用意した

https://github.com/takuya/public_dns_list/raw/master/public_dns_ipv6_list.txt

GitHub - takuya/public_dns_list: known public dns list ips and domains

これをルータに仕込む

#!/usr/bin/env bash


function v6_addrs(){

map=working_$RANDOM
ipset destroy $map &> /dev/null
ipset create  $map hash:ip family inet6

addrs=(
https://github.com/takuya/public_dns_list/raw/master/public_dns_ipv6_list.txt
)

list=()
for url in ${addrs[@]} ; do
  echo $url;
  a=$(curl -sL $url )
  list+=( $a )
done
ips=$(
  for i in "${list[@]}"; do  echo $i;  done | sort -n | uniq  \
    | sed -E 's|$|/128|' \
    | sort -n | uniq

)
for i in $ips ; do
  ipset add $map $i 2>/dev/null;
done ;
ipset list $map  | grep -E '^[0-9]' | sort -n  > /etc/config/custom/public-dns/ip6-list.txt
ipset destroy $map
}

function v4_addrs(){

map=working_$RANDOM
#iptables -D FORWARD -o pppoe-ybb -m set --match-set public_dns dst -j DROP
ipset destroy $map &> /dev/null
ipset create  $map hash:ip

addrs=(
https://github.com/takuya/public_dns_list/raw/master/dns-list-ipv4.txt
https://github.com/takuya/public_dns_list/raw/master/dns-list-doh-as-ip.txt
https://github.com/takuya/public_dns_list/raw/master/dns-list-dot-as-ip.txt
)

list=()
for url in ${addrs[@]} ; do
  echo $url;
  a=$(curl -sL $url )
  list+=( $a )
done
ips=$(
  for i in "${list[@]}"; do  echo $i;  done | sort -n | uniq  \
    | sed -E 's|:[0-9]+||' \
    | sed -E 's|$|/32|' \
    | sort -n \
    | uniq

)
for i in $ips ; do
  ipset add $map $i ;
done ;
ipset list $map  | grep -E '^[0-9]' | sort -n  > /etc/config/custom/public-dns/ip-list.txt
ipset destroy $map
#
#ipset list public_dns
#iptables -I FORWARD -o pppoe-ybb -m set --match-set public_dns dst -j DROP


}
function main(){
  v4_addrs
  v6_addrs
}


###
main;

firewall 設定

config ipset
    option name 'public_dns'
    option match 'dest_ip'
    option family 'ipv4'
    option loadfile '/etc/config/custom/public-dns/ip-list.txt'

config ipset
    option name 'public_dns_v6'
    option match 'dest_ip'
    option family 'ipv6'
    option loadfile '/etc/config/custom/public-dns/ip6-list.txt'
### 特定のネットワーク・アドレス空間だけ v6を許可する。
config rule
        option ipset 'public_dns_v6'
        option src 'lan6'
        option dest '*'
        option target 'ACCEPT'
        option name 'Allow v6 global from pc '
        list src_ip 'fd03:xxx:xxx:xxx::/64'
## public_dns(v6)への通信は全部拒否
config rule
        option src 'lan6'
        option dest '*'
        option ipset 'public_dns_v6'
        option target 'REJECT'
        option family 'ipv6'
        option name 'reject public dns:ipset'
        list proto 'all'
## public dns (v4)の通信は全部拒否
config rule
    option src 'lan'
    option dest 'wan'
    option ipset 'public_dns'
    option target 'REJECT'
    option device 'pppoe-xxx'
    option direction 'out'
    option family 'ipv4'
    option name 'reject public dns:ipset'
    list proto 'icmp'
    list proto 'any'
        

基本的には打つ手なし

DoHがChromeにはいったので、野良でDoHを起動されると、本当に打つ手はない。

証明書の書換えによる、通信監視しか手段はない。

証明書の書換えは、リスクが高すぎるし、なにより、通信の秘密に関わる部分である。日本国と国民は憲法により、通信の秘密は保持すると決めている。国民間の労働契約の場合においても雇用条件通知書や雇用契約書で明記しない限り、リーガルリスクを伴う。学生・社員が逃れたとしてもそれの責任追及をする根拠としての「通知・契約」である。不文律というわけには行かない。せいぜい漏洩した被害にたいする責任追及に留まることになる。なので、野良DoHは打つ手はない。契約で縛るしかない。でも「悪意の広告」「フィッシング」「悪質な全面広告」をブロックするには、DNS書き換えが強い力を発揮するので使わない手はない。ライトユーザ相手にはとても良い選択肢だと思う。

こっそり通信を監視するような横暴は不可能になっているので、改めて本当に必要な監視かどうか考え直してほしい。

また証明書のインストールをするのであれば、ちゃんと契約に明示したほうがいいと思う。そして証明書のインストールが「レアキャラ」であり信用できない証明書のインストールをしてはいけないことを合わせて教えておく必要がある。でなければ、「証明書のインストールをやって良いんだ」という誤った解釈を学習することにになる。

いまは、Google Chromeのインストールを「Windowsで構成」すれば、DoHを使わせないことができる。いつまできることやら。

大いなる力には大いなる責任が伴う。

adguardのキャッシュのクリア

AdguardでDNSキャッシュをクリアして、最新情報を強制的に「浸透させる」

設定画面のDNSのところにある。

「グレーアウト」したボタンなので見逃しがちなので注意する

DNS設定画面からCtrl-Fでキャッシュを探せばい。

DNSゾルバなので、上流のDNSゾルバに問い合わせて、自分にデータを取ってくる。洪水や津波のように変化が伝播するわけでないので注意

lua の配列のループに関するメモ

配列の定義とループ

local t = {}
table.insert(t, "a")
table.insert(t, "b")
print(t[1])
for key,entry in pairs(t) do
    print(string.format("%s=>%s", key, entry ))
end

実行結果

a
1=>a
2=>b

ポイント

  • local xで変数定義
  • 配列はテーブルで{}で定義。php の配列が近い
  • 添字は[1]から
  • for ループと pairs を一緒に使う
  • printf のかわりに、string.format

配列の添字は1から始まります。[0]ではありません。t[1] です。大事なので2回言いました。

lua しばらく使わないといつもここで詰まる。

joplinを完全にアンインストールする

OLD バージョンに戻したいとき

MacOSなら、次のフォルダを削除すれば良い。

rm -rf  ~/.config/joplin-desktop/

macOS Theme というスッキリしたテーマが動かないので、 joplin 2.3 以降ではちょっと困った。

どこのバージョンで動かなくなったか、バージョンを一つずつダウングレードを考えて、調査してたときに消せずに困った。

プラグインを使ってる系はアップデートで動かなくなるのでフリーなソフトウェアの罠ですね。

まぁAppleもアップデートでよく動かなくなるけど。

プラグインやテーマを考えると、いまのところ、v2.12.19が安定して動いてる。

apt-cacher で httpsなレポジトリ をプロキシ・キャッシュする。

apt-cacher 便利で使ってる。

以前導入したapt-cacher が便利である。

膨れ上がった複数台のRaspi4や、膨れ上がったLXCや、Dockerfileからのdocker build時に時短のために大活躍中である。

apt-cacher の問題点/HTTPS

apt-cacher は HTTPSに未対応である。TLSはProxy Connectしてしまうとキャッシュできないので。流石にこれどうしようもない問題である。

APTのレポジトリなどはAPT専用のキーで署名されてるのでHTTPSは無用だと思うのですが、chromeがHTTPを絶滅させに掛かってるので、早晩HTTPSになってしまうのであろう。

そこで、なんとかHTTPSのレポジトリ(docker や nodejs のレポジトリ)をキャッシュできないかと考えていた。

解決方法:強引にHTTPでアクセスする。

調べていると解決方法があって、そうかその手があったか。という感じだったのでメモとして残します。

apt-cacher 側でhttpsするー>クライアントにhttpsさせない

設定を確認すると、次のようになっている。

つまり、HTTPならすべて、プロキシするわけである。

cat /etc/apt-cacher-ng/acng.conf  | grep PassThroughPattern
# PassThroughPattern: private-ppa\.launchpad\.net:443$
PassThroughPattern: .* #
# PassThroughPattern: ^(bugs\.debian\.org|changelogs\.ubuntu\.com):443$

apt-cacher を通すクライアントのDebianから、httpsなレポジトリへアクセスすると、次のようになる。証明書検証エラーになってしまう。

Err:1 https://download.docker.com/linux/debian bookworm InRelease
  Invalid response from proxy: HTTP/1.1 400 Bad Request  Server: nginx/1.18.0 (Ubuntu)  Date: Tue, 10 Oct 2023 08:16:25 GMT  Content-Type: text/html  Content-Length: 166  Connection: close     [IP: 192.168.2.21 443]
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
All packages are up to date.
W: Failed to fetch https://download.docker.com/linux/debian/dists/bookworm/InRelease  Invalid response from proxy: HTTP/1.1 400 Bad Request  Server: nginx/1.18.0 (Ubuntu)  Date: Tue, 10 Oct 2023 08:16:25 GMT  Content-Type: text/html  Content-Length: 166  Connection: close     [IP: 192.168.2.21 443]
W: Some index files failed to download. They have been ignored, or old ones used instead.

https -> http にダウングレードする

そこで、apt レポジトリの設定をいじってあげる。

## http にする場合(Apt-Cacherを使うとき)
sudo sed -e 's|https:|http:|' -i /etc/apt/sources.list.d/docker.list
## https にする場合
sudo sed -e 's|http:|https:|' -i /etc/apt/sources.list.d/docker.list

その結果、次のような soueces.list が出来上がる。

/etc/apt/sources.list.d/docker.list

deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.gpg] http://download.docker.com/linux/ubuntu   jammy stable

この状態で、Apt-Cacherを通すと・・・

無事にキャッシュされる。

takuya@raspi-ubuntu:~$ sudo apt update
[sudo] password for takuya:
Get:1 http://download.docker.com/linux/ubuntu jammy InRelease [48.8 kB]
Hit:2 http://ports.ubuntu.com/ubuntu-ports jammy InRelease
Get:3 http://ports.ubuntu.com/ubuntu-ports jammy-updates InRelease [119 kB]
Hit:4 http://ports.ubuntu.com/ubuntu-ports jammy-backports InRelease
Get:5 http://ports.ubuntu.com/ubuntu-ports jammy-security InRelease [110 kB]

インストールも問題なく行える。

sudo apt reinstall docker-ce-cli

apt-cacher-ng へhttp でプロキシに繋いで、apt-cacher-ng プロキシ側がhttpsでデータとを取りに行って返してくれる。

debian でも試してみた.

takuya@:~$ cat /etc/apt/sources.list.d/php.list
deb [signed-by=/etc/apt/keyrings/php.gpg]  http://packages.sury.org/php/ bookworm main
takuya@:~$ cat /etc/apt/sources.list.d/nodesource.list
deb [signed-by=/etc/apt/keyrings/nodesource.gpg] http://deb.nodesource.com/node_18.x nodistro main

アップデートとインストール

takuya@:~$ sudo apt update
取得:1 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
ヒット:2 http://debian-mirror.sakura.ne.jp/debian bookworm InRelease
ヒット:3 http://deb.nodesource.com/node_18.x nodistro InRelease
ヒット:4 http://debian-mirror.sakura.ne.jp/debian bookworm-updates InRelease
ヒット:5 http://download.docker.com/linux/debian bookworm InRelease
ヒット:6 http://packages.sury.org/php bookworm InRelease
ヒット:7 http://packages.gitlab.com/gitlab/gitlab-ce/debian bookworm InRelease
ヒット:8 http://www.deb-multimedia.org bookworm InRelease
48.0 kB を 4秒 で取得しました (12.6 kB/s)
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています... 完了
状態情報を読み取っています... 完了

特に問題なく行える。

apt-cacher なしで docker のビルドとか仮想マシンの作成とかダウンロード時間が面倒くさくてやりたくないです。

結論

HTTP->HTTPSにリダイレクトがかかるサイトは、HTTPでアクセスすればいい。apt-cacherがリダイレクトを処理してくれる。

参考資料