SPF・DKIM・DMARCの簡単な解説

こんにちは岡野です。
時々分からなくなるためSPF・DKIM・DMARCについてまとめます。

前提知識

メールのFromアドレスには2種類ある。
エンベロープFrom:SMTP通信で使用するもの。メールヘッダ内ではReturn-Pathとして確認できる。
メールヘッダFrom:メール送信者が本文などとともに指定するもの。メールクライアントではこちらが表示されるため、なりすましを防ぐにはこちらが正しいことが重要。

SPF認証

エンベロープFromのドメインでDNSからIPアドレスを取得し、送信サーバのIPアドレスと一致することを確認する。

DKIM認証

メール署名時のドメインでDNSから公開鍵を取得し、ハッシュが一致することを確認する。

DMARC

SPF認証&SPFアライメント、もしくは、DKIM認証&DKIMアライメント、のいずれかが成り立つことを確認する。
SPFアライメント:エンベロープFromのドメインとメールヘッダFromのドメインが一致すること。
DKIMアライメント:DKIM署名のドメインとメールヘッダFromのドメインが一致すること。

結論

SPF・DKIM・DMARCすべて対応するのが望ましい。

OS Loginを有効にしたGCEにPuttyやWinSCPで接続する方法

こんにちは岡野です。
GCPのCompute EngineへPuttyやWinSCPで接続したいという要望があり検索したのですが、なかなか情報が無かったためここで共有します。

・GCEの状態

外部IPアドレス未設定
OS Login有効

・接続方法

SSHトンネルを利用する。

1. 以下のコマンドをWSL上などで実行。

gcloud compute ssh {{ gce name }} --ssh-flag="-L 10001:localhost:22 -N -f"
    1. PuttyやWinSCPなどでは以下の情報で接続する。
    ユーザ名:GCPで使用しているアカウント(通常はメアドの記号を_へ置換したもの、例:name_example_com)
    ホスト名:localhost
    ポート:10001
    秘密鍵:GCP用の鍵

    ImageMagickでのPNGファイル生成の高速化

     
    こんにちは岡野です。
    弊社サービスではImageMagickでPNGファイルを生成することが多いのですが、高速化するための設定を調査したので共有します。
     

    環境

    ImageMagick 6.9.12-84 Q8
    Ubuntu 22.04
     

    調査内容

    PNGファイル出力時の圧縮設定を変更した際の出力時間を調査しました。なおこの解説記事を参考にしています。

    圧縮方法を設定する-quality引数を0~99で変えながら実行した際の時間を数回計測しました。

    使用したコマンドは
    $ convert logo: -resize 1024×1024! logo.png
    $ rm log ; for q in `seq 0 1 99` ; do /usr/bin/time -f %e:$q -a -o log convert logo.png -quality $q png32:logo-$q.png ; done
    $ sort log

    で、結果は以下です。
    11,12,13,14,21,22,23,24,31,32,33,34は0.05秒
    41,42,43,44,51,52,54は0.06秒
    以降、省略

    ファイルサイズは以下の通りでした。
    $ ls -l logo-[123][1234].png logo-75.png
    384337  3月 24 17:31 logo-11.png
    384337  3月 24 17:31 logo-12.png
    384337  3月 24 17:31 logo-13.png
    384337  3月 24 17:31 logo-14.png
    378112  3月 24 17:31 logo-21.png
    378112  3月 24 17:31 logo-22.png
    378112  3月 24 17:31 logo-23.png
    378112  3月 24 17:31 logo-24.png
    374669  3月 24 17:31 logo-31.png
    374669  3月 24 17:31 logo-32.png
    374669  3月 24 17:31 logo-33.png
    374669  3月 24 17:31 logo-34.png
    295753  3月 24 21:00 logo-75.png (デフォルト設定)

    11,12,13,14,21,22,23,24,31,32,33,34のいずれかの設定が良さそうでした。ただしpng:compression-filterの仕様を見ると画像内容によって上記の結果も結構変わりそうです。
     

    結論

    今後も検討を続けますが一旦は-quality 11を使用することにしました。ただしファイルサイズがデフォルト設定より増えるため(295,753->384,337)、ユーザアクセス内容によっては弊害も大きく注意が必要です。
     

    サーバ上のエラー発生行をブラウザからPhpStormで開く方法

    こんにちは岡野です。
    今回はPHPでの開発が楽になる方法を紹介します。最初の設定は面倒ですがメリットは大きいため、PhpStorm(もしくはIntelliJのPHPプラグイン)を使っている方はぜひ試してください。
       
    • 環境
      Windows
      IntelliJ IDEA 2023.2 + PHP Plugin
      Symfony(おそらく他のフレームワークでも対応可能)
      PhpStorm Protocol (最終編集2023-08-17)
    • インストール
    1. https://github.com/aik099/PhpStormProtocol のCodeボタンからzipをダウンロードする。  
    2. zip内の「PhpStorm Protocol (Win)」 を C:\Program Files\ へコピーする。  
    3. C:\Program Files\PhpStorm Protocol (Win)\run_editor.reg をダブルクリック。OK押下。  
    4. run_editor.jsを編集。上記PhpStormProtocol説明ページに載っている方法でうまくいかない場合は、editor変数を直接変更する。  
       
      editor = ‘”C:\\Users\\ユーザ名\\AppData\\Local\\Programs\\IntelliJ IDEA Ultimate\\bin\\idea64.exe”‘;
       
    5. Symfonyの設定を行う。 例は、サーバ:/opt/app/、ローカル:c:\git\app\symfony/というマッピングの場合。
    (Symfony6.1以上の場合)
    .env.localに以下を記述。
    SYMFONY_IDE=phpstorm://open?file=%f&line=%l&/opt/app/>c:/git/app/symfony/
     
    (Symfony6.0以下の場合)
    framework.yaml内のframeworkにide要素を追加。
    framework:
      ide: 'phpstorm://open?file=%%f&line=%%l&/opt/app/>c:/git/app/symfony/'
     
    ・実行
    エラー発生時にファイルパスのリンクをクリックすると、該当箇所がIntelliJで開く。

    ImageMagick脆弱性対応(パッチ)

    こんにちは岡野です。
    先日行った、画像処理ライブラリImageMagickの脆弱性対応を共有します。当社ではオリジナルプリントやMEET MY GOODSといったサービスで処理高速化のためリコンパイルしたImageMagickを使用しています。

    脆弱性の概要

    脆弱性(Metabase Qの報告
    CVE-2022-44267:DoS攻撃
    CVE-2022-44268:任意のファイル参照
    ですが、不正なテキスト情報を埋め込んだPNGファイルをImageMagickが処理する際に発生する物でした。

    脆弱性の対応案

    ・ImageMagickのバージョンアップ
    ImageMagickを最新版へバージョンアップすれば解決するとのことでしたが、テストに時間が掛かるため他の方法を検討しました。

    ・画像データの修正
    脆弱性の原因は前述の通り不正なテキスト情報ですので、ImageMagickへ画像ファイルを渡す前に不正なテキスト情報を除去する案です。PNG最適化ツールpngcrushで除去する方法をネットで見つけ試してみましたが除去処理に時間が掛かるためあきらめました(バージョンにより違うのかもしれませんが大きめの画像では10秒以上掛かることがありました)。

    ・ImageMagickのパッチ適用
    脆弱性を修正するパッチをImageMagickのソースコードへ適用する案です。今回はこの案で対応しました。

    パッチ適用の手順

    今回はUbuntu用のパッチを流用しました。
    https://launchpad.net/ubuntu/+source/imagemagick/8:6.9.7.4+dfsg-16ubuntu6.15
    の「Available diffs」の箇所です。
    但しとあるツイートに存在したPNGファイル
    echo -en "P4\n1 1\nX" | pnmtopng -text <(echo 'fx:while(1,debug(hui=3)) 0') > payload.png
    の脆弱性を防げなかったためパッチのif文
    if ((LocaleCompare(key,"version") == 0) ||
        (LocaleCompare(key,"profile") == 0) ||
        (LocaleCompare(key,"width") == 0))
    へ「fx:」を無効化する
        (LocaleNCompare(key,"fx:",3) == 0) ||
    を追加しました(ImageMagickのバージョンによっては再現しなさそうでした、おそらくこのissue)。

    まとめ

    残念ながらImageMagickの脆弱性は今後も発見されると思いますので、バージョンアップを容易にしておく必要性を感じました。今後はバージョンアップ用の自動テストケースを準備しようと思います。

    ImageMagickで服の色を変える

    こんにちは岡野です。
    現在開発中のプロジェクトで、服の色を変更する処理を実装したので紹介します(簡易的な方法です)。

    環境

    ImageMagick 6.9.12-59 Q8

    手順

    1. 元となる画像を用意します。例:Tシャツ ホワイト。
    2. 例えばネイビーへ変更するとして変更前後の色についてRGBそれぞれの差分を計算します。
    ホワイト: rgb(255, 255, 255)
    ネイビー: rgb(32, 47, 85)
    R: 32 - 255 = -223
    G: 47 - 255 = -208
    B: 85 - 255 = -170
    3. ImageMagickのevaluate Addに差分の値を渡して色を変換します(Q16環境では各値を256倍します)。
    convert src.png \
    -channel R -evaluate Add -223 \
    -channel G -evaluate Add -208 \
    -channel B -evaluate Add -170 \
    dest.png
    

    まとめ

    ImageMagickだけでも結構良い感じになりました。将来はAIで着色したいと思います。

    その他

    最近Zabbixでハマったことです。グラフ内の文字が出力されないため悩んでいたのですがopen_basedirの設定不足でした(zabbix_server.logに気をとられNginxのログに気づくのが遅れた)。何か問題があった際は落ち着かないとダメだと感じました。

    Symfonyで2要素認証

    こんにちは、岡野です。
    最近Symfonyを使っているWebサイトで2要素認証を実現しました。その際に使用したscheb/2faバンドルは非常に使いやすく、Symfony公式マニュアルを見ながら容易に実現できました。但し一点問題があり、認証コードを入力しないままユーザが画面遷移すると中途半端な認証状態になってしまうので困りました。
    今回はその回避方法を共有します。
     

    環境

    Symfony: 4.4.20
    scheb/2fa-*: 5.13.1
     

    起きていた現象

    1. ユーザがID/PWを入力。
    2. 認証コードを尋ねるポップアップを表示。
    3. ユーザが他の画面へ遷移。

    → 2.でSymfony内部の認証状態が「Partially authenticated」になるため、3.でController#getUser()すると認証が完了していないユーザエンティティが取得できてしまう(※1)。
    また3.のページがfirewallで制限している場合は認証コードを尋ねるページが表示される。一般的な通常サイトの様にログイン画面へ遷移させたい(※2)。
     

    ※1(完全認証前のユーザが取得できてしまう)の回避方法

    EventListenerを使って、認証コードの確認が完了するまでuserをanonymousにする。
     
    class AuthAttemptListener implements EventSubscriberInterface
    {
      public static function getSubscribedEvents(): array
      {
          return [
              TwoFactorAuthenticationEvents::ATTEMPT => ['onTwoFactorAuthAttempt'],
              TwoFactorAuthenticationEvents::COMPLETE => ['onTwoFactorAuthComplete'],
              TwoFactorAuthenticationEvents::FAILURE => ['onTwoFactorAuthFailure'],
              KernelEvents::RESPONSE => ['onKernelResponse'],
              KernelEvents::TERMINATE => ['onKernelTerminate'],
          ];
        }

      /**
       * セッション内の情報が正しければ、バンドル内の2要素認証処理用にトークンへユーザを設定する。
    * ユーザは後で解除する。
       */
      public function onTwoFactorAuthAttempt(TwoFactorAuthenticationEvent $event): void
      {
          // ...
          $this->attemptingToken->setUser($user);
        }

      /**
       * 2要素認証が成功した場合、トークンを認証状態に設定する。
       */
      public function onTwoFactorAuthComplete(TwoFactorAuthenticationEvent $event): void
      {
          // ...
          $this->attemptingToken->getAuthenticatedToken()->setAuthenticated(true);
        }

      /**
       * 2要素認証失敗時、トークンのユーザを元に戻す
       */
      public function onTwoFactorAuthFailure(TwoFactorAuthenticationEvent $event): void
      {
          if ($this->attemptingToken) {
              $this->revertTokenUser();
          }
        }

      /**
       * 例外発生時、トークンのユーザを元に戻す
       */
      public function onKernelResponse(): void
      {
          if ($this->attemptingToken) {
              $this->revertTokenUser();
          }
        }

      /**
       * 安全のためSymfonyの処理が終わる際に、万が一処理中のトークンが残っていたら
    * 例外を発生させてエラー通知する。
       */
      public function onKernelTerminate(): void
      {
          if ($this->attemptingToken) {
              throw new \LogicException();
          }
        }

      private function revertTokenUser(): void
      {
          $this->attemptingToken->setUser('anon.');
          $this->attemptingToken->getAuthenticatedToken()->setAuthenticated(false);
          $this->attemptingToken = null;
      }
    }

    ※2(認証コード要求ページが表示される)の回避方法

    認証コードが要求された場合、ログイン画面へ遷移する。
     
    services.yaml
      app_2fa_required_handler:
            class: App\Security\TwoFactorAuth\AuthRequiredHandler

    class AuthRequiredHandler implements AuthenticationRequiredHandlerInterface
    {
      public function onAuthenticationRequired(Request $request, TokenInterface $token): Response
      {
          $this->tokenStorage->setToken(new AnonymousToken('', 'anon.'));
          throw new AccessDeniedException();
      }
    }
    参考になれば幸いです。

    Doctrine SQL Filterでマルチテナントを実現する

    こんにちは、岡野です。

    最近マルチテナントのシステムを開発することがあり、共通のDBをテナントごとに管理する方法を調べました。

    結論

    Doctrineの以下機能を使用するとCRUDの各SQLを改変でき、テナントを透過的に制御可能(すなわち毎回WHERE句にテナントの条件を書かなくて良い)。 但し、@OneToOneは期待通りに動作しないため要注意。

    環境

    Doctrine 2.7.4
    Symfony 4.4.16

    SELECTの調査結果

    以下の様なSQL Filterを登録。
    class TenantFilter extends SQLFilter
    {
        public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
        {
            // 100は実際には可変(後述)
            return $targetTableAlias.'.tenant_id = 100';
        }
    }

    単純なSELECT

    $em->find(Parent::class, 1);
    → SELECT ... FROM parent WHERE id = 1 AND tenant_id = 100;

    @OneToMany

    $em->find(Parent::class, 1)->getChilds();
    → SELECT ... FROM parent WHERE id = 1 AND tenant_id = 100;
    → SELECT ... FROM child WHERE parent_id = 1 AND tenant_id = 100;

    @ManyToOne

    @OneToMany同様。

    @ManyToMany

    @OneToMany同様。

    @OneToOne

    $em->find(Example1::class, 1)->getExample2();
    → SELECT ... FROM example1 e1 LEFT JOIN example2 e2 ON e2.example1_id = e1.id WHERE e1.id = 1 AND e1.tenant_id = 100;
    ⇒ example2テーブルへのtenant_id = 100が存在しない
    

    join()

    $em->createQueryBuilder('c')->join('c.parent')->where('c.id = 1');
    → SELECT ... FROM child c JOIN parent p ON c.parent_id = p.id AND p.tenant_id = 100 WHERE c.id = 1 AND c.tenant_id = 100;
    

    leftJoin()

    join()同様。



    INSERT/UPDATE/DELETEの調査結果

    以下の様なDoctrine Event Listenerを登録。
    class TenantListener
    {
        public function prePersist(LifecycleEventArgs $args): void
        {
            assert($args->getEntity()->getTenant() === null);
            // 100は実際には可変
            $args->getEntity()->setTenant($this-em->find(Tenant::class, 100));
        }
        public function preUpdate(LifecycleEventArgs $args): void
        {
            // 安全のため他テナントを操作していないかチェックする
            if ($args->getEntity()->getTenant()->getId() !== 100) {
                throw new \LogicException();
            }
        }
        public function preRemove(LifecycleEventArgs $args): void
        {
            // preUpdate()同様
        }
    }
    

    INSERT

    $em->persist(new Example());
    → INSERT INTO example (..., tenant_id) VALUES (..., 100);
    


    その他

    上記のようにDoctrine FilterのaddFilterConstraint()内でtenant_idを直接設定するとDoctrine Query CacheによりSQLが固定化されてしまい期待通りに動作しない。そのため以下の通りparameterを経由する必要があった。

    // 外部から呼ぶ(RequestListenerなどから)
    public function setTenantId(int $tenantId): void
    {
        $this->setParameter('tenant_id', $tenantId, Types::INTEGER);
    }
    
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
    {
        $tenantId = (int)$this->unquoteParameterValue($this->getParameter('tenant_id'));
        return $targetTableAlias.'.tenant_id = '.$tenantId;
    }
    
    private function unquoteParameterValue(string $value): string
    {
        $len = strlen($value);
        if ($value[0] !== "'" || $value[$len - 1] !== "'") {
            throw new \LogicException($value);
        }
        return str_replace(['\\"', '\\\\'], ['"', '\\'], substr($value, 1, $len - 2));
    }
    

    RectorでSymfony4への変換

    こんにちは、岡野です。

    最近、Symfony2.8/PHP7.0で実装されたサービスをSymfony4.4/PHP7.4へバージョンアップしました。その際ソースコードの自動変換に使用したRectorというソフトウェアを紹介します。
    詳しくは https://getrector.org/ を見ていただくとし、実際の変換結果を挙げていきます。    

    変換結果

    Symfony2.8 -> 2.8

    元々Symfony2.8で実装していますが念のためRectorを実行します。

    最初に設定ファイルを準備します。
    $ vi rector.php
    ...
    $parameters->set(Option::PATHS, [
        __DIR__.'/app',
        __DIR__.'/src',
    ]);
    $parameters->set(Option::EXCLUDE_PATHS, [
        __DIR__.'/app/SymfonyRequirements.php',
        __DIR__.'/app/cache/',
        __DIR__.'/src/AppBundle/Tests/',
    ]);
    $parameters->set(Option::PHP_VERSION_FEATURES, '7.0'); // 適宜変更
    $parameters->set(
        Option::SYMFONY_CONTAINER_XML_PATH_PARAMETER,
        __DIR__.'/appDevDebugProjectContainer.xml'
    );
    
    そしてRectorを実行します。
    $ vendor/bin/rector process --set symfony28
    以下変換されました(以降、変換結果の一部を抜粋します)。
    /**
     * @Route(defaults={"foo": ""}, ...)
     */
    ↓ ↓ ↓
    /**
     * @Route(defaults={"foo"= ""}, ...)
     */
     

    Symfony2.8 -> 3.0

    $ vendor/bin/rector process --set symfony30
    $form = $this->createForm(new Foo());
    ↓ ↓ ↓
    $form = $this->createForm(\AppBundle\Form\Foo::class);
    
     

    Symfony3.0 -> 3.4

    (symfony31~symfony33は差分が発生しませんでした)
    $ vendor/bin/rector process --set symfony34
    /**
     * @Route(...)
     * @Method("POST")
     */
    ↓ ↓ ↓
    /**
     * @Route(..., methods={"POST"})
     */
     

    PHP7.0 -> 7.0

    $ vendor/bin/rector process --set php70
    isset($foo[$bar]) ? $foo[$bar]: 0
    ↓ ↓ ↓
    $foo[$bar] ?? 0
    
    rand();
    ↓ ↓ ↓
    random_int(0, mt_getrandmax());
     

    PHP7.0 -> 7.1

    $ vendor/bin/rector process --set php71
    list($a, $b) = $this->foo();
    ↓ ↓ ↓
    [$a, $b] = $this->foo();
    
    count($foo)
    ↓ ↓ ↓
    is_array($foo) || $foo instanceof \Countable ? count($foo) : 0 // php73実行時に改善される
     

    Symfony3.4 -> 4.2

    (symfony40,symfony41は差分が発生しませんでした)
    $ vendor/bin/rector process --set symfony42
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    class FooController extends Controller
    ↓ ↓ ↓
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    class FooController extends AbstractController
     

    PHP7.1 -> 7.3

    (php72は差分が発生しませんでした)
    $ vendor/bin/rector process --set php73
    is_array($foo) || $foo instanceof \Countable ? count($foo) : 0
    ↓ ↓ ↓
    is_countable($foo) ? count($foo) : 0
    
    json_encode($foo)
    ↓ ↓ ↓
    json_encode($foo, JSON_THROW_ON_ERROR)
    
    json_decode($foo)
    ↓ ↓ ↓
    json_decode($foo, false, 512, JSON_THROW_ON_ERROR)
     

    Symfony4.2 -> 4.3

    $ vendor/bin/rector process --set symfony43
    $event->getDispatcher()->dispatch(
        FOSUserEvents::SECURITY_IMPLICIT_LOGIN,
        new UserEvent($event->getUser(), $event->getRequest()));
    ↓ ↓ ↓
    $event->getDispatcher()->dispatch(
        new UserEvent($event->getUser(), $event->getRequest()),
        FOSUserEvents::SECURITY_IMPLICIT_LOGIN);
    
    use Symfony\Component\HttpKernel\Event\GetResponseEvent;
    public function foo(GetResponseEvent $event)
    ↓ ↓ ↓
    use Symfony\Component\HttpKernel\Event\RequestEvent;
    public function foo(RequestEvent $event)
     

    Symfony4.3 -> 4.4

    $ vendor/bin/rector process --set symfony44
    // Command class
    public final function execute(InputInterface $input, OutputInterface $output)
    ↓ ↓ ↓
    public final function execute(InputInterface $input, OutputInterface $output): int
     

    PHP7.3 -> 7.4

    $ vendor/bin/rector process --set php74
    /**
     * @var integer
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
    ↓ ↓ ↓
    /**
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private int $id;
    
    /**
     * @var integer
     * @ORM\Column(name="foo", type="integer", nullable=true)
     */
    private $foo;
    ↓ ↓ ↓
    /**
     * @ORM\Column(name="foo", type="integer", nullable=true)
     */
    private ?int $foo = null;
    
    /** @var EntityManager */
    private $em;
    ↓ ↓ ↓
    private \Doctrine\ORM\EntityManager $em; // use形式への変更は手作業が必要
    
    1024
    ↓ ↓ ↓
    1_024 // この変換は微妙
     

    Twig1.x -> 2.0

    $ vendor/bin/rector process --set twig20
    new \Twig_SimpleFilter(...)
    ↓ ↓ ↓
    new \Twig_Filter(...)
     

    Twig2.0 -> 2.4.0

    $ vendor/bin/rector process --set twig240
    new \Twig_Filter(...)
    ↓ ↓ ↓
    new \Twig\TwigFilter(...)
     

    まとめ

    正規表現による置換よりもソフトウェアで一括変換した方が抜けも誤りも少ないでしょう。

    だし誤変換もごく一部あります。未定義の配列へ要素追加しているコードが $foo = (array)$foo; $foo[] = $bar; と変換されるなどです(元々未定義なことが良くないのですが)。

    って変換結果にざっと目を通すことは必要ですが、ほとんどはPHP/Symfony文法エラーのため容易に気付けます。変換後にPHPStormのInspect Code機能で確認することも有効でした。

    当然
    ながらRectorで変換できない点もありますので手動対応は必要です(src/AppBundle -> src/など)。

    その他

    ­ 上記以外にもRectorのルールセットは多数ありますので興味ありましたら以下をご覧ください。
    https://github.com/rectorphp/rector/tree/master/config/set

    LinuxサーバをVPN(L2TP)クライアントにする

    こんにちは、岡野です。

    先日、さくらインターネットのVPSと社内ネットワーク(VPN構築済み)をL2TPで接続しました。 特に問題なく接続できたのですが手順を共有します。

    前提条件

    VPN: L2TP

    VPS: Ubuntu 20.04、ufwで通信制限中(incoming & outgoing)

    接続手順

    1. VPNサーバの対応アルゴリズムを調べる

    NetworkManager-l2tp Wikiにあるシェルスクリプトを保存後、以下を実行。VPNサーバの対応アルゴリズムが表示される。

    $ chmod +x ike-scan.sh
    $ sudo ./ike-scan.sh <VPNサーバのIP> | grep SA=
    抜粋
    SA=(Enc=AES Hash=SHA1 Auth=PSK Group=2:modp1024 KeyLength=256 LifeType=Seconds LifeDuration(4)=0x00007080)
    1. VPSのネットワーク設定を確認する

    さくらインターネットで設定済みのネットワーク設定を確認。あとで使用する。

    $ cat /etc/netplan/01-netcfg.yaml
    network:
    version: 2
    renderer: networkd
    ethernets:
      ens3:
        addresses: [***.***.***.***/23]
        gateway4: ***.***.***.***
        nameservers:
          addresses: [***.***.***.***, ***.***.***.***]
    1. L2TPソフトウェアをインストールする

    $ sudo apt install network-manager-l2tp
    1. NetworkManager管理に切り替える

    $ sudo vi /etc/netplan/01-netcfg.yaml
    network:
    version: 2
    renderer: NetworkManager
    # 残りは削除
    $ sudo reboot
    1. NetworkManagerの接続情報を設定する

    以下、VPSコンソールから実施。

    $ sudo nmtui
    # 「2. VPSのネットワーク設定を確認する」の内容を設定する
    $ sudo reboot
    1. VPNの接続情報を設定する

    VPN接続情報を以下の通り設定する。IKE対応アルゴリズムには「1. VPNの対応アルゴリズムを調べる」で調べた中で、より強力なものを使用する(例:aes256-sha1-modp1024)。

    $ sudo nmcli connection add \
    type vpn \
    con-name <接続名、適当に。例:vpn> \
    autoconnect no \
    ifname -- \
    ipv4.method auto \
    vpn-type l2tp \
    vpn.secrets password=<パスワード> \
    vpn.data \
    "password-flags = 0, require-mppe = yes, user = <ユーザ名>, refuse-chap = yes, refuse-mschap = yes, gateway = <VPNサーバのIP>, refuse-pap = yes, ipsec-enabled = yes, ipsec-psk = <共有鍵>, ipsec-ike=<IKE対応アルゴリズム>, ipsec-esp=aes128-sha1"
    1. VPN接続する

    ログを表示しながらVPN接続を試みる。

    $ journalctl -f &
    $ sudo nmcli conn up vpn

    以下の様なエラーが出力されるため

    Jul 19 09:04:16 *** kernel: [148247.538797] [UFW BLOCK] IN= OUT=ens3 SRC=***.***.***.*** DST=***.***.***.*** LEN=268 TOS=0x00 PREC=0x00 TTL=64 ID=13477 DF PROTO=UDP SPT=500 DPT=500 LEN=248

    ufwで解除設定しながらsudo nmcli conn up vpnを繰り返す。具体的には以下の様なufw設定になる。

    $ sudo ufw status
    ***.***.***.*** 500/udp     ALLOW OUT   Anywhere                   # vpn (ipsec)
    ***.***.***.*** 1701/udp   ALLOW OUT   Anywhere                   # vpn (l2tp)
    ***.***.***.***/esp         ALLOW OUT   Anywhere                   # vpn (esp)

    必要に応じてルーティング設定も行う。

    1. VPN切断する

    VPN切断は以下。

    $ sudo nmcli conn down vpn