Compare commits
51 Commits
3ff47dfa19
...
0.0.3
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
4ec16513ac | |||
0d113b4e8b | |||
8cd537cd79 | |||
80043a571d | |||
998c9e217c | |||
977aef9ee2 | |||
5f10c27b0f | |||
0b876665d5 | |||
d422724556 | |||
a73854e040 | |||
d40147d61c | |||
3cbd6eace2 | |||
42e5679570 | |||
54159e2e5e | |||
e197990185 | |||
f3d84f09fd | |||
0382892f73 | |||
cb20f91223 | |||
2efe3344b4 | |||
0ad3e2b0d4 | |||
3ae568639e | |||
ead7578544 |
89
.drone.yml
Normal file
89
.drone.yml
Normal file
@ -0,0 +1,89 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/deadbeef.codes/steven/hyp
|
||||
|
||||
steps:
|
||||
|
||||
- name: create build environment
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: registry.deadbeef.codes/hyp-build
|
||||
when:
|
||||
target:
|
||||
include:
|
||||
- environment
|
||||
|
||||
|
||||
- name: build hyp (client) linux-amd64
|
||||
image: registry.deadbeef.codes/hyp-build:latest
|
||||
pull: always
|
||||
volumes:
|
||||
- name: publicrelease
|
||||
path: /dist
|
||||
environment:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: 0
|
||||
commands:
|
||||
- . /root/.profile
|
||||
- cd hyp
|
||||
- go build -o /dist/hyp-linux-amd64 .
|
||||
|
||||
|
||||
- name: build hypd (server) linux-amd64
|
||||
image: registry.deadbeef.codes/hyp-build:latest
|
||||
pull: always
|
||||
volumes:
|
||||
- name: publicrelease
|
||||
path: /dist
|
||||
environment:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: 0
|
||||
commands:
|
||||
- . /root/.profile
|
||||
- cd hypd/server
|
||||
- go generate
|
||||
- cd ..
|
||||
- go build -o /dist/hypd-linux-amd64 .
|
||||
|
||||
|
||||
- name: build hyp (client) windows-amd64
|
||||
image: registry.deadbeef.codes/hyp-build:latest
|
||||
pull: always
|
||||
volumes:
|
||||
- name: publicrelease
|
||||
path: /dist
|
||||
environment:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: 0
|
||||
commands:
|
||||
- . /root/.profile
|
||||
- cd hyp
|
||||
- go build -o /dist/hyp-windows-amd64.exe .
|
||||
|
||||
|
||||
- name: release
|
||||
image: plugins/gitea-release
|
||||
pull: always
|
||||
volumes:
|
||||
- name: publicrelease
|
||||
path: /dist
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: drone_token
|
||||
base_url: https://deadbeef.codes
|
||||
files: /dist/*
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
volumes:
|
||||
- name: publicrelease
|
||||
host:
|
||||
path: /data/public/build/hyp
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@ hyp.secret
|
||||
*.exe
|
||||
hypd/hypd
|
||||
hyp/hyp
|
||||
hypd/hypdconfig.json
|
||||
hypd/secrets/
|
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
# Build environment container
|
||||
# THIS CONTAINER IS NOT FOR RUNNING HYP, IT IS FOR BUILDING IT FROM SOURCE
|
||||
|
||||
FROM debian:stable
|
||||
LABEL maintainer="himself@stevenpolley.net"
|
||||
|
||||
# Install build
|
||||
RUN apt update -y && \
|
||||
apt upgrade -y && \
|
||||
apt install -y wget git clang linux-headers-amd64 libbpf-dev
|
||||
|
||||
# Create a few symlinks
|
||||
RUN ln -s /usr/bin/llvm-strip-14 /usr/bin/llvm-strip && \
|
||||
ln -s /usr/include/x86_64-linux-gnu/asm /usr/include/asm
|
||||
|
||||
# Install golang - Latest at: https://go.dev/dl/
|
||||
RUN wget https://go.dev/dl/go1.22.2.linux-amd64.tar.gz && \
|
||||
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.22.2.linux-amd64.tar.gz && \
|
||||
rm -rf *.tar.gz && \
|
||||
echo "export PATH=$PATH:/usr/local/go/bin" >> /root/.profile && . /root/.profile
|
49
README.md
49
README.md
@ -1,36 +1,51 @@
|
||||
# hyp | Hide Your Ports
|
||||
|
||||
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.
|
||||
[](https://drone.deadbeef.codes/steven/hyp)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Physical World Analogy
|
||||
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:
|
||||
|
||||
*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. 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.
|
||||
|
||||
1. You could leave your laptop sitting where it is, on the passenger seat
|
||||
2. You could conceal the laptop from outside view
|
||||

|
||||
|
||||
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.
|
||||
### Runtime Requirements
|
||||
|
||||
### Brute Force Simple Overview
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Protection Against Replay Attacks
|
||||
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.
|
||||
|
||||
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.
|
||||
### Build Requirements
|
||||
|
||||
hyp supports a clock skew of up to 30 seconds between client and server.
|
||||
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.
|
||||
|
||||
### TBD: Protection Against Sweeping Attacks
|
||||
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.
|
||||
|
||||
~~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.~~
|
||||
```sh
|
||||
# Clone repository
|
||||
git clone https://deadbeef.codes/steven/hyp.git
|
||||
|
||||
### Known Weaknesses
|
||||
# Build eBPF program
|
||||
cd hyp/hypd/server
|
||||
go generate
|
||||
|
||||
* 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
|
||||
# Build knock daemon
|
||||
cd ..
|
||||
go build -o hypd .
|
||||
chmod +x hypd
|
||||
|
||||
# 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 |
6
go.mod
6
go.mod
@ -3,13 +3,13 @@ module deadbeef.codes/steven/hyp
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/cilium/ebpf v0.14.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
)
|
||||
|
34
go.sum
34
go.sum
@ -1,28 +1,26 @@
|
||||
github.com/cilium/ebpf v0.14.0 h1:0PsxAjO6EjI1rcT+rkp6WcCnE0ZvfkXBYiMedJtrSUs=
|
||||
github.com/cilium/ebpf v0.14.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -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"
|
||||
@ -31,29 +32,67 @@ Example usage:
|
||||
`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// load secret and generate ports using secret and current time
|
||||
secretBytes, err := os.ReadFile("hyp.secret")
|
||||
secretFilePath, err := cmd.Flags().GetString("secret")
|
||||
if err != nil {
|
||||
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 base32 decode secret '%s': %v", secretFilePath, err)
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
ports, err := otphyp.GeneratePorts(decodedSecret, time.Now())
|
||||
if err != nil {
|
||||
log.Fatalf("failed to generate ports from shared secret: %v", err)
|
||||
}
|
||||
|
||||
// Transmit
|
||||
fmt.Println("Transmitting knock sequence:", ports)
|
||||
for _, port := range ports {
|
||||
fmt.Printf("knock | %s:%d\n", args[0], port)
|
||||
conn, _ := net.Dial("udp", fmt.Sprintf("%s:%d", args[0], port))
|
||||
conn.Write([]byte{0})
|
||||
conn.Close()
|
||||
time.Sleep(time.Millisecond * 200) // TBD: Make this configurable with flag (maxJitter)
|
||||
time.Sleep(time.Millisecond * time.Duration(maxJitter)) // TBD: Make this configurable with flag (maxJitter)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
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"
|
||||
}
|
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 .
|
||||
```
|
100
hypd/server/bpf_endian.h
Normal file
100
hypd/server/bpf_endian.h
Normal file
@ -0,0 +1,100 @@
|
||||
// Code lifted from the folks at Cilium from ebpf-go repo
|
||||
|
||||
#ifndef __BPF_ENDIAN__
|
||||
#define __BPF_ENDIAN__
|
||||
|
||||
/*
|
||||
* Isolate byte #n and put it into byte #m, for __u##b type.
|
||||
* E.g., moving byte #6 (nnnnnnnn) into byte #1 (mmmmmmmm) for __u64:
|
||||
* 1) xxxxxxxx nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx
|
||||
* 2) nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx 00000000
|
||||
* 3) 00000000 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn
|
||||
* 4) 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn 00000000
|
||||
*/
|
||||
#define ___bpf_mvb(x, b, n, m) ((__u##b)(x) << (b-(n+1)*8) >> (b-8) << (m*8))
|
||||
|
||||
#define ___bpf_swab16(x) ((__u16)( \
|
||||
___bpf_mvb(x, 16, 0, 1) | \
|
||||
___bpf_mvb(x, 16, 1, 0)))
|
||||
|
||||
#define ___bpf_swab32(x) ((__u32)( \
|
||||
___bpf_mvb(x, 32, 0, 3) | \
|
||||
___bpf_mvb(x, 32, 1, 2) | \
|
||||
___bpf_mvb(x, 32, 2, 1) | \
|
||||
___bpf_mvb(x, 32, 3, 0)))
|
||||
|
||||
#define ___bpf_swab64(x) ((__u64)( \
|
||||
___bpf_mvb(x, 64, 0, 7) | \
|
||||
___bpf_mvb(x, 64, 1, 6) | \
|
||||
___bpf_mvb(x, 64, 2, 5) | \
|
||||
___bpf_mvb(x, 64, 3, 4) | \
|
||||
___bpf_mvb(x, 64, 4, 3) | \
|
||||
___bpf_mvb(x, 64, 5, 2) | \
|
||||
___bpf_mvb(x, 64, 6, 1) | \
|
||||
___bpf_mvb(x, 64, 7, 0)))
|
||||
|
||||
/* LLVM's BPF target selects the endianness of the CPU
|
||||
* it compiles on, or the user specifies (bpfel/bpfeb),
|
||||
* respectively. The used __BYTE_ORDER__ is defined by
|
||||
* the compiler, we cannot rely on __BYTE_ORDER from
|
||||
* libc headers, since it doesn't reflect the actual
|
||||
* requested byte order.
|
||||
*
|
||||
* Note, LLVM's BPF target has different __builtin_bswapX()
|
||||
* semantics. It does map to BPF_ALU | BPF_END | BPF_TO_BE
|
||||
* in bpfel and bpfeb case, which means below, that we map
|
||||
* to cpu_to_be16(). We could use it unconditionally in BPF
|
||||
* case, but better not rely on it, so that this header here
|
||||
* can be used from application and BPF program side, which
|
||||
* use different targets.
|
||||
*/
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
# define __bpf_ntohs(x) __builtin_bswap16(x)
|
||||
# define __bpf_htons(x) __builtin_bswap16(x)
|
||||
# define __bpf_constant_ntohs(x) ___bpf_swab16(x)
|
||||
# define __bpf_constant_htons(x) ___bpf_swab16(x)
|
||||
# define __bpf_ntohl(x) __builtin_bswap32(x)
|
||||
# define __bpf_htonl(x) __builtin_bswap32(x)
|
||||
# define __bpf_constant_ntohl(x) ___bpf_swab32(x)
|
||||
# define __bpf_constant_htonl(x) ___bpf_swab32(x)
|
||||
# define __bpf_be64_to_cpu(x) __builtin_bswap64(x)
|
||||
# define __bpf_cpu_to_be64(x) __builtin_bswap64(x)
|
||||
# define __bpf_constant_be64_to_cpu(x) ___bpf_swab64(x)
|
||||
# define __bpf_constant_cpu_to_be64(x) ___bpf_swab64(x)
|
||||
#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
# define __bpf_ntohs(x) (x)
|
||||
# define __bpf_htons(x) (x)
|
||||
# define __bpf_constant_ntohs(x) (x)
|
||||
# define __bpf_constant_htons(x) (x)
|
||||
# define __bpf_ntohl(x) (x)
|
||||
# define __bpf_htonl(x) (x)
|
||||
# define __bpf_constant_ntohl(x) (x)
|
||||
# define __bpf_constant_htonl(x) (x)
|
||||
# define __bpf_be64_to_cpu(x) (x)
|
||||
# define __bpf_cpu_to_be64(x) (x)
|
||||
# define __bpf_constant_be64_to_cpu(x) (x)
|
||||
# define __bpf_constant_cpu_to_be64(x) (x)
|
||||
#else
|
||||
# error "Fix your compiler's __BYTE_ORDER__?!"
|
||||
#endif
|
||||
|
||||
#define bpf_htons(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_htons(x) : __bpf_htons(x))
|
||||
#define bpf_ntohs(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_ntohs(x) : __bpf_ntohs(x))
|
||||
#define bpf_htonl(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_htonl(x) : __bpf_htonl(x))
|
||||
#define bpf_ntohl(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_ntohl(x) : __bpf_ntohl(x))
|
||||
#define bpf_cpu_to_be64(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_cpu_to_be64(x) : __bpf_cpu_to_be64(x))
|
||||
#define bpf_be64_to_cpu(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_be64_to_cpu(x) : __bpf_be64_to_cpu(x))
|
||||
|
||||
#endif /* __BPF_ENDIAN__ */
|
66
hypd/server/hyp_bpf.c
Normal file
66
hypd/server/hyp_bpf.c
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright © 2024 Steven Polley <himself@stevenpolley.net>
|
||||
*/
|
||||
|
||||
//go:build ignore
|
||||
#include "vmlinux.h"
|
||||
#include "bpf_endian.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
|
||||
char __license[] SEC("license") = "BSD";
|
||||
|
||||
// representation of knock data that gets sent to userspace
|
||||
struct knock_data {
|
||||
__u32 srcip; // 4 bytes
|
||||
__u16 dstport; // 2 bytes
|
||||
__u16 pad; // required padding - struct must be multiple of 4 bytes
|
||||
};
|
||||
|
||||
// ring buffer used to send data to userspace
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 1 << 24);
|
||||
} rb SEC(".maps");
|
||||
|
||||
// force emitting struct event into the ELF
|
||||
const struct knock_data *unused __attribute__((unused));
|
||||
|
||||
// hook into xpress data path attach point
|
||||
SEC("xdp")
|
||||
int xdp_prog_func(struct xdp_md *ctx) {
|
||||
|
||||
// xdp gives us the raw frame with no structures, it must be parsed
|
||||
void *data = (void *)(long)ctx->data;
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
|
||||
// A knock should not contain any data
|
||||
if (data_end - data > 60) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
done:
|
||||
// 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
|
BIN
hypd/server/hyp_bpf_bpfeb.o
Normal file
BIN
hypd/server/hyp_bpf_bpfeb.o
Normal file
Binary file not shown.
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
|
BIN
hypd/server/hyp_bpf_bpfel.o
Normal file
BIN
hypd/server/hyp_bpf_bpfel.o
Normal file
Binary file not shown.
@ -5,23 +5,29 @@ Copyright © 2024 Steven Polley <himself@stevenpolley.net>
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"net"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"deadbeef.codes/steven/hyp/hypd/configuration"
|
||||
"deadbeef.codes/steven/hyp/otphyp"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/pcap"
|
||||
"github.com/cilium/ebpf/link"
|
||||
"github.com/cilium/ebpf/ringbuf"
|
||||
"github.com/cilium/ebpf/rlimit"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go --type knock_data hyp_bpf hyp_bpf.c
|
||||
|
||||
// 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
|
||||
LastUpdated time.Time // The last time the client sent a correct packet in the sequence
|
||||
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)
|
||||
@ -30,137 +36,219 @@ type KnockSequence struct {
|
||||
PortSequence [4]uint16 // Each knock sequence is four ports long
|
||||
}
|
||||
|
||||
var (
|
||||
clients map[string]*Client // Contains a map of clients
|
||||
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
|
||||
const (
|
||||
KnockSequenceTimeout = 3 // TBD: Make this a configurable value
|
||||
)
|
||||
|
||||
// packetServer is the main function when operating in server mode
|
||||
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
|
||||
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 {
|
||||
secretBytes, err := os.ReadFile("hyp.secret")
|
||||
func PacketServer(config *configuration.HypdConfiguration, secrets [][]byte) error {
|
||||
serverConfig = config
|
||||
sharedSecrets = secrets
|
||||
iface, err := net.InterfaceByName(serverConfig.NetworkInterface)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read file 'hyp.secret': %v", err)
|
||||
log.Fatalf("lookup network iface %q: %v", serverConfig.NetworkInterface, err)
|
||||
}
|
||||
sharedSecret = string(secretBytes)
|
||||
|
||||
clients = make(map[string]*Client, 0)
|
||||
clients = make(map[uint32]*Client, 0)
|
||||
knockSequences = []KnockSequence{}
|
||||
|
||||
// Open pcap handle on device
|
||||
handle, err := pcap.OpenLive(captureDevice, 1600, true, pcap.BlockForever)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open pcap on capture device: %w", err)
|
||||
}
|
||||
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
|
||||
|
||||
// Setup a goroutine to periodically rotate the authentic knock sequence
|
||||
go rotateSequence(handle)
|
||||
go rotateSequence()
|
||||
|
||||
// Read from the pcap handle until we exit
|
||||
for packet := range packetSource.Packets() {
|
||||
handlePacket(packet) // Do something with a packet here.
|
||||
////////////////////////////////////
|
||||
|
||||
// Allow the current process to lock memory for eBPF resources.
|
||||
if err := rlimit.RemoveMemlock(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Load pre-compiled programs into the kernel.
|
||||
objs := hyp_bpfObjects{}
|
||||
if err := loadHyp_bpfObjects(&objs, nil); err != nil {
|
||||
log.Fatalf("loading objects: %v", err)
|
||||
}
|
||||
defer objs.Close()
|
||||
|
||||
// Attach the program.
|
||||
l, err := link.AttachXDP(link.XDPOptions{
|
||||
Program: objs.XdpProgFunc,
|
||||
Interface: iface.Index,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("could not attach XDP program: %v", err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
log.Printf("Attached XDP program to iface %q (index %d)", iface.Name, iface.Index)
|
||||
log.Printf("Press Ctrl-C to exit and remove the program")
|
||||
|
||||
rd, err := ringbuf.NewReader(objs.Rb)
|
||||
if err != nil {
|
||||
log.Fatalf("could not open ring buffer reader: %v", err)
|
||||
}
|
||||
defer rd.Close()
|
||||
|
||||
var event hyp_bpfKnockData
|
||||
for {
|
||||
record, err := rd.Read()
|
||||
if err != nil {
|
||||
if errors.Is(err, ringbuf.ErrClosed) {
|
||||
log.Println("eBPF ring buffer closed, exiting...")
|
||||
return nil
|
||||
}
|
||||
log.Printf("error reading from ring buffer reader: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
|
||||
log.Printf("error parsing ringbuf event: %v", err)
|
||||
continue
|
||||
}
|
||||
go handleKnock(event)
|
||||
}
|
||||
}
|
||||
|
||||
// intToIP converts IPv4 number to net.IP
|
||||
func intToIP(ipNum uint32) net.IP {
|
||||
ip := make(net.IP, 4)
|
||||
binary.BigEndian.PutUint32(ip, ipNum)
|
||||
return ip
|
||||
}
|
||||
|
||||
// packets that match the BPF filter get passed to handlePacket
|
||||
func handlePacket(packet gopacket.Packet) {
|
||||
port := binary.BigEndian.Uint16(packet.TransportLayer().TransportFlow().Dst().Raw())
|
||||
srcip := packet.NetworkLayer().NetworkFlow().Src().String()
|
||||
|
||||
client, ok := clients[srcip]
|
||||
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 port == knockSequence.PortSequence[0] {
|
||||
// Create the client and mark the knock sequence as used
|
||||
clients[srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
|
||||
knockSequences[i].Used = true
|
||||
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
|
||||
client.Progress = 1
|
||||
client.Sequence = knockSequence.PortSequence
|
||||
go timeoutKnockSequence(knockEvent.Srcip)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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 port != client.Sequence[client.Progress] {
|
||||
delete(clients, srcip)
|
||||
fmt.Printf("port '%d' is in sequence, but came at unexpected order - resetting progress", port)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// Client increases progress through sequence and checks if sequence is completed
|
||||
client.Progress++
|
||||
if client.Progress >= len(client.Sequence) {
|
||||
delete(clients, srcip)
|
||||
handleSuccess(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
|
||||
}
|
||||
}
|
||||
|
||||
// Used to rotate the authentic port knock sequence
|
||||
func rotateSequence(handle *pcap.Handle) {
|
||||
for {
|
||||
// Remove the client after the timeout value has elapsed. This prevents a client from
|
||||
// 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)
|
||||
client, ok := clients[srcip]
|
||||
if ok {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Used to rotate the authentic port knock sequence
|
||||
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))))
|
||||
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)
|
||||
}
|
||||
fmt.Println("New sequences:", knockSequences)
|
||||
|
||||
// Set BPF filter
|
||||
err := setPacketFilter(handle)
|
||||
if err != nil {
|
||||
log.Printf("failed to change packet filter: %v", err)
|
||||
}
|
||||
|
||||
// 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):]
|
||||
}
|
||||
}
|
||||
|
||||
// Given a pcap handle and list of authentic port knock sequences, configures a BPF filter
|
||||
func setPacketFilter(handle *pcap.Handle) error {
|
||||
filter := "udp && ("
|
||||
for i, knockSequence := range knockSequences {
|
||||
for j, port := range knockSequence.PortSequence {
|
||||
if i == 0 && j == 0 {
|
||||
filter += fmt.Sprint("port ", port)
|
||||
} else {
|
||||
filter += fmt.Sprint(" || port ", port)
|
||||
}
|
||||
}
|
||||
}
|
||||
filter += ")"
|
||||
err := handle.SetBPFFilter(filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set BPF filter '%s': %v", filter, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 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)
|
||||
|
||||
// TBD: Implement - this is a temporary routine to demonstrate an application
|
||||
func handleSuccess(srcip string) {
|
||||
fmt.Println("Success for ", srcip)
|
||||
client, ok := clients[srcip]
|
||||
if !ok {
|
||||
log.Printf("failed to lookup %s in clients", srcipf)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "-s", srcip, "--dport", "22", "-j", "ACCEPT")
|
||||
// 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)
|
||||
}
|
||||
|
112576
hypd/server/vmlinux.h
Normal file
112576
hypd/server/vmlinux.h
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
Reference in New Issue
Block a user