mirror of
https://github.com/siderolabs/talos.git
synced 2025-11-01 00:41:40 +01:00
* correctly merge wireguard specs across multiple configuration layers (partially stolen from #3577) * fix erroneous wireguard reconfig when listen port in the config is zero * add tests for link merging (once again, partially stolen from #3577) * fix ugly bug with LinkSpec Type merging (I believe it's a major source of pain for you, Seán, in your PR). Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
537 lines
15 KiB
Go
537 lines
15 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package network
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"net"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/mdlayher/netlink"
|
|
"golang.org/x/sys/unix"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
"inet.af/netaddr"
|
|
|
|
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
|
|
)
|
|
|
|
// VLANSpec describes VLAN settings if Kind == "vlan".
|
|
type VLANSpec struct {
|
|
// VID is the vlan ID.
|
|
VID uint16 `yaml:"vlanID"`
|
|
|
|
// Protocol is the vlan protocol.
|
|
Protocol nethelpers.VLANProtocol `yaml:"vlanProtocol"`
|
|
}
|
|
|
|
// Encode the VLANSpec into netlink attributes.
|
|
func (vlan *VLANSpec) Encode() ([]byte, error) {
|
|
encoder := netlink.NewAttributeEncoder()
|
|
|
|
encoder.Uint16(unix.IFLA_VLAN_ID, vlan.VID)
|
|
|
|
buf := make([]byte, 2)
|
|
binary.BigEndian.PutUint16(buf, uint16(vlan.Protocol))
|
|
encoder.Bytes(unix.IFLA_VLAN_PROTOCOL, buf)
|
|
|
|
return encoder.Encode()
|
|
}
|
|
|
|
// Decode the VLANSpec from netlink attributes.
|
|
func (vlan *VLANSpec) Decode(data []byte) error {
|
|
decoder, err := netlink.NewAttributeDecoder(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for decoder.Next() {
|
|
switch decoder.Type() {
|
|
case unix.IFLA_VLAN_ID:
|
|
vlan.VID = decoder.Uint16()
|
|
case unix.IFLA_VLAN_PROTOCOL:
|
|
vlan.Protocol = nethelpers.VLANProtocol(binary.BigEndian.Uint16(decoder.Bytes()))
|
|
}
|
|
}
|
|
|
|
return decoder.Err()
|
|
}
|
|
|
|
// BondMasterSpec describes bond settings if Kind == "bond".
|
|
type BondMasterSpec struct {
|
|
Mode nethelpers.BondMode `yaml:"mode"`
|
|
HashPolicy nethelpers.BondXmitHashPolicy `yaml:"xmitHashPolicy"`
|
|
LACPRate nethelpers.LACPRate `yaml:"lacpRate"`
|
|
ARPValidate nethelpers.ARPValidate `yaml:"arpValidate"`
|
|
ARPAllTargets nethelpers.ARPAllTargets `yaml:"arpAllTargets"`
|
|
PrimaryIndex uint32 `yaml:"primary,omitempty"`
|
|
PrimaryReselect nethelpers.PrimaryReselect `yaml:"primaryReselect"`
|
|
FailOverMac nethelpers.FailOverMAC `yaml:"failOverMac"`
|
|
ADSelect nethelpers.ADSelect `yaml:"adSelect,omitempty"`
|
|
MIIMon uint32 `yaml:"miimon,omitempty"`
|
|
UpDelay uint32 `yaml:"updelay,omitempty"`
|
|
DownDelay uint32 `yaml:"downdelay,omitempty"`
|
|
ARPInterval uint32 `yaml:"arpInterval,omitempty"`
|
|
ResendIGMP uint32 `yaml:"resendIgmp,omitempty"`
|
|
MinLinks uint32 `yaml:"minLinks,omitempty"`
|
|
LPInterval uint32 `yaml:"lpInterval,omitempty"`
|
|
PacketsPerSlave uint32 `yaml:"packetsPerSlave,omitempty"`
|
|
NumPeerNotif uint8 `yaml:"numPeerNotif,omitempty"`
|
|
TLBDynamicLB uint8 `yaml:"tlbLogicalLb,omitempty"`
|
|
AllSlavesActive uint8 `yaml:"allSlavesActive,omitempty"`
|
|
UseCarrier bool `yaml:"useCarrier,omitempty"`
|
|
ADActorSysPrio uint16 `yaml:"adActorSysPrio,omitempty"`
|
|
ADUserPortKey uint16 `yaml:"adUserPortKey,omitempty"`
|
|
PeerNotifyDelay uint32 `yaml:"peerNotifyDelay,omitempty"`
|
|
}
|
|
|
|
// FillDefaults fills zero values with proper default values.
|
|
func (bond *BondMasterSpec) FillDefaults() {
|
|
if bond.ResendIGMP == 0 {
|
|
bond.ResendIGMP = 1
|
|
}
|
|
|
|
if bond.LPInterval == 0 {
|
|
bond.LPInterval = 1
|
|
}
|
|
|
|
if bond.PacketsPerSlave == 0 {
|
|
bond.PacketsPerSlave = 1
|
|
}
|
|
|
|
if bond.NumPeerNotif == 0 {
|
|
bond.NumPeerNotif = 1
|
|
}
|
|
|
|
if bond.Mode != nethelpers.BondModeALB && bond.Mode != nethelpers.BondModeTLB {
|
|
bond.TLBDynamicLB = 1
|
|
}
|
|
|
|
if bond.Mode == nethelpers.BondMode8023AD {
|
|
bond.ADActorSysPrio = 65535
|
|
}
|
|
}
|
|
|
|
// Encode the BondMasterSpec into netlink attributes.
|
|
//
|
|
//nolint:gocyclo
|
|
func (bond *BondMasterSpec) Encode() ([]byte, error) {
|
|
encoder := netlink.NewAttributeEncoder()
|
|
|
|
encoder.Uint8(unix.IFLA_BOND_MODE, uint8(bond.Mode))
|
|
encoder.Uint8(unix.IFLA_BOND_XMIT_HASH_POLICY, uint8(bond.HashPolicy))
|
|
|
|
if bond.Mode == nethelpers.BondMode8023AD {
|
|
encoder.Uint8(unix.IFLA_BOND_AD_LACP_RATE, uint8(bond.LACPRate))
|
|
}
|
|
|
|
if bond.Mode != nethelpers.BondMode8023AD && bond.Mode != nethelpers.BondModeALB && bond.Mode != nethelpers.BondModeTLB {
|
|
encoder.Uint32(unix.IFLA_BOND_ARP_VALIDATE, uint32(bond.ARPValidate))
|
|
}
|
|
|
|
encoder.Uint32(unix.IFLA_BOND_ARP_ALL_TARGETS, uint32(bond.ARPAllTargets))
|
|
|
|
if bond.Mode == nethelpers.BondModeActiveBackup || bond.Mode == nethelpers.BondModeALB || bond.Mode == nethelpers.BondModeTLB {
|
|
encoder.Uint32(unix.IFLA_BOND_PRIMARY, bond.PrimaryIndex)
|
|
}
|
|
|
|
encoder.Uint8(unix.IFLA_BOND_PRIMARY_RESELECT, uint8(bond.PrimaryReselect))
|
|
encoder.Uint8(unix.IFLA_BOND_FAIL_OVER_MAC, uint8(bond.FailOverMac))
|
|
encoder.Uint8(unix.IFLA_BOND_AD_SELECT, uint8(bond.ADSelect))
|
|
encoder.Uint32(unix.IFLA_BOND_MIIMON, bond.MIIMon)
|
|
|
|
if bond.MIIMon != 0 {
|
|
encoder.Uint32(unix.IFLA_BOND_UPDELAY, bond.UpDelay)
|
|
encoder.Uint32(unix.IFLA_BOND_DOWNDELAY, bond.DownDelay)
|
|
}
|
|
|
|
if bond.Mode != nethelpers.BondMode8023AD && bond.Mode != nethelpers.BondModeALB && bond.Mode != nethelpers.BondModeTLB {
|
|
encoder.Uint32(unix.IFLA_BOND_ARP_INTERVAL, bond.ARPInterval)
|
|
}
|
|
|
|
encoder.Uint32(unix.IFLA_BOND_RESEND_IGMP, bond.ResendIGMP)
|
|
encoder.Uint32(unix.IFLA_BOND_MIN_LINKS, bond.MinLinks)
|
|
encoder.Uint32(unix.IFLA_BOND_LP_INTERVAL, bond.LPInterval)
|
|
|
|
if bond.Mode == nethelpers.BondModeRoundrobin {
|
|
encoder.Uint32(unix.IFLA_BOND_PACKETS_PER_SLAVE, bond.PacketsPerSlave)
|
|
}
|
|
|
|
encoder.Uint8(unix.IFLA_BOND_NUM_PEER_NOTIF, bond.NumPeerNotif)
|
|
|
|
if bond.Mode == nethelpers.BondModeALB || bond.Mode == nethelpers.BondModeTLB {
|
|
encoder.Uint8(unix.IFLA_BOND_TLB_DYNAMIC_LB, bond.TLBDynamicLB)
|
|
}
|
|
|
|
encoder.Uint8(unix.IFLA_BOND_ALL_SLAVES_ACTIVE, bond.AllSlavesActive)
|
|
|
|
var useCarrier uint8
|
|
|
|
if bond.UseCarrier {
|
|
useCarrier = 1
|
|
}
|
|
|
|
encoder.Uint8(unix.IFLA_BOND_USE_CARRIER, useCarrier)
|
|
|
|
if bond.Mode == nethelpers.BondMode8023AD {
|
|
encoder.Uint16(unix.IFLA_BOND_AD_ACTOR_SYS_PRIO, bond.ADActorSysPrio)
|
|
encoder.Uint16(unix.IFLA_BOND_AD_USER_PORT_KEY, bond.ADUserPortKey)
|
|
}
|
|
|
|
if bond.MIIMon != 0 {
|
|
encoder.Uint32(unix.IFLA_BOND_PEER_NOTIF_DELAY, bond.PeerNotifyDelay)
|
|
}
|
|
|
|
return encoder.Encode()
|
|
}
|
|
|
|
// Decode the BondMasterSpec from netlink attributes.
|
|
//
|
|
//nolint:gocyclo,cyclop
|
|
func (bond *BondMasterSpec) Decode(data []byte) error {
|
|
decoder, err := netlink.NewAttributeDecoder(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for decoder.Next() {
|
|
switch decoder.Type() {
|
|
case unix.IFLA_BOND_MODE:
|
|
bond.Mode = nethelpers.BondMode(decoder.Uint8())
|
|
case unix.IFLA_BOND_XMIT_HASH_POLICY:
|
|
bond.HashPolicy = nethelpers.BondXmitHashPolicy(decoder.Uint8())
|
|
case unix.IFLA_BOND_AD_LACP_RATE:
|
|
bond.LACPRate = nethelpers.LACPRate(decoder.Uint8())
|
|
case unix.IFLA_BOND_ARP_VALIDATE:
|
|
bond.ARPValidate = nethelpers.ARPValidate(decoder.Uint32())
|
|
case unix.IFLA_BOND_ARP_ALL_TARGETS:
|
|
bond.ARPAllTargets = nethelpers.ARPAllTargets(decoder.Uint32())
|
|
case unix.IFLA_BOND_PRIMARY:
|
|
bond.PrimaryIndex = decoder.Uint32()
|
|
case unix.IFLA_BOND_PRIMARY_RESELECT:
|
|
bond.PrimaryReselect = nethelpers.PrimaryReselect(decoder.Uint8())
|
|
case unix.IFLA_BOND_FAIL_OVER_MAC:
|
|
bond.FailOverMac = nethelpers.FailOverMAC(decoder.Uint8())
|
|
case unix.IFLA_BOND_AD_SELECT:
|
|
bond.ADSelect = nethelpers.ADSelect(decoder.Uint8())
|
|
case unix.IFLA_BOND_MIIMON:
|
|
bond.MIIMon = decoder.Uint32()
|
|
case unix.IFLA_BOND_UPDELAY:
|
|
bond.UpDelay = decoder.Uint32()
|
|
case unix.IFLA_BOND_DOWNDELAY:
|
|
bond.DownDelay = decoder.Uint32()
|
|
case unix.IFLA_BOND_ARP_INTERVAL:
|
|
bond.ARPInterval = decoder.Uint32()
|
|
case unix.IFLA_BOND_RESEND_IGMP:
|
|
bond.ResendIGMP = decoder.Uint32()
|
|
case unix.IFLA_BOND_MIN_LINKS:
|
|
bond.MinLinks = decoder.Uint32()
|
|
case unix.IFLA_BOND_LP_INTERVAL:
|
|
bond.LPInterval = decoder.Uint32()
|
|
case unix.IFLA_BOND_PACKETS_PER_SLAVE:
|
|
bond.PacketsPerSlave = decoder.Uint32()
|
|
case unix.IFLA_BOND_NUM_PEER_NOTIF:
|
|
bond.NumPeerNotif = decoder.Uint8()
|
|
case unix.IFLA_BOND_TLB_DYNAMIC_LB:
|
|
bond.TLBDynamicLB = decoder.Uint8()
|
|
case unix.IFLA_BOND_ALL_SLAVES_ACTIVE:
|
|
bond.AllSlavesActive = decoder.Uint8()
|
|
case unix.IFLA_BOND_USE_CARRIER:
|
|
bond.UseCarrier = decoder.Uint8() == 1
|
|
case unix.IFLA_BOND_AD_ACTOR_SYS_PRIO:
|
|
bond.ADActorSysPrio = decoder.Uint16()
|
|
case unix.IFLA_BOND_AD_USER_PORT_KEY:
|
|
bond.ADUserPortKey = decoder.Uint16()
|
|
case unix.IFLA_BOND_PEER_NOTIF_DELAY:
|
|
bond.PeerNotifyDelay = decoder.Uint32()
|
|
}
|
|
}
|
|
|
|
return decoder.Err()
|
|
}
|
|
|
|
// WireguardSpec describes Wireguard settings if Kind == "wireguard".
|
|
type WireguardSpec struct {
|
|
PrivateKey string `yaml:"privateKey"`
|
|
ListenPort int `yaml:"listenPort"`
|
|
FirewallMark int `yaml:"firewallMark"`
|
|
Peers []WireguardPeer `yaml:"peers"`
|
|
}
|
|
|
|
// WireguardPeer describes a single peer.
|
|
type WireguardPeer struct {
|
|
PublicKey string `yaml:"publicKey"`
|
|
Endpoint string `yaml:"endpoint"`
|
|
PersistentKeepaliveInterval time.Duration `yaml:"persistenKeepaliveInterval"`
|
|
AllowedIPs []netaddr.IPPrefix `yaml:"allowedIPs"`
|
|
}
|
|
|
|
// Equal checks two WireguardPeer structs for equality.
|
|
func (peer *WireguardPeer) Equal(other *WireguardPeer) bool {
|
|
if peer.PublicKey != other.PublicKey {
|
|
return false
|
|
}
|
|
|
|
if peer.Endpoint != other.Endpoint {
|
|
return false
|
|
}
|
|
|
|
if peer.PersistentKeepaliveInterval != other.PersistentKeepaliveInterval {
|
|
return false
|
|
}
|
|
|
|
if len(peer.AllowedIPs) != len(other.AllowedIPs) {
|
|
return false
|
|
}
|
|
|
|
for i := range peer.AllowedIPs {
|
|
if peer.AllowedIPs[i].IP().Compare(other.AllowedIPs[i].IP()) != 0 {
|
|
return false
|
|
}
|
|
|
|
if peer.AllowedIPs[i].Bits() != other.AllowedIPs[i].Bits() {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsZero checks if the WireguardSpec is zero value.
|
|
func (spec *WireguardSpec) IsZero() bool {
|
|
return spec.PrivateKey == "" && spec.ListenPort == 0 && spec.FirewallMark == 0 && len(spec.Peers) == 0
|
|
}
|
|
|
|
// Equal checks two WireguardSpecs for equality.
|
|
//
|
|
// Both specs should be sorted before calling this method.
|
|
func (spec *WireguardSpec) Equal(other *WireguardSpec) bool {
|
|
if spec.PrivateKey != other.PrivateKey {
|
|
return false
|
|
}
|
|
|
|
// listenPort of '0' means use any available port, so we shouldn't consider this to be a "change"
|
|
if spec.ListenPort != other.ListenPort && other.ListenPort != 0 {
|
|
return false
|
|
}
|
|
|
|
if spec.FirewallMark != other.FirewallMark {
|
|
return false
|
|
}
|
|
|
|
if len(spec.Peers) != len(other.Peers) {
|
|
return false
|
|
}
|
|
|
|
for i := range spec.Peers {
|
|
if !spec.Peers[i].Equal(&other.Peers[i]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Sort the spec so that comparison is possible.
|
|
func (spec *WireguardSpec) Sort() {
|
|
sort.Slice(spec.Peers, func(i, j int) bool {
|
|
return spec.Peers[i].PublicKey < spec.Peers[j].PublicKey
|
|
})
|
|
|
|
for k := range spec.Peers {
|
|
k := k
|
|
|
|
sort.Slice(spec.Peers[k].AllowedIPs, func(i, j int) bool {
|
|
left := spec.Peers[k].AllowedIPs[i]
|
|
right := spec.Peers[k].AllowedIPs[j]
|
|
|
|
switch left.IP().Compare(right.IP()) {
|
|
case -1:
|
|
return true
|
|
case 0:
|
|
return left.Bits() < right.Bits()
|
|
default:
|
|
return false
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Encode converts WireguardSpec to wgctrl.Config "patch" to adjust the config to match the spec.
|
|
//
|
|
// Both specs should be sorted.
|
|
//
|
|
// Encode produces a "diff" as *wgtypes.Config which when applied transitions `existing` configuration into
|
|
// configuration `spec`.
|
|
//
|
|
//nolint:gocyclo,cyclop
|
|
func (spec *WireguardSpec) Encode(existing *WireguardSpec) (*wgtypes.Config, error) {
|
|
cfg := &wgtypes.Config{}
|
|
|
|
if existing.PrivateKey != spec.PrivateKey {
|
|
key, err := wgtypes.ParseKey(spec.PrivateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg.PrivateKey = &key
|
|
}
|
|
|
|
if existing.ListenPort != spec.ListenPort {
|
|
cfg.ListenPort = &spec.ListenPort
|
|
}
|
|
|
|
if existing.FirewallMark != spec.FirewallMark {
|
|
cfg.FirewallMark = &spec.FirewallMark
|
|
}
|
|
|
|
// perform a merge of two sorted list of peers producing diff
|
|
l, r := 0, 0
|
|
|
|
for l < len(existing.Peers) || r < len(spec.Peers) {
|
|
addPeer := func(peer *WireguardPeer) error {
|
|
pubKey, err := wgtypes.ParseKey(peer.PublicKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var endpoint *net.UDPAddr
|
|
|
|
if peer.Endpoint != "" {
|
|
endpoint, err = net.ResolveUDPAddr("", peer.Endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
allowedIPs := make([]net.IPNet, len(peer.AllowedIPs))
|
|
|
|
for i := range peer.AllowedIPs {
|
|
allowedIPs[i] = *peer.AllowedIPs[i].IPNet()
|
|
}
|
|
|
|
cfg.Peers = append(cfg.Peers, wgtypes.PeerConfig{
|
|
PublicKey: pubKey,
|
|
Endpoint: endpoint,
|
|
PersistentKeepaliveInterval: &peer.PersistentKeepaliveInterval,
|
|
AllowedIPs: allowedIPs,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
deletePeer := func(peer *WireguardPeer) error {
|
|
pubKey, err := wgtypes.ParseKey(peer.PublicKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg.Peers = append(cfg.Peers, wgtypes.PeerConfig{
|
|
PublicKey: pubKey,
|
|
Remove: true,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
var left, right *WireguardPeer
|
|
|
|
if l < len(existing.Peers) {
|
|
left = &existing.Peers[l]
|
|
}
|
|
|
|
if r < len(spec.Peers) {
|
|
right = &spec.Peers[r]
|
|
}
|
|
|
|
switch {
|
|
// peer from the "right" (new spec) is missing in "existing" (left), add it
|
|
case left == nil || (right != nil && left.PublicKey > right.PublicKey):
|
|
if err := addPeer(right); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r++
|
|
// peer from the "left" (existing) is missing in new spec (right), so it should be removed
|
|
case right == nil || (left != nil && left.PublicKey < right.PublicKey):
|
|
// deleting peers from the existing
|
|
if err := deletePeer(left); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
l++
|
|
// peer public keys are equal, so either they are identical or peer should be replaced
|
|
case left.PublicKey == right.PublicKey:
|
|
if !left.Equal(right) {
|
|
// replace peer
|
|
if err := addPeer(right); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
l++
|
|
r++
|
|
}
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// Decode spec from the device state.
|
|
func (spec *WireguardSpec) Decode(dev *wgtypes.Device) {
|
|
spec.PrivateKey = dev.PrivateKey.String()
|
|
spec.ListenPort = dev.ListenPort
|
|
spec.FirewallMark = dev.FirewallMark
|
|
|
|
spec.Peers = make([]WireguardPeer, len(dev.Peers))
|
|
|
|
for i := range spec.Peers {
|
|
spec.Peers[i].PublicKey = dev.Peers[i].PublicKey.String()
|
|
|
|
if dev.Peers[i].Endpoint != nil {
|
|
spec.Peers[i].Endpoint = dev.Peers[i].Endpoint.String()
|
|
}
|
|
|
|
spec.Peers[i].PersistentKeepaliveInterval = dev.Peers[i].PersistentKeepaliveInterval
|
|
spec.Peers[i].AllowedIPs = make([]netaddr.IPPrefix, len(dev.Peers[i].AllowedIPs))
|
|
|
|
for j := range dev.Peers[i].AllowedIPs {
|
|
spec.Peers[i].AllowedIPs[j], _ = netaddr.FromStdIPNet(&dev.Peers[i].AllowedIPs[j])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge with other Wireguard spec overwriting non-zero values.
|
|
func (spec *WireguardSpec) Merge(other WireguardSpec) {
|
|
if other.ListenPort != 0 {
|
|
spec.ListenPort = other.ListenPort
|
|
}
|
|
|
|
if other.FirewallMark != 0 {
|
|
spec.FirewallMark = other.FirewallMark
|
|
}
|
|
|
|
if other.PrivateKey != "" {
|
|
spec.PrivateKey = other.PrivateKey
|
|
}
|
|
|
|
// avoid adding same peer twice, no real peer information merging for now
|
|
for _, peer := range other.Peers {
|
|
exists := false
|
|
|
|
for _, p := range spec.Peers {
|
|
if p.PublicKey == peer.PublicKey {
|
|
exists = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !exists {
|
|
spec.Peers = append(spec.Peers, peer)
|
|
}
|
|
}
|
|
}
|