kvm/internal/network/static.go

368 lines
8.3 KiB
Go

package network
import (
"fmt"
"net"
"os"
"path"
"strconv"
"strings"
"sync"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
)
const defaultInterface = "eth0"
const (
AF_INET = nl.FAMILY_V4
AF_INET6 = nl.FAMILY_V6
sysctlBase = "/proc/sys"
sysctlFileMode = 0640
)
type NetworkInterfaceConfig struct {
config *NetworkConfig
l *zerolog.Logger
ifaceName string
iface *netlink.Link
lock sync.Mutex
ipv4Lock sync.Mutex
ipv6Lock sync.Mutex
}
var (
ipv4DefaultRoute = net.IPNet{
IP: net.IPv4zero,
Mask: net.CIDRMask(0, 0),
}
ipv6DefaultRoute = net.IPNet{
IP: net.IPv6zero,
Mask: net.CIDRMask(0, 0),
}
)
// NewNetworkInterfaceConfig ...
func NewNetworkInterfaceConfig(ifaceName string, config *NetworkConfig, logger *zerolog.Logger) (*NetworkInterfaceConfig, error) {
if ifaceName == "" {
ifaceName = defaultInterface
}
link, err := netlink.LinkByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("failed to get link by name: %w", err)
}
if config == nil {
return nil, fmt.Errorf("config is nil")
}
if logger == nil {
logger = logging.GetSubsystemLogger("network")
}
scopedLogger := logger.With().Str("iface", ifaceName).Logger()
return &NetworkInterfaceConfig{
config: config,
l: &scopedLogger,
ifaceName: ifaceName,
iface: &link,
lock: sync.Mutex{},
ipv4Lock: sync.Mutex{},
ipv6Lock: sync.Mutex{},
}, nil
}
func (c *NetworkInterfaceConfig) Apply(s *NetworkInterfaceState) error {
if err := c.applyIPv4(s); err != nil {
return err
}
if err := c.applyIPv6(s); err != nil {
return err
}
return nil
}
func (c *NetworkInterfaceConfig) applyIPv4(s *NetworkInterfaceState) error {
switch c.config.IPv4Mode.String {
case "static":
return c.applyIPv4Static(s)
case "dhcp":
s.dhcpClient.SetIPv4(true)
return nil
case "disabled":
s.dhcpClient.SetIPv4(false)
return nil
default:
return fmt.Errorf("invalid IPv4 mode: %s", c.config.IPv4Mode.String)
}
}
func (c *NetworkInterfaceConfig) applyIPv6(s *NetworkInterfaceState) error {
switch c.config.IPv6Mode.String {
case "static":
return c.applyIPv6Static(s)
case "dhcpv6":
return fmt.Errorf("not implemented")
case "slaac":
return c.applyIPv6Slaac()
case "slaac_and_dhcpv6":
return fmt.Errorf("not implemented")
case "link_local":
return c.applyIPv6LinkLocalOnly()
case "disabled":
return c.disableIPv6()
default:
return fmt.Errorf("invalid IPv6 mode: %s", c.config.IPv6Mode.String)
}
}
func checkIfAddressIsSetOrReturnCurrent(iface *netlink.Link, address net.IP, family int) (bool, []*netlink.Addr) {
addr, err := netlink.AddrList(*iface, family)
if err != nil {
return false, nil
}
hit := false
linkAddrs := make([]*netlink.Addr, 0)
for _, a := range addr {
if a.IP.Equal(address) {
hit = true
continue
}
// we don't want to delete link-local addresses
if family == AF_INET6 && a.IP.IsLinkLocalUnicast() {
continue
}
linkAddrs = append(linkAddrs, &a)
}
return hit, linkAddrs
}
func reconcileLinkAddrs(iface *netlink.Link, family int, expectedAddr *net.IPNet, logger *zerolog.Logger) error {
// TODO: we need to check if the netmask is the same
hit, currentLinkAddrs := checkIfAddressIsSetOrReturnCurrent(iface, expectedAddr.IP, family)
if !hit {
logger.Info().Interface("ip", expectedAddr).Msg("adding address")
if err := netlink.AddrAdd(*iface, &netlink.Addr{
IPNet: expectedAddr,
}); err != nil {
logger.Info().Interface("ip", expectedAddr).Msg("failed to set address")
return fmt.Errorf("failed to add address: %w", err)
}
}
for _, addr := range currentLinkAddrs {
logger.Info().Interface("ip", addr.IP).Msg("deleting address")
if err := netlink.AddrDel(*iface, addr); err != nil {
return fmt.Errorf("failed to delete address: %w", err)
}
}
return nil
}
func checkIfDefaultRouteIsSet(gateway net.IP, family int) bool {
defaultRoute := ipv4DefaultRoute
if family == AF_INET6 {
defaultRoute = ipv6DefaultRoute
}
routes, err := netlink.RouteListFiltered(family, &netlink.Route{Dst: &defaultRoute}, netlink.RT_FILTER_DST)
if err != nil {
return false
}
for _, r := range routes {
if r.Dst.IP.Equal(defaultRoute.IP) && r.Gw.Equal(gateway) {
return true
}
}
return false
}
func ensureInterfaceIsUp(iface *netlink.Link) error {
if (*iface).Attrs().OperState == netlink.OperUp {
return nil
}
if err := netlink.LinkSetUp(*iface); err != nil {
return fmt.Errorf("failed to set interface up: %w", err)
}
return nil
}
func (c *NetworkInterfaceConfig) applyIPv4Static(s *NetworkInterfaceState) error {
c.ipv4Lock.Lock()
defer c.ipv4Lock.Unlock()
config, err := parseAndValidateStaticIPv4Config(c.config.IPv4Static)
if err != nil {
return err
}
if c.iface == nil {
return fmt.Errorf("interface handle is nil")
}
// disable DHCPv4
s.dhcpClient.SetIPv4(false)
if err := ensureInterfaceIsUp(c.iface); err != nil {
return err
}
if err := reconcileLinkAddrs(c.iface, AF_INET, &config.network, c.l); err != nil {
return err
}
if !checkIfDefaultRouteIsSet(config.gateway, AF_INET) {
c.l.Info().Str("iface", c.ifaceName).Interface("gateway", config.gateway).Msg("adding default route")
// TODO: support point-to-point
if err := netlink.RouteReplace(&netlink.Route{
Dst: &ipv4DefaultRoute,
Gw: config.gateway,
LinkIndex: (*c.iface).Attrs().Index,
}); err != nil {
return fmt.Errorf("failed to add default route: %w", err)
}
}
return ensureInterfaceIsUp(c.iface)
}
func (c *NetworkInterfaceConfig) setSysctlValues(values map[string]int) error {
for name, value := range values {
name = fmt.Sprintf(name, c.ifaceName)
name = strings.ReplaceAll(name, ".", "/")
if err := os.WriteFile(path.Join(sysctlBase, name), []byte(strconv.Itoa(value)), sysctlFileMode); err != nil {
return fmt.Errorf("failed to set sysctl %s=%d: %w", name, value, err)
}
}
return nil
}
func (c *NetworkInterfaceConfig) applyIPv4Dhcp() error {
c.ipv4Lock.Lock()
defer c.ipv4Lock.Unlock()
return nil
}
func (c *NetworkInterfaceConfig) applyIPv4Disabled() error {
c.ipv4Lock.Lock()
defer c.ipv4Lock.Unlock()
addr, err := netlink.AddrList(*c.iface, AF_INET)
if err != nil {
return fmt.Errorf("failed to get address list: %w", err)
}
for _, a := range addr {
netlink.AddrDel(*c.iface, &a)
}
return nil
}
func (c *NetworkInterfaceConfig) applyIPv6Slaac() error {
c.ipv6Lock.Lock()
defer c.ipv6Lock.Unlock()
if err := c.setSysctlValues(map[string]int{
"net.ipv6.conf.%s.disable_ipv6": 0, // enable IPv6
"net.ipv6.conf.%s.accept_ra": 2, // accept even if forwarding is disabled
}); err != nil {
return err
}
return nil
}
func (c *NetworkInterfaceConfig) applyIPv6LinkLocalOnly() error {
c.ipv6Lock.Lock()
defer c.ipv6Lock.Unlock()
if err := c.setSysctlValues(map[string]int{
"net.ipv6.conf.%s.disable_ipv6": 0, // enable IPv6
"net.ipv6.conf.%s.accept_ra": 0, // disable RA
}); err != nil {
return err
}
addr, err := netlink.AddrList(*c.iface, AF_INET6)
if err != nil {
return fmt.Errorf("failed to get address list: %w", err)
}
for _, a := range addr {
if !a.IP.IsLinkLocalUnicast() {
netlink.AddrDel(*c.iface, &a)
}
}
return ensureInterfaceIsUp(c.iface)
}
func (c *NetworkInterfaceConfig) applyIPv6Static(s *NetworkInterfaceState) error {
c.ipv6Lock.Lock()
defer c.ipv6Lock.Unlock()
config, err := parseAndValidateStaticIPv6Config(c.config.IPv6Static)
if err != nil {
return err
}
if c.iface == nil {
return fmt.Errorf("interface handle is nil")
}
if err := c.setSysctlValues(map[string]int{
"net.ipv6.conf.%s.disable_ipv6": 0, // enable IPv6
"net.ipv6.conf.%s.accept_ra": 2, // accept even if forwarding is disabled
}); err != nil {
return err
}
// disable DHCPv6
s.dhcpClient.SetIPv6(false)
if err := reconcileLinkAddrs(c.iface, AF_INET6, &config.prefix, c.l); err != nil {
return err
}
if !checkIfDefaultRouteIsSet(config.gateway, AF_INET6) {
if err := netlink.RouteReplace(&netlink.Route{
Dst: &ipv6DefaultRoute,
Gw: config.gateway,
LinkIndex: (*c.iface).Attrs().Index,
}); err != nil {
return fmt.Errorf("failed to add default route: %w", err)
}
}
return ensureInterfaceIsUp(c.iface)
}
func (c *NetworkInterfaceConfig) disableIPv6() error {
return c.setSysctlValues(map[string]int{
"net.ipv6.conf.%s.disable_ipv6": 1, // disable IPv6
})
}