Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add supports for dual-stack #81

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions build/virt-prerunner/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ RUN go mod download
COPY cmd/ cmd/
COPY pkg/ pkg/
RUN --mount=type=cache,target=/root/.cache/go-build go build -a cmd/virt-prerunner/main.go
RUN --mount=type=cache,target=/root/.cache/go-build go build -o rad -a cmd/route-advertisement-daemon/main.go

FROM alpine
FROM alpine:3.17

RUN apk add --no-cache curl screen dnsmasq cdrkit iptables iproute2 qemu-virtiofsd dpkg util-linux s6-overlay nmap-ncat
RUN apk add --no-cache curl screen dnsmasq kea-dhcp6 cdrkit iptables ip6tables iproute2 qemu-virtiofsd dpkg util-linux s6-overlay nmap-ncat

RUN set -eux; \
mkdir /var/lib/cloud-hypervisor; \
Expand All @@ -40,6 +41,7 @@ COPY build/virt-prerunner/cloud-hypervisor-finish.sh /etc/s6-overlay/s6-rc.d/clo
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/cloud-hypervisor

COPY --from=builder /workspace/main /usr/bin/virt-prerunner
COPY --from=builder /workspace/rad /usr/bin/rad
COPY build/virt-prerunner/virt-prerunner-type /etc/s6-overlay/s6-rc.d/virt-prerunner/type
COPY build/virt-prerunner/virt-prerunner-up /etc/s6-overlay/s6-rc.d/virt-prerunner/up
COPY build/virt-prerunner/virt-prerunner-run.sh /etc/s6-overlay/scripts/virt-prerunner-run.sh
Expand All @@ -50,5 +52,7 @@ ENTRYPOINT ["/init"]

COPY build/virt-prerunner/iptables-wrapper /sbin/iptables-wrapper
RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-wrapper 100
COPY build/virt-prerunner/ip6tables-wrapper /sbin/ip6tables-wrapper
RUN update-alternatives --install /sbin/ip6tables ip6tables /sbin/ip6tables-wrapper 100

ADD build/virt-prerunner/virt-init-volume.sh /usr/bin/virt-init-volume
17 changes: 17 additions & 0 deletions build/virt-prerunner/ip6tables-wrapper
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh

set +e

ip6tables-legacy -nvL

if [ $? -eq 0 ]
then
mode=legacy
else
mode=nft
fi

update-alternatives --install /sbin/ip6tables ip6tables "/sbin/ip6tables-${mode}" 100
update-alternatives --set ip6tables "/sbin/ip6tables-${mode}" > /dev/null

exec "$0" "$@"
336 changes: 336 additions & 0 deletions cmd/route-advertisement-daemon/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
package main

import (
"flag"
"fmt"
"log"
"net"
"net/netip"
"os/exec"
"time"

"github.com/mdlayher/ndp"
"golang.org/x/net/ipv6"
)

var (
iface string
router string
isRemoteRoute bool
client string
clientHWAddr string
prefix string

linkLocalAllRouters = netip.MustParseAddr("ff02::2")
)

func main() {
log.SetPrefix("route-advertisement-daemon: ")
src, dst, cidr, err := validateVars()
if err != nil {
log.Fatalf("ERROR: validate vars: %s", err)
}
if src != nil && isRemoteRoute {
if _, err := executeCommand("ip", "-6", "neigh", "add", client, "lladdr", clientHWAddr, "dev", iface); err != nil {
log.Fatalf("ERROR: add neighbor entry for client: %s", err)
}

ipv6LLA := src
if !src.IsLinkLocalUnicast() {
mac, err := tryDiscoverNeighborMAC(iface, src, 5)
if err != nil {
log.Fatalf("ERROR: discover router MAC: %s", err)
}
ipv6LLA = generateEUI64Address(net.ParseIP("fe80::0"), mac)
mac2, err := tryDiscoverNeighborMAC(iface, ipv6LLA, 5)
if err != nil {
log.Fatalf("ERROR: discover router MAC: %s", err)
}
if mac.String() != mac2.String() {
log.Fatalf("ERROR: failed to get router link-local address")
}
}

if _, err := executeCommand("ip6tables", "-A", "OUTPUT", "-o", iface, "--src", ipv6LLA.String(), "-p", "icmpv6", "--icmpv6-type", "neighbor-solicitation", "-j", "DROP"); err != nil {
log.Fatalf("ERROR: drop neighbor solicitation on interface: %s", err)
}
if _, err := executeCommand("ip6tables", "-A", "OUTPUT", "-o", iface, "--src", ipv6LLA.String(), "-p", "icmpv6", "--icmpv6-type", "neighbor-advertisement", "-j", "DROP"); err != nil {
log.Fatalf("ERROR: drop neighbor advertisement on interface: %s", err)
}

// As described in RFC 4861 section-4.2, the srouce address of RA must be the link-local
// address assigned to the interface from which the message is sent, so the LLA of default
// router have to be added to the interface. And the followings need to be done.
// 1.Disable DAD of the interface
// 2.Add static neighbor entry for the client, otherwise the interface will send a NS
// message with it's MAC in options to client
// 3.Drop NA message from interface responsed to NS message learning default router LLA
// 4.Drop NS message from interface with default router LLA in options
if executeCommand("ip", "addr", "add", fmt.Sprintf("%s/64", ipv6LLA.String()), "dev", iface); err != nil {
log.Fatalf("ERROR: add IPv6 addr to the interface: %s", err)
}

src = ipv6LLA
}

if err := startRouteAdvertisement(iface, src, dst, cidr); err != nil {
log.Fatalf("ERROR: start route advertisement: %s", err)
}
}

func validateVars() (net.IP, net.IP, *net.IPNet, error) {
if iface == "" {
return nil, nil, nil, fmt.Errorf("the interface may not be empty")
}

var src net.IP
if router != "" {
src = net.ParseIP(router)
if src == nil {
return nil, nil, nil, fmt.Errorf("the router IPv6 address (%s) is illegal", router)
}
if isRemoteRoute {
if clientHWAddr == "" {
return nil, nil, nil, fmt.Errorf("the client-hardware-addr may not be empty when router is remote")
}
}
}

if client == "" {
if clientHWAddr == "" {
return nil, nil, nil, fmt.Errorf("the client and client-hardware-addr may not both be empty")
}
clientMAC, err := net.ParseMAC(clientHWAddr)
if err != nil {
return nil, nil, nil, fmt.Errorf("parse MAC: %s", err)
}
client = generateEUI64Address(net.ParseIP("fe80::0"), clientMAC).String()
}
dst := net.ParseIP(client)
if dst == nil {
return nil, nil, nil, fmt.Errorf("the client IPv6 address (%s) is illegal", client)
}
if !dst.IsLinkLocalUnicast() {
return nil, nil, nil, fmt.Errorf("the client IPv6 address should be a link-local address")
}

if prefix == "" {
return nil, nil, nil, fmt.Errorf("the prefix may not be empty")
}
_, cidr, err := net.ParseCIDR(prefix)
if err != nil {
return nil, nil, nil, fmt.Errorf("the prefix (%s) is illegal", prefix)
}

return src, dst, cidr, nil
}

func generateEUI64Address(prefix net.IP, mac net.HardwareAddr) net.IP {
ip := make([]byte, 16)
copy(ip[0:8], prefix[0:8])

copy(ip[8:11], mac[0:3])
ip[8] ^= 0x02
ip[11] = 0xff
ip[12] = 0xfe
copy(ip[13:16], mac[3:6])

return ip
}

func tryDiscoverNeighborMAC(ifaceName string, ip net.IP, retry int) (net.HardwareAddr, error) {
for i := retry; i > 0; i-- {
mac, err := discoverNeighborMAC(ifaceName, ip)
if err != nil {
return nil, err
}
if mac == nil {
log.Println("INFO: retry in 5s")
continue
}
return mac, nil
}

return nil, fmt.Errorf("failed to discover neighbor MAC. Try %d times", retry)
}

func discoverNeighborMAC(ifaceName string, ip net.IP) (net.HardwareAddr, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("get interface by name: %s", err)
}
conn, err := tryCreateNDPConn(iface, 5)
if err != nil {
return nil, fmt.Errorf("create NDP connection: %s", err)
}
defer conn.Close()

target := netip.MustParseAddr(ip.String())
solicitationAddr, err := ndp.SolicitedNodeMulticast(target)
if err != nil {
return nil, fmt.Errorf("determine solicited-node multicast address: %s", err)
}
solicitation := &ndp.NeighborSolicitation{
TargetAddress: target,
Options: []ndp.Option{
&ndp.LinkLayerAddress{
Direction: ndp.Source,
Addr: iface.HardwareAddr,
},
},
}
if err := conn.WriteTo(solicitation, nil, solicitationAddr); err != nil {
return nil, fmt.Errorf("write neighbor solicitation: %s", err)
}

var f ipv6.ICMPFilter
f.SetAll(true)
f.Accept(ipv6.ICMPTypeNeighborAdvertisement)
if err := conn.SetICMPFilter(&f); err != nil {
return nil, fmt.Errorf("set ICMPv6 filter: %s", err)
}

msg, _, from, err := conn.ReadFrom()
if err != nil {
return nil, fmt.Errorf("read NDP message: %s", err)
}
if target.WithZone(ifaceName).Compare(from) != 0 && target.Compare(from) != 0 {
log.Println("INFO: the NDP message is not from solicitation target")
return nil, nil
}
advertisement := msg.(*ndp.NeighborAdvertisement)
if len(advertisement.Options) != 1 {
return nil, fmt.Errorf("get %d option(s) in neighbor advertisement, but expect one", len(advertisement.Options))
}
linkLayerAddr, ok := advertisement.Options[0].(*ndp.LinkLayerAddress)
if !ok {
return nil, fmt.Errorf("advertisement option is not a link-layer address")
}
return linkLayerAddr.Addr, nil
}

func tryCreateNDPConn(iface *net.Interface, retry int) (*ndp.Conn, error) {
var err error
for i := retry; i > 0; i-- {
conn, _, err := ndp.Listen(iface, ndp.LinkLocal)
if err != nil {
// caused by tap device state down?
log.Printf("Warnning: listen interface link-local address: %s. Retry in 5s\n", err)
time.Sleep(5 * time.Second)
}
if err == nil {
return conn, nil
}
}

return nil, fmt.Errorf("listen interface link-local address: %s. Retry %d times", err, retry)
}

func startRouteAdvertisement(ifaceName string, src net.IP, dst net.IP, cidr *net.IPNet) error {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return fmt.Errorf("get interface by name: %s", err)
}
conn, err := tryCreateNDPConn(iface, 5)
if err != nil {
return fmt.Errorf("create NDP connection: %s", err)
}
defer conn.Close()

var filter ipv6.ICMPFilter
filter.SetAll(true)
filter.Accept(ipv6.ICMPTypeRouterSolicitation)
if err := conn.SetICMPFilter(&filter); err != nil {
return fmt.Errorf("apply ICMPv6 filter: %s", err)
}
if err := conn.JoinGroup(linkLocalAllRouters); err != nil {
return fmt.Errorf("join IPv6 link-local all routers multicast group: %s", err)
}

prefixLen, _ := cidr.Mask.Size()
advertisement := &ndp.RouterAdvertisement{
CurrentHopLimit: 255,
RouterLifetime: 65535 * time.Second,
ManagedConfiguration: true,
OtherConfiguration: true,
Options: []ndp.Option{
&ndp.PrefixInformation{
PrefixLength: uint8(prefixLen),
Prefix: netip.MustParseAddr(cidr.IP.String()),
OnLink: true,
ValidLifetime: 4294967295 * time.Second,
},
},
}

controlMsg := &ipv6.ControlMessage{
HopLimit: 255,
Src: src,
}

if src == nil {
advertisement.RouterLifetime = 0
controlMsg = nil
}

recivedRS := make(chan struct{}, 1)
go func(recivedRS chan struct{}) {
for {
_, _, from, err := conn.ReadFrom()
if err != nil {
log.Printf("Warnning: read NDP message: %s. Retry in 5s\n", err)
time.Sleep(5 * time.Second)
continue
}
target := netip.MustParseAddr(dst.String())
if target.WithZone(ifaceName).Compare(from) != 0 && target.Compare(from) != 0 {
continue
}
recivedRS <- struct{}{}
}
}(recivedRS)

raPeriod := time.NewTicker(time.Minute)
cnt := 0
for {
select {
case <-recivedRS:
if err := conn.WriteTo(advertisement, controlMsg, netip.MustParseAddr(dst.String())); err != nil {
return fmt.Errorf("send route advertisement: %s", err)
}
log.Printf("INFO: reply RS from %s\n", dst.String())
case <-raPeriod.C:
if err := conn.WriteTo(advertisement, controlMsg, netip.MustParseAddr(dst.String())); err != nil {
return fmt.Errorf("send route advertisement: %s", err)
}
cnt++
if cnt == 10 {
log.Printf("INFO: send RA to %s 10 times\n", dst.String())
cnt = 0
}
}
}
}

func executeCommand(name string, arg ...string) (string, error) {
cmd := exec.Command(name, arg...)
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("%q: %s: %s", cmd.String(), err, output)
}
return string(output), nil
}

func init() {
flag.StringVar(&iface, "interface", "", "The interface to listen to.")
flag.StringVar(&router, "router", "", "The IPv6 address of the default router. "+
"It's recommanded to use the link-local address of the router, "+
"otherwise the SLAAC link-local address formed by router hardware address will be used.")
flag.BoolVar(&isRemoteRoute, "is-remote-route", false, "")
flag.StringVar(&client, "client", "", "The IPv6 link-local address of the client to advertise to. "+
"The SLAAC link-local address formed by client hardware address will be used when empty.")
flag.StringVar(&clientHWAddr, "client-hardware-addr", "", "The hardware address of the client.")
flag.StringVar(&prefix, "prefix", "", "The prefix of the subnet.")

flag.Parse()
}
Loading