diff --git a/README.md b/README.md index d62bda4..d1ff93f 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,17 @@ - Efficiently process incoming packets using bpf (which runs in the kernel) - Respond to all NDP solicitations on an interface - Respond to NDP solicitations for whitelisted addresses on an interface -- Proxy NDP between interfaces +- Proxy NDP between interfaces with an optional whitelist for neighbor solicitations +- Optionally automatically determine whitelist based on the IPs assigned to the interfaces - Permissions required: root or CAP_NET_RAW ## Usage ```` -pndpd readconfig -pndpd respond -pndpd proxy +pndpd config ") +pndpd respond ") +pndpd proxy ") ```` +More options and additional documentation in the example config file (pndpd.conf). ### Developing It is easy to add functionality to PNDPD. For additions outside the core functionality you only need to keep the following methods in mind: @@ -19,11 +21,14 @@ It is easy to add functionality to PNDPD. For additions outside the core functio package main import "pndpd/pndp" -pndp.SimpleRespond(iface string, filter []*net.IPNet) pndp.ParseFilter(f string) []*net.IPNet -pndp.Proxy(iface1, iface2 string) +responderInstance := pndp.NewResponder(iface string, filter []*net.IPNet, autosenseInterface string) +responderInstance.Start() +responderInstance.Stop() -pndp.WaitForSignal() +proxyInstance := pndp.NewProxy(iface1 string, iface2 string, filter []*net.IPNet, autosenseInterface string) +proxyInstance.Start() +proxyInstance.Stop() ```` Pull request are welcome for any functionality you add. \ No newline at end of file diff --git a/config.go b/config.go index 139fb35..f1ceb17 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "fmt" "log" "os" "pndpd/pndp" @@ -9,15 +10,23 @@ import ( ) type configResponder struct { - Iface string - Filter string + Iface string + Filter string + autosense string + instance *pndp.ResponderObj } type configProxy struct { - Iface1 string - Iface2 string + Iface1 string + Iface2 string + Filter string + autosense string + instance *pndp.ProxyObj } +var allResponders []*configResponder +var allProxies []*configProxy + func readConfig(dest string) { file, err := os.Open(dest) if err != nil { @@ -33,50 +42,95 @@ func readConfig(dest string) { continue } if strings.HasPrefix(line, "debug") { - if strings.Contains(line, "off") { - pndp.GlobalDebug = false + if strings.Contains(line, "on") { + pndp.GlobalDebug = true + fmt.Println("DEBUG ON") } continue } - if strings.HasPrefix(line, "responder") { + if strings.HasPrefix(line, "responder") && strings.Contains(line, "{") { obj := configResponder{} filter := "" for { scanner.Scan() - line = scanner.Text() + line = strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "iface") { obj.Iface = strings.TrimSpace(strings.TrimPrefix(line, "iface")) } if strings.HasPrefix(line, "filter") { filter += strings.TrimSpace(strings.TrimPrefix(line, "filter")) + ";" + if strings.Contains(line, ";") { + panic("Invalid config file syntax") + } + } + if strings.HasPrefix(line, "autosense") { + obj.autosense = strings.TrimSpace(strings.TrimPrefix(line, "autosense")) } if strings.HasPrefix(line, "}") { - obj.Filter = filter + obj.Filter = strings.TrimSuffix(filter, ";") break } } - pndp.SimpleRespond(obj.Iface, pndp.ParseFilter(obj.Filter)) + + allResponders = append(allResponders, &obj) } - if strings.HasPrefix(line, "proxy") { + if strings.HasPrefix(line, "proxy") && strings.Contains(line, "{") { obj := configProxy{} + filter := "" for { scanner.Scan() - line = scanner.Text() + line = strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "iface1") { obj.Iface1 = strings.TrimSpace(strings.TrimPrefix(line, "iface1")) } if strings.HasPrefix(line, "iface2") { obj.Iface2 = strings.TrimSpace(strings.TrimPrefix(line, "iface2")) } + if strings.HasPrefix(line, "filter") { + filter += strings.TrimSpace(strings.TrimPrefix(line, "filter")) + ";" + if strings.Contains(line, ";") { + panic("Invalid config file syntax") + } + } + if strings.HasPrefix(line, "autosense") { + obj.autosense = strings.TrimSpace(strings.TrimPrefix(line, "autosense")) + } + if strings.HasPrefix(line, "}") { + obj.Filter = strings.TrimSuffix(filter, ";") + break + } if strings.HasPrefix(line, "}") { break } } - pndp.Proxy(obj.Iface1, obj.Iface2) + allProxies = append(allProxies, &obj) } } if err := scanner.Err(); err != nil { log.Fatal(err) } + + for _, n := range allProxies { + o := pndp.NewProxy(n.Iface1, n.Iface2, pndp.ParseFilter(n.Filter), n.autosense) + n.instance = o + o.Start() + } + + for _, n := range allResponders { + o := pndp.NewResponder(n.Iface, pndp.ParseFilter(n.Filter), n.autosense) + n.instance = o + o.Start() + } + + WaitForSignal() + + for _, n := range allProxies { + n.instance.Stop() + } + + for _, n := range allResponders { + n.instance.Stop() + } + } diff --git a/main.go b/main.go index 59c9f9f..aec0e74 100644 --- a/main.go +++ b/main.go @@ -3,38 +3,61 @@ package main import ( "fmt" "os" + "os/signal" "pndpd/pndp" + "syscall" ) +// WaitForSignal Waits (blocking) for the program to be interrupted by the OS +func WaitForSignal() { + var sigCh = make(chan os.Signal) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + <-sigCh + close(sigCh) +} + func main() { - fmt.Println("PNDPD Version 0.4 by Kioubit") + fmt.Println("PNDPD Version 0.5 - Kioubit 2021") if len(os.Args) <= 2 { printUsage() return } - switch os.Args[1] { case "respond": + var r *pndp.ResponderObj if len(os.Args) == 4 { - pndp.SimpleRespond(os.Args[2], pndp.ParseFilter(os.Args[3])) + r = pndp.NewResponder(os.Args[2], pndp.ParseFilter(os.Args[3]), "") + r.Start() } else { - pndp.SimpleRespond(os.Args[2], nil) + 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() + r.Stop() case "proxy": - pndp.Proxy(os.Args[2], os.Args[3]) - case "readconfig": + var p *pndp.ProxyObj + if len(os.Args) == 5 { + p = pndp.NewProxy(os.Args[2], os.Args[3], pndp.ParseFilter(os.Args[4]), "") + } else { + p = pndp.NewProxy(os.Args[2], os.Args[3], nil, "") + } + WaitForSignal() + p.Stop() + case "config": readConfig(os.Args[2]) default: printUsage() return } - pndp.WaitForSignal() + } func printUsage() { - fmt.Println("Specify command") - fmt.Println("Usage: pndpd readconfig ") - fmt.Println("Usage: pndpd respond ") - fmt.Println("Usage: pndpd proxy ") + fmt.Println("Usage:") + fmt.Println("pndpd config ") + fmt.Println("pndpd respond ") + fmt.Println("pndpd proxy ") + fmt.Println("More options and additional documentation in the example config file") } diff --git a/pndp/process.go b/pndp/process.go index 15aee2b..5078d64 100644 --- a/pndp/process.go +++ b/pndp/process.go @@ -3,43 +3,157 @@ package pndp import ( "fmt" "net" - "os" - "os/signal" - "runtime/pprof" "strings" "sync" - "syscall" "time" ) var GlobalDebug = false -// Items needed for graceful shutdown -var stop = make(chan struct{}) -var stopWg sync.WaitGroup -var sigCh = make(chan os.Signal) - -// WaitForSignal Waits (blocking) for the program to be interrupted by the OS and then gracefully shuts down releasing all resources -func WaitForSignal() { - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) - <-sigCh - Shutdown() +type ResponderObj struct { + stopChan chan struct{} + stopWG *sync.WaitGroup + iface string + filter []*net.IPNet + autosense string +} +type ProxyObj struct { + stopChan chan struct{} + stopWG *sync.WaitGroup + iface1 string + iface2 string + filter []*net.IPNet + autosense string } -// Shutdown Exits the program gracefully and releases all resources +// NewResponder // -//Do not use with WaitForSignal -func Shutdown() { - fmt.Println("Shutting down...") - close(stop) - if wgWaitTimout(&stopWg, 10*time.Second) { - fmt.Println("Done") - } else { - fmt.Println("Aborting shutdown, since it is taking too long") - pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) +// iface - The interface to listen to and respond from +// +// filter - Optional (can be nil) list of CIDRs to whitelist. Must be IPV6! ParseFilter verifies ipv6 +// +// 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 { + var s sync.WaitGroup + return &ResponderObj{ + stopChan: make(chan struct{}), + stopWG: &s, + iface: iface, + filter: filter, + autosense: autosenseInterface, } +} +func (obj *ResponderObj) Start() { + go obj.start() +} +func (obj *ResponderObj) start() { + obj.stopWG.Add(1) + requests := make(chan *ndpRequest, 100) + defer func() { + close(requests) + obj.stopWG.Done() + }() + go respond(obj.iface, requests, ndp_ADV, obj.filter, obj.autosense, obj.stopWG, obj.stopChan) + go listen(obj.iface, requests, ndp_SOL, obj.stopWG, obj.stopChan) + fmt.Println("Started responder instance") + <-obj.stopChan +} - os.Exit(0) +//Stop a running Responder instance +// Returns false on success +func (obj *ResponderObj) Stop() bool { + close(obj.stopChan) + fmt.Println("Shutting down responder instance..") + if wgWaitTimout(obj.stopWG, 10*time.Second) { + fmt.Println("Done") + return true + } else { + fmt.Println("Error shutting down instance") + return false + } +} + +// NewProxy Proxy NDP between interfaces iface1 and iface2 with an optional filter (whitelist) +// +// filter - Optional (can be nil) list of CIDRs to whitelist. Must be IPV6! ParseFilter verifies ipv6 +// +// 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 proxying +func NewProxy(iface1 string, iface2 string, filter []*net.IPNet, autosenseInterface string) *ProxyObj { + var s sync.WaitGroup + return &ProxyObj{ + stopChan: make(chan struct{}), + stopWG: &s, + iface1: iface1, + iface2: iface2, + filter: filter, + autosense: autosenseInterface, + } +} + +func (obj *ProxyObj) Start() { + go obj.start() +} +func (obj *ProxyObj) start() { + obj.stopWG.Add(1) + defer func() { + obj.stopWG.Done() + }() + + req_iface1_sol_iface2 := make(chan *ndpRequest, 100) + defer close(req_iface1_sol_iface2) + go listen(obj.iface1, req_iface1_sol_iface2, ndp_SOL, obj.stopWG, obj.stopChan) + go respond(obj.iface2, req_iface1_sol_iface2, ndp_SOL, obj.filter, obj.autosense, obj.stopWG, obj.stopChan) + + req_iface2_sol_iface1 := make(chan *ndpRequest, 100) + defer close(req_iface2_sol_iface1) + go listen(obj.iface2, req_iface2_sol_iface1, ndp_SOL, obj.stopWG, obj.stopChan) + go respond(obj.iface1, req_iface2_sol_iface1, ndp_SOL, nil, "", obj.stopWG, obj.stopChan) + + req_iface1_adv_iface2 := make(chan *ndpRequest, 100) + defer close(req_iface1_adv_iface2) + go listen(obj.iface1, req_iface1_adv_iface2, ndp_ADV, obj.stopWG, obj.stopChan) + go respond(obj.iface2, req_iface1_adv_iface2, ndp_ADV, nil, "", obj.stopWG, obj.stopChan) + + req_iface2_adv_iface1 := make(chan *ndpRequest, 100) + defer close(req_iface2_adv_iface1) + go listen(obj.iface2, req_iface2_adv_iface1, ndp_ADV, obj.stopWG, obj.stopChan) + go respond(obj.iface1, req_iface2_adv_iface1, ndp_ADV, nil, "", obj.stopWG, obj.stopChan) + + <-obj.stopChan +} + +//Stop a running Proxy instance +// Returns false on success +func (obj *ProxyObj) Stop() bool { + close(obj.stopChan) + fmt.Println("Shutting down proxy instance..") + if wgWaitTimout(obj.stopWG, 10*time.Second) { + fmt.Println("Done") + return true + } else { + fmt.Println("Error shutting down instance") + return false + } +} + +// ParseFilter Helper Function to Parse a string of CIDRs separated by a semicolon as a Whitelist for SimpleRespond +func ParseFilter(f string) []*net.IPNet { + if f == "" { + return nil + } + s := strings.Split(f, ";") + result := make([]*net.IPNet, len(s)) + for i, n := range s { + _, cidr, err := net.ParseCIDR(n) + if err != nil { + panic(err) + } + result[i] = cidr + } + return result } func wgWaitTimout(wg *sync.WaitGroup, timeout time.Duration) bool { @@ -55,70 +169,3 @@ func wgWaitTimout(wg *sync.WaitGroup, timeout time.Duration) bool { return false } } - -// SimpleRespond (Non blocking) -// -// iface - The interface to listen to and respond from -// -// filter - Optional (can be nil) list of CIDRs to whitelist. Must be IPV6! -// ParseFilter verifies ipv6 -func SimpleRespond(iface string, filter []*net.IPNet) { - go simpleRespond(iface, filter) -} - -func simpleRespond(iface string, filter []*net.IPNet) { - defer stopWg.Done() - stopWg.Add(3) // This function, 2x goroutines - requests := make(chan *ndpRequest, 100) - defer close(requests) - go respond(iface, requests, ndp_ADV, filter) - go listen(iface, requests, ndp_SOL) - <-stop -} - -// Proxy NDP between interfaces iface1 and iface2 -// -// Non blocking -func Proxy(iface1, iface2 string) { - go proxy(iface1, iface2) -} - -func proxy(iface1, iface2 string) { - defer stopWg.Done() - stopWg.Add(9) // This function, 8x goroutines - - req_iface1_sol_iface2 := make(chan *ndpRequest, 100) - defer close(req_iface1_sol_iface2) - go listen(iface1, req_iface1_sol_iface2, ndp_SOL) - go respond(iface2, req_iface1_sol_iface2, ndp_SOL, nil) - - req_iface2_sol_iface1 := make(chan *ndpRequest, 100) - defer close(req_iface2_sol_iface1) - go listen(iface2, req_iface2_sol_iface1, ndp_SOL) - go respond(iface1, req_iface2_sol_iface1, ndp_SOL, nil) - - req_iface1_adv_iface2 := make(chan *ndpRequest, 100) - defer close(req_iface1_adv_iface2) - go listen(iface1, req_iface1_adv_iface2, ndp_ADV) - go respond(iface2, req_iface1_adv_iface2, ndp_ADV, nil) - - req_iface2_adv_iface1 := make(chan *ndpRequest, 100) - defer close(req_iface2_adv_iface1) - go listen(iface2, req_iface2_adv_iface1, ndp_ADV) - go respond(iface1, req_iface2_adv_iface1, ndp_ADV, nil) - <-stop -} - -// ParseFilter Helper Function to Parse a string of CIDRs separated by a semicolon as a Whitelist for SimpleRespond -func ParseFilter(f string) []*net.IPNet { - s := strings.Split(f, ";") - result := make([]*net.IPNet, len(s)) - for i, n := range s { - _, cidr, err := net.ParseCIDR(n) - if err != nil { - panic(err) - } - result[i] = cidr - } - return result -} diff --git a/pndp/rawsocket.go b/pndp/rawsocket.go index 3c1166a..47dca89 100644 --- a/pndp/rawsocket.go +++ b/pndp/rawsocket.go @@ -5,6 +5,7 @@ import ( "golang.org/x/net/bpf" "golang.org/x/sys/unix" "net" + "sync" "syscall" "unsafe" ) @@ -40,7 +41,9 @@ func htons(v uint16) int { } func htons16(v uint16) uint16 { return v<<8 | v>>8 } -func listen(iface string, responder chan *ndpRequest, requestType ndpType) { +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()) @@ -55,9 +58,9 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType) { fmt.Println(err.Error()) } go func() { - <-stop + <-stopChan syscall.Close(fd) - stopWg.Done() // syscall.read does not release when the file descriptor is closed + stopWG.Done() // syscall.read does not release when the file descriptor is closed }() fmt.Println("Obtained fd ", fd) diff --git a/pndp/responder.go b/pndp/responder.go index 5810e07..ce45e50 100644 --- a/pndp/responder.go +++ b/pndp/responder.go @@ -4,19 +4,18 @@ import ( "bytes" "fmt" "net" + "sync" "syscall" ) -var globalFd int - -func respond(iface string, requests chan *ndpRequest, respondType ndpType, filter []*net.IPNet) { - defer stopWg.Done() +func respond(iface string, requests chan *ndpRequest, respondType ndpType, filter []*net.IPNet, autoSense string, stopWG *sync.WaitGroup, stopChan chan struct{}) { + 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(globalFd) - globalFd = fd + defer syscall.Close(fd) err = syscall.BindToDevice(fd, iface) if err != nil { panic(err) @@ -49,16 +48,36 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, filte for { var n *ndpRequest select { - case <-stop: + case <-stopChan: return case n = <-requests: } + if autoSense != "" { + autoiface, err := net.InterfaceByName(autoSense) + if err != nil { + panic(err) + } + autoifaceaddrs, err := autoiface.Addrs() + + for _, n := range autoifaceaddrs { + _, anet, err := net.ParseCIDR(n.String()) + if err != nil { + break + } + if isIpv6(anet.String()) { + filter = append(filter, anet) + } + } + } + if filter != nil { ok := false for _, i := range filter { if i.Contains(n.answeringForIP) { - fmt.Println("filter allowed IP", n.answeringForIP) + if GlobalDebug { + fmt.Println("Responded for whitelisted IP", n.answeringForIP) + } ok = true break } @@ -69,16 +88,16 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, filte } if n.sourceIface == iface { - pkt(result, n.srcIP, n.answeringForIP, niface.HardwareAddr, respondType) + pkt(fd, result, n.srcIP, n.answeringForIP, niface.HardwareAddr, respondType) } else { if !bytes.Equal(n.mac, n.receivedIfaceMac) { - pkt(n.srcIP, n.dstIP, n.answeringForIP, niface.HardwareAddr, respondType) + pkt(fd, n.srcIP, n.dstIP, n.answeringForIP, niface.HardwareAddr, respondType) } } } } -func pkt(ownIP []byte, dstIP []byte, tgtip []byte, mac []byte, respondType ndpType) { +func pkt(fd int, ownIP []byte, dstIP []byte, tgtip []byte, mac []byte, respondType ndpType) { v6, err := newIpv6Header(ownIP, dstIP) if err != nil { return @@ -101,7 +120,7 @@ func pkt(ownIP []byte, dstIP []byte, tgtip []byte, mac []byte, respondType ndpTy fmt.Println("Sending packet of type", respondType, "to") fmt.Printf("% X\n", t) } - err = syscall.Sendto(globalFd, response, 0, &d) + err = syscall.Sendto(fd, response, 0, &d) if err != nil { fmt.Println(err.Error()) } diff --git a/pndpd.conf b/pndpd.conf new file mode 100644 index 0000000..c2f5cb6 --- /dev/null +++ b/pndpd.conf @@ -0,0 +1,37 @@ +// Example config file for PNDPD + +// Enable or disable debug +// If enabled, this option can fill up your logfiles very quickly +debug off + +// Responder example +responder { + iface eth0 + filter fd01::/64 + filter fd02::/64 +} + +// Proxy example +// The whitelist is applied on iface2 +proxy { + iface1 eth0 + iface2 eth1 + filter fd01::/64 + filter fd02::/64 +} + + +// Responder example with autoconfigured whitelist +// The whitelist is configured based on the addresses assigned to the interface specified. This works even if the IP addresses change frequently. +responder { + iface eth0 + autosense eth0 +} + +// Proxy example with autoconfigured whitelist +// The whitelist is configured based on the addresses assigned to the interface specified. This works even if the IP addresses change frequently. +proxy { + iface1 eth0 + iface2 eth1 + autosense eth1 +}