tailscale/util/linuxfw/iptables_for_svcs.go
Irbe Krumina 2c16fcaa06
util/linuxfw,wgengine/router: add new netfilter rules for HA ingresses (#15896)
Add new rules to update DNAT rules for Kubernetes operator's
HA ingress where it's expected that rules will be added/removed
frequently (so we don't want to keep old rules around or rewrite
existing rules unnecessarily):
- allow deleting DNAT rules using metadata lookup
- allow inserting DNAT rules if they don't already
exist (using metadata lookup)

Updates tailscale/tailscale#15895

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: chaosinthecrd <tom@tmlabs.co.uk>
2025-05-12 17:26:23 +01:00

128 lines
4.5 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux
package linuxfw
import (
"fmt"
"net/netip"
)
// This file contains functionality to insert portmapping rules for a 'service'.
// These are currently only used by the Kubernetes operator proxies.
// An iptables rule for such a service contains a comment with the service name.
// A 'service' corresponds to a VIPService as used by the Kubernetes operator.
// EnsurePortMapRuleForSvc adds a prerouting rule that forwards traffic received
// on match port and NOT on the provided interface to target IP and target port.
// Rule will only be added if it does not already exists.
func (i *iptablesRunner) EnsurePortMapRuleForSvc(svc, tun string, targetIP netip.Addr, pm PortMap) error {
table := i.getIPTByAddr(targetIP)
args := argsForPortMapRule(svc, tun, targetIP, pm)
exists, err := table.Exists("nat", "PREROUTING", args...)
if err != nil {
return fmt.Errorf("error checking if rule exists: %w", err)
}
if exists {
return nil
}
return table.Append("nat", "PREROUTING", args...)
}
// DeleteMapRuleForSvc constructs a prerouting rule as would be created by
// EnsurePortMapRuleForSvc with the provided args and, if such a rule exists,
// deletes it.
func (i *iptablesRunner) DeletePortMapRuleForSvc(svc, excludeI string, targetIP netip.Addr, pm PortMap) error {
table := i.getIPTByAddr(targetIP)
args := argsForPortMapRule(svc, excludeI, targetIP, pm)
exists, err := table.Exists("nat", "PREROUTING", args...)
if err != nil {
return fmt.Errorf("error checking if rule exists: %w", err)
}
if !exists {
return nil
}
return table.Delete("nat", "PREROUTING", args...)
}
// EnsureDNATRuleForSvc adds a DNAT rule that forwards traffic from the
// VIPService IP address to a local address. This is used by the Kubernetes
// operator's network layer proxies to forward tailnet traffic for VIPServices
// to Kubernetes Services.
func (i *iptablesRunner) EnsureDNATRuleForSvc(svcName string, origDst, dst netip.Addr) error {
table := i.getIPTByAddr(dst)
args := argsForIngressRule(svcName, origDst, dst)
exists, err := table.Exists("nat", "PREROUTING", args...)
if err != nil {
return fmt.Errorf("error checking if rule exists: %w", err)
}
if exists {
return nil
}
return table.Append("nat", "PREROUTING", args...)
}
// DeleteDNATRuleForSvc deletes a DNAT rule created by EnsureDNATRuleForSvc.
func (i *iptablesRunner) DeleteDNATRuleForSvc(svcName string, origDst, dst netip.Addr) error {
table := i.getIPTByAddr(dst)
args := argsForIngressRule(svcName, origDst, dst)
exists, err := table.Exists("nat", "PREROUTING", args...)
if err != nil {
return fmt.Errorf("error checking if rule exists: %w", err)
}
if !exists {
return nil
}
return table.Delete("nat", "PREROUTING", args...)
}
// DeleteSvc constructs all possible rules that would have been created by
// EnsurePortMapRuleForSvc from the provided args and ensures that each one that
// exists is deleted.
func (i *iptablesRunner) DeleteSvc(svc, tun string, targetIPs []netip.Addr, pms []PortMap) error {
for _, tip := range targetIPs {
for _, pm := range pms {
if err := i.DeletePortMapRuleForSvc(svc, tun, tip, pm); err != nil {
return fmt.Errorf("error deleting rule: %w", err)
}
}
}
return nil
}
func argsForPortMapRule(svc, excludeI string, targetIP netip.Addr, pm PortMap) []string {
c := commentForSvc(svc, pm)
return []string{
"!", "-i", excludeI,
"-p", pm.Protocol,
"--dport", fmt.Sprintf("%d", pm.MatchPort),
"-m", "comment", "--comment", c,
"-j", "DNAT",
"--to-destination", fmt.Sprintf("%v:%v", targetIP, pm.TargetPort),
}
}
func argsForIngressRule(svcName string, origDst, targetIP netip.Addr) []string {
c := commentForIngressSvc(svcName, origDst, targetIP)
return []string{
"--destination", origDst.String(),
"-m", "comment", "--comment", c,
"-j", "DNAT",
"--to-destination", targetIP.String(),
}
}
// commentForSvc generates a comment to be added to an iptables DNAT rule for a
// service. This is for iptables debugging/readability purposes only.
func commentForSvc(svc string, pm PortMap) string {
return fmt.Sprintf("%s:%s:%d -> %s:%d", svc, pm.Protocol, pm.MatchPort, pm.Protocol, pm.TargetPort)
}
// commentForIngressSvc generates a comment to be added to an iptables DNAT rule for a
// service. This is for iptables debugging/readability purposes only.
func commentForIngressSvc(svc string, vip, clusterIP netip.Addr) string {
return fmt.Sprintf("svc: %s, %s -> %s", svc, vip.String(), clusterIP.String())
}