Files
weave/lb/datapath.go
T
levaliciousandClaude Opus 5.5 31bf46404a
images / images (push) Successful in 17m38s
lb: delete default routes as listed (RouteDel refuses a nil Dst)
RouteList returns a default route without Dst, and netlink's RouteDel
refuses a route with no Dst, Src or Gw. The egress unreachable table's
"unreachable default" could not be removed once installed: every later
pass failed there, before updating rules, so members stayed dropped after
their EgressService got a holder again (fail closed, but stuck). S1's
stale reply tables had the same latent fault. Both now delete through
delRoute, which spells the default destination out; the unreachable route
is compared by type alone, so it is not replaced on every pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 16:01:59 -04:00

334 lines
11 KiB
Go

package lb
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"os/exec"
"strings"
"github.com/coreos/go-iptables/iptables"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
const (
// ForwardChain (nat, from PREROUTING) DNATs held addresses' ports to endpoints.
ForwardChain = "REWEAVE-LB"
// PostChain (nat, from POSTROUTING, ahead of weave's WEAVE) keeps the client's address on what ForwardChain DNATed.
PostChain = "REWEAVE-LB-POST"
// MarkChain (mangle, from PREROUTING) marks holders' connections and steers their replies.
MarkChain = "REWEAVE-LB-MARK"
// servicePrefix names the per-port chains ForwardChain jumps to.
servicePrefix = "REWEAVE-LBS-"
markShift = 24
markMask = uint32(0xff) << markShift
// RulePriority is where the reply-steering rules sit: before the main table (32766).
RulePriority = 9000
// TableBase + mark is the routing table that sends replies to that mark's holder.
TableBase = 52000
bridge = "weave"
)
func markValue(m uint32) uint32 { return m << markShift }
// portChain is the per-(service, protocol, port) chain name: stable, short enough for iptables (28 chars).
func portChain(f Forward, p Port) string {
h := sha256.Sum256([]byte(fmt.Sprintf("%s/%s/%s/%d", f.Namespace, f.Name, p.Protocol, p.Port)))
return servicePrefix + strings.ToUpper(hex.EncodeToString(h[:])[:12])
}
// Rules is the full content of the chains this package owns, in iptables-restore form.
type Rules struct {
Nat, Mangle, Filter string
}
// Render turns the desired state (load-balancer forwards and egress) into iptables-restore input for the nat,
// mangle and filter tables. podNet is the weave allocation range (pods and bridges).
// Each payload declares every chain it owns, so a restore with --noflush replaces exactly those.
func Render(d Desired, e EgressDesired, self string, podNet *net.IPNet, stale []string) Rules {
var nat bytes.Buffer
nat.WriteString("*nat\n")
for _, c := range []string{ForwardChain, PostChain, ClassChain} {
fmt.Fprintf(&nat, ":%s - [0:0]\n", c)
}
var body bytes.Buffer
seen := map[string]bool{}
for _, f := range d.Forwards {
a := f.Address.String()
if seen[a] {
continue
}
seen[a] = true
// weave masquerades everything entering the pod network from outside unless it is for a local pod
// (its expose rule); a connection we DNATed must keep the client's address, so it leaves the nat
// table here. One from a pod (hairpin) is masqueraded to this node, so its reply comes back here.
fmt.Fprintf(&body, "-A %s -m conntrack --ctstate DNAT --ctorigdst %s/32 -s %s -j MASQUERADE\n", PostChain, a, podNet)
fmt.Fprintf(&body, "-A %s -m conntrack --ctstate DNAT --ctorigdst %s/32 -j ACCEPT\n", PostChain, a)
}
for _, f := range d.Forwards {
for _, p := range f.Ports {
c := portChain(f, p)
fmt.Fprintf(&nat, ":%s - [0:0]\n", c)
proto := strings.ToLower(string(p.Protocol))
fmt.Fprintf(&body, "-A %s -d %s/32 -p %s -m %s --dport %d -m comment --comment %q -j %s\n",
ForwardChain, f.Address, proto, proto, p.Port, f.Namespace+"/"+f.Name, c)
n := len(p.Endpoints)
for i, e := range p.Endpoints {
// DNAT to an ip:port needs the protocol in the same rule
if i < n-1 {
// the i-th endpoint takes 1/(n-i) of what the earlier ones left: uniform overall
fmt.Fprintf(&body, "-A %s -p %s -m statistic --mode random --probability %.10f -j DNAT --to-destination %s\n",
c, proto, 1/float64(n-i), e)
} else {
fmt.Fprintf(&body, "-A %s -p %s -j DNAT --to-destination %s\n", c, proto, e)
}
}
}
}
for _, c := range stale {
// chains of ports no longer forwarded: emptied here, deleted after the restore
fmt.Fprintf(&nat, ":%s - [0:0]\n", c)
}
var mangle bytes.Buffer
mangle.WriteString("*mangle\n")
for _, c := range []string{MarkChain, MSSChain} {
fmt.Fprintf(&mangle, ":%s - [0:0]\n", c)
}
if len(d.Holders) > 0 {
// replies of a holder's connection carry its mark: the policy rule routes them back to it
fmt.Fprintf(&mangle, "-A %s -m conntrack --ctdir REPLY -m connmark ! --mark 0x0/0x%x -j CONNMARK --restore-mark --nfmask 0x%x --ctmask 0x%x\n",
MarkChain, markMask, markMask, markMask)
for _, h := range d.Holders {
// a new connection to a pod here, arriving from the holder's bridge, from outside the pod network:
// the holder DNATed it and must see its replies
fmt.Fprintf(&mangle, "-A %s -i %s -m mac --mac-source %s ! -s %s -d %s -m conntrack --ctstate NEW -m comment --comment %q -j CONNMARK --set-xmark 0x%x/0x%x\n",
MarkChain, bridge, h.Node.WeaveMAC, podNet, podNet, "holder "+h.Node.Name, markValue(h.Mark), markMask)
}
}
var filter bytes.Buffer
filter.WriteString("*filter\n")
for _, c := range []string{FwdChain, InChain} {
fmt.Fprintf(&filter, ":%s - [0:0]\n", c)
}
renderEgress(e, self, podNet, &body, &mangle, &filter)
nat.Write(body.Bytes())
nat.WriteString("COMMIT\n")
mangle.WriteString("COMMIT\n")
filter.WriteString("COMMIT\n")
return Rules{Nat: nat.String(), Mangle: mangle.String(), Filter: filter.String()}
}
// Datapath applies Desired to this node.
type Datapath struct {
ipt *iptables.IPTables
// restore runs iptables-restore --noflush with the given input (swappable in tests)
restore func(input string) error
}
// NewDatapath uses the host's iptables (whichever backend the image's launch script selected).
func NewDatapath(ipt *iptables.IPTables) *Datapath {
return &Datapath{ipt: ipt, restore: func(input string) error {
cmd := exec.Command("iptables-restore", "--noflush", "-w", "5")
cmd.Stdin = strings.NewReader(input)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("iptables-restore: %v: %s", err, out)
}
return nil
}}
}
// Apply makes the node's iptables chains, tunnel, policy rules and routes exactly what d and e say.
func (dp *Datapath) Apply(d Desired, e EgressDesired, self string, local Local) error {
podNet := local.PodNet
if err := dp.hooks(); err != nil {
return err
}
// the tunnel device must exist before rules naming it are restored (-o/-i on a missing device is fine for
// iptables, but the routes and tables need it)
if err := ensureTunnel(e.Tunnelled(self), local); err != nil {
return err
}
existing, err := dp.ipt.ListChains("nat")
if err != nil {
return err
}
want := map[string]bool{}
for _, f := range d.Forwards {
for _, p := range f.Ports {
want[portChain(f, p)] = true
}
}
var stale []string
for _, c := range existing {
if strings.HasPrefix(c, servicePrefix) && !want[c] {
stale = append(stale, c)
}
}
r := Render(d, e, self, podNet, stale)
// filter first: the guards are in place before the marks and SNATs that they guard
for _, payload := range []string{r.Filter, r.Nat, r.Mangle} {
if err := dp.restore(payload); err != nil {
return err
}
}
for _, c := range stale {
if err := dp.ipt.DeleteChain("nat", c); err != nil {
return fmt.Errorf("deleting stale chain %s: %v", c, err)
}
}
if err := dp.routes(d.Holders); err != nil {
return err
}
return egressNet(e, self, local)
}
// hooks makes sure the built-in chains jump to ours: first (kube-proxy's and weave's rules must not see these
// connections first), except ClassChain, which must come after kube-proxy's KUBE-SERVICES (whose DNAT ends the
// nat walk, so a connection to a Service is never classed as going outside).
func (dp *Datapath) hooks() error {
for _, h := range []struct {
table, from, chain string
last bool
}{
{"nat", "PREROUTING", ForwardChain, false},
{"nat", "POSTROUTING", PostChain, false},
{"nat", "PREROUTING", ClassChain, true},
{"mangle", "PREROUTING", MarkChain, false},
{"mangle", "FORWARD", MSSChain, false},
{"filter", "FORWARD", FwdChain, false},
{"filter", "INPUT", InChain, false},
} {
ok, err := dp.ipt.ChainExists(h.table, h.chain)
if err != nil {
return err
}
if !ok {
if err := dp.ipt.NewChain(h.table, h.chain); err != nil {
return err
}
}
rule := []string{"-m", "comment", "--comment", "reweave lb", "-j", h.chain}
rules, err := dp.ipt.List(h.table, h.from)
if err != nil {
return err
}
if h.last {
ours, kube := -1, -1
for i, r := range rules {
if strings.HasSuffix(r, "-j "+h.chain) {
ours = i
}
if strings.HasSuffix(r, "-j KUBE-SERVICES") {
kube = i
}
}
if ours > kube {
continue
}
if err := dp.ipt.DeleteIfExists(h.table, h.from, rule...); err != nil {
return err
}
if err := dp.ipt.Append(h.table, h.from, rule...); err != nil {
return err
}
continue
}
// rules[0] is the policy line; our jump must be rules[1]
if len(rules) > 1 && strings.HasSuffix(rules[1], "-j "+h.chain) {
continue
}
if err := dp.ipt.DeleteIfExists(h.table, h.from, rule...); err != nil {
return err
}
if err := dp.ipt.Insert(h.table, h.from, 1, rule...); err != nil {
return err
}
}
return nil
}
// routes keeps one policy rule and one table per holder: marked replies go to the holder's bridge.
func (dp *Datapath) routes(holders []Holder) error {
link, err := netlink.LinkByName(bridge)
if err != nil {
if len(holders) == 0 {
return nil
}
return fmt.Errorf("weave bridge: %v", err)
}
want := map[int]Holder{}
for _, h := range holders {
want[TableBase+int(h.Mark)] = h
}
rules, err := netlink.RuleList(unix.AF_INET)
if err != nil {
return err
}
have := map[int]bool{}
for _, r := range rules {
if r.Priority != RulePriority || r.Table < TableBase || r.Table > TableBase+MaxMarks {
continue
}
h, ok := want[r.Table]
if ok && uint32(r.Mark) == markValue(h.Mark) && uint32(r.Mask) == markMask {
have[r.Table] = true
continue
}
r := r
if err := netlink.RuleDel(&r); err != nil {
return fmt.Errorf("removing stale rule to table %d: %v", r.Table, err)
}
}
for table := TableBase + 1; table <= TableBase+MaxMarks; table++ {
h, ok := want[table]
if !ok {
routes, err := netlink.RouteListFiltered(unix.AF_INET, &netlink.Route{Table: table}, netlink.RT_FILTER_TABLE)
if err != nil {
return err
}
for _, rt := range routes {
if err := delRoute(rt); err != nil {
return fmt.Errorf("removing stale route in table %d: %v", table, err)
}
}
continue
}
if err := netlink.RouteReplace(&netlink.Route{
LinkIndex: link.Attrs().Index,
Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)},
Gw: h.Node.WeaveIP,
Table: table,
Flags: int(netlink.FLAG_ONLINK),
}); err != nil {
return fmt.Errorf("route to holder %s: %v", h.Node.Name, err)
}
if !have[table] {
rule := netlink.NewRule()
rule.Priority = RulePriority
rule.Table = table
rule.Mark = int(markValue(h.Mark))
rule.Mask = int(markMask)
if err := netlink.RuleAdd(rule); err != nil {
return fmt.Errorf("rule for holder %s: %v", h.Node.Name, err)
}
}
}
return nil
}
// delRoute deletes a route as RouteList returned it: a default route lists without Dst, which RouteDel refuses.
func delRoute(rt netlink.Route) error {
if rt.Dst == nil {
rt.Dst = &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}
}
return netlink.RouteDel(&rt)
}