From aef26459d314f25938fe10a078f136b082e8a9e9 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Tue, 7 Oct 2025 23:12:20 +0000 Subject: [PATCH] use sync trace to track mutexes to make deadlock analysis easier --- .vscode/settings.json | 6 +++++ internal/network/types/type.go | 28 +++++++++++++++++++++++ internal/timesync/ntp.go | 1 + internal/timesync/timesync.go | 3 +++ pkg/nmlite/dhcp.go | 35 ++++++++++++++++------------- pkg/nmlite/hostname.go | 3 ++- pkg/nmlite/interface.go | 17 ++++++-------- pkg/nmlite/jetdhcpc/client.go | 4 +++- pkg/nmlite/jetdhcpc/lease.go | 6 +++-- pkg/nmlite/link/netlink.go | 3 ++- pkg/nmlite/manager.go | 3 ++- pkg/nmlite/udhcpc/parser.go | 4 +++- pkg/nmlite/udhcpc/udhcpc.go | 4 +++- ui/src/components/DhcpLeaseCard.tsx | 7 ++++++ ui/src/hooks/stores.ts | 1 + 15 files changed, 92 insertions(+), 33 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a86e6b63..b0e6df67 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,11 @@ "cva", "cx" ], + "gopls": { + "build.buildFlags": [ + "-tags", + "synctrace" + ] + }, "git.ignoreLimitWarning": true } \ No newline at end of file diff --git a/internal/network/types/type.go b/internal/network/types/type.go index 3544fb35..83f64521 100644 --- a/internal/network/types/type.go +++ b/internal/network/types/type.go @@ -45,6 +45,8 @@ type IPv6StaticConfig struct { // NetworkConfig represents the complete network configuration for an interface type NetworkConfig struct { + DHCPClient null.String `json:"dhcp_client,omitempty" one_of:"jetdhcpc,udhcpc" default:"jetdhcpc"` + Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"` HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"` Domain null.String `json:"domain,omitempty" validate_type:"hostname"` @@ -145,6 +147,7 @@ type DHCPLease struct { LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease InterfaceName string `json:"interface_name,omitempty"` // The name of the interface + DHCPClient string `json:"dhcp_client,omitempty"` // The DHCP client that obtained the lease } // InterfaceState represents the current state of a network interface @@ -173,6 +176,31 @@ type NetworkConfigInterface interface { IPv6Addresses() []IPAddress } +// IsIPv6 returns true if the DHCP lease is for an IPv6 address func (d *DHCPLease) IsIPv6() bool { return d.IPAddress.To4() == nil } + +// IPMask returns the IP mask for the DHCP lease +func (d *DHCPLease) IPMask() net.IPMask { + if d.IsIPv6() { + // TODO: not implemented + return nil + } + + mask := net.ParseIP(d.Netmask.String()) + return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) +} + +// IPNet returns the IP net for the DHCP lease +func (d *DHCPLease) IPNet() *net.IPNet { + if d.IsIPv6() { + // TODO: not implemented + return nil + } + + return &net.IPNet{ + IP: d.IPAddress, + Mask: d.IPMask(), + } +} diff --git a/internal/timesync/ntp.go b/internal/timesync/ntp.go index e5a18865..7ff410b0 100644 --- a/internal/timesync/ntp.go +++ b/internal/timesync/ntp.go @@ -62,6 +62,7 @@ func (t *TimeSync) filterNTPServers(ntpServers []string) ([]string, error) { if ip == nil { continue } + if hasIPv4 && ip.To4() != nil { filteredServers = append(filteredServers, server) } diff --git a/internal/timesync/timesync.go b/internal/timesync/timesync.go index d93401e0..f9f4d4f4 100644 --- a/internal/timesync/timesync.go +++ b/internal/timesync/timesync.go @@ -169,6 +169,9 @@ func (t *TimeSync) doTimeSync() { } func (t *TimeSync) Sync() error { + t.syncLock.Lock() + defer t.syncLock.Unlock() + var ( now *time.Time offset *time.Duration diff --git a/pkg/nmlite/dhcp.go b/pkg/nmlite/dhcp.go index 74e8df28..000633f0 100644 --- a/pkg/nmlite/dhcp.go +++ b/pkg/nmlite/dhcp.go @@ -14,25 +14,23 @@ import ( // DHCPClient wraps the dhclient package for use in the network manager type DHCPClient struct { - ctx context.Context - ifaceName string - logger *zerolog.Logger - client types.DHCPClient - link netlink.Link + ctx context.Context + ifaceName string + logger *zerolog.Logger + client types.DHCPClient + clientType string + link netlink.Link // Configuration ipv4Enabled bool ipv6Enabled bool - // State management - // stateManager *DHCPStateManager - // Callbacks onLeaseChange func(lease *types.DHCPLease) } // NewDHCPClient creates a new DHCP client -func NewDHCPClient(ctx context.Context, ifaceName string, logger *zerolog.Logger) (*DHCPClient, error) { +func NewDHCPClient(ctx context.Context, ifaceName string, logger *zerolog.Logger, clientType string) (*DHCPClient, error) { if ifaceName == "" { return nil, fmt.Errorf("interface name cannot be empty") } @@ -42,9 +40,10 @@ func NewDHCPClient(ctx context.Context, ifaceName string, logger *zerolog.Logger } return &DHCPClient{ - ctx: ctx, - ifaceName: ifaceName, - logger: logger, + ctx: ctx, + ifaceName: ifaceName, + logger: logger, + clientType: clientType, }, nil } @@ -70,10 +69,13 @@ func (dc *DHCPClient) SetOnLeaseChange(callback func(lease *types.DHCPLease)) { } func (dc *DHCPClient) initClient() (types.DHCPClient, error) { - if false { + switch dc.clientType { + case "jetdhcpc": return dc.initJetDHCPC() - } else { + case "udhcpc": return dc.initUDHCPC() + default: + return nil, fmt.Errorf("invalid client type: %s", dc.clientType) } } @@ -204,8 +206,11 @@ func (dc *DHCPClient) handleLeaseChange(lease *types.DHCPLease, isIPv6 bool) { Str("ip", lease.IPAddress.String()). Msg("DHCP lease changed") + // copy the lease to avoid race conditions + leaseCopy := *lease + // Notify callback if dc.onLeaseChange != nil { - dc.onLeaseChange(lease) + dc.onLeaseChange(&leaseCopy) } } diff --git a/pkg/nmlite/hostname.go b/pkg/nmlite/hostname.go index 48090cac..6bf06991 100644 --- a/pkg/nmlite/hostname.go +++ b/pkg/nmlite/hostname.go @@ -6,7 +6,8 @@ import ( "os" "os/exec" "strings" - "sync" + + "github.com/jetkvm/kvm/internal/sync" "github.com/rs/zerolog" "golang.org/x/net/idna" diff --git a/pkg/nmlite/interface.go b/pkg/nmlite/interface.go index 324a0bf0..1bae6599 100644 --- a/pkg/nmlite/interface.go +++ b/pkg/nmlite/interface.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "net" - "sync" + "time" + "github.com/jetkvm/kvm/internal/sync" + "github.com/jetkvm/kvm/internal/confparser" "github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/network/types" @@ -78,7 +80,7 @@ func NewInterfaceManager(ctx context.Context, ifaceName string, config *types.Ne } // create the dhcp client - im.dhcpClient, err = NewDHCPClient(ctx, ifaceName, &scopedLogger) + im.dhcpClient, err = NewDHCPClient(ctx, ifaceName, &scopedLogger, config.DHCPClient.String) if err != nil { return nil, fmt.Errorf("failed to create DHCP client: %w", err) } @@ -562,7 +564,6 @@ func (im *InterfaceManager) monitorInterfaceState() { defer im.wg.Done() im.logger.Debug().Msg("monitoring interface state") - // TODO: use netlink subscription instead of polling ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() @@ -574,7 +575,6 @@ func (im *InterfaceManager) monitorInterfaceState() { case <-im.stopCh: return case <-ticker.C: - im.logger.Debug().Msg("checking interface state") if err := im.updateInterfaceState(); err != nil { im.logger.Error().Err(err).Msg("failed to update interface state") } @@ -727,12 +727,9 @@ func (im *InterfaceManager) applyDHCPLease(lease *types.DHCPLease) error { // convertDHCPLeaseToIPv4Config converts a DHCP lease to IPv4Config func (im *InterfaceManager) convertDHCPLeaseToIPv4Config(lease *types.DHCPLease) *types.IPAddress { - mask := lease.Netmask - - // Create IPNet from IP and netmask - ipNet := &net.IPNet{ - IP: lease.IPAddress, - Mask: net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]), + ipNet := lease.IPNet() + if ipNet == nil { + return nil } // Create IPv4Address diff --git a/pkg/nmlite/jetdhcpc/client.go b/pkg/nmlite/jetdhcpc/client.go index 0f076ebb..2b9c1099 100644 --- a/pkg/nmlite/jetdhcpc/client.go +++ b/pkg/nmlite/jetdhcpc/client.go @@ -6,9 +6,11 @@ import ( "fmt" "net" "slices" - "sync" + "time" + "github.com/jetkvm/kvm/internal/sync" + "github.com/go-co-op/gocron/v2" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv6" diff --git a/pkg/nmlite/jetdhcpc/lease.go b/pkg/nmlite/jetdhcpc/lease.go index dbd4fef9..0c06f8fa 100644 --- a/pkg/nmlite/jetdhcpc/lease.go +++ b/pkg/nmlite/jetdhcpc/lease.go @@ -33,7 +33,9 @@ type Lease struct { // ToDHCPLease converts a lease to a DHCP lease. func (l *Lease) ToDHCPLease() *types.DHCPLease { - return &l.DHCPLease + lease := &l.DHCPLease + lease.DHCPClient = "jetdhcpc" + return lease } // fromNclient4Lease creates a lease from a nclient4.Lease. @@ -45,9 +47,9 @@ func fromNclient4Lease(l *nclient4.Lease, iface string) *Lease { // only the fields that we need are set lease.Routers = l.ACK.Router() lease.IPAddress = l.ACK.YourIPAddr + lease.Netmask = net.IP(l.ACK.SubnetMask()) lease.Broadcast = l.ACK.BroadcastAddress() - // lease.MTU = int(resp.Options.Get(dhcpv4.OptionInterfaceMTU)) lease.NTPServers = l.ACK.NTPServers() diff --git a/pkg/nmlite/link/netlink.go b/pkg/nmlite/link/netlink.go index c8c4e53a..8e342f6d 100644 --- a/pkg/nmlite/link/netlink.go +++ b/pkg/nmlite/link/netlink.go @@ -10,9 +10,10 @@ import ( "path" "strconv" "strings" - "sync" "time" + "github.com/jetkvm/kvm/internal/sync" + "github.com/jetkvm/kvm/internal/network/types" "github.com/rs/zerolog" "github.com/vishvananda/netlink" diff --git a/pkg/nmlite/manager.go b/pkg/nmlite/manager.go index ba7d3a8f..b7ef6a64 100644 --- a/pkg/nmlite/manager.go +++ b/pkg/nmlite/manager.go @@ -6,7 +6,8 @@ package nmlite import ( "context" "fmt" - "sync" + + "github.com/jetkvm/kvm/internal/sync" "github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/network/types" diff --git a/pkg/nmlite/udhcpc/parser.go b/pkg/nmlite/udhcpc/parser.go index e76dceca..115cdc17 100644 --- a/pkg/nmlite/udhcpc/parser.go +++ b/pkg/nmlite/udhcpc/parser.go @@ -40,7 +40,9 @@ func (l *Lease) ToJSON() string { // ToDHCPLease converts a lease to a DHCP lease. func (l *Lease) ToDHCPLease() *types.DHCPLease { - return &l.DHCPLease + lease := &l.DHCPLease + lease.DHCPClient = "udhcpc" + return lease } // SetLeaseExpiry sets the lease expiry time. diff --git a/pkg/nmlite/udhcpc/udhcpc.go b/pkg/nmlite/udhcpc/udhcpc.go index 12e1374d..19ce2cb5 100644 --- a/pkg/nmlite/udhcpc/udhcpc.go +++ b/pkg/nmlite/udhcpc/udhcpc.go @@ -6,9 +6,11 @@ import ( "os" "path/filepath" "reflect" - "sync" + "time" + "github.com/jetkvm/kvm/internal/sync" + "github.com/fsnotify/fsnotify" "github.com/jetkvm/kvm/internal/network/types" "github.com/rs/zerolog" diff --git a/ui/src/components/DhcpLeaseCard.tsx b/ui/src/components/DhcpLeaseCard.tsx index bf20f64d..f0826c7a 100644 --- a/ui/src/components/DhcpLeaseCard.tsx +++ b/ui/src/components/DhcpLeaseCard.tsx @@ -221,6 +221,13 @@ export default function DhcpLeaseCard({ )} + + {networkState?.dhcp_lease?.dhcp_client && ( +
+ DHCP Client + {networkState?.dhcp_lease?.dhcp_client} +
+ )} diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index e539f916..e9ef15d5 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -690,6 +690,7 @@ export interface DhcpLease { message?: string; tftp?: string; bootfile?: string; + dhcp_client?: string; } export interface IPv6Address {