Compare commits

...

35 Commits
0.0.2 ... main

Author SHA1 Message Date
bed54826d6 Update README.md
move domain from deadbeef.codes to stevenpolley.net
2024-07-10 15:39:21 +00:00
b318bcb3c1 invert conditionals, removing crazy nesting
All checks were successful
continuous-integration/drone/push Build is passing
Thank you code aesthetic
2024-05-09 20:17:02 -06:00
390fabe1b4 remove pre-built eBPF programs
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-25 21:37:15 -06:00
92f5c579e6 add README.md
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-25 21:21:27 -06:00
305ba29c50 add openwrt kernel configuration instruction
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-25 21:16:26 -06:00
f8be95c8d0 add fortinet integration example
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-23 22:11:00 -06:00
0942fb132f QoL feature - select best interface on current system
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
When generating a default config instead of using a canned value like "eth0", hypd will isntead look at what interfaces the system has and make a best guess based on progressively narrowing filters.
2024-04-20 19:25:15 -06:00
6b1bfb3a01 Better usage in hyp and hypd readme's
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-20 18:31:14 -06:00
2af574fd18 add optional refreshtime parameter to client
All checks were successful
continuous-integration/drone/push Build is passing
If refreshtime is specified, instead the client running as a one-shot command, it will instead run persistently and perform a new authentic knock sequence each specified time in minutes.
2024-04-20 17:23:52 -06:00
f660a5a2e5 More readme content
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-20 16:11:10 -06:00
d1239867ae Add support for timeout action
There is also capability of performing another new authentic knock sequence to refresh their timeout timer so they can remain open
2024-04-20 15:41:26 -06:00
af0c955987 upgrade golang.org/x/exp package
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-20 13:28:25 -06:00
caf5bd5af6 Fix incorrect string formatting directive 2024-04-20 13:27:18 -06:00
a52f3f0d43 fix incorrect logic in rotateSequence
This was introduced in the previous few commits when adding support for multiple secrets in knockd.  The logic to push and pop entries from the knockSequences slice needed to be adjusted to cound for the number of secrets that are loaded by hypd.
2024-04-20 13:27:00 -06:00
1e195c3768 fix client broke from last commit
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-19 22:08:16 -06:00
2951c1f684 add support for multiple secrets (independent agents) on the knock daemon
Some checks failed
continuous-integration/drone/push Build is failing
This allows you to generate more than one pre-shared secret on the knock daemon so that you can distribute the secret and control revocation at a more granular level.  Each additional secret creates one more concurrent authentic knock sequence.
2024-04-19 22:04:00 -06:00
334407e309 mislinked
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-19 20:34:07 -06:00
348fe6296b Add example vulnerabilities
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-19 20:20:34 -06:00
b8a16fed7f Minor formatting fixes
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-19 19:21:56 -06:00
59cd35a51e Merge branch 'main' of https://deadbeef.codes/steven/hyp
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-19 19:19:14 -06:00
35f58da4f0 Rewrite readme.md
Make the project goal more clear, less personal opinion mentioned.  Mention how hyp differentiates itself, how the authentic knock sequence is determined with a diagram.  Provide instructions to build.
2024-04-19 19:18:39 -06:00
034f3024b6 add diagram
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-19 18:46:43 -06:00
05e37193b4 Make the success action configurable
Instead of using the hardcoded proof of concept for iptables, the success action is now read from the hypd configuration file and whatever is defined there is executed.
2024-04-18 11:22:03 -06:00
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
27 changed files with 919 additions and 162 deletions

5
.gitignore vendored
View File

@ -2,4 +2,7 @@ hyp.secret
*.exe
hypd/hypd
hyp/hyp
hypd/server/hyp_bpf_bpfe*
hypd/hypdconfig.json
hypd/secrets/
hypd/server/*.o
env.sh

View File

@ -1,38 +1,51 @@
# hyp | Hide Your Ports
[![Build Status](https://drone.deadbeef.codes/api/badges/steven/hyp/status.svg)](https://drone.deadbeef.codes/steven/hyp)
[![Build Status](https://drone.stevenpolley.net/api/badges/steven/hyp/status.svg)](https://drone.deadbeef.codes/steven/hyp)
hyp is a [port knocking](https://www.youtube.com/watch?v=a7VJZEJVhD0) implementation written in Go, using spread-spectrum UDP as an authentication mechanism. It enables trusted devices to access services over the internet, wherever they are, and without the service being publicly accessible. The benefit is that the ports are not open publicly on the internet, they won't show in a port scan and are therefore less likely to be attacked by a threat actor.
hyp is a [port knocking](https://www.youtube.com/watch?v=a7VJZEJVhD0) implementation written in Go and C. hyp uses spread-spectrum UDP as an authentication mechanism and enables trusted agents to access services over the internet, wherever they are, and without the service being accessible by others. Your TCP and UDP ports are closed. They will not show in a port scan. Nobody else can connect to them. This is particularly useful as [there](https://nvd.nist.gov/vuln/detail/CVE-2024-21888) [have](https://nvd.nist.gov/vuln/detail/CVE-2023-20269) [been](https://nvd.nist.gov/vuln/detail/CVE-2021-26109) [a](https://nvd.nist.gov/vuln/detail/CVE-2024-22394) [few](https://nvd.nist.gov/vuln/detail/CVE-2024-21894) [VPN](https://nvd.nist.gov/vuln/detail/CVE-2024-3400) [gateway](https://nvd.nist.gov/vuln/detail/CVE-2023-27997) [vulnerabilities](https://nvd.nist.gov/vuln/detail/CVE-2024-21762) [over](https://nvd.nist.gov/vuln/detail/CVE-2022-3236) [the](https://nvd.nist.gov/vuln/detail/CVE-2024-21893) [years](https://nvd.nist.gov/vuln/detail/CVE-2022-42475). I often wonder what's out there and hasn't been discovered. Why take the chance of leaving your VPN open to the whole internet? With hyp, you don't have to.
hyp provides security through obscurity. Security through obscurity tends to have a negative connotation, at least in the IT world. I don't agree with this, but it's prescribed as being bad. My belief is security through obscurity is a "further step" one can take to eliminate a certain class of threats. It by no means should be the only mechanism of protection, but instead should be incorporated only as part of a layered defense.
Compared to most port knocking daemons, hyp is extremely fast, lightweight and has no dependency on libpcap. Instead of libpcap, hyp uses eBPF technology which runs in the kernel and only sends data to userspace that's absolutely required. hyp also provides additional protection against replay and sweep attacks. Each authentic knock sequence is a one time use, and new knock sequences are generated every 30 seconds. hyp makes use of pre-shared keys and time to calculate an authentic knock sequence on both the client and server. The following process describes how hyp works:
### Physical World Analogy
1. The pre-shared key is generated and distributed between both the hyp client and the hyp server.
2. The pre-shared key is run through a sha1-hmac algorithm along with the current system time, this produces the same 160 bits of output on both sides.
3. The 160 bits is reduced down to 64 bits. This helps protect the key by not revealing the entire output of the hmac... we assume you are transmitting over an untrusted network.
4. The 64 bits are divided into four 16-bit structures which are typecast to 16-bit unsigned integers. A 16-bit integer can have a value from 0-65535, the same as UDP port numbers. We have four of them now.
5. Transmit one empty datagram to the knock daemon at a time, one after another using the four integers from the previous calculation as the destination port numbers.
6. The knock daemon on the firewall verifies the sequence and performs the action of opening the firewall port configured for the client to let them in while remaining closed to everyone else.
7. The client connects to their application which has its own authentication, authorization, and auditing.
*Scenario:* You drive to the grocery store and you happen to have your laptop computer with you in the car. You're worried someone may break into your car and steal your laptop, but luckily you have some options to consider before you leave the car to go into the store:
![Authentic Knock Sequence](https://code.stevenpolley.net/steven/hyp/raw/branch/main/docs/authentic-knock-sequence-calculation.png)
1. You could leave your laptop sitting where it is, on the passenger seat
2. You could conceal the laptop from outside view
### Runtime Requirements
Option 1 is the default option and is analogous to having your services internet-accessible. Option 2 is similar to what port knocking is trying to achieve. In either case, there still exists some risk that your laptop will be stolen in a random bip, which is why port knocking should not be your sole focus when it comes to your security strategy and should instead be something you can use to reduce the risk of drive-by attacks.
Port knocking clients have minimal requirements and can run on x86, ARM, MIPS, PowerPC, IBM390, or RISC-V. Currently only supported OS's are Linux and Windows, with support for Android planned to be added in the future.
### Brute Force Simple Overview
The port knocking daemon has more strict requirements and is only available for Linux. It requires the kernel be built with CONFIG_DEBUG_INFO_BTF, which most major distributions have out of the box. Additionally, hypd has some network requirements. hypd is only expected to work on ethernet networks with IPv4.
To put it in simple terms, hyp requires an adversary to guess a number between 1 and 18,446,744,073,709,551,615 within 90 seconds. Each guess attempt requires four ordered UDP datagrams to be transmitted. The requirement for correct order on arrival, multiple network paths, and network latency means the datagrams have to be spaced out and transmitted one at a time with time spent waiting before the next datagram is sent. An odd but perhaps useful implication of this is that the further away you are (higher latency), the less reliable guess attempts you can make before the number changes. With 20ms of latency, you can perform a maximum of 4,500 reliable guesses. With 100ms of latency, you can only perform a maximum of 900 reliable guesses.
Once you get the runtime requirements sorted, be sure to check out the hyp and hypd directories of the repository for README information for how to use hyp.
### Protection Against Replay Attacks
### Build Requirements
Most port-knocking implementations are susceptible to replay attacks, a network operator could intercept your authentic knock sequence and then replay the sequence. hyp works around this by using 64-bit time-based one-time tokens. The 64 bits of the token are then divided into 4x16-bit unsigned integers representing a port number.
Pre-built binaries for configurations I've tested are available on the [releases page](https://deadbeef.codes/steven/hyp/releases). This will likely run in many CPU architectures I haven't tested yet though.
hyp supports a clock skew of up to 30 seconds between client and server.
To build this yourself, you will need Linux with packages for: git, clang, linux-headers-<architecture> libbpf-dev and golang. Check out the [Dockerfile](https://deadbeef.codes/steven/hyp/src/branch/main/Dockerfile) as a reference for how the build environment for official releases is configured. Once the environment is ready, you can clone the repo and build.
### TBD: Protection Against Sweeping Attacks
```sh
# Clone repository
git clone https://deadbeef.codes/steven/hyp.git
~~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.~~
# Build eBPF program
cd hyp/hypd/server
go generate
### Known Weaknesses
# Build knock daemon
cd ..
go build -o hypd .
chmod +x hypd
* 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
# Run knock daemon and show help
./hypd -h
```
### References

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

2
go.mod
View File

@ -10,6 +10,6 @@ require (
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/sys v0.19.0 // indirect
)

8
go.sum
View File

@ -18,12 +18,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -1,9 +1,34 @@
# hyp-client
# hyp | Hide Your Ports Client
The client expects there to be a file named hyp.secret in the same directory. This file is generated by the hypd server using ./hypd generate secret.
The hyp client is used on machines to perform an authentic knock sequence.
### Usage
You can use -h to get help for hyp and all its commands. When figuring out how to do something, -h is your friend.
```bash
# Example Usage
# ./hyp knock <server>
./hyp knock 192.168.50.5
```
# Get general hyp help
./hyp -h
# Get help specific to the hyp knock command
./hyp knock -h
```
In order to use the hyp client, it must have the secret. Secrets are generated by hypd, the knock daemon. See the hypd README.md file for more information about generating secrets.
Once you have the secret, you can then perform an authentic knock sequence to a server.
```bash
# Assumes secret is in file named my-first-secret in same directory
./hyp knock 8.69.4.20 --secret my-first-secret
# If you omit --secret, hyp will look for a file named hyp.secret
./hyp knock 8.69.4.20
```
This will perform a single one-shot knock sequence and then hyp will exit. You can also run hyp in a persistent mode where it will perform an authentic knock sequence at a specified interval.
```bash
# Performs an authentic knock sequence every 45 minutes
./hyp knock 8.69.4.20 --refreshtime=45
```

View File

@ -4,6 +4,7 @@ Copyright © 2024 Steven Polley <himself@stevenpolley.net>
package cmd
import (
"encoding/base32"
"fmt"
"log"
"net"
@ -38,24 +39,52 @@ 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"))
}
refreshTime, err := cmd.Flags().GetInt("refreshtime")
if err != nil {
panic(fmt.Errorf("failed to parse command flag 'refreshtime': %w", err))
}
secretBytes, err := os.ReadFile(secretFilePath)
if err != nil {
log.Fatalf("failed to read file 'hyp.secret': %v", err)
}
sharedSecret := string(secretBytes)
ports, err := otphyp.GeneratePorts(sharedSecret, time.Now())
decodedSecret, err := base32.StdEncoding.DecodeString(string(secretBytes))
if err != nil {
log.Fatalf("failed to generate ports from shared secret: %v", err)
log.Fatalf("failed to base32 decode secret '%s': %v", secretFilePath, err)
}
// Transmit
fmt.Println("Transmitting knock sequence:", ports)
for _, port := range ports {
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)
for {
ports, err := otphyp.GeneratePorts(decodedSecret, time.Now())
if err != nil {
log.Fatalf("failed to generate ports from shared secret: %v", err)
}
// Transmit
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 * time.Duration(maxJitter)) // TBD: Make this configurable with flag (maxJitter)
}
if refreshTime < 1 {
break
}
sleepDuration := time.Minute * time.Duration(refreshTime)
fmt.Printf("waiting until: %s...\n", time.Now().Add(sleepDuration).Format("15:04:05"))
time.Sleep(sleepDuration)
}
},
}
@ -63,5 +92,7 @@ Example usage:
func init() {
rootCmd.AddCommand(knockCmd)
knockCmd.PersistentFlags().String("secret", "hyp.secret", "Path to the file containing the hyp secret.")
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 transmissions while performing the knock sequence")
knockCmd.PersistentFlags().Int("refreshtime", 0, "If specified, the hyp client will run persistently and send a full knock sequence at this interval in minutes")
}

View File

@ -1,12 +1,47 @@
# hyp server
# hypd | Hide Your Ports Daemon
hyp server is the port knocking daemon which listens for incoming authentic knock sequences.
hypd is the pork knocking daemon which listens for incoming authentic knock sequences. When it sees an authentic knock sequence, it then performs an action.
### Usage
##### Starting the server
You can use -h to get help for hypd and all its commands. When figuring out how to do something, -h is your friend.
```bash
# As root - or user that can capture packets and modify IPTables
./hypd server eth0
```
# Get general hypd help
./hypd -h
# Get help specific to the hypd generate command
./hypd generate -h
```
Running hypd requires generating secrets which are then shared with hyp clients. hypd is used to generate these secrets, and it's recommended you create a directory just for hyp secrets.
```bash
# Example: create a directory named secrets
mkdir -p secrets
# Then generate a secret file in this directory
./hypd generate secret > secrets/my-first-secret
```
It's recommended you generate a secret for each trusted agent so you can granularly control revocation just by removing a secret file from the secrets directory.
Running hypd requires specifying a configuration file. It's recommended you generate the default configuration file and then edit it afterwards.
```bash
# Create a default configuration file
./hypd generate defaultconfig > hypd.conf
```
Make sure you take the time to review the hypd.conf file and edit it to your liking, this is the most important step. Make sure the network interface is correct, hypd will make an educated guess based on the interfaces your system has.
If you have complex requirements, you can make the successAction/timeoutAction an external shell script. If you want to disable the timeoutAction, you can set timeoutSeconds to 0.
Once you have set your config file, you can finally run hypd.
```bash
# As root or sudo, specify the configuration file
sudo ./hypd server hypd.conf
```
If you encounter any errors while trying to run, address those. If not, then you're now listening for incoming authentic knock sequences. Make sure you distribute the secret to the client.

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,63 @@ 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 load configuration file '%s': %w", args[0], err))
}
secrets, err := configuration.LoadSecrets(hypdConfiguration.PreSharedKeyDirectory)
if err != nil {
panic(fmt.Errorf("failed to load secrets from directory '%s': %w", hypdConfiguration.PreSharedKeyDirectory, err))
}
err = server.PacketServer(hypdConfiguration, secrets)
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,75 @@
package configuration
import (
"encoding/json"
"fmt"
"os"
)
type HypdConfiguration struct {
NetworkInterface string `json:"networkInterface"` // The network interface that the eBPF program attaches to
PreSharedKeyDirectory string `json:"preSharedKeyDirectory"` // hypd will load all *.secret files from this directory
SuccessAction string `json:"successAction"` // The action to take for a successful knock, each argument is a separate string
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, each argument is a separate string
}
// 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 {
var ifaceString string
iface, err := getDefaultNIC()
if err == nil {
ifaceString = iface.Name
} else {
ifaceString = "enp0s3" // fallback to fixed value
}
return &HypdConfiguration{
NetworkInterface: ifaceString,
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,81 @@
package configuration
import (
"fmt"
"net"
)
// QoL feature to try and detect the best NIC for hyp
func getDefaultNIC() (*net.Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("failed to get network interfaces on this system: %v", err)
}
if len(ifaces) < 1 {
return nil, fmt.Errorf("this system has no network interfaces: %v", err)
}
// Just pick one to start
selectedIface := ifaces[0]
filteredIfaces := make([]net.Interface, 0)
// Check for ethernet addresses
for _, iface := range ifaces {
if len(iface.HardwareAddr) == 6 {
selectedIface = iface
filteredIfaces = append(filteredIfaces, iface)
}
}
ifaces = filteredIfaces
filteredIfaces = make([]net.Interface, 0)
// Check for interfaces that are up and not loopbacks
for _, iface := range ifaces {
if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagRunning != 0 && iface.Flags&net.FlagLoopback == 0 {
selectedIface = iface
filteredIfaces = append(filteredIfaces, iface)
}
}
ifaces = filteredIfaces
filteredIfaces = make([]net.Interface, 0)
// Check for interfaces that have IPv4 addresses assigned
for _, iface := range ifaces {
addresses, err := iface.Addrs()
if err != nil {
continue
}
for _, address := range addresses {
ip, _, err := net.ParseCIDR(address.String())
if err != nil {
continue
}
if ip.To4() != nil {
selectedIface = iface
filteredIfaces = append(filteredIfaces, iface)
}
}
}
ifaces = filteredIfaces
filteredIfaces = nil
// Check for interfaces that have non RFC1918 addresses assigned
for _, iface := range ifaces {
addresses, err := iface.Addrs()
if err != nil {
continue
}
for _, address := range addresses {
ip, _, err := net.ParseCIDR(address.String())
if err != nil {
continue
}
if !ip.IsPrivate() {
selectedIface = iface
}
}
}
return &selectedIface, nil // TBD
}

View File

@ -0,0 +1,49 @@
package configuration
import (
"encoding/base32"
"fmt"
"io/fs"
"os"
"path/filepath"
)
var secrets [][]byte
// LoadSecrets processes all files within the specified directory and attempts to
// convert the file contents to secrets to by used by hypd
func LoadSecrets(preSharedKeyDirectory string) ([][]byte, error) {
secrets = make([][]byte, 0)
err := filepath.Walk(preSharedKeyDirectory, processSecretFile)
if err != nil {
return nil, fmt.Errorf("failed to walk directory '%s': %w", preSharedKeyDirectory, err)
}
return secrets, nil
}
// processSecretFile is called against each file in the preSharedKeyDirectory
// It reads each file and attemts to base32 decode their contents
func processSecretFile(path string, info fs.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("failed to process file '%s': %w", path, err)
}
if info.IsDir() {
return nil
}
secretBytes, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file '%s': %w", path, err)
}
decodedSecret, err := base32.StdEncoding.DecodeString(string(secretBytes))
if err != nil {
return fmt.Errorf("failed to base32 decode secret '%s': %w", path, err)
}
secrets = append(secrets, decodedSecret)
return nil
}

View File

@ -0,0 +1,7 @@
{
"networkInterface": "enp0s3",
"preSharedKeyDirectory": "./secrets/",
"successAction": "./examples/fortigate/openfortigate.sh %s",
"timeoutSeconds": 0,
"timeoutAction": ""
}

View File

@ -0,0 +1,33 @@
#!/bin/bash
# Make sure you have environment variables set for FORTIGATE_MGMT_URL, FORTIGATE_API_TOKEN and FORTIGATE_ADDRESS_OBJECT_GROUP
# Examples:
export FORTIGATE_MGMT_URL="https://69.4.20.10:8443"
export FORTIGATE_API_KEY="5fkwkkzgQ4s31bdH60qsxxfN093zgt"
export FORTIGATE_ADDRESS_OBJECT_GROUP="hyp-allowed-clients"
if [ $# -lt 1 ]; then
echo "Usage: $0 <srcip>"
exit 1
fi
echo $FORTIGATE_MGMT_URL
echo $1
# Create the address object
curl "$FORTIGATE_MGMT_URL/api/v2/cmdb/firewall/address?datasource=1" \
-X "POST" \
-H "Authorization: Bearer $FORTIGATE_API_KEY" \
-H "Content-Type: application/json" \
--data-raw "{\"name\":\"hyp_$1\",\"subnet\":\"$1/32\",\"color\":\"0\"}" \
--insecure # LOL - remove this if you want, but I want this to be easy for noobs
# Add to address object group
curl "$FORTIGATE_MGMT_URL/api/v2/cmdb/firewall/addrgrp/$FORTIGATE_ADDRESS_OBJECT_GROUP/member" \
-X "POST" \
-H "Authorization: Bearer $FORTIGATE_API_KEY" \
-H "Content-Type: application/json" \
--data-raw "{\"name\":\"hyp_$1\"}" \
--insecure # And here too

View File

@ -0,0 +1,12 @@
# Using hyp with OpenWrt Wireguard
This example case is to deploy hypd on OpenWrt to open up access to the WireGuard VPN service.
hyp utilizes eBPF technology to ensure runtime overhead is extremely small (in a way, but in a way not). Most Linux distributions have support for this out of the box, however OpenWrt does not. OpenWrt has a very stripped down, purpose-configured kernel and does not have the requirements built in to run hyp.
The good news is, you can build OpenWrt yourself and configure it with the requirements. Follow the directions at this page: https://openwrt.org/docs/guide-developer/toolchain/use-buildsystem
When you run *make menuconfig*, make sure you check off *Enable additional BTF type information* which is also known as CONFIG_KERNEL_DEBUG_INFO_BTF. This is required to support eBPF CO:RE.
![Kernel Config](https://deadbeef.codes/steven/hyp/raw/branch/main/hypd/examples/openwrt-wireguard/kernel_config.png)

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

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
@ -39,33 +35,40 @@ int xdp_prog_func(struct xdp_md *ctx) {
// A knock should not contain any data
if (data_end - data > 60) {
goto done;
return XDP_PASS;
}
// parse ethernet header
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) <= data_end) {
// parse IP header
struct iphdr *ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) <= data_end) {
if (ip->protocol == IPPROTO_UDP) {
// parse UDP header
struct udphdr *udp = (void *)ip + sizeof(*ip);
if ((void *)udp + sizeof(*udp) <= data_end)
{
// pack into knock structure and send to userspace
struct knock_data knock = {
.srcip = bpf_ntohl(ip->saddr),
.dstport = bpf_htons(udp->dest),
.pad = 0
};
bpf_ringbuf_output(&rb, &knock, sizeof(knock), BPF_RB_FORCE_WAKEUP);
}
}
}
if ((void *)eth + sizeof(*eth) > data_end) {
return XDP_PASS;
}
done:
// parse IP header
struct iphdr *ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_PASS;
}
// Ensure IP header protocol field is UDP (protocol 17)
if (ip->protocol != IPPROTO_UDP) {
return XDP_PASS;
}
// parse UDP header
struct udphdr *udp = (void *)ip + sizeof(*ip);
if ((void *)udp + sizeof(*udp) > data_end) {
return XDP_PASS;
}
// pack into knock structure and send to userspace
struct knock_data knock = {
.srcip = bpf_ntohl(ip->saddr),
.dstport = bpf_htons(udp->dest),
.pad = 0
};
bpf_ringbuf_output(&rb, &knock, sizeof(knock), BPF_RB_FORCE_WAKEUP);
// We send everything to XDP_PASS
return XDP_PASS;
}

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

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

View File

@ -11,10 +11,10 @@ import (
"fmt"
"log"
"net"
"os"
"os/exec"
"time"
"deadbeef.codes/steven/hyp/hypd/configuration"
"deadbeef.codes/steven/hyp/otphyp"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
@ -25,8 +25,9 @@ import (
// Client is used to keep track of a client attempting to perform an authentic knock sequence
type Client struct {
Progress int // index of current progress in sequence. Value of 1 means first port has been matched
Sequence [4]uint16 // stores the knock sequence the current client is attempting. It's set and tracked here to prevent race conditions during a knock sequence being received and key rotations
Progress int // index of current progress in sequence. Value of 1 means first port has been matched
Sequence [4]uint16 // stores the knock sequence the current client is attempting. It's set and tracked here to prevent race conditions during a knock sequence being received and key rotations
LastSuccess time.Time
}
// KnockSequence is used keep track of an ordered knock sequence and whether it's been marked for use (to prevent replay attacks)
@ -42,25 +43,21 @@ const (
var (
clients map[uint32]*Client // Contains a map of clients, key is IPv4 address
knockSequences []KnockSequence // We have 3 valid knock sequences at any time to account for clock skew
sharedSecret string // base32 encoded shared secret used for totp
serverConfig *configuration.HypdConfiguration
sharedSecrets [][]byte // A slice of byte slices, each being a secret key
)
// 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, secrets [][]byte) error {
serverConfig = config
sharedSecrets = secrets
iface, err := net.InterfaceByName(serverConfig.NetworkInterface)
if err != nil {
log.Fatalf("lookup network iface %q: %v", captureDevice, err)
log.Fatalf("lookup network iface %q: %v", serverConfig.NetworkInterface, err)
}
secretBytes, err := os.ReadFile("hyp.secret")
if err != nil {
log.Fatalf("failed to read file 'hyp.secret': %v", err)
}
sharedSecret = string(secretBytes)
clients = make(map[uint32]*Client, 0)
knockSequences = []KnockSequence{}
@ -116,7 +113,7 @@ func PacketServer(captureDevice string) error {
log.Printf("error parsing ringbuf event: %v", err)
continue
}
handleKnock(event)
go handleKnock(event)
}
}
@ -129,17 +126,21 @@ func intToIP(ipNum uint32) net.IP {
// packets that match the BPF filter get passed to handlePacket
func handleKnock(knockEvent hyp_bpfKnockData) {
client, ok := clients[knockEvent.Srcip]
if !ok { // client doesn't exist yet
for i, knockSequence := range knockSequences { // identify which of the 3 authentic knock sequences is matched
client = &Client{}
clients[knockEvent.Srcip] = client
}
if client.Progress == 0 {
for i, knockSequence := range knockSequences { // identify which of the authentic knock sequences is matched
if knockSequence.Used { // skip over sequences that are already used to prevent replay attack
continue
}
if knockEvent.Dstport == knockSequence.PortSequence[0] {
// Create the client and mark the knock sequence as used
clients[knockEvent.Srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
knockSequences[i].Used = true
knockSequences[i].Used = true // TBD: This is vulnerable to a DoS just by doing a full UDP port scan
client.Progress = 1
client.Sequence = knockSequence.PortSequence
go timeoutKnockSequence(knockEvent.Srcip)
}
}
@ -147,8 +148,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)
@ -158,8 +157,9 @@ func handleKnock(knockEvent hyp_bpfKnockData) {
// Client increases progress through sequence and checks if sequence is completed
client.Progress++
if client.Progress >= len(client.Sequence) {
delete(clients, knockEvent.Srcip)
handleSuccess(intToIP(knockEvent.Srcip)) // The magic function, the knock is completed
client.Progress = 0
client.LastSuccess = time.Now()
handleSuccess(knockEvent.Srcip) // The magic function, the knock is completed
return
}
}
@ -168,11 +168,18 @@ func handleKnock(knockEvent hyp_bpfKnockData) {
// being indefinitely stuck part way through an old knock sequence. It's also helpful
// in preventing sweep attacks as the authentic knock sequence must be correctly entered
// within the timeout value from start to finish.
// Note: This is not related to handling the timeout / clsoe ports action after a client
// has successfully completed an authentic knock sequence
func timeoutKnockSequence(srcip uint32) {
time.Sleep(time.Second * KnockSequenceTimeout)
_, ok := clients[srcip]
client, ok := clients[srcip]
if ok {
delete(clients, srcip)
if client.LastSuccess.IsZero() { // If they've never succeeded, just drop them from the map
delete(clients, srcip)
} else { // If they have succeeded, just reset their progress to 0 but keep them in map. They will be cleaned in handleSuccess
client.Progress = 0
}
}
}
@ -181,30 +188,67 @@ func rotateSequence() {
for {
// Generate new knock sequences with time skew support
t := time.Now().Add(time.Second * -30)
for i := len(knockSequences); i < 3; i++ {
portSequence, err := otphyp.GeneratePorts(sharedSecret, t.Add((time.Second * 30 * time.Duration(i))))
if err != nil {
log.Fatalf("failed to generate port knock sequence: %v", err)
for i := len(knockSequences) / len(sharedSecrets); i < 3; i++ {
for _, secret := range sharedSecrets {
portSequence, err := otphyp.GeneratePorts(secret, t.Add((time.Second * 30 * time.Duration(i))))
if err != nil {
log.Fatalf("failed to generate port knock sequence: %v", err)
}
knockSequence := KnockSequence{PortSequence: portSequence}
knockSequences = append(knockSequences, knockSequence)
}
knockSequence := KnockSequence{PortSequence: portSequence}
knockSequences = append(knockSequences, knockSequence)
}
fmt.Println("New sequences:", knockSequences)
// Sleep until next 30 second offset
time.Sleep(time.Until(time.Now().Truncate(time.Second * 30).Add(time.Second * 30)))
// pop first value, next iteration pushes new value
knockSequences = knockSequences[1:]
knockSequences = knockSequences[len(sharedSecrets):]
}
}
// TBD: Implement - this is a temporary routine to demonstrate an application
func handleSuccess(srcip net.IP) {
fmt.Println("Success for ", srcip)
cmd := exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "-s", fmt.Sprint(srcip), "--dport", "22", "-j", "ACCEPT")
// handleSuccess is ran when a source IP successfully enters the authentic knock sequence
// the configured success action is ran
func handleSuccess(srcip uint32) {
srcipf := intToIP(srcip) // formatted as net.IP
log.Printf("Successful knock from: %s", srcipf)
client, ok := clients[srcip]
if !ok {
log.Printf("failed to lookup %s in clients", srcipf)
return
}
// Don't care about command injection, the configuration file providing the command literally NEEDS to be trusted
// TBD: Use template / substitution instead of string formatting directive - allows for srcip token to be used multiple times
cmd := exec.Command("sh", "-c", fmt.Sprintf(serverConfig.SuccessAction, srcipf))
err := cmd.Run()
if err != nil {
log.Printf("failed to execute iptables command for '%s': %v", srcip, err)
log.Printf("failed to execute success action command for '%s': %v", srcipf, err)
}
// Handle timeout action
if serverConfig.TimeoutSeconds < 1 { // Timeout action is disabled
delete(clients, srcip)
return
}
// Handle checks for client timeout
// TBD: Persistence / journaling state to disk? How to handle case if knock daemon is restarted - ports would remain open
lastSuccess := client.LastSuccess
time.Sleep(time.Until(client.LastSuccess.Add(time.Duration(serverConfig.TimeoutSeconds * int(time.Second)))))
if client.LastSuccess.After(lastSuccess) { // The client has refreshed
return
}
// Don't care about command injection, the configuration file providing the command literally NEEDS to be trusted
// TBD: Use template / substitution instead of string formatting directive - allows for srcip token to be used multiple times
log.Printf("Performing timeout action on: %s", srcipf)
cmd = exec.Command("sh", "-c", fmt.Sprintf(serverConfig.TimeoutAction, srcipf))
err = cmd.Run()
if err != nil {
log.Printf("failed to execute timeout action command for '%s': %v", srcipf, err)
}
delete(clients, srcip)
}

View File

@ -15,12 +15,7 @@ import (
// A loose implementation of hotp meant for our specific purposes of generating four random port numbers
// Accepts a base32 encoded shared secret and a time
func GeneratePorts(sharedSecret string, t time.Time) (ports [4]uint16, err error) {
sharedSecretBytes, err := base32.StdEncoding.DecodeString(sharedSecret)
if err != nil {
return [4]uint16{0, 0, 0, 0}, fmt.Errorf("failed to base32 decode shared secret string to bytes: %v", err)
}
func GeneratePorts(sharedSecret []byte, t time.Time) (ports [4]uint16, err error) {
// 30 second key rotation
movingFactor := uint64(math.Floor(float64(t.Unix()) / float64(30)))
@ -28,7 +23,7 @@ func GeneratePorts(sharedSecret string, t time.Time) (ports [4]uint16, err error
binary.BigEndian.PutUint64(buf, movingFactor)
// calculate hmac and offset
mac := hmac.New(sha1.New, sharedSecretBytes)
mac := hmac.New(sha1.New, sharedSecret)
mac.Write(buf)
sum := mac.Sum(nil)
offset := sum[len(sum)-1] & 0xf