From 6ff4f37a364e0f2f09c2e06778302a418f3341fa Mon Sep 17 00:00:00 2001 From: Siyuan Date: Fri, 10 Oct 2025 10:12:41 +0000 Subject: [PATCH] show flags on ipv6 network card --- .vscode/settings.json | 3 +- internal/network/types/rpc.go | 27 +++++++++++- internal/network/types/type.go | 1 + pkg/nmlite/interface_state.go | 1 + pkg/nmlite/jetdhcpc/client.go | 60 ++++++++++++++++----------- pkg/nmlite/jetdhcpc/dhcp4.go | 17 ++++++-- pkg/nmlite/utils.go | 6 +++ ui/src/components/Ipv6NetworkCard.tsx | 21 +++++++++- ui/src/hooks/stores.ts | 9 ++++ 9 files changed, 114 insertions(+), 31 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b0e6df67..ba3550bf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "synctrace" ] }, - "git.ignoreLimitWarning": true + "git.ignoreLimitWarning": true, + "cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo" } \ No newline at end of file diff --git a/internal/network/types/rpc.go b/internal/network/types/rpc.go index 748d778e..431c5100 100644 --- a/internal/network/types/rpc.go +++ b/internal/network/types/rpc.go @@ -1,20 +1,36 @@ package types -import "time" +import ( + "time" + "golang.org/x/sys/unix" +) + +// RpcIPv6Address is the RPC representation of an IPv6 address type RpcIPv6Address struct { Address string `json:"address"` Prefix string `json:"prefix"` ValidLifetime *time.Time `json:"valid_lifetime"` PreferredLifetime *time.Time `json:"preferred_lifetime"` Scope int `json:"scope"` + Flags int `json:"flags"` + FlagSecondary bool `json:"flag_secondary"` + FlagPermanent bool `json:"flag_permanent"` + FlagTemporary bool `json:"flag_temporary"` + FlagStablePrivacy bool `json:"flag_stable_privacy"` + FlagDeprecated bool `json:"flag_deprecated"` + FlagOptimistic bool `json:"flag_optimistic"` + FlagDADFailed bool `json:"flag_dad_failed"` + FlagTentative bool `json:"flag_tentative"` } +// RpcInterfaceState is the RPC representation of an interface state type RpcInterfaceState struct { InterfaceState IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses"` } +// ToRpcInterfaceState converts an InterfaceState to a RpcInterfaceState func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState { addrs := make([]RpcIPv6Address, len(s.IPv6Addresses)) for i, addr := range s.IPv6Addresses { @@ -24,6 +40,15 @@ func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState { ValidLifetime: addr.ValidLifetime, PreferredLifetime: addr.PreferredLifetime, Scope: addr.Scope, + Flags: addr.Flags, + FlagSecondary: addr.Flags&unix.IFA_F_SECONDARY != 0, + FlagPermanent: addr.Flags&unix.IFA_F_PERMANENT != 0, + FlagTemporary: addr.Flags&unix.IFA_F_TEMPORARY != 0, + FlagStablePrivacy: addr.Flags&unix.IFA_F_STABLE_PRIVACY != 0, + FlagDeprecated: addr.Flags&unix.IFA_F_DEPRECATED != 0, + FlagOptimistic: addr.Flags&unix.IFA_F_OPTIMISTIC != 0, + FlagDADFailed: addr.Flags&unix.IFA_F_DADFAILED != 0, + FlagTentative: addr.Flags&unix.IFA_F_TENTATIVE != 0, } } return &RpcInterfaceState{ diff --git a/internal/network/types/type.go b/internal/network/types/type.go index 2bee48ac..b770fcd5 100644 --- a/internal/network/types/type.go +++ b/internal/network/types/type.go @@ -65,6 +65,7 @@ type IPv6Address struct { Prefix net.IPNet `json:"prefix"` ValidLifetime *time.Time `json:"valid_lifetime"` PreferredLifetime *time.Time `json:"preferred_lifetime"` + Flags int `json:"flags"` Scope int `json:"scope"` } diff --git a/pkg/nmlite/interface_state.go b/pkg/nmlite/interface_state.go index f29c8f22..8a60784b 100644 --- a/pkg/nmlite/interface_state.go +++ b/pkg/nmlite/interface_state.go @@ -110,6 +110,7 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool, Address: addr.IP, Prefix: *addr.IPNet, Scope: addr.Scope, + Flags: addr.Flags, ValidLifetime: lifetimeToTime(addr.ValidLft), PreferredLifetime: lifetimeToTime(addr.PreferedLft), }) diff --git a/pkg/nmlite/jetdhcpc/client.go b/pkg/nmlite/jetdhcpc/client.go index b3460621..bcab787d 100644 --- a/pkg/nmlite/jetdhcpc/client.go +++ b/pkg/nmlite/jetdhcpc/client.go @@ -107,8 +107,9 @@ type Client struct { } var ( - defaultTimerDuration = 1 * time.Second - defaultLinkUpTimeout = 30 * time.Second + defaultTimerDuration = 1 * time.Second + defaultLinkUpTimeout = 30 * time.Second + maxRenewalAttemptDuration = 2 * time.Hour ) // NewClient creates a new DHCP client for the given interface. @@ -155,10 +156,21 @@ func resetTimer(t *time.Timer, l *zerolog.Logger) { t.Reset(defaultTimerDuration) } +func getRenewalTime(lease *Lease) time.Duration { + if lease.RenewalTime <= 0 || lease.LeaseTime > maxRenewalAttemptDuration/2 { + return maxRenewalAttemptDuration + } + + return lease.RenewalTime +} + func (c *Client) requestLoop(t *time.Timer, family int, ifname string) { + l := c.l.With().Str("interface", ifname).Int("family", family).Logger() for range t.C { + l.Info().Msg("requesting lease") + if _, err := c.ensureInterfaceUp(ifname); err != nil { - c.l.Error().Err(err).Msg("failed to ensure interface up") + l.Error().Err(err).Msg("failed to ensure interface up") resetTimer(t, c.l) continue } @@ -174,12 +186,22 @@ func (c *Client) requestLoop(t *time.Timer, family int, ifname string) { lease, err = c.requestLease6(ifname) } if err != nil { - c.l.Error().Err(err).Msg("failed to request lease") + l.Error().Err(err).Msg("failed to request lease") resetTimer(t, c.l) continue } c.handleLeaseChange(lease) + + nextRenewal := getRenewalTime(lease) + + l.Info(). + Dur("nextRenewal", nextRenewal). + Dur("leaseTime", lease.LeaseTime). + Dur("rebindingTime", lease.RebindingTime). + Msg("sleeping until next renewal") + + t.Reset(nextRenewal) } } @@ -262,25 +284,9 @@ func (c *Client) handleLeaseChange(lease *Lease) { } } -func (c *Client) doRenewLoop() { - timer := time.NewTimer(time.Duration(c.currentLease4.RenewalTime) * time.Second) - defer timer.Stop() - - for range timer.C { - c.renew() - } -} - -func (c *Client) renew() { - // for lease := range c.sendRequests(c.cfg.IPv4, c.cfg.IPv6) { - // if lease, ok := lease.(*Lease); ok { - // c.handleLeaseChange(lease) - // } - // } -} - func (c *Client) Renew() error { - go c.renew() + c.timer4.Reset(defaultTimerDuration) + c.timer6.Reset(defaultTimerDuration) return nil } @@ -304,9 +310,11 @@ func (c *Client) SetIPv4(ipv4 bool) { c.lease4Mu.Lock() c.currentLease4 = nil c.lease4Mu.Unlock() + + c.timer4.Stop() } - c.timer4.Stop() + c.timer4.Reset(defaultTimerDuration) } func (c *Client) SetIPv6(ipv6 bool) { @@ -323,10 +331,12 @@ func (c *Client) SetIPv6(ipv6 bool) { if !ipv6 { c.lease6Mu.Lock() c.currentLease6 = nil - c.lease4Mu.Unlock() + c.lease6Mu.Unlock() + + c.timer6.Stop() } - c.timer6.Stop() + c.timer6.Reset(defaultTimerDuration) } func (c *Client) Start() error { diff --git a/pkg/nmlite/jetdhcpc/dhcp4.go b/pkg/nmlite/jetdhcpc/dhcp4.go index dda0350e..4eb0ee14 100644 --- a/pkg/nmlite/jetdhcpc/dhcp4.go +++ b/pkg/nmlite/jetdhcpc/dhcp4.go @@ -47,9 +47,20 @@ func (c *Client) requestLease4(ifname string) (*Lease, error) { } l.Info().Msg("attempting to get DHCPv4 lease") - lease, err := client.Request(c.ctx, reqmods...) - if err != nil { - return nil, err + var ( + lease *nclient4.Lease + reqErr error + ) + if c.currentLease4 != nil { + l.Info().Msg("current lease is not nil, renewing") + lease, reqErr = client.Renew(c.ctx, c.currentLease4.p4, reqmods...) + } else { + l.Info().Msg("current lease is nil, requesting new lease") + lease, reqErr = client.Request(c.ctx, reqmods...) + } + + if reqErr != nil { + return nil, reqErr } if lease == nil || lease.ACK == nil { diff --git a/pkg/nmlite/utils.go b/pkg/nmlite/utils.go index 15d5624c..5a6eb0b8 100644 --- a/pkg/nmlite/utils.go +++ b/pkg/nmlite/utils.go @@ -58,9 +58,15 @@ func compareIPv6AddressSlices(a, b []types.IPv6Address) bool { if a[i].Address.String() != b[i].Address.String() { return false } + if a[i].Prefix.String() != b[i].Prefix.String() { return false } + + if a[i].Flags != b[i].Flags { + return false + } + // we don't compare the lifetimes because they are not always same if a[i].Scope != b[i].Scope { return false diff --git a/ui/src/components/Ipv6NetworkCard.tsx b/ui/src/components/Ipv6NetworkCard.tsx index 5ed27ca1..d34da739 100644 --- a/ui/src/components/Ipv6NetworkCard.tsx +++ b/ui/src/components/Ipv6NetworkCard.tsx @@ -1,8 +1,21 @@ +import { cx } from "@/cva.config"; import { NetworkState } from "../hooks/stores"; import { LifeTimeLabel } from "../routes/devices.$id.settings.network"; import { GridCard } from "./Card"; +export function FlagLabel({ flag, className }: { flag: string, className?: string }) { + const classes = cx( + "ml-2 rounded-sm bg-red-500 px-2 py-1 text-[10px] font-medium leading-none text-white dark:border", + "bg-red-500 text-white dark:border-red-700 dark:bg-red-800 dark:text-red-50", + className, + ); + + return + {flag} + +} + export default function Ipv6NetworkCard({ networkState, }: { @@ -49,7 +62,13 @@ export default function Ipv6NetworkCard({ Address - {addr.address} + + {addr.address} + + {addr.flag_deprecated ? : null} + {addr.flag_dad_failed ? : null} + + {addr.valid_lifetime && ( diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index ca8ff46f..1b8dae1b 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -699,6 +699,15 @@ export interface IPv6Address { valid_lifetime: string; preferred_lifetime: string; scope: string; + flags: number; + flag_secondary?: boolean; + flag_permanent?: boolean; + flag_temporary?: boolean; + flag_stable_privacy?: boolean; + flag_deprecated?: boolean; + flag_optimistic?: boolean; + flag_dad_failed?: boolean; + flag_tentative?: boolean; } export interface NetworkState {