diff --git a/config.go b/config.go index f1ceb17..7fec5dd 100644 --- a/config.go +++ b/config.go @@ -32,7 +32,9 @@ func readConfig(dest string) { if err != nil { log.Fatal(err) } - defer file.Close() + defer func(file *os.File) { + _ = file.Close() + }(file) scanner := bufio.NewScanner(file) diff --git a/main.go b/main.go index db9012c..248b72e 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ func WaitForSignal() { } func main() { - fmt.Println("PNDPD Version 0.9 - Kioubit 2021") + fmt.Println("PNDPD Version 1.0 - Kioubit 2021") if len(os.Args) <= 2 { printUsage() @@ -31,7 +31,6 @@ func main() { r.Start() } else { r = pndp.NewResponder(os.Args[2], nil, "") - fmt.Println("WARNING: You should use a whitelist unless you know what you are doing") r.Start() } WaitForSignal() diff --git a/pndp/NDPRequest.go b/pndp/NDPRequest.go index 27c2577..05f8c2c 100644 --- a/pndp/NDPRequest.go +++ b/pndp/NDPRequest.go @@ -15,6 +15,7 @@ type ndpRequest struct { mac []byte receivedIfaceMac []byte sourceIface string + rawPacket []byte } type ndpQuestion struct { diff --git a/pndp/packet.go b/pndp/packet.go index cf14e4e..bd78a59 100644 --- a/pndp/packet.go +++ b/pndp/packet.go @@ -1,14 +1,14 @@ package pndp import ( + "bytes" "encoding/binary" "errors" + "fmt" "net" - "strings" ) var emptyIpv6 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} -var allNodesIpv6 = []byte{0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01} type payload interface { constructPacket() ([]byte, int) @@ -115,6 +115,11 @@ func (p *ndpPayload) constructPacket() ([]byte, int) { } func calculateChecksum(h *ipv6Header, payload []byte) uint16 { + if payload == nil { + return 0x0000 + } else if len(payload) == 0 { + return 0x0000 + } sumPseudoHeader := checksumAddition(h.srcIP) + checksumAddition(h.dstIP) + checksumAddition([]byte{0x00, h.protocol}) + checksumAddition(h.payloadLen) sumPayload := checksumAddition(payload) sumTotal := sumPayload + sumPseudoHeader @@ -128,7 +133,7 @@ func checksumAddition(b []byte) uint32 { var sum uint32 = 0 for i := 0; i < len(b); i++ { if i%2 == 0 { - if len(b) <= i-1 { + if len(b)-1 == i { sum += uint32(uint16(b[i])<<8 | uint16(0x0)) } else { sum += uint32(uint16(b[i])<<8 | uint16(b[i+1])) @@ -138,7 +143,41 @@ func checksumAddition(b []byte) uint32 { return sum } +func checkPacketChecksum(scrip, dstip, payload []byte) bool { + v6, err := newIpv6Header(scrip, dstip) + if err != nil { + return false + } + + packetsum := make([]byte, 2) + copy(packetsum, payload[2:4]) + + bPayloadLen := make([]byte, 2) + binary.BigEndian.PutUint16(bPayloadLen, uint16(len(payload))) + v6.payloadLen = bPayloadLen + + payload[2] = 0x0 + payload[3] = 0x0 + + bChecksum := make([]byte, 2) + binary.BigEndian.PutUint16(bChecksum, calculateChecksum(v6, payload)) + if bytes.Equal(packetsum, bChecksum) { + if GlobalDebug { + fmt.Println("Verified received packet checksum") + } + return true + } else { + if GlobalDebug { + fmt.Println("Received packet checksum validation failed") + } + return false + } +} + func isIpv6(ip string) bool { rip := net.ParseIP(ip) - return rip != nil && strings.Contains(ip, ":") + if rip.To16() == nil { + return false + } + return true } diff --git a/pndp/process.go b/pndp/process.go index b95d28a..9b24d1b 100644 --- a/pndp/process.go +++ b/pndp/process.go @@ -35,6 +35,7 @@ type ProxyObj struct { // With the optional autosenseInterface argument, the whitelist is configured based on the addresses assigned to the interface specified. This works even if the IP addresses change frequently. // Start() must be called on the object to actually start responding func NewResponder(iface string, filter []*net.IPNet, autosenseInterface string) *ResponderObj { + fmt.Println("WARNING: You should use a whitelist for the responder unless you really know what you are doing") var s sync.WaitGroup return &ResponderObj{ stopChan: make(chan struct{}), @@ -61,7 +62,7 @@ func (obj *ResponderObj) start() { } //Stop a running Responder instance -// Returns false on success +// Returns false on error func (obj *ResponderObj) Stop() bool { close(obj.stopChan) fmt.Println("Shutting down responder instance..") diff --git a/pndp/rawsocket.go b/pndp/rawsocket.go index b54b8f6..7018415 100644 --- a/pndp/rawsocket.go +++ b/pndp/rawsocket.go @@ -2,7 +2,6 @@ package pndp import ( "bytes" - "encoding/binary" "fmt" "golang.org/x/net/bpf" "golang.org/x/sys/unix" @@ -46,6 +45,7 @@ func htons16(v uint16) uint16 { return v<<8 | v>>8 } func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopWG *sync.WaitGroup, stopChan chan struct{}) { stopWG.Add(1) defer stopWG.Done() + niface, err := net.InterfaceByName(iface) if err != nil { panic(err.Error()) @@ -61,7 +61,7 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW } go func() { <-stopChan - syscall.Close(fd) + _ = syscall.Close(fd) stopWG.Done() // syscall.read does not release when the file descriptor is closed }() if GlobalDebug { @@ -99,9 +99,9 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW bpf.LoadAbsolute{Off: 54, Size: 1}, // Jump to the drop packet instruction if Type is not Neighbor Solicitation / Advertisement. bpf.JumpIf{Cond: bpf.JumpNotEqual, Val: protocolNo, SkipTrue: 1}, - // Verdict is "send up to 4k of the packet to userspace."buf + // Verdict is: send up to 4096 bytes of the packet to userspace. bpf.RetConstant{Val: 4096}, - // Verdict is "ignore packet." + // Verdict is: "ignore packet." bpf.RetConstant{Val: 0}, } @@ -118,7 +118,6 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW } if numRead < 86 { if GlobalDebug { - fmt.Println("Dropping packet since it does not meet the minimum length requirement") fmt.Printf("% X\n", buf[:numRead]) } @@ -128,72 +127,39 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW fmt.Println("Got packet on", iface, "of type", requestType) fmt.Printf("% X\n", buf[:numRead]) - fmt.Println("Source MAC ETHER") - fmt.Printf("% X\n", buf[:numRead][6:12]) + fmt.Println("Source mac on ethernet layer:") + fmt.Printf("% X\n", buf[6:12]) fmt.Println("Source IP:") - fmt.Printf("% X\n", buf[:numRead][22:38]) + fmt.Printf("% X\n", buf[22:38]) fmt.Println("Destination IP:") - fmt.Printf("% X\n", buf[:numRead][38:54]) + fmt.Printf("% X\n", buf[38:54]) fmt.Println("Requested IP:") - fmt.Printf("% X\n", buf[:numRead][62:78]) - fmt.Println("Source MAC") - fmt.Printf("% X\n", buf[:numRead][80:86]) + fmt.Printf("% X\n", buf[62:78]) + if requestType == ndp_ADV { + fmt.Println("NDP Flags") + fmt.Printf("% X\n", buf[58]) + } + fmt.Println("NDP MAC:") + fmt.Printf("% X\n", buf[80:86]) fmt.Println() } - if bytes.Equal(buf[:numRead][6:12], niface.HardwareAddr) { + if bytes.Equal(buf[6:12], niface.HardwareAddr) { if GlobalDebug { fmt.Println("Dropping packet from ourselves") } continue } - if !checkPacketChecksum(buf[:numRead][22:38], buf[:numRead][38:54], buf[:numRead][54:numRead]) { - if GlobalDebug { - fmt.Println("Dropping packet because of invalid checksum") - } - continue - } - responder <- &ndpRequest{ requestType: requestType, - srcIP: buf[:numRead][22:38], - dstIP: buf[:numRead][38:54], - answeringForIP: buf[:numRead][62:78], - mac: buf[:numRead][80:86], + srcIP: buf[22:38], + dstIP: buf[38:54], + answeringForIP: buf[62:78], + mac: buf[80:86], receivedIfaceMac: niface.HardwareAddr, sourceIface: iface, + rawPacket: buf[:numRead], } } } - -func checkPacketChecksum(scrip, dstip, payload []byte) bool { - v6, err := newIpv6Header(scrip, dstip) - if err != nil { - return false - } - - packetsum := make([]byte, 2) - copy(packetsum, payload[2:4]) - - bPayloadLen := make([]byte, 2) - binary.BigEndian.PutUint16(bPayloadLen, uint16(len(payload))) - v6.payloadLen = bPayloadLen - - payload[2] = 0x0 - payload[3] = 0x0 - - bChecksum := make([]byte, 2) - binary.BigEndian.PutUint16(bChecksum, calculateChecksum(v6, payload)) - if bytes.Equal(packetsum, bChecksum) { - if GlobalDebug { - fmt.Println("Verified received packet checksum") - } - return true - } else { - if GlobalDebug { - fmt.Println("Received packet checksum validation failed") - } - return false - } -} diff --git a/pndp/responder.go b/pndp/responder.go index ad4e971..ecf995d 100644 --- a/pndp/responder.go +++ b/pndp/responder.go @@ -9,62 +9,107 @@ import ( ) func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQuestionChan chan *ndpQuestion, filter []*net.IPNet, autoSense string, stopWG *sync.WaitGroup, stopChan chan struct{}) { - var ndpQuestionsList = make([]*ndpQuestion, 0, 100) + var ndpQuestionsList = make([]*ndpQuestion, 0, 40) stopWG.Add(1) defer stopWG.Done() fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_RAW, syscall.IPPROTO_RAW) if err != nil { panic(err) } - defer syscall.Close(fd) + defer func(fd int) { + _ = syscall.Close(fd) + }(fd) err = syscall.BindToDevice(fd, iface) if err != nil { panic(err) } - niface, err := net.InterfaceByName(iface) + nIface, err := net.InterfaceByName(iface) if err != nil { panic(err.Error()) } var result = emptyIpv6 - ifaceaddrs, err := niface.Addrs() + ifaceaddrs, err := nIface.Addrs() for _, n := range ifaceaddrs { tip, _, err := net.ParseCIDR(n.String()) if err != nil { break } + var haveUla = false if isIpv6(tip.String()) { if tip.IsGlobalUnicast() { + haveUla = true result = tip _, tnet, _ := net.ParseCIDR("fc00::/7") if !tnet.Contains(tip) { break } + } else if tip.IsLinkLocalUnicast() && !haveUla { + result = tip } } } for { var n *ndpRequest - if ndpQuestionChan == nil && respondType == ndp_ADV { + if (ndpQuestionChan == nil && respondType == ndp_ADV) || (ndpQuestionChan != nil && respondType == ndp_SOL) { select { case <-stopChan: return case n = <-requests: } } else { + // THis is if ndpQuestionChan != nil && respondType == ndp_ADV select { case <-stopChan: return case q := <-ndpQuestionChan: ndpQuestionsList = append(ndpQuestionsList, q) + ndpQuestionsList = cleanupQuestionList(ndpQuestionsList) continue case n = <-requests: } } + var _, LinkLocalSpace, _ = net.ParseCIDR("fe80::/10") + if LinkLocalSpace.Contains(n.answeringForIP) { + if GlobalDebug { + fmt.Println("Dropping packet asking for a link-local IP") + } + continue + } + + if n.requestType == ndp_ADV { + if (n.rawPacket[78] != 0x02) || (n.rawPacket[79] != 0x01) { + if GlobalDebug { + fmt.Println("Dropping Advertisement packet without target Source address set") + } + continue + } + if n.rawPacket[58] == 0x0 { + if GlobalDebug { + fmt.Println("Dropping Advertisement packet without any NDP flags set") + } + continue + } + } else { + if (n.rawPacket[78] != 0x01) || (n.rawPacket[79] != 0x01) { + if GlobalDebug { + fmt.Println("Dropping Solicitation packet without Source address set") + } + continue + } + } + + if !checkPacketChecksum(n.srcIP, n.dstIP, n.rawPacket[54:]) { + if GlobalDebug { + fmt.Println("Dropping packet because of invalid checksum") + } + continue + } + if autoSense != "" { autoiface, err := net.InterfaceByName(autoSense) if err != nil { @@ -72,8 +117,8 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu } autoifaceaddrs, err := autoiface.Addrs() - for _, n := range autoifaceaddrs { - _, anet, err := net.ParseCIDR(n.String()) + for _, l := range autoifaceaddrs { + _, anet, err := net.ParseCIDR(l.String()) if err != nil { break } @@ -104,7 +149,7 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu } if n.sourceIface == iface { - pkt(fd, result, n.srcIP, n.answeringForIP, niface.HardwareAddr, respondType) + pkt(fd, result, n.srcIP, n.answeringForIP, nIface.HardwareAddr, respondType) } else { if respondType == ndp_ADV { success := false @@ -121,7 +166,7 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu askedBy: n.srcIP, } } - pkt(fd, result, n.dstIP, n.answeringForIP, niface.HardwareAddr, respondType) + pkt(fd, result, n.dstIP, n.answeringForIP, nIface.HardwareAddr, respondType) } } } @@ -177,7 +222,7 @@ forloop: } func getAddressFromQuestionList(targetIP []byte, ndpQuestionsList []*ndpQuestion) ([]byte, bool) { - for i, _ := range ndpQuestionsList { + for i := range ndpQuestionsList { if bytes.Equal((*ndpQuestionsList[i]).targetIP, targetIP) { result := (*ndpQuestionsList[i]).askedBy ndpQuestionsList = removeFromQuestionList(ndpQuestionsList, i) @@ -190,3 +235,10 @@ func removeFromQuestionList(s []*ndpQuestion, i int) []*ndpQuestion { s[i] = s[len(s)-1] return s[:len(s)-1] } + +func cleanupQuestionList(s []*ndpQuestion) []*ndpQuestion { + for len(s) >= 40 { + s = removeFromQuestionList(s, 0) + } + return s +} diff --git a/pndpd.service b/pndpd.service new file mode 100644 index 0000000..4dab6b2 --- /dev/null +++ b/pndpd.service @@ -0,0 +1,18 @@ +[Unit] +Description=Proxy NDP Daemon +Wants=network-online.target +After=network.target network-online.target + +[Service] +Type=simple +Restart=on-failure +RestartSec=5s +ExecStart=/usr/bin/pndpd config /etc/pndpd/pndpd.conf + +DynamicUser=yes +AmbientCapabilities=CAP_NET_RAW +CapabilityBoundingSet= +ProtectHome=yes + +[Install] +WantedBy=multi-user.target \ No newline at end of file