Compare commits
35 Commits
0.7
...
burble.dn4
Author | SHA1 | Date | |
---|---|---|---|
e5b04565ec | |||
58ac05adc9 | |||
b3d2305f9d | |||
|
dff2f9ab10 | ||
|
49c6c333e9 | ||
|
a7eb52c0c5 | ||
|
ede44e8426 | ||
|
d8c439aad9 | ||
|
48486cc205 | ||
|
1f294666f3 | ||
|
95bf95784e | ||
|
1da76a4547 | ||
|
b7c5e6afba | ||
|
1ec9477d69 | ||
|
2e62c0b5d5 | ||
|
ff27eb5141 | ||
|
4ddaa98351 | ||
|
6ebee06861 | ||
|
0f580cbbbd | ||
|
5a901eada3 | ||
|
504daad9e1 | ||
|
11110fb66b | ||
|
e3d463e815 | ||
|
45b9ac5e86 | ||
|
de4d228950 | ||
|
d196469c01 | ||
|
f8f0d0d055 | ||
|
d6421a007e | ||
|
6948f5a30d | ||
|
3491a47811 | ||
|
d3a5059aa2 | ||
|
ce659c31d8 | ||
|
bef2b5d3ef | ||
|
8562c2f533 | ||
|
7d831f2f0b |
55
.drone.yml
Normal file
55
.drone.yml
Normal file
@ -0,0 +1,55 @@
|
||||
---
|
||||
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
5
.gitignore
vendored
@ -1,2 +1,7 @@
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
bin/
|
||||
|
||||
*~
|
||||
pndpd
|
||||
|
21
Makefile
Normal file
21
Makefile
Normal file
@ -0,0 +1,21 @@
|
||||
# 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
|
54
README.md
54
README.md
@ -1,21 +1,51 @@
|
||||
# PNDPD - NDP Responder + Proxy
|
||||
## Features
|
||||
- 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
|
||||
- Permissions required: root or CAP_NET_RAW
|
||||
|
||||
## Usage
|
||||
[](https://ci.burble.dn42/mirrors/Pndpd)
|
||||
|
||||
## Features
|
||||
- **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
|
||||
- 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
|
||||
````
|
||||
wget https://raw.githubusercontent.com/Kioubit/pndpd/master/pndpd.service -P /etc/systemd/system/
|
||||
systemctl enable pndpd.service
|
||||
````
|
||||
4) Download and install the config file
|
||||
````
|
||||
mkdir -p /etc/pndpd
|
||||
wget https://raw.githubusercontent.com/Kioubit/pndpd/master/pndpd.conf -P /etc/pndpd/
|
||||
````
|
||||
5) 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 respond <interface> <optional whitelist of CIDRs separated by a semicolon>
|
||||
pndpd responder <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
|
||||
## 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:
|
||||
````
|
||||
package main
|
||||
@ -31,4 +61,6 @@ proxyInstance := pndp.NewProxy(iface1 string, iface2 string, filter []*net.IPNet
|
||||
proxyInstance.Start()
|
||||
proxyInstance.Stop()
|
||||
````
|
||||
New functionality should be implemented as a module. You will find an example module under ``modules/example/``.
|
||||
|
||||
Pull requests are welcome for any functionality you add.
|
||||
|
129
config.go
129
config.go
@ -3,42 +3,27 @@ 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 {
|
||||
log.Fatal(err)
|
||||
fmt.Println("Error:", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
defer func(file *os.File) {
|
||||
_ = file.Close()
|
||||
}(file)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "//") {
|
||||
if strings.HasPrefix(line, "//") || strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "debug") {
|
||||
@ -48,89 +33,41 @@ func readConfig(dest string) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "responder") && strings.Contains(line, "{") {
|
||||
obj := configResponder{}
|
||||
filter := ""
|
||||
|
||||
if strings.HasSuffix(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 {
|
||||
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, ";")
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.Contains(line, "}") {
|
||||
break
|
||||
}
|
||||
|
||||
lines = append(lines, line)
|
||||
}
|
||||
modules.ExecuteInit(module, modules.CallbackInfo{
|
||||
CallbackType: modules.Config,
|
||||
Command: command,
|
||||
Arguments: lines,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
allResponders = append(allResponders, &obj)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if modules.ExistsBlockingModule() {
|
||||
modules.ExecuteComplete()
|
||||
waitForSignal()
|
||||
modules.ShutdownAll()
|
||||
}
|
||||
|
||||
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()
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
73
main.go
73
main.go
@ -4,60 +4,65 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"pndpd/pndp"
|
||||
"pndpd/modules"
|
||||
"syscall"
|
||||
)
|
||||
import (
|
||||
// Modules
|
||||
_ "pndpd/modules/example"
|
||||
_ "pndpd/modules/userInterface"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
var Version = "Development"
|
||||
|
||||
func main() {
|
||||
fmt.Println("PNDPD Version 0.7 - Kioubit 2021")
|
||||
fmt.Println("PNDPD Version", Version, "- Kioubit 2022")
|
||||
|
||||
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, "")
|
||||
fmt.Println("WARNING: You should use a whitelist unless you know what you are doing")
|
||||
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()
|
||||
}
|
||||
} 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>")
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
52
modules/example/example.go
Normal file
52
modules/example/example.go
Normal file
@ -0,0 +1,52 @@
|
||||
//go:build mod_example
|
||||
// +build mod_example
|
||||
|
||||
package example
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"pndpd/modules"
|
||||
)
|
||||
|
||||
// This is an example module
|
||||
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", 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)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func completeCallback() {
|
||||
//Called after the program has passed all options by calls to initCallback()
|
||||
}
|
||||
|
||||
func shutdownCallback() {
|
||||
fmt.Println("Terminate all work")
|
||||
}
|
1
modules/example/module.go
Normal file
1
modules/example/module.go
Normal file
@ -0,0 +1 @@
|
||||
package example
|
93
modules/modules.go
Normal file
93
modules/modules.go
Normal file
@ -0,0 +1,93 @@
|
||||
package modules
|
||||
|
||||
var ModuleList []*Module
|
||||
|
||||
type Module struct {
|
||||
Name string
|
||||
Commands []Command
|
||||
InitCallback func(CallbackInfo)
|
||||
CompleteCallback func()
|
||||
ShutdownCallback func()
|
||||
}
|
||||
|
||||
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()) {
|
||||
ModuleList = append(ModuleList, &Module{
|
||||
Name: name,
|
||||
Commands: commands,
|
||||
InitCallback: initCallback,
|
||||
CompleteCallback: CompleteCallback,
|
||||
ShutdownCallback: shutdownCallback,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
1
modules/userInterface/module.go
Normal file
1
modules/userInterface/module.go
Normal file
@ -0,0 +1 @@
|
||||
package userInterface
|
195
modules/userInterface/userInterface.go
Normal file
195
modules/userInterface/userInterface.go
Normal file
@ -0,0 +1,195 @@
|
||||
//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)
|
||||
}
|
@ -12,9 +12,8 @@ type ndpRequest struct {
|
||||
srcIP []byte
|
||||
answeringForIP []byte
|
||||
dstIP []byte
|
||||
mac []byte
|
||||
receivedIfaceMac []byte
|
||||
sourceIface string
|
||||
payload []byte
|
||||
}
|
||||
|
||||
type ndpQuestion struct {
|
||||
|
@ -35,6 +35,9 @@ 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{}),
|
||||
@ -56,12 +59,13 @@ 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.Println("Started responder instance on interface ", obj.iface)
|
||||
fmt.Printf("Started responder instance on interface %s", obj.iface)
|
||||
fmt.Println()
|
||||
<-obj.stopChan
|
||||
}
|
||||
|
||||
//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..")
|
||||
@ -127,7 +131,8 @@ 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.Println("Started Proxy instance for interfaces: ", obj.iface1, " and ", obj.iface2)
|
||||
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()
|
||||
<-obj.stopChan
|
||||
}
|
||||
|
||||
@ -145,7 +150,7 @@ func (obj *ProxyObj) Stop() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// ParseFilter Helper Function to Parse a string of CIDRs separated by a semicolon as a Whitelist for SimpleRespond
|
||||
// ParseFilter Helper Function to Parse a string of CIDRs separated by a semicolon as a Whitelist
|
||||
func ParseFilter(f string) []*net.IPNet {
|
||||
if f == "" {
|
||||
return nil
|
96
pndp/interface.go
Normal file
96
pndp/interface.go
Normal file
@ -0,0 +1,96 @@
|
||||
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)
|
||||
}
|
||||
// ---------------------------------------------------------------------
|
||||
}
|
146
pndp/listener.go
Normal file
146
pndp/listener.go
Normal file
@ -0,0 +1,146 @@
|
||||
package pndp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"golang.org/x/net/bpf"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Htons Convert a uint16 to host byte order (big endian)
|
||||
func htons(v uint16) int {
|
||||
return int((v << 8) | (v >> 8))
|
||||
}
|
||||
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())
|
||||
}
|
||||
tiface := &syscall.SockaddrLinklayer{
|
||||
Protocol: htons16(syscall.ETH_P_IPV6),
|
||||
Ifindex: niface.Index,
|
||||
}
|
||||
|
||||
fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, htons(syscall.ETH_P_IPV6))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
go func() {
|
||||
<-stopChan
|
||||
setPromisc(fd, iface, false, false)
|
||||
_ = syscall.Close(fd)
|
||||
stopWG.Done() // syscall.read does not release when the file descriptor is closed
|
||||
}()
|
||||
if GlobalDebug {
|
||||
fmt.Println("Obtained fd ", fd)
|
||||
}
|
||||
|
||||
if len([]byte(iface)) > syscall.IFNAMSIZ {
|
||||
panic("Interface size larger then maximum allowed by the kernel")
|
||||
}
|
||||
|
||||
err = syscall.Bind(fd, tiface)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
setPromisc(fd, iface, true, false)
|
||||
|
||||
var protocolNo uint32
|
||||
if requestType == ndp_SOL {
|
||||
//Neighbor Solicitation
|
||||
protocolNo = 0x87
|
||||
} else {
|
||||
//Neighbor Advertisement
|
||||
protocolNo = 0x88
|
||||
}
|
||||
var f bpfFilter
|
||||
f = []bpf.Instruction{
|
||||
// Load "EtherType" field from the ethernet header.
|
||||
bpf.LoadAbsolute{Off: 12, Size: 2},
|
||||
// Jump to the drop packet instruction if EtherType is not IPv6.
|
||||
bpf.JumpIf{Cond: bpf.JumpNotEqual, Val: 0x86dd, SkipTrue: 5},
|
||||
// Load "Next Header" field from IPV6 header.
|
||||
bpf.LoadAbsolute{Off: 20, Size: 1},
|
||||
// Jump to the drop packet instruction if Next Header is not ICMPv6.
|
||||
bpf.JumpIf{Cond: bpf.JumpNotEqual, Val: 0x3a, SkipTrue: 3},
|
||||
// Load "Type" field from ICMPv6 header.
|
||||
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: "ignore packet."
|
||||
bpf.RetConstant{Val: 0},
|
||||
}
|
||||
|
||||
err = f.ApplyTo(fd)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
for {
|
||||
buf := make([]byte, 86)
|
||||
numRead, err := syscall.Read(fd, buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if numRead < 78 {
|
||||
if GlobalDebug {
|
||||
fmt.Println("Dropping packet since it does not meet the minimum length requirement")
|
||||
fmt.Printf("% X\n", buf[:numRead])
|
||||
}
|
||||
continue
|
||||
}
|
||||
if GlobalDebug {
|
||||
fmt.Println("Got packet on", iface, "of type", requestType)
|
||||
fmt.Printf("% X\n", buf[:numRead])
|
||||
|
||||
fmt.Println("Source mac on ethernet layer:")
|
||||
fmt.Printf("% X\n", buf[6:12])
|
||||
fmt.Println("Source IP:")
|
||||
fmt.Printf("% X\n", buf[22:38])
|
||||
fmt.Println("Destination IP:")
|
||||
fmt.Printf("% X\n", buf[38:54])
|
||||
fmt.Println("Requested IP:")
|
||||
fmt.Printf("% X\n", buf[62:78])
|
||||
if requestType == ndp_ADV {
|
||||
fmt.Println("NDP Flags")
|
||||
fmt.Printf("% X\n", buf[58])
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if bytes.Equal(buf[6:12], niface.HardwareAddr) {
|
||||
if GlobalDebug {
|
||||
fmt.Println("Dropping packet from ourselves")
|
||||
}
|
||||
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:],
|
||||
sourceIface: iface,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
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 +116,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,8 +134,8 @@ func checksumAddition(b []byte) uint32 {
|
||||
var sum uint32 = 0
|
||||
for i := 0; i < len(b); i++ {
|
||||
if i%2 == 0 {
|
||||
if len(b) == i {
|
||||
sum += uint32(uint16(b[i])<<8 | uint16(0x0))
|
||||
if len(b)-1 == i {
|
||||
sum += uint32(uint16(b[i])<<8 | uint16(0x00))
|
||||
} else {
|
||||
sum += uint32(uint16(b[i])<<8 | uint16(b[i+1]))
|
||||
}
|
||||
@ -138,6 +144,29 @@ func checksumAddition(b []byte) uint32 {
|
||||
return sum
|
||||
}
|
||||
|
||||
func checkPacketChecksum(v6 *ipv6Header, payload []byte) bool {
|
||||
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) {
|
||||
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, ":")
|
||||
|
@ -1,199 +0,0 @@
|
||||
package pndp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"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))
|
||||
}
|
||||
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())
|
||||
}
|
||||
tiface := &syscall.SockaddrLinklayer{
|
||||
Protocol: htons16(syscall.ETH_P_IPV6),
|
||||
Ifindex: niface.Index,
|
||||
}
|
||||
|
||||
fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, htons(syscall.ETH_P_IPV6))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
go func() {
|
||||
<-stopChan
|
||||
syscall.Close(fd)
|
||||
stopWG.Done() // syscall.read does not release when the file descriptor is closed
|
||||
}()
|
||||
if GlobalDebug {
|
||||
fmt.Println("Obtained fd ", fd)
|
||||
}
|
||||
|
||||
if len([]byte(iface)) > syscall.IFNAMSIZ {
|
||||
panic("Interface size larger then maximum allowed by the kernel")
|
||||
}
|
||||
|
||||
err = syscall.Bind(fd, tiface)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
var protocolNo uint32
|
||||
if requestType == ndp_SOL {
|
||||
//Neighbor Solicitation
|
||||
protocolNo = 0x87
|
||||
} else {
|
||||
//Neighbor Advertisement
|
||||
protocolNo = 0x88
|
||||
}
|
||||
|
||||
var f bpfFilter = []bpf.Instruction{
|
||||
// Load "EtherType" field from the ethernet header.
|
||||
bpf.LoadAbsolute{Off: 12, Size: 2},
|
||||
// Jump to the drop packet instruction if EtherType is not IPv6.
|
||||
bpf.JumpIf{Cond: bpf.JumpNotEqual, Val: 0x86dd, SkipTrue: 4},
|
||||
// Load "Next Header" field from IPV6 header.
|
||||
bpf.LoadAbsolute{Off: 20, Size: 1},
|
||||
// Jump to the drop packet instruction if Next Header is not ICMPv6.
|
||||
bpf.JumpIf{Cond: bpf.JumpNotEqual, Val: 0x3a, SkipTrue: 2},
|
||||
// Load "Type" field from ICMPv6 header.
|
||||
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
|
||||
bpf.RetConstant{Val: 4096},
|
||||
// Verdict is "ignore packet."
|
||||
bpf.RetConstant{Val: 0},
|
||||
}
|
||||
|
||||
err = f.ApplyTo(fd)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
for {
|
||||
buf := make([]byte, 4096)
|
||||
numRead, err := syscall.Read(fd, buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if numRead < 86 {
|
||||
if GlobalDebug {
|
||||
|
||||
fmt.Println("Dropping packet since it does not meet the minimum length requirement")
|
||||
fmt.Printf("% X\n", buf[:numRead])
|
||||
}
|
||||
continue
|
||||
}
|
||||
if GlobalDebug {
|
||||
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 IP:")
|
||||
fmt.Printf("% X\n", buf[:numRead][22:38])
|
||||
fmt.Println("Destination IP:")
|
||||
fmt.Printf("% X\n", buf[:numRead][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.Println()
|
||||
}
|
||||
|
||||
if bytes.Equal(buf[:numRead][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],
|
||||
receivedIfaceMac: niface.HardwareAddr,
|
||||
sourceIface: iface,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -9,75 +9,84 @@ 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)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
respondIface, err := net.InterfaceByName(iface)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
var result = emptyIpv6
|
||||
ifaceaddrs, err := niface.Addrs()
|
||||
|
||||
for _, n := range ifaceaddrs {
|
||||
tip, _, err := net.ParseCIDR(n.String())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if isIpv6(tip.String()) {
|
||||
if tip.IsGlobalUnicast() {
|
||||
result = tip
|
||||
_, tnet, _ := net.ParseCIDR("fc00::/7")
|
||||
if !tnet.Contains(tip) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = selectSourceIP(respondIface)
|
||||
|
||||
for {
|
||||
var n *ndpRequest
|
||||
if ndpQuestionChan == nil && respondType == ndp_ADV {
|
||||
var req *ndpRequest
|
||||
if (ndpQuestionChan == nil && respondType == ndp_ADV) || (ndpQuestionChan != nil && respondType == ndp_SOL) {
|
||||
select {
|
||||
case <-stopChan:
|
||||
return
|
||||
case n = <-requests:
|
||||
case req = <-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:
|
||||
case req = <-requests:
|
||||
}
|
||||
}
|
||||
|
||||
if linkLocalSpace.Contains(req.answeringForIP) {
|
||||
if GlobalDebug {
|
||||
fmt.Println("Dropping packet asking for a link-local IP")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
v6Header, err := newIpv6Header(req.srcIP, req.dstIP)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !checkPacketChecksum(v6Header, req.payload) {
|
||||
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)
|
||||
}
|
||||
autoifaceaddrs, err := autoiface.Addrs()
|
||||
|
||||
for _, n := range autoifaceaddrs {
|
||||
_, anet, err := net.ParseCIDR(n.String())
|
||||
for _, l := range autoifaceaddrs {
|
||||
testIP, anet, err := net.ParseCIDR(l.String())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if isIpv6(anet.String()) {
|
||||
if isIpv6(testIP.String()) {
|
||||
filter = append(filter, anet)
|
||||
}
|
||||
}
|
||||
@ -86,9 +95,9 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu
|
||||
if filter != nil {
|
||||
ok := false
|
||||
for _, i := range filter {
|
||||
if i.Contains(n.answeringForIP) {
|
||||
if i.Contains(req.answeringForIP) {
|
||||
if GlobalDebug {
|
||||
fmt.Println("Responded for whitelisted IP", n.answeringForIP)
|
||||
fmt.Println("Responded for whitelisted IP", req.answeringForIP)
|
||||
}
|
||||
ok = true
|
||||
break
|
||||
@ -103,12 +112,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 n.sourceIface == iface {
|
||||
pkt(fd, result, n.srcIP, n.answeringForIP, niface.HardwareAddr, respondType)
|
||||
if req.sourceIface == iface {
|
||||
pkt(fd, result, req.srcIP, req.answeringForIP, respondIface.HardwareAddr, respondType)
|
||||
} else {
|
||||
if respondType == ndp_ADV {
|
||||
success := false
|
||||
n.dstIP, success = getAddressFromQuestionListRetry(n.answeringForIP, ndpQuestionChan, ndpQuestionsList)
|
||||
req.dstIP, success = getAddressFromQuestionListRetry(req.answeringForIP, ndpQuestionChan, ndpQuestionsList)
|
||||
if !success {
|
||||
if GlobalDebug {
|
||||
fmt.Println("Nobody has asked for this IP")
|
||||
@ -117,11 +126,11 @@ func respond(iface string, requests chan *ndpRequest, respondType ndpType, ndpQu
|
||||
}
|
||||
} else {
|
||||
ndpQuestionChan <- &ndpQuestion{
|
||||
targetIP: n.answeringForIP,
|
||||
askedBy: n.srcIP,
|
||||
targetIP: req.answeringForIP,
|
||||
askedBy: req.srcIP,
|
||||
}
|
||||
}
|
||||
pkt(fd, result, n.dstIP, n.answeringForIP, niface.HardwareAddr, respondType)
|
||||
pkt(fd, result, req.dstIP, req.answeringForIP, respondIface.HardwareAddr, respondType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,22 +171,24 @@ func getAddressFromQuestionListRetry(targetIP []byte, ndpQuestionChan chan *ndpQ
|
||||
if success {
|
||||
return result, true
|
||||
}
|
||||
forloop:
|
||||
for {
|
||||
|
||||
gotBuffered := false
|
||||
select {
|
||||
case q := <-ndpQuestionChan:
|
||||
ndpQuestionsList = append(ndpQuestionsList, q)
|
||||
gotBuffered = true
|
||||
default:
|
||||
break forloop
|
||||
}
|
||||
}
|
||||
|
||||
if gotBuffered {
|
||||
result, success = getAddressFromQuestionList(targetIP, ndpQuestionsList)
|
||||
return result, success
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
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 +201,40 @@ 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
debug off
|
||||
|
||||
// Responder example
|
||||
// Create an NDP responder that answers on interface "eth0"
|
||||
responder {
|
||||
iface eth0
|
||||
filter fd01::/64
|
||||
@ -12,6 +13,7 @@ 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
|
||||
|
18
pndpd.service
Normal file
18
pndpd.service
Normal file
@ -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/local/bin/pndpd config /etc/pndpd/pndpd.conf
|
||||
|
||||
DynamicUser=yes
|
||||
AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN
|
||||
CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN
|
||||
ProtectHome=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
Loading…
x
Reference in New Issue
Block a user