Skip to content

Instantly share code, notes, and snippets.

@yin1999
Last active December 29, 2023 06:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yin1999/cfa50d5b9342380f003631a2ac683ea3 to your computer and use it in GitHub Desktop.
Save yin1999/cfa50d5b9342380f003631a2ac683ea3 to your computer and use it in GitHub Desktop.
package main
import (
"fmt"
"net"
"os"
"syscall"
"time"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
func main() {
if len(os.Args) != 2 {
printError("Usage: %s <interface>\n", os.Args[0])
}
ifaceName := os.Args[1]
// remove the old ipv6 addresses before monitoring
ifaceIdx, err := getInterfaceIndex(ifaceName)
if err != nil {
printError("%v\n", err)
}
if err = deleteOldIpv6Addr(ifaceIdx); err != nil {
printError("%v\n", err)
}
conn, err := rtnetlink.Dial(&netlink.Config{
Groups: unix.RTMGRP_IPV6_IFADDR,
})
if err != nil {
printError("%v\n", err)
}
defer conn.Close()
for {
// monitor ipv6 address changes
_, msgs, err := conn.Receive()
if err != nil {
printError("%v\n", err)
}
ifaceIdx, err = getInterfaceIndex(ifaceName)
if err != nil {
printError("%v\n", err)
}
modified := false
for _, msg := range msgs {
// check if the message is new address
if msg.Header.Type != syscall.RTM_NEWADDR {
continue
}
// decode the message
var addr rtnetlink.AddressMessage
if err := addr.UnmarshalBinary(msg.Data); err != nil {
printError("%v\n", err)
}
if checkAddress(addr, ifaceIdx, true) {
modified = true
break
}
}
if modified {
// delete the old addresses
retry := 1
for retry <= 3 {
if err = deleteOldIpv6Addr(ifaceIdx); err != nil {
fmt.Fprintf(os.Stderr, "retry %d: %v\n", retry, err)
retry++
// sleep 1 second before retry
// this is to avoid the case that the address is not yet ready
// when we try to delete it
time.Sleep(time.Second)
} else {
break
}
}
if err != nil {
printError("%v\n", err)
}
}
}
}
func printError(format string, a ...any) {
fmt.Fprintf(os.Stderr, format, a...)
os.Exit(1)
}
func getInterfaceIndex(ifaceName string) (uint32, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return 0, fmt.Errorf("failed to find the interface %q: %w", ifaceName, err)
}
return uint32(iface.Index), nil
}
type ipv6Addr struct {
ip net.IP
prefixLength uint8
validLifetime uint32
}
// checkAddress check if the address is for the interface we are looking for
// and if the address is global unicast and tentative
// tentativeCheck: check if the address is tentative
//
// - true: the address should be tentative, otherwise return false
//
// - false: there is no check for tentative
//
// tentative means the address is newly added and not yet verified
// https://man7.org/linux/man-pages/man8/ip-address.8.html
func checkAddress(addr rtnetlink.AddressMessage, ifaceIdx uint32, tentativeCheck bool) bool {
// check if the message is for the interface we are looking for
// and if the address is ipv6
if addr.Index != ifaceIdx || addr.Family != unix.AF_INET6 {
return false
}
// check if the address is global unicast
if addr.Scope != syscall.RT_SCOPE_UNIVERSE {
return false
}
// check if the address is tentative
if tentativeCheck && addr.Flags&syscall.IFA_F_TENTATIVE != syscall.IFA_F_TENTATIVE {
return false
}
// check if the address is global
if addr.Attributes != nil && (!addr.Attributes.Address.IsGlobalUnicast() || addr.Attributes.Address.IsPrivate()) {
return false
}
return true
}
// deleteOldIpv6Addr delete the old ipv6 addresses except the one with the longest lifetime
func deleteOldIpv6Addr(ifaceIdx uint32) error {
// create a rtnetlink connection
conn, err := rtnetlink.Dial(nil)
if err != nil {
return err
}
defer conn.Close()
fmt.Fprintf(os.Stderr, "Deleting old ipv6 addresses...\n")
// query the ipv6 addresses
msgs, err := conn.Address.List()
if err != nil {
return err
}
var longestLifetime uint32
var prefixAddrIdx int
var addrs []ipv6Addr
// parse the messages
for _, msg := range msgs {
if checkAddress(msg, ifaceIdx, false) {
attr := msg.Attributes
if attr == nil {
continue
}
// check if the address is global unicast
if !attr.Address.IsGlobalUnicast() || attr.Address.IsPrivate() {
continue
}
if attr.CacheInfo.Valid > longestLifetime {
longestLifetime = attr.CacheInfo.Valid
prefixAddrIdx = len(addrs)
}
addrs = append(addrs, ipv6Addr{
ip: attr.Address,
prefixLength: msg.PrefixLength,
validLifetime: attr.CacheInfo.Valid,
})
}
}
if len(addrs) == 0 {
fmt.Fprintf(os.Stderr, "No proper ipv6 address found\n")
return nil
}
ipPrefix := net.IPNet{
IP: addrs[prefixAddrIdx].ip,
Mask: net.CIDRMask(int(addrs[prefixAddrIdx].prefixLength), 128),
}
// delete the old addresses
count := 0
for _, addr := range addrs {
if addr.validLifetime == longestLifetime {
continue
}
// check if the address is in the same prefix
if ipPrefix.Contains(addr.ip) {
continue
}
// delete the address
err = conn.Address.Delete(&rtnetlink.AddressMessage{
Family: unix.AF_INET6,
PrefixLength: addr.prefixLength,
Index: ifaceIdx,
Attributes: &rtnetlink.AddressAttributes{
Address: addr.ip,
},
})
if err != nil {
return fmt.Errorf("failed to delete old ipv6 addr %s: %w", addr.ip, err)
}
fmt.Fprintf(os.Stderr, "Deleted old ipv6 addr %s\n", addr.ip)
count++
}
fmt.Fprintf(os.Stderr, "Deleted %d old ipv6 addresses\n", count)
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment