Compare commits

..

No commits in common. "burble.dn42" and "1.0" have entirely different histories.

19 changed files with 340 additions and 703 deletions

View File

@ -1,55 +0,0 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: build
image: golang
environment:
CGO_ENABLED: 0
commands:
- go vet
- go build
- name: upload binary
image: plugins/s3
settings:
bucket: artifacts
access_key:
from_secret: MINIO_ACCESS_KEY
secret_key:
from_secret: MINIO_SECRET_KEY
endpoint: https://minio.burble.dn42
region: uk-lon3
path_style: true
source: pndpd
target: /pndpd/${DRONE_BRANCH}
- name: upload service
image: plugins/s3
settings:
bucket: artifacts
access_key:
from_secret: MINIO_ACCESS_KEY
secret_key:
from_secret: MINIO_SECRET_KEY
endpoint: https://minio.burble.dn42
region: uk-lon3
path_style: true
source: pndpd.service
target: /pndpd/${DRONE_BRANCH}
---
kind: secret
name: MINIO_ACCESS_KEY
get:
path: burble.dn42/kv/data/drone/minio
name: ACCESS_KEY
---
kind: secret
name: MINIO_SECRET_KEY
get:
path: burble.dn42/kv/data/drone/minio
name: SECRET_KEY

5
.gitignore vendored
View File

@ -1,7 +1,2 @@
*.iml
.idea
bin/
*~
pndpd

View File

@ -1,21 +0,0 @@
# Makefile for PNDPD
BINARY=pndpd
MODULES=
VERSION=`git describe --tags`
LDFLAGS=-ldflags "-X main.Version=${VERSION}"
build:
go build -tags=${MODULES} -o bin/${BINARY} .
release:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags=${MODULES} ${LDFLAGS} -o bin/${BINARY}_${VERSION}_linux_amd64.bin .
release-all:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags=${MODULES} ${LDFLAGS} -o bin/${BINARY}_${VERSION}_linux_amd64.bin
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags=${MODULES} ${LDFLAGS} -o bin/${BINARY}_${VERSION}_linux_arm64.bin
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -tags=${MODULES} ${LDFLAGS} -o bin/${BINARY}_${VERSION}_linux_arm.bin
clean:
find bin/ -type f -delete
if [ -d "bin/" ]; then rm -d bin/ ;fi

View File

@ -1,50 +1,38 @@
# PNDPD - NDP Responder + Proxy
[![Build Status](https://ci.burble.dn42/api/badges/mirrors/Pndpd/status.svg?ref=refs/heads/burble.dn42)](https://ci.burble.dn42/mirrors/Pndpd)
## Features
- **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 NDP solicitations for whitelisted addresses on an interface
- **Proxy** NDP between interfaces with an optional whitelist
- Optionally determine whitelist **automatically** based on the IPs assigned to the interfaces
- Respond to NDP solicitations for whitelisted addresses on an interface
- Proxy NDP between interfaces with an optional whitelist
- Optionally determine whitelist automatically based on the IPs assigned to the interfaces
- Permissions required: root or CAP_NET_RAW
- Easily expandable with modules
## Installing & Updating
1) Download the latest release from the [releases page](https://github.com/Kioubit/pndpd/releases) and move the binary to the ``/usr/local/bin/`` directory under the filename ``pndpd``.
2) Allow executing the file by running ``chmod +x /usr/local/bin/pndpd``
3) **For systemd users:** Install the service unit file
1) Download the latest release from the releases page and move the binary to the ``/urs/bin/``
2) For systemd users: Install the service
````
wget https://raw.githubusercontent.com/Kioubit/pndpd/master/pndpd.service -P /etc/systemd/system/
wget https://git.dn42.dev/Kioubit/Pndpd/src/branch/master/pndpd.service
mv pndpd.service /usr/lib/systemd/system/
systemctl enable pndpd.service
````
4) Download and install the config file
3) Download and install the config file
````
mkdir -p /etc/pndpd
wget https://raw.githubusercontent.com/Kioubit/pndpd/master/pndpd.conf -P /etc/pndpd/
wget https://git.dn42.dev/Kioubit/Pndpd/src/branch/master/pndpd.conf
mkdir -p /etc/pndpd/
mv pndpd.conf /etc/pndpd/
````
5) Edit the config at ``/etc/pndpd/pndpd.conf`` and then start the service using ``service pndpd start``
4) Edit the config at ``/etc/pndpd/pndpd.conf`` and then start the service using ``service pndpd start``
## Manual Usage
````
pndpd config <path to file>
pndpd responder <interface> <optional whitelist of CIDRs separated by a semicolon>
pndpd respond <interface> <optional whitelist of CIDRs separated by a semicolon>
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``).
More options and additional documentation in the example config file (pndpd.conf).
## Developing
### Building
For building, the version of go needs to be installed that is specified in the go.mod file. A makefile is available. Optionally adjust the ``MODULES`` variable to include or exclude modules from the modules directory.
````
make clean
make release-all
````
Find the binaries in the ``bin/`` directory
### Adding Modules
It is easy to add functionality to PNDPD. For additions outside the core functionality you only need to keep the following methods in mind:
````

137
config.go
View File

@ -3,17 +3,35 @@ package main
import (
"bufio"
"fmt"
"log"
"os"
"pndpd/modules"
"pndpd/pndp"
"strings"
)
type configResponder struct {
Iface string
Filter string
autosense string
instance *pndp.ResponderObj
}
type configProxy struct {
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 {
fmt.Println("Error:", err.Error())
os.Exit(1)
log.Fatal(err)
}
defer func(file *os.File) {
_ = file.Close()
@ -34,40 +52,107 @@ func readConfig(dest string) {
continue
}
if strings.HasSuffix(line, "{") {
if strings.HasPrefix(line, "responder") && strings.Contains(line, "{") {
obj := configResponder{}
filter := ""
for {
scanner.Scan()
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 = strings.TrimSuffix(filter, ";")
break
}
}
allResponders = append(allResponders, &obj)
} else if strings.HasPrefix(line, "proxy") && strings.Contains(line, "{") {
obj := configProxy{}
filter := ""
for {
scanner.Scan()
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
}
}
allProxies = append(allProxies, &obj)
} else if strings.Contains(line, "{") {
option := strings.TrimSuffix(strings.TrimSpace(line), "{")
option = strings.TrimSpace(option)
module, command := modules.GetCommand(option, modules.Config)
var lines = make([]string, 0)
if module != nil {
if modules.ModuleList != nil {
for i := range modules.ModuleList {
if (*modules.ModuleList[i]).Option == option {
var lines []string
for {
if !scanner.Scan() {
break
}
line := strings.TrimSpace(scanner.Text())
if strings.Contains(line, "}") {
break
}
scanner.Scan()
line = strings.TrimSpace(scanner.Text())
lines = append(lines, line)
}
modules.ExecuteInit(module, modules.CallbackInfo{
CallbackType: modules.Config,
Command: command,
Arguments: lines,
})
if strings.HasPrefix(line, "}") {
break
}
}
(*modules.ModuleList[i]).ConfigCallback(lines)
}
}
}
}
if modules.ExistsBlockingModule() {
modules.ExecuteComplete()
waitForSignal()
modules.ShutdownAll()
}
if err := scanner.Err(); err != nil {
panic(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()
}
}

82
main.go
View File

@ -5,64 +5,72 @@ import (
"os"
"os/signal"
"pndpd/modules"
"pndpd/pndp"
"syscall"
)
import (
// Modules
_ "pndpd/modules/example"
_ "pndpd/modules/userInterface"
)
var Version = "Development"
// 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", Version, "- Kioubit 2022")
fmt.Println("PNDPD Version 1.0 - Kioubit 2021")
if modules.ModuleList != nil {
fmt.Print("Loaded Modules: ")
for i := range modules.ModuleList {
fmt.Print((*modules.ModuleList[i]).Name + " ")
}
fmt.Println()
}
if len(os.Args) <= 2 {
printUsage()
return
}
switch os.Args[1] {
case "respond":
var r *pndp.ResponderObj
if len(os.Args) == 4 {
r = pndp.NewResponder(os.Args[2], pndp.ParseFilter(os.Args[3]), "")
r.Start()
} else {
r = pndp.NewResponder(os.Args[2], nil, "")
r.Start()
}
WaitForSignal()
r.Stop()
case "proxy":
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:
module, command := modules.GetCommand(os.Args[1], modules.CommandLine)
if module != nil {
modules.ExecuteInit(module, modules.CallbackInfo{
CallbackType: modules.CommandLine,
Command: command,
Arguments: os.Args[2:],
})
if modules.ExistsBlockingModule() {
modules.ExecuteComplete()
waitForSignal()
modules.ShutdownAll()
for i := range modules.ModuleList {
if (*modules.ModuleList[i]).Option == os.Args[1] {
(*modules.ModuleList[i]).CommandLineCallback(os.Args)
return
}
}
} else {
printUsage()
}
return
}
}
func printUsage() {
fmt.Println("More options and additional documentation in the example config file")
fmt.Println("Usage:")
fmt.Println("pndpd config <path to file>")
for i := range modules.ModuleList {
for d := range (*modules.ModuleList[i]).Commands {
if (*modules.ModuleList[i]).Commands[d].CommandLineEnabled {
fmt.Println((*modules.ModuleList[i]).Commands[d].Description)
}
}
}
}
// waitForSignal Waits (blocking) for the program to be interrupted by the OS
func waitForSignal() {
var sigCh = make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
<-sigCh
close(sigCh)
fmt.Println("pndpd respond <interface> <optional whitelist of CIDRs separated by a semicolon>")
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

@ -1,6 +1,3 @@
//go:build mod_example
// +build mod_example
package example
import (
@ -8,45 +5,21 @@ import (
"pndpd/modules"
)
// This is an example module
// This is an example module that is not imported by the main program
func init() {
commands := []modules.Command{{
CommandText: "pndpd command1",
Description: "This is the usage description for command1",
BlockTerminate: true,
CommandLineEnabled: true,
ConfigEnabled: true,
}, {
CommandText: "pndpd command2",
Description: "This is the usage description for command2",
BlockTerminate: false,
CommandLineEnabled: false,
ConfigEnabled: true,
},
modules.RegisterModule("Example", "example", "example <parameter 1> <parameter 2>", commandLineRead, configRead)
}
func configRead(s []string) {
// Prints out the contents of the config file that are relevant for this module (that are inside the example{} option)
for _, n := range s {
fmt.Println(n)
}
modules.RegisterModule("Example", commands, initCallback, completeCallback, shutdownCallback)
}
func initCallback(callback modules.CallbackInfo) {
if callback.CallbackType == modules.CommandLine {
// The command registered by the module has been run in the commandline
// "arguments" contains the os.Args[] passed to the program after the command registered by this module
fmt.Println("Command: ", callback.Command.CommandText)
fmt.Println(callback.Arguments)
} else {
// The command registered by the module was found in the config file
// "arguments" contains the lines between the curly braces
fmt.Println("Command: ", callback.Command.CommandText)
fmt.Println(callback.Arguments)
func commandLineRead(s []string) {
// Prints out the command line options given to the program if the command starts with "example"
for _, n := range s {
fmt.Println(n)
}
fmt.Println()
}
func completeCallback() {
//Called after the program has passed all options by calls to initCallback()
}
func shutdownCallback() {
fmt.Println("Terminate all work")
}

View File

@ -1 +0,0 @@
package example

View File

@ -4,90 +4,18 @@ var ModuleList []*Module
type Module struct {
Name string
Commands []Command
InitCallback func(CallbackInfo)
CompleteCallback func()
ShutdownCallback func()
Option string
OptionDescription string
CommandLineCallback func([]string)
ConfigCallback func([]string)
}
type Command struct {
CommandText string
Description string
BlockTerminate bool
CommandLineEnabled bool
ConfigEnabled bool
}
type CallbackType int
const (
CommandLine CallbackType = 0
Config CallbackType = 1
)
type CallbackInfo struct {
CallbackType CallbackType
Command Command
Arguments []string
}
func RegisterModule(name string, commands []Command, initCallback func(CallbackInfo), CompleteCallback func(), shutdownCallback func()) {
func RegisterModule(name string, option string, description string, commandLineCallback func([]string), configCallback func([]string)) {
ModuleList = append(ModuleList, &Module{
Name: name,
Commands: commands,
InitCallback: initCallback,
CompleteCallback: CompleteCallback,
ShutdownCallback: shutdownCallback,
Option: option,
OptionDescription: description,
CommandLineCallback: commandLineCallback,
ConfigCallback: configCallback,
})
}
func GetCommand(target string, scope CallbackType) (*Module, Command) {
for i := range ModuleList {
for _, command := range ModuleList[i].Commands {
if command.CommandText == target {
if scope == CommandLine && command.CommandLineEnabled {
return ModuleList[i], command
}
if scope == Config && command.ConfigEnabled {
return ModuleList[i], command
}
return nil, Command{}
}
}
}
return nil, Command{}
}
var runningModules []*Module
func ExecuteInit(module *Module, info CallbackInfo) {
if info.Command.BlockTerminate {
found := false
for _, n := range runningModules {
if n == module {
found = true
break
}
}
if !found {
runningModules = append(runningModules, module)
}
}
module.InitCallback(info)
}
func ExecuteComplete() {
for i := range runningModules {
(*runningModules[i]).CompleteCallback()
}
}
func ShutdownAll() {
for i := range runningModules {
(*runningModules[i]).ShutdownCallback()
}
}
func ExistsBlockingModule() bool {
return len(runningModules) != 0
}

View File

@ -1 +0,0 @@
package userInterface

View File

@ -1,195 +0,0 @@
//go:build !noUserInterface
// +build !noUserInterface
package userInterface
import (
"fmt"
"os"
"pndpd/modules"
"pndpd/pndp"
"strings"
)
func init() {
commands := []modules.Command{{
CommandText: "proxy",
Description: "pndpd proxy <interface1> <interface2> <optional whitelist of CIDRs separated by a semicolon applied to interface2>",
BlockTerminate: true,
ConfigEnabled: true,
CommandLineEnabled: true,
}, {
CommandText: "responder",
Description: "pndpd responder <interface> <optional whitelist of CIDRs separated by a semicolon>",
BlockTerminate: true,
ConfigEnabled: true,
CommandLineEnabled: true,
}, {
CommandText: "modules",
Description: "pndpd modules available - list available modules",
BlockTerminate: false,
ConfigEnabled: false,
CommandLineEnabled: true,
}}
modules.RegisterModule("Core", commands, initCallback, completeCallback, shutdownCallback)
}
type configResponder struct {
Iface string
Filter string
autosense string
instance *pndp.ResponderObj
}
type configProxy struct {
Iface1 string
Iface2 string
Filter string
autosense string
instance *pndp.ProxyObj
}
var allResponders []*configResponder
var allProxies []*configProxy
func initCallback(callback modules.CallbackInfo) {
if callback.CallbackType == modules.CommandLine {
switch callback.Command.CommandText {
case "proxy":
if len(callback.Arguments) == 3 {
allProxies = append(allProxies, &configProxy{
Iface1: callback.Arguments[0],
Iface2: callback.Arguments[1],
Filter: callback.Arguments[2],
autosense: "",
instance: nil,
})
} else {
allProxies = append(allProxies, &configProxy{
Iface1: callback.Arguments[0],
Iface2: callback.Arguments[1],
Filter: "",
autosense: "",
instance: nil,
})
}
case "responder":
if len(callback.Arguments) == 2 {
allResponders = append(allResponders, &configResponder{
Iface: callback.Arguments[0],
Filter: callback.Arguments[1],
autosense: "",
instance: nil,
})
} else {
allResponders = append(allResponders, &configResponder{
Iface: callback.Arguments[0],
Filter: "",
autosense: "",
instance: nil,
})
}
case "modules":
if modules.ModuleList != nil {
fmt.Print("Available Modules: ")
for i := range modules.ModuleList {
fmt.Print((*modules.ModuleList[i]).Name + " ")
}
fmt.Println()
}
}
} else {
switch callback.Command.CommandText {
case "proxy":
obj := configProxy{}
filter := ""
for _, n := range callback.Arguments {
if strings.HasPrefix(n, "iface1") {
obj.Iface1 = strings.TrimSpace(strings.TrimPrefix(n, "iface1"))
}
if strings.HasPrefix(n, "iface2") {
obj.Iface2 = strings.TrimSpace(strings.TrimPrefix(n, "iface2"))
}
if strings.HasPrefix(n, "filter") {
filter += strings.TrimSpace(strings.TrimPrefix(n, "filter")) + ";"
if strings.Contains(n, ";") {
showError("config: the use of semicolons is not allowed in the filter arguments")
}
}
if strings.HasPrefix(n, "autosense") {
obj.autosense = strings.TrimSpace(strings.TrimPrefix(n, "autosense"))
}
if strings.Contains(n, "//") {
showError("config: comments are not allowed after arguments")
}
}
obj.Filter = strings.TrimSuffix(filter, ";")
if obj.autosense != "" && obj.Filter != "" {
showError("config: cannot have both a filter and autosense enabled on a proxy object")
}
if obj.Iface2 == "" || obj.Iface1 == "" {
showError("config: two interfaces need to be specified in the config file for a proxy object. (iface1 and iface2 parameters)")
}
allProxies = append(allProxies, &obj)
case "responder":
obj := configResponder{}
filter := ""
for _, n := range callback.Arguments {
if strings.HasPrefix(n, "iface") {
obj.Iface = strings.TrimSpace(strings.TrimPrefix(n, "iface"))
}
if strings.HasPrefix(n, "filter") {
filter += strings.TrimSpace(strings.TrimPrefix(n, "filter")) + ";"
if strings.Contains(n, ";") {
showError("config: the use of semicolons is not allowed in the filter arguments")
}
}
if strings.HasPrefix(n, "autosense") {
obj.autosense = strings.TrimSpace(strings.TrimPrefix(n, "autosense"))
}
if obj.autosense != "" && obj.Filter != "" {
showError("config: cannot have both a filter and autosense enabled on a responder object")
}
if obj.Iface == "" {
showError("config: interface not specified in the responder object. (iface parameter)")
}
if strings.Contains(n, "//") {
showError("config: comments are not allowed after arguments")
}
}
obj.Filter = strings.TrimSuffix(filter, ";")
allResponders = append(allResponders, &obj)
}
}
}
func completeCallback() {
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()
}
}
func shutdownCallback() {
for _, n := range allProxies {
n.instance.Stop()
}
for _, n := range allResponders {
n.instance.Stop()
}
}
func showError(error string) {
fmt.Println(error)
fmt.Println("Exiting due to error")
os.Exit(1)
}

View File

@ -12,8 +12,10 @@ type ndpRequest struct {
srcIP []byte
answeringForIP []byte
dstIP []byte
mac []byte
receivedIfaceMac []byte
sourceIface string
payload []byte
rawPacket []byte
}
type ndpQuestion struct {

View File

@ -1,96 +0,0 @@
package pndp
import (
"golang.org/x/net/bpf"
"golang.org/x/sys/unix"
"net"
"syscall"
"unsafe"
)
// bpfFilter represents a classic BPF filter program that can be applied to a socket
type bpfFilter []bpf.Instruction
// ApplyTo applies the current filter onto the provided file descriptor
func (filter bpfFilter) ApplyTo(fd int) (err error) {
var assembled []bpf.RawInstruction
if assembled, err = bpf.Assemble(filter); err != nil {
return err
}
var program = unix.SockFprog{
Len: uint16(len(assembled)),
Filter: (*unix.SockFilter)(unsafe.Pointer(&assembled[0])),
}
var b = (*[unix.SizeofSockFprog]byte)(unsafe.Pointer(&program))[:unix.SizeofSockFprog]
if _, _, errno := syscall.Syscall6(syscall.SYS_SETSOCKOPT,
uintptr(fd), uintptr(syscall.SOL_SOCKET), uintptr(syscall.SO_ATTACH_FILTER),
uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)), 0); errno != 0 {
return errno
}
return nil
}
type iflags struct {
name [syscall.IFNAMSIZ]byte
flags uint16
}
func setPromisc(fd int, iface string, enable bool, withInterfaceFlags bool) {
//TODO re-test ALLMULTI
// -------------------------- Interface flags --------------------------
if withInterfaceFlags {
tFD, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_DGRAM, 0)
if err != nil {
panic(err)
}
var ifl iflags
copy(ifl.name[:], []byte(iface))
_, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(tFD), syscall.SIOCGIFFLAGS, uintptr(unsafe.Pointer(&ifl)))
if ep != 0 {
panic(ep)
}
if enable {
ifl.flags |= uint16(syscall.IFF_PROMISC)
} else {
ifl.flags &^= uint16(syscall.IFF_PROMISC)
}
_, _, ep = syscall.Syscall(syscall.SYS_IOCTL, uintptr(tFD), syscall.SIOCSIFFLAGS, uintptr(unsafe.Pointer(&ifl)))
if ep != 0 {
panic(ep)
}
_ = syscall.Close(tFD)
}
// ---------------------------------------------------------------------
// -------------------------- Socket Options ---------------------------
iFace, err := net.InterfaceByName(iface)
if err != nil {
panic(err.Error())
}
mReq := unix.PacketMreq{
Ifindex: int32(iFace.Index),
Type: unix.PACKET_MR_PROMISC,
}
var opt int
if enable {
opt = unix.PACKET_ADD_MEMBERSHIP
} else {
opt = unix.PACKET_DROP_MEMBERSHIP
}
err = unix.SetsockoptPacketMreq(fd, unix.SOL_PACKET, opt, &mReq)
if err != nil {
panic(err)
}
// ---------------------------------------------------------------------
}

View File

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"net"
"strings"
)
var emptyIpv6 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
@ -135,7 +134,7 @@ func checksumAddition(b []byte) uint32 {
for i := 0; i < len(b); i++ {
if i%2 == 0 {
if len(b)-1 == i {
sum += uint32(uint16(b[i])<<8 | uint16(0x00))
sum += uint32(uint16(b[i])<<8 | uint16(0x0))
} else {
sum += uint32(uint16(b[i])<<8 | uint16(b[i+1]))
}
@ -144,7 +143,12 @@ func checksumAddition(b []byte) uint32 {
return sum
}
func checkPacketChecksum(v6 *ipv6Header, payload []byte) bool {
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])
@ -158,6 +162,9 @@ func checkPacketChecksum(v6 *ipv6Header, payload []byte) bool {
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 {
@ -169,5 +176,8 @@ func checkPacketChecksum(v6 *ipv6Header, payload []byte) bool {
func isIpv6(ip string) bool {
rip := net.ParseIP(ip)
return rip != nil && strings.Contains(ip, ":")
if rip.To16() == nil {
return false
}
return true
}

View File

@ -35,9 +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 {
if filter == nil && autosenseInterface == "" {
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{}),
@ -59,8 +57,7 @@ func (obj *ResponderObj) start() {
}()
go respond(obj.iface, requests, ndp_ADV, nil, obj.filter, obj.autosense, obj.stopWG, obj.stopChan)
go listen(obj.iface, requests, ndp_SOL, obj.stopWG, obj.stopChan)
fmt.Printf("Started responder instance on interface %s", obj.iface)
fmt.Println()
fmt.Println("Started responder instance on interface", obj.iface)
<-obj.stopChan
}
@ -131,8 +128,7 @@ func (obj *ProxyObj) start() {
go listen(obj.iface2, req_iface2_adv_iface1, ndp_ADV, obj.stopWG, obj.stopChan)
go respond(obj.iface1, req_iface2_adv_iface1, ndp_ADV, out_iface2_sol_questions_iface1_adv, nil, "", obj.stopWG, obj.stopChan)
fmt.Printf("Started Proxy instance on interfaces %s and %s (if enabled, the whitelist is applied on %s)", obj.iface1, obj.iface2, obj.iface2)
fmt.Println()
fmt.Println("Started Proxy instance for interfaces:", obj.iface1, "and", obj.iface2)
<-obj.stopChan
}
@ -150,7 +146,7 @@ func (obj *ProxyObj) Stop() bool {
}
}
// ParseFilter Helper Function to Parse a string of CIDRs separated by a semicolon as a Whitelist
// 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

View File

@ -4,11 +4,38 @@ import (
"bytes"
"fmt"
"golang.org/x/net/bpf"
"golang.org/x/sys/unix"
"net"
"sync"
"syscall"
"unsafe"
)
// bpfFilter represents a classic BPF filter program that can be applied to a socket
type bpfFilter []bpf.Instruction
// ApplyTo applies the current filter onto the provided file descriptor
func (filter bpfFilter) ApplyTo(fd int) (err error) {
var assembled []bpf.RawInstruction
if assembled, err = bpf.Assemble(filter); err != nil {
return err
}
var program = unix.SockFprog{
Len: uint16(len(assembled)),
Filter: (*unix.SockFilter)(unsafe.Pointer(&assembled[0])),
}
var b = (*[unix.SizeofSockFprog]byte)(unsafe.Pointer(&program))[:unix.SizeofSockFprog]
if _, _, errno := syscall.Syscall6(syscall.SYS_SETSOCKOPT,
uintptr(fd), uintptr(syscall.SOL_SOCKET), uintptr(syscall.SO_ATTACH_FILTER),
uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)), 0); errno != 0 {
return errno
}
return nil
}
// Htons Convert a uint16 to host byte order (big endian)
func htons(v uint16) int {
return int((v << 8) | (v >> 8))
@ -34,7 +61,6 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW
}
go func() {
<-stopChan
setPromisc(fd, iface, false, false)
_ = syscall.Close(fd)
stopWG.Done() // syscall.read does not release when the file descriptor is closed
}()
@ -51,8 +77,6 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW
panic(err.Error())
}
setPromisc(fd, iface, true, false)
var protocolNo uint32
if requestType == ndp_SOL {
//Neighbor Solicitation
@ -75,8 +99,8 @@ 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 86 bytes of the packet to userspace.
bpf.RetConstant{Val: 86},
// Verdict is: send up to 4096 bytes of the packet to userspace.
bpf.RetConstant{Val: 4096},
// Verdict is: "ignore packet."
bpf.RetConstant{Val: 0},
}
@ -87,12 +111,12 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW
}
for {
buf := make([]byte, 86)
buf := make([]byte, 4096)
numRead, err := syscall.Read(fd, buf)
if err != nil {
panic(err)
}
if numRead < 78 {
if numRead < 86 {
if GlobalDebug {
fmt.Println("Dropping packet since it does not meet the minimum length requirement")
fmt.Printf("% X\n", buf[:numRead])
@ -115,6 +139,8 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW
fmt.Println("NDP Flags")
fmt.Printf("% X\n", buf[58])
}
fmt.Println("NDP MAC:")
fmt.Printf("% X\n", buf[80:86])
fmt.Println()
}
@ -125,22 +151,15 @@ func listen(iface string, responder chan *ndpRequest, requestType ndpType, stopW
continue
}
if requestType == ndp_ADV {
if buf[58] == 0x0 {
if GlobalDebug {
fmt.Println("Dropping Advertisement packet without any NDP flags set")
}
continue
}
}
responder <- &ndpRequest{
requestType: requestType,
srcIP: buf[22:38],
dstIP: buf[38:54],
answeringForIP: buf[62:78],
payload: buf[54:],
mac: buf[80:86],
receivedIfaceMac: niface.HardwareAddr,
sourceIface: iface,
rawPacket: buf[:numRead],
}
}
}

View File

@ -9,12 +9,9 @@ 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, 40)
stopWG.Add(1)
defer stopWG.Done()
var ndpQuestionsList = make([]*ndpQuestion, 0, 40)
var _, linkLocalSpace, _ = net.ParseCIDR("fe80::/10")
fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
panic(err)
@ -27,23 +24,44 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu
panic(err)
}
respondIface, err := net.InterfaceByName(iface)
nIface, err := net.InterfaceByName(iface)
if err != nil {
panic(err.Error())
}
var result = selectSourceIP(respondIface)
var result = emptyIpv6
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 req *ndpRequest
var n *ndpRequest
if (ndpQuestionChan == nil && respondType == ndp_ADV) || (ndpQuestionChan != nil && respondType == ndp_SOL) {
select {
case <-stopChan:
return
case req = <-requests:
case n = <-requests:
}
} else {
// This is if ndpQuestionChan != nil && respondType == ndp_ADV
// THis is if ndpQuestionChan != nil && respondType == ndp_ADV
select {
case <-stopChan:
return
@ -51,30 +69,48 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu
ndpQuestionsList = append(ndpQuestionsList, q)
ndpQuestionsList = cleanupQuestionList(ndpQuestionsList)
continue
case req = <-requests:
case n = <-requests:
}
}
if linkLocalSpace.Contains(req.answeringForIP) {
var _, LinkLocalSpace, _ = net.ParseCIDR("fe80::/10")
if LinkLocalSpace.Contains(n.answeringForIP) {
if GlobalDebug {
fmt.Println("Dropping packet asking for a link-local IP")
}
continue
}
v6Header, err := newIpv6Header(req.srcIP, req.dstIP)
if err != nil {
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 !checkPacketChecksum(v6Header, req.payload) {
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
}
// Auto-sense
if autoSense != "" {
//TODO Future work: Use another sub goroutine to monitor the interface instead of checking here
filter = make([]*net.IPNet, 0)
result = selectSourceIP(respondIface)
autoiface, err := net.InterfaceByName(autoSense)
if err != nil {
panic(err)
@ -82,11 +118,11 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu
autoifaceaddrs, err := autoiface.Addrs()
for _, l := range autoifaceaddrs {
testIP, anet, err := net.ParseCIDR(l.String())
_, anet, err := net.ParseCIDR(l.String())
if err != nil {
break
}
if isIpv6(testIP.String()) {
if isIpv6(anet.String()) {
filter = append(filter, anet)
}
}
@ -95,9 +131,9 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu
if filter != nil {
ok := false
for _, i := range filter {
if i.Contains(req.answeringForIP) {
if i.Contains(n.answeringForIP) {
if GlobalDebug {
fmt.Println("Responded for whitelisted IP", req.answeringForIP)
fmt.Println("Responded for whitelisted IP", n.answeringForIP)
}
ok = true
break
@ -112,12 +148,12 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu
fmt.Println("Getting ready to send packet of type", respondType, "out on interface", iface)
}
if req.sourceIface == iface {
pkt(fd, result, req.srcIP, req.answeringForIP, respondIface.HardwareAddr, respondType)
if n.sourceIface == iface {
pkt(fd, result, n.srcIP, n.answeringForIP, nIface.HardwareAddr, respondType)
} else {
if respondType == ndp_ADV {
success := false
req.dstIP, success = getAddressFromQuestionListRetry(req.answeringForIP, ndpQuestionChan, ndpQuestionsList)
n.dstIP, success = getAddressFromQuestionListRetry(n.answeringForIP, ndpQuestionChan, ndpQuestionsList)
if !success {
if GlobalDebug {
fmt.Println("Nobody has asked for this IP")
@ -126,11 +162,11 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu
}
} else {
ndpQuestionChan <- &ndpQuestion{
targetIP: req.answeringForIP,
askedBy: req.srcIP,
targetIP: n.answeringForIP,
askedBy: n.srcIP,
}
}
pkt(fd, result, req.dstIP, req.answeringForIP, respondIface.HardwareAddr, respondType)
pkt(fd, result, n.dstIP, n.answeringForIP, nIface.HardwareAddr, respondType)
}
}
}
@ -171,20 +207,18 @@ func getAddressFromQuestionListRetry(targetIP []byte, ndpQuestionChan chan *ndpQ
if success {
return result, true
}
gotBuffered := false
forloop:
for {
select {
case q := <-ndpQuestionChan:
ndpQuestionsList = append(ndpQuestionsList, q)
gotBuffered = true
default:
break forloop
}
}
if gotBuffered {
result, success = getAddressFromQuestionList(targetIP, ndpQuestionsList)
}
return nil, false
return result, success
}
func getAddressFromQuestionList(targetIP []byte, ndpQuestionsList []*ndpQuestion) ([]byte, bool) {
@ -208,33 +242,3 @@ func cleanupQuestionList(s []*ndpQuestion) []*ndpQuestion {
}
return s
}
func selectSourceIP(iface *net.Interface) []byte {
var _, ulaSpace, _ = net.ParseCIDR("fc00::/7")
var result = emptyIpv6
ifaceaddrs, err := iface.Addrs()
if err != nil {
return result
}
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
if !ulaSpace.Contains(tip) {
break
}
} else if tip.IsLinkLocalUnicast() && !haveUla {
result = tip
}
}
}
return result
}

View File

@ -5,7 +5,6 @@
debug off
// Responder example
// Create an NDP responder that answers on interface "eth0"
responder {
iface eth0
filter fd01::/64
@ -13,7 +12,6 @@ responder {
}
// Proxy example
// Create an NDP proxy for proxying NDP between iface1 ("eth0") and iface2 ("eth1")
// The whitelist is applied on iface2
proxy {
iface1 eth0

View File

@ -7,11 +7,11 @@ After=network.target network-online.target
Type=simple
Restart=on-failure
RestartSec=5s
ExecStart=/usr/local/bin/pndpd config /etc/pndpd/pndpd.conf
ExecStart=/usr/bin/pndpd config /etc/pndpd/pndpd.conf
DynamicUser=yes
AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN
CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN
AmbientCapabilities=CAP_NET_RAW
CapabilityBoundingSet=
ProtectHome=yes
[Install]