続・SameSite指定されたCookieはCORS fetch時にどう働くか

まとめ

A request is "same-site" if its target's URI's origin's registrable domain is an exact match for the request's client's "site for cookies", or if the request has no client.

  • 例えば
    • www.example.com から api.example.com を fetch → 「Registrable Domain」が合致するので「Same-Site」
    • www.foobar.com から api.example.com を fetch → 「Registrable Domain」が違うので「Cross-Site」
  • CORS fetch リクエストに Cookie が付くかどうか
SameSite判定 Set-Cookie時のSameSiteの指定 リクエストにCookieが…
Same-Site none ○付く
Same-Site lax ○付く
Same-Site strict ○付く
Cross-Site none ○付く
Cross-Site lax ×付かない
Cross-Site strict ×付かない
  • ここでの「Same-Site」「Cross-Site」はあくまでも Cookie に対しての話なので、 CORSリクエストのそれとは分けて考えること。
  • サブドメインが違うだけの CORS fetch では SameSite=lax/strict 指定の Cookie もリクエストに付加されるので、そういう構成がとれる場合は API の認証に SameSite=lax/strict な Cookie が使える。
    • ただしブラウザ設定でサードパーティーCookieをブロックするようになっている場合はSet-Cookieがブロックされてしまうので使えない。

経緯とか

昨日すっとぼけたことを書いていたトピック の続きとして、よく見かけるサイトURLが www.example.comAPI用URLが api.example.com のようなサブドメインが違うだけという構成を試してみたところ、Set-Cookie に domain=example.com 指定もしていないのに APIサーバに Cookie が送られてる……という謎に遭遇して調べ回った結果が↑のまとめの通り。


確認するには、ブラウザアクセス用のドメインとして www.example.com を hosts に追加(テストが済んだら削除すること)。

 127.0.0.1       apitest.example.com
+127.0.0.1       www.example.com

この状態で、ブラウザで http://www.example.com:5500/ を開くと、 http://www.example.com:5500/ のサイトから http://apitest.example.com:3000/ に CORS fetch リクエストをすることになるので、SameSite のセレクトボックスから strict や lax を選んで Cookie を SET すると、 GET, POST には Cookie が付かないはず……が、 msg = undefined にはならず、入力したテキストが表示される。
デベロッパーツールで確認してもちゃんとリクエストには Cookie が付いている。


「もしかしてサブドメインが無視されてる?」と考え、 hosts にちょっと違うドメインを追加。

 127.0.0.1       apitest.example.com
 127.0.0.1       www.example.com
+127.0.0.1       www.example2.com

その上で http://www.example2.com:5500/ から strict / lax で Cookie SET すると、その後の GET/POST には Cookie は付かず。
ブラウザのバグという線は限りなく低いので調べていった結果、上記のサイトに出会った次第。

SameSite指定されたCookieはCORS fetch時にどう働くか

まとめ

2020/05/17 修正

fetch などを使った CORS リクエストにおいて、API サーバから SameSite 設定付きで Set-Cookie が返された場合、以降の CORS リクエストに Cookie は付くのかどうか → SameSite=none の場合のみ Cookie が付く
ただし、サブドメイン部だけが異なるドメイン間での CORS の場合、lax/strict でも Cookie が付くもうちょっと調べたトピック 参照


以下の通り、lax や strict を指定された Cookieは 別ドメインに対する CORS fetch リクエストには付かない。

SameSite Cookie
none ○付く
lax ×付かない
strict ×付かない

CORS で Cookie を使う場合、 SameSite=none にしつつ API の応答に Access-Control-Allow-Credentials: true を付けたり、 fetch 時のオプションに credentials: 'include' 付けたりと、CORSの仕様に沿う必要もある。

また、ブラウザの設定でサードパーティーCookieがブロックされている場合、 SameSite の指定に関係なく Cookie は付かない(Set-Cookieが無視されるのでブラウザに記憶されない)。


この挙動を考えると、Cookie認証なAPI サーバを別ドメインにする構成では SameSite=none の指定が必須になるわけですね。

サードパーティーCookieが拒否される風潮を鑑みると、次のような対応にしたほうがいいのやも。

  • リバースプロキシなどを使い、表向きは同一ドメインでのAPIアクセスにして裏で振り分ける構成にする
  • Cookie の代わりに JWT などの Token で認証するようにする(XSS攻撃された場合のToken漏洩問題が出てきますが…)

以下打ち消し線部分は間違い

Cookie の挙動と Same Origin Policy を混同してました。
Cookie の場合、ドメイン名のみで識別されスキーマとポート番号は無視されるので、APIサーバを別ポートで動かしただけでは CORS が必要になるけれども Cookie の挙動は同一ドメイン時と同じ、という状態でのテストになっていた。という次第。
(そりゃ全部Cookie付くわなorz)
fetch などを使った CORS リクエストにおいて、API サーバから SameSite 設定付きで Set-Cookie が返された場合、以降の CORS リクエストに Cookie は付くのかどうか → 付く。

SameSite Cookie
none
lax
strict

ただし、API サーバの応答に Access-Control-Allow-Credentials: true を付けたり、 fetch 時のオプションに credentials: 'include' 付けたりと、CORS 下で Cookie を使う仕様に沿う必要はある。

Cookieを使ったAPIの認証をする場合は、 httponly + samesite としておけばよさそう。

検証に使った適当なコード

簡易API サーバ

指定された条件で Set-Cookie を発行する API と、リクエスト中の Cookie を取得して返す API の簡易APIサーバ。
ドメインに見せかけるため、 hosts ファイルに以下を追加して apitest.example.com でアクセスする(検証が終わったら削除する)。

127.0.0.1       apitest.example.com

/api/ に { 'msg': 'hoge', 'samesite': 0} みたいな JSON を POST すると Set-Cookie 入りのレスポンスを返す。
/api/cookie/ に POST したり GET したりすると、リクエスト中の Cookie の中身を JSON で返す。

// cors_api.js
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');

const app = express();
app.use(cookieParser());
app.use(express.json()); // application/json

// see https://expressjs.com/en/resources/middleware/cors.html#configuration-options
const cors_options = {
    origin: true,       // Access-Control-Allow-Origin: <Origin>
    credentials: true   // Access-Control-Allow-Credentials: true
}

// set cookie to response
app.options('/api/', cors(cors_options));
app.post('/api/', cors(cors_options), (req, res) => {
    console.log(req.cookies);
    const msg = req.body.msg;
    const samesite = req.body.samesite == 0 ? 'none' : req.body.samesite == 1 ? 'lax' : 'strict';

    res.cookie('msg', msg, {
        httpOnly: true,
        sameSite: samesite
    });
    res.json({ msg : `msg=${msg}; samesite=${samesite}`});
});

// get cookie from request
const getCookie = (req, res) => {
    console.log(req.cookies);

    const msg = req.cookies['msg'];

    res.json({ msg : `msg=${msg}`});
};
app.options('/api/cookie/', cors(cors_options));
app.get('/api/cookie/',  cors(cors_options), getCookie);
app.post('/api/cookie/', cors(cors_options), getCookie);

// start server
app.listen(3000, 'localhost', () => console.log('Listening on port 3000'));
ドライバHTML

簡易APIサーバの API を叩くドライバHTMLを、VS Code の LiveServerなどの適当なWebサーバ介して localhost ドメインとしてブラウザで開く。
テキストボックスに Cookie に設定する文字列を入れ、その後ろのセレクトボックスは Cookie に設定する SameSite の種類を選ぶ。SET ボタンを押すと /api/ が叩かれて Set-Cookie 付きのレスポンスが得られる。
そして、GETボタンやPOSTボタンを押すと、それぞれのメソッドで /api/cookie/ が叩かれて、それらのリクエストに付いてくる Cookie の中身が表示される。
msg=undefined となったり、テキストボックスに入れた文字列と違うものが表示された場合はサーバ側で Cookie が取得できていないことを表す。

<!DOCTYPE html>
<html>
  <body>
    <input type="text" id="msg"></input>
    <select id="samesite">
      <option value="0">none</option>
      <option value="1">lax</option>
      <option value="2">strict</option>
    </select>
    <button id="s">SET</button>
    <button id="s_get">GET</button><button id="s_post">POST</button><span id="ans"></span>

    <script>
        document.getElementById('s').addEventListener('click', async (e) => {
            e.preventDefault();
            const msg = document.getElementById('msg').value;
            const samesite = document.getElementById('samesite').value * 1;
            const req = { msg, samesite };
            res = await fetch("http://apitest.example.com:3000/api/",
                { method: 'POST',
                  body: JSON.stringify(req),
                  mode: "cors",
                  credentials: 'include',
                  headers: {
                      "Content-Type": "application/json; charset=utf-8",
                  }
                });
            json = await res.json();
            document.getElementById('ans').innerText = json.msg;
        });
        document.getElementById('s_get').addEventListener('click', async (e) => {
            e.preventDefault();
            res = await fetch("http://apitest.example.com:3000/api/cookie/",
                { method: 'GET',
                  mode: "cors",
                  credentials: 'include'});
            json = await res.json();
            document.getElementById('ans').innerText = json.msg;
        });
        document.getElementById('s_post').addEventListener('click', async (e) => {
            e.preventDefault();
            res = await fetch("http://apitest.example.com:3000/api/cookie/",
                { method: 'POST',
                  mode: "cors",
                  credentials: 'include'});
            json = await res.json();
            document.getElementById('ans').innerText = json.msg;
        });
    </script>
  </body>
</html>

速い Python コードを書くためのマイクロベンチマーク

Python コードを書くにあたって特性というかクセのようなものは知っておいた方がいいだろうと、Pythonコードの書き方の違いによる時間計測してみた。

計測環境は 2020/5/9 時点の Google の Colaboratory で以下のスペック。

  • Python 3.6.9
  • OS Ubuntu 18.04.3 LTS
  • CPU Xeon 2.30GHz 2core Model 63 Stepping 0
  • メモリ 12GB

まとめ

  • malloc(N) 的なサイズ指定で list を作成する場合、 * 演算子が速い。
  • list に入れる要素数が既知の場合、事前にサイズ指定の list を作ると速い。
  • ループ中で変化しない値で比較する場合、一時変数に代入しておく方が速い。
  • Google Colaboratory 便利かも

指定サイズでの list の確保

malloc(N) 的なサイズ指定で list を作成する場合、 * 演算子を使うほうが10倍速い。
* 演算子 < リスト内包表現 < たぶん for ループ

import timeit

t1 = timeit.timeit('[0] * 1000', number=10000)
t2 = timeit.timeit('[0 for _ in range(1000)]', number=10000)

print(f'[0] * 1000               => {t1:.3f} [sec]')
print(f'[0 for _ in range(1000)] => {t2:.3f} [sec]')
print(f'{t2/t1:.3f} times')
[0] * 1000               => 0.026 [sec]
[0 for _ in range(1000)] => 0.333 [sec]
12.774 times

入れる要素数が既知な場合の list 作成

list に入れる要素数が既知の場合、事前にサイズ指定の list を作っておいた方が1.4倍ほど速い。
append ではバッファの拡張があるので、リストに入れる要素数がより大きくなると影響も大きくなる予感。それは計ってないけれども。

import timeit

def pre_alloc():
  buf = [None] * 1000
  for i in range(1000):
    buf[i] = 'a'

def simple_append():
  buf = []
  for i in range(1000):
    buf.append('a')

t1 = timeit.timeit('pre_alloc()', number=10000, globals=globals())
t2 = timeit.timeit('simple_append()', number=10000, globals=globals())
print(f'pre_alloc     => {t1:.3f} [sec]')
print(f'simple_append => {t2:.3f} [sec]')
print(f'{t2/t1:.3f} times faster')
pre_alloc     => 0.454 [sec]
simple_append => 0.667 [sec]
1.468 times faster

ループ内不変値の比較

例えばNested loop joinのような処理で、外ループで決まって内ループでは変化しない値と比較する場合は、一時変数に代入しておく方が速い。(完全に固定値の場合は、即値で書く方が速い)
リストやタプル、dict、その他オブジェクトを介したアクセスはその分重くなる模様。
動的型付けなので比較そのもの以外の処理の影響も大きい&ループ内不変値の最適化はしてないっぽい。

import timeit

def cmp_by_immidiate():  # 完全固定値の場合
  for i in range(1000):
    if i == 999:
      break

def cmp_by_scala():
  buf = [None] * 1000
  buf[999] = 999
  local_static = buf[999]
  for i in range(1000):
    if i == local_static:
      break

def cmp_by_list():
  buf = [None] * 1000
  buf[999] = 999
  for i in range(1000):
    if i == buf[999]:
      break

t1 = timeit.timeit('cmp_by_immidiate()', number=10000, globals=globals())
t2 = timeit.timeit('cmp_by_scala()', number=10000, globals=globals())
t3 = timeit.timeit('cmp_by_list()', number=10000, globals=globals())
print(f'eval_by_immidiate => {t1:.3f} [sec]')
print(f'eval_by_scala     => {t2:.3f} [sec]')
print(f'eval_by_list      => {t3:.3f} [sec]')
print(f'eval_by_immidiate {t2/t1:.3f} times faster than cmp_by_scala')
print(f'eval_by_scala     {t3/t2:.3f} times faster than cmp_by_list')
eval_by_immidiate => 0.389 [sec]
eval_by_scala     => 0.415 [sec]
eval_by_list      => 0.530 [sec]
eval_by_immidiate 1.068 times faster than cmp_by_scala
eval_by_scala     1.276 times faster than cmp_by_list

計測環境

計測は 2020/5/9 時点の Google Colaboratory の python 環境を利用。

import sys
from pathlib import Path

osinfo = Path('/etc/issue')
cpuinfo = Path('/proc/cpuinfo')
meminfo = Path('/proc/meminfo')

for path in [osinfo, cpuinfo, meminfo]:
  with path.open(mode='r') as fp:
   for line in fp:
      print(line.strip())
print(sys.version_info)
Ubuntu 18.04.3 LTS \n \l

processor	: 0
vendor_id	: GenuineIntel
cpu family	: 6
model		: 63
model name	: Intel(R) Xeon(R) CPU @ 2.30GHz
stepping	: 0
microcode	: 0x1
cpu MHz		: 2300.000
cache size	: 46080 KB
physical id	: 0
siblings	: 2
core id		: 0
cpu cores	: 1
apicid		: 0
initial apicid	: 0
fpu		: yes
fpu_exception	: yes
cpuid level	: 13
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt arat md_clear arch_capabilities
bugs		: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit
bogomips	: 4600.00
clflush size	: 64
cache_alignment	: 64
address sizes	: 46 bits physical, 48 bits virtual
power management:

processor	: 1
vendor_id	: GenuineIntel
cpu family	: 6
model		: 63
model name	: Intel(R) Xeon(R) CPU @ 2.30GHz
stepping	: 0
microcode	: 0x1
cpu MHz		: 2300.000
cache size	: 46080 KB
physical id	: 0
siblings	: 2
core id		: 0
cpu cores	: 1
apicid		: 1
initial apicid	: 1
fpu		: yes
fpu_exception	: yes
cpuid level	: 13
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt arat md_clear arch_capabilities
bugs		: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit
bogomips	: 4600.00
clflush size	: 64
cache_alignment	: 64
address sizes	: 46 bits physical, 48 bits virtual
power management:

MemTotal:       13333540 kB
MemFree:        10687540 kB
MemAvailable:   12492736 kB
Buffers:           74588 kB
Cached:          1878092 kB
SwapCached:            0 kB
Active:           711776 kB
Inactive:        1681196 kB
Active(anon):     409164 kB
Inactive(anon):      320 kB
Active(file):     302612 kB
Inactive(file):  1680876 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:               480 kB
Writeback:             0 kB
AnonPages:        440280 kB
Mapped:           223268 kB
Shmem:               904 kB
Slab:             162112 kB
SReclaimable:     125380 kB
SUnreclaim:        36732 kB
KernelStack:        3408 kB
PageTables:         5316 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     6666768 kB
Committed_AS:    2539472 kB
VmallocTotal:   34359738367 kB
VmallocUsed:           0 kB
VmallocChunk:          0 kB
Percpu:              920 kB
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
DirectMap4k:       79036 kB
DirectMap2M:     6211584 kB
DirectMap1G:     9437184 kB
sys.version_info(major=3, minor=6, micro=9, releaselevel='final', serial=0)

Python str 1個入りタプルやリストを作る時の罠を踏んだ

まとめ

str 要素 1個入りの tuple, list を作るときは括弧で作ると安全。

t = ('abc',)   # ケツカンマがないと怒られる
l = ['abc']

tuple() や list() のコンストラクタ形式を使うと罠にはまるかも。

経緯

('abc',)
['abc']

↑の結果が欲しくて↓のようにすると、

t = tuple('abc')
l = list('abc')

↓のようになって Why となったわけです。

('a', 'b', 'c')
['a', 'b', 'c']

よくよく考えると、tuple, list のコンストラクタは class tuple([iterable]) , class list([iterable]) で、 Python では str 自体が Iterable なので、

t = tuple('abc')
l = list('abc')

とすると、'abc' が Itarable 扱いされて 1 文字ごとになったタプル/リストが作られたと。

('a', 'b', 'c')
['a', 'b', 'c']


str 単体でも str の Iterable でも受け付けるようなメソッド書くとき

def hoge (arg: Union[str. Iterable[str]]) -> None:

後のコードを共通にするため、単体の時は tuple や list でラップして、というのがよくやる手かと思いますが、↓のようにしてしまうと罠を踏み抜くわけですね。

def hoge (arg: Union[str. Iterable[str]]) -> None:
  if instanceof(arg, str):
    arg = tuple(arg)

  for x in arg:
     # 何かする

こういうラップするときは

  if not instanceof(arg, Iterable):
    arg1 = (arg, )

とするのが安全ぽい

mojimoji 0.0.10 での ImportError: DLL load failed ( Windows 10 64bit + Anaconda 環境 )

Windows 10 64bit の Anaconda 環境で mojimoji を使おうとしたときに遭遇した問題と解決法。

概要

経緯

カタカナの全角半角変換したかったので、速いらしい mojimoji を使ってみようとしたら、 conda リポジトリには mojimoji はないということなので、 pip でインストール。 もちろん conda と pip を混ぜるな危険(condaとpip:混ぜるな危険 - onoz000’s blog )は承知の上で。

> pip install mojimoji
Collecting mojimoji
  Using cached mojimoji-0.0.10-cp37-cp37m-win_amd64.whl (35 kB)
Installing collected packages: mojimoji
Successfully installed mojimoji-0.0.10

そしておもむろに、

import mojimoji 

すると、以下のようなエラーが。

Traceback (most recent call last):
  File "test.py", line 2, in <module>
    import mojimoji
ImportError: DLL load failed: 指定されたモジュールが見つかりません。

「以前は使ったときは出なかったのになぁ」と思いつつ、ModuleNotFoundError ではないので、モジュールファイルそのものが実行環境から見えていると判断。
ImportError: DLL load failed は pyd なモジュールの読み込み時に、 pyd が参照している必要なファイルがない場合や、依存している DLL が見えてないといった、何かしらのロード時エラーが発生したことを意味してる。

一応インストール済みパッケージを確認。 → 大丈夫。

> conda list
# packages in environment at C:\Users\yishi\Anaconda3\envs\make-train-text:
#
# Name                    Version                   Build  Channel
ca-certificates           2020.1.1                      0
certifi                   2020.4.5.1               py37_0
mojimoji                  0.0.10                   pypi_0    pypi


パッケージのインストール先である %USERPROFILE%\Anaconda3\envs\virtual_env\Lib\site-packages を確認すると、
mojimoji.cp37-win_amd64.pyd というファイルが入っている。
そのファイル名を変えると、インポート時のエラーが ModuleNotFoundError になるので、ptyhon からは見えてはいる。

ファイル破損の疑いもあるので、 https://pypi.org/project/mojimoji/0.0.10/#files から mojimoji-0.0.10-cp37-cp37m-win_amd64.whl をダウンロードして、拡張子を zip にして展開して出てきた同ファイルと比較したが正常。

となると mojimoji.cp37-win_amd64.pyd が参照している何かしらが問題を起こしている可能性が大きく、mojimoji には設定ファイルは無いので、参照している DLL が読めていないのか、参照している DLL 内でエラーになっているか。

と言うことで、 pyd が参照している DLL を Dependency Walkerで知らべる。
今は Dependencies というOSSになっている とのことなので、そちらを公式GitHubのリリースからダウンロードして展開。
perview.exe で拡張子を dll に変更した mojimoji.cp37-win_amd64.dll を読ませて、参照している DLL を確認。

f:id:naga_sawa:20200420091056p:plain


Imports タブに一覧されている DLL があるか %USERPROFILE%\Anaconda3\envs\virtual_env 内の DLL をざっと見ていく。
api-ms-win-core*.dll はある。 python37.dll もある。 VCRUNTIME140.dll もある。

そして最後の VCRUNTIME140_1.DLL がない。ドライブ全体から VCRUNTIME140_1.DLL を検索しても見つからない。

ファイル名前から察するに、 VC++ のランタイムだと思われるので、ファイル名でググる2019年の後半になってから追加された DLL らしい



となると最新の VC++ ランタイムを入れてやれば解決しそうなので、「Visual C++ 再頒布可能パッケージ」でググって以下のMS公式サイトより
https://support.microsoft.com/ja-jp/help/2977003/the-latest-supported-visual-c-downloads
Visual Studio 2015、2017 および 2019」の x64: vc_redist.x64.exe をダウンロードしてインストールする。

そして再び import mojimoji するとエラーは出なくなりました。めでたしめでたし。


VCRUNTIME140_1.DLL は2019年の後半に入ってからx64環境向けに追加されたファイルのようなので、VS2019 環境でコンパイルされているx64バイナリ入りのモジュールで類似の問題にあたるやもしれない。



以前動いていたのは、mojimoji 0.0.9 で、パッケージが tar.gz 配布で手元コンパイルされてたので問題にならなかったもよう。
0.0.10 でコンパイル済みバイナリが配布されるようになったので、出るようになったと。

続・VyOS と PPPoE と MSS clamp の設定と

続・VyOS と PPPoE と MSS clamp の設定と - ..たれろぐろぐ.. からの移動トピで、 2019/03/21 当時の内容です。

要点

  • 以前書いてたエントリのアップデート(別解とも)

d.hatena.ne.jp

  • 以前は MSS 制限の必要なインタフェースと直接関係のないインタフェースにも MSS 制限ポリシーの設定が必要で、いまいちスマートでなかった
  • vyatta-postconfig-bootup.script を使い、インタフェースから出て行くパケットに対して MSS 制限ルールを追加して、無関係なインタフェースへのポリシー設定を排除した
  • VyOS のカバー外で設定しているので、別機能や将来の仕様変更時に衝突する不安あり

やりかた

MSS 制限ポリシーが次のように PPPoE インタフェースと LAN インタフェースの両方に設定されていることを前提環境とします。

# set interfaces ethernet eth0 pppoe 0 policy route MSSCLAMP
# set interfaces ethernet eth1 policy route MSSCLAMP

MSSCLAMP は以下のようなものを想定(Flet's用)。

policy {
    route MSSCLAMP {
        rule 10 {
            protocol tcp
            set {
                tcp-mss 1414
            }
            tcp {
                flags SYN,!RST
            }
        }
    }
}


まず、vi などで /config/scripts に次のスクリプトを作成します。
PPPoE インタフェースから出て行くパケットに対して MSS 制限するルールを iptables に追加するスクリプトです(以下では add-ifout-policy.sh としておきます)。

#!/bin/sh
# add mangle POSTROUTING rules
iptables -t mangle -N PPPOE-OUT
iptables -t mangle -A VYATTA_FW_OUT_HOOK -o pppoe0 -j MSSCLAMP-OUT
iptables -t mangle -A MSSCLAMP-OUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1414
iptables -t mangle -A MSSCLAMP-OUT -m comment --comment "MSSCLAMP-OUT-10000 default-action accept" -j RETURN

実行属性付与することを忘れずに。

$ chmod +x /config/scripts/add-ifout-policy.sh


次に、 /config/scripts にある vyatta-postconfig-bootup.script というファイルの末尾に、先ほどのスクリプトを追記します。

$ vi  /config/scripts/vyatta-postconfig-bootup.script
#!/bin/sh
# This script is called from /etc/rc.local on boot after the Vyatta
# configuration is fully applied. Any modifications done to work around
# unfixed bugs and implement enhancements which are not complete in the Vyatta
# system can be placed here.

# add mangle POSTROUTING rules
/config/scripts/add-ifout-policy.sh

vyatta-postconfig-bootup.script は VyOS が設定を読み込み終えた後に呼ばれるスクリプトなので、種々の設定が行われた最後に add-ifout-policy.sh で設定される MSS 制限ルールが追加されることになります。

最後に、 LAN インタフェースに指定されている MSS 制限ポリシーを削除し、 commit します。

# delete interfaces ethernet eth1 policy route MSSCLAMP
# commit
# save

これで VyOS を再起動すると LAN インタフェース側で制限しなくても、PPPoE インタフェースに出入りするパケットに対して MSS 制限が効くようになります。

なんで?のお話

そもそもの問題は、PPPoE などの MTU が1500バイトに満たない経路を通る場合、MSS 制限をしてやらないと Webサイトによって見られたり見られなかったり妙に応答が遅くなったり、というものでした。
ご家庭用ルータでは、ここらへん自動的にやってくれてるのか大したトラブルにならないのですが、 VyOS はそこまで優しくありません。

VyOS の作法に則ると、次のような MSS 制限ポリシーを PPPoEインタフェースと LAN インタフェースの両方に指定することで、双方向で MSS 制限を働かせてやることになります。

なぜに PPPoE インタフェースと LAN インタフェースの双方に指定しないとダメなのか、というと VyOS でのポリシーベースルーティングは iptables の mangle テーブルの PREROUTING を使って実現しているので、インタフェースから出て行く(egress)パケットには影響を与えてません。

ポリシー設定後の iptables の具合を見てやるとよくわかるのですが、VyOS のポリシーベースルーティングの設定は、 VYATTA_FW_IN_HOOK チェインにて各インタフェースに対応するポリシー相当のチェインに飛ぶように設定されます。

vyos@vyos:~$ sudo iptables -t mangle -L -n -v
Chain PREROUTING (policy ACCEPT 6251K packets, 6256M bytes)
 pkts bytes target     prot opt in     out     source               destination 
6477K 6284M VYATTA_FW_IN_HOOK  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain INPUT (policy ACCEPT 846K packets, 53M bytes)
 pkts bytes target     prot opt in     out     source               destination 

Chain FORWARD (policy ACCEPT 5631K packets, 6232M bytes)
 pkts bytes target     prot opt in     out     source               destination 

Chain OUTPUT (policy ACCEPT 780K packets, 51M bytes)
 pkts bytes target     prot opt in     out     source               destination 

Chain POSTROUTING (policy ACCEPT 6410K packets, 6283M bytes)
 pkts bytes target     prot opt in     out     source               destination 
6410K 6283M VYATTA_FW_OUT_HOOK  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain VYATTA_FW_IN_HOOK (1 references)
 pkts bytes target     prot opt in     out     source               destination 
 2876  219K MSSCLAMP   all  --  eth1 *         0.0.0.0/0            0.0.0.0/0   
 187K   28M MSSCLAMP   all  --  pppoe0 *       0.0.0.0/0            0.0.0.0/0   

Chain MSSCLAMP (2 references)
 pkts bytes target     prot opt in     out     source               destination 
57092 2831K TCPMSS     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* MSSCLAMP-10 */ tcp flags:0x06/0x02 TCPMSS set 1414
 187K   28M RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* MSSCLAMP-10000 default-action accept */

Chain VYATTA_FW_OUT_HOOK (1 references)
 pkts bytes target     prot opt in     out     source               destination 

VyOS に入ってきたパケットは mangle PREROUTING -> VYATTA_FW_IN_HOOK -> 各インタフェースのポリシールール と渡されていくことになります。
この動作は PREROUTING の名の通り、パケットが入ってきた場合のみに働くので、VyOS から出て行くパケットにはなにも対処できません。
ここらのポリシーベースルーティングの仕組みについては、さくらインターネットの松本氏がまとめられてるのでそちらを参照してください。
research.sakura.ad.jp
iptables でのパケットの流れがわからんという人は https://ja.wikipedia.org/wiki/Iptables#/media/File:Netfilter-packet-flow.svg の絵を参照で)


外部との接続が PPPoE 1本だけ、という場合はこの設定で不自由しないわけですが、昨今速くなると話題の MAP-E やら DSLite やらの v4 over v6 な接続や、LAN直結専用線、のようなものと併存してやろうとすると LAN 側インタフェースに一律に MSS 制限ポリシーを仕込むと MSS 制限しなくていい通信まで MSS 制限してしまい、微妙に効率が悪くなったり、その他のポリシーベースルーティングの設定がし辛くなるなどの問題が出てきます。

そこで注目するのは、同 mangle テーブルに登録されているものの、現時点(VyOS1.1.8)で使われていない VYATTA_FW_OUT_HOOK に、時前で VYATTA_FW_IN_HOOK と同じようなルールを仕込んでしまえという、このエントリで紹介する手になります。
(前エントリの最後に書いていた『裏技臭がひどくて…』というのをやってしまったと)

先に書いたスクリプトを実行すると、 iptables の状態は次のような形になります。
これで出て行くパケットは VYATTA_FW_OUT_HOOK で対応するインタフェースに応じて MSSCLAMP-OUT に飛び、結果として MSS が制限されることになります。

vyos@vyos:~$ sudo iptables -t mangle -L -n -v
Chain PREROUTING (policy ACCEPT 6251K packets, 6256M bytes)
 pkts bytes target     prot opt in     out     source               destination 
6477K 6284M VYATTA_FW_IN_HOOK  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain INPUT (policy ACCEPT 846K packets, 53M bytes)
 pkts bytes target     prot opt in     out     source               destination 

Chain FORWARD (policy ACCEPT 5631K packets, 6232M bytes)
 pkts bytes target     prot opt in     out     source               destination 

Chain OUTPUT (policy ACCEPT 780K packets, 51M bytes)
 pkts bytes target     prot opt in     out     source               destination 

Chain POSTROUTING (policy ACCEPT 6410K packets, 6283M bytes)
 pkts bytes target     prot opt in     out     source               destination 
6410K 6283M VYATTA_FW_OUT_HOOK  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain VYATTA_FW_IN_HOOK (1 references)
 pkts bytes target     prot opt in     out     source               destination 
 187K   28M MSSCLAMP   all  --  pppoe0 *       0.0.0.0/0            0.0.0.0/0   

Chain MSSCLAMP (1 references)
 pkts bytes target     prot opt in     out     source               destination 
57092 2831K TCPMSS     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* MSSCLAMP-10 */ tcp flags:0x06/0x02 TCPMSS set 1414
 187K   28M RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* MSSCLAMP-10000 default-action accept */

Chain MSSCLAMP-OUT (1 references)
 pkts bytes target     prot opt in     out     source               destination 
 2152  116K TCPMSS     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp flags:0x06/0x02 TCPMSS set 1414
 101K   49M RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* MSSCLAMP-OUT-10000 default-action accept */

Chain VYATTA_FW_OUT_HOOK (1 references)
 pkts bytes target     prot opt in     out     source               destination 
 101K   49M MSSCLAMP-OUT  all  --  *      pppoe0  0.0.0.0/0            0.0.0.0/0   

これにてあるインタフェースに出入りするパケット、双方向で MSS 制限できるようになるので、その他インタフェースからは MSS 制限ポリシーを削除しても大丈夫、となります。

LAN 側から curl -X GET http://b.hatena.ne.jp/ したときのパケットキャプチャが次の通り。

monitor interfaces ethernet eth1 traffic filter "net 52.85.6.0 mask 255.255.255.0"
Capturing traffic on eth1.5 ...
[A]  0.007107 192.168.0.17 -> 52.85.6.14   TCP 54629 > 80 [SYN] Seq=0 Win=8192 Len=0 MSS=1460 WS=2
[B]  0.013234   52.85.6.14 -> 192.168.0.17 TCP 80 > 54629 [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0 MSS=1414 WS=8
[C]  0.014533 192.168.0.17 -> 52.85.6.14   TCP 54629 > 80 [ACK] Seq=1 Ack=1 Win=66456 Len=0
  • 同 PPPoE 側
vyos@vyos:~$ monitor interfaces ethernet pppoe0 traffic filter "net 52.85.6.0 mask 255.255.255.0"
Capturing traffic on pppoe0 ...
[A]  0.000000 220.147.153.14 -> 52.85.6.234  TCP 54637 > 80 [SYN] Seq=0 Win=8192 Len=0 MSS=1460 WS=2
[B]  0.005243  52.85.6.234 -> 220.147.153.14 TCP 80 > 54637 [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0 MSS=1460 WS=8
[C]  0.006409 220.147.153.14 -> 52.85.6.234  TCP 54637 > 80 [ACK] Seq=1 Ack=1 Win=66456 Len=0

LAN 側から PPPoE 側に出ていくパケット [A] の MSS option は変更されていないのに対して、PPPoE 側から LAN 側に戻ってきたパケット [B] は、 PPPoE インタフェースに指定した policy route 設定により MSS option が 1460 -> 1414 に変更されています。

monitor interfaces ethernet eth1 traffic filter "net 52.85.6.0 mask 255.255.255.0"
vyos@vyos:~$ monitor interfaces ethernet eth1 traffic filter "net 52.85.6.0 mask 255.255.255.0"
Capturing traffic on eth1.5 ...
[A]  0.000000 192.168.0.17 -> 52.85.6.58   TCP 54666 > 80 [SYN] Seq=0 Win=8192 Len=0 MSS=1460 WS=2
[B]  0.005228   52.85.6.58 -> 192.168.0.17 TCP 80 > 54666 [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0 MSS=1414 WS=8
[C]  0.005950 192.168.0.17 -> 52.85.6.58   TCP 54666 > 80 [ACK] Seq=1 Ack=1 Win=66456 Len=0
  • 同 PPPoE 側
vyos@vyos:~$ monitor interfaces ethernet pppoe0 traffic filter "net 52.85.6.0 mask 255.255.255.0"
Capturing traffic on pppoe0 ...
[A]  0.000000 220.147.153.14 -> 52.85.6.58   TCP 54667 > 80 [SYN] Seq=0 Win=8192 Len=0 MSS=1414 WS=2
[B]  0.005181   52.85.6.58 -> 220.147.153.14 TCP 80 > 54667 [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0 MSS=1460 WS=8
[C]  0.005900 220.147.153.14 -> 52.85.6.58   TCP 54667 > 80 [ACK] Seq=1 Ack=1 Win=66456 Len=0

対してスクリプトを実行して VYATTA_FW_OUT_HOOK にルールを追加した後では、LAN 側から PPPoE 側に出ていくパケット [A] も、PPPoE 側から LAN 側に戻ってきたパケット [B] も、両方とも MSS option が 1460 -> 1414 に変更されています。
前者(PPPoE 側に出ていくパケットへの変更)が、スクリプトで追加したルールの効果になります(後者はPPPoE インタフェースに指定した policy route 設定の効果)。


難点というか要注意なのは、 VyOS のあずかり知らぬ部分で設定を入れているため、なにかしらの別の設定と衝突したり、将来に機能が追加された際に衝突する可能性がある。というところです。

ここらはインタフェースの制限事項なので interface の項目内で設定できるようになるのが一番なのでしょうが 1.2.0 でも対応してないようで……


それではよい VyOS ライフを。

JWT 認証のメリットとセキュリティトレードオフの私感

2020/5/9追記: 考えた結果、Authorization Bearer ヘッダを使った正規のJWTの場合、同一ドメイン下で読み込む全 JavaScript が信用できる場合でないとブラウザ上で安全にトークンを保持できないのでブラウザからのAPIアクセス時の認証用には使うべきではないというところに着陸しました。ブラウザからのアクセスでは http only cookieトークンを入れ、 CSRF 対策も忘れずにというこれまで通りの定石が手堅いように思います。 JWTを使うのはトークンの安全な保管ができる非ブラウザなネイティブクライアントからのAPIアクセス時に限った方がよさそうです。 APIサーバ側ではアクセス元に合わせて認証方法を使い分ける両対応が要求されるので手間は増えますが手抜きできる場所でもないので仕方なしと。


2018/9/25追記: https://gist.github.com/issm/63889b931b8c658f23634070b64f8b23 も参考になるかも。 あと、以下の議論は『セッション』の意味するところに認識違いがあるのかもしれない(認証継続の意味でのセッションと、ステート保持機構としてのセッションと)。


どうして JWT をセッションに使っちゃうわけ? - co3k.org (←のはてブ
とか
JWT認証、便利やん? - ブログ (←のはてブ
で話題になってるので、Webシステム素人の理解と私感をメモしてみる。
OpenID connect とかで使われてるって話だけど、そっちはノータッチで単純にAPIアクセス時の認証の仕組みとして使うことを前提としています。


JWT ベースの認証って、負荷分散などのために複数台のフロントサーバを使う場合や、複数のマイクロサービスを使ったサービスを実現するにあたって、認証(セッション)状態を都度セッションストアに確認したくない・できない場合に生きてくる仕組みなように思う。

一般に短寿命のアクセストークンと比較的長寿命なリフレッシュトークンのセット利用を前提としていて、次のような利用形態が前提(なはず)。

  • 普段の APIリクエスト時はアクセストークンのみで認証する。
  • アクセストークンの寿命が来た場合には、リフレッシュトークン使ってアクセストークンを更新する。
  • そのリフレッシュ時には都度ユーザDBなりにアクセスしてリフレッシュ可否を判定する。
  • APIリクエスト頻度 <<< アクセストークンのリフレッシュ頻度である。


従来の SessionID を使ったトークンベース・cookieベースの認証では、SessionID からユーザID などの認証情報を引かないといけないので、その対応関係(セッション情報)を一時保存するセッションストアが必要になる。

API リクエストが高頻度に発生する場合では、複数台のフロントサーバで分散させる構成にするのが一般的だけれども、セッション情報は全体で共有しないと破綻するので、 memcached や Redis のような共有セッションストアサーバを別途用意して、フロントサーバ全体で共有しなければならなかった。


それに対して JWT だと普段の APIリクエストに対しては、各フロントサーバで JWT から認証情報を取り出せるので、共有セッションストアサーバが不要になる。(そこで改竄検知可能なのが JWT のキモだと思う)

リフレッシュの頻度は APIリクエストに対して十分に低頻度で、バックエンドの普通の DB サーバで十分に対処できる。
このため、リフレッシュが要求された時点で DB を参照してアクセストークンをリフレッシュする/拒否することで認証状態をコントロールする。


トークンの漏洩に対しては、漏洩発覚後、アクセストークンの寿命が来るまでは不正アクセスを許容できるサービスが適用対象で、即時止めたいというサービスには向かない。
そういうサービスの場合は、セッションストアサーバを使って確実にセッション無効化できるように構築しましょう。
無理に JWT で実現しようとしても、blacklist をフロントサーバ間で共有させないといけないので結局共有セッションストアのような仕組みが必要になってしまうので。


共有セッションストアサーバが不要になるというのが、JWT 認証での一番のメリットで、あとはアクセストークンの寿命の設定次第で、許容するリスクとリフレッシュ負荷とのトレードオフを調整する。


この方面は素人なんで要点外したら申し訳ないけれど、調べた範囲だとこういう感じなのかなぁと。

自前サービスのAPI認証用途で使う場合であれば、ユーザの無効化操作と同時にフロントサーバ群にそのユーザ情報を管理APIなどで上手くばらまく仕組みができれば即時無効化もできるはず。
無効化情報はせいぜいアクセストークンの寿命時間分保持すれば十分なので、再起動を考えなければオンメモリでもいけそうな。


各種フレームワークで用意されてる・対応してるセキュアな実装が使えるなら、それに乗っかるのが一番無難な選択よね。

あ、マサカリはウレタンでお願いします。