Compare commits

22 Commits

Author SHA1 Message Date
4ec16513ac give meaningful names to build artifacts
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2024-04-14 19:18:35 -06:00
0d113b4e8b fix dist files path for releases
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2024-04-14 19:05:11 -06:00
8cd537cd79 Merge branch 'main' of https://deadbeef.codes/steven/hyp
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2024-04-14 18:39:17 -06:00
80043a571d release on tag 2024-04-14 18:39:08 -06:00
998c9e217c add build badge to readme
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/release Build is failing
2024-04-14 18:20:05 -06:00
977aef9ee2 separate hyp and hypd build into different steps, add windows build
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-04-14 18:19:52 -06:00
5f10c27b0f add knock sequence timeout
All checks were successful
continuous-integration/drone/push Build is passing
This provides another layer of additional protection against sweep attacks by ensuring the correct sequence be entered rapidly, within 3 seconds by default.  It also prevents a client from sitting stuck forever part way through an old knock sequence.
2024-04-14 18:14:24 -06:00
0b876665d5 add step to create symlinks - required for compiling eBPF program
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/environment Build is passing
2024-04-14 09:59:23 -06:00
d422724556 source /root/.profile
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-14 09:54:43 -06:00
a73854e040 do explicitly create not create /dist, it's created during mount
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-14 09:53:52 -06:00
d40147d61c source command is a bash thing
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/environment Build is failing
it doesn't exist in sh
2024-04-14 09:10:35 -06:00
3cbd6eace2 add public release volume to build pipeline
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/promote/environment Build is failing
Allows us to get the latest build at any time from: https://public.deadbeef.codes/build/
2024-04-14 08:49:10 -06:00
42e5679570 Fix EOF error when building in dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-14 08:43:27 -06:00
54159e2e5e Add golang bin to path of build image
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/environment Build is failing
2024-04-14 08:29:21 -06:00
e197990185 mfw: half my commits are fixing yaml indentations
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/promote/environment Build is failing
2024-04-13 23:00:02 -06:00
f3d84f09fd fix indenting i do love yaml tho
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2024-04-13 22:59:21 -06:00
0382892f73 add condition for release
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/promote/environment Build was killed
2024-04-13 22:58:24 -06:00
cb20f91223 add CICD pipeline (untested)
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2024-04-13 22:56:21 -06:00
2efe3344b4 knock frames should not contain any data, only headers 2024-04-13 21:50:20 -06:00
0ad3e2b0d4 enter eBPF, libpcap and CGO = bloat 2024-04-13 21:22:22 -06:00
3ae568639e add flag to specify alternative filepath to secret
For the hyp client to be able to support multiple servers, each with their own secret, this capability is requried.
2024-04-11 15:31:08 -06:00
ead7578544 change pcap snaplen to 126 bytes
We really only care getting as far as the UDP header and can discard the rest.  This should reduce load, and perhaps enable full pcap with ports on the BPF filter

UDP header = 8 bytes
IPv4 max size = 60 bytes
IPv6 fixed size = 40 bytes
Ethernet header size = 18 bytes
2024-04-11 15:21:48 -06:00
11 changed files with 112997 additions and 84 deletions

89
.drone.yml Normal file
View 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

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ hyp.secret
*.exe *.exe
hypd/hypd hypd/hypd
hyp/hyp hyp/hyp
hypd/server/hyp_bpf_bpfe*

20
Dockerfile Normal file
View 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

View File

@ -1,5 +1,7 @@
# hyp | Hide Your Ports # hyp | Hide Your Ports
[![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, using spread-spectrum UDP as an authentication mechanism. It enables trusted devices to access services over the internet, wherever they are, and without the service being publicly accessible. The benefit is that the ports are not open publicly on the internet, they won't show in a port scan and are therefore less likely to be attacked by a threat actor. hyp is a [port knocking](https://www.youtube.com/watch?v=a7VJZEJVhD0) implementation written in Go, using spread-spectrum UDP as an authentication mechanism. It enables trusted devices to access services over the internet, wherever they are, and without the service being publicly accessible. The benefit is that the ports are not open publicly on the internet, they won't show in a port scan and are therefore less likely to be attacked by a threat actor.
hyp 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 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.

6
go.mod
View File

@ -3,13 +3,13 @@ module deadbeef.codes/steven/hyp
go 1.22.0 go 1.22.0
require ( require (
github.com/google/gopacket v1.1.19 github.com/cilium/ebpf v0.14.0
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
) )
require ( require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.19.0 // indirect golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect
golang.org/x/sys v0.15.0 // indirect golang.org/x/sys v0.19.0 // indirect
) )

34
go.sum
View File

@ -1,28 +1,30 @@
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/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/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 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
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 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -31,8 +31,14 @@ Example usage:
`, `,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// load secret and generate ports using secret and current time // 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))
}
secretBytes, err := os.ReadFile(secretFilePath)
if err != nil { if err != nil {
log.Fatalf("failed to read file 'hyp.secret': %v", err) log.Fatalf("failed to read file 'hyp.secret': %v", err)
} }
@ -56,4 +62,6 @@ Example usage:
func init() { func init() {
rootCmd.AddCommand(knockCmd) rootCmd.AddCommand(knockCmd)
knockCmd.PersistentFlags().String("secret", "hyp.secret", "Path to the file containing the hyp secret.")
} }

100
hypd/server/bpf_endian.h Normal file
View 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__ */

71
hypd/server/hyp_bpf.c Normal file
View File

@ -0,0 +1,71 @@
/*
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";
#define ETH_P_IP 0x0800
#define IP_FRAGMENTED 65343
// representation of knock data that gets sent to userspace
struct knock_data {
__u32 srcip; // 4 bytes
__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;
}

View File

@ -5,23 +5,28 @@ Copyright © 2024 Steven Polley <himself@stevenpolley.net>
package server package server
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"log" "log"
"net"
"os" "os"
"os/exec" "os/exec"
"time" "time"
"deadbeef.codes/steven/hyp/otphyp" "deadbeef.codes/steven/hyp/otphyp"
"github.com/google/gopacket" "github.com/cilium/ebpf/link"
"github.com/google/gopacket/pcap" "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 // 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
LastUpdated time.Time // The last time the client sent a correct packet in the sequence
} }
// 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)
@ -30,57 +35,112 @@ type KnockSequence struct {
PortSequence [4]uint16 // Each knock sequence is four ports long PortSequence [4]uint16 // Each knock sequence is four ports long
} }
const (
KnockSequenceTimeout = 3 // TBD: Make this a configurable value
)
var ( var (
clients map[string]*Client // Contains a map of clients 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 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 sharedSecret string // base32 encoded shared secret used for totp
) )
// packetServer is the main function when operating in server mode // PacketServer is the main function when operating in server mode
// it sets up the pcap on the capture device and starts a goroutine // it sets up the pcap on the capture device and starts a goroutine
// to rotate the knock sequence // to rotate the knock sequence
func PacketServer(captureDevice string) error { func PacketServer(captureDevice string) error {
iface, err := net.InterfaceByName(captureDevice)
if err != nil {
log.Fatalf("lookup network iface %q: %v", captureDevice, err)
}
secretBytes, err := os.ReadFile("hyp.secret") secretBytes, err := os.ReadFile("hyp.secret")
if err != nil { if err != nil {
log.Fatalf("failed to read file 'hyp.secret': %v", err) log.Fatalf("failed to read file 'hyp.secret': %v", err)
} }
sharedSecret = string(secretBytes) sharedSecret = string(secretBytes)
clients = make(map[string]*Client, 0) clients = make(map[uint32]*Client, 0)
knockSequences = []KnockSequence{} 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 // 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)
} }
return nil
// 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
}
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 // packets that match the BPF filter get passed to handlePacket
func handlePacket(packet gopacket.Packet) { func handleKnock(knockEvent hyp_bpfKnockData) {
port := binary.BigEndian.Uint16(packet.TransportLayer().TransportFlow().Dst().Raw())
srcip := packet.NetworkLayer().NetworkFlow().Src().String()
client, ok := clients[srcip] client, ok := clients[knockEvent.Srcip]
if !ok { // client doesn't exist yet if !ok { // client doesn't exist yet
for i, knockSequence := range knockSequences { // identify which of the 3 authentic knock sequences is matched for i, knockSequence := range knockSequences { // identify which of the 3 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 port == knockSequence.PortSequence[0] { if knockEvent.Dstport == knockSequence.PortSequence[0] {
// Create the client and mark the knock sequence as used // Create the client and mark the knock sequence as used
clients[srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence} clients[knockEvent.Srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
knockSequences[i].Used = true knockSequences[i].Used = true
go timeoutKnockSequence(knockEvent.Srcip)
} }
} }
return return
@ -89,25 +149,36 @@ func handlePacket(packet gopacket.Packet) {
// if it's wrong, reset progress // 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: 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. // 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] { if knockEvent.Dstport != client.Sequence[client.Progress] {
delete(clients, srcip) delete(clients, knockEvent.Srcip)
fmt.Printf("port '%d' is in sequence, but came at unexpected order - resetting progress", port) fmt.Printf("port '%d' is in sequence, but came at unexpected order - resetting progress", knockEvent.Dstport)
return return
} }
// 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) {
delete(clients, srcip) delete(clients, knockEvent.Srcip)
handleSuccess(srcip) // The magic function, the knock is completed handleSuccess(intToIP(knockEvent.Srcip)) // The magic function, the knock is completed
return return
} }
} }
// Used to rotate the authentic port knock sequence // Remove the client after the timeout value has elapsed. This prevents a client from
func rotateSequence(handle *pcap.Handle) { // being indefinitely stuck part way through an old knock sequence. It's also helpful
for { // in preventing sweep attacks as the authentic knock sequence must be correctly entered
// within the timeout value from start to finish.
func timeoutKnockSequence(srcip uint32) {
time.Sleep(time.Second * KnockSequenceTimeout)
_, ok := clients[srcip]
if ok {
delete(clients, srcip)
}
}
// Used to rotate the authentic port knock sequence
func rotateSequence() {
for {
// Generate new knock sequences with time skew support // Generate new knock sequences with time skew support
t := time.Now().Add(time.Second * -30) t := time.Now().Add(time.Second * -30)
for i := len(knockSequences); i < 3; i++ { for i := len(knockSequences); i < 3; i++ {
@ -120,12 +191,6 @@ func rotateSequence(handle *pcap.Handle) {
} }
fmt.Println("New sequences:", knockSequences) 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 // Sleep until next 30 second offset
time.Sleep(time.Until(time.Now().Truncate(time.Second * 30).Add(time.Second * 30))) time.Sleep(time.Until(time.Now().Truncate(time.Second * 30).Add(time.Second * 30)))
@ -134,31 +199,10 @@ func rotateSequence(handle *pcap.Handle) {
} }
} }
// 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
}
// TBD: Implement - this is a temporary routine to demonstrate an application // TBD: Implement - this is a temporary routine to demonstrate an application
func handleSuccess(srcip string) { func handleSuccess(srcip net.IP) {
fmt.Println("Success for ", srcip) fmt.Println("Success for ", srcip)
cmd := exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "-s", fmt.Sprint(srcip), "--dport", "22", "-j", "ACCEPT")
cmd := exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "-s", srcip, "--dport", "22", "-j", "ACCEPT")
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
log.Printf("failed to execute iptables command for '%s': %v", srcip, err) log.Printf("failed to execute iptables command for '%s': %v", srcip, err)

112576
hypd/server/vmlinux.h Normal file

File diff suppressed because it is too large Load Diff