Add autosense, add config, overhaul code

This commit is contained in:
Kioubit 2021-12-24 11:19:36 -05:00
parent 04c3526c8b
commit 282205f743
7 changed files with 326 additions and 138 deletions

View File

@ -3,15 +3,17 @@
- Efficiently process incoming packets using bpf (which runs in the kernel) - Efficiently process incoming packets using bpf (which runs in the kernel)
- Respond to all NDP solicitations on an interface - Respond to all NDP solicitations on an interface
- Respond to NDP solicitations for whitelisted addresses 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 - Permissions required: root or CAP_NET_RAW
## Usage ## Usage
```` ````
pndpd readconfig <path to file> pndpd config <path to file>")
pndpd respond <interface> <optional whitelist of CIDRs separated with a semicolon> pndpd respond <interface> <optional whitelist of CIDRs separated by a semicolon>")
pndpd proxy <interface1> <interface2> pndpd proxy <interface1> <interface2> <optional whitelist of CIDRs separated by a semicolon applied to interface2>")
```` ````
More options and additional documentation in the example config file (pndpd.conf).
### Developing ### 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: 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 package main
import "pndpd/pndp" import "pndpd/pndp"
pndp.SimpleRespond(iface string, filter []*net.IPNet)
pndp.ParseFilter(f string) []*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. Pull request are welcome for any functionality you add.

View File

@ -2,6 +2,7 @@ package main
import ( import (
"bufio" "bufio"
"fmt"
"log" "log"
"os" "os"
"pndpd/pndp" "pndpd/pndp"
@ -11,13 +12,21 @@ import (
type configResponder struct { type configResponder struct {
Iface string Iface string
Filter string Filter string
autosense string
instance *pndp.ResponderObj
} }
type configProxy struct { type configProxy struct {
Iface1 string Iface1 string
Iface2 string Iface2 string
Filter string
autosense string
instance *pndp.ProxyObj
} }
var allResponders []*configResponder
var allProxies []*configProxy
func readConfig(dest string) { func readConfig(dest string) {
file, err := os.Open(dest) file, err := os.Open(dest)
if err != nil { if err != nil {
@ -33,50 +42,95 @@ func readConfig(dest string) {
continue continue
} }
if strings.HasPrefix(line, "debug") { if strings.HasPrefix(line, "debug") {
if strings.Contains(line, "off") { if strings.Contains(line, "on") {
pndp.GlobalDebug = false pndp.GlobalDebug = true
fmt.Println("DEBUG ON")
} }
continue continue
} }
if strings.HasPrefix(line, "responder") { if strings.HasPrefix(line, "responder") && strings.Contains(line, "{") {
obj := configResponder{} obj := configResponder{}
filter := "" filter := ""
for { for {
scanner.Scan() scanner.Scan()
line = scanner.Text() line = strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "iface") { if strings.HasPrefix(line, "iface") {
obj.Iface = strings.TrimSpace(strings.TrimPrefix(line, "iface")) obj.Iface = strings.TrimSpace(strings.TrimPrefix(line, "iface"))
} }
if strings.HasPrefix(line, "filter") { if strings.HasPrefix(line, "filter") {
filter += strings.TrimSpace(strings.TrimPrefix(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, "}") { if strings.HasPrefix(line, "}") {
obj.Filter = filter obj.Filter = strings.TrimSuffix(filter, ";")
break 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{} obj := configProxy{}
filter := ""
for { for {
scanner.Scan() scanner.Scan()
line = scanner.Text() line = strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "iface1") { if strings.HasPrefix(line, "iface1") {
obj.Iface1 = strings.TrimSpace(strings.TrimPrefix(line, "iface1")) obj.Iface1 = strings.TrimSpace(strings.TrimPrefix(line, "iface1"))
} }
if strings.HasPrefix(line, "iface2") { if strings.HasPrefix(line, "iface2") {
obj.Iface2 = strings.TrimSpace(strings.TrimPrefix(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, "}") { if strings.HasPrefix(line, "}") {
break break
} }
} }
pndp.Proxy(obj.Iface1, obj.Iface2) allProxies = append(allProxies, &obj)
} }
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
log.Fatal(err) 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()
}
} }

45
main.go
View File

@ -3,38 +3,61 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/signal"
"pndpd/pndp" "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() { func main() {
fmt.Println("PNDPD Version 0.4 by Kioubit") fmt.Println("PNDPD Version 0.5 - Kioubit 2021")
if len(os.Args) <= 2 { if len(os.Args) <= 2 {
printUsage() printUsage()
return return
} }
switch os.Args[1] { switch os.Args[1] {
case "respond": case "respond":
var r *pndp.ResponderObj
if len(os.Args) == 4 { 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 { } 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": case "proxy":
pndp.Proxy(os.Args[2], os.Args[3]) var p *pndp.ProxyObj
case "readconfig": 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]) readConfig(os.Args[2])
default: default:
printUsage() printUsage()
return return
} }
pndp.WaitForSignal()
} }
func printUsage() { func printUsage() {
fmt.Println("Specify command") fmt.Println("Usage:")
fmt.Println("Usage: pndpd readconfig <path to file>") fmt.Println("pndpd config <path to file>")
fmt.Println("Usage: pndpd respond <interface> <optional whitelist of CIDRs separated with a semicolon>") fmt.Println("pndpd respond <interface> <optional whitelist of CIDRs separated by a semicolon>")
fmt.Println("Usage: pndpd proxy <interface1> <interface2>") fmt.Println("pndpd proxy <interface1> <interface2> <optional whitelist of CIDRs separated by a semicolon applied to interface2>")
fmt.Println("More options and additional documentation in the example config file")
} }

View File

@ -3,43 +3,157 @@ package pndp
import ( import (
"fmt" "fmt"
"net" "net"
"os"
"os/signal"
"runtime/pprof"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
) )
var GlobalDebug = false var GlobalDebug = false
// Items needed for graceful shutdown type ResponderObj struct {
var stop = make(chan struct{}) stopChan chan struct{}
var stopWg sync.WaitGroup stopWG *sync.WaitGroup
var sigCh = make(chan os.Signal) iface string
filter []*net.IPNet
// WaitForSignal Waits (blocking) for the program to be interrupted by the OS and then gracefully shuts down releasing all resources autosense string
func WaitForSignal() { }
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) type ProxyObj struct {
<-sigCh stopChan chan struct{}
Shutdown() 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 // iface - The interface to listen to and respond from
func Shutdown() { //
fmt.Println("Shutting down...") // filter - Optional (can be nil) list of CIDRs to whitelist. Must be IPV6! ParseFilter verifies ipv6
close(stop) //
if wgWaitTimout(&stopWg, 10*time.Second) { // 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.
fmt.Println("Done") // Start() must be called on the object to actually start responding
} else { func NewResponder(iface string, filter []*net.IPNet, autosenseInterface string) *ResponderObj {
fmt.Println("Aborting shutdown, since it is taking too long") var s sync.WaitGroup
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) 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 { func wgWaitTimout(wg *sync.WaitGroup, timeout time.Duration) bool {
@ -55,70 +169,3 @@ func wgWaitTimout(wg *sync.WaitGroup, timeout time.Duration) bool {
return false 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
}

View File

@ -5,6 +5,7 @@ import (
"golang.org/x/net/bpf" "golang.org/x/net/bpf"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"net" "net"
"sync"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@ -40,7 +41,9 @@ func htons(v uint16) int {
} }
func htons16(v uint16) uint16 { return v<<8 | v>>8 } 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) niface, err := net.InterfaceByName(iface)
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
@ -55,9 +58,9 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType) {
fmt.Println(err.Error()) fmt.Println(err.Error())
} }
go func() { go func() {
<-stop <-stopChan
syscall.Close(fd) 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) fmt.Println("Obtained fd ", fd)

View File

@ -4,19 +4,18 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net"
"sync"
"syscall" "syscall"
) )
var globalFd int func respond(iface string, requests chan *ndpRequest, respondType ndpType, filter []*net.IPNet, autoSense string, stopWG *sync.WaitGroup, stopChan chan struct{}) {
stopWG.Add(1)
func respond(iface string, requests chan *ndpRequest, respondType ndpType, filter []*net.IPNet) { defer stopWG.Done()
defer stopWg.Done()
fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_RAW, syscall.IPPROTO_RAW) fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer syscall.Close(globalFd) defer syscall.Close(fd)
globalFd = fd
err = syscall.BindToDevice(fd, iface) err = syscall.BindToDevice(fd, iface)
if err != nil { if err != nil {
panic(err) panic(err)
@ -49,16 +48,36 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, filte
for { for {
var n *ndpRequest var n *ndpRequest
select { select {
case <-stop: case <-stopChan:
return return
case n = <-requests: 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 { if filter != nil {
ok := false ok := false
for _, i := range filter { for _, i := range filter {
if i.Contains(n.answeringForIP) { if i.Contains(n.answeringForIP) {
fmt.Println("filter allowed IP", n.answeringForIP) if GlobalDebug {
fmt.Println("Responded for whitelisted IP", n.answeringForIP)
}
ok = true ok = true
break break
} }
@ -69,16 +88,16 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, filte
} }
if n.sourceIface == iface { if n.sourceIface == iface {
pkt(result, n.srcIP, n.answeringForIP, niface.HardwareAddr, respondType) pkt(fd, result, n.srcIP, n.answeringForIP, niface.HardwareAddr, respondType)
} else { } else {
if !bytes.Equal(n.mac, n.receivedIfaceMac) { 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) v6, err := newIpv6Header(ownIP, dstIP)
if err != nil { if err != nil {
return 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.Println("Sending packet of type", respondType, "to")
fmt.Printf("% X\n", t) fmt.Printf("% X\n", t)
} }
err = syscall.Sendto(globalFd, response, 0, &d) err = syscall.Sendto(fd, response, 0, &d)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
} }

37
pndpd.conf Normal file
View File

@ -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
}