├── .github └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker.go ├── go.mod ├── go.sum ├── hostinfo.go ├── main.go ├── ndp.go └── ndpresponder.service /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: build 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | permissions: {} 8 | jobs: 9 | build: 10 | runs-on: ubuntu-22.04 11 | env: 12 | CGO_ENABLED: 0 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version-file: go.mod 18 | - run: go build . 19 | docker: 20 | runs-on: ubuntu-22.04 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: docker/build-push-action@v5 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ndpresponder 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-alpine3.20 AS build 2 | WORKDIR /app 3 | COPY . . 4 | RUN env CGO_ENABLED=0 GOBIN=/build go install . 5 | 6 | FROM scratch 7 | COPY --from=build /build/* / 8 | ENTRYPOINT ["/ndpresponder"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2021-2024, Junxiao Shi 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IPv6 Neighbor Discovery Responder 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/yoursunny/ndpresponder/build.yml)](https://github.com/yoursunny/ndpresponder/actions) [![GitHub code size](https://img.shields.io/github/languages/code-size/yoursunny/ndpresponder?style=flat&logo=GitHub)](https://github.com/yoursunny/ndpresponder) 4 | 5 | **ndpresponder** is a Go program that listens for ICMPv6 neighbor solicitations on a network interface and responds with neighbor advertisements, as described in [RFC 4861](https://tools.ietf.org/html/rfc4861) - IPv6 Neighbor Discovery Protocol. 6 | 7 | This program differs from [ndppd - NDP Proxy Daemon](https://github.com/DanielAdolfsson/ndppd) in that the source IPv6 address of neighbor advertisement is set to the same value as the target address in the neighbor solicitation. 8 | This change enables **ndpresponder** to work in certain KVM virtual servers where NDP uses link-local addresses but *ebtables* drops outgoing packets from link-local addresses. 9 | See my [blog post](https://yoursunny.com/t/2021/ndpresponder/) for more information. 10 | 11 | ## Installation 12 | 13 | This program is written in Go. 14 | You can compile and install this program with: 15 | 16 | ```bash 17 | env CGO_ENABLED=0 go install github.com/yoursunny/ndpresponder@main 18 | ``` 19 | 20 | This program is also available as a Docker container: 21 | 22 | ```bash 23 | docker build -t localhost/ndpresponder 'github.com/yoursunny/ndpresponder#main' 24 | docker run -d --name localhost/ndpresponder --network host ndpresponder [arguments] 25 | ``` 26 | 27 | ## Static Mode 28 | 29 | The program can respond to neighbor solicitations for any address under one or more subnets. 30 | It's recommended to keep the subnets as small as possible. 31 | 32 | Sample command: 33 | 34 | ```bash 35 | sudo ndpresponder -i eth0 -n 2001:db8:3988:486e:ff2f:add3:31e3:7b00/120 36 | ``` 37 | 38 | * `-i` flag specifies the network interface name. 39 | * `-n` flag specifies the IPv6 subnet to respond to. 40 | You may repeat this flag to specify multiple subnets. 41 | 42 | See [ndpresponder.service](ndpresponder.service) for a sample systemd unit file. 43 | 44 | ## Docker Network Mode 45 | 46 | The program can respond to neighbor solicitations for assigned addresses in Docker networks. 47 | When a container connects to a network, it attempts to inform the gateway router about the presence of a new address. 48 | 49 | Sample command: 50 | 51 | ```bash 52 | docker network create --ipv6 --subnet=172.26.0.0/16 \ 53 | --subnet=2001:db8:1972:beb0:dce3:9c1a:d150::/112 ipv6exposed 54 | 55 | docker run -d \ 56 | --restart always --cpus 0.02 --memory 64M \ 57 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 58 | --cap-drop=ALL --cap-add=NET_RAW --cap-add=NET_ADMIN \ 59 | --network host --name ndpresponder \ 60 | localhost/ndpresponder -i eth0 -N ipv6exposed 61 | ``` 62 | 63 | * `-i` flag specifies the network interface name. 64 | * `-N` flag specifies the Docker network name. 65 | You may repeat this flag to specify multiple networks. 66 | 67 | ## Other Options 68 | 69 | You may change log level of this program by setting the `NDPRESPONDER_LOG` environment variable. 70 | Acceptable values are `DEBUG`, `INFO`, `WARN`, `ERROR`, and `FATAL`. 71 | 72 | Sample command: 73 | 74 | ```bash 75 | sudo NDPRESPONDER_LOG=WARN ndpresponder [arguments] 76 | docker run -e NDPRESPONDER_LOG=WARN [other arguments] 77 | ``` 78 | -------------------------------------------------------------------------------- /docker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/netip" 5 | 6 | docker "github.com/fsouza/go-dockerclient" 7 | "go.uber.org/zap" 8 | "go4.org/netipx" 9 | ) 10 | 11 | var ( 12 | dockerLogger = logger.Named("Docker") 13 | dockerNetworks []string 14 | dockerClient *docker.Client 15 | dockerNetIPSets = map[string]*netipx.IPSet{} 16 | dockerActiveIPs = &netipx.IPSet{} 17 | dockerNewIP = make(chan netip.Addr, 64) 18 | ) 19 | 20 | func dockerListen() (e error) { 21 | if dockerClient, e = docker.NewClientFromEnv(); e != nil { 22 | return e 23 | } 24 | events := make(chan *docker.APIEvents, 64) 25 | if e = dockerClient.AddEventListenerWithOptions(docker.EventsOptions{ 26 | Filters: map[string][]string{ 27 | "type": {"network"}, 28 | "event": {"connect", "disconnect"}, 29 | "network": dockerNetworks, 30 | }, 31 | }, events); e != nil { 32 | return e 33 | } 34 | 35 | for _, network := range dockerNetworks { 36 | dockerRefreshNetwork(network, func(string) bool { return true }) 37 | } 38 | 39 | go func() { 40 | for evt := range events { 41 | ctID := evt.Actor.Attributes["container"] 42 | dockerRefreshNetwork(evt.Actor.Attributes["name"], 43 | func(ct string) bool { return ct == ctID }) 44 | } 45 | }() 46 | 47 | return nil 48 | } 49 | 50 | func dockerRefreshNetwork(name string, isNewContainer func(ctID string) bool) { 51 | network, e := dockerClient.NetworkInfo(name) 52 | if e != nil { 53 | dockerLogger.Warn("NetworkInfo error", zap.Error(e)) 54 | return 55 | } 56 | 57 | var b netipx.IPSetBuilder 58 | var ipAddrs []string 59 | var newIPs []netip.Addr 60 | for ctID, ct := range network.Containers { 61 | prefix, _ := netip.ParsePrefix(ct.IPv6Address) 62 | ip := prefix.Addr() 63 | b.Add(ip) 64 | ipAddrs = append(ipAddrs, ip.String()) 65 | 66 | if isNewContainer(ctID) { 67 | newIPs = append(newIPs, ip) 68 | } 69 | } 70 | dockerLogger.Info("active IPs updated", 71 | zap.String("network", network.Name), 72 | zap.Strings("ip", ipAddrs), 73 | ) 74 | dockerNetIPSets[network.ID], _ = b.IPSet() 75 | 76 | for net, ipset := range dockerNetIPSets { 77 | if net != network.ID { 78 | b.AddSet(ipset) 79 | } 80 | } 81 | dockerActiveIPs, _ = b.IPSet() 82 | 83 | for _, ip := range newIPs { 84 | dockerNewIP <- ip 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yoursunny/ndpresponder 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/fsouza/go-dockerclient v1.12.0 7 | github.com/gopacket/gopacket v1.3.1 8 | github.com/urfave/cli/v2 v2.27.5 9 | github.com/vishvananda/netlink v1.3.0 10 | go.uber.org/zap v1.27.0 11 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba 12 | golang.org/x/net v0.32.0 13 | golang.org/x/sys v0.28.0 14 | ) 15 | 16 | require ( 17 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect 18 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 19 | github.com/Microsoft/go-winio v0.6.2 // indirect 20 | github.com/containerd/log v0.1.0 // indirect 21 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 22 | github.com/docker/docker v27.4.0+incompatible // indirect 23 | github.com/docker/go-connections v0.5.0 // indirect 24 | github.com/docker/go-units v0.5.0 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/klauspost/compress v1.17.11 // indirect 27 | github.com/moby/docker-image-spec v1.3.1 // indirect 28 | github.com/moby/patternmatcher v0.6.0 // indirect 29 | github.com/moby/sys/sequential v0.6.0 // indirect 30 | github.com/moby/sys/user v0.3.0 // indirect 31 | github.com/moby/sys/userns v0.1.0 // indirect 32 | github.com/moby/term v0.5.0 // indirect 33 | github.com/morikuni/aec v1.0.0 // indirect 34 | github.com/opencontainers/go-digest v1.0.0 // indirect 35 | github.com/opencontainers/image-spec v1.1.0 // indirect 36 | github.com/pkg/errors v0.9.1 // indirect 37 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 38 | github.com/sirupsen/logrus v1.9.3 // indirect 39 | github.com/vishvananda/netns v0.0.5 // indirect 40 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 41 | go.uber.org/multierr v1.11.0 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= 2 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 3 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 4 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 8 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 11 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 12 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/docker/docker v27.4.0+incompatible h1:I9z7sQ5qyzO0BfAb9IMOawRkAGxhYsidKiTMcm0DU+A= 17 | github.com/docker/docker v27.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 18 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 19 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 20 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 21 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 22 | github.com/fsouza/go-dockerclient v1.12.0 h1:S2f2crEUbBNCFiF06kR/GvioEB8EMsb3Td/bpawD+aU= 23 | github.com/fsouza/go-dockerclient v1.12.0/go.mod h1:YWUtjg8japrqD/80L98nTtCoxQFp5B5wrSsnyeB5lFo= 24 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 25 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 26 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 27 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 | github.com/gopacket/gopacket v1.3.1 h1:ZppWyLrOJNZPe5XkdjLbtuTkfQoxQ0xyMJzQCqtqaPU= 29 | github.com/gopacket/gopacket v1.3.1/go.mod h1:3I13qcqSpB2R9fFQg866OOgzylYkZxLTmkvcXhvf6qg= 30 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 31 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 32 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 33 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 34 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 35 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 36 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 37 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 38 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 39 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 40 | github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= 41 | github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 42 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 43 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 44 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 45 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 46 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 47 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 48 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 49 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 50 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 51 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 52 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 53 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 57 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 58 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 59 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 60 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 61 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 63 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 64 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 65 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 66 | github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= 67 | github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= 68 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 69 | github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= 70 | github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 71 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 72 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 73 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 74 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 75 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 76 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 77 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 78 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 79 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 80 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 81 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= 82 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= 83 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 84 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 85 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 86 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 87 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 88 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 90 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 92 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= 93 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= 94 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 105 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 106 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 107 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 108 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 109 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 110 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 112 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 113 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 114 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 115 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 116 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 117 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 118 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 120 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 121 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 122 | gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= 123 | gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 124 | -------------------------------------------------------------------------------- /hostinfo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "net/netip" 6 | "os/exec" 7 | "time" 8 | 9 | "github.com/vishvananda/netlink" 10 | "go.uber.org/zap" 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | // HostInfo contains address information of the host machine. 15 | type HostInfo struct { 16 | HostMAC net.HardwareAddr 17 | GatewayIP netip.Addr 18 | } 19 | 20 | func gatherHostInfo() (hi HostInfo, e error) { 21 | logEntry := logger.Named("HostInfo") 22 | hi.HostMAC = netif.HardwareAddr 23 | logEntry.Info("found MAC", zap.Stringer("mac", hi.HostMAC)) 24 | 25 | nl, e := netlink.NewHandle() 26 | if e != nil { 27 | logEntry.Error("netlink.NewHandle error", zap.Error(e)) 28 | return hi, nil 29 | } 30 | defer nl.Close() 31 | 32 | link, e := nl.LinkByIndex(netif.Index) 33 | if e != nil { 34 | logEntry.Error("netlink.LinkByIndex error", zap.Error(e)) 35 | return hi, nil 36 | } 37 | 38 | routes, e := nl.RouteList(link, unix.AF_INET6) 39 | if e != nil { 40 | logEntry.Error("netlink.RouteList error", zap.Error(e)) 41 | return hi, nil 42 | } 43 | 44 | for _, route := range routes { 45 | maskLen := 0 46 | if route.Dst != nil { 47 | maskLen, _ = route.Dst.Mask.Size() 48 | } 49 | if maskLen == 0 { 50 | hi.GatewayIP, _ = netip.AddrFromSlice(route.Gw) 51 | } 52 | } 53 | if !hi.GatewayIP.IsValid() { 54 | logEntry.Warn("no default gateway") 55 | return hi, nil 56 | } 57 | logEntry.Info("found gateway", zap.Stringer("gateway", hi.GatewayIP)) 58 | 59 | var gatewayNeigh *netlink.Neigh 60 | for { 61 | neighs, e := nl.NeighList(netif.Index, unix.AF_INET6) 62 | if e != nil { 63 | logEntry.Error("netlink.NeighList error", zap.Error(e)) 64 | return hi, nil 65 | } 66 | for _, neigh := range neighs { 67 | ip, _ := netip.AddrFromSlice(neigh.IP) 68 | if ip != hi.GatewayIP || len(neigh.HardwareAddr) != 6 { 69 | continue 70 | } 71 | switch neigh.State { 72 | case unix.NUD_REACHABLE, unix.NUD_NOARP: 73 | gatewayNeigh = &neigh 74 | goto NEIGH_SET 75 | case unix.NUD_PERMANENT: 76 | goto NEIGH_SKIP 77 | } 78 | } 79 | 80 | exec.Command("/usr/bin/ping", "-c", "1", hi.GatewayIP.String()).Run() 81 | logEntry.Debug("waiting for gateway neigh entry") 82 | time.Sleep(time.Second) 83 | } 84 | 85 | NEIGH_SET: 86 | gatewayNeigh.State = unix.NUD_NOARP 87 | if e = nl.NeighSet(gatewayNeigh); e != nil { 88 | logEntry.Error("netlink.NeighSet error", zap.Error(e)) 89 | } else { 90 | logEntry.Info("netlink.NeighSet OK", zap.Stringer("lladdr", gatewayNeigh.HardwareAddr)) 91 | } 92 | NEIGH_SKIP: 93 | 94 | return hi, nil 95 | } 96 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "net/netip" 6 | "os" 7 | 8 | "github.com/gopacket/gopacket" 9 | "github.com/gopacket/gopacket/afpacket" 10 | "github.com/urfave/cli/v2" 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/zapcore" 13 | "go4.org/netipx" 14 | ) 15 | 16 | var logger = func() *zap.Logger { 17 | var lvl zapcore.Level 18 | if environ, ok := os.LookupEnv("NDPRESPONDER_LOG"); ok { 19 | lvl.Set(environ) 20 | } 21 | 22 | core := zapcore.NewCore( 23 | zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), 24 | os.Stderr, 25 | lvl, 26 | ) 27 | return zap.New(core) 28 | }() 29 | 30 | var ( 31 | netif *net.Interface 32 | targetSubnets *netipx.IPSet 33 | handle *afpacket.TPacket 34 | ) 35 | 36 | var app = &cli.App{ 37 | Name: "ndpresponder", 38 | Description: "IPv6 Neighbor Discovery Responder", 39 | Flags: []cli.Flag{ 40 | &cli.StringFlag{ 41 | Name: "ifname", 42 | Aliases: []string{"i"}, 43 | Usage: "uplink network interface", 44 | Required: true, 45 | }, 46 | &cli.StringSliceFlag{ 47 | Name: "subnet", 48 | Aliases: []string{"n"}, 49 | Usage: "static target subnet", 50 | }, 51 | &cli.StringSliceFlag{ 52 | Name: "docker-network", 53 | Aliases: []string{"N"}, 54 | Usage: "Docker network name", 55 | }, 56 | }, 57 | HideHelpCommand: true, 58 | Before: func(c *cli.Context) (e error) { 59 | if netif, e = net.InterfaceByName(c.String("ifname")); e != nil { 60 | return cli.Exit(e, 1) 61 | } 62 | 63 | var ipset netipx.IPSetBuilder 64 | for _, subnet := range c.StringSlice("subnet") { 65 | prefix, e := netip.ParsePrefix(subnet) 66 | if e != nil { 67 | return cli.Exit(e, 1) 68 | } 69 | ipset.AddPrefix(prefix) 70 | } 71 | targetSubnets, e = ipset.IPSet() 72 | if e != nil { 73 | return cli.Exit(e, 1) 74 | } 75 | 76 | dockerNetworks = c.StringSlice("docker-network") 77 | 78 | return nil 79 | }, 80 | Action: func(c *cli.Context) error { 81 | hi, e := gatherHostInfo() 82 | if e != nil { 83 | return cli.Exit(e, 1) 84 | } 85 | h, e := afpacket.NewTPacket(afpacket.OptInterface(netif.Name)) 86 | if e != nil { 87 | return cli.Exit(e, 1) 88 | } 89 | if e = h.SetBPF(bpfFilter); e != nil { 90 | return cli.Exit(e, 1) 91 | } 92 | solicitations := CaptureNeighSolicitation(h) 93 | 94 | if len(dockerNetworks) > 0 { 95 | if e = dockerListen(); e != nil { 96 | return cli.Exit(e, 1) 97 | } 98 | } 99 | 100 | sbuf := gopacket.NewSerializeBuffer() 101 | L: 102 | for { 103 | select { 104 | case ns := <-solicitations: 105 | logEntry := logger.With(zap.Stringer("ns", ns)) 106 | switch { 107 | case dockerActiveIPs.Contains(ns.TargetIP): 108 | logEntry = logEntry.With(zap.String("reason", "docker")) 109 | case ns.DestIP.IsMulticast() && targetSubnets.Contains(ns.TargetIP): 110 | logEntry = logEntry.With(zap.String("reason", "static")) 111 | default: 112 | logEntry.Debug("IGNORE") 113 | continue L 114 | } 115 | 116 | if e := ns.Respond(sbuf, hi); e != nil { 117 | logEntry.Warn("RESPOND error", zap.Error(e)) 118 | continue L 119 | } 120 | logEntry.Info("RESPOND") 121 | h.WritePacketData(sbuf.Bytes()) 122 | 123 | case ip := <-dockerNewIP: 124 | logEntry := logger.With(zap.Stringer("ip", ip)) 125 | if e := Gratuitous(sbuf, hi, ip); e != nil { 126 | logEntry.Warn("GRATUITOUS error", zap.Error(e)) 127 | continue L 128 | } 129 | logEntry.Info("GRATUITOUS") 130 | h.WritePacketData(sbuf.Bytes()) 131 | 132 | if !hi.GatewayIP.IsValid() { 133 | break 134 | } 135 | if e := Solicit(sbuf, hi, ip); e != nil { 136 | logEntry.Warn("SOLICIT error", zap.Error(e)) 137 | continue L 138 | } 139 | logEntry.Info("SOLICIT") 140 | h.WritePacketData(sbuf.Bytes()) 141 | } 142 | } 143 | }, 144 | After: func(c *cli.Context) error { 145 | if handle != nil { 146 | handle.Close() 147 | } 148 | return nil 149 | }, 150 | } 151 | 152 | func main() { 153 | app.Run(os.Args) 154 | } 155 | -------------------------------------------------------------------------------- /ndp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math" 9 | "net" 10 | "net/netip" 11 | 12 | "github.com/gopacket/gopacket" 13 | "github.com/gopacket/gopacket/layers" 14 | "golang.org/x/net/bpf" 15 | ) 16 | 17 | // tcpdump -dd 'icmp6 && ip6[40]==135' 18 | var bpfFilter = []bpf.RawInstruction{ 19 | {0x28, 0, 0, 0x0000000c}, 20 | {0x15, 0, 8, 0x000086dd}, 21 | {0x30, 0, 0, 0x00000014}, 22 | {0x15, 3, 0, 0x0000003a}, 23 | {0x15, 0, 5, 0x0000002c}, 24 | {0x30, 0, 0, 0x00000036}, 25 | {0x15, 0, 3, 0x0000003a}, 26 | {0x30, 0, 0, 0x00000036}, 27 | {0x15, 0, 1, 0x00000087}, 28 | {0x6, 0, 0, 0x00040000}, 29 | {0x6, 0, 0, 0x00000000}, 30 | } 31 | var packetSerializeOpts = gopacket.SerializeOptions{ 32 | FixLengths: true, 33 | ComputeChecksums: true, 34 | } 35 | var solicitTypeCode = layers.CreateICMPv6TypeCode(layers.ICMPv6TypeNeighborSolicitation, 0) 36 | var advertTypeCode = layers.CreateICMPv6TypeCode(layers.ICMPv6TypeNeighborAdvertisement, 0) 37 | 38 | // Gratuitous creates a gratuitous ICMPv6 neighbor solicitation packet. 39 | func Gratuitous(w gopacket.SerializeBuffer, hi HostInfo, targetIP netip.Addr) error { 40 | ip16 := targetIP.As16() 41 | eth := layers.Ethernet{ 42 | SrcMAC: hi.HostMAC, 43 | DstMAC: net.HardwareAddr{0x33, 0x33, 0xFF, ip16[13], ip16[14], ip16[15]}, 44 | EthernetType: layers.EthernetTypeIPv6, 45 | } 46 | 47 | dstIP := net.IP{0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x01, 0xFF, ip16[13], ip16[14], ip16[15]} 49 | ip6 := layers.IPv6{ 50 | Version: 6, 51 | SrcIP: make(net.IP, net.IPv6len), 52 | DstIP: dstIP, 53 | NextHeader: layers.IPProtocolICMPv6, 54 | HopLimit: math.MaxUint8, 55 | } 56 | 57 | icmp6 := layers.ICMPv6{ 58 | TypeCode: solicitTypeCode, 59 | } 60 | icmp6.SetNetworkLayerForChecksum(&ip6) 61 | 62 | nonce := make([]byte, 6) 63 | rand.Read(nonce) 64 | solicit := layers.ICMPv6NeighborSolicitation{ 65 | TargetAddress: targetIP.AsSlice(), 66 | Options: layers.ICMPv6Options{ 67 | { 68 | Type: 0x0E, 69 | Data: nonce, 70 | }, 71 | }, 72 | } 73 | 74 | return gopacket.SerializeLayers(w, packetSerializeOpts, ð, &ip6, &icmp6, &solicit) 75 | 76 | } 77 | 78 | // Solicit creates an ICMPv6 neighbor solicitation packet. 79 | func Solicit(w gopacket.SerializeBuffer, hi HostInfo, sourceIP netip.Addr) error { 80 | eth := layers.Ethernet{ 81 | SrcMAC: hi.HostMAC, 82 | DstMAC: net.HardwareAddr{0x33, 0x33, 0xFF, 0x00, 0x00, 0x01}, 83 | EthernetType: layers.EthernetTypeIPv6, 84 | } 85 | 86 | dstIP := net.IP{0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 87 | 0x00, 0x00, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x01} 88 | ip6 := layers.IPv6{ 89 | Version: 6, 90 | SrcIP: sourceIP.AsSlice(), 91 | DstIP: dstIP, 92 | NextHeader: layers.IPProtocolICMPv6, 93 | HopLimit: math.MaxUint8, 94 | } 95 | 96 | icmp6 := layers.ICMPv6{ 97 | TypeCode: solicitTypeCode, 98 | } 99 | icmp6.SetNetworkLayerForChecksum(&ip6) 100 | 101 | nonce := make([]byte, 6) 102 | rand.Read(nonce) 103 | solicit := layers.ICMPv6NeighborSolicitation{ 104 | TargetAddress: hi.GatewayIP.AsSlice(), 105 | Options: layers.ICMPv6Options{ 106 | { 107 | Type: layers.ICMPv6OptSourceAddress, 108 | Data: hi.HostMAC, 109 | }, 110 | }, 111 | } 112 | 113 | return gopacket.SerializeLayers(w, packetSerializeOpts, ð, &ip6, &icmp6, &solicit) 114 | } 115 | 116 | // NeighSolicitation contains information from an ICMPv6 neighbor solicitation packet. 117 | type NeighSolicitation struct { 118 | RouterMAC [6]byte 119 | RouterIP netip.Addr 120 | DestIP netip.Addr 121 | TargetIP netip.Addr 122 | } 123 | 124 | func (ns NeighSolicitation) String() string { 125 | if ns.DestIP.IsMulticast() { 126 | return fmt.Sprintf("who-has %s tell %s", ns.TargetIP, ns.RouterIP) 127 | } 128 | return fmt.Sprintf("is-alive %s tell %s", ns.TargetIP, ns.RouterIP) 129 | } 130 | 131 | // Respond creates an ICMPv6 neighbor advertisement packet. 132 | func (ns NeighSolicitation) Respond(w gopacket.SerializeBuffer, hi HostInfo) error { 133 | eth := layers.Ethernet{ 134 | SrcMAC: hi.HostMAC, 135 | DstMAC: ns.RouterMAC[:], 136 | EthernetType: layers.EthernetTypeIPv6, 137 | } 138 | 139 | ip6 := layers.IPv6{ 140 | Version: 6, 141 | SrcIP: ns.TargetIP.AsSlice(), 142 | DstIP: ns.RouterIP.AsSlice(), 143 | NextHeader: layers.IPProtocolICMPv6, 144 | HopLimit: math.MaxUint8, 145 | } 146 | 147 | icmp6 := layers.ICMPv6{ 148 | TypeCode: advertTypeCode, 149 | } 150 | icmp6.SetNetworkLayerForChecksum(&ip6) 151 | 152 | var advertFlags uint8 = 0x80 | 0x40 // router, solicited 153 | if ns.DestIP.IsMulticast() { 154 | advertFlags |= 0x20 // override 155 | } 156 | advert := layers.ICMPv6NeighborAdvertisement{ 157 | Flags: advertFlags, 158 | TargetAddress: ns.TargetIP.AsSlice(), 159 | Options: layers.ICMPv6Options{ 160 | { 161 | Type: layers.ICMPv6OptTargetAddress, 162 | Data: hi.HostMAC, 163 | }, 164 | }, 165 | } 166 | 167 | return gopacket.SerializeLayers(w, packetSerializeOpts, ð, &ip6, &icmp6, &advert) 168 | } 169 | 170 | // CaptureNeighSolicitation captures ICMPv6 neighbor solicitation packets. 171 | func CaptureNeighSolicitation(src gopacket.ZeroCopyPacketDataSource) <-chan NeighSolicitation { 172 | ch := make(chan NeighSolicitation) 173 | go func() { 174 | var eth layers.Ethernet 175 | var ip6 layers.IPv6 176 | var icmp6 layers.ICMPv6 177 | var solicit layers.ICMPv6NeighborSolicitation 178 | parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, ð, &ip6, &icmp6, &solicit) 179 | decoded := []gopacket.LayerType{} 180 | 181 | for { 182 | pkt, _, e := src.ZeroCopyReadPacketData() 183 | if errors.Is(e, io.EOF) { 184 | close(ch) 185 | return 186 | } 187 | 188 | if e := parser.DecodeLayers(pkt, &decoded); e != nil { 189 | continue 190 | } 191 | 192 | if len(decoded) == 4 && decoded[3] == layers.LayerTypeICMPv6NeighborSolicitation { 193 | ns := NeighSolicitation{} 194 | copy(ns.RouterMAC[:], eth.SrcMAC) 195 | ns.RouterIP, _ = netip.AddrFromSlice(ip6.SrcIP) 196 | ns.DestIP, _ = netip.AddrFromSlice(ip6.DstIP) 197 | ns.TargetIP, _ = netip.AddrFromSlice(solicit.TargetAddress) 198 | ch <- ns 199 | } 200 | } 201 | }() 202 | return ch 203 | } 204 | -------------------------------------------------------------------------------- /ndpresponder.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ndpresponder 3 | After=network-online.target 4 | 5 | [Service] 6 | ExecStartPre=+/usr/bin/ip route get 2000:: 7 | ExecStart=+/usr/local/bin/ndpresponder -i eth0 -n 2001:db8:3988:486e:ff2f:add3:31e3:7b00/120 8 | Restart=on-failure 9 | RestartSec=10s 10 | CPUAccounting=yes 11 | CPUQuota=1% 12 | ProtectSystem=full 13 | PrivateTmp=yes 14 | PrivateDevices=yes 15 | ProtectHome=yes 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | --------------------------------------------------------------------------------