Commit 15b8fb96 authored by Brad Davidson's avatar Brad Davidson Committed by Brad Davidson

Refactor egress-selector pods mode to watch pods

Watching pods appears to be the most reliable way to ensure that the proxy routes and authorizes connections. Signed-off-by: 's avatarBrad Davidson <brad.davidson@rancher.com>
parent 8456d982
...@@ -30,8 +30,17 @@ const ( ...@@ -30,8 +30,17 @@ const (
EgressSelectorModeDisabled = "disabled" EgressSelectorModeDisabled = "disabled"
EgressSelectorModePod = "pod" EgressSelectorModePod = "pod"
CertificateRenewDays = 90 CertificateRenewDays = 90
StreamServerPort = "10010"
KubeletPort = "10250"
) )
// These ports can always be accessed via the tunnel server, at the loopback address.
// Other addresses and ports are only accessible via the tunnel on newer agents, when used by a pod.
var KubeletReservedPorts = map[string]bool{
StreamServerPort: true,
KubeletPort: true,
}
type Node struct { type Node struct {
Docker bool Docker bool
ContainerRuntimeEndpoint string ContainerRuntimeEndpoint string
......
...@@ -11,12 +11,18 @@ import ( ...@@ -11,12 +11,18 @@ import (
"github.com/k3s-io/k3s/pkg/daemons/config" "github.com/k3s-io/k3s/pkg/daemons/config"
"github.com/k3s-io/k3s/pkg/daemons/control/proxy" "github.com/k3s-io/k3s/pkg/daemons/control/proxy"
"github.com/k3s-io/k3s/pkg/generated/clientset/versioned/scheme"
"github.com/k3s-io/k3s/pkg/nodeconfig" "github.com/k3s-io/k3s/pkg/nodeconfig"
"github.com/k3s-io/k3s/pkg/util"
"github.com/k3s-io/k3s/pkg/version" "github.com/k3s-io/k3s/pkg/version"
"github.com/pkg/errors"
"github.com/rancher/remotedialer" "github.com/rancher/remotedialer"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/yl2chen/cidranger" "github.com/yl2chen/cidranger"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
) )
...@@ -67,9 +73,9 @@ type TunnelServer struct { ...@@ -67,9 +73,9 @@ type TunnelServer struct {
var _ cidranger.RangerEntry = &tunnelEntry{} var _ cidranger.RangerEntry = &tunnelEntry{}
type tunnelEntry struct { type tunnelEntry struct {
cidr net.IPNet cidr net.IPNet
node string nodeName string
kubelet bool node bool
} }
func (n *tunnelEntry) Network() net.IPNet { func (n *tunnelEntry) Network() net.IPNet {
...@@ -98,10 +104,9 @@ func (t *TunnelServer) watch(ctx context.Context) { ...@@ -98,10 +104,9 @@ func (t *TunnelServer) watch(ctx context.Context) {
for { for {
if t.config.Runtime.Core != nil { if t.config.Runtime.Core != nil {
t.config.Runtime.Core.Core().V1().Node().OnChange(ctx, version.Program+"-tunnel-server", t.onChangeNode) t.config.Runtime.Core.Core().V1().Node().OnChange(ctx, version.Program+"-tunnel-server", t.onChangeNode)
if t.config.EgressSelectorMode == config.EgressSelectorModeCluster { switch t.config.EgressSelectorMode {
// Cluster mode watches Endpoints to find what Node is hosting an Endpoint address, as the CNI case config.EgressSelectorModeCluster, config.EgressSelectorModePod:
// may be using its own IPAM that does not repsect the Node's PodCIDR. t.config.Runtime.Core.Core().V1().Pod().OnChange(ctx, version.Program+"-tunnel-server", t.onChangePod)
t.config.Runtime.Core.Core().V1().Endpoints().OnChange(ctx, version.Program+"-tunnel-server", t.onChangeEndpoints)
} }
return return
} }
...@@ -110,41 +115,22 @@ func (t *TunnelServer) watch(ctx context.Context) { ...@@ -110,41 +115,22 @@ func (t *TunnelServer) watch(ctx context.Context) {
} }
} }
// onChangeNode updates the node address/CIDR mappings by observing changes to nodes. // onChangeNode updates the node address mappings by observing changes to nodes.
// Node addresses are updated in Agent, Cluster, and Pod mode.
// Pod CIDRs are updated only in Pod mode
func (t *TunnelServer) onChangeNode(nodeName string, node *v1.Node) (*v1.Node, error) { func (t *TunnelServer) onChangeNode(nodeName string, node *v1.Node) (*v1.Node, error) {
if node != nil { if node != nil {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
logrus.Debugf("Tunnel server egress proxy updating node %s", nodeName)
_, t.egress[nodeName] = node.Labels[nodeconfig.ClusterEgressLabel] _, t.egress[nodeName] = node.Labels[nodeconfig.ClusterEgressLabel]
// Add all node IP addresses // Add all node IP addresses
for _, addr := range node.Status.Addresses { for _, addr := range node.Status.Addresses {
if addr.Type == v1.NodeInternalIP || addr.Type == v1.NodeExternalIP { if addr.Type == v1.NodeInternalIP || addr.Type == v1.NodeExternalIP {
address := addr.Address if n, err := util.IPStringToIPNet(addr.Address); err == nil {
if strings.Contains(address, ":") {
address += "/128"
} else {
address += "/32"
}
if _, n, err := net.ParseCIDR(address); err == nil {
if node.DeletionTimestamp != nil {
t.cidrs.Remove(*n)
} else {
t.cidrs.Insert(&tunnelEntry{cidr: *n, node: nodeName, kubelet: true})
}
}
}
}
// Add all Node PodCIDRs, if in pod mode
if t.config.EgressSelectorMode == config.EgressSelectorModePod {
for _, cidr := range node.Spec.PodCIDRs {
if _, n, err := net.ParseCIDR(cidr); err == nil {
if node.DeletionTimestamp != nil { if node.DeletionTimestamp != nil {
logrus.Debugf("Tunnel server egress proxy removing Node %s IP %v", nodeName, n)
t.cidrs.Remove(*n) t.cidrs.Remove(*n)
} else { } else {
t.cidrs.Insert(&tunnelEntry{cidr: *n, node: nodeName}) logrus.Debugf("Tunnel server egress proxy updating Node %s IP %v", nodeName, n)
t.cidrs.Insert(&tunnelEntry{cidr: *n, nodeName: nodeName, node: true})
} }
} }
} }
...@@ -153,45 +139,29 @@ func (t *TunnelServer) onChangeNode(nodeName string, node *v1.Node) (*v1.Node, e ...@@ -153,45 +139,29 @@ func (t *TunnelServer) onChangeNode(nodeName string, node *v1.Node) (*v1.Node, e
return node, nil return node, nil
} }
// onChangeEndpoits updates the pod address mappings by observing changes to endpoints. // onChangePod updates the pod address mappings by observing changes to pods.
// Only Pod endpoints with a defined NodeName are used, and only in Cluster mode. func (t *TunnelServer) onChangePod(podName string, pod *v1.Pod) (*v1.Pod, error) {
func (t *TunnelServer) onChangeEndpoints(endpointsName string, endpoints *v1.Endpoints) (*v1.Endpoints, error) { if pod != nil {
if endpoints != nil {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
logrus.Debugf("Tunnel server egress proxy updating endpoints %s", endpointsName) // Add all pod IPs, unless the pod uses host network
// Add all Pod endpoints if !pod.Spec.HostNetwork {
for _, subset := range endpoints.Subsets { nodeName := pod.Spec.NodeName
for _, addr := range subset.Addresses { for _, ip := range pod.Status.PodIPs {
if addr.NodeName != nil && addr.TargetRef != nil && addr.TargetRef.Kind == "Pod" { if cidr, err := util.IPStringToIPNet(ip.IP); err == nil {
nodeName := *addr.NodeName if pod.DeletionTimestamp != nil {
address := addr.IP logrus.Debugf("Tunnel server egress proxy removing Node %s Pod IP %v", nodeName, cidr)
if strings.Contains(address, ":") { t.cidrs.Remove(*cidr)
address += "/128"
} else { } else {
address += "/32" logrus.Debugf("Tunnel server egress proxy updating Node %s Pod IP %s", nodeName, cidr)
} t.cidrs.Insert(&tunnelEntry{cidr: *cidr, nodeName: nodeName})
if _, n, err := net.ParseCIDR(address); err == nil {
t.cidrs.Insert(&tunnelEntry{cidr: *n, node: nodeName})
}
}
}
for _, addr := range subset.NotReadyAddresses {
if addr.TargetRef != nil && addr.TargetRef.Kind == "Pod" {
address := addr.IP
if strings.Contains(address, ":") {
address += "/128"
} else {
address += "/32"
}
if _, n, err := net.ParseCIDR(address); err == nil {
t.cidrs.Remove(*n)
} }
} }
} }
} }
} }
return endpoints, nil return pod, nil
} }
// serveConnect attempts to handle the HTTP CONNECT request by dialing // serveConnect attempts to handle the HTTP CONNECT request by dialing
...@@ -199,20 +169,29 @@ func (t *TunnelServer) onChangeEndpoints(endpointsName string, endpoints *v1.End ...@@ -199,20 +169,29 @@ func (t *TunnelServer) onChangeEndpoints(endpointsName string, endpoints *v1.End
func (t *TunnelServer) serveConnect(resp http.ResponseWriter, req *http.Request) { func (t *TunnelServer) serveConnect(resp http.ResponseWriter, req *http.Request) {
bconn, err := t.dialBackend(req.Host) bconn, err := t.dialBackend(req.Host)
if err != nil { if err != nil {
http.Error(resp, fmt.Sprintf("no tunnels available: %v", err), http.StatusInternalServerError) responsewriters.ErrorNegotiated(
apierrors.NewInternalError(errors.Wrap(err, "no tunnels available")),
scheme.Codecs.WithoutConversion(), schema.GroupVersion{}, resp, req,
)
return return
} }
hijacker, ok := resp.(http.Hijacker) hijacker, ok := resp.(http.Hijacker)
if !ok { if !ok {
http.Error(resp, "hijacking not supported", http.StatusInternalServerError) responsewriters.ErrorNegotiated(
apierrors.NewInternalError(errors.New("hijacking not supported")),
scheme.Codecs.WithoutConversion(), schema.GroupVersion{}, resp, req,
)
return return
} }
resp.WriteHeader(http.StatusOK) resp.WriteHeader(http.StatusOK)
rconn, _, err := hijacker.Hijack() rconn, _, err := hijacker.Hijack()
if err != nil { if err != nil {
http.Error(resp, err.Error(), http.StatusInternalServerError) responsewriters.ErrorNegotiated(
apierrors.NewInternalError(err),
scheme.Codecs.WithoutConversion(), schema.GroupVersion{}, resp, req,
)
return return
} }
...@@ -231,46 +210,46 @@ func (t *TunnelServer) dialBackend(addr string) (net.Conn, error) { ...@@ -231,46 +210,46 @@ func (t *TunnelServer) dialBackend(addr string) (net.Conn, error) {
} }
loopback := t.config.Loopback() loopback := t.config.Loopback()
var node string var nodeName string
var toKubelet, useTunnel bool var toKubelet, useTunnel bool
if ip := net.ParseIP(host); ip != nil { if ip := net.ParseIP(host); ip != nil {
// Destination is an IP address, check to see if the target is a kubelet or pod address. // Destination is an IP address, which could be either a pod, or node by IP.
// We can only use the tunnel for egress to pods if the agent supports it. // We can only use the tunnel for egress to pods if the agent supports it.
if nets, err := t.cidrs.ContainingNetworks(ip); err == nil && len(nets) > 0 { if nets, err := t.cidrs.ContainingNetworks(ip); err == nil && len(nets) > 0 {
if n, ok := nets[0].(*tunnelEntry); ok { if n, ok := nets[0].(*tunnelEntry); ok {
node = n.node nodeName = n.nodeName
if n.kubelet { if n.node && config.KubeletReservedPorts[port] {
toKubelet = true toKubelet = true
useTunnel = true useTunnel = true
} else { } else {
useTunnel = t.egress[node] useTunnel = t.egress[nodeName]
} }
} else { } else {
logrus.Debugf("Tunnel server egress proxy CIDR lookup returned unknown type for address %s", ip) logrus.Debugf("Tunnel server egress proxy CIDR lookup returned unknown type for address %s", ip)
} }
} }
} else { } else {
// Destination is a kubelet by name, it is safe to use the tunnel. // Destination is a node by name, it is safe to use the tunnel.
node = host nodeName = host
toKubelet = true toKubelet = true
useTunnel = true useTunnel = true
} }
// Always dial kubelets via the loopback address. // Always dial kubelet via the loopback address.
if toKubelet { if toKubelet {
addr = fmt.Sprintf("%s:%s", loopback, port) addr = fmt.Sprintf("%s:%s", loopback, port)
} }
// If connecting to something hosted by the local node, don't tunnel // If connecting to something hosted by the local node, don't tunnel
if node == t.config.ServerNodeName { if nodeName == t.config.ServerNodeName {
useTunnel = false useTunnel = false
} }
if t.server.HasSession(node) { if t.server.HasSession(nodeName) {
if useTunnel { if useTunnel {
// Have a session and it is safe to use for this destination, do so. // Have a session and it is safe to use for this destination, do so.
logrus.Debugf("Tunnel server egress proxy dialing %s via session to %s", addr, node) logrus.Debugf("Tunnel server egress proxy dialing %s via session to %s", addr, nodeName)
return t.server.Dial(node, 15*time.Second, "tcp", addr) return t.server.Dial(nodeName, 15*time.Second, "tcp", addr)
} }
// Have a session but the agent doesn't support tunneling to this destination or // Have a session but the agent doesn't support tunneling to this destination or
// the destination is local; fall back to direct connection. // the destination is local; fall back to direct connection.
......
...@@ -256,3 +256,26 @@ func IsIPv6OnlyCIDRs(cidrs []*net.IPNet) (bool, error) { ...@@ -256,3 +256,26 @@ func IsIPv6OnlyCIDRs(cidrs []*net.IPNet) (bool, error) {
return !v4Found && v6Found, nil return !v4Found && v6Found, nil
} }
// IPToIPNet converts an IP to an IPNet, using a fully filled mask appropriate for the address family.
func IPToIPNet(ip net.IP) (*net.IPNet, error) {
address := ip.String()
if strings.Contains(address, ":") {
address += "/128"
} else {
address += "/32"
}
_, cidr, err := net.ParseCIDR(address)
return cidr, err
}
// IPStringToIPNet converts an IP string to an IPNet, using a fully filled mask appropriate for the address family.
func IPStringToIPNet(address string) (*net.IPNet, error) {
if strings.Contains(address, ":") {
address += "/128"
} else {
address += "/32"
}
_, cidr, err := net.ParseCIDR(address)
return cidr, err
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment