Compare commits
14 Commits
1e195c3768
...
main
Author | SHA1 | Date | |
---|---|---|---|
bed54826d6 | |||
b318bcb3c1 | |||
390fabe1b4 | |||
92f5c579e6 | |||
305ba29c50 | |||
f8be95c8d0 | |||
0942fb132f | |||
6b1bfb3a01 | |||
2af574fd18 | |||
f660a5a2e5 | |||
d1239867ae | |||
af0c955987 | |||
caf5bd5af6 | |||
a52f3f0d43 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -2,4 +2,7 @@ hyp.secret
|
||||
*.exe
|
||||
hypd/hypd
|
||||
hyp/hyp
|
||||
hypd/hypdconfig.json
|
||||
hypd/hypdconfig.json
|
||||
hypd/secrets/
|
||||
hypd/server/*.o
|
||||
env.sh
|
||||
|
16
README.md
16
README.md
@ -1,32 +1,34 @@
|
||||
# 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 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.
|
||||
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.
|
||||
|
||||
Compared to most port knocking daemons, hyp provides additional protection against replay and sweep attacks. Each authentic knock sequence is a one time use, and new knock sequences are generate 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:
|
||||
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:
|
||||
|
||||
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 will be transmitting over an untrusted network after all.
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
### Runtime Requirements
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Build Requirements
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```sh
|
||||
# Clone repository
|
||||
|
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
|
||||
```
|
||||
|
@ -47,6 +47,11 @@ Example usage:
|
||||
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)
|
||||
@ -54,21 +59,32 @@ Example usage:
|
||||
|
||||
decodedSecret, err := base32.StdEncoding.DecodeString(string(secretBytes))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to base32 decode secret '%s': %w", secretFilePath, err)
|
||||
log.Fatalf("failed to base32 decode secret '%s': %v", secretFilePath, err)
|
||||
}
|
||||
|
||||
ports, err := otphyp.GeneratePorts(decodedSecret, time.Now())
|
||||
if err != nil {
|
||||
log.Fatalf("failed to generate ports from shared secret: %v", err)
|
||||
}
|
||||
for {
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -76,6 +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().Int("maxjitter", 200, "Specifies the time in milliseconds between knock sequence transmissions.")
|
||||
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.
|
@ -57,8 +57,16 @@ func LoadConfiguration(configFilePath string) (*HypdConfiguration, error) {
|
||||
}
|
||||
|
||||
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: "enp0s3",
|
||||
NetworkInterface: ifaceString,
|
||||
PreSharedKeyDirectory: "./secrets/",
|
||||
SuccessAction: "iptables -A INPUT -p tcp -s %s --dport 22 -j ACCEPT",
|
||||
TimeoutSeconds: 1440,
|
||||
|
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
|
||||
}
|
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.
|
||||
|
||||

|
||||
|
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 |
@ -35,32 +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;
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
@ -112,7 +113,7 @@ func PacketServer(config *configuration.HypdConfiguration, secrets [][]byte) err
|
||||
log.Printf("error parsing ringbuf event: %v", err)
|
||||
continue
|
||||
}
|
||||
handleKnock(event)
|
||||
go handleKnock(event)
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,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)
|
||||
}
|
||||
}
|
||||
@ -152,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
|
||||
}
|
||||
}
|
||||
@ -162,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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +188,7 @@ 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++ {
|
||||
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 {
|
||||
@ -190,19 +203,52 @@ func rotateSequence() {
|
||||
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):]
|
||||
}
|
||||
}
|
||||
|
||||
// handleSuccess is ran when a source IP successfully enters the authentic knock sequence
|
||||
// the configured success action is ran
|
||||
func handleSuccess(srcip net.IP) {
|
||||
fmt.Println("Successful knock from:", srcip)
|
||||
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, srcip))
|
||||
cmd := exec.Command("sh", "-c", fmt.Sprintf(serverConfig.SuccessAction, srcipf))
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("failed to execute success action 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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user