Compare commits
No commits in common. "f660a5a2e565adae3462549d3aaed0a05115ebb7" and "af0c9559876c44624a840d36625b4c0af5f31177" have entirely different histories.
f660a5a2e5
...
af0c955987
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
[![Build Status](https://drone.deadbeef.codes/api/badges/steven/hyp/status.svg)](https://drone.deadbeef.codes/steven/hyp)
|
[![Build Status](https://drone.deadbeef.codes/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 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 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.
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
1. The pre-shared key is generated and distributed between both the hyp client and the hyp server.
|
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.
|
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.
|
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.
|
||||||
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.
|
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.
|
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.
|
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.
|
||||||
|
@ -1,33 +1,12 @@
|
|||||||
# hypd | Hide Your Ports Daemon
|
# hyp server
|
||||||
|
|
||||||
hypd is the pork knocking daemon which listens for incoming authentic knock sequences.
|
hyp server is the port knocking daemon which listens for incoming authentic knock sequences.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
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.
|
##### Starting the server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example: create a directory named secrets
|
# As root - or user that can capture packets and modify IPTables
|
||||||
mkdir -p secrets
|
./hypd server eth0
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
```
|
@ -25,9 +25,8 @@ import (
|
|||||||
|
|
||||||
// Client is used to keep track of a client attempting to perform an authentic knock sequence
|
// Client is used to keep track of a client attempting to perform an authentic knock sequence
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Progress int // index of current progress in sequence. Value of 1 means first port has been matched
|
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
|
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)
|
// KnockSequence is used keep track of an ordered knock sequence and whether it's been marked for use (to prevent replay attacks)
|
||||||
@ -113,7 +112,7 @@ func PacketServer(config *configuration.HypdConfiguration, secrets [][]byte) err
|
|||||||
log.Printf("error parsing ringbuf event: %v", err)
|
log.Printf("error parsing ringbuf event: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go handleKnock(event)
|
handleKnock(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,21 +125,17 @@ func intToIP(ipNum uint32) net.IP {
|
|||||||
|
|
||||||
// packets that match the BPF filter get passed to handlePacket
|
// packets that match the BPF filter get passed to handlePacket
|
||||||
func handleKnock(knockEvent hyp_bpfKnockData) {
|
func handleKnock(knockEvent hyp_bpfKnockData) {
|
||||||
|
|
||||||
client, ok := clients[knockEvent.Srcip]
|
client, ok := clients[knockEvent.Srcip]
|
||||||
if !ok { // client doesn't exist yet
|
if !ok { // client doesn't exist yet
|
||||||
client = &Client{}
|
for i, knockSequence := range knockSequences { // identify which of the 3 authentic knock sequences is matched
|
||||||
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
|
if knockSequence.Used { // skip over sequences that are already used to prevent replay attack
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if knockEvent.Dstport == knockSequence.PortSequence[0] {
|
if knockEvent.Dstport == knockSequence.PortSequence[0] {
|
||||||
knockSequences[i].Used = true // TBD: This is vulnerable to a DoS just by doing a full UDP port scan
|
// Create the client and mark the knock sequence as used
|
||||||
client.Progress = 1
|
clients[knockEvent.Srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
|
||||||
client.Sequence = knockSequence.PortSequence
|
knockSequences[i].Used = true
|
||||||
go timeoutKnockSequence(knockEvent.Srcip)
|
go timeoutKnockSequence(knockEvent.Srcip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,9 +152,8 @@ func handleKnock(knockEvent hyp_bpfKnockData) {
|
|||||||
// Client increases progress through sequence and checks if sequence is completed
|
// Client increases progress through sequence and checks if sequence is completed
|
||||||
client.Progress++
|
client.Progress++
|
||||||
if client.Progress >= len(client.Sequence) {
|
if client.Progress >= len(client.Sequence) {
|
||||||
client.Progress = 0
|
delete(clients, knockEvent.Srcip)
|
||||||
client.LastSuccess = time.Now()
|
handleSuccess(intToIP(knockEvent.Srcip)) // The magic function, the knock is completed
|
||||||
handleSuccess(knockEvent.Srcip) // The magic function, the knock is completed
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,18 +162,11 @@ func handleKnock(knockEvent hyp_bpfKnockData) {
|
|||||||
// being indefinitely stuck part way through an old knock sequence. It's also helpful
|
// 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
|
// in preventing sweep attacks as the authentic knock sequence must be correctly entered
|
||||||
// within the timeout value from start to finish.
|
// 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) {
|
func timeoutKnockSequence(srcip uint32) {
|
||||||
time.Sleep(time.Second * KnockSequenceTimeout)
|
time.Sleep(time.Second * KnockSequenceTimeout)
|
||||||
client, ok := clients[srcip]
|
_, ok := clients[srcip]
|
||||||
if ok {
|
if ok {
|
||||||
if client.LastSuccess.IsZero() { // If they've never succeeded, just drop them from the map
|
delete(clients, srcip)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,46 +196,13 @@ func rotateSequence() {
|
|||||||
|
|
||||||
// handleSuccess is ran when a source IP successfully enters the authentic knock sequence
|
// handleSuccess is ran when a source IP successfully enters the authentic knock sequence
|
||||||
// the configured success action is ran
|
// the configured success action is ran
|
||||||
func handleSuccess(srcip uint32) {
|
func handleSuccess(srcip net.IP) {
|
||||||
srcipf := intToIP(srcip) // formatted as net.IP
|
fmt.Println("Successful knock from:", srcip)
|
||||||
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
|
// 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
|
// 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))
|
cmd := exec.Command("sh", "-c", fmt.Sprintf(serverConfig.SuccessAction, srcip))
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to execute success action command for '%s': %v", srcipf, err)
|
log.Printf("failed to execute success action command for '%s': %v", srcip, 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)
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user