From de93159d086a5287c5bd9c61e61b547afbb8c967 Mon Sep 17 00:00:00 2001 From: Trekkie Coder Date: Tue, 3 Dec 2024 00:25:57 +0900 Subject: [PATCH 1/2] gh-14 Added L2 advertisements for VIPv6 --- pkg/loxinet/rules.go | 42 ++++++++---- pkg/utils/net.go | 151 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 175 insertions(+), 18 deletions(-) diff --git a/pkg/loxinet/rules.go b/pkg/loxinet/rules.go index 13daf79e4..185ef4172 100644 --- a/pkg/loxinet/rules.go +++ b/pkg/loxinet/rules.go @@ -3134,7 +3134,7 @@ func (R *RuleH) AdvRuleVIPIfL2(IP net.IP, eIP net.IP, inst string) error { inst = cmn.CIDefault } - if IP.String() == "0.0.0.0" { + if IP.String() == "0.0.0.0" || IP.String() == "::" { return nil } @@ -3143,10 +3143,14 @@ func (R *RuleH) AdvRuleVIPIfL2(IP net.IP, eIP net.IP, inst string) error { dev := fmt.Sprintf("llb-rule-%s", IP.String()) ret, _ := R.zone.L3.IfaFindAddr(dev, IP) if ret == 0 { - R.zone.L3.IfaDelete(dev, IP.String()+"/32") + R.zone.L3.IfaDelete(dev, utils.IPHostCIDRString(IP)) } ev, _, iface := R.zone.L3.IfaSelectAny(IP, false) if ev == 0 { + ifname := "lo" + if tk.IsNetIPv6(IP.String()) { + ifname = iface + } if !utils.IsIPHostAddr(IP.String()) { if mh.cloudHook != nil { err := mh.cloudHook.CloudUpdatePrivateIP(IP, eIP, true) @@ -3156,17 +3160,17 @@ func (R *RuleH) AdvRuleVIPIfL2(IP net.IP, eIP net.IP, inst string) error { } } - if loxinlp.AddAddrNoHook(IP.String()+"/32", "lo") != 0 { - tk.LogIt(tk.LogError, "lb-rule vip %s:%s add failed\n", IP.String(), "lo") + if loxinlp.AddAddrNoHook(utils.IPHostCIDRString(IP), ifname) != 0 { + tk.LogIt(tk.LogError, "lb-rule vip %s:%s add failed\n", IP.String(), ifname) } else { - tk.LogIt(tk.LogInfo, "lb-rule vip %s:%s added\n", IP.String(), "lo") + tk.LogIt(tk.LogInfo, "lb-rule vip %s:%s added\n", IP.String(), ifname) } loxinlp.DelNeighNoHook(IP.String(), "") } ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) defer cancel() rCh := make(chan int) - go utils.GratArpReqWithCtx(ctx, rCh, IP, iface) + go utils.NetAdvertiseVIPReqWithCtx(ctx, rCh, IP, iface) select { case <-rCh: break @@ -3177,10 +3181,17 @@ func (R *RuleH) AdvRuleVIPIfL2(IP net.IP, eIP net.IP, inst string) error { } else if ciState != "NOT_DEFINED" { if utils.IsIPHostAddr(IP.String()) { - if loxinlp.DelAddrNoHook(IP.String()+"/32", "lo") != 0 { - tk.LogIt(tk.LogError, "lb-rule vip %s:%s delete failed\n", IP.String(), "lo") + ifname := "lo" + ev, _, iface := R.zone.L3.IfaSelectAny(IP, false) + if ev == 0 { + if tk.IsNetIPv6(IP.String()) { + ifname = iface + } + } + if loxinlp.DelAddrNoHook(utils.IPHostCIDRString(IP), ifname) != 0 { + tk.LogIt(tk.LogError, "lb-rule vip %s:%s delete failed\n", IP.String(), ifname) } else { - tk.LogIt(tk.LogInfo, "lb-rule vip %s:%s deleted\n", IP.String(), "lo") + tk.LogIt(tk.LogInfo, "lb-rule vip %s:%s deleted\n", IP.String(), ifname) } } } else { @@ -3188,7 +3199,7 @@ func (R *RuleH) AdvRuleVIPIfL2(IP net.IP, eIP net.IP, inst string) error { dev := fmt.Sprintf("llb-rule-%s", IP.String()) ret, _ := R.zone.L3.IfaFindAddr(dev, IP) if ret != 0 { - _, err := R.zone.L3.IfaAdd(dev, IP.String()+"/32") + _, err := R.zone.L3.IfaAdd(dev, utils.IPHostCIDRString(IP)) if err != nil { fmt.Printf("Failed to add IP : %s:%s\n", dev, err) } @@ -3264,7 +3275,14 @@ func (R *RuleH) DeleteRuleVIP(VIP net.IP) { xVIP = vipEnt.pVIP } if utils.IsIPHostAddr(xVIP.String()) { - loxinlp.DelAddrNoHook(xVIP.String()+"/32", "lo") + ifname := "lo" + ev, _, iface := R.zone.L3.IfaSelectAny(xVIP, false) + if ev == 0 { + if tk.IsNetIPv6(xVIP.String()) { + ifname = iface + } + } + loxinlp.DelAddrNoHook(utils.IPHostCIDRString(xVIP), ifname) if mh.cloudHook != nil { err := mh.cloudHook.CloudUpdatePrivateIP(xVIP, VIP, false) if err != nil { @@ -3275,7 +3293,7 @@ func (R *RuleH) DeleteRuleVIP(VIP net.IP) { dev := fmt.Sprintf("llb-rule-%s", xVIP.String()) ret, _ := mh.zr.L3.IfaFindAddr(dev, xVIP) if ret == 0 { - mh.zr.L3.IfaDelete(dev, xVIP.String()+"/32") + mh.zr.L3.IfaDelete(dev, utils.IPHostCIDRString(xVIP)) } delete(R.vipMap, VIP.String()) } diff --git a/pkg/utils/net.go b/pkg/utils/net.go index 4dbbd4edf..a99a99042 100644 --- a/pkg/utils/net.go +++ b/pkg/utils/net.go @@ -23,7 +23,6 @@ import ( "crypto/x509" "encoding/binary" "errors" - "golang.org/x/sys/unix" "io" "net" "net/http" @@ -32,10 +31,133 @@ import ( "time" "unsafe" + "golang.org/x/sys/unix" + tk "github.com/loxilb-io/loxilib" nlp "github.com/vishvananda/netlink" ) +const ( + ICMPv6NeighborAdvertisement = 136 // ICMPv6 Type for Neighbor Advertisement +) + +type icmpv6Header struct { + Type uint8 + Code uint8 + Checksum uint16 + Reserved uint32 +} + +func NetAdvertiseVI64Req(targetIP net.IP, ifName string) (int, error) { + if targetIP == nil || ifName == "" || ifName == "lo" { + return -1, errors.New("invalid parameters") + } + + ifi, err := net.InterfaceByName(ifName) + if err != nil { + return -1, errors.New("intfv6-err") + } + + srcIP := net.IPv6linklocalallnodes + dstIP := net.IPv6linklocalallnodes + + // Create an ICMPv6 header for Neighbor Advertisement + icmpHeader := &icmpv6Header{ + Type: ICMPv6NeighborAdvertisement, + Code: 0, + Checksum: 0, // To be calculated later + Reserved: 0, + } + + payload := newNeighborAdvertisementPayload(targetIP, ifi.HardwareAddr) + icmpData := append(icmpHeader.Marshal(), payload...) + + // Calculate checksum + icmpHeader.Checksum = calculateChecksum(icmpData, srcIP, dstIP) + icmpData = append(icmpHeader.Marshal(), payload...) + + fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_RAW, syscall.IPPROTO_ICMPV6) + if err != nil { + return -1, err + } + defer syscall.Close(fd) + + if err := syscall.BindToDevice(fd, ifName); err != nil { + return -1, errors.New("bindv6-err") + } + + dstAddr := &syscall.SockaddrInet6{ + Port: 0, + Addr: [16]byte{}, + } + copy(dstAddr.Addr[:], dstIP) + + err = syscall.Sendto(fd, icmpData, 0, dstAddr) + if err != nil { + return -1, err + } + + return 0, nil +} + +func (h *icmpv6Header) Marshal() []byte { + buf := make([]byte, 8) + buf[0] = h.Type + buf[1] = h.Code + binary.BigEndian.PutUint16(buf[2:], h.Checksum) + binary.BigEndian.PutUint32(buf[4:], 0) + return buf +} + +func newNeighborAdvertisementPayload(targetIP net.IP, macAddr net.HardwareAddr) []byte { + buf := make([]byte, 32) + + targetIP = targetIP.To16() + if targetIP == nil { + panic("Invalid IPv6 address") + } + + // Target Address + copy(buf[0:16], targetIP) + + // Option: Target Link-Layer Address + buf[16] = 2 // Option Type + buf[17] = 1 // Length in units of 8 bytes + copy(buf[18:], macAddr) + + return buf +} + +func calculateChecksum(data []byte, srcIP, dstIP net.IP) uint16 { + pseudoHeader := createPseudoHeader(srcIP, dstIP, len(data)) + fullPacket := append(pseudoHeader, data...) + return checksum(fullPacket) +} + +func createPseudoHeader(srcIP, dstIP net.IP, length int) []byte { + buf := bytes.Buffer{} + buf.Write(srcIP.To16()) + buf.Write(dstIP.To16()) + buf.WriteByte(0) + buf.WriteByte(58) // Next Header (ICMPv6) + binary.Write(&buf, binary.BigEndian, uint32(length)) + return buf.Bytes() +} + +func checksum(data []byte) uint16 { + var sum uint32 + for i := 0; i < len(data)-1; i += 2 { + sum += uint32(binary.BigEndian.Uint16(data[i:])) + } + if len(data)%2 != 0 { + sum += uint32(data[len(data)-1]) << 8 + } + for sum > 0xFFFF { + sum = (sum >> 16) + (sum & 0xFFFF) + } + return ^uint16(sum) +} + // HTTPSProber - Do a https probe for given url // returns true/false depending on whether probing was successful func HTTPSProber(urls string, cert tls.Certificate, certPool *x509.CertPool, resp string) bool { @@ -116,8 +238,8 @@ func IsIPHostNetAddr(ip net.IP) bool { return false } -// GratArpReq - sends a gratuitous arp reply given the DIP, SIP and interface name -func GratArpReq(AdvIP net.IP, ifName string) (int, error) { +// NetAdvertiseVIP4Req - sends a gratuitous arp reply given the DIP, SIP and interface name +func NetAdvertiseVIP4Req(AdvIP net.IP, ifName string) (int, error) { bcAddr := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_DGRAM, int(tk.Htons(syscall.ETH_P_ARP))) if err != nil { @@ -177,14 +299,19 @@ func GratArpReq(AdvIP net.IP, ifName string) (int, error) { return 0, nil } -// GratArpReq - sends a gratuitous arp reply given the DIP, SIP and interface name -func GratArpReqWithCtx(ctx context.Context, rCh chan<- int, AdvIP net.IP, ifName string) (int, error) { +// NetAdvertiseVIPReqWithCtx - sends a gratuitous arp reply given the DIP interface name +func NetAdvertiseVIPReqWithCtx(ctx context.Context, rCh chan<- int, AdvIP net.IP, ifName string) (int, error) { for { select { case <-ctx.Done(): return -1, ctx.Err() default: - ret, _ := GratArpReq(AdvIP, ifName) + var ret int + if tk.IsNetIPv4(AdvIP.String()) { + ret, _ = NetAdvertiseVIP4Req(AdvIP, ifName) + } else { + ret, _ = NetAdvertiseVI64Req(AdvIP, ifName) + } rCh <- ret return 0, nil } @@ -356,3 +483,15 @@ func MkTunFsIfNotExist() error { } return nil } + +// sIPHostNetAddr - Check if provided address is a local subnet +func IPHostCIDRString(ip net.IP) string { + if ip == nil { + return "0.0.0.0/0" + } + if tk.IsNetIPv4(ip.String()) { + return ip.String() + "/32" + } else { + return ip.String() + "/128" + } +} From bbb0272fe21cc00e3f75ab71b230a9b9e0201e69 Mon Sep 17 00:00:00 2001 From: Trekkie Coder Date: Tue, 3 Dec 2024 21:46:14 +0900 Subject: [PATCH 2/2] gh-14 Support for unsolicited advertisements for VIPs --- pkg/utils/net.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/pkg/utils/net.go b/pkg/utils/net.go index a99a99042..447bad96b 100644 --- a/pkg/utils/net.go +++ b/pkg/utils/net.go @@ -53,6 +53,10 @@ func NetAdvertiseVI64Req(targetIP net.IP, ifName string) (int, error) { return -1, errors.New("invalid parameters") } + if !tk.IsNetIPv6(targetIP.String()) { + return -1, errors.New("invalid parameters") + } + ifi, err := net.InterfaceByName(ifName) if err != nil { return -1, errors.New("intfv6-err") @@ -92,7 +96,14 @@ func NetAdvertiseVI64Req(targetIP net.IP, ifName string) (int, error) { } copy(dstAddr.Addr[:], dstIP) - err = syscall.Sendto(fd, icmpData, 0, dstAddr) + cmsgBuf := make([]byte, syscall.CmsgSpace(4)) // 4 bytes for hop limit + cmsg := (*syscall.Cmsghdr)(unsafe.Pointer(&cmsgBuf[0])) + cmsg.Level = syscall.IPPROTO_IPV6 + cmsg.Type = syscall.IPV6_HOPLIMIT + cmsg.SetLen(syscall.CmsgLen(4)) + *(*int32)(unsafe.Pointer(&cmsgBuf[syscall.CmsgLen(0)])) = 255 + + _, err = syscall.SendmsgN(fd, icmpData, cmsgBuf, dstAddr, 0) if err != nil { return -1, err } @@ -105,12 +116,12 @@ func (h *icmpv6Header) Marshal() []byte { buf[0] = h.Type buf[1] = h.Code binary.BigEndian.PutUint16(buf[2:], h.Checksum) - binary.BigEndian.PutUint32(buf[4:], 0) + binary.BigEndian.PutUint32(buf[4:], 0x20000000) return buf } func newNeighborAdvertisementPayload(targetIP net.IP, macAddr net.HardwareAddr) []byte { - buf := make([]byte, 32) + buf := make([]byte, 24) targetIP = targetIP.To16() if targetIP == nil { @@ -128,6 +139,18 @@ func newNeighborAdvertisementPayload(targetIP net.IP, macAddr net.HardwareAddr) return buf } +// ConvertToSolicitedNodeMulticast converts an IPv6 address to its solicited-node multicast address +func ConvertToSolicitedNodeMulticast(ip net.IP) net.IP { + + last24 := ip[len(ip)-3:] // Last 3 bytes of the IPv6 address + + solicitedNode := net.IPv6unspecified + copy(solicitedNode[:], net.ParseIP("ff02::1:ff00:0")[:]) + copy(solicitedNode[13:], last24) + + return solicitedNode +} + func calculateChecksum(data []byte, srcIP, dstIP net.IP) uint16 { pseudoHeader := createPseudoHeader(srcIP, dstIP, len(data)) fullPacket := append(pseudoHeader, data...)