show flags on ipv6 network card

This commit is contained in:
Siyuan 2025-10-10 10:12:41 +00:00
parent a9cd36c5fb
commit 6ff4f37a36
9 changed files with 114 additions and 31 deletions

View File

@ -9,5 +9,6 @@
"synctrace" "synctrace"
] ]
}, },
"git.ignoreLimitWarning": true "git.ignoreLimitWarning": true,
"cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo"
} }

View File

@ -1,20 +1,36 @@
package types package types
import "time" import (
"time"
"golang.org/x/sys/unix"
)
// RpcIPv6Address is the RPC representation of an IPv6 address
type RpcIPv6Address struct { type RpcIPv6Address struct {
Address string `json:"address"` Address string `json:"address"`
Prefix string `json:"prefix"` Prefix string `json:"prefix"`
ValidLifetime *time.Time `json:"valid_lifetime"` ValidLifetime *time.Time `json:"valid_lifetime"`
PreferredLifetime *time.Time `json:"preferred_lifetime"` PreferredLifetime *time.Time `json:"preferred_lifetime"`
Scope int `json:"scope"` 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 { type RpcInterfaceState struct {
InterfaceState InterfaceState
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses"` IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses"`
} }
// ToRpcInterfaceState converts an InterfaceState to a RpcInterfaceState
func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState { func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState {
addrs := make([]RpcIPv6Address, len(s.IPv6Addresses)) addrs := make([]RpcIPv6Address, len(s.IPv6Addresses))
for i, addr := range s.IPv6Addresses { for i, addr := range s.IPv6Addresses {
@ -24,6 +40,15 @@ func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState {
ValidLifetime: addr.ValidLifetime, ValidLifetime: addr.ValidLifetime,
PreferredLifetime: addr.PreferredLifetime, PreferredLifetime: addr.PreferredLifetime,
Scope: addr.Scope, 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{ return &RpcInterfaceState{

View File

@ -65,6 +65,7 @@ type IPv6Address struct {
Prefix net.IPNet `json:"prefix"` Prefix net.IPNet `json:"prefix"`
ValidLifetime *time.Time `json:"valid_lifetime"` ValidLifetime *time.Time `json:"valid_lifetime"`
PreferredLifetime *time.Time `json:"preferred_lifetime"` PreferredLifetime *time.Time `json:"preferred_lifetime"`
Flags int `json:"flags"`
Scope int `json:"scope"` Scope int `json:"scope"`
} }

View File

@ -110,6 +110,7 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool,
Address: addr.IP, Address: addr.IP,
Prefix: *addr.IPNet, Prefix: *addr.IPNet,
Scope: addr.Scope, Scope: addr.Scope,
Flags: addr.Flags,
ValidLifetime: lifetimeToTime(addr.ValidLft), ValidLifetime: lifetimeToTime(addr.ValidLft),
PreferredLifetime: lifetimeToTime(addr.PreferedLft), PreferredLifetime: lifetimeToTime(addr.PreferedLft),
}) })

View File

@ -107,8 +107,9 @@ type Client struct {
} }
var ( var (
defaultTimerDuration = 1 * time.Second defaultTimerDuration = 1 * time.Second
defaultLinkUpTimeout = 30 * time.Second defaultLinkUpTimeout = 30 * time.Second
maxRenewalAttemptDuration = 2 * time.Hour
) )
// NewClient creates a new DHCP client for the given interface. // 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) 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) { 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 { for range t.C {
l.Info().Msg("requesting lease")
if _, err := c.ensureInterfaceUp(ifname); err != nil { 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) resetTimer(t, c.l)
continue continue
} }
@ -174,12 +186,22 @@ func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
lease, err = c.requestLease6(ifname) lease, err = c.requestLease6(ifname)
} }
if err != nil { 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) resetTimer(t, c.l)
continue continue
} }
c.handleLeaseChange(lease) 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 { func (c *Client) Renew() error {
go c.renew() c.timer4.Reset(defaultTimerDuration)
c.timer6.Reset(defaultTimerDuration)
return nil return nil
} }
@ -304,9 +310,11 @@ func (c *Client) SetIPv4(ipv4 bool) {
c.lease4Mu.Lock() c.lease4Mu.Lock()
c.currentLease4 = nil c.currentLease4 = nil
c.lease4Mu.Unlock() c.lease4Mu.Unlock()
c.timer4.Stop()
} }
c.timer4.Stop() c.timer4.Reset(defaultTimerDuration)
} }
func (c *Client) SetIPv6(ipv6 bool) { func (c *Client) SetIPv6(ipv6 bool) {
@ -323,10 +331,12 @@ func (c *Client) SetIPv6(ipv6 bool) {
if !ipv6 { if !ipv6 {
c.lease6Mu.Lock() c.lease6Mu.Lock()
c.currentLease6 = nil c.currentLease6 = nil
c.lease4Mu.Unlock() c.lease6Mu.Unlock()
c.timer6.Stop()
} }
c.timer6.Stop() c.timer6.Reset(defaultTimerDuration)
} }
func (c *Client) Start() error { func (c *Client) Start() error {

View File

@ -47,9 +47,20 @@ func (c *Client) requestLease4(ifname string) (*Lease, error) {
} }
l.Info().Msg("attempting to get DHCPv4 lease") l.Info().Msg("attempting to get DHCPv4 lease")
lease, err := client.Request(c.ctx, reqmods...) var (
if err != nil { lease *nclient4.Lease
return nil, err 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 { if lease == nil || lease.ACK == nil {

View File

@ -58,9 +58,15 @@ func compareIPv6AddressSlices(a, b []types.IPv6Address) bool {
if a[i].Address.String() != b[i].Address.String() { if a[i].Address.String() != b[i].Address.String() {
return false return false
} }
if a[i].Prefix.String() != b[i].Prefix.String() { if a[i].Prefix.String() != b[i].Prefix.String() {
return false return false
} }
if a[i].Flags != b[i].Flags {
return false
}
// we don't compare the lifetimes because they are not always same // we don't compare the lifetimes because they are not always same
if a[i].Scope != b[i].Scope { if a[i].Scope != b[i].Scope {
return false return false

View File

@ -1,8 +1,21 @@
import { cx } from "@/cva.config";
import { NetworkState } from "../hooks/stores"; import { NetworkState } from "../hooks/stores";
import { LifeTimeLabel } from "../routes/devices.$id.settings.network"; import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
import { GridCard } from "./Card"; 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 <span className={classes}>
{flag}
</span>
}
export default function Ipv6NetworkCard({ export default function Ipv6NetworkCard({
networkState, networkState,
}: { }: {
@ -49,7 +62,13 @@ export default function Ipv6NetworkCard({
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Address Address
</span> </span>
<span className="text-sm font-medium">{addr.address}</span> <span className="text-sm font-medium flex">
<span className="flex-1">{addr.address}</span>
<span className="text-sm font-medium flex gap-x-1">
{addr.flag_deprecated ? <FlagLabel flag="Deprecated" /> : null}
{addr.flag_dad_failed ? <FlagLabel flag="DAD Failed" /> : null}
</span>
</span>
</div> </div>
{addr.valid_lifetime && ( {addr.valid_lifetime && (

View File

@ -699,6 +699,15 @@ export interface IPv6Address {
valid_lifetime: string; valid_lifetime: string;
preferred_lifetime: string; preferred_lifetime: string;
scope: 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 { export interface NetworkState {