nginxで特定パスだけクライアント認証を除外しつつtry_filesで内部リダイレクトしたい。

特殊なケースすぎてもう使うことはない設定だと思うけど、全然思い通りにいかなくて悩んでしまったのでメモ。
検証に使ったnginxのバージョンは1.18.0です。

達成したいこと

条件

  • / 全体 および /ignore 内では、php-fpmによりphpが実行できるようにしたい。対象の拡張子は.phpとする。
  • / 全体 および /ignore は設定で try_files $uri $uri/ /index.php?$args; による内部リダイレクトをしているので、https://example.com/index.php は状況に応じてクライアント認証を適用・除外する必要がある。

ダメだった方法

server {
    listen 443 ssl http2;
    ### ...いろいろ省略...

    ### クライアント認証設定
    ssl_client_certificate /etc/nginx/ssl/xxx.pem;
    ssl_verify_client optional;

    location / {
        if ($ssl_client_verify != SUCCESS) {
            return 403;
        }
        try_files $uri $uri/ /index.php?$args;
    }

    location /ignore {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        if ($ssl_client_verify != SUCCESS) {
            return 403;
        }
        fastcgi_pass unix:/run/php-fpm/www.sock;
        ### いろいろ省略
    }
}

locationディレクティブでのifの使用は推奨されていませんが、ifを使わずに特定ディレクトリを除外する方法はおそらく存在しないため、ifを使いました。
If is Evil… when used in location context | NGINX

ぱっと見は動いてるように見えたのですが https://example.com/ignore/example.php にアクセスするとクライアント認証を要求されます。location ~ \.php$が先に評価されるからですね。

今回採用した方法

server {
    listen 443 ssl http2;
    ### ...いろいろ省略...

    ### クライアント認証設定
    ssl_client_certificate /etc/nginx/ssl/xxx.pem;
    ssl_verify_client optional;

    location / {
        if ($ssl_client_verify != SUCCESS) {
            return 403;
        }
        set $verified T;
        try_files $uri $uri/ /index.php?$args;
    }

    location /ignore {
        set $verified T;
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        set $check NG;
        if ($ssl_client_verify = SUCCESS) {
            set $check OK;
        }
        if ($verified = T) {
            set $check OK;
        }
        if ($check != OK) {
            return 403;
        }

        fastcgi_pass unix:/run/php-fpm/www.sock;
        ### いろいろ省略
    }
}

認証する必要がない・認証済みの場合を表すフラグを設定して、php実行時にそのフラグを評価するようにしました。ifを使わずに実現する方法がわかったらそちらを使いたいですね…。

VTuberがポケモンランクマ配信すると対戦環境に変化があるのかをmetabaseで確認する。

今のガラルの状況

端的に説明されてるツイートがあったので引用します。

前置き

2020/11/6現在のシングル環境トップは大方の予想通りれいじゅうランドロスです。ただでさえダイジェット最強の現環境でこんな化け物きたらトップになるに決まってるだろ。

私はあまりゲームの配信は見ないのですが、特定の配信者の方の動画は例外的に見ています。私の好きなガラルがアローラになっていく中、以下の動画が話題になりました。

話題になってるのでここで改めて説明する必要はないと思いますが、一応概要を書くとこんな感じです。

  1. ゾロアークにねらいのまとをもたせてレジエレキに化ける
  2. レジエレキ(実際はゾロアーク)を見た相手がランドロスを出してくる or 初手ランドロスならランドロスで居座る
  3. そのままゾロアークのトリックで相手のランドロスにねらいのまとを押し付ける
  4. ねらいのまとの効果で、本来電気無効のランドロスに電気が効果抜群で入るようになる
  5. 電気が一貫するためレジエレキで全員倒す

ランドロスにねらいのまとを押し付ける戦法は以前からあったようなのですが、トリック役にゾロアークを採用したのは本当に天才だと思いました。
(この動画を投稿されてる方、毎回戦術が狂っていてめちゃめちゃ面白いので、対人戦をしていない方にも視聴をお勧めしたいのですが、トークの内容がセンシティブすぎて人を選ぶのでなかなかそうはいかないのがつらいです。)

今回の戦術は非常に強力で話題になっていたので、実際に環境にも影響があったのでは?と思いmetabaseで確認することにしました。

ゾロアークの持ち物に変化があるかを確認する

ランクマ実況の配信開始が2020/11/02 22:00なので、配信開始から2020-11-03 23時台までのデータで変化を確認してみます。
ポケモンホームのデータ配信時刻の取得に失敗して、何時間分かデータが欠損しているのですがそこは目をつむってください。

import json
import urllib.request
import sqlite3
import datetime
from contextlib import closing

# ポケモンホームのデータ配信時刻の配列
ts2 = [
    1604323178,
    1604326778,
    1604330378,
    1604333978,
    1604337577,
    1604341178,
    1604344778,
    1604348380,
    1604355578,
    1604359179,
    1604373579,
    1604377179,
    1604380780,
    1604384379,
    1604387979,
    1604395179,
    1604398780,
    1604405980,
    1604413180,
]

pokedex = ''
#  https://resource.pokemon-home.com/battledata/js/bundle.js の図鑑と持ち物情報を抜き出してjsonにしたものを読み込む
with open('./bundle.json', 'r') as json_open:
    pokedex = json.load(json_open)

user_agent = 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36'
headers = {
    'User-Agent': user_agent,
    'countrycode': '304',
    'authorization': 'Bearer',
    'langcode': '1',
    'accept': 'application/json, text/javascript, */*; q=0.01',
}

with closing(sqlite3.connect("zoroark.db")) as conn:
    c = conn.cursor()
    create_table = '''create table item (time text, item text, adoption_rate real)'''
    c.execute(create_table)

    for item in ts2:
        # シーズン12のシングルのID:10121
        url = f"https://resource.pokemon-home.com/battledata/ranking/10121/0/{item}/pdetail-3"
        pdetail = ""
        req = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(req) as res:
            pdetail = json.load(res)
        dt = datetime.datetime.fromtimestamp(item)
        # ゾロアークの図鑑番号は571で単フォルム
        zoroark = pdetail["571"]["0"]["temoti"]["motimono"]
        for pokeitem in zoroark:
            sql = 'insert into item (time, item, adoption_rate) values (?,?,?)'
            c.execute(sql, (dt.strftime('%Y-%m-%d %H:%M:%S'),
                            pokedex['item'][pokeitem['id']], pokeitem['val']))
    conn.commit()

metabaseでテーブルを読み込ませます。v0.37.0.2を使いました。
VisualizationからLineを選ぶだけでグラフが表示されます。さすが簡単すぎる。

file

file

ほんとに使用率上がってて笑いました。すごいですね。

ゾロアークの使用率に変化があるかを確認する

使用率の順位を取得します。

import json
import urllib.request
import sqlite3
import datetime
from contextlib import closing

ts2 = [
    1604323178,
    1604326778,
    1604330378,
    1604333978,
    1604337577,
    1604341178,
    1604344778,
    1604348380,
    1604355578,
    1604359179,
    1604373579,
    1604377179,
    1604380780,
    1604384379,
    1604387979,
    1604395179,
    1604398780,
    1604405980,
    1604413180,
]

pokedex = ''
#  https://resource.pokemon-home.com/battledata/js/bundle.js の図鑑と持ち物情報を抜き出してjsonにしたものを読み込む
with open('./bundle.json', 'r') as json_open:
    pokedex = json.load(json_open)

user_agent = 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36'
headers = {
    'User-Agent': user_agent,
    'countrycode': '304',
    'authorization': 'Bearer',
    'langcode': '1',
    'accept': 'application/json, text/javascript, */*; q=0.01',
}

with closing(sqlite3.connect("rank.db")) as conn:
    c = conn.cursor()
    create_table = '''create table pokemon (time text, pokemon text, rank integer)'''
    c.execute(create_table)

    for item in ts2:
        url = f"https://resource.pokemon-home.com/battledata/ranking/10121/0/{item}/pokemon"
        pokemon = ""
        req = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(req) as res:
            pokemon = json.load(res)
        dt = datetime.datetime.fromtimestamp(item)
        rank = 1
        for poke in pokemon:
            sql = 'insert into pokemon (time, pokemon, rank) values (?,?,?)'
            c.execute(sql, (dt.strftime('%Y-%m-%d %H:%M:%S'),
                            pokedex['poke'][int(poke['id']) - 1], rank))
            rank += 1
    conn.commit()

file

metabaseでy軸を昇順ではなく降順で表示する方法がわからなかったので、ちょっとわかりにくいグラフになっています。順位でプロットしているので、0に近づくほど使用率が高いです。
ゾロアーク自体は配信直後で大幅に使用率が変わったわけではないんですね。

シーズン12は刑期が終わったサッカーうさぎと準伝説とUBが跋扈するとんでも環境なので、7世代やってないし剣盾から対戦始めた人間には非常につらいです。地獄かな?

Gridsomeでsource-wordpress使用時に目次機能をつける。

実現したいこと

ZennのUIが好きなのですが、自分のブログにも記事の目次を付けたいなと思いました。こういうやつです。

file

@gridsome/vue-remarkだとそこまで苦労せずに生成できそうなのですが、私が使ってるのは@gridsome/source-wordpressなので、WordPressのAPIから取得したhtmlタグから<h2>,<h3>,<h4>タグを抜き出して目次機能を作ることにしました。

動作確認環境

 System:
    OS: Linux 4.19 Ubuntu 20.04.1 LTS (Focal Fossa)
  Binaries:
    Node: 14.15.0 - /usr/local/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 6.14.8 - /usr/local/bin/npm
  npmPackages:
    @gridsome/source-wordpress: ^0.5.3 => 0.5.3
    gridsome: ^0.7.21 => 0.7.21
  npmGlobalPackages:
    @gridsome/cli: 0.3.4

大まかにやること

  1. gridsome.server.jsで記事本文から<h2>,<h3>,<h4>textContent(見出し)を抜き出す
  2. template内で目次を表示する

gridsome.server.jsで見出しタグのtextContentを抜き出す

見出しタグのtextContentを毎回動的に取得するのは無駄なので、textContentの内容をGraphQLのクエリで参照できるようにします。
見出しタグのレベルに応じてCSSのクラスを変更する都合上、該当の見出しタグのレベルも参照したいので、専用のScheme Type:Tocを定義しています。

const DOMParser = require('universal-dom-parser');
module.exports = api => {
  api.loadSource(({ addSchemaTypes, addSchemaResolvers }) => {
    // 見出しタグ用のスキーマを定義する
    addSchemaTypes(`
      type Toc implements Node {
        id: ID!
        textContent: String
        nodeName: String
      }
    `)
    addSchemaResolvers({
      BlogPost: {
        tocTargets: {
          type: "[Toc]",
          resolve(node) {
            // WordPressから取得した記事本文のhtmlをパースする
            const parser = new DOMParser();
            const doc = parser.parseFromString(`<html>${node.content}</html>`, "text/html");
            const targets = doc.querySelectorAll("h2,h3,h4");
            const tocTargets = [];
            let countId = 1;
            targets.forEach((target) => {
              tocTargets.push({
                id: `title-${countId}`,
                textContent: target.textContent,
                nodeName: `level-${target.nodeName.toLowerCase()}`,
              });
              countId++;
            });
            return tocTargets;
          },
          // 目次をクリックした際に記事の該当箇所にジャンプしたいので、
          // 見出しのタグに目次のアンカーと同じIDを追加する
          replacedContent: {
            type: "String",
            resolve(node) {
              const parser = new DOMParser();
              const doc = parser.parseFromString(`<html>${node.content}</html>`, 'text/html');
              const targets = doc.querySelectorAll("h2,h3,h4");
              let countId = 1;
              targets.forEach((target) => {
                target.id = `title-${countId}`;
                countId++;
              });
              // 見出しタグにIDを追加したhtmlを返却する
              return doc.body.innerHTML;
            }
          },
        }
      }
    })
  })
}

WordPressから取得した記事本文のhtmlをパースするためにDOMParserを使用したいのですが、Server Side Rendaringだと使えないAPIなので、coolov/universal-dom-parserでDOMParserを呼び出しています。
(もちろん、DOMParserに頼らなくても正規表現を使えば抜き出せます。)

templateで目次を表示する

<li>タグにv-forを設定し、前工程で設定したTocの配列を一覧で表示します。
h3の次にh4が来る見出しの場合、それに合わせて目次のulも入れ子にするべきですが、今回はそこまでこだわらないので、ulは入れ子にせずCSSで表示を変えています。

<template>
  <div>
    <ul>
      <!-- 記事の目次 -->
      <li
        v-for="target in $page.blogPost.tocTargets"
        :key="target.id"
        :class="target.nodeName"
      >
        <a :href="'#' + target.id">{{ target.textContent }}</a>
      </li>
    </ul>
    <!-- 記事本文 -->
    <div v-html="$page.blogPost.replacedContent" />
  </div>
</template>

<style>
.level-h2 {
  padding-left: 10px;
}
.level-h3 {
  padding-left: 20px;
}
.level-h4 {
  padding-left: 30px;
}
</style>

<page-query>
query BlogPost($path: String){
  blogPost(path:$path) {
    replacedContent
    tocTargets {
      id
      textContent
      nodeName
    }
  }
}
</page-query>

動作デモ

この記事自身の右サイドバーで動作確認できます。

余談

yaowei9363/vue-side-catalog: A Catalog Component for Vue.jsを使用すると同じことが実現できるのですが、目次のpositionをstickyにすると動作しなくなるため、導入を諦めました。自分で実装したほうが自由度が高いですし、Vueのバージョンアップ時にバグっても自分で直せるので、結果的に自作してよかったと思います。

あと、Zennのほうには月別アーカイブ機能の実装方法の記事を書きました。
Gridsomeで月別アーカイブ機能を作成する。

WordPressで記事を投稿したらGitHub Actions経由でGridsomeのビルドをする。

Markdownに変換した既存のブログ記事をちまちま修正するのがしんどいのと、WordPressをHeadless CMSとして運用した方が楽だと判断したので、以下の構成でこのブログを運用することにしました。

  • 記事の作成
    • Headless CMS化したWordPress
  • 記事・RSS・サイトマップのビルド
    • Gridsome
  • ホスティング
    • 自分が借りてるVPS

自分以外の人はWordPressにアクセスできないため、セキュリティ対策やプラグインの脆弱性に煩わされることがなくなり、運用の負荷が軽くなったのが最高です。あとテンプレートを修正するのにPHPをいじる必要がないのがうれしすぎる。
今の構成だと、WordPressで記事を投稿したタイミングでGridsomeにビルドしてもらう必要があるのですが、毎回自分でビルドコマンドをたたくのは馬鹿らしいし、手元にNode.jsの実行環境がないとビルドできなくて詰むという問題点があります。GitHub ActionsならWebhook経由でgridsomeコマンドが実行できるので、GitHub Actionsを使ってこの問題を解消することにします。

全体の流れ

前提条件として、サイトジェネレーターでビルドするためのソースコードをGitHub上にpushしておく必要があります。プライベートかパブリックかは問いません。

  1. WordPressのアクションフックを使って、記事投稿時にrepository dispatch eventでGitHub Actionsのワークフローを呼び出す
  2. GitHub Actionsのワークフローでyarn->gridsome buildを実行
  3. ビルド後にdistの中身をVPS(ホスティング先)にrsync

1の作業はWordPressのプラグインの作成が必要で、2,3の作業はGitHub Actionsでワークフローを定義する必要があります。

WordPressのプラグインを作成する

gistにNetlifyのWebhookを叩くプラグインがあったので、それを流用しました。ありがたいです。

GitHubのWebhookを呼び出すにあたり、repoにアクセス権のあるPersonal Access Tokenが必要になるので、ない場合は発行しておきます。

<?php
/**
 * Plugin Name: GitHub Actions build hook
 */

// 記事予約投稿時。私は使わないのでコメントアウト
// add_action('publish_future_post', 'nb_webhook_future_post', 10);

// 記事投稿時
add_action('publish_post', 'nb_webhook_post', 10, 2);

// 記事削除時
add_action('delete_post', 'nb_webhook_post', 10, 2);

// 固定ページ投稿時。私は使わないのでコメントアウト
// add_action('publish_page', 'nb_webhook_post', 10, 2);

//  記事更新時。私の環境では記事投稿時にupdatedのアクションも実行されるため、コメントアウト
// add_action('post_updated', 'nb_webhook_update', 10, 3);

function nb_webhook_future_post( $post_id ) {
  nb_webhook_post($post_id, get_post($post_id));
}

function nb_webhook_update($post_id, $post_after, $post_before) {
  nb_webhook_post($post_id, $post_after);
}

function nb_webhook_post($post_id, $post) {
  $header = [
    'Authorization: token {GitHubのPersonal Access Token}',
    'Accept: application/vnd.github.everest-preview+json',
    // phpのcURLはデフォルトでUAが設定されていない
    // GitHubのAPI実行にはUAが必須
    // http://developer.github.com/v3/#user-agent-required
    'User-Agent: WordPress_webhook_post'
  ];
  $data = [
    'event_type' => '{Workflowで使いたい名前}',
  ];
  if ($post->post_status === 'publish') {
    $url = curl_init('https://api.github.com/repos/{アカウント or organization名}/{リポジトリ名}/dispatches');
    curl_setopt($url, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($url, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($url, CURLOPT_HEADER, true);
    curl_setopt($url, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($url, CURLOPT_HTTPHEADER, $header);
    curl_exec($url);
  }
}
?>

GitHub Actionsでワークフローを定義する

特筆すべきことはないです。他人に知られたくない情報はsecretsに設定して、rsync用のアクションに Burnett01/rsync-deployments: GitHub Action for deploying code via rsync over ssh を使っています。

name: webhook-trigger

on:
  # 手動でも実行したいのでworkflow_dispatchを設定
  workflow_dispatch:
  repository_dispatch:
    types:
      - {WordPressプラグインのevent_typeに設定した名前}

jobs:
  build:
    name: build
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v2
    # 私の環境では .envにWordPressのURLを設定している
    # - name: echo env
    #   run: echo "${{ secrets.API_URL }}" > .env
    - name: Install modules
      run: yarn
    - name: Run build
      run:  ./node_modules/.bin/gridsome build > /dev/null
    - name: rsync deployments
      uses: burnett01/rsync-deployments@4.1
      with:
        switches: -avzr
        path: dist/
        remote_path: ${{secrets.DEPLOY_PATH}}
        remote_host: ${{secrets.SSH_HOSTNAME}}
        remote_port: ${{secrets.SSH_PORT}}
        remote_user: ${{secrets.SSH_USERNAME}}
        remote_key: ${{secrets.SSH_PRIVATE_KEY}}

これで記事を投稿したときにGitHub Actions経由でビルド結果がrsyncされるようになりました。

問題点

このままでも特に困らないのですが、repository_dispatchがデフォルトブランチでしか使えないのはかなり痛いです。event_typeの値に応じてワークフロー内でブランチを切り替えるとか、工夫が必要です。
ワークフローをトリガーするイベント – GitHub Docs

Note: This event will only trigger a workflow run if the workflow file is on the default branch

そもそも

ホスティングにNetlifyVercelを使っている場合は、Webhook経由でビルドからデプロイまで勝手にやってくれるので、ここまで苦労しません。
Netlifyは運が悪いとSSL周りでトラブルを引きまくるのと、日本からだと遅いので個人的にはVercelを推しています。シンプルでとても使いやすいですし、情報量はNetlifyに劣りますが、ドキュメントを見れば大体わかるので今のところ困っていないです。

Gridsome周りでいくつか質問を受けたので、やる気が出たら詳細を記事にしようと思います。

RTX1200をCATVで使う。

RTX1100をCATVで使う。 – return $lock;で「RTX1200かRTX810の安いやつが手に入ったら使いたい」と書いたのですが、RTX1200を譲っていただきました。うれしいです。ありがとうございます。(NETGEAR JGS524もいただきましたが、こちらはどう使うか考え中です。)

LAN2をWAN(ケーブルモデムに接続)、LAN1をLANで使用することにします。

セットアップ

前回RTX1100をいじった以上の知識は持ち合わせていない状態からスタートします。ドキュメントを見たり、ぐぐりながらいじってみます。

初期化

前面のDOWNLOADとMicroSDとUSBのボタンを同時押ししながら電源を入れます。ちゃんと押せてなかったようで、私は2回失敗しました。指が短い人には難しいですね。

ファームウェアアップデート

RTX1100と違って、初期化するとIPv4のIP 192.168.100.1が振られるのでtelnet 192.168.100.1でアクセスできます。show configでファームウェアのバージョンが初期状態に戻っていることを確認したため、アップデートします。なお、Rev.10.01.76以下は脆弱性があります。

TFTPを使うしかないと思っていたのですが、USBメモリからもファームウェアの更新ができるそうなので、試してみます。
USBメモリを利用してRTX1200のファームウェアーをアップデートする – YAMAHA RTXルーターの操作方法 設定 Tips
USBボタンとDOWNLOADボタンを押すとアップデートできます。楽でいいですね…。特にトラブルも起こらず完了できました。

初期設定

RTX1100の時と変わらないので割愛。時刻同期の設定ができるので追加で設定しました。午前3時7分にntp.nict.jpに問合せします。
40.1 スケジュールの設定

schedule at 1 */* 03:07 * ntpdate ntp.nict.jp syslog

CATV向けの設定

公式サイトの設定例をそのまま流用しました。
CATVインターネットなどイーサネット回線を利用する
RTX1100の設定をしたときは、設定例と同様にDNSサーバのIPを自分で指定したのですが、前回と同じだとあまり勉強にならないので今回はDNSサーバのIPを自動取得する設定にしてみました。
DNS の設定

dns server dhcp lan2
dns server select 500001 dhcp lan2 any .
dns private address spoof on

WAN側の状態を確認してみましたが、正しくIPが取得できていました。

# 状態確認
# show status dhcpc
Interface: LAN2 primary
            IP address: 163.xxx.xxx.xxx/24
           DHCP server: 119.xxx.xxx.xxx
       Remaining lease: 6hours 59min. 16secs.
      (type) Client ID: xxxxxxxxxxxxxxx
Common information
            DNS server: 219.xxx.xxx.xxx
                      : 219.xxx.xxx.xxx
       Default gateway: 163.xxx.xxx.xxx

無事に外につながるようになりました。ポートもふさがっています。

LAN1に接続した端末のIPアドレスの固定

IPが変わると困る端末はIPを固定します。公式サイトにそのまま設定例がありました。
ゲーム端末のIPアドレスを固定する方法 Example for YAMAHA RT Series / Network Game
家の端末は以下のコマンドを使わないと固定できませんでした。

dhcp scope bind 1 192.168.0.2 ethernet 00:80:98:e0:ee:6f

セキュリティ対策

(RTX1100でできたかわからないのですが、)RTX1200ではできるようなのでやってみます。
9.1.17 侵入検知機能の動作の設定

ip lan2 intrusion detection in on reject=on
ip lan2 intrusion detection out on reject=on
# 破棄されたパケットをログに出力する
syslog notice on

1回だけNintendo SwitchがAWS宛てにTCP FIN and no ACKしてるログが出てきたんですが、謎いです。ネット対戦は普通にできていますし、NATタイプもBなので様子見です。

今後必要になるかもしれない設定

今のところ困ってないですが、VPNの接続先によっては設定が必要になるかもしれないです。
IPsecパススルー : コマンド設定 + Web GUI設定

所感

ルータ入れ替え前と入れ替え後でダウンロード速度を計測しましたが、遅くなった等の問題はなく、むしろちょっと速くなったかな(誤差の範囲)?という感じです。

以前使っていたルータではできないブラックリスト形式のアクセス制限や、(今は必要ないですが、前住んでいる家でやりたかった)ポリシーベースルーティングでIPv6 IPoEを使いながら自宅サーバの公開もできるので、積もっていた不満が解消できました。
RTXシリーズでコマンドが統一されているので、RTX1100と特に変わらず設定できるのは本当にありがたいです。あと、使っている人が多いので、調べるとすぐに解決策が出てくるのは最高だなと思いました。使わないけど無駄にVLAN切ってみたくなります。

自宅サーバを外に出そうと思っているので、DMZ周りの設定をしていこうと思います。
自社サーバーを公開(1つの固定グローバルIPアドレス / DMZセグメント : LAN3) : コマンド設定