English: API.md
UI(TCP-UDP-rproxy-ui)と rproxy-api の間の取り決め。どちらかを変えるときは、このファイルも合わせて更新する。
- HTTP/1.1、JSON(UTF-8)。
GET /metricsだけは Prometheus のテキスト形式。 - 待ち受けアドレスは
--api-addrで指定する(複数回指定できる)。ポートは--api-portで指定する(既定 8080。0で TCP を使わない)。 - Unix ソケットでも受けられる(
--api-socket、--api-socket-mode、--api-socket-group)。中身は TCP と同じ HTTP/1.1 で、トークンも同じく要る。 - 認証:
--token-fileを指定した場合、/healthz以外のエンドポイントはAuthorization: Bearer <token>が必須になる。- トークンファイルは 2 つの書き方がある。
-
1 行に 1 つトークンを書く(すべての権限。ログでの名前は
token-1、token-2…)。空行と#で始まる行は無視する。 -
YAML で
tokens:に名前・SHA-256・スコープを書く(トークンそのものはファイルに置かない)。tokens: - name: ui sha256: 9f86d081... # printf %s "$TOKEN" | sha256sum scopes: [rules:read, rules:write, metrics:read] - name: ci-deploy sha256: 2c26b46b... scopes: [rules:write] allow_listen_ports: 20000-29999 # 作成・変更・削除できる待ち受けポート(範囲ルールは全体が収まること) allow_rulesets: [ci/] # v0.4:PUT / DELETE できるルールの組の名前の先頭(省略ですべて) allow_certs: [ci-] # v0.4.2(#240):PUT / DELETE / 読む・ルールで使える保存した証明書の名前の先頭(省略ですべて) expires: 2027-03-31 # この日(UTC)まで有効 persist: true # v0.4(#144):作ったルールを rproxy_rules に保存する(既定 false)
-
スコープ:
rules:read(GET /rules・/interfaces)、rules:write(POST/PATCH/DELETE /rules)、metrics:read(GET /metrics)、acme:write(ACME の証明書を使うルールの作成・変更とPOST /acme/...。docs/ACME.md)、certs:read(GET /certs。v0.4.2)、certs:write(PUT/DELETE /certs/{name}。v0.4.2)、admin(すべて)。GET /capabilitiesはどのトークンでも読める。足りないときは403 forbidden。 -
ルールの作成・変更・削除は
event: "audit"のログに残る(token、client、action、rule、outcome(ok/error/forbidden)、失敗時のcode)。clientは送信元の IP(Unix ソケットからはunix)。 -
断ったリクエストも
event: "audit"に残る(client・method・pathつき。トークンそのものは出さない):トークンがない・知らない・期限切れ(401)はoutcome: "unauthorized"とreason(missing/invalid/expired、トークンに結びついたクライアント証明書がないときはclient_cert)、一時停止中(429)はoutcome: "locked_out"、スコープが足りない(403)はoutcome: "forbidden"とtoken・scope。ログがあふれないように、断ったリクエストの行は送信元ごとに続けて 20 行まで、その後は 1 秒に 1 行にする。出した行のsuppressedは、その送信元でその前に省いた行の数(省いた行の合計は/metricsのrproxy_log_suppressed_total)。
-
- 複数のトークンを同時に有効にできる。入れ替えのときは新旧を両方書いておき、あとで古い方を消す。
- SIGHUP を受けるとトークンファイルを読み直す。
- SIGHUP がなくても、
--tokens-check-secs/RPROXY_TOKENS_CHECK_SECS(既定 10 秒、0なら SIGHUP のときだけ)ごとにファイルの大きさ・更新時刻・inode・権限を確かめ、変わっていれば読み直す(v0.4.2、#253、features.tokens_reload)。パスはシンボリックリンクをたどって確かめるので、Kubernetes の Secret のボリューム(..dataのリンクの差し替え)の更新も見つける。消したトークンは次の確認から通らない。処理中のリクエストと一時停止(lockout)の状態はそのまま。 - 読めない・誤りのある版では今のトークンを使い続け、その版について
reload.tokensの警告を 1 回だけ出す(ファイルがまた変われば読み直す)。読み直したときのreload.tokensはトークンの数(tokens)だけで、トークンやハッシュは出さない。起動時に読めなかった(権限)ファイルも、読めるようになれば次の確認で使い始める。
- トークンファイルは 2 つの書き方がある。
--api-addrに loopback 以外のアドレスを含める場合は、--token-file、--tls-cert、--tls-keyの指定が必須。どれかが欠けていると起動を拒否する。
TCP の制御 API の守りを足す:クライアント証明書(mTLS)、トークンの期限の知らせ、認証の失敗が続く送信元の一時停止。Unix ソケットはどれの対象でもない(TLS がなく、ソケットのファイルの権限で守る。止められることもない)。
| 引数 / 環境変数 | 既定 | 意味 |
|---|---|---|
--tls-client-ca / RPROXY_TLS_CLIENT_CA |
なし | クライアント証明書を確かめる CA(PEM。複数可)。--tls-cert が要る。SIGHUP と証明書のファイルの確認(RPROXY_CERT_CHECK_SECS)で読み直す |
--tls-client-auth / RPROXY_TLS_CLIENT_AUTH |
none |
none・optional(出されれば確かめる。ない接続はトークンで)・required(証明書がない・確かめられない接続は TLS のハンドシェイクで断る)。optional / required は --tls-client-ca が要る |
--token-warn-days / RPROXY_TOKEN_WARN_DAYS |
14 |
トークンの expires がこの日数より近いと token.expiring を出す(1〜3650) |
--api-lockout-failures / RPROXY_API_LOCKOUT_FAILURES |
20 |
window の間に認証に失敗(401)した回数がこれに達した送信元を止める。0 で止めない |
--api-lockout-window / RPROXY_API_LOCKOUT_WINDOW |
1m |
数える時間(1s〜24h) |
--api-lockout-duration / RPROXY_API_LOCKOUT_DURATION |
5m |
止める時間(1s〜24h) |
--api-lockout-exempt / RPROXY_API_LOCKOUT_EXEMPT |
なし | 数えず止めない送信元(カンマ区切りの CIDR。NAT の後ろのコントローラなど)。検証済みのクライアント証明書(mTLS)で来た接続も止めない(同じアドレスから推測を続ける攻撃者に、正しい証明書を持つ管理者やコントローラを締め出させないため。セキュリティレビュー M4)。トークンだけの接続は、止まっているあいだトークンを確かめる前に 429 |
クライアント証明書(mTLS):トークンファイル(YAML)のエントリに client_cert を書く。値は証明書の名前で、DNS か URI の subjectAltName(なければ subject の CN)と完全一致で比べる。
tokens:
- name: ui
client_cert: ui.rproxy.internal # 証明書だけで通す(Authorization は要らない)
scopes: [rules:read, rules:write, metrics:read]
- name: gateway-controller
sha256: 9f86d0...
client_cert: spiffe://cluster.local/ns/rproxy/sa/controller # トークンと証明書の両方が要る
scopes: [rules:read, rules:write]sha256とclient_certのどちらかは要る。両方あれば両方が要る:トークンが合っても、接続の証明書にその名前がなければ401(監査ログのreason: "client_cert")。client_certだけのエントリは証明書だけで通す(sha256のない 2 つのエントリに同じclient_certは書けない)。Authorization: Bearerを付けたリクエストは、そのトークンで確かめる(知らないトークンなら、証明書だけで通るエントリがあっても401)。付けなければ、証明書の名前でclient_certだけのエントリを探す。- 証明書は
--tls-client-caの CA が発行したもの(期限内・クライアント認証の用途)だけが通る。ほかの CA のものはハンドシェイクで断る。 client_certのあるトークンファイルで--tls-client-authがnoneなら、起動を止める設定のエラー(SIGHUP の読み直しなら今のトークンを使い続ける)。使えないエントリを黙って無視しない。平文の HTTP や Unix ソケットでは証明書がないので、client_certだけのエントリは使えない。- 監査ログ(
event = "audit")にauth(token・cert・token+cert)が付く。
トークンの期限:
- 起動・SIGHUP・1 日 1 回、
expiresが--token-warn-daysより近いトークンをtoken.expiring(token・expires・days_left)、切れたトークンをtoken.expired(token・expires)で知らせる(warn)。トークンごとに状態が変わったときに 1 回だけ。expiresの日(UTC)の終わりまで有効。 /metricsにrproxy_token_expiry_timestamp_seconds{token}(有効でなくなる時刻、Unix 秒)。
失敗が続く送信元の一時停止(既定で有効:1 分に 20 回で 5 分):
- TCP の制御 API で、
401になった送信元の IP(IPv6 は /64 でまとめる)ごとに数え、windowの間にfailuresに達したらdurationの間、その送信元のリクエストをトークンを見ずに429 locked_outで断る(Retry-Afterに残りの秒)。/healthz・/readyzは止めない。403(スコープ不足)は数えない。 - 止めたときに
api.lockout(client・failures・until(Unix 秒)・duration_secs、warn)、解いたときにapi.unlock(client)。止めている間の拒否はevent = "audit"・outcome: "locked_out"(ほかの拒否と同じく送信元ごとに間引く)。 /metricsにrproxy_api_lockouts_total(止めた回数)・rproxy_api_locked_sources(今止めている送信元の数)。- 覚える送信元は 4096 まで(あふれたら、止めていない送信元のうち古いものから忘れる)。プロセスの中だけで覚え、再起動で消える。
- 同じ IP の後ろにいる正しいクライアントも一緒に止まる。UI を同じホストに置くなら Unix ソケットでつなぐと影響を受けない。
トークン・証明書の入れ替え(止めずに入れ替える):
- 新しいトークン(
sha256)をトークンファイルに足して SIGHUP(systemctl reload rproxy-api)。v0.4.2 からは SIGHUP を送らなくてもRPROXY_TOKENS_CHECK_SECSのうちに読み直す。この間は新旧どちらでも通る。 - クライアント(UI・CI など)を新しいトークンに切り替える。
- 古いトークンをトークンファイルから消して SIGHUP。
expiresを付けておくと、切れる前にtoken.expiringで知らせる(--token-warn-days)。- クライアント証明書は、同じ名前で新しい証明書を発行してクライアントに配る(トークンファイルは変えなくてよい)。名前を変えるときは、新しい名前のエントリを足す → クライアントを切り替える → 古いエントリを消す(どれも SIGHUP)。
- CA を入れ替えるときは、
--tls-client-caのファイルに新旧の CA を並べて SIGHUP → クライアントの証明書を新しい CA のものに替える → 古い CA を消して SIGHUP。
UI(TCP-UDP-rproxy-ui)の側(UI が実装するときの取り決め):
UI の環境変数 / nodes.yaml のノードの項目 |
意味 |
|---|---|
RPROXY_API_CA_FILE / ca_file |
rproxy の制御 API のサーバ証明書を確かめる CA(PEM)。https:// で、公的な CA でないときに使う |
RPROXY_API_CERT_FILE / cert_file |
UI が出すクライアント証明書(PEM。中間 CA があれば続けて書く) |
RPROXY_API_KEY_FILE / key_file |
その秘密鍵(PEM)。UI を動かすユーザーだけが読めるようにする |
RPROXY_API_URL(ノードのurl)はhttps://にする。unix:では証明書を使わない(トークンで通す)。cert_file/key_fileを指定し、rproxy のclient_certだけのエントリ(例:client_cert: ui.rproxy.internal)に当たるなら、RPROXY_API_TOKEN(token_file)は省ける。sha256とclient_certの両方があるエントリなら両方を指定する。- 片方だけ(
cert_fileだけ・key_fileだけ)は UI の設定のエラー。ファイルが変わったら(証明書の更新)新しい接続から読み直す(または UI を再起動する)。 - rproxy が
429 locked_outを返したら、Retry-Afterの間は送り直さず、画面に「認証の失敗が続いたため一時的に止められている」と出す。401を自動で繰り返さない(一時停止の回数に入る)。 GET /capabilitiesのfeatures.client_cert_authで、rproxy がクライアント証明書に対応しているかを確かめられる。
ルールは (protocol, listen_addr, listen_port) の組で一意に識別する。
{
"protocol": "tcp",
"listen_addr": "0.0.0.0",
"listen_port": 8888,
"remote_addr": "example.com",
"remote_port": 80,
"source_ip": "proxy",
"udp_idle_secs": 30
}| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
protocol |
"tcp" | "udp" |
○ | 大文字・小文字は区別しない。応答では常に小文字で返す |
listen_addr |
string | ○ | IP アドレス(ホスト名は不可) |
extra_listen_addrs |
string[] | 同じポート(範囲)で追加で待ち受ける IP アドレス(最大 16 件。例 listen_addr が代表の IPv4 で、ここに GUA の IPv6)。ルールのキーは listen_addr のまま。統計・ログは 1 つのルールとしてまとめ、conn.open の listen に受けたアドレスが出る。追加のアドレスがあるルールの IPv6 の待ち受けは IPV6_V6ONLY で開くので、0.0.0.0 と :: を並べられる(:: だけのルールは OS の既定のまま:Linux の既定では IPv4 も受ける)。ほかのルール・制御 API との重なりは、追加のアドレスも含めて確かめる(409 already_exists / reserved)。transparent では、追加のアドレスのファミリーの宛先(IP で書いたもの)が 1 つもなければ invalid、IPv6 のアドレスは transparent_ipv6 が要る。http3 の QUIC も全部のアドレスで受ける。一覧では空なら省く。UDP の返信の送信元は、0.0.0.0 / :: で待ち受けていてもクライアントが送った宛先のアドレスになるので、アドレスを複数持つホストで返信元を固定するためにアドレスを並べる必要はない(v0.3.10 から) |
|
listen_freebind |
bool | true で、待ち受けるアドレス(listen_addr・extra_listen_addrs)がまだホストになくても待ち受ける(IP_FREEBIND / IPV6_FREEBIND。v0.4.3、features.listen_freebind)。下の「まだホストにないアドレスで待ち受ける」。既定 false。PATCH では変えられない(違う値は unsupported)。一覧では true のときだけ出す |
|
listen_port |
1–65535 | ○ | |
remote_addr |
string | ○ | IP アドレスまたはホスト名。ホスト名は 30 秒ごとに再解決する。http のルールでは書かない(転送先は http.services。一覧では "" / 0)。targets を使うときも書かない(一覧では targets の先頭が入る) |
remote_port |
1–65535 | ○ | http のルール・targets を使うルールでは書かない |
targets |
object の配列 | 宛先を複数にする(v0.3.3、remote_addr / remote_port の代わり。どちらか一方)。{"addr", "port", "weight"?, "backup"?}:addr は IP かホスト名(それぞれ再解決する)、weight は 1 以上(既定 1)、backup: true はほかの宛先がすべて down のときだけ使う(全部を backup にはできない)。最大 64 件。ポート範囲では各宛先の port も範囲の分ずれる。下の「複数の宛先」 |
|
balance |
"round_robin" | "least_conn" | "failover" |
targets の振り分け方。既定 round_robin。一覧では targets があるときだけ出す |
|
health_check |
object | 宛先の生死を TCP の接続で確かめる(v0.3.3)。{"interval"?, "timeout"?, "port"?}:interval 既定 10s、timeout 既定 3s、port は各宛先のポートの代わりに接続するポート(UDP のルールでは必須)。remote_addr だけのルールでも使える。http のルールでは使えない(http.services.<名前>.health_check) |
|
connect_timeout |
string | 宛先への TCP の接続にかけてよい時間(100ms〜10m。v0.4.3、features.connect_timeout)。過ぎたら失敗として数え(outlier_detection の connect)、次の宛先で接続し直す。宛先が 1 つならクライアントの接続を閉じる。省くと今までどおり(ほかに宛先があれば 5 秒、なければ OS の既定(Linux は約 2 分))。tcp のルールだけ(udp と http のルールは invalid。http は http.services.<名前>.timeouts.connect)。tls.routes の宛先にも効く。PATCH で付けると置き換え(0s で外す、省けば今のまま)、次の接続から効く |
|
source_ip |
"proxy" | "proxy_v1" | "proxy_v2" | "transparent" |
既定は "proxy"(送信元 IP を引き渡さない)。proxy_v1 は TCP でのみ使える。proxy_v2 は UDP でも使え、転送先へのデータグラムごとに PROXY v2(DGRAM)のヘッダを付ける(応答にはヘッダがない。宛先アドレスはクライアントが送った宛先のアドレス。0.0.0.0 / :: で待ち受けていても、受けたアドレスになる)。UDP の proxy_v2 と tls.upstream.tls(転送先への DTLS)は組み合わせられない(unsupported)。transparent は GET /capabilities の transparent(IPv4)/ transparent_ipv6(IPv6 の待ち受け)が true のときだけ指定できる。クライアントと転送先は同じアドレスファミリーであること(docs/TRANSPARENT.md) 。説明と転送先の設定の例は docs/SOURCE-IP.md。TLS を終端しない tls.routes[] には route ごとの source_ip も書ける(下の「tls.routes[] の source_ip」) |
|
udp_idle_secs |
1–86400 | UDP セッションを無通信で破棄するまでの秒数。既定は 30。TCP では無視する | |
listen_port_end |
1–65535 | ポート範囲の終わり(listen_port 以上)。listen_port..listen_port_end の各ポートを、remote_port から順に同じ数だけずらした転送先へ送る。上限は GET /capabilities の max_range_ports(既定 20000) |
|
tls |
object | TLS(tcp)/ DTLS(udp)の扱い。省略すると {"mode": "passthrough"}。下の「TLS」を参照 |
|
starttls |
"smtp" | "imap" | "pop3" |
STARTTLS の手前の平文のやり取りに rproxy が答え、TLS を終端する。tls.mode が terminate の tcp ルールでのみ使える |
|
starttls_required |
bool | 既定 true。false にすると、SMTP で STARTTLS をしないクライアントも平文のまま通す(IMAP / POP3 では常に必須として扱う)。starttls なしで false を指定すると invalid |
|
allow_from |
string の配列 | 接続を受け付ける送信元。CIDR(172.16.0.0/16、fd00::/8)または単一の IP。省略または空ならすべて受け付ける。最大 64 件。範囲外からの TCP 接続は、TLS より前に切断する(proxy_protocol のルールでは、from の前段からの接続はヘッダを読んでから、ヘッダのクライアントで判定する)。UDP は範囲外の送信元のデータグラムを捨てる(セッションを作らない)。断った数は stats.denied(UDP はデータグラムごと)、ログは conn.denied(reason: allow_from。UDP は送信元ごとに続けて 20 行まで、その後は 1 秒に 1 行。送信元を偽った大量のデータグラムに備えて、全体でも続けて 200 行、その後は 1 秒に 50 行まで。suppressed はその前に省いた行の数。v0.3.20 から。それより前の UDP は debug でだけ出ていた) |
|
proxy_protocol |
object | 前段(ロードバランサ・別の rproxy・Kubernetes の Gateway など)が付ける PROXY protocol v1 / v2 のヘッダを読み、そのクライアントのアドレスを使う(v0.4.13、#296、features.proxy_protocol_in)。{"from": [<CIDR>, ...], "required"?: bool}:from(必須、空にできない。最大 64 件、/0 は invalid)はヘッダを信じる TCP の接続元、required(既定 true)は from からの接続がヘッダを付けてこなければ断る。from の外からヘッダで始まる接続は断る。udp のルールでは v2 の DGRAM のヘッダをデータグラムごとに読んで外す(v0.4.16、#312、features.proxy_protocol_in_udp)。一覧では required も出す。PATCH で付けると置き換え({} で外す、省けば今のまま)、次の接続(udp は次のデータグラム)から効く。下の「PROXY protocol を受け取る」 |
|
crowdsec |
bool | 既定 false(v0.3.2)。true にすると、CrowdSec の判定(global.crowdsec、scope Ip / Range)に入っている送信元を、allow_from と同じく受け付けた直後(TLS より前。proxy_protocol ではヘッダのクライアントで判定)に切断する。UDP はそのデータグラムを捨てる(開いているセッションのものも)。LAPI から一度も判定を取れていない間は通す。global.crowdsec がないと invalid。http のルールでも使えるが、見るのは接続元の IP(前段のプロキシの後ろでは crowdsec ミドルウェアを使う)。一覧では false のとき省く。断った数は stats.denied、ログは conn.denied(reason: crowdsec。UDP は allow_from と同じく間引く) |
範囲ルールのキーは listen_port(範囲の先頭)。同じプロトコルで待ち受けアドレスとポートが重なるルールは作れない(already_exists)。
UDP のルールを 0.0.0.0 / :: で待ち受けると、rproxy は受けたデータグラムの宛先アドレスを覚え(IP_PKTINFO / IPV6_RECVPKTINFO)、返信はそのアドレスから送る(v0.3.10 から。Linux)。アドレスを複数持つホストでも、クライアントは送った宛先から返信を受け取れる(IKE・WebRTC・QUIC・DTLS など、宛先と違うアドレスからの返信を捨てるクライアントのため)。同じクライアント(アドレスとポート)が別のアドレスに送ったものは別のセッションになり、それぞれのアドレスから返す。L4 の中継・DTLS の終端・UDP のサーバ名での振り分け・HTTP/3 のどれも同じ。conn.open の listen と PROXY v2 のヘッダの宛先も受けたアドレスになる。
{
"protocol": "tcp", "listen_addr": "0.0.0.0", "listen_port": 5432,
"targets": [
{"addr": "10.0.0.11", "port": 5432, "weight": 2},
{"addr": "10.0.0.12", "port": 5432},
{"addr": "db-backup.internal", "port": 5432, "backup": true}
],
"balance": "least_conn",
"health_check": {"interval": "10s", "timeout": "3s"}
}- 新しい接続(TCP)・新しいセッション(UDP)ごとに宛先を選ぶ。
round_robin:weightの比率で順に回す。least_conn:いま開いている接続(UDP はセッション)の数 ÷weightが一番小さい宛先。同じなら順に回す。failover:targetsの上から順に、up の最初の宛先だけを使う。上位が戻れば、新しい接続から戻る。
- up / down:
health_checkがあれば、その結果(最初の確認までは up)。なくても、TCP の接続を断られた(または 5 秒以内に応答がない)宛先は、10 秒のあいだ down として飛ばし(外す)、同じ接続を次の宛先で接続し直す。UDP は、転送先から ICMP の到達不能が返った宛先を同じく外す。外す回数・時間・割合は v0.4 のoutlier_detection(下の「v0.4 の設定」)で変えられる。 - 宛先ごとの上限
max_connections(v0.4.19):いっぱいの宛先は down と同じく飛ばし、すべていっぱいなら断る(下の「宛先ごとの上限」)。 backupの宛先は、ほかの宛先がすべて down のときだけ使う。すべて down なら、down の宛先も順に試す(接続を断らない)。- UDP の既存のセッションは、自分の宛先が down になったら次の宛先へ移る(
conn.retarget、reason: target down)。failoverで上位が戻っても、既存のセッションはそのまま。 - 名前解決できない宛先があっても、ほかの宛先が解決できればルールは動く(解決できない宛先は後から再解決する)。すべて解決できなければ、これまでどおり
resolve_failed。 tls.routes(サーバ名ごとの転送先)は今までどおり route ごとに 1 つ。targetsは一致しない名前(とサーバ名なし)の転送先。- 状態が変わると
event: "target.down"(reason: health_check/outlier、error。outlierはcause(connect・refused・short_lived)とejection_secs・ejectionsも)/"target.up"(reason: health_check/outlier)のログ。v0.3 までは接続の失敗がreason: connectで、外した時間が過ぎたときのtarget.upはなかった。ルールのstats.targetsに宛先ごとの[{"addr","port","backup"?,"up","connections","total_connections","resolved","ejected_until","ejections"}](targetsが 2 つ以上かhealth_checkがあるときだけ。ejected_untilは外している間だけ Unix 秒、ほかは null)、/metricsにrproxy_target_up{protocol,listen,target}(1 / 0)とrproxy_target_connections{protocol,listen,target}。 - 宛先がすべて down のときは、ルールの
all_targets_downがtrue、/metricsのrproxy_rule_all_targets_down{protocol,listen}が 1(v0.3.20。stats.targetsを出すルールだけ。ほかのルールではall_targets_downはいつもfalse)。backup の宛先も含めて、どれも down のとき。 - 宛先・
balance・health_checkを変えると、宛先ごとの接続数と up / down は最初からになる(開いている接続はそのまま)。
{
"mode": "terminate",
"routes": [
{"server_name": "imap.example.com", "remote_addr": "10.0.0.5", "remote_port": 143},
{"server_name": "*.example.com", "remote_addr": "10.0.0.6", "remote_port": 8443}
],
"certificates": [
{"cert_file": "/etc/rproxy/certs/example.pem",
"chain_file": "/etc/rproxy/certs/intermediates.pem",
"key_file": "/etc/rproxy/certs/example.key"}
],
"client_auth": {"mode": "required", "ca_file": "/etc/rproxy/clients-root.pem",
"chain_file": "/etc/rproxy/clients-intermediates.pem"},
"alpn": ["h2", "http/1.1"],
"upstream": {"tls": true, "server_name": "backend.internal", "ca_file": "/etc/rproxy/internal-ca.pem"}
}| フィールド | 説明 |
|---|---|
mode |
passthrough(既定。暗号化されたまま流す)、sni(ClientHello のサーバ名で転送先を選び、復号しない。tcp は TLS、udp は DTLS と QUIC(HTTP/3 など)。下の「UDP のサーバ名での振り分け」)、terminate(rproxy で復号する。tcp は TLS、udp は DTLS) |
routes |
サーバ名ごとの転送先(sni と terminate):{"server_name" または "server_names", "remote_addr", "remote_port", "passthrough"?, "source_ip"?}。範囲ルールでは、remote_port に範囲の長さを足して 65535 を超えないこと。一致しない名前はルールの remote_addr / remote_port(targets)へ。ポート範囲では、ここの remote_port も同じだけずれる。名前の書き方: mail.example.com(完全一致)、*.example.com(1 階層だけ)、**.example.com(1 階層以上、何階層でも。example.com 自体には一致しない)。server_names で 1 つの route に名前を複数書ける(server_name とどちらか一方)。複数の route に一致するときは、完全一致 → *. → **.(接尾辞が長い方)→ 書いた順。passthrough: true(terminate だけ):その名前の接続は終端せず、ClientHello ごと転送先へそのまま流す(sni と同じ。証明書は転送先が持つ)。allow_from・ルールの crowdsec・source_ip・統計は効く(source_ip は route ごとにも書ける。v0.4.13、下の「tls.routes[] の source_ip」)。starttls とは組み合わせられない。HTTP/3(QUIC)では扱わない(その名前の QUIC の接続は閉じる) |
unmatched |
default(既定。どの routes にも一致しない名前・SNI なしは、ルールの remote_addr / remote_port へ)または reject(切断する。terminate ではハンドシェイクを完了せずに切る)。sni(tcp・udp)と tcp の terminate で、routes があるときだけ指定できる |
certificates |
terminate で必須。cert_file はサーバ証明書、chain_file は中間 CA の証明書(サーバ証明書を発行した CA から、ルートへ向かう順。ルートは入れなくてよい)、key_file は秘密鍵。cert_file にチェーンを連結しても使える。読み込むときに、チェーンの順番と、鍵がサーバ証明書と対になっていることを確かめる。複数あれば SNI で選び、どれにも一致しなければ先頭を使う。DTLS の鍵は PKCS#8(-----BEGIN PRIVATE KEY-----)に限る |
client_auth |
クライアント証明書の検証(mTLS)。mode は none(既定)/ optional(送られてきたら検証する)/ required / optional_no_verify(#238。下の「証明書を確かめない optional_no_verify」)。optional と required では ca_file が必須。ca_file はルート CA(信頼の起点)。chain_file はクライアント証明書の中間 CA で、中間 CA を送ってこないクライアントのために、検証の途中経路を補う(信頼の起点にはしない)。TLS と DTLS で同じ規則で検証する |
alpn |
terminate でクライアントに提示する ALPN(tcp のみ) |
upstream |
terminate の転送先側。tls: true で再暗号化する(tcp は TLS、udp は DTLS)。server_name(既定は転送先のホスト名)、ca_file(既定は Mozilla のルート証明書)、insecure_skip_verify(検証しない。テスト用)、cert_file / chain_file / key_file(転送先へのクライアント証明書と、その中間 CA) |
tls.client_auth.mode: optional_no_verify は、クライアント証明書を求めるが、送られなくても、検証に通らなくても接続を受ける(Gateway API の AllowInsecureFallback)。tcp の terminate(http のルールと HTTP/3 を含む)と udp の DTLS の終端で使える。GET /capabilities の features.client_auth_modes に optional_no_verify があれば使える。
udp の DTLS の終端は、ClientHello を断片に分けずに 1 つのレコードで送るクライアントを受ける(v0.4.11 から)。断片に分かれた ClientHello と、拡張の欄が途中で切れた ClientHello は捨てる(debug の udp.drop)。rproxy が使う DTLS のライブラリ(dtls 0.17.2)は、拡張が途中で切れた ClientHello を読むとそのセッションの処理が panic で止まるため(#287)。DTLS 1.2 の ClientHello はふつう 1 つのデータグラムに収まる。名前で振り分ける sni は rproxy の自前の読み取りなので、断片に分かれていても扱える。
"client_auth": {"mode": "optional_no_verify", "ca_file": "/etc/rproxy/clients-root.pem"}ca_fileは省ける。あれば、送られた証明書をそれで確かめた結果(SUCCESS/FAILED)を下の形で知らせる。なければ送られた証明書はいつもFAILED。chain_fileはca_fileがあるときだけ。- 証明書を送ったクライアントは、その鍵を持っていることだけは TLS のハンドシェイクで確かめる(署名)。チェーン・期限・CA は確かめない(rproxy は断らない)。
- 知らせ方:
httpのルール(client_authがあるとき。どのモードでも):転送先へX-Client-Verify: SUCCESS | FAILED | NONE(nginx の$ssl_client_verifyと同じ値)と、証明書があればX-Forwarded-Client-Cert: Hash=<SHA-256 の 16 進>;Subject="<RFC 4514 の subject>"(Envoy の形。Subjectは確かめられた証明書のときだけ)。forward_authの問い合わせとmirrorの写しにも同じ値を付ける。アクセスログにclient_cn・client_verify。- クライアントが送ってきた
X-Client-Verify・X-Forwarded-Client-Certは、どのルールでも(client_authのない平文・HTTPS のルール、HTTP/1.1・HTTP/2・HTTP/3、Upgrade、forward_auth・mirrorを含む)、ミドルウェアより前に必ず消す(Envoy の SANITIZE と同じ。同じ HTTPRoute を mTLS の 443 と平文の 80 に付けても、80 から偽れない)。mirrorの写しのX-Forwarded-For・X-Real-IP・X-Forwarded-*も、転送先へのリクエストと同じものにする。 - L4 の終端:
conn.openのログにclient_cn・client_verify。source_ip: proxy_v2なら、PROXY v2 の SSL の TLV のverifyが 0(確かめた、または証明書なし)か 1(確かめられなかった証明書)。確かめられなかった証明書の CN は TLV(SSL_CN)に入れない。
- 安全上の注意:このモードは認証にならない。誰でも(証明書なし・偽の証明書で)接続できるので、許すかどうかは転送先が
X-Client-Verify(または PROXY v2 のverify)を見て決めなければならない。転送先へは rproxy を通る経路だけにする(直接つながると、転送先はヘッダが rproxy のものか分からない)。Gateway API も試験や一時的な移行のためのものとしている。認証に使うならrequired、使えるなら確かめるoptional。
udp のルールでも tls.mode: sni と tls.routes で、最初のデータグラムのサーバ名(SNI)から転送先を選べる。終端しないので rproxy に証明書は要らず、証明書は転送先が持つ。
- 読めるもの:
- DTLS 1.2 / 1.3 の ClientHello(平文。断片に分かれていても、データグラムをまたいでもつなぎ合わせる)
- QUIC v1(RFC 9000 / 9001)/ v2(RFC 9369)の Initial パケット(HTTP/3 など)。Initial の鍵はクライアントが選んだ接続 ID から誰でも計算できる(RFC 9001 §5.2)ので、ヘッダの保護と暗号を外して CRYPTO フレームから ClientHello を読む。ClientHello が複数の Initial にまたがっても(大きな鍵共有など)つなぎ合わせる
- 新しいクライアント(アドレスとポート)の最初のデータグラムを、名前が分かるまで持つ(最大 3 秒・16 データグラム・64 KiB)。名前が分かったら、持っていたデータグラムを順番どおり転送先へ送り、あとは今までの UDP と同じくそのクライアントのセッションとして中継する。名前が 1 つのデータグラムで分からず続きを待つセッションは 1 ポートあたり 4096 まで、そのうち 1 つの送信元のプレフィックス(IPv4 は /24、IPv6 は /48)が持てるのは 64 まで(#332。送信元は偽れるので、1 つの網からの洪水がほかの網のクライアントの枠を取らないように)。超えたらその網のいちばん古いものを、4096 がすべて埋まったら全体のいちばん古いものをあきらめて(そのセッションは終わり、持っていたデータグラムは捨てる。クライアントが再送する)新しいものに渡す。新しいセッションは断らないので、洪水を受けている網のクライアントも、次のデータグラムが届くまでにその網から 64 の新しいセッションが来なければ通る。どれも
stats.droppedに数える。1 つのデータグラムで名前が分かる ClientHello(ふつうの場合)は枠を使わない - つなぎ合わせる断片にも上限がある(#332):1 つのセッションで ClientHello の断片(DTLS の断片・QUIC の CRYPTO フレーム。空のもの・同じものの再送も数える)は 512 まで、すき間の後の飛び飛びの部分は 1 つのメッセージで 128 まで、DTLS の 1 セッションで確保したメモリが 64 KiB を超えたら(ClientHello は 32 KiB まで。断片を受け取るごとに確かめるので、あきらめる前に一時的にもう 1 メッセージ分ほど増えることはある。持っているデータグラムの 64 KiB は別)あきらめる。これらを超えたら名前のないもの(下の
unmatched)として扱う。持つのは受け取ったバイトだけで、断片が書く位置の分のメモリは取らない。断片が重なって中身が違うときは、先に届いたバイトを使う - DTLS でも QUIC でもないデータグラム、SNI がないもの、どの route にも一致しない名前:
unmatched: default(既定)ならルールの宛先(remote_addr/targets)、rejectなら捨てる(stats.denied、conn.deniedのreason: unmatched) - 同じクライアントのソケットから、別の名前への新しい QUIC の接続(違う接続 ID の Initial)が来たら、名前を読み直し、違う名前ならセッションを作り直す(quinn などは 1 つのソケットから次の接続を始める)。同じ名前(Retry の後など)なら今の転送先のまま
allow_from・ルールのcrowdsecは名前を読む前に効く。conn.openのログにsniroutesのpassthroughは使えない(sniはすべて passthrough)。DTLS を終端するterminateの udp のルールは名前で振り分けない- できないこと:
- QUIC の接続の移動(クライアントのアドレスやポートが変わる)は追いかけない(移った先は新しいクライアントとして名前を読み直す。Initial でないので、ルールの宛先へ)
- ECH(Encrypted Client Hello)を使う接続では、本当の名前は読めない(外側の public name で振り分ける)
terminate と source_ip: "proxy_v2" を組み合わせると、PROXY v2 ヘッダに TLS の情報を TLV で付ける。
PP2_TYPE_AUTHORITY:SNIPP2_TYPE_ALPN:ALPNPP2_TYPE_SSL:TLS であること、クライアント証明書の有無、PP2_SUBTYPE_SSL_VERSION、クライアント証明書の CN(PP2_SUBTYPE_SSL_CN)
証明書ファイルは、ルールの作成・変更のときに読み込む。読み込んだ証明書は証明書の単位で共有する(同じファイルを使う複数のルールは同じものを使う)。そのあとは:
RPROXY_CERT_CHECK_SECS(既定 60 秒、0で止める)ごとに、証明書ごとにファイル(証明書・鍵・中間 CA・CA)の大きさ・更新時刻・inode を確かめ、変わった証明書だけ読み直して、それを使うルールに反映する。certbot などでの上書きも、Kubernetes の Secret のようにシンボリックリンクを差し替える方式も検知する。制御 API の証明書(RPROXY_TLS_CERT/RPROXY_TLS_KEY)も同じ。- 読み直せなかったとき(書き込み途中で鍵と証明書が合わないなど)は今の証明書のまま使い、次の確認でもう一度試す(
reload.tlsの警告はファイルの版ごとに 1 回)。 - SIGHUP を送ると、変わったかどうかにかかわらず全部の証明書をすぐに読み直す。
証明書を読み込むとき(作成・変更・ファイルの変化・SIGHUP)と、RPROXY_CERT_EXPIRY_CHECK_SECS(既定 86400 秒 = 1 日、0 で止める)ごとに、期限(notAfter)を確かめる。
- サーバ証明書(
tls.certificates。中間 CA を含めて、どれか 1 枚でも切れていれば切れた扱い):- 切れた証明書だけを外し、ほかの証明書で動き続ける。外した証明書の名前には、残りの先頭の証明書を返す(クライアントからは名前の不一致に見える。切れた証明書を返すことはない)。
- すべて切れたら、ルールを
failed(errorはcertificate expired: ...)にして待ち受けを閉じる。更新されたファイルを読み込めた時点(ファイルの変化・SIGHUP・PATCH)で、自動でrunningに戻る。 - API で作成・変更するときに、すべて切れていれば
400 tls_config(certificate expired: ...)で断る(作らない)。起動時(DB・設定ファイル)と読み直しのときはfailedとして登録する。
- クライアント認証の CA・中間 CA、転送先向けの CA・証明書、制御 API の証明書:ルールは止めない。表示・ログ・メトリクスで知らせるだけ。
passthroughの route は対象外(証明書は転送先のもの)。- 期限の
RPROXY_CERT_WARN_DAYS(既定 14 日)前からexpiring。状態が変わったときに 1 回だけ、ログcert.expiring(警告)・cert.expired(エラー)・cert.ok(更新された)を出す。 /metricsのrproxy_cert_expiry_seconds{protocol,listen,role,file}(制御 API の証明書は{role="api",file}):期限までの秒数(切れたら負)。
証明書は ACME で rproxy に取らせることもできる(v0.3.21 から。tls.certificates[] に {"acme": "<resolver>", "domains": [...]}、設定ファイルの global.acme。取った証明書も同じ証明書のストアで読み込み、更新は上と同じく自動で反映される)。詳しくは docs/ACME.md。certbot・acme.sh・cert-manager などで取ったファイルを cert_file / key_file に指定するやり方もそのまま使える(certbot の http-01 は、80 番の http のルールで /.well-known/acme-challenge/ を certbot の webroot / standalone のポートへ振り分ければよい。rproxy 自身が答えているトークンでなければルートに渡る)。
応答で返すルールには、次の稼働情報が加わる(allow_from は正規化した CIDR の形で返す。例:10.0.0.5 → 10.0.0.5/32)。
| フィールド | 説明 |
|---|---|
state |
"running" または "failed" |
error |
failed の理由。running なら null |
resolved |
最後に名前解決できた転送先("ip:port" の配列)。まだ解決できていなければ空 |
connections |
現在の接続数(UDP はセッション数) |
stats |
ルールが開始してからの累計:total_connections、rx_bytes(クライアント → 転送先)、tx_bytes(転送先 → クライアント)、tls_failures(TLS / DTLS のハンドシェイクや STARTTLS の失敗) |
started_at |
待ち受けを始めた時刻(Unix 秒)。failed のときは null |
all_targets_down |
宛先がすべて down(v0.3.20。下の「複数の宛先」) |
down_services |
http のルールで、health_check のあるサービスのうち、up の転送先が 1 つもないものの名前(v0.3.20)。なければ省く |
cert_status |
terminate のルールが使う証明書の期限(下の「証明書の期限」)。証明書がなければ省く。各要素は role(certificate / client_ca / client_chain / upstream_ca / upstream_certificate)、file(証明書のファイル)、not_after(RFC 3339、UTC)、days_left(残りの日数。切れたら負)、state(ok / expiring / expired) |
acme |
ACME の証明書(tls.certificates[].acme)の状態。なければ省く。各要素は resolver、domains、state(pending:まだ取れていない(自己署名の仮の証明書を返す)/valid/renewing:更新の時期/error:最後の試みが失敗(取れていた証明書はそのまま使う))、not_after・renew_at(RFC 3339)、next_attempt(失敗や rate_limit で待っている次の時刻)、error、ari(CA の更新の窓 start・end。ARI、RFC 9773) |
origin |
dynamic(API で作ったルール、または DB から復元したルール)か static(固定ルール。下を参照) |
stats には denied(allow_from の範囲外、crowdsec の判定、または unmatched: reject で切断した接続の数)と、dropped(UDP で rproxy が転送できずに捨てたデータグラムの数:セッションの待ち行列があふれた、送信に失敗した、名前を読んでいるセッションが多すぎる。v0.3.9 から。カーネルのソケットの受信バッファがあふれて捨てたものは rproxy からは見えないので数えない)も含む。GET /metrics では rproxy_udp_dropped_total{protocol,listen}(UDP のルールだけ)。
- TCP:片方が閉じた(FIN)ときは、もう片方にも半分閉じた(FIN)として伝え、反対の向きはそのまま続ける。片方がリセット(RST)した、または書き込めなくなったときは、もう片方もリセットで切る(正常に終わったように見せない。
SO_LINGER0)。TLS を終端するルールでは、転送先がリセットしたらクライアントの TLS は close_notify なしで切れる。 - UDP:データグラムは 1 つずつそのまま、届いた順に転送する(まとめない・分けない・並べ替えない・重ねない)。rproxy が捨てたものは上の
droppedに数える。 - tests/integrity.rs で、何十 MiB の擬似乱数のデータを流して SHA-256 を比べて確かめている(TCP の passthrough・terminate・
upstream.tls・proxy_v2、UDP・DTLS、HTTP/1.1・HTTP/2・HTTP/3・WebSocket、compress・buffering・retry、転送先への接続の再利用)。RPROXY_TEST_INTEGRITY_MBで大きさを変えられ、CI(.github/workflows/integrity.yml)で毎週 512 MiB で動かす。 宛先が 2 つ以上かhealth_checkのあるルールでは、stats.targetsに宛先ごとの状態が入る(上の「複数の宛先」)。httpのルールでは、stats.httpにリクエストの数も入る(ほかのルールでは省く)。
"http": {"requests": 5, "by_status": {"2xx": 3, "4xx": 2},
"routes": {"site": {"requests": 3, "by_status": {"2xx": 3}}, "(none)": {"requests": 1, "by_status": {"4xx": 1}}}}health_check のあるサービスがあれば、services に転送先ごとの状態が入る({"app": [{"url": "http://10.0.0.20:80", "up": true}, ...]}。max_connections の転送先(v0.4.19)は max_connections・in_flight・full も)。rate_limit / in_flight で断ったリクエストがあれば、limited(合計)とルートごとの limited(ミドルウェアの名前ごと)も入る。crowdsec で断ったリクエストは同じ形で blocked に入る(どちらも by_status の 4xx にも数える)。cache(v0.4.19)のあるルールは cache(hit・miss・stale・bypass)も入る(「応答のキャッシュ」)。
by_statusは状態コードの百の位ごと(1xx〜5xx。0 件の区分は省く)。routesはルートの名前ごとで、どのルートにも一致しなかったリクエストは(none)。- 応答の本文を送り終えた(またはクライアントが切断した)ときに数える。
RPROXY_CONFIG(--config)に設定ファイル(またはそのディレクトリ)を指定すると、起動時にそのルールを開始し、ファイルが変わると再起動なしで反映する。0.2 の RPROXY_STATIC_RULES(--static-rules)も同じ意味で使える(両方は指定できない)。
- 形式は拡張子で決まる:
.yaml/.ymlは YAML(コメント、アンカー&name/*nameが使える)、それ以外は JSON。YAML と JSON は同じ形で、同じ意味になる。 - 中身は次のどちらか。
{"version": 1, "global": {...}, "rules": [...]}(v0.3)- ルールの配列(0.2 の形)
rulesの各要素はPOST /rulesの本文と同じ形。globalはプロセス全体の設定(trusted_proxies、access_log、acme、crowdsec。docs/DESIGN-v0.3.md の 2.)。acme: ACME のアカウント・DNS のプロバイダ・resolver・許可する名前(docs/ACME.md)。秘密はファイルで指し、API からは触れない。files(v0.4):{"owner_check": "strict" | "off", "trusted_dirs": ["/var/run/rproxy-gateway/certs"]}(既定strict、trusted_dirsなし)。trusted_dirs(なければ環境変数RPROXY_FILES_TRUSTED_DIRS、:か,区切り。両方あれば設定ファイル)の下にある(シンボリックリンクをたどった本当のパスで判断)ファイルは root のものでもよい(Kubernetes の Secret のボリューム)。絶対パスでなければ設定の誤り。ルールとglobalが指す証明書・鍵・秘密のファイルは、rproxy のユーザー(rproxy-api)のもので、グループ・ほかの人が書けず、鍵・秘密はほかの人が読めないものだけ使う(docs/PERMISSIONS.md の「ルールが指すファイルの所有者」)。offは root のファイルをそのまま使うときだけ(危険を承知で。起動時にdegraded)。trusted_proxies: CIDR の配列。httpのルールで、接続元がこの範囲ならX-Forwarded-Forを信用する(API で作ったルールにも効く)。crowdsec: CrowdSec の bouncer(crowdsecミドルウェアと、ルールのcrowdsec: trueが使う。書かずにそれらを使うとinvalid、設定ファイルなら起動しない)。lapi_url(例http://127.0.0.1:8080)のGET /v1/decisions/streamをupdate_interval(既定10s)ごとに呼び、判定を覚えておく(最初と、失敗した後はstartup=trueで全部を取り直す)。X-Api-Keyはapi_key_fileの中身(cscli bouncers add rproxyで作ったキー。SIGHUP で読み直す)。- 使う判定は scope が
IpとRange、type がbanとcaptcha(captcha は出せないので ban として扱う)。ほかの scope(Country など)と type は使わない。 - LAPI に届かないときは、それまでの判定を使い続け、間隔を倍々に延ばして(最大 5 分)取り直す(
crowdsec.error。取得できたらcrowdsec.sync)。一度も取得できていない間は、ミドルウェアのon_errorに従う。 appsec_url(例http://127.0.0.1:7422): AppSec に問い合わせる(ミドルウェアのappsec: true。書かずにappsec: trueを使うとinvalid)。api_key_fileがないか空なら起動しない。読めない(権限)なら、読めるようになって SIGHUP するまで判定なしで動く(part: global.crowdsec)。
access_log:httpのルールのアクセスログのファイル(JSON Lines。RPROXY_LOG_FILEと同じく日ごとに<名前>.<日付>.<拡張子>へローテーションし、RPROXY_LOG_KEEP個残す)。省略するとアクセスログはメインのログ(event: "http.access")に出す。ディレクトリがなければ起動しない。書き込めなければメインのログに出す(part: global.access_log)。
- ディレクトリを指定すると、その中の
*.yaml/*.yml/*.jsonを名前の順に読み、1 つの設定としてまとめる(.で始まるものは読まない。Kubernetes の ConfigMap をそのままマウントできる)。- 各ファイルは上のどちらかの形。
rulesはつなげる。globalを書けるのは 1 つのファイルだけ(2 つにあるとエラー)。 - 同じキー(プロトコル・アドレス・ポート)のルールが 2 つあると、両方のファイル名と位置(
web.yaml rule #2など)を示してエラーにする。
- 各ファイルは上のどちらかの形。
- DB からの復元より前に開始する。DB に接続できなくても動く。
- API からは変更・削除できない(
409 static)。変えるときはファイルを書き換える。 - 再起動なしの反映:
RPROXY_CONFIG_CHECK_SECS(既定 10 秒。0なら SIGHUP のときだけ)ごとに、ファイルの大きさ・更新時刻・inode(ディレクトリならファイルの増減も)を確かめ、変わっていれば読み直す。SIGHUP を送ると、変わっていなくても読み直す。結果をその場で知りたいときはPOST /config/reload(下のエンドポイントの表)。シンボリックリンクの差し替え(ConfigMap の更新)も検知する。- 読み直した設定は、まず全体を検証する。誤りがあれば何も変えず、それまでのルールを使い続ける(
event: "config.error"。同じ内容では 1 回だけ)。読めない(権限)ときも同じで、読めるようになるまで確認のたびに試す。 - 正しければ差分だけを反映する(
event: "config.reload"。added・removed・changed・unchanged・failedの件数)。- 増えたルールは開始し、なくなったルールは停止する(既存の接続は切る)。
- 変わったルールは、PATCH で変えられる項目(転送先、
udp_idle_secs、tls、starttls、allow_from、http)だけの違いなら、そのまま変える(既存の接続は切らない。TCP は新しい接続から)。ポート範囲・source_ipなどが変わったときは、停止してから作り直す。 - 変わっていないルールには触らない(接続も切らない)。
- 同じキーのルールが API(DB)から作られていれば、そちらを残してファイルのルールを
rule.failedとしてログに出す。
- v0.4.19 から(#342):
globalのうちその場で変えられる項目(trusted_proxies・access_log・crowdsec・http.timeouts)は、読み直したときに入れ替える(新しいリクエスト・接続から。config.global。前は再起動まで効かなかった)。API で同じ項目に値を入れていても、ファイルに書いた値が勝つ(下の「global の設定の API」)。入れ替えるのはルールを確かめた後で、ルールの誤りでも live の項目の誤り(ないaccess_logのディレクトリ、読めない CrowdSec のキー)でも、何も変えずに読み直しを断る(前は通っていた誤りで読み直しが止まるようになった)。書けないアクセスログは主のログに出して試し直し、答えない LAPI のcrowdsecは今のものを残し、ルールが使っているcrowdsecをファイルから消したときは使うルールがなくなるまで残す(config.global_in_use)。 - 起動のときだけ読む項目(
acme・geoip・performance・files)の変更は再起動するまで効かない(起動時の値と違うとconfig.reloadの警告とGET /configのrestart_neededで知らせる)。 - 状態は
GET /configで見える:{"configured":true,"path":"/etc/rproxy/conf.d","files":[...],"loaded_at":1790000000,"rules":5,"last_reload":{"added":1,"removed":0,"changed":1,"unchanged":3,"failed":0},"error":null,"restart_needed":[]}(設定ファイルを使っていなければ{"configured":false})。errorは最新の版を反映できなかった理由(それまでの版が動いている)。
- 読み直した設定は、まず全体を検証する。誤りがあれば何も変えず、それまでのルールを使い続ける(
- 反映する前に確かめる:
rproxy-api --check-config [PATH]が、起動時・再読み込みと同じ検証(書式、ルールの値、待ち受けの重なり・制御 API との重なり、証明書・鍵・CA のファイルと期限、global、ミドルウェアの秘密のファイル)をして、問題がなければ 0、誤りがあれば 1 で終わる(待ち受けも DB も開かない。--check-config-format jsonで{"ok","path","files","rules","errors":[{"rule","message"}],"warnings":[...]})。名前解決はしない。パッケージのユニットのExecReloadは、先にこの確認をする(誤りがあれば reload は失敗し、SIGHUP を送らない)。 - 同じキーや重なるポートのルールを API や DB から作ろうとすると、
already_existsになる。 - ファイルが存在しない、書式や形が不正(知らないキー、
versionが 1 以外、存在しない ACME の resolver の参照、global.acmeの許可の外の名前、ない秘密のファイルなど)の場合は、rproxy は起動しない。読めない(権限)ときは、固定ルールなしで起動する。 - この版で動かせない機能(
GET /capabilitiesのfeaturesが false)を使うルールは、failed(理由つき)として登録し、設定の内容はGET /rulesで見える。名前解決や bind の失敗はほかのルールと同じ扱いになる。
例:ダッシュボード(Web UI)を dashboard.proxy.home だけで、社内から公開する。
# /etc/rproxy/rproxy.yaml
version: 1
rules:
- protocol: tcp
listen_addr: 0.0.0.0
listen_port: 443
remote_addr: 127.0.0.1
remote_port: 3001
allow_from: [172.16.0.0/16]
tls:
mode: terminate
certificates:
- cert_file: /etc/rproxy/certs/dashboard.pem
chain_file: /etc/rproxy/certs/intermediates.pem
key_file: /etc/rproxy/certs/dashboard.key
routes:
- {server_name: dashboard.proxy.home, remote_addr: 127.0.0.1, remote_port: 3001}
unmatched: rejectv0.3.0 で形を決め、中身は v0.3.x のパッチで順に使えるようにする(docs/RELEASING.md)。この版で動かせるかは GET /capabilities の features で分かる。動かせない設定を使うルールは、形を検証したうえで 400 unsupported で断る(形が不正なら 400 invalid / tls_config)。設計の全体と例は docs/DESIGN-v0.3.md。
| 項目 | 場所 | 形 | features |
|---|---|---|---|
| L7 のルーティング | ルールの http |
routes(name、match、priority、service か to、middlewares)、default、services、middlewares、http3 |
http(v0.3.1 から)、http3(v0.3.2 から)、middlewares |
| サービス | http.services.<名前> |
servers(url、weight)、pass_host_header、timeouts(connect、response)、health_check、sticky、balance |
http。health_check / sticky / balance は services に含まれるもの |
match |
http.routes[].match |
Traefik と同じ式。Host・HostRegexp・Path・PathPrefix・PathRegexp・Method・Header・HeaderRegexp・Query・QueryRegexp・ClientIP を &&・||・!・括弧で組み合わせる |
http |
| ミドルウェア | http.middlewares.<名前> |
{種類: {設定}}。種類は redirect_scheme・redirect_regex・rate_limit・in_flight・crowdsec・ip_allow・headers・forward_auth・oidc・basic_auth・strip_prefix・add_prefix・replace_path・replace_path_regex・compress・buffering・retry・circuit_breaker・errors・respond |
middlewares に種類が含まれるもの |
| ACME の証明書 | tls.certificates[] |
{"acme": "<resolver>", "domains": [...]}(cert_file / key_file の代わり)。resolver は設定ファイルの global.acme.resolvers。acme:write のスコープが要り、名前は resolver のアカウント(と DNS のプロバイダ)の allowed_names の内だけ(外なら 400 invalid)。tcp の terminate だけ(udp は tls_config)。docs/ACME.md |
acme(v0.3.21 から true) |
| TLS のオプション | tls.options |
min_version("1.2" / "1.3")、cipher_suites(下) |
tls_options(v0.3.2 から true) |
tls.options(v0.3.2)は tcp のterminate(httpのルールを含む)の、クライアントとの TLS に効く。転送先への TLS(upstream)には効かない。UDP(DTLS)では使えない(unsupported)。min_version:"1.3"で TLS 1.2 のクライアントを断る。省略・"1.2"なら 1.2 と 1.3。cipher_suites: 使う暗号スイートの名前(rustls の名前。TLS 1.3 はTLS13_AES_128_GCM_SHA256・TLS13_AES_256_GCM_SHA384・TLS13_CHACHA20_POLY1305_SHA256、TLS 1.2 はTLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256・TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256など)。知らない名前はtls_config(エラーに使える名前の一覧が出る)。書いたスイートがない版は提示しない(TLS 1.3 のスイートだけを書けば TLS 1.3 だけになる)。min_version: "1.3"で TLS 1.3 のスイートが 1 つもないとtls_config。- 鍵交換の曲線と、一致しない SNI を断る設定(Traefik の sniStrict。rproxy では
tls.unmatched: reject)はoptionsにはない。 - 決まった版と暗号スイートは
conn.openのtls_version/tls_cipherに出る。
httpはprotocol: tcpで、tls.modeがterminate(HTTPS)か、TLS なし(平文の HTTP)のときだけ。sni・starttls・ポート範囲とは組み合わせられない。remote_addr/remote_portは書かない(書くと400 invalid)。httpのルールでも、tls.routesのpassthrough: trueの route は使える(同じポートで、その名前だけを終端せずに流す。例:cdn・gitlab は L7、registry と**.tenant.example.comは Kubernetes へそのまま)。passthrough でないtls.routesとunmatched: rejectはtls_config(振り分けはhttp.routes、一致しないときはhttp.default)。http.http3: true(v0.3.2)で、同じアドレス・ポートの UDP でも QUIC + HTTP/3 を受ける。tls.mode: terminateのときだけ(TLS なしなら400 tls_config)。source_ip: transparentとは組み合わせられない(unsupported)。- 証明書・クライアント認証(
client_auth)は TCP と同じもの。QUIC は TLS 1.3 だけなので、tls.options.cipher_suitesに TLS 1.3 の暗号スイートが要る(TLS13_AES_128_GCM_SHA256がないと QUIC の初期化に使えない)。証明書の読み直し(SIGHUP、RPROXY_CERT_CHECK_SECS)は新しい QUIC 接続から効く。 - リクエストは HTTP/1.1・HTTP/2 と同じルート・ミドルウェア・転送先に渡る(転送先へはサービスの
protocolのとおり。既定は HTTP/1.1)。本文は流しながら送る。アクセスログのprotocolはHTTP/3.0。 allow_fromとルールのcrowdsecは QUIC の接続を受ける前に確かめる(conn.denied、transport: quic)。- TCP 側(HTTP/1.1・HTTP/2)の応答には
Alt-Svc: h3=":<ポート>"; ma=86400を付ける(転送先がAlt-Svcを返したときはそのまま)。HTTP/3 を受けていないあいだは付けない。 - UDP のポートを使えない(使用中・権限)、または TLS の設定が QUIC に使えないときは、ルールは TCP だけで動き、
stats.http.http3に{"listening": false, "error": "..."}と出る(ログはevent: degraded、part: http3)。受けているときは{"listening": true}。http3のないルールにはhttp3の項目がない。 PATCHでhttp3を付け外しすると、UDP の待ち受けを始める・止める(止めると開いている QUIC 接続は切る)。受けられなかったルールは、http3: trueのままhttpを PATCH するともう一度試す。ルールを削除すると UDP のポートも閉じる。- ファイアウォールでは TCP と同じポートの UDP も開ける。
PATCHでhttpを付けると、L7 の設定を丸ごと置き換える(次のリクエストから)。httpのないルールにPATCHでhttpを付けることはできない(unsupported。作り直す)。
- クライアントとは HTTP/1.1 と HTTP/2 で話す。
terminateでは、tls.alpnを指定していなければ ALPN でh2とhttp/1.1を提示する(GET /rulesのtls.alpnは指定どおりのまま)。平文では HTTP/1.1 と、前置きで始まる HTTP/2(h2c)を受ける。 - ルートは
priorityの大きい順(省略時はmatchの文字数。Traefik と同じ)、同じなら書いた順に試し、最初に一致したものを使う。どれにも一致しなければdefault(serviceかstatus。省略時は 404)。 Hostはポートを除き、大文字小文字を区別せず、末尾のドットを除いて比べる(EXAMPLE.com.はexample.com。v0.4.17、#320)。HTTP/2 では:authorityを使う。パスは照合の前に正規化する(下の「HTTP の転送の扱い」の「パス」)。ClientIPはクライアントの IP:接続元、または接続元がglobal.trusted_proxiesの範囲なら、X-Forwarded-Forを右から見て最初の信頼しないアドレス(Traefik と同じ。クライアントが左に書き足したアドレスは使わない)。ip_allow・X-Real-IP・アクセスログも同じ IP を使う。matchの式の上限(v0.3.18):括弧と!の入れ子は 32 段まで、1 つの式に書ける条件(Host(...)など。引数の数は数えない)は 256 個まで。超えると設定の誤り(API は 400、設定ファイルは起動・再読み込み・--check-configの誤り)。- 時間の値(
10s・500ms・1m・2hの形。period・timeouts・health_checkのinterval/timeout・initial_interval・window/recovery・update_intervalなど)は 365 日(8760h)まで(v0.3.18)。それより長い値は設定の誤り。 - URL の値(
servers[].url・ルートのto・forward_auth.address・oidc.issuerと discovery の各エンドポイント・global.crowdsecのlapi_url/appsec_url・ACME のdirectory/api_url/url)は、どれも同じ厳しい読み方をする(v0.4.17、#324。ブラウザや UI の WHATWG の URL と rproxy の読み方がずれて、前段の確かめをすり抜けられないように)。外れたものは設定の誤り(API は項目の名前つきで400 invalid、設定ファイルは起動・再読み込み・--check-configの誤り。OIDC の discovery の応答なら、そのプロバイダへのサインインが 502):http://かhttps://(小文字)で始まる。- 見える ASCII の文字だけ(空白・制御文字・ASCII 以外は不可)。
\(WHATWG は/と読む)と#(フラグメント)も不可。 - ユーザー情報(
user:pass@)は不可(Hostに入ってしまい、WHATWG は取り除く)。 - ホストは空でない。名前は英数字・
-・_・.(空のラベル(a..b・.)と、-で始まる・終わるラベルは不可。末尾の.1 つは可)、IPv4 はa.b.c.dの形だけ(127.1・0x7f.0.0.1・010.0.0.1のように最後のラベルが数字で、この形でないものは不可)、IPv6 は[...]でゾーン ID(%25eth0)なし。 - ポートを書くなら 1〜65535 の 10 進の数字(
:・:0・:65536・:8aは不可。v0.4.16 までは、こうした URL をポートなしとして 80 / 443 につないでいた)。 - 後ろにパスを付ける URL(
oidc.issuer・lapi_url・appsec_url・--diff-api)はクエリ(?)も不可。 - OIDC の issuer が
https://なら、discovery のauthorization_endpoint・token_endpoint・jwks_uri・end_session_endpointもhttps://。必須のエンドポイントが外れたら discovery の失敗、任意のend_session_endpointが外れたら警告(oidc.error)を出してないものとして扱う(ログアウトは/へ)。 RPROXY_UPDATE_SOURCE(https://だけ)と--check-config --diff-apiの URL も同じ読み方で、外れたら起動・--check-configの誤り。
- 転送先とは HTTP/1.1 で話す(サービスの
protocolで HTTP/2 も。下の「Gateway API 向けの L7・TLS」)。serversはweight(既定 1)の重みつきラウンドロビン。urlにパスがあれば、リクエストのパスの前に付ける。https://の転送先の証明書は、ルールのtls.upstreamのca_file(なければ Mozilla のルート)で検証し、server_name/insecure_skip_verify/ クライアント証明書もそれに従う。tls.upstream.tlsは使わない(URL のhttps://で決まる。指定するとtls_config)。サービスにtls(#236)があれば、そのサービスではtls.upstreamの代わりにそれを使う。 - 転送先への接続は、応答の本文を読み終えたあと、次のリクエストに使い回す(転送先ごとに待機中の接続は 1024 本まで、使われないまま 4 秒たったものは閉じる。
source_ip: transparentのルールでは、送信元がクライアントごとに違うので使い回さない)。使い回そうとした接続を転送先が閉じていたら、新しい接続で送り直す。 health_check(v0.3.2):interval(既定10s)ごとに各serversへGET <URLのパス><path>(Hostは転送先のホスト)を送り、timeout(既定3s)以内に 2xx / 3xx が返れば up、そうでなければ down。down の転送先はラウンドロビンから外し、戻れば入れる。最初の確認までは up として扱う。すべて down なら 503。状態が変わるとevent: "http.health"のログ(service、server、up、down の理由のerror)。ルールのstats.http.services.<サービス名>に[{"url","up"}]、/metricsにrproxy_http_server_up{protocol,listen,service,server}(1 / 0)。サービスの転送先がすべて down なら、ルールのdown_servicesにサービスの名前が入り、rproxy_http_service_down{protocol,listen,service}が 1(v0.3.20)。balance(v0.3.3):round_robin(既定。weightの比率)、least_conn(処理中のリクエストがweightあたり一番少ない転送先)、failover(serversの上から順に、up の最初の転送先)。どれも down の転送先は外す(health_checkの結果)。stickyのクッキーの転送先が up なら、そちらが優先。sticky(v0.3.2): 初めてのクライアントには、選んだ転送先を示すクッキー(<cookie>=<URL から作った 16 桁の値>; Path=/; HttpOnly; SameSite=Lax、HTTPS ならSecureも)を付け、以後そのクッキーの転送先へ送る。その転送先が down か、知らない値なら選び直してクッキーを付け直す。値は URL から作るので、rproxy を再起動してもほかの転送先を足しても変わらない。weightで転送先を切り替えられる(例:新しい版をweight: 1、今の版をweight: 9にして 1 割だけ流す)。pass_host_header(既定 true)が false なら、Hostは転送先の URL のホスト(とポート)にする。- 転送先へは
X-Forwarded-For・X-Real-IP(クライアントの IP)、X-Forwarded-Proto(http/https)、X-Forwarded-Host、X-Forwarded-Portを付ける。クライアントが送ってきた同名のヘッダは置き換える。ただし接続元がglobal.trusted_proxiesの範囲なら、X-Forwarded-Forは受けた値の後ろに接続元を足し、X-Forwarded-Proto/-Host/-Portは受けた値を保つ。RFC 7239 のForwardedも同じく、global.trusted_proxiesの範囲の接続元から受けたものだけを渡し、それ以外のクライアントが送ったものはリクエストの最初に(ミドルウェアより前に)取り除く(v0.4.18、#333。v0.4.17 まではそのまま渡していた。rproxy はForwardedを付けないが、ルールのheadersやサーバの ext_authz が付けたものは渡す)。ホップごとのヘッダ(Connectionとそこに書かれたもの、Keep-Alive、TE、Transfer-Encodingなど)は取り除く。 Connection: Upgrade(WebSocket など)は、転送先が 101 を返せばそのまま中継する。ルールを削除すると切れる。HTTP/2 への Upgrade(h2c、RFC 9113 が Upgrade に許さないh2)は中継しない(v0.4.18、#333):Upgradeからその名前を除き、ほかに残らなければ普通の HTTP/1.1 のリクエストとして転送する(RFC 9110 §7.8 のとおりサーバは Upgrade を無視してよい。HTTP/2 はクライアントとは rproxy が話す:TLS の ALPN か、平文の prior knowledge)。Upgrade: h2c, websocketならwebsocketだけを中継する。- 転送先に接続できなければ 502、
timeouts.connect(既定 5 秒)・timeouts.response(既定 60 秒)を過ぎると 504。event: "http.error"のログを出す。timeouts.responseは、リクエストの本文を送り終えてから応答ヘッダが届くまでの時間(アップロードにかかる時間は含めない。応答の本文にも上限はない)。
rproxy はクライアントとは HTTP/1.1・HTTP/2・HTTP/3 で、転送先とは HTTP/1.1(サービスの protocol で HTTP/2、#233)で話す。そのあいだで次のように扱う(tests/http_semantics.rs で HTTP/1.1・HTTP/2・HTTP/3 のクライアントから確かめている)。
| 項目 | 扱い |
|---|---|
Cookie |
HTTP/2・HTTP/3 で複数のフィールドに分けて届いたもの(Chrome はそうする)は "; " で 1 本にまとめる(RFC 9113 §8.2.3 / RFC 9114 §4.2.1)。まとめたものをミドルウェア(oidc・sticky・forward_auth)も読む |
Set-Cookie |
転送先の複数の Set-Cookie は 1 本ずつそのままクライアントへ(まとめない。compress・headers を通っても同じ)。Domain / Path / Secure などの属性、Location は書き換えない |
| そのほかのヘッダ | 同じ名前の複数のフィールドは順番どおり、値はバイトのまま(ASCII 以外も)渡す。Authorization は渡す |
| ホップごとのヘッダ | 両方向で取り除く:Connection とそこに書かれた名前、Keep-Alive、Proxy-Connection、Proxy-Authenticate、Proxy-Authorization、TE、Trailer、Transfer-Encoding、HTTP2-Settings、Upgrade(WebSocket などの Upgrade は付け直して中継する。h2c は中継しない)。クライアントが Connection に書いた名前は、リクエストの最初に、ミドルウェアより前に取り除く(v0.4.18、#333):なので Connection: X-Forwarded-User のように書いても、認証のミドルウェアが付けた身元のヘッダや headers で付けたヘッダ、rproxy の付けるヘッダは消えない。逆に Connection: Authorization・Connection: Cookie と書いたクライアントの Authorization・Cookie は、basic_auth・oidc・forward_auth にも届かない(ホップごとのヘッダなので。v0.4.17 までは転送先にだけ届かなかった)。Via と Forwarded(RFC 7239)は付けない(Traefik・nginx の既定と同じ。X-Forwarded-* を使う)。HTTP/2 の転送先(protocol)へは、クライアントの TE に trailers があれば te: trailers だけを渡す |
| トレーラー | 本文の後のトレーラーは、HTTP/2 の転送先と HTTP/2・HTTP/3 のクライアントのあいだで両方向にそのまま渡す(gRPC の grpc-status など。tests/http_semantics.rs)。HTTP/1.1 のクライアントへは、TE: trailers を送ってきたときだけ chunked の後に付ける |
Host |
HTTP/2・HTTP/3 の :authority、HTTP/1.1 の absolute-form の宛先(GET https://a.example/ HTTP/1.1)の authority を、Host フィールドより優先する(RFC 9112 §3.2.2)。転送先には origin-form(パスとクエリ)で送る。pass_host_header: false なら転送先の URL のホスト。ルートの照合(Host・HostRegexp・tls.misdirected)では、ポートを除き、小文字にし、末尾のドットを 1 つ落とす(v0.4.17、#320。Host: ADMIN.example. は Host(admin.example) に当たる)。転送先へ送る Host・X-Forwarded-Host はクライアントが送ったまま(正規化しない) |
| パス(v0.4.17、#320) | ルートの照合の前に、HTTP/1.1・HTTP/2・HTTP/3 とも Traefik v3 の sanitizePath と同じく正規化し、ミドルウェア・forward_auth・mirror・転送先・アクセスログにも正規化したパスを渡す(/%61dmin・//admin・/x/../admin・/admin/. が PathPrefix(/admin) の認証や ip_allow を素通りしないように)。決まり:(1) パーセントで符号化した unreserved の文字(RFC 3986 2.3 の A-Z a-z 0-9 - . _ ~)だけを戻し、ほかの符号化(%2F・%20・%E3 など)はそのまま、16 進は大文字にそろえる(%2f → %2F。%2F は区切りにならない)。(2) . の区切りを除き、.. は前の区切りを取り除く。ルートより上へは行かない(/../a は /a、/.. は /)。(3) 空の区切り(//)はまとめる。(4) 末尾の / は元のパスの末尾が / のときだけ残す(/admin/. は /admin、/admin/./ は /admin/。Go の path.Clean に末尾の / を戻す Traefik と同じ)。(5) % の後に 16 進が 2 つ続かないパスは 400(refused_by: bad_path)。クエリは変えない。*(OPTIONS *)と空のパスはそのまま。すでに正規化されたパスは写さない(割り当てなし)。http.raw_path: true のルールは送られたまま照合・転送する(Traefik の sanitizePath: false。パスでの認証・ip_allow と組み合わせると上の素通りが起きるので安全でない)。正規化のあとにも、転送先の解釈の違いで残る回り込みがある:Tomcat などの ..;/(; の後をパスの引数として読む)、\ や %5C を区切りとして読む転送先(Windows・IIS)、%2F を戻してから区切りとして読む転送先、二重に戻す転送先(%252e%252e → %2e%2e → ..)。そういう転送先の前では http.strict_path: true(v0.4.17、既定は無効)で、%2F・%5C・%3B・%25・;・\ を含むパスを 400(refused_by: strict_path)にできる。アクセスログの path は正規化した後のパスで、変わったときは送られたままのパスを raw_path に残す(検知のルールが ../ を見られるように) |
| 身元のヘッダ(v0.4.17、#320) | ルールの認証のミドルウェアが転送先に付けるヘッダの名前は、クライアントが送ったものを、どのルートでも(認証のないルートでも)リクエストの最初に、ミドルウェアより前に 1 回だけ消す(なので oidc の後の forward_auth には oidc の付けた値が届く):basic_auth の user_header、oidc の X-Forwarded-User・-Email・-Groups・-Sub、forward_auth の response_headers。response_headers: ["*"] と protocol: grpc の forward_auth(名前を認証サーバが決める)では、よく使われる名前も消す:oauth2-proxy の X-Auth-Request-User・-Email・-Groups・-Preferred-Username・-Access-Token と X-Forwarded-User・-Email・-Groups・-Sub・-Preferred-Username、Authelia の Remote-User・-Email・-Groups・-Name、Authentik の X-Authentik-Username・-Groups・-Email・-Name・-Uid・-Jwt。_ を - と読むと同じ名前になるもの(X-Forwarded_User。CGI・WSGI の転送先は両方を HTTP_X_FORWARDED_USER にする)も消す。消さないのは認証に使う Authorization・Proxy-Authorization・Cookie と Host だけ(forward_auth の request_headers に書いた名前でも、身元のヘッダの名前なら消す)。認証のミドルウェアのないルールでは何も消さない。前に認証のプロキシ(信頼する前段)がいて同じ名前の身元のヘッダを付けてくる構成でも、認証のミドルウェアのあるルールでは消える(rproxy の認証のミドルウェアの値だけを信じる) |
| ヘッダの大きさ | HTTP/2・HTTP/3 は 1 リクエストのヘッダの合計 64 KiB まで(hyper の既定の 16 KiB では、大きなクッキーのブラウザで足りない)。HTTP/1.1 は約 400 KB まで。超えると 431 |
| 本文 | 流しながら中継する(buffering がなければため込まない)。chunked、Expect: 100-continue、HEAD(Content-Length を保つ)、204 / 304 に対応 |
| タイムアウト | timeouts.response は本文を送り終えてから応答ヘッダまで。長いダウンロード・SSE・ロングポーリングの応答の本文は切らない |
| 接続の時間の上限(v0.4.17、#320) | global.http.timeouts(下の表)。長いアップロード・ダウンロードは長さでは切らず、黙っている時間だけで切る。gRPC と応答の始まったストリームの本文は切らない |
| 途中で切れた応答 | 転送先が応答の途中で切れたら(Content-Length に足りない、chunked の最後のチャンクがない、リセット)、クライアントにも完全な応答に見えないように切る:HTTP/1.1 は足りないまま接続を閉じる(chunked なら最後のチャンクを送らない)、HTTP/2 は RST_STREAM、HTTP/3 は RESET_STREAM。compress を通していても同じ(圧縮の終わりを付けない)。長さのない(接続を閉じて終わる)HTTP/1.0 型の応答は、転送先の側で途中かどうかが分からない |
| 途中で切れたリクエスト | クライアントが本文の途中で切れたら(HTTP/1.1 の切断、HTTP/2 の RST_STREAM、HTTP/3 のリセット)、転送先への接続も本文を終えずに切り、転送先に完全なリクエストとして渡さない |
global.http.timeouts(v0.4.17、#320):http のルールのクライアントの接続の時間の上限。設定ファイルの global か、ファイルに書いていなければ PATCH /config/global(v0.4.19、#342。環境変数はない)。どちらも新しい接続から効く(今の接続はそのまま)。値は 10s などの時間、0s は上限なし。省略した項目は既定。形の誤り(時間でない・知らない名前・http2_ping_timeout: 0s)は設定の誤り(起動・再読み込み・--check-config)。
| 項目 | 既定 | 意味 |
|---|---|---|
tls_handshake |
10s |
terminate の TLS のハンドシェイク(HTTP/3 は QUIC のハンドシェイク)。今までの決まった 10 秒と同じ |
first_byte |
10s |
接続(TLS の後)から最初の 1 バイトまで。何も送らない接続を閉じる |
request_headers |
30s |
接続から最初のリクエストのヘッダがそろうまで(HTTP/1.1・HTTP/2 の前置き・HTTP/2 のヘッダ。1 バイトずつ送り続ける slowloris も)。HTTP/1.1 では、2 つめからのリクエストのヘッダ(前の応答の後の keep-alive の待ちを含む)にも hyper の header_read_timeout として効く(今までの hyper の既定の 30 秒と同じ)。HTTP/3 はストリームごとのリクエストのヘッダ |
idle |
180s |
処理中のリクエストがない接続を閉じるまで(HTTP/2 は GOAWAY、HTTP/3 も GOAWAY の後に閉じる。PING を返すだけのクライアントも閉じる)。応答の本文を送っているあいだ(長いダウンロード・SSE)と WebSocket は処理中 |
request_body_idle |
60s |
リクエストの本文を読むあいだ、クライアントが何も送らない時間(全体の時間ではない。少しずつでも送り続ける長いアップロードは切らない)。応答のヘッダを送った後は効かない(そこからは両方向のやり取りでありうる:gRPC の双方向のストリーム、購読など)。content-type: application/grpc… のリクエストには最初から効かない(etcd の Watch・xDS のように最初のメッセージの後に長く黙るストリーム)。過ぎたら本文を誤りで終え(転送先へも完全なリクエストとして渡さない。tests/integrity.rs と同じ扱い)、HTTP/3 はストリームを止める |
http2_ping_interval |
60s |
HTTP/2 で、受け取るものがこの間ないと PING を送る(0s で送らない) |
http2_ping_timeout |
20s |
PING の答えを待つ時間。過ぎたら接続を閉じる(いなくなったクライアント) |
-
上限で閉じるときは GOAWAY を送り、処理中のリクエスト(GOAWAY の最後のストリームまでに始まったもの)は終わるまで待つ。処理中のものがなければ 1 秒で閉じる。上限で閉じた接続は
conn.closeのreasonがfirst_byte_timeout・request_headers_timeout・idle(HTTP/3 はidle)。HTTP/1.1 のヘッダの時間切れを hyper が先に見つけたときは、今までどおりconn.error(read header from client timeout)。 -
使えるかは
features.http_optionsのhttp_timeouts(パスの正規化はpath_normalize、raw_path、strict_path、身元のヘッダはidentity_headers)。 -
アクセスログ(
event: "http.access")はリクエストごとに 1 行:rule、route(一致しなければ(none))、service、backend、client、method、host、path(クエリは含めない)、query(クエリ。?なし、なければ空。v0.3.8 から。秘密になりやすい名前のパラメータ(token・code・state・password・secret・key・signature・auth・sessionなどを名前に含むもの)は値をREDACTEDに置き換える。GitLab のprivate_token、OIDC のcode/stateなどをログに残さないため)、protocol(HTTP/1.1/HTTP/2.0)、status、duration_ms(応答の本文を送り終えるまで)、bytes_in(Content-Length)、bytes_out(応答の本文)、user_agent、sni、tls_version、refused_by、middleware、user、auth_error、raw_path(v0.4.17、#320。正規化でパスが変わったときだけ、送られたままのパス)、cache(v0.4.19、#340。cacheのあるルートだけHIT・MISS・STALE・BYPASS)。出す先はglobal.access_log。refused_by・middleware(v0.3.20):リクエストを断ったミドルウェアの種類(ip_allow・basic_auth・forward_auth・oidc・crowdsec・rate_limit・in_flight・buffering・circuit_breaker・respondなど)と、設定での名前。ミドルウェアがエラー(4xx / 5xx)を返したときだけ入れ、ほかは空。リダイレクトやrespondの 2xx は入れない。user(v0.3.20):basic_authが通したユーザー名。auth_error(v0.3.20):basic_authが断った理由:no_credentials(Authorizationがない)、unknown_user、bad_password、unavailable(ユーザーのファイルを読めない)。断ったときのユーザー名とパスワードは出さない。
-
GET /metricsのrproxy_http_requests_total{protocol,listen,route,code}(codeは2xxなど)とrproxy_http_request_duration_seconds{protocol,listen,route}(ヒストグラム。境界は 5ms〜10s)、rproxy_http_limited_total{protocol,listen,route,middleware}(rate_limit/in_flightで断った数)、rproxy_http_blocked_total{protocol,listen,route,middleware}(crowdsecで断った数)、rproxy_http_cache_total{protocol,listen,route,middleware,result}・rproxy_http_cache_entries・rproxy_http_cache_bytes(cache、v0.4.19)、rproxy_http_cache_memory_bytes・rproxy_http_cache_memory_limit_bytes(すべてのcacheの合計とRPROXY_CACHE_MAX_MEMORY、v0.4.19、#349)。global.crowdsecがあればrproxy_crowdsec_decisions(判定で止めているアドレスと範囲の数)、rproxy_crowdsec_synced(LAPI から一度でも取得できたら 1)、rproxy_crowdsec_connected(最後の取得が成功していれば 1。v0.3.20)、rproxy_crowdsec_last_success_timestamp_seconds(最後に取得できた時刻。v0.3.20。一度も取れていなければ出さない)。ラベルにパスは入れない。 -
ミドルウェアはルートの
middlewaresに書いた順にリクエストへ働き、応答へは逆の順に働く(Traefik と同じ)。途中のミドルウェアが応答を返したら(リダイレクト・respond・拒否)、その先へは進まない。その応答にも、それまでに通ったミドルウェアの応答側(headersなど)が働く。 -
使えるミドルウェア(v0.3.1。
features.middlewares):redirect_scheme:schemeと違う方式で受けたリクエストを、同じホスト・パス・クエリのscheme://へリダイレクトする。portは既定のポート(80 / 443)なら省く。redirect_regex:http://host[:port]/path?query(受けた URL)がregexに一致すれば、replacement($1・${name}が使える)へリダイレクトする。一致しなければ次へ進む。- リダイレクトの状態コードは、
permanentなら 301、そうでなければ 302。GET / HEAD 以外は 308 / 307(メソッドと本文を保つ)。status(301・302・303・307・308、#226)を書けばそれを使う。 respond:status・body・content_type(既定text/plain; charset=utf-8)で応答する。serviceのないルート(ブロックやメンテナンス表示)に使う。ip_allow: 接続元の IP がsource_rangeになければ 403。headers:request/responseのset(空の値は削除)・remove。frame_deny(X-Frame-Options: DENY)、content_type_nosniff、referrer_policy、csp。hstsは HTTPS で受けたときだけ付ける。corsはOriginがallow_origins(*も可)にあるときAccess-Control-Allow-Origin(allow_credentialsならAccess-Control-Allow-Credentialsも)を付け、プリフライト(OPTIONSとAccess-Control-Request-Method)には rproxy が 204 で答える。Vary: Originは許したオリジンでもそうでなくても付ける(v0.4.19、#340。前は許したときだけ。キャッシュがオリジンごとに答えを分けるように)。rate_limit: トークンバケット。period(既定1s)あたり平均average件、一度にburst件まで(既定 1。Traefik と同じく、省くと 1 件ずつしか通さない)。超えたら 429 とRetry-After(秒)。sourceはip(既定。global.trusted_proxiesを反映したクライアントの IP。IPv6 は /64 ごと:v0.4.18、#333。1 台のホストがふつう /64 を持ち、リクエストごとにアドレスを替えられるため)かheader:<名前>(そのヘッダの値ごと。ヘッダがなければクライアントの IP。64 バイトより長い値は SHA-256 で覚える:v0.4.18、#333)。in_flight: クライアントの IP ごとに、同時に処理するリクエストをamount件まで(超えたら 429)。応答の本文を送り終えたとき(WebSocket なら接続が終わったとき)に空く。crowdsec: クライアントの IP(global.trusted_proxiesを反映)が LAPI の判定にあれば 403。appsec: trueなら、続けて AppSec にリクエストを問い合わせ、403 が返れば 403(X-Crowdsec-Appsec-Ip/-Uri/-Host/-Verb/-Api-Key/-User-Agent/-Http-Versionと元のヘッダを送る。本文はContent-Lengthが 1 MiB 以下のときだけ送り、それより大きいか長さのない本文はヘッダだけで問い合わせる)。on_error(既定allow)は、LAPI から一度も取得できていないときと AppSec に問い合わせできないとき(時間切れ 3 秒、200 / 403 以外の応答)に通すか 403 にするか。rate_limit/in_flightの数はルールのhttpを変えると最初からになる。覚えておく送信元は 1 つのミドルウェアで 10 万件まで(超えたら長く使っていない方から半分を忘れる)で、満杯に戻ったバケットは定期的に捨てる。strip_prefix: パスがprefixesのどれか(先に書いたもの優先)で始まれば取り除き、X-Forwarded-Prefixを付ける。add_prefix: パスの前に付ける。replace_path: パスを置き換え、元のパスをX-Replaced-Pathに入れる。replace_path_regex: 一致したときだけ置き換える(X-Replaced-Pathも)。クエリは保つ。
-
v0.3.2 で使えるようになったミドルウェア:
compress: クライアントのAccept-Encodingに合わせて応答をbr・zstd・gzipで圧縮する。encodings(既定[br, zstd, gzip])は使う形式と優先順(q値が同じときの順)。min_size(既定 1024 バイト)よりContent-Lengthが小さい応答、すでにContent-Encodingのある応答、画像(SVG を除く)・動画・音声・font/woff*・圧縮済みの形式(zip・gzip・zstd・pdf など)・text/event-stream・gRPC、Cache-Control: no-transform、HEAD・204・206・304 はそのまま。圧縮したらContent-Lengthを外し、Vary: Accept-Encodingを足し、強いETagを弱いW/にする。本文は流れてきた分ずつ圧縮して送る(長く続く応答も止めない)。buffering: リクエストの本文を先に読み切る。max_request_body(バイト)を超えたら 413(Content-Lengthで分かればすぐに、分からなければ読みながら)で、転送先には送らない。読み切った本文はretryで送り直せる。retry: 転送先に接続できない・応答がない(502 / 504 になるもの)ときに、次の転送先へ送り直す。attemptsは最初の 1 回を含む回数、initial_interval(既定100ms)は最初の待ち時間で、回ごとに倍になる。送り直すのは冪等なメソッド(GET・HEAD・OPTIONS・PUT・DELETE・TRACE)で、本文がないか、bufferingで読み切った本文のときだけ(WebSocket などの Upgrade は送り直さない)。転送先が返した 5xx は、status(#231)に書いたものだけ送り直す。circuit_breaker: 応答のうち 5xx(502 / 504 を含む)の割合がwindowの中でfailure_percent以上になったら(10 件以上あるときに判定)、recoveryのあいだ転送先へ送らずに 503 を返す。recoveryを過ぎたら 1 件だけ通し、成功すれば元に戻し、失敗すればまたrecoveryだけ止める。状態はevent: "http.breaker"のログ。数はルールのhttpを変えると最初からになる。errors: 応答の状態コードがstatus("500-599"・"404"のような範囲か値)に入れば、serviceのpath({status}は状態コードに置き換える)を GET し、その本文とヘッダで返す。状態コードは元のまま。ページを取れなければ元の応答を返す。メンテナンス表示には、respondのルートをpriorityを上げて一時的に足すか、errorsのページを使う。
-
認証のミドルウェア(v0.3.2、#59)。HTTP/1.1・HTTP/2・HTTP/3 のどのリクエストにも働く:
basic_auth:users_file(htpasswd 形式。bcrypt$2y$(htpasswd -B)・$apr1$(htpasswdの既定)・{SHA}を読む。ほかの形式や平文の行があればファイル全体を誤りとして扱う)の利用者だけを通す。通らなければ 401 とWWW-Authenticate: Basic realm="<realm>"(realmの既定rproxy)。- 通ったリクエストの
Authorizationは転送先に渡さない(keep_authorization: trueで渡す。Traefik の既定のremoveHeader: falseと同じにするならこちら)。user_header(例X-Forwarded-User)を書くと、利用者の名前をそのヘッダで渡す(クライアントが送った同名のヘッダは取り除く)。 - bcrypt の照合は別のスレッドで行い、通った組み合わせは覚えておく(ファイルが変われば忘れる)。ファイルにないユーザーでも、ファイルで最も重いハッシュ(コストの最も大きい bcrypt、なければ
$apr1$、{SHA}の順。ファイルを読むたびに 1 回選ぶ)と照合してから断る(v0.4.18、#333)。応答の時間でどのユーザー名があるかが分からないのは、ファイルのハッシュがすべて同じ形式・同じコストのときだけ(形式やコストが混ざると、軽いハッシュのユーザーは速く答える)。すべて同じコストの bcrypt(htpasswd -B -C <コスト>)にそろえるのを勧める。
- 通ったリクエストの
forward_auth: リクエストごとにaddressへGETを送り、2xx なら通す(Traefik の forwardAuth と同じ)。- 認証サーバへは、クライアントのヘッダ(
request_headersを書けばその名前だけ。ホップごとのヘッダとHostは除く)と、X-Forwarded-Method・X-Forwarded-Proto・X-Forwarded-Host・X-Forwarded-Uri(パスとクエリ)・X-Forwarded-Forを送る。X-Real-IP(rproxy の見たクライアントの IP)も送る(v0.4.18、#333)。trust_forward_header: false(既定)ならクライアントが送ったX-Forwarded-*・X-Real-IP・Forwarded(RFC 7239)は捨てて付け直し(Forwardedは付けない)、true なら受けた値を保つ(X-Forwarded-Forは後ろに接続元を足す。信頼しない接続元のForwardedは転送先には渡さないが、認証サーバには送る)。 - 2xx なら、認証サーバの応答のうち
response_headersに書いたヘッダで、転送先へのリクエストの同名のヘッダを置き換える。2xx 以外(401・302 のサインイン画面へのリダイレクトなど)は、その応答をそのままクライアントに返す。 - 認証サーバに接続できなければ 502、
timeout(既定10s)以内に答えがなければ 504(event: "http.error")。認証サーバへの接続は使い回す。 - v0.4.3 から(Gateway API の
ExternalAuthのため。どれも省略でき、省略すれば上のとおり。features.forward_authに使える項目の名前):service:addressの代わりに、ルールのhttp.servicesの名前(転送先・balance・health_check・tlsをそのまま使う)。問い合わせるパスはpath(既定/)。timeoutは 1 回の問い合わせの上限(サービスのtimeouts.responseより先に効く)。client_request: true:Envoy の HTTP の ext_authz の形。GETではなくクライアントのメソッドで、パスはaddressのパス(serviceならpath)の後ろにクライアントのパスとクエリをつないだもの(/auth+/app/x?q=1→/auth/app/x?q=1)、Hostはクライアントのもの、Content-Lengthは送る本文の長さ(本文を送らなければ 0)。X-Forwarded-*はそのまま付ける。request_headersを書かなければクライアントのヘッダをすべて送る(Gateway API の「書かなければAuthorizationなどだけ」はコントローラが名前を書く)。allow_status:通す状態コード("200"・"200-204"のような値か範囲の一覧。既定は 2xx)。ほかの状態コードの応答はそのままクライアントに返す。response_headers: ["*"]:通したとき、応答のヘッダをすべて転送先へのリクエストに写す(同名のヘッダは置き換え)。応答そのものを表すもの(ホップごとのヘッダ、Host・Content-Length・Content-Type・Content-Encoding・Transfer-Encoding・Date)は写さない。"*"はほかの名前と一緒に書けない。forward_body: {max_size: <バイト>}:クライアントの本文をmax_sizeまで読み切って(bufferingと同じ)認証サーバにも送る。長いものは 413、本文が途中で切れたら 400。読み切った本文は転送先にも送る(retryで送り直せる)。protocol: grpc:Envoy の ext_authz v3 の gRPC(envoy.service.auth.v3.Authorization/Check)で問い合わせる。addressはhttp://(HTTP/2 の prior knowledge、h2c)だけ。TLS ならservice(protocol: h2・tls)を使う(serviceはprotocolがh2かh2cのもの)。CheckRequestには接続元・接続先のアドレス、時刻、メソッド、ヘッダ(request_headersを書けばその名前だけ。:authority・:method・:path・:schemeはいつも。同じ名前のヘッダは,でつなぐ)、パス(クエリを含む)、ホスト、スキーム、プロトコル、本文の大きさ、forward_bodyの本文を入れる。X-Forwarded-*・X-Real-IP・Forwardedは HTTP のforward_authと同じ(X-Forwarded-Method・-Proto・-Host(ルートの照合と同じ正規化したホスト)・-Uri・-Forと rproxy の見たクライアントの IP のx-real-ipを付け、クライアントが送ったものはtrust_forward_header: trueのときだけ、request_headersを書いたならその名前のものだけ渡す。ただしX-Forwarded-Forは rproxy がいつも付けるので、true ならrequest_headersになくても受けた値の後ろに接続元を足して送る(HTTP のforward_authも同じ)。v0.4.18、#333。v0.4.17 まではクライアントのX-Forwarded-*をそのまま渡していた)。content-lengthは Envoy と同じくヘッダに含める。HTTP のforward_authと違い、Hostはヘッダではなく:authorityで渡す。CheckResponseのstatus.codeが 0 なら通し、ok_response.headers(appendの指定どおり。指定がなければ置き換え)を転送先へのリクエストに、headers_to_removeを外し、response_headers_to_addをクライアントへの応答に付ける。0 でなければdenied_responseの状態コード(なければ 403)・ヘッダ・本文で答える。gRPC の誤り(grpc-statusが 0 でない、応答が読めない)は 403、つながらなければ 502、時間切れは 504。client_request・allow_status・path・response_headersは使えない(認証サーバの答えが決める)。- 転送先ごとのミドルウェア(
servers[].middlewares)にも書ける(その転送先へ送るリクエストだけを問い合わせる。Gateway API の backendRef のExternalAuth)。
- 認証サーバへは、クライアントのヘッダ(
oidc: OpenID Connect(Keycloak など)のサインイン(認可コードフロー + PKCE)。oauth2-proxy なしで保護できる。issuer(例https://sso.example.com/realms/main)の/.well-known/openid-configurationを最初に使うときに読む(失敗したら 10 秒おいて試し直す)。HTTPS の証明書はca_file(なければ Mozilla のルート)で検証する。- プロバイダには、クライアント
client_id(confidential。シークレットはclient_secret_fileの 1 行目、client_secret_basic で送る)と、リダイレクト URIhttps://<ホスト><callback_path>(callback_pathの既定/_rproxy/oidc/callback)を登録する。scopesはopenidに足すスコープ(例[profile, email])。 - サインインしていない GET / HEAD はプロバイダのサインインへ 302(元の URL はサインイン後に戻る。戻り先は同じサイトのパスだけ)。それ以外のメソッドは 401。
callback_pathとlogout_path(既定/_rproxy/oidc/logout)はどのルートに一致したかにかかわらず、このoidcミドルウェアが答える(ルートのmatchに含めなくてよい)。ログアウトはクッキーを消して、プロバイダのend_session_endpointへclient_idとpost_logout_redirect_uri=<scheme>://<ホスト>/を付けて送る(プロバイダ側に許可する URI として登録する)。- ログアウトは
POST(v0.4.18、#333。v0.4.17 まではGETでログアウトしていた):ほかのサイトが画像やリンクでログアウトさせられないように、GET/HEADにはlogout_pathへPOSTするボタン 1 つのページ(Content-Security-Policy: default-src 'none'; frame-ancestors 'none'、X-Frame-Options: DENY)を返す。POSTは、Originがそのサイト(<scheme>://<ホスト>)か、OriginがなくSec-Fetch-Siteがsame-originかnoneのときだけログアウトする(ほかのサイトからは 403。どちらも送らないブラウザ以外のクライアントは通す)。ほかのメソッドは 405。アプリのログアウトのリンクは、logout_pathへPOSTするフォームにする(リンクのままでも、確かめのページを 1 つ挟んでログアウトできる)。 - ID トークンは JWKS(RS256/384/512・PS256/384/512・ES256/384。知らない
kidなら 1 分に 1 回まで取り直す)で署名を確かめ、iss・aud(client_idを含む)・exp・nonceを確かめる。 - セッションはクッキー
cookie_name(既定_rproxy_oidc。HttpOnly・SameSite=Lax、HTTPS ならSecure)に AES-256-GCM で暗号化して持つ(中身は利用者の名前・メール・グループ・期限・リフレッシュトークン)。鍵はcookie_secret_fileの 1 行目(16 文字以上。openssl rand -base64 32など)から作る。変えるとすべてのセッションが無効になる。 - ID トークンの期限の 30 秒前を過ぎたら、リフレッシュトークンで取り直してクッキーを付け直す。取り直せなければサインインし直す。
- 転送先へは
X-Forwarded-User(preferred_username、なければemail、なければsub)・X-Forwarded-Sub・X-Forwarded-Email・X-Forwarded-Groups(groups_claim(既定groups、ドット区切りでrealm_access.rolesなども)の値をカンマ区切り)を付ける。クライアントが送った同名のヘッダと、rproxy のクッキーは取り除く。Keycloak ではクライアントに「Group Membership」のマッパーを足すとgroupsが入る。 - サインイン・ログアウト・失敗は
event: "oidc.login"/"oidc.error"/"oidc.refresh"。
- 秘密のファイル(
users_file・client_secret_file・cookie_secret_file)がない・中身が誤っていればinvalid(設定ファイルなら起動しない)。読めない(権限)なら起動は続け、そのミドルウェアを通るリクエストに 503 を返す。ファイルは変わったら(数秒以内に)読み直し、SIGHUP でも読み直す。読み直しに失敗したら今の中身を使い続ける(reload.secretの警告)。所有者とモードは docs/PERMISSIONS.md。
-
source_ipはproxyかtransparent(転送先への接続の送信元をクライアントにする)。proxy_v1/proxy_v2は使えない(invalid。クライアントの IP はX-Forwarded-Forで渡す)。ただし passthrough の route には route ごとのsource_ip(proxy_v1/proxy_v2)を書ける(v0.4.13、#297)。passthrough でないtls.routesは使えない(tls_config。Host(...)で振り分ける)。 -
接続の統計(
stats)はクライアントとの接続単位で、rx_bytesはクライアントから、tx_bytesはクライアントへのバイト数。 -
DB の
options列の JSON にもhttpを保存できる({"tls", "starttls", "starttls_required", "allow_from", "http", "crowdsec", "targets", "balance", "health_check"}。crowdsecは v0.3.2、targets/balance/health_checkは v0.3.3 から)。targetsが空でなければdist_addr/dist_portは読まない(UI は''/0を入れる)。
v0.4.0 で形を決めて中身まで入れた設定(docs/DESIGN-v0.4.md。設計との違いはその「15. 実装での設計との違い」)。v0.4.0 では下の表の項目がすべて動き、GET /capabilities の features の v0.4 の項目はすべて true(performance はすべての項目の名前、middlewares に geoip、services に outlier_detection)。形が不正なら 400 invalid。どれも省略でき、省略したときの動きは v0.3 と同じ。制御 API のクライアント証明書(--tls-client-auth・--tls-client-ca、トークンの client_cert)の組み合わせの誤りは、守りが弱くならないように起動を止める設定のエラー。
| 項目 | 場所 | 形 | features |
|---|---|---|---|
| ラベル(#28・#166) | ルールの labels |
{キー: 値}。キーは英数字と ._/-(63 文字まで)、値は 253 文字まで、16 個まで。動きには使わない(ログ・/metrics) |
labels |
| L4 の制限(#165) | ルールの limits |
max_connections、per_source(prefix_v4・prefix_v6・max_connections・new_connections・packets(udp だけ)・max_sources)。速さは {average, period, burst}(L7 の rate_limit と同じ) |
limits |
| 帯域(#166) | ルールの bandwidth |
upload・download("10Mbps"、8kbps〜100Gbps)、burst("1MiB")、per_source(upload・download・prefix_v4・prefix_v6・max_sources)。TCP は待たせ、UDP は捨てる |
bandwidth |
| GeoIP(#168。下の「GeoIP」) | ルールの geoip、ミドルウェアの geoip、global.geoip |
allow_countries・deny_countries(ISO 3166-1 alpha-2)、allow_asns・deny_asns、unknown(allow / deny)。global.geoip は country_db・asn_db(mmdb)・check_interval・log_country。国のリストは country_db、ASN のリストは asn_db が要る |
geoip、middlewares の geoip |
| 受け身のヘルスチェック(#170。下の「受け身のヘルスチェック」) | ルールの outlier_detection(L4。http のルールでは invalid)、http.services.<名前>.outlier_detection |
L4:consecutive_failures・short_lived・ejection_time・max_ejection_time・max_ejected_percent。L7:consecutive_5xx・consecutive_gateway_failures・failure_percent・min_requests・window・ejection_time・max_ejection_time・max_ejected_percent |
outlier_detection、services の outlier_detection |
| performance(#194・#184) | global.performance |
workers、udp_shards(1〜64 か auto)、cpu_affinity(none / auto / "0-3,6")、busy_poll_usecs、splice(enabled・after・full_reads・pipe_size)、xdp(#260、カーネルでの転送。下の「performance」)、dpdk(#261、DPDK のデータプレーン。--features dpdk のビルドだけ)。項目ごとに、設定ファイル → 環境変数 RPROXY_WORKERS・RPROXY_UDP_SHARDS(数か auto)・RPROXY_CPU_AFFINITY・RPROXY_BUSY_POLL_USECS・RPROXY_SPLICE* → 既定の順。再起動まで効かない。下の「performance」 |
performance(効く項目の名前。すべて) |
| ルールの組(#28) | GET /rulesets、GET / PUT / DELETE /rulesets/{name} |
下の「ルールの組・状態・readiness」 | rulesets |
| 状態(#28) | ルールの表示の conditions |
[{"type","status","reason","message","last_transition"}]。type は Accepted・Programmed・ResolvedRefs・BackendsHealthy(下の「ルールの組・状態・readiness」) |
conditions |
| readiness(#28) | GET /readyz |
認証なし。200 {"ready": true} / 503 {"ready": false, "reason": "starting" | "draining"} |
readyz |
| 変更前の差分(#169) | ?dry_run=true(POST /rules・PATCH・DELETE・PUT /rulesets/{name}・POST /config/reload)、POST /config/plan、--check-config --diff |
応答は {"dry_run","action","change","rule","before","after","diff":[{"path","before","after"}],"warnings"}。change は none・in_place・recreate |
dry_run |
| API で作ったルールの保存(#144) | トークンの persist: true、テーブル rproxy_rules、--node-name |
表示に origin: "api"・persisted・created_by・created_at |
persistence |
| 制御 API の守り(#167) | --tls-client-ca・--tls-client-auth、トークンの client_cert、--token-warn-days、--api-lockout-failures・--api-lockout-window・--api-lockout-duration |
client_cert はトークンの sha256 の代わり、両方あれば両方が要る。期限が近いトークンは token.expiring、続けて失敗した送信元は 429 locked_out(既定で有効)。上の「制御 API の守り」 |
client_cert_auth、token_expiry、api_lockout |
| 再起動なしの更新・自動更新(#174) | SIGUSR2・POST /admin/upgrade、--handoff-*、RPROXY_UPDATE*、GET / POST /admin/update、rproxy-api launch |
同じマイナーの中で待ち受けのソケットを新しいプロセスに渡す。自動更新は署名(minisign)を確かめてから。docs/UPGRADE.md | handoff、self_update |
limits・bandwidth・geoip・outlier_detection・labelsはPATCHで付けると丸ごと置き換える({}で外す、省けば今のまま)。DB のoptionsでも同じ形で読む。- ルールの
statsにlimited(#165)とcounters_since(#166、数え始めの Unix 秒。引き継ぎでは変わらない)が出る(動いているルール)。数はメモリにだけあり、再起動(異常終了の後も)では 0 から数え直す(counters_sinceも起動の時刻になる)。stats.targets[]にejected_until(外していなければ null)・ejections(#170)が出る。 - トークンの入れ替え:新しいトークンを足して SIGHUP、クライアントを切り替えてから古いトークンを消して SIGHUP(
expiresを付けておくとtoken.expiringで知らせる)。詳しくは上の「制御 API の守り」。
global:
performance:
workers: 8 # tokio のワーカースレッドの数(既定:使える CPU の数。cpu_affinity の一覧があればその数)
udp_shards: auto # UDP のポートごとの SO_REUSEPORT のソケットの数。1〜64 か auto(= workers)。既定 1
cpu_affinity: none # none・auto(ワーカー i を i 番目の CPU に固定)・"0-3,6"(ワーカーを一覧の CPU に順に固定し、ほかのスレッドは一覧の中で動かす)
busy_poll_usecs: 0 # 待ち受けのソケット(受け付けた接続も引き継ぐ)の SO_BUSY_POLL(マイクロ秒)。0〜1000、0 で使わない
splice: {enabled: true, after: 0, full_reads: 4, pipe_size: 0} # L4 の平文の TCP の splice(2)(docs/PERFORMANCE.md)
xdp: # UDP の L4 を XDP / AF_XDP で(#260。オプトイン、起動時の試験に通ったときだけ使う)
mode: off # off(既定)・af_xdp(受信・送信を XSK で)・native(XDP のプログラムだけで転送)
interfaces: [eth0] # 使う NIC(省けばルールの待ち受けアドレスから決める)
attach: auto # auto・driver(ネイティブ)・generic(skb)
zero_copy: auto # auto・true・false
queues: auto # 使う RX キューの数(1〜256 か auto)
ring_size: 2048 # XSK の ring の大きさ(64〜16384 の 2 のべき)
frame_size: 4096 # UMEM の 1 フレーム(2048 か 4096)
batch: 64 # 1 回にまとめるパケットの数(1〜1024、ring_size 以下)
busy_poll: false # SO_PREFER_BUSY_POLL
fallback: true
dpdk: # L4 の UDP を DPDK で(#261、実験。--features dpdk のビルドだけ。docs/PERFORMANCE.md)
enabled: false # 既定 off
eal_args: [] # EAL にそのまま渡す(例 ["--in-memory", "--file-prefix=rproxy"])。-l・-a・--vdev は下の項目から作るので書かない
lcores: "2-5" # 転送に専有する CPU(enabled のとき必須。cpu_affinity と重ねない)
ports: # pci か vdev のどちらか
- pci: "0000:3b:00.0" # vfio-pci に付け替えた NIC(vdev なら "net_tap0,iface=dtap0"・"net_af_packet0,iface=eth1" など)
rx_queues: 4 # 1〜64、既定 1(RSS で lcore に配る)
tx_queues: 4 # 1〜64、既定 1。lcore の数以上
rx_desc: 1024 # 64〜16384、既定 1024
tx_desc: 1024
addresses: ["198.51.100.2/24"] # ポートの IPv4 のアドレス(enabled のとき必須)。UDP のルールはここで待ち受ける
gateway: 198.51.100.1 # ほかのネットワークへの次の宛先(ポートのネットワークの中)
mempool: { mbufs: 65535, cache: 256 } # mbufs 1023〜16777216、cache 512 まで(mbufs / 1.5 まで)
hugepages: { size: 2MB } # 2MB か 1GB。起動の前に空きを確かめるだけ(用意はホストで)
burst: 32 # 1〜512
fallback: true # 使えないとき今の処理に戻る(false なら起動を止める)- 項目ごとに、設定ファイル → 環境変数・引数(
RPROXY_WORKERS・RPROXY_UDP_SHARDS・RPROXY_CPU_AFFINITY・RPROXY_BUSY_POLL_USECS・RPROXY_SPLICE・RPROXY_SPLICE_AFTER・RPROXY_SPLICE_FULL_READS・RPROXY_SPLICE_PIPE_SIZE)→ 既定の順で決める。spliceの中も項目ごと。どれも起動のときだけ決まり、ファイルを変えたらrestart_neededに出る。 - 起動時に
event = "performance"の行に、効いている値と出どころ(sources:workers=fileなど)を出す。ワーカーのスレッドの名前はrproxy-wrk-<番号>。 cpu_affinityの一覧の CPU が存在しない・このプロセスが使えない(cgroup・taskset)ものは除いてdegraded(part: global.performance.cpu_affinity)。一覧がworkersより少ないのは設定の誤り。busy_poll_usecsをnet.core.busy_readより大きくするにはCAP_NET_ADMINが要る。設定できなければdegradedを 1 回出して普通に待つ。xdp(#260):環境変数RPROXY_XDP_MODE・RPROXY_XDP_INTERFACES(カンマ区切り)・RPROXY_XDP_ATTACH・RPROXY_XDP_ZERO_COPY・RPROXY_XDP_QUEUES・RPROXY_XDP_RING_SIZE・RPROXY_XDP_FRAME_SIZE・RPROXY_XDP_BATCH・RPROXY_XDP_BUSY_POLL・RPROXY_XDP_FALLBACKでも指定できる(項目ごとに設定ファイルが先)。値の誤りは設定の誤りで起動を止める。- 起動時に、求められた速い道ごとにテストデータを実際に流して確かめてから使う(カーネルの版では決めない)。結果は機能ごとに
event = "performance.probe"の行(feature・requested・active・mode・reason・tests)とGET /capabilitiesのperformance({"xdp": {"requested", "active", "mode", "reason"}})に出る。使えなければdegraded(part: global.performance.xdp.mode、reason)を出して今の処理で動く。fallback: falseなら起動を止める。何も求めなければ何も試さない。 - 動いている間は数えたり見張ったりしない。速い道の操作が失敗して接続を今の処理に戻したときだけログに出す。
rproxy-api --check-kernel(--config/RPROXY_CONFIGで何を求めているかを読む):すべての速い道を同じテストで確かめて表で出す(--check-kernel-format jsonで JSON)。求めたものがすべて使えれば 0、そうでなければ 1 で終わる(導入前の確認・Kubernetes の initContainer)。dpdk(#261):環境変数はなし(設定ファイルだけ)。features.performanceにdpdkがあるのは--features dpdkのビルドだけで、ほかのビルドでは無視してdegraded。起動時の試験(ヒュージページ・EAL・mempool・rproxy のループバックのポートnet_ring_rpchkにテストのデータグラムを通して比べる・ポートの起動)も同じ枠組みで、performance.probe(feature: dpdk)・degraded(part: global.performance.dpdk)・GET /capabilitiesのperformance.dpdk(requestedはon/off、modeに DPDK の版・lcore・ポートのリンク)・--check-kernelのdpdk.enabled=trueの行に出る。使えるときは、待ち受けのアドレスがports[].addressesの UDP のルールを DPDK の lcore が転送する(ログのconn.openなどにpath: dpdk、ルールごとにdpdk.rule)。そのアドレスではsource_ipのproxy_v1/proxy_v2/transparent、tls(DTLS・sni)、httpは使えない(bind_failed)。IPv4 だけ。要るもの:root(か vfio と hugetlbfs への権限)、ヒュージページ、NIC の vfio-pci への付け替え(docs/PERFORMANCE.md の手順)。- 要る権限は
CAP_BPF(古いカーネルはCAP_SYS_ADMIN)とCAP_NET_ADMIN(AF_XDP はCAP_NET_RAWも)。docs/PERMISSIONS.md。この版では速い道で転送する処理は入っておらず、求めても理由つきで今の処理に戻る。AF_XDP は cargo の機能kernel-offloadのビルドで起動時の試験(--check-kernel)まで入っているが、試験に通っても転送には使わない(active: false。NIC・キューの調整と実機での測定が要るので保留、docs/PERFORMANCE.md)。 - 平文の L4 TCP を eBPF の sockmap でカーネルの中で中継する案(
global.performance.ebpf)は試して採らなかった(並列・両方向の大きな転送でカーネルのバックログが止まる。docs/PERFORMANCE.md)。設定にはない(書けば誤り)。
- 起動時に、求められた速い道ごとにテストデータを実際に流して確かめてから使う(カーネルの版では決めない)。結果は機能ごとに
POST /rules?dry_run=true・PATCH /rules/...?dry_run=true・DELETE /rules/...?dry_run=true:実際の操作と同じ検証(同じ400/403/404/409)をして、200で差分を返す。何も変えない:待ち受けを開かず、名前解決もしない(証明書・秘密のファイルは読んで確かめる)。スコープ・allow_listen_ports・acme:writeも実際の操作と同じ。auditのログには残さない。{"dry_run": true, "action": "update", "change": "in_place", "rule": "tcp/0.0.0.0:443", "before": {<今のルールの表示>}, "after": {<POST /rules の本文の形>}, "diff": [{"path": "remote_port", "before": 80, "after": 8080}], "warnings": []}action:create・update・delete・none(変わらない)。change:updateの効き方だけを表す。in_place(接続を切らずに変わる。PATCH の変更はすべてこれで、PATCH で変えられないものは実際の操作と同じく400 unsupported)・recreate(待ち受けを作り直す。今の接続は切れる:failedのルールを動かすとき、設定ファイル・組の変更で PATCH では変えられないもの)。create・delete・noneは常にnone(PUT /rulesetsのresultsと同じ。削除で切れる接続の数はwarnings)。beforeはルールの表示(GET /rules/...と同じ)、afterはルールの形(POST /rulesの本文の形。状態・統計・originはない)。diffは形どうしを比べた、変わった項目の JSON のパス(.でつなぐ。配列は丸ごと)と前後の値。作成は{}から、削除は{}へ。warnings:削除で切れる接続の数など。
POST /config/reload?dry_run=true:設定ファイルを読んで、反映したら何が変わるかを返す(反映しない)。POST /config/plan:本文の設定(設定ファイルと同じ形の JSON)を同じように比べる(ファイルは読まない。設定ファイルを使っていなければ、固定ルールはまだないものとして比べる)。どちらもスコープ(admin)と Unix ソケットの決まりはPOST /config/reloadと同じ。{"dry_run": true, "added": 1, "removed": 0, "changed": 1, "unchanged": 3, "failed": 0, "restart_needed": ["global.trusted_proxies"], "changes": [{"rule": "tcp/0.0.0.0:443", "action": "update", "change": "in_place", "diff": [...]}], "warnings": [{"rule": "...", "message": "..."}]}- 数は
POST /config/reloadの答えと同じ意味(failedは作られるがfailedになるもの:この版で動かない設定、証明書を読めない、API のルールが同じアドレスを持っている)。changesは変わるルールだけ(変わらないものはunchangedの数だけ)。restart_neededは起動したときのglobalとの違い。 - 誤りがあれば、反映と同じく
400 {"code":"invalid","error","errors":[...],"warnings":[...]}。
- 数は
rproxy-api --check-config [PATH] --diff [--diff-api unix:/path|URL] [--diff-token-file FILE]:検証に通ったら、動いている rproxy にPOST /config/planで問い合わせて差分を出す。問い合わせ先の既定はRPROXY_API_SOCKET、なければhttp://<RPROXY_API_ADDR の先頭(0.0.0.0 / :: なら loopback)>:<RPROXY_API_PORT>(RPROXY_TLS_CERTがあれば https で、その証明書を信頼する)。トークンは平文の 1 行のファイル(adminのスコープが要る)。textの出力は検証の結果の後に 1 行に 1 つの変更(+作成・~変更(変わった項目)・-削除、!再起動が要るglobal)とplan: N to add, ...。jsonはReportにplan(上の答え)を足したもの。- 終了コード:検証の誤り・問い合わせの失敗(つながらない、断られた)は 1、差分があってもなくても成功なら 0。
- 組(
PUT /rulesets/{name}?dry_run=true、#28)も同じ差分の作り方(config::planのin_place・rule_diff)で、resultsのaction・changeの意味も同じ(diffはupdateだけ)。
- トークンファイルで
persist: trueを付けたトークン(YAML の書き方だけ。既定は false)で作ったルールはorigin: "api"になり、rproxy のテーブルrproxy_rulesに保存する。UI のテーブル(forward_rules)には書かない。UI 用のトークンには付けない(UI は自分の DB に保存するので二重になる)。 - 書くのは作成・変更・削除の応答の前。
apiのルールは、どのトークンで変えても・消しても行を書き直す・消す(行がルールと食い違わないように)。persist: trueのトークンでも、dynamicのルール(UI・保存しないトークンのもの)を変えたときは保存しない。 - 表示:
origin: "api"のルールにpersisted(行が最新か)・created_by(作ったトークンの名前)・created_at(Unix 秒)。書けなかったとき(DB に届かない、テーブルがない)はルールを動かしたままpersisted: false、ログにevent = "degraded"(part: "db")。RPROXY_DATABASE_URLがなければ保存しない(persisted: false)。保存したらrule.persist(action: save/delete、token)。 - 起動時は UI の
forward_rulesを復元してから、rproxy_rulesの自分のnode(--node-name/RPROXY_NODE_NAME、既定はホスト名)の行をorigin: "api"で復元する。同じキーが両方にあれば UI の行を使い、restore.conflict(warn)を出す。テーブルが読めなければdegraded(part: "db")を出して、UI のルールだけで起動する。spec_versionがこの版より新しい行はrestore.skipで読み飛ばす。 - 書き込みの道(v0.4.14):DB への仕事(ルール・組の書き込み、起動時と定期の読み込み、ノードのロック)はすべて 1 本の列に並び、1 つの係が 1 本の接続で順に実行する。API の変更は列に入れて確定(COMMIT)を待ってから答える(
persisted: trueは DB で確定したこと)。列がいっぱい(256 件)・DB に届かないときはすぐにpersisted: falseで答え、待ち続けない。- 書き込みは 1 件ずつのトランザクションで、接続のセッションは 1 つの文を 3 秒で終わらせる(
max_statement_time・innodb_lock_wait_timeout・lock_wait_timeout)。止まったプロセスが送っていた書き込みも、6 秒(文と COMMIT)のうちに確定か取り消しで終わる(確定する前に接続が切れたものは MariaDB が取り消す)。 - MySQL(MariaDB でない):
max_statement_timeがなく(max_execution_timeは SELECT だけ)、書き込みの文そのものには時間の上限がない。行のロック(innodb_lock_wait_timeout)とテーブルのメタデータのロック(lock_wait_timeout。既定は 1 年)を待つのは 3 秒までにするが、それ以外で長くかかる文は止めない。接続のときにdegraded(part: "db")を出す。kill の直後の起動が、まだ動いている書き込みより先に行を読むことがありうる(MariaDB を勧める)。 - 接続はノードごとのロック(
GET_LOCK。名前はrproxy-node-とnodeの SHA-256 の先頭 32 桁)を持つ。MariaDB はその接続の文を終えるまでロックを放さないので、起動時はロックを最大 8 秒待ってから行を読む(kill -9・OOM killer の直後の起動で、確定の途中だった書き込みより先に読まない)。セッションのwait_timeoutは 30 秒で(空いている接続は 10 秒ごとに ping。ping はロックを持っているかも確かめる文で、接続が切れて開き直したときはそこでロックを取り直す)、ホストごと止まって閉じられなかった接続も MariaDB が 30 秒のうちに切る。8 秒で取れないとき:持っている接続が 15 秒より長く何もしていなければ(生きている rproxy は 10 秒ごとに文を送る)残った接続とみなし、読める行で復元してrestore.wait(warn、持っている接続のconnection・host・idle_secs)を出し、5 秒ごとにロックを試し、取れたら行と照らし直す。そうでなければ合わせて 40 秒まで待ち、それでも持っていて、その間に文を送っていれば(PROCESSLISTのTIMEが 0 に戻る、文が始まる)同じ名前の生きているプロセスとみなして起動をやめる(下)。1 回目は文の途中、2 回目は 15 秒より短く何もしていない(文が間に終わった)ときは決められないので(MySQL では、落ちたホストの長い書き込みが終わったときもこう見える)、断らずにもう 20 秒待ってもう 1 回見る。それでも決められなければ断らずにrestore.waitで起動する(v0.4.15、#307)。ロックを取ったプロセスは、取った時から(復元の前から)10 秒ごとに ping を送るので、復元に時間がかかっても生きて見える(v0.4.15、#307)。 - 定期の照合(
RPROXY_PERSIST_CHECK_SECS、既定 60 秒、0 でロックを持っていないときだけ):列が空のとき、ロックを待たずに取れたら、まず書けなかったもの(persisted: falseのルール・組、消せなかった行)を API の変更と同じように書き直し(1 回に 32 件まで、1 件失敗したらその回はやめる。API の変更は止めない)、次に行を読み、API の変更が途中でなく読んだ後に書き込みが列に入っていなければ、自分のnodeの行に合わせてorigin: "api"のルール(組のものを除く)を作る・置き換える・消す(行が正:再起動で動くのは行のほう。1 回に 64 件まで。persist.reconcile)。UI・固定・組のルールには触らない(そのキーの行はrestore.conflictを 1 回出して残す)。ルールにならない行(検証で落ちる)はGET /rulesにfailedとして出し(v0.4.18、#327。下の「戻すときに検証で落ちたルール」)、行が変わるまで試さない。組(rproxy_rule_sets)は書き直すだけで、動いている組は変えない。取れなければ次の回に回す。 nodeの名前はノードごとに違うこと(既定はホスト名。--node-name/RPROXY_NODE_NAME)。DB があって、同じ名前のプロセスがほかに動いていると、後から起動した方は上の 40 秒(決められないときは 60 秒)の待ちの後にpersist.duplicate_node(error、phase: "startup"、相手のconnection・host、--node-name/RPROXY_NODE_NAMEを設定するように)を出し、fatalで終わる(終了コード 1。ロックは取り上げない)。systemd のユニット(Restart=on-failure)ではこの誤りをログに出しながら再起動を繰り返すので、名前を直すか片方を止める。kill の後の残った接続(同じホストのkill -9は接続がすぐ閉じる。ホストごと止まったときはwait_timeoutの 30 秒)では止まらない。引き継ぎ(SIGUSR2)の新しいプロセスは、古いプロセスがロックを持っていると知っているので確かめない。この確かめは待ち受け(固定ルール・制御 API)を開く前にするので、断られるプロセスは転送も API も始めない。RPROXY_NODE_NAMEを固定した配置で、新旧のプロセスが重なる入れ替え(blue/green、Kubernetes のRollingUpdateでmaxSurgeが 1 以上など)では、新しい方が断られる。古い方を先に止める(Kubernetes ではstrategy: Recreate)か、インスタンスごとに違う名前にする(その名前の行だけが戻ることに注意)。同じホストでの入れ替えは引き継ぎ(SIGUSR2)を使う。- DB に届かないあいだに起動して後からロックが取れないと分かったとき(相手が先にいた)は、止めずにロックを取らず(取り上げない)、その書き込みは
persisted: falseになり、ロックが 46 秒(wait_timeoutと書き込みの時間より長く)取れないままならpersist.duplicate_node(error、相手のconnection・host)を出す。書き込みが通ればそこで数え直す。 - 照合が行を正とするのは、行が最新と分かっているルールだけ:書き込みは列に入れる前に「書きかけ」として覚え、通ったら消す。書きかけ・書けなかった(
persisted: false)ルールと、消せなかった行のキーには、照合は行を当てない(書き直すだけ)。クライアントが応答の前に切れても(フロントのタイムアウトなど)、変更は最後まで(行を書くまで)進む。 - 引き継ぎ(SIGUSR2)では、古いプロセスは状態の写しを取る前に途中の API の変更(ルール・組の作成・変更・削除)が終わるのを最大 10 秒待ち、写しから drain に入るまで変更を止める(v0.4.15、#305。写しが古いプロセスの最後の状態になる)。引き継ぎが始まる前に受け付けて順番を待っていた変更は、何も変えずに
503 upgradingで断る(新しいプロセスに送り直す。古いプロセスでだけ動いて失われることはない)。10 秒で終わらない変更があれば引き継ぎをしない(handoff.failed。古いプロセスが動き続け、変更はそこで書かれる。自動更新はその版を悪い版にしない)。古いプロセスは drain に入るときに列を終えてから接続を閉じてロックを放し、新しいプロセスが取る。drain を待つ削除(DELETE ?drain_secs)は、行の削除を drain の前に列に入れ、drain のあいだはほかの変更もロックの手放しも止めない。古いプロセスで書けていなかったもの(persisted: falseのルール・組、消せなかった行)は新しいプロセスに渡り、新しいプロセスが書き直す。 - 列は trait(
config::persist::queue::PersistQueue)の後ろにあり、今はプロセスの中の実装(上限つきの channel と 1 つのタスク)だけ。外の列(Redis など)に替えても呼び出し側は変わらない。
- 書き込みは 1 件ずつのトランザクションで、接続のセッションは 1 つの文を 3 秒で終わらせる(
- テーブルの定義と GRANT は UI リポジトリの
db/の migration に置く。rproxy が使う定義:
CREATE TABLE rproxy_rules (
node VARCHAR(255) NOT NULL, -- RPROXY_NODE_NAME (default: the host name)
protocol VARCHAR(3) NOT NULL, -- tcp / udp
listen_addr VARCHAR(45) NOT NULL, -- IPv6 without brackets
listen_port INT UNSIGNED NOT NULL,
spec JSON NOT NULL, -- the rule in the shape of the body of POST /rules
spec_version INT UNSIGNED NOT NULL DEFAULT 1,
created_by VARCHAR(255) NOT NULL, -- token name
created_at DATETIME(3) NOT NULL,
updated_by VARCHAR(255) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (node, protocol, listen_addr, listen_port)
);
GRANT SELECT, INSERT, UPDATE, DELETE ON rproxy.rproxy_rules TO 'rproxy'@'%';spec はルールの形(POST /rules の本文の形、dry run の after と同じ)。spec_version は spec の読み方の版(今は 1)。時刻は DB のセッションのタイムゾーンで書き、UNIX_TIMESTAMP で読む。
rproxy-gateway(#28)が Gateway API の HTTPRoute・GRPCRoute・TLSRoute・BackendTLSPolicy を写すための設定。どれも省略でき、省略したときの動きはいままでと同じ。使えるかは GET /capabilities の features で分かる(middlewares に cors・mirror・replace_host、services に protocol・tls、http_options に下の名前、tls_route_targets)。
| 項目 | 場所 | 形 | features |
|---|---|---|---|
| ヘッダを足す(#224) | headers の request / response |
add: {名前: 値} |
http_options の headers_add |
| リダイレクトの状態コード(#226) | redirect_scheme・redirect_regex |
status: 301・302・303・307・308 |
http_options の redirect_status |
| ルートの時間の上限(#227) | http.routes[].timeouts |
{"request": "10s", "backend_request": "5s"} |
http_options の route_timeouts |
| Host の書き換え(#228) | ミドルウェア replace_host |
{"host": "one.example.org"} |
middlewares の replace_host |
転送先ごとのミドルウェア(#229、v0.4.3 で cors・リダイレクト・mirror) |
http.services.<名前>.servers[].middlewares |
http.middlewares の名前の一覧 |
http_options の server_middlewares(使える種類は server_middleware_kinds) |
| CORS(#230) | ミドルウェア cors |
allow_origins・allow_methods・allow_headers・expose_headers・allow_credentials・max_age |
middlewares の cors |
| 状態コードでの送り直し(#231) | retry |
status: ["500", "502-504"] |
http_options の retry_status |
| ミラー(#232) | ミドルウェア mirror |
{"service": "<名前>", "percent": 20} か {"service": "<名前>", "fraction": {"numerator": 1, "denominator": 3}} |
middlewares の mirror |
| 転送先との HTTP/2(#233) | http.services.<名前>.protocol |
http1(既定)・h2・h2c・auto |
services の protocol |
| サービスごとの転送先の TLS(#236) | http.services.<名前>.tls |
server_name・ca_file・subject_alt_names・cert_file・key_file・chain_file・insecure_skip_verify |
services の tls |
| 名前ごとの複数の宛先(#234) | tls.routes[] |
targets: [{addr, port, weight, backup}]・balance(remote_addr / remote_port の代わり) |
tls_route_targets |
| 名前ごとの転送先への TLS(v0.4.7、TLSRoute の終端の BackendTLSPolicy) | tls.routes[].upstream |
ルールの tls.upstream と同じ項目(tls・server_name・ca_file・cert_file・key_file・chain_file・insecure_skip_verify)。tcp の terminate の passthrough でない route だけ。あればその route ではルールの tls.upstream の代わりに使う(tls: false なら平文)。ファイルはルールの作成・変更のときに読む |
tls_route_upstream |
| 名前ごとの送信元 IP の渡し方(v0.4.13、#297) | tls.routes[].source_ip |
proxy・proxy_v1・proxy_v2。TLS を終端しない route(terminate の passthrough の route、sni のルールの route)だけ。省けばルールの source_ip(下の「tls.routes[] の source_ip」) |
route_source_ip |
クライアント証明書を確かめない(#238、AllowInsecureFallback) |
tls.client_auth.mode |
optional_no_verify(上の「証明書を確かめない optional_no_verify」) |
client_auth_modes |
| 固定の状態コードの転送先(#235) | http.services.<名前>.servers[] |
{"status": 500, "weight": 1}(url の代わり) |
http_options の server_status |
| 421 Misdirected Request(v0.4.3) | tls.misdirected |
{"groups": [["*"], ["b.example"], ["**.w.example"]]} |
http_options の misdirected |
外部の認可(v0.4.3、ExternalAuth) |
ミドルウェア forward_auth |
service・path・client_request・allow_status・response_headers: ["*"]・forward_body・protocol: grpc(上の「forward_auth」) |
forward_auth |
例(HTTPRoute 1 つの規則を写したもの):
{
"routes": [{
"name": "r0", "match": "Host(`app.example`) && PathPrefix(`/api/`)",
"service": "r0", "middlewares": ["r0-hdr", "r0-cors", "r0-mirror", "r0-retry"],
"timeouts": {"request": "10s", "backend_request": "2s"}
}],
"services": {
"r0": {
"protocol": "h2c",
"servers": [
{"url": "http://10.1.0.5:8080", "weight": 5, "middlewares": ["r0-b0"]},
{"url": "http://10.1.0.6:8080", "weight": 5, "middlewares": ["r0-b0"]},
{"status": 500, "weight": 10}
]
},
"r0-shadow": {"servers": [{"url": "http://10.1.0.9:8080"}]},
"tls-svc": {"servers": [{"url": "https://10.1.0.7:8443"}],
"tls": {"server_name": "abc.example.com", "ca_file": "/var/run/rproxy-gateway/certs/0123456789abcdef.crt",
"subject_alt_names": ["abc.example.com", "spiffe://abc.example.com/test-identity"]}}
},
"middlewares": {
"r0-hdr": {"headers": {"request": {"set": {"X-Header-Set": "v"}, "add": {"X-Header-Add": "v"}, "remove": ["X-Header-Remove"]}}},
"r0-b0": {"headers": {"request": {"set": {"Backend": "v1"}}}},
"r0-cors": {"cors": {"allow_origins": ["https://www.foo.com", "https://*.bar.com"], "allow_methods": ["GET", "OPTIONS"],
"allow_headers": ["x-header-1"], "expose_headers": ["x-header-3"], "allow_credentials": true, "max_age": 3600}},
"r0-mirror": {"mirror": {"service": "r0-shadow", "percent": 20}},
"r0-retry": {"retry": {"attempts": 4, "status": ["500", "502-504"], "initial_interval": "100ms"}},
"r0-host": {"replace_host": {"host": "one.example.org"}},
"r0-redirect": {"redirect_regex": {"regex": "^http://([^/:]+)(:\\d+)?/(.*)$", "replacement": "https://$1/$3", "status": 303}}
}
}headersのadd(#224):同じ名前のヘッダ(大文字小文字を区別しない)があれば、その値(複数のフィールドなら,でつないだもの)の後ろに,で足して 1 本にする(a→a,v)。なければ足す。順はremove→set→add。転送先へ(request)も応答へ(response)も同じ。- リダイレクトの
status(#226):指定すると、permanentとメソッドによる切り替え(GET・HEAD 以外は 308 / 307)より優先する。301・302・303・307・308 のほかは400 invalid。 - ルートの
timeouts(#227):0sは上限なし(省略と同じ)。request:リクエストを受けてから応答の本文を送り終えるまで(ミドルウェア・retryの送り直し・待ち時間を含む)。応答ヘッダの前に過ぎたら 504(event: "http.error"、error: "request timed out")。応答ヘッダの後に過ぎたら応答を途中で切る(HTTP/1.1 は接続を閉じる、HTTP/2 は RST_STREAM、HTTP/3 はストリームのリセット。完全な応答に見せない)。backend_request:転送先への 1 回の送信の、送り始めから応答の本文の終わりまで。応答ヘッダの前に過ぎたら 504(retryが送り直せるなら次の転送先へ)。応答ヘッダの後はrequestと同じく切る。このルートではサービスのtimeouts.responseの代わりに使う(timeouts.connectはそのまま)。- 101(WebSocket など)の後の中継は数えない。
replace_host(#228):転送先へ送るHost(HTTP/2 の転送先なら:authority)をhost(host[:port])にする。サービスのpass_host_headerより優先。X-Forwarded-Hostはクライアントが送った値のまま。matchには効かない(ルートを選んだ後に働く)。- 転送先ごとのミドルウェア(#229):
servers[].middlewaresのミドルウェアは、その転送先へ送るリクエストにだけ、ルートのミドルウェアの後に働く(retryの送り直しでは、送り直す先の転送先のもの)。応答へは逆の順で、ルートのミドルウェアの応答側より先に働く。使える種類はheaders・replace_host・strip_prefix・add_prefix・replace_path・replace_path_regex、v0.4.3 からcors・redirect_scheme・redirect_regex・mirror・forward_auth(ほかは400 invalid。features.server_middleware_kindsに一覧)。- v0.4.3 で足した種類(Gateway API の backendRef の
CORS・RequestRedirect・RequestMirrorのフィルタ):corsはその転送先に当たったプリフライトに答え、その転送先の応答に CORS のヘッダを付ける。redirect_scheme・redirect_regexはその転送先に当たったリクエストに転送先へ送らずリダイレクトで答える(retryの送り直しはしない)。mirrorはその転送先へ送るリクエストの写しを、そこまでのミドルウェア(ルートのもの、その転送先の前のもの)を通った形でserviceの転送先へも送る(X-Forwarded-*もそのまま。1 つのリクエストで 1 回だけ:retryの送り直しでは、最初にmirrorのある転送先に当たったときだけ)。本文は流れてくるそのまま、bufferingで読み切った本文はそれを写す。mirrorの先のサービスの転送先が自分でmirrorを持つと400 invalid(写しを写さない)。
- v0.4.3 で足した種類(Gateway API の backendRef の
cors(#230):allow_origins:https://www.foo.com(スキーム・ホスト・ポートの完全一致、大文字小文字を区別しない)、*(すべて)、https://*.bar.com(*は 1 文字以上の何にでも一致。.を含む)。*は先頭のラベルとしてだけ書ける(https://*bar.comはevilbar.comに一致してしまうので400 invalid。セキュリティレビュー L13)。*とallow_credentials: trueを同時に書くと、どのサイトにも資格情報つきのリクエストを許すことになるのでconfig.warningを出す(Gateway API の決まりどおりOriginをそのまま返す)。- プリフライト(
OPTIONSでOriginとAccess-Control-Request-Methodがあるもの)は、オリジンを許すなら rproxy が 204 で答える:Access-Control-Allow-Origin(allow_originsが*でallow_credentialsが false なら*、ほかはOriginの値)、Access-Control-Allow-Methods(allow_methodsをカンマ区切りで。*なら、allow_credentialsのときは求められたメソッド、そうでなければ*)、Access-Control-Allow-Headers(同じく。*ならallow_credentialsのときはAccess-Control-Request-Headersの値)、Access-Control-Expose-Headers、Access-Control-Max-Age(max_ageがあれば)、Access-Control-Allow-Credentials: true(allow_credentialsのとき)、Vary: Origin。許さないオリジンのプリフライトも rproxy が 204 で答え、CORS のヘッダは付けない(Vary: Originだけ。転送先へは送らない。#238。転送先が許してしまわないように)。OriginのないOPTIONSはプリフライトではないので転送先へ。 - ふつうのリクエストは転送先へ送り、オリジンを許すなら応答に
Access-Control-Allow-Origin・Access-Control-Allow-Credentials・Access-Control-Expose-Headersを付ける(転送先が返した同じ名前のヘッダは置き換える)。Vary: Originは許さないオリジン・Originのないリクエストにも付ける(v0.4.19、#340。前は許したときだけ)。allow_originsが*でallow_credentialsが false のとき、Access-Control-Allow-Originは*だが、資格情報(CookieかAuthorization)付きのリクエストにはOriginの値を返す(v0.4.12 から。Gateway API v1.5 の conformance の期待。v1.6 はどちらでもよい)。Access-Control-Allow-Credentialsは付けないので、ブラウザがその応答を使わないのは前と同じ。 headersのcorsはいままでのまま。
retryのstatus(#231):転送先がこの状態コード("500"・"502-504"のような値か範囲)を返したら、その応答を捨てて次の転送先へ送り直す(balanceで選び直すので、転送先が 1 つなら同じ転送先へ。#238)。最後の回の応答はそのまま返す。送り直せる条件(冪等なメソッド、本文がないかbufferingで読み切ったもの、Upgrade でない)とattempts(最初の 1 回を含む回数)・initial_interval(待ち時間。回ごとに倍)はいままでと同じ。Gateway API のattemptsは送り直しの回数なので、写すときはattempts + 1。mirror(#232):- 転送先へ送るリクエストの写しを、
service(http.servicesの名前)の転送先へも送る。写しはルートのmiddlewaresでそこまでのミドルウェアを通った形(ヘッダの書き換えの後ろに書けば書き換えた形)。Hostはserviceのpass_host_headerに従う。 percent(0〜100)かfraction(numerator/denominator、denominatorの既定 100)の割合だけ写す(両方は400 invalid。省略で全部)。割合は数で揃える(無作為ではなく、n件目を黄金比で散らす)。- ミラーの応答は読み捨て、つながらない・遅い・失敗はクライアントの応答に影響しない(ログは
event: "http.mirror"の debug)。本文は流れてくる分を写し、ミラーが追いつかない(64 フレーム分たまった)ときはミラーの送信だけを途中で止める。 - 前のミドルウェアが答えたリクエスト(リダイレクト・拒否)は写さない。1 つのルートに複数書ける。
- 転送先へ送るリクエストの写しを、
protocol(#233):http1(既定):いままでどおり HTTP/1.1。h2:https://の転送先と TLS(ALPNh2)の HTTP/2。転送先がh2を選ばなければ 502。h2c:http://の転送先と、前置きから始める HTTP/2(prior knowledge)。auto:https://の転送先は ALPN でh2とhttp/1.1を提示して、転送先が選んだほうで話す。http://の転送先は HTTP/1.1。h2はhttp://、h2cはhttps://の転送先とは組み合わせられない(400 invalid)。- HTTP/2 の転送先とは、接続を多重化して使い回す(閉じられたら次のリクエストでつなぎ直す)。1 本の接続に 100 件を超えて流すときは、転送先ごとに 8 本まで接続を足す。それでも空きがなく、転送先の
SETTINGS_MAX_CONCURRENT_STREAMSで本文のあるリクエストがストリームを得られないときは、timeouts.connectの間待って 504(セキュリティレビュー M6:遅い本文で全部のリクエストが止まらないように)。接続を開くのは一度に 1 つで、空きのある接続を使うリクエストは待たない。source_ip: transparentのルールではクライアントごとに新しい接続。 - トレーラーは両方向にそのまま流す(gRPC の
grpc-statusなど)。クライアントのTEにtrailersがあれば、HTTP/2 の転送先へte: trailersを渡す(ほかのホップごとのヘッダはいままでどおり取り除く)。 timeouts・retry・health_check(HTTP/2 でGET)・outlier_detection・stickyは HTTP/1.1 の転送先と同じ。HTTP/2 の転送先への Upgrade(WebSocket)は 502。- gRPC:クライアントは HTTP/2(TLS か h2c)で rproxy につなぐ。サービス・メソッドの一致は
Path(/<package.Service>/)・PathPrefix(/<package.Service>/)で書く。
- サービスの
tls(#236):- そのサービスの
https://の転送先には、ルールのtls.upstreamの代わりにこれを使う(項目ごとに混ぜない)。平文の HTTP のルール(tlsなし)でも使える。 server_name:SNI と、証明書で確かめる名前(既定は URL のホスト)。ca_file:転送先の証明書を確かめる CA(既定は Mozilla のルート)。subject_alt_names:指定すると、証明書の SAN の DNS 名か URI(spiffe://...など)のどれかがこの一覧にあることを確かめる(server_nameでの名前の確認の代わり。署名・期限は確かめる)。cert_file・key_file(・chain_file):転送先へ出すクライアント証明書。insecure_skip_verify:確かめない(試験用)。- ファイルはルールを作る・変えるときに読む(変わったファイルはルールの変更で読み直す。rproxy-gateway は中身のハッシュの名前にする)。
http://の転送先だけのサービスに書いても使わない。
- そのサービスの
tls.routes[]のtargets(#234):remote_addr/remote_portの代わりにtargets(ルールのtargetsと同じ形、weight・backupも同じ)とbalance(round_robin(既定)・least_conn・failover)。どちらか一方が要る(両方・どちらもなしは400 tls_config)。接続できない宛先は次の宛先へ移り、10 秒外す(ルールのtargetsと同じ)。passthroughの route、sniのルールの route のどちらでも使える。名前の再解決もルールのtargetsと同じ。ポート範囲のルールでは各宛先のポートも同じだけずれる。servers[]のstatus(#235):urlの代わりにstatus(100〜599)を書くと、その転送先に当たったリクエスト(weightの割合)に rproxy がその状態コードで答える(本文は500 Internal Server Errorのような文)。ヘルスチェック・outlier_detection・stickyの対象にしない(stickyのクッキーも付けない)。urlとstatusはどちらか一方(両方・どちらもなしは400 invalid)。middlewaresは付けられない。tls.misdirected(v0.4.3、Gateway API のGatewayHTTPSListenerDetectMisdirectedRequests):同じポートの名前の違う HTTPS のリスナーを 1 つのルールにまとめたとき、HTTP/2 の接続の使い回し(connection coalescing。証明書の SAN にある別の名前のリクエストを同じ接続で送る)で、ほかのリスナーの名前のリクエストが届くのを 421 Misdirected Request(RFC 9110 §15.5.20)で断る。groups:名前のパターンのグループの一覧(tls.routesと同じ書き方:完全一致・*.(1 階層)・**.(何階層でも)。*はどの名前にも一致するグループ。同じパターンを 2 か所に書くと400 tls_config)。名前は最も近いパターンのグループに入る(完全一致 →*.→ 長い**.→*)。- 接続の TLS のサーバ名(SNI)と、リクエストの
Host(HTTP/2・HTTP/3 は:authority、ポートを除く)がどちらもグループに入り、違うグループなら 421(ルートもミドルウェアも通らない。アクセスログはroute: "(none)"・refused_by: "misdirected")。どちらかがどのグループにも入らないとき(*のグループがなく、ほかのリスナーの名前でもない)と SNI のない接続は確かめず、いつものとおりhttp.routesで選ぶ(一致しなければhttp.default、既定 404)。HTTP/1.1・HTTP/3 も同じ。 httpとtls.mode: terminateのルールだけ(ほかは400 tls_config)。省略すれば確かめない(v0.4.2 までと同じ)。
Kubernetes のコントローラ(別のリポジトリ max3584/rproxy-gateway)は、この API だけで rproxy を動かす。正確な形は docs/openapi.json(GET /openapi.json)。
ルールの組(ruleset):PUT /rulesets/{name} に「その組のルールの全体」を毎回まとめて送ると、rproxy が今のルールとの差を当てる(宣言的。途中で落ちても次の PUT で揃う)。
- 名前は
[a-z0-9]([a-z0-9._/-]{0,251}[a-z0-9])?(例k8s/default/web-gateway)。パスの/はそのまま書く(PUT /rulesets/k8s/default/web-gateway)。 - 本文は
{"generation": <整数>, "rules": [<POST /rules と同じルール>...]}(ルールは 10,000 個まで、本文は 32 MiB まで)。同じキーのルールを 2 つ書くと400 invalid。 - 順番:まず全部を確かめ、どれかが通らなければ何も変えない。
0. 持ち主(セキュリティレビュー M3):組はそれを作ったトークン(
owner)のもので、ほかのトークンはadminでなければPUT/DELETEできない(403 forbidden)。トークンのallow_rulesets(名前の先頭の一覧)の外の名前も403。rproxy の再起動のあと先に組を作られないように、コントローラのトークンにはallow_rulesets: [k8s/]のように付け、ほかのrules:writeのトークンには付けない名前にする。generationは 2^53 - 1 まで(400 invalid。JSON のクライアントが正しく読める数)。If-Match(あれば):今の etag と違う、または組がまだないと412 precondition_failed。*は「組があれば」。ETag ヘッダの引用符つきの値でも、本文のetagの値そのままでもよい(W/・カンマ区切りも受ける)。generation:覚えている値より小さいと409 stale_generation(同じ値はよい)。- 各ルールの形(
POST /rulesと同じ検証。この版で動かせない設定はunsupported)と、本文の中のルール同士の重なり(400 invalid)。 - 組の外のルールとの取り合い:設定ファイルのルールと同じキーは
409 static、ほかの組のルールは409 owned、POST /rulesで作ったルールや待ち受けが重なるルールは409 already_exists、制御 API のアドレスは409 reserved。 - 作る・変える・消すルールの待ち受けポートがトークンの
allow_listen_portsの内か(外なら403。変えるルールは、今の待ち受けの範囲も内であること:狭めて範囲の外の待ち受けを消せないように。セキュリティレビュー M2)、ACME の証明書を使うルールにはacme:write。 - 作る・変えるルールの証明書・秘密のファイルが読めるか(
400 tls_config/invalid)。
- 断るときの本文は
{"code","error","errors":[{"index","rule","code","message"}]}。codeとerror(rules[i]: ...)は最初の問題、errorsはルールの問題のすべて(indexは本文のrulesの何番目か)。
- 当て方:組から外れたルールを先に止め(接続は切れる)、変わったルールは PATCH で接続を切らずに変えられる違い(宛先・
balance・health_check・時間・TLS・allow_from・extra_listen_addrs・http・labelsなどの v0.4 の設定)ならその場で変え(change: in_place)、そうでなければ(ポートの範囲・source_ip・httpの有無)待ち受けを作り直す(recreate)。新しいルールは作り、変わらず動いているルールには触らない(none)。failedのルールは変わっていなくても作り直す。 - bind・名前解決に失敗したルールはそのルールだけ
failedに登録して(理由はconditions)、残りは当てる。応答は200で、ルールごとの結果を返す:{"name","generation","etag","dry_run":false,"results":[{"rule":"tcp/0.0.0.0:443","action":"create|update|delete|none","change":"none|in_place|recreate","state":"running|failed","error"}]}(本文の順、その後に消したルール。deleteにはstateがない)。応答のETagヘッダはetagを"で囲んだもの。 - etag は
g<generation>-<ルールの正規化した JSON(キーの順)の SHA-256 の先頭 16 桁>。ルールかgenerationが変わると変わり、状態(running/failed)では変わらない。 ?dry_run=true:同じ確かめをして、変えずに結果(dry_run: true、なるはずのetag、updateにはdiff)を返す。changeとdiffの意味は上の「変更前の差分」と同じ。- 組のルールは
GET /rulesにも出てruleset: "<名前>"が付く(originはdynamic)。個別のPATCH/DELETE /rules/...は409 owned(変えるなら組を PUT する)。POST /rulesで同じキーは409 already_exists。 GET /rulesets/{name}:{"name","generation","etag","updated_at","updated_by","owner","rules":[<GET /rules と同じ表示>...]}(キーの順、ETagヘッダつき)。updated_byは最後に PUT したトークンの名前(トークンファイルがなければ空)。GET /rulesetsは名前の順の一覧(rulesは数)。DELETE /rulesets/{name}?drain_secs=N:組のルールを同時に止めて組を消す(drain_secsの間、各ルールの接続の終わりを待つ)。If-Matchも使える。全部の接続が終わってから204。- 組は rproxy のメモリにだけあり、DB にも設定ファイルにも書かない(v0.4.2 から、
persist: trueのトークンの組だけはrproxy_rule_setsに保存できる:下の「ルールの組の保存」)。rproxy を再起動したら、コントローラはGET /readyzが 200 になってから組を PUT し直す。組の変更は 1 つずつ順に行う(読むのは待たない)。 - ログ:
event = "ruleset.apply"(ruleset・generation・etag・created・updated・deleted・unchanged・failed・by)、ruleset.delete(ruleset・rules)。個々のルールのrule.create/rule.update/rule.deleteにもrulesetが付く。auditはaction: ruleset.put/ruleset.deleteとruleset。
ラベル:ルールの labels。動きには使わず、rule.create / rule.update のログ(labels: "k=v,k2=v2")と /metrics の rproxy_rule_labels{rule="tcp/0.0.0.0:443",label_tenant="act"} 1 に出す(キーの英数字以外は _。同じ名前になるキーは名前の順で先のものだけ)。PATCH で付けると丸ごと置き換える({} で外す、省けば今のまま)。
状態(conditions):どのルールの表示にも(組でなくても)、次の 4 つがこの順で付く(Gateway API の status にそのまま写せる形)。message は True のとき空。
| type | True の reason |
False の reason |
|---|---|---|
Accepted |
Accepted |
Unsupported(この版・この環境で動かせない設定。起動時・再読み込みのルール) |
Programmed |
Listening(state: running) |
BindFailed(ポートを開けない)、Pending(転送先の名前解決を待って再試行中)、Failed(その他) |
ResolvedRefs |
ResolvedRefs |
ResolveFailed(名前解決できない転送先がある)、CertificateExpired(サーバ証明書が切れた)、CertificateUnreadable(証明書・鍵・CA を読めない)、SecretUnreadable(ミドルウェアの秘密のファイルを読めない) |
BackendsHealthy |
Healthy |
AllTargetsDown(すべての宛先が down)、ServiceDown(サーバがすべて down の http のサービスがある。message に名前)。動いていないルールは status: "Unknown"・NotProgrammed |
last_transitionはstatusが最後に変わった Unix 秒(reason・messageだけが変わっても動かない)。ProgrammedのTrueはstarted_atから。ほかの変化はルールを読んだとき(GET /rules・GET /rulesets/{name}・PUT の応答)に気づいた時刻。ルールを消す・作り直すと最初から。- 今までの
state・error・all_targets_down・down_servicesもそのまま(UI が使う)。
readiness:GET /readyz(認証なし、/healthz と同じ)。起動時の復元(設定ファイル・DB)が終わると 200 {"ready": true}、その前と、終了の処理に入った後(#174 の引き継ぎでも、RPROXY_SHUTDOWN_DELAY の間も)は 503 {"ready": false, "reason": "starting" | "draining"}。ルールの失敗は readiness に含めない(ルールの状態は conditions)。生きているかは今までどおり /healthz。
features.limits・features.bandwidth は true(形は上の表と docs/DESIGN-v0.4.md の 4・5)。
limitsは受け付けた直後(allow_from・geoip・crowdsecの後、TLS より前。proxy_protocolではヘッダのクライアントで)に確かめる。超えたら TCP は何も送らずに閉じ、UDP はデータグラムを捨てる(新しいセッションは作らない)。httpのルールでは TCP の接続に効く(HTTP/3 には効かない)。max_connectionsは TCP の接続・UDP のセッションの数(per_source.max_connectionsも同じ。udp のルールでmax_connectionsがなければ、既定の上限RPROXY_UDP_MAX_SESSIONS(既定 16384、0 でなし。#332)が効く:偽った送信元でも 1 つずつ転送先へのソケットを持つセッションをudp_idle_secsまで作れるので。名前を読んでいるセッションも数え、断ったものはmax_connectionsと同じく数えて記録する)、new_connectionsは新しい接続・セッションの速さ、packetsは UDP のデータグラムの速さ(送信元ごと)。- 断った数は
stats.limited、/metricsのrproxy_rule_limited_total{protocol,listen,reason}(limitsか宛先のmax_connections(v0.4.19)のあるルールと、UDP のセッションの既定の上限で断ったことのあるルール。ほかの指標と同じくruleの代わりにprotocol・listenのラベル)。ログはconn.limited(rule・client・reason(max_connections/source_connections/new_connections/packets、v0.4.19 から宛先がすべていっぱいのtarget_full)・transport(tcp/udp、target_fullはhttpも)。送信元ごとに続けて 20 行まで、その後は 1 秒に 1 行、suppressedは省いた行の数)。 - 送信元は
prefix_v4/prefix_v6でまとめ、覚えるのはmax_sourcesまで(16 に分けた表ごとに上限の 1/16。いっぱいになったら、接続の残っていない古いものから忘れる。接続の残っている送信元は忘れない(数え直しで制限をすり抜けられないように。セキュリティレビュー L8)。表がそういう送信元でいっぱいのあいだ、新しい送信元はlimitsでは断り(reason: source_connections)、bandwidthでは 1 つの共有のバケツを使う)。max_sourcesは 1,000,000 まで。 PATCHでlimitsを変えると、次の接続・データグラムから効く。ルール全体の接続数は引き継ぎ、送信元ごとの数とバケツはprefix_v4・prefix_v6・max_sourcesが同じなら引き継ぐ。{}で外すと数えるのをやめる(あとで付け直したときは、そこから数える)。bandwidth:TCP(httpのルールを含む)は読むのを待たせて絞る(捨てない)。L4 は上り(クライアントから読む)・下り(転送先から読む)、httpのルールはクライアントからの読み込みとクライアントへの書き込み。待っている間は中継のバッファをプールに返す。UDP は超えたデータグラムを捨て、stats.droppedとrproxy_rule_bandwidth_dropped_total{protocol,listen}に数える。HTTP/3 は絞らない。- 速さはトークンバケツ(
burstが大きさ、既定は 100 ms 分)。ルール全体のバケツはルールの全接続、送信元ごとのバケツはその送信元の全接続で分ける。TCP は 4 KiB(burstが小さければその大きさ)たまるまで待ってから読む。データグラムや読んだ量が残りより大きければ借りにして、その分あとで待つ(長い目で見て速さを守る)。 - splice(#184)は帯域の上限のないルールでだけ使う。上限のあるルールの平文の TCP はユーザー空間のコピーで絞る。
PATCHで上限を付けると、splice している接続も次の splice の前にユーザー空間のコピーに戻り、それからは上限を外しても splice に戻らない。 PATCHでbandwidthを変えると、開いている接続にも次に読むところから効く(バケツは満杯から)。- 上限を付けていないルールの転送は、読み込みごとに 1 回の不可分な読み出し(relaxed load)が増えるだけ。
- 集計(#166 5.2):
stats.rx_bytes・tx_bytes・total_connectionsは単調増加、stats.counters_sinceは数え始めた時刻(Unix 秒。ルールを作り直すと変わる、PATCHでは変わらない)、stats.limitedを足した。/metricsにrproxy_process_start_time_seconds。
global:
geoip:
country_db: /var/lib/GeoIP/GeoLite2-Country.mmdb # Country か City の mmdb
asn_db: /var/lib/GeoIP/GeoLite2-ASN.mmdb # 任意
check_interval: 1m # ファイルが変わったか確かめる間隔(既定 1m、0s で確かめない)
log_country: true # conn.open・http.access に country(と asn)を足す(既定 false)
rules:
- {protocol: tcp, listen_addr: 0.0.0.0, listen_port: 25565, remote_addr: 10.0.0.5, remote_port: 25565,
geoip: {allow_countries: [JP], deny_asns: [64496], unknown: allow}}- データベースは同梱しない。MaxMind の GeoLite2(アカウントを作って
geoipupdateで取る)か、同じ項目(country.iso_code、なければregistered_country.iso_code、autonomous_system_number)を持つ mmdb を使う。ファイルはメモリに読み込み(mmap はしない)、check_intervalごとと SIGHUP で変わっていれば読み直す(event: "geoip.reload")。読み直せない(書きかけ・壊れている・権限)ときは今のものを使い続ける(event: "degraded"、part: "geoip"。同じ問題は 1 回だけ)。起動時(と--check-config)は、ファイルがない・mmdb でないなら設定のエラーで起動しない。権限で読めないならdegradedを出して起動し、読めるまでそのデータベースの判定はすべて「分からない」になる。 - 判定:まず
deny_*に当たれば拒否。allow_*のどれかが書いてあれば、どれかのallow_*に当たるものだけ通す(分かっている国か ASN が、そのリストに当たらなければ拒否。もう一方が分からなくても拒否する。セキュリティレビュー L18)。リストが要る国・ASN がどれも分からない(データベースにない・私用アドレス・データベースが読めない)ものはunknown(既定allow)。データベースが読めないときもunknownになるので、拒否に倒したいときはunknown: denyにする。データベースは 1 GiB まで(それより大きいファイルは読まない)。 - L4(ルールの
geoip):allow_fromの後、crowdsecの前、受け付けた直後(TLS より前。proxy_protocolではヘッダのクライアントで)に判定する。TCP は接続を閉じ、UDP はデータグラムを捨てる(開いているセッションのものも。セッションは作らない)。HTTP/3 は QUIC の接続を受ける前。httpのルールでも使える(見るのは接続元の IP)。拒否はstats.deniedに数え、conn.denied(reason: "geoip"、分かればcountry・asn。UDP はallow_fromと同じく送信元ごとに間引く)。 - L7(ミドルウェアの
geoip):global.trusted_proxiesで決めたクライアントの IP で判定し、拒否は403(ip_allowと同じ)。http.accessのrefused_by: "geoip"・middleware、country・asn。 log_country: trueで、conn.open(TCP・UDP)とhttp.accessにcountry(とasn)が付く(分からないときは付かない)。- 国のリストは
country_db、ASN のリストはasn_dbがないと400 invalid(設定ファイルは検証の誤り)。PATCHでgeoipを付けると丸ごと置き換え、{}で外す(接続は切らない。次の接続・データグラムから)。 - CrowdSec と組み合わせるとき:国・ASN での粗い絞り込みは
geoip、振る舞いでの ban は CrowdSec(L4 のルールのcrowdsec: true、L7 のcrowdsecミドルウェア)。判定の順はallow_from→geoip→crowdsec。CrowdSec の側でもcrowdsecurity/geoip-enrichで国を付けられるので、rproxy のlog_countryは SIEM などで rproxy のログだけを見るとき向け(docs/CROWDSEC.md)。
実際の通信で失敗が続いた宛先を、しばらく外す(outlier detection)。外した宛先は down と同じく選ばない(L4 は、すべての宛先が外れている・down のときは今までどおり順に試す)。外す時間が過ぎたら自然に戻る(target.up、reason: "outlier")。health_check で up に戻った宛先はすぐ戻る。
L4(ルールの outlier_detection。宛先が 1 つでも使えるが、意味があるのは複数のとき):
| 項目 | 既定 | 意味 |
|---|---|---|
consecutive_failures |
1 |
続けて失敗した回数(1〜1000)。失敗は TCP の接続を断られた・時間切れ(cause: connect)、UDP の ICMP の到達不能(refused)、short_lived |
short_lived |
0s(数えない) |
TCP で、転送先が先に閉じた(切った)接続のうち、つながってからこの時間(0s〜1m)より短いものも失敗に数える。そのとき接続の成功は接続が終わるときに数える。UDP には効かない |
ejection_time |
10s |
最初に外す時間(1s〜1h) |
max_ejection_time |
ejection_time |
外すたびに倍にし、ここで止める(戻ってからこの時間のあいだ外されなければ、ejection_time に戻る) |
max_ejected_percent |
100 |
同時に外せる宛先の割合(0〜100。0 で外さない) |
- 書かなければ v0.3 までと同じ(接続に 1 回失敗したら 10 秒外す)。
{}も同じ。 - 外しているあいだにまた失敗した宛先(すべて外れていて試されたもの)は、外す時間を最初から数え直す(回数は増やさない)。
PATCHで変えると次の接続から効き、宛先ごとの回数と外している状態はそのまま(宛先・balance・health_checkも変えたときは最初から)。
L7(http.services.<名前>.outlier_detection。書いたサービスだけ):
| 項目 | 既定 | 意味 |
|---|---|---|
consecutive_5xx |
5 |
続けて 5xx を返した回数(0 で見ない)。下の gateway の失敗も数える |
consecutive_gateway_failures |
3 |
502・503・504、接続できない、timeouts.response の時間切れが続いた回数(0 で見ない) |
failure_percent |
なし | window の中で失敗(5xx・gateway の失敗)の割合がこれ以上なら外す(1〜100) |
min_requests |
20 |
failure_percent を見る最小のリクエスト数 |
window |
30s |
failure_percent を数える窓(区切りごとに数え直す) |
ejection_time / max_ejection_time |
30s / 5m |
L4 と同じ |
max_ejected_percent |
50 |
同時に外せるサーバの割合。既定ではサーバが 1 つのサービスは外さない |
circuit_breaker(ミドルウェア。サービス全体を止める)とは別で、サーバごとに外す。数えるのは転送先への 1 回ごとのリクエスト(retryの試し直しもそれぞれ数える。errors・forward_authのページの取得も)。- 外したサーバは
stats.http.services.<名前>のサーバに"ejected": true(upは false。outlier_detectionのあるサービスはhealth_checkがなくても出る)、/metricsのrproxy_http_server_upも 0。すべてのサーバが外れていて(max_ejected_percent: 100)、ヘルスチェックで up のものがあれば、それを使う。 - ログは
target.down(reason: "outlier"、rule、service、server、cause:越えたしきい値consecutive_5xx・consecutive_gateway_failures・failure_percent、ejection_secs、ejections)/target.up(reason: "outlier")。 httpを変えると数え直し(Routerを組み立て直すため)。
v0.4.0 の後にパッチで足した設定(docs/DESIGN-v0.4.x.md。足すだけの形はパッチで出す:docs/RELEASING.md の「v0.4.0 の後」)。どれも省略でき、省略したときの動きは v0.4.0 と同じ。使えるかは GET /capabilities の features で分かる。
| 引数 / 環境変数 | 既定 | 意味 |
|---|---|---|
--shutdown-delay / RPROXY_SHUTDOWN_DELAY |
0s |
SIGTERM の後、/readyz を 503 draining にしたまま、これだけ今までどおり受け付ける |
--shutdown-drain / RPROXY_SHUTDOWN_DRAIN |
0s |
その後、待ち受けを閉じて、今の接続・セッションの終わりをこれだけ待つ。過ぎたら切る |
- 既定(両方
0s)は今までと同じで、SIGTERM ですぐ止める。値は30s・2m・秒の数で、それぞれ 1 時間まで(超えると起動を止める設定のエラー)。 delayの間:転送は今のまま(新しい接続も受ける)。drainの間:TCP の待ち受けを閉じ(新しい接続は断られる)、今の接続は続く。httpのルールは処理中のリクエストの応答にConnection: closeを付けてから閉じ(HTTP/2 は GOAWAY)、アイドルの接続はすぐ閉じる。HTTP/3 は新しい QUIC の接続を受けない。UDP は新しいセッションを作らず(そのデータグラムはstats.dropped)、今のセッションは続く。delay・drainの間、制御 API の読むだけの要求(GET・HEAD。/healthz・/readyz・/metricsも)は答え、変更(POST・PUT・PATCH・DELETE、dry run も)は503 shutting_down。設定ファイルの変化は反映しない。再起動なしの更新(SIGUSR2・自動更新)も始めない。- 2 回目の SIGTERM・SIGINT で残りを待たずに止める。
- 引き継ぎ(SIGUSR2)の後の古いプロセスの終わり方は今までどおり(
RPROXY_HANDOFF_DRAIN)。 - ログ:
shutdown.start(delay_secs・drain_secs)、shutdown.drain(connections)、shutdown.now(2 回目のシグナル)、shutdown.done(cut)。systemd での推奨値は README の「SIGTERM での終わり方」。
証明書と鍵を制御 API で渡して rproxy に保存し、ルールから名前で使う(cert-manager などの Secret を外から押し込む使い方)。鍵は DB に置かず、応答・ログに出さない。
| メソッドとパス | 本文 | 成功時 | 説明 |
|---|---|---|---|
PUT /certs/{name} |
{"cert": "<PEM>", "key": "<PEM>", "chain": "<PEM>"?} |
201(新しく)/ 200(差し替え) | 確かめて保存する:PEM が読める、鍵と証明書が合う、チェーンの順、期限内(切れていれば 400 invalid、RPROXY_CERT_WARN_DAYS 以内なら応答に warnings)。If-Match(fingerprint_sha256、* はあれば)が違えば 412 precondition_failed。応答は下の 1 件の形と ETag。本文は 1 MiB まで(超えると 413) |
GET /certs |
200 | [{"name","sans","not_before","not_after","fingerprint_sha256","issuer","used_by":["tcp/0.0.0.0:443",...],"updated_at","updated_by"}](トークンの allow_certs の内だけ)。鍵は返さない |
|
GET /certs/{name} |
200 | 上の 1 件(なければ 404 not_found) |
|
DELETE /certs/{name} |
204 | 使うルール(設定ファイル・API・組)があれば 409 in_use(本文に used_by)。If-Match も受ける |
- 名前:
[a-z0-9]([a-z0-9._-]{0,61}[a-z0-9])?(外は400 invalid)。 - スコープ:
certs:read(GET)、certs:write(PUT・DELETE)。トークンのallow_certs(名前の先頭)の外は403 forbidden。 - 鍵が流れるので、TCP の制御 API では TLS か loopback からだけ受け付ける(Unix ソケットもよい)。ほかの平文の TCP からの
PUTは403 tls_required。 - ルールからは
tls.certificates[]に{"cert": "<name>"}(cert_file・key_file・chain_file・acmeの代わり。どれか 1 つ)。API・設定ファイル・DB・組で同じ形。tcpのterminate(httpのルールを含む)とudpの DTLS で使える。client_auth・転送先の証明書にはまだ使えない。使うにはrules:writeと、名前がトークンのallow_certsの内であること(外は403)。- 保存していない名前:API の作成・変更・組の PUT は
400 tls_config(certificate "<name>" is not in the certificate store)。起動時・再読み込みのルールはfailedになり、その名前がPUTされると動き出す。 - 差し替え(同じ名前の
PUT):使っているルールは接続を切らずに新しい証明書になる(RPROXY_CERT_CHECK_SECSを待たない)。
- 保存していない名前:API の作成・変更・組の PUT は
- 保存先:
--cert-store/RPROXY_CERT_STORE(既定/var/lib/rproxy/certs)。<name>/<指紋の先頭 16 桁>/tls.crt(チェーンつき)・tls.keyに書き、<name>/currentのシンボリックリンクを付け替える。ディレクトリは 0700、ファイルは 0600 で rproxy-api のもの(global.files.owner_check: strictで使える)。trusted_dirsの下には置けない(起動を止める)。書けないときはPUTが503 cert_store_unavailable(RPROXY_CERT_STOREを指定していれば起動時にdegraded、part: "cert_store")。 - 監査:
event=audit、action: cert.put/cert.delete、cert(名前)・fingerprint_sha256だけ。 - 引き継ぎ(#174)ではファイルなので何も渡さない。複数の rproxy にはノードごとに
PUTする。rproxy-gateway は使わない(Secret のボリューム)。
persist: trueのトークン(上の「API で作ったルールの保存」と同じ印。PUTの本文には足さない)でPUT /rulesets/{name}した組を、テーブルrproxy_rule_setsに 1 行(組のルールを JSON で)保存する。一度保存した組は、どのトークンで変えても・消しても行を書き直す・消す。persistのないトークン(Kubernetes のコントローラなど)の組は今までどおりメモリだけ。- 書くのは
PUTの応答の前(dry_runは書かない)。行のgenerationがこのPUTより新しければ書き換えない。DELETEで行を消す。書けなければ(DB に届かない、テーブルがない、max_allowed_packetを超える)組は動かしたままpersisted: false、ログにevent = "degraded"(part: "db")。保存したらruleset.persist(action: save/delete)。 - 表示:保存した組の
PUTの応答・GET /rulesets・GET /rulesets/{name}にpersisted(行が最新か)。保存しない組には付かない。 - 起動時:設定ファイル → UI の
forward_rules→rproxy_rulesの後に、自分のnodeの行を戻す(持ち主・generationも。etagは同じルールなら同じ値になる)。前に戻したルールとキー・待ち受けが重なるルールや、ファイルを読めないルールはその組から外して(restore.conflict/restore.skip)、残りを当てる(ruleset.restore)。/readyzは戻し終わってから ready。テーブルがなければ(migration 012 の前)何もしない。 - ノードの間では分けあわない(自分の
nodeの行だけ)。複数の rproxy には呼ぶ側がそれぞれにPUTする。rproxy-gateway は使わない(正は Kubernetes にあり、再起動の後はコントローラが PUT し直す)。 - 大きさ:
PUTの本文は 32 MiB までだが、MariaDB のmax_allowed_packet(既定 16 MiB)を超える組は書けない(persisted: false)。 - テーブル(UI リポジトリの
db/migrations/012_rproxy_rule_sets.sql):
CREATE TABLE IF NOT EXISTS rproxy_rule_sets (
node VARCHAR(255) NOT NULL, -- RPROXY_NODE_NAME (as in rproxy_rules)
name VARCHAR(253) NOT NULL, -- the rule set's name
generation BIGINT UNSIGNED NOT NULL,
etag VARCHAR(64) NOT NULL,
owner VARCHAR(255) NOT NULL, -- the owning token's name
rules JSON NOT NULL, -- the set's rules, each in the shape of the body of POST /rules
spec_version INT UNSIGNED NOT NULL DEFAULT 1,
updated_by VARCHAR(255) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (node, name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
GRANT SELECT, INSERT, UPDATE, DELETE ON rproxy.rproxy_rule_sets TO 'rproxy'@'%';
GRANT SELECT ON rproxy.rproxy_rule_sets TO 'rproxy_ui'@'%';global の項目のうち設定ファイル(と環境変数)に書いていないものを、制御 API で変える(UI の設定の画面向け)。固定の値はファイル、変わる値は API に分ける。
- ファイル(と環境変数)に書いた項目はファイルが勝つ:その項目は API で変えられない(
409 static、itemsにファイル・環境変数のものになっている項目)。固定のルールと同じ考え方。RPROXY_CONFIGがディレクトリなら、globalを書いたファイル(どれか 1 つ)の項目がファイルのもの。 - 項目と変え方:
| 項目 | 変え方 | Unix ソケットだけ | ファイル・環境変数 |
|---|---|---|---|
trusted_proxies |
live(新しいリクエストから) |
○(広げると、アドレスで守るルールを X-Forwarded-For で抜けられる) |
global.trusted_proxies |
access_log |
live(次の行から。前のファイルは書き終えてから閉じる) |
○(ファイルを書く) | global.access_log |
crowdsec |
live(新しい bouncer が LAPI から最初に取れてから入れ替える。取れなければ今のまま) |
○(キーのファイルを読んで LAPI に送る) | global.crowdsec |
http.timeouts |
live(新しい接続から) |
global.http.timeouts |
|
udp_max_sessions |
live(新しいセッションから。今のセッションは切らない) |
RPROXY_UDP_MAX_SESSIONS |
|
cache_max_memory |
live(次に持つ答えから。下げても持っている答えは期限まで残る。v0.4.19、#349) |
RPROXY_CACHE_MAX_MEMORY |
|
acme |
restart(API では変えない。後の版で、#345) |
○ | global.acme |
geoip |
restart(同上) |
global.geoip |
|
performance |
restart(workers・udp_shards・CPU の固定・busy_poll_usecs・splice・xdp) |
global.performance、RPROXY_WORKERS など |
|
files |
restart |
global.files、RPROXY_FILES_TRUSTED_DIRS |
|
control_api |
restart(制御 API の待ち受け) |
RPROXY_API_ADDR・RPROXY_API_PORT・RPROXY_API_SOCKET |
GET /config/global(admin):{"items":{"trusted_proxies":{"value":[...],"source":"api","apply":"live","unix_only":true,"settable":true,"api_value":[...],"shadowed":false},...},"storage":"database","persisted":true,"loaded":true,"updated_by":"ui","updated_at":1790000000,"warnings":[]}。valueは今効いている値(restartの項目は起動したときの値。手で書いた値で秘密を含みうるので、acmeは形だけ(文字列はすべて***。キー・数・真偽は残す)、ほかのrestartの項目もヘッダの値・body・名前にsecret・token・key・password・credential・eabを含むもの(ファイルの場所も)・URL の利用者とクエリを***にする。api_valueも同じ。crowdsecのlapi_url・appsec_urlは利用者・クエリ・#以降を伏せる、#347)、sourceはfile/env/api/default、settableは今 API で変えられるか(Unix ソケットだけの項目も true。unix_onlyを見る)、api_valueは API の値(あるときだけ)、shadowedは API の値があるがファイル・環境変数が勝っていて効いていない、errorは戻した API の値を効かせられなかった理由(効いていないが保存したまま)。crowdsecのkept: trueは、ファイルから消えたがルールが使っているので残しているもの(sourceはfile)。loadedは保存した値を読めたか(下の「保存」)。PATCH /config/global(admin):本文は設定ファイルのglobalと同じ形({"trusted_proxies":["10.0.0.0/8"],"http":{"timeouts":{"idle":"5m"}},"udp_max_sessions":4096})。名前を挙げた項目だけを、項目ごと丸ごと置き換える(http.timeoutsの中も丸ごと)。nullで API の値を消す(既定に戻る)。[]・{}は値として入る(trusted_proxies: []は「信じる前段なし」を API で決めたもの、http: {"timeouts": {}}はすべて既定の値を API で決めたもの)。答えはGETと同じ形にchanges([{"item","before","after","api_before","api_after"}])とapplied(効いている値が変わった項目)。?dry_run=trueは何も変えずに{"dry_run":true,"changes":[...],"applied":[...]}(ファイルを開かず、bouncer も動かさない。形・パス・ルールが使うかは確かめる)。- 断るとき:
409 static(ファイル・環境変数の項目)、409 restart_only(restartの項目)。どちらもitems。400 invalid:知らない項目、形や値の誤り、ないaccess_logのディレクトリ、書けないアクセスログ、読めない CrowdSec のキーのファイル。409 in_use:ルールが使っているcrowdsecを消す、appsecを使うルールがあるのにappsec_urlを外す。403 protected_file:パスが証明書のストア・ACME の保存先・rproxy の秘密(トークンファイルなど)・/procなどを指す。アクセスログが下の場所の外。403 forbidden:Unix ソケットだけの項目を TCP から(dry run も同じ。誤りの文は「Unix ソケット(RPROXY_API_SOCKET)からだけ」)。この守りはPOST /config/reloadなどと同じRPROXY_API_RELOAD_UNIX_ONLYで、falseにすると TCP からも変えられる(勧めない)。event=auditにoutcome: forbidden・code: unix_only。502 crowdsec_unavailable:新しい LAPI が 10 秒のうちに答えない(今の bouncer のまま、何も変えない)。そのためcrowdsecの PATCH は最大 10 秒かかり、その間ほかのglobalの変更(PATCH・設定ファイルの読み直し)は順番を待つ。503 not_loaded:起動のときに保存した値を読めなかった(DB に届かない、行が読めない形)。書けば保存したものを消してしまうので、定期の照合が読めるまで断る(読めない形の行は直すまで)。503 upgrading/shutting_down:引き継ぎ・終了の途中。
- パス(
access_log・crowdsec.api_key_file)は絶対パス。API で入れるアクセスログはRPROXY_API_ACCESS_LOG_DIRのすぐ下のファイルだけ(設定しなければ API では入れられない。下のディレクトリ・..・シンボリックリンク・.で始まる名前・拡張子が空の名前(x.)は不可)。日ごとに<名前>.<日付>.<拡張子>で書き、古いもの(同じ名前で始まり同じ拡張子で終わるファイル)をRPROXY_LOG_KEEPまで残して消すので、そのディレクトリは rproxy のアクセスログだけのものにする。消してしまうもの(<名前>.<YYYY-MM-DD>.<拡張子>の形でない、名前で始まり拡張子で終わるファイル)があるときと、rproxy 自身のログ(RPROXY_LOG_FILE)と名前が重なるときは断る。確かめるのは PATCH のときで、その後にディレクトリに置かれたものまでは守らない(ディレクトリは rproxy だけが書けるようにしておく)。 event=audit(action: config.global。dry run もdry_run: trueで)とconfig.global(項目・効いた項目)に残る。
- 断るとき:
- 入れ替え:live の項目はまとめて 1 つの写しとして入れ替わり、1 つのリクエストは始まったときの写しを最後まで使う(いくつかの項目を変える PATCH の途中の組み合わせは見えない)。CrowdSec を使うルールの作成・変更と
crowdsecの変更はルールの表の鍵の下で順に行うので、crowdsecを使うルールがcrowdsecなしで動くことはない。 - 保存:DB(
RPROXY_DATABASE_URL)があれば、応答の前にテーブルrproxy_globalに自分のnodeの 1 行(API の値を JSON で)を書く(persisted)。書けなければ(DB に届かない、テーブルがない)値は効かせたままpersisted: false、degraded(part: "db")、定期の照合(RPROXY_PERSIST_CHECK_SECS)が書き直す。行は項目ごとに読み、この版が知らない項目(後の版のacmeなど)や読めない値は効かせずにそのまま残して書き戻す(spec_versionはrproxy_globalだけの番号)。- 起動のときは
forward_rules・rproxy_rulesのルールより先に読んで効かせる(restore.global。ルールのcrowdsecが要るため)。戻した値も API の値と同じく確かめ(守るファイル・アクセスログの場所)、断ったもの・作れないものは効かせずGETのerror(degraded、part: "global")。戻したcrowdsec(起動のとき・引き継ぎ・定期の照合が後から読んだとき)は最初の取得を 2 秒まで待ち、取れなくても入れる(LAPI が答えなければ、起動はルールを戻す前に、照合もそれぞれ最大 2 秒遅れる。設定ファイルのcrowdsecを起動のときに入れるのと同じく、取れるまでの判定はルールのon_error。L4 のcrowdsecは通す)。 - 読めなければ(DB に届かない、行が読めない形)
loaded: falseで、PATCHは503 not_loaded、行も書かない。定期の照合が読めたら効かせる。そのあいだcrowdsecがなくて起動できなかったルールは、crowdsecが効いたときに動かし直す(PATCH・読み直しで入ったときも同じ)。 - DB がなければ値はこのプロセスの中だけ(
storage: "memory"、再起動で消える)。行が正になることはない(書くのは rproxy だけ。照合は書き直すだけ)。
- 起動のときは
- 引き継ぎ(
/admin/upgrade)では、API の値(知らない項目も)・書いた人・時刻・persisted・読めていないこと・残しているcrowdsecを新しいプロセスに渡し、ルールより先に効かせる。v0.4.19 より前の版への引き継ぎ(戻すとき)では API の値は渡らない(その版は知らない。DB の行は残るので、v0.4.19 以後に戻せばまた読む)。 - 設定ファイルを読み直したとき:
- API の値がある項目がファイルに現れたらファイルが勝つ:API の値は残すが効かず、
GET /config/globalにshadowed: trueとwarnings({"code":"config.global_shadowed","item","message"})、ログにconfig.global_shadowed(1 回)。ファイルからまた消えたら API の値が効く。 - live の項目は、ルールを確かめてから入れ替える(ルールの誤りで読み直しを断るときは何も変えない)。live の項目の誤りも読み直しを断る。
- 書けないアクセスログは主のログに出し(
degraded)、設定ファイルを確かめるたびに試し直す。 - 新しい
crowdsecの LAPI が答えなければ今のものを残す(config.global_kept、GETのerror。次の読み直しでまた試す)。読み直しが LAPI を待つあいだ、ほかのglobalの変更は順番を待つ(config.reloadの行)。ルールが使うcrowdsec(appsecを使うルールがあればappsec_urlも)をファイルから消したときは、使うルールがなくなるまで残す(config.global_in_use、GETのkept、ほかの項目の PATCH でも残り、引き継ぎでも渡す)。使うルールがなくなった後の読み直しで外す。
- API の値がある項目がファイルに現れたらファイルが勝つ:API の値は残すが効かず、
- テーブル(UI リポジトリの
db/migrations/013_rproxy_global.sql):
CREATE TABLE IF NOT EXISTS rproxy_global (
node VARCHAR(255) NOT NULL, -- RPROXY_NODE_NAME (as in rproxy_rules)
spec JSON NOT NULL, -- the values set through the API, in the shape of global
spec_version INT UNSIGNED NOT NULL DEFAULT 1,
updated_by VARCHAR(255) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (node)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
GRANT SELECT, INSERT, UPDATE ON rproxy.rproxy_global TO 'rproxy'@'%';権限が要るのは rproxy のノードの DB ユーザーだけで、自分の行に SELECT・INSERT・UPDATE(複数の rproxy では UI の db/node-view.mjs が作るノードごとのビュー経由)。UI はこのテーブルを読まない(値は GET / PATCH /config/global で読み書きする)ので、UI の DB ユーザーには権限を与えない。
VIP のように、今は別のホストが持っていて後からこのホストに来るアドレスで待ち受ける(Kubernetes の fleet で、VIP ごとに別の Gateway が同じポートを使う形。rproxy-gateway の docs/DESIGN-v0.4.x.md 7.)。
- ルールに
"listen_freebind": true。ソケットをIP_FREEBIND(IPv6 はIPV6_FREEBIND)で開くので、アドレスがインタフェースになくても bind でき、ルールはrunningになる。アドレスが足されたら、そのアドレスに来たものから届く(ルールを作り直さない)。外されても待ち受けはそのまま。 - Linux だけ(ほかでは
features.listen_freebindが false で、使うと400 unsupported)。権限は要らない(IP_TRANSPARENTと違いCAP_NET_ADMINは要らない)。net.ipv4.ip_nonlocal_bind=1のホストでは付けなくても bind できる。 - 付けないルールは今までどおり:ホストにないアドレスは bind に失敗し、
failed(bind_failed)。打ち間違えたアドレスを黙って受け付けないように、自動では付けない。 - UDP:特定のアドレスで待ち受けるので、返信はそのアドレスから出る(
IP_PKTINFOは要らない)。HTTP/3(http.http3)の QUIC のソケットも同じく開く。 - PATCH では変えられない(ソケットを開き直すため。違う値は
unsupported、同じ値か省けば受け付ける)。組の PUT と設定ファイルの再読み込みでは作り直し(change: recreate)。 - 同じポートの 2 つのルール:特定のアドレスどうし(
192.0.2.10:443と192.0.2.11:443)は並べられる(キーがlisten_addrを含む)。0.0.0.0/::のルールはそのポートをすべてのアドレスで取るので、同じポートの特定のアドレスのルールとは並べられない(どちらを先に作っても後のものが409 already_exists。SO_REUSEADDRで「より狭いアドレスが勝つ」形にはしない:どちらに届くかがアドレスの有無で変わり、取り合いが見えなくなるため)。誤りの文に重なる相手のルールと、組のルールならその組の名前が出る(tcp/0.0.0.0:443 overlaps with tcp/192.0.2.10:443 (rule set k8s/a/web): a rule on 0.0.0.0 / :: takes the port on every address; ...)。 - 実経路は
scripts/test-freebind.sh(名前空間。CI の transparent のジョブ)で確かめる。
L4 の tcp のルールの connect_timeout(上の表)。止まったノードの宛先は SYN に答えないので、断られる(RST)のと違い、接続は OS の再送(Linux で約 2 分)か、ほかに宛先があるときの 5 秒まで待つ。connect_timeout でこれを短くすると、そのあいだに来た接続も早く次の宛先に移り、宛先は outlier_detection で外れる(既定は 1 回で 10 秒)。
- 宛先が 1 つ・複数のどちらでも効く(複数のときは 5 秒の代わり)。
tls.routesの宛先も同じ。 - 短すぎると、混んだ宛先・遠い宛先を落ちたとみなす。同じクラスタの中なら
1s〜3sを目安に(SYN の最初の再送が 1 秒後)。 httpのルールは今までどおりサービスのtimeouts.connect(既定 5 秒)。
前段(ロードバランサ・別の rproxy・Kubernetes の Gateway など)が PROXY protocol v1 / v2 のヘッダを付けて送ってくるとき、tcp のルールの proxy_protocol でヘッダを読み、そこに書かれたアドレスを本当のクライアントとして扱う。PROXY v2 で受けて PROXY v2 で送る(source_ip: proxy_v2)こともできる。
{"protocol": "tcp", "listen_addr": "0.0.0.0", "listen_port": 25,
"remote_addr": "10.0.0.25", "remote_port": 25, "source_ip": "proxy_v2",
"proxy_protocol": {"from": ["10.2.0.0/16", "172.16.10.19/32"], "required": true}}- 信じるのは
fromの接続元(TCP の相手のアドレス、すぐ前の段)が送るヘッダだけ。ヘッダの中のアドレスでは判定しない。fromは空にできず、0.0.0.0/0・::/0もinvalid(誰のヘッダでも信じる設定は作れない)。 fromの接続元からは、接続の最初にヘッダを読む(v1 か v2 かは先頭のバイトで見分ける)。required: true(既定)なら、ヘッダがなければ断る。falseなら、ヘッダがなければ接続元のアドレスのまま。壊れた・長すぎる・届き終わらないヘッダはどちらでも断る。fromの外からの接続:ヘッダは読まず、ふつうの接続として扱う。ただし、ヘッダらしきもの(v2 の署名\r\n\r\n\0\r\nQUIT\nかPROXY)で始まる接続は断る(fromに前段を書き漏らしたことに気づけるように。そのヘッダがデータとして転送先に届くこともない)。クライアントから先に送るルール(TLS のsni・terminate、http)では最初のバイトを待って確かめる。サーバから先に送ることのある平文の TCP と STARTTLS では待たず、受け付けたときと転送先につながったとき(平文の TCP)にもう届いているバイトだけを見る。- 大きさの上限:v1 は 107 バイト(仕様の上限)、v2 は固定の 16 バイトの後が 4096 バイトまで(アドレスと TLV を合わせて。前段が付ける TLV はずっと小さい)。ヘッダは受け付けてから 5 秒以内に届き終わること(ゆっくり送る相手に接続を持たせ続けない。変えられない)。
- v2 の TLV(ALPN・SNI・TLS の情報など)は読み飛ばす(転送先へは rproxy 自身が見たものを書く)。v2 の
LOCAL(前段のヘルスチェック)、v1 のUNKNOWN、v2 のAF_UNSPEC・AF_UNIXは接続元のアドレスのまま。TCP の接続に UDP(DGRAM)のヘッダが来たら断る。 - ヘッダのクライアントを使うところ:
allow_from・geoip・crowdsec・limits(送信元ごと)・bandwidth(送信元ごと)、ログのclient(conn.open・conn.close・conn.error・conn.denied・conn.limited・http.access)、httpのルールのX-Forwarded-For・X-Real-IP(global.trusted_proxiesで信じるかもこのアドレスで決める)、転送先へのsource_ip(proxy_v1/proxy_v2はヘッダのクライアントを書き、transparentはヘッダのクライアントを送信元にする。戻りの経路の前提は今までと同じ、docs/TRANSPARENT.md)。転送先へのヘッダの宛先のアドレスは、今までどおり rproxy が受けたアドレス。 - 前段(ヘッダを送ってきた接続元)のアドレスとポートは、ログの
proxy_peerに残す(ヘッダのない接続では省く)。 - 断った接続は
stats.deniedに数え、conn.deniedをwarnで出す:reason: proxy_protocol(fromの前段からのヘッダがない・壊れている・遅い。clientは前段、errorに理由)、reason: proxy_protocol_untrusted(fromの外からのヘッダ。versionにv1/v2)。 required: falseのルールに、サーバから先に送るプロトコル(SMTP・MySQL など)でヘッダなしの接続が来ると、ヘッダを待つ 5 秒の後に通す。前段が必ずヘッダを付けるならrequired: trueのままにする。- HTTP/3(
http.http3、UDP)にはヘッダがなく、QUIC の接続は接続元のアドレスで扱う。udpのルールのデータグラムごとのヘッダは下の「udpのルール」。 - PATCH で変えると次の接続から効く(接続は切らない)。組の PUT・設定ファイルの再読み込みでもその場で変わる(
change: in_place)。
多段の構成(外側の rproxy → 内側の rproxy → 転送先)で、UDP でもクライアントのアドレスを最後まで保つ。外側の source_ip: proxy_v2 はデータグラムごとに PROXY v2(DGRAM)のヘッダを前に付けて送る。内側の udp のルールの proxy_protocol がそれを読み、外してから転送先へ送る。
{"protocol": "udp", "listen_addr": "0.0.0.0", "listen_port": 443,
"remote_addr": "10.0.0.43", "remote_port": 443, "source_ip": "proxy_v2",
"proxy_protocol": {"from": ["10.2.0.0/16"]}}from・requiredの意味は TCP と同じ。違うのは、ヘッダが接続の最初に 1 回ではなく、fromの前段からのデータグラムの 1 つ 1 つの前に付くこと(rproxy 自身のsource_ip: proxy_v2が送る形)。fromは外から偽れない網にいる前段だけにする(#332)。UDP には接続(TCP のハンドシェイク)がないので、rproxy は前段をデータグラムの送信元の IP だけで信じる。fromのアドレスを名乗ったデータグラムを外から届けられる網(送信元の偽装を止めていない網、インターネットに面したアドレス)では、だれでも好きなクライアントを名乗れる(allow_from・geoip・crowdsec・limits・ログ・転送先へのsource_ipがそのアドレスになる)。前段と rproxy の間は、外からのそのアドレスのパケットを捨てる私的な網(同じ L2・VPC の中、境界で送信元の検査(BCP 38)をする網)にする。- 受け付けるのは v2 の DGRAM(UDP over IPv4 / IPv6)だけ。v1(UDP の形がない)、STREAM(TCP)のヘッダ、壊れたヘッダ、データグラムの中で終わらないヘッダは捨てる。大きさの上限は TCP と同じ(固定の 16 バイトの後が 4096 バイトまで)。v2 の
LOCAL・AF_UNSPECは前段のアドレスのまま。TLV は読み飛ばす。 required: true(既定)なら、fromからのヘッダのないデータグラムは捨てる。fromの外からヘッダ(v2 の署名かPROXY)で始まるデータグラムも捨てる。捨てた数はstats.denied、ログはconn.denied(warn、reason: proxy_protocol/proxy_protocol_untrusted、clientは前段)。ログは送信元ごとに間引く(ほかの UDP の拒否と同じ。省いた数は次の行のsuppressed)。- セッションは(前段のアドレス・ポート, ヘッダのクライアント)ごと(ワイルドカードの待ち受けでは受けたアドレスごとにも分かれる)。返事は、データグラムが来た前段のアドレス・ポートへ、ヘッダを付けずに返す(外側の rproxy はセッションごとのソケットで受けて、クライアントへ返す)。同じ前段のポートからのヘッダが別のクライアントを書いてきたら、別のセッションになる(前のセッションは
udp_idle_secsで終わる)。 - ヘッダのクライアントを使うところは TCP と同じ:
allow_from・geoip・crowdsec・limits(送信元ごとのセッションとデータグラム)・bandwidth(送信元ごと)、ログのclient(前段はproxy_peer)、転送先へのsource_ip(proxy_v2はヘッダのクライアントを書き、transparentはヘッダのクライアントを送信元にする)とtls.routes[]のsource_ip(#297)。 tls.mode: sni(DTLS・QUIC)は、ヘッダを外してから名前を読む。同じソケットからの別の名前の新しい QUIC の接続でセッションを作り直すのも同じ。tls.mode: terminate(DTLS の終端)も、ヘッダを外したデータグラムで DTLS を話す。proxy_protocolのないルールの道筋は変わらない(データグラムのまとまりごとに 1 回、値を読むだけ)。- DPDK の道(
global.performance.dpdk)はヘッダを読まない:DPDK のポートのアドレスで待ち受けるudpのルールにproxy_protocolを付けると、どの道でも断る:作成はbind_failed、PATCH(dry_runも)は同じ誤り、組の PUT と設定ファイルの再読み込みはその場で変えずに作り直すので、そのルールは同じ誤りでfailedになる。 httpのルールの HTTP/3(quinn の UDP)は範囲の外で、今までどおりヘッダを読まない(QUIC の接続は接続元のアドレス)。- PATCH で付ける・外すと、次のデータグラムのまとまりから効く(ルールは止めない、
change: in_place)。
1 つのポートで、名前ごとに転送先へのクライアントのアドレスの渡し方を変える。例::443 の http のルールはそのまま L7 で扱い、Kubernetes へそのまま流す名前だけ PROXY v2 を付ける。
{"protocol": "tcp", "listen_addr": "0.0.0.0", "listen_port": 443,
"tls": {"mode": "terminate", "certificates": [{"cert_file": "/etc/rproxy/tls/www.pem", "key_file": "/etc/rproxy/tls/www.key"}],
"routes": [{"server_names": ["**.tenant.example.com"], "passthrough": true,
"remote_addr": "10.2.255.131", "remote_port": 443, "source_ip": "proxy_v2"}]},
"http": {"routes": [{"name": "www", "match": "Host(`www.example.com`)", "service": "www"}],
"services": {"www": {"servers": [{"url": "http://10.0.0.80:8080"}]}}}}- route の
source_ipは、ルールのsource_ipより先に使う。省けばルールのもの(今までどおり)。一覧・GET /rulesには書いたときだけ出る。 - 書ける route:TLS を終端せずに L4 で転送先へつなぐもの。
terminateのルール(httpのルールを含む)のpassthrough: trueの route と、sniのルール(tcp・udp)の route。rproxy が終端する route(passthroughでないterminateの route)に書くと400 unsupported(ルールのsource_ipを使う)。 - 値:
proxy(ヘッダなし)・proxy_v1・proxy_v2。udpのsniのルールの route ではproxy_v1はunsupported(ルールと同じ。proxy_v2はデータグラムごとの DGRAM のヘッダ)。 transparentは route ごとには選べない(400 unsupported)。IP_TRANSPARENTのソケット・ポリシールーティング・転送先からの戻りの経路の準備はルールの単位で、名前ごとに変えると戻りの経路が合わなくなるため。transparentのルールの route にproxy・proxy_v1・proxy_v2を書くと、その route の転送先へは rproxy のアドレスからつなぐ。httpのルールそのものはX-Forwarded-Forで渡す(ルールのproxy_v1/proxy_v2は今までどおりinvalid)が、その passthrough の route には付けられる。- ヘッダの中身はルールの
source_ipと同じ:送信元はクライアント(proxy_protocolで受けた接続ではヘッダのクライアント)、宛先は rproxy が受けたアドレス。終端していないので PROXY v2 の TLS の TLV は付けない。 - PATCH(
tlsを送る)・組の PUT・設定ファイルの再読み込みで、接続を切らずに変わる(change: in_place、次の接続から)。ルールのsource_ipは今までどおり変えられない(作り直し)。
ルールのファイルの項目(tls と http のすべての *_file:証明書・鍵・チェーン・CA(client_auth・upstream・routes[].upstream・サービスの tls)、basic_auth.users_file、oidc.client_secret_file・cookie_secret_file・ca_file)を、API(POST / PATCH /rules・PUT /rulesets/{name}、dry_run も)からは次のように制限する。rules:write のトークンが、保存した証明書や ACME の鍵で TLS を終端したり、秘密を自分の用意した issuer に送らせたりできないように。
- 証明書のストア(
RPROXY_CERT_STORE)と ACME の保存先(global.acme.storage)の下、設定とオプションの秘密のファイル(global.crowdsec.api_key_file、global.acmeの DNS のプロバイダのapi_key_file・secret_file・tsig_secret_file・credentials_file・EAB のhmac_key_file・アカウントのkey_file、RPROXY_TOKEN_FILE、RPROXY_TLS_KEY、RPROXY_DIFF_TOKEN_FILE)は指せない:403 protected_file。トークンのスコープ(acme:write・admin)に関係なく断る。保存した証明書は{"cert": "<name>"}、ACME の証明書は{"acme": ...}(acme:write)で使う。- パスは書いたとおりと実際に行き着く先の両方で比べる:ファイルを開いて(読まずに、
O_PATH)その fd の本当のパス(/proc/self/fd)と inode を見るので、シンボリックリンク・..では回り込めない(秘密のファイルはハードリンクも inode で見つける。ストア・ACME の保存先は、行き着く先のディレクトリのどれかがその(デバイス, inode)なら同じとみなすので、bind mount で別の場所に見せても断る)。まだないパス(後で保存される証明書)は、ある親のディレクトリまでたどって比べる。 - カーネルのファイルシステム(
/proc・/sys・/devの下と、procfs・sysfs・devtmpfs などの上のファイル。statfsの種類で見る)も断る:/proc/self/environ(DB のパスワードなどの環境変数)・/proc/self/fd/N(開いているファイル)は rproxy のもので所有者の確かめを通ってしまうため。 - 秘密のファイルはそのファイルだけ(同じディレクトリのほかのファイルは指せる。既定の
/etc/rproxyにはトークンとルールのファイルが並ぶため)。
- パスは書いたとおりと実際に行き着く先の両方で比べる:ファイルを開いて(読まずに、
allow_certsのあるトークン(保存した証明書の一部に絞ったトークン)は、ルールにファイルのパスを書けない:403 file_not_allowed。証明書はallow_certsの内の{"cert": "<name>"}で使う。- OIDC の client secret のファイル(rproxy が issuer に送る)は、同じ
issuer(末尾の/を除く)とclient_idの client secret としてだけ使える。ほかのルール(設定ファイル・API・組)を含めて、cookie の秘密・別の issuer や client の client secret・users_file・鍵などにも使っていれば409 secret_reuse(同じ client を複数のルールで使うのはよい)。PUT /rulesetsでの断りも/rulesと同じくevent: audit(outcome: forbidden、code)に残る。誤りの文はこの変更のルールのことだけで、ほかのルールのキー・項目・パスは出さない。確かめは 1 つずつで、通った変更のファイルはその変更が終わるまでほかの確かめにも数える(同時の 2 つの変更が両方通ることはない。変更そのもの(名前解決など)は待たせ合わない)。 - 設定ファイルのルール(
origin: static)は今までどおり(ホストの管理者のもの)。--check-configが同じものを警告にする(... is under the certificate store ...; allowed in the settings file, but the same rule from the API is refused (403 protected_file)、409 secret_reuse)。 - 前の版で API から作ったルール(DB・組の保存から戻すもの、DB との照合、引き継ぎ)は断らずに動かし、守るファイルを指していれば
rule.protected_file(warn。ルールと項目だけで、パスは出さない)をログに出す。そのルールの API での変更(PATCH)は、PATCH で出来上がるルール全体で確かめるので、宛先だけの PATCH でも守るファイルを指したままなら403 protected_file(ファイルを置き換える PATCH は通る)。 oidcのセッションの cookie は、cookie の名前に加えてissuerとclient_idにも結びつける(AES-256-GCM の関連データ)。同じ cookie の秘密を使うほかのoidcのミドルウェアの cookie は通らない。この版に上げると、今のセッションは一度だけサインインし直しになる。
ルールの鍵・秘密のファイル(tls と http の key_file(証明書の鍵・サービスのクライアント証明書の鍵)・client_secret_file・cookie_secret_file・users_file)は、ほかの持ち主のルールが指していれば、API(POST / PATCH /rules・PUT /rulesets/{name}、dry_run も)からは指せない:409 file_in_use。ルールの組やポートに絞ったトークンが、別のチームのルールの鍵で自分のポートの TLS を終端して、そのサイトになりすませないように。証明書・チェーン・CA のファイルは公開のものなので共有してよい。
- 持ち主:API で作ったルールは作ったトークン(
origin: "api"はcreated_by。dynamicは rproxy が覚える。ほかのトークンが PATCH しても持ち主は変わらない)、組のルールは組の持ち主、設定ファイルのルール(origin: static)は設定ファイル、UI のforward_rulesから戻したルール(作ったトークンが分からない)はどのトークンのものでもない。 adminのスコープのトークン(トークンファイルがないときも)は断らない。同じトークンは自分のほかのルール・組と共有できる。- 同じファイルかは inode で比べる(ハードリンク・シンボリックリンク・
..でも同じ)。中身が同じでも写しは別のファイル。 - PATCH・組の PUT は、置き換えるルールが今指しているファイルを指したままにできる(新しく足すファイルだけを比べる。宛先だけの PATCH は通る)。
- 誤りの文はこの変更のルールの項目とパスだけで、相手のルール・持ち主・項目は出さない。
- 前の版で作られ、すでに持ち主の違うルールと同じファイルを指しているルール(DB・組の保存から戻すもの、DB との照合、引き継ぎ)は断らずに動かし、
rule.shared_file(warn。rule・field・other_rule・other_field・phase、パスは出さない)を組み合わせごとに 1 回出す。 - 作ったトークンは引き継ぎ(SIGUSR2)で渡す。再起動の後、
origin: "dynamic"のルール(forward_rulesから戻る)は作ったトークンが分からないので、どのトークンのものでもない。 - UI は #155 からルールにファイルのパスを書かせないので、UI からの操作には関わらない。制御 API を直接使う複数のトークン(rproxy-gateway のコントローラ以外)の構成が対象。
DB(forward_rules・rproxy_rules)から戻すルール(と引き継ぎで渡ったもの)が検証に通らないとき、起動は止めず、GET /rules・GET /rules/{protocol}/{listen_addr}/{listen_port} に state: "failed"・error(理由)で出す(前の版はログだけだった)。
- 本文は保存されていたまま(省いた項目は新しいルールの既定)。
originはdynamic(forward_rules)かapi(rproxy_rules。persisted・created_by・created_atも)。conditionsはAcceptedがFalse(reason: Invalid)、ProgrammedがFailed。ログはrule.failed(phase: restore、照合ではreconcile、code)。 - 待ち受けは開かず、ポートも持たないが、キーは持つ:同じキーの
POST(組の PUT・設定ファイルのルールも)は409 already_exists。 DELETEで消せる(apiのものはrproxy_rulesの行も消す。forward_rulesは rproxy が書かないので、UI で直さなければ次の起動でまた出る)。PATCHで直せる:保存されていた本文に PATCH の項目を重ね(ある項目だけ。宛先は今までどおりまとめて置き換える)、新しいルールとして検証する。通らなければ400で何も変えない。通れば同じoriginで起動する(apiは行を書き直す)。動いていないのでsource_ip・listen_freebind・listen_port_endも変えられる。起動できなければ(ポートが使用中など)ほかの PATCH と同じくfailedのルールになる。dry_runも同じ(action: update、change: recreate)。- 引き継ぎ(SIGUSR2)では保存されていたままの本文を渡し、新しいプロセスでもう一度検証する(新しい版で通れば動く)。
- 照合(
RPROXY_PERSIST_CHECK_SECS):rproxy_rulesの行が変わればもう一度試し、行がなくなれば一覧から消す。 listen_addrが IP アドレスでない行は API で指せないので、今までどおりログ(rule.failed)だけ。- 鍵・秘密のファイルはそのルールの持ち主(
apiはcreated_by)のものとして数える(ほかのトークンは指せない、409 file_in_use。直す PATCH(そのルールを PATCH できるトークンならどれでも。動いているルールと同じ)は同じファイルを指したままにできる)。消すと持ち主も忘れる。 - 起動時・引き継ぎで戻すルールの組のルールと同じキーなら、組のルールを残し、このルールは一覧から外す(
restore.conflict。照合でも行はrestore.conflictのまま)。組がそのルールを入れなかったとき(この版で検証に通らない・ポートが重なるなど)は、このルールをそのまま残す。組のPUTでそのキーのルールを足すと、組の外のルールと同じく先に断る(409、errorsのalready_exists)。組が取ったキーの行のcreated_byも忘れる。 /metricsのrproxy_rules{state="failed"}に数え、rproxy_rule_upは 0。
ミドルウェア cache は GET の応答をメモリに ttl のあいだ持ち、その間の同じリクエストに転送先へ送らずに答える。多くのクライアントが同じ JSON を数秒おきに取りに来る構成(例:ワールドごとに 5 秒に 1 回取りに来て、元のデータは 1 秒ごとに新しくなる)で、転送先へのリクエストを減らすためのもの(ttl: 1s なら、1 秒ごとに最新を配りながら、転送先へはキーごとに 1 秒に 1 回)。
http:
routes:
- name: state
match: Host(`api.example.com`) && Path(`/state.json`)
service: app
middlewares: [gzip, micro]
middlewares:
gzip: {compress: {}}
micro:
cache:
ttl: 1s # 必須。100ms〜24h
stale_on_error: 30s # 既定 0s
max_body: 1048576 # 既定 1 MiB(64 MiB まで)
max_entries: 1024 # 既定 1024
max_memory: 67108864 # 既定 64 MiB
vary: [Accept-Language]
status: [200] # 既定 [200]
header: X-Cache # 既定 X-Cache。"" で付けない- キー:ルート(同じミドルウェアを書いたルートでも、ルートごとに別の答え)、受けた方式とポート(
http:80・https:443など)、ホスト(小文字・ポートなし)・パス・クエリ(前のミドルウェアが書き換えた後。転送先が受け取るもの)、varyに書いたリクエストのヘッダの値(Accept-Encodingは符号化の名前を小文字にして並べ替えて比べる)、tls.client_authのあるルールではクライアント証明書(rproxy が転送先へ渡すX-Client-VerifyとX-Forwarded-Client-Certの中身。証明書のないクライアントも 1 つの値)。メソッドは GET だけを持ち、HEAD は GET の答えから本文なしで答える(HEAD だけでは持たない)。 - 同時のリクエストをまとめる:持っていない(期限が切れた)キーに最初に来たリクエストだけが転送先へ行き、その間に来た同じキーのリクエストはその答えを待って同じものを受け取る。
- 最初のリクエストのクライアントが切れたら、待っていたもののうち 1 つが代わりに行く(3 回まで。その後はそれぞれ行く)。
- 待つのは 10 秒まで。過ぎたら待っていたものもそれぞれ転送先へ行く(持たない)。
- 答えが持てないもの(下)だったら、待っていたものはそれぞれ転送先へ行く。答えそのものが理由で持てない(
Cache-Controlのprivateなど・Set-Cookie・Vary: *・大きさ・text/event-stream)ことが 3 回続いたら、その後のttlのあいだそのキー(答えのVaryの値が同じリクエスト)は待たずに転送先へ行く(持てない答えの後ろに並ばないように)。statusにない状態コードや、遅い・切れた本文はリクエストのせいでも起きる(転送先が断るヘッダを 1 人が送るなど)ので数えず、数え直しにする(1 人のクライアントがほかのクライアントのキャッシュを止められないように)。持っている新しい答えはいつも先に返す。 - 転送先が 5xx を返し、古い答えもないときは、その誤りの答えを待っていたものにも返す(
MISS。持たない。次のリクエストはまた転送先へ行く。Set-Cookieなどのある誤りは返さず、それぞれ転送先へ)。
- 持つのはプロセスのメモリ(ノード間・引き継ぎで共有しない)。N 台なら転送先へはキーごとに最大 N 回 /
ttl。ルールのhttpを変えると空から(rate_limitと同じ)。max_entriesかmax_memory(本文とヘッダ)を超えるときは古いものから捨てる。ttlとstale_on_errorを過ぎたものも捨てる(/metricsの数にも入れない)。 - プロセス全体の上限(v0.4.19、#349、
features.cache_process_memory):すべてのcacheの持つ量(答えと、持てない答えの続いたキーの記録)の合計はRPROXY_CACHE_MAX_MEMORY(--cache-max-memory、既定 512 MiB = 536870912、0でなし)まで。max_memoryはミドルウェアごとなので、ルールを何本も作っても 1 つのプロセスのメモリを使い切れないように。入らない答えは、ほかのcacheの期限(ttlとstale_on_error)を過ぎた答えを捨て、次に同じcacheの古い答えから捨てて持つ。同じcacheの答えを全部捨てても入らないときは捨てずに持たない(答えはMISSで返す。ほかのcacheの答えは消さない)。PATCH /config/globalのcache_max_memoryで稼働中に変えられる(環境変数・引数で与えたら環境変数が勝つ)。下げても持っている答えは期限まで残り、新しい答えは入るときだけ持つ。/metricsのrproxy_http_cache_memory_bytes(合計)・rproxy_http_cache_memory_limit_bytes(効いている上限。0はなし)。- 同じキーの新しい答えが入らないときは、古い答えを残す(
stale_on_errorで返せるように)。 - ほかの
cacheの期限内の答えは取らない:ttlの長い・大きいcacheは期限まで上限を占められ、そのあいだほかのcacheは持てないことがある。rproxy_http_cache_memory_bytesを見て、ttl・max_memoryか上限を決める。 - 上限はプロセスごと:再起動なしの更新(引き継ぎ)の間は、古いプロセスと新しいプロセスがそれぞれの上限まで持つ(合わせて最大 2 倍。古いプロセスの分は drain で終わると戻る)。
- 同じキーの新しい答えが入らないときは、古い答えを残す(
- 応答には
X-Cache(header):HIT(メモリから)・MISS(転送先から。持ったかどうかは問わない)・STALE(期限切れのものを、転送先が落ちたので)・BYPASS(キャッシュの対象でないリクエスト)。HIT・STALEにはAge(持ってからの秒を転送先のAgeに足す)、MISSは転送先のAgeのまま。headerにはホップごと・本文の形・キャッシュのヘッダ(Connection・Transfer-Encoding・Content-Length・Age・Vary・Set-Cookieなど)は使えない。 stale_on_error:期限が切れた後この時間のあいだは、転送先が 5xx を返した・つながらない(502)・時間切れ(504)のとき、持っていた答えをSTALEで返す。落ちた後(先に行ったリクエストでも、待たずに 1 人で行ったリクエストでも)のttlのあいだは転送先へ行かずにすぐSTALEで答え、その後は 1 つのリクエストが試し、そのあいだのほかのリクエストもすぐSTALEで答える(落ちている間、どのクライアントも転送先の時間切れを待たないように)。- 転送先の
Cache-Controlのmax-age・s-maxage・Expiresは見ない(時間はttlだけで決める)。クライアントのCache-Control(no-cacheなど)も見ない(どのクライアントでも転送先に毎回送らせられないように)。 - 条件つきのリクエスト:転送先へ行くリクエストからは
If-None-Match・If-Modified-Sinceを外して全体を取り、If-None-Matchが持っている答えのETagに合えば 304 で答える(HITでもMISSでも。引用符の中の,も読む)。 - 本文を読む時間:持つために本文を読むのは 5 秒まで。それより長く続く本文(長いポーリング、ゆっくり流れるもの)は読んだ分から続けてそのまま流し、持たない。
text/event-streamは持たない。 - 持たないもの(決めたこと):
- リクエスト:GET・HEAD 以外(
BYPASS。POST などで持っているものは消さない:ttlが短い前提)、Authorization・Cookie・Proxy-Authorizationのあるもの・tls.client_authのあるルールでクライアント証明書を出したもの(share_credentials: trueで持つ:転送先の答えがそれで変わらないときだけ。証明書はshare_credentialsでもキーに入るので、証明書の違うクライアントには渡らない。利用者ごとに分けたいならvary: [Authorization]かvary: [Cookie]も)、Range・Upgrade・Expect(100-continueを送らせないように)・本文のあるもの(HTTP/2・HTTP/3 でContent-Lengthなしに DATA フレームを送るものも。ヘッダでほかの条件に合うときだけ、長さのないリクエストの本文があるかを 1 秒まで見る。空の DATA フレームで終わる HTTP/2 のストリームと、ヘッダで終わる HTTP/3 のストリームは本文なし)。 - 応答:
statusにない状態コード(選べるのは 200・203・204・300・301・308・404・410)、Set-Cookie、Cache-Controlのno-store・private・no-cache、Vary: *、Content-Range、text/event-stream、max_bodyより大きい本文(Content-Lengthで分かればすぐに、分からなければ読みながら。読んだ分から続けてそのまま送る)、5 秒で読み終わらない本文、トレーラー、途中で切れた本文(そのまま誤りとして送る)。 - 応答の
Varyの名前(とContent-Encodingのある応答のAccept-Encoding)は、答えたリクエストの値と一緒に持ち、値の違うリクエストには使わない(そのキーで持つのは 1 つ。よく違う値になるヘッダはvaryに書いてキーを分ける)。
- リクエスト:GET・HEAD 以外(
- 認証との組み合わせ(ルートの検証、
400 invalid):cacheより後ろに、通すかを決めるミドルウェア(ip_allow・geoip・crowdsec・basic_auth・forward_auth・oidc)を置けない(メモリから答えると通らないため)。前に置く。ルートのサービスの転送先ごとのforward_auth(servers[].middlewares)も同じ理由で断る。cacheより前に利用者を確かめるミドルウェア(basic_auth・forward_auth・oidc)があるときはshare_after_auth: trueが要る:転送先は利用者ごとに違う答えを返せる(X-Forwarded-Userなど)が、キャッシュは誰のものか区別しないので、通った利用者のだれにでも同じ答えを見せてよいときだけ。oidcはセッションのCookieが付くのでshare_credentials: trueも要る(ないとすべてBYPASS。--check-configが警告する)。- 認証のミドルウェアがクライアントへの応答に付けるもの(
oidcのSet-Cookie、gRPC のforward_authのresponse_headers_to_add)は、キャッシュが転送先の答えを持った後に、そのリクエストの応答にだけ付ける(持たない。ほかの利用者に渡らない)。 - 1 つのルートに
cacheは 1 つ。servers[].middlewaresには書けない。 - 後ろの
rate_limit・in_flightもメモリから答えたときは動かない(転送先を守るためのものなのでそれでよい)。前に置けばすべてのリクエストに効く。mirrorはメモリから答えたリクエストを、前でも後ろでも写さない(転送先へ行くものだけ)。
- ほかのミドルウェアとの順:
compressはキャッシュより前(外側)に置く([compress, cache])。メモリからの答えをそのクライアントのAccept-Encodingで圧縮するので、持つのは 1 つで済む。後ろに置くと、最初のクライアントのAccept-Encodingで圧縮したものを持ち(Vary: Accept-Encoding)、違うAccept-Encodingのクライアントでは持ち直しになる。cors・headersも前に置くと、メモリからの答えにもリクエストごとに働く。後ろのcors(とheadersのcors)は、許したオリジンでもそうでなくてもVary: Originを付ける(v0.4.19 から。オリジンごとに別の答えになる)。 - 統計:
stats.httpのcache(hit・miss・stale・bypassの合計)とルートごとのcache(ミドルウェアの名前ごと)。引き継ぎで数を渡す。/metricsのrproxy_http_cache_total{protocol,listen,route,middleware,result}(resultはhit・miss・stale・bypass)と、持っている数・バイトrproxy_http_cache_entries{protocol,listen,middleware}・rproxy_http_cache_bytes{protocol,listen,middleware}。アクセスログのcache(HITなど。cacheのあるルートだけ)。メモリから答えた 404 などはrefused_byに入れない。 - 共有のキャッシュ(Redis など)は持たない。転送先への回数を全体で絞る必要が出たら別の issue。
宛先が受けられる量まで使い、いっぱいになったら別の宛先へ回す。
{"protocol": "tcp", "listen_addr": "0.0.0.0", "listen_port": 5432, "balance": "failover",
"targets": [{"addr": "10.0.0.11", "port": 5432, "max_connections": 200},
{"addr": "10.0.0.12", "port": 5432, "max_connections": 200},
{"addr": "10.0.0.13", "port": 5432, "backup": true}]}- L4 の
targets[](tls.routes[].targets[]も)のmax_connections:その宛先へ同時に張る TCP の接続・UDP のセッションの数の上限(1 以上)。httpのサービスのservers[].max_connections:その転送先で同時に処理するリクエストの数の上限(応答の本文を送り終えるまで。WebSocket などの upgrade は接続が終わるまで。retryで次を待つ間は数えない。statusの転送先には書けない)。省けば上限なし(今までと同じ)。 - 上限はルールの宛先(
targets[]の 1 つ、route の宛先の 1 つ)・サービスの転送先ごと。同じアドレスの転送先をほかのルール・route・サービスでも使えば、それぞれが別々に数える(アドレスごとの合計ではない)。 - 上限に達した宛先は、
health_checkの down・outlier_detectionで外した宛先と同じく飛ばす(round_robin・least_conn・failoverのどれでも)。failoverと組むと、上から順に上限まで詰めて次へ移る(spillover)。backupの宛先は、ほかの宛先がすべて down かいっぱいのときに使う。 stickyの転送先がいっぱいなら、そのリクエストはほかの転送先へ送るが、クッキーは付け替えない(いっぱいだったのは一時のことなので、次のリクエストはまた元の転送先へ。元の転送先が down なら今までどおり付け替える)。- 数えるのと確かめるのは 1 つの不可分な操作で、同時の接続でも上限を超えない。接続・セッション・リクエストがどう終わっても(正常に閉じる・接続の失敗・リセット・ルールの停止・UDP のセッションのアイドル・別の宛先への移動)空く。
- 断るとき:up の宛先がすべていっぱいなら(down の宛先があっても、それは試さずに。
httpと同じ)、あるいは up の宛先がなく、すべていっぱいなら、新しいものを断る。TCP は何も送らずに閉じる、UDP はデータグラムを捨てる(セッションを作らない。次のデータグラムでまた試す)、httpは 503。up の宛先がないときに down の宛先を順に試すのは今までどおり(いっぱいでない宛先だけ)。 - 断った数は
stats.limitedと/metricsのrproxy_rule_limited_total{protocol,listen,reason="target_full"}、ログはconn.limited(reason: target_full、transportはtcp/udp/http。送信元ごとに間引く)。受け付けた時点ですべていっぱいなら(ふつうはそう)、TCP・UDP のstats.total_connectionsには数えない(TCP で、その確かめの後にいっぱいになったときとtls.routesの宛先のときは、接続として数える。数は減らさない)。CrowdSec のmax3584/rproxy-conn-limitedはtarget_fullを数えない(クライアントのせいではない)。 - 状態:
stats.targets[]にmax_connections(あるときだけ)とfull(いま上限か)。connectionsが上限に対して数える今の数。宛先が 1 つのルールでも、その宛先にmax_connectionsがあればstats.targetsを出す(ただし宛先が 1 つでhealth_checkもないルールの up / down は今までどおり報告しない:target.down/target.upのログ・all_targets_down・rproxy_rule_all_targets_down・rproxy_target_upは出ない)。tls.routes[]の宛先(targetsが 2 つ以上かmax_connectionsのあるもの)はstats.tls_routes:[{"server_names": ["a.example"], "targets": [...]}](tls.routesの順、targetsの中身はstats.targets[]と同じ形)。/metricsにrproxy_target_full{protocol,listen,target}(max_connectionsのある宛先だけ。1 / 0)、rproxy_target_connectionsとrproxy_target_fullにはrouteのラベル(route の宛先は route の名前を,でつないだもの、ルールの宛先は空のroute=""。v0.4.19 からルールの宛先のrproxy_target_connectionsにも付く)。httpはstats.http.services.<名前>[]にmax_connections・in_flight・full(max_connectionsのある転送先だけ。そのサービスはhealth_checkがなくても出す)。 - PATCH で宛先を置き換えても、同じアドレス・ポートの宛先には開いている接続の数を引き継ぐ(前の宛先の接続も上限に数える。PATCH で一度外して次の PATCH で戻した宛先も、外していた間の接続が残っていれば数える。
tls.routesは同じ名前の route どうしで、route の名前(server_name/server_names)を変えると引き継がない)。UDP のセッションは、自分の宛先が新しい宛先にも up で残っていれば移らない。httpのルールのhttpを変えると、処理中のリクエストの数は最初からになる(in_flightと同じ)。 - 数はプロセスごと。引き継ぎ(SIGUSR2)の間は、古いプロセスが終えるまでの接続と新しいプロセスの接続を別々に数える(その間は 1 つの宛先に上限の 2 倍まで届きうる)。
forward_auth・mirror・errorsのページへの問い合わせは、いっぱいの転送先を選ばないが、数には入れない。- DPDK の道(UDP)も同じく数えて飛ばす。
- 前の版に戻すとき:v0.4.18 以前は
max_connectionsを知らないので、それを書いたルールは読めない(API で作ったルールの DB からの復元・設定ファイル・引き継ぎで誤りになる)。戻す前に外す。
| メソッドとパス | 本文 | 成功時 | 説明 |
|---|---|---|---|
GET /healthz |
200 ok |
認証不要 | |
GET /capabilities |
200 | {"version":"0.4.0","source_ip":[...],"transparent":true,"transparent_ipv6":true,"tls_modes":["passthrough","sni","terminate"],"dtls":true,"starttls":["smtp","imap","pop3"],"max_range_ports":20000,"features":{"http":true,"http3":true,"acme":true,"tls_options":true,"middlewares":["redirect_scheme","redirect_regex","ip_allow","headers","strip_prefix","add_prefix","replace_path","replace_path_regex","respond","rate_limit","in_flight","crowdsec","compress","buffering","retry","circuit_breaker","errors","basic_auth","forward_auth","oidc","geoip","cors","mirror","replace_host"],"services":["health_check","sticky","balance","outlier_detection","protocol","tls"],"http_options":["headers_add","redirect_status","route_timeouts","server_middlewares","server_status","retry_status","misdirected"],"server_middleware_kinds":["headers","replace_host","strip_prefix","add_prefix","replace_path","replace_path_regex","cors","redirect_scheme","redirect_regex","mirror","forward_auth"],"forward_auth":["service","grpc","client_request","allow_status","forward_body","all_response_headers"],"tls_route_targets":true,"client_auth_modes":["none","optional","required","optional_no_verify"],"rulesets":true,"labels":true,"conditions":true,"readyz":true,"limits":true,"bandwidth":true,"geoip":true,"outlier_detection":true,"dry_run":true,"persistence":true,"client_cert_auth":true,"token_expiry":true,"api_lockout":true,"handoff":true,"self_update":true,"performance":["workers","udp_shards","cpu_affinity","busy_poll_usecs","splice","xdp"],"graceful_shutdown":true,"cert_store":true,"ruleset_persistence":true},"build":{"version":"0.4.0","sha256":"…"}}。version はこの rproxy-api のリリースの版(Cargo.toml の version。v0.3.18 から。それより古い版では含まれない。UI が組み合わせを確かめるのに使う)。features はこの版で動かせる v0.3・v0.4 の設定(上の「v0.3 の設定」「v0.4 の設定」。v0.4.0 ではすべて true、performance はすべての項目の名前)。source_ip の transparent は IP_TRANSPARENT が使えるときだけ含まれる。transparent_ipv6 は IPv6 の待ち受けで transparent を使えるか(IPV6_TRANSPARENT)。build は動いているバイナリ {"version","sha256"}(v0.4、#174。sha256 は起動の直後だけ null)。performance はカーネルでの転送(#260)の機能ごとの実際の状態 {"xdp": {"requested": "off", "active": false, "mode": null, "reason": null}}(起動時の試験の結果。上の「performance」) |
|
GET /openapi.json |
200 | この API の OpenAPI 3.0 の定義(docs/openapi.json と同じ)。どのトークンでも読める |
|
GET /config |
200 | 設定ファイル(RPROXY_CONFIG)の状態(上の「設定ファイル」)。global.crowdsec があれば crowdsec に LAPI との接続の状態(v0.3.20):{"connected":true,"synced":true,"last_success":1790000000,"last_error":null,"last_error_at":null,"failures":0,"decisions":12}。connected は最後の取得が成功したか、synced は一度でも取得できたか、failures は続けて失敗した回数、時刻は Unix 秒。rules:read |
|
POST /config/reload |
200 | 設定ファイルをその場で読み直して反映し、結果を返す:{"added","removed","changed","unchanged","failed","restart_needed":[...],"files":[...],"rules","warnings":[{"rule","message"}]}(?dry_run=true の答えには、効いている値が変わる global の項目の global_changes も)。誤りがあれば何も変えずに 400 {"code":"invalid","error","errors":[...],"warnings":[...]}(errors は --check-config と同じ検証の結果)。設定ファイルがなければ 409 no_config。admin のスコープが要る(トークンファイルを使っていなければ、ほかのエンドポイントと同じく誰でも使える)。既定では Unix ソケット(RPROXY_API_SOCKET)から来たリクエストだけを受け付け、TCP からは 403(RPROXY_API_RELOAD_UNIX_ONLY=false で TCP も受け付ける)。ファイルの変化の検知・SIGHUP と同じ処理で、同時には動かない。event=audit(action: config.reload)に残る |
|
GET /interfaces |
200 | 待ち受けに使えるアドレス:{"interfaces":[{"name":"ens18","addr":"172.16.5.1","family":"ipv4","loopback":false,"link_local":false}, ...],"reserved":[{"protocol":"tcp","addr":"127.0.0.1","port":8080,"purpose":"control API"}]}。動作中のインターフェースだけを返す。reserved は rproxy 自身が使うアドレスで、ルールには使えない |
|
GET /rules |
200 | ルールの配列 | |
GET /rules/{protocol}/{listen_addr}/{listen_port} |
200 | ルール 1 件 | |
POST /rules |
ルール | 201 | 転送を開始する。名前解決と bind まで済ませてから応答する |
PATCH /rules/{protocol}/{listen_addr}/{listen_port} |
{"remote_addr","remote_port" または "targets", "balance"?,"health_check"?,"udp_idle_secs"?,"tls"?,"starttls"?,"starttls_required"?,"allow_from"?,"proxy_protocol"?,"crowdsec"?,"extra_listen_addrs"?,"http"?} |
200 | 転送先を変える。extra_listen_addrs を付けると追加の待ち受けアドレスを丸ごと置き換える([] ですべて外す。省けば今のまま):足したアドレスだけを開き、外したアドレスだけを閉じる(ほかのアドレスと、外したアドレスで開いている接続はそのまま)。:: で待ち受けるルールで、追加のアドレスの有無(dual-stack と IPv6 だけ)が変わる変更は unsupported(作り直す)。転送先(remote_addr / remote_port か targets、balance、health_check)は毎回まとめて置き換える:省いた balance は round_robin、省いた health_check はなし。宛先 1 つに戻すときは remote_addr / remote_port を送る("targets": [] は付けてもよい)。crowdsec を付けると、判定での切断を有効・無効にする(次の接続から)。新しい接続から即時に反映する。tls を付けると TLS の設定を丸ごと置き換える(starttls も一緒に指定する。省略すると STARTTLS なし)。http を付けると L7 の設定を丸ごと置き換える(次のリクエストから。http のないルールに付けるのは unsupported。上の「v0.3 の設定」)。source_ip とポート範囲は変更できない:source_ip・listen_port_end を今と違う値で送ると unsupported(同じ値なら受け付ける。変えるときは作り直す) |
DELETE /rules/{protocol}/{listen_addr}/{listen_port}?drain_secs=N |
204 | 転送を停止する。既存の接続は即座に切断する。drain_secs を付けた場合は、その秒数だけ既存の接続の終了を待ってから切断する |
|
GET /acme |
200 | ACME の状態(docs/ACME.md):{"accounts":[{"name","directory","contact","eab","allowed_names","registered"}],"dns_providers":[{"name","type","zones","allowed_names"}],"resolvers":[{"name","account","challenge","dns_provider"}],"certificates":[{"resolver","domains","state","not_after","renew_at","next_attempt","error","ari"}],"rate_limit":{"orders","period_secs","used"},"helper"}。秘密も秘密のファイルの場所も出さない。rules:read。global.acme がなければ 404 |
|
POST /acme/renew |
{"resolver","domains"} |
202 | その証明書を今すぐ更新する(rate_limit の内で。結果は GET /acme)。acme:write。POST /config/reload と同じく既定では Unix ソケットからだけ(RPROXY_API_RELOAD_UNIX_ONLY)。どのルールも使っていなければ 404。event=audit(action: acme.renew) |
POST /acme/revoke |
{"resolver","domains","reason"?} |
200 | 取った証明書を CA で失効させ、すぐに新しい証明書を注文する。スコープと Unix ソケットは POST /acme/renew と同じ。event=audit(action: acme.revoke)。docs/ACME.md |
POST /acme/accounts/{name}/register |
200 | アカウントを CA に作る(鍵があればそのアカウントを探す)。スコープと Unix ソケットは POST /acme/renew と同じ |
|
POST /acme/accounts/{name}/deactivate |
200 | アカウントを CA で無効にし、鍵を <key_file>.deactivated に退ける(次の注文で新しいアカウントを作る)。スコープと Unix ソケットは POST /acme/renew と同じ |
|
GET /readyz |
200 / 503 | v0.4(#28):認証不要の readiness。200 {"ready":true} / 503 {"ready":false,"reason":"starting"|"draining"}(上の「ルールの組・状態・readiness」) |
|
GET /rulesets |
200 | v0.4(#28):ルールの組の一覧 [{"name","generation","etag","rules","updated_at","updated_by"}]。rules:read |
|
GET /rulesets/{name} |
200 | v0.4(#28):{"name","generation","etag","updated_at","updated_by","owner","rules":[...]}(ETag ヘッダも)。名前の / はそのまま書ける。rules:read |
|
PUT /rulesets/{name}?dry_run=true |
{"generation","rules":[<ルール>...]} |
200 | v0.4(#28):その組のルールを本文のとおりにする(作る・変える・消す)。If-Match が今の etag と違えば 412 precondition_failed、古い generation は 409 stale_generation、組に属さないルールと同じキーは 409 already_exists / static。どれかのルールの形が不正なら何も変えない(400、rules[i]: ...)。応答 {"name","generation","etag","dry_run","results":[{"rule","action","change","state","error"}]}。rules:write、各ルールは allow_listen_ports の内。組のルールを個別に PATCH / DELETE すると 409 owned。詳しくは上の「ルールの組・状態・readiness」 |
DELETE /rulesets/{name}?drain_secs=N |
204 | v0.4(#28):その組のルールをすべて同時に止めて組を消す(If-Match も使える)。rules:write |
|
GET /config/global |
200 | v0.4.19(#342):global の項目ごとの今の値・出どころ(file / env / api / default)・変え方(live / restart)(上の「global の設定の API」)。admin |
|
PATCH /config/global |
global の形の JSON(null で消す) |
200 | v0.4.19(#342):ファイル・環境変数にない項目の API の値を変える。live の項目は新しいリクエスト・接続・セッションから。dry_run。409 static / restart_only / in_use、403 protected_file、502 crowdsec_unavailable、503 not_loaded。admin、trusted_proxies・access_log・crowdsec は既定で Unix ソケットからだけ |
POST /config/plan |
設定ファイルの形の JSON | 200 | v0.4(#169):本文の設定を今動いているものと比べて差分を返す(何も変えない。--check-config --diff が使う)。admin、既定では Unix ソケットからだけ |
POST /admin/upgrade |
202 | v0.4(#174):ディスクの上の今のバイナリに引き継ぐ(SIGUSR2 と同じ。docs/UPGRADE.md)。{"status":"started"} を返し、引き継ぎは後ろで進む(結果はログの handoff.* と /metrics の rproxy_handoffs_total)。すでに動いていれば 409 upgrading。admin、既定では Unix ソケットからだけ |
|
GET /admin/update |
200 | v0.4(#174):自動更新の状態 {"mode","current":{"version","sha256"},"available":{"version","sha256"}|null,"last_check","error","bad_versions"}。admin |
|
POST /admin/update |
202 | v0.4(#174):今すぐ新しいパッチを確かめ({"status":"checking"}。結果は GET /admin/update)、RPROXY_UPDATE=auto なら入れ替える。RPROXY_UPDATE=off なら 400 unsupported。admin、既定では Unix ソケットからだけ |
|
GET /certs |
200 | v0.4.2(#240):保存した証明書(certs:read、allow_certs の内だけ)。上の「証明書の API」 |
|
GET / PUT / DELETE /certs/{name} |
{"cert","key","chain"?} |
200 / 201 / 204 | v0.4.2(#240):1 件を読む(certs:read)・保存する・消す(certs:write。PUT は TLS か loopback か Unix ソケットから)。上の「証明書の API」 |
GET /metrics |
200 | Prometheus 形式。ルールの数は rproxy_rules{state}(running / failed)、ルールごと(ラベル protocol・listen)に rproxy_rule_up(動いていれば 1)・rproxy_connections(開いている TCP の接続・UDP のセッション)・rproxy_connections_total・rproxy_bytes_total{direction}(rx はクライアントから転送先、tx はその逆)・rproxy_tls_failures_total(TLS / DTLS のハンドシェイクと STARTTLS のやり取りの失敗)・rproxy_udp_dropped_total(UDP のルールだけ)。http のルールのリクエストは rproxy_http_requests_total・rproxy_http_request_duration_seconds・rproxy_http_limited_total、転送先のヘルスチェックは rproxy_http_server_up・rproxy_http_service_down(上の「v0.3 の設定」)、宛先の全滅は rproxy_rule_all_targets_down、CrowdSec の LAPI は rproxy_crowdsec_connected、間引いたログの行は rproxy_log_suppressed_total、制御 API のトークンの期限は rproxy_token_expiry_timestamp_seconds、一時停止は rproxy_api_lockouts_total・rproxy_api_locked_sources(上の「制御 API の守り」)、ルールのラベルは rproxy_rule_labels(上の「ルールの組・状態・readiness」)、L4 の制限・帯域は rproxy_rule_limited_total・rproxy_rule_bandwidth_dropped_total、プロセスの開始時刻は rproxy_process_start_time_seconds(上の「v0.4 の設定」)。動いているバイナリは rproxy_build_info{version,sha256}、引き継ぎは rproxy_handoffs_total{outcome}(#174。rproxy_process_start_time_seconds は引き継ぎでも変わらない) |
IPv6 の listen_addr をパスに入れるときは URL エンコードする。
失敗時は次の形で返す。
{"error": "address already in use (os error 98)", "code": "bind_failed"}code |
HTTP | 意味 |
|---|---|---|
unauthorized |
401 | トークンがない、一致しない、期限切れ、またはトークンに結びついたクライアント証明書がない |
forbidden |
403 | トークンのスコープ、または allow_listen_ports の外 |
invalid |
400 | 本文やパスが不正 |
tls_config |
400 | TLS の設定の組み合わせが不正、または証明書・鍵・CA のファイルを読めない |
unsupported |
400 | この環境では使えない指定(transparent など)、または変更できない項目 |
not_found |
404 | ルールがない |
already_exists |
409 | 同じキーのルールが既にある |
static |
409 | 固定ルールは API から変更・削除できない |
reserved |
409 | rproxy 自身の制御 API のアドレスとポートに重なる(0.0.0.0 / :: とポート範囲も含めて判定する) |
bind_failed |
409 | 待ち受けポートを開けない |
resolve_failed |
502 | 転送先の名前解決に失敗し、キャッシュもない |
owned |
409 | v0.4:ルールが組(ruleset)に属するので個別には変えられない |
precondition_failed |
412 | v0.4:If-Match の etag が今の組と違う |
stale_generation |
409 | v0.4:組の generation が今より古い |
locked_out |
429 | v0.4:認証の失敗が続いたので、この送信元を一時的に止めている(Retry-After) |
upgrading |
503 / 409 | v0.4(#174):再起動なしの更新の途中なので変更を受け付けない(503。少し待って送り直す。v0.4.15 から、引き継ぎが始まる前に受け付けて順番を待っていた変更も何も変えずに 503)/すでに更新が動いている(POST /admin/upgrade の 409) |
shutting_down |
503 | v0.4.1:SIGTERM の後の終わり方(RPROXY_SHUTDOWN_DELAY / _DRAIN)の途中なので変更を受け付けない(ほかの rproxy に送る) |
tls_required |
403 | v0.4.2:鍵を含む PUT /certs を平文の TCP(loopback 以外)で送った |
in_use |
409 | v0.4.2:保存した証明書をルールが使っている(used_by) |
cert_store_unavailable |
503 | v0.4.2:証明書の保存先に書けない |
protected_file |
403 | v0.4.17(#319):API のルールが、証明書のストア・ACME の保存先の下か、設定・オプションの秘密のファイルを指している |
file_not_allowed |
403 | v0.4.17(#319):allow_certs のあるトークンがルールにファイルのパスを書いた |
secret_reuse |
409 | v0.4.17(#319):OIDC の client secret のファイルを、同じ issuer・client の client secret のほかにも使っている |
file_in_use |
409 | v0.4.18(#323):ほかの持ち主(ほかのトークン・ほかのトークンの組・設定ファイル・作ったトークンの分からない戻したルール)のルールが指す鍵・秘密のファイルを、admin でないトークンが指した |
internal |
500 | その他 |
rproxy のルールには 4 つの出どころがある。どれも GET /rules に出る。
| 出どころ | origin |
正はどこか | 変え方 |
|---|---|---|---|
設定ファイル(RPROXY_CONFIG) |
static |
ファイル | ファイルを書き換える(自動で反映)。API からは 409 static |
| UI(TCP-UDP-rproxy-ui) | dynamic |
UI の DB(forward_rules) |
UI から。UI は DB に書いてから rproxy の API を呼ぶ。rproxy は起動時に DB から復元する |
| API を直接呼ぶ(CI・スクリプト) | dynamic |
rproxy のメモリだけ | API から。DB には書かれないので、rproxy を再起動すると消える |
persist: true のトークンで API を呼ぶ(v0.4、#144) |
api |
rproxy のテーブル rproxy_rules |
API から。rproxy が作成・変更・削除のたびに rproxy_rules に書き、起動時に復元する(上の「API で作ったルールの保存」) |
| ルールの組(Kubernetes のコントローラ、v0.4) | dynamic(ruleset つき) |
コントローラ(rproxy はメモリだけ) | PUT /rulesets/{name}。個別の PATCH / DELETE は 409 owned。再起動したらコントローラが PUT し直す |
persist: true のトークンのルールの組(v0.4.2、#241) |
dynamic(ruleset つき) |
rproxy の rproxy_rule_sets(組ごとに 1 行) |
PUT / DELETE /rulesets/{name}。起動時に rproxy_rules の後に戻す |
- 長く残すルールは、設定ファイルか UI(DB)、または
persist: trueのトークン(v0.4)で作る。保存しないトークンで API を直接呼んで作ったルールは一時的なもの(CI のプレビュー環境など)として扱う。 - UI は DB にないルールを編集しない。API で作ったルールは UI の一覧に出ず、DB にあるが rproxy にないルールは UI で「未登録」(missing)になる。
- API を直接使うときは、スコープと
allow_listen_portsで UI のルールと範囲を分けたトークンを使う(「基本」の認証)。
--database-url mysql://user:pass@host:port/db を指定すると、起動時に forward_rules テーブルの全ルールを読み込んで開始する。DB ユーザーには SELECT 権限だけを与えればよい。失敗したルールは failed として登録し、残りのルールは開始する(検証に通らない行も、v0.4.18 から failed として出す。#327)。名前解決に失敗して failed になったルールは、再解決に成功した時点で自動的に開始する。
v0.4(#144)からは、続けて rproxy のテーブル rproxy_rules の自分の node の行も復元する(origin: "api"。同じキーは forward_rules の行が先。上の「API で作ったルールの保存」)。このテーブルには SELECT・INSERT・UPDATE・DELETE が要る。
forward_rules のノードでの振り分けは UI 側でする(target 列、UI PR #103。ノードごとの DB のビューがその rproxy の行だけを見せる)ので、rproxy は forward_rules を絞り込まない。rproxy_rules は rproxy が自分の node の行だけを読む。
テーブル定義は UI リポジトリの db/ で管理する。rproxy が読む列は protocol、src_addr、src_port、src_port_end、dist_addr、dist_port、source_ip、udp_idle_secs、options。options.targets(複数の宛先)があれば dist_addr / dist_port は使わない。options.enabled が false の行(UI で一時停止したルール)は起動時に作らない(ログ restore.paused に数)。
options は JSON で {"tls": <TLS>, "starttls": "smtp" | "imap" | "pop3" | null, "starttls_required": bool, "allow_from": [<CIDR>, ...], "http": <L7>, "crowdsec": bool}(allow_from・http・crowdsec は省略できる)。古いテーブルにこれらの列がなければ、既定値で読み込む。v0.4 の labels・limits・bandwidth・geoip・outlier_detection と proxy_protocol(v0.4.13)も API と同じ形で読む(省略できる。上の「v0.4 の設定」)。