Compare commits

12 Commits

Author SHA1 Message Date
79aa8136c5 add openwrt-wireguard example
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-18 09:39:21 -06:00
344d874c02 minor formatting changes 2024-04-18 09:39:05 -06:00
1ffadf5c86 BREAKING: Interface name is now specified by configuration file
All checks were successful
continuous-integration/drone/push Build is passing
The syntax for the hypd server command has changed.  Now instead of specifying an interface name as an argument to the server command, you instead specify a configuration file path.

Example:
./hypd server hypdconfig.json
2024-04-17 19:41:24 -06:00
e95b4972da add scaffolding for configuration file 2024-04-17 19:12:01 -06:00
a0d118b987 Ensure generated code is checked in
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-16 20:09:54 -06:00
e9aefaf8d6 README add/edit 2024-04-16 20:09:37 -06:00
beed9726e3 remove unreferenced macros...
These were previously used while trying to parse out specific headers.  They are no longer required though because the current length bounds checks covers edge cases.
2024-04-16 20:09:01 -06:00
e85b644e82 Add maxjitter flag to hyp client
All checks were successful
continuous-integration/drone/push Build is passing
This to allow configurable time between knock sequence transmissions.  It's important the sequence arrive in the correct order, and some networks have multiple paths.
2024-04-16 19:44:25 -06:00
2c43affac9 fix typo in help message 2024-04-16 19:43:39 -06:00
fbf1758ccb added generated go code from ebpg-go
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-14 21:03:22 -06:00
ffb4b7681f Merge branch 'main' of https://deadbeef.codes/steven/hyp
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-14 21:01:03 -06:00
7f2e3c0ed9 Added pre-compiled ebpf programs 2024-04-14 21:00:31 -06:00
17 changed files with 476 additions and 58 deletions

2
.gitignore vendored
View File

@ -2,4 +2,4 @@ hyp.secret
*.exe
hypd/hypd
hyp/hyp
hypd/server/hyp_bpf_bpfe*
hypd/hypdconfig.json

View File

@ -25,14 +25,13 @@ Most port-knocking implementations are susceptible to replay attacks, a network
hyp supports a clock skew of up to 30 seconds between client and server.
### TBD: Protection Against Sweeping Attacks
### Protection Against Sweeping Attacks
~~hyp protects against sweeping attacks where an adversary modulates over the entire port range multiple times by ensuring the authentic knock sequence is strict and ordered correctly. If the first port is guessed, but the next pack arrives and is the incorrect second port in the sequence, the progress gets reset.~~
hyp protects against sweeping attacks where an adversary modulates over the entire port range multiple times by ensuring the authentic knock sequence is strict and ordered correctly. If the first port is guessed, but the next pack arrives and is the incorrect second port in the sequence, the progress gets reset. In addition to this, the correct authentic knock sequence must be entered within 5 seconds of the start of the sequence.
### Known Weaknesses
* Lossy networks can result in the knock sequence failing
* Networks with latency > 500ms can result in the knock sequence failing if packets arrive out of order
### References

View File

@ -38,6 +38,14 @@ Example usage:
panic(fmt.Errorf("failed to parse command flag 'secret': %w", err))
}
maxJitter, err := cmd.Flags().GetInt("maxjitter")
if err != nil {
panic(fmt.Errorf("failed to parse command flag 'maxjitter': %w", err))
}
if maxJitter < 1 || maxJitter > 1500 {
panic(fmt.Errorf("maxjitter must be value between 1 and 1500"))
}
secretBytes, err := os.ReadFile(secretFilePath)
if err != nil {
log.Fatalf("failed to read file 'hyp.secret': %v", err)
@ -50,12 +58,12 @@ Example usage:
}
// Transmit
fmt.Println("Transmitting knock sequence:", ports)
for _, port := range ports {
fmt.Printf("knock | %s:%d\n", args[0], port)
conn, _ := net.Dial("udp", fmt.Sprintf("%s:%d", args[0], port))
conn.Write([]byte{0})
conn.Close()
time.Sleep(time.Millisecond * 200) // TBD: Make this configurable with flag (maxJitter)
time.Sleep(time.Millisecond * time.Duration(maxJitter)) // TBD: Make this configurable with flag (maxJitter)
}
},
}
@ -64,4 +72,5 @@ func init() {
rootCmd.AddCommand(knockCmd)
knockCmd.PersistentFlags().String("secret", "hyp.secret", "Path to the file containing the hyp secret.")
knockCmd.PersistentFlags().Int("maxjitter", 200, "Specifies the time in milliseconds between knock sequence transmissions.")
}

34
hypd/cmd/defaultconfig.go Normal file
View File

@ -0,0 +1,34 @@
/*
Copyright © 2024 Steven Polley <himself@stevenpolley.net>
*/
package cmd
import (
"encoding/json"
"fmt"
"deadbeef.codes/steven/hyp/hypd/configuration"
"github.com/spf13/cobra"
)
// defaultconfigCmd represents the defaultconfig command
var defaultconfigCmd = &cobra.Command{
Use: "defaultconfig",
Short: "Prints the default configuration to stdout",
Long: `The default configuration is used if one is not set. The default configuration
can be used as a reference to build your own.
hypd generate defaultconfig | tee hypdconfig.json`,
Run: func(cmd *cobra.Command, args []string) {
config := configuration.DefaultConfig()
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
panic(fmt.Errorf("failed to marshal default configuration to json (this should never happen): %v", err))
}
fmt.Println(string(b))
},
}
func init() {
generateCmd.AddCommand(defaultconfigCmd)
}

View File

@ -19,7 +19,7 @@ server and to clients.
Example:
hypd generatesecret > hyp.secret`,
hypd generate secret > hyp.secret`,
Run: func(cmd *cobra.Command, args []string) {
sharedSecret, err := otphyp.GenerateSecret()
if err != nil {
@ -31,14 +31,4 @@ hypd generatesecret > hyp.secret`,
func init() {
generateCmd.AddCommand(secretCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// secretCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// secretCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -5,57 +5,57 @@ package cmd
import (
"fmt"
"os/user"
"deadbeef.codes/steven/hyp/hypd/configuration"
"deadbeef.codes/steven/hyp/hypd/server"
"github.com/spf13/cobra"
)
// serverCmd represents the server command
var serverCmd = &cobra.Command{
Use: "server <NIC>",
Use: "server <configFilePath>",
Args: cobra.ExactArgs(1),
Short: "Runs hyp in server mode",
Long: `Runs the hyp server and begins capture on the NIC specified
Long: `Runs the hyp server and begins watching for authentic knock sequences.
Before running this command, you must first have a configuration file. You can
generate a configuration file with: hypd generate defaultconfig > hypdconfig.json
You should then edit the config file to meet your needs.
In addition to a config file you will need to generate pre-shared keys:
mkdir -p ./secrets
hypd generate secret > secrets/mykey.secret
Example Usage:
# Linux - capture enp0s0
hyp server enp0s0
# Linux - capture eth0
hyp server eth0
# Windows - get-netadapter | where {$_.Name -eq “Ethernet”} | Select-Object -Property DeviceName
hyp.exe server "\\Device\\NPF_{A6F067DE-C2DC-4B4E-9C74-BE649C4C0F03}"
# Use config file in local directory
hypd server hypdconfig.json
# Use config file in /etc/hyp/
hypd server /etc/hyp/hypdconfig.json
`,
Run: func(cmd *cobra.Command, args []string) {
err := server.PacketServer(args[0])
currentUser, err := user.Current()
if err != nil {
panic(fmt.Errorf("could not determine current user: %w", err))
}
if currentUser.Username != "root" {
fmt.Println("WARNING: It's recommended you run this as root, but will proceed anyways...")
}
hypdConfiguration, err := configuration.LoadConfiguration(args[0])
if err != nil {
panic(fmt.Errorf("failed to start packet server: %w", err))
}
err = server.PacketServer(hypdConfiguration)
if err != nil {
panic(fmt.Errorf("failed to start packet server: %w", err))
}
},
}
func init() {
rootCmd.AddCommand(serverCmd)
/*
viper.SetConfigName("hypconfig")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/hyp/")
viper.AddConfigPath("$HOME/.hyp")
viper.AddConfigPath(".")
viper.SetDefault("RefreshInterval", 7200)
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found
// TBD: Implement
} else {
// Config file was found, but another error was produced
panic(fmt.Errorf("failed reading existing config file: %w", err))
}
}*/
}

View File

@ -0,0 +1,68 @@
package configuration
import (
"encoding/json"
"fmt"
"os"
)
type HypdConfiguration struct {
NetworkInterface string `json:"networkInterface"`
PreSharedKeyDirectory string `json:"preSharedKeyDirectory"` // hypd will load all *.secret files from this directory
SuccessAction string `json:"successAction"` // The action to take
TimeoutSeconds int `json:"timeoutSeconds"` // If > 0, once a knock sequence has been successful this value will count down and when it reaches 0, it will perform the TimeoutAction on the client.
TimeoutAction string `json:"timeoutAction"` // The action to take after TimeoutSeconds has elapsed. only applicable if TimeoutSeconds is > 0
}
// LoadConfiguration opens and parses the configuration file into a HypdConfiguration struct
// If a configFilePath is not specified, it will search in common locations
func LoadConfiguration(configFilePath string) (*HypdConfiguration, error) {
if configFilePath == "" {
commonLocations := []string{"hypdconfig.json",
"~/.hypdconfig.json",
"~/.config/hyp/hypdConfig.json",
"/etc/hyp/hypdConfig.json",
"/usr/local/etc/hyp/hypdConfig.json",
}
for _, loc := range commonLocations {
if _, err := os.Stat(loc); err == nil {
configFilePath = loc
break
}
}
}
// if it's still not found after checking common locations, load default config
if configFilePath == "" {
fmt.Println("no configuration file found. You can generate one with ./hypd generate defaultconfig | tee hypdconfig.json")
return DefaultConfig(), nil
}
// Otherwise if a config is specified, try to load it and error if it fails.
// I think it's better to error here if a config was intended and failed
// rather than failing back to default
b, err := os.ReadFile(configFilePath)
if err != nil {
return nil, fmt.Errorf("failed to read config file '%s': %w", configFilePath, err)
}
hypdConfiguration := &HypdConfiguration{}
err = json.Unmarshal(b, hypdConfiguration)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal config file json to HypdConfiguration (is the config file malformed?): %w", err)
}
return hypdConfiguration, nil
}
func DefaultConfig() *HypdConfiguration {
return &HypdConfiguration{
NetworkInterface: "enp0s3",
PreSharedKeyDirectory: "./secrets/",
SuccessAction: "iptables -A INPUT -p tcp -s %s --dport 22 -j ACCEPT",
TimeoutSeconds: 1440,
TimeoutAction: "iptables -D INPUT -p tcp -s %s --dport 22 -j ACCEPT",
}
}

View File

@ -0,0 +1,14 @@
#!/bin/sh
if [ $# -lt 1 ]; then
echo "Usage: $0 <srcip>"
exit 1
fi
# Can't use dots in rule name, so swap for underscores
rulename="hypd_${1//./_}_wireguard"
# Configure the rule in OpenWRT's uci interface
uci delete firewall.$rulename
uci commit firewall
service firewall restart

View File

@ -0,0 +1,7 @@
{
"networkInterface": "enp0s3",
"preSharedKeyDirectory": "./secrets/",
"successAction": "./examples/openwrt-wireguard/openwireguard.sh %s",
"timeoutSeconds": 1440,
"timeoutAction": "./examples/openwrt-wireguard/closewireguard.sh %s"
}

View File

@ -0,0 +1,24 @@
#!/bin/sh
if [ $# -lt 1 ]; then
echo "Usage: $0 <srcip>"
exit 1
fi
# Can't use dots in rule name, so swap for underscores
# example: 10.69.69.100 changes to hypd_10_69_69_100_wireguard
rulename="hypd_${1//./_}_wireguard"
# Configure the rule in OpenWRT's uci interface
uci set firewall.$rulename=redirect
uci set firewall.$rulename.dest=lan
uci set firewall.$rulename.target=DNAT
uci set firewall.$rulename.name=$rulename
uci set firewall.$rulename.src=wan
uci set firewall.$rulename.src_dport=51820
uci set firewall.$rulename.dest_ip=10.0.100.1
uci set firewall.$rulename.dest_port=51820
uci set firewall.$rulename.src_ip=$1
uci add_list firewall.$rulename.proto=udp
uci commit firewall
service firewall restart

30
hypd/server/README.md Normal file
View File

@ -0,0 +1,30 @@
# hypd server
hypd is the port knocking daemon which runs on an edge device connecting to an untrusted network. Leveraging eBPF's XDP hook point, it extracts header information directly and sends to userspace the specific information required. This method is faster than alternative methods such as using libpcap.
### eBPF
The hyp_bpf.c program can be recompiled using go generate.
```bash
# Debian: sudo apt install git clang linux-headers-amd64 libbpf-dev
go generate .
```
### Generating vmlinux.h
vmlinux.h is included in hyp_bpf.c and can be regenerated with bpftool.
```bash
# Debian: sudo apt install bpftool
sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > ../headers/vmlinux.h
```
### Building hypd
hypd has no CGO dependencies and so can run on musl systems as well.
```bash
# To ensure it can run on systems don't use CGO
CGO_ENABLED=0 go build .
```

View File

@ -7,12 +7,8 @@ Copyright © 2024 Steven Polley <himself@stevenpolley.net>
#include "bpf_endian.h"
#include <bpf/bpf_helpers.h>
char __license[] SEC("license") = "BSD";
#define ETH_P_IP 0x0800
#define IP_FRAGMENTED 65343
// representation of knock data that gets sent to userspace
struct knock_data {
__u32 srcip; // 4 bytes
@ -44,7 +40,6 @@ int xdp_prog_func(struct xdp_md *ctx) {
// parse ethernet header
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) <= data_end) {
// parse IP header
struct iphdr *ip = data + sizeof(*eth);

View File

@ -0,0 +1,125 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build mips || mips64 || ppc64 || s390x
package server
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
type hyp_bpfKnockData struct {
Srcip uint32
Dstport uint16
Pad uint16
}
// loadHyp_bpf returns the embedded CollectionSpec for hyp_bpf.
func loadHyp_bpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_Hyp_bpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load hyp_bpf: %w", err)
}
return spec, err
}
// loadHyp_bpfObjects loads hyp_bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *hyp_bpfObjects
// *hyp_bpfPrograms
// *hyp_bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadHyp_bpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadHyp_bpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// hyp_bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type hyp_bpfSpecs struct {
hyp_bpfProgramSpecs
hyp_bpfMapSpecs
}
// hyp_bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type hyp_bpfProgramSpecs struct {
XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"`
}
// hyp_bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type hyp_bpfMapSpecs struct {
Rb *ebpf.MapSpec `ebpf:"rb"`
}
// hyp_bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadHyp_bpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type hyp_bpfObjects struct {
hyp_bpfPrograms
hyp_bpfMaps
}
func (o *hyp_bpfObjects) Close() error {
return _Hyp_bpfClose(
&o.hyp_bpfPrograms,
&o.hyp_bpfMaps,
)
}
// hyp_bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadHyp_bpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type hyp_bpfMaps struct {
Rb *ebpf.Map `ebpf:"rb"`
}
func (m *hyp_bpfMaps) Close() error {
return _Hyp_bpfClose(
m.Rb,
)
}
// hyp_bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadHyp_bpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type hyp_bpfPrograms struct {
XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"`
}
func (p *hyp_bpfPrograms) Close() error {
return _Hyp_bpfClose(
p.XdpProgFunc,
)
}
func _Hyp_bpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed hyp_bpf_bpfeb.o
var _Hyp_bpfBytes []byte

BIN
hypd/server/hyp_bpf_bpfeb.o Normal file

Binary file not shown.

View File

@ -0,0 +1,125 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
package server
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
type hyp_bpfKnockData struct {
Srcip uint32
Dstport uint16
Pad uint16
}
// loadHyp_bpf returns the embedded CollectionSpec for hyp_bpf.
func loadHyp_bpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_Hyp_bpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load hyp_bpf: %w", err)
}
return spec, err
}
// loadHyp_bpfObjects loads hyp_bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *hyp_bpfObjects
// *hyp_bpfPrograms
// *hyp_bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadHyp_bpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadHyp_bpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// hyp_bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type hyp_bpfSpecs struct {
hyp_bpfProgramSpecs
hyp_bpfMapSpecs
}
// hyp_bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type hyp_bpfProgramSpecs struct {
XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"`
}
// hyp_bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type hyp_bpfMapSpecs struct {
Rb *ebpf.MapSpec `ebpf:"rb"`
}
// hyp_bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadHyp_bpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type hyp_bpfObjects struct {
hyp_bpfPrograms
hyp_bpfMaps
}
func (o *hyp_bpfObjects) Close() error {
return _Hyp_bpfClose(
&o.hyp_bpfPrograms,
&o.hyp_bpfMaps,
)
}
// hyp_bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadHyp_bpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type hyp_bpfMaps struct {
Rb *ebpf.Map `ebpf:"rb"`
}
func (m *hyp_bpfMaps) Close() error {
return _Hyp_bpfClose(
m.Rb,
)
}
// hyp_bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadHyp_bpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type hyp_bpfPrograms struct {
XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"`
}
func (p *hyp_bpfPrograms) Close() error {
return _Hyp_bpfClose(
p.XdpProgFunc,
)
}
func _Hyp_bpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed hyp_bpf_bpfel.o
var _Hyp_bpfBytes []byte

BIN
hypd/server/hyp_bpf_bpfel.o Normal file

Binary file not shown.

View File

@ -15,6 +15,7 @@ import (
"os/exec"
"time"
"deadbeef.codes/steven/hyp/hypd/configuration"
"deadbeef.codes/steven/hyp/otphyp"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
@ -48,11 +49,10 @@ var (
// PacketServer is the main function when operating in server mode
// it sets up the pcap on the capture device and starts a goroutine
// to rotate the knock sequence
func PacketServer(captureDevice string) error {
iface, err := net.InterfaceByName(captureDevice)
func PacketServer(config *configuration.HypdConfiguration) error {
iface, err := net.InterfaceByName(config.NetworkInterface)
if err != nil {
log.Fatalf("lookup network iface %q: %v", captureDevice, err)
log.Fatalf("lookup network iface %q: %v", config.NetworkInterface, err)
}
secretBytes, err := os.ReadFile("hyp.secret")
@ -147,8 +147,6 @@ func handleKnock(knockEvent hyp_bpfKnockData) {
}
// if it's wrong, reset progress
// TBD: vulnerable to sweep attack - this won't be triggered if a wrong packet doesn't match BPF filter
// TBD: make the sweep attack fix on by default, but configurable to be off to allow for limited BPF filter for extremely low overhead as compromise.
if knockEvent.Dstport != client.Sequence[client.Progress] {
delete(clients, knockEvent.Srcip)
fmt.Printf("port '%d' is in sequence, but came at unexpected order - resetting progress", knockEvent.Dstport)