Files
moby/libnetwork/drivers/overlay/encryption.go
Cory Snider 33564a0c03 libnetwork/d/overlay: support IPv6 transport
The forwarding database (fdb) of Linux VXLAN links are restricted to
entries with destination VXLAN tunnel endpoint (VTEP) address of a
single address family. Which address family is permitted is set when the
link is created and cannot be modified. The overlay network driver
creates VXLAN links such that the kernel only allows fdb entries to be
created with IPv4 destination VTEP addresses. If the Swarm is configured
with IPv6 advertise addresses, creating fdb entries for remote peers
fails with EAFNOSUPPORT (address family not supported by protocol).

Make overlay networks functional over IPv6 transport by configuring the
VXLAN links for IPv6 VTEPs if the local node's advertise address is an
IPv6 address. Make encrypted overlay networks secure over IPv6 transport
by applying the iptables rules to the ip6tables when appropriate.

Signed-off-by: Cory Snider <csnider@mirantis.com>
2023-11-09 12:04:47 -05:00

702 lines
17 KiB
Go

//go:build linux
package overlay
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"hash/fnv"
"net"
"strconv"
"sync"
"syscall"
"github.com/containerd/log"
"github.com/docker/docker/libnetwork/drivers/overlay/overlayutils"
"github.com/docker/docker/libnetwork/iptables"
"github.com/docker/docker/libnetwork/ns"
"github.com/docker/docker/libnetwork/types"
"github.com/vishvananda/netlink"
)
/*
Encrypted overlay networks use IPsec in transport mode to encrypt and
authenticate the VXLAN UDP datagrams. This driver implements a bespoke control
plane which negotiates the security parameters for each peer-to-peer tunnel.
IPsec Terminology
- ESP: IPSec Encapsulating Security Payload
- SPI: Security Parameter Index
- ICV: Integrity Check Value
- SA: Security Association https://en.wikipedia.org/wiki/IPsec#Security_association
Developer documentation for Linux IPsec is rather sparse online. The following
slide deck provides a decent overview.
https://libreswan.org/wiki/images/e/e0/Netdev-0x12-ipsec-flow.pdf
The Linux IPsec stack is part of XFRM, the netlink packet transformation
interface.
https://man7.org/linux/man-pages/man8/ip-xfrm.8.html
*/
const (
// Value used to mark outgoing packets which should have our IPsec
// processing applied. It is also used as a label to identify XFRM
// states (Security Associations) and policies (Security Policies)
// programmed by us so we know which ones we can clean up without
// disrupting other VPN connections on the system.
mark = 0xD0C4E3
pktExpansion = 26 // SPI(4) + SeqN(4) + IV(8) + PadLength(1) + NextHeader(1) + ICV(8)
)
const (
forward = iota + 1
reverse
bidir
)
// Mark value for matching packets which should have our IPsec security policy
// applied.
var spMark = netlink.XfrmMark{Value: mark, Mask: 0xffffffff}
type key struct {
value []byte
tag uint32
}
func (k *key) String() string {
if k != nil {
return fmt.Sprintf("(key: %s, tag: 0x%x)", hex.EncodeToString(k.value)[0:5], k.tag)
}
return ""
}
// Security Parameter Indices for the IPsec flows between local node and a
// remote peer, which identify the Security Associations (XFRM states) to be
// applied when encrypting and decrypting packets.
type spi struct {
forward int
reverse int
}
func (s *spi) String() string {
return fmt.Sprintf("SPI(FWD: 0x%x, REV: 0x%x)", uint32(s.forward), uint32(s.reverse))
}
type encrMap struct {
nodes map[string][]*spi
sync.Mutex
}
func (e *encrMap) String() string {
e.Lock()
defer e.Unlock()
b := new(bytes.Buffer)
for k, v := range e.nodes {
b.WriteString("\n")
b.WriteString(k)
b.WriteString(":")
b.WriteString("[")
for _, s := range v {
b.WriteString(s.String())
b.WriteString(",")
}
b.WriteString("]")
}
return b.String()
}
func (d *driver) checkEncryption(nid string, rIP net.IP, isLocal, add bool) error {
log.G(context.TODO()).Debugf("checkEncryption(%.7s, %v, %t)", nid, rIP, isLocal)
n := d.network(nid)
if n == nil || !n.secure {
return nil
}
if len(d.keys) == 0 {
return types.ForbiddenErrorf("encryption key is not present")
}
lIP := d.bindAddress
aIP := d.advertiseAddress
nodes := map[string]net.IP{}
switch {
case isLocal:
if err := d.peerDbNetworkWalk(nid, func(pKey *peerKey, pEntry *peerEntry) bool {
if !aIP.Equal(pEntry.vtep) {
nodes[pEntry.vtep.String()] = pEntry.vtep
}
return false
}); err != nil {
log.G(context.TODO()).Warnf("Failed to retrieve list of participating nodes in overlay network %.5s: %v", nid, err)
}
default:
if len(d.network(nid).endpoints) > 0 {
nodes[rIP.String()] = rIP
}
}
log.G(context.TODO()).Debugf("List of nodes: %s", nodes)
if add {
for _, rIP := range nodes {
if err := setupEncryption(lIP, aIP, rIP, d.secMap, d.keys); err != nil {
log.G(context.TODO()).Warnf("Failed to program network encryption between %s and %s: %v", lIP, rIP, err)
}
}
} else {
if len(nodes) == 0 {
if err := removeEncryption(lIP, rIP, d.secMap); err != nil {
log.G(context.TODO()).Warnf("Failed to remove network encryption between %s and %s: %v", lIP, rIP, err)
}
}
}
return nil
}
// setupEncryption programs the encryption parameters for secure communication
// between the local node and a remote node.
func setupEncryption(localIP, advIP, remoteIP net.IP, em *encrMap, keys []*key) error {
log.G(context.TODO()).Debugf("Programming encryption between %s and %s", localIP, remoteIP)
rIPs := remoteIP.String()
indices := make([]*spi, 0, len(keys))
for i, k := range keys {
spis := &spi{buildSPI(advIP, remoteIP, k.tag), buildSPI(remoteIP, advIP, k.tag)}
dir := reverse
if i == 0 {
dir = bidir
}
fSA, rSA, err := programSA(localIP, remoteIP, spis, k, dir, true)
if err != nil {
log.G(context.TODO()).Warn(err)
}
indices = append(indices, spis)
if i != 0 {
continue
}
err = programSP(fSA, rSA, true)
if err != nil {
log.G(context.TODO()).Warn(err)
}
}
em.Lock()
em.nodes[rIPs] = indices
em.Unlock()
return nil
}
func removeEncryption(localIP, remoteIP net.IP, em *encrMap) error {
em.Lock()
indices, ok := em.nodes[remoteIP.String()]
em.Unlock()
if !ok {
return nil
}
for i, idxs := range indices {
dir := reverse
if i == 0 {
dir = bidir
}
fSA, rSA, err := programSA(localIP, remoteIP, idxs, nil, dir, false)
if err != nil {
log.G(context.TODO()).Warn(err)
}
if i != 0 {
continue
}
err = programSP(fSA, rSA, false)
if err != nil {
log.G(context.TODO()).Warn(err)
}
}
return nil
}
func (d *driver) transportIPTable() (*iptables.IPTable, error) {
v6, err := d.isIPv6Transport()
if err != nil {
return nil, err
}
version := iptables.IPv4
if v6 {
version = iptables.IPv6
}
return iptables.GetIptable(version), nil
}
func (d *driver) programMangle(vni uint32, add bool) error {
var (
m = strconv.FormatUint(mark, 10)
chain = "OUTPUT"
rule = append(matchVXLAN(overlayutils.VXLANUDPPort(), vni), "-j", "MARK", "--set-mark", m)
a = iptables.Append
action = "install"
)
iptable, err := d.transportIPTable()
if err != nil {
// Fail closed if unsure. Better safe than cleartext.
return err
}
if !add {
a = iptables.Delete
action = "remove"
}
if err := iptable.ProgramRule(iptables.Mangle, chain, a, rule); err != nil {
return fmt.Errorf("could not %s mangle rule: %w", action, err)
}
return nil
}
func (d *driver) programInput(vni uint32, add bool) error {
var (
plainVxlan = matchVXLAN(overlayutils.VXLANUDPPort(), vni)
chain = "INPUT"
msg = "add"
)
rule := func(policy, jump string) []string {
args := append([]string{"-m", "policy", "--dir", "in", "--pol", policy}, plainVxlan...)
return append(args, "-j", jump)
}
iptable, err := d.transportIPTable()
if err != nil {
// Fail closed if unsure. Better safe than cleartext.
return err
}
if !add {
msg = "remove"
}
action := func(a iptables.Action) iptables.Action {
if !add {
return iptables.Delete
}
return a
}
// Drop incoming VXLAN datagrams for the VNI which were received in cleartext.
// Insert at the top of the chain so the packets are dropped even if an
// administrator-configured rule exists which would otherwise unconditionally
// accept incoming VXLAN traffic.
if err := iptable.ProgramRule(iptables.Filter, chain, action(iptables.Insert), rule("none", "DROP")); err != nil {
return fmt.Errorf("could not %s input drop rule: %w", msg, err)
}
return nil
}
func programSA(localIP, remoteIP net.IP, spi *spi, k *key, dir int, add bool) (fSA *netlink.XfrmState, rSA *netlink.XfrmState, err error) {
var (
action = "Removing"
xfrmProgram = ns.NlHandle().XfrmStateDel
)
if add {
action = "Adding"
xfrmProgram = ns.NlHandle().XfrmStateAdd
}
if dir&reverse > 0 {
rSA = &netlink.XfrmState{
Src: remoteIP,
Dst: localIP,
Proto: netlink.XFRM_PROTO_ESP,
Spi: spi.reverse,
Mode: netlink.XFRM_MODE_TRANSPORT,
Reqid: mark,
}
if add {
rSA.Aead = buildAeadAlgo(k, spi.reverse)
}
exists, err := saExists(rSA)
if err != nil {
exists = !add
}
if add != exists {
log.G(context.TODO()).Debugf("%s: rSA{%s}", action, rSA)
if err := xfrmProgram(rSA); err != nil {
log.G(context.TODO()).Warnf("Failed %s rSA{%s}: %v", action, rSA, err)
}
}
}
if dir&forward > 0 {
fSA = &netlink.XfrmState{
Src: localIP,
Dst: remoteIP,
Proto: netlink.XFRM_PROTO_ESP,
Spi: spi.forward,
Mode: netlink.XFRM_MODE_TRANSPORT,
Reqid: mark,
}
if add {
fSA.Aead = buildAeadAlgo(k, spi.forward)
}
exists, err := saExists(fSA)
if err != nil {
exists = !add
}
if add != exists {
log.G(context.TODO()).Debugf("%s fSA{%s}", action, fSA)
if err := xfrmProgram(fSA); err != nil {
log.G(context.TODO()).Warnf("Failed %s fSA{%s}: %v.", action, fSA, err)
}
}
}
return
}
// getMinimalIP returns the address in its shortest form
// If ip contains an IPv4-mapped IPv6 address, the 4-octet form of the IPv4 address will be returned.
// Otherwise ip is returned unchanged.
func getMinimalIP(ip net.IP) net.IP {
if ip != nil && ip.To4() != nil {
return ip.To4()
}
return ip
}
func programSP(fSA *netlink.XfrmState, rSA *netlink.XfrmState, add bool) error {
action := "Removing"
xfrmProgram := ns.NlHandle().XfrmPolicyDel
if add {
action = "Adding"
xfrmProgram = ns.NlHandle().XfrmPolicyAdd
}
// Create a congruent cidr
s := getMinimalIP(fSA.Src)
d := getMinimalIP(fSA.Dst)
fullMask := net.CIDRMask(8*len(s), 8*len(s))
fPol := &netlink.XfrmPolicy{
Src: &net.IPNet{IP: s, Mask: fullMask},
Dst: &net.IPNet{IP: d, Mask: fullMask},
Dir: netlink.XFRM_DIR_OUT,
Proto: syscall.IPPROTO_UDP,
DstPort: int(overlayutils.VXLANUDPPort()),
Mark: &spMark,
Tmpls: []netlink.XfrmPolicyTmpl{
{
Src: fSA.Src,
Dst: fSA.Dst,
Proto: netlink.XFRM_PROTO_ESP,
Mode: netlink.XFRM_MODE_TRANSPORT,
Spi: fSA.Spi,
Reqid: mark,
},
},
}
exists, err := spExists(fPol)
if err != nil {
exists = !add
}
if add != exists {
log.G(context.TODO()).Debugf("%s fSP{%s}", action, fPol)
if err := xfrmProgram(fPol); err != nil {
log.G(context.TODO()).Warnf("%s fSP{%s}: %v", action, fPol, err)
}
}
return nil
}
func saExists(sa *netlink.XfrmState) (bool, error) {
_, err := ns.NlHandle().XfrmStateGet(sa)
switch err {
case nil:
return true, nil
case syscall.ESRCH:
return false, nil
default:
err = fmt.Errorf("Error while checking for SA existence: %v", err)
log.G(context.TODO()).Warn(err)
return false, err
}
}
func spExists(sp *netlink.XfrmPolicy) (bool, error) {
_, err := ns.NlHandle().XfrmPolicyGet(sp)
switch err {
case nil:
return true, nil
case syscall.ENOENT:
return false, nil
default:
err = fmt.Errorf("Error while checking for SP existence: %v", err)
log.G(context.TODO()).Warn(err)
return false, err
}
}
func buildSPI(src, dst net.IP, st uint32) int {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, st)
h := fnv.New32a()
h.Write(src)
h.Write(b)
h.Write(dst)
return int(binary.BigEndian.Uint32(h.Sum(nil)))
}
func buildAeadAlgo(k *key, s int) *netlink.XfrmStateAlgo {
salt := make([]byte, 4)
binary.BigEndian.PutUint32(salt, uint32(s))
return &netlink.XfrmStateAlgo{
Name: "rfc4106(gcm(aes))",
Key: append(k.value, salt...),
ICVLen: 64,
}
}
func (d *driver) secMapWalk(f func(string, []*spi) ([]*spi, bool)) error {
d.secMap.Lock()
for node, indices := range d.secMap.nodes {
idxs, stop := f(node, indices)
if idxs != nil {
d.secMap.nodes[node] = idxs
}
if stop {
break
}
}
d.secMap.Unlock()
return nil
}
func (d *driver) setKeys(keys []*key) error {
// Remove any stale policy, state
clearEncryptionStates()
// Accept the encryption keys and clear any stale encryption map
d.Lock()
d.keys = keys
d.secMap = &encrMap{nodes: map[string][]*spi{}}
d.Unlock()
log.G(context.TODO()).Debugf("Initial encryption keys: %v", keys)
return nil
}
// updateKeys allows to add a new key and/or change the primary key and/or prune an existing key
// The primary key is the key used in transmission and will go in first position in the list.
func (d *driver) updateKeys(newKey, primary, pruneKey *key) error {
log.G(context.TODO()).Debugf("Updating Keys. New: %v, Primary: %v, Pruned: %v", newKey, primary, pruneKey)
log.G(context.TODO()).Debugf("Current: %v", d.keys)
var (
newIdx = -1
priIdx = -1
delIdx = -1
lIP = d.bindAddress
aIP = d.advertiseAddress
)
d.Lock()
defer d.Unlock()
// add new
if newKey != nil {
d.keys = append(d.keys, newKey)
newIdx += len(d.keys)
}
for i, k := range d.keys {
if primary != nil && k.tag == primary.tag {
priIdx = i
}
if pruneKey != nil && k.tag == pruneKey.tag {
delIdx = i
}
}
if (newKey != nil && newIdx == -1) ||
(primary != nil && priIdx == -1) ||
(pruneKey != nil && delIdx == -1) {
return types.InvalidParameterErrorf("cannot find proper key indices while processing key update:"+
"(newIdx,priIdx,delIdx):(%d, %d, %d)", newIdx, priIdx, delIdx)
}
if priIdx != -1 && priIdx == delIdx {
return types.InvalidParameterErrorf("attempting to both make a key (index %d) primary and delete it", priIdx)
}
d.secMapWalk(func(rIPs string, spis []*spi) ([]*spi, bool) {
rIP := net.ParseIP(rIPs)
return updateNodeKey(lIP, aIP, rIP, spis, d.keys, newIdx, priIdx, delIdx), false
})
// swap primary
if priIdx != -1 {
d.keys[0], d.keys[priIdx] = d.keys[priIdx], d.keys[0]
}
// prune
if delIdx != -1 {
if delIdx == 0 {
delIdx = priIdx
}
d.keys = append(d.keys[:delIdx], d.keys[delIdx+1:]...)
}
log.G(context.TODO()).Debugf("Updated: %v", d.keys)
return nil
}
/********************************************************
* Steady state: rSA0, rSA1, rSA2, fSA1, fSP1
* Rotation --> -rSA0, +rSA3, +fSA2, +fSP2/-fSP1, -fSA1
* Steady state: rSA1, rSA2, rSA3, fSA2, fSP2
*********************************************************/
// Spis and keys are sorted in such away the one in position 0 is the primary
func updateNodeKey(lIP, aIP, rIP net.IP, idxs []*spi, curKeys []*key, newIdx, priIdx, delIdx int) []*spi {
log.G(context.TODO()).Debugf("Updating keys for node: %s (%d,%d,%d)", rIP, newIdx, priIdx, delIdx)
spis := idxs
log.G(context.TODO()).Debugf("Current: %v", spis)
// add new
if newIdx != -1 {
spis = append(spis, &spi{
forward: buildSPI(aIP, rIP, curKeys[newIdx].tag),
reverse: buildSPI(rIP, aIP, curKeys[newIdx].tag),
})
}
if delIdx != -1 {
// -rSA0
programSA(lIP, rIP, spis[delIdx], nil, reverse, false)
}
if newIdx > -1 {
// +rSA2
programSA(lIP, rIP, spis[newIdx], curKeys[newIdx], reverse, true)
}
if priIdx > 0 {
// +fSA2
fSA2, _, _ := programSA(lIP, rIP, spis[priIdx], curKeys[priIdx], forward, true)
// +fSP2, -fSP1
s := getMinimalIP(fSA2.Src)
d := getMinimalIP(fSA2.Dst)
fullMask := net.CIDRMask(8*len(s), 8*len(s))
fSP1 := &netlink.XfrmPolicy{
Src: &net.IPNet{IP: s, Mask: fullMask},
Dst: &net.IPNet{IP: d, Mask: fullMask},
Dir: netlink.XFRM_DIR_OUT,
Proto: syscall.IPPROTO_UDP,
DstPort: int(overlayutils.VXLANUDPPort()),
Mark: &spMark,
Tmpls: []netlink.XfrmPolicyTmpl{
{
Src: fSA2.Src,
Dst: fSA2.Dst,
Proto: netlink.XFRM_PROTO_ESP,
Mode: netlink.XFRM_MODE_TRANSPORT,
Spi: fSA2.Spi,
Reqid: mark,
},
},
}
log.G(context.TODO()).Debugf("Updating fSP{%s}", fSP1)
if err := ns.NlHandle().XfrmPolicyUpdate(fSP1); err != nil {
log.G(context.TODO()).Warnf("Failed to update fSP{%s}: %v", fSP1, err)
}
// -fSA1
programSA(lIP, rIP, spis[0], nil, forward, false)
}
// swap
if priIdx > 0 {
swp := spis[0]
spis[0] = spis[priIdx]
spis[priIdx] = swp
}
// prune
if delIdx != -1 {
if delIdx == 0 {
delIdx = priIdx
}
spis = append(spis[:delIdx], spis[delIdx+1:]...)
}
log.G(context.TODO()).Debugf("Updated: %v", spis)
return spis
}
func (n *network) maxMTU() int {
mtu := 1500
if n.mtu != 0 {
mtu = n.mtu
}
mtu -= vxlanEncap
if n.secure {
// In case of encryption account for the
// esp packet expansion and padding
mtu -= pktExpansion
mtu -= (mtu % 4)
}
return mtu
}
func clearEncryptionStates() {
nlh := ns.NlHandle()
spList, err := nlh.XfrmPolicyList(netlink.FAMILY_ALL)
if err != nil {
log.G(context.TODO()).Warnf("Failed to retrieve SP list for cleanup: %v", err)
}
saList, err := nlh.XfrmStateList(netlink.FAMILY_ALL)
if err != nil {
log.G(context.TODO()).Warnf("Failed to retrieve SA list for cleanup: %v", err)
}
for _, sp := range spList {
sp := sp
if sp.Mark != nil && sp.Mark.Value == spMark.Value {
if err := nlh.XfrmPolicyDel(&sp); err != nil {
log.G(context.TODO()).Warnf("Failed to delete stale SP %s: %v", sp, err)
continue
}
log.G(context.TODO()).Debugf("Removed stale SP: %s", sp)
}
}
for _, sa := range saList {
sa := sa
if sa.Reqid == mark {
if err := nlh.XfrmStateDel(&sa); err != nil {
log.G(context.TODO()).Warnf("Failed to delete stale SA %s: %v", sa, err)
continue
}
log.G(context.TODO()).Debugf("Removed stale SA: %s", sa)
}
}
}