Documentation
¶
Overview ¶
Package nft は、Plan から VPS の table inet wgft を組み立てて適用する(仕様 6.1 節、設計文書 7a.2 節)。 internal/dataplane/linuxkernel の nftables 実装で、internal/vpsd を import しない(設計文書 7a.7 節)。
Apply/emit は internal/planner.Plan と、frontend が実際に待ち受けている Relay ポートの集合 (design.md 7a.2 節の dataplane.Desired.RelayListening に当たる Runtime 側の入力)だけから組み立てる。 Plan は無効なルールとエージェントが未登録のルールを既に除いているので、ここでは検査し直さない (設計文書 7a.8 節 Phase 3:「ルール集合から nftables を組み立てる」から「Plan から組み立てる」への移行)。
nft が暗黙に足す条件(udp dport の前の meta l4proto、ip saddr の前の meta nfproto)を 自分で入れ、ポート範囲は nft と同じく gte / lte の 2 つの比較で表す。 これで生成したテーブルの `nft list` が、同じ内容を `nft -f` で流したものと一致する(実験で確認)。
Index ¶
- Constants
- func AgentTablePresent() (bool, error)
- func Apply(plan planner.Plan, relayListening map[uint16]bool, cfg Config) error
- func ApplyAgent(pub AgentPublication, cfg AgentConfig) error
- func Comment(ruleID, kind string) string
- func DeleteAgentTable() (bool, error)
- func DeleteTable() error
- func Fingerprint(table string) (fp string, present bool, err error)
- func ReadReplies() (map[string]uint64, error)
- func ResolveAgentTargets(ctx context.Context, rules []proto.AgentRule, lookup LookupFunc) map[string]Resolution
- type AgentConfig
- type AgentDNAT
- type AgentInput
- type AgentInspection
- type AgentPublication
- type AgentRange
- type AgentRuleResult
- type Config
- type Drop
- type LookupFunc
- type MissingItem
- type Resolution
- type RowRole
- type Staged
Constants ¶
const ( // GuardPreDrop は filter_pre の drop の行である GuardPreDrop = "filter_pre_drop" // GuardInputDrop は input の drop の行である GuardInputDrop = "input_drop" // GuardForwardFromDrop は forward の wgft0 から入るものの drop の行である GuardForwardFromDrop = "forward_from_drop" // GuardForwardToDrop は forward の wgft0 へ出るものの drop の行である GuardForwardToDrop = "forward_to_drop" // GuardHairpinDrop は forward の wgft0 から wgft0 への drop の行である GuardHairpinDrop = "forward_hairpin_drop" // GuardMSS は MSS のクランプの行である GuardMSS = "mss" )
守りの行の名前である(MissingItem.Guard)。drop の行は層になっていて、ある面が開くのは、その面を閉じる 行がすべて欠けたときだけである。どの組み合わせで何が開くかは読み手が決める(設計文書 10.2c 節)。
const AgentTableName = "wgft_agent"
AgentTableName はエージェントのテーブル名である。server の TableName と衝突しない(7b.1 節)。
const DNATKind = "dnat"
DNATKind はエージェントの DNAT の行のコメントの種類である(wgft:<ルール ID>:dnat)。
const PassKind = "pass"
PassKind は、filter_pre で公開したポートの新しい接続を通す行のコメントの種類である(wgft:<ルール ID>:pass)。
const ReplyKind = "reply"
ReplyKind は UDP の応答のカウンタの行のコメントの種類である。drop の種類(ReadDrops)とは 別のチェーンに置くので、drop として累積されない。
const TableName = "wgft"
TableName は vpsd 専用のテーブル名。他のテーブルには一切触れない。
const UDPReplyChain = "udp_reply"
UDPReplyChain は UDP の応答を数える通常のチェーンの名前である。行はルールごとに 1 つで、 コメント wgft:<ルール ID>:reply で持ち主を特定する(ReadReplies)。
Variables ¶
This section is empty.
Functions ¶
func AgentTablePresent ¶ added in v1.2.0
AgentTablePresent は table inet wgft_agent があるかどうかを返す。何も変えない。
func Apply ¶
Apply はテーブル全体を 1 トランザクションで差し替える。 「空テーブルの追加 → 削除 → 定義」の順にするので、テーブルがまだない初回でも失敗しない。 conntrack のエントリは差し替えの影響を受けず、既存のセッションは切れない。 生成の途中で失敗すると未送信のメッセージが Conn に残るので、Conn は呼び出しごとに作って捨てる。
relayListening は、vpsd がプロキシモードの待ち受けを実際に開いているポート(design.md 7a.2 節の dataplane.Desired.RelayListening)。Relay のルールは、ここにあるポートだけに Admission Policy の 行を持つ。bind に失敗したポートでは、同じポートの別のプロセスへの通信に wgft の判定を掛けて しまうため(仕様 6.1 節)。nil なら行を持たない。
Apply は Stage と Flush を続けて行う。kernel backend は 2 つを Prepare と Commit に分けて呼ぶ (design.md 7a.2 節)。
func ApplyAgent ¶ added in v1.2.0
func ApplyAgent(pub AgentPublication, cfg AgentConfig) error
ApplyAgent は StageAgent と Flush を続けて行う。
func Comment ¶
Comment はルールの行に付けるコメント。差し替えのたびにハンドルは振り直されるので、 カウンタの持ち主はこのコメントで特定する。 形式は internal/policy/nftables.Comment が決める。
func DeleteAgentTable ¶ added in v1.2.0
DeleteAgentTable は table inet wgft_agent を削除し、削除したかどうかを返す(設計文書 10.3 節の agent teardown)。他のテーブルには触れない。すでに無ければ何もせず false を返す。
func DeleteTable ¶
func DeleteTable() error
DeleteTable は table inet wgft を削除する。他のテーブルには触れない。 すでに無ければ何もしない(撤去を手作業の途中からでも走らせられるように)。
func Fingerprint ¶ added in v0.5.0
Fingerprint reads the inet table named table back from the kernel and returns a digest of what wgft wrote into it (design.md 7a.3 節: 実際の状態への収束). present is false when the table does not exist. The server passes TableName and the agent AgentTableName (design.md 7b.1 節); the digest does not depend on which table it reads.
The digest covers, per chain (by name): its type, hook and priority, and per rule its handle, its comment and its expressions; plus the elements of every set wgft fills itself (deny_N, allow_N). It leaves out what changes while forwarding without the declaration changing: counter values (set to zero before hashing) and the elements of sets packets add to (meters and flows_udp/flows_tcp, flags dynamic). A rule deleted, added or replaced, a chain or set removed, or the table recreated with other contents therefore changes the digest; traffic does not. google/nftables v0.3.0 skips, without an error, the expressions it cannot decode (rt and byteorder, which the agent's MSS rows use); the digest covers the rest of such a row. `nft replace rule` keeps a rule's handle, so replacing an MSS row by one that differs only in those two expressions leaves the digest unchanged; a replacement that changes any other expression, such as a fixed MSS value, changes it.
It is a handful of netlink dumps (tables, chains, one per chain for its rules, sets, one per static set for its elements), cheap enough to run after every Commit and on every Observe.
func ReadReplies ¶ added in v1.2.0
ReadReplies は udp_reply の各行のカウンタを、ルール ID ごとのパケット数で返す(設計文書 6.1、 10.2a 節「UDP の応答の観測」)。drop のカウンタと違い累積しない。呼び出し側は値が増えたかどうか だけを見る。テーブルかチェーンが無ければ誤りを返す。UDP のルールを公開している間はチェーンが あるはずなので、無いことは観測できないことである。
func ResolveAgentTargets ¶ added in v1.2.0
func ResolveAgentTargets(ctx context.Context, rules []proto.AgentRule, lookup LookupFunc) map[string]Resolution
ResolveAgentTargets は、ルールの宛先のホスト名を DNAT のアドレスへ解決する(7b.2 節の Prepare)。 IP リテラルの宛先は引かない。無効のルールも引かない。lookup が nil なら net.DefaultResolver を使う。
使うのは A レコード(IPv4)だけであり、そのすべてを昇順に返す。どのアドレスを使うかは、許可一覧を 知る PlanAgent が選ぶ。AAAA レコードだけを持つ名前は、IPv4 の宛先が無いという理由で公開しない。
Types ¶
type AgentConfig ¶ added in v1.2.0
type AgentConfig struct {
// WGInterface は WireGuard インタフェースの名前(既定は wgft0。11a 節の WGFT_WG_INTERFACE)。
WGInterface string
// AllowTarget は宛先の許可一覧の判定である(7b.2 節、WGFT_AGENT_ALLOW_TARGETS)。nil なら制限しない。
AllowTarget func(netip.AddrPort) bool
// AllowTargetSource は許可一覧の設定の名前である。拒否の理由に出す。
AllowTargetSource string
}
AgentConfig は、ルール以外にエージェントの表の組み立てに要る値である。
type AgentDNAT ¶ added in v1.2.0
AgentDNAT は、表の DNAT をルールと連続するポートごとに平らに並べた 1 項目である。記録した公開 (AgentPublication.DNATs)と、読み戻した表(InspectAgent)を同じ形で比べるために使う。 Dest は Ports.Lo の実効宛先である。
type AgentInput ¶ added in v1.2.0
type AgentInput struct {
// Generation は、ルールを受け取った全体状態の世代である。公開の記録にそのまま写す。
Generation uint64
// Rules は全体状態のルールである。enabled:false のルールは表に載せない。
Rules []proto.AgentRule
// Resolved は、宛先のホスト名からその解決の結果への対応である。IP リテラルの宛先は引かない。
// 対応の無いホスト名は解決できなかったものとして扱う。
Resolved map[string]Resolution
}
AgentInput は、組み立ての入力のうち全体状態から来るものである。
type AgentInspection ¶ added in v1.2.0
type AgentInspection struct {
// DNATs は nat_pre の wgft の行から読んだ、ルールと連続するポートごとの宛先である。
// (Proto, Ports.Lo, RuleID) の順。
DNATs []AgentDNAT
// Unrecognized は、nat_pre のうち wgft が書く DNAT の形として読めなかった行の数である。外から足された
// 行か、書き換えられた行である。
Unrecognized int
// MissingDNATs は、記録にあって表に無い DNAT である。宛先が違うポートもここに入る。
MissingDNATs []AgentDNAT
// ExtraDNATs は、表にあって記録に無い DNAT である。宛先が違うポートもここに入る。
ExtraDNATs []AgentDNAT
// Missing は、行とチェーンのうち、記録から組んだ表にあって実際の表に無いものである。nat_pre の DNAT の
// 行は、map の要素を除いた行の形で比べる。
// filter_pre、MASQUERADE、forward の通す行のどれかが欠ければ、LAN への転送は止まる。
Missing []string
// Unexpected は、実際の表にあって、記録から組んだ表に無い行とチェーンである。
Unexpected []string
// MissingItems は、Missing と MissingDNATs の各項目を、欠けたときに転送が止まるかどうかの役割と
// 組にしたものである(設計文書 10.2c 節の「dataplane.table の判定」)。役割は、同じチェーンの drop の
// 行やチェーンそのものが欠けているかどうかも見て決める。通す行は、それが通さなければ落とす drop の
// 行があるときだけ転送に要るためである
MissingItems []MissingItem
// Moved は、記録から組む表の行のうち、同じチェーンにあるが期待する位置に無いものである。欠けた行には
// 数えない。行の並びが変わった効果は、行が加わった場合と同じく分からないためである(設計文書 10.2c 節)
Moved []string
// ExtraDNATsInPlace は、ExtraDNATs のうち、wgft が書いた形の行(記録から組む表にある行)の map に
// 加わった要素から読んだものである。加わった行から読んだ DNAT は、その行が Unexpected に入るので
// 含めない。読み手が同じ行を二重に数えないために持つ
ExtraDNATsInPlace []AgentDNAT
}
AgentInspection は、InspectAgent が読み戻した table inet wgft_agent と、記録した公開との比較である。
func InspectAgent ¶ added in v1.2.0
func InspectAgent(want AgentPublication, wg string) (ins AgentInspection, present bool, err error)
InspectAgent は table inet wgft_agent を読み戻し、記録した公開 want から組む表と比べる(7b.4 節。停止中の agent doctor が使う)。nat_pre の DNAT はルールとポートごとの宛先と行の形で比べ、残りのチェーンは行の形で比べる。 wg は WireGuard インタフェースの名前である。テーブルを変えない。present はテーブルがあるかどうかである。
行の形の比較は、google/nftables が読み戻せない rt と byteorder の式を見ない(Fingerprint の注)。
func (AgentInspection) Matches ¶ added in v1.2.0
func (i AgentInspection) Matches() bool
Matches は、表が記録どおりであるかどうかである。
type AgentPublication ¶ added in v1.2.0
type AgentPublication struct {
Generation uint64 `json:"generation"`
Rules []AgentRuleResult `json:"rules"`
}
AgentPublication は 1 回の組み立ての結果であり、公開の記録である(7b.4 節)。ルールごとに、DNAT を 公開したポートの範囲と実効宛先、または公開できなかった理由を持つ(7b.3 節の 1 つ目の種類)。 記録はポートごとではなく範囲ごとに持つので、ポートの数に比例して大きくならない。
func PlanAgent ¶ added in v1.2.0
func PlanAgent(in AgentInput, cfg AgentConfig) AgentPublication
PlanAgent はルールごとに、DNAT を公開するポートと実効宛先を決める(7b.2、7b.3 節)。 次の場合はそのルールの DNAT を作らず、理由を Reason に書く。他のルールの判定は続ける。
- 宛先のホスト名を解決できない場合、または IPv4 のアドレスを持たない場合
- 宛先が IPv6 のアドレスの場合。カーネルモードは IPv4 の宛先だけを扱う
- 宛先がループバック(127.0.0.0/8)か未指定のアドレス(0.0.0.0)の場合
宛先の許可一覧はポートごとの実効宛先で判定する。ホスト名が複数のアドレスに解決されたときは、ポートごとに 一覧が通すアドレスだけを残し、その中で最も小さいアドレスを使う(7b.2 節)。どのアドレスも通らない ポートにだけ DNAT を作らない。 結果は (Proto, ListenPort.Lo, RuleID) の順に並ぶ。この順がテーブルの行の順になる。
func (AgentPublication) DNATs ¶ added in v1.2.0
func (p AgentPublication) DNATs() []AgentDNAT
DNATs は、公開した範囲を (Proto, Ports.Lo, RuleID) の順に平らに並べる。
type AgentRange ¶ added in v1.2.0
AgentRange は DNAT を公開した連続するポートと、その先頭のポートの実効宛先である。範囲の i 番目の ポートは、Dest のアドレスの Dest のポート + i へ向かう(7 節の実効宛先)。
type AgentRuleResult ¶ added in v1.2.0
type AgentRuleResult struct {
RuleID string `json:"rule_id"`
Proto proto.Proto `json:"proto"`
ListenPort proto.PortRange `json:"listen_port"`
// Target は宣言の宛先の文字列である。成立済みのフローを残すかどうかは、この文字列で比べる(7b.4 節)。
Target string `json:"target"`
Ranges []AgentRange `json:"ranges,omitempty"`
Reason string `json:"reason,omitempty"`
}
AgentRuleResult は 1 つのルールの公開の結果である。Reason が空なら、宣言のすべてのポートを公開した。 範囲のルールで一部のポートだけを許可一覧が拒んだ場合は、残りのポートを Ranges に持ち、Reason も持つ。
type Drop ¶
type Drop struct {
RuleID string
Kind string // deny | allow | per_source | src_flow | new_flow | packet
Packets uint64
Bytes uint64
}
Drop はルールごと・種類ごとの累積 drop 数(コメントで識別する)。
type LookupFunc ¶ added in v1.2.0
LookupFunc はホスト名のアドレスを引く。net.Resolver.LookupNetIP(ctx, "ip", host) と同じ形である。
type MissingItem ¶ added in v1.2.0
type MissingItem struct {
Desc string
Role RowRole
// Guard は、欠けたのが名前の付いた守りの行なら、その名前(Guard*)である
Guard string
}
MissingItem は欠けた行、チェーン、DNAT の 1 つである。
type Resolution ¶ added in v1.2.0
Resolution は、ルールの宛先のホスト名を解決した結果である(7b.2 節)。ResolveAgentTargets が作る。 Addrs は IPv4 のアドレスのすべてである。どれを使うかは PlanAgent が許可一覧を見て選ぶ。
func (Resolution) Failed ¶ added in v1.2.0
func (r Resolution) Failed() bool
Failed は、名前の解決そのものが失敗したかどうかである。IPv6 のアドレスだけに解決された名前は、 解決できたうえで公開できない名前なので含めない。エージェントは、解決が失敗した名前にだけ、直前に 解決できたアドレスを使い続ける(7b.2 節)。
type RowRole ¶ added in v1.2.0
type RowRole int
RowRole は、表の行かチェーンが欠けたときに転送が止まるかどうかである(設計文書 10.2c 節の 「dataplane.table の判定」)。分け方は、ラボで行を 1 種類ずつ消し、転送が通るかどうかを見て決めた。
const ( // RoleGuard は、欠けても転送が止まらない行である。wgft0 から届く面を閉じる drop の行、成立済みの // フローを通す行のうち転送に要らないもの、MSS のクランプの行、守りのチェーンが当たる RoleGuard RowRole = iota // RoleCarry は、欠けるとそのルールか表全体の転送が止まる行である。DNAT と filter_pre の通す行が当たる RoleCarry // RoleCarryLAN は、欠けると宛先がホスト自身でないルールの転送が止まる行である RoleCarryLAN // RoleCarrySelf は、欠けると宛先がホスト自身のアドレスのルールの転送が止まる行である RoleCarrySelf )
type Staged ¶ added in v0.5.0
type Staged struct {
// contains filtered or unexported fields
}
Staged は、組み立て終えてまだ送っていないテーブルの差し替え(1 トランザクション分のメッセージ)。
func Stage ¶ added in v0.5.0
Stage はテーブルの差し替えを組み立てるが、送らない(kernel backend の Prepare。design.md 7a.2 節)。 組み立ての誤りはここで返り、何も公開されない。捨てるときは何もしなくてよい(Conn は送るまで カーネルに何も書かない)。
func StageAgent ¶ added in v1.2.0
func StageAgent(pub AgentPublication, cfg AgentConfig) (*Staged, error)
StageAgent は table inet wgft_agent の差し替えを組み立てるが、送らない(7a.2 節の Prepare)。 送るのは Staged.Flush である。server の Stage と同じく、テーブル全体を 1 つのバッチで差し替える。
func (*Staged) Flush ¶ added in v0.5.0
Flush は組み立てた差し替えを 1 トランザクションで送り、wgft が要素を書く set をカーネルから 読み直して、送った要素がそのまま入っているかを確かめる(kernel backend の Commit)。 カーネル側の差し替え自体は不可分である。ただし、誤りが返ったときに旧いテーブルが残っているとは 限らない。カーネルは commit の後に応答を返すので、応答の受信に失敗した場合(ENOBUFS)は、 テーブルが差し替わった後で誤りが返る。読み直した set が送った要素と一致しない場合も、差し替わった 後で誤りが返る。送信が拒まれた場合(EMSGSIZE)とカーネルがバッチを拒んだ場合は、旧いテーブルが 残る。実際の状態との食い違いは、reconciler の Observe による drift の検出で収束させる(設計文書 6.1 節、7a.3 節)。