Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
bed54826d6 | |||
b318bcb3c1 | |||
390fabe1b4 | |||
92f5c579e6 | |||
305ba29c50 | |||
f8be95c8d0 | |||
0942fb132f | |||
6b1bfb3a01 | |||
2af574fd18 | |||
f660a5a2e5 | |||
d1239867ae | |||
af0c955987 | |||
caf5bd5af6 | |||
a52f3f0d43 | |||
1e195c3768 | |||
2951c1f684 | |||
334407e309 | |||
348fe6296b | |||
b8a16fed7f | |||
59cd35a51e | |||
35f58da4f0 | |||
034f3024b6 | |||
05e37193b4 | |||
79aa8136c5 | |||
344d874c02 | |||
1ffadf5c86 | |||
e95b4972da | |||
a0d118b987 | |||
e9aefaf8d6 | |||
beed9726e3 | |||
e85b644e82 | |||
2c43affac9 | |||
fbf1758ccb | |||
ffb4b7681f | |||
7f2e3c0ed9 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
||||
|
49
README.md
49
README.md
@ -1,38 +1,51 @@
|
||||
# hyp | Hide Your Ports
|
||||
|
||||
[](https://drone.deadbeef.codes/steven/hyp)
|
||||
[](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:
|
||||

|
||||
|
||||
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
|
||||
|
||||
|
BIN
docs/authentic-knock-sequence-calculation.png
Normal file
BIN
docs/authentic-knock-sequence-calculation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 196 KiB |
2
go.mod
2
go.mod
@ -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
8
go.sum
@ -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=
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
34
hypd/cmd/defaultconfig.go
Normal 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)
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
|
75
hypd/configuration/configuration.go
Normal file
75
hypd/configuration/configuration.go
Normal 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",
|
||||
}
|
||||
}
|
81
hypd/configuration/defaultNIC.go
Normal file
81
hypd/configuration/defaultNIC.go
Normal 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
|
||||
}
|
49
hypd/configuration/secrets.go
Normal file
49
hypd/configuration/secrets.go
Normal 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
|
||||
}
|
7
hypd/examples/fortigate/hypdconfig.json
Normal file
7
hypd/examples/fortigate/hypdconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"networkInterface": "enp0s3",
|
||||
"preSharedKeyDirectory": "./secrets/",
|
||||
"successAction": "./examples/fortigate/openfortigate.sh %s",
|
||||
"timeoutSeconds": 0,
|
||||
"timeoutAction": ""
|
||||
}
|
33
hypd/examples/fortigate/openfortigate.sh
Executable file
33
hypd/examples/fortigate/openfortigate.sh
Executable 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
|
12
hypd/examples/openwrt-wireguard/README.md
Normal file
12
hypd/examples/openwrt-wireguard/README.md
Normal 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.
|
||||
|
||||

|
||||
|
14
hypd/examples/openwrt-wireguard/closewireguard.sh
Normal file
14
hypd/examples/openwrt-wireguard/closewireguard.sh
Normal 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
|
7
hypd/examples/openwrt-wireguard/hypdconfig.json
Normal file
7
hypd/examples/openwrt-wireguard/hypdconfig.json
Normal 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"
|
||||
}
|
BIN
hypd/examples/openwrt-wireguard/kernel_config.png
Normal file
BIN
hypd/examples/openwrt-wireguard/kernel_config.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 148 KiB |
24
hypd/examples/openwrt-wireguard/openwireguard.sh
Normal file
24
hypd/examples/openwrt-wireguard/openwireguard.sh
Normal 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
30
hypd/server/README.md
Normal 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 .
|
||||
```
|
@ -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;
|
||||
}
|
||||
|
125
hypd/server/hyp_bpf_bpfeb.go
Normal file
125
hypd/server/hyp_bpf_bpfeb.go
Normal 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
|
125
hypd/server/hyp_bpf_bpfel.go
Normal file
125
hypd/server/hyp_bpf_bpfel.go
Normal 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
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user