Port service/endpoint chain creation/cleanup to nftables
This commit is contained in:
		@@ -21,7 +21,6 @@ package nftables
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"crypto/sha256"
 | 
						"crypto/sha256"
 | 
				
			||||||
	"encoding/base32"
 | 
						"encoding/base32"
 | 
				
			||||||
@@ -103,12 +102,13 @@ func newServiceInfo(port *v1.ServicePort, service *v1.Service, bsvcPortInfo *pro
 | 
				
			|||||||
	// Store the following for performance reasons.
 | 
						// Store the following for performance reasons.
 | 
				
			||||||
	svcName := types.NamespacedName{Namespace: service.Namespace, Name: service.Name}
 | 
						svcName := types.NamespacedName{Namespace: service.Namespace, Name: service.Name}
 | 
				
			||||||
	svcPortName := proxy.ServicePortName{NamespacedName: svcName, Port: port.Name}
 | 
						svcPortName := proxy.ServicePortName{NamespacedName: svcName, Port: port.Name}
 | 
				
			||||||
	protocol := strings.ToLower(string(svcPort.Protocol()))
 | 
					 | 
				
			||||||
	svcPort.nameString = svcPortName.String()
 | 
						svcPort.nameString = svcPortName.String()
 | 
				
			||||||
	svcPort.clusterPolicyChainName = servicePortPolicyClusterChain(svcPort.nameString, protocol)
 | 
					
 | 
				
			||||||
	svcPort.localPolicyChainName = servicePortPolicyLocalChainName(svcPort.nameString, protocol)
 | 
						chainNameBase := servicePortChainNameBase(&svcPortName, strings.ToLower(string(svcPort.Protocol())))
 | 
				
			||||||
	svcPort.firewallChainName = serviceFirewallChainName(svcPort.nameString, protocol)
 | 
						svcPort.clusterPolicyChainName = servicePortPolicyClusterChainNamePrefix + chainNameBase
 | 
				
			||||||
	svcPort.externalChainName = serviceExternalChainName(svcPort.nameString, protocol)
 | 
						svcPort.localPolicyChainName = servicePortPolicyLocalChainNamePrefix + chainNameBase
 | 
				
			||||||
 | 
						svcPort.firewallChainName = serviceFirewallChainNamePrefix + chainNameBase
 | 
				
			||||||
 | 
						svcPort.externalChainName = serviceExternalChainNamePrefix + chainNameBase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return svcPort
 | 
						return svcPort
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -124,7 +124,7 @@ type endpointInfo struct {
 | 
				
			|||||||
func newEndpointInfo(baseInfo *proxy.BaseEndpointInfo, svcPortName *proxy.ServicePortName) proxy.Endpoint {
 | 
					func newEndpointInfo(baseInfo *proxy.BaseEndpointInfo, svcPortName *proxy.ServicePortName) proxy.Endpoint {
 | 
				
			||||||
	return &endpointInfo{
 | 
						return &endpointInfo{
 | 
				
			||||||
		BaseEndpointInfo: baseInfo,
 | 
							BaseEndpointInfo: baseInfo,
 | 
				
			||||||
		chainName:        servicePortEndpointChainName(svcPortName.String(), strings.ToLower(string(svcPortName.Protocol)), baseInfo.String()),
 | 
							chainName:        servicePortEndpointChainNamePrefix + servicePortEndpointChainNameBase(svcPortName, strings.ToLower(string(svcPortName.Protocol)), baseInfo.String()),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -174,10 +174,7 @@ type Proxier struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// The following buffers are used to reuse memory and avoid allocations
 | 
						// The following buffers are used to reuse memory and avoid allocations
 | 
				
			||||||
	// that are significantly impacting performance.
 | 
						// that are significantly impacting performance.
 | 
				
			||||||
	iptablesData *bytes.Buffer
 | 
					 | 
				
			||||||
	filterChains proxyutil.LineBuffer
 | 
					 | 
				
			||||||
	filterRules proxyutil.LineBuffer
 | 
						filterRules proxyutil.LineBuffer
 | 
				
			||||||
	natChains    proxyutil.LineBuffer
 | 
					 | 
				
			||||||
	natRules    proxyutil.LineBuffer
 | 
						natRules    proxyutil.LineBuffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// conntrackTCPLiberal indicates whether the system sets the kernel nf_conntrack_tcp_be_liberal
 | 
						// conntrackTCPLiberal indicates whether the system sets the kernel nf_conntrack_tcp_be_liberal
 | 
				
			||||||
@@ -188,6 +185,9 @@ type Proxier struct {
 | 
				
			|||||||
	// networkInterfacer defines an interface for several net library functions.
 | 
						// networkInterfacer defines an interface for several net library functions.
 | 
				
			||||||
	// Inject for test purpose.
 | 
						// Inject for test purpose.
 | 
				
			||||||
	networkInterfacer proxyutil.NetworkInterfacer
 | 
						networkInterfacer proxyutil.NetworkInterfacer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// staleChains contains information about chains to be deleted later
 | 
				
			||||||
 | 
						staleChains map[string]time.Time
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Proxier implements proxy.Provider
 | 
					// Proxier implements proxy.Provider
 | 
				
			||||||
@@ -263,14 +263,12 @@ func NewProxier(ipFamily v1.IPFamily,
 | 
				
			|||||||
		serviceHealthServer:      serviceHealthServer,
 | 
							serviceHealthServer:      serviceHealthServer,
 | 
				
			||||||
		healthzServer:            healthzServer,
 | 
							healthzServer:            healthzServer,
 | 
				
			||||||
		precomputedProbabilities: make([]string, 0, 1001),
 | 
							precomputedProbabilities: make([]string, 0, 1001),
 | 
				
			||||||
		iptablesData:             bytes.NewBuffer(nil),
 | 
					 | 
				
			||||||
		filterChains:             proxyutil.NewLineBuffer(),
 | 
					 | 
				
			||||||
		filterRules:              proxyutil.NewLineBuffer(),
 | 
							filterRules:              proxyutil.NewLineBuffer(),
 | 
				
			||||||
		natChains:                proxyutil.NewLineBuffer(),
 | 
					 | 
				
			||||||
		natRules:                 proxyutil.NewLineBuffer(),
 | 
							natRules:                 proxyutil.NewLineBuffer(),
 | 
				
			||||||
		nodePortAddresses:        nodePortAddresses,
 | 
							nodePortAddresses:        nodePortAddresses,
 | 
				
			||||||
		networkInterfacer:        proxyutil.RealNetwork{},
 | 
							networkInterfacer:        proxyutil.RealNetwork{},
 | 
				
			||||||
		conntrackTCPLiberal:      conntrackTCPLiberal,
 | 
							conntrackTCPLiberal:      conntrackTCPLiberal,
 | 
				
			||||||
 | 
							staleChains:              make(map[string]time.Time),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	burstSyncs := 2
 | 
						burstSyncs := 2
 | 
				
			||||||
@@ -667,75 +665,113 @@ func (proxier *Proxier) OnNodeDelete(node *v1.Node) {
 | 
				
			|||||||
func (proxier *Proxier) OnNodeSynced() {
 | 
					func (proxier *Proxier) OnNodeSynced() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// portProtoHash takes the ServicePortName and protocol for a service
 | 
					 | 
				
			||||||
// returns the associated 16 character hash. This is computed by hashing (sha256)
 | 
					 | 
				
			||||||
// then encoding to base32 and truncating to 16 chars. We do this because IPTables
 | 
					 | 
				
			||||||
// Chain Names must be <= 28 chars long, and the longer they are the harder they are to read.
 | 
					 | 
				
			||||||
func portProtoHash(servicePortName string, protocol string) string {
 | 
					 | 
				
			||||||
	hash := sha256.Sum256([]byte(servicePortName + protocol))
 | 
					 | 
				
			||||||
	encoded := base32.StdEncoding.EncodeToString(hash[:])
 | 
					 | 
				
			||||||
	return encoded[:16]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	servicePortPolicyClusterChainNamePrefix = "KUBE-SVC-"
 | 
						// Maximum length for one of our chain name prefixes, including the trailing
 | 
				
			||||||
	servicePortPolicyLocalChainNamePrefix   = "KUBE-SVL-"
 | 
						// hyphen.
 | 
				
			||||||
	serviceFirewallChainNamePrefix          = "KUBE-FW-"
 | 
						chainNamePrefixLengthMax = 16
 | 
				
			||||||
	serviceExternalChainNamePrefix          = "KUBE-EXT-"
 | 
					
 | 
				
			||||||
	servicePortEndpointChainNamePrefix      = "KUBE-SEP-"
 | 
						// Maximum length of the string returned from servicePortChainNameBase or
 | 
				
			||||||
 | 
						// servicePortEndpointChainNameBase.
 | 
				
			||||||
 | 
						chainNameBaseLengthMax = knftables.NameLengthMax - chainNamePrefixLengthMax
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// servicePortPolicyClusterChain returns the name of the KUBE-SVC-XXXX chain for a service, which is the
 | 
					const (
 | 
				
			||||||
// main iptables chain for that service, used for dispatching to endpoints when using `Cluster`
 | 
						servicePortPolicyClusterChainNamePrefix = "service-"
 | 
				
			||||||
// traffic policy.
 | 
						servicePortPolicyLocalChainNamePrefix   = "local-"
 | 
				
			||||||
func servicePortPolicyClusterChain(servicePortName string, protocol string) string {
 | 
						serviceFirewallChainNamePrefix          = "firewall-"
 | 
				
			||||||
	return servicePortPolicyClusterChainNamePrefix + portProtoHash(servicePortName, protocol)
 | 
						serviceExternalChainNamePrefix          = "external-"
 | 
				
			||||||
}
 | 
						servicePortEndpointChainNamePrefix      = "endpoint-"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// servicePortPolicyLocalChainName returns the name of the KUBE-SVL-XXXX chain for a service, which
 | 
					// hashAndTruncate prefixes name with a hash of itself and then truncates to
 | 
				
			||||||
// handles dispatching to local endpoints when using `Local` traffic policy. This chain only
 | 
					// chainNameBaseLengthMax. The hash ensures that (a) the name is still unique if we have
 | 
				
			||||||
// exists if the service has `Local` internal or external traffic policy.
 | 
					// to truncate the end, and (b) it's visually distinguishable from other chains that would
 | 
				
			||||||
func servicePortPolicyLocalChainName(servicePortName string, protocol string) string {
 | 
					// otherwise have nearly identical names (e.g., different endpoint chains for a given
 | 
				
			||||||
	return servicePortPolicyLocalChainNamePrefix + portProtoHash(servicePortName, protocol)
 | 
					// service that differ in only a single digit).
 | 
				
			||||||
}
 | 
					func hashAndTruncate(name string) string {
 | 
				
			||||||
 | 
						hash := sha256.Sum256([]byte(name))
 | 
				
			||||||
// serviceFirewallChainName returns the name of the KUBE-FW-XXXX chain for a service, which
 | 
					 | 
				
			||||||
// is used to implement the filtering for the LoadBalancerSourceRanges feature.
 | 
					 | 
				
			||||||
func serviceFirewallChainName(servicePortName string, protocol string) string {
 | 
					 | 
				
			||||||
	return serviceFirewallChainNamePrefix + portProtoHash(servicePortName, protocol)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// serviceExternalChainName returns the name of the KUBE-EXT-XXXX chain for a service, which
 | 
					 | 
				
			||||||
// implements "short-circuiting" for internally-originated external-destination traffic when using
 | 
					 | 
				
			||||||
// `Local` external traffic policy.  It forwards traffic from local sources to the KUBE-SVC-XXXX
 | 
					 | 
				
			||||||
// chain and traffic from external sources to the KUBE-SVL-XXXX chain.
 | 
					 | 
				
			||||||
func serviceExternalChainName(servicePortName string, protocol string) string {
 | 
					 | 
				
			||||||
	return serviceExternalChainNamePrefix + portProtoHash(servicePortName, protocol)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// servicePortEndpointChainName returns the name of the KUBE-SEP-XXXX chain for a particular
 | 
					 | 
				
			||||||
// service endpoint.
 | 
					 | 
				
			||||||
func servicePortEndpointChainName(servicePortName string, protocol string, endpoint string) string {
 | 
					 | 
				
			||||||
	hash := sha256.Sum256([]byte(servicePortName + protocol + endpoint))
 | 
					 | 
				
			||||||
	encoded := base32.StdEncoding.EncodeToString(hash[:])
 | 
						encoded := base32.StdEncoding.EncodeToString(hash[:])
 | 
				
			||||||
	return servicePortEndpointChainNamePrefix + encoded[:16]
 | 
						name = encoded[:8] + "-" + name
 | 
				
			||||||
 | 
						if len(name) > chainNameBaseLengthMax {
 | 
				
			||||||
 | 
							name = name[:chainNameBaseLengthMax-3] + "..."
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return name
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// servicePortChainNameBase returns the base name for a chain for the given ServicePort.
 | 
				
			||||||
 | 
					// This is something like "HASH-namespace/serviceName/protocol/portName", e.g,
 | 
				
			||||||
 | 
					// "ULMVA6XW-ns1/svc1/tcp/p80".
 | 
				
			||||||
 | 
					func servicePortChainNameBase(servicePortName *proxy.ServicePortName, protocol string) string {
 | 
				
			||||||
 | 
						// nftables chains can contain the characters [A-Za-z0-9_./-] (but must start with
 | 
				
			||||||
 | 
						// a letter, underscore, or dot).
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// Namespace, Service, and Port names can contain [a-z0-9-] (with some additional
 | 
				
			||||||
 | 
						// restrictions that aren't relevant here).
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// Protocol is /(tcp|udp|sctp)/.
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// Thus, we can safely use all Namespace names, Service names, protocol values,
 | 
				
			||||||
 | 
						// and Port names directly in nftables chain names (though note that this assumes
 | 
				
			||||||
 | 
						// that the chain name won't *start* with any of those strings, since that might
 | 
				
			||||||
 | 
						// be illegal). We use "/" to separate the parts of the name, which is one of the
 | 
				
			||||||
 | 
						// two characters allowed in a chain name that isn't allowed in our input strings.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						name := fmt.Sprintf("%s/%s/%s/%s",
 | 
				
			||||||
 | 
							servicePortName.NamespacedName.Namespace,
 | 
				
			||||||
 | 
							servicePortName.NamespacedName.Name,
 | 
				
			||||||
 | 
							protocol,
 | 
				
			||||||
 | 
							servicePortName.Port,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The namespace, service, and port name can each be up to 63 characters, protocol
 | 
				
			||||||
 | 
						// can be up to 4, plus 8 for the hash and 4 additional punctuation characters.
 | 
				
			||||||
 | 
						// That's a total of 205, which is less than chainNameBaseLengthMax (240). So this
 | 
				
			||||||
 | 
						// will never actually return a truncated name.
 | 
				
			||||||
 | 
						return hashAndTruncate(name)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// servicePortEndpointChainNameBase returns the suffix for chain names for the given
 | 
				
			||||||
 | 
					// endpoint. This is something like
 | 
				
			||||||
 | 
					// "HASH-namespace/serviceName/protocol/portName__endpointIP/endpointport", e.g.,
 | 
				
			||||||
 | 
					// "5OJB2KTY-ns1/svc1/tcp/p80__10.180.0.1/80".
 | 
				
			||||||
 | 
					func servicePortEndpointChainNameBase(servicePortName *proxy.ServicePortName, protocol, endpoint string) string {
 | 
				
			||||||
 | 
						// As above in servicePortChainNameBase: Namespace, Service, Port, Protocol, and
 | 
				
			||||||
 | 
						// EndpointPort are all safe to copy into the chain name directly. But if
 | 
				
			||||||
 | 
						// EndpointIP is IPv6 then it will contain colons, which aren't allowed in a chain
 | 
				
			||||||
 | 
						// name. IPv6 IPs are also quite long, but we can't safely truncate them (e.g. to
 | 
				
			||||||
 | 
						// only the final segment) because (especially for manually-created external
 | 
				
			||||||
 | 
						// endpoints), we can't know for sure that any part of them is redundant.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						endpointIP, endpointPort, _ := net.SplitHostPort(endpoint)
 | 
				
			||||||
 | 
						if strings.Contains(endpointIP, ":") {
 | 
				
			||||||
 | 
							endpointIP = strings.ReplaceAll(endpointIP, ":", ".")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// As above, we use "/" to separate parts of the name, and "__" to separate the
 | 
				
			||||||
 | 
						// "service" part from the "endpoint" part.
 | 
				
			||||||
 | 
						name := fmt.Sprintf("%s/%s/%s/%s__%s/%s",
 | 
				
			||||||
 | 
							servicePortName.NamespacedName.Namespace,
 | 
				
			||||||
 | 
							servicePortName.NamespacedName.Name,
 | 
				
			||||||
 | 
							protocol,
 | 
				
			||||||
 | 
							servicePortName.Port,
 | 
				
			||||||
 | 
							endpointIP,
 | 
				
			||||||
 | 
							endpointPort,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The part of name before the "__" can be up to 205 characters (as with
 | 
				
			||||||
 | 
						// servicePortChainNameBase above). An IPv6 address can be up to 39 characters, and
 | 
				
			||||||
 | 
						// a port can be up to 5 digits, plus 3 punctuation characters gives a max total
 | 
				
			||||||
 | 
						// length of 252, well over chainNameBaseLengthMax (240), so truncation is
 | 
				
			||||||
 | 
						// theoretically possible (though incredibly unlikely).
 | 
				
			||||||
 | 
						return hashAndTruncate(name)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func isServiceChainName(chainString string) bool {
 | 
					func isServiceChainName(chainString string) bool {
 | 
				
			||||||
	prefixes := []string{
 | 
						// The chains returned from servicePortChainNameBase and
 | 
				
			||||||
		servicePortPolicyClusterChainNamePrefix,
 | 
						// servicePortEndpointChainNameBase will always have at least one "/" in them.
 | 
				
			||||||
		servicePortPolicyLocalChainNamePrefix,
 | 
						// Since none of our "stock" chain names use slashes, we can distinguish them this
 | 
				
			||||||
		servicePortEndpointChainNamePrefix,
 | 
						// way.
 | 
				
			||||||
		serviceFirewallChainNamePrefix,
 | 
						return strings.Contains(chainString, "/")
 | 
				
			||||||
		serviceExternalChainNamePrefix,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, p := range prefixes {
 | 
					 | 
				
			||||||
		if strings.HasPrefix(chainString, p) {
 | 
					 | 
				
			||||||
			return true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return false
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// This is where all of the nftables calls happen.
 | 
					// This is where all of the nftables calls happen.
 | 
				
			||||||
@@ -774,18 +810,45 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If there are sufficiently-stale chains left over from previous transactions,
 | 
				
			||||||
 | 
						// try to delete them now.
 | 
				
			||||||
 | 
						if len(proxier.staleChains) > 0 {
 | 
				
			||||||
 | 
							oneSecondAgo := start.Add(-time.Second)
 | 
				
			||||||
 | 
							tx := proxier.nftables.NewTransaction()
 | 
				
			||||||
 | 
							deleted := 0
 | 
				
			||||||
 | 
							for chain, modtime := range proxier.staleChains {
 | 
				
			||||||
 | 
								if modtime.Before(oneSecondAgo) {
 | 
				
			||||||
 | 
									tx.Delete(&knftables.Chain{
 | 
				
			||||||
 | 
										Name: chain,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									delete(proxier.staleChains, chain)
 | 
				
			||||||
 | 
									deleted++
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if deleted > 0 {
 | 
				
			||||||
 | 
								klog.InfoS("Deleting stale nftables chains", "numChains", deleted)
 | 
				
			||||||
 | 
								err := proxier.nftables.Run(context.TODO(), tx)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									// We already deleted the entries from staleChains, but if
 | 
				
			||||||
 | 
									// the chains still exist, they'll just get added back
 | 
				
			||||||
 | 
									// (with a later timestamp) at the end of the sync.
 | 
				
			||||||
 | 
									klog.ErrorS(err, "Unable to delete stale chains; will retry later")
 | 
				
			||||||
 | 
									// FIXME: metric
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Now start the actual syncing transaction
 | 
				
			||||||
	tx := proxier.nftables.NewTransaction()
 | 
						tx := proxier.nftables.NewTransaction()
 | 
				
			||||||
	proxier.setupNFTables(tx)
 | 
						proxier.setupNFTables(tx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Reset all buffers used later.
 | 
						// Reset all buffers used later.
 | 
				
			||||||
	// This is to avoid memory reallocations and thus improve performance.
 | 
						// This is to avoid memory reallocations and thus improve performance.
 | 
				
			||||||
	proxier.filterChains.Reset()
 | 
					 | 
				
			||||||
	proxier.filterRules.Reset()
 | 
						proxier.filterRules.Reset()
 | 
				
			||||||
	proxier.natChains.Reset()
 | 
					 | 
				
			||||||
	proxier.natRules.Reset()
 | 
						proxier.natRules.Reset()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Accumulate NAT chains to keep.
 | 
						// Accumulate service/endpoint chains to keep.
 | 
				
			||||||
	activeNATChains := sets.New[string]()
 | 
						activeChains := sets.New[string]()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Compute total number of endpoint chains across all services
 | 
						// Compute total number of endpoint chains across all services
 | 
				
			||||||
	// to get a sense of how big the cluster is.
 | 
						// to get a sense of how big the cluster is.
 | 
				
			||||||
@@ -819,7 +882,7 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
		// Note the endpoint chains that will be used
 | 
							// Note the endpoint chains that will be used
 | 
				
			||||||
		for _, ep := range allLocallyReachableEndpoints {
 | 
							for _, ep := range allLocallyReachableEndpoints {
 | 
				
			||||||
			if epInfo, ok := ep.(*endpointInfo); ok {
 | 
								if epInfo, ok := ep.(*endpointInfo); ok {
 | 
				
			||||||
				activeNATChains.Insert(epInfo.chainName)
 | 
									ensureChain(epInfo.chainName, tx, activeChains)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -827,14 +890,14 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
		clusterPolicyChain := svcInfo.clusterPolicyChainName
 | 
							clusterPolicyChain := svcInfo.clusterPolicyChainName
 | 
				
			||||||
		usesClusterPolicyChain := len(clusterEndpoints) > 0 && svcInfo.UsesClusterEndpoints()
 | 
							usesClusterPolicyChain := len(clusterEndpoints) > 0 && svcInfo.UsesClusterEndpoints()
 | 
				
			||||||
		if usesClusterPolicyChain {
 | 
							if usesClusterPolicyChain {
 | 
				
			||||||
			activeNATChains.Insert(clusterPolicyChain)
 | 
								ensureChain(clusterPolicyChain, tx, activeChains)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// localPolicyChain contains the endpoints used with "Local" traffic policy
 | 
							// localPolicyChain contains the endpoints used with "Local" traffic policy
 | 
				
			||||||
		localPolicyChain := svcInfo.localPolicyChainName
 | 
							localPolicyChain := svcInfo.localPolicyChainName
 | 
				
			||||||
		usesLocalPolicyChain := len(localEndpoints) > 0 && svcInfo.UsesLocalEndpoints()
 | 
							usesLocalPolicyChain := len(localEndpoints) > 0 && svcInfo.UsesLocalEndpoints()
 | 
				
			||||||
		if usesLocalPolicyChain {
 | 
							if usesLocalPolicyChain {
 | 
				
			||||||
			activeNATChains.Insert(localPolicyChain)
 | 
								ensureChain(localPolicyChain, tx, activeChains)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// internalPolicyChain is the chain containing the endpoints for
 | 
							// internalPolicyChain is the chain containing the endpoints for
 | 
				
			||||||
@@ -876,7 +939,7 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
		// are no externally-usable endpoints.
 | 
							// are no externally-usable endpoints.
 | 
				
			||||||
		usesExternalTrafficChain := hasEndpoints && svcInfo.ExternallyAccessible()
 | 
							usesExternalTrafficChain := hasEndpoints && svcInfo.ExternallyAccessible()
 | 
				
			||||||
		if usesExternalTrafficChain {
 | 
							if usesExternalTrafficChain {
 | 
				
			||||||
			activeNATChains.Insert(externalTrafficChain)
 | 
								ensureChain(externalTrafficChain, tx, activeChains)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Traffic to LoadBalancer IPs can go directly to externalTrafficChain
 | 
							// Traffic to LoadBalancer IPs can go directly to externalTrafficChain
 | 
				
			||||||
@@ -886,7 +949,7 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
		fwChain := svcInfo.firewallChainName
 | 
							fwChain := svcInfo.firewallChainName
 | 
				
			||||||
		usesFWChain := hasEndpoints && len(svcInfo.LoadBalancerVIPStrings()) > 0 && len(svcInfo.LoadBalancerSourceRanges()) > 0
 | 
							usesFWChain := hasEndpoints && len(svcInfo.LoadBalancerVIPStrings()) > 0 && len(svcInfo.LoadBalancerSourceRanges()) > 0
 | 
				
			||||||
		if usesFWChain {
 | 
							if usesFWChain {
 | 
				
			||||||
			activeNATChains.Insert(fwChain)
 | 
								ensureChain(fwChain, tx, activeChains)
 | 
				
			||||||
			loadBalancerTrafficChain = fwChain
 | 
								loadBalancerTrafficChain = fwChain
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1069,8 +1132,6 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
		// jump to externalTrafficChain, which will handle some special cases and
 | 
							// jump to externalTrafficChain, which will handle some special cases and
 | 
				
			||||||
		// then jump to externalPolicyChain.
 | 
							// then jump to externalPolicyChain.
 | 
				
			||||||
		if usesExternalTrafficChain {
 | 
							if usesExternalTrafficChain {
 | 
				
			||||||
			proxier.natChains.Write(utiliptables.MakeChainLine(utiliptables.Chain(externalTrafficChain)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if !svcInfo.ExternalPolicyLocal() {
 | 
								if !svcInfo.ExternalPolicyLocal() {
 | 
				
			||||||
				// If we are using non-local endpoints we need to masquerade,
 | 
									// If we are using non-local endpoints we need to masquerade,
 | 
				
			||||||
				// in case we cross nodes.
 | 
									// in case we cross nodes.
 | 
				
			||||||
@@ -1123,8 +1184,6 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// Set up firewall chain, if needed
 | 
							// Set up firewall chain, if needed
 | 
				
			||||||
		if usesFWChain {
 | 
							if usesFWChain {
 | 
				
			||||||
			proxier.natChains.Write(utiliptables.MakeChainLine(utiliptables.Chain(fwChain)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// The service firewall rules are created based on the
 | 
								// The service firewall rules are created based on the
 | 
				
			||||||
			// loadBalancerSourceRanges field. This only works for VIP-like
 | 
								// loadBalancerSourceRanges field. This only works for VIP-like
 | 
				
			||||||
			// loadbalancers that preserve source IPs. For loadbalancers which
 | 
								// loadbalancers that preserve source IPs. For loadbalancers which
 | 
				
			||||||
@@ -1171,17 +1230,15 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
			)
 | 
								)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If Cluster policy is in use, create the chain and create rules jumping
 | 
							// If Cluster policy is in use, create rules jumping from
 | 
				
			||||||
		// from clusterPolicyChain to the clusterEndpoints
 | 
							// clusterPolicyChain to the clusterEndpoints
 | 
				
			||||||
		if usesClusterPolicyChain {
 | 
							if usesClusterPolicyChain {
 | 
				
			||||||
			proxier.natChains.Write(utiliptables.MakeChainLine(utiliptables.Chain(clusterPolicyChain)))
 | 
					 | 
				
			||||||
			proxier.writeServiceToEndpointRules(svcPortNameString, svcInfo, clusterPolicyChain, clusterEndpoints)
 | 
								proxier.writeServiceToEndpointRules(svcPortNameString, svcInfo, clusterPolicyChain, clusterEndpoints)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If Local policy is in use, create the chain and create rules jumping
 | 
							// If Local policy is in use, create rules jumping from localPolicyChain
 | 
				
			||||||
		// from localPolicyChain to the localEndpoints
 | 
							// to the localEndpoints
 | 
				
			||||||
		if usesLocalPolicyChain {
 | 
							if usesLocalPolicyChain {
 | 
				
			||||||
			proxier.natChains.Write(utiliptables.MakeChainLine(utiliptables.Chain(localPolicyChain)))
 | 
					 | 
				
			||||||
			proxier.writeServiceToEndpointRules(svcPortNameString, svcInfo, localPolicyChain, localEndpoints)
 | 
								proxier.writeServiceToEndpointRules(svcPortNameString, svcInfo, localPolicyChain, localEndpoints)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1195,10 +1252,6 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			endpointChain := epInfo.chainName
 | 
								endpointChain := epInfo.chainName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Create the endpoint chain
 | 
					 | 
				
			||||||
			proxier.natChains.Write(utiliptables.MakeChainLine(utiliptables.Chain(endpointChain)))
 | 
					 | 
				
			||||||
			activeNATChains.Insert(endpointChain)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Handle traffic that loops back to the originator with SNAT.
 | 
								// Handle traffic that loops back to the originator with SNAT.
 | 
				
			||||||
			proxier.natRules.Write(
 | 
								proxier.natRules.Write(
 | 
				
			||||||
				"-A", string(endpointChain),
 | 
									"-A", string(endpointChain),
 | 
				
			||||||
@@ -1220,33 +1273,6 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Delete chains no longer in use.
 | 
					 | 
				
			||||||
	deletedChains := 0
 | 
					 | 
				
			||||||
	var existingNATChains map[utiliptables.Chain]struct{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	proxier.iptablesData.Reset()
 | 
					 | 
				
			||||||
	if err := proxier.iptables.SaveInto(utiliptables.TableNAT, proxier.iptablesData); err == nil {
 | 
					 | 
				
			||||||
		existingNATChains = utiliptables.GetChainsFromTable(proxier.iptablesData.Bytes())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		for chain := range existingNATChains {
 | 
					 | 
				
			||||||
			if !activeNATChains.Has(string(chain)) {
 | 
					 | 
				
			||||||
				chainString := string(chain)
 | 
					 | 
				
			||||||
				if !isServiceChainName(chainString) {
 | 
					 | 
				
			||||||
					// Ignore chains that aren't ours.
 | 
					 | 
				
			||||||
					continue
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				// We must (as per iptables) write a chain-line
 | 
					 | 
				
			||||||
				// for it, which has the nice effect of flushing
 | 
					 | 
				
			||||||
				// the chain. Then we can remove the chain.
 | 
					 | 
				
			||||||
				proxier.natChains.Write(utiliptables.MakeChainLine(chain))
 | 
					 | 
				
			||||||
				proxier.natRules.Write("-X", chainString)
 | 
					 | 
				
			||||||
				deletedChains++
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		klog.ErrorS(err, "Failed to execute iptables-save: stale chains will not be deleted")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Finally, tail-call to the nodePorts chain.  This needs to be after all
 | 
						// Finally, tail-call to the nodePorts chain.  This needs to be after all
 | 
				
			||||||
	// other service portal rules.
 | 
						// other service portal rules.
 | 
				
			||||||
	if proxier.nodePortAddresses.MatchAll() {
 | 
						if proxier.nodePortAddresses.MatchAll() {
 | 
				
			||||||
@@ -1285,23 +1311,41 @@ func (proxier *Proxier) syncProxyRules() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Figure out which chains are now stale. Unfortunately, we can't delete them
 | 
				
			||||||
 | 
						// right away, because with kernels before 6.2, if there is a map element pointing
 | 
				
			||||||
 | 
						// to a chain, and you delete that map element, the kernel doesn't notice until a
 | 
				
			||||||
 | 
						// short amount of time later that the chain is now unreferenced. So we flush them
 | 
				
			||||||
 | 
						// now, and record the time that they become stale in staleChains so they can be
 | 
				
			||||||
 | 
						// deleted later.
 | 
				
			||||||
 | 
						existingChains, err := proxier.nftables.List(context.TODO(), "chains")
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							for _, chain := range existingChains {
 | 
				
			||||||
 | 
								if isServiceChainName(chain) && !activeChains.Has(chain) {
 | 
				
			||||||
 | 
									tx.Flush(&knftables.Chain{
 | 
				
			||||||
 | 
										Name: chain,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									proxier.staleChains[chain] = start
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if !knftables.IsNotFound(err) {
 | 
				
			||||||
 | 
							klog.ErrorS(err, "Failed to list nftables chains: stale chains will not be deleted")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	metrics.IptablesRulesTotal.WithLabelValues(string(utiliptables.TableFilter)).Set(float64(proxier.filterRules.Lines()))
 | 
						metrics.IptablesRulesTotal.WithLabelValues(string(utiliptables.TableFilter)).Set(float64(proxier.filterRules.Lines()))
 | 
				
			||||||
	metrics.IptablesRulesTotal.WithLabelValues(string(utiliptables.TableNAT)).Set(float64(proxier.natRules.Lines() - deletedChains))
 | 
						metrics.IptablesRulesTotal.WithLabelValues(string(utiliptables.TableNAT)).Set(float64(proxier.natRules.Lines()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Sync rules.
 | 
						// Sync rules.
 | 
				
			||||||
	klog.V(2).InfoS("Reloading service nftables data",
 | 
						klog.V(2).InfoS("Reloading service nftables data",
 | 
				
			||||||
		"numServices", len(proxier.svcPortMap),
 | 
							"numServices", len(proxier.svcPortMap),
 | 
				
			||||||
		"numEndpoints", totalEndpoints,
 | 
							"numEndpoints", totalEndpoints,
 | 
				
			||||||
		"numFilterChains", proxier.filterChains.Lines(),
 | 
					 | 
				
			||||||
		"numFilterRules", proxier.filterRules.Lines(),
 | 
							"numFilterRules", proxier.filterRules.Lines(),
 | 
				
			||||||
		"numNATChains", proxier.natChains.Lines(),
 | 
					 | 
				
			||||||
		"numNATRules", proxier.natRules.Lines(),
 | 
							"numNATRules", proxier.natRules.Lines(),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// FIXME
 | 
						// FIXME
 | 
				
			||||||
	// klog.V(9).InfoS("Running nftables transaction", "transaction", tx.Bytes())
 | 
						// klog.V(9).InfoS("Running nftables transaction", "transaction", tx.Bytes())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := proxier.nftables.Run(context.TODO(), tx)
 | 
						err = proxier.nftables.Run(context.TODO(), tx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		klog.ErrorS(err, "nftables sync failed")
 | 
							klog.ErrorS(err, "nftables sync failed")
 | 
				
			||||||
		metrics.IptablesRestoreFailuresTotal.Inc()
 | 
							metrics.IptablesRestoreFailuresTotal.Inc()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,6 @@ limitations under the License.
 | 
				
			|||||||
package nftables
 | 
					package nftables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
@@ -322,14 +321,12 @@ func NewFakeProxier(ipFamily v1.IPFamily) (*knftables.Fake, *Proxier) {
 | 
				
			|||||||
		hostname:                 testHostname,
 | 
							hostname:                 testHostname,
 | 
				
			||||||
		serviceHealthServer:      healthcheck.NewFakeServiceHealthServer(),
 | 
							serviceHealthServer:      healthcheck.NewFakeServiceHealthServer(),
 | 
				
			||||||
		precomputedProbabilities: make([]string, 0, 1001),
 | 
							precomputedProbabilities: make([]string, 0, 1001),
 | 
				
			||||||
		iptablesData:             bytes.NewBuffer(nil),
 | 
					 | 
				
			||||||
		filterChains:             proxyutil.NewLineBuffer(),
 | 
					 | 
				
			||||||
		filterRules:              proxyutil.NewLineBuffer(),
 | 
							filterRules:              proxyutil.NewLineBuffer(),
 | 
				
			||||||
		natChains:                proxyutil.NewLineBuffer(),
 | 
					 | 
				
			||||||
		natRules:                 proxyutil.NewLineBuffer(),
 | 
							natRules:                 proxyutil.NewLineBuffer(),
 | 
				
			||||||
		nodeIP:                   netutils.ParseIPSloppy(testNodeIP),
 | 
							nodeIP:                   netutils.ParseIPSloppy(testNodeIP),
 | 
				
			||||||
		nodePortAddresses:        proxyutil.NewNodePortAddresses(ipFamily, nil),
 | 
							nodePortAddresses:        proxyutil.NewNodePortAddresses(ipFamily, nil),
 | 
				
			||||||
		networkInterfacer:        networkInterfacer,
 | 
							networkInterfacer:        networkInterfacer,
 | 
				
			||||||
 | 
							staleChains:              make(map[string]time.Time),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	p.setInitialized(true)
 | 
						p.setInitialized(true)
 | 
				
			||||||
	p.syncRunner = async.NewBoundedFrequencyRunner("test-sync-runner", p.syncProxyRules, 0, time.Minute, 1)
 | 
						p.syncRunner = async.NewBoundedFrequencyRunner("test-sync-runner", p.syncProxyRules, 0, time.Minute, 1)
 | 
				
			||||||
@@ -534,6 +531,32 @@ func TestOverallNFTablesRules(t *testing.T) {
 | 
				
			|||||||
		add rule ip kube-proxy nat-postrouting jump masquerading
 | 
							add rule ip kube-proxy nat-postrouting jump masquerading
 | 
				
			||||||
		add chain ip kube-proxy nat-prerouting { type nat hook prerouting priority -100 ; }
 | 
							add chain ip kube-proxy nat-prerouting { type nat hook prerouting priority -100 ; }
 | 
				
			||||||
		add rule ip kube-proxy nat-prerouting jump services
 | 
							add rule ip kube-proxy nat-prerouting jump services
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# svc1
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5OJB2KTY-ns1/svc1/tcp/p80__10.180.0.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# svc2
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-42NFTM6N-ns2/svc2/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy external-42NFTM6N-ns2/svc2/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-SGOXE6O3-ns2/svc2/tcp/p80__10.180.0.2/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# svc3
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy external-4AT6LBPK-ns3/svc3/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-UEIP74TE-ns3/svc3/tcp/p80__10.180.0.3/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# svc4
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-LAUZTJTB-ns4/svc4/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy external-LAUZTJTB-ns4/svc4/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5RFCDDV7-ns4/svc4/tcp/p80__10.180.0.5/80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-UNZV3OEC-ns4/svc4/tcp/p80__10.180.0.4/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# svc5
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-HVFWP5L3-ns5/svc5/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy external-HVFWP5L3-ns5/svc5/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy firewall-HVFWP5L3-ns5/svc5/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80
 | 
				
			||||||
		`)
 | 
							`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
@@ -4229,6 +4252,14 @@ func TestSyncProxyRulesRepeated(t *testing.T) {
 | 
				
			|||||||
		add rule ip kube-proxy nat-prerouting jump services
 | 
							add rule ip kube-proxy nat-prerouting jump services
 | 
				
			||||||
		`)
 | 
							`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Helper function to make it look like time has passed (from the point of view of
 | 
				
			||||||
 | 
						// the stale-chain-deletion code).
 | 
				
			||||||
 | 
						ageStaleChains := func() {
 | 
				
			||||||
 | 
							for chain, t := range fp.staleChains {
 | 
				
			||||||
 | 
								fp.staleChains[chain] = t.Add(-2 * time.Second)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create initial state
 | 
						// Create initial state
 | 
				
			||||||
	var svc2 *v1.Service
 | 
						var svc2 *v1.Service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -4282,7 +4313,11 @@ func TestSyncProxyRulesRepeated(t *testing.T) {
 | 
				
			|||||||
	fp.syncProxyRules()
 | 
						fp.syncProxyRules()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	expected := baseRules + dedent.Dedent(`
 | 
						expected := baseRules + dedent.Dedent(`
 | 
				
			||||||
		# FIXME
 | 
							add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5TPGNJF2-ns1/svc1/tcp/p80__10.0.1.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-MHHHYRWA-ns2/svc2/tcp/p8080
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-7RVP4LUQ-ns2/svc2/tcp/p8080__10.0.2.1/8080
 | 
				
			||||||
                `)
 | 
					                `)
 | 
				
			||||||
	assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -4316,16 +4351,30 @@ func TestSyncProxyRulesRepeated(t *testing.T) {
 | 
				
			|||||||
	fp.syncProxyRules()
 | 
						fp.syncProxyRules()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	expected = baseRules + dedent.Dedent(`
 | 
						expected = baseRules + dedent.Dedent(`
 | 
				
			||||||
		# FIXME
 | 
							add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5TPGNJF2-ns1/svc1/tcp/p80__10.0.1.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-MHHHYRWA-ns2/svc2/tcp/p8080
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-7RVP4LUQ-ns2/svc2/tcp/p8080__10.0.2.1/8080
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-2OCDJSZQ-ns3/svc3/tcp/p80__10.0.3.1/80
 | 
				
			||||||
		`)
 | 
							`)
 | 
				
			||||||
	assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Delete a service.
 | 
						// Delete a service; its chains will be flushed, but not immediately deleted.
 | 
				
			||||||
	fp.OnServiceDelete(svc2)
 | 
						fp.OnServiceDelete(svc2)
 | 
				
			||||||
	fp.syncProxyRules()
 | 
						fp.syncProxyRules()
 | 
				
			||||||
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ageStaleChains()
 | 
				
			||||||
 | 
						fp.syncProxyRules()
 | 
				
			||||||
	expected = baseRules + dedent.Dedent(`
 | 
						expected = baseRules + dedent.Dedent(`
 | 
				
			||||||
		# FIXME
 | 
							add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5TPGNJF2-ns1/svc1/tcp/p80__10.0.1.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-2OCDJSZQ-ns3/svc3/tcp/p80__10.0.3.1/80
 | 
				
			||||||
		`)
 | 
							`)
 | 
				
			||||||
	assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -4343,7 +4392,11 @@ func TestSyncProxyRulesRepeated(t *testing.T) {
 | 
				
			|||||||
	)
 | 
						)
 | 
				
			||||||
	fp.syncProxyRules()
 | 
						fp.syncProxyRules()
 | 
				
			||||||
	expected = baseRules + dedent.Dedent(`
 | 
						expected = baseRules + dedent.Dedent(`
 | 
				
			||||||
		# FIXME
 | 
							add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5TPGNJF2-ns1/svc1/tcp/p80__10.0.1.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-2OCDJSZQ-ns3/svc3/tcp/p80__10.0.3.1/80
 | 
				
			||||||
		`)
 | 
							`)
 | 
				
			||||||
	assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -4362,7 +4415,14 @@ func TestSyncProxyRulesRepeated(t *testing.T) {
 | 
				
			|||||||
	)
 | 
						)
 | 
				
			||||||
	fp.syncProxyRules()
 | 
						fp.syncProxyRules()
 | 
				
			||||||
	expected = baseRules + dedent.Dedent(`
 | 
						expected = baseRules + dedent.Dedent(`
 | 
				
			||||||
		# FIXME
 | 
							add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5TPGNJF2-ns1/svc1/tcp/p80__10.0.1.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-2OCDJSZQ-ns3/svc3/tcp/p80__10.0.3.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-LAUZTJTB-ns4/svc4/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-WAHRBT2B-ns4/svc4/tcp/p80__10.0.4.1/80
 | 
				
			||||||
		`)
 | 
							`)
 | 
				
			||||||
	assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -4372,11 +4432,23 @@ func TestSyncProxyRulesRepeated(t *testing.T) {
 | 
				
			|||||||
	fp.OnEndpointSliceUpdate(eps3, eps3update)
 | 
						fp.OnEndpointSliceUpdate(eps3, eps3update)
 | 
				
			||||||
	fp.syncProxyRules()
 | 
						fp.syncProxyRules()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The old endpoint chain (for 10.0.3.1) will not be deleted yet.
 | 
				
			||||||
	expected = baseRules + dedent.Dedent(`
 | 
						expected = baseRules + dedent.Dedent(`
 | 
				
			||||||
		# FIXME
 | 
							add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5TPGNJF2-ns1/svc1/tcp/p80__10.0.1.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-2OCDJSZQ-ns3/svc3/tcp/p80__10.0.3.1/80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-SWWHDC7X-ns3/svc3/tcp/p80__10.0.3.2/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-LAUZTJTB-ns4/svc4/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-WAHRBT2B-ns4/svc4/tcp/p80__10.0.4.1/80
 | 
				
			||||||
		`)
 | 
							`)
 | 
				
			||||||
	assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// (Ensure the old svc3 chain gets deleted in the next sync.)
 | 
				
			||||||
 | 
						ageStaleChains()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Add an endpoint to a service.
 | 
						// Add an endpoint to a service.
 | 
				
			||||||
	eps3update2 := eps3update.DeepCopy()
 | 
						eps3update2 := eps3update.DeepCopy()
 | 
				
			||||||
	eps3update2.Endpoints = append(eps3update2.Endpoints, discovery.Endpoint{Addresses: []string{"10.0.3.3"}})
 | 
						eps3update2.Endpoints = append(eps3update2.Endpoints, discovery.Endpoint{Addresses: []string{"10.0.3.3"}})
 | 
				
			||||||
@@ -4384,7 +4456,15 @@ func TestSyncProxyRulesRepeated(t *testing.T) {
 | 
				
			|||||||
	fp.syncProxyRules()
 | 
						fp.syncProxyRules()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	expected = baseRules + dedent.Dedent(`
 | 
						expected = baseRules + dedent.Dedent(`
 | 
				
			||||||
		# FIXME
 | 
							add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-5TPGNJF2-ns1/svc1/tcp/p80__10.0.1.1/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-SWWHDC7X-ns3/svc3/tcp/p80__10.0.3.2/80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-TQ2QKHCZ-ns3/svc3/tcp/p80__10.0.3.3/80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							add chain ip kube-proxy service-LAUZTJTB-ns4/svc4/tcp/p80
 | 
				
			||||||
 | 
							add chain ip kube-proxy endpoint-WAHRBT2B-ns4/svc4/tcp/p80__10.0.4.1/80
 | 
				
			||||||
		`)
 | 
							`)
 | 
				
			||||||
	assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
						assertNFTablesTransactionEqual(t, getLine(), expected, nft.Dump())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -4670,3 +4750,156 @@ func TestLoadBalancerIngressRouteTypeProxy(t *testing.T) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Test_servicePortChainNameBase(t *testing.T) {
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name     string
 | 
				
			||||||
 | 
							spn      proxy.ServicePortName
 | 
				
			||||||
 | 
							protocol string
 | 
				
			||||||
 | 
							expected string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "simple",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "testing",
 | 
				
			||||||
 | 
										Name:      "service",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "http",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "tcp",
 | 
				
			||||||
 | 
								expected: "P4ZYZVCF-testing/service/tcp/http",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "different port, different hash",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "testing",
 | 
				
			||||||
 | 
										Name:      "service",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "https",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "tcp",
 | 
				
			||||||
 | 
								expected: "LZBRENCP-testing/service/tcp/https",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "max length",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "very-long-namespace-name-abcdefghijklmnopqrstuvwxyz0123456789xx",
 | 
				
			||||||
 | 
										Name:      "very-long-service-name-why-would-you-even-do-this-i-mean-really",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "port-443-providing-the-hypertext-transmission-protocol-with-tls",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "sctp",
 | 
				
			||||||
 | 
								expected: "KR6NACJP-very-long-namespace-name-abcdefghijklmnopqrstuvwxyz0123456789xx/very-long-service-name-why-would-you-even-do-this-i-mean-really/sctp/port-443-providing-the-hypertext-transmission-protocol-with-tls",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								name := servicePortChainNameBase(&tc.spn, tc.protocol)
 | 
				
			||||||
 | 
								if name != tc.expected {
 | 
				
			||||||
 | 
									t.Errorf("expected %q, got %q", tc.expected, name)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Test_servicePortEndpointChainNameBase(t *testing.T) {
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name     string
 | 
				
			||||||
 | 
							spn      proxy.ServicePortName
 | 
				
			||||||
 | 
							protocol string
 | 
				
			||||||
 | 
							endpoint string
 | 
				
			||||||
 | 
							expected string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "simple",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "testing",
 | 
				
			||||||
 | 
										Name:      "service",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "http",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "tcp",
 | 
				
			||||||
 | 
								endpoint: "10.180.0.1:80",
 | 
				
			||||||
 | 
								expected: "JO2XBXZR-testing/service/tcp/http__10.180.0.1/80",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "different endpoint, different hash",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "testing",
 | 
				
			||||||
 | 
										Name:      "service",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "http",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "tcp",
 | 
				
			||||||
 | 
								endpoint: "10.180.0.2:80",
 | 
				
			||||||
 | 
								expected: "5S6H3H22-testing/service/tcp/http__10.180.0.2/80",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "ipv6",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "testing",
 | 
				
			||||||
 | 
										Name:      "service",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "http",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "tcp",
 | 
				
			||||||
 | 
								endpoint: "[fd80:abcd:12::a1b2:c3d4:e5f6:9999]:80",
 | 
				
			||||||
 | 
								expected: "U7E2ET36-testing/service/tcp/http__fd80.abcd.12..a1b2.c3d4.e5f6.9999/80",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "max length without truncation",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "very-long-namespace-name-abcdefghijklmnopqrstuvwxyz0123456789xx",
 | 
				
			||||||
 | 
										Name:      "very-long-service-name-why-would-you-even-do-this-i-mean-really",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "port-443-providing-the-hypertext-transmission-protocol-with-tls",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "sctp",
 | 
				
			||||||
 | 
								endpoint: "[1234:5678:9abc:def0::abc:1234]:443",
 | 
				
			||||||
 | 
								expected: "5YS7AFEA-very-long-namespace-name-abcdefghijklmnopqrstuvwxyz0123456789xx/very-long-service-name-why-would-you-even-do-this-i-mean-really/sctp/port-443-providing-the-hypertext-transmission-protocol-with-tls__1234.5678.9abc.def0..abc.1234/443",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "truncated, 1",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "very-long-namespace-name-abcdefghijklmnopqrstuvwxyz0123456789xx",
 | 
				
			||||||
 | 
										Name:      "very-long-service-name-why-would-you-even-do-this-i-mean-really",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "port-443-providing-the-hypertext-transmission-protocol-with-tls",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "sctp",
 | 
				
			||||||
 | 
								endpoint: "[1234:5678:9abc:def0::abcd:1234:5678]:443",
 | 
				
			||||||
 | 
								expected: "CI6C53Q3-very-long-namespace-name-abcdefghijklmnopqrstuvwxyz0123456789xx/very-long-service-name-why-would-you-even-do-this-i-mean-really/sctp/port-443-providing-the-hypertext-transmission-protocol-with-tls__1234.5678.9abc.def0..abcd.1234...",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "truncated, 2 (different IP, which is not visible in the result)",
 | 
				
			||||||
 | 
								spn: proxy.ServicePortName{
 | 
				
			||||||
 | 
									NamespacedName: types.NamespacedName{
 | 
				
			||||||
 | 
										Namespace: "very-long-namespace-name-abcdefghijklmnopqrstuvwxyz0123456789xx",
 | 
				
			||||||
 | 
										Name:      "very-long-service-name-why-would-you-even-do-this-i-mean-really",
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									Port: "port-443-providing-the-hypertext-transmission-protocol-with-tls",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								protocol: "sctp",
 | 
				
			||||||
 | 
								endpoint: "[1234:5678:9abc:def0::abcd:1234:8765]:443",
 | 
				
			||||||
 | 
								expected: "2FLXFK6X-very-long-namespace-name-abcdefghijklmnopqrstuvwxyz0123456789xx/very-long-service-name-why-would-you-even-do-this-i-mean-really/sctp/port-443-providing-the-hypertext-transmission-protocol-with-tls__1234.5678.9abc.def0..abcd.1234...",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							t.Run(tc.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								name := servicePortEndpointChainNameBase(&tc.spn, tc.protocol, tc.endpoint)
 | 
				
			||||||
 | 
								if name != tc.expected {
 | 
				
			||||||
 | 
									t.Errorf("expected %q, got %q", tc.expected, name)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user