2026/07/06(月)nginxのリバースプロキシが上手くいかない問題を解消した

ConoHa VPSをフロントエンドにし、自宅サーバーにリバースプロキシする構成を組んだ時のトラブルメモ。

起きていた条件

nginxからnginxへリバースプロキシする構成で前段がTLS終端で、後段へはHTTPで接続していた。前段はVPS、後段は物理サーバーで別の環境。

起きていた事象

lycolia.info*.lycolia.info全体で前段が後段に接続できない状態になっていた。

curl -iSsl4 https://blog.lycolia.info/
HTTP/2 000 
server: nginx
date: Fri, 03 Jul 2026 15:24:44 GMT

前段のnginxにcurlを投げるとステータスコード000が返ってきて、curlが終了コード23で異常終了していた。

2026/07/04 00:11:33 [error] 2537#2537: *12468 upstream sent no valid HTTP/1.0 header while reading response header from upstream, client: xxx.xxx.xxx.xxx, server: lycolia.info, request: "GET /pub/lycolia/rss.xml HTTP/2.0", upstream: "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80/pub/lycolia/rss.xml", host: "blog.lycolia.info", referrer: "https://blog.lycolia.info/pub/lycolia/rss.xml"

nginxのエラーログには「アップストリームからのレスポンスヘッダーの読み取り中に、アップストリームが有効な HTTP/1.0 ヘッダーを送信しませんでした。」という意味のエラーを出力していた。

後段のnginxには何もログが出ていなかった。

curl -v --http1.1 -H 'Host: blog.lycolia.info' "http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80/"
*   Trying [2400:4153:8f01:c800:c14b:3f7a:2b54:353a]:80...
* Connected to 2400:4153:8f01:c800:c14b:3f7a:2b54:353a (2400:4153:8f01:c800:c14b:3f7a:2b54:353a) port 80
* using HTTP/1.x
> GET / HTTP/1.1
> Host: blog.lycolia.info
> User-Agent: curl/8.14.1
> Accept: */*
> 
* Request completely sent off
* Received HTTP/0.9 when not allowed
* closing connection #0
curl: (1) Received HTTP/0.9 when not allowed

前段のnginxのあるVPSから、後段のnginxに対してhttpバージョン1.1を明示して叩くと「HTTP/0.9 when not allowed」というエラーが出てきた。HTTP/0.9…?初めて聞く概念だ…。

curl -s --http0.9 'http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]/' | xxd           
00000000: 0000 1204 0000 0000 0000 0300 0000 8000  ................
00000010: 0400 0100 0000 0500 ffff ff00 0004 0800  ................
00000020: 0000 0000 7fff 0000 0000 0807 0000 0000  ................
00000030: 0000 0000 0000 0000 01                   .........

httpバージョン0.9を明示して叩き、帰ってきたレスポンスのバイナリ。端的に言うとプロトコルエラーで帰れと言われているが、理由は後述する。

解決した方法

+listen [::]:80;
-listen [::]:80 http2;

後段のmstdn.lycolia.infoの設定を上記に変更することで、他のドメインでも問題が解決した。

何故解決したかはわからないが、恐らく80 http2と書くと、他の80番ポートのリッスンにも影響が波及するのだと思う。

http2通信にしなくても問題ないのかでいうと、Mastodonは問題なさそうに見えたのでたぶん大丈夫なんだと思う。知らんけど。

何故解決したのか

使用しているnginxのバージョンがproxy_passするときにhttp 2.0をサポートしてなかったからだ。

nginx 1.29.4になるとproxy_passでhttp 2.0がサポートされるようで、2025年12月9日のリリースには以下の一文があった。

*) Feature: the ngx_http_proxy_module supports HTTP/2.

この時ばかりは今までnginxのバージョンに興味が微塵になかった私もバージョンを上げたくなった瞬間だった。

ただ後続のバージョンのバグフィックスを見るに結構バグが出てそうなので、今あげるのは時期尚早かもしれない。まぁ現状困ってないので上げなくてもいいのは幸いだ。

ところでv1.25.1になるとlisten単位のhttp2が非推奨になり、代わりにhttp2 onという全体単位?が推奨設定になるため、将来的に現状のポート単位にhttpバージョンを分ける技は使えなくなりそうである。

あとがき

バイナリ読解

前述したHTTP 2.0のバイナリレスポンスを読んでみようのコーナー。

curl -s --http0.9 'http://[2400:4153:8f01:c800:c14b:3f7a:2b54:353a]/' | xxd           
00000000: 0000 1204 0000 0000 0000 0300 0000 8000  ................
00000010: 0400 0100 0000 0500 ffff ff00 0004 0800  ................
00000020: 0000 0000 7fff 0000 0000 0807 0000 0000  ................
00000030: 0000 0000 0000 0000 01                   .........

そのバイナリを見るといいという情報をやってみたので試してみた結果。

まず16進数なので1桁に0-Fが入る。これは0000~1111ということだ。つまり1桁が4bitであることが分かる。

さて、HTTP/0.9は一旦忘れて、先ほどやってきたHTTP/2.0として解釈してRFC 9113 - HTTP/2を読んでみる。

「4. HTTPフレーム」によればHTTPフレームの先頭24bitがLength、8bitがType、8bitがFlag、1bitがReserved、31bitがStream Identifierらしい。

つまり先頭を分解するとこうなる。

Length: 00 00 12
Type: 04
Flag: 00
Reserved+Stream Identifier: 00 00 00 00

Lengthだ。0x000012なので18個の8bitフィールドがある。

次の8bitがTypeで0x04となっている。0x046.5.1. 設定フォーマットだ。

Reserved+Streamは全て0なので無価値だろう。

設定フォーマットではそこから48bit単位が設定フィールドとなる。先頭16bitがIdentifier、後ろ32bitがValueだ。

18個の8bitフィールドが設定になるため18 * 8 = 144, 144 / 48 = 3で3つの設定が存在すると読める。

00 03 00 00 00 80

つまりこの部分になる。

Identifierは00 03なのでSETTINGS_MAX_CONCURRENT_STREAMS。最大同時ストリーム数だ。

Valueは00 00 00 80なので128

次は00 00 00 00 10なのでSETTINGS_HEADER_TABLE_SIZE。ヘッダ圧縮テーブルの最大サイズ。

04 00 01 00なので67,109,120。オクテット単位なので単位はバイトと思われる。オクテットのオクはオクトパスのOctと同じなのでタコの脚は八本と考えると覚えやすい。Octoberも旧暦の8月だからOct。

次は00 00 05なので、SETTINGS_MAX_FRAME_SIZE

00 ff ff ff 00なので4,294,967,040。これも単位なので単位はバイトと思われる。

これで設定が終わり、またフレームに戻る。

00 0004 0800
0000 0000 7fff 0000 0000 0807 0000 0000
0000 0000 0000 0000 01

これが残り。

Length: 00 00 04 → 4 * 8 = 32bit
Type: 08WINDOW_UPDATE
Unused Flags: 00
Reserved+Stream: 00 00 00 00
Reserved+Window Size Increment: 7f ff 00 002,147,418,112

次の残り

0000 0807 0000 0000
0000 0000 0000 0000 01

Length: 00 00 08 → 64bit
Type: 07帰れ
ここまでくれば尻だけ読めばいいので中間は読み飛ばす。
最後の32bitがエラーコードらしいので00 00 00 01が恐らくエラーコード。意味合い的にはPROTOCOL_ERROR

要するにHTTP 2に対してHTTP 1.1で接続していたのでプロトコルエラーが返却され、しかしnginxは理解できなかったのでHTTP 0.9として解釈していたのだろう。

今回組んだ構成

今回nginxからnginxにリバースプロキシをしたわけだが、結果として上図のような構成にした。前段にはConoHa VPSを利用している。

これをした理由としては、以前フレッツ光クロスでフルポート使えるIPv4が取れないことが判明したため、クロスに移行出来るようにするためというのと、IPv4のためにDDNSし続けるのが地味に面倒なのと、宅内からIPv4を排除したかったところによる。

ただこれはこれでVPS側にもTLS証明書が必要になるため、DDNSが消えてもIPv4保守のための呪いはまだ残る。DDNSよりはマシだが…。

また、この構成では前後でnginxのバージョンが異なり前段がv1.26.3、後段がv1.24.0となっているため、前段ではhttp2 onにしないと警告が出るなど、バージョン差異による違いが微妙にある。

結果的にできた構成

今回の施策によって自宅サーバーからIPv4が失われても、AレコードをConoHa VPSに向けることでIPv4を受けることが可能な体制の検証をすることができた。これによってフレッツ光クロスに乗り換えたときのIPv4不在問題を解消できる状態となった。

実運用に移すためにはVPS側にもTLS証明が必要になるので自宅サーバーからSCPで送るか、VPS側でも取得する必要が出てくるので、まだまだ対策が必要だ。後者は同一ドメインに二重に証明書が存在する状態になるのでなんか嫌だし、恐らく前者でやるだろう。TCP パススルーなる技術も健闘したが、真のクライアントIP転送問題など、いろいろ厄介そうなのでやめた。

SCPで送る場合はパスフレーズ付きの鍵認証を突破させ、更にnginxをリロードさせないといけないからこちらもやや頭が痛い。パスフレーズ付きのSSH鍵を無人突破させるにはどこかに平文でパスフレーズを持つ必要がある。暗号化もできるがどんどん複雑に…。

或いはDNS-PERSIST-01を使うことで証明書更新を不要にするのも一つの選択肢かもしれないので、こちらも検討していいかもしれない。

しかしVPSがあることで監視対象が増えるなど、なかなか複雑化してしまうので、これを軌道に乗せるかどうかは現時点では不透明だ。ただConoHa VPSは最低スペックで36ヶ月契約であれば月額293円のため、36ヶ月換算でも10,548円にしかならず、IPv4を保持する手段としてはお手頃である。安いISPでもv4を買おうとすると月2~3,000円はするし、まともなところだと一万円以上するので、IPv4にサーバーがついてこの価格は破格と言える。

帯域は100Mbpsだが、100Mbpsあれば十分だろう。

おまけConoHa VPSで帯域実測

自宅サーバーとConoHa VPS間でiperf3をやってみた結果。自宅サーバーへの穴あけが面倒だったので自宅サーバー→ConoHaという逆経路でしか試していない。

Interval Transfer Bitrate Retr Cwnd
0.00-1.00 sec 24.4 MBytes 204 Mbits/sec 0 1.42 MBytes
1.00-2.00 sec 10.9 MBytes 91.2 Mbits/sec 0 1.43 MBytes
2.00-3.00 sec 12.2 MBytes 103 Mbits/sec 0 1.43 MBytes
3.00-4.00 sec 11.0 MBytes 92.3 Mbits/sec 0 1.43 MBytes
4.00-5.00 sec 10.9 MBytes 91.2 Mbits/sec 0 1.43 MBytes
5.00-6.00 sec 12.2 MBytes 103 Mbits/sec 0 1.43 MBytes
6.00-7.00 sec 11.0 MBytes 92.3 Mbits/sec 11 1.26 MBytes
7.00-8.00 sec 11.0 MBytes 92.3 Mbits/sec 169 762 KBytes
8.00-9.00 sec 11.0 MBytes 92.3 Mbits/sec 0 807 KBytes
9.00-10.00 sec 12.2 MBytes 103 Mbits/sec 0 838 KBytes
  • sender: 106 Mbits/sec
  • receiver: 103 Mbits/sec

2026/06/01(月)フレッツ光クロスでフルポート使えるIPv4が取れるか調べた

フレッツ光クロスとフレッツ光ネクスト隼のコスト比較をしてみたの続きのような記事。

結論から言うと、現実的は難しい。金があれば取れる。

フレッツ光クロス x OCNでの対応状況

取り敢えず私はOCNユーザーなので、まずはOCNの対応状況から調べてみることにした。

OCN IPv6インターネット接続|光回線 | OCNお客さまサポートによると次のようにあり、使えないことが分かった。

(OCN 光 with フレッツ クロス / OCN インターネット 10ギガはPPPoE接続できません。)

またOCNサポートに問い合わせたところ、OCNインターネット10ギガはIPoEのみ対応で、方式はMAP-E、OCNバーチャルコネクトとのことだった。そしてPPPoEは使えず、方式上フルポート使えるIPv4の提供はないと言う話だった。

兵庫県下におけるフレッツ光クロスで従来型IPv4の提供があるISPについて

フレッツ光 対応プロバイダー検索|NTT西日本公式に一覧があるが、対応しているのはIIJのみ。

IIJ IPv6 FiberAccess/F サービス タイプIPoEであればIPv4アドレスの固定割り当てが取れるそうだが、月額14,000円~ということで、到底支払える値段ではない。

IIJのフレッツ光ネクストはIPv6 IPoE、動的IPv4アドレスの場合は2,200円とのことなので、恐らくフレッツ光クロスにするとIPv4が高くなるのだと思う。

あとがき

今回の調査では、前段に外部のリバースプロキシやそれに類するものを置かず自宅サーバーを運用する場合にIPv4を切る必要が出てくることが判明した。

前回の調査ではフレッツ光クロスにすると年3万円コスト増になることが分かっているため、これと合わせるとクロスに移行する場合、年3万の負担増とIPv4でのサーバーサービスを終了しないとならないことが分かった。

中々悩ましい結果だが、回線が逼迫しているわけではないので、OpenWrtのメトリクスなどをもっとちゃんととって、必要が出たあたりで移行できたらいいなとは思う。今でも偶に不審な速度になることはあるので、それの裏が取れたら移行してもいいだろうとは思った。

2026/02/22(日)OpenWrtからValue-Domainに複数サブドメインのDDNSを行うツールを作った

Value-DomainのダイナミックDNSエンドポイントは60秒以内に叩くとエラーが返ってくるので、これを回避するためのDDNSの仕組みを作った話。

やったこと

まず、Value-DomainのDNS APIに対し、既存のaレコードをバルクで差し替えるためのツールとしてvd-ddns_v4.plを作った。

そしてhotplug.dにPPPoEインターフェースのIPが変わったときに、このスクリプトを蹴る処理を書いた。

この記事の前提構成

OpenWrtにIPv4用のPPPoEインターフェースがある。

やったこと

  1. 今回利用するのに必要なPerl周りをセットアップする。ストレージの空きが3.2MBほど必要
  2. value-domain-dns-util/root配下とか適当な場所に放り込む
    • opkg install openssh-sftp-serverでSFTPを導入しておくとファイル移動に便利
  3. vi /etc/hotplug.d/iface/40-pppoeとかして、OpenWrtのインターフェースが変化したときのHookを作る

     #!/bin/sh
     # デバイスが存在しなければ終了
     [ -n "$DEVICE" ] || exit 0
     # リンクアップでなければ終了
     [ "$ACTION" = ifup ] || exit 0
     # インターフェース名がPPPoEのものでなければ終了
     [ "$INTERFACE" = wanppp ] || exit 0
     # pppoe-wanpppのIPv4アドレスを取得
     pppoeaddr=$(ip -4 addr show pppoe-wanppp | head -2 | tail -1 | awk '{print $2}')
     # ログに吐く
     logger -t "DDNS - PPPoE IP" $pppoeaddr
     # DDNSもどきを叩く
     /root/vd-dns-util/vd-ddns_v4.pl 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' example.com $pppoeaddr hoge fuga piyo
     # 結果をログに吐きたいが何故か動いていない
     if [ $? -eq 0 ]; then
     logger -t "DDNS - UPDATE SUCCEED"
     else
     logger -t "DDNS - UPDATE FAIL"
     fi
    
  4. PPPoEインターフェスをRestart

  5. Value-Domainのコンパネで指定したドメインのaレコードが更新されていることを確認

備考

一般的にDNSレコードの浸透時間は更新前に浸透していたTTLに依存し、Value DomainのAPI経由で普段から操作している場合は120秒以下にならないため、最大120秒のダウンタイムが発生するが、理論上は公式のDDNS機能と大差ないはずと思われる。

aレコードのみの対応にしているのは私の環境だとIPv6は変動しないが、v4はそれなりの頻度で変わるためだ。

運用しているMastodonのv4が変わったのに気づかないまま疎通できないインスタンスが出てくることがしばしばあり、手動で対応するのが手間なのと、毎回一日くらい気づくのに遅れるので今回自動化に踏み切った。前々からやり勝ったのだが、中々腰が重く進んでいなかった。

そもそもこの手のものは監視システムで検知できて然るべきなので、おいおい監視システムの構築もしていきたいところだ。

2025/08/22(金)OCNバーチャルコネクト環境でIPoE・PPPoE併用のIPv4、IPv6デュアルスタックでサーバーを公開する

この記事はフレッツ光ネクストを対象としたものです。フレッツ光クロスには当てはまらないので注意してください。

OCNバーチャルコネクト環境でIPv6サーバーをポート443で公開した時にやったことを派生させて更にIPv4にも対応させるプラン。

OCNバーチャルコネクトのIPoEでIPv4 over IPv6のMAP-E環境だと、IPv4ではWell-known portsが開けないが、PPPoEを使って取得したIPv4であれば開けるので、これとIPoE側のIPv6の併用でデュアルスタックでWell-known portsの開放を行い、サーバーを運用できるようにするのが目的だ。

確認環境

環境 内容
ISP OCN光
ISP契約 OCN 光 with フレッツ マンション・スーパーハイスピード 隼・プラン1・西日本
ISP接続方式 OCNバーチャルコネクト(IPoE, MAP-E)
RouterOS OpenWrt 24.10.0
ServerOS Ubuntu 24.04.3 LTS
HTTPD nginx 1.26.1
ドメインレジストラ Value Domain

今回の要件

やり方

サーバー側の準備

ufwではv4にも穴が開いている状態とする

  1. nginxの設定を開きIPv4向けにlisten 443 ssl;を追記して保存する
  2. nginxを再起動する

PPPoEインターフェースの作成

  1. SSHでOpenWrtに入る
  2. /etc/iproute2/rt_tablesを開き次のようにpppoeの行を追加、保存
    #
    # reserved values
    #
    128     prelocal
    255     local
    254     main
    253     default
    300     pppoe
    0       unspec
    #
    # local
    #
    #1      inr.ruhep
    
  3. 反映する
    • service network restart
  4. LuCIに入る
  5. Network→Interfacesを開く
  6. Add new interfaceから、Nameをwanppp、ProtocolをPPPoEとしたインターフェースを作成する
  7. General SettingsタブのDeviceにONUと疎通しているポートを指定し、PAP/CHAP usernameとPAP/CHAP passwordにISPの接続情報を入れる
  8. Advanced Settingsタブを開き、Use DNS servers advertised by peerのチェックを外し、Use gateway metricを10、Override IPv4 routing tableをpppoe (300)にする
  9. Saveボタンを押して保存する
  10. wanインターフェースのEditを押す
  11. Advanced Settingsタブを開き、Use gateway metricを1にする
  12. Saveボタンを押して保存する

ファイアーウォールの作成

  1. 引き続きLuCI上で作業する
  2. Network→Firewallを開く
  3. ZonesのAddボタンを押す
  4. Nameをwan_pppoeにし、MasqueradingとMSS clampingにチェックを入れ、Covered networksでwanpppを選択し保存する
  5. lan⇒wanのゾーン設定のEditボタンを押す
  6. Allow forward to destination zones:にwan_pppoeを追加し、保存する
  7. Network→Interfacesを開く
  8. wanpppインターフェースのEditボタンを押す
  9. Firewall Settingsタブを開き、Create / Assign firewall-zoneにwan_pppoeが設定されていることを確認する。設定されていなかったら設定する

サーバーだけをPPPoEのIPv4通信の対象にする

  1. 引き続きLuCI上で作業する
  2. Network→Routingを開き、IPv4 Rulesタブを開く
  3. General SettingsタブのIncoming interfaceをlanに、SourceをサーバーのIPにする(例:192.168.1.5/32
  4. Advanced Settingsタブを開く
  5. Tableにpppoe (300)を設定し、保存する

NATを設定する

  1. 引き続きLuCI上で作業する
  2. Network→Firewallを開き、Port Forwardsタブを開く
  3. Addボタンを押す
  4. Nameに適当な名前を付け、Restrict to address familyをIPv4 only、Source zoneをwan_pppoe、External portを443、Internal IP addressを転送先のサーバーIP、Internal portを443にして、保存する
  5. 最後にこれまでの内容をSave & Applyする

疎通確認

  1. Network→Interfacesを開き、wanpppのIPv4を拾う
  2. ドメインレジストラの設定でAレコードにこれを登録
  3. 適当な外部サーバーからcurlを投げて中身が返ってきたらOK
    curl -4 -v https://example.com
    

IP変動対策にDDNSを設定する

/etc/hotplug.d/iface40-pppoeというファイルを755で作り、以下の内容を記述する。

#!/bin/sh

[ -n "$DEVICE" ] || exit 0
[ "$ACTION" = ifup ] || exit 0
[ "$INTERFACE" = wanppp ] || exit 0

vdpw=ここにDDNS更新用のパスワード
pppoeaddr=$(ip -4 addr show pppoe-wanppp | head -2 | tail -1 | awk '{print $2}')
logger -t "DDNS - PPPoE IP" $pppoeaddr
# hoge.example.comに対して更新リクエストを飛ばす例
logger -t "DDNS - DOMAIN: hoge" $(wget -O - "https://dyn.value-domain.com/cgi-bin/dyn.fcg?d=example.com&p=$vdpw$&h=hoge&i=$pppoeaddr")

# When making requests for multiple domains, wait 60 seconds between each request

このスクリプトはhotplugイベントが発生したときにファイルパス順に呼ばれるスクリプトらしい。

ひとまずデバイスでなく、インターフェイスが起動したときで、かつインターフェース名がwanpppの時に実行されるように組んである。

注意点として、このエンドポイントはレートリミットがシビアで、60秒以内に送ると503を返してくるようなので、複数回投げる場合はsleep 61とかして間隔をあけると良さそうだ。数が増えてきたらバリュードメインAPIのDNS設定APIを利用した方が早いだろう。

ただしこのAPIはTTLの制御に難があり、処理方法によっては3600秒に固定される罠が潜んでいるので注意が必要だ。

トラブルシュート

curlで「Connection refused」や「接続が拒絶されました」と出る

  • /var/log/ufw.logにログがあればufwで穴をあける(まずこれはない)
  • ファイアーウォールかルーティングかNATの設定のどこかがおかしいので見直す。

同一LANにぶら下がっている端末でDNS解決に失敗する

PPPoEインターフェース(wanppp)の設定を開き、Advanced SettingsからUse DNS servers advertised by peerのチェックを外すと直る。

雑感:やや苦しい

IPv4, v6のデュアルスタックに出来たのはいいが、幾つかの端末で繋いだところIPv4側に流れるケースがあった。メインのv6でなく、オプションのv4に行くという状況はやや残念だ。

構成的にもマルチセッションという微妙な形態で、v4とv6でインターフェースが二系統あり、セキュリティもNATとFWがあり、なかなか複雑な状態になってしまった。

Mastodonにおいてはv4IPのインスタンス(兵庫丼)との疎通はTLレベルでは出来たものの、画像が出なくなるという頭が痛い問題に出会った。

Misskey.ioからは疎通できず、こちらからはユーザープロフィールまではアクセスできるがフォローを押しても音沙汰がない。

結論としてはリッチなアプリケーション通信を伴う用途において、この方式は向かない気がした。恐れくこれはルーターより外側がIPoEとPPPoEで別系統になっている関係で、通信が混乱して上手くいかないパターンがあるのではないかと感じている。

DDNSも制約が厳しいし、Value DomainのAPIはあるだけマシではあるものの、超絶使いづらい問題もあり、この環境においてのデュアルスタックはあきらめようと思った。

まぁいい勉強にはなったので、そのうち何かに活かせるだろう、きっと。

2026-05-23追記:この状態で安定運用しているがv4側が疎通しない相手については諦めた

OpenWrtからValue-Domainに複数サブドメインのDDNSを行うツールを作ったのもあり、IPv4が変わったときも自動で切り替えることでダウンタイムを減らす運用ができたので、今のところ不満はない。

ただFediverseで繋がらないところは繋がらないので諦めることにした。