kvm/network.go

348 lines
8.1 KiB
Go

package kvm
import (
"fmt"
"net"
"os"
"sync"
"time"
"github.com/Masterminds/semver/v3"
"github.com/jetkvm/kvm/internal/udhcpc"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
)
var (
networkState *NetworkInterfaceState
)
type DhcpTargetState int
const (
DhcpTargetStateDoNothing DhcpTargetState = iota
DhcpTargetStateStart
DhcpTargetStateStop
DhcpTargetStateRenew
DhcpTargetStateRelease
)
type NetworkInterfaceState struct {
interfaceName string
interfaceUp bool
ipv4Addr *net.IP
ipv6Addr *net.IP
macAddr *net.HardwareAddr
l *zerolog.Logger
stateLock sync.Mutex
dhcpClient *udhcpc.DHCPClient
onStateChange func(state *NetworkInterfaceState)
onInitialCheck func(state *NetworkInterfaceState)
checked bool
}
func (s *NetworkInterfaceState) IsUp() bool {
return s.interfaceUp
}
func (s *NetworkInterfaceState) HasIPAssigned() bool {
return s.ipv4Addr != nil || s.ipv6Addr != nil
}
func (s *NetworkInterfaceState) IsOnline() bool {
return s.IsUp() && s.HasIPAssigned()
}
func (s *NetworkInterfaceState) IPv4() *net.IP {
return s.ipv4Addr
}
func (s *NetworkInterfaceState) IPv4String() string {
if s.ipv4Addr == nil {
return "..."
}
return s.ipv4Addr.String()
}
func (s *NetworkInterfaceState) IPv6() *net.IP {
return s.ipv6Addr
}
func (s *NetworkInterfaceState) IPv6String() string {
if s.ipv6Addr == nil {
return "..."
}
return s.ipv6Addr.String()
}
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
return s.macAddr
}
func (s *NetworkInterfaceState) MACString() string {
if s.macAddr == nil {
return ""
}
return s.macAddr.String()
}
const (
// TODO: add support for multiple interfaces
NetIfName = "eth0"
)
func NewNetworkInterfaceState(ifname string) *NetworkInterfaceState {
logger := networkLogger.With().Str("interface", ifname).Logger()
s := &NetworkInterfaceState{
interfaceName: ifname,
stateLock: sync.Mutex{},
l: &logger,
onStateChange: func(state *NetworkInterfaceState) {
go func() {
waitCtrlClientConnected()
requestDisplayUpdate()
}()
},
onInitialCheck: func(state *NetworkInterfaceState) {
go func() {
waitCtrlClientConnected()
requestDisplayUpdate()
}()
},
}
// use a pid file for udhcpc if the system version is 0.2.4 or higher
dhcpPidFile := ""
systemVersionLocal, _, _ := GetLocalVersion()
if systemVersionLocal != nil &&
systemVersionLocal.Compare(semver.MustParse("0.2.4")) >= 0 {
dhcpPidFile = fmt.Sprintf("/run/udhcpc.%s.pid", ifname)
}
// create the dhcp client
dhcpClient := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{
InterfaceName: ifname,
PidFile: dhcpPidFile,
Logger: &logger,
OnLeaseChange: func(lease *udhcpc.Lease) {
s.update()
},
})
s.dhcpClient = dhcpClient
return s
}
func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
s.stateLock.Lock()
defer s.stateLock.Unlock()
dhcpTargetState := DhcpTargetStateDoNothing
iface, err := netlink.LinkByName(s.interfaceName)
if err != nil {
s.l.Error().Err(err).Msg("failed to get interface")
return dhcpTargetState, err
}
// detect if the interface status changed
var changed bool
attrs := iface.Attrs()
state := attrs.OperState
newInterfaceUp := state == netlink.OperUp
// check if the interface is coming up
interfaceGoingUp := s.interfaceUp == false && newInterfaceUp == true
interfaceGoingDown := s.interfaceUp == true && newInterfaceUp == false
if s.interfaceUp != newInterfaceUp {
s.interfaceUp = newInterfaceUp
changed = true
}
if changed {
if interfaceGoingUp {
s.l.Info().Msg("interface state transitioned to up")
dhcpTargetState = DhcpTargetStateRenew
} else if interfaceGoingDown {
s.l.Info().Msg("interface state transitioned to down")
}
}
// set the mac address
s.macAddr = &attrs.HardwareAddr
// get the ip addresses
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
if err != nil {
s.l.Error().Err(err).Msg("failed to get ip addresses")
return dhcpTargetState, err
}
var (
ipv4Addresses = make([]net.IP, 0)
ipv6Addresses = make([]net.IP, 0)
)
for _, addr := range addrs {
if addr.IP.To4() != nil {
scopedLogger := s.l.With().Str("ipv4", addr.IP.String()).Logger()
if interfaceGoingDown {
// remove all IPv4 addresses from the interface.
scopedLogger.Info().Msg("state transitioned to down, removing IPv4 address")
err := netlink.AddrDel(iface, &addr)
if err != nil {
scopedLogger.Warn().Err(err).Msg("failed to delete address")
}
// notify the DHCP client to release the lease
dhcpTargetState = DhcpTargetStateRelease
continue
}
ipv4Addresses = append(ipv4Addresses, addr.IP)
} else if addr.IP.To16() != nil {
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
// check if it's a link local address
if !addr.IP.IsGlobalUnicast() {
scopedLogger.Trace().Msg("not a global unicast address, skipping")
continue
}
if interfaceGoingDown {
scopedLogger.Info().Msg("state transitioned to down, removing IPv6 address")
err := netlink.AddrDel(iface, &addr)
if err != nil {
scopedLogger.Warn().Err(err).Msg("failed to delete address")
}
continue
}
ipv6Addresses = append(ipv6Addresses, addr.IP)
}
}
if len(ipv4Addresses) > 0 {
// compare the addresses to see if there's a change
if s.ipv4Addr == nil || s.ipv4Addr.String() != ipv4Addresses[0].String() {
scopedLogger := s.l.With().Str("ipv4", ipv4Addresses[0].String()).Logger()
if s.ipv4Addr != nil {
scopedLogger.Info().
Str("old_ipv4", s.ipv4Addr.String()).
Msg("IPv4 address changed")
changed = true
} else {
scopedLogger.Info().Msg("IPv4 address found")
}
s.ipv4Addr = &ipv4Addresses[0]
changed = true
}
}
if len(ipv6Addresses) > 0 {
// compare the addresses to see if there's a change
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].String() {
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].String()).Logger()
if s.ipv6Addr != nil {
scopedLogger.Info().
Str("old_ipv6", s.ipv6Addr.String()).
Msg("IPv6 address changed")
} else {
scopedLogger.Info().Msg("IPv6 address found")
}
s.ipv6Addr = &ipv6Addresses[0]
changed = true
}
}
// if it's the initial check, we'll set changed to false
initialCheck := !s.checked
if initialCheck {
s.checked = true
changed = false
}
if initialCheck {
s.onInitialCheck(s)
} else if changed {
s.onStateChange(s)
}
return dhcpTargetState, nil
}
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
dhcpTargetState, err := s.update()
if err != nil {
return ErrorfL(s.l, "failed to update network state", err)
}
switch dhcpTargetState {
case DhcpTargetStateRenew:
s.l.Info().Msg("renewing DHCP lease")
s.dhcpClient.Renew()
case DhcpTargetStateRelease:
s.l.Info().Msg("releasing DHCP lease")
s.dhcpClient.Release()
case DhcpTargetStateStart:
s.l.Warn().Msg("dhcpTargetStateStart not implemented")
case DhcpTargetStateStop:
s.l.Warn().Msg("dhcpTargetStateStop not implemented")
}
return nil
}
func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) {
if update.Link.Attrs().Name == s.interfaceName {
s.l.Info().Interface("update", update).Msg("interface link update received")
s.CheckAndUpdateDhcp()
}
}
func initNetwork() {
ensureConfigLoaded()
updates := make(chan netlink.LinkUpdate)
done := make(chan struct{})
if err := netlink.LinkSubscribe(updates, done); err != nil {
networkLogger.Warn().Err(err).Msg("failed to subscribe to link updates")
return
}
// TODO: support multiple interfaces
networkState = NewNetworkInterfaceState(NetIfName)
go networkState.dhcpClient.Run()
if err := networkState.CheckAndUpdateDhcp(); err != nil {
os.Exit(1)
}
go func() {
waitCtrlClientConnected()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case update := <-updates:
networkState.HandleLinkUpdate(update)
case <-ticker.C:
_ = networkState.CheckAndUpdateDhcp()
case <-done:
return
}
}
}()
err := startMDNS()
if err != nil {
logger.Warn().Err(err).Msg("failed to run mDNS")
}
}